diff options
Diffstat (limited to 'aria/multivim-plugin/nova_plugin/server.py')
-rw-r--r-- | aria/multivim-plugin/nova_plugin/server.py | 944 |
1 files changed, 944 insertions, 0 deletions
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)] |