From cb606f45e3852432787ed895dc55665caa950161 Mon Sep 17 00:00:00 2001 From: Tim Rozet Date: Fri, 8 Sep 2017 16:57:36 -0400 Subject: 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 --- apex/clean.py | 83 +++++++++- apex/common/exceptions.py | 8 + apex/common/parsers.py | 25 +++ apex/network/jumphost.py | 236 +++++++++++++++++++------- apex/tests/config/bad_ifcfg-br-external | 8 + apex/tests/config/bad_nova_output.json | 23 +++ apex/tests/config/ifcfg-br-dummy | 9 + apex/tests/config/ifcfg-br-external | 10 ++ apex/tests/config/ifcfg-dummy | 7 + apex/tests/test_apex_clean.py | 69 +++++++- apex/tests/test_apex_common_parsers.py | 21 ++- apex/tests/test_apex_network_jumphost.py | 276 +++++++++++++++++++++++++++++++ 12 files changed, 706 insertions(+), 69 deletions(-) create mode 100644 apex/tests/config/bad_ifcfg-br-external create mode 100644 apex/tests/config/bad_nova_output.json create mode 100644 apex/tests/config/ifcfg-br-dummy create mode 100644 apex/tests/config/ifcfg-br-external create mode 100644 apex/tests/config/ifcfg-dummy create mode 100644 apex/tests/test_apex_network_jumphost.py (limited to 'apex') 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) -- cgit 1.2.3-korg