From ae309d644224e1637ece5474abc29a7a6aa6c555 Mon Sep 17 00:00:00 2001 From: Moshe Date: Mon, 18 Mar 2019 08:50:46 +0200 Subject: introduce heat context Issue-ID: VNFSDK-350 Change-Id: I2936ba654109475145ad8bd673c944aea3fcac65 Signed-off-by: Moshe --- coverage.xml | 1783 +++++++++++++++++++++++-------- vnftest/common/constants.py | 28 +- vnftest/common/openstack_utils.py | 1196 +++++++++++---------- vnftest/common/utils.py | 46 +- vnftest/contexts/base.py | 52 +- vnftest/contexts/heat.py | 591 ++++++++++ vnftest/contexts/model.py | 433 ++++++++ vnftest/core/task.py | 26 +- vnftest/orchestrator/__init__.py | 0 vnftest/orchestrator/heat.py | 661 ++++++++++++ vnftest/tests/unit/common/test_utils.py | 13 - 11 files changed, 3766 insertions(+), 1063 deletions(-) create mode 100644 vnftest/contexts/heat.py create mode 100644 vnftest/contexts/model.py create mode 100644 vnftest/orchestrator/__init__.py create mode 100644 vnftest/orchestrator/heat.py diff --git a/coverage.xml b/coverage.xml index eb42172..250900c 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,5 +1,5 @@ - + @@ -525,13 +525,13 @@ - + - + @@ -562,48 +562,37 @@ - - + + - - + + - - - - + + + + + + - - - + - + - - - - - - - - - - - @@ -686,61 +675,232 @@ - + + - + - - - + - - - - - - - - - - - + + + + - - - - - + + + + + + + - + + + - - - + + - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -941,7 +1101,7 @@ - + @@ -1070,228 +1230,237 @@ - - + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - + + + - - + + - - + + + - - - - - + + - + + - + - - - + + + + - + + - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - - - + + + - - - - - - - - + + + + + + + - - - - + + + + + - - - + + + - - - - - - - - - + + + + + + + + + + - + - - + - - - - - - - - + + + + + + + + + + + - + - - - - - - - - - - + + + + + + + + - - + + + + + + - - - + + - - - - - - + + + + + - + + - - - + + + + - - - + + + - - - - - + + - - + + - - + + - - - + + + - + - - + + - - - - - - + + + + + - - - - - - - - - - - - + + + + + + + + + + + + - + + + + + + + + + + @@ -1311,44 +1480,65 @@ - + - + - + - - - + - - - - + + - - - - - + + + + - - - - - + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + @@ -1388,9 +1578,444 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -1569,7 +2194,7 @@ - + @@ -1582,7 +2207,7 @@ - + @@ -1590,7 +2215,6 @@ - @@ -1602,114 +2226,131 @@ + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - + + + + + + - + - - - + + + + - - + + + + + + - - - - - - - + + + + + + + - - - - - + + + - + + - - - - - - - - - + + + + + + + + + - - - - - + + + + + + - - - + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -2182,6 +2823,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -2987,7 +3879,7 @@ - + @@ -3136,7 +4028,7 @@ - + @@ -3287,197 +4179,188 @@ - - + - - + - - - + + + - - + + - - - - - - + + + + + + + - + - + + + - - - - - - - + + + + + + - - - - - - + + + + + - - - - - - - - + + + + - - - + + + + + + - - - - - - - - - + + + + + + - - - - + + + + - + + + + + + + + - - - + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + - + - - + + + + - - + - + - - + - - - + - + + - - - + + + + - - + + + + - + - + - - - + - - + + - - - - - - - - - - - + + + + + + + + + + + + + - + - - + - - - - - - - - - - diff --git a/vnftest/common/constants.py b/vnftest/common/constants.py index 9634708..a613ff0 100644 --- a/vnftest/common/constants.py +++ b/vnftest/common/constants.py @@ -61,26 +61,6 @@ def get_param(key, default=''): raise return default - -try: - SERVER_IP = get_param('api.server_ip') -except KeyError: - try: - from pyroute2 import IPDB - except ImportError: - SERVER_IP = '172.17.0.1' - else: - with IPDB() as ip: - try: - SERVER_IP = ip.routes['default'].gateway - except KeyError: - # during unittests ip.routes['default'] can be invalid - SERVER_IP = '127.0.0.1' - -if not SERVER_IP: - SERVER_IP = '127.0.0.1' - - # dir CONF_DIR = get_param('dir.conf', '/etc/vnftest') IMAGE_DIR = get_param('dir.images', join(VNFTEST_ROOT_PATH, '../../images/')) @@ -111,6 +91,14 @@ BASE_URL = 'http://localhost:5000' ENV_ACTION_API = BASE_URL + '/vnftest/env/action' ASYNC_TASK_API = BASE_URL + '/vnftest/asynctask' +# flags +IS_EXISTING = 'is_existing' +IS_PUBLIC = 'is_public' + # general TESTCASE_PRE = 'onap_vnftest_' TESTSUITE_PRE = 'onap_' + + +# OpenStack cloud default config parameters +OS_CLOUD_DEFAULT_CONFIG = {'verify': False} diff --git a/vnftest/common/openstack_utils.py b/vnftest/common/openstack_utils.py index 1bbdc43..048a9d9 100644 --- a/vnftest/common/openstack_utils.py +++ b/vnftest/common/openstack_utils.py @@ -14,60 +14,49 @@ # vnftest comment: this is a modified copy of # yardstick/common/openstack_utils.py -from __future__ import absolute_import - -import os -import time -import sys +import copy import logging +import os -from keystoneauth1 import loading -from keystoneauth1 import session from cinderclient import client as cinderclient from novaclient import client as novaclient from glanceclient import client as glanceclient +from keystoneauth1 import loading +from keystoneauth1 import session from neutronclient.neutron import client as neutronclient -from heatclient.client import Client as heatclient +import shade +from shade import exc + +from vnftest.common import constants + log = logging.getLogger(__name__) DEFAULT_HEAT_API_VERSION = '1' DEFAULT_API_VERSION = '2' -creds = {} - # ********************************************* # CREDENTIALS # ********************************************* -def initialize(openstack_env_config): - keystone_api_version = openstack_env_config.get('OS_IDENTITY_API_VERSION', None) - - if keystone_api_version is None or keystone_api_version == '2': - keystone_v3 = False - creds['tenant_name'] = openstack_env_config['OS_TENANT_NAME'] - else: - keystone_v3 = True - creds['tenant_name'] = openstack_env_config['OS_PROJECT_NAME'] - creds['project_name'] = openstack_env_config['OS_PROJECT_NAME'] - - creds["username"] = openstack_env_config["OS_USERNAME"] - creds["password"] = openstack_env_config["OS_PASSWORD"] - creds["auth_url"] = openstack_env_config["OS_AUTH_URL"] - creds["tenant_id"] = openstack_env_config["OS_TENANT_ID"] - - if keystone_v3: - if 'OS_USER_DOMAIN_NAME' in openstack_env_config: - creds.update({ - "user_domain_name": openstack_env_config['OS_USER_DOMAIN_NAME'] - }) - if 'OS_PROJECT_DOMAIN_NAME' in openstack_env_config: - creds.update({ - "project_domain_name": openstack_env_config['OS_PROJECT_DOMAIN_NAME'] - }) - - def get_credentials(): + """Returns a creds dictionary filled with parsed from env + + Keystone API version used is 3; v2 was deprecated in 2014 (Icehouse). Along + with this deprecation, environment variable 'OS_TENANT_NAME' is replaced by + 'OS_PROJECT_NAME'. + """ + creds = {'username': os.environ.get('OS_USERNAME'), + 'password': os.environ.get('OS_PASSWORD'), + 'auth_url': os.environ.get('OS_AUTH_URL'), + 'project_name': os.environ.get('OS_PROJECT_NAME') + } + + if os.getenv('OS_USER_DOMAIN_NAME'): + creds['user_domain_name'] = os.getenv('OS_USER_DOMAIN_NAME') + if os.getenv('OS_PROJECT_DOMAIN_NAME'): + creds['project_domain_name'] = os.getenv('OS_PROJECT_DOMAIN_NAME') + return creds @@ -159,11 +148,6 @@ def get_neutron_client(): # pragma: no cover return neutronclient.Client(get_neutron_client_version(), session=sess) -def get_heat_client(): # pragma: no cover - sess = get_session() - return heatclient(get_heat_api_version(), session=sess) - - def get_glance_client_version(): # pragma: no cover try: api_version = os.environ['OS_IMAGE_API_VERSION'] @@ -179,225 +163,226 @@ def get_glance_client(): # pragma: no cover return glanceclient.Client(get_glance_client_version(), session=sess) -# ********************************************* -# NOVA -# ********************************************* -def get_instances(nova_client): # pragma: no cover - try: - return nova_client.servers.list(search_opts={'all_tenants': 1}) - except Exception: - log.exception("Error [get_instances(nova_client)]") - - -def get_instance_status(nova_client, instance): # pragma: no cover - try: - return nova_client.servers.get(instance.id).status - except Exception: - log.exception("Error [get_instance_status(nova_client)]") - - -def get_instance_by_name(nova_client, instance_name): # pragma: no cover - try: - return nova_client.servers.find(name=instance_name) - except Exception: - log.exception("Error [get_instance_by_name(nova_client, '%s')]", - instance_name) - - -def get_instance_by_id(instance_id): # pragma: no cover - try: - return get_nova_client().servers.find(id=instance_id) - except Exception: - log.exception("Error [get_instance_by_id(nova_client, '%s')]", - instance_id) - +def get_shade_client(**os_cloud_config): + """Get Shade OpenStack cloud client -def get_aggregates(nova_client): # pragma: no cover - try: - return nova_client.aggregates.list() - except Exception: - log.exception("Error [get_aggregates(nova_client)]") + By default, the input parameters given to "shade.openstack_cloud" method + are stored in "constants.OS_CLOUD_DEFAULT_CONFIG". The input parameters + passed in this function, "os_cloud_config", will overwrite the default + ones. + :param os_cloud_config: (kwargs) input arguments for + "shade.openstack_cloud" method. + :return: ``shade.OpenStackCloud`` object. + """ + params = copy.deepcopy(constants.OS_CLOUD_DEFAULT_CONFIG) + params.update(os_cloud_config) + return shade.openstack_cloud(**params) -def get_availability_zones(nova_client): # pragma: no cover - try: - return nova_client.availability_zones.list() - except Exception: - log.exception("Error [get_availability_zones(nova_client)]") - +def get_shade_operator_client(**os_cloud_config): + """Get Shade Operator cloud client -def get_availability_zone_names(nova_client): # pragma: no cover - try: - return [az.zoneName for az in get_availability_zones(nova_client)] - except Exception: - log.exception("Error [get_availability_zone_names(nova_client)]") + :return: ``shade.OperatorCloud`` object. + """ + params = copy.deepcopy(constants.OS_CLOUD_DEFAULT_CONFIG) + params.update(os_cloud_config) + return shade.operator_cloud(**params) -def create_aggregate(nova_client, aggregate_name, av_zone): # pragma: no cover - try: - nova_client.aggregates.create(aggregate_name, av_zone) - except Exception: - log.exception("Error [create_aggregate(nova_client, %s, %s)]", - aggregate_name, av_zone) - return False - else: - return True - - -def get_aggregate_id(nova_client, aggregate_name): # pragma: no cover - try: - aggregates = get_aggregates(nova_client) - _id = next((ag.id for ag in aggregates if ag.name == aggregate_name)) - except Exception: - log.exception("Error [get_aggregate_id(nova_client, %s)]", - aggregate_name) - else: - return _id - - -def add_host_to_aggregate(nova_client, aggregate_name, - compute_host): # pragma: no cover - try: - aggregate_id = get_aggregate_id(nova_client, aggregate_name) - nova_client.aggregates.add_host(aggregate_id, compute_host) - except Exception: - log.exception("Error [add_host_to_aggregate(nova_client, %s, %s)]", - aggregate_name, compute_host) - return False - else: - return True - - -def create_aggregate_with_host(nova_client, aggregate_name, av_zone, - compute_host): # pragma: no cover - try: - create_aggregate(nova_client, aggregate_name, av_zone) - add_host_to_aggregate(nova_client, aggregate_name, compute_host) - except Exception: - log.exception("Error [create_aggregate_with_host(" - "nova_client, %s, %s, %s)]", - aggregate_name, av_zone, compute_host) - return False - else: - return True - - -def create_keypair(nova_client, name, key_path=None): # pragma: no cover - try: - with open(key_path) as fpubkey: - keypair = get_nova_client().keypairs.create(name=name, public_key=fpubkey.read()) - return keypair - except Exception: - log.exception("Error [create_keypair(nova_client)]") - - -def create_instance(json_body): # pragma: no cover - try: - return get_nova_client().servers.create(**json_body) - except Exception: - log.exception("Error create instance failed") - return None - - -def create_instance_and_wait_for_active(json_body): # pragma: no cover - SLEEP = 3 - VM_BOOT_TIMEOUT = 180 - nova_client = get_nova_client() - instance = create_instance(json_body) - count = VM_BOOT_TIMEOUT / SLEEP - for n in range(count, -1, -1): - status = get_instance_status(nova_client, instance) - if status.lower() == "active": - return instance - elif status.lower() == "error": - log.error("The instance went to ERROR status.") - return None - time.sleep(SLEEP) - log.error("Timeout booting the instance.") - return None - - -def attach_server_volume(server_id, volume_id, device=None): # pragma: no cover - try: - get_nova_client().volumes.create_server_volume(server_id, volume_id, device) - except Exception: - log.exception("Error [attach_server_volume(nova_client, '%s', '%s')]", - server_id, volume_id) - return False - else: +# ********************************************* +# NOVA +# ********************************************* +def create_keypair(shade_client, name, public_key=None): + """Create a new keypair. + + :param name: Name of the keypair being created. + :param public_key: Public key for the new keypair. + + :return: Created keypair. + """ + try: + return shade_client.create_keypair(name, public_key=public_key) + except exc.OpenStackCloudException as o_exc: + log.error("Error [create_keypair(shade_client)]. " + "Exception message, '%s'", o_exc.orig_message) + + +def create_instance_and_wait_for_active(shade_client, name, image, + flavor, auto_ip=True, ips=None, + ip_pool=None, root_volume=None, + terminate_volume=False, wait=True, + timeout=180, reuse_ips=True, + network=None, boot_from_volume=False, + volume_size='20', boot_volume=None, + volumes=None, nat_destination=None, + **kwargs): + """Create a virtual server instance. + + :param name:(string) Name of the server. + :param image:(dict) Image dict, name or ID to boot with. Image is required + unless boot_volume is given. + :param flavor:(dict) Flavor dict, name or ID to boot onto. + :param auto_ip: Whether to take actions to find a routable IP for + the server. + :param ips: List of IPs to attach to the server. + :param ip_pool:(string) Name of the network or floating IP pool to get an + address from. + :param root_volume:(string) Name or ID of a volume to boot from. + (defaults to None - deprecated, use boot_volume) + :param boot_volume:(string) Name or ID of a volume to boot from. + :param terminate_volume:(bool) If booting from a volume, whether it should + be deleted when the server is destroyed. + :param volumes:(optional) A list of volumes to attach to the server. + :param wait:(optional) Wait for the address to appear as assigned to the server. + :param timeout: Seconds to wait, defaults to 60. + :param reuse_ips:(bool)Whether to attempt to reuse pre-existing + floating ips should a floating IP be needed. + :param network:(dict) Network dict or name or ID to attach the server to. + Mutually exclusive with the nics parameter. Can also be be + a list of network names or IDs or network dicts. + :param boot_from_volume:(bool) Whether to boot from volume. 'boot_volume' + implies True, but boot_from_volume=True with + no boot_volume is valid and will create a + volume from the image and use that. + :param volume_size: When booting an image from volume, how big should + the created volume be? + :param nat_destination: Which network should a created floating IP + be attached to, if it's not possible to infer from + the cloud's configuration. + :param meta:(optional) A dict of arbitrary key/value metadata to store for + this server. Both keys and values must be <=255 characters. + :param reservation_id: A UUID for the set of servers being requested. + :param min_count:(optional extension) The minimum number of servers to + launch. + :param max_count:(optional extension) The maximum number of servers to + launch. + :param security_groups: A list of security group names. + :param userdata: User data to pass to be exposed by the metadata server + this can be a file type object as well or a string. + :param key_name:(optional extension) Name of previously created keypair to + inject into the instance. + :param availability_zone: Name of the availability zone for instance + placement. + :param block_device_mapping:(optional) A dict of block device mappings for + this server. + :param block_device_mapping_v2:(optional) A dict of block device mappings + for this server. + :param nics:(optional extension) An ordered list of nics to be added to + this server, with information about connected networks, fixed + IPs, port etc. + :param scheduler_hints:(optional extension) Arbitrary key-value pairs + specified by the client to help boot an instance. + :param config_drive:(optional extension) Value for config drive either + boolean, or volume-id. + :param disk_config:(optional extension) Control how the disk is partitioned + when the server is created. Possible values are 'AUTO' + or 'MANUAL'. + :param admin_pass:(optional extension) Add a user supplied admin password. + + :returns: The created server. + """ + try: + return shade_client.create_server( + name, image, flavor, auto_ip=auto_ip, ips=ips, ip_pool=ip_pool, + root_volume=root_volume, terminate_volume=terminate_volume, + wait=wait, timeout=timeout, reuse_ips=reuse_ips, network=network, + boot_from_volume=boot_from_volume, volume_size=volume_size, + boot_volume=boot_volume, volumes=volumes, + nat_destination=nat_destination, **kwargs) + except exc.OpenStackCloudException as o_exc: + log.error("Error [create_instance(shade_client)]. " + "Exception message, '%s'", o_exc.orig_message) + + +def attach_volume_to_server(shade_client, server_name_or_id, volume_name_or_id, + device=None, wait=True, timeout=None): + """Attach a volume to a server. + + This will attach a volume, described by the passed in volume + dict, to the server described by the passed in server dict on the named + device on the server. + + If the volume is already attached to the server, or generally not + available, then an exception is raised. To re-attach to a server, + but under a different device, the user must detach it first. + + :param server_name_or_id:(string) The server name or id to attach to. + :param volume_name_or_id:(string) The volume name or id to attach. + :param device:(string) The device name where the volume will attach. + :param wait:(bool) If true, waits for volume to be attached. + :param timeout: Seconds to wait for volume attachment. None is forever. + + :returns: True if attached successful, False otherwise. + """ + try: + server = shade_client.get_server(name_or_id=server_name_or_id) + volume = shade_client.get_volume(volume_name_or_id) + shade_client.attach_volume( + server, volume, device=device, wait=wait, timeout=timeout) return True - - -def delete_instance(nova_client, instance_id): # pragma: no cover - try: - nova_client.servers.force_delete(instance_id) - except Exception: - log.exception("Error [delete_instance(nova_client, '%s')]", - instance_id) + except exc.OpenStackCloudException as o_exc: + log.error("Error [attach_volume_to_server(shade_client)]. " + "Exception message: %s", o_exc.orig_message) return False - else: - return True -def remove_host_from_aggregate(nova_client, aggregate_name, - compute_host): # pragma: no cover - try: - aggregate_id = get_aggregate_id(nova_client, aggregate_name) - nova_client.aggregates.remove_host(aggregate_id, compute_host) - except Exception: - log.exception("Error remove_host_from_aggregate(nova_client, %s, %s)", - aggregate_name, compute_host) +def delete_instance(shade_client, name_or_id, wait=False, timeout=180, + delete_ips=False, delete_ip_retry=1): + """Delete a server instance. + + :param name_or_id: name or ID of the server to delete + :param wait:(bool) If true, waits for server to be deleted. + :param timeout:(int) Seconds to wait for server deletion. + :param delete_ips:(bool) If true, deletes any floating IPs associated with + the instance. + :param delete_ip_retry:(int) Number of times to retry deleting + any floating ips, should the first try be + unsuccessful. + :returns: True if delete succeeded, False otherwise. + """ + try: + return shade_client.delete_server( + name_or_id, wait=wait, timeout=timeout, delete_ips=delete_ips, + delete_ip_retry=delete_ip_retry) + except exc.OpenStackCloudException as o_exc: + log.error("Error [delete_instance(shade_client, '%s')]. " + "Exception message: %s", name_or_id, + o_exc.orig_message) return False - else: - return True - -def remove_hosts_from_aggregate(nova_client, - aggregate_name): # pragma: no cover - aggregate_id = get_aggregate_id(nova_client, aggregate_name) - hosts = nova_client.aggregates.get(aggregate_id).hosts - assert( - all(remove_host_from_aggregate(nova_client, aggregate_name, host) - for host in hosts)) +def get_server(shade_client, name_or_id=None, filters=None, detailed=False, + bare=False): + """Get a server by name or ID. -def delete_aggregate(nova_client, aggregate_name): # pragma: no cover - try: - remove_hosts_from_aggregate(nova_client, aggregate_name) - nova_client.aggregates.delete(aggregate_name) - except Exception: - log.exception("Error [delete_aggregate(nova_client, %s)]", - aggregate_name) - return False - else: - return True - + :param name_or_id: Name or ID of the server. + :param filters:(dict) A dictionary of meta data to use for further + filtering. + :param detailed:(bool) Whether or not to add detailed additional + information. + :param bare:(bool) Whether to skip adding any additional information to the + server record. -def get_server_by_name(name): # pragma: no cover + :returns: A server ``munch.Munch`` or None if no matching server is found. + """ try: - return get_nova_client().servers.list(search_opts={'name': name})[0] - except IndexError: - log.exception('Failed to get nova client') - raise + return shade_client.get_server(name_or_id=name_or_id, filters=filters, + detailed=detailed, bare=bare) + except exc.OpenStackCloudException as o_exc: + log.error("Error [get_server(shade_client, '%s')]. " + "Exception message: %s", name_or_id, o_exc.orig_message) def create_flavor(name, ram, vcpus, disk, **kwargs): # pragma: no cover try: - return get_nova_client().flavors.create(name, ram, vcpus, disk, **kwargs) - except Exception: + return get_nova_client().flavors.create(name, ram, vcpus, + disk, **kwargs) + except Exception: # pylint: disable=broad-except log.exception("Error [create_flavor(nova_client, %s, %s, %s, %s, %s)]", name, ram, disk, vcpus, kwargs['is_public']) return None -def get_image_by_name(name): # pragma: no cover - images = get_nova_client().images.list() - try: - return next((a for a in images if a.name == name)) - except StopIteration: - log.exception('No image matched') - - def get_flavor_id(nova_client, flavor_name): # pragma: no cover flavors = nova_client.flavors.list(detailed=True) flavor_id = '' @@ -408,405 +393,512 @@ def get_flavor_id(nova_client, flavor_name): # pragma: no cover return flavor_id -def get_flavor_by_name(name): # pragma: no cover - flavors = get_nova_client().flavors.list() - try: - return next((a for a in flavors if a.name == name)) - except StopIteration: - log.exception('No flavor matched') - - -def check_status(status, name, iterations, interval): # pragma: no cover - for i in range(iterations): - try: - server = get_server_by_name(name) - except IndexError: - log.error('Cannot found %s server', name) - raise +def get_flavor(shade_client, name_or_id, filters=None, get_extra=True): + """Get a flavor by name or ID. - if server.status == status: - return True + :param name_or_id: Name or ID of the flavor. + :param filters: A dictionary of meta data to use for further filtering. + :param get_extra: Whether or not the list_flavors call should get the extra + flavor specs. - time.sleep(interval) - return False + :returns: A flavor ``munch.Munch`` or None if no matching flavor is found. + """ + try: + return shade_client.get_flavor(name_or_id, filters=filters, + get_extra=get_extra) + except exc.OpenStackCloudException as o_exc: + log.error("Error [get_flavor(shade_client, '%s')]. " + "Exception message: %s", name_or_id, o_exc.orig_message) def delete_flavor(flavor_id): # pragma: no cover try: get_nova_client().flavors.delete(flavor_id) - except Exception: + except Exception: # pylint: disable=broad-except log.exception("Error [delete_flavor(nova_client, %s)]", flavor_id) return False else: return True -def delete_keypair(nova_client, key): # pragma: no cover +def delete_keypair(shade_client, name): + """Delete a keypair. + + :param name: Name of the keypair to delete. + + :returns: True if delete succeeded, False otherwise. + """ try: - nova_client.keypairs.delete(key=key) - return True - except Exception: - log.exception("Error [delete_keypair(nova_client)]") + return shade_client.delete_keypair(name) + except exc.OpenStackCloudException as o_exc: + log.error("Error [delete_neutron_router(shade_client, '%s')]. " + "Exception message: %s", name, o_exc.orig_message) return False # ********************************************* # NEUTRON # ********************************************* -def get_network_by_name(network_name): # pragma: no cover - try: - networks = get_neutron_client().list_networks()['networks'] - return next((n for n in networks if n['name'] == network_name), None) - except Exception: - log.exception("Error [get_instance_by_id(nova_client, '%s')]", - network_name) - - -def get_network_id(neutron_client, network_name): # pragma: no cover - networks = neutron_client.list_networks()['networks'] - return next((n['id'] for n in networks if n['name'] == network_name), None) - - -def get_port_id_by_ip(neutron_client, ip_address): # pragma: no cover - ports = neutron_client.list_ports()['ports'] - return next((i['id'] for i in ports for j in i.get( - 'fixed_ips') if j['ip_address'] == ip_address), None) - - -def create_neutron_net(neutron_client, json_body): # pragma: no cover - try: - network = neutron_client.create_network(body=json_body) - return network['network']['id'] - except Exception: - log.error("Error [create_neutron_net(neutron_client)]") - raise Exception("operation error") +def create_neutron_net(shade_client, network_name, shared=False, + admin_state_up=True, external=False, provider=None, + project_id=None): + """Create a neutron network. + + :param network_name:(string) name of the network being created. + :param shared:(bool) whether the network is shared. + :param admin_state_up:(bool) set the network administrative state. + :param external:(bool) whether this network is externally accessible. + :param provider:(dict) a dict of network provider options. + :param project_id:(string) specify the project ID this network + will be created on (admin-only). + :returns:(string) the network id. + """ + try: + networks = shade_client.create_network( + name=network_name, shared=shared, admin_state_up=admin_state_up, + external=external, provider=provider, project_id=project_id) + return networks['id'] + except exc.OpenStackCloudException as o_exc: + log.error("Error [create_neutron_net(shade_client)]." + "Exception message, '%s'", o_exc.orig_message) return None -def delete_neutron_net(neutron_client, network_id): # pragma: no cover +def delete_neutron_net(shade_client, network_id): try: - neutron_client.delete_network(network_id) - return True - except Exception: - log.error("Error [delete_neutron_net(neutron_client, '%s')]" % network_id) + return shade_client.delete_network(network_id) + except exc.OpenStackCloudException: + log.error("Error [delete_neutron_net(shade_client, '%s')]", network_id) return False -def create_neutron_subnet(neutron_client, json_body): # pragma: no cover - try: - subnet = neutron_client.create_subnet(body=json_body) - return subnet['subnets'][0]['id'] - except Exception: - log.error("Error [create_neutron_subnet") - raise Exception("operation error") +def create_neutron_subnet(shade_client, network_name_or_id, cidr=None, + ip_version=4, enable_dhcp=False, subnet_name=None, + tenant_id=None, allocation_pools=None, + gateway_ip=None, disable_gateway_ip=False, + dns_nameservers=None, host_routes=None, + ipv6_ra_mode=None, ipv6_address_mode=None, + use_default_subnetpool=False): + """Create a subnet on a specified network. + + :param network_name_or_id:(string) the unique name or ID of the + attached network. If a non-unique name is + supplied, an exception is raised. + :param cidr:(string) the CIDR. + :param ip_version:(int) the IP version. + :param enable_dhcp:(bool) whether DHCP is enable. + :param subnet_name:(string) the name of the subnet. + :param tenant_id:(string) the ID of the tenant who owns the network. + :param allocation_pools: A list of dictionaries of the start and end + addresses for the allocation pools. + :param gateway_ip:(string) the gateway IP address. + :param disable_gateway_ip:(bool) whether gateway IP address is enabled. + :param dns_nameservers: A list of DNS name servers for the subnet. + :param host_routes: A list of host route dictionaries for the subnet. + :param ipv6_ra_mode:(string) IPv6 Router Advertisement mode. + Valid values are: 'dhcpv6-stateful', + 'dhcpv6-stateless', or 'slaac'. + :param ipv6_address_mode:(string) IPv6 address mode. + Valid values are: 'dhcpv6-stateful', + 'dhcpv6-stateless', or 'slaac'. + :param use_default_subnetpool:(bool) use the default subnetpool for + ``ip_version`` to obtain a CIDR. It is + required to pass ``None`` to the ``cidr`` + argument when enabling this option. + :returns:(string) the subnet id. + """ + try: + subnet = shade_client.create_subnet( + network_name_or_id, cidr=cidr, ip_version=ip_version, + enable_dhcp=enable_dhcp, subnet_name=subnet_name, + tenant_id=tenant_id, allocation_pools=allocation_pools, + gateway_ip=gateway_ip, disable_gateway_ip=disable_gateway_ip, + dns_nameservers=dns_nameservers, host_routes=host_routes, + ipv6_ra_mode=ipv6_ra_mode, ipv6_address_mode=ipv6_address_mode, + use_default_subnetpool=use_default_subnetpool) + return subnet['id'] + except exc.OpenStackCloudException as o_exc: + log.error("Error [create_neutron_subnet(shade_client)]. " + "Exception message: %s", o_exc.orig_message) return None -def create_neutron_router(neutron_client, json_body): # pragma: no cover - try: - router = neutron_client.create_router(json_body) - return router['router']['id'] - except Exception: - log.error("Error [create_neutron_router(neutron_client)]") - raise Exception("operation error") - return None +def create_neutron_router(shade_client, name=None, admin_state_up=True, + ext_gateway_net_id=None, enable_snat=None, + ext_fixed_ips=None, project_id=None): + """Create a logical router. + :param name:(string) the router name. + :param admin_state_up:(bool) the administrative state of the router. + :param ext_gateway_net_id:(string) network ID for the external gateway. + :param enable_snat:(bool) enable Source NAT (SNAT) attribute. + :param ext_fixed_ips: List of dictionaries of desired IP and/or subnet + on the external network. + :param project_id:(string) project ID for the router. -def delete_neutron_router(neutron_client, router_id): # pragma: no cover + :returns:(string) the router id. + """ try: - neutron_client.delete_router(router=router_id) - return True - except Exception: - log.error("Error [delete_neutron_router(neutron_client, '%s')]" % router_id) - return False + router = shade_client.create_router( + name, admin_state_up, ext_gateway_net_id, enable_snat, + ext_fixed_ips, project_id) + return router['id'] + except exc.OpenStackCloudException as o_exc: + log.error("Error [create_neutron_router(shade_client)]. " + "Exception message: %s", o_exc.orig_message) -def remove_gateway_router(neutron_client, router_id): # pragma: no cover +def delete_neutron_router(shade_client, router_id): try: - neutron_client.remove_gateway_router(router_id) - return True - except Exception: - log.error("Error [remove_gateway_router(neutron_client, '%s')]" % router_id) + return shade_client.delete_router(router_id) + except exc.OpenStackCloudException as o_exc: + log.error("Error [delete_neutron_router(shade_client, '%s')]. " + "Exception message: %s", router_id, o_exc.orig_message) return False -def remove_interface_router(neutron_client, router_id, subnet_id, - **json_body): # pragma: no cover - json_body.update({"subnet_id": subnet_id}) +def remove_gateway_router(neutron_client, router_id): # pragma: no cover try: - neutron_client.remove_interface_router(router=router_id, - body=json_body) + neutron_client.remove_gateway_router(router_id) return True - except Exception: - log.error("Error [remove_interface_router(neutron_client, '%s', " - "'%s')]" % (router_id, subnet_id)) + except Exception: # pylint: disable=broad-except + log.error("Error [remove_gateway_router(neutron_client, '%s')]", + router_id) return False -def create_floating_ip(neutron_client, extnet_id): # pragma: no cover - props = {'floating_network_id': extnet_id} - try: - ip_json = neutron_client.create_floatingip({'floatingip': props}) - fip_addr = ip_json['floatingip']['floating_ip_address'] - fip_id = ip_json['floatingip']['id'] - except Exception: - log.error("Error [create_floating_ip(neutron_client)]") - return None - return {'fip_addr': fip_addr, 'fip_id': fip_id} +def remove_router_interface(shade_client, router, subnet_id=None, + port_id=None): + """Detach a subnet from an internal router interface. + At least one of subnet_id or port_id must be supplied. If you specify both + subnet and port ID, the subnet ID must correspond to the subnet ID of the + first IP address on the port specified by the port ID. + Otherwise an error occurs. -def delete_floating_ip(nova_client, floatingip_id): # pragma: no cover + :param router: The dict object of the router being changed + :param subnet_id:(string) The ID of the subnet to use for the interface + :param port_id:(string) The ID of the port to use for the interface + :returns: True on success + """ try: - nova_client.floating_ips.delete(floatingip_id) + shade_client.remove_router_interface( + router, subnet_id=subnet_id, port_id=port_id) return True - except Exception: - log.error("Error [delete_floating_ip(nova_client, '%s')]" % floatingip_id) + except exc.OpenStackCloudException as o_exc: + log.error("Error [remove_interface_router(shade_client)]. " + "Exception message: %s", o_exc.orig_message) return False -def get_security_groups(neutron_client): # pragma: no cover - try: - security_groups = neutron_client.list_security_groups()[ - 'security_groups'] - return security_groups - except Exception: - log.error("Error [get_security_groups(neutron_client)]") - return None - - -def get_security_group_id(neutron_client, sg_name): # pragma: no cover - security_groups = get_security_groups(neutron_client) - id = '' - for sg in security_groups: - if sg['name'] == sg_name: - id = sg['id'] - break - return id - - -def create_security_group(neutron_client, sg_name, sg_description): # pragma: no cover - json_body = {'security_group': {'name': sg_name, - 'description': sg_description}} - try: - secgroup = neutron_client.create_security_group(json_body) - return secgroup['security_group'] - except Exception: - log.error("Error [create_security_group(neutron_client, '%s', " - "'%s')]" % (sg_name, sg_description)) - return None +def create_floating_ip(shade_client, network_name_or_id=None, server=None, + fixed_address=None, nat_destination=None, + port=None, wait=False, timeout=60): + """Allocate a new floating IP from a network or a pool. + + :param network_name_or_id: Name or ID of the network + that the floating IP should come from. + :param server: Server dict for the server to create + the IP for and to which it should be attached. + :param fixed_address: Fixed IP to attach the floating ip to. + :param nat_destination: Name or ID of the network + that the fixed IP to attach the floating + IP to should be on. + :param port: The port ID that the floating IP should be + attached to. Specifying a port conflicts with specifying a + server,fixed_address or nat_destination. + :param wait: Whether to wait for the IP to be active.Only applies + if a server is provided. + :param timeout: How long to wait for the IP to be active.Only + applies if a server is provided. + + :returns:Floating IP id and address + """ + try: + fip = shade_client.create_floating_ip( + network=network_name_or_id, server=server, + fixed_address=fixed_address, nat_destination=nat_destination, + port=port, wait=wait, timeout=timeout) + return {'fip_addr': fip['floating_ip_address'], 'fip_id': fip['id']} + except exc.OpenStackCloudException as o_exc: + log.error("Error [create_floating_ip(shade_client)]. " + "Exception message: %s", o_exc.orig_message) + + +def delete_floating_ip(shade_client, floating_ip_id, retry=1): + try: + return shade_client.delete_floating_ip(floating_ip_id=floating_ip_id, + retry=retry) + except exc.OpenStackCloudException as o_exc: + log.error("Error [delete_floating_ip(shade_client,'%s')]. " + "Exception message: %s", floating_ip_id, o_exc.orig_message) + return False -def create_secgroup_rule(neutron_client, sg_id, direction, protocol, - port_range_min=None, port_range_max=None, - **json_body): # pragma: no cover - # We create a security group in 2 steps - # 1 - we check the format and set the json body accordingly - # 2 - we call neturon client to create the security group - - # Format check - json_body.update({'security_group_rule': {'direction': direction, - 'security_group_id': sg_id, 'protocol': protocol}}) - # parameters may be - # - both None => we do nothing - # - both Not None => we add them to the json description - # but one cannot be None is the other is not None - if (port_range_min is not None and port_range_max is not None): - # add port_range in json description - json_body['security_group_rule']['port_range_min'] = port_range_min - json_body['security_group_rule']['port_range_max'] = port_range_max - log.debug("Security_group format set (port range included)") - else: - # either both port range are set to None => do nothing - # or one is set but not the other => log it and return False - if port_range_min is None and port_range_max is None: - log.debug("Security_group format set (no port range mentioned)") - else: - log.error("Bad security group format." - "One of the port range is not properly set:" - "range min: {}," - "range max: {}".format(port_range_min, - port_range_max)) - return False - - # Create security group using neutron client - try: - neutron_client.create_security_group_rule(json_body) +def create_security_group_rule(shade_client, secgroup_name_or_id, + port_range_min=None, port_range_max=None, + protocol=None, remote_ip_prefix=None, + remote_group_id=None, direction='ingress', + ethertype='IPv4', project_id=None): + """Create a new security group rule + + :param secgroup_name_or_id:(string) The security group name or ID to + associate with this security group rule. If a + non-unique group name is given, an exception is + raised. + :param port_range_min:(int) The minimum port number in the range that is + matched by the security group rule. If the protocol + is TCP or UDP, this value must be less than or equal + to the port_range_max attribute value. If nova is + used by the cloud provider for security groups, then + a value of None will be transformed to -1. + :param port_range_max:(int) The maximum port number in the range that is + matched by the security group rule. The + port_range_min attribute constrains the + port_range_max attribute. If nova is used by the + cloud provider for security groups, then a value of + None will be transformed to -1. + :param protocol:(string) The protocol that is matched by the security group + rule. Valid values are None, tcp, udp, and icmp. + :param remote_ip_prefix:(string) The remote IP prefix to be associated with + this security group rule. This attribute matches + the specified IP prefix as the source IP address of + the IP packet. + :param remote_group_id:(string) The remote group ID to be associated with + this security group rule. + :param direction:(string) Ingress or egress: The direction in which the + security group rule is applied. + :param ethertype:(string) Must be IPv4 or IPv6, and addresses represented + in CIDR must match the ingress or egress rules. + :param project_id:(string) Specify the project ID this security group will + be created on (admin-only). + + :returns: True on success. + """ + + try: + shade_client.create_security_group_rule( + secgroup_name_or_id, port_range_min=port_range_min, + port_range_max=port_range_max, protocol=protocol, + remote_ip_prefix=remote_ip_prefix, remote_group_id=remote_group_id, + direction=direction, ethertype=ethertype, project_id=project_id) return True - except Exception: - log.exception("Impossible to create_security_group_rule," - "security group rule probably already exists") + except exc.OpenStackCloudException as op_exc: + log.error("Failed to create_security_group_rule(shade_client). " + "Exception message: %s", op_exc.orig_message) return False -def create_security_group_full(neutron_client, - sg_name, sg_description): # pragma: no cover - sg_id = get_security_group_id(neutron_client, sg_name) - if sg_id != '': - log.info("Using existing security group '%s'..." % sg_name) - else: - log.info("Creating security group '%s'..." % sg_name) - SECGROUP = create_security_group(neutron_client, - sg_name, - sg_description) - if not SECGROUP: - log.error("Failed to create the security group...") - return None - - sg_id = SECGROUP['id'] - - log.debug("Security group '%s' with ID=%s created successfully." - % (SECGROUP['name'], sg_id)) - - log.debug("Adding ICMP rules in security group '%s'..." - % sg_name) - if not create_secgroup_rule(neutron_client, sg_id, - 'ingress', 'icmp'): - log.error("Failed to create the security group rule...") - return None - - log.debug("Adding SSH rules in security group '%s'..." - % sg_name) - if not create_secgroup_rule( - neutron_client, sg_id, 'ingress', 'tcp', '22', '22'): - log.error("Failed to create the security group rule...") - return None - - if not create_secgroup_rule( - neutron_client, sg_id, 'egress', 'tcp', '22', '22'): - log.error("Failed to create the security group rule...") - return None - return sg_id +def create_security_group_full(shade_client, sg_name, + sg_description, project_id=None): + security_group = shade_client.get_security_group(sg_name) + + if security_group: + log.info("Using existing security group '%s'...", sg_name) + return security_group['id'] + + log.info("Creating security group '%s'...", sg_name) + try: + security_group = shade_client.create_security_group( + sg_name, sg_description, project_id=project_id) + except (exc.OpenStackCloudException, + exc.OpenStackCloudUnavailableFeature) as op_exc: + log.error("Error [create_security_group(shade_client, %s, %s)]. " + "Exception message: %s", sg_name, sg_description, + op_exc.orig_message) + return + + log.debug("Security group '%s' with ID=%s created successfully.", + security_group['name'], security_group['id']) + + log.debug("Adding ICMP rules in security group '%s'...", sg_name) + if not create_security_group_rule(shade_client, security_group['id'], + direction='ingress', protocol='icmp'): + log.error("Failed to create the security group rule...") + shade_client.delete_security_group(sg_name) + return + + log.debug("Adding SSH rules in security group '%s'...", sg_name) + if not create_security_group_rule(shade_client, security_group['id'], + direction='ingress', protocol='tcp', + port_range_min='22', + port_range_max='22'): + log.error("Failed to create the security group rule...") + shade_client.delete_security_group(sg_name) + return + + if not create_security_group_rule(shade_client, security_group['id'], + direction='egress', protocol='tcp', + port_range_min='22', + port_range_max='22'): + log.error("Failed to create the security group rule...") + shade_client.delete_security_group(sg_name) + return + return security_group['id'] # ********************************************* # GLANCE # ********************************************* -def get_image_id(glance_client, image_name): # pragma: no cover - images = glance_client.images.list() - return next((i.id for i in images if i.name == image_name), None) - - -def create_image(glance_client, image_name, file_path, disk_format, - container_format, min_disk, min_ram, protected, tag, - public, **kwargs): # pragma: no cover - if not os.path.isfile(file_path): - log.error("Error: file %s does not exist." % file_path) - return None - try: - image_id = get_image_id(glance_client, image_name) +def create_image(shade_client, name, filename=None, container='images', + md5=None, sha256=None, disk_format=None, + container_format=None, disable_vendor_agent=True, + wait=False, timeout=3600, allow_duplicates=False, meta=None, + volume=None, **kwargs): + """Upload an image. + + :param name:(str) Name of the image to create. If it is a pathname of an + image, the name will be constructed from the extensionless + basename of the path. + :param filename:(str) The path to the file to upload, if needed. + :param container:(str) Name of the container in swift where images should + be uploaded for import if the cloud requires such a thing. + :param md5:(str) md5 sum of the image file. If not given, an md5 will + be calculated. + :param sha256:(str) sha256 sum of the image file. If not given, an md5 + will be calculated. + :param disk_format:(str) The disk format the image is in. + :param container_format:(str) The container format the image is in. + :param disable_vendor_agent:(bool) Whether or not to append metadata + flags to the image to inform the cloud in + question to not expect a vendor agent to be running. + :param wait:(bool) If true, waits for image to be created. + :param timeout:(str) Seconds to wait for image creation. + :param allow_duplicates:(bool) If true, skips checks that enforce unique + image name. + :param meta:(dict) A dict of key/value pairs to use for metadata that + bypasses automatic type conversion. + :param volume:(str) Name or ID or volume object of a volume to create an + image from. + Additional kwargs will be passed to the image creation as additional + metadata for the image and will have all values converted to string + except for min_disk, min_ram, size and virtual_size which will be + converted to int. + If you are sure you have all of your data types correct or have an + advanced need to be explicit, use meta. If you are just a normal + consumer, using kwargs is likely the right choice. + If a value is in meta and kwargs, meta wins. + :returns: Image id + """ + try: + image_id = shade_client.get_image_id(name) if image_id is not None: - log.info("Image %s already exists." % image_name) - else: - log.info("Creating image '%s' from '%s'...", image_name, file_path) - - image = glance_client.images.create(name=image_name, - visibility=public, - disk_format=disk_format, - container_format=container_format, - min_disk=min_disk, - min_ram=min_ram, - tags=tag, - protected=protected, - **kwargs) - image_id = image.id - with open(file_path) as image_data: - glance_client.images.upload(image_id, image_data) + log.info("Image %s already exists.", name) + return image_id + log.info("Creating image '%s'", name) + image = shade_client.create_image( + name, filename=filename, container=container, md5=md5, sha256=sha256, + disk_format=disk_format, container_format=container_format, + disable_vendor_agent=disable_vendor_agent, wait=wait, timeout=timeout, + allow_duplicates=allow_duplicates, meta=meta, volume=volume, **kwargs) + image_id = image["id"] return image_id - except Exception: - log.error("Error [create_glance_image(glance_client, '%s', '%s', '%s')]", - image_name, file_path, public) - return None + except exc.OpenStackCloudException as op_exc: + log.error("Failed to create_image(shade_client). " + "Exception message: %s", op_exc.orig_message) -def delete_image(glance_client, image_id): # pragma: no cover +def delete_image(shade_client, name_or_id, wait=False, timeout=3600, + delete_objects=True): try: - glance_client.images.delete(image_id) + return shade_client.delete_image(name_or_id, wait=wait, + timeout=timeout, + delete_objects=delete_objects) - except Exception: - log.exception("Error [delete_flavor(glance_client, %s)]", image_id) + except exc.OpenStackCloudException as op_exc: + log.error("Failed to delete_image(shade_client). " + "Exception message: %s", op_exc.orig_message) + return False + + +def list_images(shade_client=None): + if shade_client is None: + shade_client = get_shade_client() + + try: + return shade_client.list_images() + except exc.OpenStackCloudException as o_exc: + log.error("Error [list_images(shade_client)]." + "Exception message, '%s'", o_exc.orig_message) return False - else: - return True # ********************************************* # CINDER # ********************************************* -def get_volume_id(volume_name): # pragma: no cover - volumes = get_cinder_client().volumes.list() - return next((v.id for v in volumes if v.name == volume_name), None) - - -def create_volume(cinder_client, volume_name, volume_size, - volume_image=False): # pragma: no cover - try: - if volume_image: - volume = cinder_client.volumes.create(name=volume_name, - size=volume_size, - imageRef=volume_image) - else: - volume = cinder_client.volumes.create(name=volume_name, - size=volume_size) - return volume - except Exception: - log.exception("Error [create_volume(cinder_client, %s)]", - (volume_name, volume_size)) - return None +def get_volume_id(shade_client, volume_name): + return shade_client.get_volume_id(volume_name) -def delete_volume(cinder_client, volume_id, forced=False): # pragma: no cover - try: - if forced: - try: - cinder_client.volumes.detach(volume_id) - except: - log.error(sys.exc_info()[0]) - cinder_client.volumes.force_delete(volume_id) - else: - while True: - volume = get_cinder_client().volumes.get(volume_id) - if volume.status.lower() == 'available': - break - cinder_client.volumes.delete(volume_id) - return True - except Exception: - log.exception("Error [delete_volume(cinder_client, '%s')]" % volume_id) - return False +def get_volume(shade_client, name_or_id, filters=None): + """Get a volume by name or ID. + :param name_or_id: Name or ID of the volume. + :param filters: A dictionary of meta data to use for further filtering. -def detach_volume(server_id, volume_id): # pragma: no cover - try: - get_nova_client().volumes.delete_server_volume(server_id, volume_id) - return True - except Exception: - log.exception("Error [detach_server_volume(nova_client, '%s', '%s')]", - server_id, volume_id) - return False -# ********************************************* -# HEAT -# ********************************************* + :returns: A volume ``munch.Munch`` or None if no matching volume is found. + """ + return shade_client.get_volume(name_or_id, filters=filters) + + +def create_volume(shade_client, size, wait=True, timeout=None, + image=None, **kwargs): + """Create a volume. + :param size: Size, in GB of the volume to create. + :param name: (optional) Name for the volume. + :param description: (optional) Name for the volume. + :param wait: If true, waits for volume to be created. + :param timeout: Seconds to wait for volume creation. None is forever. + :param image: (optional) Image name, ID or object from which to create + the volume. -def get_stack(heat_stack_id): # pragma: no cover + :returns: The created volume object. + + """ try: - client = get_heat_client() - return client.stacks.get(heat_stack_id) - except Exception as e: - log.exception("Error [get_stack(heat_stack_id)]", e) + return shade_client.create_volume(size, wait=wait, timeout=timeout, + image=image, **kwargs) + except (exc.OpenStackCloudException, exc.OpenStackCloudTimeout) as op_exc: + log.error("Failed to create_volume(shade_client). " + "Exception message: %s", op_exc.orig_message) + + +def delete_volume(shade_client, name_or_id=None, wait=True, timeout=None): + """Delete a volume. + :param name_or_id:(string) Name or unique ID of the volume. + :param wait:(bool) If true, waits for volume to be deleted. + :param timeout:(string) Seconds to wait for volume deletion. None is forever. -def get_stack_resources(heat_stack_id): # pragma: no cover + :return: True on success, False otherwise. + """ try: - client = get_heat_client() - return client.resources.list(heat_stack_id) - except Exception as e: - log.exception("Error [get_stack_resources(heat_stack_id)]: %s", e) + return shade_client.delete_volume(name_or_id=name_or_id, + wait=wait, timeout=timeout) + except (exc.OpenStackCloudException, exc.OpenStackCloudTimeout) as o_exc: + log.error("Error [delete_volume(shade_client,'%s')]. " + "Exception message: %s", name_or_id, o_exc.orig_message) + return False + +def detach_volume(shade_client, server_name_or_id, volume_name_or_id, + wait=True, timeout=None): + """Detach a volume from a server. -def get_stack_vms(heat_stack_id): # pragma: no cover - resources = get_stack_resources(heat_stack_id) - ret_vms = [] - for resource in resources: - if resource.resource_type == "OS::Nova::Server": - ret_vms.append(resource) - return ret_vms + :param server_name_or_id: The server name or id to detach from. + :param volume_name_or_id: The volume name or id to detach. + :param wait: If true, waits for volume to be detached. + :param timeout: Seconds to wait for volume detachment. None is forever. + + :return: True on success. + """ + try: + volume = shade_client.get_volume(volume_name_or_id) + server = get_server(shade_client, name_or_id=server_name_or_id) + shade_client.detach_volume(server, volume, wait=wait, timeout=timeout) + return True + except (exc.OpenStackCloudException, exc.OpenStackCloudTimeout) as o_exc: + log.error("Error [detach_volume(shade_client)]. " + "Exception message: %s", o_exc.orig_message) + return False diff --git a/vnftest/common/utils.py b/vnftest/common/utils.py index dfd32d5..b3f0c05 100644 --- a/vnftest/common/utils.py +++ b/vnftest/common/utils.py @@ -204,16 +204,6 @@ def result_handler(status, data): return jsonify(result) -def change_obj_to_dict(obj): - dic = {} - for k, v in vars(obj).items(): - try: - vars(v) - except TypeError: - dic.update({k: v}) - return dic - - def set_dict_value(dic, keys, value): return_dic = dic @@ -413,9 +403,26 @@ class dotdict(dict): __delattr__ = dict.__delitem__ +def deep_dotdict(obj): + if isinstance(obj, dict): + dot_dict = {} + for k, v in obj.items(): + if isinstance(k, basestring) and not k.startswith('_'): + v = deep_dotdict(v) + dot_dict[k] = v + return dotdict(dot_dict) + if isinstance(obj, list): + new_list = [] + for element in obj: + element = deep_dotdict(element) + new_list.append(element) + return new_list + return obj + + def normalize_data_struct(obj): - if isinstance(obj, basestring): - return [obj] + if obj is None: + return None if isinstance(obj, list): nomalized_list = [] for element in obj: @@ -424,11 +431,15 @@ def normalize_data_struct(obj): return nomalized_list if isinstance(obj, dict): normalized_dict = {} - for k, v in obj: - v = normalize_data_struct(v) - normalized_dict[k] = v + for k, v in obj.items(): + if isinstance(k, basestring) and not k.startswith('_'): + v = normalize_data_struct(v) + normalized_dict[k] = v return normalized_dict - return change_obj_to_dict(obj) + # return obj if it is string, integer, bool ect. + if not hasattr(obj, '__dict__'): + return obj + return normalize_data_struct(obj.__dict__) def xml_to_dict(xml_str): @@ -510,7 +521,6 @@ def format(in_obj, params): if not isinstance(in_obj, basestring): return in_obj - dotdict(params) ret_str = "" ret_obj = None for literal_text, field_name, format_spec, conversion in \ @@ -522,7 +532,7 @@ def format(in_obj, params): try: value = tmp_dict[field_name] except KeyError: - tmp_dict = dotdict(tmp_dict) + tmp_dict = deep_dotdict(tmp_dict) field_name = '{' + field_name + '}' value = field_name.format(**tmp_dict) if isinstance(value, basestring): diff --git a/vnftest/contexts/base.py b/vnftest/contexts/base.py index 29e3a19..c10732a 100644 --- a/vnftest/contexts/base.py +++ b/vnftest/contexts/base.py @@ -13,27 +13,69 @@ ############################################################################## import abc import six -from vnftest.common import openstack_utils import vnftest.common.utils as utils -import yaml import logging + +from vnftest.common import constants + LOG = logging.getLogger(__name__) +class Flags(object): + """Class to represent the status of the flags in a context""" + + _FLAGS = {'no_setup': False, + 'no_teardown': False, + 'os_cloud_config': constants.OS_CLOUD_DEFAULT_CONFIG} + + def __init__(self, **kwargs): + for name, value in self._FLAGS.items(): + setattr(self, name, value) + + for name, value in ((name, value) for (name, value) in kwargs.items() + if name in self._FLAGS): + setattr(self, name, value) + + def parse(self, **kwargs): + """Read in values matching the flags stored in this object""" + if not kwargs: + return + + for name, value in ((name, value) for (name, value) in kwargs.items() + if name in self._FLAGS): + setattr(self, name, value) + + @six.add_metaclass(abc.ABCMeta) class Context(object): """Class that represents a context in the logical model""" - list = [] + _list = [] def __init__(self): - Context.list.append(self) + Context._list.append(self) + self._flags = Flags() self._task_id = None self._name = None + self._name_task_id = None def init(self, attrs): self._task_id = attrs['task_id'] self._name = attrs['name'] + self._flags.parse(**attrs.get('flags', {})) + self._name_task_id = '{}-{}'.format( + self._name, self._task_id[:8]) + + @property + def name(self): + if self._flags.no_setup or self._flags.no_teardown: + return self._name + else: + return self._name_task_id + + @property + def assigned_name(self): + return self._name @staticmethod def get_cls(context_type): @@ -50,7 +92,7 @@ class Context(object): return Context.get_cls(context_type)() def _delete_context(self): - Context.list.remove(self) + Context._list.remove(self) @abc.abstractmethod def deploy(self): diff --git a/vnftest/contexts/heat.py b/vnftest/contexts/heat.py new file mode 100644 index 0000000..f6ca611 --- /dev/null +++ b/vnftest/contexts/heat.py @@ -0,0 +1,591 @@ +############################################################################## +# Copyright 2018 EuropeanSoftwareMarketingLtd. +# =================================================================== +# Licensed under the ApacheLicense, Version2.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 +# +# 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 +############################################################################## +# vnftest comment: this is a modified copy of +# yardstick/benchmark/contexts/heat.py + +import collections +import logging +import os +import errno +from collections import OrderedDict + +import ipaddress +import pkg_resources + +from vnftest.contexts.base import Context +from vnftest.contexts.model import Network +from vnftest.contexts.model import PlacementGroup, ServerGroup +from vnftest.contexts.model import Server +from vnftest.contexts.model import update_scheduler_hints +from vnftest.common import exceptions as y_exc +from vnftest.common.openstack_utils import get_shade_client +from vnftest.orchestrator.heat import HeatStack +from vnftest.orchestrator.heat import HeatTemplate +from vnftest.common import constants as consts +from vnftest.common import utils +from vnftest.common.utils import source_env +from vnftest.common import openstack_utils + +LOG = logging.getLogger(__name__) + +DEFAULT_HEAT_TIMEOUT = 3600 + + +def join_args(sep, *args): + return sep.join(args) + + +def h_join(*args): + return '-'.join(args) + + +class HeatContext(Context): + """Class that represents a context in the logical model""" + + __context_type__ = "Heat" + + def __init__(self): + self.stack = None + self.networks = OrderedDict() + self.heat_timeout = None + self.servers = {} + self.placement_groups = [] + self.server_groups = [] + self.keypair_name = None + self.secgroup_name = None + self.security_group = None + self.attrs = {} + self._image = None + self._flavor = None + self.flavors = set() + self._user = None + self.template_file = None + self.heat_parameters = None + self.heat_timeout = None + self.key_filename = None + self._shade_client = None + self._operator_client = None + self.nodes = [] + self.controllers = [] + self.computes = [] + self.baremetals = [] + super(HeatContext, self).__init__() + + @staticmethod + def assign_external_network(networks): + if networks: + sorted_networks = sorted(networks.items()) + external_network = os.environ.get("EXTERNAL_NETWORK", "net04_ext") + + have_external_network = any(net.get("external_network") for net in networks.values()) + if not have_external_network: + # try looking for mgmt network first + try: + networks['mgmt']["external_network"] = external_network + except KeyError: + if sorted_networks: + # otherwise assign it to first network using os.environ + sorted_networks[0][1]["external_network"] = external_network + + return sorted_networks + return networks + + def init(self, attrs): + """Initializes itself from the supplied arguments""" + super(HeatContext, self).init(attrs) + + self.check_environment() + self._user = attrs.get("user") + + self.template_file = attrs.get("heat_template") + + self._shade_client = openstack_utils.get_shade_client() + self._operator_client = openstack_utils.get_shade_operator_client() + + self.heat_timeout = attrs.get("timeout", DEFAULT_HEAT_TIMEOUT) + if self.template_file: + self.heat_parameters = attrs.get("heat_parameters") + return + + self.keypair_name = None # h_join(self.name, "key") + + self.secgroup_name = None # h_join(self.name, "secgroup") + + self.security_group = attrs.get("security_group") + + self._image = attrs.get("image") + + self._flavor = attrs.get("flavor") + + self.placement_groups = [PlacementGroup(name, self, pg_attrs["policy"]) + for name, pg_attrs in attrs.get( + "placement_groups", {}).items()] + + self.server_groups = [ServerGroup(name, self, sg_attrs["policy"]) + for name, sg_attrs in attrs.get( + "server_groups", {}).items()] + + # we have to do this first, because we are injecting external_network + # into the dict + networks = attrs.get("networks", {}) + sorted_networks = self.assign_external_network(networks) + + if sorted_networks: + self.networks = OrderedDict( + (name, Network(name, self, net_attrs)) for name, net_attrs in + sorted_networks) + + for name, server_attrs in sorted(attrs["servers"].items()): + server = Server(name, self, server_attrs) + self.servers[name] = server + + self.attrs = attrs + + def check_environment(self): + try: + os.environ['OS_AUTH_URL'] + except KeyError: + try: + source_env(consts.OPENRC) + except IOError as e: + if e.errno != errno.EEXIST: + LOG.error('OPENRC file not found') + raise + else: + LOG.error('OS_AUTH_URL not found') + + @property + def image(self): + """returns application's default image name""" + return self._image + + @property + def flavor(self): + """returns application's default flavor name""" + return self._flavor + + @property + def user(self): + """return login user name corresponding to image""" + return self._user + + def _add_resources_to_template(self, template): + """add to the template the resources represented by this context""" + + if self.flavor: + if isinstance(self.flavor, dict): + flavor = self.flavor.setdefault("name", self.name + "-flavor") + template.add_flavor(**self.flavor) + self.flavors.add(flavor) + + # template.add_keypair(self.keypair_name, self.name) + # template.add_security_group(self.secgroup_name, self.security_group) + + for network in self.networks.values(): + # Using existing network + if network.is_existing(): + continue + template.add_network(network.stack_name, + network.physical_network, + network.provider, + network.segmentation_id, + network.port_security_enabled, + network.network_type) + template.add_subnet(network.subnet_stack_name, network.stack_name, + network.subnet_cidr, + network.enable_dhcp, + network.gateway_ip) + + if network.router: + template.add_router(network.router.stack_name, + network.router.external_gateway_info, + network.subnet_stack_name) + template.add_router_interface(network.router.stack_if_name, + network.router.stack_name, + network.subnet_stack_name) + + # create a list of servers sorted by increasing no of placement groups + list_of_servers = sorted(self.servers.itervalues(), + key=lambda s: len(s.placement_groups)) + + # + # add servers with scheduler hints derived from placement groups + # + + # create list of servers with availability policy + availability_servers = [] + for server in list_of_servers: + for pg in server.placement_groups: + if pg.policy == "availability": + availability_servers.append(server) + break + + for server in availability_servers: + if isinstance(server.flavor, dict): + try: + self.flavors.add(server.flavor["name"]) + except KeyError: + self.flavors.add(h_join(server.stack_name, "flavor")) + + # add servers with availability policy + added_servers = [] + for server in availability_servers: + scheduler_hints = {} + for pg in server.placement_groups: + update_scheduler_hints(scheduler_hints, added_servers, pg) + # workaround for openstack nova bug, check JIRA: vnftest-200 + # for details + if len(availability_servers) == 2: + if not scheduler_hints["different_host"]: + scheduler_hints.pop("different_host", None) + server.add_to_template(template, + list(self.networks.values()), + scheduler_hints) + else: + scheduler_hints["different_host"] = \ + scheduler_hints["different_host"][0] + server.add_to_template(template, + list(self.networks.values()), + scheduler_hints) + else: + server.add_to_template(template, + list(self.networks.values()), + scheduler_hints) + added_servers.append(server.stack_name) + + # create list of servers with affinity policy + affinity_servers = [] + for server in list_of_servers: + for pg in server.placement_groups: + if pg.policy == "affinity": + affinity_servers.append(server) + break + + # add servers with affinity policy + for server in affinity_servers: + if server.stack_name in added_servers: + continue + scheduler_hints = {} + for pg in server.placement_groups: + update_scheduler_hints(scheduler_hints, added_servers, pg) + server.add_to_template(template, list(self.networks.values()), + scheduler_hints) + added_servers.append(server.stack_name) + + # add server group + for sg in self.server_groups: + template.add_server_group(sg.name, sg.policy) + + # add remaining servers with no placement group configured + for server in list_of_servers: + # TODO placement_group and server_group should combine + if not server.placement_groups: + scheduler_hints = {} + # affinity/anti-aff server group + sg = server.server_group + if sg: + scheduler_hints["group"] = {'get_resource': sg.name} + server.add_to_template(template, + list(self.networks.values()), + scheduler_hints) + + def get_neutron_info(self): + if not self._shade_client: + self._shade_client = get_shade_client() + + networks = self._shade_client.list_networks() + for network in self.networks.values(): + for neutron_net in (net for net in networks if net.name == network.stack_name): + network.segmentation_id = neutron_net.get('provider:segmentation_id') + # we already have physical_network + # network.physical_network = neutron_net.get('provider:physical_network') + network.network_type = neutron_net.get('provider:network_type') + network.neutron_info = neutron_net + + def _create_new_stack(self, heat_template): + try: + return heat_template.create(block=True, + timeout=self.heat_timeout) + except KeyboardInterrupt: + raise y_exc.StackCreationInterrupt + except Exception: + LOG.exception("stack failed") + # let the other failures happen, we want stack trace + raise + + def _retrieve_existing_stack(self, stack_name): + stack = HeatStack(stack_name) + if stack.get(): + return stack + else: + LOG.warning("Stack %s does not exist", self.name) + return None + + def deploy(self): + """deploys template into a stack using cloud""" + LOG.info("Deploying context '%s' START", self.name) + + # self.key_filename = ''.join( + # [consts.VNFTEST_ROOT_PATH, + # '/vnftest/resources/files/vnftest_key-', + # self.name]) + # Permissions may have changed since creation; this can be fixed. If we + # overwrite the file, we lose future access to VMs using this key. + # As long as the file exists, even if it is unreadable, keep it intact + # if not os.path.exists(self.key_filename): + # SSH.gen_keys(self.key_filename) + + heat_template = HeatTemplate( + self.name, template_file=self.template_file, + heat_parameters=self.heat_parameters, + os_cloud_config=self._flags.os_cloud_config) + + if self.template_file is None: + self._add_resources_to_template(heat_template) + + if self._flags.no_setup: + # Try to get an existing stack, returns a stack or None + self.stack = self._retrieve_existing_stack(self.name) + if not self.stack: + self.stack = self._create_new_stack(heat_template) + + else: + self.stack = self._create_new_stack(heat_template) + + # TODO: use Neutron to get segmentation-id + self.get_neutron_info() + + # copy some vital stack output into server objects + for server in self.servers.itervalues(): + if server.networks: + self.update_networks(server) + if server.ports: + self.add_server_port(server) + + if server.floating_ip: + server.public_ip = \ + self.stack.outputs[server.floating_ip["stack_name"]] + + LOG.info("Deploying context '%s' DONE", self.name) + + @staticmethod + def _port_net_is_existing(port_info): + net_flags = port_info.get('net_flags', {}) + return net_flags.get(consts.IS_EXISTING) + + @staticmethod + def _port_net_is_public(port_info): + net_flags = port_info.get('net_flags', {}) + return net_flags.get(consts.IS_PUBLIC) + + def update_networks(self, server): + for network in server.networks: + if 'network' in network: + network_name = network['network'] + output_key = server.stack_name + ".networks.%s-ip" % network_name + ip = self.stack.outputs[output_key] + network["ip"] = ip + + def add_server_port(self, server): + server_ports = server.ports.values() + for server_port in server_ports: + port_info = server_port[0] + port_ip = self.stack.outputs[port_info["stack_name"]] + port_net_is_existing = self._port_net_is_existing(port_info) + port_net_is_public = self._port_net_is_public(port_info) + if port_net_is_existing and (port_net_is_public or + len(server_ports) == 1): + server.public_ip = port_ip + if not server.private_ip or len(server_ports) == 1: + server.private_ip = port_ip + + server.interfaces = {} + for network_name, ports in server.ports.items(): + for port in ports: + # port['port'] is either port name from mapping or default network_name + if self._port_net_is_existing(port): + continue + server.interfaces[port['port']] = self.make_interface_dict(network_name, + port['port'], + port['stack_name'], + self.stack.outputs) + server.override_ip(network_name, port) + + def make_interface_dict(self, network_name, port, stack_name, outputs): + private_ip = outputs[stack_name] + mac_address = outputs[h_join(stack_name, "mac_address")] + # these are attributes of the network, not the port + output_subnet_cidr = outputs[h_join(self.name, network_name, + 'subnet', 'cidr')] + + # these are attributes of the network, not the port + output_subnet_gateway = outputs[h_join(self.name, network_name, + 'subnet', 'gateway_ip')] + + return { + # add default port name + "name": port, + "private_ip": private_ip, + "subnet_id": outputs[h_join(stack_name, "subnet_id")], + "subnet_cidr": output_subnet_cidr, + "network": str(ipaddress.ip_network(output_subnet_cidr).network_address), + "netmask": str(ipaddress.ip_network(output_subnet_cidr).netmask), + "gateway_ip": output_subnet_gateway, + "mac_address": mac_address, + "device_id": outputs[h_join(stack_name, "device_id")], + "network_id": outputs[h_join(stack_name, "network_id")], + # this should be == vld_id for NSB tests + "network_name": network_name, + # to match vnf_generic + "local_mac": mac_address, + "local_ip": private_ip, + } + + def _delete_key_file(self): + if self.key_filename is not None: + try: + utils.remove_file(self.key_filename) + utils.remove_file(self.key_filename + ".pub") + except OSError: + LOG.exception("There was an error removing the key file %s", + self.key_filename) + + def undeploy(self): + """undeploys stack from cloud""" + if self._flags.no_teardown: + LOG.info("Undeploying context '%s' SKIP", self.name) + return + + if self.stack: + LOG.info("Undeploying context '%s' START", self.name) + self.stack.delete() + self.stack = None + LOG.info("Undeploying context '%s' DONE", self.name) + + self._delete_key_file() + + super(HeatContext, self).undeploy() + + @staticmethod + def generate_routing_table(server): + routes = [ + { + "network": intf["network"], + "netmask": intf["netmask"], + "if": name, + # We have to encode a None gateway as '' for Jinja2 to YAML conversion + "gateway": intf["gateway_ip"] if intf["gateway_ip"] else '', + } + for name, intf in server.interfaces.items() + ] + return routes + + def _get_server(self, attr_name): + """lookup server info by name from context + attr_name: either a name for a server created by vnftest or a dict + with attribute name mapping when using external heat templates + """ + if isinstance(attr_name, collections.Mapping): + node_name, cname = self.split_host_name(attr_name['name']) + if cname is None or cname != self.name: + return None + + # Create a dummy server instance for holding the *_ip attributes + server = Server(node_name, self, {}) + server.public_ip = self.stack.outputs.get( + attr_name.get("public_ip_attr", object()), None) + + server.private_ip = self.stack.outputs.get( + attr_name.get("private_ip_attr", object()), None) + else: + try: + server = self.servers[attr_name] + except KeyError: + attr_name_no_suffix = attr_name.split("-")[0] + server = self.servers.get(attr_name_no_suffix, None) + if server is None: + return None + + pkey = pkg_resources.resource_string( + 'vnftest.resources', + h_join('files/vnftest_key', self.name)).decode('utf-8') + key_filename = pkg_resources.resource_filename('vnftest.resources', + h_join('files/vnftest_key', self.name)) + result = { + "user": server._context.user, + "pkey": pkey, + "key_filename": key_filename, + "private_ip": server.private_ip, + "interfaces": server.interfaces, + "routing_table": self.generate_routing_table(server), + # empty IPv6 routing table + "nd_route_tbl": [], + # we want to save the contex name so we can generate pod.yaml + "name": server.name, + } + # Target server may only have private_ip + if server.public_ip: + result["ip"] = server.public_ip + + return result + + def _get_network(self, attr_name): + if not isinstance(attr_name, collections.Mapping): + network = self.networks.get(attr_name, None) + + else: + # Only take the first key, value + key, value = next(iter(attr_name.items()), (None, None)) + if key is None: + return None + network_iter = (n for n in self.networks.values() if getattr(n, key) == value) + network = next(network_iter, None) + + if network is None: + return None + + result = { + "name": network.name, + "segmentation_id": network.segmentation_id, + "network_type": network.network_type, + "physical_network": network.physical_network, + } + return result + + def _get_physical_nodes(self): + return self.nodes + + def _get_physical_node_for_server(self, server_name): + node_name, ctx_name = self.split_host_name(server_name) + if ctx_name is None or self.name != ctx_name: + return None + + matching_nodes = [s for s in self.servers.itervalues() if s.name == node_name] + if len(matching_nodes) == 0: + return None + + server = openstack_utils.get_server(self._shade_client, + name_or_id=server_name) + + if server: + server = server.toDict() + list_hypervisors = self._operator_client.list_hypervisors() + + for hypervisor in list_hypervisors: + if hypervisor.hypervisor_hostname == server['OS-EXT-SRV-ATTR:hypervisor_hostname']: + for node in self.nodes: + if node['ip'] == hypervisor.host_ip: + return "{}.{}".format(node['name'], self.name) + + return None diff --git a/vnftest/contexts/model.py b/vnftest/contexts/model.py new file mode 100644 index 0000000..52b8d34 --- /dev/null +++ b/vnftest/contexts/model.py @@ -0,0 +1,433 @@ +############################################################################## +# Copyright 2018 EuropeanSoftwareMarketingLtd. +# =================================================================== +# Licensed under the ApacheLicense, Version2.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 +# +# 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 +############################################################################## +# vnftest comment: this is a modified copy of +# yardstick/benchmark/contexts/model.py +""" Logical model + +""" +from __future__ import absolute_import + +import six +import logging + +from collections import Mapping +from six.moves import range + +from vnftest.common import constants as consts + + +LOG = logging.getLogger(__name__) + + +class Object(object): + """Base class for classes in the logical model + Contains common attributes and methods + """ + + def __init__(self, name, context): + # model identities and reference + self.name = name + self._context = context + + # stack identities + self.stack_name = None + self.stack_id = None + + @property + def dn(self): + """returns distinguished name for object""" + return self.name + "." + self._context.name + + +class PlacementGroup(Object): + """Class that represents a placement group in the logical model + Concept comes from the OVF specification. Policy should be one of + "availability" or "affinity (there are more but they are not supported)" + """ + map = {} + + def __init__(self, name, context, policy): + if policy not in ["affinity", "availability"]: + raise ValueError("placement group '%s', policy '%s' is not valid" % + (name, policy)) + self.name = name + self.members = set() + self.stack_name = context.name + "-" + name + self.policy = policy + PlacementGroup.map[name] = self + + def add_member(self, name): + self.members.add(name) + + @staticmethod + def get(name): + return PlacementGroup.map.get(name) + + +class ServerGroup(Object): # pragma: no cover + """Class that represents a server group in the logical model + Policy should be one of "anti-affinity" or "affinity" + """ + map = {} + + def __init__(self, name, context, policy): + super(ServerGroup, self).__init__(name, context) + if policy not in {"affinity", "anti-affinity"}: + raise ValueError("server group '%s', policy '%s' is not valid" % + (name, policy)) + self.name = name + self.members = set() + self.stack_name = context.name + "-" + name + self.policy = policy + ServerGroup.map[name] = self + + def add_member(self, name): + self.members.add(name) + + @staticmethod + def get(name): + return ServerGroup.map.get(name) + + +class Router(Object): + """Class that represents a router in the logical model""" + + def __init__(self, name, network_name, context, external_gateway_info): + super(Router, self).__init__(name, context) + + self.stack_name = context.name + "-" + network_name + "-" + self.name + self.stack_if_name = self.stack_name + "-if0" + self.external_gateway_info = external_gateway_info + + +class Network(Object): + """Class that represents a network in the logical model""" + list = [] + + def __init__(self, name, context, attrs): + super(Network, self).__init__(name, context) + self.stack_name = context.name + "-" + self.name + self.subnet_stack_name = self.stack_name + "-subnet" + self.subnet_cidr = attrs.get('cidr', '10.0.1.0/24') + self.enable_dhcp = attrs.get('enable_dhcp', 'true') + self.router = None + self.physical_network = attrs.get('physical_network', 'physnet1') + self.provider = attrs.get('provider') + self.segmentation_id = attrs.get('segmentation_id') + self.network_type = attrs.get('network_type') + self.port_security_enabled = attrs.get('port_security_enabled') + self.vnic_type = attrs.get('vnic_type', 'normal') + self.allowed_address_pairs = attrs.get('allowed_address_pairs', []) + try: + # we require 'null' or '' to disable setting gateway_ip + self.gateway_ip = attrs['gateway_ip'] + except KeyError: + # default to explicit None + self.gateway_ip = None + else: + # null is None in YAML, so we have to convert back to string + if self.gateway_ip is None: + self.gateway_ip = "null" + + self.net_flags = attrs.get('net_flags', {}) + if self.is_existing(): + self.subnet = attrs.get('subnet') + if not self.subnet: + raise Warning('No subnet set in existing netwrok!') + else: + if "external_network" in attrs: + self.router = Router("router", self.name, + context, attrs["external_network"]) + Network.list.append(self) + + def is_existing(self): + net_is_existing = self.net_flags.get(consts.IS_EXISTING) + if net_is_existing and not isinstance(net_is_existing, bool): + raise SyntaxError('Network flags should be bool type!') + return net_is_existing + + def is_public(self): + net_is_public = self.net_flags.get(consts.IS_PUBLIC) + if net_is_public and not isinstance(net_is_public, bool): + raise SyntaxError('Network flags should be bool type!') + return net_is_public + + def has_route_to(self, network_name): + """determines if this network has a route to the named network""" + if self.router and self.router.external_gateway_info == network_name: + return True + return False + + @staticmethod + def find_by_route_to(external_network): + """finds a network that has a route to the specified network""" + for network in Network.list: + if network.has_route_to(external_network): + return network + + @staticmethod + def find_external_network(): + """return the name of an external network some network in this + context has a route to + """ + for network in Network.list: + if network.router: + return network.router.external_gateway_info + return None + + +class Server(Object): # pragma: no cover + """Class that represents a server in the logical model""" + list = [] + + def __init__(self, name, context, attrs): + super(Server, self).__init__(name, context) + self.stack_name = self.name + "." + context.name + self.keypair_name = context.keypair_name + self.secgroup_name = context.secgroup_name + self.user = context.user + self._context = context + self.public_ip = None + self.private_ip = None + self.user_data = '' + self.interfaces = {} + self.networks = None + self.ports = {} + self.floating_ip = {} + + if attrs is None: + attrs = {} + + self.placement_groups = [] + placement = attrs.get("placement", []) + placement = placement if isinstance(placement, list) else [placement] + for p in placement: + pg = PlacementGroup.get(p) + if not pg: + raise ValueError("server '%s', placement '%s' is invalid" % + (name, p)) + self.placement_groups.append(pg) + pg.add_member(self.stack_name) + + self.volume = None + if "volume" in attrs: + self.volume = attrs.get("volume") + + self.volume_mountpoint = None + if "volume_mountpoint" in attrs: + self.volume_mountpoint = attrs.get("volume_mountpoint") + + # support servergroup attr + self.server_group = None + sg = attrs.get("server_group") + if sg: + server_group = ServerGroup.get(sg) + if not server_group: + raise ValueError("server '%s', server_group '%s' is invalid" % + (name, sg)) + self.server_group = server_group + server_group.add_member(self.stack_name) + + self.instances = 1 + if "instances" in attrs: + self.instances = attrs["instances"] + + if "networks" in attrs: + self.networks = attrs["networks"] + else: + # dict with key network name, each item is a dict with port name and ip + self.network_ports = attrs.get("network_ports", {}) + + self.floating_ip = None + self.floating_ip_assoc = None + if "floating_ip" in attrs: + self.floating_ip_assoc = {} + + if self.floating_ip is not None: + ext_net = Network.find_external_network() + assert ext_net is not None + self.floating_ip["external_network"] = ext_net + + self._image = None + if "image" in attrs: + self._image = attrs["image"] + + self._flavor = None + if "flavor" in attrs: + self._flavor = attrs["flavor"] + + self.user_data = attrs.get('user_data', '') + self.availability_zone = attrs.get('availability_zone') + + Server.list.append(self) + + def override_ip(self, network_name, port): + def find_port_overrides(): + for p in ports: + # p can be string or dict + # we can't just use p[port['port'] in case p is a string + # and port['port'] is an int? + if isinstance(p, Mapping): + g = p.get(port['port']) + # filter out empty dicts + if g: + yield g + + ports = self.network_ports.get(network_name, []) + intf = self.interfaces[port['port']] + for override in find_port_overrides(): + intf['local_ip'] = override.get('local_ip', intf['local_ip']) + intf['netmask'] = override.get('netmask', intf['netmask']) + # only use the first value + break + + @property + def image(self): + """returns a server's image name""" + if self._image: + return self._image + else: + return self._context.image + + @property + def flavor(self): + """returns a server's flavor name""" + if self._flavor: + return self._flavor + else: + return self._context.flavor + + def _add_instance(self, template, server_name, networks, scheduler_hints): + """adds to the template one server and corresponding resources""" + port_name_list = None + if self.networks is None: + port_name_list = [] + for network in networks: + # if explicit mapping skip unused networks + if self.network_ports: + try: + ports = self.network_ports[network.name] + except KeyError: + # no port for this network + continue + else: + if isinstance(ports, six.string_types): + # because strings are iterable we have to check specifically + raise SyntaxError("network_port must be a list '{}'".format(ports)) + # convert port subdicts into their just port name + # port subdicts are used to override Heat IP address, + # but we just need the port name + # we allow duplicates here and let Heat raise the error + ports = [next(iter(p)) if isinstance(p, dict) else p for p in ports] + # otherwise add a port for every network with port name as network name + else: + ports = [network.name] + net_flags = network.net_flags + for port in ports: + port_name = "{0}-{1}-port".format(server_name, port) + port_info = {"stack_name": port_name, "port": port} + if net_flags: + port_info['net_flags'] = net_flags + self.ports.setdefault(network.name, []).append(port_info) + # we can't use secgroups if port_security_enabled is False + if network.port_security_enabled is False: + sec_group_id = None + else: + # if port_security_enabled is None we still need to add to secgroup + sec_group_id = self.secgroup_name + # don't refactor to pass in network object, that causes JSON + # circular ref encode errors + template.add_port(port_name, network, + sec_group_id=sec_group_id, + provider=network.provider, + allowed_address_pairs=network.allowed_address_pairs) + if network.is_public(): + port_name_list.insert(0, port_name) + else: + port_name_list.append(port_name) + + if self.floating_ip: + external_network = self.floating_ip["external_network"] + if network.has_route_to(external_network): + self.floating_ip["stack_name"] = server_name + "-fip" + template.add_floating_ip(self.floating_ip["stack_name"], + external_network, + port_name, + network.router.stack_if_name, + sec_group_id) + self.floating_ip_assoc["stack_name"] = \ + server_name + "-fip-assoc" + template.add_floating_ip_association( + self.floating_ip_assoc["stack_name"], + self.floating_ip["stack_name"], + port_name) + if self.flavor: + if isinstance(self.flavor, dict): + self.flavor["name"] = \ + self.flavor.setdefault("name", self.stack_name + "-flavor") + template.add_flavor(**self.flavor) + self.flavor_name = self.flavor["name"] + else: + self.flavor_name = self.flavor + + if self.volume: + if isinstance(self.volume, dict): + self.volume["name"] = \ + self.volume.setdefault("name", server_name + "-volume") + template.add_volume(**self.volume) + template.add_volume_attachment(server_name, self.volume["name"], + mountpoint=self.volume_mountpoint) + else: + template.add_volume_attachment(server_name, self.volume, + mountpoint=self.volume_mountpoint) + + template.add_server(server_name, self.image, flavor=self.flavor_name, + flavors=self._context.flavors, ports=port_name_list, + networks=self.networks, + scheduler_hints=scheduler_hints, user=self.user, + key_name=self.keypair_name, user_data=self.user_data, + availability_zone=self.availability_zone) + + def add_to_template(self, template, networks, scheduler_hints=None): + """adds to the template one or more servers (instances)""" + if self.instances == 1: + server_name = self.stack_name + self._add_instance(template, server_name, networks, + scheduler_hints=scheduler_hints) + else: + # TODO(hafe) fix or remove, no test/sample for this + for i in range(self.instances): + server_name = "%s-%d" % (self.stack_name, i) + self._add_instance(template, server_name, networks, + scheduler_hints=scheduler_hints) + + +def update_scheduler_hints(scheduler_hints, added_servers, placement_group): + """update scheduler hints from server's placement configuration + TODO: this code is openstack specific and should move somewhere else + """ + if placement_group.policy == "affinity": + if "same_host" in scheduler_hints: + host_list = scheduler_hints["same_host"] + else: + host_list = scheduler_hints["same_host"] = [] + else: + if "different_host" in scheduler_hints: + host_list = scheduler_hints["different_host"] + else: + host_list = scheduler_hints["different_host"] = [] + + for name in added_servers: + if name in placement_group.members: + host_list.append({'get_resource': name}) diff --git a/vnftest/core/task.py b/vnftest/core/task.py index a9718bb..8797eee 100644 --- a/vnftest/core/task.py +++ b/vnftest/core/task.py @@ -25,6 +25,7 @@ import copy import logging import sys import time +import traceback import uuid import ipaddress @@ -32,6 +33,7 @@ import os import yaml from jinja2 import Environment from six.moves import filter + from vnftest.runners import base as base_runner from vnftest.contexts.base import Context @@ -160,6 +162,7 @@ class Task(object): # pragma: no cover one_task_end_time - one_task_start_time) except Exception as e: LOG.error("Task fatal error: %s", e) + traceback.print_exc() self.task_info.task_fatal() finally: self.task_info.task_end() @@ -170,11 +173,6 @@ class Task(object): # pragma: no cover total_end_time = time.time() LOG.info("Total finished in %d secs", total_end_time - total_start_time) - - step = steps[0] - LOG.info("To generate report, execute command " - "'vnftest report generate %(task_id)s %(tc)s'", step) - LOG.info("Task ALL DONE, exiting") return self.task_info.result() def _generate_reporting(self): @@ -279,6 +277,7 @@ class Task(object): # pragma: no cover return result except Exception as e: LOG.exception('Case fatal error: %s', e) + traceback.print_exc() self.task_info.testcase_fatal(case_name) finally: self.task_info.testcase_end(case_name) @@ -316,6 +315,7 @@ class Task(object): # pragma: no cover LOG.info("Starting runner of type '%s'", runner_cfg["type"]) # Previous steps output is the input of the next step. inputs.update(self.outputs) + _resolve_step_options(step_cfg, self.contexts, inputs) runner.run(step_cfg, self.contexts, inputs) return runner @@ -330,6 +330,22 @@ class Task(object): # pragma: no cover result.extend(step_result_list) +def _resolve_step_options(step_cfg, contexts, inputs): + inputs = copy.deepcopy(inputs) + contexts_dict = {} + inputs['context'] = contexts_dict + if contexts is not None: + for context in contexts: + context_as_dict = utils.normalize_data_struct(context) + contexts_dict[context.assigned_name] = context_as_dict + options = step_cfg.get("options", {}) + resolved_options = {} + for k, v in options.items(): + v = utils.format(v, inputs) + resolved_options[k] = v + step_cfg["options"] = resolved_options + + class TaskParser(object): # pragma: no cover """Parser for task config files in yaml format""" diff --git a/vnftest/orchestrator/__init__.py b/vnftest/orchestrator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vnftest/orchestrator/heat.py b/vnftest/orchestrator/heat.py new file mode 100644 index 0000000..14c6d73 --- /dev/null +++ b/vnftest/orchestrator/heat.py @@ -0,0 +1,661 @@ +############################################################################## +# Copyright 2018 EuropeanSoftwareMarketingLtd. +# =================================================================== +# Licensed under the ApacheLicense, Version2.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 +# +# 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 +############################################################################## +# vnftest comment: this is a modified copy of +# yardstick/orchestrator/heat.py +"""Heat template and stack management""" + +from __future__ import absolute_import +import collections +import datetime +import getpass +import logging +import pkg_resources +import pprint +import socket +import tempfile +import time + +from oslo_serialization import jsonutils +from oslo_utils import encodeutils +from shade._heat import event_utils + +from vnftest.common import constants as consts +from vnftest.common import exceptions +from vnftest.common import template_format +from vnftest.common import openstack_utils as op_utils + + +log = logging.getLogger(__name__) + + +PROVIDER_SRIOV = "sriov" + +_DEPLOYED_STACKS = {} + + +class HeatStack(object): + """Represents a Heat stack (deployed template) """ + + def __init__(self, name, os_cloud_config=None): + self.name = name + self.outputs = {} + os_cloud_config = {} if not os_cloud_config else os_cloud_config + self._cloud = op_utils.get_shade_client(**os_cloud_config) + self._stack = None + + def _update_stack_tracking(self): + outputs = self._stack.outputs + self.outputs = {output['output_key']: output['output_value'] for output + in outputs} + if self.uuid: + _DEPLOYED_STACKS[self.uuid] = self._stack + + def create(self, template, heat_parameters, wait, timeout): + """Creates an OpenStack stack from a template""" + with tempfile.NamedTemporaryFile('wb', delete=False) as template_file: + template_file.write(jsonutils.dump_as_bytes(template)) + template_file.close() + self._stack = self._cloud.create_stack( + self.name, template_file=template_file.name, wait=wait, + timeout=timeout, **heat_parameters) + + self._update_stack_tracking() + + def get_failures(self): + return event_utils.get_events(self._cloud, self._stack.id, + event_args={'resource_status': 'FAILED'}) + + def get(self): + """Retrieves an existing stack from the target cloud + + Returns a bool indicating whether the stack exists in the target cloud + If the stack exists, it will be stored as self._stack + """ + self._stack = self._cloud.get_stack(self.name) + if not self._stack: + return False + + self._update_stack_tracking() + return True + + @staticmethod + def stacks_exist(): + """Check if any stack has been deployed""" + return len(_DEPLOYED_STACKS) > 0 + + def delete(self, wait=True): + """Deletes a stack in the target cloud""" + if self.uuid is None: + return + + try: + ret = self._cloud.delete_stack(self.uuid, wait=wait) + except TypeError: + # NOTE(ralonsoh): this exception catch solves a bug in Shade, which + # tries to retrieve and read the stack status when it's already + # deleted. + ret = True + + _DEPLOYED_STACKS.pop(self.uuid) + self._stack = None + return ret + + @staticmethod + def delete_all(): + """Delete all deployed stacks""" + for stack in _DEPLOYED_STACKS: + stack.delete() + + @property + def status(self): + """Retrieve the current stack status""" + if self._stack: + return self._stack.status + + @property + def uuid(self): + """Retrieve the current stack ID""" + if self._stack: + return self._stack.id + + +class HeatTemplate(object): + """Describes a Heat template and a method to deploy template to a stack""" + + DESCRIPTION_TEMPLATE = """ +Stack built by the vnftest framework for %s on host %s %s. +All referred generated resources are prefixed with the template +name (i.e. %s). +""" + + HEAT_WAIT_LOOP_INTERVAL = 2 + HEAT_STATUS_COMPLETE = 'COMPLETE' + + def _init_template(self): + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + self._template = { + 'heat_template_version': '2013-05-23', + 'description': self.DESCRIPTION_TEMPLATE % ( + getpass.getuser(), + socket.gethostname(), + timestamp, + self.name + ), + 'resources': {}, + 'outputs': {} + } + + # short hand for resources part of template + self.resources = self._template['resources'] + + def __init__(self, name, template_file=None, heat_parameters=None, + os_cloud_config=None): + self.name = name + self.keystone_client = None + self.heat_parameters = {} + self._os_cloud_config = {} if not os_cloud_config else os_cloud_config + + # heat_parameters is passed to heat in stack create, empty dict when + # vnftest creates the template (no get_param in resources part) + if heat_parameters: + self.heat_parameters = heat_parameters + + if template_file: + with open(template_file) as stream: + log.info('Parsing external template: %s', template_file) + template_str = stream.read() + self._template = template_format.parse(template_str) + self._parameters = heat_parameters + else: + self._init_template() + + log.debug("template object '%s' created", name) + + def add_flavor(self, name, vcpus=1, ram=1024, disk=1, ephemeral=0, + is_public=True, rxtx_factor=1.0, swap=0, + extra_specs=None): + """add to the template a Flavor description""" + if name is None: + name = 'auto' + log.debug("adding Nova::Flavor '%s' vcpus '%d' ram '%d' disk '%d' " + "ephemeral '%d' is_public '%s' rxtx_factor '%d' " + "swap '%d' extra_specs '%s'", + name, vcpus, ram, disk, ephemeral, is_public, + rxtx_factor, swap, str(extra_specs)) + + if extra_specs: + assert isinstance(extra_specs, collections.Mapping) + + self.resources[name] = { + 'type': 'OS::Nova::Flavor', + 'properties': {'name': name, + 'disk': disk, + 'vcpus': vcpus, + 'swap': swap, + 'flavorid': name, + 'rxtx_factor': rxtx_factor, + 'ram': ram, + 'is_public': is_public, + 'ephemeral': ephemeral, + 'extra_specs': extra_specs} + } + + self._template['outputs'][name] = { + 'description': 'Flavor %s ID' % name, + 'value': {'get_resource': name} + } + + def add_volume(self, name, size=10): + """add to the template a volume description""" + log.debug("adding Cinder::Volume '%s' size '%d' ", name, size) + + self.resources[name] = { + 'type': 'OS::Cinder::Volume', + 'properties': {'name': name, + 'size': size} + } + + self._template['outputs'][name] = { + 'description': 'Volume %s ID' % name, + 'value': {'get_resource': name} + } + + def add_volume_attachment(self, server_name, volume_name, mountpoint=None): + """add to the template an association of volume to instance""" + log.debug("adding Cinder::VolumeAttachment server '%s' volume '%s' ", + server_name, volume_name) + name = "%s-%s" % (server_name, volume_name) + volume_id = {'get_resource': volume_name} + self.resources[name] = { + 'type': 'OS::Cinder::VolumeAttachment', + 'properties': {'instance_uuid': {'get_resource': server_name}, + 'volume_id': volume_id} + } + + if mountpoint: + self.resources[name]['properties']['mountpoint'] = mountpoint + + def add_network(self, name, physical_network='physnet1', provider=None, + segmentation_id=None, port_security_enabled=None, network_type=None): + """add to the template a Neutron Net""" + log.debug("adding Neutron::Net '%s'", name) + if provider is None: + self.resources[name] = { + 'type': 'OS::Neutron::Net', + 'properties': { + 'name': name, + } + } + else: + self.resources[name] = { + 'type': 'OS::Neutron::ProviderNet', + 'properties': { + 'name': name, + 'network_type': 'flat' if network_type is None else network_type, + 'physical_network': physical_network, + }, + } + if segmentation_id: + self.resources[name]['properties']['segmentation_id'] = segmentation_id + if network_type is None: + self.resources[name]['properties']['network_type'] = 'vlan' + # if port security is not defined then don't add to template: + # some deployments don't have port security plugin installed + if port_security_enabled is not None: + self.resources[name]['properties']['port_security_enabled'] = port_security_enabled + + def add_server_group(self, name, policies): # pragma: no cover + """add to the template a ServerGroup""" + log.debug("adding Nova::ServerGroup '%s'", name) + policies = policies if isinstance(policies, list) else [policies] + self.resources[name] = { + 'type': 'OS::Nova::ServerGroup', + 'properties': {'name': name, + 'policies': policies} + } + + def add_subnet(self, name, network, cidr, enable_dhcp='true', gateway_ip=None): + """add to the template a Neutron Subnet + """ + log.debug("adding Neutron::Subnet '%s' in network '%s', cidr '%s'", + name, network, cidr) + self.resources[name] = { + 'type': 'OS::Neutron::Subnet', + 'depends_on': network, + 'properties': { + 'name': name, + 'cidr': cidr, + 'network_id': {'get_resource': network}, + 'enable_dhcp': enable_dhcp, + } + } + if gateway_ip == 'null': + self.resources[name]['properties']['gateway_ip'] = None + elif gateway_ip is not None: + self.resources[name]['properties']['gateway_ip'] = gateway_ip + + self._template['outputs'][name] = { + 'description': 'subnet %s ID' % name, + 'value': {'get_resource': name} + } + self._template['outputs'][name + "-cidr"] = { + 'description': 'subnet %s cidr' % name, + 'value': {'get_attr': [name, 'cidr']} + } + self._template['outputs'][name + "-gateway_ip"] = { + 'description': 'subnet %s gateway_ip' % name, + 'value': {'get_attr': [name, 'gateway_ip']} + } + + def add_router(self, name, ext_gw_net, subnet_name): + """add to the template a Neutron Router and interface""" + log.debug("adding Neutron::Router:'%s', gw-net:'%s'", name, ext_gw_net) + self.resources[name] = { + 'type': 'OS::Neutron::Router', + 'depends_on': [subnet_name], + 'properties': { + 'name': name, + 'external_gateway_info': { + 'network': ext_gw_net + } + } + } + + def add_router_interface(self, name, router_name, subnet_name): + """add to the template a Neutron RouterInterface and interface""" + log.debug("adding Neutron::RouterInterface '%s' router:'%s', " + "subnet:'%s'", name, router_name, subnet_name) + self.resources[name] = { + 'type': 'OS::Neutron::RouterInterface', + 'depends_on': [router_name, subnet_name], + 'properties': { + 'router_id': {'get_resource': router_name}, + 'subnet_id': {'get_resource': subnet_name} + } + } + + def add_port(self, name, network, sec_group_id=None, + provider=None, allowed_address_pairs=None): + """add to the template a named Neutron Port + """ + net_is_existing = network.net_flags.get(consts.IS_EXISTING) + depends_on = [] if net_is_existing else [network.subnet_stack_name] + fixed_ips = [{'subnet': network.subnet}] if net_is_existing else [ + {'subnet': {'get_resource': network.subnet_stack_name}}] + network_ = network.name if net_is_existing else { + 'get_resource': network.stack_name} + self.resources[name] = { + 'type': 'OS::Neutron::Port', + 'depends_on': depends_on, + 'properties': { + 'name': name, + 'binding:vnic_type': network.vnic_type, + 'fixed_ips': fixed_ips, + 'network': network_, + } + } + + if provider == PROVIDER_SRIOV: + self.resources[name]['properties']['binding:vnic_type'] = \ + 'direct' + + if sec_group_id: + self.resources[name]['depends_on'].append(sec_group_id) + self.resources[name]['properties']['security_groups'] = \ + [sec_group_id] + + if allowed_address_pairs: + self.resources[name]['properties'][ + 'allowed_address_pairs'] = allowed_address_pairs + + log.debug("adding Neutron::Port %s", self.resources[name]) + + self._template['outputs'][name] = { + 'description': 'Address for interface %s' % name, + 'value': {'get_attr': [name, 'fixed_ips', 0, 'ip_address']} + } + self._template['outputs'][name + "-subnet_id"] = { + 'description': 'Address for interface %s' % name, + 'value': {'get_attr': [name, 'fixed_ips', 0, 'subnet_id']} + } + self._template['outputs'][name + "-mac_address"] = { + 'description': 'MAC Address for interface %s' % name, + 'value': {'get_attr': [name, 'mac_address']} + } + self._template['outputs'][name + "-device_id"] = { + 'description': 'Device ID for interface %s' % name, + 'value': {'get_attr': [name, 'device_id']} + } + self._template['outputs'][name + "-network_id"] = { + 'description': 'Network ID for interface %s' % name, + 'value': {'get_attr': [name, 'network_id']} + } + + def add_floating_ip(self, name, network_name, port_name, router_if_name, + secgroup_name=None): + """add to the template a Nova FloatingIP resource + see: https://bugs.launchpad.net/heat/+bug/1299259 + """ + log.debug("adding Nova::FloatingIP '%s', network '%s', port '%s', " + "rif '%s'", name, network_name, port_name, router_if_name) + + self.resources[name] = { + 'type': 'OS::Nova::FloatingIP', + 'depends_on': [port_name, router_if_name], + 'properties': { + 'pool': network_name + } + } + + if secgroup_name: + self.resources[name]["depends_on"].append(secgroup_name) + + self._template['outputs'][name] = { + 'description': 'floating ip %s' % name, + 'value': {'get_attr': [name, 'ip']} + } + + def add_floating_ip_association(self, name, floating_ip_name, port_name): + """add to the template a Nova FloatingIP Association resource + """ + log.debug("adding Nova::FloatingIPAssociation '%s', server '%s', " + "floating_ip '%s'", name, port_name, floating_ip_name) + + self.resources[name] = { + 'type': 'OS::Neutron::FloatingIPAssociation', + 'depends_on': [port_name], + 'properties': { + 'floatingip_id': {'get_resource': floating_ip_name}, + 'port_id': {'get_resource': port_name} + } + } + + def add_keypair(self, name, key_id): + """add to the template a Nova KeyPair""" + log.debug("adding Nova::KeyPair '%s'", name) + self.resources[name] = { + 'type': 'OS::Nova::KeyPair', + 'properties': { + 'name': name, + # resource_string returns bytes, so we must decode to unicode + 'public_key': encodeutils.safe_decode( + pkg_resources.resource_string( + 'vnftest.resources', + 'files/vnftest_key-' + + key_id + '.pub'), + 'utf-8') + } + } + + def add_servergroup(self, name, policy): + """add to the template a Nova ServerGroup""" + log.debug("adding Nova::ServerGroup '%s', policy '%s'", name, policy) + if policy not in ["anti-affinity", "affinity"]: + raise ValueError(policy) + + self.resources[name] = { + 'type': 'OS::Nova::ServerGroup', + 'properties': { + 'name': name, + 'policies': [policy] + } + } + + self._template['outputs'][name] = { + 'description': 'ID Server Group %s' % name, + 'value': {'get_resource': name} + } + + def add_security_group(self, name, security_group=None): + """add to the template a Neutron SecurityGroup""" + log.debug("adding Neutron::SecurityGroup '%s'", name) + description = ("Group allowing IPv4 and IPv6 for icmp and upd/tcp on" + "all ports") + rules = [ + {'remote_ip_prefix': '0.0.0.0/0', + 'protocol': 'tcp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '0.0.0.0/0', + 'protocol': 'udp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '0.0.0.0/0', + 'protocol': 'icmp'}, + {'remote_ip_prefix': '::/0', + 'ethertype': 'IPv6', + 'protocol': 'tcp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '::/0', + 'ethertype': 'IPv6', + 'protocol': 'udp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '::/0', + 'ethertype': 'IPv6', + 'protocol': 'ipv6-icmp'}, + {'remote_ip_prefix': '0.0.0.0/0', + 'direction': 'egress', + 'protocol': 'tcp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '0.0.0.0/0', + 'direction': 'egress', + 'protocol': 'udp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '0.0.0.0/0', + 'direction': 'egress', + 'protocol': 'icmp'}, + {'remote_ip_prefix': '::/0', + 'direction': 'egress', + 'ethertype': 'IPv6', + 'protocol': 'tcp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '::/0', + 'direction': 'egress', + 'ethertype': 'IPv6', + 'protocol': 'udp', + 'port_range_min': '1', + 'port_range_max': '65535'}, + {'remote_ip_prefix': '::/0', + 'direction': 'egress', + 'ethertype': 'IPv6', + 'protocol': 'ipv6-icmp'}, + ] + if security_group: + description = "Custom security group rules defined by the user" + rules = security_group.get('rules') + + log.debug("The security group rules is %s", rules) + + self.resources[name] = { + 'type': 'OS::Neutron::SecurityGroup', + 'properties': { + 'name': name, + 'description': description, + 'rules': rules + } + } + + self._template['outputs'][name] = { + 'description': 'ID of Security Group', + 'value': {'get_resource': name} + } + + def add_server(self, name, image, flavor, flavors, ports=None, networks=None, + scheduler_hints=None, user=None, key_name=None, user_data=None, metadata=None, + additional_properties=None, availability_zone=None): + """add to the template a Nova Server """ + log.debug("adding Nova::Server '%s', image '%s', flavor '%s', " + "ports %s", name, image, flavor, ports) + + self.resources[name] = { + 'type': 'OS::Nova::Server', + 'depends_on': [] + } + + server_properties = { + 'name': name, + 'image': image, + 'flavor': {}, + 'networks': [] # list of dictionaries + } + if availability_zone: + server_properties["availability_zone"] = availability_zone + + if flavor in flavors: + self.resources[name]['depends_on'].append(flavor) + server_properties["flavor"] = {'get_resource': flavor} + else: + server_properties["flavor"] = flavor + + if user: + server_properties['admin_user'] = user + + if key_name: + self.resources[name]['depends_on'].append(key_name) + server_properties['key_name'] = {'get_resource': key_name} + + if ports: + self.resources[name]['depends_on'].extend(ports) + for port in ports: + server_properties['networks'].append( + {'port': {'get_resource': port}} + ) + + if networks: + for n in networks: + server_properties['networks'].append(n) + if 'network' in n: + network_name = n['network'] + self._template['outputs'][name + ".networks.%s-ip" % network_name] = { + 'description': 'network %s ip' % network_name, + 'value': {'get_attr': [name, 'networks', network_name, 0]} + } + + if scheduler_hints: + server_properties['scheduler_hints'] = scheduler_hints + + if user_data: + server_properties['user_data'] = user_data + + if metadata: + assert isinstance(metadata, collections.Mapping) + server_properties['metadata'] = metadata + + if additional_properties: + assert isinstance(additional_properties, collections.Mapping) + for prop in additional_properties: + server_properties[prop] = additional_properties[prop] + + server_properties['config_drive'] = True + + self.resources[name]['properties'] = server_properties + + self._template['outputs'][name] = { + 'description': 'VM UUID', + 'value': {'get_resource': name} + } + + def create(self, block=True, timeout=3600): + """Creates a stack in the target based on the stored template + + :param block: (bool) Wait for Heat create to finish + :param timeout: (int) Timeout in seconds for Heat create, + default 3600s + :return A dict with the requested output values from the template + """ + log.info("Creating stack '%s' START", self.name) + + start_time = time.time() + stack = HeatStack(self.name, os_cloud_config=self._os_cloud_config) + stack.create(self._template, self.heat_parameters, block, timeout) + + if not block: + log.info("Creating stack '%s' DONE in %d secs", + self.name, time.time() - start_time) + return stack + + if stack.status != self.HEAT_STATUS_COMPLETE: + for event in stack.get_failures(): + log.error("%s", event.resource_status_reason) + log.error(pprint.pformat(self._template)) + raise exceptions.HeatTemplateError(stack_name=self.name) + + log.info("Creating stack '%s' DONE in %d secs", + self.name, time.time() - start_time) + return stack diff --git a/vnftest/tests/unit/common/test_utils.py b/vnftest/tests/unit/common/test_utils.py index d152a98..6f82537 100644 --- a/vnftest/tests/unit/common/test_utils.py +++ b/vnftest/tests/unit/common/test_utils.py @@ -798,19 +798,6 @@ power management: assert sockets == [0, 1] -class ChangeObjToDictTestCase(unittest.TestCase): - - def test_change_obj_to_dict(self): - class A(object): - def __init__(self): - self.name = 'vnftest' - - obj = A() - obj_r = utils.change_obj_to_dict(obj) - obj_s = {'name': 'vnftest'} - self.assertEqual(obj_r, obj_s) - - class SetDictValueTestCase(unittest.TestCase): def test_set_dict_value(self): -- cgit 1.2.3-korg