diff options
author | Tim Rozet <trozet@redhat.com> | 2017-09-08 16:57:36 -0400 |
---|---|---|
committer | Tim Rozet <trozet@redhat.com> | 2017-09-13 10:13:06 -0400 |
commit | cb606f45e3852432787ed895dc55665caa950161 (patch) | |
tree | a987204e28ed22ee3e149c7a03d9e723c2e53a21 | |
parent | 8d3d5e679fba8e4140730e60809fc4f71cdc098e (diff) |
Migrates clean to python
ci/clean.sh will be removed in a future patch after releng is updated to
use python.
JIRA: APEX-509
JIRA: APEX-319
Change-Id: If890db2fc5a31833ad28ec6f04589e25457bd380
Signed-off-by: Tim Rozet <trozet@redhat.com>
-rw-r--r-- | apex/clean.py | 83 | ||||
-rw-r--r-- | apex/common/exceptions.py | 8 | ||||
-rw-r--r-- | apex/common/parsers.py | 25 | ||||
-rw-r--r-- | apex/network/jumphost.py | 236 | ||||
-rw-r--r-- | apex/tests/config/bad_ifcfg-br-external | 8 | ||||
-rw-r--r-- | apex/tests/config/bad_nova_output.json | 23 | ||||
-rw-r--r-- | apex/tests/config/ifcfg-br-dummy | 9 | ||||
-rw-r--r-- | apex/tests/config/ifcfg-br-external | 10 | ||||
-rw-r--r-- | apex/tests/config/ifcfg-dummy | 7 | ||||
-rw-r--r-- | apex/tests/test_apex_clean.py | 69 | ||||
-rw-r--r-- | apex/tests/test_apex_common_parsers.py | 21 | ||||
-rw-r--r-- | apex/tests/test_apex_network_jumphost.py | 276 | ||||
-rw-r--r-- | build/rpm_specs/opnfv-apex-common.spec | 3 | ||||
-rwxr-xr-x | ci/clean.sh | 215 | ||||
-rw-r--r-- | setup.cfg | 1 |
15 files changed, 716 insertions, 278 deletions
diff --git a/apex/clean.py b/apex/clean.py index af9e8ce0..81ae1770 100644 --- a/apex/clean.py +++ b/apex/clean.py @@ -7,16 +7,21 @@ # http://www.apache.org/licenses/LICENSE-2.0 ############################################################################## -# Clean will eventually be migrated to this file - import argparse +import fileinput +import libvirt import logging import os import pyipmi import pyipmi.interfaces import sys -from .common import utils +from apex.common import ( + constants, + utils) +from apex.network import jumphost +from apex.common.exceptions import ApexCleanException +from virtualbmc import manager as vbmc_lib def clean_nodes(inventory): @@ -41,11 +46,59 @@ def clean_nodes(inventory): sys.exit(1) +def clean_vbmcs(): + vbmc_manager = vbmc_lib.VirtualBMCManager() + vbmcs = vbmc_manager.list() + for vbmc in vbmcs: + logging.info("Deleting vbmc: {}".format(vbmc['domain_name'])) + vbmc_manager.delete(vbmc['domain_name']) + + +def clean_vms(): + logging.info('Destroying all Apex VMs') + conn = libvirt.open('qemu:///system') + if not conn: + raise ApexCleanException('Unable to open libvirt connection') + pool = conn.storagePoolLookupByName('default') + domains = conn.listAllDomains() + + for domain in domains: + vm = domain.name() + if vm != 'undercloud' and not vm.startswith('baremetal'): + continue + logging.info("Cleaning domain: {}".format(vm)) + if domain.isActive(): + logging.debug('Destroying domain') + domain.destroy() + domain.undefine() + # delete storage volume + try: + stgvol = pool.storageVolLookupByName("{}.qcow2".format(vm)) + except libvirt.libvirtError: + logging.warning("Skipping volume cleanup as volume not found for " + "vm: {}".format(vm)) + stgvol = None + if stgvol: + logging.info('Deleting storage volume') + stgvol.wipe(0) + stgvol.delete(0) + pool.refresh() + + +def clean_ssh_keys(key_file='/root/.ssh/authorized_keys'): + logging.info('Removing any stack pub keys from root authorized keys') + for line in fileinput.input(key_file, inplace=True): + line = line.strip('\n') + if 'stack@undercloud' not in line: + print(line) + + def main(): clean_parser = argparse.ArgumentParser() clean_parser.add_argument('-f', dest='inv_file', - required=True, + required=False, + default=None, help='File which contains inventory') args = clean_parser.parse_args(sys.argv[1:]) os.makedirs(os.path.dirname('./apex_clean.log'), exist_ok=True) @@ -58,8 +111,28 @@ def main(): console.setLevel(logging.DEBUG) console.setFormatter(logging.Formatter(formatter)) logging.getLogger('').addHandler(console) - clean_nodes(args.inv_file) + if args.inv_file: + if not os.path.isfile(args.inv_file): + logging.error("Inventory file not found: {}".format(args.inv_file)) + raise FileNotFoundError("Inventory file does not exist") + else: + logging.info("Shutting down baremetal nodes") + clean_nodes(args.inv_file) + # Delete all VMs + clean_vms() + # Delete vbmc + clean_vbmcs() + # Clean network config + for network in constants.ADMIN_NETWORK, constants.EXTERNAL_NETWORK: + logging.info("Cleaning Jump Host Network config for network " + "{}".format(network)) + jumphost.detach_interface_from_ovs(network) + jumphost.remove_ovs_bridge(network) + + # clean pub keys from root's auth keys + clean_ssh_keys() + logging.info('Apex clean complete!') if __name__ == '__main__': main() diff --git a/apex/common/exceptions.py b/apex/common/exceptions.py index c660213f..54d99834 100644 --- a/apex/common/exceptions.py +++ b/apex/common/exceptions.py @@ -10,3 +10,11 @@ class ApexDeployException(Exception): pass + + +class JumpHostNetworkException(Exception): + pass + + +class ApexCleanException(Exception): + pass diff --git a/apex/common/parsers.py b/apex/common/parsers.py index 8744c862..91b8905b 100644 --- a/apex/common/parsers.py +++ b/apex/common/parsers.py @@ -71,3 +71,28 @@ def parse_overcloudrc(in_file): logging.debug("os cred not found in: {}".format(line)) return creds + + +def parse_ifcfg_file(in_file): + """ + Parses ifcfg file information + :param in_file: + :return: dictionary of ifcfg key value pairs + """ + ifcfg_params = { + 'IPADDR': '', + 'NETMASK': '', + 'GATEWAY': '', + 'METRIC': '', + 'DNS1': '', + 'DNS2': '', + 'PREFIX': '' + } + with open(in_file, 'r') as fh: + for line in fh: + for param in ifcfg_params.keys(): + match = re.search("^\s*{}=(.*)$".format(param), line) + if match: + ifcfg_params[param] = match.group(1) + break + return ifcfg_params diff --git a/apex/network/jumphost.py b/apex/network/jumphost.py index f3f06ad6..2ecb7f4e 100644 --- a/apex/network/jumphost.py +++ b/apex/network/jumphost.py @@ -9,11 +9,11 @@ import logging import os -import re import shutil import subprocess -from apex.common.exceptions import ApexDeployException +from apex.common.exceptions import JumpHostNetworkException +from apex.common import parsers from apex.network import ip_utils NET_MAP = { @@ -24,6 +24,8 @@ NET_MAP = { 'api': 'br-api' } +NET_CFG_PATH = '/etc/sysconfig/network-scripts' + def configure_bridges(ns): """ @@ -68,81 +70,98 @@ def configure_bridges(ns): except subprocess.CalledProcessError: logging.error("Unable to configure IP address on " "bridge {}".format(NET_MAP[network])) + raise -def attach_interface_to_ovs(bridge, interface, network): +def generate_ifcfg_params(if_file, network): """ - Attaches jumphost interface to OVS for baremetal deployments - :param bridge: bridge to attach to - :param interface: interface to attach to bridge - :param network: Apex network type for these interfaces - :return: None + Generates and validates ifcfg parameters required for a network + :param if_file: ifcfg file to parse + :param network: Apex network + :return: dictionary of generated/validated ifcfg params """ + ifcfg_params = parsers.parse_ifcfg_file(if_file) + if not ifcfg_params['IPADDR']: + logging.error("IPADDR missing in {}".format(if_file)) + raise JumpHostNetworkException("IPADDR missing in {}".format(if_file)) + if not (ifcfg_params['NETMASK'] or ifcfg_params['PREFIX']): + logging.error("NETMASK/PREFIX missing in {}".format(if_file)) + raise JumpHostNetworkException("NETMASK/PREFIX missing in {}".format( + if_file)) + if network == 'external' and not ifcfg_params['GATEWAY']: + logging.error("GATEWAY is required to be in {} for external " + "network".format(if_file)) + raise JumpHostNetworkException("GATEWAY is required to be in {} for " + "external network".format(if_file)) - net_cfg_path = '/etc/sysconfig/network-scripts' - if_file = os.path.join(net_cfg_path, "ifcfg-{}".format(interface)) - ovs_file = os.path.join(net_cfg_path, "ifcfg-{}".format(bridge)) + if ifcfg_params['DNS1'] or ifcfg_params['DNS2']: + ifcfg_params['PEERDNS'] = 'yes' + else: + ifcfg_params['PEERDNS'] = 'no' + return ifcfg_params - logging.info("Attaching interface: {} to bridge: {} on network {}".format( - bridge, interface, network - )) +def is_ovs_bridge(bridge): + """ + Finds an OVS bridge + :param bridge: OVS bridge to find + :return: boolean if OVS bridge exists + """ try: output = subprocess.check_output(['ovs-vsctl', 'show'], stderr=subprocess.STDOUT) if bridge not in output.decode('utf-8'): - logging.debug("Bridge {} not found. Creating...".format(bridge)) - subprocess.check_call(['ovs-vsctl', 'add-br', bridge]) + logging.debug("Bridge {} not found".format(bridge)) + return False else: logging.debug("Bridge {} found".format(bridge)) + return True except subprocess.CalledProcessError: - logging.error("Unable to validate/create OVS bridge {}".format(bridge)) + logging.error("Unable to validate OVS bridge {}".format(bridge)) raise + + +def dump_ovs_ports(bridge): + """ + Returns + :param bridge: OVS bridge to list ports + :return: list of ports + """ try: output = subprocess.check_output(['ovs-vsctl', 'list-ports', bridge], stderr=subprocess.STDOUT) - if interface in output.decode('utf-8'): - logging.debug("Interface already attached to bridge") - return - except subprocess.CalledProcessError as e: - logging.error("Unable to dump ports for bridge: {}".format(bridge)) - logging.error("Error output: {}".format(e.output)) + except subprocess.CalledProcessError: + logging.error("Unable to show ports for {}".format(bridge)) raise + return output.decode('utf-8').strip().split('\n') - if not os.path.isfile(if_file): - logging.error("Interface ifcfg not found: {}".format(if_file)) - raise FileNotFoundError("Interface file missing: {}".format(if_file)) - ifcfg_params = { - 'IPADDR': '', - 'NETMASK': '', - 'GATEWAY': '', - 'METRIC': '', - 'DNS1': '', - 'DNS2': '', - 'PREFIX': '' - } - with open(if_file, 'r') as fh: - interface_output = fh.read() - - for param in ifcfg_params.keys(): - match = re.search("{}=(.*)\n".format(param), interface_output) - if match: - ifcfg_params[param] = match.group(1) +def attach_interface_to_ovs(bridge, interface, network): + """ + Attaches jumphost interface to OVS for baremetal deployments + :param bridge: bridge to attach to + :param interface: interface to attach to bridge + :param network: Apex network type for these interfaces + :return: None + """ - if not ifcfg_params['IPADDR']: - logging.error("IPADDR missing in {}".format(if_file)) - raise ApexDeployException("IPADDR missing in {}".format(if_file)) - if not (ifcfg_params['NETMASK'] or ifcfg_params['PREFIX']): - logging.error("NETMASK/PREFIX missing in {}".format(if_file)) - raise ApexDeployException("NETMASK/PREFIX missing in {}".format( - if_file)) - if network == 'external' and not ifcfg_params['GATEWAY']: - logging.error("GATEWAY is required to be in {} for external " - "network".format(if_file)) - raise ApexDeployException("GATEWAY is required to be in {} for " - "external network".format(if_file)) + if_file = os.path.join(NET_CFG_PATH, "ifcfg-{}".format(interface)) + ovs_file = os.path.join(NET_CFG_PATH, "ifcfg-{}".format(bridge)) + + logging.info("Attaching interface: {} to bridge: {} on network {}".format( + bridge, interface, network + )) + if not is_ovs_bridge(bridge): + subprocess.check_call(['ovs-vsctl', 'add-br', bridge]) + elif interface in dump_ovs_ports(bridge): + logging.debug("Interface already attached to bridge") + return + + if not os.path.isfile(if_file): + logging.error("Interface ifcfg not found: {}".format(if_file)) + raise FileNotFoundError("Interface file missing: {}".format(if_file)) + ifcfg_params = generate_ifcfg_params(if_file, network) shutil.move(if_file, "{}.orig".format(if_file)) if_content = """DEVICE={} DEVICETYPE=ovs @@ -160,13 +179,9 @@ BOOTPROTO=static ONBOOT=yes TYPE=OVSBridge PROMISC=yes""".format(bridge) - peer_dns = 'no' for param, value in ifcfg_params.items(): if value: bridge_content += "\n{}={}".format(param, value) - if param == 'DNS1' or param == 'DNS2': - peer_dns = 'yes' - bridge_content += "\n{}={}".format('PEERDNS', peer_dns) logging.debug("New interface file content:\n{}".format(if_content)) logging.debug("New bridge file content:\n{}".format(bridge_content)) @@ -181,3 +196,108 @@ PROMISC=yes""".format(bridge) except subprocess.CalledProcessError: logging.error("Failed to restart Linux networking") raise + + +def detach_interface_from_ovs(network): + """ + Detach interface from OVS for baremetal deployments + :param network: Apex network to detach single interface from + :return: None + """ + + bridge = NET_MAP[network] + logging.debug("Detaching interfaces from bridge on network: {}".format( + network)) + # ensure bridge exists + if not is_ovs_bridge(bridge): + return + + # check if real port is on bridge + for interface in dump_ovs_ports(bridge): + if interface and not interface.startswith('vnet'): + logging.debug("Interface found: {}".format(interface)) + real_interface = interface + break + else: + logging.info("No jumphost interface exists on bridge {}".format( + bridge)) + return + + # check if original backup ifcfg file exists or create + orig_ifcfg_file = os.path.join(NET_CFG_PATH, + "ifcfg-{}.orig".format(real_interface)) + ifcfg_file = orig_ifcfg_file[:-len('.orig')] + if os.path.isfile(orig_ifcfg_file): + logging.debug("Original interface file found: " + "{}".format(orig_ifcfg_file)) + shutil.move(orig_ifcfg_file, ifcfg_file) + else: + logging.info("No original ifcfg file found...will attempt to use " + "bridge icfg file and re-create") + bridge_ifcfg_file = os.path.join(NET_CFG_PATH, + "ifcfg-{}".format(bridge)) + if os.path.isfile(bridge_ifcfg_file): + ifcfg_params = generate_ifcfg_params(bridge_ifcfg_file, network) + if_content = """DEVICE={} +BOOTPROTO=static +ONBOOT=yes +TYPE=Ethernet +NM_CONTROLLED=no""".format(real_interface) + for param, value in ifcfg_params.items(): + if value: + if_content += "\n{}={}".format(param, value) + logging.debug("Interface file content:\n{}".format(if_content)) + # write original backup + with open(orig_ifcfg_file, 'w') as fh: + fh.write(if_content) + logging.debug("Original interface file created: " + "{}".format(orig_ifcfg_file)) + else: + logging.error("Unable to find original interface config file: {} " + "or bridge config file:{}".format(orig_ifcfg_file, + bridge_ifcfg_file)) + raise FileNotFoundError("Unable to locate bridge or original " + "interface ifcfg file") + + # move original file back and rewrite bridge ifcfg + shutil.move(orig_ifcfg_file, ifcfg_file) + bridge_content = """DEVICE={} +DEVICETYPE=ovs +BOOTPROTO=static +ONBOOT=yes +TYPE=OVSBridge +PROMISC=yes""".format(bridge) + with open(bridge_ifcfg_file, 'w') as fh: + fh.write(bridge_content) + # restart linux networking + logging.info("Restarting Linux networking") + try: + subprocess.check_call(['systemctl', 'restart', 'network']) + except subprocess.CalledProcessError: + logging.error("Failed to restart Linux networking") + raise + + +def remove_ovs_bridge(network): + """ + Unconfigure and remove an OVS bridge + :param network: Apex network to remove OVS bridge for + :return: + """ + bridge = NET_MAP[network] + if is_ovs_bridge(bridge): + logging.info("Removing bridge: {}".format(bridge)) + try: + subprocess.check_call(['ovs-vsctl', 'del-br', bridge]) + except subprocess.CalledProcessError: + logging.error('Unable to destroy OVS bridge') + raise + + logging.debug('Bridge destroyed') + bridge_ifcfg_file = os.path.join(NET_CFG_PATH, + "ifcfg-{}".format(bridge)) + if os.path.isfile(bridge_ifcfg_file): + os.remove(bridge_ifcfg_file) + logging.debug("Bridge ifcfg file removed: {}".format) + else: + logging.debug('Bridge ifcfg file not found') diff --git a/apex/tests/config/bad_ifcfg-br-external b/apex/tests/config/bad_ifcfg-br-external new file mode 100644 index 00000000..85b81959 --- /dev/null +++ b/apex/tests/config/bad_ifcfg-br-external @@ -0,0 +1,8 @@ +DEVICE=br-external +DEVICETYPE=ovs +BOOTPROTO=static +ONBOOT=yes +TYPE=OVSBridge +PROMISC=yes +IPADDR=172.30.9.66 +NETMASK=255.255.255.0 diff --git a/apex/tests/config/bad_nova_output.json b/apex/tests/config/bad_nova_output.json new file mode 100644 index 00000000..137750e5 --- /dev/null +++ b/apex/tests/config/bad_nova_output.json @@ -0,0 +1,23 @@ +[ + { + "Status": "ACTIVE", + "Networks": "", + "ID": "a5ff8aeb-5fd0-467f-9d89-791dfbc6267b", + "Image Name": "overcloud-full", + "Name": "test3" + }, + { + "Status": "ACTIVE", + "Networks": "", + "ID": "c8be26ae-6bef-4841-bb03-c7f336cfd785", + "Image Name": "overcloud-full", + "Name": "test2" + }, + { + "Status": "ACTIVE", + "Networks": "", + "ID": "105d1c61-78d3-498f-9191-6b21823b8544", + "Image Name": "overcloud-full", + "Name": "test1" + } +] diff --git a/apex/tests/config/ifcfg-br-dummy b/apex/tests/config/ifcfg-br-dummy new file mode 100644 index 00000000..117ca726 --- /dev/null +++ b/apex/tests/config/ifcfg-br-dummy @@ -0,0 +1,9 @@ +DEVICE=br-dummy +DEVICETYPE=ovs +BOOTPROTO=static +ONBOOT=yes +TYPE=OVSBridge +PROMISC=yes +IPADDR=152.30.9.11 +NETMASK=255.255.255.0 +PEERDNS=no
\ No newline at end of file diff --git a/apex/tests/config/ifcfg-br-external b/apex/tests/config/ifcfg-br-external new file mode 100644 index 00000000..9717d6e3 --- /dev/null +++ b/apex/tests/config/ifcfg-br-external @@ -0,0 +1,10 @@ +DEVICE=br-external +DEVICETYPE=ovs +BOOTPROTO=static +ONBOOT=yes +TYPE=OVSBridge +PROMISC=yes +IPADDR=172.30.9.66 +NETMASK=255.255.255.0 +GATEWAY=172.30.9.1 +#DNS1=1.1.1.1 diff --git a/apex/tests/config/ifcfg-dummy b/apex/tests/config/ifcfg-dummy new file mode 100644 index 00000000..f9ca21d4 --- /dev/null +++ b/apex/tests/config/ifcfg-dummy @@ -0,0 +1,7 @@ +DEVICE=enpfakes0 +TYPE=Ethernet +ONBOOT=yes +BOOTPROTO=static +NM_CONTROLLED=no +IPADDR=152.30.9.11 +NETMASK=255.255.255.0 diff --git a/apex/tests/test_apex_clean.py b/apex/tests/test_apex_clean.py index 7b7df512..b6b9d428 100644 --- a/apex/tests/test_apex_clean.py +++ b/apex/tests/test_apex_clean.py @@ -8,12 +8,48 @@ ############################################################################## import mock +import os import pyipmi import pyipmi.chassis from mock import patch -from nose import tools +from nose.tools import ( + assert_raises, + assert_equal +) from apex import clean_nodes +from apex import clean +from apex.tests import constants as con + + +class dummy_domain: + + def isActive(self): + return True + + def destroy(self): + pass + + def undefine(self): + pass + + +class dummy_vol: + + def wipe(self, *args): + pass + + def delete(self, *args): + pass + + +class dummy_pool: + + def storageVolLookupByName(self, *args, **kwargs): + return dummy_vol() + + def refresh(self): + pass class TestClean: @@ -31,11 +67,36 @@ class TestClean: def teardown(self): """This method is run once after _each_ test method is executed""" - def test_clean(self): + def test_clean_nodes(self): with mock.patch.object(pyipmi.Session, 'establish') as mock_method: with patch.object(pyipmi.chassis.Chassis, 'chassis_control_power_down') as mock_method2: clean_nodes('apex/tests/config/inventory.yaml') - tools.assert_equal(mock_method.call_count, 5) - tools.assert_equal(mock_method2.call_count, 5) + assert_equal(mock_method.call_count, 5) + assert_equal(mock_method2.call_count, 5) + + @patch('virtualbmc.manager.VirtualBMCManager.list', + return_value=[{'domain_name': 'dummy1'}, {'domain_name': 'dummy2'}]) + @patch('virtualbmc.manager.VirtualBMCManager.delete') + def test_vmbc_clean(self, vbmc_del_func, vbmc_list_func): + assert clean.clean_vbmcs() is None + + def test_clean_ssh_keys(self): + ssh_file = os.path.join(con.TEST_DUMMY_CONFIG, 'authorized_dummy') + with open(ssh_file, 'w') as fh: + fh.write('ssh-rsa 2LwlofGD8rNUFAlafY2/oUsKOf1mQ1 stack@undercloud') + assert clean.clean_ssh_keys(ssh_file) is None + with open(ssh_file, 'r') as fh: + output = fh.read() + assert 'stack@undercloud' not in output + if os.path.isfile(ssh_file): + os.remove(ssh_file) + + @patch('libvirt.open') + def test_clean_vms(self, mock_libvirt): + ml = mock_libvirt.return_value + ml.storagePoolLookupByName.return_value = dummy_pool() + ml.listDefinedDomains.return_value = ['undercloud'] + ml.lookupByName.return_value = dummy_domain() + assert clean.clean_vms() is None diff --git a/apex/tests/test_apex_common_parsers.py b/apex/tests/test_apex_common_parsers.py index bed2a8c5..d272a749 100644 --- a/apex/tests/test_apex_common_parsers.py +++ b/apex/tests/test_apex_common_parsers.py @@ -11,9 +11,11 @@ import os from apex.tests import constants as con from apex.common import parsers as apex_parsers +from apex.common.exceptions import ApexDeployException from nose.tools import ( assert_is_instance, - assert_dict_equal + assert_dict_equal, + assert_raises ) @@ -41,9 +43,13 @@ class TestCommonParsers: 'overcloud-novacompute-0': '192.30.9.10', 'overcloud-novacompute-1': '192.30.9.9' } - print(output) assert_dict_equal(output, nodes) + def test_negative_parse_nova_output(self): + assert_raises(ApexDeployException, apex_parsers.parse_nova_output, + os.path.join(con.TEST_DUMMY_CONFIG, + 'bad_nova_output.json')) + def test_parse_overcloudrc(self): output = apex_parsers.parse_overcloudrc( os.path.join(con.TEST_DUMMY_CONFIG, 'test_overcloudrc')) @@ -52,3 +58,14 @@ class TestCommonParsers: assert output['OS_AUTH_TYPE'] == 'password' assert 'OS_PASSWORD' in output.keys() assert output['OS_PASSWORD'] == 'Wd8ruyf6qG8cmcms6dq2HM93f' + + def test_parse_ifcfg(self): + output = apex_parsers.parse_ifcfg_file( + os.path.join(con.TEST_DUMMY_CONFIG, 'ifcfg-br-external')) + assert_is_instance(output, dict) + assert 'IPADDR' in output.keys() + assert output['IPADDR'] == '172.30.9.66' + assert 'NETMASK' in output.keys() + assert output['NETMASK'] == '255.255.255.0' + assert 'DNS1' in output.keys() + assert not output['DNS1'] diff --git a/apex/tests/test_apex_network_jumphost.py b/apex/tests/test_apex_network_jumphost.py new file mode 100644 index 00000000..a23f1c56 --- /dev/null +++ b/apex/tests/test_apex_network_jumphost.py @@ -0,0 +1,276 @@ +############################################################################## +# Copyright (c) 2016 Dan Radez (Red Hat) +# +# All rights reserved. This program and the accompanying materials +# are made available under the terms of the Apache License, Version 2.0 +# which accompanies this distribution, and is available at +# http://www.apache.org/licenses/LICENSE-2.0 +############################################################################## + +import os +import shutil +import subprocess + +from apex import NetworkSettings +from apex.tests import constants as con +from apex.common import constants as apex_constants +from apex.network import jumphost +from apex.common.exceptions import JumpHostNetworkException +from ipaddress import IPv4Interface +from mock import patch +from nose.tools import ( + assert_is_instance, + assert_dict_equal, + assert_raises, + assert_true, + assert_false +) + + +def bridge_show_output(*args, **kwargs): + return b""" + b6f1b54a-b8ba-4e86-9c5b-733ab71b5712 + Bridge br-admin + Port br-admin + Interface br-admin + type: internal + ovs_version: "2.5.0" +""" + + +def bridge_port_list(*args, **kwargs): + return b""" +enp6s0 +vnet1 +""" + + +def subprocess_exception(*args, **kwargs): + raise subprocess.CalledProcessError(returncode=2, cmd='dummy') + + +class TestNetworkJumpHost: + @classmethod + def setup_class(cls): + """This method is run once for each class before any tests are run""" + + @classmethod + def teardown_class(cls): + """This method is run once for each class _after_ all tests are run""" + + def setup(self): + """This method is run once before _each_ test method is executed""" + + def teardown(self): + """This method is run once after _each_ test method is executed""" + + @patch('subprocess.check_output', side_effect=bridge_show_output) + def test_is_ovs_bridge(self, bridge_output_function): + assert_true(jumphost.is_ovs_bridge('br-admin')) + assert_false(jumphost.is_ovs_bridge('br-blah')) + + @patch('subprocess.check_output', side_effect=bridge_port_list) + def test_dump_ovs_ports(self, bridge_function): + output = jumphost.dump_ovs_ports('br-admin') + assert_is_instance(output, list) + assert 'enp6s0' in output + + def test_generate_ifcfg_params(self): + output = jumphost.generate_ifcfg_params( + os.path.join(con.TEST_DUMMY_CONFIG, 'ifcfg-br-external'), + apex_constants.EXTERNAL_NETWORK) + assert_is_instance(output, dict) + assert output['IPADDR'] == '172.30.9.66' + assert output['PEERDNS'] == 'no' + + def test_negative_generate_ifcfg_params(self): + assert_raises(JumpHostNetworkException, jumphost.generate_ifcfg_params, + os.path.join(con.TEST_DUMMY_CONFIG, + 'bad_ifcfg-br-external'), + apex_constants.EXTERNAL_NETWORK) + + @patch('subprocess.check_call') + @patch('apex.network.ip_utils.get_interface', return_value=IPv4Interface( + '10.10.10.2')) + def test_configure_bridges_ip_exists(self, interface_function, + subprocess_func): + ns = NetworkSettings(os.path.join(con.TEST_CONFIG_DIR, + 'network', 'network_settings.yaml')) + assert jumphost.configure_bridges(ns) is None + + @patch('subprocess.check_call') + @patch('apex.network.ip_utils.get_interface', return_value=None) + def test_configure_bridges_no_ip(self, interface_function, + subprocess_func): + ns = NetworkSettings(os.path.join(con.TEST_CONFIG_DIR, + 'network', 'network_settings.yaml')) + assert jumphost.configure_bridges(ns) is None + + @patch('subprocess.check_call', side_effect=subprocess_exception) + @patch('apex.network.ip_utils.get_interface', return_value=None) + def test_negative_configure_bridges(self, interface_function, + subprocess_func): + ns = NetworkSettings(os.path.join(con.TEST_CONFIG_DIR, + 'network', 'network_settings.yaml')) + assert_raises(subprocess.CalledProcessError, + jumphost.configure_bridges, ns) + + @patch('subprocess.check_call') + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=[]) + def test_attach_interface(self, dump_ports_func, is_bridge_func, + subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + shutil.copyfile(os.path.join(ifcfg_dir, 'ifcfg-dummy'), + os.path.join(ifcfg_dir, 'ifcfg-enpfakes0')) + shutil.copyfile(os.path.join(ifcfg_dir, 'ifcfg-br-dummy'), + os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + jumphost.NET_CFG_PATH = ifcfg_dir + output = jumphost.attach_interface_to_ovs('br-admin', 'enpfakes0', + 'admin') + assert output is None + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-enpfakes0')) + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-enpfakes0.orig')) + + for ifcfg in ('ifcfg-enpfakes0', 'ifcfg-enpfakes0.orig', + 'ifcfg-br-admin'): + ifcfg_path = os.path.join(ifcfg_dir, ifcfg) + if os.path.isfile(ifcfg_path): + os.remove(ifcfg_path) + + @patch('subprocess.check_call') + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=['dummy_int']) + def test_already_attached_interface(self, dump_ports_func, is_bridge_func, + subprocess_func): + output = jumphost.attach_interface_to_ovs('br-dummy', 'dummy_int', + 'admin') + assert output is None + + @patch('subprocess.check_call') + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=[]) + def test_negative_attach_interface(self, dump_ports_func, is_bridge_func, + subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + jumphost.NET_CFG_PATH = ifcfg_dir + assert_raises(FileNotFoundError, jumphost.attach_interface_to_ovs, + 'br-dummy', 'dummy_int', 'admin') + + @patch('subprocess.check_call', side_effect=subprocess_exception) + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=[]) + def test_negative_attach_interface_process_error( + self, dump_ports_func, is_bridge_func, subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + shutil.copyfile(os.path.join(ifcfg_dir, 'ifcfg-dummy'), + os.path.join(ifcfg_dir, 'ifcfg-enpfakes0')) + shutil.copyfile(os.path.join(ifcfg_dir, 'ifcfg-br-dummy'), + os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + jumphost.NET_CFG_PATH = ifcfg_dir + assert_raises(subprocess.CalledProcessError, + jumphost.attach_interface_to_ovs, + 'br-admin', 'enpfakes0', 'admin') + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-enpfakes0')) + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-enpfakes0.orig')) + + for ifcfg in ('ifcfg-enpfakes0', 'ifcfg-enpfakes0.orig', + 'ifcfg-br-admin'): + ifcfg_path = os.path.join(ifcfg_dir, ifcfg) + if os.path.isfile(ifcfg_path): + os.remove(ifcfg_path) + + @patch('subprocess.check_call') + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=['enpfakes0']) + def test_detach_interface(self, dump_ports_func, is_bridge_func, + subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + shutil.copyfile(os.path.join(ifcfg_dir, 'ifcfg-br-dummy'), + os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + jumphost.NET_CFG_PATH = ifcfg_dir + output = jumphost.detach_interface_from_ovs('admin') + assert output is None + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-enpfakes0')) + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + + for ifcfg in ('ifcfg-enpfakes0', 'ifcfg-enpfakes0.orig', + 'ifcfg-br-admin'): + ifcfg_path = os.path.join(ifcfg_dir, ifcfg) + if os.path.isfile(ifcfg_path): + os.remove(ifcfg_path) + + @patch('subprocess.check_call') + @patch('apex.network.jumphost.is_ovs_bridge', return_value=False) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=[]) + def test_detach_interface_no_bridge(self, dump_ports_func, + is_bridge_func, subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + jumphost.NET_CFG_PATH = ifcfg_dir + output = jumphost.detach_interface_from_ovs('admin') + assert output is None + + @patch('subprocess.check_call') + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=[]) + def test_detach_interface_no_int_to_remove(self, dump_ports_func, + is_bridge_func, + subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + jumphost.NET_CFG_PATH = ifcfg_dir + output = jumphost.detach_interface_from_ovs('admin') + assert output is None + + @patch('subprocess.check_call') + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=['enpfakes0']) + def test_negative_detach_interface(self, dump_ports_func, is_bridge_func, + subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + jumphost.NET_CFG_PATH = ifcfg_dir + assert_raises(FileNotFoundError, jumphost.detach_interface_from_ovs, + 'admin') + + @patch('subprocess.check_call', side_effect=subprocess_exception) + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + @patch('apex.network.jumphost.dump_ovs_ports', return_value=['enpfakes0']) + def test_negative_detach_interface_process_error( + self, dump_ports_func, is_bridge_func, subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + shutil.copyfile(os.path.join(ifcfg_dir, 'ifcfg-br-dummy'), + os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + jumphost.NET_CFG_PATH = ifcfg_dir + assert_raises(subprocess.CalledProcessError, + jumphost.detach_interface_from_ovs, 'admin') + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-enpfakes0')) + assert os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + + for ifcfg in ('ifcfg-enpfakes0', 'ifcfg-enpfakes0.orig', + 'ifcfg-br-admin'): + ifcfg_path = os.path.join(ifcfg_dir, ifcfg) + if os.path.isfile(ifcfg_path): + os.remove(ifcfg_path) + + @patch('subprocess.check_call') + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + def test_remove_ovs_bridge(self, is_bridge_func, subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + jumphost.NET_CFG_PATH = ifcfg_dir + shutil.copyfile(os.path.join(ifcfg_dir, 'ifcfg-br-dummy'), + os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + assert jumphost.remove_ovs_bridge(apex_constants.ADMIN_NETWORK) is None + assert not os.path.isfile(os.path.join(ifcfg_dir, 'ifcfg-br-admin')) + + # test without file + assert jumphost.remove_ovs_bridge(apex_constants.ADMIN_NETWORK) is None + + @patch('subprocess.check_call', side_effect=subprocess_exception) + @patch('apex.network.jumphost.is_ovs_bridge', return_value=True) + def test_negative_remove_ovs_bridge(self, is_bridge_func, subprocess_func): + ifcfg_dir = con.TEST_DUMMY_CONFIG + jumphost.NET_CFG_PATH = ifcfg_dir + assert_raises(subprocess.CalledProcessError, + jumphost.remove_ovs_bridge, + apex_constants.ADMIN_NETWORK) diff --git a/build/rpm_specs/opnfv-apex-common.spec b/build/rpm_specs/opnfv-apex-common.spec index 37e32145..c2e2f14e 100644 --- a/build/rpm_specs/opnfv-apex-common.spec +++ b/build/rpm_specs/opnfv-apex-common.spec @@ -35,7 +35,6 @@ rst2html docs/release/release-notes/release-notes.rst docs/release/release-notes %install mkdir -p %{buildroot}%{_bindir}/ %py3_install -install ci/clean.sh %{buildroot}%{_bindir}/opnfv-clean install ci/util.sh %{buildroot}%{_bindir}/opnfv-util mkdir -p %{buildroot}%{_sysconfdir}/bash_completion.d/ @@ -113,6 +112,8 @@ install config/inventory/pod_example_settings.yaml %{buildroot}%{_docdir}/opnfv/ %doc %{_docdir}/opnfv/inventory.yaml.example %changelog +* Fri Sep 08 2017 Tim Rozet <trozet@redhat.com> - 5.0-6 +- Updates clean to use python * Wed Aug 23 2017 Tim Rozet <trozet@redhat.com> - 5.0-5 - Updated requirements * Mon Aug 14 2017 Tim Rozet <trozet@redhat.com> - 5.0-4 diff --git a/ci/clean.sh b/ci/clean.sh index ef810416..c2b987c0 100755 --- a/ci/clean.sh +++ b/ci/clean.sh @@ -11,211 +11,10 @@ #Clean script to uninstall provisioning server for Apex #author: Dan Radez (dradez@redhat.com) #author: Tim Rozet (trozet@redhat.com) - -reset=$(tput sgr0 || echo "") -blue=$(tput setaf 4 || echo "") -red=$(tput setaf 1 || echo "") -green=$(tput setaf 2 || echo "") - -vm_index=4 -ovs_bridges="br-admin br-tenant br-external br-storage" -ovs_bridges+=" br-private br-public" # Legacy names, remove in E river - -#OPNFV_NETWORK_TYPES=$(python3 -c 'from apex.common.constants import OPNFV_NETWORK_TYPES; print(" ".join(OPNFV_NETWORK_TYPES))') -OPNFV_NETWORK_TYPES+=" admin tenant external storage api" -OPNFV_NETWORK_TYPES+=" admin_network private_network public_network storage_network api_network" # Legecy names, remove in E river - -##detach interface from OVS and set the network config correctly -##params: bridge to detach from -##assumes only 1 real interface attached to OVS -function detach_interface_from_ovs { - local bridge - local port_output ports_no_orig - local net_path - local if_ip if_mask if_gw if_prefix - local if_metric if_dns1 if_dns2 - - net_path=/etc/sysconfig/network-scripts/ - if [[ -z "$1" ]]; then - return 1 - else - bridge=$1 - fi - - # if no interfaces attached then return - if ! ovs-vsctl list-ports ${bridge} | grep -Ev "vnet[0-9]*"; then - return 0 - fi - - # look for .orig ifcfg files to use - port_output=$(ovs-vsctl list-ports ${bridge} | grep -Ev "vnet[0-9]*") - while read -r line; do - if [ -z "$line" ]; then - continue - elif [ -e ${net_path}/ifcfg-${line}.orig ]; then - mv -f ${net_path}/ifcfg-${line}.orig ${net_path}/ifcfg-${line} - elif [ -e ${net_path}/ifcfg-${bridge} ]; then - if_ip=$(sed -n 's/^IPADDR=\(.*\)$/\1/p' ${net_path}/ifcfg-${bridge}) - if_mask=$(sed -n 's/^NETMASK=\(.*\)$/\1/p' ${net_path}/ifcfg-${bridge}) - if_gw=$(sed -n 's/^GATEWAY=\(.*\)$/\1/p' ${net_path}/ifcfg-${bridge}) - if_metric=$(sed -n 's/^METRIC=\(.*\)$/\1/p' ${net_path}/ifcfg-${bridge}) - if_dns1=$(sed -n 's/^DNS1=\(.*\)$/\1/p' ${net_path}/ifcfg-${bridge}) - if_dns2=$(sed -n 's/^DNS2=\(.*\)$/\1/p' ${net_path}/ifcfg-${bridge}) - - if [ -z "$if_mask" ]; then - if_prefix=$(sed -n 's/^PREFIX=[^0-9]*\([0-9][0-9]*\)[^0-9]*$/\1/p' ${net_path}/ifcfg-${bridge}) - if_mask=$(prefix2mask ${if_prefix}) - fi - - if [[ -z "$if_ip" || -z "$if_mask" ]]; then - echo "ERROR: IPADDR or PREFIX/NETMASK missing for ${bridge} and no .orig file for interface ${line}" - return 1 - fi - - # create if cfg - echo "DEVICE=${line} -IPADDR=${if_ip} -NETMASK=${if_mask} -BOOTPROTO=static -ONBOOT=yes -TYPE=Ethernet -NM_CONTROLLED=no -PEERDNS=no" > ${net_path}/ifcfg-${line} - - if [ -n "$if_gw" ]; then - echo "GATEWAY=${if_gw}" >> ${net_path}/ifcfg-${line} - fi - - if [ -n "$if_metric" ]; then - echo "METRIC=${if_metric}" >> ${net_path}/ifcfg-${line} - fi - - if [[ -n "$if_dns1" || -n "$if_dns2" ]]; then - sed -i '/PEERDNS/c\PEERDNS=yes' ${net_path}/ifcfg-${line} - - if [ -n "$if_dns1" ]; then - echo "DNS1=${if_dns1}" >> ${net_path}/ifcfg-${line} - fi - - if [ -n "$if_dns2" ]; then - echo "DNS2=${if_dns2}" >> ${net_path}/ifcfg-${line} - fi - fi - break - else - echo "ERROR: Real interface ${line} attached to bridge, but no interface or ${bridge} ifcfg file exists" - return 1 - fi - - done <<< "$port_output" - - # modify the bridge ifcfg file - # to remove IP params - sudo sed -i 's/IPADDR=.*//' ${net_path}/ifcfg-${bridge} - sudo sed -i 's/NETMASK=.*//' ${net_path}/ifcfg-${bridge} - sudo sed -i 's/GATEWAY=.*//' ${net_path}/ifcfg-${bridge} - sudo sed -i 's/DNS1=.*//' ${net_path}/ifcfg-${bridge} - sudo sed -i 's/DNS2=.*//' ${net_path}/ifcfg-${bridge} - sudo sed -i 's/METRIC=.*//' ${net_path}/ifcfg-${bridge} - sudo sed -i 's/PEERDNS=.*//' ${net_path}/ifcfg-${bridge} - - sudo systemctl restart network -} - -display_usage() { - echo -e "Usage:\n$0 [arguments] \n" - echo -e " -i|--inventory : Full path to inventory yaml file. Required only for baremetal node clean" -} - -##translates the command line parameters into variables -##params: $@ the entire command line is passed -##usage: parse_cmd_line() "$@" -parse_cmdline() { - echo -e "\n\n${blue}This script is used to clean an Apex environment${reset}\n\n" - echo "Use -h to display help" - sleep 2 - - while [ "${1:0:1}" = "-" ] - do - case "$1" in - -h|--help) - display_usage - exit 0 - ;; - -i|--inventory) - INVENTORY_FILE=$2 - shift 2 - ;; - *) - display_usage - exit 1 - ;; - esac - done - - if [[ ! -z "$INVENTORY_FILE" && ! -f "$INVENTORY_FILE" ]]; then - echo -e "{$red}ERROR: Inventory File: ${INVENTORY_FILE} does not exist! Exiting...${reset}" - exit 1 - fi -} - -parse_cmdline "$@" - -if [ -n "$INVENTORY_FILE" ]; then - echo -e "${blue}INFO: Parsing inventory file...${reset}" - # hack for now (until we switch fully over to clean.py) to tell if - # we should install apex from python or if rpm is being used - if ! rpm -q python34-opnfv-apex > /dev/null; then - pushd ../ && python3 setup.py install > /dev/null - popd - fi - if ! python3 -m apex.clean -f ${INVENTORY_FILE}; then - echo -e "${red}WARN: Unable to shutdown all nodes! Please check /var/log/apex.log${reset}" - else - echo -e "${blue}INFO: Node shutdown complete...${reset}" - fi -fi - -# Clean off instack/undercloud VM -for vm in instack undercloud; do - virsh destroy $vm 2> /dev/null | xargs echo -n - virsh undefine --nvram $vm 2> /dev/null | xargs echo -n - /usr/bin/touch /var/lib/libvirt/images/${vm}.qcow2 - virsh vol-delete ${vm}.qcow2 --pool default 2> /dev/null | xargs echo -n - rm -f /var/lib/libvirt/images/${vm}.qcow2 2> /dev/null -done - -# Clean off baremetal VMs in case they exist -for i in $(seq 0 $vm_index); do - virsh destroy baremetal$i 2> /dev/null | xargs echo -n - virsh undefine baremetal$i 2> /dev/null | xargs echo -n - /usr/bin/touch /var/lib/libvirt/images/baremetal${i}.qcow2 - virsh vol-delete baremetal${i}.qcow2 --pool default 2> /dev/null | xargs echo -n - rm -f /var/lib/libvirt/images/baremetal${i}.qcow2 2> /dev/null - if [ -e /root/.vbmc/baremetal$i ]; then vbmc delete baremetal$i; fi -done - -for network in ${OPNFV_NETWORK_TYPES}; do - virsh net-destroy ${network} 2> /dev/null - virsh net-undefine ${network} 2> /dev/null -done - -# Clean off created bridges -for bridge in ${ovs_bridges}; do - if detach_interface_from_ovs ${bridge} 2> /dev/null; then - ovs-vsctl del-br ${bridge} 2> /dev/null - rm -f /etc/sysconfig/network-scripts/ifcfg-${bridge} - fi -done - -# clean pub keys from root's auth keys -sed -i '/stack@undercloud.localdomain/d' /root/.ssh/authorized_keys - - -# force storage cleanup -virsh pool-refresh default - -# remove temporary files -rm -f /tmp/network-environment.yaml - -echo "Cleanup Completed" +set -e +yum -y install python34 python34-devel libvirt-devel python34-pip python-tox ansible +mkdir -p ~/tmp +mv -f .build ~/tmp/ +sudo pip3 install --upgrade --force-reinstall . +mv -f ~/tmp/.build . +opnfv-clean $@ @@ -23,6 +23,7 @@ setup-hooks = [entry_points] console_scripts = opnfv-deploy = apex.deploy:main + opnfv-clean = apex.clean:main [files] packages = |