From 57777f3df521553a06cd01a3861b415d2905ceca Mon Sep 17 00:00:00 2001 From: spisarski Date: Wed, 15 Feb 2017 09:13:54 -0700 Subject: Initial patch with all code from CableLabs repository. Change-Id: I70a2778718c5e7f21fd14e4ad28c9269d3761cc7 Signed-off-by: spisarski --- snaps/openstack/__init__.py | 15 + snaps/openstack/create_flavor.py | 167 +++ snaps/openstack/create_image.py | 188 +++ snaps/openstack/create_instance.py | 739 ++++++++++ snaps/openstack/create_keypairs.py | 121 ++ snaps/openstack/create_network.py | 519 +++++++ snaps/openstack/create_project.py | 139 ++ snaps/openstack/create_router.py | 244 ++++ snaps/openstack/create_security_group.py | 521 +++++++ snaps/openstack/create_user.py | 137 ++ snaps/openstack/os_credentials.py | 103 ++ snaps/openstack/tests/__init__.py | 15 + snaps/openstack/tests/conf/os_env.yaml.template | 39 + snaps/openstack/tests/conf/overcloudrc_test | 9 + snaps/openstack/tests/create_flavor_tests.py | 311 +++++ snaps/openstack/tests/create_image_tests.py | 362 +++++ snaps/openstack/tests/create_instance_tests.py | 1474 ++++++++++++++++++++ snaps/openstack/tests/create_keypairs_tests.py | 203 +++ snaps/openstack/tests/create_network_tests.py | 533 +++++++ snaps/openstack/tests/create_project_tests.py | 228 +++ snaps/openstack/tests/create_router_tests.py | 264 ++++ .../openstack/tests/create_security_group_tests.py | 355 +++++ snaps/openstack/tests/create_user_tests.py | 155 ++ snaps/openstack/tests/openstack_tests.py | 144 ++ snaps/openstack/tests/os_source_file_test.py | 131 ++ snaps/openstack/tests/validation_utils.py | 69 + snaps/openstack/utils/__init__.py | 15 + snaps/openstack/utils/deploy_utils.py | 151 ++ snaps/openstack/utils/glance_utils.py | 78 ++ snaps/openstack/utils/keystone_utils.py | 204 +++ snaps/openstack/utils/neutron_utils.py | 405 ++++++ snaps/openstack/utils/nova_utils.py | 282 ++++ snaps/openstack/utils/tests/__init__.py | 15 + snaps/openstack/utils/tests/glance_utils_tests.py | 115 ++ .../openstack/utils/tests/keystone_utils_tests.py | 100 ++ snaps/openstack/utils/tests/neutron_utils_tests.py | 651 +++++++++ snaps/openstack/utils/tests/nova_utils_tests.py | 208 +++ 37 files changed, 9409 insertions(+) create mode 100644 snaps/openstack/__init__.py create mode 100644 snaps/openstack/create_flavor.py create mode 100644 snaps/openstack/create_image.py create mode 100644 snaps/openstack/create_instance.py create mode 100644 snaps/openstack/create_keypairs.py create mode 100644 snaps/openstack/create_network.py create mode 100644 snaps/openstack/create_project.py create mode 100644 snaps/openstack/create_router.py create mode 100644 snaps/openstack/create_security_group.py create mode 100644 snaps/openstack/create_user.py create mode 100644 snaps/openstack/os_credentials.py create mode 100644 snaps/openstack/tests/__init__.py create mode 100644 snaps/openstack/tests/conf/os_env.yaml.template create mode 100644 snaps/openstack/tests/conf/overcloudrc_test create mode 100644 snaps/openstack/tests/create_flavor_tests.py create mode 100644 snaps/openstack/tests/create_image_tests.py create mode 100644 snaps/openstack/tests/create_instance_tests.py create mode 100644 snaps/openstack/tests/create_keypairs_tests.py create mode 100644 snaps/openstack/tests/create_network_tests.py create mode 100644 snaps/openstack/tests/create_project_tests.py create mode 100644 snaps/openstack/tests/create_router_tests.py create mode 100644 snaps/openstack/tests/create_security_group_tests.py create mode 100644 snaps/openstack/tests/create_user_tests.py create mode 100644 snaps/openstack/tests/openstack_tests.py create mode 100644 snaps/openstack/tests/os_source_file_test.py create mode 100644 snaps/openstack/tests/validation_utils.py create mode 100644 snaps/openstack/utils/__init__.py create mode 100644 snaps/openstack/utils/deploy_utils.py create mode 100644 snaps/openstack/utils/glance_utils.py create mode 100644 snaps/openstack/utils/keystone_utils.py create mode 100644 snaps/openstack/utils/neutron_utils.py create mode 100644 snaps/openstack/utils/nova_utils.py create mode 100644 snaps/openstack/utils/tests/__init__.py create mode 100644 snaps/openstack/utils/tests/glance_utils_tests.py create mode 100644 snaps/openstack/utils/tests/keystone_utils_tests.py create mode 100644 snaps/openstack/utils/tests/neutron_utils_tests.py create mode 100644 snaps/openstack/utils/tests/nova_utils_tests.py (limited to 'snaps/openstack') diff --git a/snaps/openstack/__init__.py b/snaps/openstack/__init__.py new file mode 100644 index 0000000..e3e876e --- /dev/null +++ b/snaps/openstack/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' diff --git a/snaps/openstack/create_flavor.py b/snaps/openstack/create_flavor.py new file mode 100644 index 0000000..60a87cd --- /dev/null +++ b/snaps/openstack/create_flavor.py @@ -0,0 +1,167 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from novaclient.exceptions import NotFound + +from snaps.openstack.utils import nova_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_image') + + +class OpenStackFlavor: + """ + Class responsible for creating a user in OpenStack + """ + + def __init__(self, os_creds, flavor_settings): + """ + Constructor + :param os_creds: The OpenStack connection credentials + :param flavor_settings: The flavor settings + :return: + """ + self.__os_creds = os_creds + self.flavor_settings = flavor_settings + self.__flavor = None + self.__nova = nova_utils.nova_client(self.__os_creds) + + def create(self, cleanup=False): + """ + Creates the image in OpenStack if it does not already exist + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: The OpenStack flavor object + """ + self.__flavor = nova_utils.get_flavor_by_name(self.__nova, self.flavor_settings.name) + if self.__flavor: + logger.info('Found flavor with name - ' + self.flavor_settings.name) + elif not cleanup: + self.__flavor = nova_utils.create_flavor(self.__nova, self.flavor_settings) + else: + logger.info('Did not create flavor due to cleanup mode') + + return self.__flavor + + def clean(self): + """ + Cleanse environment of all artifacts + :return: void + """ + if self.__flavor: + try: + nova_utils.delete_flavor(self.__nova, self.__flavor) + except NotFound: + pass + + self.__flavor = None + + def get_flavor(self): + """ + Returns the OpenStack flavor object + :return: + """ + return self.__flavor + + +class FlavorSettings: + """ + Configuration settings for OpenStack flavor creation + """ + + def __init__(self, config=None, name=None, flavor_id='auto', ram=None, disk=None, vcpus=None, ephemeral=0, swap=0, + rxtx_factor=1.0, is_public=True): + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the flavor's name (required) + :param flavor_id: the string ID (default 'auto') + :param ram: the required RAM in MB (required) + :param disk: the size of the root disk in GB (required) + :param vcpus: the number of virtual CPUs (required) + :param ephemeral: the size of the ephemeral disk in GB (default 0) + :param swap: the size of the dedicated swap disk in GB (default 0) + :param rxtx_factor: the receive/transmit factor to be set on ports if backend supports + QoS extension (default 1.0) + :param is_public: denotes whether or not the flavor is public (default True) + """ + + if config: + self.name = config.get('name') + + if config.get('flavor_id'): + self.flavor_id = config['flavor_id'] + else: + self.flavor_id = flavor_id + + self.ram = config.get('ram') + self.disk = config.get('disk') + self.vcpus = config.get('vcpus') + + if config.get('ephemeral'): + self.ephemeral = config['ephemeral'] + else: + self.ephemeral = ephemeral + + if config.get('swap'): + self.swap = config['swap'] + else: + self.swap = swap + + if config.get('rxtx_factor'): + self.rxtx_factor = config['rxtx_factor'] + else: + self.rxtx_factor = rxtx_factor + + if config.get('is_public') is not None: + self.is_public = config['is_public'] + else: + self.is_public = is_public + else: + self.name = name + self.flavor_id = flavor_id + self.ram = ram + self.disk = disk + self.vcpus = vcpus + self.ephemeral = ephemeral + self.swap = swap + self.rxtx_factor = rxtx_factor + self.is_public = is_public + + if not self.name or not self.ram or not self.disk or not self.vcpus: + raise Exception('The attributes name, ram, disk, and vcpus are required for FlavorSettings') + + if not isinstance(self.ram, int): + raise Exception('The ram attribute must be a integer') + + if not isinstance(self.disk, int): + raise Exception('The ram attribute must be a integer') + + if not isinstance(self.vcpus, int): + raise Exception('The vcpus attribute must be a integer') + + if self.ephemeral and not isinstance(self.ephemeral, int): + raise Exception('The ephemeral attribute must be an integer') + + if self.swap and not isinstance(self.swap, int): + raise Exception('The swap attribute must be an integer') + + if self.rxtx_factor and not isinstance(self.rxtx_factor, (int, float)): + raise Exception('The is_public attribute must be an integer or float') + + if self.is_public and not isinstance(self.is_public, bool): + raise Exception('The is_public attribute must be a boolean') diff --git a/snaps/openstack/create_image.py b/snaps/openstack/create_image.py new file mode 100644 index 0000000..e1b8d94 --- /dev/null +++ b/snaps/openstack/create_image.py @@ -0,0 +1,188 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import time + +from glanceclient.exc import HTTPNotFound + +from snaps.openstack.utils import glance_utils, nova_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_image') + +IMAGE_ACTIVE_TIMEOUT = 600 +POLL_INTERVAL = 3 +STATUS_ACTIVE = 'active' + + +class OpenStackImage: + """ + Class responsible for creating an image in OpenStack + """ + + def __init__(self, os_creds, image_settings): + """ + Constructor + :param os_creds: The OpenStack connection credentials + :param image_settings: The image settings + :return: + """ + self.__os_creds = os_creds + self.image_settings = image_settings + self.__image = None + self.__glance = glance_utils.glance_client(os_creds) + + def create(self, cleanup=False): + """ + Creates the image in OpenStack if it does not already exist + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: The OpenStack Image object + """ + from snaps.openstack.utils import nova_utils + nova = nova_utils.nova_client(self.__os_creds) + + self.__image = glance_utils.get_image(nova, self.__glance, self.image_settings.name) + if self.__image: + logger.info('Found image with name - ' + self.image_settings.name) + return self.__image + elif not cleanup: + self.__image = glance_utils.create_image(self.__glance, self.image_settings) + logger.info('Creating image') + if self.image_active(block=True): + logger.info('Image is now active with name - ' + self.image_settings.name) + return self.__image + else: + raise Exception('Image did not activate in the alloted amount of time') + else: + logger.info('Did not create image due to cleanup mode') + + return self.__image + + def clean(self): + """ + Cleanse environment of all artifacts + :return: void + """ + if self.__image: + try: + glance_utils.delete_image(self.__glance, self.__image) + except HTTPNotFound: + pass + self.__image = None + + def get_image(self): + """ + Returns the OpenStack image object as it was populated when create() was called + :return: the object + """ + return self.__image + + def image_active(self, block=False, timeout=IMAGE_ACTIVE_TIMEOUT, poll_interval=POLL_INTERVAL): + """ + Returns true when the image status returns the value of expected_status_code + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param timeout: The timeout value + :param poll_interval: The polling interval in seconds + :return: T/F + """ + return self._image_status_check(STATUS_ACTIVE, block, timeout, poll_interval) + + def _image_status_check(self, expected_status_code, block, timeout, poll_interval): + """ + Returns true when the image status returns the value of expected_status_code + :param expected_status_code: instance status evaluated with this string value + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param timeout: The timeout value + :param poll_interval: The polling interval in seconds + :return: T/F + """ + # sleep and wait for image status change + if block: + start = time.time() + else: + start = time.time() - timeout + + while timeout > time.time() - start: + status = self._status(expected_status_code) + if status: + logger.info('Image is active with name - ' + self.image_settings.name) + return True + + logger.debug('Retry querying image status in ' + str(poll_interval) + ' seconds') + time.sleep(poll_interval) + logger.debug('Image status query timeout in ' + str(timeout - (time.time() - start))) + + logger.error('Timeout checking for image status for ' + expected_status_code) + return False + + def _status(self, expected_status_code): + """ + Returns True when active else False + :param expected_status_code: instance status evaluated with this string value + :return: T/F + """ + # TODO - Place this API call into glance_utils. + nova = nova_utils.nova_client(self.__os_creds) + instance = glance_utils.get_image(nova, self.__glance, self.image_settings.name) + # instance = self.__glance.images.get(self.__image) + if not instance: + logger.warn('Cannot find instance with id - ' + self.__image.id) + return False + + if instance.status == 'ERROR': + raise Exception('Instance had an error during deployment') + logger.debug('Instance status is - ' + instance.status) + return instance.status == expected_status_code + + +class ImageSettings: + def __init__(self, config=None, name=None, image_user=None, img_format=None, url=None, image_file=None, + nic_config_pb_loc=None): + """ + + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the image's name (required) + :param image_user: the image's default sudo user (required) + :param img_format: the image type (required) + :param url: the image download location (requires url or img_file) + :param image_file: the image file location (requires url or img_file) + :param nic_config_pb_loc: the file location to the Ansible Playbook that can configure multiple NICs + """ + + if config: + self.name = config.get('name') + self.image_user = config.get('image_user') + self.format = config.get('format') + self.url = config.get('download_url') + self.image_file = config.get('image_file') + self.nic_config_pb_loc = config.get('nic_config_pb_loc') + else: + self.name = name + self.image_user = image_user + self.format = img_format + self.url = url + self.image_file = image_file + self.nic_config_pb_loc = nic_config_pb_loc + + if not self.name or not self.image_user or not self.format: + raise Exception("The attributes name, image_user, format, and url are required for ImageSettings") + + if not self.url and not self.image_file: + raise Exception('URL or image file must be set') + + if self.url and self.image_file: + raise Exception('Please set either URL or image file, not both') diff --git a/snaps/openstack/create_instance.py b/snaps/openstack/create_instance.py new file mode 100644 index 0000000..caddc05 --- /dev/null +++ b/snaps/openstack/create_instance.py @@ -0,0 +1,739 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import time + +from neutronclient.common.exceptions import PortNotFoundClient +from novaclient.exceptions import NotFound + +from snaps.openstack.utils import glance_utils +from snaps.openstack.utils import neutron_utils +from snaps.openstack.create_network import PortSettings +from snaps.provisioning import ansible_utils +from snaps.openstack.utils import nova_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_instance') + +POLL_INTERVAL = 3 +STATUS_ACTIVE = 'ACTIVE' +STATUS_DELETED = 'DELETED' + + +class OpenStackVmInstance: + """ + Class responsible for creating a VM instance in OpenStack + """ + + def __init__(self, os_creds, instance_settings, image_settings, keypair_settings=None): + """ + Constructor + :param os_creds: The connection credentials to the OpenStack API + :param instance_settings: Contains the settings for this VM + :param image_settings: The OpenStack image object settings + :param keypair_settings: The keypair metadata (Optional) + :raises Exception + """ + self.__os_creds = os_creds + + self.__nova = nova_utils.nova_client(self.__os_creds) + self.__neutron = neutron_utils.neutron_client(self.__os_creds) + + self.instance_settings = instance_settings + self.image_settings = image_settings + self.keypair_settings = keypair_settings + + # TODO - get rid of FIP list and only use the dict(). Need to fix populating this object when already exists + self.__floating_ips = list() + self.__floating_ip_dict = dict() + + # Instantiated in self.create() + self.__ports = list() + + # Note: this object does not change after the VM becomes active + self.__vm = None + + def create(self, cleanup=False, block=False): + """ + Creates a VM instance + :param cleanup: When true, only perform lookups for OpenStack objects. + :param block: Thread will block until instance has either become active, error, or timeout waiting. + Additionally, when True, floating IPs will not be applied until VM is active. + :return: The VM reference object + """ + try: + self.__ports = self.__setup_ports(self.instance_settings.port_settings, cleanup) + self.__lookup_existing_vm_by_name() + if not self.__vm and not cleanup: + self.__create_vm(block) + return self.__vm + except Exception as e: + logger.exception('Error occurred while setting up instance') + self.clean() + raise e + + def __lookup_existing_vm_by_name(self): + """ + Populates the member variables 'self.vm' and 'self.floating_ips' if a VM with the same name already exists + within the project + """ + servers = nova_utils.get_servers_by_name(self.__nova, self.instance_settings.name) + for server in servers: + if server.name == self.instance_settings.name: + self.__vm = server + logger.info('Found existing machine with name - ' + self.instance_settings.name) + fips = self.__nova.floating_ips.list() + for fip in fips: + if fip.instance_id == server.id: + self.__floating_ips.append(fip) + # TODO - Determine a means to associate to the FIP configuration and add to FIP map + + def __create_vm(self, block=False): + """ + Responsible for creating the VM instance + :param block: Thread will block until instance has either become active, error, or timeout waiting. + Floating IPs will be assigned after active when block=True + """ + nics = [] + for key, port in self.__ports: + kv = dict() + kv['port-id'] = port['port']['id'] + nics.append(kv) + + logger.info('Creating VM with name - ' + self.instance_settings.name) + keypair_name = None + if self.keypair_settings: + keypair_name = self.keypair_settings.name + + flavor = nova_utils.get_flavor_by_name(self.__nova, self.instance_settings.flavor) + if not flavor: + raise Exception('Flavor not found with name - ' + self.instance_settings.flavor) + + image = glance_utils.get_image(self.__nova, glance_utils.glance_client(self.__os_creds), + self.image_settings.name) + if image: + self.__vm = self.__nova.servers.create( + name=self.instance_settings.name, + flavor=flavor, + image=image, + nics=nics, + key_name=keypair_name, + security_groups=list(self.instance_settings.security_group_names), + userdata=self.instance_settings.userdata, + availability_zone=self.instance_settings.availability_zone) + else: + raise Exception('Cannot create instance, image cannot be located with name ' + self.image_settings.name) + + logger.info('Created instance with name - ' + self.instance_settings.name) + + if block: + self.vm_active(block=True) + + self.__apply_floating_ips() + + def __apply_floating_ips(self): + """ + Applies the configured floating IPs to the necessary ports + """ + port_dict = dict() + for key, port in self.__ports: + port_dict[key] = port + + # Apply floating IPs + for floating_ip_setting in self.instance_settings.floating_ip_settings: + port = port_dict.get(floating_ip_setting.port_name) + + if not port: + raise Exception('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_by_name(self.__neutron, floating_ip_setting.subnet_name) + floating_ip = nova_utils.create_floating_ip(self.__nova, ext_gateway) + self.__floating_ips.append(floating_ip) + self.__floating_ip_dict[floating_ip_setting.name] = floating_ip + + logger.info('Created floating IP ' + floating_ip.ip + ' via router - ' + + floating_ip_setting.router_name) + self.__add_floating_ip(floating_ip, port, subnet) + else: + raise Exception('Unable to add floating IP to port,' + + ' cannot locate router with an external gateway ') + + def __ext_gateway_by_router(self, router_name): + """ + Returns network name for the external network attached to a router or None if not found + :param router_name: The name of the router to lookup + :return: the external network name or None + """ + router = neutron_utils.get_router_by_name(self.__neutron, router_name) + if router and router['router'].get('external_gateway_info'): + network = neutron_utils.get_network_by_id(self.__neutron, + router['router']['external_gateway_info']['network_id']) + if network: + return network['network']['name'] + return None + + def clean(self): + """ + Destroys the VM instance + """ + + # Cleanup floating IPs + for floating_ip in self.__floating_ips: + try: + logger.info('Deleting Floating IP - ' + floating_ip.ip) + nova_utils.delete_floating_ip(self.__nova, floating_ip) + except Exception as e: + logger.error('Error deleting Floating IP - ' + e.message) + self.__floating_ips = list() + self.__floating_ip_dict = dict() + + # Cleanup ports + for name, port in self.__ports: + logger.info('Deleting Port - ' + name) + try: + neutron_utils.delete_port(self.__neutron, port) + except PortNotFoundClient as e: + logger.warn('Unexpected error deleting port - ' + e.message) + pass + self.__ports = list() + + # Cleanup VM + if self.__vm: + try: + logger.info('Deleting VM instance - ' + self.instance_settings.name) + nova_utils.delete_vm_instance(self.__nova, self.__vm) + except Exception as e: + logger.error('Error deleting VM - ' + str(e)) + + # Block until instance cannot be found or returns the status of DELETED + logger.info('Checking deletion status') + + try: + if self.vm_deleted(block=True): + logger.info('VM has been properly deleted VM with name - ' + self.instance_settings.name) + self.__vm = None + else: + logger.error('VM not deleted within the timeout period of ' + + str(self.instance_settings.vm_delete_timeout) + ' seconds') + except Exception as e: + logger.error('Unexpected error while checking VM instance status - ' + e.message) + + def __setup_ports(self, port_settings, cleanup): + """ + Returns the previously configured ports or creates them if they do not exist + :param port_settings: A list of PortSetting objects + :param cleanup: When true, only perform lookups for OpenStack objects. + :return: a list of OpenStack port tuples where the first member is the port name and the second is the port + object + """ + ports = list() + + for port_setting in port_settings: + # First check to see if network already has this port + # TODO/FIXME - this could potentially cause problems if another port with the same name exists + # VM has the same network/port name pair + found = False + + # TODO/FIXME - should we not be iterating on ports for the specific network in question as unique port names + # seem to only be important by network + existing_ports = self.__neutron.list_ports()['ports'] + for existing_port in existing_ports: + if existing_port['name'] == port_setting.name: + ports.append((port_setting.name, {'port': existing_port})) + found = True + break + + if not found and not cleanup: + ports.append((port_setting.name, neutron_utils.create_port(self.__neutron, self.__os_creds, + port_setting))) + + return ports + + def __add_floating_ip(self, floating_ip, port, subnet, timeout=30, poll_interval=POLL_INTERVAL): + """ + Returns True when active else False + TODO - Make timeout and poll_interval configurable... + """ + ip = None + + if subnet: + # Take IP of subnet if there is one configured on which to place the floating IP + for fixed_ip in port['port']['fixed_ips']: + if fixed_ip['subnet_id'] == subnet['subnet']['id']: + ip = fixed_ip['ip_address'] + break + else: + # Simply take the first + ip = port['port']['fixed_ips'][0]['ip_address'] + + if ip: + count = timeout / poll_interval + while count > 0: + logger.debug('Attempting to add floating IP to instance') + try: + self.__vm.add_floating_ip(floating_ip, ip) + logger.info('Added floating IP ' + floating_ip.ip + ' to port IP - ' + ip + + ' on instance - ' + self.instance_settings.name) + return + except Exception as e: + logger.debug('Retry adding floating IP to instance. Last attempt failed with - ' + e.message) + time.sleep(poll_interval) + count -= 1 + pass + else: + raise Exception('Unable find IP address on which to place the floating IP') + + logger.error('Timeout attempting to add the floating IP to instance.') + raise Exception('Timeout while attempting add floating IP to instance') + + def get_os_creds(self): + """ + Returns the OpenStack credentials used to create these objects + :return: the credentials + """ + return self.__os_creds + + def get_vm_inst(self): + """ + Returns the latest version of this server object from OpenStack + :return: Server object + """ + return nova_utils.get_latest_server_object(self.__nova, self.__vm) + + def get_port_ip(self, port_name, subnet_name=None): + """ + Returns the first IP for the port corresponding with the port_name parameter when subnet_name is None + else returns the IP address that corresponds to the subnet_name parameter + :param port_name: the name of the port from which to return the IP + :param subnet_name: the name of the subnet attached to this IP + :return: the IP or None if not found + """ + port = self.get_port_by_name(port_name) + if port: + port_dict = port['port'] + if subnet_name: + subnet = neutron_utils.get_subnet_by_name(self.__neutron, subnet_name) + if not subnet: + logger.warn('Cannot retrieve port IP as subnet could not be located with name - ' + subnet_name) + return None + for fixed_ip in port_dict['fixed_ips']: + if fixed_ip['subnet_id'] == subnet['subnet']['id']: + return fixed_ip['ip_address'] + else: + fixed_ips = port_dict['fixed_ips'] + if fixed_ips and len(fixed_ips) > 0: + return fixed_ips[0]['ip_address'] + return None + + def get_port_mac(self, port_name): + """ + Returns the first IP for the port corresponding with the port_name parameter + TODO - Add in the subnet as an additional parameter as a port may have multiple fixed_ips + :param port_name: the name of the port from which to return the IP + :return: the IP or None if not found + """ + port = self.get_port_by_name(port_name) + if port: + port_dict = port['port'] + return port_dict['mac_address'] + return None + + def get_port_by_name(self, port_name): + """ + Retrieves the OpenStack port object by its given name + :param port_name: the name of the port + :return: the OpenStack port object or None if not exists + """ + for key, port in self.__ports: + if key == port_name: + return port + logger.warn('Cannot find port with name - ' + port_name) + return None + + def config_nics(self): + """ + Responsible for configuring NICs on RPM systems where the instance has more than one configured port + :return: None + """ + if len(self.__ports) > 1 and len(self.__floating_ips) > 0: + if self.vm_active(block=True) and self.vm_ssh_active(block=True): + for key, port in self.__ports: + port_index = self.__ports.index((key, port)) + if port_index > 0: + nic_name = 'eth' + repr(port_index) + self.__config_nic(nic_name, port, self.__get_first_provisioning_floating_ip().ip) + logger.info('Configured NIC - ' + nic_name + ' on VM - ' + self.instance_settings.name) + + def __get_first_provisioning_floating_ip(self): + """ + Returns the first floating IP tagged with the Floating IP name if exists else the first one found + :return: + """ + for floating_ip_setting in self.instance_settings.floating_ip_settings: + if floating_ip_setting.provisioning: + fip = self.__floating_ip_dict.get(floating_ip_setting.name) + if fip: + return fip + elif len(self.__floating_ips) > 0: + return self.__floating_ips[0] + + def __config_nic(self, nic_name, port, floating_ip): + """ + Although ports/NICs can contain multiple IPs, this code currently only supports the first. + + Your CWD at this point must be the /python directory. + TODO - fix this restriction. + + :param nic_name: Name of the interface + :param port: The port information containing the expected IP values. + :param floating_ip: The floating IP on which to apply the playbook. + """ + ip = port['port']['fixed_ips'][0]['ip_address'] + variables = { + 'floating_ip': floating_ip, + 'nic_name': nic_name, + 'nic_ip': ip + } + + if self.image_settings.nic_config_pb_loc and self.keypair_settings: + ansible_utils.apply_playbook(self.image_settings.nic_config_pb_loc, + [floating_ip], self.get_image_user(), self.keypair_settings.private_filepath, + variables, self.__os_creds.proxy_settings) + else: + logger.warn('VM ' + self.instance_settings.name + ' cannot self configure NICs eth1++. ' + + 'No playbook or keypairs found.') + + def get_image_user(self): + """ + Returns the instance sudo_user if it has been configured in the instance_settings else it returns the + image_settings.image_user value + """ + if self.instance_settings.sudo_user: + return self.instance_settings.sudo_user + else: + return self.image_settings.image_user + + def vm_deleted(self, block=False, poll_interval=POLL_INTERVAL): + """ + Returns true when the VM status returns the value of expected_status_code or instance retrieval throws + a NotFound exception. + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param poll_interval: The polling interval in seconds + :return: T/F + """ + try: + return self.__vm_status_check(STATUS_DELETED, block, self.instance_settings.vm_delete_timeout, + poll_interval) + except NotFound as e: + logger.debug("Instance not found when querying status for " + STATUS_DELETED + ' with message ' + e.message) + return True + + def vm_active(self, block=False, poll_interval=POLL_INTERVAL): + """ + Returns true when the VM status returns the value of expected_status_code + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :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) + + def __vm_status_check(self, expected_status_code, block, timeout, poll_interval): + """ + Returns true when the VM status returns the value of expected_status_code + :param expected_status_code: instance status evaluated with this string value + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param timeout: The timeout value + :param poll_interval: The polling interval in seconds + :return: T/F + """ + # sleep and wait for VM status change + if block: + start = time.time() + else: + start = time.time() - timeout + + while timeout > time.time() - start: + status = self.__status(expected_status_code) + if status: + logger.info('VM is - ' + expected_status_code) + return True + + logger.debug('Retry querying VM status in ' + str(poll_interval) + ' seconds') + time.sleep(poll_interval) + logger.debug('VM status query timeout in ' + str(timeout - (time.time() - start))) + + logger.error('Timeout checking for VM status for ' + expected_status_code) + return False + + def __status(self, expected_status_code): + """ + Returns True when active else False + :param expected_status_code: instance status evaluated with this string value + :return: T/F + """ + instance = self.__nova.servers.get(self.__vm.id) + if not instance: + logger.warn('Cannot find instance with id - ' + self.__vm.id) + return False + + if instance.status == 'ERROR': + raise Exception('Instance had an error during deployment') + logger.debug('Instance status [' + self.instance_settings.name + '] is - ' + instance.status) + return instance.status == expected_status_code + + def vm_ssh_active(self, block=False, poll_interval=POLL_INTERVAL): + """ + Returns true when the VM can be accessed via SSH + :param block: When true, thread will block until active or timeout value in seconds has been exceeded (False) + :param poll_interval: The polling interval + :return: T/F + """ + # sleep and wait for VM status change + logger.info('Checking if VM is active') + + timeout = self.instance_settings.ssh_connect_timeout + + if self.vm_active(block=True): + if block: + start = time.time() + else: + start = time.time() - timeout + + while timeout > time.time() - start: + status = self.__ssh_active() + if status: + logger.info('SSH is active for VM instance') + return True + + logger.debug('Retry SSH connection in ' + str(poll_interval) + ' seconds') + time.sleep(poll_interval) + logger.debug('SSH connection timeout in ' + str(timeout - (time.time() - start))) + + logger.error('Timeout attempting to connect with VM via SSH') + return False + + def __ssh_active(self): + """ + Returns True when can create a SSH session else False + :return: T/F + """ + if len(self.__floating_ips) > 0: + ssh = self.ssh_client() + if ssh: + return True + return False + + def get_floating_ip(self, fip_name=None): + """ + Returns the floating IP object byt name if found, else the first known, else None + :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 and len(self.__floating_ips) > 0: + return self.__floating_ips[0] + return None + + def ssh_client(self, fip_name=None): + """ + Returns an SSH client using the name or the first known floating IP if exists, else None + :param fip_name: the name of the floating IP to return + :return: the SSH client or None + """ + fip = self.get_floating_ip(fip_name) + if fip: + return ansible_utils.ssh_client(self.__floating_ips[0].ip, self.get_image_user(), + self.keypair_settings.private_filepath, + proxy_settings=self.__os_creds.proxy_settings) + else: + logger.warn('Cannot return an SSH client. No Floating IP configured') + + def add_security_group(self, security_group): + """ + Adds a security group to this VM. Call will block until VM is active. + :param security_group: the OpenStack security group object + :return True if successful else False + """ + self.vm_active(block=True) + + if not security_group: + logger.warn('Security group object is None, cannot add') + return False + + try: + nova_utils.add_security_group(self.__nova, self.get_vm_inst(), security_group['security_group']['name']) + return True + except NotFound as e: + logger.warn('Security group not added - ' + e.message) + return False + + def remove_security_group(self, security_group): + """ + Removes a security group to this VM. Call will block until VM is active. + :param security_group: the OpenStack security group object + :return True if successful else False + """ + self.vm_active(block=True) + + if not security_group: + logger.warn('Security group object is None, cannot add') + return False + + try: + nova_utils.remove_security_group(self.__nova, self.get_vm_inst(), security_group['security_group']['name']) + return True + except NotFound as e: + logger.warn('Security group not added - ' + e.message) + return False + + +class VmInstanceSettings: + """ + Class responsible for holding configuration setting for a VM Instance + """ + def __init__(self, config=None, name=None, flavor=None, port_settings=list(), security_group_names=set(), + floating_ip_settings=list(), sudo_user=None, vm_boot_timeout=900, + vm_delete_timeout=300, ssh_connect_timeout=180, availability_zone=None, userdata=None): + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the name of the VM + :param flavor: the VM's flavor + :param port_settings: the port configuration settings + :param security_group_names: a set of names of the security groups to add to the VM + :param floating_ip_settings: the floating IP configuration settings + :param sudo_user: the sudo user of the VM that will override the instance_settings.image_user when trying to + connect to the VM + :param vm_boot_timeout: the amount of time a thread will sleep waiting for an instance to boot + :param vm_delete_timeout: the amount of time a thread will sleep waiting for an instance to be deleted + :param ssh_connect_timeout: the amount of time a thread will sleep waiting obtaining an SSH connection to a VM + :param availability_zone: the name of the compute server on which to deploy the VM (optional) + :param userdata: the cloud-init script to run after the VM has been started + """ + if config: + self.name = config.get('name') + self.flavor = config.get('flavor') + self.sudo_user = config.get('sudo_user') + self.userdata = config.get('userdata') + + self.port_settings = list() + if config.get('ports'): + for port_config in config['ports']: + if isinstance(port_config, PortSettings): + self.port_settings.append(port_config) + else: + self.port_settings.append(PortSettings(config=port_config['port'])) + + if config.get('security_group_names'): + if isinstance(config['security_group_names'], list): + self.security_group_names = set(config['security_group_names']) + elif isinstance(config['security_group_names'], set): + self.security_group_names = config['security_group_names'] + elif isinstance(config['security_group_names'], basestring): + self.security_group_names = [config['security_group_names']] + else: + raise Exception('Invalid data type for security_group_names attribute') + else: + self.security_group_names = set() + + self.floating_ip_settings = list() + if config.get('floating_ips'): + for floating_ip_config in config['floating_ips']: + if isinstance(floating_ip_config, FloatingIpSettings): + self.floating_ip_settings.append(floating_ip_config) + else: + self.floating_ip_settings.append(FloatingIpSettings(config=floating_ip_config['floating_ip'])) + + if config.get('vm_boot_timeout'): + self.vm_boot_timeout = config['vm_boot_timeout'] + else: + self.vm_boot_timeout = vm_boot_timeout + + if config.get('vm_delete_timeout'): + self.vm_delete_timeout = config['vm_delete_timeout'] + else: + self.vm_delete_timeout = vm_delete_timeout + + if config.get('ssh_connect_timeout'): + self.ssh_connect_timeout = config['ssh_connect_timeout'] + else: + self.ssh_connect_timeout = ssh_connect_timeout + + if config.get('availability_zone'): + self.availability_zone = config['availability_zone'] + else: + self.availability_zone = None + else: + self.name = name + self.flavor = flavor + self.port_settings = port_settings + self.security_group_names = security_group_names + self.floating_ip_settings = floating_ip_settings + self.sudo_user = sudo_user + self.vm_boot_timeout = vm_boot_timeout + self.vm_delete_timeout = vm_delete_timeout + self.ssh_connect_timeout = ssh_connect_timeout + self.availability_zone = availability_zone + self.userdata = userdata + + if not self.name or not self.flavor: + raise Exception('Instance configuration requires the attributes: name, flavor') + + +class FloatingIpSettings: + """ + Class responsible for holding configuration settings for a floating IP + """ + def __init__(self, config=None, name=None, port_name=None, router_name=None, subnet_name=None, provisioning=True): + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the name of the floating IP + :param port_name: the name of the router to the external network + :param router_name: the name of the router to the external network + :param subnet_name: the name of the subnet on which to attach the floating IP + :param provisioning: when true, this floating IP can be used for provisioning + + TODO - provisioning flag is a hack as I have only observed a single Floating IPs that actually works on + an instance. Multiple floating IPs placed on different subnets from the same port are especially troublesome + as you cannot predict which one will actually connect. For now, it is recommended not to setup multiple + floating IPs on an instance unless absolutely necessary. + """ + if config: + self.name = config.get('name') + self.port_name = config.get('port_name') + self.router_name = config.get('router_name') + self.subnet_name = config.get('subnet_name') + if config.get('provisioning') is not None: + self.provisioning = config['provisioning'] + else: + self.provisioning = provisioning + else: + self.name = name + self.port_name = port_name + self.router_name = router_name + self.subnet_name = subnet_name + self.provisioning = provisioning + + if not self.name or not self.port_name or not self.router_name: + raise Exception('The attributes name, port_name and router_name are required for FloatingIPSettings') diff --git a/snaps/openstack/create_keypairs.py b/snaps/openstack/create_keypairs.py new file mode 100644 index 0000000..ea7c811 --- /dev/null +++ b/snaps/openstack/create_keypairs.py @@ -0,0 +1,121 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os + +from Crypto.PublicKey import RSA +from novaclient.exceptions import NotFound + +from snaps.openstack.utils import nova_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('OpenStackKeypair') + + +class OpenStackKeypair: + """ + Class responsible for creating a keypair in OpenStack + """ + + def __init__(self, os_creds, keypair_settings): + """ + Constructor - all parameters are required + :param os_creds: The credentials to connect with OpenStack + :param keypair_settings: The settings used to create a keypair + """ + self.__os_creds = os_creds + self.keypair_settings = keypair_settings + self.__nova = nova_utils.nova_client(os_creds) + + # Attributes instantiated on create() + self.__keypair = None + + def create(self, cleanup=False): + """ + Responsible for creating the keypair object. + :param cleanup: Denotes whether or not this is being called for cleanup or not + """ + logger.info('Creating keypair %s...' % self.keypair_settings.name) + + try: + self.__keypair = nova_utils.get_keypair_by_name(self.__nova, self.keypair_settings.name) + + if not self.__keypair and not cleanup: + if self.keypair_settings.public_filepath and os.path.isfile(self.keypair_settings.public_filepath): + logger.info("Uploading existing keypair") + self.__keypair = nova_utils.upload_keypair_file(self.__nova, self.keypair_settings.name, + self.keypair_settings.public_filepath) + else: + logger.info("Creating new keypair") + # TODO - Make this value configurable + keys = RSA.generate(1024) + self.__keypair = nova_utils.upload_keypair(self.__nova, self.keypair_settings.name, + keys.publickey().exportKey('OpenSSH')) + nova_utils.save_keys_to_files(keys, self.keypair_settings.public_filepath, + self.keypair_settings.private_filepath) + + return self.__keypair + except Exception as e: + logger.error('Unexpected error creating keypair named - ' + self.keypair_settings.name) + self.clean() + raise Exception(e.message) + + def clean(self): + """ + Removes and deletes the keypair. + """ + if self.__keypair: + try: + nova_utils.delete_keypair(self.__nova, self.__keypair) + except NotFound: + pass + self.__keypair = None + + def get_keypair(self): + """ + Returns the OpenStack keypair object + :return: + """ + return self.__keypair + + +class KeypairSettings: + """ + Class representing a keypair configuration + """ + + def __init__(self, config=None, name=None, public_filepath=None, private_filepath=None): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: The keypair name. + :param public_filepath: The path to/from the filesystem where the public key file is or will be stored + :param private_filepath: The path where the generated private key file will be stored + :return: + """ + + if config: + self.name = config.get('name') + self.public_filepath = config.get('public_filepath') + self.private_filepath = config.get('private_filepath') + else: + self.name = name + self.public_filepath = public_filepath + self.private_filepath = private_filepath + + if not self.name: + raise Exception('The attributes name, public_filepath, and private_filepath are required') diff --git a/snaps/openstack/create_network.py b/snaps/openstack/create_network.py new file mode 100644 index 0000000..a214ba1 --- /dev/null +++ b/snaps/openstack/create_network.py @@ -0,0 +1,519 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from neutronclient.common.exceptions import NotFound + +from snaps.openstack.utils import keystone_utils, neutron_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('OpenStackNetwork') + + +class OpenStackNetwork: + """ + Class responsible for creating a network in OpenStack + """ + + def __init__(self, os_creds, network_settings): + """ + Constructor - all parameters are required + :param os_creds: The credentials to connect with OpenStack + :param network_settings: The settings used to create a network + """ + self.__os_creds = os_creds + self.network_settings = network_settings + self.__neutron = neutron_utils.neutron_client(self.__os_creds) + + # Attributes instantiated on create() + self.__network = None + self.__subnets = list() + + def create(self, cleanup=False): + """ + Responsible for creating not only the network but then a private subnet, router, and an interface to the router. + :param cleanup: When true, only perform lookups for OpenStack objects. + :return: the created network object or None + """ + try: + logger.info('Creating neutron network %s...' % self.network_settings.name) + net_inst = neutron_utils.get_network(self.__neutron, self.network_settings.name, + self.network_settings.get_project_id(self.__os_creds)) + if net_inst: + self.__network = net_inst + else: + if not cleanup: + self.__network = neutron_utils.create_network(self.__neutron, self.__os_creds, + self.network_settings) + else: + logger.info('Network does not exist and will not create as in cleanup mode') + return + logger.debug("Network '%s' created successfully" % self.__network['network']['id']) + + logger.debug('Creating Subnets....') + for subnet_setting in self.network_settings.subnet_settings: + sub_inst = neutron_utils.get_subnet_by_name(self.__neutron, subnet_setting.name) + if sub_inst: + self.__subnets.append(sub_inst) + logger.debug("Subnet '%s' created successfully" % sub_inst['subnet']['id']) + else: + if not cleanup: + self.__subnets.append(neutron_utils.create_subnet(self.__neutron, subnet_setting, + self.__os_creds, self.__network)) + + return self.__network + except Exception as e: + logger.error('Unexpected exception thrown while creating network - ' + str(e)) + self.clean() + raise e + + def clean(self): + """ + Removes and deletes all items created in reverse order. + """ + for subnet in self.__subnets: + try: + logger.info('Deleting subnet with name ' + subnet['subnet']['name']) + neutron_utils.delete_subnet(self.__neutron, subnet) + except NotFound as e: + logger.warn('Error deleting subnet with message - ' + e.message) + pass + self.__subnets = list() + + if self.__network: + try: + neutron_utils.delete_network(self.__neutron, self.__network) + except NotFound: + pass + + self.__network = None + + def get_network(self): + """ + Returns the created OpenStack network object + :return: the OpenStack network object + """ + return self.__network + + def get_subnets(self): + """ + Returns the OpenStack subnet objects + :return: + """ + return self.__subnets + + +class NetworkSettings: + """ + Class representing a network configuration + """ + + def __init__(self, config=None, name=None, admin_state_up=True, shared=None, project_name=None, + external=False, network_type=None, physical_network=None, subnet_settings=list()): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: The network name. + :param admin_state_up: The administrative status of the network. True = up / False = down (default True) + :param shared: Boolean value indicating whether this network is shared across all projects/tenants. By default, + only administrative users can change this value. + :param project_name: Admin-only. The name of the project that will own the network. This project can be + different from the project that makes the create network request. However, only + administrative users can specify a project ID other than their own. You cannot change this + value through authorization policies. + :param external: when true, will setup an external network (default False). + :param network_type: the type of network (i.e. vlan|flat). + :param physical_network: the name of the physical network (this is required when network_type is 'flat') + :param subnet_settings: List of SubnetSettings objects. + :return: + """ + + self.project_id = None + + if config: + self.name = config.get('name') + if config.get('admin_state_up') is not None: + self.admin_state_up = bool(config['admin_state_up']) + else: + self.admin_state_up = admin_state_up + + if config.get('shared') is not None: + self.shared = bool(config['shared']) + else: + self.shared = None + + self.project_name = config.get('project_name') + + if config.get('external') is not None: + self.external = bool(config.get('external')) + else: + self.external = external + + self.network_type = config.get('network_type') + self.physical_network = config.get('physical_network') + + self.subnet_settings = list() + if config.get('subnets'): + for subnet_config in config['subnets']: + self.subnet_settings.append(SubnetSettings(config=subnet_config['subnet'])) + + else: + self.name = name + self.admin_state_up = admin_state_up + self.shared = shared + self.project_name = project_name + self.external = external + self.network_type = network_type + self.physical_network = physical_network + self.subnet_settings = subnet_settings + + if not self.name or len(self.name) < 1: + raise Exception('Name required for networks') + + def get_project_id(self, os_creds): + """ + Returns the project ID for a given project_name or None + :param os_creds: the credentials required for keystone client retrieval + :return: the ID or None + """ + if self.project_id: + return self.project_id + else: + if self.project_name: + keystone = keystone_utils.keystone_client(os_creds) + project = keystone_utils.get_project(keystone, self.project_name) + if project: + return project.id + + return None + + def dict_for_neutron(self, os_creds): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + TODO - expand automated testing to exercise all parameters + + :param os_creds: the OpenStack credentials + :return: the dictionary object + """ + out = dict() + + if self.name: + out['name'] = self.name + if self.admin_state_up is not None: + out['admin_state_up'] = self.admin_state_up + if self.shared: + out['shared'] = self.shared + if self.project_name: + project_id = self.get_project_id(os_creds) + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + if self.network_type: + out['provider:network_type'] = self.network_type + if self.physical_network: + out['provider:physical_network'] = self.physical_network + if self.external: + out['router:external'] = self.external + return {'network': out} + + +class SubnetSettings: + """ + Class representing a subnet configuration + """ + + def __init__(self, config=None, cidr=None, ip_version=4, name=None, project_name=None, start=None, + end=None, gateway_ip=None, enable_dhcp=None, dns_nameservers=None, host_routes=None, destination=None, + nexthop=None, ipv6_ra_mode=None, ipv6_address_mode=None): + """ + Constructor - all parameters are optional except cidr (subnet mask) + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param cidr: The CIDR. REQUIRED if config parameter is None + :param ip_version: The IP version, which is 4 or 6. + :param name: The subnet name. + :param project_name: The name of the project who owns the network. Only administrative users can specify a + project ID other than their own. You cannot change this value through authorization + policies. + :param start: The start address for the allocation pools. + :param end: The end address for the allocation pools. + :param gateway_ip: The gateway IP address. + :param enable_dhcp: Set to true if DHCP is enabled and false if DHCP is disabled. + :param dns_nameservers: A list of DNS name servers for the subnet. Specify each name server as an IP address + and separate multiple entries with a space. For example [8.8.8.7 8.8.8.8]. + :param host_routes: A list of host route dictionaries for the subnet. For example: + "host_routes":[ + { + "destination":"0.0.0.0/0", + "nexthop":"123.456.78.9" + }, + { + "destination":"192.168.0.0/24", + "nexthop":"192.168.0.1" + } + ] + :param destination: The destination for static route + :param nexthop: The next hop for the destination. + :param ipv6_ra_mode: A valid value is dhcpv6-stateful, dhcpv6-stateless, or slaac. + :param ipv6_address_mode: A valid value is dhcpv6-stateful, dhcpv6-stateless, or slaac. + :raise: Exception when config does not have or cidr values are None + """ + if not dns_nameservers: + dns_nameservers = ['8.8.8.8'] + + if config: + self.cidr = config['cidr'] + if config.get('ip_version'): + self.ip_version = config['ip_version'] + else: + self.ip_version = ip_version + + # Optional attributes that can be set after instantiation + self.name = config.get('name') + self.project_name = config.get('project_name') + self.start = config.get('start') + self.end = config.get('end') + self.gateway_ip = config.get('gateway_ip') + self.enable_dhcp = config.get('enable_dhcp') + + if config.get('dns_nameservers'): + self.dns_nameservers = config.get('dns_nameservers') + else: + self.dns_nameservers = dns_nameservers + + self.host_routes = config.get('host_routes') + self.destination = config.get('destination') + self.nexthop = config.get('nexthop') + self.ipv6_ra_mode = config.get('ipv6_ra_mode') + self.ipv6_address_mode = config.get('ipv6_address_mode') + else: + # Required attributes + self.cidr = cidr + self.ip_version = ip_version + + # Optional attributes that can be set after instantiation + self.name = name + self.project_name = project_name + self.start = start + self.end = end + self.gateway_ip = gateway_ip + self.enable_dhcp = enable_dhcp + self.dns_nameservers = dns_nameservers + self.host_routes = host_routes + self.destination = destination + self.nexthop = nexthop + self.ipv6_ra_mode = ipv6_ra_mode + self.ipv6_address_mode = ipv6_address_mode + + if not self.name or not self.cidr: + raise Exception('Name and cidr required for subnets') + + def dict_for_neutron(self, os_creds, network=None): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + :param os_creds: the OpenStack credentials + :param network: (Optional) the network object on which the subnet will be created + :return: the dictionary object + """ + out = { + 'cidr': self.cidr, + 'ip_version': self.ip_version, + } + + if network: + out['network_id'] = network['network']['id'] + if self.name: + out['name'] = self.name + if self.project_name: + keystone = keystone_utils.keystone_client(os_creds) + project = keystone_utils.get_project(keystone, self.project_name) + project_id = None + if project: + project_id = project.id + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + if self.start and self.end: + out['allocation_pools'] = [{'start': self.start, 'end': self.end}] + if self.gateway_ip: + out['gateway_ip'] = self.gateway_ip + if self.enable_dhcp is not None: + out['enable_dhcp'] = self.enable_dhcp + if self.dns_nameservers and len(self.dns_nameservers) > 0: + out['dns_nameservers'] = self.dns_nameservers + if self.host_routes and len(self.host_routes) > 0: + out['host_routes'] = self.host_routes + if self.destination: + out['destination'] = self.destination + if self.nexthop: + out['nexthop'] = self.nexthop + if self.ipv6_ra_mode: + out['ipv6_ra_mode'] = self.ipv6_ra_mode + if self.ipv6_address_mode: + out['ipv6_address_mode'] = self.ipv6_address_mode + return out + + +class PortSettings: + """ + Class representing a port configuration + """ + + def __init__(self, config=None, name=None, network_name=None, admin_state_up=True, project_name=None, + mac_address=None, ip_addrs=None, fixed_ips=None, security_groups=None, allowed_address_pairs=None, + opt_value=None, opt_name=None, device_owner=None, device_id=None): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: A symbolic name for the port. + :param network_name: The name of the network on which to create the port. + :param admin_state_up: A boolean value denoting the administrative status of the port. True = up / False = down + :param project_name: The name of the project who owns the network. Only administrative users can specify a + project ID other than their own. You cannot change this value through authorization + policies. + :param mac_address: The MAC address. If you specify an address that is not valid, a Bad Request (400) status + code is returned. If you do not specify a MAC address, OpenStack Networking tries to + allocate one. If a failure occurs, a Service Unavailable (503) status code is returned. + :param ip_addrs: A list of dict objects where each contains two keys 'subnet_name' and 'ip' values which will + get mapped to self.fixed_ips. + These values will be directly translated into the fixed_ips dict + :param fixed_ips: A dict where the key is the subnet IDs and value is the IP address to assign to the port + :param security_groups: One or more security group IDs. + :param allowed_address_pairs: A dictionary containing a set of zero or more allowed address pairs. An address + pair contains an IP address and MAC address. + :param opt_value: The extra DHCP option value. + :param opt_name: The extra DHCP option name. + :param device_owner: The ID of the entity that uses this port. For example, a DHCP agent. + :param device_id: The ID of the device that uses this port. For example, a virtual server. + :return: + """ + self.network = None + + if config: + self.name = config.get('name') + self.network_name = config.get('network_name') + + if config.get('admin_state_up') is not None: + self.admin_state_up = bool(config['admin_state_up']) + else: + self.admin_state_up = admin_state_up + + self.project_name = config.get('project_name') + self.mac_address = config.get('mac_address') + self.ip_addrs = config.get('ip_addrs') + self.fixed_ips = config.get('fixed_ips') + self.security_groups = config.get('security_groups') + self.allowed_address_pairs = config.get('allowed_address_pairs') + self.opt_value = config.get('opt_value') + self.opt_name = config.get('opt_name') + self.device_owner = config.get('device_owner') + self.device_id = config.get('device_id') + else: + self.name = name + self.network_name = network_name + self.admin_state_up = admin_state_up + self.project_name = project_name + self.mac_address = mac_address + self.ip_addrs = ip_addrs + self.fixed_ips = fixed_ips + self.security_groups = security_groups + self.allowed_address_pairs = allowed_address_pairs + self.opt_value = opt_value + self.opt_name = opt_name + self.device_owner = device_owner + self.device_id = device_id + + if not self.name or not self.network_name: + raise Exception('The attributes neutron, name, and network_name are required for PortSettings') + + def __set_fixed_ips(self, neutron): + """ + Sets the self.fixed_ips value + :param neutron: the Neutron client + :return: None + """ + if not self.fixed_ips and self.ip_addrs: + self.fixed_ips = list() + + for ip_addr_dict in self.ip_addrs: + subnet = neutron_utils.get_subnet_by_name(neutron, ip_addr_dict['subnet_name']) + if subnet: + self.fixed_ips.append({'ip_address': ip_addr_dict['ip'], 'subnet_id': subnet['subnet']['id']}) + else: + raise Exception('Invalid port configuration, subnet does not exist with name - ' + + ip_addr_dict['subnet_name']) + + def dict_for_neutron(self, neutron, os_creds): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + TODO - expand automated testing to exercise all parameters + :param neutron: the Neutron client + :param os_creds: the OpenStack credentials + :return: the dictionary object + """ + self.__set_fixed_ips(neutron) + + out = dict() + + project_id = None + if self.project_name: + keystone = keystone_utils.keystone_client(os_creds) + project = keystone_utils.get_project(keystone, self.project_name) + if project: + project_id = project.id + + if not self.network: + self.network = neutron_utils.get_network(neutron, self.network_name, project_id) + if not self.network: + raise Exception('Cannot locate network with name - ' + self.network_name) + + out['network_id'] = self.network['network']['id'] + + if self.admin_state_up is not None: + out['admin_state_up'] = self.admin_state_up + if self.name: + out['name'] = self.name + if self.project_name: + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + if self.mac_address: + out['mac_address'] = self.mac_address + if self.fixed_ips and len(self.fixed_ips) > 0: + out['fixed_ips'] = self.fixed_ips + if self.security_groups: + out['security_groups'] = self.security_groups + if self.allowed_address_pairs and len(self.allowed_address_pairs) > 0: + out['allowed_address_pairs'] = self.allowed_address_pairs + if self.opt_value: + out['opt_value'] = self.opt_value + if self.opt_name: + out['opt_name'] = self.opt_name + if self.device_owner: + out['device_owner'] = self.device_owner + if self.device_id: + out['device_id'] = self.device_id + return {'port': out} diff --git a/snaps/openstack/create_project.py b/snaps/openstack/create_project.py new file mode 100644 index 0000000..60f9ed0 --- /dev/null +++ b/snaps/openstack/create_project.py @@ -0,0 +1,139 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from keystoneclient.exceptions import NotFound + +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_image') + + +class OpenStackProject: + """ + Class responsible for creating a project/project in OpenStack + """ + + def __init__(self, os_creds, project_settings): + """ + Constructor + :param os_creds: The OpenStack connection credentials + :param project_settings: The project's settings + :return: + """ + self.__os_creds = os_creds + self.project_settings = project_settings + self.__project = None + self.__role = None + self.__keystone = keystone_utils.keystone_client(self.__os_creds) + + def create(self, cleanup=False): + """ + Creates the image in OpenStack if it does not already exist + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: The OpenStack Image object + """ + try: + self.__project = keystone_utils.get_project(keystone=self.__keystone, + project_name=self.project_settings.name) + if self.__project: + logger.info('Found project with name - ' + self.project_settings.name) + elif not cleanup: + self.__project = keystone_utils.create_project(self.__keystone, self.project_settings) + else: + logger.info('Did not create image due to cleanup mode') + except Exception as e: + logger.error('Unexpected error. Rolling back') + self.clean() + raise Exception(e.message) + + return self.__project + + def clean(self): + """ + Cleanse environment of all artifacts + :return: void + """ + if self.__project: + try: + keystone_utils.delete_project(self.__keystone, self.__project) + except NotFound: + pass + self.__project = None + + if self.__role: + try: + keystone_utils.delete_role(self.__keystone, self.__role) + except NotFound: + pass + self.__project = None + + def get_project(self): + """ + Returns the OpenStack project object populated on create() + :return: + """ + return self.__project + + def assoc_user(self, user): + """ + The user object to associate with the project + :param user: the OpenStack user object to associate with project + :return: + """ + if not self.__role: + self.__role = keystone_utils.create_role(self.__keystone, self.project_settings.name + '-role') + + keystone_utils.assoc_user_to_project(self.__keystone, self.__role, user, self.__project) + + +class ProjectSettings: + """ + Class to hold the configuration settings required for creating OpenStack project objects + """ + def __init__(self, config=None, name=None, domain='default', description=None, enabled=True): + + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the project's name (required) + :param domain: the project's domain name (default 'default'). Field is used for v3 clients + :param description: the description (optional) + :param enabled: denotes whether or not the user is enabled (default True) + """ + + if config: + self.name = config.get('name') + if config.get('domain'): + self.domain = config['domain'] + else: + self.domain = domain + + self.description = config.get('description') + if config.get('enabled') is not None: + self.enabled = config['enabled'] + else: + self.enabled = enabled + else: + self.name = name + self.domain = domain + self.description = description + self.enabled = enabled + + if not self.name: + raise Exception("The attribute name is required for ProjectSettings") diff --git a/snaps/openstack/create_router.py b/snaps/openstack/create_router.py new file mode 100644 index 0000000..70c6b76 --- /dev/null +++ b/snaps/openstack/create_router.py @@ -0,0 +1,244 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from neutronclient.common.exceptions import NotFound + +from snaps.openstack.create_network import PortSettings +from snaps.openstack.utils import neutron_utils, keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('OpenStackNetwork') + + +class OpenStackRouter: + """ + Class responsible for creating a router in OpenStack + """ + + def __init__(self, os_creds, router_settings): + """ + Constructor - all parameters are required + :param os_creds: The credentials to connect with OpenStack + :param router_settings: The settings used to create a router object (must be an instance of the + RouterSettings class) + """ + self.__os_creds = os_creds + + if not router_settings: + raise Exception('router_settings is required') + + self.router_settings = router_settings + self.__neutron = neutron_utils.neutron_client(os_creds) + + # Attributes instantiated on create() + self.__router = None + self.__internal_subnets = list() + self.__internal_router_interface = None + + # Dict where the port object is the key and any newly created router interfaces are the value + self.__ports = list() + + def create(self, cleanup=False): + """ + Responsible for creating the router. + :param cleanup: When true, only perform lookups for OpenStack objects. + :return: the router object + """ + logger.debug('Creating Router with name - ' + self.router_settings.name) + try: + existing = False + router_inst = neutron_utils.get_router_by_name(self.__neutron, self.router_settings.name) + if router_inst: + self.__router = router_inst + existing = True + else: + if not cleanup: + self.__router = neutron_utils.create_router(self.__neutron, self.__os_creds, self.router_settings) + + for internal_subnet_name in self.router_settings.internal_subnets: + internal_subnet = neutron_utils.get_subnet_by_name(self.__neutron, internal_subnet_name) + if internal_subnet: + self.__internal_subnets.append(internal_subnet) + if internal_subnet and not cleanup and not existing: + logger.debug('Adding router to subnet...') + self.__internal_router_interface = neutron_utils.add_interface_router( + self.__neutron, self.__router, subnet=internal_subnet) + else: + raise Exception('Subnet not found with name ' + internal_subnet_name) + + for port_setting in self.router_settings.port_settings: + port = neutron_utils.get_port_by_name(self.__neutron, port_setting.name) + logger.info('Retrieved port ' + port_setting.name + ' for router - ' + self.router_settings.name) + if port: + self.__ports.append(port) + + if not port and not cleanup and not existing: + port = neutron_utils.create_port(self.__neutron, self.__os_creds, port_setting) + if port: + logger.info('Created port ' + port_setting.name + ' for router - ' + self.router_settings.name) + self.__ports.append(port) + neutron_utils.add_interface_router(self.__neutron, self.__router, port=port) + else: + raise Exception('Error creating port with name - ' + port_setting.name) + + return self.__router + except Exception as e: + self.clean() + raise Exception(e.message) + + def clean(self): + """ + Removes and deletes all items created in reverse order. + """ + for port in self.__ports: + logger.info('Removing router interface from router ' + self.router_settings.name + + ' and port ' + port['port']['name']) + try: + neutron_utils.remove_interface_router(self.__neutron, self.__router, port=port) + except NotFound: + pass + self.__ports = list() + + for internal_subnet in self.__internal_subnets: + logger.info('Removing router interface from router ' + self.router_settings.name + + ' and subnet ' + internal_subnet['subnet']['name']) + try: + neutron_utils.remove_interface_router(self.__neutron, self.__router, subnet=internal_subnet) + except NotFound: + pass + self.__internal_subnets = list() + + if self.__router: + logger.info('Removing router ' + self.router_settings.name) + try: + neutron_utils.delete_router(self.__neutron, self.__router) + except NotFound: + pass + self.__router = None + + def get_router(self): + """ + Returns the OpenStack router object + :return: + """ + return self.__router + + def get_internal_router_interface(self): + """ + Returns the OpenStack internal router interface object + :return: + """ + return self.__internal_router_interface + + +class RouterSettings: + """ + Class representing a router configuration + """ + + def __init__(self, config=None, name=None, project_name=None, external_gateway=None, + admin_state_up=True, external_fixed_ips=None, internal_subnets=list(), + port_settings=list()): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: The router name. + :param project_name: The name of the project who owns the network. Only administrative users can specify a + project ID other than their own. You cannot change this value through authorization + policies. + :param external_gateway: Name of the external network to which to route + :param admin_state_up: The administrative status of the router. True = up / False = down (default True) + :param enable_snat: Boolean value. Enable Source NAT (SNAT) attribute. Default is True. To persist this + attribute value, set the enable_snat_by_default option in the neutron.conf file. + :param external_fixed_ips: Dictionary containing the IP address parameters. + :param internal_subnets: List of subnet names to which to connect this router for Floating IP purposes + :param port_settings: List of PortSettings objects + :return: + """ + if config: + self.name = config.get('name') + self.project_name = config.get('project_name') + self.external_gateway = config.get('external_gateway') + + self.admin_state_up = config.get('admin_state_up') + self.enable_snat = config.get('enable_snat') + self.external_fixed_ips = config.get('external_fixed_ips') + if config.get('internal_subnets'): + self.internal_subnets = config['internal_subnets'] + else: + self.internal_subnets = internal_subnets + + self.port_settings = list() + if config.get('interfaces'): + interfaces = config['interfaces'] + for interface in interfaces: + if interface.get('port'): + self.port_settings.append(PortSettings(config=interface['port'])) + else: + self.name = name + self.project_name = project_name + self.external_gateway = external_gateway + self.admin_state_up = admin_state_up + self.external_fixed_ips = external_fixed_ips + self.internal_subnets = internal_subnets + self.port_settings = port_settings + + if not self.name: + raise Exception('Name is required') + + def dict_for_neutron(self, neutron, os_creds): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + TODO - expand automated testing to exercise all parameters + :param neutron: The neutron client to retrieve external network information if necessary + :param os_creds: The OpenStack credentials + :return: the dictionary object + """ + out = dict() + ext_gw = dict() + + project_id = None + + if self.name: + out['name'] = self.name + if self.project_name: + keystone = keystone_utils.keystone_client(os_creds) + project = keystone_utils.get_project(keystone, self.project_name) + project_id = None + if project: + project_id = project.id + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + if self.admin_state_up is not None: + out['admin_state_up'] = self.admin_state_up + if self.external_gateway: + ext_net = neutron_utils.get_network(neutron, self.external_gateway, project_id) + if ext_net: + ext_gw['network_id'] = ext_net['network']['id'] + out['external_gateway_info'] = ext_gw + else: + raise Exception('Could not find the external network named - ' + self.external_gateway) + + #TODO: Enable SNAT option for Router + #TODO: Add external_fixed_ips Tests + + return {'router': out} diff --git a/snaps/openstack/create_security_group.py b/snaps/openstack/create_security_group.py new file mode 100644 index 0000000..fc1ee98 --- /dev/null +++ b/snaps/openstack/create_security_group.py @@ -0,0 +1,521 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +import enum +from neutronclient.common.exceptions import NotFound +from snaps.openstack.utils import neutron_utils +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('OpenStackSecurityGroup') + + +class OpenStackSecurityGroup: + """ + Class responsible for creating Security Groups + """ + + def __init__(self, os_creds, sec_grp_settings): + """ + Constructor - all parameters are required + :param os_creds: The credentials to connect with OpenStack + :param sec_grp_settings: The settings used to create a security group + """ + self.__os_creds = os_creds + self.sec_grp_settings = sec_grp_settings + self.__neutron = neutron_utils.neutron_client(os_creds) + self.__keystone = keystone_utils.keystone_client(os_creds) + + # Attributes instantiated on create() + self.__security_group = None + + # dict where the rule settings object is the key + self.__rules = dict() + + def create(self, cleanup=False): + """ + Responsible for creating the security group. + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: the OpenStack security group object + """ + logger.info('Creating security group %s...' % self.sec_grp_settings.name) + + self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name) + if not self.__security_group and not cleanup: + # Create the security group + self.__security_group = neutron_utils.create_security_group(self.__neutron, self.__keystone, + self.sec_grp_settings) + + # Get the rules added for free + auto_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group) + + ctr = 0 + for auto_rule in auto_rules: + auto_rule_setting = self.__generate_rule_setting(auto_rule) + self.__rules[auto_rule_setting] = auto_rule + ctr += 1 + + # Create the custom rules + for sec_grp_rule_setting in self.sec_grp_settings.rule_settings: + custom_rule = neutron_utils.create_security_group_rule(self.__neutron, sec_grp_rule_setting) + self.__rules[sec_grp_rule_setting] = custom_rule + + # Refresh security group object to reflect the new rules added to it + self.__security_group = neutron_utils.get_security_group(self.__neutron, self.sec_grp_settings.name) + else: + # Populate rules + existing_rules = neutron_utils.get_rules_by_security_group(self.__neutron, self.__security_group) + + for existing_rule in existing_rules: + # For Custom Rules + rule_setting = self.__get_setting_from_rule(existing_rule) + ctr = 0 + if not rule_setting: + # For Free Rules + rule_setting = self.__generate_rule_setting(existing_rule) + ctr += 1 + + self.__rules[rule_setting] = existing_rule + + return self.__security_group + + def __generate_rule_setting(self, rule): + """ + Creates a SecurityGroupRuleSettings object for a given rule + :param rule: the rule from which to create the SecurityGroupRuleSettings object + :return: the newly instantiated SecurityGroupRuleSettings object + """ + rule_dict = rule['security_group_rule'] + sec_grp_name = None + if rule_dict['security_group_id']: + sec_grp = neutron_utils.get_security_group_by_id(self.__neutron, rule_dict['security_group_id']) + if sec_grp: + sec_grp_name = sec_grp['security_group']['name'] + + setting = SecurityGroupRuleSettings(description=rule_dict['description'], + direction=rule_dict['direction'], ethertype=rule_dict['ethertype'], + port_range_min=rule_dict['port_range_min'], + port_range_max=rule_dict['port_range_max'], protocol=rule_dict['protocol'], + remote_group_id=rule_dict['remote_group_id'], + remote_ip_prefix=rule_dict['remote_ip_prefix'], sec_grp_name=sec_grp_name) + return setting + + def clean(self): + """ + Removes and deletes the rules then the security group. + """ + for setting, rule in self.__rules.iteritems(): + try: + neutron_utils.delete_security_group_rule(self.__neutron, rule) + except NotFound as e: + logger.warn('Rule not found, cannot delete - ' + e.message) + pass + self.__rules = dict() + + if self.__security_group: + try: + neutron_utils.delete_security_group(self.__neutron, self.__security_group) + except NotFound as e: + logger.warn('Security Group not found, cannot delete - ' + e.message) + + self.__security_group = None + + def get_security_group(self): + """ + Returns the OpenStack security group object + :return: + """ + return self.__security_group + + def get_rules(self): + """ + Returns the associated rules + :return: + """ + return self.__rules + + def add_rule(self, rule_setting): + """ + Adds a rule to this security group + :param rule_setting: the rule configuration + """ + rule_setting.sec_grp_name = self.sec_grp_settings.name + new_rule = neutron_utils.create_security_group_rule(self.__neutron, rule_setting) + self.__rules[rule_setting] = new_rule + self.sec_grp_settings.rule_settings.append(rule_setting) + + def remove_rule(self, rule_id=None, rule_setting=None): + """ + Removes a rule to this security group by id, name, or rule_setting object + :param rule_id: the rule's id + :param rule_setting: the rule's setting object + """ + rule_to_remove = None + if rule_id or rule_setting: + if rule_id: + rule_to_remove = neutron_utils.get_rule_by_id(self.__neutron, self.__security_group, rule_id) + elif rule_setting: + rule_to_remove = self.__rules.get(rule_setting) + + if rule_to_remove: + neutron_utils.delete_security_group_rule(self.__neutron, rule_to_remove) + rule_setting = self.__get_setting_from_rule(rule_to_remove) + if rule_setting: + self.__rules.pop(rule_setting) + else: + logger.warn('Rule setting is None, cannot remove rule') + + def __get_setting_from_rule(self, rule): + """ + Returns the associated RuleSetting object for a given rule + :param rule: the Rule object + :return: the associated RuleSetting object or None + """ + for rule_setting in self.sec_grp_settings.rule_settings: + if rule_setting.rule_eq(rule): + return rule_setting + return None + + +class SecurityGroupSettings: + """ + Class representing a keypair configuration + """ + + def __init__(self, config=None, name=None, description=None, project_name=None, + rule_settings=list()): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param name: The keypair name. + :param description: The security group's description + :param project_name: The name of the project under which the security group will be created + :return: + """ + if config: + self.name = config.get('name') + self.description = config.get('description') + self.project_name = config.get('project_name') + self.rule_settings = list() + if config.get('rules') and type(config['rules']) is list: + for config_rule in config['rules']: + self.rule_settings.append(SecurityGroupRuleSettings(config=config_rule)) + else: + self.name = name + self.description = description + self.project_name = project_name + self.rule_settings = rule_settings + + if not self.name: + raise Exception('The attribute name is required') + + for rule_setting in self.rule_settings: + if rule_setting.sec_grp_name is not self.name: + raise Exception('Rule settings must correspond with the name of this security group') + + def dict_for_neutron(self, keystone): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + TODO - expand automated testing to exercise all parameters + :param keystone: the Keystone client + :return: the dictionary object + """ + out = dict() + + if self.name: + out['name'] = self.name + if self.description: + out['description'] = self.description + if self.project_name: + project = keystone_utils.get_project(keystone, self.project_name) + project_id = None + if project: + project_id = project.id + if project_id: + out['project_id'] = project_id + else: + raise Exception('Could not find project ID for project named - ' + self.project_name) + + return {'security_group': out} + + +class Direction(enum.Enum): + """ + A rule's direction + """ + ingress = 'ingress' + egress = 'egress' + + +class Protocol(enum.Enum): + """ + A rule's protocol + """ + icmp = 'icmp' + tcp = 'tcp' + udp = 'udp' + null = 'null' + + +class Ethertype(enum.Enum): + """ + A rule's ethertype + """ + IPv4 = 4 + IPv6 = 6 + + +class SecurityGroupRuleSettings: + """ + Class representing a keypair configuration + """ + + def __init__(self, config=None, sec_grp_name=None, description=None, direction=None, + remote_group_id=None, protocol=None, ethertype=None, port_range_min=None, port_range_max=None, + sec_grp_rule=None, remote_ip_prefix=None): + """ + Constructor - all parameters are optional + :param config: Should be a dict object containing the configuration settings using the attribute names below + as each member's the key and overrides any of the other parameters. + :param sec_grp_name: The security group's name on which to add the rule. (required) + :param description: The rule's description + :param direction: An enumeration of type create_security_group.RULE_DIRECTION (required) + :param remote_group_id: The group ID to associate with this rule (this should be changed to group name + once snaps support Groups) (optional) + :param protocol: An enumeration of type create_security_group.RULE_PROTOCOL or a string value that will be + mapped accordingly (optional) + :param ethertype: An enumeration of type create_security_group.RULE_ETHERTYPE (optional) + :param port_range_min: The minimum port number in the range that is matched by the security group rule. When + the protocol is TCP or UDP, this value must be <= port_range_max. When the protocol is + ICMP, this value must be an ICMP type. + :param port_range_max: The maximum port number in the range that is matched by the security group rule. When + the protocol is TCP or UDP, this value must be <= port_range_max. When the protocol is + ICMP, this value must be an ICMP type. + :param sec_grp_rule: The OpenStack rule object to a security group rule object to associate + (note: Cannot be set using the config object nor can I see any real uses for this + parameter) + :param remote_ip_prefix: The remote IP prefix to associate with this metering rule packet (optional) + + TODO - Need to support the tenant... + """ + + if config: + self.description = config.get('description') + self.sec_grp_name = config.get('sec_grp_name') + self.remote_group_id = config.get('remote_group_id') + self.direction = None + if config.get('direction'): + self.direction = map_direction(config['direction']) + + self.protocol = None + if config.get('protocol'): + self.protocol = map_protocol(config['protocol']) + else: + self.protocol = Protocol.null + + self.ethertype = None + if config.get('ethertype'): + self.ethertype = map_ethertype(config['ethertype']) + + self.port_range_min = config.get('port_range_min') + self.port_range_max = config.get('port_range_max') + self.remote_ip_prefix = config.get('remote_ip_prefix') + else: + self.description = description + self.sec_grp_name = sec_grp_name + self.remote_group_id = remote_group_id + self.direction = map_direction(direction) + self.protocol = map_protocol(protocol) + self.ethertype = map_ethertype(ethertype) + self.port_range_min = port_range_min + self.port_range_max = port_range_max + self.sec_grp_rule = sec_grp_rule + self.remote_ip_prefix = remote_ip_prefix + + if not self.direction or not self.sec_grp_name: + raise Exception('direction and sec_grp_name are required') + + def dict_for_neutron(self, neutron): + """ + Returns a dictionary object representing this object. + This is meant to be converted into JSON designed for use by the Neutron API + + :param neutron: the neutron client for performing lookups + :return: the dictionary object + """ + out = dict() + + if self.description: + out['description'] = self.description + if self.direction: + out['direction'] = self.direction.name + if self.port_range_min: + out['port_range_min'] = self.port_range_min + if self.port_range_max: + out['port_range_max'] = self.port_range_max + if self.ethertype: + out['ethertype'] = self.ethertype.name + if self.protocol: + out['protocol'] = self.protocol.name + if self.sec_grp_name: + sec_grp = neutron_utils.get_security_group(neutron, self.sec_grp_name) + if sec_grp: + out['security_group_id'] = sec_grp['security_group']['id'] + else: + raise Exception('Cannot locate security group with name - ' + self.sec_grp_name) + if self.remote_group_id: + out['remote_group_id'] = self.remote_group_id + if self.sec_grp_rule: + out['security_group_rule'] = self.sec_grp_rule + if self.remote_ip_prefix: + out['remote_ip_prefix'] = self.remote_ip_prefix + + return {'security_group_rule': out} + + def rule_eq(self, rule): + """ + Returns True if this setting created the rule + :param rule: the rule to evaluate + :return: T/F + """ + rule_dict = rule['security_group_rule'] + + if self.description is not None: + if rule_dict['description'] is not None and rule_dict['description'] != '': + return False + elif self.description != rule_dict['description']: + if rule_dict['description'] != '': + return False + + if self.direction.name != rule_dict['direction']: + return False + + if self.ethertype and rule_dict.get('ethertype'): + if self.ethertype.name != rule_dict['ethertype']: + return False + + if self.port_range_min and rule_dict.get('port_range_min'): + if self.port_range_min != rule_dict['port_range_min']: + return False + + if self.port_range_max and rule_dict.get('port_range_max'): + if self.port_range_max != rule_dict['port_range_max']: + return False + + if self.protocol and rule_dict.get('protocol'): + if self.protocol.name != rule_dict['protocol']: + return False + + if self.remote_group_id and rule_dict.get('remote_group_id'): + if self.remote_group_id != rule_dict['remote_group_id']: + return False + + if self.remote_ip_prefix and rule_dict.get('remote_ip_prefix'): + if self.remote_ip_prefix != rule_dict['remote_ip_prefix']: + return False + + return True + + def __eq__(self, other): + return self.description == other.description and \ + self.direction == other.direction and \ + self.port_range_min == other.port_range_min and \ + self.port_range_max == other.port_range_max and \ + self.ethertype == other.ethertype and \ + self.protocol == other.protocol and \ + self.sec_grp_name == other.sec_grp_name and \ + self.remote_group_id == other.remote_group_id and \ + self.sec_grp_rule == other.sec_grp_rule and \ + self.remote_ip_prefix == other.remote_ip_prefix + + def __hash__(self): + return hash((self.sec_grp_name, self.description, self.direction, self.remote_group_id, + self.protocol, self.ethertype, self.port_range_min, self.port_range_max, self.sec_grp_rule, + self.remote_ip_prefix)) + + +def map_direction(direction): + """ + Takes a the direction value maps it to the Direction enum. When None return None + :param direction: the direction value + :return: the Direction enum object + :raise: Exception if value is invalid + """ + if not direction: + return None + if type(direction) is Direction: + return direction + elif isinstance(direction, basestring): + if direction == 'egress': + return Direction.egress + elif direction == 'ingress': + return Direction.ingress + else: + raise Exception('Invalid Direction - ' + direction) + else: + raise Exception('Invalid Direction object - ' + str(direction)) + + +def map_protocol(protocol): + """ + Takes a the protocol value maps it to the Protocol enum. When None return None + :param protocol: the protocol value + :return: the Protocol enum object + :raise: Exception if value is invalid + """ + if not protocol: + return None + elif type(protocol) is Protocol: + return protocol + elif isinstance(protocol, basestring): + if protocol == 'icmp': + return Protocol.icmp + elif protocol == 'tcp': + return Protocol.tcp + elif protocol == 'udp': + return Protocol.udp + elif protocol == 'null': + return Protocol.null + else: + raise Exception('Invalid Protocol - ' + protocol) + else: + raise Exception('Invalid Protocol object - ' + str(protocol)) + + +def map_ethertype(ethertype): + """ + Takes a the ethertype value maps it to the Ethertype enum. When None return None + :param ethertype: the ethertype value + :return: the Ethertype enum object + :raise: Exception if value is invalid + """ + if not ethertype: + return None + elif type(ethertype) is Ethertype: + return ethertype + elif isinstance(ethertype, basestring): + if ethertype == 'IPv6': + return Ethertype.IPv6 + elif ethertype == 'IPv4': + return Ethertype.IPv4 + else: + raise Exception('Invalid Ethertype - ' + ethertype) + else: + raise Exception('Invalid Ethertype object - ' + str(ethertype)) diff --git a/snaps/openstack/create_user.py b/snaps/openstack/create_user.py new file mode 100644 index 0000000..a8d0fcc --- /dev/null +++ b/snaps/openstack/create_user.py @@ -0,0 +1,137 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +from keystoneclient.exceptions import NotFound +from snaps.openstack.os_credentials import OSCreds + +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('create_user') + + +class OpenStackUser: + """ + Class responsible for creating a user in OpenStack + """ + + def __init__(self, os_creds, user_settings): + """ + Constructor + :param os_creds: The OpenStack connection credentials + :param user_settings: The user settings + :return: + """ + self.__os_creds = os_creds + self.user_settings = user_settings + self.__user = None + self.__keystone = keystone_utils.keystone_client(self.__os_creds) + + def create(self, cleanup=False): + """ + Creates the user in OpenStack if it does not already exist + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: The OpenStack user object + """ + self.__user = keystone_utils.get_user(self.__keystone, self.user_settings.name) + if self.__user: + logger.info('Found user with name - ' + self.user_settings.name) + elif not cleanup: + self.__user = keystone_utils.create_user(self.__keystone, self.user_settings) + else: + logger.info('Did not create user due to cleanup mode') + + return self.__user + + def clean(self): + """ + Cleanse environment of user + :return: void + """ + if self.__user: + try: + keystone_utils.delete_user(self.__keystone, self.__user) + except NotFound: + pass + self.__user = None + + def get_user(self): + """ + Returns the OpenStack user object populated in create() + :return: the Object or None if not created + """ + return self.__user + + def get_os_creds(self, project_name=None): + """ + Returns an OSCreds object based on this user account and a project + :param project_name: the name of the project to leverage in the credentials + :return: + """ + return OSCreds(username=self.user_settings.name, + password=self.user_settings.password, + auth_url=self.__os_creds.auth_url, + project_name=project_name, + identity_api_version=self.__os_creds.identity_api_version, + user_domain_id=self.__os_creds.user_domain_id, + project_domain_id=self.__os_creds.project_domain_id, + proxy_settings=self.__os_creds.proxy_settings) + + +class UserSettings: + def __init__(self, config=None, name=None, password=None, project_name=None, domain_name='default', email=None, + enabled=True): + + """ + Constructor + :param config: dict() object containing the configuration settings using the attribute names below as each + member's the key and overrides any of the other parameters. + :param name: the user's name (required) + :param password: the user's password (required) + :param project_name: the user's primary project name (optional) + :param domain_name: the user's domain name (default='default'). For v3 APIs + :param email: the user's email address (optional) + :param enabled: denotes whether or not the user is enabled (default True) + """ + + if config: + self.name = config.get('name') + self.password = config.get('password') + self.project_name = config.get('project_name') + self.email = config.get('email') + + if config.get('domain_name'): + self.domain_name = config['domain_name'] + else: + self.domain_name = domain_name + + if config.get('enabled') is not None: + self.enabled = config['enabled'] + else: + self.enabled = enabled + else: + self.name = name + self.password = password + self.project_name = project_name + self.email = email + self.enabled = enabled + self.domain_name = domain_name + + if not self.name or not self.password: + raise Exception('The attributes name and password are required for UserSettings') + + if not isinstance(self.enabled, bool): + raise Exception('The attribute enabled must be of type boolean') diff --git a/snaps/openstack/os_credentials.py b/snaps/openstack/os_credentials.py new file mode 100644 index 0000000..c173bf7 --- /dev/null +++ b/snaps/openstack/os_credentials.py @@ -0,0 +1,103 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' + + +class OSCreds: + """ + Represents the credentials required to connect with OpenStack servers + """ + + def __init__(self, username, password, auth_url, project_name, identity_api_version=2, image_api_version=1, + network_api_version=2, compute_api_version=2, user_domain_id='default', project_domain_id='default', + proxy_settings=None): + """ + Constructor + :param username: The user (required) + :param password: The user's password (required) + :param auth_url: The OpenStack cloud's authorization URL (required) + :param project_name: The project/tenant name + :param identity_api_version: The OpenStack's API version to use for Keystone clients + :param image_api_version: The OpenStack's API version to use for Glance clients + :param network_api_version: The OpenStack's API version to use for Neutron clients + :param compute_api_version: The OpenStack's API version to use for Nova clients + :param user_domain_id: Used for v3 APIs + :param project_domain_id: Used for v3 APIs + :param proxy_settings: instance of os_credentials.ProxySettings class + """ + self.username = username + self.password = password + self.auth_url = auth_url + self.project_name = project_name + self.identity_api_version = identity_api_version + self.image_api_version = image_api_version + self.network_api_version = network_api_version + self.compute_api_version = compute_api_version + self.user_domain_id = user_domain_id + self.project_domain_id = project_domain_id + self.proxy_settings = proxy_settings + + if self.proxy_settings and not isinstance(self.proxy_settings, ProxySettings): + raise Exception('proxy_settings must be an instance of the class ProxySettings') + + if self.auth_url: + auth_url_tokens = self.auth_url.split('/') + last_token = auth_url_tokens[len(auth_url_tokens) - 1] + if len(last_token) == 0: + last_token = auth_url_tokens[len(auth_url_tokens) - 2] + + if not last_token.startswith('v'): + raise Exception('auth_url last toke must start with \'v\'') + + def __str__(self): + """Converts object to a string""" + return 'OSCreds - username=' + str(self.username) + \ + ', password=' + str(self.password) + \ + ', auth_url=' + str(self.auth_url) + \ + ', project_name=' + str(self.project_name) + \ + ', identity_api_version=' + str(self.identity_api_version) + \ + ', image_api_version=' + str(self.image_api_version) + \ + ', network_api_version=' + str(self.network_api_version) + \ + ', compute_api_version=' + str(self.compute_api_version) + \ + ', user_domain_id=' + str(self.user_domain_id) + \ + ', proxy_settings=' + str(self.proxy_settings) + + +class ProxySettings: + """ + Represents the information required for sending traffic (HTTP & SSH) through a proxy + """ + + def __init__(self, host, port, ssh_proxy_cmd=None): + """ + Constructor + :param host: the HTTP proxy host + :param port: the HTTP proxy port + :param ssh_proxy_cmd: the SSH proxy command string (optional) + """ + # TODO - Add necessary fields here when adding support for secure proxies + + self.host = host + self.port = port + self.ssh_proxy_cmd = ssh_proxy_cmd + + if not self.host and not self.port: + raise Exception('host & port are required') + + def __str__(self): + """Converts object to a string""" + return 'ProxySettings - host=' + str(self.host) + \ + ', port=' + str(self.port) + \ + ', ssh_proxy_cmd=' + str(self.ssh_proxy_cmd) diff --git a/snaps/openstack/tests/__init__.py b/snaps/openstack/tests/__init__.py new file mode 100644 index 0000000..e3e876e --- /dev/null +++ b/snaps/openstack/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' diff --git a/snaps/openstack/tests/conf/os_env.yaml.template b/snaps/openstack/tests/conf/os_env.yaml.template new file mode 100644 index 0000000..da6b942 --- /dev/null +++ b/snaps/openstack/tests/conf/os_env.yaml.template @@ -0,0 +1,39 @@ +# Keystone v2.0 +#username: admin +#password: admin +#os_auth_url: http://:/v2.0/ +#project_name: admin +#ext_net: +#http_proxy: : +#ssh_proxy_cmd: '/usr/local/bin/corkscrew %h %p' +#ssh_proxy_cmd: 'ssh nc %h %p' + +# Keystone v2.0 +#username: admin +#password: admin +#os_auth_url: http://:/v3 +#project_name: admin +#identity_api_version: 3 +#ext_net: + + + +#username: admin +#password: cable123 +#os_auth_url: http://192.168.67.10:5000/v2.0/ +#project_name: admin +#ext_net: external +#http_proxy: 10.197.123.27:3128 +#ssh_proxy_cmd: '/usr/local/bin/corkscrew 10.197.123.27 3128 %h %p' + + + + +username: admin +password: admin +os_auth_url: http://192.168.0.2:5000/v3 +project_name: admin +identity_api_version: 3 +ext_net: admin_floating_net +http_proxy: 10.197.123.27:3128 +ssh_proxy_cmd: '/usr/local/bin/corkscrew localhost 3128 %h %p' \ No newline at end of file diff --git a/snaps/openstack/tests/conf/overcloudrc_test b/snaps/openstack/tests/conf/overcloudrc_test new file mode 100644 index 0000000..87746d8 --- /dev/null +++ b/snaps/openstack/tests/conf/overcloudrc_test @@ -0,0 +1,9 @@ +export NOVA_VERSION=1.1 +export OS_PASSWORD=test_pw +export OS_AUTH_URL=http://foo:5000/v2.0/ +export OS_USERNAME=admin +export OS_TENANT_NAME=admin +export COMPUTE_API_VERSION=1.1 +export OS_NO_CACHE=True +export OS_CLOUDNAME=undercloud +export OS_IMAGE_API_VERSION=1 \ No newline at end of file diff --git a/snaps/openstack/tests/create_flavor_tests.py b/snaps/openstack/tests/create_flavor_tests.py new file mode 100644 index 0000000..c75bdf6 --- /dev/null +++ b/snaps/openstack/tests/create_flavor_tests.py @@ -0,0 +1,311 @@ +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest + +from snaps.openstack.create_flavor import FlavorSettings, OpenStackFlavor +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import nova_utils + +__author__ = 'spisarski' + + +class FlavorSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the FlavorSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + FlavorSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + FlavorSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo'}) + + def test_name_ram_only(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1) + + def test_config_with_name_ram_only(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1}) + + def test_name_ram_disk_only(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=1) + + def test_config_with_name_ram_disk_only(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 1}) + + def test_ram_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram='bar', disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, + is_public=False) + + def test_config_ram_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 'bar', 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_ram_float(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1.5, disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, is_public=False) + + def test_config_ram_float(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1.5, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_disk_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk='bar', vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, + is_public=False) + + def test_config_disk_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 'bar', 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_disk_float(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2.5, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, is_public=False) + + def test_config_disk_float(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2.5, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_vcpus_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus='bar', ephemeral=4, swap=5, rxtx_factor=6.0, + is_public=False) + + def test_config_vcpus_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 'bar', 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_ephemeral_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral='bar', swap=5, rxtx_factor=6.0, + is_public=False) + + def test_config_ephemeral_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 'bar', 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_ephemeral_float(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4.5, swap=5, rxtx_factor=6.0, is_public=False) + + def test_config_ephemeral_float(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4.5, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_swap_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4, swap='bar', rxtx_factor=6.0, + is_public=False) + + def test_config_swap_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 'bar', + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_swap_float(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5.5, rxtx_factor=6.0, is_public=False) + + def test_config_swap_float(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5.5, + 'rxtx_factor': 6.0, 'is_public': False}) + + def test_rxtx_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor='bar', is_public=False) + + def test_config_rxtx_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 'bar', 'is_public': False}) + + def test_is_pub_string(self): + with self.assertRaises(Exception): + FlavorSettings(name='foo', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, is_public='bar') + + def test_config_is_pub_string(self): + with self.assertRaises(Exception): + FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, 'swap': 5, + 'rxtx_factor': 6.0, 'is_public': 'bar'}) + + def test_name_ram_disk_vcpus_only(self): + settings = FlavorSettings(name='foo', ram=1, disk=2, vcpus=3) + self.assertEquals('foo', settings.name) + self.assertEquals('auto', settings.flavor_id) + self.assertEquals(1, settings.ram) + self.assertEquals(2, settings.disk) + self.assertEquals(3, settings.vcpus) + self.assertEquals(0, settings.ephemeral) + self.assertEquals(0, settings.swap) + self.assertEquals(1.0, settings.rxtx_factor) + self.assertEquals(True, settings.is_public) + + def test_config_with_name_ram_disk_vcpus_only(self): + settings = FlavorSettings(config={'name': 'foo', 'ram': 1, 'disk': 2, 'vcpus': 3}) + self.assertEquals('foo', settings.name) + self.assertEquals('auto', settings.flavor_id) + self.assertEquals(1, settings.ram) + self.assertEquals(2, settings.disk) + self.assertEquals(3, settings.vcpus) + self.assertEquals(0, settings.ephemeral) + self.assertEquals(0, settings.swap) + self.assertEquals(1.0, settings.rxtx_factor) + self.assertEquals(True, settings.is_public) + + def test_all(self): + settings = FlavorSettings(name='foo', flavor_id='bar', ram=1, disk=2, vcpus=3, ephemeral=4, swap=5, rxtx_factor=6.0, + is_public=False) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor_id) + self.assertEquals(1, settings.ram) + self.assertEquals(2, settings.disk) + self.assertEquals(3, settings.vcpus) + self.assertEquals(4, settings.ephemeral) + self.assertEquals(5, settings.swap) + self.assertEquals(6.0, settings.rxtx_factor) + self.assertEquals(False, settings.is_public) + + def test_config_all(self): + settings = FlavorSettings(config={'name': 'foo', 'flavor_id': 'bar', 'ram': 1, 'disk': 2, 'vcpus': 3, 'ephemeral': 4, + 'swap': 5, 'rxtx_factor': 6.0, 'is_public': False}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor_id) + self.assertEquals(1, settings.ram) + self.assertEquals(2, settings.disk) + self.assertEquals(3, settings.vcpus) + self.assertEquals(4, settings.ephemeral) + self.assertEquals(5, settings.swap) + self.assertEquals(6.0, settings.rxtx_factor) + self.assertEquals(False, settings.is_public) + + +class CreateFlavorTests(OSComponentTestCase): + """ + Test for the CreateSecurityGroup class defined in create_security_group.py + """ + + def setUp(self): + """ + Instantiates the CreateSecurityGroup object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.flavor_name = guid + 'name' + + self.nova = nova_utils.nova_client(self.os_creds) + + # Initialize for cleanup + self.flavor_creator = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.flavor_creator: + self.flavor_creator.clean() + + def test_create_flavor(self): + """ + Tests the creation of an OpenStack flavor. + """ + # Create Flavor + flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1, vcpus=1) + self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings) + flavor = self.flavor_creator.create() + self.assertTrue(validate_flavor(flavor_settings, flavor)) + + def test_create_flavor_existing(self): + """ + Tests the creation of an OpenStack flavor then starts another creator to ensure it has not been done twice. + """ + # Create Flavor + flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1, vcpus=1) + self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings) + flavor = self.flavor_creator.create() + self.assertTrue(validate_flavor(flavor_settings, flavor)) + + flavor_creator_2 = OpenStackFlavor(self.os_creds, flavor_settings) + flavor2 = flavor_creator_2.create() + + self.assertEquals(flavor.id, flavor2.id) + + def test_create_clean_flavor(self): + """ + Tests the creation and cleanup of an OpenStack flavor. + """ + # Create Flavor + flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1, vcpus=1) + self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings) + flavor = self.flavor_creator.create() + self.assertTrue(validate_flavor(flavor_settings, flavor)) + + # Clean Flavor + self.flavor_creator.clean() + + self.assertIsNone(self.flavor_creator.get_flavor()) + self.assertIsNone(nova_utils.get_flavor_by_name(self.nova, flavor_settings.name)) + + def test_create_delete_flavor(self): + """ + Tests the creation of an OpenStack Security Group, the deletion, then cleanup to ensure clean() does not + raise any exceptions. + """ + # Create Flavor + flavor_settings = FlavorSettings(name=self.flavor_name, ram=1, disk=1, vcpus=1) + self.flavor_creator = OpenStackFlavor(self.os_creds, flavor_settings) + flavor = self.flavor_creator.create() + self.assertTrue(validate_flavor(flavor_settings, flavor)) + + # Delete Flavor + nova_utils.delete_flavor(self.nova, flavor) + self.assertIsNone(nova_utils.get_flavor_by_name(self.nova, flavor_settings.name)) + + # Attempt to cleanup + self.flavor_creator.clean() + + self.assertIsNone(self.flavor_creator.get_flavor()) + + # TODO - Add more tests to exercise all configuration options + + +def validate_flavor(flavor_settings, flavor): + """ + Validates the flavor_settings against the OpenStack flavor object + :param flavor_settings: the settings used to create the flavor + :param flavor: the OpenStack flavor object + """ + return flavor is not None \ + and flavor_settings.name == flavor.name \ + and flavor_settings.ram == flavor.ram \ + and flavor_settings.disk == flavor.disk \ + and flavor_settings.vcpus == flavor.vcpus diff --git a/snaps/openstack/tests/create_image_tests.py b/snaps/openstack/tests/create_image_tests.py new file mode 100644 index 0000000..24bf0f2 --- /dev/null +++ b/snaps/openstack/tests/create_image_tests.py @@ -0,0 +1,362 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import shutil +import uuid +import unittest + +from snaps import file_utils +from snaps.openstack.create_image import ImageSettings + +import openstack_tests +from snaps.openstack.utils import glance_utils, nova_utils +from snaps.openstack import create_image +from snaps.openstack import os_credentials +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase + +__author__ = 'spisarski' + + +class ImageSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the ImageSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + ImageSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + ImageSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + ImageSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + ImageSettings(config={'name': 'foo'}) + + def test_name_user_only(self): + with self.assertRaises(Exception): + ImageSettings(name='foo', image_user='bar') + + def test_config_with_name_user_only(self): + with self.assertRaises(Exception): + ImageSettings(config={'name': 'foo', 'image_user': 'bar'}) + + def test_name_user_format_only(self): + with self.assertRaises(Exception): + ImageSettings(name='foo', image_user='bar', img_format='qcow2') + + def test_config_with_name_user_format_only(self): + with self.assertRaises(Exception): + ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2'}) + + def test_name_user_format_url_file_only(self): + with self.assertRaises(Exception): + ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com', + image_file='/foo/bar.qcow') + + def test_config_with_name_user_format_url_file_only(self): + with self.assertRaises(Exception): + ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'download_url': 'http://foo.com', 'image_file': '/foo/bar.qcow'}) + + def test_name_user_format_url_only(self): + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertEquals('http://foo.com', settings.url) + self.assertIsNone(settings.image_file) + self.assertIsNone(settings.nic_config_pb_loc) + + def test_config_with_name_user_format_url_only(self): + settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'download_url': 'http://foo.com'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertEquals('http://foo.com', settings.url) + self.assertIsNone(settings.image_file) + self.assertIsNone(settings.nic_config_pb_loc) + + def test_name_user_format_file_only(self): + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', image_file='/foo/bar.qcow') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertIsNone(settings.url) + self.assertEquals('/foo/bar.qcow', settings.image_file) + self.assertIsNone(settings.nic_config_pb_loc) + + def test_config_with_name_user_format_file_only(self): + settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'image_file': '/foo/bar.qcow'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertIsNone(settings.url) + self.assertEquals('/foo/bar.qcow', settings.image_file) + self.assertIsNone(settings.nic_config_pb_loc) + + def test_all_url(self): + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', url='http://foo.com', + nic_config_pb_loc='/foo/bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertEquals('http://foo.com', settings.url) + self.assertIsNone(settings.image_file) + self.assertEquals('/foo/bar', settings.nic_config_pb_loc) + + def test_config_all_url(self): + settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'download_url': 'http://foo.com', 'nic_config_pb_loc': '/foo/bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertEquals('http://foo.com', settings.url) + self.assertIsNone(settings.image_file) + self.assertEquals('/foo/bar', settings.nic_config_pb_loc) + + def test_all_file(self): + settings = ImageSettings(name='foo', image_user='bar', img_format='qcow2', image_file='/foo/bar.qcow', + nic_config_pb_loc='/foo/bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertIsNone(settings.url) + self.assertEquals('/foo/bar.qcow', settings.image_file) + self.assertEquals('/foo/bar', settings.nic_config_pb_loc) + + def test_config_all_file(self): + settings = ImageSettings(config={'name': 'foo', 'image_user': 'bar', 'format': 'qcow2', + 'image_file': '/foo/bar.qcow', 'nic_config_pb_loc': '/foo/bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.image_user) + self.assertEquals('qcow2', settings.format) + self.assertIsNone(settings.url) + self.assertEquals('/foo/bar.qcow', settings.image_file) + self.assertEquals('/foo/bar', settings.nic_config_pb_loc) + + +class CreateImageSuccessTests(OSIntegrationTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = uuid.uuid4() + self.image_name = self.__class__.__name__ + '-' + str(guid) + + self.nova = nova_utils.nova_client(self.os_creds) + self.glance = glance_utils.glance_client(self.os_creds) + + self.tmp_dir = 'tmp/' + str(guid) + if not os.path.exists(self.tmp_dir): + os.makedirs(self.tmp_dir) + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.image_creator: + self.image_creator.clean() + + if os.path.exists(self.tmp_dir) and os.path.isdir(self.tmp_dir): + shutil.rmtree(self.tmp_dir) + + super(self.__class__, self).__clean__() + + def test_create_image_clean_url(self): + """ + Tests the creation of an OpenStack image from a URL. + """ + # Create Image + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings) + + created_image = self.image_creator.create() + self.assertIsNotNone(created_image) + + retrieved_image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name) + self.assertIsNotNone(retrieved_image) + + self.assertEquals(created_image.name, retrieved_image.name) + self.assertEquals(created_image.id, retrieved_image.id) + + def test_create_image_clean_file(self): + """ + Tests the creation of an OpenStack image from a file. + """ + url_image_settings = openstack_tests.cirros_url_image('foo') + image_file = file_utils.download(url_image_settings.url, self.tmp_dir) + file_image_settings = openstack_tests.file_image_test_settings(name=self.image_name, file_path=image_file.name) + self.image_creator = create_image.OpenStackImage(self.os_creds, file_image_settings) + + self.image = self.image_creator.create() + self.assertIsNotNone(self.image) + self.assertEqual(self.image_name, self.image.name) + + created_image = self.image_creator.create() + self.assertIsNotNone(created_image) + + retrieved_image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name) + self.assertIsNotNone(retrieved_image) + + self.assertEquals(created_image.name, retrieved_image.name) + self.assertEquals(created_image.id, retrieved_image.id) + + def test_create_delete_image(self): + """ + Tests the creation then deletion of an OpenStack image to ensure clean() does not raise an Exception. + """ + # Create Image + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings) + created_image = self.image_creator.create() + self.assertIsNotNone(created_image) + + # Delete Image manually + glance_utils.delete_image(self.glance, created_image) + + self.assertIsNone(glance_utils.get_image(self.nova, self.glance, self.image_creator.image_settings.name)) + + # Must not throw an exception when attempting to cleanup non-existent image + self.image_creator.clean() + self.assertIsNone(self.image_creator.get_image()) + + def test_create_same_image(self): + """ + Tests the creation of an OpenStack image when the image already exists. + """ + # Create Image + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage(self.os_creds, os_image_settings) + image1 = self.image_creator.create() + # Should be retrieving the instance data + os_image_2 = create_image.OpenStackImage(self.os_creds, os_image_settings) + image2 = os_image_2.create() + self.assertEquals(image1.id, image2.id) + + +class CreateImageNegativeTests(OSIntegrationTestCase): + """ + Negative test cases for the CreateImage class + """ + + def setUp(self): + super(self.__class__, self).__start__() + + self.image_name = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.image_creator = None + + def tearDown(self): + if self.image_creator: + self.image_creator.clean() + + super(self.__class__, self).__clean__() + + def test_none_image_name(self): + """ + Expect an exception when the image name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + self.os_creds, create_image.ImageSettings( + name=None, image_user=os_image_settings.image_user, img_format=os_image_settings.format, + url=os_image_settings.url)) + + self.fail('Exception should have been thrown prior to this line') + + def test_bad_image_url(self): + """ + Expect an exception when the image download url is bad + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage(self.os_creds, create_image.ImageSettings( + name=os_image_settings.name, image_user=os_image_settings.image_user, + img_format=os_image_settings.format, url="http://foo.bar")) + with self.assertRaises(Exception): + self.image_creator.create() + + def test_bad_image_file(self): + """ + Expect an exception when the image file does not exist + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + self.image_creator = create_image.OpenStackImage( + self.os_creds, + create_image.ImageSettings(name=os_image_settings.name, image_user=os_image_settings.image_user, + img_format=os_image_settings.format, image_file="/foo/bar.qcow")) + with self.assertRaises(Exception): + self.image_creator.create() + + def test_none_proj_name(self): + """ + Expect an exception when the project name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + os_credentials.OSCreds(self.os_creds.username, self.os_creds.password, self.os_creds.auth_url, None, + proxy_settings=self.os_creds.proxy_settings), + os_image_settings) + self.image_creator.create() + + def test_none_auth_url(self): + """ + Expect an exception when the project name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + os_credentials.OSCreds(self.os_creds.username, self.os_creds.password, None, + self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings), + os_image_settings) + self.image_creator.create() + + def test_none_password(self): + """ + Expect an exception when the project name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + os_credentials.OSCreds(self.os_creds.username, None, self.os_creds.os_auth_url, + self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings), + os_image_settings) + + def test_none_user(self): + """ + Expect an exception when the project name is None + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + with self.assertRaises(Exception): + self.image_creator = create_image.OpenStackImage( + os_credentials.OSCreds(None, self.os_creds.password, self.os_creds.os_auth_url, + self.os_creds.project_name, + proxy_settings=self.os_creds.proxy_settings), + os_image_settings) diff --git a/snaps/openstack/tests/create_instance_tests.py b/snaps/openstack/tests/create_instance_tests.py new file mode 100644 index 0000000..756b45f --- /dev/null +++ b/snaps/openstack/tests/create_instance_tests.py @@ -0,0 +1,1474 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os +import time +import unittest +import uuid + +from snaps.openstack.create_instance import VmInstanceSettings, OpenStackVmInstance, FloatingIpSettings +from snaps.openstack.create_flavor import OpenStackFlavor, FlavorSettings +from snaps.openstack.create_keypairs import OpenStackKeypair, KeypairSettings +from snaps.openstack.create_network import OpenStackNetwork, PortSettings +from snaps.openstack.create_router import OpenStackRouter +from snaps.openstack.create_image import OpenStackImage +from snaps.openstack.create_security_group import SecurityGroupSettings, OpenStackSecurityGroup +from snaps.openstack.tests import openstack_tests, validation_utils +from snaps.openstack.utils import nova_utils +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase, OSIntegrationTestCase + +__author__ = 'spisarski' + +VM_BOOT_TIMEOUT = 600 + +logger = logging.getLogger('create_instance_tests') + + +class VmInstanceSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the VmInstanceSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + VmInstanceSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + VmInstanceSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + VmInstanceSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + VmInstanceSettings(config={'name': 'foo'}) + + def test_name_flavor_only(self): + settings = VmInstanceSettings(name='foo', flavor='bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor) + self.assertEquals(0, len(settings.port_settings)) + self.assertEquals(0, len(settings.security_group_names)) + self.assertEquals(0, len(settings.floating_ip_settings)) + self.assertIsNone(settings.sudo_user) + self.assertEquals(900, settings.vm_boot_timeout) + self.assertEquals(300, settings.vm_delete_timeout) + self.assertEquals(180, settings.ssh_connect_timeout) + self.assertIsNone(settings.availability_zone) + + def test_config_with_name_flavor_only(self): + settings = VmInstanceSettings(config={'name': 'foo', 'flavor': 'bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor) + self.assertEquals(0, len(settings.port_settings)) + self.assertEquals(0, len(settings.security_group_names)) + self.assertEquals(0, len(settings.floating_ip_settings)) + self.assertIsNone(settings.sudo_user) + self.assertEquals(900, settings.vm_boot_timeout) + self.assertEquals(300, settings.vm_delete_timeout) + self.assertEquals(180, settings.ssh_connect_timeout) + self.assertIsNone(settings.availability_zone) + + def test_all(self): + port_settings = PortSettings(name='foo-port', network_name='bar-net') + fip_settings = FloatingIpSettings(name='foo-fip', port_name='bar-port', router_name='foo-bar-router') + + settings = VmInstanceSettings(name='foo', flavor='bar', port_settings=[port_settings], + security_group_names=['sec_grp_1'], floating_ip_settings=[fip_settings], + sudo_user='joe', vm_boot_timeout=999, vm_delete_timeout=333, + ssh_connect_timeout=111, availability_zone='server name') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor) + self.assertEquals(1, len(settings.port_settings)) + self.assertEquals('foo-port', settings.port_settings[0].name) + self.assertEquals('bar-net', settings.port_settings[0].network_name) + self.assertEquals(1, len(settings.security_group_names)) + self.assertEquals('sec_grp_1', settings.security_group_names[0]) + self.assertEquals(1, len(settings.floating_ip_settings)) + self.assertEquals('foo-fip', settings.floating_ip_settings[0].name) + self.assertEquals('bar-port', settings.floating_ip_settings[0].port_name) + self.assertEquals('foo-bar-router', settings.floating_ip_settings[0].router_name) + self.assertEquals('joe', settings.sudo_user) + self.assertEquals(999, settings.vm_boot_timeout) + self.assertEquals(333, settings.vm_delete_timeout) + self.assertEquals(111, settings.ssh_connect_timeout) + self.assertEquals('server name', settings.availability_zone) + + def test_config_all(self): + port_settings = PortSettings(name='foo-port', network_name='bar-net') + fip_settings = FloatingIpSettings(name='foo-fip', port_name='bar-port', router_name='foo-bar-router') + + settings = VmInstanceSettings(config={'name': 'foo', 'flavor': 'bar', 'ports': [port_settings], + 'security_group_names': ['sec_grp_1'], + 'floating_ips': [fip_settings], 'sudo_user': 'joe', + 'vm_boot_timeout': 999, 'vm_delete_timeout': 333, + 'ssh_connect_timeout': 111, 'availability_zone': 'server name'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.flavor) + self.assertEquals(1, len(settings.port_settings)) + self.assertEquals('foo-port', settings.port_settings[0].name) + self.assertEquals('bar-net', settings.port_settings[0].network_name) + self.assertEquals(1, len(settings.security_group_names)) + self.assertEquals(1, len(settings.floating_ip_settings)) + self.assertEquals('foo-fip', settings.floating_ip_settings[0].name) + self.assertEquals('bar-port', settings.floating_ip_settings[0].port_name) + self.assertEquals('foo-bar-router', settings.floating_ip_settings[0].router_name) + self.assertEquals('joe', settings.sudo_user) + self.assertEquals(999, settings.vm_boot_timeout) + self.assertEquals(333, settings.vm_delete_timeout) + self.assertEquals(111, settings.ssh_connect_timeout) + self.assertEquals('server name', settings.availability_zone) + + +class FloatingIpSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the FloatingIpSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + FloatingIpSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + FloatingIpSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(config={'name': 'foo'}) + + def test_name_port_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(name='foo', port_name='bar') + + def test_config_with_name_port_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(config={'name': 'foo', 'port_name': 'bar'}) + + def test_name_router_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(name='foo', router_name='bar') + + def test_config_with_name_router_only(self): + with self.assertRaises(Exception): + FloatingIpSettings(config={'name': 'foo', 'router_name': 'bar'}) + + def test_name_port_router_only(self): + settings = FloatingIpSettings(name='foo', port_name='foo-port', router_name='bar-router') + self.assertEquals('foo', settings.name) + self.assertEquals('foo-port', settings.port_name) + self.assertEquals('bar-router', settings.router_name) + self.assertIsNone(settings.subnet_name) + self.assertTrue(settings.provisioning) + + def test_config_with_name_port_router_only(self): + settings = FloatingIpSettings(config={'name': 'foo', 'port_name': 'foo-port', 'router_name': 'bar-router'}) + self.assertEquals('foo', settings.name) + self.assertEquals('foo-port', settings.port_name) + self.assertEquals('bar-router', settings.router_name) + self.assertIsNone(settings.subnet_name) + self.assertTrue(settings.provisioning) + + def test_all(self): + settings = FloatingIpSettings(name='foo', port_name='foo-port', router_name='bar-router', + subnet_name='bar-subnet', provisioning=False) + self.assertEquals('foo', settings.name) + self.assertEquals('foo-port', settings.port_name) + self.assertEquals('bar-router', settings.router_name) + self.assertEquals('bar-subnet', settings.subnet_name) + self.assertFalse(settings.provisioning) + + def test_config_all(self): + settings = FloatingIpSettings(config={'name': 'foo', 'port_name': 'foo-port', 'router_name': 'bar-router', + 'subnet_name': 'bar-subnet', 'provisioning': False}) + self.assertEquals('foo', settings.name) + self.assertEquals('foo-port', settings.port_name) + self.assertEquals('bar-router', settings.router_name) + self.assertEquals('bar-subnet', settings.subnet_name) + self.assertFalse(settings.provisioning) + + +class SimpleHealthCheck(OSIntegrationTestCase): + """ + Test for the CreateInstance class with a single NIC/Port with Floating IPs + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.keypair_priv_filepath = 'tmp/' + guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = guid + '-kp' + self.vm_inst_name = guid + '-inst' + self.port_1_name = guid + 'port-1' + self.port_2_name = guid + 'port-2' + self.floating_ip_name = guid + 'fip1' + + # Initialize for tearDown() + self.image_creator = None + self.network_creator = None + self.flavor_creator = None + self.inst_creator = None + + self.priv_net_config = openstack_tests.get_priv_net_config( + net_name=guid + '-priv-net', subnet_name=guid + '-priv-subnet') + self.port_settings = PortSettings( + name=self.port_1_name, network_name=self.priv_net_config.network_settings.name) + + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.priv_net_config.network_settings) + self.network_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=1024, disk=10, vcpus=1)) + self.flavor_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_check_vm_ip_dhcp(self): + """ + Tests the creation of an OpenStack instance with a single port and ensures that it's assigned IP address is + the actual. + """ + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[self.port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm = self.inst_creator.create() + + ip = self.inst_creator.get_port_ip(self.port_settings.name) + self.assertIsNotNone(ip) + + self.assertTrue(self.inst_creator.vm_active(block=True)) + + found = False + timeout = 100 + start_time = time.time() + match_value = 'Lease of ' + ip + ' obtained,' + + while timeout > time.time() - start_time: + output = vm.get_console_output() + if match_value in output: + found = True + break + self.assertTrue(found) + + +class CreateInstanceSimpleTests(OSIntegrationTestCase): + """ + Simple instance creation tests without any other objects + """ + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.vm_inst_name = guid + '-inst' + self.nova = nova_utils.nova_client(self.os_creds) + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + # Initialize for tearDown() + self.image_creator = None + self.flavor_creator = None + self.inst_creator = None + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_create_delete_instance(self): + """ + Tests the creation of an OpenStack instance with a single port with a static IP without a Floating IP. + """ + instance_settings = VmInstanceSettings(name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name) + + self.inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings) + + vm_inst = self.inst_creator.create() + self.assertEquals(1, len(nova_utils.get_servers_by_name(self.nova, instance_settings.name))) + + # Delete instance + nova_utils.delete_vm_instance(self.nova, vm_inst) + + self.assertTrue(self.inst_creator.vm_deleted(block=True)) + self.assertEquals(0, len(nova_utils.get_servers_by_name(self.nova, instance_settings.name))) + + # Exception should not be thrown + self.inst_creator.clean() + + +class CreateInstanceSingleNetworkTests(OSIntegrationTestCase): + """ + Test for the CreateInstance class with a single NIC/Port with Floating IPs + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.keypair_priv_filepath = 'tmp/' + guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = guid + '-kp' + self.vm_inst_name = guid + '-inst' + self.port_1_name = guid + 'port-1' + self.port_2_name = guid + 'port-2' + self.floating_ip_name = guid + 'fip1' + + # Initialize for tearDown() + self.image_creator = None + self.network_creator = None + self.router_creator = None + self.flavor_creator = None + self.keypair_creator = None + self.inst_creators = list() + + self.pub_net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings) + self.network_creator.create() + + # Create Router + self.router_creator = OpenStackRouter(self.os_creds, self.pub_net_config.router_settings) + self.router_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings( + name=self.keypair_name, public_filepath=self.keypair_pub_filepath, + private_filepath=self.keypair_priv_filepath)) + self.keypair_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + for inst_creator in self.inst_creators: + try: + inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.keypair_creator: + try: + self.keypair_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning keypair with message - ' + e.message) + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.router_creator: + try: + self.router_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning router with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_single_port_static(self): + """ + Tests the creation of an OpenStack instance with a single port with a static IP without a Floating IP. + """ + ip_1 = '10.55.1.100' + + port_settings = PortSettings( + name=self.port_1_name, network_name=self.pub_net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.pub_net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings], + floating_ip_settings=[FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_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) + vm_inst = inst_creator.create() + + self.assertEquals(ip_1, inst_creator.get_port_ip(self.port_1_name)) + self.assertTrue(inst_creator.vm_active(block=True)) + self.assertEquals(vm_inst, inst_creator.get_vm_inst()) + + def test_ssh_client_fip_before_active(self): + """ + Tests the ability to access a VM via SSH and a floating IP when it has been assigned prior to being active. + """ + port_settings = PortSettings( + name=self.port_1_name, network_name=self.pub_net_config.network_settings.name) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings], + floating_ip_settings=[FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_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) + vm_inst = inst_creator.create() + self.assertIsNotNone(vm_inst) + + self.assertTrue(inst_creator.vm_active(block=True)) + self.assertEquals(vm_inst, inst_creator.get_vm_inst()) + + validate_ssh_client(inst_creator) + + def test_ssh_client_fip_after_active(self): + """ + Tests the ability to access a VM via SSH and a floating IP when it has been assigned prior to being active. + """ + port_settings = PortSettings( + name=self.port_1_name, network_name=self.pub_net_config.network_settings.name) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings], + floating_ip_settings=[FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_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)) + self.assertEquals(vm_inst, inst_creator.get_vm_inst()) + + validate_ssh_client(inst_creator) + + # TODO - Determine how allowed_address_pairs is supposed to operate before continuing this test + # see http://docs.openstack.org/developer/dragonflow/specs/allowed_address_pairs.html for a functional description + # def test_allowed_address_port_access(self): + # """ + # Tests to ensure that setting allowed_address_pairs on a port functions as designed + # """ + # port_settings_1 = PortSettings( + # name=self.port_1_name + '-1', network_name=self.pub_net_config.network_settings.name) + # + # instance_settings_1 = VmInstanceSettings( + # name=self.vm_inst_name + '-1', flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings_1], + # floating_ip_settings=[FloatingIpSettings( + # name=self.floating_ip_name + '-1', port_name=port_settings_1.name, + # router_name=self.pub_net_config.router_settings.name)]) + # + # inst_creator_1 = OpenStackVmInstance( + # self.os_creds, instance_settings_1, self.image_creator.image_settings, + # keypair_settings=self.keypair_creator.keypair_settings) + # self.inst_creators.append(inst_creator_1) + # + # # block=True will force the create() method to block until the + # vm_inst_1 = inst_creator_1.create(block=True) + # self.assertIsNotNone(vm_inst_1) + # + # port_settings_1 = PortSettings( + # name=self.port_1_name + '-1', network_name=self.pub_net_config.network_settings.name) + # + # instance_settings_1 = VmInstanceSettings( + # name=self.vm_inst_name + '-1', flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings_1], + # floating_ip_settings=[FloatingIpSettings( + # name=self.floating_ip_name + '-1', port_name=port_settings_1.name, + # router_name=self.pub_net_config.router_settings.name)]) + # + # inst_creator_1 = OpenStackVmInstance( + # self.os_creds, instance_settings_1, self.image_creator.image_settings, + # keypair_settings=self.keypair_creator.keypair_settings) + # self.inst_creators.append(inst_creator_1) + # inst_creator_1.create(block=True) + # + # ip = inst_creator_1.get_port_ip(port_settings_1.name, + # subnet_name=self.pub_net_config.network_settings.subnet_settings[0].name) + # self.assertIsNotNone(ip) + # mac_addr = inst_creator_1.get_port_mac(port_settings_1.name) + # self.assertIsNotNone(mac_addr) + # + # allowed_address_pairs = [{'ip_address': ip, 'mac_address': mac_addr}] + # + # # Create VM that can be accessed by vm_inst_1 + # port_settings_2 = PortSettings( + # name=self.port_1_name + '-2', network_name=self.pub_net_config.network_settings.name, + # allowed_address_pairs=allowed_address_pairs) + # + # instance_settings_2 = VmInstanceSettings( + # name=self.vm_inst_name + '-2', flavor=self.flavor_creator.flavor_settings.name, + # port_settings=[port_settings_2]) + # + # inst_creator_2 = OpenStackVmInstance( + # self.os_creds, instance_settings_2, self.image_creator.image_settings) + # self.inst_creators.append(inst_creator_2) + # inst_creator_2.create(block=True) + # + # # Create VM that cannot be accessed by vm_inst_1 + # ip = '10.55.0.101' + # mac_addr = '0a:1b:2c:3d:4e:5f' + # invalid_address_pairs = [{'ip_address': ip, 'mac_address': mac_addr}] + # + # port_settings_3 = PortSettings( + # name=self.port_1_name + '-3', network_name=self.pub_net_config.network_settings.name, + # allowed_address_pairs=invalid_address_pairs) + # + # instance_settings_3 = VmInstanceSettings( + # name=self.vm_inst_name + '-3', flavor=self.flavor_creator.flavor_settings.name, + # port_settings=[port_settings_3]) + # + # inst_creator_3 = OpenStackVmInstance( + # self.os_creds, instance_settings_3, self.image_creator.image_settings) + # self.inst_creators.append(inst_creator_3) + # inst_creator_3.create(block=True) + # + # print 'foo' + # I expected that this feature would block/allow traffic from specific endpoints (VMs). In this case, I would expect + # inst_1 to be able to access inst_2 but not inst_3; however, they all can access each other. + # TODO - Add validation + + +class CreateInstancePortManipulationTests(OSIntegrationTestCase): + """ + Test for the CreateInstance class with a single NIC/Port where mac and IP values are manually set + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.vm_inst_name = guid + '-inst' + self.port_1_name = guid + 'port-1' + self.port_2_name = guid + 'port-2' + self.floating_ip_name = guid + 'fip1' + + # Initialize for tearDown() + self.image_creator = None + self.network_creator = None + self.flavor_creator = None + self.inst_creator = None + + self.net_config = openstack_tests.get_priv_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.network_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_set_custom_valid_ip_one_subnet(self): + """ + Tests the creation of an OpenStack instance with a single port with a static IP on a network with one subnet. + """ + ip = '10.55.0.101' + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creator.create() + + self.assertEquals(ip, self.inst_creator.get_port_ip( + self.port_1_name, subnet_name=self.net_config.network_settings.subnet_settings[0].name)) + + def test_set_custom_invalid_ip_one_subnet(self): + """ + Tests the creation of an OpenStack instance with a single port with a static IP on a network with one subnet. + """ + ip = '10.66.0.101' + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + + with self.assertRaises(Exception): + self.inst_creator.create() + + def test_set_custom_valid_mac(self): + """ + Tests the creation of an OpenStack instance with a single port where the MAC address is assigned. + """ + mac_addr = '0a:1b:2c:3d:4e:5f' + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address=mac_addr) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creator.create() + + self.assertEquals(mac_addr, self.inst_creator.get_port_mac(self.port_1_name)) + + def test_set_custom_invalid_mac(self): + """ + Tests the creation of an OpenStack instance with a single port where an invalid MAC address value is being + assigned. This should raise an Exception + """ + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address='foo') + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings) + + with self.assertRaises(Exception): + self.inst_creator.create() + + def test_set_custom_mac_and_ip(self): + """ + Tests the creation of an OpenStack instance with a single port where the IP and MAC address is assigned. + """ + ip = '10.55.0.101' + mac_addr = '0a:1b:2c:3d:4e:5f' + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, mac_address=mac_addr, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip}]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creator.create() + + self.assertEquals(ip, self.inst_creator.get_port_ip( + self.port_1_name, subnet_name=self.net_config.network_settings.subnet_settings[0].name)) + self.assertEquals(mac_addr, self.inst_creator.get_port_mac(self.port_1_name)) + + def test_set_allowed_address_pairs(self): + """ + Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set. + """ + ip = '10.55.0.101' + mac_addr = '0a:1b:2c:3d:4e:5f' + pair = {'ip_address': ip, 'mac_address': mac_addr} + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creator.create() + + port = self.inst_creator.get_port_by_name(port_settings.name) + self.assertIsNotNone(port) + self.assertIsNotNone(port['port'].get('allowed_address_pairs')) + self.assertEquals(1, len(port['port']['allowed_address_pairs'])) + validation_utils.objects_equivalent(pair, port['port']['allowed_address_pairs'][0]) + + def test_set_allowed_address_pairs_bad_mac(self): + """ + Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set with an + invalid MAC address. + """ + ip = '10.55.0.101' + mac_addr = 'foo' + pair = {'ip_address': ip, 'mac_address': mac_addr} + pairs = set() + pairs.add((ip, mac_addr)) + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + with self.assertRaises(Exception): + self.inst_creator.create() + + def test_set_allowed_address_pairs_bad_ip(self): + """ + Tests the creation of an OpenStack instance with a single port where max_allowed_address_pair is set with an + invalid MAC address. + """ + ip = 'foo' + mac_addr = '0a:1b:2c:3d:4e:5f' + pair = {'ip_address': ip, 'mac_address': mac_addr} + pairs = set() + pairs.add((ip, mac_addr)) + port_settings = PortSettings( + name=self.port_1_name, network_name=self.net_config.network_settings.name, allowed_address_pairs=[pair]) + + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=[port_settings]) + + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + with self.assertRaises(Exception): + self.inst_creator.create() + + +class CreateInstanceOnComputeHost(OSComponentTestCase): + """ + Test for the CreateInstance where one VM is deployed to each compute node + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.vm_inst_name = guid + '-inst' + self.port_base_name = guid + 'port' + + # Initialize for tearDown() + self.image_creator = None + self.flavor_creator = None + self.network_creator = None + self.inst_creators = list() + + self.priv_net_config = openstack_tests.get_priv_net_config( + net_name=guid + '-priv-net', subnet_name=guid + '-priv-subnet') + + self.os_image_settings = openstack_tests.cirros_url_image(name=guid + '-image') + + try: + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.priv_net_config.network_settings) + self.network_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.os_creds, + FlavorSettings(name=guid + '-flavor-name', ram=512, disk=1, vcpus=1)) + self.flavor_creator.create() + + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + for inst_creator in self.inst_creators: + try: + inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + def test_deploy_vm_to_each_compute_node(self): + """ + Tests the creation of OpenStack VM instances to each compute node. + """ + from snaps.openstack.utils import nova_utils + nova = nova_utils.nova_client(self.os_creds) + zones = nova_utils.get_nova_availability_zones(nova) + + # Create Instance on each server/zone + ctr = 0 + for zone in zones: + inst_name = self.vm_inst_name + '-' + zone + ctr += 1 + port_settings = PortSettings(name=self.port_base_name + '-' + str(ctr), + network_name=self.priv_net_config.network_settings.name) + + instance_settings = VmInstanceSettings( + name=inst_name, flavor=self.flavor_creator.flavor_settings.name, availability_zone=zone, + port_settings=[port_settings]) + inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings) + self.inst_creators.append(inst_creator) + inst_creator.create() + + # Validate instances to ensure they've been deployed to the correct server + index = 0 + for zone in zones: + creator = self.inst_creators[index] + self.assertTrue(creator.vm_active(block=True)) + vm = creator.get_vm_inst() + deployed_zone = vm._info['OS-EXT-AZ:availability_zone'] + deployed_host = vm._info['OS-EXT-SRV-ATTR:host'] + self.assertEquals(zone, deployed_zone + ':' + deployed_host) + index += 1 + + +class CreateInstancePubPrivNetTests(OSIntegrationTestCase): + """ + Test for the CreateInstance class with two NIC/Ports, eth0 with floating IP and eth1 w/o + These tests require a Centos image + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + # Initialize for tearDown() + self.image_creator = None + self.network_creators = list() + self.router_creators = list() + self.flavor_creator = None + self.keypair_creator = None + self.inst_creator = None + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.keypair_priv_filepath = 'tmp/' + self.guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = self.guid + '-kp' + self.vm_inst_name = self.guid + '-inst' + self.port_1_name = self.guid + '-port-1' + self.port_2_name = self.guid + '-port-2' + self.floating_ip_name = self.guid + 'fip1' + self.priv_net_config = openstack_tests.get_priv_net_config( + net_name=self.guid + '-priv-net', subnet_name=self.guid + '-priv-subnet', + router_name=self.guid + '-priv-router', external_net=self.ext_net_name) + self.pub_net_config = openstack_tests.get_pub_net_config( + net_name=self.guid + '-pub-net', subnet_name=self.guid + '-pub-subnet', + router_name=self.guid + '-pub-router', external_net=self.ext_net_name) + image_name = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.os_image_settings = openstack_tests.centos_url_image(name=image_name) + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # First network is public + self.network_creators.append(OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings)) + # Second network is private + self.network_creators.append(OpenStackNetwork(self.os_creds, self.priv_net_config.network_settings)) + for network_creator in self.network_creators: + network_creator.create() + + self.router_creators.append(OpenStackRouter(self.os_creds, self.pub_net_config.router_settings)) + self.router_creators.append(OpenStackRouter(self.os_creds, self.priv_net_config.router_settings)) + + # Create Routers + for router_creator in self.router_creators: + router_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=self.guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + + # Create Keypair + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings( + name=self.keypair_name, public_filepath=self.keypair_pub_filepath, + private_filepath=self.keypair_priv_filepath)) + self.keypair_creator.create() + except Exception as e: + self.tearDown() + raise Exception(e.message) + + def tearDown(self): + """ + Cleans the created objects + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + if self.keypair_creator: + try: + self.keypair_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning keypair with message - ' + e.message) + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + for router_creator in self.router_creators: + try: + router_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning router with message - ' + e.message) + + for network_creator in self.network_creators: + try: + network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_dual_ports_dhcp(self): + """ + Tests the creation of an OpenStack instance with a dual ports/NICs with a DHCP assigned IP. + NOTE: This test and any others that call ansible will most likely fail unless you do one of + two things: + 1. Have a ~/.ansible.cfg (or alternate means) to set host_key_checking = False + 2. Set the following environment variable in your executing shell: ANSIBLE_HOST_KEY_CHECKING=False + Should this not be performed, the creation of the host ssh key will cause your ansible calls to fail. + """ + # Create ports/NICs for instance + ports_settings = [] + ctr = 1 + for network_creator in self.network_creators: + ports_settings.append(PortSettings( + name=self.guid + '-port-' + str(ctr), + network_name=network_creator.network_settings.name)) + ctr += 1 + + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, port_settings=ports_settings, + floating_ip_settings=[FloatingIpSettings( + name=self.floating_ip_name, port_name=self.port_1_name, + router_name=self.pub_net_config.router_settings.name)]) + + self.inst_creator = OpenStackVmInstance( + self.os_creds, instance_settings, self.image_creator.image_settings, + keypair_settings=self.keypair_creator.keypair_settings) + + vm_inst = self.inst_creator.create(block=True) + + self.assertEquals(vm_inst, self.inst_creator.get_vm_inst()) + + # Effectively blocks until VM has been properly activated + self.assertTrue(self.inst_creator.vm_active(block=True)) + + # Effectively blocks until VM's ssh port has been opened + self.assertTrue(self.inst_creator.vm_ssh_active(block=True)) + + self.inst_creator.config_nics() + + # TODO - *** ADD VALIDATION HERE *** + # TODO - Add validation that both floating IPs work + # TODO - Add tests where only one NIC has a floating IP + # TODO - Add tests where one attempts to place a floating IP on a network/router without an external gateway + + +class InstanceSecurityGroupTests(OSIntegrationTestCase): + """ + Tests that include, add, and remove security groups from VM instances + """ + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.vm_inst_name = self.guid + '-inst' + self.nova = nova_utils.nova_client(self.os_creds) + self.os_image_settings = openstack_tests.cirros_url_image(name=self.guid + '-image') + + self.keypair_priv_filepath = 'tmp/' + self.guid + self.keypair_pub_filepath = self.keypair_priv_filepath + '.pub' + self.keypair_name = self.guid + '-kp' + self.vm_inst_name = self.guid + '-inst' + self.port_1_name = self.guid + 'port-1' + self.port_2_name = self.guid + 'port-2' + self.floating_ip_name = self.guid + 'fip1' + + self.pub_net_config = openstack_tests.get_pub_net_config( + net_name=self.guid + '-pub-net', subnet_name=self.guid + '-pub-subnet', + router_name=self.guid + '-pub-router', external_net=self.ext_net_name) + + # Initialize for tearDown() + self.image_creator = None + self.keypair_creator = None + self.flavor_creator = None + self.network_creator = None + self.router_creator = None + self.inst_creator = None + self.sec_grp_creators = list() + + try: + # Create Image + self.image_creator = OpenStackImage(self.os_creds, self.os_image_settings) + self.image_creator.create() + + # Create Network + self.network_creator = OpenStackNetwork(self.os_creds, self.pub_net_config.network_settings) + self.network_creator.create() + + # Create Router + self.router_creator = OpenStackRouter(self.os_creds, self.pub_net_config.router_settings) + self.router_creator.create() + + # Create Flavor + self.flavor_creator = OpenStackFlavor( + self.admin_os_creds, + FlavorSettings(name=self.guid + '-flavor-name', ram=2048, disk=10, vcpus=2)) + self.flavor_creator.create() + + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings( + name=self.keypair_name, public_filepath=self.keypair_pub_filepath, + private_filepath=self.keypair_priv_filepath)) + self.keypair_creator.create() + except Exception as e: + self.tearDown() + raise e + + def tearDown(self): + """ + Cleans the created object + """ + if self.inst_creator: + try: + self.inst_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning VM instance with message - ' + e.message) + + for sec_grp_creator in self.sec_grp_creators: + try: + sec_grp_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning security group with message - ' + e.message) + + if self.keypair_creator: + try: + self.keypair_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning keypair with message - ' + e.message) + + if os.path.isfile(self.keypair_pub_filepath): + os.remove(self.keypair_pub_filepath) + + if os.path.isfile(self.keypair_priv_filepath): + os.remove(self.keypair_priv_filepath) + + if self.flavor_creator: + try: + self.flavor_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning flavor with message - ' + e.message) + + if self.router_creator: + try: + self.router_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning router with message - ' + e.message) + + if self.network_creator: + try: + self.network_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning network with message - ' + e.message) + + if self.image_creator: + try: + self.image_creator.clean() + except Exception as e: + logger.error('Unexpected exception cleaning image with message - ' + e.message) + + super(self.__class__, self).__clean__() + + def test_add_security_group(self): + """ + Tests the addition of a security group created after the instance. + """ + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + self.sec_grp_creators.append(sec_grp_creator) + + # Check that group has not been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + # Add security group to instance after activated + self.inst_creator.add_security_group(sec_grp) + + # Validate that security group has been added + self.assertTrue(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + def test_add_invalid_security_group(self): + """ + Tests the addition of a security group that no longer exists. + """ + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + sec_grp_creator.clean() + self.sec_grp_creators.append(sec_grp_creator) + + # Check that group has not been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + # Add security group to instance after activated + self.assertFalse(self.inst_creator.add_security_group(sec_grp)) + + # Validate that security group has been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + def test_remove_security_group(self): + """ + Tests the removal of a security group created before and added to the instance. + """ + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + self.sec_grp_creators.append(sec_grp_creator) + + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, + security_group_names=[sec_grp_settings.name]) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Check that group has been added + self.assertTrue(inst_has_sec_grp(vm_inst, sec_grp_settings.name)) + + # Add security group to instance after activated + self.assertTrue(self.inst_creator.remove_security_group(sec_grp)) + + # Validate that security group has been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + def test_remove_security_group_never_added(self): + """ + Tests the removal of a security group that was never added in the first place. + """ + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + self.sec_grp_creators.append(sec_grp_creator) + + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Check that group has been added + self.assertFalse(inst_has_sec_grp(vm_inst, sec_grp_settings.name)) + + # Add security group to instance after activated + self.assertFalse(self.inst_creator.remove_security_group(sec_grp)) + + # Validate that security group has been added + self.assertFalse(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + def test_add_same_security_group(self): + """ + Tests the addition of a security group created before add added to the instance. + """ + # Create security group object to add to instance + sec_grp_settings = SecurityGroupSettings(name=self.guid + '-name', description='hello group') + sec_grp_creator = OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + sec_grp = sec_grp_creator.create() + self.sec_grp_creators.append(sec_grp_creator) + + # Create instance + instance_settings = VmInstanceSettings( + name=self.vm_inst_name, flavor=self.flavor_creator.flavor_settings.name, + security_group_names=[sec_grp_settings.name]) + self.inst_creator = OpenStackVmInstance(self.os_creds, instance_settings, self.image_creator.image_settings) + vm_inst = self.inst_creator.create() + self.assertIsNotNone(vm_inst) + + # Check that group has been added + self.assertTrue(inst_has_sec_grp(vm_inst, sec_grp_settings.name)) + + # Add security group to instance after activated + self.assertTrue(self.inst_creator.add_security_group(sec_grp)) + + # Validate that security group has been added + self.assertTrue(inst_has_sec_grp(self.inst_creator.get_vm_inst(), sec_grp_settings.name)) + + +def inst_has_sec_grp(vm_inst, sec_grp_name): + """ + Returns true if instance has a security group of a given name + :return: + """ + if not hasattr(vm_inst, 'security_groups'): + return False + + found = False + for sec_grp_dict in vm_inst.security_groups: + if sec_grp_name in sec_grp_dict['name']: + found = True + break + return found + + +def validate_ssh_client(instance_creator): + """ + Returns True if instance_creator returns an SSH client that is valid + :param instance_creator: the object responsible for creating the VM instance + :return: T/F + """ + ssh_active = instance_creator.vm_ssh_active(block=True) + + if ssh_active: + ssh_client = instance_creator.ssh_client() + if ssh_client: + out = ssh_client.exec_command('pwd')[1] + else: + return False + + channel = out.channel + in_buffer = channel.in_buffer + pwd_out = in_buffer.read(1024) + if not pwd_out or len(pwd_out) < 10: + return False + return True + + return False diff --git a/snaps/openstack/tests/create_keypairs_tests.py b/snaps/openstack/tests/create_keypairs_tests.py new file mode 100644 index 0000000..e4409a9 --- /dev/null +++ b/snaps/openstack/tests/create_keypairs_tests.py @@ -0,0 +1,203 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import uuid +import unittest + +from Crypto.PublicKey import RSA + +from snaps.openstack.create_keypairs import KeypairSettings, OpenStackKeypair +from snaps.openstack.utils import nova_utils +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase + +__author__ = 'spisarski' + + +class KeypairSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the KeypairSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + KeypairSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + KeypairSettings(config=dict()) + + def test_name_only(self): + settings = KeypairSettings(name='foo') + self.assertEquals('foo', settings.name) + self.assertIsNone(settings.public_filepath) + self.assertIsNone(settings.private_filepath) + + def test_config_with_name_only(self): + settings = KeypairSettings(config={'name': 'foo'}) + self.assertEquals('foo', settings.name) + self.assertIsNone(settings.public_filepath) + self.assertIsNone(settings.private_filepath) + + def test_name_pub_only(self): + settings = KeypairSettings(name='foo', public_filepath='/foo/bar.pub') + self.assertEquals('foo', settings.name) + self.assertEquals('/foo/bar.pub', settings.public_filepath) + self.assertIsNone(settings.private_filepath) + + def test_config_with_name_pub_only(self): + settings = KeypairSettings(config={'name': 'foo', 'public_filepath': '/foo/bar.pub'}) + self.assertEquals('foo', settings.name) + self.assertEquals('/foo/bar.pub', settings.public_filepath) + self.assertIsNone(settings.private_filepath) + + def test_name_priv_only(self): + settings = KeypairSettings(name='foo', private_filepath='/foo/bar') + self.assertEquals('foo', settings.name) + self.assertIsNone(settings.public_filepath) + self.assertEquals('/foo/bar', settings.private_filepath) + + def test_config_with_name_priv_only(self): + settings = KeypairSettings(config={'name': 'foo', 'private_filepath': '/foo/bar'}) + self.assertEquals('foo', settings.name) + self.assertIsNone(settings.public_filepath) + self.assertEquals('/foo/bar', settings.private_filepath) + + def test_all(self): + settings = KeypairSettings(name='foo', public_filepath='/foo/bar.pub', private_filepath='/foo/bar') + self.assertEquals('foo', settings.name) + self.assertEquals('/foo/bar.pub', settings.public_filepath) + self.assertEquals('/foo/bar', settings.private_filepath) + + def test_config_all(self): + settings = KeypairSettings(config={'name': 'foo', 'public_filepath': '/foo/bar.pub', + 'private_filepath': '/foo/bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('/foo/bar.pub', settings.public_filepath) + self.assertEquals('/foo/bar', settings.private_filepath) + + +class CreateKeypairsTests(OSIntegrationTestCase): + """ + Tests for the OpenStackKeypair class + """ + + def setUp(self): + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.priv_file_path = 'tmp/' + guid + self.pub_file_path = self.priv_file_path + '.pub' + self.nova = nova_utils.nova_client(self.os_creds) + self.keypair_name = guid + + self.keypair_creator = None + + def tearDown(self): + """ + Cleanup of created keypair + """ + if self.keypair_creator: + self.keypair_creator.clean() + + try: + os.remove(self.pub_file_path) + except: + pass + + try: + os.remove(self.priv_file_path) + except: + pass + + super(self.__class__, self).__clean__() + + def test_create_keypair_only(self): + """ + Tests the creation of a generated keypair without saving to file + :return: + """ + self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(name=self.keypair_name)) + self.keypair_creator.create() + + keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair()) + self.assertEquals(self.keypair_creator.get_keypair(), keypair) + + def test_create_delete_keypair(self): + """ + Tests the creation then deletion of an OpenStack keypair to ensure clean() does not raise an Exception. + """ + # Create Image + self.keypair_creator = OpenStackKeypair(self.os_creds, KeypairSettings(name=self.keypair_name)) + created_keypair = self.keypair_creator.create() + self.assertIsNotNone(created_keypair) + + # Delete Image manually + nova_utils.delete_keypair(self.nova, created_keypair) + + self.assertIsNone(nova_utils.get_keypair_by_name(self.nova, self.keypair_name)) + + # Must not throw an exception when attempting to cleanup non-existent image + self.keypair_creator.clean() + self.assertIsNone(self.keypair_creator.get_keypair()) + + def test_create_keypair_save_pub_only(self): + """ + Tests the creation of a generated keypair and saves the public key only + :return: + """ + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings(name=self.keypair_name, public_filepath=self.pub_file_path)) + self.keypair_creator.create() + + keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair()) + self.assertEquals(self.keypair_creator.get_keypair(), keypair) + + file_key = open(os.path.expanduser(self.pub_file_path)).read() + self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key) + + def test_create_keypair_save_both(self): + """ + Tests the creation of a generated keypair and saves both private and public key files[ + :return: + """ + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings(name=self.keypair_name, public_filepath=self.pub_file_path, + private_filepath=self.priv_file_path)) + self.keypair_creator.create() + + keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair()) + self.assertEquals(self.keypair_creator.get_keypair(), keypair) + + file_key = open(os.path.expanduser(self.pub_file_path)).read() + self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key) + + self.assertTrue(os.path.isfile(self.priv_file_path)) + + def test_create_keypair_from_file(self): + """ + Tests the creation of an existing public keypair from a file + :return: + """ + keys = RSA.generate(1024) + nova_utils.save_keys_to_files(keys=keys, pub_file_path=self.pub_file_path) + self.keypair_creator = OpenStackKeypair( + self.os_creds, KeypairSettings(name=self.keypair_name, public_filepath=self.pub_file_path)) + self.keypair_creator.create() + + keypair = nova_utils.keypair_exists(self.nova, self.keypair_creator.get_keypair()) + self.assertEquals(self.keypair_creator.get_keypair(), keypair) + + file_key = open(os.path.expanduser(self.pub_file_path)).read() + self.assertEquals(self.keypair_creator.get_keypair().public_key, file_key) diff --git a/snaps/openstack/tests/create_network_tests.py b/snaps/openstack/tests/create_network_tests.py new file mode 100644 index 0000000..a2b17f8 --- /dev/null +++ b/snaps/openstack/tests/create_network_tests.py @@ -0,0 +1,533 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest + +from snaps.openstack.create_network import OpenStackNetwork, NetworkSettings, SubnetSettings, PortSettings +from snaps.openstack import create_router +from snaps.openstack.tests import openstack_tests +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase, OSComponentTestCase +from snaps.openstack.utils import neutron_utils +from snaps.openstack.utils.tests import neutron_utils_tests + +__author__ = 'spisarski' + + +class NetworkSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the NetworkSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + NetworkSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + NetworkSettings(config=dict()) + + def test_name_only(self): + settings = NetworkSettings(name='foo') + self.assertEquals('foo', settings.name) + self.assertTrue(settings.admin_state_up) + self.assertIsNone(settings.shared) + self.assertIsNone(settings.project_name) + self.assertFalse(settings.external) + self.assertIsNone(settings.network_type) + self.assertEquals(0, len(settings.subnet_settings)) + + def test_config_with_name_only(self): + settings = NetworkSettings(config={'name': 'foo'}) + self.assertEquals('foo', settings.name) + self.assertTrue(settings.admin_state_up) + self.assertIsNone(settings.shared) + self.assertIsNone(settings.project_name) + self.assertFalse(settings.external) + self.assertIsNone(settings.network_type) + self.assertEquals(0, len(settings.subnet_settings)) + + def test_all(self): + sub_settings = SubnetSettings(name='foo-subnet', cidr='10.0.0.0/24') + settings = NetworkSettings(name='foo', admin_state_up=False, shared=True, project_name='bar', external=True, + network_type='flat', physical_network='phy', subnet_settings=[sub_settings]) + self.assertEquals('foo', settings.name) + self.assertFalse(settings.admin_state_up) + self.assertTrue(settings.shared) + self.assertEquals('bar', settings.project_name) + self.assertTrue(settings.external) + self.assertEquals('flat', settings.network_type) + self.assertEquals('phy', settings.physical_network) + self.assertEquals(1, len(settings.subnet_settings)) + self.assertEquals('foo-subnet', settings.subnet_settings[0].name) + + def test_config_all(self): + settings = NetworkSettings(config={'name': 'foo', 'admin_state_up': False, 'shared': True, + 'project_name': 'bar', 'external': True, 'network_type': 'flat', + 'physical_network': 'phy', + 'subnets': + [{'subnet': {'name': 'foo-subnet', 'cidr': '10.0.0.0/24'}}]}) + self.assertEquals('foo', settings.name) + self.assertFalse(settings.admin_state_up) + self.assertTrue(settings.shared) + self.assertEquals('bar', settings.project_name) + self.assertTrue(settings.external) + self.assertEquals('flat', settings.network_type) + self.assertEquals('phy', settings.physical_network) + self.assertEquals(1, len(settings.subnet_settings)) + self.assertEquals('foo-subnet', settings.subnet_settings[0].name) + + +class SubnetSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the SubnetSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + SubnetSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + SubnetSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + SubnetSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + SubnetSettings(config={'name': 'foo'}) + + def test_name_cidr_only(self): + settings = SubnetSettings(name='foo', cidr='10.0.0.0/24') + self.assertEquals('foo', settings.name) + self.assertEquals('10.0.0.0/24', settings.cidr) + self.assertEquals(4, settings.ip_version) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.start) + self.assertIsNone(settings.end) + self.assertIsNone(settings.enable_dhcp) + self.assertEquals(1, len(settings.dns_nameservers)) + self.assertEquals('8.8.8.8', settings.dns_nameservers[0]) + self.assertIsNone(settings.host_routes) + self.assertIsNone(settings.destination) + self.assertIsNone(settings.nexthop) + self.assertIsNone(settings.ipv6_ra_mode) + self.assertIsNone(settings.ipv6_address_mode) + + def test_config_with_name_cidr_only(self): + settings = SubnetSettings(config={'name': 'foo', 'cidr': '10.0.0.0/24'}) + self.assertEquals('foo', settings.name) + self.assertEquals('10.0.0.0/24', settings.cidr) + self.assertEquals(4, settings.ip_version) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.start) + self.assertIsNone(settings.end) + self.assertIsNone(settings.gateway_ip) + self.assertIsNone(settings.enable_dhcp) + self.assertEquals(1, len(settings.dns_nameservers)) + self.assertEquals('8.8.8.8', settings.dns_nameservers[0]) + self.assertIsNone(settings.host_routes) + self.assertIsNone(settings.destination) + self.assertIsNone(settings.nexthop) + self.assertIsNone(settings.ipv6_ra_mode) + self.assertIsNone(settings.ipv6_address_mode) + + def test_all(self): + host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'} + settings = SubnetSettings(name='foo', cidr='10.0.0.0/24', ip_version=6, project_name='bar-project', + start='10.0.0.2', end='10.0.0.101', gateway_ip='10.0.0.1', enable_dhcp=False, + dns_nameservers=['8.8.8.8'], host_routes=[host_routes], destination='dest', + nexthop='hop', ipv6_ra_mode='dhcpv6-stateful', ipv6_address_mode='slaac') + self.assertEquals('foo', settings.name) + self.assertEquals('10.0.0.0/24', settings.cidr) + self.assertEquals(6, settings.ip_version) + self.assertEquals('bar-project', settings.project_name) + self.assertEquals('10.0.0.2', settings.start) + self.assertEquals('10.0.0.101', settings.end) + self.assertEquals('10.0.0.1', settings.gateway_ip) + self.assertEquals(False, settings.enable_dhcp) + self.assertEquals(1, len(settings.dns_nameservers)) + self.assertEquals('8.8.8.8', settings.dns_nameservers[0]) + self.assertEquals(1, len(settings.host_routes)) + self.assertEquals(host_routes, settings.host_routes[0]) + self.assertEquals('dest', settings.destination) + self.assertEquals('hop', settings.nexthop) + self.assertEquals('dhcpv6-stateful', settings.ipv6_ra_mode) + self.assertEquals('slaac', settings.ipv6_address_mode) + + def test_config_all(self): + host_routes = {'destination': '0.0.0.0/0', 'nexthop': '123.456.78.9'} + settings = SubnetSettings(config={'name': 'foo', 'cidr': '10.0.0.0/24', 'ip_version': 6, + 'project_name': 'bar-project', 'start': '10.0.0.2', 'end': '10.0.0.101', + 'gateway_ip': '10.0.0.1', 'enable_dhcp': False, + 'dns_nameservers': ['8.8.8.8'], 'host_routes': [host_routes], + 'destination': 'dest', 'nexthop': 'hop', 'ipv6_ra_mode': 'dhcpv6-stateful', + 'ipv6_address_mode': 'slaac'}) + self.assertEquals('foo', settings.name) + self.assertEquals('10.0.0.0/24', settings.cidr) + self.assertEquals(6, settings.ip_version) + self.assertEquals('bar-project', settings.project_name) + self.assertEquals('10.0.0.2', settings.start) + self.assertEquals('10.0.0.101', settings.end) + self.assertEquals('10.0.0.1', settings.gateway_ip) + self.assertEquals(False, settings.enable_dhcp) + self.assertEquals(1, len(settings.dns_nameservers)) + self.assertEquals('8.8.8.8', settings.dns_nameservers[0]) + self.assertEquals(1, len(settings.host_routes)) + self.assertEquals(host_routes, settings.host_routes[0]) + self.assertEquals('dest', settings.destination) + self.assertEquals('hop', settings.nexthop) + self.assertEquals('dhcpv6-stateful', settings.ipv6_ra_mode) + self.assertEquals('slaac', settings.ipv6_address_mode) + + +class PortSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the PortSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + PortSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + PortSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + PortSettings(name='foo') + + def test_config_name_only(self): + with self.assertRaises(Exception): + PortSettings(config={'name': 'foo'}) + + def test_name_netname_only(self): + settings = PortSettings(name='foo', network_name='bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.network_name) + self.assertTrue(settings.admin_state_up) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.mac_address) + self.assertIsNone(settings.ip_addrs) + self.assertIsNone(settings.fixed_ips) + self.assertIsNone(settings.security_groups) + self.assertIsNone(settings.allowed_address_pairs) + self.assertIsNone(settings.opt_value) + self.assertIsNone(settings.opt_name) + self.assertIsNone(settings.device_owner) + self.assertIsNone(settings.device_id) + + def test_config_with_name_netname_only(self): + settings = PortSettings(config={'name': 'foo', 'network_name': 'bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.network_name) + self.assertTrue(settings.admin_state_up) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.mac_address) + self.assertIsNone(settings.ip_addrs) + self.assertIsNone(settings.fixed_ips) + self.assertIsNone(settings.security_groups) + self.assertIsNone(settings.allowed_address_pairs) + self.assertIsNone(settings.opt_value) + self.assertIsNone(settings.opt_name) + self.assertIsNone(settings.device_owner) + self.assertIsNone(settings.device_id) + + def test_all(self): + ip_addrs = [{'subnet_name', 'foo-sub', 'ip', '10.0.0.10'}] + fixed_ips = {'sub_id', '10.0.0.10'} + allowed_address_pairs = {'10.0.0.101', '1234.5678'} + + settings = PortSettings(name='foo', network_name='bar', admin_state_up=False, project_name='foo-project', + mac_address='1234', ip_addrs=ip_addrs, fixed_ips=fixed_ips, + security_groups=['foo_grp_id'], allowed_address_pairs=allowed_address_pairs, + opt_value='opt value', opt_name='opt name', device_owner='owner', + device_id='device number') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.network_name) + self.assertFalse(settings.admin_state_up) + self.assertEquals('foo-project', settings.project_name) + self.assertEquals('1234', settings.mac_address) + self.assertEquals(ip_addrs, settings.ip_addrs) + self.assertEquals(fixed_ips, settings.fixed_ips) + self.assertEquals(1, len(settings.security_groups)) + self.assertEquals('foo_grp_id', settings.security_groups[0]) + self.assertEquals(allowed_address_pairs, settings.allowed_address_pairs) + self.assertEquals('opt value', settings.opt_value) + self.assertEquals('opt name', settings.opt_name) + self.assertEquals('owner', settings.device_owner) + self.assertEquals('device number', settings.device_id) + + def test_config_all(self): + ip_addrs = [{'subnet_name', 'foo-sub', 'ip', '10.0.0.10'}] + fixed_ips = {'sub_id', '10.0.0.10'} + allowed_address_pairs = {'10.0.0.101', '1234.5678'} + + settings = PortSettings(config={'name': 'foo', 'network_name': 'bar', 'admin_state_up': False, + 'project_name': 'foo-project', 'mac_address': '1234', 'ip_addrs': ip_addrs, + 'fixed_ips': fixed_ips, 'security_groups': ['foo_grp_id'], + 'allowed_address_pairs': allowed_address_pairs, 'opt_value': 'opt value', + 'opt_name': 'opt name', 'device_owner': 'owner', 'device_id': 'device number'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.network_name) + self.assertFalse(settings.admin_state_up) + self.assertEquals('foo-project', settings.project_name) + self.assertEquals('1234', settings.mac_address) + self.assertEquals(ip_addrs, settings.ip_addrs) + self.assertEquals(fixed_ips, settings.fixed_ips) + self.assertEquals(1, len(settings.security_groups)) + self.assertEquals('foo_grp_id', settings.security_groups[0]) + self.assertEquals(allowed_address_pairs, settings.allowed_address_pairs) + self.assertEquals('opt value', settings.opt_value) + self.assertEquals('opt name', settings.opt_name) + self.assertEquals('owner', settings.device_owner) + self.assertEquals('device number', settings.device_id) + + +class CreateNetworkSuccessTests(OSIntegrationTestCase): + """ + Test for the CreateNework class defined in create_nework.py + """ + + def setUp(self): + """ + Sets up object for test + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + + self.neutron = neutron_utils.neutron_client(self.os_creds) + + # Initialize for cleanup + self.net_creator = None + self.router_creator = None + self.neutron = neutron_utils.neutron_client(self.os_creds) + + def tearDown(self): + """ + Cleans the network + """ + if self.router_creator: + self.router_creator.clean() + + if self.net_creator: + if len(self.net_creator.get_subnets()) > 0: + # Validate subnet has been deleted + neutron_utils_tests.validate_subnet( + self.neutron, self.net_creator.network_settings.subnet_settings[0].name, + self.net_creator.network_settings.subnet_settings[0].cidr, False) + + if self.net_creator.get_network(): + # Validate network has been deleted + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, + False) + self.net_creator.clean() + + super(self.__class__, self).__clean__() + + def test_create_network_without_router(self): + """ + Tests the creation of an OpenStack network without a router. + """ + # Create Nework + self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator.create() + + # Validate network was created + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True) + + # Validate subnets + neutron_utils_tests.validate_subnet( + self.neutron, self.net_creator.network_settings.subnet_settings[0].name, + self.net_creator.network_settings.subnet_settings[0].cidr, True) + + def test_create_delete_network(self): + """ + Tests the creation of an OpenStack network, it's deletion, then cleanup. + """ + # Create Nework + self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator.create() + + # Validate network was created + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True) + + neutron_utils.delete_network(self.neutron, self.net_creator.get_network()) + self.assertIsNone(neutron_utils.get_network(self.neutron, self.net_creator.network_settings.name)) + + # This shall not throw an exception here + self.net_creator.clean() + + def test_create_network_with_router(self): + """ + Tests the creation of an OpenStack network with a router. + """ + # Create Network + self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator.create() + + # Create Router + self.router_creator = create_router.OpenStackRouter(self.os_creds, self.net_config.router_settings) + self.router_creator.create() + + # Validate network was created + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, True) + + # Validate subnets + neutron_utils_tests.validate_subnet( + self.neutron, self.net_creator.network_settings.subnet_settings[0].name, + self.net_creator.network_settings.subnet_settings[0].cidr, True) + + # Validate routers + neutron_utils_tests.validate_router(self.neutron, self.router_creator.router_settings.name, True) + + neutron_utils_tests.validate_interface_router(self.router_creator.get_internal_router_interface(), + self.router_creator.get_router(), + self.net_creator.get_subnets()[0]) + + def test_create_networks_same_name(self): + """ + Tests the creation of an OpenStack network and ensures that the OpenStackNetwork object will not create + a second. + """ + # Create Nework + self.net_creator = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator.create() + + self.net_creator2 = OpenStackNetwork(self.os_creds, self.net_config.network_settings) + self.net_creator2.create() + + self.assertEquals(self.net_creator.get_network()['network']['id'], + self.net_creator2.get_network()['network']['id']) + + +class CreateNetworkTypeTests(OSComponentTestCase): + """ + Test for the CreateNework class defined in create_nework.py for testing creating networks of different types + """ + + def setUp(self): + """ + Sets up object for test + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet') + + self.neutron = neutron_utils.neutron_client(self.os_creds) + + # Initialize for cleanup + self.net_creator = None + self.neutron = neutron_utils.neutron_client(self.os_creds) + + def tearDown(self): + """ + Cleans the network + """ + if self.net_creator: + if len(self.net_creator.get_subnets()) > 0: + # Validate subnet has been deleted + neutron_utils_tests.validate_subnet( + self.neutron, self.net_creator.network_settings.subnet_settings[0].name, + self.net_creator.network_settings.subnet_settings[0].cidr, False) + + if self.net_creator.get_network(): + # Validate network has been deleted + neutron_utils_tests.validate_network(self.neutron, self.net_creator.network_settings.name, + False) + self.net_creator.clean() + # TODO - determine why this is not working on Newton + # - Unable to create the network. No tenant network is available for allocation. + # def test_create_network_type_vlan(self): + # """ + # Tests the creation of an OpenStack network of type vlan. + # """ + # # Create Network + # network_type = 'vlan' + # net_settings = NetworkSettings(name=self.net_config.network_settings.name, + # subnet_settings=self.net_config.network_settings.subnet_settings, + # network_type=network_type) + # + # # When setting the network_type, creds must be admin + # self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + # network = self.net_creator.create() + # + # # Validate network was created + # neutron_utils_tests.validate_network(self.neutron, net_settings.name, True) + # + # self.assertEquals(network_type, network['network']['provider:network_type']) + + def test_create_network_type_vxlan(self): + """ + Tests the creation of an OpenStack network of type vxlan. + """ + # Create Network + network_type = 'vxlan' + net_settings = NetworkSettings(name=self.net_config.network_settings.name, + subnet_settings=self.net_config.network_settings.subnet_settings, + network_type=network_type) + + # When setting the network_type, creds must be admin + self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + network = self.net_creator.create() + + # Validate network was created + neutron_utils_tests.validate_network(self.neutron, net_settings.name, True) + + self.assertEquals(network_type, network['network']['provider:network_type']) + + # TODO - determine what value we need to place into physical_network + # - Do not know what vaule to place into the 'physical_network' setting. + # def test_create_network_type_flat(self): + # """ + # Tests the creation of an OpenStack network of type flat. + # """ + # # Create Network + # network_type = 'flat' + # + # # Unable to find documentation on how to find a value that will work here. + # # https://visibilityspots.org/vlan-flat-neutron-provider.html + # # https://community.rackspace.com/products/f/45/t/4225 + # # It appears that this may be due to how OPNFV is configuring OpenStack. + # physical_network = '???' + # net_settings = NetworkSettings(name=self.net_config.network_settings.name, + # subnet_settings=self.net_config.network_settings.subnet_settings, + # network_type=network_type, physical_network=physical_network) + # self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + # network = self.net_creator.create() + # + # # Validate network was created + # neutron_utils_tests.validate_network(self.neutron, net_settings.name, True) + # + # self.assertEquals(network_type, network['network']['provider:network_type']) + + def test_create_network_type_foo(self): + """ + Tests the creation of an OpenStack network of type foo which should raise an exception. + """ + # Create Network + network_type = 'foo' + net_settings = NetworkSettings(name=self.net_config.network_settings.name, + subnet_settings=self.net_config.network_settings.subnet_settings, + network_type=network_type) + self.net_creator = OpenStackNetwork(self.os_creds, net_settings) + with self.assertRaises(Exception): + self.net_creator.create() diff --git a/snaps/openstack/tests/create_project_tests.py b/snaps/openstack/tests/create_project_tests.py new file mode 100644 index 0000000..9d53467 --- /dev/null +++ b/snaps/openstack/tests/create_project_tests.py @@ -0,0 +1,228 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest + +from snaps.openstack.create_project import OpenStackProject, ProjectSettings +from snaps.openstack.create_security_group import OpenStackSecurityGroup +from snaps.openstack.create_security_group import SecurityGroupSettings +from snaps.openstack.create_user import OpenStackUser +from snaps.openstack.create_user import UserSettings +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + + +class ProjectSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the ProjectSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + ProjectSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + ProjectSettings(config=dict()) + + def test_name_only(self): + settings = ProjectSettings(name='foo') + self.assertEquals('foo', settings.name) + self.assertEquals('default', settings.domain) + self.assertIsNone(settings.description) + self.assertTrue(settings.enabled) + + def test_config_with_name_only(self): + settings = ProjectSettings(config={'name': 'foo'}) + self.assertEquals('foo', settings.name) + self.assertEquals('default', settings.domain) + self.assertIsNone(settings.description) + self.assertTrue(settings.enabled) + + def test_all(self): + settings = ProjectSettings(name='foo', domain='bar', description='foobar', enabled=False) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.domain) + self.assertEquals('foobar', settings.description) + self.assertFalse(settings.enabled) + + def test_config_all(self): + settings = ProjectSettings(config={'name': 'foo', 'domain': 'bar', 'description': 'foobar', 'enabled': False}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.domain) + self.assertEquals('foobar', settings.description) + self.assertFalse(settings.enabled) + + +class CreateProjectSuccessTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = str(uuid.uuid4())[:-19] + guid = self.__class__.__name__ + '-' + guid + self.project_settings = ProjectSettings(name=guid + '-name') + + self.keystone = keystone_utils.keystone_client(self.os_creds) + + # Initialize for cleanup + self.project_creator = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.project_creator: + self.project_creator.clean() + + def test_create_project(self): + """ + Tests the creation of an OpenStack project. + """ + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + retrieved_project = keystone_utils.get_project(keystone=self.keystone, project_name=self.project_settings.name) + self.assertIsNotNone(retrieved_project) + self.assertEquals(created_project, retrieved_project) + + def test_create_project_2x(self): + """ + Tests the creation of an OpenStack project twice to ensure it only creates one. + """ + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + retrieved_project = keystone_utils.get_project(keystone=self.keystone, project_name=self.project_settings.name) + self.assertIsNotNone(retrieved_project) + self.assertEquals(created_project, retrieved_project) + + project2 = OpenStackProject(self.os_creds, self.project_settings).create() + self.assertEquals(retrieved_project, project2) + + def test_create_delete_project(self): + """ + Tests the creation of an OpenStack project, it's deletion, then cleanup. + """ + # Create Image + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + keystone_utils.delete_project(self.keystone, created_project) + + self.project_creator.clean() + + self.assertIsNone(self.project_creator.get_project()) + + # TODO - Expand tests + + +class CreateProjectUserTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = str(uuid.uuid4())[:-19] + self.guid = self.__class__.__name__ + '-' + guid + self.project_settings = ProjectSettings(name=self.guid + '-name') + + self.keystone = keystone_utils.keystone_client(self.os_creds) + + # Initialize for cleanup + self.project_creator = None + self.user_creators = list() + + self.sec_grp_creators = list() + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + for sec_grp_creator in self.sec_grp_creators: + sec_grp_creator.clean() + + for user_creator in self.user_creators: + user_creator.clean() + + if self.project_creator: + self.project_creator.clean() + + def test_create_project_sec_grp_one_user(self): + """ + Tests the creation of an OpenStack object to a project with a new users and to create a security group + """ + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + user_creator = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user', password=self.guid)) + self.project_creator.assoc_user(user_creator.create()) + self.user_creators.append(user_creator) + + sec_grp_os_creds = user_creator.get_os_creds(self.project_creator.get_project().name) + sec_grp_creator = OpenStackSecurityGroup( + sec_grp_os_creds, SecurityGroupSettings(name=self.guid + '-name', description='hello group')) + sec_grp = sec_grp_creator.create() + self.assertIsNotNone(sec_grp) + self.sec_grp_creators.append(sec_grp_creator) + + if self.keystone.version == keystone_utils.V2_VERSION: + self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['tenant_id']) + else: + self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['project_id']) + + def test_create_project_sec_grp_two_users(self): + """ + Tests the creation of an OpenStack object to a project with two new users and use each user to create a + security group + """ + self.project_creator = OpenStackProject(self.os_creds, self.project_settings) + created_project = self.project_creator.create() + self.assertIsNotNone(created_project) + + user_creator_1 = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user1', password=self.guid)) + self.project_creator.assoc_user(user_creator_1.create()) + self.user_creators.append(user_creator_1) + + user_creator_2 = OpenStackUser(self.os_creds, UserSettings(name=self.guid + '-user2', password=self.guid)) + self.project_creator.assoc_user(user_creator_2.create()) + self.user_creators.append(user_creator_2) + + ctr = 0 + for user_creator in self.user_creators: + ctr += 1 + sec_grp_os_creds = user_creator.get_os_creds(self.project_creator.get_project().name) + + sec_grp_creator = OpenStackSecurityGroup( + sec_grp_os_creds, SecurityGroupSettings(name=self.guid + '-name', description='hello group')) + sec_grp = sec_grp_creator.create() + self.assertIsNotNone(sec_grp) + self.sec_grp_creators.append(sec_grp_creator) + self.assertEquals(self.project_creator.get_project().id, sec_grp['security_group']['tenant_id']) diff --git a/snaps/openstack/tests/create_router_tests.py b/snaps/openstack/tests/create_router_tests.py new file mode 100644 index 0000000..3e22714 --- /dev/null +++ b/snaps/openstack/tests/create_router_tests.py @@ -0,0 +1,264 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import uuid + +from snaps.openstack import create_network +from snaps.openstack import create_router +from snaps.openstack.create_network import NetworkSettings +from snaps.openstack.create_network import OpenStackNetwork +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase +from snaps.openstack.create_router import RouterSettings +from snaps.openstack.utils import neutron_utils + +__author__ = 'mmakati' + +cidr1 = '10.200.201.0/24' +cidr2 = '10.200.202.0/24' +static_gateway_ip1 = '10.200.201.1' +static_gateway_ip2 = '10.200.202.1' + + +class CreateRouterSuccessTests(OSIntegrationTestCase): + """ + Class for testing routers with various positive scenarios expected to succeed + """ + + def setUp(self): + """ + Initializes objects used for router testing + """ + super(self.__class__, self).__start__() + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.router_creator = None + self.network_creator1 = None + self.network_creator2 = None + self.neutron = neutron_utils.neutron_client(self.os_creds) + + def tearDown(self): + """ + Cleans the remote OpenStack objects used for router testing + """ + if self.router_creator: + self.router_creator.clean() + + if self.network_creator1: + self.network_creator1.clean() + + if self.network_creator2: + self.network_creator2.clean() + + super(self.__class__, self).__clean__() + + def test_create_router_vanilla(self): + """ + Test creation of a most basic router with minimal options. + """ + router_settings = RouterSettings(name=self.guid + '-pub-router', external_gateway=self.ext_net_name) + + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + self.assertIsNotNone(router) + + self.assertTrue(verify_router_attributes(router, self.router_creator, ext_gateway=self.ext_net_name)) + + def test_create_delete_router(self): + """ + Test that clean() will not raise an exception if the router is deleted by another process. + """ + self.router_settings = RouterSettings(name=self.guid + '-pub-router', external_gateway=self.ext_net_name) + + self.router_creator = create_router.OpenStackRouter(self.os_creds, self.router_settings) + created_router = self.router_creator.create() + self.assertIsNotNone(created_router) + retrieved_router = neutron_utils.get_router_by_name(self.neutron, self.router_settings.name) + self.assertIsNotNone(retrieved_router) + + neutron_utils.delete_router(self.neutron, created_router) + + retrieved_router = neutron_utils.get_router_by_name(self.neutron, self.router_settings.name) + self.assertIsNone(retrieved_router) + + # Should not raise an exception + self.router_creator.clean() + + def test_create_router_admin_state_false(self): + """ + Test creation of a basic router with admin state down. + """ + router_settings = RouterSettings(name=self.guid + '-pub-router', admin_state_up=False) + + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + self.assertIsNotNone(router) + + self.assertTrue(verify_router_attributes(router, self.router_creator, admin_state=False)) + + def test_create_router_admin_state_True(self): + """ + Test creation of a basic router with admin state Up. + """ + router_settings = RouterSettings(name=self.guid + '-pub-router', admin_state_up=True) + + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + self.assertIsNotNone(router) + + self.assertTrue(verify_router_attributes(router, self.router_creator, admin_state=True)) + + def test_create_router_private_network(self): + """ + Test creation of a router connected with two private networks and no external gateway + """ + network_settings1 = NetworkSettings(name=self.guid + '-pub-net1', + subnet_settings=[ + create_network.SubnetSettings(cidr=cidr1, + name=self.guid + '-pub-subnet1', + gateway_ip=static_gateway_ip1)]) + network_settings2 = NetworkSettings(name=self.guid + '-pub-net2', + subnet_settings=[ + create_network.SubnetSettings(cidr=cidr2, + name=self.guid + '-pub-subnet2', + gateway_ip=static_gateway_ip2)]) + + self.network_creator1 = OpenStackNetwork(self.os_creds, network_settings1) + self.network_creator2 = OpenStackNetwork(self.os_creds, network_settings2) + + self.network_creator1.create() + self.network_creator2.create() + + port_settings = [create_network.PortSettings(name=self.guid + '-port1', ip_addrs=[ + {'subnet_name': network_settings1.subnet_settings[0].name, 'ip': static_gateway_ip1}], + network_name=network_settings1.name) + , create_network.PortSettings(name=self.guid + '-port2', ip_addrs=[ + {'subnet_name': network_settings2.subnet_settings[0].name, 'ip': static_gateway_ip2}], + network_name=network_settings2.name)] + + router_settings = RouterSettings(name=self.guid + '-pub-router', port_settings=port_settings) + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + + self.assertTrue(verify_router_attributes(router, self.router_creator)) + + def test_create_router_external_network(self): + """ + Test creation of a router connected to an external network and a private network. + """ + network_settings = NetworkSettings(name=self.guid + '-pub-net1', + subnet_settings=[ + create_network.SubnetSettings(cidr=cidr1, + name=self.guid + '-pub-subnet1', + gateway_ip=static_gateway_ip1)]) + self.network_creator1 = OpenStackNetwork(self.os_creds, network_settings) + self.network_creator1.create() + + port_settings = [create_network.PortSettings(name=self.guid + '-port1', ip_addrs=[ + {'subnet_name': network_settings.subnet_settings[0].name, 'ip': static_gateway_ip1}], + network_name=network_settings.name)] + + router_settings = RouterSettings(name=self.guid + '-pub-router', + external_gateway=self.ext_net_name, port_settings=port_settings) + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + router = neutron_utils.get_router_by_name(self.neutron, router_settings.name) + + self.assertTrue(verify_router_attributes(router, self.router_creator, ext_gateway=self.ext_net_name)) + + +class CreateRouterNegativeTests(OSIntegrationTestCase): + """ + Class for testing routers with various negative scenarios expected to fail. + """ + + def setUp(self): + """ + Initializes objects used for router testing + """ + super(self.__class__, self).__start__() + + self.guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.router_creator = None + + def tearDown(self): + """ + Cleans the remote OpenStack objects used for router testing + """ + if self.router_creator: + self.router_creator.clean() + + super(self.__class__, self).__clean__() + + def test_create_router_noname(self): + """ + Test creating a router without a name. + """ + with self.assertRaises(Exception): + router_settings = RouterSettings(name=None, external_gateway=self.ext_net_name) + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + def test_create_router_invalid_gateway_name(self): + """ + Test creating a router without a valid network gateway name. + """ + with self.assertRaises(Exception): + router_settings = RouterSettings(name=self.guid + '-pub-router', external_gateway="Invalid_name") + self.router_creator = create_router.OpenStackRouter(self.os_creds, router_settings) + self.router_creator.create() + + +def verify_router_attributes(router_operational, router_creator, admin_state=True, ext_gateway=None): + """ + Helper function to validate the attributes of router created with the one operational + :param router_operational: Operational Router object returned from neutron utils + :param router_creator: router_creator object returned from creating a router in the router test functions + :param admin_state: True if router is expected to be Up, else False + :param snat: True is enable_snat is True, else False + :param ext_gateway: None if router is not connected to external gateway + :return: + """ + + router = router_creator.get_router() + + if not router_operational: + return False + elif not router_creator: + return False + elif not (router_operational['router']['name'] == router_creator.router_settings.name): + return False + elif not (router_operational['router']['id'] == router['router']['id']): + return False + elif not (router_operational['router']['status'] == router['router']['status']): + return False + elif not (router_operational['router']['tenant_id'] == router['router']['tenant_id']): + return False + elif not (admin_state == router_operational['router']['admin_state_up']): + return False + elif (ext_gateway is None) and (router_operational['router']['external_gateway_info'] is not None): + return False + elif ext_gateway is not None: + if router_operational['router']['external_gateway_info'] is None: + return False + return True diff --git a/snaps/openstack/tests/create_security_group_tests.py b/snaps/openstack/tests/create_security_group_tests.py new file mode 100644 index 0000000..079be0c --- /dev/null +++ b/snaps/openstack/tests/create_security_group_tests.py @@ -0,0 +1,355 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest + +from snaps.openstack import create_security_group +from snaps.openstack.create_security_group import SecurityGroupSettings, SecurityGroupRuleSettings, Direction, \ + Ethertype, Protocol +from snaps.openstack.tests import validation_utils +from snaps.openstack.tests.os_source_file_test import OSIntegrationTestCase +from snaps.openstack.utils import neutron_utils + +__author__ = 'spisarski' + + +class SecurityGroupRuleSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the SecurityGroupRuleSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + SecurityGroupRuleSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + SecurityGroupRuleSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + SecurityGroupRuleSettings(sec_grp_name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + SecurityGroupRuleSettings(config={'sec_grp_name': 'foo'}) + + def test_name_and_direction(self): + settings = SecurityGroupRuleSettings(sec_grp_name='foo', direction=Direction.ingress) + self.assertEquals('foo', settings.sec_grp_name) + self.assertEquals(Direction.ingress, settings.direction) + + def test_config_name_and_direction(self): + settings = SecurityGroupRuleSettings(config={'sec_grp_name': 'foo', 'direction': 'ingress'}) + self.assertEquals('foo', settings.sec_grp_name) + self.assertEquals(Direction.ingress, settings.direction) + + def test_all(self): + settings = SecurityGroupRuleSettings( + sec_grp_name='foo', description='fubar', direction=Direction.egress, remote_group_id='rgi', + protocol=Protocol.icmp, ethertype=Ethertype.IPv6, port_range_min=1, port_range_max=2, + remote_ip_prefix='prfx') + self.assertEquals('foo', settings.sec_grp_name) + self.assertEquals('fubar', settings.description) + self.assertEquals(Direction.egress, settings.direction) + self.assertEquals('rgi', settings.remote_group_id) + self.assertEquals(Protocol.icmp, settings.protocol) + self.assertEquals(Ethertype.IPv6, settings.ethertype) + self.assertEquals(1, settings.port_range_min) + self.assertEquals(2, settings.port_range_max) + self.assertEquals('prfx', settings.remote_ip_prefix) + + def test_config_all(self): + settings = SecurityGroupRuleSettings( + config={'sec_grp_name': 'foo', + 'description': 'fubar', + 'direction': 'egress', + 'remote_group_id': 'rgi', + 'protocol': 'tcp', + 'ethertype': 'IPv6', + 'port_range_min': 1, + 'port_range_max': 2, + 'remote_ip_prefix': 'prfx'}) + self.assertEquals('foo', settings.sec_grp_name) + self.assertEquals('fubar', settings.description) + self.assertEquals(Direction.egress, settings.direction) + self.assertEquals('rgi', settings.remote_group_id) + self.assertEquals(Protocol.tcp, settings.protocol) + self.assertEquals(Ethertype.IPv6, settings.ethertype) + self.assertEquals(1, settings.port_range_min) + self.assertEquals(2, settings.port_range_max) + self.assertEquals('prfx', settings.remote_ip_prefix) + + +class SecurityGroupSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the SecurityGroupSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + SecurityGroupSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + SecurityGroupSettings(config=dict()) + + def test_name_only(self): + settings = SecurityGroupSettings(name='foo') + self.assertEquals('foo', settings.name) + + def test_config_with_name_only(self): + settings = SecurityGroupSettings(config={'name': 'foo'}) + self.assertEquals('foo', settings.name) + + def test_invalid_rule(self): + rule_setting = SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.ingress) + with self.assertRaises(Exception): + SecurityGroupSettings(name='foo', rule_settings=[rule_setting]) + + def test_all(self): + rule_settings = list() + rule_settings.append(SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.egress)) + rule_settings.append(SecurityGroupRuleSettings(sec_grp_name='bar', direction=Direction.ingress)) + settings = SecurityGroupSettings( + name='bar', description='fubar', project_name='foo', rule_settings=rule_settings) + + self.assertEquals('bar', settings.name) + self.assertEquals('fubar', settings.description) + self.assertEquals('foo', settings.project_name) + self.assertEquals(rule_settings[0], settings.rule_settings[0]) + self.assertEquals(rule_settings[1], settings.rule_settings[1]) + + def test_config_all(self): + settings = SecurityGroupSettings( + config={'name': 'bar', + 'description': 'fubar', + 'project_name': 'foo', + 'rules': [{'sec_grp_name': 'bar', 'direction': 'ingress'}]}) + + self.assertEquals('bar', settings.name) + self.assertEquals('fubar', settings.description) + self.assertEquals('foo', settings.project_name) + self.assertEquals(1, len(settings.rule_settings)) + self.assertEquals('bar', settings.rule_settings[0].sec_grp_name) + self.assertEquals(Direction.ingress, settings.rule_settings[0].direction) + + +class CreateSecurityGroupTests(OSIntegrationTestCase): + """ + Test for the CreateSecurityGroup class defined in create_security_group.py + """ + + def setUp(self): + """ + Instantiates the CreateSecurityGroup object that is responsible for downloading and creating an OS image file + within OpenStack + """ + super(self.__class__, self).__start__() + + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.sec_grp_name = guid + 'name' + self.neutron = neutron_utils.neutron_client(self.os_creds) + + # Initialize for cleanup + self.sec_grp_creator = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.sec_grp_creator: + self.sec_grp_creator.clean() + + super(self.__class__, self).__clean__() + + def test_create_group_without_rules(self): + """ + Tests the creation of an OpenStack Security Group without custom rules. + """ + # Create Image + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group') + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + self.assertIsNotNone(sec_grp) + + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + def test_create_delete_group(self): + """ + Tests the creation of an OpenStack Security Group without custom rules. + """ + # Create Image + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group') + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + created_sec_grp = self.sec_grp_creator.create() + self.assertIsNotNone(created_sec_grp) + + neutron_utils.delete_security_group(self.neutron, created_sec_grp) + self.assertIsNone(neutron_utils.get_security_group(self.neutron, self.sec_grp_creator.sec_grp_settings.name)) + + self.sec_grp_creator.clean() + + def test_create_group_with_one_simple_rule(self): + """ + Tests the creation of an OpenStack Security Group with one simple custom rule. + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + def test_create_group_with_several_rules(self): + """ + Tests the creation of an OpenStack Security Group with one simple custom rule. + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv6)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv4, + port_range_min=10, + port_range_max=20)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + def test_add_rule(self): + """ + Tests the creation of an OpenStack Security Group with one simple custom rule then adds one after creation. + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + self.sec_grp_creator.add_rule(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_creator.sec_grp_settings.name, + direction=Direction.egress, protocol=Protocol.icmp)) + rules2 = neutron_utils.get_rules_by_security_group(self.neutron, self.sec_grp_creator.get_security_group()) + self.assertEquals(len(rules) + 1, len(rules2)) + + def test_remove_rule_by_id(self): + """ + Tests the creation of an OpenStack Security Group with two simple custom rules then removes one by the rule ID. + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv6)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv4, + port_range_min=10, + port_range_max=20)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + self.sec_grp_creator.remove_rule(rule_id=rules[0]['security_group_rule']['id']) + rules_after_del = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(rules) - 1, len(rules_after_del)) + + def test_remove_rule_by_setting(self): + """ + Tests the creation of an OpenStack Security Group with two simple custom rules then removes one by the rule + setting object + """ + # Create Image + sec_grp_rule_settings = list() + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.ingress)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv6)) + sec_grp_rule_settings.append(SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, + direction=Direction.egress, + protocol=Protocol.udp, + ethertype=Ethertype.IPv4, + port_range_min=10, + port_range_max=20)) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=sec_grp_rule_settings) + self.sec_grp_creator = create_security_group.OpenStackSecurityGroup(self.os_creds, sec_grp_settings) + self.sec_grp_creator.create() + + sec_grp = neutron_utils.get_security_group(self.neutron, self.sec_grp_name) + validation_utils.objects_equivalent(self.sec_grp_creator.get_security_group(), sec_grp) + rules = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(self.sec_grp_creator.get_rules()), len(rules)) + validation_utils.objects_equivalent(self.sec_grp_creator.get_rules(), rules) + + self.sec_grp_creator.remove_rule(rule_setting=sec_grp_rule_settings[0]) + rules_after_del = neutron_utils.get_rules_by_security_group(self.neutron, + self.sec_grp_creator.get_security_group()) + self.assertEquals(len(rules) - 1, len(rules_after_del)) + +# TODO - Add more tests with different rules. Rule creation parameters can be somewhat complex diff --git a/snaps/openstack/tests/create_user_tests.py b/snaps/openstack/tests/create_user_tests.py new file mode 100644 index 0000000..1f7a163 --- /dev/null +++ b/snaps/openstack/tests/create_user_tests.py @@ -0,0 +1,155 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid +import unittest +from snaps.openstack.create_user import OpenStackUser, UserSettings +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + + +class UserSettingsUnitTests(unittest.TestCase): + """ + Tests the construction of the UserSettings class + """ + + def test_no_params(self): + with self.assertRaises(Exception): + UserSettings() + + def test_empty_config(self): + with self.assertRaises(Exception): + UserSettings(config=dict()) + + def test_name_only(self): + with self.assertRaises(Exception): + UserSettings(name='foo') + + def test_config_with_name_only(self): + with self.assertRaises(Exception): + UserSettings(config={'name': 'foo'}) + + def test_name_pass_enabled_str(self): + with self.assertRaises(Exception): + UserSettings(name='foo', password='bar', enabled='true') + + def test_config_with_name_pass_enabled_str(self): + with self.assertRaises(Exception): + UserSettings(config={'name': 'foo', 'password': 'bar', 'enabled': 'true'}) + + def test_name_pass_only(self): + settings = UserSettings(name='foo', password='bar') + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.password) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.email) + self.assertTrue(settings.enabled) + + def test_config_with_name_pass_only(self): + settings = UserSettings(config={'name': 'foo', 'password': 'bar'}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.password) + self.assertIsNone(settings.project_name) + self.assertIsNone(settings.email) + self.assertTrue(settings.enabled) + + def test_all(self): + settings = UserSettings(name='foo', password='bar', project_name='proj-foo', email='foo@bar.com', enabled=False) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.password) + self.assertEquals('proj-foo', settings.project_name) + self.assertEquals('foo@bar.com', settings.email) + self.assertFalse(settings.enabled) + + def test_config_all(self): + settings = UserSettings(config={'name': 'foo', 'password': 'bar', 'project_name': 'proj-foo', + 'email': 'foo@bar.com', 'enabled': False}) + self.assertEquals('foo', settings.name) + self.assertEquals('bar', settings.password) + self.assertEquals('proj-foo', settings.project_name) + self.assertEquals('foo@bar.com', settings.email) + self.assertFalse(settings.enabled) + + +class CreateUserSuccessTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = str(uuid.uuid4())[:-19] + guid = self.__class__.__name__ + '-' + guid + self.user_settings = UserSettings(name=guid + '-name', password=guid + '-password') + + self.keystone = keystone_utils.keystone_client(self.os_creds) + + # Initialize for cleanup + self.user_creator = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.user_creator: + self.user_creator.clean() + + def test_create_user(self): + """ + Tests the creation of an OpenStack user. + """ + self.user_creator = OpenStackUser(self.os_creds, self.user_settings) + created_user = self.user_creator.create() + self.assertIsNotNone(created_user) + + retrieved_user = keystone_utils.get_user(self.keystone, self.user_settings.name) + self.assertIsNotNone(retrieved_user) + self.assertEquals(created_user, retrieved_user) + + def test_create_user_2x(self): + """ + Tests the creation of an OpenStack user twice to ensure it only creates one. + """ + self.user_creator = OpenStackUser(self.os_creds, self.user_settings) + created_user = self.user_creator.create() + self.assertIsNotNone(created_user) + + retrieved_user = keystone_utils.get_user(self.keystone, self.user_settings.name) + self.assertIsNotNone(retrieved_user) + self.assertEquals(created_user, retrieved_user) + + # Create user for the second time to ensure it is the same + user2 = OpenStackUser(self.os_creds, self.user_settings).create() + self.assertEquals(retrieved_user, user2) + + def test_create_delete_user(self): + """ + Tests the creation of an OpenStack user then delete. + """ + # Create Image + self.user_creator = OpenStackUser(self.os_creds, self.user_settings) + created_user = self.user_creator.create() + self.assertIsNotNone(created_user) + + keystone_utils.delete_user(self.keystone, created_user) + + # Delete user + self.user_creator.clean() + self.assertIsNone(self.user_creator.get_user()) + diff --git a/snaps/openstack/tests/openstack_tests.py b/snaps/openstack/tests/openstack_tests.py new file mode 100644 index 0000000..dab2ea2 --- /dev/null +++ b/snaps/openstack/tests/openstack_tests.py @@ -0,0 +1,144 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import re + +from snaps import file_utils +from snaps.openstack.create_network import NetworkSettings, SubnetSettings +from snaps.openstack.create_router import RouterSettings +from snaps.openstack.os_credentials import OSCreds, ProxySettings +from snaps.openstack.create_image import ImageSettings +import logging + +__author__ = 'spisarski' + + +logger = logging.getLogger('openstack_tests') + + +def get_credentials(os_env_file=None, proxy_settings_str=None, ssh_proxy_cmd=None, dev_os_env_file=None): + """ + Returns the OpenStack credentials object. It first attempts to retrieve them from a standard OpenStack source file. + If that file is None, it will attempt to retrieve them with a YAML file. + it will retrieve them from a + :param os_env_file: the OpenStack source file + :param proxy_settings_str: proxy settings string : (optional) + :param ssh_proxy_cmd: the SSH proxy command for your environment (optional) + :param dev_os_env_file: the YAML file to retrieve both the OS credentials and proxy settings + :return: the SNAPS credentials object + """ + if os_env_file: + logger.debug('Reading RC file - ' + os_env_file) + config = file_utils.read_os_env_file(os_env_file) + proj_name = config.get('OS_PROJECT_NAME') + if not proj_name: + proj_name = config.get('OS_TENANT_NAME') + + proj_domain_id = 'default' + user_domain_id = 'default' + + if config.get('OS_PROJECT_DOMAIN_ID'): + proj_domain_id = config['OS_PROJECT_DOMAIN_ID'] + if config.get('OS_USER_DOMAIN_ID'): + user_domain_id = config['OS_USER_DOMAIN_ID'] + if config.get('OS_IDENTITY_API_VERSION'): + version = int(config['OS_IDENTITY_API_VERSION']) + else: + version = 2 + + proxy_settings = None + if proxy_settings_str: + tokens = re.split(':', proxy_settings_str) + proxy_settings = ProxySettings(tokens[0], tokens[1], ssh_proxy_cmd) + + os_creds = OSCreds(username=config['OS_USERNAME'], + password=config['OS_PASSWORD'], + auth_url=config['OS_AUTH_URL'], + project_name=proj_name, + identity_api_version=version, + user_domain_id=user_domain_id, + project_domain_id=proj_domain_id, + proxy_settings=proxy_settings) + else: + logger.info('Reading development os_env file - ' + dev_os_env_file) + config = file_utils.read_yaml(dev_os_env_file) + identity_api_version = config.get('identity_api_version') + if not identity_api_version: + identity_api_version = 2 + + proxy_settings = None + proxy_str = config.get('http_proxy') + if proxy_str: + tokens = re.split(':', proxy_str) + proxy_settings = ProxySettings(tokens[0], tokens[1], config.get('ssh_proxy_cmd')) + + os_creds = OSCreds(username=config['username'], password=config['password'], + auth_url=config['os_auth_url'], project_name=config['project_name'], + identity_api_version=identity_api_version, + proxy_settings=proxy_settings) + + logger.info('OS Credentials = ' + str(os_creds)) + return os_creds + + +def cirros_url_image(name): + return ImageSettings(name=name, image_user='cirros', img_format='qcow2', + url='http://download.cirros-cloud.net/0.3.4/cirros-0.3.4-x86_64-disk.img') + + +def file_image_test_settings(name, file_path): + return ImageSettings(name=name, image_user='cirros', img_format='qcow2', + image_file=file_path) + + +def centos_url_image(name): + return ImageSettings(name=name, image_user='centos', img_format='qcow2', + url='http://cloud.centos.org/centos/7/images/CentOS-7-x86_64-GenericCloud.qcow2', + nic_config_pb_loc='./provisioning/ansible/centos-network-setup/playbooks/configure_host.yml') + + +def ubuntu_url_image(name): + return ImageSettings( + name=name, image_user='ubuntu', img_format='qcow2', + url='http://uec-images.ubuntu.com/releases/trusty/14.04/ubuntu-14.04-server-cloudimg-amd64-disk1.img', + nic_config_pb_loc='./provisioning/ansible/ubuntu-network-setup/playbooks/configure_host.yml') + + +def get_priv_net_config(net_name, subnet_name, router_name=None, cidr='10.55.0.0/24', external_net=None): + return OSNetworkConfig(net_name, subnet_name, cidr, router_name, external_gateway=external_net) + + +def get_pub_net_config(net_name, subnet_name=None, router_name=None, cidr='10.55.1.0/24', external_net=None): + return OSNetworkConfig(net_name, subnet_name, cidr, router_name, external_gateway=external_net) + + +class OSNetworkConfig: + """ + Represents the settings required for the creation of a network in OpenStack + """ + + def __init__(self, net_name, subnet_name=None, subnet_cidr=None, router_name=None, external_gateway=None): + + if subnet_name and subnet_cidr: + self.network_settings = NetworkSettings( + name=net_name, subnet_settings=[SubnetSettings(cidr=subnet_cidr, name=subnet_name)]) + else: + self.network_settings = NetworkSettings(name=net_name) + + if router_name: + if subnet_name: + self.router_settings = RouterSettings(name=router_name, external_gateway=external_gateway, + internal_subnets=[subnet_name]) + else: + self.router_settings = RouterSettings(name=router_name, external_gateway=external_gateway) diff --git a/snaps/openstack/tests/os_source_file_test.py b/snaps/openstack/tests/os_source_file_test.py new file mode 100644 index 0000000..fa8d197 --- /dev/null +++ b/snaps/openstack/tests/os_source_file_test.py @@ -0,0 +1,131 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest +import uuid + +from snaps import file_utils +import openstack_tests +import logging + +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# To run these tests from an IDE, the CWD must be set to the python directory of this project +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +from snaps.openstack.create_project import ProjectSettings +from snaps.openstack.create_user import UserSettings +from snaps.openstack.utils import deploy_utils, keystone_utils + +dev_os_env_file = 'openstack/tests/conf/os_env.yaml' + + +class OSComponentTestCase(unittest.TestCase): + + """ + Super for test classes requiring a connection to OpenStack + """ + def __init__(self, method_name='runTest', os_env_file=None, ext_net_name=None, http_proxy_str=None, + ssh_proxy_cmd=None, log_level=logging.DEBUG): + super(OSComponentTestCase, self).__init__(method_name) + + logging.basicConfig(level=log_level) + + self.os_creds = openstack_tests.get_credentials(os_env_file=os_env_file, proxy_settings_str=http_proxy_str, + ssh_proxy_cmd=ssh_proxy_cmd, dev_os_env_file=dev_os_env_file) + self.ext_net_name = ext_net_name + + if not self.ext_net_name and file_utils.file_exists(dev_os_env_file): + test_conf = file_utils.read_yaml(dev_os_env_file) + self.ext_net_name = test_conf.get('ext_net') + + @staticmethod + def parameterize(testcase_klass, os_env_file, ext_net_name, http_proxy_str=None, ssh_proxy_cmd=None, + log_level=logging.DEBUG): + """ Create a suite containing all tests taken from the given + subclass, passing them the parameter 'param'. + """ + test_loader = unittest.TestLoader() + test_names = test_loader.getTestCaseNames(testcase_klass) + suite = unittest.TestSuite() + for name in test_names: + suite.addTest(testcase_klass(name, os_env_file, ext_net_name, http_proxy_str, ssh_proxy_cmd, log_level)) + return suite + + +class OSIntegrationTestCase(OSComponentTestCase): + + """ + Super for test classes requiring a connection to OpenStack + """ + def __init__(self, method_name='runTest', os_env_file=None, ext_net_name=None, http_proxy_str=None, + ssh_proxy_cmd=None, use_keystone=False, log_level=logging.DEBUG): + super(OSIntegrationTestCase, self).__init__(method_name=method_name, os_env_file=os_env_file, + ext_net_name=ext_net_name, http_proxy_str=http_proxy_str, + ssh_proxy_cmd=ssh_proxy_cmd, log_level=log_level) + self.use_keystone = use_keystone + self.keystone = None + + @staticmethod + def parameterize(testcase_klass, os_env_file, ext_net_name, http_proxy_str=None, ssh_proxy_cmd=None, + use_keystone=False, log_level=logging.DEBUG): + """ Create a suite containing all tests taken from the given + subclass, passing them the parameter 'param'. + """ + test_loader = unittest.TestLoader() + test_names = test_loader.getTestCaseNames(testcase_klass) + suite = unittest.TestSuite() + for name in test_names: + suite.addTest(testcase_klass(name, os_env_file, ext_net_name, http_proxy_str, ssh_proxy_cmd, use_keystone, + log_level)) + return suite + + """ + Super for test classes that should be run within their own project/tenant as they can run for quite some time + """ + def __start__(self): + """ + Creates a project and user to be leveraged by subclass test methods. If implementing class uses this method, + it must call __clean__() else you will be left with unwanted users and tenants + """ + self.project_creator = None + self.user_creator = None + self.admin_os_creds = self.os_creds + self.role = None + + if self.use_keystone: + self.keystone = keystone_utils.keystone_client(self.os_creds) + guid = self.__class__.__name__ + '-' + str(uuid.uuid4())[:-19] + project_name = guid + '-proj' + self.project_creator = deploy_utils.create_project(self.admin_os_creds, ProjectSettings(name=project_name)) + + self.user_creator = deploy_utils.create_user( + self.admin_os_creds, UserSettings(name=guid + '-user', password=guid, project_name=project_name)) + self.os_creds = self.user_creator.get_os_creds(self.project_creator.project_settings.name) + + # add user to project + self.project_creator.assoc_user(self.user_creator.get_user()) + + def __clean__(self): + """ + Cleans up test user and project. + Must be called at the end of child classes tearDown() if __start__() is called during setUp() else these + objects will persist after the test is run + """ + if self.role: + keystone_utils.delete_role(self.keystone, self.role) + + if self.project_creator: + self.project_creator.clean() + + if self.user_creator: + self.user_creator.clean() diff --git a/snaps/openstack/tests/validation_utils.py b/snaps/openstack/tests/validation_utils.py new file mode 100644 index 0000000..7c9bd7f --- /dev/null +++ b/snaps/openstack/tests/validation_utils.py @@ -0,0 +1,69 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from neutronclient.v2_0.client import _DictWithMeta + +__author__ = 'spisarski' + + +def objects_equivalent(obj1, obj2): + """ + Returns true if both objects are equivalent + :param obj1: + :param obj2: + :return: T/F + """ + if obj1 is None and obj2 is None: + return True + if type(obj1) is dict or type(obj1) is _DictWithMeta: + return dicts_equivalent(obj1, obj2) + elif type(obj1) is list: + return lists_equivalent(obj1, obj2) + else: + return obj1 == obj2 + + +def dicts_equivalent(dict1, dict2): + """ + Returns true when each key/value pair is equal + :param dict1: dict 1 + :param dict2: dict 2 + :return: T/F + """ + if (type(dict1) is dict or type(dict1) is _DictWithMeta) and (type(dict2) is dict or type(dict2) is _DictWithMeta): + for key, value1 in dict1.iteritems(): + if not objects_equivalent(value1, dict2.get(key)): + return False + return True + return False + + +def lists_equivalent(list1, list2): + """ + Returns true when an item in list1 is also contained in list2 + :param list1: list 1 + :param list2: list 2 + :return: T/F + """ + if len(list1) == len(list2) and type(list1) is list and type(list2) is list: + for item1 in list1: + has_equivalent = False + for item2 in list2: + has_equivalent = objects_equivalent(item1, item2) + if has_equivalent: + break + if not has_equivalent: + return False + return True + return False diff --git a/snaps/openstack/utils/__init__.py b/snaps/openstack/utils/__init__.py new file mode 100644 index 0000000..7f92908 --- /dev/null +++ b/snaps/openstack/utils/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' \ No newline at end of file diff --git a/snaps/openstack/utils/deploy_utils.py b/snaps/openstack/utils/deploy_utils.py new file mode 100644 index 0000000..ade8811 --- /dev/null +++ b/snaps/openstack/utils/deploy_utils.py @@ -0,0 +1,151 @@ +# +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This utility makes it easy to create OpenStack objects +import logging + +from snaps.openstack.create_project import OpenStackProject +from snaps.openstack.create_user import OpenStackUser +from snaps.openstack.create_image import OpenStackImage +from snaps.openstack.create_network import OpenStackNetwork +from snaps.openstack.create_router import OpenStackRouter +from snaps.openstack.create_keypairs import OpenStackKeypair +from snaps.openstack.create_instance import OpenStackVmInstance +from snaps.openstack.create_security_group import OpenStackSecurityGroup + +logger = logging.getLogger('deploy_utils') + + +def create_image(os_creds, image_settings, cleanup=False): + """ + Creates an image in OpenStack if necessary + :param os_creds: The OpenStack credentials object + :param image_settings: The image settings object + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: A reference to the image creator object from which the image object can be accessed + """ + image_creator = OpenStackImage(os_creds, image_settings) + image_creator.create(cleanup) + return image_creator + + +def create_network(os_creds, network_settings, cleanup=False): + """ + Creates a network on which the CMTSs can attach + :param os_creds: The OpenStack credentials object + :param network_settings: The network settings object + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: A reference to the network creator objects for each network from which network elements such as the + subnet, router, interface router, and network objects can be accessed. + """ + # Check for OS for network existence + # If exists return network instance data + # Else, create network and return instance data + + logger.info('Attempting to create network with name - ' + network_settings.name) + + network_creator = OpenStackNetwork(os_creds, network_settings) + network_creator.create(cleanup) + logger.info('Created network ') + return network_creator + + +def create_router(os_creds, router_settings, cleanup=False): + """ + Creates a network on which the CMTSs can attach + :param os_creds: The OpenStack credentials object + :param router_settings: The RouterSettings instance + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: A reference to the network creator objects for each network from which network elements such as the + subnet, router, interface router, and network objects can be accessed. + """ + # Check for OS for network existence + # If exists return network instance data + # Else, create network and return instance data + logger.info('Attempting to create router with name - ' + router_settings.name) + router_creator = OpenStackRouter(os_creds, router_settings) + router_creator.create(cleanup) + logger.info('Created router ') + return router_creator + + +def create_keypair(os_creds, keypair_settings, cleanup=False): + """ + Creates a keypair that can be applied to an instance + :param os_creds: The OpenStack credentials object + :param keypair_settings: The KeypairSettings object + :param cleanup: Denotes whether or not this is being called for cleanup or not + :return: A reference to the keypair creator object + """ + keypair_creator = OpenStackKeypair(os_creds, keypair_settings) + keypair_creator.create(cleanup) + return keypair_creator + + +def create_vm_instance(os_creds, instance_settings, image_settings, keypair_creator=None, cleanup=False): + """ + Creates a VM instance + :param os_creds: The OpenStack credentials + :param instance_settings: Instance of VmInstanceSettings + :param image_settings: The object containing image settings + :param keypair_creator: The object responsible for creating the keypair associated with this VM instance. (optional) + :param sg_names: The names of the security groups to apply to VM. (optional) + :param cleanup: Denotes whether or not this is being called for cleanup or not (default False) + :return: A reference to the VM instance object + """ + kp_settings = None + if keypair_creator: + kp_settings = keypair_creator.keypair_settings + vm_creator = OpenStackVmInstance(os_creds, instance_settings, image_settings, kp_settings) + vm_creator.create(cleanup=cleanup) + return vm_creator + + +def create_user(os_creds, user_settings): + """ + Creates an OpenStack user + :param os_creds: The OpenStack credentials + :param user_settings: The user configuration settings + :return: A reference to the user instance object + """ + user_creator = OpenStackUser(os_creds, user_settings) + user_creator.create() + return user_creator + + +def create_project(os_creds, project_settings): + """ + Creates an OpenStack user + :param os_creds: The OpenStack credentials + :param project_settings: The user project configuration settings + :return: A reference to the project instance object + """ + project_creator = OpenStackProject(os_creds, project_settings) + project_creator.create() + return project_creator + + +def create_security_group(os_creds, sec_grp_settings): + """ + Creates an OpenStack Security Group + :param os_creds: The OpenStack credentials + :param sec_grp_settings: The security group settings + :return: A reference to the project instance object + """ + sg_creator = OpenStackSecurityGroup(os_creds, sec_grp_settings) + sg_creator.create() + return sg_creator + diff --git a/snaps/openstack/utils/glance_utils.py b/snaps/openstack/utils/glance_utils.py new file mode 100644 index 0000000..6d90d3e --- /dev/null +++ b/snaps/openstack/utils/glance_utils.py @@ -0,0 +1,78 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from snaps import file_utils +from glanceclient.client import Client +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('glance_utils') + +""" +Utilities for basic neutron API calls +""" + + +def glance_client(os_creds): + """ + Creates and returns a glance client object + :return: the glance client + """ + return Client(version=os_creds.image_api_version, session=keystone_utils.keystone_session(os_creds)) + + +def get_image(nova, glance, image_name): + """ + Returns an OpenStack image object for a given name + :param nova: the Nova client + :param glance: the Glance client + :param image_name: the image name to lookup + :return: the image object or None + """ + try: + image_dict = nova.images.find(name=image_name) + if image_dict: + return glance.images.get(image_dict.id) + except: + pass + return None + + +def create_image(glance, image_settings): + """ + Creates and returns OpenStack image object with an external URL + :param glance: the glance client + :param image_settings: the image settings object + :return: the OpenStack image object + :raise Exception if using a file and it cannot be found + """ + if image_settings.url: + return glance.images.create(name=image_settings.name, disk_format=image_settings.format, + container_format="bare", location=image_settings.url) + elif image_settings.image_file: + image_file = file_utils.get_file(image_settings.image_file) + return glance.images.create(name=image_settings.name, disk_format=image_settings.format, + container_format="bare", data=image_file) + + +def delete_image(glance, image): + """ + Deletes an image from OpenStack + :param glance: the glance client + :param image: the image to delete + """ + glance.images.delete(image) diff --git a/snaps/openstack/utils/keystone_utils.py b/snaps/openstack/utils/keystone_utils.py new file mode 100644 index 0000000..8175b9a --- /dev/null +++ b/snaps/openstack/utils/keystone_utils.py @@ -0,0 +1,204 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import requests +from keystoneclient.client import Client +from keystoneauth1.identity import v3, v2 +from keystoneauth1 import session +import logging + + +logger = logging.getLogger('keystone_utils') + +V2_VERSION = 'v2.0' + + +def keystone_session(os_creds): + """ + Creates a keystone session used for authenticating OpenStack clients + :param os_creds: The connection credentials to the OpenStack API + :return: the client object + """ + logger.debug('Retrieving Keystone Session') + + if os_creds.identity_api_version == 3: + auth = v3.Password(auth_url=os_creds.auth_url, username=os_creds.username, password=os_creds.password, + project_name=os_creds.project_name, user_domain_id=os_creds.user_domain_id, + project_domain_id=os_creds.project_domain_id) + else: + auth = v2.Password(auth_url=os_creds.auth_url, username=os_creds.username, password=os_creds.password, + tenant_name=os_creds.project_name) + + req_session = None + if os_creds.proxy_settings: + req_session = requests.Session() + req_session.proxies = {'http': os_creds.proxy_settings.host + ':' + os_creds.proxy_settings.port} + return session.Session(auth=auth, session=req_session) + + +def keystone_client(os_creds): + """ + Returns the keystone client + :param os_creds: the OpenStack credentials (OSCreds) object + :return: the client + """ + return Client(version=os_creds.identity_api_version, session=keystone_session(os_creds)) + + +def get_project(keystone=None, os_creds=None, project_name=None): + """ + Returns the first project object or None if not found + :param keystone: the Keystone client + :param os_creds: the OpenStack credentials used to obtain the Keystone client if the keystone parameter is None + :param project_name: the name to query + :return: the ID or None + """ + if not project_name: + return None + + if not keystone: + if os_creds: + keystone = keystone_client(os_creds) + else: + raise Exception('Cannot lookup project without the proper credentials') + + if keystone.version == V2_VERSION: + projects = keystone.tenants.list() + else: + projects = keystone.projects.list(**{'name': project_name}) + + for project in projects: + if project.name == project_name: + return project + + return None + + +def create_project(keystone, project_settings): + """ + Creates a project + :param keystone: the Keystone client + :param project_settings: the project configuration + :return: + """ + if keystone.version == V2_VERSION: + return keystone.tenants.create(project_settings.name, project_settings.description, project_settings.enabled) + + return keystone.projects.create(project_settings.name, project_settings.domain, + description=project_settings.description, + enabled=project_settings.enabled) + + +def delete_project(keystone, project): + """ + Deletes a project + :param keystone: the Keystone clien + :param project: the OpenStack project object + """ + if keystone.version == V2_VERSION: + keystone.tenants.delete(project) + else: + keystone.projects.delete(project) + + +def get_user(keystone, username, project_name=None): + """ + Returns a user for a given name and optionally project + :param keystone: the keystone client + :param username: the username to lookup + :param project_name: the associated project (optional) + :return: + """ + project = get_project(keystone=keystone, project_name=project_name) + + if project: + users = keystone.users.list(tenant_id=project.id) + else: + users = keystone.users.list() + + for user in users: + if user.name == username: + return user + + return None + + +def create_user(keystone, user_settings): + """ + Creates a user + :param keystone: the Keystone client + :param user_settings: the user configuration + :return: + """ + project = None + if user_settings.project_name: + project = get_project(keystone=keystone, project_name=user_settings.project_name) + + if keystone.version == V2_VERSION: + project_id = None + if project: + project_id = project.id + return keystone.users.create(name=user_settings.name, password=user_settings.password, + email=user_settings.email, tenant_id=project_id, enabled=user_settings.enabled) + else: + # TODO - need to support groups + return keystone.users.create(name=user_settings.name, password=user_settings.password, + email=user_settings.email, project=project, + # email=user_settings.email, project=project, group='default', + domain=user_settings.domain_name, + enabled=user_settings.enabled) + + +def delete_user(keystone, user): + """ + Deletes a user + :param keystone: the Keystone client + :param user: the OpenStack user object + """ + keystone.users.delete(user) + + +def create_role(keystone, name): + """ + Creates an OpenStack role + :param keystone: the keystone client + :param name: the role name + :return: + """ + return keystone.roles.create(name) + + +def delete_role(keystone, role): + """ + Deletes an OpenStack role + :param keystone: the keystone client + :param role: the role to delete + :return: + """ + keystone.roles.delete(role) + + +def assoc_user_to_project(keystone, role, user, project): + """ + Adds a user to a project + :param keystone: the Keystone client + :param role: the role used to join a project/user + :param user: the user to add to the project + :param project: the project to which to add a user + :return: + """ + if keystone.version == V2_VERSION: + keystone.roles.add_user_role(user, role, tenant=project) + else: + keystone.roles.grant(role, user=user, project=project) diff --git a/snaps/openstack/utils/neutron_utils.py b/snaps/openstack/utils/neutron_utils.py new file mode 100644 index 0000000..6c92d2e --- /dev/null +++ b/snaps/openstack/utils/neutron_utils.py @@ -0,0 +1,405 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +from neutronclient.common.exceptions import NotFound +from neutronclient.neutron.client import Client +import keystone_utils + +__author__ = 'spisarski' + +logger = logging.getLogger('neutron_utils') + +""" +Utilities for basic neutron API calls +""" + + +def neutron_client(os_creds): + """ + Instantiates and returns a client for communications with OpenStack's Neutron server + :param os_creds: the credentials for connecting to the OpenStack remote API + :return: the client object + """ + return Client(api_version=os_creds.network_api_version, session=keystone_utils.keystone_session(os_creds)) + + +def create_network(neutron, os_creds, network_settings): + """ + Creates a network for OpenStack + :param neutron: the client + :param os_creds: the OpenStack credentials + :param network_settings: A dictionary containing the network configuration and is responsible for creating the + network request JSON body + :return: the network object + """ + if neutron and network_settings: + logger.info('Creating network with name ' + network_settings.name) + json_body = network_settings.dict_for_neutron(os_creds) + return neutron.create_network(body=json_body) + else: + logger.error("Failed to create network") + raise Exception + + +def delete_network(neutron, network): + """ + Deletes a network for OpenStack + :param neutron: the client + :param network: the network object + """ + if neutron and network: + logger.info('Deleting network with name ' + network['network']['name']) + neutron.delete_network(network['network']['id']) + + +def get_network(neutron, network_name, project_id=None): + """ + Returns an object (dictionary) of the first network found with a given name and project_id (if included) + :param neutron: the client + :param network_name: the name of the network to retrieve + :param project_id: the id of the network's project + :return: + """ + net_filter = dict() + if network_name: + net_filter['name'] = network_name + if project_id: + net_filter['project_id'] = project_id + + networks = neutron.list_networks(**net_filter) + for network, netInsts in networks.iteritems(): + for inst in netInsts: + if inst.get('name') == network_name: + if project_id and inst.get('project_id') == project_id: + return {'network': inst} + else: + return {'network': inst} + return None + + +def get_network_by_id(neutron, network_id): + """ + Returns the network object (dictionary) with the given ID + :param neutron: the client + :param network_id: the id of the network to retrieve + :return: + """ + networks = neutron.list_networks(**{'id': network_id}) + for network, netInsts in networks.iteritems(): + for inst in netInsts: + if inst.get('id') == network_id: + return {'network': inst} + return None + + +def create_subnet(neutron, subnet_settings, os_creds, network=None): + """ + Creates a network subnet for OpenStack + :param neutron: the client + :param network: the network object + :param subnet_settings: A dictionary containing the subnet configuration and is responsible for creating the subnet + request JSON body + :param os_creds: the OpenStack credentials + :return: the subnet object + """ + if neutron and network and subnet_settings: + json_body = {'subnets': [subnet_settings.dict_for_neutron(os_creds, network=network)]} + logger.info('Creating subnet with name ' + subnet_settings.name) + subnets = neutron.create_subnet(body=json_body) + return {'subnet': subnets['subnets'][0]} + else: + logger.error("Failed to create subnet.") + raise Exception + + +def delete_subnet(neutron, subnet): + """ + Deletes a network subnet for OpenStack + :param neutron: the client + :param subnet: the subnet object + """ + if neutron and subnet: + logger.info('Deleting subnet with name ' + subnet['subnet']['name']) + neutron.delete_subnet(subnet['subnet']['id']) + + +def get_subnet_by_name(neutron, subnet_name): + """ + Returns the first subnet object (dictionary) found with a given name + :param neutron: the client + :param subnet_name: the name of the network to retrieve + :return: + """ + subnets = neutron.list_subnets(**{'name': subnet_name}) + for subnet, subnetInst in subnets.iteritems(): + for inst in subnetInst: + if inst.get('name') == subnet_name: + return {'subnet': inst} + return None + + +def create_router(neutron, os_creds, router_settings): + """ + Creates a router for OpenStack + :param neutron: the client + :param os_creds: the OpenStack credentials + :param router_settings: A dictionary containing the router configuration and is responsible for creating the subnet + request JSON body + :return: the router object + """ + if neutron: + json_body = router_settings.dict_for_neutron(neutron, os_creds) + logger.info('Creating router with name - ' + router_settings.name) + return neutron.create_router(json_body) + else: + logger.error("Failed to create router.") + raise Exception + + +def delete_router(neutron, router): + """ + Deletes a router for OpenStack + :param neutron: the client + :param router: the router object + """ + if neutron and router: + logger.info('Deleting router with name - ' + router['router']['name']) + neutron.delete_router(router=router['router']['id']) + return True + + +def get_router_by_name(neutron, router_name): + """ + Returns the first router object (dictionary) found with a given name + :param neutron: the client + :param router_name: the name of the network to retrieve + :return: + """ + routers = neutron.list_routers(**{'name': router_name}) + for router, routerInst in routers.iteritems(): + for inst in routerInst: + if inst.get('name') == router_name: + return {'router': inst} + return None + + +def add_interface_router(neutron, router, subnet=None, port=None): + """ + Adds an interface router for OpenStack for either a subnet or port. Exception will be raised if requesting for both. + :param neutron: the client + :param router: the router object + :param subnet: the subnet object + :param port: the port object + :return: the interface router object + """ + if subnet and port: + raise Exception('Cannot add interface to the router. Both subnet and port were sent in. Either or please.') + + if neutron and router and (router or subnet): + logger.info('Adding interface to router with name ' + router['router']['name']) + return neutron.add_interface_router(router=router['router']['id'], body=__create_port_json_body(subnet, port)) + else: + raise Exception("Unable to create interface router as neutron client, router or subnet were not created") + + +def remove_interface_router(neutron, router, subnet=None, port=None): + """ + Removes an interface router for OpenStack + :param neutron: the client + :param router: the router object + :param subnet: the subnet object (either subnet or port, not both) + :param port: the port object + """ + if router: + try: + logger.info('Removing router interface from router named ' + router['router']['name']) + neutron.remove_interface_router(router=router['router']['id'], body=__create_port_json_body(subnet, port)) + except NotFound as e: + logger.warn('Could not remove router interface. NotFound - ' + e.message) + pass + else: + logger.warn('Could not remove router interface, No router object') + + +def __create_port_json_body(subnet=None, port=None): + """ + Returns the dictionary required for creating and deleting router interfaces. Will only work on a subnet or port + object. Will throw and exception if parameters contain both or neither + :param subnet: the subnet object + :param port: the port object + :return: the dict + """ + if subnet and port: + raise Exception('Cannot create JSON body with both subnet and port') + if not subnet and not port: + raise Exception('Cannot create JSON body without subnet or port') + + if subnet: + return {"subnet_id": subnet['subnet']['id']} + else: + return {"port_id": port['port']['id']} + + +def create_port(neutron, os_creds, port_settings): + """ + Creates a port for OpenStack + :param neutron: the client + :param os_creds: the OpenStack credentials + :param port_settings: the settings object for port configuration + :return: the port object + """ + json_body = port_settings.dict_for_neutron(neutron, os_creds) + logger.info('Creating port for network with name - ' + port_settings.network_name) + return neutron.create_port(body=json_body) + + +def delete_port(neutron, port): + """ + Removes an OpenStack port + :param neutron: the client + :param port: the port object + :return: + """ + logger.info('Deleting port with name ' + port['port']['name']) + neutron.delete_port(port['port']['id']) + + +def get_port_by_name(neutron, port_name): + """ + Returns the first port object (dictionary) found with a given name + :param neutron: the client + :param port_name: the name of the port to retrieve + :return: + """ + ports = neutron.list_ports(**{'name': port_name}) + for port in ports['ports']: + if port['name'] == port_name: + return {'port': port} + return None + + +def create_security_group(neutron, keystone, sec_grp_settings): + """ + Creates a security group object in OpenStack + :param neutron: the Neutron client + :param keystone: the Keystone client + :param sec_grp_settings: the security group settings + :return: the security group object + """ + logger.info('Creating security group with name - ' + sec_grp_settings.name) + return neutron.create_security_group(sec_grp_settings.dict_for_neutron(keystone)) + + +def delete_security_group(neutron, sec_grp): + """ + Deletes a security group object from OpenStack + :param neutron: the client + :param sec_grp: the security group object to delete + """ + logger.info('Deleting security group with name - ' + sec_grp['security_group']['name']) + return neutron.delete_security_group(sec_grp['security_group']['id']) + + +def get_security_group(neutron, name): + """ + Returns the first security group object of the given name else None + :param neutron: the client + :param name: the name of security group object to retrieve + """ + logger.info('Retrieving security group with name - ' + name) + + groups = neutron.list_security_groups(**{'name': name}) + for group in groups['security_groups']: + if group['name'] == name: + return {'security_group': group} + return None + + +def get_security_group_by_id(neutron, sec_grp_id): + """ + Returns the first security group object of the given name else None + :param neutron: the client + :param sec_grp_id: the id of the security group to retrieve + """ + logger.info('Retrieving security group with ID - ' + sec_grp_id) + + groups = neutron.list_security_groups(**{'sec_grp_id': sec_grp_id}) + for group in groups['security_groups']: + return {'security_group': group} + return None + + +def create_security_group_rule(neutron, sec_grp_rule_settings): + """ + Creates a security group object in OpenStack + :param neutron: the client + :param sec_grp_rule_settings: the security group rule settings + :return: the security group object + """ + logger.info('Creating security group to security group - ' + sec_grp_rule_settings.sec_grp_name) + return neutron.create_security_group_rule(sec_grp_rule_settings.dict_for_neutron(neutron)) + + +def delete_security_group_rule(neutron, sec_grp_rule): + """ + Deletes a security group object from OpenStack + :param neutron: the client + :param sec_grp_rule: the security group rule object to delete + """ + logger.info('Deleting security group rule with ID - ' + sec_grp_rule['security_group_rule']['id']) + neutron.delete_security_group_rule(sec_grp_rule['security_group_rule']['id']) + + +def get_rules_by_security_group(neutron, sec_grp): + """ + Retrieves all of the rules for a given security group + :param neutron: the client + :param sec_grp: the security group object + """ + logger.info('Retrieving security group rules associate with the security group - ' + + sec_grp['security_group']['name']) + out = list() + rules = neutron.list_security_group_rules(**{'security_group_id': sec_grp['security_group']['id']}) + for rule in rules['security_group_rules']: + if rule['security_group_id'] == sec_grp['security_group']['id']: + out.append({'security_group_rule': rule}) + return out + + +def get_rule_by_id(neutron, sec_grp, rule_id): + """ + Deletes a security group object from OpenStack + :param neutron: the client + :param sec_grp: the security group object + :param rule_id: the rule's ID + """ + rules = neutron.list_security_group_rules(**{'security_group_id': sec_grp['security_group']['id']}) + for rule in rules['security_group_rules']: + if rule['id'] == rule_id: + return {'security_group_rule': rule} + return None + + +def get_external_networks(neutron): + """ + Returns a list of external OpenStack network object/dict for all external networks + :param neutron: the client + :return: a list of external networks (empty list if none configured) + """ + out = list() + for network in neutron.list_networks(**{'router:external': True})['networks']: + out.append({'network': network}) + return out diff --git a/snaps/openstack/utils/nova_utils.py b/snaps/openstack/utils/nova_utils.py new file mode 100644 index 0000000..9d0f70f --- /dev/null +++ b/snaps/openstack/utils/nova_utils.py @@ -0,0 +1,282 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import logging +import keystone_utils + +from novaclient.client import Client +from novaclient.exceptions import NotFound + +__author__ = 'spisarski' + +logger = logging.getLogger('nova_utils') + +""" +Utilities for basic OpenStack Nova API calls +""" + + +def nova_client(os_creds): + """ + Instantiates and returns a client for communications with OpenStack's Nova server + :param os_creds: The connection credentials to the OpenStack API + :return: the client object + """ + logger.debug('Retrieving Nova Client') + return Client(os_creds.compute_api_version, session=keystone_utils.keystone_session(os_creds)) + + +def get_servers_by_name(nova, name): + """ + Returns a list of servers with a given name + :param nova: the Nova client + :param name: the server name + :return: the list of servers + """ + return nova.servers.list(search_opts={'name': name}) + + +def get_latest_server_object(nova, server): + """ + Returns a server with a given id + :param nova: the Nova client + :param server: the old server object + :return: the list of servers or None if not found + """ + return nova.servers.get(server) + + +def save_keys_to_files(keys=None, pub_file_path=None, priv_file_path=None): + """ + Saves the generated RSA generated keys to the filesystem + :param keys: the keys to save + :param pub_file_path: the path to the public keys + :param priv_file_path: the path to the private keys + :return: None + """ + if keys: + if pub_file_path: + pub_dir = os.path.dirname(pub_file_path) + if not os.path.isdir(pub_dir): + os.mkdir(pub_dir) + public_handle = open(pub_file_path, 'wb') + public_handle.write(keys.publickey().exportKey('OpenSSH')) + public_handle.close() + os.chmod(pub_file_path, 0o400) + logger.info("Saved public key to - " + pub_file_path) + if priv_file_path: + priv_dir = os.path.dirname(priv_file_path) + if not os.path.isdir(priv_dir): + os.mkdir(priv_dir) + private_handle = open(priv_file_path, 'wb') + private_handle.write(keys.exportKey()) + private_handle.close() + os.chmod(priv_file_path, 0o400) + logger.info("Saved private key to - " + priv_file_path) + + +def upload_keypair_file(nova, name, file_path): + """ + Uploads a public key from a file + :param nova: the Nova client + :param name: the keypair name + :param file_path: the path to the public key file + :return: the keypair object + """ + with open(os.path.expanduser(file_path)) as fpubkey: + logger.info('Saving keypair to - ' + file_path) + return upload_keypair(nova, name, fpubkey.read()) + + +def upload_keypair(nova, name, key): + """ + Uploads a public key from a file + :param nova: the Nova client + :param name: the keypair name + :param key: the public key object + :return: the keypair object + """ + logger.info('Creating keypair with name - ' + name) + return nova.keypairs.create(name=name, public_key=key) + + +def keypair_exists(nova, keypair_obj): + """ + Returns a copy of the keypair object if found + :param nova: the Nova client + :param keypair_obj: the keypair object + :return: the keypair object or None if not found + """ + try: + return nova.keypairs.get(keypair_obj) + except: + return None + + +def get_keypair_by_name(nova, name): + """ + Returns a list of all available keypairs + :param nova: the Nova client + :param name: the name of the keypair to lookup + :return: the keypair object or None if not found + """ + keypairs = nova.keypairs.list() + + for keypair in keypairs: + if keypair.name == name: + return keypair + + return None + + +def delete_keypair(nova, key): + """ + Deletes a keypair object from OpenStack + :param nova: the Nova client + :param key: the keypair object to delete + """ + logger.debug('Deleting keypair - ' + key.name) + nova.keypairs.delete(key) + + +def get_floating_ip_pools(nova): + """ + Returns all of the available floating IP pools + :param nova: the Nova client + :return: a list of pools + """ + return nova.floating_ip_pools.list() + + +def get_floating_ips(nova): + """ + Returns all of the floating IPs + :param nova: the Nova client + :return: a list of floating IPs + """ + return nova.floating_ips.list() + + +def create_floating_ip(nova, ext_net_name): + """ + Returns the floating IP object that was created with this call + :param nova: the Nova client + :param ext_net_name: the name of the external network on which to apply the floating IP address + :return: the floating IP object + """ + logger.info('Creating floating ip to external network - ' + ext_net_name) + return nova.floating_ips.create(ext_net_name) + + +def get_floating_ip(nova, floating_ip): + """ + Returns a floating IP object that should be identical to the floating_ip parameter + :param nova: the Nova client + :param floating_ip: the floating IP object to lookup + :return: hopefully the same floating IP object input + """ + logger.debug('Attempting to retrieve existing floating ip with IP - ' + floating_ip.ip) + return nova.floating_ips.get(floating_ip) + + +def delete_floating_ip(nova, floating_ip): + """ + Responsible for deleting a floating IP + :param nova: the Nova client + :param floating_ip: the floating IP object to delete + :return: + """ + logger.debug('Attempting to delete existing floating ip with IP - ' + floating_ip.ip) + return nova.floating_ips.delete(floating_ip) + + +def get_nova_availability_zones(nova): + """ + Returns the names of all nova compute servers + :param nova: the Nova client + :return: a list of compute server names + """ + out = list() + zones = nova.availability_zones.list() + for zone in zones: + if zone.zoneName == 'nova': + for key, host in zone.hosts.iteritems(): + out.append(zone.zoneName + ':' + key) + + return out + + +def delete_vm_instance(nova, vm_inst): + """ + Deletes a VM instance + :param nova: the nova client + :param vm_inst: the OpenStack instance object to delete + """ + nova.servers.delete(vm_inst) + + +def get_flavor_by_name(nova, name): + """ + Returns a flavor by name + :param nova: the Nova client + :param name: the flavor name to return + :return: the OpenStack flavor object or None if not exists + """ + try: + return nova.flavors.find(name=name) + except NotFound: + return None + + +def create_flavor(nova, flavor_settings): + """ + Creates and returns and OpenStack flavor object + :param nova: the Nova client + :param flavor_settings: the flavor settings + :return: the Flavor + """ + return nova.flavors.create(name=flavor_settings.name, flavorid=flavor_settings.flavor_id, ram=flavor_settings.ram, + vcpus=flavor_settings.vcpus, disk=flavor_settings.disk, + ephemeral=flavor_settings.ephemeral, swap=flavor_settings.swap, + rxtx_factor=flavor_settings.rxtx_factor, is_public=flavor_settings.is_public) + + +def delete_flavor(nova, flavor): + """ + Deletes a flavor + :param nova: the Nova client + :param flavor: the OpenStack flavor object + """ + nova.flavors.delete(flavor) + + +def add_security_group(nova, vm, security_group_name): + """ + Adds a security group to an existing VM + :param nova: the nova client + :param vm: the OpenStack server object (VM) to alter + :param security_group_name: the name of the security group to add + """ + nova.servers.add_security_group(vm.id, security_group_name) + + +def remove_security_group(nova, vm, security_group): + """ + Removes a security group from an existing VM + :param nova: the nova client + :param vm: the OpenStack server object (VM) to alter + :param security_group: the OpenStack security group object to add + """ + nova.servers.remove_security_group(vm.id, security_group) diff --git a/snaps/openstack/utils/tests/__init__.py b/snaps/openstack/utils/tests/__init__.py new file mode 100644 index 0000000..7f92908 --- /dev/null +++ b/snaps/openstack/utils/tests/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +__author__ = 'spisarski' \ No newline at end of file diff --git a/snaps/openstack/utils/tests/glance_utils_tests.py b/snaps/openstack/utils/tests/glance_utils_tests.py new file mode 100644 index 0000000..d13908b --- /dev/null +++ b/snaps/openstack/utils/tests/glance_utils_tests.py @@ -0,0 +1,115 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import os +import shutil +import uuid + +from snaps import file_utils +from snaps.openstack.tests import openstack_tests + +from snaps.openstack.utils import nova_utils +from snaps.openstack.tests import validation_utils +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import glance_utils + +__author__ = 'spisarski' + + +class GlanceSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the neutron client can communicate with the cloud + """ + + def test_glance_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + glance = glance_utils.glance_client(self.os_creds) + + users = glance.images.list() + self.assertIsNotNone(users) + + def test_glance_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + with self.assertRaises(Exception): + neutron = glance_utils.glance_client(OSCreds('user', 'pass', 'url', 'project')) + neutron.list_networks() + + +class GlanceUtilsTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = uuid.uuid4() + self.image_name = self.__class__.__name__ + '-' + str(guid) + self.image = None + self.nova = nova_utils.nova_client(self.os_creds) + self.glance = glance_utils.glance_client(self.os_creds) + + self.tmp_dir = 'tmp/' + str(guid) + if not os.path.exists(self.tmp_dir): + os.makedirs(self.tmp_dir) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.image: + glance_utils.delete_image(self.glance, self.image) + + if os.path.exists(self.tmp_dir) and os.path.isdir(self.tmp_dir): + shutil.rmtree(self.tmp_dir) + + def test_create_image_minimal_url(self): + """ + Tests the glance_utils.create_image() function with a URL + """ + os_image_settings = openstack_tests.cirros_url_image(name=self.image_name) + + self.image = glance_utils.create_image(self.glance, os_image_settings) + self.assertIsNotNone(self.image) + + self.assertEqual(self.image_name, self.image.name) + + image = glance_utils.get_image(self.nova, self.glance, os_image_settings.name) + self.assertIsNotNone(image) + + validation_utils.objects_equivalent(self.image, image) + + def test_create_image_minimal_file(self): + """ + Tests the glance_utils.create_image() function with a file + """ + url_image_settings = openstack_tests.cirros_url_image('foo') + image_file = file_utils.download(url_image_settings.url, self.tmp_dir) + file_image_settings = openstack_tests.file_image_test_settings(name=self.image_name, file_path=image_file.name) + + self.image = glance_utils.create_image(self.glance, file_image_settings) + self.assertIsNotNone(self.image) + self.assertEqual(self.image_name, self.image.name) + + image = glance_utils.get_image(self.nova, self.glance, file_image_settings.name) + self.assertIsNotNone(image) + validation_utils.objects_equivalent(self.image, image) diff --git a/snaps/openstack/utils/tests/keystone_utils_tests.py b/snaps/openstack/utils/tests/keystone_utils_tests.py new file mode 100644 index 0000000..76a43ef --- /dev/null +++ b/snaps/openstack/utils/tests/keystone_utils_tests.py @@ -0,0 +1,100 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid + +from snaps.openstack.create_project import ProjectSettings +from snaps.openstack.create_user import UserSettings +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.utils import keystone_utils + +__author__ = 'spisarski' + + +class KeystoneSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the neutron client can communicate with the cloud + """ + + def test_keystone_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + keystone = keystone_utils.keystone_client(self.os_creds) + + users = keystone.users.list() + self.assertIsNotNone(users) + + def test_keystone_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + with self.assertRaises(Exception): + keystone = keystone_utils.keystone_client(OSCreds('user', 'pass', 'url', 'project')) + keystone.users.list() + + +class KeystoneUtilsTests(OSComponentTestCase): + """ + Test for the CreateImage class defined in create_image.py + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = uuid.uuid4() + self.username = self.__class__.__name__ + '-' + str(guid) + self.user = None + + self.project_name = self.__class__.__name__ + '-' + str(guid) + self.project = None + self.keystone = keystone_utils.keystone_client(self.os_creds) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.project: + keystone_utils.delete_project(self.keystone, self.project) + + if self.user: + keystone_utils.delete_user(self.keystone, self.user) + + def test_create_user_minimal(self): + """ + Tests the keystone_utils.create_user() function + """ + user_settings = UserSettings(name=self.username, password='test123') + self.user = keystone_utils.create_user(self.keystone, user_settings) + self.assertEqual(self.username, self.user.name) + + user = keystone_utils.get_user(self.keystone, self.username) + self.assertIsNotNone(user) + self.assertEqual(self.user, user) + + def test_create_project_minimal(self): + """ + Tests the keyston_utils.create_project() funtion + """ + project_settings = ProjectSettings(name=self.project_name) + self.project = keystone_utils.create_project(self.keystone, project_settings) + self.assertEquals(self.project_name, self.project.name) + + project = keystone_utils.get_project(keystone=self.keystone, project_name=project_settings.name) + self.assertIsNotNone(project) + self.assertEquals(self.project_name, self.project.name) diff --git a/snaps/openstack/utils/tests/neutron_utils_tests.py b/snaps/openstack/utils/tests/neutron_utils_tests.py new file mode 100644 index 0000000..5f95fc9 --- /dev/null +++ b/snaps/openstack/utils/tests/neutron_utils_tests.py @@ -0,0 +1,651 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import uuid + +from snaps.openstack.utils import keystone_utils +from snaps.openstack.create_security_group import SecurityGroupSettings, SecurityGroupRuleSettings, Direction +from snaps.openstack.tests import openstack_tests +from snaps.openstack.utils import neutron_utils +from snaps.openstack.create_network import NetworkSettings, SubnetSettings, PortSettings +from snaps.openstack import create_router +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.tests import validation_utils + +__author__ = 'spisarski' + +ip_1 = '10.55.1.100' +ip_2 = '10.55.1.200' + + +class NeutronSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the neutron client can communicate with the cloud + """ + + def test_neutron_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + neutron = neutron_utils.neutron_client(self.os_creds) + + networks = neutron.list_networks() + + found = False + networks = networks.get('networks') + for network in networks: + if network.get('name') == self.ext_net_name: + found = True + self.assertTrue(found) + + def test_neutron_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + with self.assertRaises(Exception): + neutron = neutron_utils.neutron_client( + OSCreds(username='user', password='pass', auth_url='url', project_name='project')) + neutron.list_networks() + + def test_retrieve_ext_network_name(self): + """ + Tests the neutron_utils.get_external_network_names to ensure the configured self.ext_net_name is contained + within the returned list + :return: + """ + neutron = neutron_utils.neutron_client(self.os_creds) + ext_networks = neutron_utils.get_external_networks(neutron) + found = False + for network in ext_networks: + if network['network']['name'] == self.ext_net_name: + found = True + break + self.assertTrue(found) + + +class NeutronUtilsNetworkTests(OSComponentTestCase): + """ + Test for creating networks via neutron_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.port_name = str(guid) + '-port' + self.neutron = neutron_utils.neutron_client(self.os_creds) + self.network = None + self.net_config = openstack_tests.get_pub_net_config(net_name=guid + '-pub-net') + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.network: + neutron_utils.delete_network(self.neutron, self.network) + validate_network(self.neutron, self.network['network']['name'], False) + + def test_create_network(self): + """ + Tests the neutron_utils.create_neutron_net() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + def test_create_network_empty_name(self): + """ + Tests the neutron_utils.create_neutron_net() function with an empty network name + """ + with self.assertRaises(Exception): + self.network = neutron_utils.create_network(self.neutron, NetworkSettings(name='')) + + def test_create_network_null_name(self): + """ + Tests the neutron_utils.create_neutron_net() function when the network name is None + """ + with self.assertRaises(Exception): + self.network = neutron_utils.create_network(self.neutron, NetworkSettings()) + + +class NeutronUtilsSubnetTests(OSComponentTestCase): + """ + Test for creating networks with subnets via neutron_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.port_name = str(guid) + '-port' + self.neutron = neutron_utils.neutron_client(self.os_creds) + self.network = None + self.subnet = None + self.net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', external_net=self.ext_net_name) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.subnet: + neutron_utils.delete_subnet(self.neutron, self.subnet) + validate_subnet(self.neutron, self.subnet.get('name'), + self.net_config.network_settings.subnet_settings[0].cidr, False) + + if self.network: + neutron_utils.delete_network(self.neutron, self.network) + validate_network(self.neutron, self.network['network']['name'], False) + + def test_create_subnet(self): + """ + Tests the neutron_utils.create_neutron_net() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, network=self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + def test_create_subnet_null_name(self): + """ + Tests the neutron_utils.create_neutron_subnet() function for an Exception when the subnet name is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + with self.assertRaises(Exception): + SubnetSettings(cidr=self.net_config.subnet_cidr) + + def test_create_subnet_empty_name(self): + """ + Tests the neutron_utils.create_neutron_net() function with an empty name + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, network=self.network) + validate_subnet(self.neutron, '', self.net_config.network_settings.subnet_settings[0].cidr, True) + + def test_create_subnet_null_cidr(self): + """ + Tests the neutron_utils.create_neutron_subnet() function for an Exception when the subnet CIDR value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + with self.assertRaises(Exception): + sub_sets = SubnetSettings(cidr=None, name=self.net_config.subnet_name) + neutron_utils.create_subnet(self.neutron, sub_sets, self.os_creds, network=self.network) + + def test_create_subnet_empty_cidr(self): + """ + Tests the neutron_utils.create_neutron_subnet() function for an Exception when the subnet CIDR value is empty + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + with self.assertRaises(Exception): + sub_sets = SubnetSettings(cidr='', name=self.net_config.subnet_name) + neutron_utils.create_subnet(self.neutron, sub_sets, self.os_creds, network=self.network) + + +class NeutronUtilsRouterTests(OSComponentTestCase): + """ + Test for creating routers via neutron_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.port_name = str(guid) + '-port' + self.neutron = neutron_utils.neutron_client(self.os_creds) + self.network = None + self.subnet = None + self.port = None + self.router = None + self.interface_router = None + self.net_config = openstack_tests.get_pub_net_config( + net_name=guid + '-pub-net', subnet_name=guid + '-pub-subnet', + router_name=guid + '-pub-router', external_net=self.ext_net_name) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + if self.interface_router: + neutron_utils.remove_interface_router(self.neutron, self.router, self.subnet) + + if self.router: + neutron_utils.delete_router(self.neutron, self.router) + validate_router(self.neutron, self.router.get('name'), False) + + if self.port: + neutron_utils.delete_port(self.neutron, self.port) + + if self.subnet: + neutron_utils.delete_subnet(self.neutron, self.subnet) + validate_subnet(self.neutron, self.subnet.get('name'), + self.net_config.network_settings.subnet_settings[0].cidr, False) + + if self.network: + neutron_utils.delete_network(self.neutron, self.network) + validate_network(self.neutron, self.network['network']['name'], False) + + def test_create_router_simple(self): + """ + Tests the neutron_utils.create_neutron_net() function when an external gateway is requested + """ + self.router = neutron_utils.create_router(self.neutron, self.os_creds, self.net_config.router_settings) + validate_router(self.neutron, self.net_config.router_settings.name, True) + + def test_create_router_with_public_interface(self): + """ + Tests the neutron_utils.create_neutron_net() function when an external gateway is requested + """ + self.net_config = openstack_tests.OSNetworkConfig( + self.net_config.network_settings.name, + self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, self.net_config.router_settings.name, + self.ext_net_name) + self.router = neutron_utils.create_router(self.neutron, self.os_creds, self.net_config.router_settings) + validate_router(self.neutron, self.net_config.router_settings.name, True) + # TODO - Add validation that the router gatway has been set + + def test_create_router_empty_name(self): + """ + Tests the neutron_utils.create_neutron_net() function + """ + with self.assertRaises(Exception): + this_router_settings = create_router.RouterSettings(name='') + self.router = neutron_utils.create_router(self.neutron, self.os_creds, this_router_settings) + + def test_create_router_null_name(self): + """ + Tests the neutron_utils.create_neutron_subnet() function when the subnet CIDR value is None + """ + with self.assertRaises(Exception): + this_router_settings = create_router.RouterSettings() + self.router = neutron_utils.create_router(self.neutron, self.os_creds, this_router_settings) + validate_router(self.neutron, None, True) + + def test_add_interface_router(self): + """ + Tests the neutron_utils.add_interface_router() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + self.router = neutron_utils.create_router(self.neutron, self.os_creds, self.net_config.router_settings) + validate_router(self.neutron, self.net_config.router_settings.name, True) + + self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet) + validate_interface_router(self.interface_router, self.router, self.subnet) + + def test_add_interface_router_null_router(self): + """ + Tests the neutron_utils.add_interface_router() function for an Exception when the router value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet) + + def test_add_interface_router_null_subnet(self): + """ + Tests the neutron_utils.add_interface_router() function for an Exception when the subnet value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.router = neutron_utils.create_router(self.neutron, self.os_creds, self.net_config.router_settings) + validate_router(self.neutron, self.net_config.router_settings.name, True) + + with self.assertRaises(Exception): + self.interface_router = neutron_utils.add_interface_router(self.neutron, self.router, self.subnet) + + def test_create_port(self): + """ + Tests the neutron_utils.create_port() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + self.port = neutron_utils.create_port( + self.neutron, self.os_creds, PortSettings( + name=self.port_name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}], + network_name=self.net_config.network_settings.name)) + validate_port(self.neutron, self.port, self.port_name) + + def test_create_port_empty_name(self): + """ + Tests the neutron_utils.create_port() function + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + self.port = neutron_utils.create_port( + self.neutron, self.os_creds, PortSettings( + name=self.port_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings[0].name, 'ip': ip_1}])) + validate_port(self.neutron, self.port, self.port_name) + + def test_create_port_null_name(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the port name value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': ip_1}])) + + def test_create_port_null_network_object(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the network object is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + self.neutron, self.port_name, self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': ip_1}])) + + def test_create_port_null_ip(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the IP value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + name=self.port_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': None}])) + + def test_create_port_invalid_ip(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the IP value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + name=self.port_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, 'ip': 'foo'}])) + + def test_create_port_invalid_ip_to_subnet(self): + """ + Tests the neutron_utils.create_port() function for an Exception when the IP value is None + """ + self.network = neutron_utils.create_network(self.neutron, self.os_creds, self.net_config.network_settings) + self.assertEqual(self.net_config.network_settings.name, self.network['network']['name']) + self.assertTrue(validate_network(self.neutron, self.net_config.network_settings.name, True)) + + self.subnet = neutron_utils.create_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0], + self.os_creds, self.network) + validate_subnet(self.neutron, self.net_config.network_settings.subnet_settings[0].name, + self.net_config.network_settings.subnet_settings[0].cidr, True) + + with self.assertRaises(Exception): + self.port = neutron_utils.create_port(self.neutron, self.os_creds, PortSettings( + name=self.port_name, network_name=self.net_config.network_settings.name, + ip_addrs=[{'subnet_name': self.net_config.network_settings.subnet_settings.name, + 'ip': '10.197.123.100'}])) + + +class NeutronUtilsSecurityGroupTests(OSComponentTestCase): + """ + Test for creating security groups via neutron_utils.py + """ + + def setUp(self): + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.sec_grp_name = guid + 'name' + + self.security_group = None + self.security_group_rules = list() + self.neutron = neutron_utils.neutron_client(self.os_creds) + self.keystone = keystone_utils.keystone_client(self.os_creds) + + def tearDown(self): + """ + Cleans the remote OpenStack objects + """ + for rule in self.security_group_rules: + neutron_utils.delete_security_group_rule(self.neutron, rule) + + if self.security_group: + neutron_utils.delete_security_group(self.neutron, self.security_group) + + def test_create_delete_simple_sec_grp(self): + """ + Tests the neutron_utils.create_security_group() function + """ + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name) + self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings) + + self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name']) + + sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + self.assertIsNotNone(sec_grp_get) + self.assertTrue(validation_utils.objects_equivalent( + self.security_group['security_group'], sec_grp_get['security_group'])) + + neutron_utils.delete_security_group(self.neutron, self.security_group) + sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + self.assertIsNone(sec_grp_get) + self.security_group = None + + def test_create_sec_grp_no_name(self): + """ + Tests the SecurityGroupSettings constructor and neutron_utils.create_security_group() function to ensure + that attempting to create a security group without a name will raise an exception + """ + with self.assertRaises(Exception): + sec_grp_settings = SecurityGroupSettings() + self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings) + + def test_create_sec_grp_no_rules(self): + """ + Tests the neutron_utils.create_security_group() function + """ + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group') + self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings) + + self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name']) + self.assertTrue(sec_grp_settings.description, self.security_group['security_group']['description']) + + sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + self.assertIsNotNone(sec_grp_get) + self.assertTrue(validation_utils.objects_equivalent( + self.security_group['security_group'], sec_grp_get['security_group'])) + + def test_create_sec_grp_one_rule(self): + """ + Tests the neutron_utils.create_security_group() function + """ + + sec_grp_rule_settings = SecurityGroupRuleSettings(sec_grp_name=self.sec_grp_name, direction=Direction.ingress) + sec_grp_settings = SecurityGroupSettings(name=self.sec_grp_name, description='hello group', + rule_settings=[sec_grp_rule_settings]) + + self.security_group = neutron_utils.create_security_group(self.neutron, self.keystone, sec_grp_settings) + free_rules = neutron_utils.get_rules_by_security_group(self.neutron, self.security_group) + for free_rule in free_rules: + self.security_group_rules.append(free_rule) + + self.security_group_rules.append( + neutron_utils.create_security_group_rule(self.neutron, sec_grp_settings.rule_settings[0])) + + # Refresh object so it is populated with the newly added rule + self.security_group = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + + rules = neutron_utils.get_rules_by_security_group(self.neutron, self.security_group) + + self.assertTrue(validation_utils.objects_equivalent(self.security_group_rules, rules)) + + self.assertTrue(sec_grp_settings.name, self.security_group['security_group']['name']) + self.assertTrue(sec_grp_settings.description, self.security_group['security_group']['description']) + + sec_grp_get = neutron_utils.get_security_group(self.neutron, sec_grp_settings.name) + self.assertIsNotNone(sec_grp_get) + self.assertTrue(validation_utils.objects_equivalent( + self.security_group['security_group'], sec_grp_get['security_group'])) + + +""" +Validation routines +""" + + +def validate_network(neutron, name, exists): + """ + Returns true if a network for a given name DOES NOT exist if the exists parameter is false conversely true. + Returns false if a network for a given name DOES exist if the exists parameter is true conversely false. + :param neutron: The neutron client + :param name: The expected network name + :param exists: Whether or not the network name should exist or not + :return: True/False + """ + network = neutron_utils.get_network(neutron, name) + if exists and network: + return True + if not exists and not network: + return True + return False + + +def validate_subnet(neutron, name, cidr, exists): + """ + Returns true if a subnet for a given name DOES NOT exist if the exists parameter is false conversely true. + Returns false if a subnet for a given name DOES exist if the exists parameter is true conversely false. + :param neutron: The neutron client + :param name: The expected subnet name + :param cidr: The expected CIDR value + :param exists: Whether or not the network name should exist or not + :return: True/False + """ + subnet = neutron_utils.get_subnet_by_name(neutron, name) + if exists and subnet: + return subnet.get('cidr') == cidr + if not exists and not subnet: + return True + return False + + +def validate_router(neutron, name, exists): + """ + Returns true if a router for a given name DOES NOT exist if the exists parameter is false conversely true. + Returns false if a router for a given name DOES exist if the exists parameter is true conversely false. + :param neutron: The neutron client + :param name: The expected router name + :param exists: Whether or not the network name should exist or not + :return: True/False + """ + router = neutron_utils.get_router_by_name(neutron, name) + if exists and router: + return True + return False + + +def validate_interface_router(interface_router, router, subnet): + """ + Returns true if the router ID & subnet ID have been properly included into the interface router object + :param interface_router: the object to validate + :param router: to validate against the interface_router + :param subnet: to validate against the interface_router + :return: True if both IDs match else False + """ + subnet_id = interface_router.get('subnet_id') + router_id = interface_router.get('port_id') + + return subnet.get('id') == subnet_id and router.get('id') == router_id + + +def validate_port(neutron, port_obj, this_port_name): + """ + Returns true if a port for a given name DOES NOT exist if the exists parameter is false conversely true. + Returns false if a port for a given name DOES exist if the exists parameter is true conversely false. + :param neutron: The neutron client + :param port_obj: The port object to lookup + :param this_port_name: The expected router name + :return: True/False + """ + ports = neutron.list_ports() + for port, port_insts in ports.iteritems(): + for inst in port_insts: + if inst['id'] == port_obj['port']['id']: + return inst['name'] == this_port_name + return False diff --git a/snaps/openstack/utils/tests/nova_utils_tests.py b/snaps/openstack/utils/tests/nova_utils_tests.py new file mode 100644 index 0000000..f6c9156 --- /dev/null +++ b/snaps/openstack/utils/tests/nova_utils_tests.py @@ -0,0 +1,208 @@ +# Copyright (c) 2016 Cable Television Laboratories, Inc. ("CableLabs") +# and others. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at: +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging +import os +import uuid + +from Crypto.PublicKey import RSA + +from snaps.openstack.utils import nova_utils +from snaps.openstack.tests.os_source_file_test import OSComponentTestCase +from snaps.openstack.create_flavor import FlavorSettings + +__author__ = 'spisarski' + +logger = logging.getLogger('nova_utils_tests') + + +class NovaSmokeTests(OSComponentTestCase): + """ + Tests to ensure that the nova client can communicate with the cloud + """ + + def test_nova_connect_success(self): + """ + Tests to ensure that the proper credentials can connect. + """ + nova = nova_utils.nova_client(self.os_creds) + + # This should not throw an exception + nova.flavors.list() + + def test_nova_connect_fail(self): + """ + Tests to ensure that the improper credentials cannot connect. + """ + from snaps.openstack.os_credentials import OSCreds + + nova = nova_utils.nova_client( + OSCreds(username='user', password='pass', auth_url=self.os_creds.auth_url, + project_name=self.os_creds.project_name, proxy_settings=self.os_creds.proxy_settings)) + + # This should throw an exception + with self.assertRaises(Exception): + nova.flavors.list() + + +class NovaUtilsKeypairTests(OSComponentTestCase): + """ + Test basic nova keypair functionality + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.priv_key_file_path = 'tmp/' + guid + self.pub_key_file_path = self.priv_key_file_path + '.pub' + + self.nova = nova_utils.nova_client(self.os_creds) + self.keys = RSA.generate(1024) + self.public_key = self.keys.publickey().exportKey('OpenSSH') + self.keypair_name = guid + self.keypair = None + self.floating_ip = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.keypair: + try: + nova_utils.delete_keypair(self.nova, self.keypair) + except: + pass + + try: + os.remove(self.priv_key_file_path) + except: + pass + + try: + os.remove(self.pub_key_file_path) + except: + pass + + if self.floating_ip: + nova_utils.delete_floating_ip(self.nova, self.floating_ip) + + def test_create_keypair(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.keypair = nova_utils.upload_keypair(self.nova, self.keypair_name, self.public_key) + result = nova_utils.keypair_exists(self.nova, self.keypair) + self.assertEquals(self.keypair, result) + keypair = nova_utils.get_keypair_by_name(self.nova, self.keypair_name) + self.assertEquals(self.keypair, keypair) + + def test_create_delete_keypair(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.keypair = nova_utils.upload_keypair(self.nova, self.keypair_name, self.public_key) + result = nova_utils.keypair_exists(self.nova, self.keypair) + self.assertEquals(self.keypair, result) + nova_utils.delete_keypair(self.nova, self.keypair) + result2 = nova_utils.keypair_exists(self.nova, self.keypair) + self.assertIsNone(result2) + + def test_create_key_from_file(self): + """ + Tests that the generated RSA keys are properly saved to files + :return: + """ + nova_utils.save_keys_to_files(self.keys, self.pub_key_file_path, self.priv_key_file_path) + self.keypair = nova_utils.upload_keypair_file(self.nova, self.keypair_name, self.pub_key_file_path) + pub_key = open(os.path.expanduser(self.pub_key_file_path)).read() + self.assertEquals(self.keypair.public_key, pub_key) + + def test_floating_ips(self): + """ + Tests the creation of a floating IP + :return: + """ + ips = nova_utils.get_floating_ips(self.nova) + self.assertIsNotNone(ips) + + self.floating_ip = nova_utils.create_floating_ip(self.nova, self.ext_net_name) + returned = nova_utils.get_floating_ip(self.nova, self.floating_ip) + self.assertEquals(self.floating_ip, returned) + + +class NovaUtilsFlavorTests(OSComponentTestCase): + """ + Test basic nova flavor functionality + """ + + def setUp(self): + """ + Instantiates the CreateImage object that is responsible for downloading and creating an OS image file + within OpenStack + """ + guid = self.__class__.__name__ + '-' + str(uuid.uuid4()) + self.flavor_settings = FlavorSettings(name=guid + '-name', flavor_id=guid + '-id', ram=1, disk=1, vcpus=1, + ephemeral=1, swap=2, rxtx_factor=3.0, is_public=False) + self.nova = nova_utils.nova_client(self.os_creds) + self.flavor = None + + def tearDown(self): + """ + Cleans the image and downloaded image file + """ + if self.flavor: + try: + nova_utils.delete_flavor(self.nova, self.flavor) + except: + pass + + def test_create_flavor(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.flavor = nova_utils.create_flavor(self.nova, self.flavor_settings) + self.validate_flavor() + + def test_create_delete_flavor(self): + """ + Tests the creation of an OpenStack keypair that does not exist. + """ + self.flavor = nova_utils.create_flavor(self.nova, self.flavor_settings) + self.validate_flavor() + nova_utils.delete_flavor(self.nova, self.flavor) + flavor = nova_utils.get_flavor_by_name(self.nova, self.flavor_settings.name) + self.assertIsNone(flavor) + + def validate_flavor(self): + """ + Validates the flavor_settings against the OpenStack flavor object + """ + self.assertIsNotNone(self.flavor) + self.assertEquals(self.flavor_settings.name, self.flavor.name) + self.assertEquals(self.flavor_settings.flavor_id, self.flavor.id) + self.assertEquals(self.flavor_settings.ram, self.flavor.ram) + self.assertEquals(self.flavor_settings.disk, self.flavor.disk) + self.assertEquals(self.flavor_settings.vcpus, self.flavor.vcpus) + self.assertEquals(self.flavor_settings.ephemeral, self.flavor.ephemeral) + + if self.flavor_settings.swap == 0: + self.assertEquals('', self.flavor.swap) + else: + self.assertEquals(self.flavor_settings.swap, self.flavor.swap) + + self.assertEquals(self.flavor_settings.rxtx_factor, self.flavor.rxtx_factor) + self.assertEquals(self.flavor_settings.is_public, self.flavor.is_public) -- cgit 1.2.3-korg