diff options
author | Seshu Kumar M <seshu.kumar.m@huawei.com> | 2017-08-10 06:48:03 +0000 |
---|---|---|
committer | Gerrit Code Review <gerrit@onap.org> | 2017-08-10 06:48:03 +0000 |
commit | b2356570d9aa82f9e8ea259f9e282499c98efcbc (patch) | |
tree | ebccf8677dcc645ef1c27a09a646acd013fb033e /aria/multivim-plugin/nova_plugin | |
parent | d48b750fc08b11a758b4c71ac71fe6cd2a2437d3 (diff) | |
parent | 9981f55920a6f1c1f20396d42e35b075b22f6a8f (diff) |
Merge "ARIA multivim plugin initial checkin"
Diffstat (limited to 'aria/multivim-plugin/nova_plugin')
15 files changed, 2702 insertions, 0 deletions
diff --git a/aria/multivim-plugin/nova_plugin/__init__.py b/aria/multivim-plugin/nova_plugin/__init__.py new file mode 100644 index 0000000000..bb533273be --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/__init__.py @@ -0,0 +1,16 @@ +######### +# Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +__author__ = 'idanmo' diff --git a/aria/multivim-plugin/nova_plugin/floatingip.py b/aria/multivim-plugin/nova_plugin/floatingip.py new file mode 100644 index 0000000000..e770c540a8 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/floatingip.py @@ -0,0 +1,60 @@ +######### +# Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +from cloudify import ctx +from cloudify.decorators import operation +from openstack_plugin_common import with_nova_client +from openstack_plugin_common.floatingip import ( + use_external_floatingip, + set_floatingip_runtime_properties, + delete_floatingip, + floatingip_creation_validation +) + + +# random note regarding nova floating-ips: floating ips on nova-net have +# pre-assigned ids, and thus a call "nova.floating_ips.get(<fip_id>)" will +# return a value even if the floating-ip isn't even allocated. +# currently all lookups in the code, including by id, use search (i.e. +# nova.<type>.findall) and lists, which won't return such unallocated +# resources. + +@operation +@with_nova_client +def create(nova_client, args, **kwargs): + + if use_external_floatingip(nova_client, 'ip', + lambda ext_fip: ext_fip.ip): + return + + floatingip = { + 'pool': None + } + floatingip.update(ctx.node.properties['floatingip'], **args) + + fip = nova_client.floating_ips.create(floatingip['pool']) + set_floatingip_runtime_properties(fip.id, fip.ip) + + +@operation +@with_nova_client +def delete(nova_client, **kwargs): + delete_floatingip(nova_client) + + +@operation +@with_nova_client +def creation_validation(nova_client, **kwargs): + floatingip_creation_validation(nova_client, 'ip') diff --git a/aria/multivim-plugin/nova_plugin/keypair.py b/aria/multivim-plugin/nova_plugin/keypair.py new file mode 100644 index 0000000000..92281ab9e5 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/keypair.py @@ -0,0 +1,202 @@ +######### +# Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +import os +import errno +from getpass import getuser + +from cloudify import ctx +from cloudify.decorators import operation +from cloudify.exceptions import NonRecoverableError +from openstack_plugin_common import ( + with_nova_client, + validate_resource, + use_external_resource, + transform_resource_name, + is_external_resource, + is_external_resource_not_conditionally_created, + delete_runtime_properties, + get_resource_id, + delete_resource_and_runtime_properties, + OPENSTACK_ID_PROPERTY, + OPENSTACK_TYPE_PROPERTY, + OPENSTACK_NAME_PROPERTY, + COMMON_RUNTIME_PROPERTIES_KEYS +) + +RUNTIME_PROPERTIES_KEYS = COMMON_RUNTIME_PROPERTIES_KEYS +KEYPAIR_OPENSTACK_TYPE = 'keypair' + +PRIVATE_KEY_PATH_PROP = 'private_key_path' + + +@operation +@with_nova_client +def create(nova_client, args, **kwargs): + + private_key_path = _get_private_key_path() + pk_exists = _check_private_key_exists(private_key_path) + + if use_external_resource(ctx, nova_client, KEYPAIR_OPENSTACK_TYPE): + if not pk_exists: + delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS) + raise NonRecoverableError( + 'Failed to use external keypair (node {0}): the public key {1}' + ' is available on Openstack, but the private key could not be ' + 'found at {2}'.format(ctx.node.id, + ctx.node.properties['resource_id'], + private_key_path)) + return + + if pk_exists: + raise NonRecoverableError( + "Can't create keypair - private key path already exists: {0}" + .format(private_key_path)) + + keypair = { + 'name': get_resource_id(ctx, KEYPAIR_OPENSTACK_TYPE), + } + keypair.update(ctx.node.properties['keypair'], **args) + transform_resource_name(ctx, keypair) + + keypair = nova_client.keypairs.create(keypair['name'], + keypair.get('public_key')) + ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = keypair.id + ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] = \ + KEYPAIR_OPENSTACK_TYPE + ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = keypair.name + + try: + # write private key file + _mkdir_p(os.path.dirname(private_key_path)) + with open(private_key_path, 'w') as f: + f.write(keypair.private_key) + os.chmod(private_key_path, 0600) + except Exception: + _delete_private_key_file() + delete_resource_and_runtime_properties(ctx, nova_client, + RUNTIME_PROPERTIES_KEYS) + raise + + +@operation +@with_nova_client +def delete(nova_client, **kwargs): + if not is_external_resource(ctx): + ctx.logger.info('deleting keypair') + + _delete_private_key_file() + + nova_client.keypairs.delete( + ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY]) + else: + ctx.logger.info('not deleting keypair since an external keypair is ' + 'being used') + + delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS) + + +@operation +@with_nova_client +def creation_validation(nova_client, **kwargs): + + def validate_private_key_permissions(private_key_path): + ctx.logger.debug('checking whether private key file {0} has the ' + 'correct permissions'.format(private_key_path)) + if not os.access(private_key_path, os.R_OK): + err = 'private key file {0} is not readable'\ + .format(private_key_path) + ctx.logger.error('VALIDATION ERROR: ' + err) + raise NonRecoverableError(err) + ctx.logger.debug('OK: private key file {0} has the correct ' + 'permissions'.format(private_key_path)) + + def validate_path_owner(path): + ctx.logger.debug('checking whether directory {0} is owned by the ' + 'current user'.format(path)) + from pwd import getpwnam, getpwuid + + user = getuser() + owner = getpwuid(os.stat(path).st_uid).pw_name + current_user_id = str(getpwnam(user).pw_uid) + owner_id = str(os.stat(path).st_uid) + + if not current_user_id == owner_id: + err = '{0} is not owned by the current user (it is owned by {1})'\ + .format(path, owner) + ctx.logger.warning('VALIDATION WARNING: {0}'.format(err)) + return + ctx.logger.debug('OK: {0} is owned by the current user'.format(path)) + + validate_resource(ctx, nova_client, KEYPAIR_OPENSTACK_TYPE) + + private_key_path = _get_private_key_path() + pk_exists = _check_private_key_exists(private_key_path) + + if is_external_resource_not_conditionally_created(ctx): + if pk_exists: + if os.name == 'posix': + validate_private_key_permissions(private_key_path) + validate_path_owner(private_key_path) + else: + err = "can't use external keypair: the public key {0} is " \ + "available on Openstack, but the private key could not be " \ + "found at {1}".format(ctx.node.properties['resource_id'], + private_key_path) + ctx.logger.error('VALIDATION ERROR: {0}'.format(err)) + raise NonRecoverableError(err) + else: + if pk_exists: + err = 'private key path already exists: {0}'.format( + private_key_path) + ctx.logger.error('VALIDATION ERROR: {0}'.format(err)) + raise NonRecoverableError(err) + else: + err = 'private key directory {0} is not writable' + while private_key_path: + if os.path.isdir(private_key_path): + if not os.access(private_key_path, os.W_OK | os.X_OK): + raise NonRecoverableError(err.format(private_key_path)) + else: + break + private_key_path, _ = os.path.split(private_key_path) + + ctx.logger.debug('OK: keypair configuration is valid') + + +def _get_private_key_path(): + return os.path.expanduser(ctx.node.properties[PRIVATE_KEY_PATH_PROP]) + + +def _delete_private_key_file(): + private_key_path = _get_private_key_path() + ctx.logger.debug('deleting private key file at {0}'.format( + private_key_path)) + try: + os.remove(private_key_path) + except OSError as e: + if e.errno == errno.ENOENT: + # file was already deleted somehow + pass + raise + + +def _check_private_key_exists(private_key_path): + return os.path.isfile(private_key_path) + + +def _mkdir_p(path): + if path and not os.path.isdir(path): + os.makedirs(path) diff --git a/aria/multivim-plugin/nova_plugin/security_group.py b/aria/multivim-plugin/nova_plugin/security_group.py new file mode 100644 index 0000000000..283eae85cf --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/security_group.py @@ -0,0 +1,81 @@ +######### +# Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +from cloudify import ctx +from cloudify.decorators import operation +from openstack_plugin_common import ( + transform_resource_name, + with_nova_client, + delete_resource_and_runtime_properties +) +from openstack_plugin_common.security_group import ( + build_sg_data, + process_rules, + use_external_sg, + set_sg_runtime_properties, + delete_sg, + sg_creation_validation, + RUNTIME_PROPERTIES_KEYS +) + + +@operation +@with_nova_client +def create(nova_client, args, **kwargs): + + security_group = build_sg_data(args) + security_group['description'] = ctx.node.properties['description'] + + sgr_default_values = { + 'ip_protocol': 'tcp', + 'from_port': 1, + 'to_port': 65535, + 'cidr': '0.0.0.0/0', + # 'group_id': None, + # 'parent_group_id': None, + } + sg_rules = process_rules(nova_client, sgr_default_values, + 'cidr', 'group_id', 'from_port', 'to_port') + + if use_external_sg(nova_client): + return + + transform_resource_name(ctx, security_group) + + sg = nova_client.security_groups.create( + security_group['name'], security_group['description']) + + set_sg_runtime_properties(sg, nova_client) + + try: + for sgr in sg_rules: + sgr['parent_group_id'] = sg.id + nova_client.security_group_rules.create(**sgr) + except Exception: + delete_resource_and_runtime_properties(ctx, nova_client, + RUNTIME_PROPERTIES_KEYS) + raise + + +@operation +@with_nova_client +def delete(nova_client, **kwargs): + delete_sg(nova_client) + + +@operation +@with_nova_client +def creation_validation(nova_client, **kwargs): + sg_creation_validation(nova_client, 'cidr') diff --git a/aria/multivim-plugin/nova_plugin/server.py b/aria/multivim-plugin/nova_plugin/server.py new file mode 100644 index 0000000000..6726f24804 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/server.py @@ -0,0 +1,944 @@ +######### +# Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + + +import os +import time +import copy +import operator + +from novaclient import exceptions as nova_exceptions + +from cloudify import ctx +from cloudify.manager import get_rest_client +from cloudify.decorators import operation +from cloudify.exceptions import NonRecoverableError, RecoverableError +from cinder_plugin import volume +from openstack_plugin_common import ( + provider, + transform_resource_name, + get_resource_id, + get_openstack_ids_of_connected_nodes_by_openstack_type, + with_nova_client, + with_cinder_client, + assign_payload_as_runtime_properties, + get_openstack_id_of_single_connected_node_by_openstack_type, + get_openstack_names_of_connected_nodes_by_openstack_type, + get_single_connected_node_by_openstack_type, + is_external_resource, + is_external_resource_by_properties, + is_external_resource_not_conditionally_created, + is_external_relationship_not_conditionally_created, + use_external_resource, + delete_runtime_properties, + is_external_relationship, + validate_resource, + USE_EXTERNAL_RESOURCE_PROPERTY, + OPENSTACK_AZ_PROPERTY, + OPENSTACK_ID_PROPERTY, + OPENSTACK_TYPE_PROPERTY, + OPENSTACK_NAME_PROPERTY, + COMMON_RUNTIME_PROPERTIES_KEYS, + with_neutron_client) +from nova_plugin.keypair import KEYPAIR_OPENSTACK_TYPE +from nova_plugin import userdata +from openstack_plugin_common.floatingip import (IP_ADDRESS_PROPERTY, + get_server_floating_ip) +from neutron_plugin.network import NETWORK_OPENSTACK_TYPE +from neutron_plugin.port import PORT_OPENSTACK_TYPE +from cinder_plugin.volume import VOLUME_OPENSTACK_TYPE +from openstack_plugin_common.security_group import \ + SECURITY_GROUP_OPENSTACK_TYPE +from glance_plugin.image import handle_image_from_relationship + +SERVER_OPENSTACK_TYPE = 'server' + +# server status constants. Full lists here: http://docs.openstack.org/api/openstack-compute/2/content/List_Servers-d1e2078.html # NOQA +SERVER_STATUS_ACTIVE = 'ACTIVE' +SERVER_STATUS_BUILD = 'BUILD' +SERVER_STATUS_SHUTOFF = 'SHUTOFF' + +OS_EXT_STS_TASK_STATE = 'OS-EXT-STS:task_state' +SERVER_TASK_STATE_POWERING_ON = 'powering-on' + +MUST_SPECIFY_NETWORK_EXCEPTION_TEXT = 'More than one possible network found.' +SERVER_DELETE_CHECK_SLEEP = 2 + +# Runtime properties +NETWORKS_PROPERTY = 'networks' # all of the server's ips +IP_PROPERTY = 'ip' # the server's private ip +ADMIN_PASSWORD_PROPERTY = 'password' # the server's password +RUNTIME_PROPERTIES_KEYS = COMMON_RUNTIME_PROPERTIES_KEYS + \ + [NETWORKS_PROPERTY, IP_PROPERTY, ADMIN_PASSWORD_PROPERTY] + + +def _get_management_network_id_and_name(neutron_client, ctx): + """Examine the context to find the management network id and name.""" + management_network_id = None + management_network_name = None + provider_context = provider(ctx) + + if ('management_network_name' in ctx.node.properties) and \ + ctx.node.properties['management_network_name']: + management_network_name = \ + ctx.node.properties['management_network_name'] + management_network_name = transform_resource_name( + ctx, management_network_name) + management_network_id = neutron_client.cosmo_get_named( + 'network', management_network_name) + management_network_id = management_network_id['id'] + else: + int_network = provider_context.int_network + if int_network: + management_network_id = int_network['id'] + management_network_name = int_network['name'] # Already transform. + + return management_network_id, management_network_name + + +def _merge_nics(management_network_id, *nics_sources): + """Merge nics_sources into a single nics list, insert mgmt network if + needed. + nics_sources are lists of networks received from several sources + (server properties, relationships to networks, relationships to ports). + Merge them into a single list, and if the management network isn't present + there, prepend it as the first network. + """ + merged = [] + for nics in nics_sources: + merged.extend(nics) + if management_network_id is not None and \ + not any(nic['net-id'] == management_network_id for nic in merged): + merged.insert(0, {'net-id': management_network_id}) + return merged + + +def _normalize_nics(nics): + """Transform the NICs passed to the form expected by openstack. + + If both net-id and port-id are provided, remove net-id: it is ignored + by openstack anyway. + """ + def _normalize(nic): + if 'port-id' in nic and 'net-id' in nic: + nic = nic.copy() + del nic['net-id'] + return nic + return [_normalize(nic) for nic in nics] + + +def _prepare_server_nics(neutron_client, ctx, server): + """Update server['nics'] based on declared relationships. + + server['nics'] should contain the pre-declared nics, then the networks + that the server has a declared relationship to, then the networks + of the ports the server has a relationship to. + + If that doesn't include the management network, it should be prepended + as the first network. + + The management network id and name are stored in the server meta properties + """ + network_ids = get_openstack_ids_of_connected_nodes_by_openstack_type( + ctx, NETWORK_OPENSTACK_TYPE) + port_ids = get_openstack_ids_of_connected_nodes_by_openstack_type( + ctx, PORT_OPENSTACK_TYPE) + management_network_id, management_network_name = \ + _get_management_network_id_and_name(neutron_client, ctx) + if management_network_id is None and (network_ids or port_ids): + # Known limitation + raise NonRecoverableError( + "Nova server with NICs requires " + "'management_network_name' in properties or id " + "from provider context, which was not supplied") + + nics = _merge_nics( + management_network_id, + server.get('nics', []), + [{'net-id': net_id} for net_id in network_ids], + get_port_networks(neutron_client, port_ids)) + + nics = _normalize_nics(nics) + + server['nics'] = nics + if management_network_id is not None: + server['meta']['cloudify_management_network_id'] = \ + management_network_id + if management_network_name is not None: + server['meta']['cloudify_management_network_name'] = \ + management_network_name + + +def _get_boot_volume_relationships(type_name, ctx): + ctx.logger.debug('Instance relationship target instances: {0}'.format(str([ + rel.target.instance.runtime_properties + for rel in ctx.instance.relationships]))) + targets = [ + rel.target.instance + for rel in ctx.instance.relationships + if rel.target.instance.runtime_properties.get( + OPENSTACK_TYPE_PROPERTY) == type_name and + rel.target.node.properties.get('boot', False)] + + if not targets: + return None + elif len(targets) > 1: + raise NonRecoverableError("2 boot volumes not supported") + return targets[0] + + +def _handle_boot_volume(server, ctx): + boot_volume = _get_boot_volume_relationships(VOLUME_OPENSTACK_TYPE, ctx) + if boot_volume: + boot_volume_id = boot_volume.runtime_properties[OPENSTACK_ID_PROPERTY] + ctx.logger.info('boot_volume_id: {0}'.format(boot_volume_id)) + az = boot_volume.runtime_properties[OPENSTACK_AZ_PROPERTY] + # If a block device mapping already exists we shouldn't overwrite it + # completely + bdm = server.setdefault('block_device_mapping', {}) + bdm['vda'] = '{0}:::0'.format(boot_volume_id) + # Some nova configurations allow cross-az server-volume connections, so + # we can't treat that as an error. + if not server.get('availability_zone'): + server['availability_zone'] = az + + +@operation +@with_nova_client +@with_neutron_client +def create(nova_client, neutron_client, args, **kwargs): + """ + Creates a server. Exposes the parameters mentioned in + http://docs.openstack.org/developer/python-novaclient/api/novaclient.v1_1 + .servers.html#novaclient.v1_1.servers.ServerManager.create + """ + + external_server = use_external_resource(ctx, nova_client, + SERVER_OPENSTACK_TYPE) + + if external_server: + _set_network_and_ip_runtime_properties(external_server) + if ctx._local: + return + else: + network_ids = \ + get_openstack_ids_of_connected_nodes_by_openstack_type( + ctx, NETWORK_OPENSTACK_TYPE) + port_ids = get_openstack_ids_of_connected_nodes_by_openstack_type( + ctx, PORT_OPENSTACK_TYPE) + try: + _validate_external_server_nics( + neutron_client, + network_ids, + port_ids + ) + _validate_external_server_keypair(nova_client) + return + except Exception: + delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS) + raise + + provider_context = provider(ctx) + + def rename(name): + return transform_resource_name(ctx, name) + + server = { + 'name': get_resource_id(ctx, SERVER_OPENSTACK_TYPE), + } + server.update(copy.deepcopy(ctx.node.properties['server'])) + server.update(copy.deepcopy(args)) + + _handle_boot_volume(server, ctx) + handle_image_from_relationship(server, 'image', ctx) + + if 'meta' not in server: + server['meta'] = dict() + + transform_resource_name(ctx, server) + + ctx.logger.debug( + "server.create() server before transformations: {0}".format(server)) + + for key in 'block_device_mapping', 'block_device_mapping_v2': + if key in server: + # if there is a connected boot volume, don't require the `image` + # property. + # However, python-novaclient requires an `image` input anyway, and + # checks it for truthiness when deciding whether to pass it along + # to the API + if 'image' not in server: + server['image'] = ctx.node.properties.get('image') + break + else: + _handle_image_or_flavor(server, nova_client, 'image') + _handle_image_or_flavor(server, nova_client, 'flavor') + + if provider_context.agents_security_group: + security_groups = server.get('security_groups', []) + asg = provider_context.agents_security_group['name'] + if asg not in security_groups: + security_groups.append(asg) + server['security_groups'] = security_groups + elif not server.get('security_groups', []): + # Make sure that if the server is connected to a security group + # from CREATE time so that there the user can control + # that there is never a time that a running server is not protected. + security_group_names = \ + get_openstack_names_of_connected_nodes_by_openstack_type( + ctx, + SECURITY_GROUP_OPENSTACK_TYPE) + server['security_groups'] = security_group_names + + # server keypair handling + keypair_id = get_openstack_id_of_single_connected_node_by_openstack_type( + ctx, KEYPAIR_OPENSTACK_TYPE, True) + + if 'key_name' in server: + if keypair_id: + raise NonRecoverableError("server can't both have the " + '"key_name" nested property and be ' + 'connected to a keypair via a ' + 'relationship at the same time') + server['key_name'] = rename(server['key_name']) + elif keypair_id: + server['key_name'] = _get_keypair_name_by_id(nova_client, keypair_id) + elif provider_context.agents_keypair: + server['key_name'] = provider_context.agents_keypair['name'] + else: + server['key_name'] = None + ctx.logger.info( + 'server must have a keypair, yet no keypair was connected to the ' + 'server node, the "key_name" nested property ' + "wasn't used, and there is no agent keypair in the provider " + "context. Agent installation can have issues.") + + _fail_on_missing_required_parameters( + server, + ('name', 'flavor'), + 'server') + + _prepare_server_nics(neutron_client, ctx, server) + + ctx.logger.debug( + "server.create() server after transformations: {0}".format(server)) + + userdata.handle_userdata(server) + + ctx.logger.info("Creating VM with parameters: {0}".format(str(server))) + # Store the server dictionary contents in runtime properties + assign_payload_as_runtime_properties(ctx, SERVER_OPENSTACK_TYPE, server) + ctx.logger.debug( + "Asking Nova to create server. All possible parameters are: {0})" + .format(','.join(server.keys()))) + + try: + s = nova_client.servers.create(**server) + except nova_exceptions.BadRequest as e: + if 'Block Device Mapping is Invalid' in str(e): + return ctx.operation.retry( + message='Block Device Mapping is not created yet', + retry_after=30) + if str(e).startswith(MUST_SPECIFY_NETWORK_EXCEPTION_TEXT): + raise NonRecoverableError( + "Can not provision server: management_network_name or id" + " is not specified but there are several networks that the " + "server can be connected to.") + raise + ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] = s.id + ctx.instance.runtime_properties[OPENSTACK_TYPE_PROPERTY] = \ + SERVER_OPENSTACK_TYPE + ctx.instance.runtime_properties[OPENSTACK_NAME_PROPERTY] = server['name'] + + +def get_port_networks(neutron_client, port_ids): + + def get_network(port_id): + port = neutron_client.show_port(port_id) + return { + 'net-id': port['port']['network_id'], + 'port-id': port['port']['id'] + } + + return map(get_network, port_ids) + + +@operation +@with_nova_client +def start(nova_client, start_retry_interval, private_key_path, **kwargs): + server = get_server_by_context(nova_client) + + if is_external_resource_not_conditionally_created(ctx): + ctx.logger.info('Validating external server is started') + if server.status != SERVER_STATUS_ACTIVE: + raise NonRecoverableError( + 'Expected external resource server {0} to be in ' + '"{1}" status'.format(server.id, SERVER_STATUS_ACTIVE)) + return + + if server.status == SERVER_STATUS_ACTIVE: + ctx.logger.info('Server is {0}'.format(server.status)) + + if ctx.node.properties['use_password']: + private_key = _get_private_key(private_key_path) + ctx.logger.debug('retrieving password for server') + password = server.get_password(private_key) + + if not password: + return ctx.operation.retry( + message='Waiting for server to post generated password', + retry_after=start_retry_interval) + + ctx.instance.runtime_properties[ADMIN_PASSWORD_PROPERTY] = password + ctx.logger.info('Server has been set with a password') + + _set_network_and_ip_runtime_properties(server) + return + + server_task_state = getattr(server, OS_EXT_STS_TASK_STATE) + + if server.status == SERVER_STATUS_SHUTOFF and \ + server_task_state != SERVER_TASK_STATE_POWERING_ON: + ctx.logger.info('Server is in {0} status - starting server...'.format( + SERVER_STATUS_SHUTOFF)) + server.start() + server_task_state = SERVER_TASK_STATE_POWERING_ON + + if server.status == SERVER_STATUS_BUILD or \ + server_task_state == SERVER_TASK_STATE_POWERING_ON: + return ctx.operation.retry( + message='Waiting for server to be in {0} state but is in {1}:{2} ' + 'state. Retrying...'.format(SERVER_STATUS_ACTIVE, + server.status, + server_task_state), + retry_after=start_retry_interval) + + raise NonRecoverableError( + 'Unexpected server state {0}:{1}'.format(server.status, + server_task_state)) + + +@operation +@with_nova_client +def stop(nova_client, **kwargs): + """ + Stop server. + + Depends on OpenStack implementation, server.stop() might not be supported. + """ + if is_external_resource(ctx): + ctx.logger.info('Not stopping server since an external server is ' + 'being used') + return + + server = get_server_by_context(nova_client) + + if server.status != SERVER_STATUS_SHUTOFF: + nova_client.servers.stop(server) + else: + ctx.logger.info('Server is already stopped') + + +@operation +@with_nova_client +def delete(nova_client, **kwargs): + if not is_external_resource(ctx): + ctx.logger.info('deleting server') + server = get_server_by_context(nova_client) + nova_client.servers.delete(server) + _wait_for_server_to_be_deleted(nova_client, server) + else: + ctx.logger.info('not deleting server since an external server is ' + 'being used') + + delete_runtime_properties(ctx, RUNTIME_PROPERTIES_KEYS) + + +def _wait_for_server_to_be_deleted(nova_client, + server, + timeout=120, + sleep_interval=5): + timeout = time.time() + timeout + while time.time() < timeout: + try: + server = nova_client.servers.get(server) + ctx.logger.debug('Waiting for server "{}" to be deleted. current' + ' status: {}'.format(server.id, server.status)) + time.sleep(sleep_interval) + except nova_exceptions.NotFound: + return + # recoverable error + raise RuntimeError('Server {} has not been deleted. waited for {} seconds' + .format(server.id, timeout)) + + +def get_server_by_context(nova_client): + return nova_client.servers.get( + ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY]) + + +def _set_network_and_ip_runtime_properties(server): + + ips = {} + + if not server.networks: + raise NonRecoverableError( + 'The server was created but not attached to a network. ' + 'Cloudify requires that a server is connected to ' + 'at least one port.' + ) + + manager_network_ip = None + management_network_name = server.metadata.get( + 'cloudify_management_network_name') + + for network, network_ips in server.networks.items(): + if (management_network_name and + network == management_network_name) or not \ + manager_network_ip: + manager_network_ip = next(iter(network_ips or []), None) + ips[network] = network_ips + ctx.instance.runtime_properties[NETWORKS_PROPERTY] = ips + # The ip of this instance in the management network + ctx.instance.runtime_properties[IP_PROPERTY] = manager_network_ip + + +@operation +@with_nova_client +def connect_floatingip(nova_client, fixed_ip, **kwargs): + server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + floating_ip_id = ctx.target.instance.runtime_properties[ + OPENSTACK_ID_PROPERTY] + + if is_external_relationship_not_conditionally_created(ctx): + ctx.logger.info('Validating external floatingip and server ' + 'are associated') + if nova_client.floating_ips.get(floating_ip_id).instance_id ==\ + server_id: + return + raise NonRecoverableError( + 'Expected external resources server {0} and floating-ip {1} to be ' + 'connected'.format(server_id, floating_ip_id)) + + floating_ip_address = ctx.target.instance.runtime_properties[ + IP_ADDRESS_PROPERTY] + server = nova_client.servers.get(server_id) + server.add_floating_ip(floating_ip_address, fixed_ip or None) + + server = nova_client.servers.get(server_id) + all_server_ips = reduce(operator.add, server.networks.values()) + if floating_ip_address not in all_server_ips: + return ctx.operation.retry(message='Failed to assign floating ip {0}' + ' to machine {1}.' + .format(floating_ip_address, server_id)) + + +@operation +@with_nova_client +@with_neutron_client +def disconnect_floatingip(nova_client, neutron_client, **kwargs): + if is_external_relationship(ctx): + ctx.logger.info('Not disassociating floatingip and server since ' + 'external floatingip and server are being used') + return + + server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + ctx.logger.info("Remove floating ip {0}".format( + ctx.target.instance.runtime_properties[IP_ADDRESS_PROPERTY])) + server_floating_ip = get_server_floating_ip(neutron_client, server_id) + if server_floating_ip: + server = nova_client.servers.get(server_id) + server.remove_floating_ip(server_floating_ip['floating_ip_address']) + ctx.logger.info("Floating ip {0} detached from server" + .format(server_floating_ip['floating_ip_address'])) + + +@operation +@with_nova_client +def connect_security_group(nova_client, **kwargs): + server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + security_group_id = ctx.target.instance.runtime_properties[ + OPENSTACK_ID_PROPERTY] + security_group_name = ctx.target.instance.runtime_properties[ + OPENSTACK_NAME_PROPERTY] + + if is_external_relationship_not_conditionally_created(ctx): + ctx.logger.info('Validating external security group and server ' + 'are associated') + server = nova_client.servers.get(server_id) + if [sg for sg in server.list_security_group() if sg.id == + security_group_id]: + return + raise NonRecoverableError( + 'Expected external resources server {0} and security-group {1} to ' + 'be connected'.format(server_id, security_group_id)) + + server = nova_client.servers.get(server_id) + for security_group in server.list_security_group(): + # Since some security groups are already attached in + # create this will ensure that they are not attached twice. + if security_group_id != security_group.id and \ + security_group_name != security_group.name: + # to support nova security groups as well, + # we connect the security group by name + # (as connecting by id + # doesn't seem to work well for nova SGs) + server.add_security_group(security_group_name) + + _validate_security_group_and_server_connection_status(nova_client, + server_id, + security_group_id, + security_group_name, + is_connected=True) + + +@operation +@with_nova_client +def disconnect_security_group(nova_client, **kwargs): + if is_external_relationship(ctx): + ctx.logger.info('Not disconnecting security group and server since ' + 'external security group and server are being used') + return + + server_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + security_group_id = ctx.target.instance.runtime_properties[ + OPENSTACK_ID_PROPERTY] + security_group_name = ctx.target.instance.runtime_properties[ + OPENSTACK_NAME_PROPERTY] + server = nova_client.servers.get(server_id) + # to support nova security groups as well, we disconnect the security group + # by name (as disconnecting by id doesn't seem to work well for nova SGs) + server.remove_security_group(security_group_name) + + _validate_security_group_and_server_connection_status(nova_client, + server_id, + security_group_id, + security_group_name, + is_connected=False) + + +@operation +@with_nova_client +@with_cinder_client +def attach_volume(nova_client, cinder_client, status_attempts, + status_timeout, **kwargs): + server_id = ctx.target.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + volume_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + + if is_external_relationship_not_conditionally_created(ctx): + ctx.logger.info('Validating external volume and server ' + 'are connected') + attachment = volume.get_attachment(cinder_client=cinder_client, + volume_id=volume_id, + server_id=server_id) + if attachment: + return + else: + raise NonRecoverableError( + 'Expected external resources server {0} and volume {1} to be ' + 'connected'.format(server_id, volume_id)) + + # Note: The 'device_name' property should actually be a property of the + # relationship between a server and a volume; It'll move to that + # relationship type once relationship properties are better supported. + device = ctx.source.node.properties[volume.DEVICE_NAME_PROPERTY] + nova_client.volumes.create_server_volume( + server_id, + volume_id, + device if device != 'auto' else None) + try: + vol, wait_succeeded = volume.wait_until_status( + cinder_client=cinder_client, + volume_id=volume_id, + status=volume.VOLUME_STATUS_IN_USE, + num_tries=status_attempts, + timeout=status_timeout + ) + if not wait_succeeded: + raise RecoverableError( + 'Waiting for volume status {0} failed - detaching volume and ' + 'retrying..'.format(volume.VOLUME_STATUS_IN_USE)) + if device == 'auto': + # The device name was assigned automatically so we + # query the actual device name + attachment = volume.get_attachment( + cinder_client=cinder_client, + volume_id=volume_id, + server_id=server_id + ) + device_name = attachment['device'] + ctx.logger.info('Detected device name for attachment of volume ' + '{0} to server {1}: {2}' + .format(volume_id, server_id, device_name)) + ctx.source.instance.runtime_properties[ + volume.DEVICE_NAME_PROPERTY] = device_name + except Exception, e: + if not isinstance(e, NonRecoverableError): + _prepare_attach_volume_to_be_repeated( + nova_client, cinder_client, server_id, volume_id, + status_attempts, status_timeout) + raise + + +def _prepare_attach_volume_to_be_repeated( + nova_client, cinder_client, server_id, volume_id, + status_attempts, status_timeout): + + ctx.logger.info('Cleaning after a failed attach_volume() call') + try: + _detach_volume(nova_client, cinder_client, server_id, volume_id, + status_attempts, status_timeout) + except Exception, e: + ctx.logger.error('Cleaning after a failed attach_volume() call failed ' + 'raising a \'{0}\' exception.'.format(e)) + raise NonRecoverableError(e) + + +def _detach_volume(nova_client, cinder_client, server_id, volume_id, + status_attempts, status_timeout): + attachment = volume.get_attachment(cinder_client=cinder_client, + volume_id=volume_id, + server_id=server_id) + if attachment: + nova_client.volumes.delete_server_volume(server_id, attachment['id']) + volume.wait_until_status(cinder_client=cinder_client, + volume_id=volume_id, + status=volume.VOLUME_STATUS_AVAILABLE, + num_tries=status_attempts, + timeout=status_timeout) + + +@operation +@with_nova_client +@with_cinder_client +def detach_volume(nova_client, cinder_client, status_attempts, + status_timeout, **kwargs): + if is_external_relationship(ctx): + ctx.logger.info('Not detaching volume from server since ' + 'external volume and server are being used') + return + + server_id = ctx.target.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + volume_id = ctx.source.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + + _detach_volume(nova_client, cinder_client, server_id, volume_id, + status_attempts, status_timeout) + + +def _fail_on_missing_required_parameters(obj, required_parameters, hint_where): + for k in required_parameters: + if k not in obj: + raise NonRecoverableError( + "Required parameter '{0}' is missing (under host's " + "properties.{1}). Required parameters are: {2}" + .format(k, hint_where, required_parameters)) + + +def _validate_external_server_keypair(nova_client): + keypair_id = get_openstack_id_of_single_connected_node_by_openstack_type( + ctx, KEYPAIR_OPENSTACK_TYPE, True) + if not keypair_id: + return + + keypair_instance_id = \ + [node_instance_id for node_instance_id, runtime_props in + ctx.capabilities.get_all().iteritems() if + runtime_props.get(OPENSTACK_ID_PROPERTY) == keypair_id][0] + keypair_node_properties = _get_properties_by_node_instance_id( + keypair_instance_id) + if not is_external_resource_by_properties(keypair_node_properties): + raise NonRecoverableError( + "Can't connect a new keypair node to a server node " + "with '{0}'=True".format(USE_EXTERNAL_RESOURCE_PROPERTY)) + + server = get_server_by_context(nova_client) + if keypair_id == _get_keypair_name_by_id(nova_client, server.key_name): + return + raise NonRecoverableError( + "Expected external resources server {0} and keypair {1} to be " + "connected".format(server.id, keypair_id)) + + +def _get_keypair_name_by_id(nova_client, key_name): + keypair = nova_client.cosmo_get_named(KEYPAIR_OPENSTACK_TYPE, key_name) + return keypair.id + + +def _validate_external_server_nics(neutron_client, network_ids, port_ids): + # validate no new nics are being assigned to an existing server (which + # isn't possible on Openstack) + new_nic_nodes = \ + [node_instance_id for node_instance_id, runtime_props in + ctx.capabilities.get_all().iteritems() if runtime_props.get( + OPENSTACK_TYPE_PROPERTY) in (PORT_OPENSTACK_TYPE, + NETWORK_OPENSTACK_TYPE) and + not is_external_resource_by_properties( + _get_properties_by_node_instance_id(node_instance_id))] + if new_nic_nodes: + raise NonRecoverableError( + "Can't connect new port and/or network nodes to a server node " + "with '{0}'=True".format(USE_EXTERNAL_RESOURCE_PROPERTY)) + + # validate all expected connected networks and ports are indeed already + # connected to the server. note that additional networks (e.g. the + # management network) may be connected as well with no error raised + if not network_ids and not port_ids: + return + + server_id = ctx.instance.runtime_properties[OPENSTACK_ID_PROPERTY] + connected_ports = neutron_client.list_ports(device_id=server_id)['ports'] + + # not counting networks connected by a connected port since allegedly + # the connection should be on a separate port + connected_ports_networks = {port['network_id'] for port in + connected_ports if port['id'] not in port_ids} + connected_ports_ids = {port['id'] for port in + connected_ports} + disconnected_networks = [network_id for network_id in network_ids if + network_id not in connected_ports_networks] + disconnected_ports = [port_id for port_id in port_ids if port_id not + in connected_ports_ids] + if disconnected_networks or disconnected_ports: + raise NonRecoverableError( + 'Expected external resources to be connected to external server {' + '0}: Networks - {1}; Ports - {2}'.format(server_id, + disconnected_networks, + disconnected_ports)) + + +def _get_properties_by_node_instance_id(node_instance_id): + client = get_rest_client() + node_instance = client.node_instances.get(node_instance_id) + node = client.nodes.get(ctx.deployment.id, node_instance.node_id) + return node.properties + + +@operation +@with_nova_client +def creation_validation(nova_client, args, **kwargs): + + def validate_server_property_value_exists(server_props, property_name): + ctx.logger.debug( + 'checking whether {0} exists...'.format(property_name)) + + serv_props_copy = server_props.copy() + try: + handle_image_from_relationship(serv_props_copy, 'image', ctx) + _handle_image_or_flavor(serv_props_copy, nova_client, + property_name) + except (NonRecoverableError, nova_exceptions.NotFound) as e: + # temporary error - once image/flavor_name get removed, these + # errors won't be relevant anymore + err = str(e) + ctx.logger.error('VALIDATION ERROR: ' + err) + raise NonRecoverableError(err) + + prop_value_id = str(serv_props_copy[property_name]) + prop_values = list(nova_client.cosmo_list(property_name)) + for f in prop_values: + if prop_value_id == f.id: + ctx.logger.debug('OK: {0} exists'.format(property_name)) + return + err = '{0} {1} does not exist'.format(property_name, prop_value_id) + ctx.logger.error('VALIDATION ERROR: ' + err) + if prop_values: + ctx.logger.info('list of available {0}s:'.format(property_name)) + for f in prop_values: + ctx.logger.info(' {0:>10} - {1}'.format(f.id, f.name)) + else: + ctx.logger.info('there are no available {0}s'.format( + property_name)) + raise NonRecoverableError(err) + + validate_resource(ctx, nova_client, SERVER_OPENSTACK_TYPE) + + server_props = dict(ctx.node.properties['server'], **args) + validate_server_property_value_exists(server_props, 'flavor') + + +def _get_private_key(private_key_path): + pk_node_by_rel = \ + get_single_connected_node_by_openstack_type( + ctx, KEYPAIR_OPENSTACK_TYPE, True) + + if private_key_path: + if pk_node_by_rel: + raise NonRecoverableError("server can't both have a " + '"private_key_path" input and be ' + 'connected to a keypair via a ' + 'relationship at the same time') + key_path = private_key_path + else: + if pk_node_by_rel and pk_node_by_rel.properties['private_key_path']: + key_path = pk_node_by_rel.properties['private_key_path'] + else: + key_path = ctx.bootstrap_context.cloudify_agent.agent_key_path + + if key_path: + key_path = os.path.expanduser(key_path) + if os.path.isfile(key_path): + return key_path + + err_message = 'Cannot find private key file' + if key_path: + err_message += '; expected file path was {0}'.format(key_path) + raise NonRecoverableError(err_message) + + +def _validate_security_group_and_server_connection_status( + nova_client, server_id, sg_id, sg_name, is_connected): + + # verifying the security group got connected or disconnected + # successfully - this is due to Openstack concurrency issues that may + # take place when attempting to connect/disconnect multiple SGs to the + # same server at the same time + server = nova_client.servers.get(server_id) + + if is_connected ^ any(sg for sg in server.list_security_group() if + sg.id == sg_id): + raise RecoverableError( + message='Security group {0} did not get {2} server {1} ' + 'properly' + .format( + sg_name, + server.name, + 'connected to' if is_connected else 'disconnected from')) + + +def _handle_image_or_flavor(server, nova_client, prop_name): + if prop_name not in server and '{0}_name'.format(prop_name) not in server: + # setting image or flavor - looking it up by name; if not found, then + # the value is assumed to be the id + server[prop_name] = ctx.node.properties[prop_name] + + # temporary error message: once the 'image' and 'flavor' properties + # become mandatory, this will become less relevant + if not server[prop_name]: + raise NonRecoverableError( + 'must set {0} by either setting a "{0}" property or by setting' + ' a "{0}" or "{0}_name" (deprecated) field under the "server" ' + 'property'.format(prop_name)) + + image_or_flavor = \ + nova_client.cosmo_get_if_exists(prop_name, name=server[prop_name]) + if image_or_flavor: + server[prop_name] = image_or_flavor.id + else: # Deprecated sugar + if '{0}_name'.format(prop_name) in server: + prop_name_plural = nova_client.cosmo_plural(prop_name) + server[prop_name] = \ + getattr(nova_client, prop_name_plural).find( + name=server['{0}_name'.format(prop_name)]).id + del server['{0}_name'.format(prop_name)] diff --git a/aria/multivim-plugin/nova_plugin/tests/__init__.py b/aria/multivim-plugin/nova_plugin/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/__init__.py diff --git a/aria/multivim-plugin/nova_plugin/tests/resources/test-keypair-validation-blueprint.yaml b/aria/multivim-plugin/nova_plugin/tests/resources/test-keypair-validation-blueprint.yaml new file mode 100644 index 0000000000..22b7fb5362 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/resources/test-keypair-validation-blueprint.yaml @@ -0,0 +1,23 @@ +tosca_definitions_version: cloudify_dsl_1_3 + +imports: + - https://raw.githubusercontent.com/cloudify-cosmo/cloudify-manager/4.1/resources/rest-service/cloudify/types/types.yaml + - plugin.yaml + +inputs: + private_key: {} + is_keypair_external: {} + + +node_templates: + + keypair: + type: cloudify.openstack.nodes.KeyPair + properties: + private_key_path: { get_input: private_key } + use_external_resource: { get_input: is_keypair_external } + openstack_config: + username: aaa + password: aaa + tenant_name: aaa + auth_url: aaa diff --git a/aria/multivim-plugin/nova_plugin/tests/resources/test-server-create-secgroup.yaml b/aria/multivim-plugin/nova_plugin/tests/resources/test-server-create-secgroup.yaml new file mode 100644 index 0000000000..70b75f6bf5 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/resources/test-server-create-secgroup.yaml @@ -0,0 +1,31 @@ +tosca_definitions_version: cloudify_dsl_1_3 + +imports: + - https://raw.githubusercontent.com/cloudify-cosmo/cloudify-manager/4.1/resources/rest-service/cloudify/types/types.yaml + - plugin.yaml + +inputs: + use_password: + type: boolean + default: false + +node_templates: + + security_group: + type: cloudify.openstack.nodes.SecurityGroup + + server: + type: cloudify.openstack.nodes.Server + properties: + install_agent: false + use_password: { get_input: use_password } + openstack_config: + username: aaa + password: aaa + tenant_name: aaa + auth_url: aaa + server: + key_name: 'aa' + relationships: + - type: cloudify.openstack.server_connected_to_security_group + target: security_group diff --git a/aria/multivim-plugin/nova_plugin/tests/resources/test-start-operation-retry-blueprint.yaml b/aria/multivim-plugin/nova_plugin/tests/resources/test-start-operation-retry-blueprint.yaml new file mode 100644 index 0000000000..275806cf5a --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/resources/test-start-operation-retry-blueprint.yaml @@ -0,0 +1,31 @@ +tosca_definitions_version: cloudify_dsl_1_3 + +imports: + - https://raw.githubusercontent.com/cloudify-cosmo/cloudify-manager/4.1/resources/rest-service/cloudify/types/types.yaml + - plugin.yaml + +inputs: + use_password: + type: boolean + default: false + +node_templates: + server: + type: cloudify.openstack.nodes.Server + properties: + install_agent: false + use_password: { get_input: use_password } + server: + key_name: key + scheduler_hints: + group: affinity-group-id + openstack_config: + username: aaa + password: aaa + tenant_name: aaa + auth_url: aaa + interfaces: + cloudify.interfaces.lifecycle: + start: + inputs: + start_retry_interval: 1 diff --git a/aria/multivim-plugin/nova_plugin/tests/test_relationships.py b/aria/multivim-plugin/nova_plugin/tests/test_relationships.py new file mode 100644 index 0000000000..2814057fb7 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/test_relationships.py @@ -0,0 +1,228 @@ +######### +# Copyright (c) 2016 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +"""Test the functions related to retrieving relationship information. + +Functions under test are mostly inside openstack_plugin_common: +get_relationships_by_openstack_type +get_connected_nodes_by_openstack_type +get_openstack_ids_of_connected_nodes_by_openstack_type +get_single_connected_node_by_openstack_type +""" + +import uuid +from unittest import TestCase + +from neutron_plugin.network import NETWORK_OPENSTACK_TYPE + +from cloudify.exceptions import NonRecoverableError + +from cloudify.mocks import ( + MockCloudifyContext, + MockNodeContext, + MockNodeInstanceContext, + MockRelationshipContext, + MockRelationshipSubjectContext, +) +from openstack_plugin_common import ( + OPENSTACK_ID_PROPERTY, + OPENSTACK_TYPE_PROPERTY, + get_openstack_id_of_single_connected_node_by_openstack_type, + get_openstack_ids_of_connected_nodes_by_openstack_type, + get_relationships_by_openstack_type, + get_single_connected_node_by_openstack_type, +) + + +class RelationshipsTestBase(TestCase): + def _make_vm_ctx_with_relationships(self, rel_specs, properties=None): + """Prepare a mock CloudifyContext from the given relationship spec. + + rel_specs is an ordered collection of relationship specs - dicts + with the keys "node" and "instance" used to construct the + MockNodeContext and the MockNodeInstanceContext, and optionally a + "type" key. + Examples: [ + {}, + {"node": {"id": 5}}, + { + "type": "some_type", + "instance": { + "id": 3, + "runtime_properties":{} + } + } + ] + """ + if properties is None: + properties = {} + relationships = [] + for rel_spec in rel_specs: + node = rel_spec.get('node', {}) + node_id = node.pop('id', uuid.uuid4().hex) + + instance = rel_spec.get('instance', {}) + instance_id = instance.pop('id', '{0}_{1}'.format( + node_id, uuid.uuid4().hex)) + if 'properties' not in node: + node['properties'] = {} + node_ctx = MockNodeContext(id=node_id, **node) + instance_ctx = MockNodeInstanceContext(id=instance_id, **instance) + + rel_subject_ctx = MockRelationshipSubjectContext( + node=node_ctx, instance=instance_ctx) + rel_type = rel_spec.get('type') + rel_ctx = MockRelationshipContext(target=rel_subject_ctx, + type=rel_type) + relationships.append(rel_ctx) + return MockCloudifyContext(node_id='vm', properties=properties, + relationships=relationships) + + +class TestGettingRelatedResources(RelationshipsTestBase): + + def test_get_relationships_finds_all_by_type(self): + """get_relationships_by_openstack_type returns all rels that match.""" + rel_specs = [{ + 'instance': { + 'id': instance_id, + 'runtime_properties': { + OPENSTACK_TYPE_PROPERTY: NETWORK_OPENSTACK_TYPE + } + } + } for instance_id in range(3)] + + rel_specs.append({ + 'instance': { + 'runtime_properties': { + OPENSTACK_TYPE_PROPERTY: 'something else' + } + } + }) + + ctx = self._make_vm_ctx_with_relationships(rel_specs) + filtered = get_relationships_by_openstack_type(ctx, + NETWORK_OPENSTACK_TYPE) + self.assertEqual(3, len(filtered)) + + def test_get_ids_of_nodes_by_type(self): + + rel_spec = { + 'instance': { + 'runtime_properties': { + OPENSTACK_TYPE_PROPERTY: NETWORK_OPENSTACK_TYPE, + OPENSTACK_ID_PROPERTY: 'the node id' + } + } + } + ctx = self._make_vm_ctx_with_relationships([rel_spec]) + ids = get_openstack_ids_of_connected_nodes_by_openstack_type( + ctx, NETWORK_OPENSTACK_TYPE) + self.assertEqual(['the node id'], ids) + + +class TestGetSingleByID(RelationshipsTestBase): + def _make_instances(self, ids): + """Mock a context with relationships to instances with given ids.""" + rel_specs = [{ + 'node': { + 'id': node_id + }, + 'instance': { + 'runtime_properties': { + OPENSTACK_TYPE_PROPERTY: NETWORK_OPENSTACK_TYPE, + OPENSTACK_ID_PROPERTY: node_id + } + } + } for node_id in ids] + return self._make_vm_ctx_with_relationships(rel_specs) + + def test_get_single_id(self): + ctx = self._make_instances(['the node id']) + found_id = get_openstack_id_of_single_connected_node_by_openstack_type( + ctx, NETWORK_OPENSTACK_TYPE) + self.assertEqual('the node id', found_id) + + def test_get_single_id_two_found(self): + ctx = self._make_instances([0, 1]) + self.assertRaises( + NonRecoverableError, + get_openstack_id_of_single_connected_node_by_openstack_type, ctx, + NETWORK_OPENSTACK_TYPE) + + def test_get_single_id_two_found_if_exists_true(self): + ctx = self._make_instances([0, 1]) + + try: + get_openstack_id_of_single_connected_node_by_openstack_type( + ctx, NETWORK_OPENSTACK_TYPE, if_exists=True) + except NonRecoverableError as e: + self.assertIn(NETWORK_OPENSTACK_TYPE, e.message) + else: + self.fail() + + def test_get_single_id_if_exists_none_found(self): + ctx = self._make_instances([]) + found = get_openstack_id_of_single_connected_node_by_openstack_type( + ctx, NETWORK_OPENSTACK_TYPE, if_exists=True) + self.assertIsNone(found) + + def test_get_single_id_none_found(self): + rel_spec = [] + ctx = self._make_vm_ctx_with_relationships(rel_spec) + self.assertRaises( + NonRecoverableError, + get_openstack_id_of_single_connected_node_by_openstack_type, + ctx, + NETWORK_OPENSTACK_TYPE) + + def test_get_single_node(self): + ctx = self._make_instances(['the node id']) + found_node = get_single_connected_node_by_openstack_type( + ctx, NETWORK_OPENSTACK_TYPE) + self.assertEqual('the node id', found_node.id) + + def test_get_single_node_two_found(self): + ctx = self._make_instances([0, 1]) + self.assertRaises( + NonRecoverableError, + get_single_connected_node_by_openstack_type, + ctx, NETWORK_OPENSTACK_TYPE) + + def test_get_single_node_two_found_if_exists(self): + ctx = self._make_instances([0, 1]) + + self.assertRaises( + NonRecoverableError, + get_single_connected_node_by_openstack_type, + ctx, + NETWORK_OPENSTACK_TYPE, + if_exists=True) + + def test_get_single_node_if_exists_none_found(self): + ctx = self._make_instances([]) + + found = get_single_connected_node_by_openstack_type( + ctx, NETWORK_OPENSTACK_TYPE, if_exists=True) + self.assertIsNone(found) + + def test_get_single_node_none_found(self): + ctx = self._make_instances([]) + + self.assertRaises( + NonRecoverableError, + get_single_connected_node_by_openstack_type, + ctx, + NETWORK_OPENSTACK_TYPE) diff --git a/aria/multivim-plugin/nova_plugin/tests/test_server.py b/aria/multivim-plugin/nova_plugin/tests/test_server.py new file mode 100644 index 0000000000..a50930555c --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/test_server.py @@ -0,0 +1,551 @@ +######### +# Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +from os import path +import tempfile + +import unittest +import mock + +import nova_plugin +from cloudify.test_utils import workflow_test + +from openstack_plugin_common import NeutronClientWithSugar, \ + OPENSTACK_TYPE_PROPERTY, OPENSTACK_ID_PROPERTY +from neutron_plugin.network import NETWORK_OPENSTACK_TYPE +from neutron_plugin.port import PORT_OPENSTACK_TYPE +from nova_plugin.tests.test_relationships import RelationshipsTestBase +from nova_plugin.server import _prepare_server_nics +from cinder_plugin.volume import VOLUME_OPENSTACK_TYPE +from cloudify.exceptions import NonRecoverableError +from cloudify.state import current_ctx + +from cloudify.utils import setup_logger + +from cloudify.mocks import ( + MockNodeContext, + MockCloudifyContext, + MockNodeInstanceContext, + MockRelationshipContext, + MockRelationshipSubjectContext +) + + +class TestServer(unittest.TestCase): + + blueprint_path = path.join('resources', + 'test-start-operation-retry-blueprint.yaml') + + @mock.patch('nova_plugin.server.create') + @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties') + @workflow_test(blueprint_path, copy_plugin_yaml=True) + def test_nova_server_lifecycle_start(self, cfy_local, *_): + + test_vars = { + 'counter': 0, + 'server': mock.MagicMock() + } + + def mock_get_server_by_context(*_): + s = test_vars['server'] + if test_vars['counter'] == 0: + s.status = nova_plugin.server.SERVER_STATUS_BUILD + else: + s.status = nova_plugin.server.SERVER_STATUS_ACTIVE + test_vars['counter'] += 1 + return s + + with mock.patch('nova_plugin.server.get_server_by_context', + new=mock_get_server_by_context): + cfy_local.execute('install', task_retries=3) + + self.assertEqual(2, test_vars['counter']) + self.assertEqual(0, test_vars['server'].start.call_count) + + @workflow_test(blueprint_path, copy_plugin_yaml=True) + @mock.patch('nova_plugin.server.create') + @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties') + def test_nova_server_lifecycle_start_after_stop(self, cfy_local, *_): + + test_vars = { + 'counter': 0, + 'server': mock.MagicMock() + } + + def mock_get_server_by_context(_): + s = test_vars['server'] + if test_vars['counter'] == 0: + s.status = nova_plugin.server.SERVER_STATUS_SHUTOFF + elif test_vars['counter'] == 1: + setattr(s, + nova_plugin.server.OS_EXT_STS_TASK_STATE, + nova_plugin.server.SERVER_TASK_STATE_POWERING_ON) + else: + s.status = nova_plugin.server.SERVER_STATUS_ACTIVE + test_vars['counter'] += 1 + test_vars['server'] = s + return s + + with mock.patch('nova_plugin.server.get_server_by_context', + new=mock_get_server_by_context): + cfy_local.execute('install', task_retries=3) + + self.assertEqual(1, test_vars['server'].start.call_count) + self.assertEqual(3, test_vars['counter']) + + @workflow_test(blueprint_path, copy_plugin_yaml=True) + @mock.patch('nova_plugin.server.create') + @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties') + def test_nova_server_lifecycle_start_unknown_status(self, cfy_local, *_): + test_vars = { + 'counter': 0, + 'server': mock.MagicMock() + } + + def mock_get_server_by_context(_): + s = test_vars['server'] + if test_vars['counter'] == 0: + s.status = '### unknown-status ###' + test_vars['counter'] += 1 + test_vars['server'] = s + return s + + with mock.patch('nova_plugin.server.get_server_by_context', + new=mock_get_server_by_context): + self.assertRaisesRegexp(RuntimeError, + 'Unexpected server state', + cfy_local.execute, + 'install') + + self.assertEqual(0, test_vars['server'].start.call_count) + self.assertEqual(1, test_vars['counter']) + + @workflow_test(blueprint_path, copy_plugin_yaml=True) + @mock.patch('nova_plugin.server.start') + @mock.patch('nova_plugin.server._handle_image_or_flavor') + @mock.patch('nova_plugin.server._fail_on_missing_required_parameters') + @mock.patch('openstack_plugin_common.nova_client') + def test_nova_server_creation_param_integrity( + self, cfy_local, mock_nova, *args): + cfy_local.execute('install', task_retries=0) + calls = mock_nova.Client.return_value.servers.method_calls + self.assertEqual(1, len(calls)) + kws = calls[0][2] + self.assertIn('scheduler_hints', kws) + self.assertEqual(kws['scheduler_hints'], + {'group': 'affinity-group-id'}, + 'expecting \'scheduler_hints\' value to exist') + + @workflow_test(blueprint_path, copy_plugin_yaml=True, + inputs={'use_password': True}) + @mock.patch('nova_plugin.server.create') + @mock.patch('nova_plugin.server._set_network_and_ip_runtime_properties') + @mock.patch( + 'nova_plugin.server.get_single_connected_node_by_openstack_type', + autospec=True, return_value=None) + def test_nova_server_with_use_password(self, cfy_local, *_): + + test_vars = { + 'counter': 0, + 'server': mock.MagicMock() + } + + tmp_path = tempfile.NamedTemporaryFile(prefix='key_name') + key_path = tmp_path.name + + def mock_get_server_by_context(_): + s = test_vars['server'] + if test_vars['counter'] == 0: + s.status = nova_plugin.server.SERVER_STATUS_BUILD + else: + s.status = nova_plugin.server.SERVER_STATUS_ACTIVE + test_vars['counter'] += 1 + + def check_agent_key_path(private_key): + self.assertEqual(private_key, key_path) + return private_key + + s.get_password = check_agent_key_path + return s + + with mock.patch('nova_plugin.server.get_server_by_context', + mock_get_server_by_context): + with mock.patch( + 'cloudify.context.BootstrapContext.' + 'CloudifyAgent.agent_key_path', + new_callable=mock.PropertyMock, return_value=key_path): + cfy_local.execute('install', task_retries=5) + + +class TestMergeNICs(unittest.TestCase): + def test_merge_prepends_management_network(self): + """When the mgmt network isnt in a relationship, its the 1st nic.""" + mgmt_network_id = 'management network' + nics = [{'net-id': 'other network'}] + + merged = nova_plugin.server._merge_nics(mgmt_network_id, nics) + + self.assertEqual(len(merged), 2) + self.assertEqual(merged[0]['net-id'], 'management network') + + def test_management_network_in_relationships(self): + """When the mgmt network was in a relationship, it's not prepended.""" + mgmt_network_id = 'management network' + nics = [{'net-id': 'other network'}, {'net-id': 'management network'}] + + merged = nova_plugin.server._merge_nics(mgmt_network_id, nics) + + self.assertEqual(nics, merged) + + +class TestNormalizeNICs(unittest.TestCase): + def test_normalize_port_priority(self): + """Whe there's both net-id and port-id, port-id is used.""" + nics = [{'net-id': '1'}, {'port-id': '2'}, {'net-id': 3, 'port-id': 4}] + normalized = nova_plugin.server._normalize_nics(nics) + expected = [{'net-id': '1'}, {'port-id': '2'}, {'port-id': 4}] + self.assertEqual(expected, normalized) + + +class MockNeutronClient(NeutronClientWithSugar): + """A fake neutron client with hard-coded test data.""" + + @mock.patch('openstack_plugin_common.OpenStackClient.__init__', + new=mock.Mock()) + def __init__(self): + super(MockNeutronClient, self).__init__() + + @staticmethod + def _search_filter(objs, search_params): + """Mock neutron's filtering by attributes in list_* methods. + + list_* methods (list_networks, list_ports) + """ + def _matches(obj, search_params): + return all(obj[k] == v for k, v in search_params.items()) + return [obj for obj in objs if _matches(obj, search_params)] + + def list_networks(self, **search_params): + networks = [ + {'name': 'network1', 'id': '1'}, + {'name': 'network2', 'id': '2'}, + {'name': 'network3', 'id': '3'}, + {'name': 'network4', 'id': '4'}, + {'name': 'network5', 'id': '5'}, + {'name': 'network6', 'id': '6'}, + {'name': 'other', 'id': 'other'} + ] + return {'networks': self._search_filter(networks, search_params)} + + def list_ports(self, **search_params): + ports = [ + {'name': 'port1', 'id': '1', 'network_id': '1'}, + {'name': 'port2', 'id': '2', 'network_id': '1'}, + {'name': 'port3', 'id': '3', 'network_id': '2'}, + {'name': 'port4', 'id': '4', 'network_id': '2'}, + ] + return {'ports': self._search_filter(ports, search_params)} + + def show_port(self, port_id): + ports = self.list_ports(id=port_id) + return {'port': ports['ports'][0]} + + +class NICTestBase(RelationshipsTestBase): + """Base test class for the NICs tests. + + It comes with helper methods to create a mock cloudify context, with + the specified relationships. + """ + mock_neutron = MockNeutronClient() + + def _relationship_spec(self, obj, objtype): + return {'node': {'properties': obj}, + 'instance': { + 'runtime_properties': {OPENSTACK_TYPE_PROPERTY: objtype, + OPENSTACK_ID_PROPERTY: obj['id']}}} + + def _make_vm_ctx_with_ports(self, management_network_name, ports): + port_specs = [self._relationship_spec(obj, PORT_OPENSTACK_TYPE) + for obj in ports] + vm_properties = {'management_network_name': management_network_name} + return self._make_vm_ctx_with_relationships(port_specs, + vm_properties) + + def _make_vm_ctx_with_networks(self, management_network_name, networks): + network_specs = [self._relationship_spec(obj, NETWORK_OPENSTACK_TYPE) + for obj in networks] + vm_properties = {'management_network_name': management_network_name} + return self._make_vm_ctx_with_relationships(network_specs, + vm_properties) + + +class TestServerNICs(NICTestBase): + """Test preparing the NICs list from server<->network relationships. + + Each test creates a cloudify context that represents a openstack VM + with relationships to networks. Then, examine the NICs list produced from + the relationships. + """ + def test_nova_server_creation_nics_ordering(self): + """NIC list keeps the order of the relationships. + + The nics= list passed to nova.server.create should be ordered + depending on the relationships to the networks (as defined in the + blueprint). + """ + ctx = self._make_vm_ctx_with_networks( + management_network_name='network1', + networks=[ + {'id': '1'}, + {'id': '2'}, + {'id': '3'}, + {'id': '4'}, + {'id': '5'}, + {'id': '6'}, + ]) + server = {'meta': {}} + + _prepare_server_nics( + self.mock_neutron, ctx, server) + + self.assertEqual( + ['1', '2', '3', '4', '5', '6'], + [n['net-id'] for n in server['nics']]) + + def test_server_creation_prepends_mgmt_network(self): + """If the management network isn't in a relation, it's the first NIC. + + Creating the server examines the relationships, and if it doesn't find + a relationship to the management network, it adds the management + network to the NICs list, as the first element. + """ + ctx = self._make_vm_ctx_with_networks( + management_network_name='other', + networks=[ + {'id': '1'}, + {'id': '2'}, + {'id': '3'}, + {'id': '4'}, + {'id': '5'}, + {'id': '6'}, + ]) + server = {'meta': {}} + + _prepare_server_nics( + self.mock_neutron, ctx, server) + + first_nic = server['nics'][0] + self.assertEqual('other', first_nic['net-id']) + self.assertEqual(7, len(server['nics'])) + + def test_server_creation_uses_relation_mgmt_nic(self): + """If the management network is in a relation, it isn't prepended. + + If the server has a relationship to the management network, + a new NIC isn't prepended to the list. + """ + ctx = self._make_vm_ctx_with_networks( + management_network_name='network1', + networks=[ + {'id': '1'}, + {'id': '2'}, + {'id': '3'}, + {'id': '4'}, + {'id': '5'}, + {'id': '6'}, + ]) + server = {'meta': {}} + + _prepare_server_nics( + self.mock_neutron, ctx, server) + self.assertEqual(6, len(server['nics'])) + + +class TestServerPortNICs(NICTestBase): + """Test preparing the NICs list from server<->port relationships. + + Create a cloudify ctx representing a vm with relationships to + openstack ports. Then examine the resulting NICs list: check that it + contains the networks that the ports were connected to, and that each + connection uses the port that was provided. + """ + + def test_network_with_port(self): + """Port on the management network is used to connect to it. + + The NICs list entry for the management network contains the + port-id of the port from the relationship, but doesn't contain net-id. + """ + ports = [{'id': '1'}] + ctx = self._make_vm_ctx_with_ports('network1', ports) + server = {'meta': {}} + + _prepare_server_nics( + self.mock_neutron, ctx, server) + + self.assertEqual([{'port-id': '1'}], server['nics']) + + def test_port_not_to_mgmt_network(self): + """A NICs list entry is added with the network and the port. + + A relationship to a port must not only add a NIC, but the NIC must + also make sure to use that port. + """ + ports = [{'id': '1'}] + ctx = self._make_vm_ctx_with_ports('other', ports) + server = {'meta': {}} + + _prepare_server_nics( + self.mock_neutron, ctx, server) + expected = [ + {'net-id': 'other'}, + {'port-id': '1'} + ] + self.assertEqual(expected, server['nics']) + + +class TestBootFromVolume(unittest.TestCase): + + @mock.patch('nova_plugin.server._get_boot_volume_relationships', + autospec=True) + def test_handle_boot_volume(self, mock_get_rels): + mock_get_rels.return_value.runtime_properties = { + 'external_id': 'test-id', + 'availability_zone': 'test-az', + } + server = {} + ctx = mock.MagicMock() + nova_plugin.server._handle_boot_volume(server, ctx) + self.assertEqual({'vda': 'test-id:::0'}, + server['block_device_mapping']) + self.assertEqual('test-az', + server['availability_zone']) + + @mock.patch('nova_plugin.server._get_boot_volume_relationships', + autospec=True, return_value=[]) + def test_handle_boot_volume_no_boot_volume(self, *_): + server = {} + ctx = mock.MagicMock() + nova_plugin.server._handle_boot_volume(server, ctx) + self.assertNotIn('block_device_mapping', server) + + +class TestImageFromRelationships(unittest.TestCase): + + @mock.patch('glance_plugin.image.' + 'get_openstack_ids_of_connected_nodes_by_openstack_type', + autospec=True, return_value=['test-id']) + def test_handle_boot_image(self, *_): + server = {} + ctx = mock.MagicMock() + nova_plugin.server.handle_image_from_relationship(server, 'image', ctx) + self.assertEqual({'image': 'test-id'}, server) + + @mock.patch('glance_plugin.image.' + 'get_openstack_ids_of_connected_nodes_by_openstack_type', + autospec=True, return_value=[]) + def test_handle_boot_image_no_image(self, *_): + server = {} + ctx = mock.MagicMock() + nova_plugin.server.handle_image_from_relationship(server, 'image', ctx) + self.assertNotIn('image', server) + + +class TestServerRelationships(unittest.TestCase): + + def _get_ctx_mock(self, instance_id, boot): + rel_specs = [MockRelationshipContext( + target=MockRelationshipSubjectContext(node=MockNodeContext( + properties={'boot': boot}), instance=MockNodeInstanceContext( + runtime_properties={ + OPENSTACK_TYPE_PROPERTY: VOLUME_OPENSTACK_TYPE, + OPENSTACK_ID_PROPERTY: instance_id + })))] + ctx = mock.MagicMock() + ctx.instance = MockNodeInstanceContext(relationships=rel_specs) + ctx.logger = setup_logger('mock-logger') + return ctx + + def test_boot_volume_relationship(self): + instance_id = 'test-id' + ctx = self._get_ctx_mock(instance_id, True) + result = nova_plugin.server._get_boot_volume_relationships( + VOLUME_OPENSTACK_TYPE, ctx) + self.assertEqual( + instance_id, + result.runtime_properties['external_id']) + + def test_no_boot_volume_relationship(self): + instance_id = 'test-id' + ctx = self._get_ctx_mock(instance_id, False) + result = nova_plugin.server._get_boot_volume_relationships( + VOLUME_OPENSTACK_TYPE, ctx) + self.assertFalse(result) + + +class TestServerNetworkRuntimeProperties(unittest.TestCase): + + @property + def mock_ctx(self): + return MockCloudifyContext( + node_id='test', + deployment_id='test', + properties={}, + operation={'retry_number': 0}, + provider_context={'resources': {}} + ) + + def test_server_networks_runtime_properties_empty_server(self): + ctx = self.mock_ctx + current_ctx.set(ctx=ctx) + server = mock.MagicMock() + setattr(server, 'networks', {}) + with self.assertRaisesRegexp( + NonRecoverableError, + 'The server was created but not attached to a network.'): + nova_plugin.server._set_network_and_ip_runtime_properties(server) + + def test_server_networks_runtime_properties_valid_networks(self): + ctx = self.mock_ctx + current_ctx.set(ctx=ctx) + server = mock.MagicMock() + network_id = 'management_network' + network_ips = ['good', 'bad1', 'bad2'] + setattr(server, + 'networks', + {network_id: network_ips}) + nova_plugin.server._set_network_and_ip_runtime_properties(server) + self.assertIn('networks', ctx.instance.runtime_properties.keys()) + self.assertIn('ip', ctx.instance.runtime_properties.keys()) + self.assertEquals(ctx.instance.runtime_properties['ip'], 'good') + self.assertEquals(ctx.instance.runtime_properties['networks'], + {network_id: network_ips}) + + def test_server_networks_runtime_properties_empty_networks(self): + ctx = self.mock_ctx + current_ctx.set(ctx=ctx) + server = mock.MagicMock() + network_id = 'management_network' + network_ips = [] + setattr(server, + 'networks', + {network_id: network_ips}) + nova_plugin.server._set_network_and_ip_runtime_properties(server) + self.assertIn('networks', ctx.instance.runtime_properties.keys()) + self.assertIn('ip', ctx.instance.runtime_properties.keys()) + self.assertEquals(ctx.instance.runtime_properties['ip'], None) + self.assertEquals(ctx.instance.runtime_properties['networks'], + {network_id: network_ips}) diff --git a/aria/multivim-plugin/nova_plugin/tests/test_server_image_and_flavor.py b/aria/multivim-plugin/nova_plugin/tests/test_server_image_and_flavor.py new file mode 100644 index 0000000000..2ae475843c --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/test_server_image_and_flavor.py @@ -0,0 +1,228 @@ +######### +# Copyright (c) 2014 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + + +import unittest + +import mock +from novaclient import exceptions as nova_exceptions + +import nova_plugin.server as server +from cloudify.exceptions import NonRecoverableError +from cloudify.mocks import MockCloudifyContext + + +class TestServerImageAndFlavor(unittest.TestCase): + + def test_no_image_and_no_flavor(self): + node_props = { + 'image': '', + 'flavor': '' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + self.assertRaises(NonRecoverableError, + server._handle_image_or_flavor, + serv, nova_client, 'image') + self.assertRaises(NonRecoverableError, + server._handle_image_or_flavor, + serv, nova_client, 'flavor') + + def test_image_and_flavor_properties_as_names(self): + node_props = { + 'image': 'some-image-name', + 'flavor': 'some-flavor-name' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + server._handle_image_or_flavor(serv, nova_client, 'image') + server._handle_image_or_flavor(serv, nova_client, 'flavor') + + self.assertEquals('some-image-id', serv.get('image')) + self.assertEquals('some-flavor-id', serv.get('flavor')) + + def test_image_and_flavor_properties_as_ids(self): + node_props = { + 'image': 'some-image-id', + 'flavor': 'some-flavor-id' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + server._handle_image_or_flavor(serv, nova_client, 'image') + server._handle_image_or_flavor(serv, nova_client, 'flavor') + + self.assertEquals('some-image-id', serv.get('image')) + self.assertEquals('some-flavor-id', serv.get('flavor')) + + def test_image_id_and_flavor_id(self): + node_props = { + 'image': '', + 'flavor': '' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + serv['image'] = 'some-image-id' + serv['flavor'] = 'some-flavor-id' + server._handle_image_or_flavor(serv, nova_client, 'image') + server._handle_image_or_flavor(serv, nova_client, 'flavor') + + self.assertEquals('some-image-id', serv.get('image')) + self.assertEquals('some-flavor-id', serv.get('flavor')) + + def test_image_name_and_flavor_name(self): + node_props = { + 'image': '', + 'flavor': '' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + serv['image_name'] = 'some-image-name' + serv['flavor_name'] = 'some-flavor-name' + server._handle_image_or_flavor(serv, nova_client, 'image') + server._handle_image_or_flavor(serv, nova_client, 'flavor') + + self.assertEquals('some-image-id', serv.get('image')) + self.assertNotIn('image_name', serv) + self.assertEquals('some-flavor-id', serv.get('flavor')) + self.assertNotIn('flavor_name', serv) + + def test_unknown_image_name_and_flavor_name(self): + node_props = { + 'image': '', + 'flavor': '' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + serv['image_name'] = 'some-unknown-image-name' + serv['flavor_name'] = 'some-unknown-flavor-name' + + self.assertRaises(nova_exceptions.NotFound, + server._handle_image_or_flavor, + serv, nova_client, 'image') + self.assertRaises(nova_exceptions.NotFound, + server._handle_image_or_flavor, + serv, nova_client, 'flavor') + + def test_image_id_and_flavor_id_override_on_properties(self): + node_props = { + 'image': 'properties-image-id', + 'flavor': 'properties-flavor-id' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + serv['image'] = 'some-image-id' + serv['flavor'] = 'some-flavor-id' + server._handle_image_or_flavor(serv, nova_client, 'image') + server._handle_image_or_flavor(serv, nova_client, 'flavor') + + self.assertEquals('some-image-id', serv.get('image')) + self.assertEquals('some-flavor-id', serv.get('flavor')) + + def test_image_name_and_flavor_name_override_on_properties(self): + node_props = { + 'image': 'properties-image-id', + 'flavor': 'properties-flavor-id' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + serv['image_name'] = 'some-image-name' + serv['flavor_name'] = 'some-flavor-name' + server._handle_image_or_flavor(serv, nova_client, 'image') + server._handle_image_or_flavor(serv, nova_client, 'flavor') + + self.assertEquals('some-image-id', serv.get('image')) + self.assertNotIn('image_name', serv) + self.assertEquals('some-flavor-id', serv.get('flavor')) + self.assertNotIn('flavor_name', serv) + + def test_image_name_and_flavor_name_override_on_image_and_flavor_ids(self): + node_props = { + 'image': '', + 'flavor': '' + } + with mock.patch('nova_plugin.server.ctx', + self._get_mock_ctx_with_node_properties(node_props)): + nova_client = self._get_mocked_nova_client() + + serv = {} + serv['image'] = 'some-bad-image-id' + serv['image_name'] = 'some-image-name' + serv['flavor'] = 'some-bad-flavor-id' + serv['flavor_name'] = 'some-flavor-name' + server._handle_image_or_flavor(serv, nova_client, 'image') + server._handle_image_or_flavor(serv, nova_client, 'flavor') + + self.assertEquals('some-image-id', serv.get('image')) + self.assertNotIn('image_name', serv) + self.assertEquals('some-flavor-id', serv.get('flavor')) + self.assertNotIn('flavor_name', serv) + + @staticmethod + def _get_mocked_nova_client(): + nova_client = mock.MagicMock() + + def mock_get_if_exists(prop_name, **kwargs): + is_image = prop_name == 'image' + searched_name = kwargs.get('name') + if (is_image and searched_name == 'some-image-name') or \ + (not is_image and searched_name == 'some-flavor-name'): + result = mock.MagicMock() + result.id = 'some-image-id' if \ + is_image else 'some-flavor-id' + return result + return [] + + def mock_find_generator(prop_name): + def mock_find(**kwargs): + result = mock_get_if_exists(prop_name, **kwargs) + if not result: + raise nova_exceptions.NotFound(404) + return result + return mock_find + + nova_client.cosmo_plural = lambda x: '{0}s'.format(x) + nova_client.cosmo_get_if_exists = mock_get_if_exists + nova_client.images.find = mock_find_generator('image') + nova_client.flavors.find = mock_find_generator('flavor') + return nova_client + + @staticmethod + def _get_mock_ctx_with_node_properties(properties): + return MockCloudifyContext(node_id='test_node_id', + properties=properties) diff --git a/aria/multivim-plugin/nova_plugin/tests/test_userdata.py b/aria/multivim-plugin/nova_plugin/tests/test_userdata.py new file mode 100644 index 0000000000..d7f056d72c --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/test_userdata.py @@ -0,0 +1,63 @@ +######### +# Copyright (c) 2015 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +import unittest + +import mock + +from cloudify.mocks import MockCloudifyContext + +from nova_plugin import userdata + + +def ctx_mock(): + result = MockCloudifyContext( + node_id='d', + properties={}) + result.node.type_hierarchy = ['cloudify.nodes.Compute'] + return result + + +class TestServerUserdataHandling(unittest.TestCase): + + @mock.patch('nova_plugin.userdata.ctx', ctx_mock()) + def test_no_userdata(self): + server_conf = {} + userdata.handle_userdata(server_conf) + self.assertEqual(server_conf, {}) + + def test_agent_installation_userdata(self): + ctx = ctx_mock() + ctx.agent.init_script = lambda: 'SCRIPT' + with mock.patch('nova_plugin.userdata.ctx', ctx): + server_conf = {} + userdata.handle_userdata(server_conf) + self.assertEqual(server_conf, {'userdata': 'SCRIPT'}) + + @mock.patch('nova_plugin.userdata.ctx', ctx_mock()) + def test_existing_userdata(self): + server_conf = {'userdata': 'EXISTING'} + server_conf_copy = server_conf.copy() + userdata.handle_userdata(server_conf) + self.assertEqual(server_conf, server_conf_copy) + + def test_existing_and_agent_installation_userdata(self): + ctx = ctx_mock() + ctx.agent.init_script = lambda: '#! SCRIPT' + with mock.patch('nova_plugin.userdata.ctx', ctx): + server_conf = {'userdata': '#! EXISTING'} + userdata.handle_userdata(server_conf) + self.assertTrue(server_conf['userdata'].startswith( + 'Content-Type: multi')) diff --git a/aria/multivim-plugin/nova_plugin/tests/test_validation.py b/aria/multivim-plugin/nova_plugin/tests/test_validation.py new file mode 100644 index 0000000000..aa1dfdd814 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/tests/test_validation.py @@ -0,0 +1,194 @@ +######### +# Copyright (c) 2016 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +import os +from os import path +import tempfile +import shutil + +import unittest +import mock + +from cloudify.test_utils import workflow_test +from nova_plugin.keypair import creation_validation +from cloudify.exceptions import NonRecoverableError + +PRIVATE_KEY_NAME = 'private_key' + + +class TestValidation(unittest.TestCase): + + blueprint_path = path.join('resources', + 'test-keypair-validation-blueprint.yaml') + + def setUp(self): + _, fp = tempfile.mkstemp() + self.private_key = fp + _, fp = tempfile.mkstemp() + self.not_readable_private_key = fp + os.chmod(self.not_readable_private_key, 0o200) + self.temp_dir = tempfile.mkdtemp() + self.not_writable_temp_dir_r = tempfile.mkdtemp() + os.chmod(self.not_writable_temp_dir_r, 0o400) + self.not_writable_temp_dir_rx = tempfile.mkdtemp() + os.chmod(self.not_writable_temp_dir_rx, 0o500) + self.not_writable_temp_dir_rw = tempfile.mkdtemp() + os.chmod(self.not_writable_temp_dir_rw, 0o600) + + def tearDown(self): + if self.private_key: + os.remove(self.private_key) + + if self.not_readable_private_key: + os.remove(self.not_readable_private_key) + + shutil.rmtree(self.not_writable_temp_dir_r, ignore_errors=True) + shutil.rmtree(self.not_writable_temp_dir_rx, ignore_errors=True) + shutil.rmtree(self.not_writable_temp_dir_rw, ignore_errors=True) + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def new_keypair_create(self, *args, **kwargs): + creation_validation(*args, **kwargs) + + def new_keypair_create_with_exception(self, *args, **kwargs): + self.assertRaises(NonRecoverableError, creation_validation, + *args, **kwargs) + + def get_keypair_inputs_private_key(self, is_external, **kwargs): + return { + 'private_key': self.private_key, + 'is_keypair_external': is_external + } + + def get_keypair_inputs_not_readable_private_key(self, + is_external, **kwargs): + return { + 'private_key': self.not_readable_private_key, + 'is_keypair_external': is_external + } + + def get_keypair_inputs_not_writable_dir_r(self, is_external, **kwargs): + return { + 'private_key': path.join(self.not_writable_temp_dir_r, + PRIVATE_KEY_NAME), + 'is_keypair_external': is_external + } + + def get_keypair_inputs_not_writable_dir_rx(self, is_external, **kwargs): + return { + 'private_key': path.join(self.not_writable_temp_dir_rx, + PRIVATE_KEY_NAME), + 'is_keypair_external': is_external + } + + def get_keypair_inputs_not_writable_dir_rw(self, is_external, **kwargs): + return { + 'private_key': path.join(self.not_writable_temp_dir_rw, + PRIVATE_KEY_NAME), + 'is_keypair_external': is_external + } + + def get_keypair_inputs_temp_dir(self, is_external, **kwargs): + return { + 'private_key': path.join(self.temp_dir, PRIVATE_KEY_NAME), + 'is_keypair_external': is_external + } + + @workflow_test(blueprint_path, inputs={ + 'private_key': '', + 'is_keypair_external': False + }) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_valid_config(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create): + cfy_local.execute('install', task_retries=0) + + @workflow_test(blueprint_path, inputs='get_keypair_inputs_private_key', + input_func_kwargs={'is_external': True}) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_valid_config_external(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create): + cfy_local.execute('install', task_retries=0) + + @workflow_test(blueprint_path, inputs='get_keypair_inputs_temp_dir', + input_func_kwargs={'is_external': True}) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_no_private_key(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create_with_exception): + cfy_local.execute('install', task_retries=0) + + @workflow_test(blueprint_path, inputs='get_keypair_inputs_private_key', + input_func_kwargs={'is_external': False}) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_local_and_exists(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create_with_exception): + cfy_local.execute('install', task_retries=0) + + @workflow_test(blueprint_path, inputs='get_keypair_inputs_temp_dir', + input_func_kwargs={'is_external': False}) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_local_temp_dir(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create): + cfy_local.execute('install', task_retries=0) + + @workflow_test(blueprint_path, + inputs='get_keypair_inputs_not_writable_dir_r', + input_func_kwargs={'is_external': False}) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_local_non_writable_dir_r(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create_with_exception): + cfy_local.execute('install', task_retries=0) + + @workflow_test(blueprint_path, + inputs='get_keypair_inputs_not_writable_dir_rx', + input_func_kwargs={'is_external': False}) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_local_non_writable_dir_rx(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create_with_exception): + cfy_local.execute('install', task_retries=0) + + @workflow_test(blueprint_path, + inputs='get_keypair_inputs_not_writable_dir_rw', + input_func_kwargs={'is_external': False}) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_local_non_writable_dir_rw(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create_with_exception): + cfy_local.execute('install', task_retries=0) + + @workflow_test(blueprint_path, + inputs='get_keypair_inputs_not_readable_private_key', + input_func_kwargs={'is_external': True}) + @mock.patch('nova_plugin.keypair.validate_resource') + def test_keypair_not_readable_private_key(self, cfy_local, *args): + + with mock.patch('nova_plugin.keypair.create', + new=self.new_keypair_create_with_exception): + cfy_local.execute('install', task_retries=0) diff --git a/aria/multivim-plugin/nova_plugin/userdata.py b/aria/multivim-plugin/nova_plugin/userdata.py new file mode 100644 index 0000000000..ba63bb5328 --- /dev/null +++ b/aria/multivim-plugin/nova_plugin/userdata.py @@ -0,0 +1,50 @@ +######### +# Copyright (c) 2015 GigaSpaces Technologies Ltd. All rights reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# * See the License for the specific language governing permissions and +# * limitations under the License. + +import requests + +from cloudify import compute +from cloudify import exceptions +from cloudify import ctx + + +def handle_userdata(server): + + existing_userdata = server.get('userdata') + install_agent_userdata = ctx.agent.init_script() + + if not (existing_userdata or install_agent_userdata): + return + + if isinstance(existing_userdata, dict): + ud_type = existing_userdata['type'] + if ud_type not in userdata_handlers: + raise exceptions.NonRecoverableError( + "Invalid type '{0}' for server userdata)".format(ud_type)) + existing_userdata = userdata_handlers[ud_type](existing_userdata) + + if not existing_userdata: + final_userdata = install_agent_userdata + elif not install_agent_userdata: + final_userdata = existing_userdata + else: + final_userdata = compute.create_multi_mimetype_userdata( + [existing_userdata, install_agent_userdata]) + server['userdata'] = final_userdata + + +userdata_handlers = { + 'http': lambda params: requests.get(params['url']).text +} |