######### # 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)]