aboutsummaryrefslogtreecommitdiffstats
path: root/aria/multivim-plugin/nova_plugin
diff options
context:
space:
mode:
authordfilppi <dewayne@gigaspaces.com>2017-08-07 20:10:53 +0000
committerdfilppi <dewayne@gigaspaces.com>2017-08-07 20:10:53 +0000
commit9981f55920a6f1c1f20396d42e35b075b22f6a8f (patch)
tree1199993b9bae728c5274ae3062988dc9f357eb5b /aria/multivim-plugin/nova_plugin
parent4538e26e2a60bd325d63c19bcc7d0fed37ccce96 (diff)
ARIA multivim plugin initial checkin
Change-Id: I3a24ab6fc5ba54466bfecaf596a13b8907248ae8 Issue-id: SO-77 Signed-off-by: DeWayne Filppi <dewayne@gigaspaces.com>
Diffstat (limited to 'aria/multivim-plugin/nova_plugin')
-rw-r--r--aria/multivim-plugin/nova_plugin/__init__.py16
-rw-r--r--aria/multivim-plugin/nova_plugin/floatingip.py60
-rw-r--r--aria/multivim-plugin/nova_plugin/keypair.py202
-rw-r--r--aria/multivim-plugin/nova_plugin/security_group.py81
-rw-r--r--aria/multivim-plugin/nova_plugin/server.py944
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/__init__.py0
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/resources/test-keypair-validation-blueprint.yaml23
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/resources/test-server-create-secgroup.yaml31
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/resources/test-start-operation-retry-blueprint.yaml31
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/test_relationships.py228
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/test_server.py551
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/test_server_image_and_flavor.py228
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/test_userdata.py63
-rw-r--r--aria/multivim-plugin/nova_plugin/tests/test_validation.py194
-rw-r--r--aria/multivim-plugin/nova_plugin/userdata.py50
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
+}