def test_initialized(self): sot = schema.Operation('des', schema={'foo': schema.StringParam()}) self.assertEqual('des', sot['description']) self.assertEqual({'foo': { 'required': False, 'type': 'String' }}, sot['parameters'])
class DummyProfile(pb.Profile): VERSION = '1.0' CONTEXT = 'context' properties_schema = { CONTEXT: schema.Map('context data'), 'key1': schema.String( 'first key', default='value1', updatable=True, ), 'key2': schema.Integer( 'second key', required=True, updatable=True, ), 'key3': schema.String('third key', ), } OPERATIONS = { 'op1': schema.Operation( 'Operation 1', schema={'param1': schema.StringParam('description of param1', )}) } def __init__(self, name, spec, **kwargs): super(DummyProfile, self).__init__(name, spec, **kwargs)
def test_validate_unrecognizable_param(self): sot = schema.Operation('des', schema={'foo': schema.StringParam()}) ex = self.assertRaises(exc.ESchema, sot.validate, {'baar': 'baar'}) self.assertEqual("Unrecognizable parameter 'baar'", str(ex))
def test_validate_failed_type(self): sot = schema.Operation('des', schema={'foo': schema.StringParam()}) ex = self.assertRaises(exc.ESchema, sot.validate, {'foo': ['baaar']}) self.assertEqual("value is not a string", str(ex))
def test_validate_failed_required(self): sot = schema.Operation('des', schema={ 'foo': schema.StringParam(), 'bar': schema.StringParam(required=True) }) ex = self.assertRaises(exc.ESchema, sot.validate, {'foo': 'baaar'}) self.assertEqual("Required parameter 'bar' not provided", six.text_type(ex))
def test_validate_failed_version(self): sot = schema.Operation('des', schema={ 'foo': schema.StringParam(min_version='2.0'), }) ex = self.assertRaises(exc.ESchema, sot.validate, {'foo': 'baaar'}, '1.0') self.assertEqual( "foo (min_version=2.0) is not supported by spec " "version 1.0.", six.text_type(ex))
def test_validate_failed_constraint(self): sot = schema.Operation( 'des', schema={ 'foo': schema.StringParam( constraints=[constraints.AllowedValues(['bar'])]) }) ex = self.assertRaises(exc.ESchema, sot.validate, {'foo': 'baaar'}) self.assertEqual("'baaar' must be one of the allowed values: bar", six.text_type(ex))
class ServerProfile(base.Profile): """Profile for an OpenStack Nova server.""" KEYS = ( CONTEXT, ADMIN_PASS, AUTO_DISK_CONFIG, AVAILABILITY_ZONE, BLOCK_DEVICE_MAPPING_V2, CONFIG_DRIVE, FLAVOR, IMAGE, KEY_NAME, METADATA, NAME, NETWORKS, PERSONALITY, SECURITY_GROUPS, USER_DATA, SCHEDULER_HINTS, ) = ( 'context', 'admin_pass', 'auto_disk_config', 'availability_zone', 'block_device_mapping_v2', 'config_drive', 'flavor', 'image', 'key_name', 'metadata', 'name', 'networks', 'personality', 'security_groups', 'user_data', 'scheduler_hints', ) BDM2_KEYS = ( BDM2_UUID, BDM2_SOURCE_TYPE, BDM2_DESTINATION_TYPE, BDM2_DISK_BUS, BDM2_DEVICE_NAME, BDM2_VOLUME_SIZE, BDM2_GUEST_FORMAT, BDM2_BOOT_INDEX, BDM2_DEVICE_TYPE, BDM2_DELETE_ON_TERMINATION, ) = ( 'uuid', 'source_type', 'destination_type', 'disk_bus', 'device_name', 'volume_size', 'guest_format', 'boot_index', 'device_type', 'delete_on_termination', ) NETWORK_KEYS = ( PORT, FIXED_IP, NETWORK, ) = ( 'port', 'fixed_ip', 'network', ) PERSONALITY_KEYS = ( PATH, CONTENTS, ) = ( 'path', 'contents', ) SCHEDULER_HINTS_KEYS = (GROUP, ) = ('group', ) properties_schema = { CONTEXT: schema.Map(_('Customized security context for operating servers.'), ), ADMIN_PASS: schema.String(_('Password for the administrator account.'), ), AUTO_DISK_CONFIG: schema.Boolean( _('Whether the disk partition is done automatically.'), default=True, ), AVAILABILITY_ZONE: schema.String( _('Name of availability zone for running the server.'), ), BLOCK_DEVICE_MAPPING_V2: schema.List( _('A list specifying the properties of block devices to be used ' 'for this server.'), schema=schema.Map( _('A map specifying the properties of a block device to be ' 'used by the server.'), schema={ BDM2_UUID: schema.String( _('ID of the source image, snapshot or volume'), ), BDM2_SOURCE_TYPE: schema.String( _('Volume source type, should be image, snapshot, ' 'volume or blank'), required=True, ), BDM2_DESTINATION_TYPE: schema.String( _('Volume destination type, should be volume or ' 'local'), required=True, ), BDM2_DISK_BUS: schema.String(_('Bus of the device.'), ), BDM2_DEVICE_NAME: schema.String( _('Name of the device(e.g. vda, xda, ....).'), ), BDM2_VOLUME_SIZE: schema.Integer( _('Size of the block device in MB(for swap) and ' 'in GB(for other formats)'), required=True, ), BDM2_GUEST_FORMAT: schema.String( _('Specifies the disk file system format(e.g. swap, ' 'ephemeral, ...).'), ), BDM2_BOOT_INDEX: schema.Integer(_('Define the boot order of the device'), ), BDM2_DEVICE_TYPE: schema.String( _('Type of the device(e.g. disk, cdrom, ...).'), ), BDM2_DELETE_ON_TERMINATION: schema.Boolean( _('Whether to delete the volume when the server ' 'stops.'), ), }), ), CONFIG_DRIVE: schema.Boolean( _('Whether config drive should be enabled for the server.'), ), FLAVOR: schema.String( _('ID of flavor used for the server.'), required=True, updatable=True, ), IMAGE: schema.String( # IMAGE is not required, because there could be BDM or BDMv2 # support and the corresponding settings effective _('ID of image to be used for the new server.'), updatable=True, ), KEY_NAME: schema.String(_('Name of Nova keypair to be injected to server.'), ), METADATA: schema.Map( _('A collection of key/value pairs to be associated with the ' 'server created. Both key and value should be <=255 chars.'), updatable=True, ), NAME: schema.String( _('Name of the server. When omitted, the node name will be used.'), updatable=True, ), NETWORKS: schema.List( _('List of networks for the server.'), schema=schema.Map( _('A map specifying the properties of a network for uses.'), schema={ NETWORK: schema.String( _('Name or ID of network to create a port on.'), ), PORT: schema.String(_('Port ID to be used by the network.'), ), FIXED_IP: schema.String(_('Fixed IP to be used by the network.'), ), }, ), updatable=True, ), PERSONALITY: schema.List( _('List of files to be injected into the server, where each.'), schema=schema.Map( _('A map specifying the path & contents for an injected ' 'file.'), schema={ PATH: schema.String( _('In-instance path for the file to be injected.'), required=True, ), CONTENTS: schema.String( _('Contents of the file to be injected.'), required=True, ), }, ), ), SCHEDULER_HINTS: schema.Map( _('A collection of key/value pairs to be associated with the ' 'Scheduler hints. Both key and value should be <=255 chars.'), ), SECURITY_GROUPS: schema.List( _('List of security groups.'), schema=schema.String( _('Name of a security group'), required=True, ), ), USER_DATA: schema.String(_('User data to be exposed by the metadata server.'), ), } OP_NAMES = ( OP_REBOOT, OP_CHANGE_PASSWORD, ) = ( 'reboot', 'change_password', ) REBOOT_TYPE = 'type' REBOOT_TYPES = (REBOOT_SOFT, REBOOT_HARD) = ('SOFT', 'HARD') ADMIN_PASSWORD = '******' OPERATIONS = { OP_REBOOT: schema.Operation( _("Reboot the nova server."), schema={ REBOOT_TYPE: schema.StringParam( _("Type of reboot which can be 'SOFT' or 'HARD'."), default=REBOOT_SOFT, constraints=[ constraints.AllowedValues(REBOOT_TYPES), ]) }), OP_CHANGE_PASSWORD: schema.Operation(_("Change the administrator password."), schema={ ADMIN_PASSWORD: schema.StringParam( _("New password for the administrator.")) }), } def __init__(self, type_name, name, **kwargs): super(ServerProfile, self).__init__(type_name, name, **kwargs) self.server_id = None def _validate_az(self, obj, az_name, reason=None): try: res = self.compute(obj).validate_azs([az_name]) except exc.InternalError as ex: if reason == 'create': raise exc.EResourceCreation(type='server', message=six.text_type(ex)) else: raise if not res: msg = _("The specified %(key)s '%(value)s' could not be found") % { 'key': self.AVAILABILITY_ZONE, 'value': az_name } if reason == 'create': raise exc.EResourceCreation(type='server', message=msg) else: raise exc.InvalidSpec(message=msg) return az_name def _validate_flavor(self, obj, name_or_id, reason=None): flavor = None msg = '' try: flavor = self.compute(obj).flavor_find(name_or_id, False) except exc.InternalError as ex: msg = six.text_type(ex) if reason is None: # reaons is 'validate' if ex.code == 404: msg = _( "The specified %(k)s '%(v)s' could not be found.") % { 'k': self.FLAVOR, 'v': name_or_id } raise exc.InvalidSpec(message=msg) else: raise if flavor is not None: if not flavor.is_disabled: return flavor msg = _("The specified %(k)s '%(v)s' is disabled") % { 'k': self.FLAVOR, 'v': name_or_id } if reason == 'create': raise exc.EResourceCreation(type='server', message=msg) elif reason == 'update': raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=msg) else: raise exc.InvalidSpec(message=msg) def _validate_image(self, obj, name_or_id, reason=None): try: return self.compute(obj).image_find(name_or_id, False) except exc.InternalError as ex: if reason == 'create': raise exc.EResourceCreation(type='server', message=six.text_type(ex)) elif reason == 'update': raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) elif ex.code == 404: msg = _("The specified %(k)s '%(v)s' could not be found.") % { 'k': self.IMAGE, 'v': name_or_id } raise exc.InvalidSpec(message=msg) else: raise def _validate_keypair(self, obj, name_or_id, reason=None): try: return self.compute(obj).keypair_find(name_or_id, False) except exc.InternalError as ex: if reason == 'create': raise exc.EResourceCreation(type='server', message=six.text_type(ex)) elif reason == 'update': raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) elif ex.code == 404: msg = _("The specified %(k)s '%(v)s' could not be found.") % { 'k': self.KEY_NAME, 'v': name_or_id } raise exc.InvalidSpec(message=msg) else: raise def do_validate(self, obj): """Validate if the spec has provided valid info for server creation. :param obj: The node object. """ # validate availability_zone az_name = self.properties[self.AVAILABILITY_ZONE] if az_name is not None: self._validate_az(obj, az_name) # validate flavor flavor = self.properties[self.FLAVOR] self._validate_flavor(obj, flavor) # validate image image = self.properties[self.IMAGE] if image is not None: self._validate_image(obj, image) # validate key_name keypair = self.properties[self.KEY_NAME] if keypair is not None: self._validate_keypair(obj, keypair) # validate networks networks = self.properties[self.NETWORKS] for net in networks: self._validate_network(obj, net) return True def _resolve_bdm(self, bdm): for bd in bdm: for key in self.BDM2_KEYS: if bd[key] is None: del bd[key] return bdm def _validate_network(self, obj, network, reason=None): result = {} error = None # check network net_ident = network.get(self.NETWORK) if net_ident: try: net = self.network(obj).network_get(net_ident) if reason == 'update': result['net_id'] = net.id else: result['uuid'] = net.id except exc.InternalError as ex: error = six.text_type(ex) # check port port_ident = network.get(self.PORT) if not error and port_ident: try: port = self.network(obj).port_find(port_ident) if port.status != 'DOWN': error = _( "The status of the port %(port)s must be DOWN") % { 'port': port_ident } if reason == 'update': result['port_id'] = port.id else: result['port'] = port.id except exc.InternalError as ex: error = six.text_type(ex) elif port_ident is None and net_ident is None: error = _("'%(port)s' is required if '%(net)s' is omitted") % { 'port': self.PORT, 'net': self.NETWORK } fixed_ip = network.get(self.FIXED_IP) if not error and fixed_ip: if port_ident is not None: error = _("The '%(port)s' property and the '%(fixed_ip)s' " "property cannot be specified at the same time") % { 'port': self.PORT, 'fixed_ip': self.FIXED_IP } else: if reason == 'update': result['fixed_ips'] = [{'ip_address': fixed_ip}] else: result['fixed_ip'] = fixed_ip if error: if reason == 'create': raise exc.EResourceCreation(type='server', message=error) elif reason == 'update': raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=error) else: raise exc.InvalidSpec(message=error) return result def _build_metadata(self, obj, usermeta): """Build custom metadata for server. :param obj: The node object to operate on. :return: A dictionary containing the new metadata. """ metadata = usermeta or {} metadata['cluster_node_id'] = obj.id if obj.cluster_id: metadata['cluster_id'] = obj.cluster_id metadata['cluster_node_index'] = six.text_type(obj.index) return metadata def do_create(self, obj): """Create a server for the node object. :param obj: The node object for which a server will be created. """ kwargs = {} for key in self.KEYS: # context is treated as connection parameters if key == self.CONTEXT: continue if self.properties[key] is not None: kwargs[key] = self.properties[key] admin_pass = self.properties[self.ADMIN_PASS] if admin_pass: kwargs.pop(self.ADMIN_PASS) kwargs['adminPass'] = admin_pass auto_disk_config = self.properties[self.AUTO_DISK_CONFIG] kwargs.pop(self.AUTO_DISK_CONFIG) kwargs['OS-DCF:diskConfig'] = 'AUTO' if auto_disk_config else 'MANUAL' image_ident = self.properties[self.IMAGE] if image_ident is not None: image = self._validate_image(obj, image_ident, 'create') kwargs.pop(self.IMAGE) kwargs['imageRef'] = image.id flavor_ident = self.properties[self.FLAVOR] flavor = self._validate_flavor(obj, flavor_ident, 'create') kwargs.pop(self.FLAVOR) kwargs['flavorRef'] = flavor.id keypair_name = self.properties[self.KEY_NAME] if keypair_name: keypair = self._validate_keypair(obj, keypair_name, 'create') kwargs['key_name'] = keypair.name kwargs['name'] = self.properties[self.NAME] or obj.name metadata = self._build_metadata(obj, self.properties[self.METADATA]) kwargs['metadata'] = metadata block_device_mapping_v2 = self.properties[self.BLOCK_DEVICE_MAPPING_V2] if block_device_mapping_v2 is not None: kwargs['block_device_mapping_v2'] = self._resolve_bdm( block_device_mapping_v2) user_data = self.properties[self.USER_DATA] if user_data is not None: ud = encodeutils.safe_encode(user_data) kwargs['user_data'] = encodeutils.safe_decode(base64.b64encode(ud)) networks = self.properties[self.NETWORKS] if networks is not None: kwargs['networks'] = [] for net_spec in networks: net = self._validate_network(obj, net_spec, 'create') kwargs['networks'].append(net) secgroups = self.properties[self.SECURITY_GROUPS] if secgroups: kwargs['security_groups'] = [{'name': sg} for sg in secgroups] if 'placement' in obj.data: if 'zone' in obj.data['placement']: kwargs['availability_zone'] = obj.data['placement']['zone'] if 'servergroup' in obj.data['placement']: group_id = obj.data['placement']['servergroup'] hints = self.properties.get(self.SCHEDULER_HINTS, {}) hints.update({'group': group_id}) kwargs['scheduler_hints'] = hints server = None resource_id = 'UNKNOWN' try: server = self.compute(obj).server_create(**kwargs) self.compute(obj).wait_for_server(server.id) return server.id except exc.InternalError as ex: if server and server.id: resource_id = server.id raise exc.EResourceCreation(type='server', message=ex.message, resource_id=resource_id) def do_delete(self, obj, **params): """Delete the physical resource associated with the specified node. :param obj: The node object to operate on. :param kwargs params: Optional keyword arguments for the delete operation. :returns: This operation always return True unless exception is caught. :raises: `EResourceDeletion` if interaction with compute service fails. """ if not obj.physical_id: return True server_id = obj.physical_id ignore_missing = params.get('ignore_missing', True) force = params.get('force', False) try: driver = self.compute(obj) if force: driver.server_force_delete(server_id, ignore_missing) else: driver.server_delete(server_id, ignore_missing) driver.wait_for_server_delete(server_id) return True except exc.InternalError as ex: raise exc.EResourceDeletion(type='server', id=server_id, message=six.text_type(ex)) def _check_server_name(self, obj, profile): """Check if there is a new name to be assigned to the server. :param obj: The node object to operate on. :param new_profile: The new profile which may contain a name for the server instance. :return: A tuple consisting a boolean indicating whether the name needs change and the server name determined. """ old_name = self.properties[self.NAME] or obj.name new_name = profile.properties[self.NAME] or obj.name if old_name == new_name: return False, new_name return True, new_name def _update_name(self, obj, new_name): """Update the name of the server. :param obj: The node object to operate. :param new_name: The new name for the server instance. :return: ``None``. :raises: ``EResourceUpdate``. """ try: self.compute(obj).server_update(obj.physical_id, name=new_name) except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) def _check_password(self, obj, new_profile): """Check if the admin password has been changed in the new profile. :param obj: The server node to operate, not used currently. :param new_profile: The new profile which may contain a new password for the server instance. :return: A tuple consisting a boolean indicating whether the password needs a change and the password determined which could be '' if new password is not set. """ old_passwd = self.properties.get(self.ADMIN_PASS) or '' new_passwd = new_profile.properties[self.ADMIN_PASS] or '' if old_passwd == new_passwd: return False, new_passwd return True, new_passwd def _update_password(self, obj, new_password): """Update the admin password for the server. :param obj: The node object to operate. :param new_password: The new password for the server instance. :return: ``None``. :raises: ``EResourceUpdate``. """ try: self.compute(obj).server_change_password(obj.physical_id, new_password) except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) def _update_metadata(self, obj, new_profile): """Update the server metadata. :param obj: The node object to operate on. :param new_profile: The new profile that may contain some changes to the metadata. :returns: ``None`` :raises: `EResourceUpdate`. """ old_meta = self._build_metadata(obj, self.properties[self.METADATA]) new_meta = self._build_metadata(obj, new_profile.properties[self.METADATA]) if new_meta == old_meta: return try: self.compute(obj).server_metadata_update(obj.physical_id, new_meta) except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) def _update_flavor(self, obj, new_profile): """Update server flavor. :param obj: The node object to operate on. :param old_flavor: The identity of the current flavor. :param new_flavor: The identity of the new flavor. :returns: ``None``. :raises: `EResourceUpdate` when operation was a failure. """ old_flavor = self.properties[self.FLAVOR] new_flavor = new_profile.properties[self.FLAVOR] cc = self.compute(obj) oldflavor = self._validate_flavor(obj, old_flavor, 'update') newflavor = self._validate_flavor(obj, new_flavor, 'update') if oldflavor.id == newflavor.id: return try: cc.server_resize(obj.physical_id, newflavor.id) cc.wait_for_server(obj.physical_id, 'VERIFY_RESIZE') except exc.InternalError as ex: msg = six.text_type(ex) try: cc.server_resize_revert(obj.physical_id) cc.wait_for_server(obj.physical_id, 'ACTIVE') except exc.InternalError as ex1: msg = six.text_type(ex1) raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=msg) try: cc.server_resize_confirm(obj.physical_id) cc.wait_for_server(obj.physical_id, 'ACTIVE') except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) def _update_image(self, obj, new_profile, new_name, new_password): """Update image used by server node. :param obj: The node object to operate on. :param new_profile: The profile which may contain a new image name or ID to use. :param new_name: The name for the server node. :param newn_password: The new password for the administrative account if provided. :returns: A boolean indicating whether the image needs an update. :raises: ``InternalError`` if operation was a failure. """ old_image = self.properties[self.IMAGE] new_image = new_profile.properties[self.IMAGE] if not new_image: msg = _("Updating Nova server with image set to None is not " "supported by Nova") raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=msg) # check the new image first img_new = self._validate_image(obj, new_image, reason='update') new_image_id = img_new.id driver = self.compute(obj) if old_image: img_old = self._validate_image(obj, old_image, reason='update') old_image_id = img_old.id else: try: server = driver.server_get(obj.physical_id) except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) # Still, this 'old_image_id' could be empty, but it doesn't matter # because the comparison below would fail if that is the case old_image_id = server.image.get('id', None) if new_image_id == old_image_id: return False try: driver.server_rebuild(obj.physical_id, new_image_id, new_name, new_password) driver.wait_for_server(obj.physical_id, 'ACTIVE') except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) return True def _create_interfaces(self, obj, networks): """Create new interfaces for the server node. :param obj: The node object to operate. :param networks: A list containing information about new network interfaces to be created. :returns: ``None``. :raises: ``EResourceUpdate`` if interaction with drivers failed. """ cc = self.compute(obj) try: server = cc.server_get(obj.physical_id) except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) for net_spec in networks: net_attrs = self._validate_network(obj, net_spec, 'update') if net_attrs: try: cc.server_interface_create(server, **net_attrs) except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) def _delete_interfaces(self, obj, networks): """Delete existing interfaces from the node. :param obj: The node object to operate. :param networks: A list containing information about network interfaces to be created. :returns: ``None`` :raises: ``EResourceUpdate`` """ def _get_network(nc, net_id, server_id): try: net = nc.network_get(net_id) return net.id except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=server_id, message=six.text_type(ex)) def _do_delete(port_id, server_id): try: cc.server_interface_delete(port_id, server_id) except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=server_id, message=six.text_type(ex)) cc = self.compute(obj) nc = self.network(obj) try: existing = list(cc.server_interface_list(obj.physical_id)) except exc.InternalError as ex: raise exc.EResourceUpdate(type='server', id=obj.physical_id, message=six.text_type(ex)) ports = [] for intf in existing: fixed_ips = [addr['ip_address'] for addr in intf.fixed_ips] ports.append({ 'id': intf.port_id, 'net': intf.net_id, 'ips': fixed_ips }) for n in networks: network = n.get('network', None) port = n.get('port', None) fixed_ip = n.get('fixed_ip', None) if port: for p in ports: if p['id'] == port: ports.remove(p) _do_delete(port, obj.physical_id) elif fixed_ip: net_id = _get_network(nc, network, obj.physical_id) for p in ports: if (fixed_ip in p['ips'] and net_id == p['net']): ports.remove(p) _do_delete(p['id'], obj.physical_id) elif port is None and fixed_ip is None: net_id = _get_network(nc, network, obj.physical_id) for p in ports: if p['net'] == net_id: ports.remove(p) _do_delete(p['id'], obj.physical_id) def _update_network(self, obj, new_profile): """Updating server network interfaces. :param obj: The node object to operate. :param new_profile: The new profile which may contain new network settings. :return: ``None`` :raises: ``EResourceUpdate`` if there are driver failures. """ networks_current = self.properties[self.NETWORKS] networks_create = new_profile.properties[self.NETWORKS] networks_delete = copy.deepcopy(networks_current) for network in networks_current: if network in networks_create: networks_create.remove(network) networks_delete.remove(network) # Detach some existing interfaces if networks_delete: self._delete_interfaces(obj, networks_delete) # Attach new interfaces if networks_create: self._create_interfaces(obj, networks_create) return def do_update(self, obj, new_profile=None, **params): """Perform update on the server. :param obj: the server to operate on :param new_profile: the new profile for the server. :param params: a dictionary of optional parameters. :returns: True if update was successful or False otherwise. :raises: `EResourceUpdate` if operation fails. """ self.server_id = obj.physical_id if not self.server_id: return False if not new_profile: return False if not self.validate_for_update(new_profile): return False name_changed, new_name = self._check_server_name(obj, new_profile) passwd_changed, new_passwd = self._check_password(obj, new_profile) # Update server image: may have side effect of changing server name # and/or admin password image_changed = self._update_image(obj, new_profile, new_name, new_passwd) if not image_changed: # we do this separately only when rebuild wasn't performed if name_changed: self._update_name(obj, new_name) if passwd_changed: self._update_password(obj, new_passwd) # Update server flavor: note that flavor is a required property self._update_flavor(obj, new_profile) self._update_network(obj, new_profile) # TODO(Yanyan Hu): Update block_device properties # Update server metadata self._update_metadata(obj, new_profile) return True def do_get_details(self, obj): known_keys = { 'OS-DCF:diskConfig', 'OS-EXT-AZ:availability_zone', 'OS-EXT-STS:power_state', 'OS-EXT-STS:vm_state', 'accessIPv4', 'accessIPv6', 'config_drive', 'created', 'hostId', 'id', 'key_name', 'locked', 'metadata', 'name', 'os-extended-volumes:volumes_attached', 'progress', 'status', 'updated' } if obj.physical_id is None or obj.physical_id == '': return {} driver = self.compute(obj) try: server = driver.server_get(obj.physical_id) except exc.InternalError as ex: return {'Error': {'code': ex.code, 'message': six.text_type(ex)}} if server is None: return {} server_data = server.to_dict() details = { 'image': server_data['image']['id'], 'flavor': server_data['flavor']['id'], } for key in known_keys: if key in server_data: details[key] = server_data[key] # process special keys like 'OS-EXT-STS:task_state': these keys have # a default value '-' when not existing special_keys = [ 'OS-EXT-STS:task_state', 'OS-SRV-USG:launched_at', 'OS-SRV-USG:terminated_at', ] for key in special_keys: if key in server_data: val = server_data[key] details[key] = val if val else '-' # process network addresses details['addresses'] = copy.deepcopy(server_data['addresses']) # process security groups sgroups = [] if 'security_groups' in server_data: for sg in server_data['security_groups']: sgroups.append(sg['name']) if len(sgroups) == 0: details['security_groups'] = '' elif len(sgroups) == 1: details['security_groups'] = sgroups[0] else: details['security_groups'] = sgroups return dict((k, details[k]) for k in sorted(details)) def do_join(self, obj, cluster_id): if not obj.physical_id: return False driver = self.compute(obj) metadata = driver.server_metadata_get(obj.physical_id) or {} metadata['cluster_id'] = cluster_id metadata['cluster_node_index'] = six.text_type(obj.index) driver.server_metadata_update(obj.physical_id, metadata) return super(ServerProfile, self).do_join(obj, cluster_id) def do_leave(self, obj): if not obj.physical_id: return False keys = ['cluster_id', 'cluster_node_index'] self.compute(obj).server_metadata_delete(obj.physical_id, keys) return super(ServerProfile, self).do_leave(obj) def do_rebuild(self, obj): if not obj.physical_id: return False self.server_id = obj.physical_id driver = self.compute(obj) try: server = driver.server_get(self.server_id) except exc.InternalError as ex: raise exc.EResourceOperation(op='rebuilding', type='server', id=self.server_id, message=six.text_type(ex)) if server is None or server.image is None: return False image_id = server.image['id'] admin_pass = self.properties.get(self.ADMIN_PASS) try: driver.server_rebuild(self.server_id, image_id, self.properties.get(self.NAME), admin_pass) driver.wait_for_server(self.server_id, 'ACTIVE') except exc.InternalError as ex: raise exc.EResourceOperation(op='rebuilding', type='server', id=self.server_id, message=six.text_type(ex)) return True def do_check(self, obj): if not obj.physical_id: return False try: server = self.compute(obj).server_get(obj.physical_id) except exc.InternalError as ex: raise exc.EResourceOperation(op='checking', type='server', id=obj.physical_id, message=six.text_type(ex)) if (server is None or server.status != 'ACTIVE'): return False return True def do_recover(self, obj, **options): # NOTE: We do a 'get' not a 'pop' here, because the operations may # get fall back to the base class for handling operation = options.get('operation', None) if operation and not isinstance(operation, six.string_types): operation = operation[0] # TODO(Qiming): Handle the case that the operation contains other # alternative recover operation # Depends-On: https://review.openstack.org/#/c/359676/ if operation == 'REBUILD': return self.do_rebuild(obj) return super(ServerProfile, self).do_recover(obj, **options) def handle_reboot(self, obj, **options): """Handler for the reboot operation.""" if not obj.physical_id: return False reboot_type = options.get(self.REBOOT_TYPE, self.REBOOT_SOFT) if (not isinstance(reboot_type, six.string_types) or reboot_type not in self.REBOOT_TYPES): return False self.compute(obj).server_reboot(obj.physical_id, reboot_type) self.compute(obj).wait_for_server(obj.physical_id, 'ACTIVE') return True def handle_change_password(self, obj, **options): """Handler for the change_password operation.""" if not obj.physical_id: return False password = options.get(self.ADMIN_PASSWORD, None) if (password is None or not isinstance(password, six.string_types)): return False self.compute(obj).server_change_password(obj.physical_id, password) return True
def test_validate(self): sot = schema.Operation('des', schema={'foo': schema.StringParam()}) res = sot.validate({'foo': 'bar'}) self.assertIsNone(res)
def test_basic(self): sot = schema.Operation() self.assertEqual('Undocumented', sot['description']) self.assertEqual({}, sot['parameters'])
class DockerProfile(base.Profile): """Profile for a docker container.""" VERSIONS = {'1.0': [{'status': consts.EXPERIMENTAL, 'since': '2017.02'}]} _VALID_HOST_TYPES = [ HOST_NOVA_SERVER, HOST_HEAT_STACK, ] = [ "os.nova.server", "os.heat.stack", ] KEYS = ( CONTEXT, IMAGE, NAME, COMMAND, HOST_NODE, HOST_CLUSTER, PORT, ) = ( 'context', 'image', 'name', 'command', 'host_node', 'host_cluster', 'port', ) properties_schema = { CONTEXT: schema.Map(_('Customized security context for operating containers.')), IMAGE: schema.String( _('The image used to create a container'), required=True, ), NAME: schema.String( _('The name of the container.'), updatable=True, ), COMMAND: schema.String(_('The command to run when container is started.')), PORT: schema.Integer(_('The port number used to connect to docker daemon.'), default=2375), HOST_NODE: schema.String(_('The node on which container will be launched.')), HOST_CLUSTER: schema.String(_('The cluster on which container will be launched.')), } OP_NAMES = ( OP_RESTART, OP_PAUSE, OP_UNPAUSE, ) = ( 'restart', 'pause', 'unpause', ) _RESTART_WAIT = (RESTART_WAIT) = ('wait_time') OPERATIONS = { OP_RESTART: schema.Operation( _("Restart a container."), schema={ RESTART_WAIT: schema.IntegerParam( _("Number of seconds to wait before killing the " "container.")) }), OP_PAUSE: schema.Operation(_("Pause a container.")), OP_UNPAUSE: schema.Operation(_("Unpause a container.")) } def __init__(self, type_name, name, **kwargs): super(DockerProfile, self).__init__(type_name, name, **kwargs) self._dockerclient = None self.container_id = None self.host = None self.cluster = None @classmethod def create(cls, ctx, name, spec, metadata=None): profile = super(DockerProfile, cls).create(ctx, name, spec, metadata) host_cluster = profile.properties.get(profile.HOST_CLUSTER, None) if host_cluster: db_api.cluster_add_dependents(ctx, host_cluster, profile.id) host_node = profile.properties.get(profile.HOST_NODE, None) if host_node: db_api.node_add_dependents(ctx, host_node, profile.id, 'profile') return profile @classmethod def delete(cls, ctx, profile_id): obj = cls.load(ctx, profile_id=profile_id) cluster_id = obj.properties.get(obj.HOST_CLUSTER, None) if cluster_id: db_api.cluster_remove_dependents(ctx, cluster_id, profile_id) node_id = obj.properties.get(obj.HOST_NODE, None) if node_id: db_api.node_remove_dependents(ctx, node_id, profile_id, 'profile') super(DockerProfile, cls).delete(ctx, profile_id) def docker(self, obj): """Construct docker client based on object. :param obj: Object for which the client is created. It is expected to be None when retrieving an existing client. When creating a client, it contains the user and project to be used. """ if self._dockerclient is not None: return self._dockerclient host_node = self.properties.get(self.HOST_NODE, None) host_cluster = self.properties.get(self.HOST_CLUSTER, None) ctx = context.get_admin_context() self.host = self._get_host(ctx, host_node, host_cluster) # TODO(Anyone): Check node.data for per-node host selection host_type = self.host.rt['profile'].type_name if host_type not in self._VALID_HOST_TYPES: msg = _("Type of host node (%s) is not supported") % host_type raise exc.InternalError(message=msg) host_ip = self._get_host_ip(obj, self.host.physical_id, host_type) if host_ip is None: msg = _("Unable to determine the IP address of host node") raise exc.InternalError(message=msg) url = 'tcp://%(ip)s:%(port)d' % { 'ip': host_ip, 'port': self.properties[self.PORT] } self._dockerclient = docker_driver.DockerClient(url) return self._dockerclient def _get_host(self, ctx, host_node, host_cluster): """Determine which node to launch container on. :param ctx: An instance of the request context. :param host_node: The uuid of the hosting node. :param host_cluster: The uuid of the hosting cluster. """ host = None if host_node is not None: try: host = node_mod.Node.load(ctx, node_id=host_node) except exc.ResourceNotFound as ex: msg = ex.enhance_msg('host', ex) raise exc.InternalError(message=msg) return host if host_cluster is not None: host = self._get_random_node(ctx, host_cluster) return host def _get_random_node(self, ctx, host_cluster): """Get a node randomly from the host cluster. :param ctx: An instance of the request context. :param host_cluster: The uuid of the hosting cluster. """ self.cluster = None try: self.cluster = cluster.Cluster.load(ctx, cluster_id=host_cluster) except exc.ResourceNotFound as ex: msg = ex.enhance_msg('host', ex) raise exc.InternalError(message=msg) filters = {consts.NODE_STATUS: consts.NS_ACTIVE} nodes = no.Node.get_all_by_cluster(ctx, cluster_id=host_cluster, filters=filters) if len(nodes) == 0: msg = _("The cluster (%s) contains no active nodes") % host_cluster raise exc.InternalError(message=msg) # TODO(anyone): Should pick a node by its load db_node = nodes[random.randrange(len(nodes))] return node_mod.Node.load(ctx, db_node=db_node) def _get_host_ip(self, obj, host_node, host_type): """Fetch the ip address of physical node. :param obj: The node object representing the container instance. :param host_node: The name or ID of the hosting node object. :param host_type: The type of the hosting node, which can be either a nova server or a heat stack. :returns: The fixed IP address of the hosting node. """ host_ip = None if host_type == self.HOST_NOVA_SERVER: server = self.compute(obj).server_get(host_node) private_addrs = server.addresses['private'] for addr in private_addrs: if addr['version'] == 4 and addr['OS-EXT-IPS:type'] == 'fixed': host_ip = addr['addr'] elif host_type == self.HOST_HEAT_STACK: stack = self.orchestration(obj).stack_get(host_node) outputs = stack.outputs or {} if outputs: for output in outputs: if output['output_key'] == 'fixed_ip': host_ip = output['output_value'] break if not outputs or host_ip is None: msg = _("Output 'fixed_ip' is missing from the provided stack" " node") raise exc.InternalError(message=msg) return host_ip def do_validate(self, obj): """Validate if the spec has provided valid configuration. :param obj: The node object. """ cluster = self.properties[self.HOST_CLUSTER] node = self.properties[self.HOST_NODE] if all([cluster, node]): msg = _("Either '%(c)s' or '%(n)s' must be specified, but not " "both.") % { 'c': self.HOST_CLUSTER, 'n': self.HOST_NODE } raise exc.InvalidSpec(message=msg) if not any([cluster, node]): msg = _("Either '%(c)s' or '%(n)s' must be specified.") % { 'c': self.HOST_CLUSTER, 'n': self.HOST_NODE } raise exc.InvalidSpec(message=msg) if cluster: try: co.Cluster.find(self.context, cluster) except (exc.ResourceNotFound, exc.MultipleChoices): msg = _("The specified %(key)s '%(val)s' could not be found " "or is not unique.") % { 'key': self.HOST_CLUSTER, 'val': cluster } raise exc.InvalidSpec(message=msg) if node: try: no.Node.find(self.context, node) except (exc.ResourceNotFound, exc.MultipleChoices): msg = _("The specified %(key)s '%(val)s' could not be found " "or is not unique.") % { 'key': self.HOST_NODE, 'val': node } raise exc.InvalidSpec(message=msg) def do_create(self, obj): """Create a container instance using the given profile. :param obj: The node object for this container. :returns: ID of the container instance or ``None`` if driver fails. :raises: `EResourceCreation` """ name = self.properties[self.NAME] if name is None: name = '-'.join([obj.name, utils.random_name()]) params = { 'image': self.properties[self.IMAGE], 'name': name, 'command': self.properties[self.COMMAND], } try: ctx = context.get_service_context(project=obj.project, user=obj.user) dockerclient = self.docker(obj) db_api.node_add_dependents(ctx, self.host.id, obj.id) container = dockerclient.container_create(**params) dockerclient.start(container['Id']) except exc.InternalError as ex: raise exc.EResourceCreation(type='container', message=str(ex)) self.container_id = container['Id'][:36] return self.container_id def do_delete(self, obj): """Delete a container node. :param obj: The node object representing the container. :returns: `None` """ if not obj.physical_id: return try: self.handle_stop(obj) self.docker(obj).container_delete(obj.physical_id) except exc.InternalError as ex: raise exc.EResourceDeletion(type='container', id=obj.physical_id, message=str(ex)) ctx = context.get_admin_context() db_api.node_remove_dependents(ctx, self.host.id, obj.id) return def do_update(self, obj, new_profile=None, **params): """Perform update on the container. :param obj: the container to operate on :param new_profile: the new profile for the container. :param params: a dictionary of optional parameters. :returns: True if update was successful or False otherwise. :raises: `EResourceUpdate` if operation fails. """ self.server_id = obj.physical_id if not self.server_id: return False if not new_profile: return False if not self.validate_for_update(new_profile): return False name_changed, new_name = self._check_container_name(obj, new_profile) if name_changed: self._update_name(obj, new_name) return True def _check_container_name(self, obj, profile): """Check if there is a new name to be assigned to the container. :param obj: The node object to operate on. :param new_profile: The new profile which may contain a name for the container. :return: A tuple consisting a boolean indicating whether the name needs change and the container name determined. """ old_name = self.properties[self.NAME] or obj.name new_name = profile.properties[self.NAME] or obj.name if old_name == new_name: return False, new_name return True, new_name def _update_name(self, obj, new_name): try: self.docker(obj).rename(obj.physical_id, new_name) except exc.InternalError as ex: raise exc.EResourceUpdate(type='container', id=obj.physical_id, message=str(ex)) def handle_reboot(self, obj, **options): """Handler for a reboot operation. :param obj: The node object representing the container. :returns: None """ if not obj.physical_id: return if 'timeout' in options: params = {'timeout': options['timeout']} else: params = {} try: self.docker(obj).restart(obj.physical_id, **params) except exc.InternalError as ex: raise exc.EResourceOperation(type='container', id=obj.physical_id[:8], op='rebooting', message=str(ex)) return def handle_pause(self, obj): """Handler for a pause operation. :param obj: The node object representing the container. :returns: None """ if not obj.physical_id: return try: self.docker(obj).pause(obj.physical_id) except exc.InternalError as ex: raise exc.EResourceOperation(type='container', id=obj.physical_id[:8], op='pausing', message=str(ex)) return def handle_unpause(self, obj): """Handler for an unpause operation. :param obj: The node object representing the container. :returns: None """ if not obj.physical_id: return try: self.docker(obj).unpause(obj.physical_id) except exc.InternalError as ex: raise exc.EResourceOperation(type='container', id=obj.physical_id[:8], op='unpausing', message=str(ex)) return def handle_stop(self, obj, **options): """Handler for the stop operation.""" if not obj.physical_id: return timeout = options.get('timeout', None) if timeout: timeout = int(timeout) try: self.docker(obj).stop(obj.physical_id, timeout=timeout) except exc.InternalError as ex: raise exc.EResourceOperation(type='container', id=obj.physical_id[:8], op='stop', message=str(ex))
class ServerProfile(base.Profile): '''Profile for an OpenStack Nova server.''' KEYS = ( CONTEXT, ADMIN_PASS, AUTO_DISK_CONFIG, AVAILABILITY_ZONE, BLOCK_DEVICE_MAPPING, BLOCK_DEVICE_MAPPING_V2, CONFIG_DRIVE, FLAVOR, IMAGE, KEY_NAME, METADATA, NAME, NETWORKS, PERSONALITY, SECURITY_GROUPS, USER_DATA, SCHEDULER_HINTS, ) = ( 'context', 'adminPass', 'auto_disk_config', 'availability_zone', 'block_device_mapping', 'block_device_mapping_v2', 'config_drive', 'flavor', 'image', 'key_name', 'metadata', 'name', 'networks', 'personality', 'security_groups', 'user_data', 'scheduler_hints', ) BDM_KEYS = ( BDM_DEVICE_NAME, BDM_VOLUME_SIZE, ) = ( 'device_name', 'volume_size', ) BDM2_KEYS = ( BDM2_UUID, BDM2_SOURCE_TYPE, BDM2_DESTINATION_TYPE, BDM2_DISK_BUS, BDM2_DEVICE_NAME, BDM2_VOLUME_SIZE, BDM2_GUEST_FORMAT, BDM2_BOOT_INDEX, BDM2_DEVICE_TYPE, BDM2_DELETE_ON_TERMINATION, ) = ( 'uuid', 'source_type', 'destination_type', 'disk_bus', 'device_name', 'volume_size', 'guest_format', 'boot_index', 'device_type', 'delete_on_termination', ) NETWORK_KEYS = ( PORT, FIXED_IP, NETWORK, ) = ( 'port', 'fixed-ip', 'network', ) PERSONALITY_KEYS = ( PATH, CONTENTS, ) = ( 'path', 'contents', ) SCHEDULER_HINTS_KEYS = (GROUP, ) = ('group', ) properties_schema = { CONTEXT: schema.Map(_('Customized security context for operating servers.'), ), ADMIN_PASS: schema.String(_('Password for the administrator account.'), ), AUTO_DISK_CONFIG: schema.Boolean( _('Whether the disk partition is done automatically.'), default=True, ), AVAILABILITY_ZONE: schema.String( _('Name of availability zone for running the server.'), ), BLOCK_DEVICE_MAPPING: schema.List( _('A list specifying the properties of block devices to be used ' 'for this server.'), schema=schema.Map( _('A map specifying the properties of a block device to be ' 'used by the server.'), schema={ BDM_DEVICE_NAME: schema.String( _('Block device name, should be <=255 chars.'), ), BDM_VOLUME_SIZE: schema.Integer(_('Block device size in GB.'), ), }), ), BLOCK_DEVICE_MAPPING_V2: schema.List( _('A list specifying the properties of block devices to be used ' 'for this server.'), schema=schema.Map( _('A map specifying the properties of a block device to be ' 'used by the server.'), schema={ BDM2_UUID: schema.String( _('ID of the source image, snapshot or volume'), ), BDM2_SOURCE_TYPE: schema.String( _('Volume source type, should be image, snapshot, ' 'volume or blank'), required=True, ), BDM2_DESTINATION_TYPE: schema.String( _('Volume destination type, should be volume or ' 'local'), required=True, ), BDM2_DISK_BUS: schema.String(_('Bus of the device.'), ), BDM2_DEVICE_NAME: schema.String( _('Name of the device(e.g. vda, xda, ....).'), ), BDM2_VOLUME_SIZE: schema.Integer( _('Size of the block device in MB(for swap) and ' 'in GB(for other formats)'), required=True, ), BDM2_GUEST_FORMAT: schema.String( _('Specifies the disk file system format(e.g. swap, ' 'ephemeral, ...).'), ), BDM2_BOOT_INDEX: schema.Integer(_('Define the boot order of the device'), ), BDM2_DEVICE_TYPE: schema.String( _('Type of the device(e.g. disk, cdrom, ...).'), ), BDM2_DELETE_ON_TERMINATION: schema.Boolean( _('Whether to delete the volume when the server ' 'stops.'), ), }), ), CONFIG_DRIVE: schema.Boolean( _('Whether config drive should be enabled for the server.'), ), FLAVOR: schema.String( _('ID of flavor used for the server.'), required=True, updatable=True, ), IMAGE: schema.String( # IMAGE is not required, because there could be BDM or BDMv2 # support and the corresponding settings effective _('ID of image to be used for the new server.'), updatable=True, ), KEY_NAME: schema.String(_('Name of Nova keypair to be injected to server.'), ), METADATA: schema.Map( _('A collection of key/value pairs to be associated with the ' 'server created. Both key and value should be <=255 chars.'), updatable=True, ), NAME: schema.String( _('Name of the server.'), updatable=True, ), NETWORKS: schema.List( _('List of networks for the server.'), schema=schema.Map( _('A map specifying the properties of a network for uses.'), schema={ NETWORK: schema.String( _('Name or ID of network to create a port on.'), ), PORT: schema.String(_('Port ID to be used by the network.'), ), FIXED_IP: schema.String(_('Fixed IP to be used by the network.'), ), }, ), updatable=True, ), PERSONALITY: schema.List( _('List of files to be injected into the server, where each.'), schema=schema.Map( _('A map specifying the path & contents for an injected ' 'file.'), schema={ PATH: schema.String( _('In-instance path for the file to be injected.'), required=True, ), CONTENTS: schema.String( _('Contents of the file to be injected.'), required=True, ), }, ), ), SCHEDULER_HINTS: schema.Map( _('A collection of key/value pairs to be associated with the ' 'Scheduler hints. Both key and value should be <=255 chars.'), ), SECURITY_GROUPS: schema.List( _('List of security groups.'), schema=schema.String( _('Name of a security group'), required=True, ), ), USER_DATA: schema.String(_('User data to be exposed by the metadata server.'), ), } OP_NAMES = (OP_REBOOT, ) = ('reboot', ) REBOOT_TYPE = 'type' REBOOT_TYPES = (REBOOT_SOFT, REBOOT_HARD) = ('SOFT', 'HARD') OPERATIONS = { OP_REBOOT: schema.Operation( _("Reboot the nova server."), schema={ REBOOT_TYPE: schema.String( _("Type of reboot which can be 'SOFT' or 'HARD'."), default=REBOOT_SOFT, constraints=[ constraints.AllowedValues(REBOOT_TYPES), ]) }) } def __init__(self, type_name, name, **kwargs): super(ServerProfile, self).__init__(type_name, name, **kwargs) self._novaclient = None self._neutronclient = None self.server_id = None def nova(self, obj): '''Construct nova client based on object. :param obj: Object for which the client is created. It is expected to be None when retrieving an existing client. When creating a client, it contains the user and project to be used. ''' if self._novaclient is not None: return self._novaclient params = self._build_conn_params(obj.user, obj.project) self._novaclient = driver_base.SenlinDriver().compute(params) return self._novaclient def neutron(self, obj): '''Construct neutron client based on object. :param obj: Object for which the client is created. It is expected to be None when retrieving an existing client. When creating a client, it contains the user and project to be used. ''' if self._neutronclient is not None: return self._neutronclient params = self._build_conn_params(obj.user, obj.project) self._neutronclient = driver_base.SenlinDriver().network(params) return self._neutronclient def do_validate(self, obj): '''Validate if the spec has provided valid info for server creation.''' return True def _resolve_bdm(self, bdm): for bd in bdm: for key in self.BDM2_KEYS: if bd[key] is None: del bd[key] return bdm def _resolve_network(self, networks, client): for network in networks: net_name_id = network.get(self.NETWORK) if net_name_id: res = client.network_get(net_name_id) network['uuid'] = res.id del network[self.NETWORK] if network['port'] is None: del network['port'] if network['fixed-ip'] is None: del network['fixed-ip'] return networks def do_create(self, obj): '''Create a server using the given profile.''' kwargs = {} for key in self.KEYS: # context is treated as connection parameters if key == self.CONTEXT: continue if self.properties[key] is not None: kwargs[key] = self.properties[key] name_or_id = self.properties[self.IMAGE] if name_or_id is not None: image = self.nova(obj).image_find(name_or_id) # wait for new version of openstacksdk to fix this kwargs.pop(self.IMAGE) kwargs['imageRef'] = image.id flavor_id = self.properties[self.FLAVOR] flavor = self.nova(obj).flavor_find(flavor_id, False) # wait for new verson of openstacksdk to fix this kwargs.pop(self.FLAVOR) kwargs['flavorRef'] = flavor.id name = self.properties[self.NAME] if name: kwargs['name'] = name else: kwargs['name'] = obj.name metadata = self.properties[self.METADATA] or {} if obj.cluster_id: metadata['cluster'] = obj.cluster_id kwargs['metadata'] = metadata block_device_mapping_v2 = self.properties[self.BLOCK_DEVICE_MAPPING_V2] if block_device_mapping_v2 is not None: kwargs['block_device_mapping_v2'] = self._resolve_bdm( block_device_mapping_v2) user_data = self.properties[self.USER_DATA] if user_data is not None: ud = encodeutils.safe_encode(user_data) kwargs['user_data'] = encodeutils.safe_decode(base64.b64encode(ud)) networks = self.properties[self.NETWORKS] if networks is not None: kwargs['networks'] = self._resolve_network(networks, self.neutron(obj)) secgroups = self.properties[self.SECURITY_GROUPS] if secgroups: kwargs['security_groups'] = [{'name': sg} for sg in secgroups] if 'placement' in obj.data: if 'zone' in obj.data['placement']: kwargs['availability_zone'] = obj.data['placement']['zone'] if 'servergroup' in obj.data['placement']: group_id = obj.data['placement']['servergroup'] hints = self.properties.get(self.SCHEDULER_HINTS, {}) hints.update({'group': group_id}) kwargs['scheduler_hints'] = hints LOG.info('Creating server: %s' % kwargs) server = self.nova(obj).server_create(**kwargs) self.nova(obj).wait_for_server(server.id) self.server_id = server.id return server.id def do_delete(self, obj): self.server_id = obj.physical_id if not obj.physical_id: return True try: self.nova(obj).server_delete(self.server_id) self.nova(obj).wait_for_server_delete(self.server_id) except Exception as ex: LOG.error('Error: %s' % six.text_type(ex)) return False return True def do_update(self, obj, new_profile=None, **params): '''Perform update on the server. :param obj: the server to operate on :param new_profile: the new profile for the server. :param params: a dictionary of optional parameters. ''' self.server_id = obj.physical_id if not self.server_id: return True if not new_profile: return True if not self.validate_for_update(new_profile): return False # TODO(Yanyan Hu): Update block_device properties # Update basic properties of server if not self._update_basic_properties(obj, new_profile): return False # Update server flavor flavor = self.properties[self.FLAVOR] new_flavor = new_profile.properties[self.FLAVOR] if new_flavor != flavor: try: self._update_flavor(obj, flavor, new_flavor) except Exception as ex: LOG.exception(_('Failed in updating server flavor: %s'), six.text_type(ex)) return False # Update server image old_passwd = self.properties.get(self.ADMIN_PASS) passwd = old_passwd if new_profile.properties[self.ADMIN_PASS] is not None: passwd = new_profile.properties[self.ADMIN_PASS] image = self.properties[self.IMAGE] new_image = new_profile.properties[self.IMAGE] if new_image != image: try: self._update_image(obj, image, new_image, passwd) except Exception as ex: LOG.exception(_('Failed in updating server image: %s'), six.text_type(ex)) return False elif old_passwd != passwd: # TODO(Jun Xu): update server admin password pass # Update server network networks_current = self.properties[self.NETWORKS] networks_create = new_profile.properties[self.NETWORKS] networks_delete = copy.deepcopy(networks_current) for network in networks_current: if network in networks_create: networks_create.remove(network) networks_delete.remove(network) if networks_create or networks_delete: # We have network interfaces to be deleted and/or created try: self._update_network(obj, networks_create, networks_delete) except Exception as ex: LOG.exception(_('Failed in updating server network: %s'), six.text_type(ex)) return False return True def _update_basic_properties(self, obj, new_profile): '''Updating basic server properties including name, metadata''' # Update server metadata metadata = self.properties[self.METADATA] new_metadata = new_profile.properties[self.METADATA] if new_metadata != metadata: if new_metadata is None: new_metadata = {} try: self.nova(obj).server_metadata_update(self.server_id, new_metadata) except Exception as ex: LOG.exception(_('Failed in updating server metadata: %s'), six.text_type(ex)) return False # Update server name name = self.properties[self.NAME] new_name = new_profile.properties[self.NAME] if new_name != name: attrs = {'name': new_name if new_name else obj.name} try: self.nova(obj).server_update(self.server_id, **attrs) except Exception as ex: LOG.exception(_('Failed in updating server name: %s'), six.text_type(ex)) return False return True def _update_flavor(self, obj, old_flavor, new_flavor): '''Updating server flavor''' res = self.nova(obj).flavor_find(old_flavor) old_flavor_id = res.id res = self.nova(obj).flavor_find(new_flavor) new_flavor_id = res.id if new_flavor_id == old_flavor_id: return try: self.nova(obj).server_resize(obj.physical_id, new_flavor_id) self.nova(obj).wait_for_server(obj.physical_id, 'VERIFY_RESIZE') except Exception as ex: LOG.error(_("Server resizing failed, revert it: %s"), six.text_type(ex)) self.nova(obj).server_resize_revert(obj.physical_id) self.nova(obj).wait_for_server(obj.physical_id, 'ACTIVE') raise exception.ResourceUpdateFailure(resource=obj.physical_id) self.nova(obj).server_resize_confirm(obj.physical_id) self.nova(obj).wait_for_server(obj.physical_id, 'ACTIVE') def _update_image(self, obj, old_image, new_image, admin_password): '''Updating server image''' if old_image: res = self.nova(obj).image_find(old_image) image_id = res.id else: server = self.nova(obj).server_get(obj.physical_id) image_id = server.image['id'] if new_image: res = self.nova(obj).image_find(new_image) new_image_id = res.id if new_image_id != image_id: # (Jun Xu): Not update name here if name changed, # it should be updated in do_update self.nova(obj).server_rebuild(obj.physical_id, new_image_id, self.properties.get(self.NAME), admin_password) self.nova(obj).wait_for_server(obj.physical_id, 'ACTIVE') else: # TODO(Yanyan Hu): Allow server update with new_image # set to None if Nova service supports it LOG.error( _("Updating Nova server with image set to None is " "not supported by Nova.")) raise exception.ResourceUpdateFailure(resource=obj.physical_id) def _update_network(self, obj, networks_create, networks_delete): '''Updating server network interfaces''' server = self.nova(obj).server_get(self.server_id) ports_existing = list(self.nova(obj).server_interface_list(server)) ports = [] for p in ports_existing: fixed_ips = [] for addr in p['fixed_ips']: fixed_ips.append(addr['ip_address']) ports.append({ 'port_id': p['port_id'], 'net_id': p['net_id'], 'fixed_ips': fixed_ips }) # Detach some existing ports # Step1. Accurately search port with port_id or fixed-ip/net_id for n in networks_delete: if n['port'] is not None: for p in ports: if p['port_id'] == n['port']: ports.remove(p) break res = self.nova(obj).server_interface_delete(n['port'], server) elif n['fixed-ip'] is not None: res = self.neutron(obj).network_get(n['network']) net_id = res.id for p in ports: if (n['fixed-ip'] in p['fixed_ips']) and (p['net_id'] == net_id): res = self.nova(obj).server_interface_delete( p['port_id'], server) ports.remove(p) break # Step2. Fuzzy search port with net_id for n in networks_delete: if n['port'] is None and n['fixed-ip'] is None: res = self.neutron(obj).network_get(n['network']) net_id = res.id for p in ports: if p['net_id'] == net_id: res = self.nova(obj).server_interface_delete( p['port_id'], server) ports.remove(p) break # Attach new ports added in new network definition for n in networks_create: net_name_id = n.get(self.NETWORK, None) if net_name_id: res = self.neutron(obj).network_get(net_name_id) n['net_id'] = res.id if n['fixed-ip'] is not None: n['fixed_ips'] = [{'ip_address': n['fixed-ip']}] if n['port'] is not None: n['port_id'] = n['port'] del n['network'] del n['port'] del n['fixed-ip'] self.nova(obj).server_interface_create(server, **n) return def do_check(self, obj): if not obj.physical_id: return False self.server_id = obj.physical_id try: server = self.nova(obj).server_get(self.server_id) except Exception as ex: LOG.error('Error: %s' % six.text_type(ex)) return False if (server is None or server.status != 'ACTIVE'): return False return True def do_get_details(self, obj): known_keys = { 'OS-DCF:diskConfig', 'OS-EXT-AZ:availability_zone', 'OS-EXT-STS:power_state', 'OS-EXT-STS:vm_state', 'accessIPv4', 'accessIPv6', 'config_drive', 'created', 'hostId', 'id', 'key_name', 'locked', 'metadata', 'name', 'os-extended-volumes:volumes_attached', 'progress', 'status', 'updated' } if obj.physical_id is None or obj.physical_id == '': return {} try: server = self.nova(obj).server_get(obj.physical_id) except exception.InternalError as ex: return {'Error': {'code': ex.code, 'message': six.text_type(ex)}} if server is None: return {} server_data = server.to_dict() details = { 'image': server_data['image']['id'], 'flavor': server_data['flavor']['id'], } for key in known_keys: if key in server_data: details[key] = server_data[key] # process special keys like 'OS-EXT-STS:task_state': these keys have # a default value '-' when not existing special_keys = [ 'OS-EXT-STS:task_state', 'OS-SRV-USG:launched_at', 'OS-SRV-USG:terminated_at', ] for key in special_keys: if key in server_data: val = server_data[key] details[key] = val if val else '-' # process network addresses details['addresses'] = {} for net in server_data['addresses']: addresses = [] for addr in server_data['addresses'][net]: # Ignore IPv6 address if addr['version'] == 4: addresses.append(addr['addr']) details['addresses'][net] = addresses # process security groups sgroups = [] if 'security_groups' in server_data: for sg in server_data['security_groups']: sgroups.append(sg['name']) if len(sgroups) == 0: details['security_groups'] = '' elif len(sgroups) == 1: details['security_groups'] = sgroups[0] else: details['security_groups'] = sgroups return dict((k, details[k]) for k in sorted(details)) def do_join(self, obj, cluster_id): if not obj.physical_id: return False metadata = self.nova(obj).server_metadata_get(obj.physical_id) or {} metadata['cluster'] = cluster_id self.nova(obj).server_metadata_update(obj.physical_id, metadata) return super(ServerProfile, self).do_join(obj, cluster_id) def do_leave(self, obj): if not obj.physical_id: return False self.nova(obj).server_metadata_delete(obj.physical_id, ['cluster']) return super(ServerProfile, self).do_leave(obj) def do_rebuild(self, obj): if not obj.physical_id: return False self.server_id = obj.physical_id try: server = self.nova(obj).server_get(self.server_id) except Exception as ex: LOG.exception(_('Failed at getting server: %s'), six.text_type(ex)) return False if server is None or server.image is None: return False image_id = server.image['id'] admin_pass = self.properties.get(self.ADMIN_PASS) try: self.nova(obj).server_rebuild(self.server_id, image_id, self.properties.get(self.NAME), admin_pass) self.nova(obj).wait_for_server(self.server_id, 'ACTIVE') except Exception as ex: LOG.exception(_('Failed at rebuilding server: %s'), six.text_type(ex)) return False return True def do_recover(self, obj, **options): if 'operation' in options: if options['operation'] == 'REBUILD': return self.do_rebuild(obj) res = super(ServerProfile, self).do_recover(obj, **options) return res def handle_reboot(self, obj, **options): """Handler for the reboot operation.""" pass