diff --git a/.gitignore b/.gitignore index 276135e..8dda292 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ dist/ zaza.openstack.egg-info/ .coverage .vscode/ +*.swp # Sphinx doc/build diff --git a/unit_tests/utilities/test_zaza_utilities_openstack.py b/unit_tests/utilities/test_zaza_utilities_openstack.py index cc5fa68..3cd33a6 100644 --- a/unit_tests/utilities/test_zaza_utilities_openstack.py +++ b/unit_tests/utilities/test_zaza_utilities_openstack.py @@ -1500,6 +1500,20 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): self.configure_networking_charms.assert_called_once_with( 'fakenetworkingdata', expect, use_juju_wait=False) + def test_update_subnet_dhcp(self): + neutron_client = mock.MagicMock() + openstack_utils.update_subnet_dhcp( + neutron_client, {'id': 'aId'}, True) + neutron_client.update_subnet.assert_called_once_with( + 'aId', + {'subnet': {'enable_dhcp': True}}) + neutron_client.reset_mock() + openstack_utils.update_subnet_dhcp( + neutron_client, {'id': 'aId'}, False) + neutron_client.update_subnet.assert_called_once_with( + 'aId', + {'subnet': {'enable_dhcp': False}}) + class TestAsyncOpenstackUtils(ut_utils.AioTestCase): diff --git a/zaza/openstack/charm_tests/neutron/setup.py b/zaza/openstack/charm_tests/neutron/setup.py index 290483a..1402c8f 100644 --- a/zaza/openstack/charm_tests/neutron/setup.py +++ b/zaza/openstack/charm_tests/neutron/setup.py @@ -43,6 +43,8 @@ OVERCLOUD_NETWORK_CONFIG = { "prefix_len": "24", "subnetpool_name": "pooled_subnets", "subnetpool_prefix": "192.168.0.0/16", + "project_net_name": "private", + "project_subnet_name": "private_subnet", } OVERCLOUD_PROVIDER_VLAN_NETWORK_CONFIG = { @@ -67,29 +69,19 @@ DEFAULT_UNDERCLOUD_NETWORK_CONFIG = { } -def basic_overcloud_network(limit_gws=None): - """Run setup for neutron networking. - - Configure the following: - The overcloud network using subnet pools +def undercloud_and_charm_setup(limit_gws=None): + """Perform undercloud and charm setup for network plumbing. :param limit_gws: Limit the number of gateways that get a port attached :type limit_gws: int """ - cli_utils.setup_logging() - # Get network configuration settings network_config = {} - # Declared overcloud settings - network_config.update(OVERCLOUD_NETWORK_CONFIG) # Default undercloud settings network_config.update(DEFAULT_UNDERCLOUD_NETWORK_CONFIG) # Environment specific settings network_config.update(generic_utils.get_undercloud_env_vars()) - # Get keystone session - keystone_session = openstack_utils.get_overcloud_keystone_session() - # Get optional use_juju_wait for network option options = (lifecycle_utils .get_charm_config(fatal=False) @@ -118,6 +110,33 @@ def basic_overcloud_network(limit_gws=None): ' charm network configuration.' .format(provider_type)) + +def basic_overcloud_network(limit_gws=None): + """Run setup for neutron networking. + + Configure the following: + The overcloud network using subnet pools + + :param limit_gws: Limit the number of gateways that get a port attached + :type limit_gws: int + """ + cli_utils.setup_logging() + + # Get network configuration settings + network_config = {} + # Declared overcloud settings + network_config.update(OVERCLOUD_NETWORK_CONFIG) + # Default undercloud settings + network_config.update(DEFAULT_UNDERCLOUD_NETWORK_CONFIG) + # Environment specific settings + network_config.update(generic_utils.get_undercloud_env_vars()) + + # Get keystone session + keystone_session = openstack_utils.get_overcloud_keystone_session() + + # Perform undercloud and charm setup for network plumbing + undercloud_and_charm_setup(limit_gws=limit_gws) + # Configure the overcloud network network.setup_sdn(network_config, keystone_session=keystone_session) diff --git a/zaza/openstack/charm_tests/neutron/tests.py b/zaza/openstack/charm_tests/neutron/tests.py index 19bda01..cf925b0 100644 --- a/zaza/openstack/charm_tests/neutron/tests.py +++ b/zaza/openstack/charm_tests/neutron/tests.py @@ -28,10 +28,12 @@ from neutronclient.common import exceptions as neutronexceptions import yaml import zaza +import zaza.openstack.charm_tests.neutron.setup as neutron_setup import zaza.openstack.charm_tests.nova.utils as nova_utils import zaza.openstack.charm_tests.test_utils as test_utils import zaza.openstack.configure.guest as guest import zaza.openstack.utilities.openstack as openstack_utils +import zaza.utilities.machine_os class NeutronPluginApiSharedTests(test_utils.OpenStackBaseTest): @@ -805,6 +807,11 @@ class NeutronOvsVsctlTest(NeutronPluginApiSharedTests): self.assertEqual(actual_external_id, expected_external_id) +def router_address_from_subnet(subnet): + """Retrieve router address from subnet.""" + return subnet['gateway_ip'] + + class NeutronNetworkingBase(test_utils.OpenStackBaseTest): """Base for checking openstack instances have valid networking.""" @@ -818,6 +825,21 @@ class NeutronNetworkingBase(test_utils.OpenStackBaseTest): cls.neutron_client = ( openstack_utils.get_neutron_session_client(cls.keystone_session)) + cls.project_subnet = cls.neutron_client.find_resource( + 'subnet', + neutron_setup.OVERCLOUD_NETWORK_CONFIG['project_subnet_name']) + cls.external_subnet = cls.neutron_client.find_resource( + 'subnet', + neutron_setup.OVERCLOUD_NETWORK_CONFIG['external_subnet_name']) + + # Override this if you want your test to attach instances directly to + # the external provider network + cls.attach_to_external_network = False + + # Override this if you want your test to launch instances with a + # specific flavor + cls.instance_flavor = None + @tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60), reraise=True, stop=tenacity.stop_after_attempt(8)) def validate_instance_can_reach_other(self, @@ -840,8 +862,10 @@ class NeutronNetworkingBase(test_utils.OpenStackBaseTest): :param mtu: Check that we can send non-fragmented packets of given size :type mtu: Optional[int] """ - floating_1 = floating_ips_from_instance(instance_1)[0] - floating_2 = floating_ips_from_instance(instance_2)[0] + if not self.attach_to_external_network: + floating_1 = floating_ips_from_instance(instance_1)[0] + floating_2 = floating_ips_from_instance(instance_2)[0] + address_1 = fixed_ips_from_instance(instance_1)[0] address_2 = fixed_ips_from_instance(instance_2)[0] username = guest.boot_tests['bionic']['username'] @@ -859,15 +883,21 @@ class NeutronNetworkingBase(test_utils.OpenStackBaseTest): 'ping -M do -s {} -c 1'.format(packetsize)) for cmd in cmds: - openstack_utils.ssh_command( - username, floating_1, 'instance-1', - '{} {}'.format(cmd, address_2), - password=password, privkey=privkey, verify=verify) + if self.attach_to_external_network: + openstack_utils.ssh_command( + username, address_1, 'instance-1', + '{} {}'.format(cmd, address_2), + password=password, privkey=privkey, verify=verify) + else: + openstack_utils.ssh_command( + username, floating_1, 'instance-1', + '{} {}'.format(cmd, address_2), + password=password, privkey=privkey, verify=verify) - openstack_utils.ssh_command( - username, floating_1, 'instance-1', - '{} {}'.format(cmd, floating_2), - password=password, privkey=privkey, verify=verify) + openstack_utils.ssh_command( + username, floating_1, 'instance-1', + '{} {}'.format(cmd, floating_2), + password=password, privkey=privkey, verify=verify) @tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60), reraise=True, stop=tenacity.stop_after_attempt(8)) @@ -875,11 +905,6 @@ class NeutronNetworkingBase(test_utils.OpenStackBaseTest): """ Validate that an instance can reach it's primary gateway. - We make the assumption that the router's IP is 192.168.0.1 - as that's the network that is setup in - neutron.setup.basic_overcloud_network which is used in all - Zaza Neutron validations. - :param instance: The instance to check networking from :type instance: nova_client.Server @@ -889,7 +914,12 @@ class NeutronNetworkingBase(test_utils.OpenStackBaseTest): :param mtu: Check that we can send non-fragmented packets of given size :type mtu: Optional[int] """ - address = floating_ips_from_instance(instance)[0] + if self.attach_to_external_network: + router = router_address_from_subnet(self.external_subnet) + address = fixed_ips_from_instance(instance)[0] + else: + router = router_address_from_subnet(self.project_subnet) + address = floating_ips_from_instance(instance)[0] username = guest.boot_tests['bionic']['username'] password = guest.boot_tests['bionic'].get('password') @@ -907,7 +937,7 @@ class NeutronNetworkingBase(test_utils.OpenStackBaseTest): for cmd in cmds: openstack_utils.ssh_command( - username, address, 'instance', '{} 192.168.0.1'.format(cmd), + username, address, 'instance', '{} {}'.format(cmd, router), password=password, privkey=privkey, verify=verify) @tenacity.retry(wait=tenacity.wait_exponential(min=5, max=60), @@ -1086,13 +1116,77 @@ class NeutronNetworkingTest(NeutronNetworkingBase): """ instance_1, instance_2 = self.retrieve_guests() if not all([instance_1, instance_2]): - self.launch_guests() + self.launch_guests( + attach_to_external_network=self.attach_to_external_network, + flavor_name=self.instance_flavor) instance_1, instance_2 = self.retrieve_guests() self.check_connectivity(instance_1, instance_2) self.run_resource_cleanup = self.get_my_tests_options( 'run_resource_cleanup', True) +class DPDKNeutronNetworkingTest(NeutronNetworkingTest): + """Ensure that openstack instances have valid networking with DPDK.""" + + @classmethod + def setUpClass(cls): + """Run class setup for running Neutron API Networking tests.""" + super(DPDKNeutronNetworkingTest, cls).setUpClass() + + # At this point in time the charms do not support configuring overlay + # networks with DPDK. To perform end to end validation we need to + # attach instances directly to the provider network and subsequently + # DHCP needs to be enabled on that network. + # + # Note that for instances wired with DPDK the DHCP request/response is + # handled as private communication between the ovn-controller and the + # instance, and as such there is no risk of rogue DHCP replies escaping + # to the surrounding network. + cls.attach_to_external_network = True + cls.instance_flavor = 'hugepages' + cls.external_subnet = cls.neutron_client.find_resource( + 'subnet', + neutron_setup.OVERCLOUD_NETWORK_CONFIG['external_subnet_name']) + if ('dhcp_enabled' not in cls.external_subnet or + not cls.external_subnet['dhcp_enabled']): + logging.info('Enabling DHCP on subnet {}' + .format(cls.external_subnet['name'])) + openstack_utils.update_subnet_dhcp( + cls.neutron_client, cls.external_subnet, True) + + def test_instances_have_networking(self): + """Enable DPDK then Validate North/South and East/West networking.""" + self.enable_hugepages_vfio_on_hvs_in_vms(4) + with self.config_change( + { + 'enable-dpdk': False, + 'dpdk-driver': '', + }, + { + 'enable-dpdk': True, + 'dpdk-driver': 'vfio-pci', + }, + application_name='ovn-chassis'): + super().test_instances_have_networking() + self.run_resource_cleanup = self.get_my_tests_options( + 'run_resource_cleanup', True) + + def resource_cleanup(self): + """Extend to also revert VFIO NOIOMMU mode on units under test.""" + super().resource_cleanup() + if not self.run_resource_cleanup: + return + + if ('dhcp_enabled' not in self.external_subnet or + not self.external_subnet['dhcp_enabled']): + logging.info('Disabling DHCP on subnet {}' + .format(self.external_subnet['name'])) + openstack_utils.update_subnet_dhcp( + self.neutron_client, self.external_subnet, False) + + self.disable_hugepages_vfio_on_hvs_in_vms() + + class NeutronNetworkingVRRPTests(NeutronNetworkingBase): """Check networking when gateways are restarted.""" diff --git a/zaza/openstack/charm_tests/nova/utils.py b/zaza/openstack/charm_tests/nova/utils.py index f2c8af9..87af7d5 100644 --- a/zaza/openstack/charm_tests/nova/utils.py +++ b/zaza/openstack/charm_tests/nova/utils.py @@ -65,5 +65,14 @@ FLAVORS = { 'hw:tpm_model': 'tpm-crb', }, }, + 'hugepages': { + 'flavorid': 'auto', + 'ram': 1024, + 'disk': 20, + 'vcpus': 1, + 'extra-specs': { + 'hw:mem_page_size': 'large', + }, + }, } KEYPAIR_NAME = 'zaza' diff --git a/zaza/openstack/charm_tests/ovn/tests.py b/zaza/openstack/charm_tests/ovn/tests.py index 3b43d65..5bf551b 100644 --- a/zaza/openstack/charm_tests/ovn/tests.py +++ b/zaza/openstack/charm_tests/ovn/tests.py @@ -26,6 +26,7 @@ import zaza.model import zaza.openstack.charm_tests.test_utils as test_utils import zaza.openstack.utilities.generic as generic_utils import zaza.openstack.utilities.openstack as openstack_utils +import zaza.utilities.juju class BaseCharmOperationTest(test_utils.BaseCharmTest): @@ -177,7 +178,8 @@ class ChassisCharmOperationTest(BaseCharmOperationTest): 'target_deploy_status', {}) new_target_deploy_status = stored_target_deploy_status.copy() new_target_deploy_status[self.application_name] = { - 'ovn-chassis': 'blocked', + 'workload-status': 'blocked', + 'workload-status-message': 'Wrong format', } if 'target_deploy_status' in self.test_config: self.test_config['target_deploy_status'].update( @@ -186,13 +188,146 @@ class ChassisCharmOperationTest(BaseCharmOperationTest): self.test_config['target_deploy_status'] = new_target_deploy_status with self.config_change( - {'bridge-interface-mappings': ''}, + self.config_current( + application_name=self.application_name, + keys=['bridge-interface-mappings']), {'bridge-interface-mappings': 'incorrect'}): logging.info('Charm went into blocked state as expected, restore ' 'configuration') self.test_config[ 'target_deploy_status'] = stored_target_deploy_status + def _openvswitch_switch_dpdk_installed(self): + """Assert that the openvswitch-switch-dpdk package is installed. + + :raises: zaza.model.CommandRunFailed + """ + cmd = 'dpkg-query -s openvswitch-switch-dpdk' + for unit in zaza.model.get_units(self.application_name): + zaza.utilities.juju.remote_run( + unit.name, cmd, model_name=self.model_name, fatal=True) + + def _ovs_dpdk_init_configured(self): + """Assert that DPDK is configured. + + :raises: AssertionError, zaza.model.CommandRunFailed + """ + cmd = 'ovs-vsctl get open-vswitch . other_config:dpdk-init' + for unit in zaza.model.get_units(self.application_name): + result = zaza.utilities.juju.remote_run( + unit.name, + cmd, + model_name=self.model_name, + fatal=True).rstrip() + assert result == '"true"', ( + 'DPDK not configured on {}'.format(unit.name)) + + def _ovs_dpdk_initialized(self): + """Assert that OVS successfully initialized DPDK. + + :raises: AssertionError, zaza.model.CommandRunFailed + """ + cmd = 'ovs-vsctl get open-vswitch . dpdk_initialized' + for unit in zaza.model.get_units(self.application_name): + result = zaza.utilities.juju.remote_run( + unit.name, + cmd, + model_name=self.model_name, + fatal=True).rstrip() + assert result == 'true', ( + 'DPDK not initialized on {}'.format(unit.name)) + + def _ovs_br_ex_port_is_system_interface(self): + """Assert br-ex bridge is created and has system port in it. + + :raises: zaza.model.CommandRunFailed + """ + cmd = ('ip link show dev $(ovs-vsctl --bare --columns name ' + 'find port external_ids:charm-ovn-chassis=br-ex)') + for unit in zaza.model.get_units(self.application_name): + zaza.utilities.juju.remote_run( + unit.name, cmd, model_name=self.model_name, fatal=True) + + def _ovs_br_ex_port_is_dpdk_interface(self): + """Assert br-ex bridge is created and has DPDK port in it. + + :raises: zaza.model.CommandRunFailed + """ + cmd = ( + 'dpdk-devbind.py --status-dev net ' + '| grep ^$(ovs-vsctl --bare --columns options ' + 'find interface external_ids:charm-ovn-chassis=br-ex ' + '|cut -f2 -d=)' + '|grep "drv=vfio-pci unused=$"') + for unit in zaza.model.get_units(self.application_name): + zaza.utilities.juju.remote_run( + unit.name, cmd, model_name=self.model_name, fatal=True) + + def _ovs_br_ex_interface_not_in_error(self): + """Assert br-ex bridge is created and interface is not in error. + + :raises: AssertionError, zaza.model.CommandRunFailed + """ + cmd = ( + 'ovs-vsctl --bare --columns error ' + 'find interface external_ids:charm-ovn-chassis=br-ex') + for unit in zaza.model.get_units(self.application_name): + result = zaza.utilities.juju.remote_run( + unit.name, + cmd, + model_name=self.model_name, + fatal=True).rstrip() + assert result == '', result + + def _dpdk_pre_post_flight_check(self): + """Assert state of the system before and after enable/disable DPDK.""" + with self.assertRaises( + zaza.model.CommandRunFailed, + msg='openvswitch-switch-dpdk unexpectedly installed'): + self._openvswitch_switch_dpdk_installed() + with self.assertRaises( + zaza.model.CommandRunFailed, + msg='OVS unexpectedly configured for DPDK'): + self._ovs_dpdk_init_configured() + with self.assertRaises( + AssertionError, + msg='OVS unexpectedly has DPDK initialized'): + self._ovs_dpdk_initialized() + + def test_enable_dpdk(self): + """Confirm that transitioning to/from DPDK works.""" + logging.info('Pre-flight check') + self._dpdk_pre_post_flight_check() + self._ovs_br_ex_port_is_system_interface() + + self.enable_hugepages_vfio_on_hvs_in_vms(4) + with self.config_change( + { + 'enable-dpdk': False, + 'dpdk-driver': '', + }, + { + 'enable-dpdk': True, + 'dpdk-driver': 'vfio-pci', + }, + application_name='ovn-chassis'): + logging.info('Checking openvswitch-switch-dpdk is installed') + self._openvswitch_switch_dpdk_installed() + logging.info('Checking DPDK is configured in OVS') + self._ovs_dpdk_init_configured() + logging.info('Checking DPDK is successfully initialized in OVS') + self._ovs_dpdk_initialized() + logging.info('Checking that br-ex configed with DPDK interface...') + self._ovs_br_ex_port_is_dpdk_interface() + logging.info('and is not in error.') + self._ovs_br_ex_interface_not_in_error() + + logging.info('Post-flight check') + self._dpdk_pre_post_flight_check() + + self.disable_hugepages_vfio_on_hvs_in_vms() + self._ovs_br_ex_port_is_system_interface() + class OVSOVNMigrationTest(test_utils.BaseCharmTest): """OVS to OVN migration tests.""" diff --git a/zaza/openstack/charm_tests/test_utils.py b/zaza/openstack/charm_tests/test_utils.py index 9041df7..7fa2261 100644 --- a/zaza/openstack/charm_tests/test_utils.py +++ b/zaza/openstack/charm_tests/test_utils.py @@ -28,6 +28,7 @@ import zaza.openstack.configure.guest as configure_guest import zaza.openstack.utilities.openstack as openstack_utils import zaza.openstack.utilities.generic as generic_utils import zaza.openstack.charm_tests.glance.setup as glance_setup +import zaza.utilities.machine_os def skipIfNotHA(service_name): @@ -599,6 +600,122 @@ class BaseCharmTest(unittest.TestCase): for unit in units: model.run_on_unit(unit, "hooks/update-status") + def assert_unit_cpu_topology(self, unit, nr_1g_hugepages): + r"""Assert unit under test CPU topology. + + When using OpenStack as CI substrate: + + By default, when instance NUMA placement is not specified, + a topology of N sockets, each with one core and one thread, + is used for an instance, where N corresponds to the number of + instance vCPUs requested. + + In this context a socket is a physical socket on the motherboard + where a CPU is connected. + + The DPDK Environment Abstraction Layer (EAL) allocates memory per + CPU socket, so we want the CPU topology inside the instance to + mimic something we would be likely to find in the real world and + at the same time not make the test too heavy. + + The charm default is to have Open vSwitch allocate 1GB RAM per + CPU socket. + + The following command would set the apropriate CPU topology for a + 4 VCPU, 8 GB RAM flavor: + + openstack flavor set onesocketm1.large \ + --property hw:cpu_sockets=1 \ + --property hw:cpu_cores=2 \ + --property hw:cpu_threads=2 + + For validation of operation with multiple sockets, the following + command would set the apropriate CPU topology for a + 8 VCPU, 16GB RAM flavor: + + openstack flavor set twosocketm1.xlarge \ + --property hw:cpu_sockets=2 \ + --property hw:cpu_cores=2 \ + --property hw:cpu_threads=2 \ + --property hw:numa_nodes=2 + """ + # Get number of sockets + cmd = 'lscpu -p|grep -v ^#|cut -f3 -d,|sort|uniq|wc -l' + sockets = int(zaza.utilities.juju.remote_run( + unit.name, cmd, model_name=self.model_name, fatal=True).rstrip()) + + # Get total memory + cmd = 'cat /proc/meminfo |grep ^MemTotal' + _, meminfo_value, _ = zaza.utilities.juju.remote_run( + unit.name, + cmd, + model_name=self.model_name, + fatal=True).rstrip().split() + mbtotal = int(meminfo_value) * 1024 / 1000 / 1000 + mbtotalhugepages = nr_1g_hugepages * 1024 + + # headroom for operating system and daemons in instance + mbsystemheadroom = 2048 + # memory to be consumed by the nested instance + mbinstance = 1024 + + # the amount of hugepage memory OVS / DPDK EAL will allocate + mbovshugepages = sockets * 1024 + # the amount of hugepage memory available for nested instance + mbfreehugepages = mbtotalhugepages - mbovshugepages + + assert (mbtotal - mbtotalhugepages >= mbsystemheadroom and + mbfreehugepages >= mbinstance), ( + 'Unit {} is not suitable for test, please adjust instance ' + 'type CPU topology or provide suitable physical machine. ' + 'CPU Sockets: {} ' + 'Available memory: {} MB ' + 'Details:\n{}' + .format(unit.name, + sockets, + mbtotal, + self.assert_unit_cpu_topology.__doc__)) + + def enable_hugepages_vfio_on_hvs_in_vms(self, nr_1g_hugepages): + """Enable hugepages and unsafe VFIO NOIOMMU on virtual hypervisors.""" + for unit in model.get_units( + zaza.utilities.machine_os.get_hv_application(), + model_name=self.model_name): + if not zaza.utilities.machine_os.is_vm(unit.name, + model_name=self.model_name): + logging.info('Unit {} is a physical machine, assuming ' + 'hugepages and IOMMU configuration already ' + 'performed through kernel command line.') + continue + logging.info('Checking CPU topology on {}'.format(unit.name)) + self.assert_unit_cpu_topology(unit, nr_1g_hugepages) + logging.info('Enabling hugepages on {}'.format(unit.name)) + zaza.utilities.machine_os.enable_hugepages( + unit, nr_1g_hugepages, model_name=self.model_name) + logging.info('Enabling unsafe VFIO NOIOMMU mode on {}' + .format(unit.name)) + zaza.utilities.machine_os.enable_vfio_unsafe_noiommu_mode( + unit, model_name=self.model_name) + + def disable_hugepages_vfio_on_hvs_in_vms(self): + """Disable hugepages and unsafe VFIO NOIOMMU on virtual hypervisors.""" + for unit in model.get_units( + zaza.utilities.machine_os.get_hv_application(), + model_name=self.model_name): + if not zaza.utilities.machine_os.is_vm(unit.name, + model_name=self.model_name): + logging.info('Unit {} is a physical machine, assuming ' + 'hugepages and IOMMU configuration already ' + 'performed through kernel command line.') + continue + logging.info('Disabling hugepages on {}'.format(unit.name)) + zaza.utilities.machine_os.disable_hugepages( + unit, model_name=self.model_name) + logging.info('Disabling unsafe VFIO NOIOMMU mode on {}' + .format(unit.name)) + zaza.utilities.machine_os.disable_vfio_unsafe_noiommu_mode( + unit, model_name=self.model_name) + class OpenStackBaseTest(BaseCharmTest): """Generic helpers for testing OpenStack API charms.""" @@ -634,7 +751,8 @@ class OpenStackBaseTest(BaseCharmTest): pass def launch_guest(self, guest_name, userdata=None, use_boot_volume=False, - instance_key=None, flavor_name=None): + instance_key=None, flavor_name=None, + attach_to_external_network=False): """Launch one guest to use in tests. Note that it is up to the caller to have set the RESOURCE_PREFIX class @@ -651,6 +769,9 @@ class OpenStackBaseTest(BaseCharmTest): :type use_boot_volume: boolean :param instance_key: Key to collect associated config data with. :type instance_key: Optional[str] + :param attach_to_external_network: Attach instance directly to external + network. + :type attach_to_external_network: bool :returns: Nova instance objects :rtype: Server """ @@ -679,9 +800,11 @@ class OpenStackBaseTest(BaseCharmTest): vm_name=instance_name, use_boot_volume=use_boot_volume, userdata=userdata, - flavor_name=flavor_name) + flavor_name=flavor_name, + attach_to_external_network=attach_to_external_network) - def launch_guests(self, userdata=None): + def launch_guests(self, userdata=None, attach_to_external_network=False, + flavor_name=None): """Launch two guests to use in tests. Note that it is up to the caller to have set the RESOURCE_PREFIX class @@ -689,6 +812,9 @@ class OpenStackBaseTest(BaseCharmTest): :param userdata: Userdata to attach to instance :type userdata: Optional[str] + :param attach_to_external_network: Attach instance directly to external + network. + :type attach_to_external_network: bool :returns: List of launched Nova instance objects :rtype: List[Server] """ @@ -697,7 +823,9 @@ class OpenStackBaseTest(BaseCharmTest): launched_instances.append( self.launch_guest( guest_name='ins-{}'.format(guest_number), - userdata=userdata)) + userdata=userdata, + attach_to_external_network=attach_to_external_network, + flavor_name=flavor_name)) return launched_instances def retrieve_guest(self, guest_name): diff --git a/zaza/openstack/configure/guest.py b/zaza/openstack/configure/guest.py index 4e20ef4..d5034b3 100644 --- a/zaza/openstack/configure/guest.py +++ b/zaza/openstack/configure/guest.py @@ -53,7 +53,7 @@ boot_tests = { def launch_instance(instance_key, use_boot_volume=False, vm_name=None, private_network_name=None, image_name=None, flavor_name=None, external_network_name=None, meta=None, - userdata=None): + userdata=None, attach_to_external_network=False): """Launch an instance. :param instance_key: Key to collect associated config data with. @@ -76,6 +76,9 @@ def launch_instance(instance_key, use_boot_volume=False, vm_name=None, :type meta: dict :param userdata: Configuration to use upon launch, used by cloud-init. :type userdata: str + :param attach_to_external_network: Attach instance directly to external + network. + :type attach_to_external_network: bool :returns: the created instance :rtype: novaclient.Server """ @@ -94,12 +97,18 @@ def launch_instance(instance_key, use_boot_volume=False, vm_name=None, flavor = nova_client.flavors.find(name=flavor_name) private_network_name = private_network_name or "private" - net = neutron_client.find_resource("network", private_network_name) - nics = [{'net-id': net.get('id')}] meta = meta or {} external_network_name = external_network_name or "ext_net" + if attach_to_external_network: + instance_network_name = external_network_name + else: + instance_network_name = private_network_name + + net = neutron_client.find_resource("network", instance_network_name) + nics = [{'net-id': net.get('id')}] + if use_boot_volume: bdmv2 = [{ 'boot_index': '0', @@ -143,12 +152,19 @@ def launch_instance(instance_key, use_boot_volume=False, vm_name=None, port = openstack_utils.get_ports_from_device_id( neutron_client, instance.id)[0] - logging.info('Assigning floating ip.') - ip = openstack_utils.create_floating_ip( - neutron_client, - external_network_name, - port=port)['floating_ip_address'] - logging.info('Assigned floating IP {} to {}'.format(ip, vm_name)) + if attach_to_external_network: + logging.info('attach_to_external_network={}, not assigning floating IP' + .format(attach_to_external_network)) + ip = port['fixed_ips'][0]['ip_address'] + logging.info('Using fixed IP {} on network {} for {}' + .format(ip, instance_network_name, vm_name)) + else: + logging.info('Assigning floating ip.') + ip = openstack_utils.create_floating_ip( + neutron_client, + external_network_name, + port=port)['floating_ip_address'] + logging.info('Assigned floating IP {} to {}'.format(ip, vm_name)) try: for attempt in Retrying( stop=stop_after_attempt(8), diff --git a/zaza/openstack/configure/network.py b/zaza/openstack/configure/network.py index c0bbe1c..cef23f9 100755 --- a/zaza/openstack/configure/network.py +++ b/zaza/openstack/configure/network.py @@ -87,6 +87,7 @@ from zaza.openstack.utilities import ( generic as generic_utils, openstack as openstack_utils, ) +import zaza.openstack.utilities.exceptions import zaza.utilities.juju as juju_utils @@ -139,7 +140,8 @@ def setup_sdn(network_config, keystone_session=None): network_config["default_gateway"], network_config["external_net_cidr"], network_config["start_floating_ip"], - network_config["end_floating_ip"]) + network_config["end_floating_ip"], + dhcp=True) provider_router = ( openstack_utils.create_provider_router(neutron_client, project_id)) openstack_utils.plug_extnet_into_router( @@ -164,14 +166,16 @@ def setup_sdn(network_config, keystone_session=None): neutron_client, project_id, shared=False, - network_type=network_config["network_type"]) + network_type=network_config["network_type"], + net_name=network_config["project_net_name"]) project_subnet = openstack_utils.create_project_subnet( neutron_client, project_id, project_network, network_config.get("private_net_cidr"), subnetpool=subnetpool, - ip_version=ip_version) + ip_version=ip_version, + subnet_name=network_config["project_subnet_name"]) openstack_utils.update_subnet_dns( neutron_client, project_subnet, @@ -273,14 +277,19 @@ def setup_gateway_ext_port(network_config, keystone_session=None, else: net_id = None - # If we're using netplan, we need to add the new interface to the guest - current_release = openstack_utils.get_os_release() - bionic_queens = openstack_utils.get_os_release('bionic_queens') - if current_release >= bionic_queens: - logging.warn("Adding second interface for dataport to guest netplan " - "for bionic-queens and later") - add_dataport_to_netplan = True - else: + try: + # If we're using netplan, we need to add the new interface to the guest + current_release = openstack_utils.get_os_release() + bionic_queens = openstack_utils.get_os_release('bionic_queens') + if current_release >= bionic_queens: + logging.warn("Adding second interface for dataport to guest " + "netplan for bionic-queens and later") + add_dataport_to_netplan = True + else: + add_dataport_to_netplan = False + except zaza.openstack.utilities.exceptions.ApplicationNotFound: + # The setup_gateway_ext_port helper may be used with non-OpenStack + # workloads. add_dataport_to_netplan = False logging.info("Configuring network for OpenStack undercloud/provider") diff --git a/zaza/openstack/utilities/openstack.py b/zaza/openstack/utilities/openstack.py index bc04a1d..1dfe5a5 100644 --- a/zaza/openstack/utilities/openstack.py +++ b/zaza/openstack/utilities/openstack.py @@ -650,7 +650,10 @@ def dvr_enabled(): :returns: True when DVR is enabled, False otherwise :rtype: bool """ - return get_application_config_option('neutron-api', 'enable-dvr') + try: + return get_application_config_option('neutron-api', 'enable-dvr') + except KeyError: + return False def ngw_present(): @@ -1328,6 +1331,24 @@ def update_subnet_dns(neutron_client, subnet, dns_servers): neutron_client.update_subnet(subnet['id'], msg) +def update_subnet_dhcp(neutron_client, subnet, enable_dhcp): + """Update subnet DHCP status. + + :param neutron_client: Authenticated neutronclient + :type neutron_client: neutronclient.Client object + :param subnet: Subnet object + :type subnet: dict + :param enable_dhcp: Whether DHCP should be enabled or not + :type enable_dhcp: bool + """ + msg = { + 'subnet': { + 'enable_dhcp': enable_dhcp, + } + } + neutron_client.update_subnet(subnet['id'], msg) + + def create_provider_router(neutron_client, project_id): """Create the provider router.