From 10f76b2bc63bdfa41b59028a2f06217ae54f97bd Mon Sep 17 00:00:00 2001 From: spisarski Date: Fri, 1 Dec 2017 09:48:50 -0700 Subject: Added ability to add a floating IP to an existing instance Patch includes: 1. Ability to easily retrive an initialized OpenStackVmInstance object 2. Added method to OpenStackVmInstance named "add_floating_ip" 3. Tests to ensure floating IPs added after initialization actually work JIRA: SNAPS-241, SNAPS-242 Change-Id: I1f413645a752c17fd624ecff26e851068bb77e3f Signed-off-by: spisarski --- docs/how-to-use/IntegrationTests.rst | 6 ++ snaps/openstack/create_instance.py | 108 ++++++++++++++++++------- snaps/openstack/tests/create_instance_tests.py | 86 +++++++++++++++++++- snaps/openstack/utils/neutron_utils.py | 12 +-- snaps/openstack/utils/settings_utils.py | 16 ++-- 5 files changed, 180 insertions(+), 48 deletions(-) diff --git a/docs/how-to-use/IntegrationTests.rst b/docs/how-to-use/IntegrationTests.rst index 1856bb9..a11afb9 100644 --- a/docs/how-to-use/IntegrationTests.rst +++ b/docs/how-to-use/IntegrationTests.rst @@ -542,6 +542,12 @@ create_instance_tests.py - CreateInstanceSingleNetworkTests | test_ssh_client_fip_after_active | Nova 2 | Ensures that an instance can be reached over SSH when the | | | Neutron 2 | floating IP is assigned after to the VM becoming ACTIVE | +---------------------------------------+---------------+-----------------------------------------------------------+ +| test_ssh_client_fip_after_init | Nova 2 | Ensures that an instance can have a floating IP assigned | +| | Neutron 2 | added after initialization | ++---------------------------------------+---------------+-----------------------------------------------------------+ +| test_ssh_client_fip_reverse_engineer | Nova 2 | Ensures that an instance can be reverse engineered and | +| | Neutron 2 | allows for a floating IP to be added after initialization | ++---------------------------------------+---------------+-----------------------------------------------------------+ | test_ssh_client_fip_second_creator | Nova 2 | Ensures that an instance can be reached over SSH via a | | | Neutron 2 | second identical creator object | +---------------------------------------+---------------+-----------------------------------------------------------+ diff --git a/snaps/openstack/create_instance.py b/snaps/openstack/create_instance.py index 59bb8e4..d7300e6 100644 --- a/snaps/openstack/create_instance.py +++ b/snaps/openstack/create_instance.py @@ -20,7 +20,7 @@ from novaclient.exceptions import NotFound, BadRequest from snaps.config.vm_inst import VmInstanceConfig, FloatingIpConfig from snaps.openstack.openstack_creator import OpenStackComputeObject -from snaps.openstack.utils import glance_utils, cinder_utils +from snaps.openstack.utils import glance_utils, cinder_utils, settings_utils from snaps.openstack.utils import neutron_utils from snaps.openstack.utils import nova_utils from snaps.provisioning import ansible_utils @@ -188,33 +188,45 @@ class OpenStackVmInstance(OpenStackComputeObject): # Apply floating IPs for floating_ip_setting in self.instance_settings.floating_ip_settings: - port = port_dict.get(floating_ip_setting.port_name) + self.add_floating_ip(floating_ip_setting) - if not port: - raise VmInstanceCreationError( - 'Cannot find port object with name - ' + - floating_ip_setting.port_name) + def add_floating_ip(self, floating_ip_setting): + """ + Adds a floating IP to a running instance + :param floating_ip_setting - the floating IP configuration + """ + port_dict = dict() + for key, port in self.__ports: + port_dict[key] = port - # Setup Floating IP only if there is a router with an external - # gateway - ext_gateway = self.__ext_gateway_by_router( - floating_ip_setting.router_name) - if ext_gateway: - subnet = neutron_utils.get_subnet( - self.__neutron, - subnet_name=floating_ip_setting.subnet_name) - floating_ip = neutron_utils.create_floating_ip( - self.__neutron, ext_gateway) - self.__floating_ip_dict[floating_ip_setting.name] = floating_ip + # Apply floating IP + port = port_dict.get(floating_ip_setting.port_name) - logger.info( - 'Created floating IP %s via router - %s', floating_ip.ip, - floating_ip_setting.router_name) - self.__add_floating_ip(floating_ip, port, subnet) - else: - raise VmInstanceCreationError( - 'Unable to add floating IP to port, cannot locate router ' - 'with an external gateway ') + if not port: + raise VmInstanceCreationError( + 'Cannot find port object with name - ' + + floating_ip_setting.port_name) + + # Setup Floating IP only if there is a router with an external + # gateway + ext_gateway = self.__ext_gateway_by_router( + floating_ip_setting.router_name) + if ext_gateway: + subnet = neutron_utils.get_subnet( + self.__neutron, + subnet_name=floating_ip_setting.subnet_name) + floating_ip = neutron_utils.create_floating_ip( + self.__neutron, ext_gateway) + self.__floating_ip_dict[floating_ip_setting.name] = floating_ip + + logger.info( + 'Created floating IP %s via router - %s', floating_ip.ip, + floating_ip_setting.router_name) + self.__add_floating_ip(floating_ip, port, subnet) + else: + raise VmInstanceCreationError( + 'Unable to add floating IP to port, cannot locate router ' + 'with an external gateway ') def __ext_gateway_by_router(self, router_name): """ @@ -506,6 +518,11 @@ class OpenStackVmInstance(OpenStackComputeObject): for key, fip in self.__floating_ip_dict.items(): return fip + # When cannot be found above + if len(self.__floating_ip_dict) > 0: + for key, fip in self.__floating_ip_dict.items(): + return fip + def __config_nic(self, nic_name, port, ip): """ Although ports/NICs can contain multiple IPs, this code currently only @@ -585,9 +602,13 @@ class OpenStackVmInstance(OpenStackComputeObject): :param poll_interval: The polling interval in seconds :return: T/F """ - return self.__vm_status_check(STATUS_ACTIVE, block, - self.instance_settings.vm_boot_timeout, - poll_interval) + if self.__vm_status_check( + STATUS_ACTIVE, block, self.instance_settings.vm_boot_timeout, + poll_interval): + self.__vm = nova_utils.get_server_object_by_id( + self._nova, self.__vm.id) + return True + return False def __vm_status_check(self, expected_status_code, block, timeout, poll_interval): @@ -703,10 +724,9 @@ class OpenStackVmInstance(OpenStackComputeObject): :param fip_name: the name of the floating IP to return :return: the SSH client or None """ - fip = None if fip_name and self.__floating_ip_dict.get(fip_name): return self.__floating_ip_dict.get(fip_name) - if not fip: + else: return self.__get_first_provisioning_floating_ip() def ssh_client(self, fip_name=None): @@ -724,7 +744,7 @@ class OpenStackVmInstance(OpenStackComputeObject): self.keypair_settings.private_filepath, proxy_settings=self._os_creds.proxy_settings) else: - logger.warning( + FloatingIPAllocationError( 'Cannot return an SSH client. No Floating IP configured') def add_security_group(self, security_group): @@ -768,6 +788,26 @@ class OpenStackVmInstance(OpenStackComputeObject): return False +def generate_creator(os_creds, vm_inst, image_config, keypair_config=None): + """ + Initializes an OpenStackVmInstance object + :param os_creds: the OpenStack credentials + :param vm_inst: the SNAPS-OO VmInst domain object + :param image_config: the associated ImageConfig object + :param keypair_config: the associated KeypairConfig object (optional) + :return: an initialized OpenStackVmInstance object + """ + nova = nova_utils.nova_client(os_creds) + neutron = neutron_utils.neutron_client(os_creds) + derived_inst_config = settings_utils.create_vm_inst_config( + nova, neutron, vm_inst) + + derived_inst_creator = OpenStackVmInstance( + os_creds, derived_inst_config, image_config, keypair_config) + derived_inst_creator.initialize() + return derived_inst_creator + + class VmInstanceSettings(VmInstanceConfig): """ Deprecated, use snaps.config.vm_inst.VmInstanceConfig instead @@ -794,3 +834,9 @@ class VmInstanceCreationError(Exception): """ Exception to be thrown when an VM instance cannot be created """ + + +class FloatingIPAllocationError(Exception): + """ + Exception to be thrown when an VM instance cannot allocate a floating IP + """ diff --git a/snaps/openstack/tests/create_instance_tests.py b/snaps/openstack/tests/create_instance_tests.py index 4a516d3..1721ce3 100644 --- a/snaps/openstack/tests/create_instance_tests.py +++ b/snaps/openstack/tests/create_instance_tests.py @@ -35,7 +35,7 @@ from snaps.config.vm_inst import ( VmInstanceConfig, FloatingIpConfig, VmInstanceConfigError, FloatingIpConfigError) from snaps.config.volume import VolumeConfig -from snaps.openstack import create_network, create_router +from snaps.openstack import create_network, create_router, create_instance from snaps.openstack.create_flavor import OpenStackFlavor from snaps.openstack.create_image import OpenStackImage from snaps.openstack.create_instance import ( @@ -48,7 +48,7 @@ from snaps.openstack.create_volume import OpenStackVolume from snaps.openstack.tests import openstack_tests, validation_utils from snaps.openstack.tests.os_source_file_test import ( OSIntegrationTestCase, OSComponentTestCase) -from snaps.openstack.utils import nova_utils +from snaps.openstack.utils import nova_utils, settings_utils, neutron_utils __author__ = 'spisarski' @@ -765,6 +765,83 @@ class CreateInstanceSingleNetworkTests(OSIntegrationTestCase): self.assertTrue(validate_ssh_client(inst_creator)) + def test_ssh_client_fip_after_init(self): + """ + Tests the ability to assign a floating IP to an already initialized + OpenStackVmInstance object. After the floating IP has been allocated + and assigned, this test will ensure that it can be accessed via SSH. + """ + port_settings = PortConfig( + name=self.port_1_name, + network_name=self.pub_net_config.network_settings.name) + + instance_settings = VmInstanceConfig( + name=self.vm_inst_name, + flavor=self.flavor_creator.flavor_settings.name, + port_settings=[port_settings], + security_group_names=[self.sec_grp_creator.sec_grp_settings.name]) + + inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, + self.image_creator.image_settings, + keypair_settings=self.keypair_creator.keypair_settings) + self.inst_creators.append(inst_creator) + + # block=True will force the create() method to block until the + vm_inst = inst_creator.create(block=True) + self.assertIsNotNone(vm_inst) + + self.assertTrue(inst_creator.vm_active(block=True)) + ip = inst_creator.get_port_ip(port_settings.name) + self.assertTrue(check_dhcp_lease(inst_creator, ip)) + self.assertEqual(vm_inst.id, inst_creator.get_vm_inst().id) + + inst_creator.add_floating_ip(FloatingIpConfig( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_settings.name)) + + self.assertTrue(validate_ssh_client(inst_creator)) + + def test_ssh_client_fip_reverse_engineer(self): + """ + Tests the ability to assign a floating IP to a reverse engineered + OpenStackVmInstance object. After the floating IP has been allocated + and assigned, this test will ensure that it can be accessed via SSH. + """ + port_settings = PortConfig( + name=self.port_1_name, + network_name=self.pub_net_config.network_settings.name) + + instance_settings = VmInstanceConfig( + name=self.vm_inst_name, + flavor=self.flavor_creator.flavor_settings.name, + port_settings=[port_settings], + security_group_names=[self.sec_grp_creator.sec_grp_settings.name]) + + inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, + self.image_creator.image_settings, + keypair_settings=self.keypair_creator.keypair_settings) + self.inst_creators.append(inst_creator) + + # block=True will force the create() method to block until the + vm_inst = inst_creator.create(block=True) + self.assertIsNotNone(vm_inst) + + self.assertTrue(inst_creator.vm_active(block=True)) + + derived_inst_creator = create_instance.generate_creator( + self.os_creds, vm_inst, self.image_creator.image_settings, + self.keypair_creator.keypair_settings) + + derived_inst_creator.add_floating_ip(FloatingIpConfig( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_settings.name)) + self.inst_creators.append(derived_inst_creator) + + self.assertTrue(validate_ssh_client( + derived_inst_creator, fip_name=self.floating_ip_name)) + def test_ssh_client_fip_second_creator(self): """ Tests the ability to access a VM via SSH and a floating IP via a @@ -1979,17 +2056,18 @@ def inst_has_sec_grp(nova, vm_inst, sec_grp_name): return False -def validate_ssh_client(instance_creator): +def validate_ssh_client(instance_creator, fip_name=None): """ Returns True if instance_creator returns an SSH client that is valid :param instance_creator: the object responsible for creating the VM instance + :param fip_name: the name of the floating IP to use :return: T/F """ ssh_active = instance_creator.vm_ssh_active(block=True) if ssh_active: - ssh_client = instance_creator.ssh_client() + ssh_client = instance_creator.ssh_client(fip_name=fip_name) if ssh_client: try: out = ssh_client.exec_command('pwd')[1] diff --git a/snaps/openstack/utils/neutron_utils.py b/snaps/openstack/utils/neutron_utils.py index eaceb37..9b6379a 100644 --- a/snaps/openstack/utils/neutron_utils.py +++ b/snaps/openstack/utils/neutron_utils.py @@ -636,7 +636,7 @@ def get_security_group_by_id(neutron, sec_grp_id): def create_security_group_rule(neutron, sec_grp_rule_settings): """ - Creates a security group object in OpenStack + Creates a security group rule in OpenStack :param neutron: the client :param sec_grp_rule_settings: the security group rule settings :return: a SNAPS-OO SecurityGroupRule domain object @@ -650,7 +650,7 @@ def create_security_group_rule(neutron, sec_grp_rule_settings): def delete_security_group_rule(neutron, sec_grp_rule): """ - Deletes a security group object from OpenStack + Deletes a security group rule object from OpenStack :param neutron: the client :param sec_grp_rule: the SNAPS SecurityGroupRule object to delete """ @@ -670,7 +670,7 @@ def get_rules_by_security_group(neutron, sec_grp): def get_rules_by_security_group_id(neutron, sec_grp_id): """ - Retrieves all of the rules for a given security group + Retrieves all of the rules for a given security group by it's ID :param neutron: the client :param sec_grp_id: the ID of the associated security group """ @@ -687,7 +687,7 @@ def get_rules_by_security_group_id(neutron, sec_grp_id): def get_rule_by_id(neutron, sec_grp, rule_id): """ - Deletes a security group object from OpenStack + Returns a SecurityGroupRule object from OpenStack :param neutron: the client :param sec_grp: the SNAPS SecurityGroup domain object :param rule_id: the rule's ID @@ -727,7 +727,7 @@ def get_floating_ips(neutron, ports=None): :param ports: a list of tuple 2 where index 0 is the port name and index 1 is the SNAPS-OO Port object :return: a list of tuple 2 (port_id, SNAPS FloatingIp) objects when ports - is not None else a list of Port objects + is not None else a list of FloatingIp objects """ out = list() fips = neutron.list_floatingips() @@ -811,7 +811,7 @@ def delete_floating_ip(neutron, floating_ip): def get_network_quotas(neutron, project_id): """ - Returns a list of all available keypairs + Returns a list of NetworkQuotas objects :param neutron: the neutron client :param project_id: the project's ID of the quotas to lookup :return: an object of type NetworkQuotas or None if not found diff --git a/snaps/openstack/utils/settings_utils.py b/snaps/openstack/utils/settings_utils.py index d1a9cc6..4ad30fd 100644 --- a/snaps/openstack/utils/settings_utils.py +++ b/snaps/openstack/utils/settings_utils.py @@ -43,7 +43,7 @@ def create_network_config(neutron, network): def create_security_group_config(neutron, security_group): """ - Returns a NetworkConfig object + Returns a SecurityGroupConfig object :param neutron: the neutron client :param security_group: a SNAPS-OO SecurityGroup domain object :return: @@ -148,7 +148,7 @@ def create_router_config(neutron, router): def create_volume_config(volume): """ - Returns a VolumeSettings object + Returns a VolumeConfig object :param volume: a SNAPS-OO Volume object """ @@ -161,7 +161,7 @@ def create_volume_config(volume): def create_volume_type_config(volume_type): """ - Returns a VolumeTypeSettings object + Returns a VolumeTypeConfig object :param volume_type: a SNAPS-OO VolumeType object """ @@ -194,8 +194,8 @@ def create_volume_type_config(volume_type): def create_flavor_config(flavor): """ - Returns a VolumeSettings object - :param flavor: a SNAPS-OO Volume object + Returns a FlavorConfig object + :param flavor: a FlavorConfig object """ return FlavorConfig( name=flavor.name, flavor_id=flavor.id, ram=flavor.ram, @@ -232,7 +232,9 @@ def create_keypair_config(heat_cli, stack, keypair, pk_output_key): def create_vm_inst_config(nova, neutron, server): """ - Returns a NetworkConfig object + Returns a VmInstanceConfig object + note: if the server instance is not active, the PortSettings objects will + not be generated resulting in an invalid configuration :param nova: the nova client :param neutron: the neutron client :param server: a SNAPS-OO VmInst domain object @@ -262,7 +264,7 @@ def create_vm_inst_config(nova, neutron, server): def __create_port_config(neutron, networks): """ - Returns a list of port settings based on the networks parameter + Returns a list of PortConfig objects based on the networks parameter :param neutron: the neutron client :param networks: a list of tuples where #1 is the SNAPS Network domain object and #2 is a list of IP addresses -- cgit 1.2.3-korg