class PortgroupCRUDPayload(notification.NotificationPayloadBase): # Version 1.0: Initial version VERSION = '1.0' SCHEMA = { 'address': ('portgroup', 'address'), 'extra': ('portgroup', 'extra'), 'mode': ('portgroup', 'mode'), 'name': ('portgroup', 'name'), 'properties': ('portgroup', 'properties'), 'standalone_ports_supported': ('portgroup', 'standalone_ports_supported'), 'created_at': ('portgroup', 'created_at'), 'updated_at': ('portgroup', 'updated_at'), 'uuid': ('portgroup', 'uuid') } fields = { 'address': object_fields.MACAddressField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'mode': object_fields.StringField(nullable=True), 'name': object_fields.StringField(nullable=True), 'node_uuid': object_fields.UUIDField(), 'properties': object_fields.FlexibleDictField(nullable=True), 'standalone_ports_supported': object_fields.BooleanField( nullable=True), 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField() } def __init__(self, portgroup, node_uuid): super(PortgroupCRUDPayload, self).__init__(node_uuid=node_uuid) self.populate_schema(portgroup=portgroup)
class VolumeConnectorCRUDPayload(notification.NotificationPayloadBase): """Payload schema for CRUD of a volume connector.""" # Version 1.0: Initial version VERSION = '1.0' SCHEMA = { 'extra': ('connector', 'extra'), 'type': ('connector', 'type'), 'connector_id': ('connector', 'connector_id'), 'created_at': ('connector', 'created_at'), 'updated_at': ('connector', 'updated_at'), 'uuid': ('connector', 'uuid'), } fields = { 'extra': object_fields.FlexibleDictField(nullable=True), 'type': object_fields.StringField(nullable=True), 'connector_id': object_fields.StringField(nullable=True), 'node_uuid': object_fields.UUIDField(), 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField(), } def __init__(self, connector, node_uuid): super(VolumeConnectorCRUDPayload, self).__init__(node_uuid=node_uuid) self.populate_schema(connector=connector)
class PortCRUDPayload(notification.NotificationPayloadBase): # Version 1.0: Initial version VERSION = '1.0' SCHEMA = { 'address': ('port', 'address'), 'extra': ('port', 'extra'), 'local_link_connection': ('port', 'local_link_connection'), 'pxe_enabled': ('port', 'pxe_enabled'), 'created_at': ('port', 'created_at'), 'updated_at': ('port', 'updated_at'), 'uuid': ('port', 'uuid') } fields = { 'address': object_fields.MACAddressField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'local_link_connection': object_fields.FlexibleDictField(nullable=True), 'pxe_enabled': object_fields.BooleanField(nullable=True), 'node_uuid': object_fields.UUIDField(), 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField() # TODO(yuriyz): add "portgroup_uuid" field with portgroup notifications } def __init__(self, port, node_uuid): super(PortCRUDPayload, self).__init__(node_uuid=node_uuid) self.populate_schema(port=port)
class VolumeTargetCRUDPayload(notification.NotificationPayloadBase): # Version 1.0: Initial Version VERSION = '1.0' SCHEMA = { 'boot_index': ('target', 'boot_index'), 'extra': ('target', 'extra'), 'properties': ('target', 'properties'), 'volume_id': ('target', 'volume_id'), 'volume_type': ('target', 'volume_type'), 'created_at': ('target', 'created_at'), 'updated_at': ('target', 'updated_at'), 'uuid': ('target', 'uuid'), } fields = { 'boot_index': object_fields.IntegerField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'node_uuid': object_fields.UUIDField(), 'properties': object_fields.FlexibleDictField(nullable=True), 'volume_id': object_fields.StringField(nullable=True), 'volume_type': object_fields.StringField(nullable=True), 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField(), } def __init__(self, target, node_uuid): super(VolumeTargetCRUDPayload, self).__init__(node_uuid=node_uuid) self.populate_schema(target=target)
class DeployTemplateCRUDPayload(notification.NotificationPayloadBase): # Version 1.0: Initial version VERSION = '1.0' SCHEMA = { 'created_at': ('deploy_template', 'created_at'), 'extra': ('deploy_template', 'extra'), 'name': ('deploy_template', 'name'), 'steps': ('deploy_template', 'steps'), 'updated_at': ('deploy_template', 'updated_at'), 'uuid': ('deploy_template', 'uuid') } fields = { 'created_at': object_fields.DateTimeField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'name': object_fields.StringField(nullable=False), 'steps': object_fields.ListOfFlexibleDictsField(nullable=False), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField() } def __init__(self, deploy_template, **kwargs): super(DeployTemplateCRUDPayload, self).__init__(**kwargs) self.populate_schema(deploy_template=deploy_template)
class IronicObject(object_base.VersionedObject): """Base class and object factory. This forms the base of all objects that can be remoted or instantiated via RPC. Simply defining a class that inherits from this base class will make it remotely instantiatable. Objects should implement the necessary "get" classmethod routines as well as "save" object methods as appropriate. """ OBJ_SERIAL_NAMESPACE = 'ironic_object' OBJ_PROJECT_NAMESPACE = 'ironic' # TODO(lintan) Refactor these fields and create PersistentObject and # TimeStampObject like Nova when it is necessary. fields = { 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), } def as_dict(self): return dict( (k, getattr(self, k)) for k in self.fields if hasattr(self, k)) def obj_refresh(self, loaded_object): """Applies updates for objects that inherit from base.IronicObject. Checks for updated attributes in an object. Updates are applied from the loaded object column by column in comparison with the current object. """ for field in self.fields: if (self.obj_attr_is_set(field) and self[field] != loaded_object[field]): self[field] = loaded_object[field]
class AllocationCRUDPayload(notification.NotificationPayloadBase): # Version 1.0: Initial version VERSION = '1.0' SCHEMA = { 'candidate_nodes': ('allocation', 'candidate_nodes'), 'created_at': ('allocation', 'created_at'), 'extra': ('allocation', 'extra'), 'last_error': ('allocation', 'last_error'), 'name': ('allocation', 'name'), 'resource_class': ('allocation', 'resource_class'), 'state': ('allocation', 'state'), 'traits': ('allocation', 'traits'), 'updated_at': ('allocation', 'updated_at'), 'uuid': ('allocation', 'uuid') } fields = { 'uuid': object_fields.UUIDField(nullable=True), 'name': object_fields.StringField(nullable=True), 'node_uuid': object_fields.StringField(nullable=True), 'state': object_fields.StringField(nullable=True), 'last_error': object_fields.StringField(nullable=True), 'resource_class': object_fields.StringField(nullable=True), 'traits': object_fields.ListOfStringsField(nullable=True), 'candidate_nodes': object_fields.ListOfStringsField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), } def __init__(self, allocation, node_uuid=None): super(AllocationCRUDPayload, self).__init__(node_uuid=node_uuid) self.populate_schema(allocation=allocation)
def test_refresh(self): host = self.fake_conductor['hostname'] t0 = self.fake_conductor['updated_at'] t1 = t0 + datetime.timedelta(seconds=10) returns = [ dict(self.fake_conductor, updated_at=t0), dict(self.fake_conductor, updated_at=t1) ] expected = [mock.call(host), mock.call(host)] with mock.patch.object(self.dbapi, 'get_conductor', side_effect=returns, autospec=True) as mock_get_cdr: c = objects.Conductor.get_by_hostname(self.context, host) # ensure timestamps have tzinfo datetime_field = fields.DateTimeField() self.assertEqual( datetime_field.coerce(datetime_field, 'updated_at', t0), c.updated_at) c.refresh() self.assertEqual( datetime_field.coerce(datetime_field, 'updated_at', t1), c.updated_at) self.assertEqual(expected, mock_get_cdr.call_args_list) self.assertEqual(self.context, c._context)
class PortCRUDPayload(notification.NotificationPayloadBase): # Version 1.0: Initial version # Version 1.1: Add "portgroup_uuid" field # Version 1.2: Add "physical_network" field # Version 1.3: Add "is_smartnic" field # Version 1.4: Add "name" field VERSION = '1.4' SCHEMA = { 'address': ('port', 'address'), 'extra': ('port', 'extra'), 'local_link_connection': ('port', 'local_link_connection'), 'pxe_enabled': ('port', 'pxe_enabled'), 'physical_network': ('port', 'physical_network'), 'created_at': ('port', 'created_at'), 'updated_at': ('port', 'updated_at'), 'uuid': ('port', 'uuid'), 'is_smartnic': ('port', 'is_smartnic'), 'name': ('port', 'name'), } fields = { 'address': object_fields.MACAddressField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'local_link_connection': object_fields.FlexibleDictField(nullable=True), 'pxe_enabled': object_fields.BooleanField(nullable=True), 'node_uuid': object_fields.UUIDField(), 'portgroup_uuid': object_fields.UUIDField(nullable=True), 'physical_network': object_fields.StringField(nullable=True), 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField(), 'is_smartnic': object_fields.BooleanField(nullable=True, default=False), 'name': object_fields.StringField(nullable=True), } def __init__(self, port, node_uuid, portgroup_uuid): super(PortCRUDPayload, self).__init__(node_uuid=node_uuid, portgroup_uuid=portgroup_uuid) self.populate_schema(port=port)
class ChassisCRUDPayload(notification.NotificationPayloadBase): # Version 1.0: Initial version VERSION = '1.0' SCHEMA = { 'description': ('chassis', 'description'), 'extra': ('chassis', 'extra'), 'created_at': ('chassis', 'created_at'), 'updated_at': ('chassis', 'updated_at'), 'uuid': ('chassis', 'uuid') } fields = { 'description': object_fields.StringField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField() } def __init__(self, chassis, **kwargs): super(ChassisCRUDPayload, self).__init__(**kwargs) self.populate_schema(chassis=chassis)
def test_base_attributes(self): dt = datetime.datetime(1955, 11, 5, 0, 0, tzinfo=iso8601.UTC) datatime = fields.DateTimeField() obj = MyObj(self.context) obj.created_at = dt obj.updated_at = dt expected = { 'ironic_object.name': 'MyObj', 'ironic_object.namespace': 'ironic', 'ironic_object.version': '1.5', 'ironic_object.changes': ['created_at', 'updated_at'], 'ironic_object.data': { 'created_at': datatime.stringify(dt), 'updated_at': datatime.stringify(dt), } } actual = obj.obj_to_primitive() # ironic_object.changes is built from a set and order is undefined self.assertEqual(sorted(expected['ironic_object.changes']), sorted(actual['ironic_object.changes'])) del expected['ironic_object.changes'], actual['ironic_object.changes'] self.assertEqual(expected, actual)
class IronicObject(object_base.VersionedObject): """Base class and object factory. This forms the base of all objects that can be remoted or instantiated via RPC. Simply defining a class that inherits from this base class will make it remotely instantiatable. Objects should implement the necessary "get" classmethod routines as well as "save" object methods as appropriate. """ OBJ_SERIAL_NAMESPACE = 'ironic_object' OBJ_PROJECT_NAMESPACE = 'ironic' # TODO(lintan) Refactor these fields and create PersistentObject and # TimeStampObject like Nova when it is necessary. fields = { 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), } def as_dict(self): return dict( (k, getattr(self, k)) for k in self.fields if hasattr(self, k)) def obj_refresh(self, loaded_object): """Applies updates for objects that inherit from base.IronicObject. Checks for updated attributes in an object. Updates are applied from the loaded object column by column in comparison with the current object. """ for field in self.fields: if (self.obj_attr_is_set(field) and self[field] != loaded_object[field]): self[field] = loaded_object[field] @staticmethod def _from_db_object(obj, db_object): """Converts a database entity to a formal object. :param obj: An object of the class. :param db_object: A DB model of the object :return: The object of the class with the database entity added """ for field in obj.fields: obj[field] = db_object[field] obj.obj_reset_changes() return obj @classmethod def _from_db_object_list(cls, context, db_objects): """Returns objects corresponding to database entities. Returns a list of formal objects of this class that correspond to the list of database entities. :param context: security context :param db_objects: A list of DB models of the object :returns: A list of objects corresponding to the database entities """ return [ cls._from_db_object(cls(context), db_obj) for db_obj in db_objects ]
class Node(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version # Version 1.1: Added instance_info # Version 1.2: Add get() and get_by_id() and make get_by_uuid() # only work with a uuid # Version 1.3: Add create() and destroy() # Version 1.4: Add get_by_instance_uuid() # Version 1.5: Add list() # Version 1.6: Add reserve() and release() # Version 1.7: Add conductor_affinity # Version 1.8: Add maintenance_reason # Version 1.9: Add driver_internal_info # Version 1.10: Add name and get_by_name() # Version 1.11: Add clean_step # Version 1.12: Add raid_config and target_raid_config # Version 1.13: Add touch_provisioning() # Version 1.14: Add _validate_property_values() and make create() # and save() validate the input of property values. # Version 1.15: Add get_by_port_addresses # Version 1.16: Add network_interface field # Version 1.17: Add resource_class field # Version 1.18: Add default setting for network_interface # Version 1.19: Add fields: boot_interface, console_interface, # deploy_interface, inspect_interface, management_interface, # power_interface, raid_interface, vendor_interface # Version 1.20: Type of network_interface changed to just nullable string # Version 1.21: Add storage_interface field # Version 1.22: Add rescue_interface field # Version 1.23: Add traits field # Version 1.24: Add bios_interface field # Version 1.25: Add fault field # Version 1.26: Add deploy_step field # Version 1.27: Add conductor_group field # Version 1.28: Add automated_clean field # Version 1.29: Add protected and protected_reason fields VERSION = '1.29' dbapi = db_api.get_instance() fields = { 'id': object_fields.IntegerField(), 'uuid': object_fields.UUIDField(nullable=True), 'name': object_fields.StringField(nullable=True), 'chassis_id': object_fields.IntegerField(nullable=True), 'instance_uuid': object_fields.UUIDField(nullable=True), 'driver': object_fields.StringField(nullable=True), 'driver_info': object_fields.FlexibleDictField(nullable=True), 'driver_internal_info': object_fields.FlexibleDictField(nullable=True), # A clean step dictionary, indicating the current clean step # being executed, or None, indicating cleaning is not in progress # or has not yet started. 'clean_step': object_fields.FlexibleDictField(nullable=True), # A deploy step dictionary, indicating the current step # being executed, or None, indicating deployment is not in progress # or has not yet started. 'deploy_step': object_fields.FlexibleDictField(nullable=True), 'raid_config': object_fields.FlexibleDictField(nullable=True), 'target_raid_config': object_fields.FlexibleDictField(nullable=True), 'instance_info': object_fields.FlexibleDictField(nullable=True), 'properties': object_fields.FlexibleDictField(nullable=True), 'reservation': object_fields.StringField(nullable=True), # a reference to the id of the conductor service, not its hostname, # that has most recently performed some action which could require # local state to be maintained (eg, built a PXE config) 'conductor_affinity': object_fields.IntegerField(nullable=True), 'conductor_group': object_fields.StringField(nullable=True), # One of states.POWER_ON|POWER_OFF|NOSTATE|ERROR 'power_state': object_fields.StringField(nullable=True), # Set to one of states.POWER_ON|POWER_OFF when a power operation # starts, and set to NOSTATE when the operation finishes # (successfully or unsuccessfully). 'target_power_state': object_fields.StringField(nullable=True), 'provision_state': object_fields.StringField(nullable=True), 'provision_updated_at': object_fields.DateTimeField(nullable=True), 'target_provision_state': object_fields.StringField(nullable=True), 'maintenance': object_fields.BooleanField(), 'maintenance_reason': object_fields.StringField(nullable=True), 'fault': object_fields.StringField(nullable=True), 'console_enabled': object_fields.BooleanField(), # Any error from the most recent (last) asynchronous transaction # that started but failed to finish. 'last_error': object_fields.StringField(nullable=True), # Used by nova to relate the node to a flavor 'resource_class': object_fields.StringField(nullable=True), 'inspection_finished_at': object_fields.DateTimeField(nullable=True), 'inspection_started_at': object_fields.DateTimeField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'automated_clean': objects.fields.BooleanField(nullable=True), 'protected': objects.fields.BooleanField(), 'protected_reason': object_fields.StringField(nullable=True), 'bios_interface': object_fields.StringField(nullable=True), 'boot_interface': object_fields.StringField(nullable=True), 'console_interface': object_fields.StringField(nullable=True), 'deploy_interface': object_fields.StringField(nullable=True), 'inspect_interface': object_fields.StringField(nullable=True), 'management_interface': object_fields.StringField(nullable=True), 'network_interface': object_fields.StringField(nullable=True), 'power_interface': object_fields.StringField(nullable=True), 'raid_interface': object_fields.StringField(nullable=True), 'rescue_interface': object_fields.StringField(nullable=True), 'storage_interface': object_fields.StringField(nullable=True), 'vendor_interface': object_fields.StringField(nullable=True), 'traits': object_fields.ObjectField('TraitList', nullable=True), } def as_dict(self, secure=False): d = super(Node, self).as_dict() if secure: d['driver_info'] = strutils.mask_dict_password( d.get('driver_info', {}), "******") d['instance_info'] = strutils.mask_dict_password( d.get('instance_info', {}), "******") return d def _validate_property_values(self, properties): """Check if the input of local_gb, cpus and memory_mb are valid. :param properties: a dict contains the node's information. """ if not properties: return invalid_msgs_list = [] for param in REQUIRED_INT_PROPERTIES: value = properties.get(param) if value is None: continue try: int_value = int(value) if int_value < 0: raise ValueError("Value must be non-negative") except (ValueError, TypeError): msg = (('%(param)s=%(value)s') % { 'param': param, 'value': value }) invalid_msgs_list.append(msg) if invalid_msgs_list: msg = (_('The following properties for node %(node)s ' 'should be non-negative integers, ' 'but provided values are: %(msgs)s') % { 'node': self.uuid, 'msgs': ', '.join(invalid_msgs_list) }) raise exception.InvalidParameterValue(msg) def _set_from_db_object(self, context, db_object, fields=None): fields = set(fields or self.fields) - {'traits'} super(Node, self)._set_from_db_object(context, db_object, fields) self.traits = object_base.obj_make_list(context, objects.TraitList(context), objects.Trait, db_object['traits']) self.traits.obj_reset_changes() # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get(cls, context, node_id): """Find a node based on its id or uuid and return a Node object. :param context: Security context :param node_id: the id *or* uuid of a node. :returns: a :class:`Node` object. """ if strutils.is_int_like(node_id): return cls.get_by_id(context, node_id) elif uuidutils.is_uuid_like(node_id): return cls.get_by_uuid(context, node_id) else: raise exception.InvalidIdentity(identity=node_id) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_id(cls, context, node_id): """Find a node based on its integer ID and return a Node object. :param cls: the :class:`Node` :param context: Security context :param node_id: the ID of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_id(node_id) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_uuid(cls, context, uuid): """Find a node based on UUID and return a Node object. :param cls: the :class:`Node` :param context: Security context :param uuid: the UUID of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_uuid(uuid) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_name(cls, context, name): """Find a node based on name and return a Node object. :param cls: the :class:`Node` :param context: Security context :param name: the logical name of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_name(name) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_instance_uuid(cls, context, instance_uuid): """Find a node based on the instance UUID and return a Node object. :param cls: the :class:`Node` :param context: Security context :param uuid: the UUID of the instance. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_instance(instance_uuid) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def list(cls, context, limit=None, marker=None, sort_key=None, sort_dir=None, filters=None): """Return a list of Node objects. :param cls: the :class:`Node` :param context: Security context. :param limit: maximum number of resources to return in a single result. :param marker: pagination marker for large data sets. :param sort_key: column to sort results by. :param sort_dir: direction to sort. "asc" or "desc". :param filters: Filters to apply. :returns: a list of :class:`Node` object. """ db_nodes = cls.dbapi.get_node_list(filters=filters, limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir) return cls._from_db_object_list(context, db_nodes) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def reserve(cls, context, tag, node_id): """Get and reserve a node. To prevent other ManagerServices from manipulating the given Node while a Task is performed, mark it reserved by this host. :param cls: the :class:`Node` :param context: Security context. :param tag: A string uniquely identifying the reservation holder. :param node_id: A node ID or UUID. :raises: NodeNotFound if the node is not found. :returns: a :class:`Node` object. """ db_node = cls.dbapi.reserve_node(tag, node_id) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def release(cls, context, tag, node_id): """Release the reservation on a node. :param context: Security context. :param tag: A string uniquely identifying the reservation holder. :param node_id: A node id or uuid. :raises: NodeNotFound if the node is not found. """ cls.dbapi.release_node(tag, node_id) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def create(self, context=None): """Create a Node record in the DB. Column-wise updates will be made based on the result of self.what_changed(). If target_power_state is provided, it will be checked against the in-database copy of the node before updates are made. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) :raises: InvalidParameterValue if some property values are invalid. """ values = self.do_version_changes_for_db() self._validate_property_values(values.get('properties')) self._validate_and_remove_traits(values) self._validate_and_format_conductor_group(values) db_node = self.dbapi.create_node(values) self._from_db_object(self._context, self, db_node) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def destroy(self, context=None): """Delete the Node from the DB. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) """ self.dbapi.destroy_node(self.uuid) self.obj_reset_changes() # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def save(self, context=None): """Save updates to this Node. Column-wise updates will be made based on the result of self.what_changed(). If target_power_state is provided, it will be checked against the in-database copy of the node before updates are made. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) :raises: InvalidParameterValue if some property values are invalid. """ updates = self.do_version_changes_for_db() self._validate_property_values(updates.get('properties')) if 'driver' in updates and 'driver_internal_info' not in updates: # Clean driver_internal_info when changes driver self.driver_internal_info = {} updates = self.do_version_changes_for_db() self._validate_and_remove_traits(updates) self._validate_and_format_conductor_group(updates) db_node = self.dbapi.update_node(self.uuid, updates) self._from_db_object(self._context, self, db_node) @staticmethod def _validate_and_remove_traits(fields): """Validate traits in fields for create or update, remove if present. :param fields: a dict of Node fields for create or update. :raises: BadRequest if fields contains a traits that are not None. """ if 'traits' in fields: # NOTE(mgoddard): Traits should be updated via the node # object's traits field, which is itself an object. We shouldn't # get here with changes to traits, as this should be handled by the # API. When services are pinned to Pike, we can get here with # traits set to None in updates, due to changes made to the object # in _convert_to_version. if fields['traits']: # NOTE(mgoddard): We shouldn't get here as this should be # handled by the API. raise exception.BadRequest() fields.pop('traits') def _validate_and_format_conductor_group(self, fields): """Validate conductor_group and format it for our use. Currently formatting is just lowercasing it. :param fields: a dict of Node fields for create or update. :raises: InvalidConductorGroup if validation fails. """ if 'conductor_group' in fields: utils.validate_conductor_group(fields['conductor_group']) fields['conductor_group'] = fields['conductor_group'].lower() # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def refresh(self, context=None): """Refresh the object by re-fetching from the DB. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) """ current = self.get_by_uuid(self._context, self.uuid) self.obj_refresh(current) self.obj_reset_changes() # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def touch_provisioning(self, context=None): """Touch the database record to mark the provisioning as alive.""" self.dbapi.touch_node_provisioning(self.id) @classmethod def get_by_port_addresses(cls, context, addresses): """Get a node by associated port addresses. :param cls: the :class:`Node` :param context: Security context. :param addresses: A list of port addresses. :raises: NodeNotFound if the node is not found. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_port_addresses(addresses) node = cls._from_db_object(context, cls(), db_node) return node def _convert_fault_field(self, target_version, remove_unavailable_fields=True): fault_is_set = self.obj_attr_is_set('fault') if target_version >= (1, 25): if not fault_is_set: self.fault = None elif fault_is_set: if remove_unavailable_fields: delattr(self, 'fault') elif self.fault is not None: self.fault = None def _convert_deploy_step_field(self, target_version, remove_unavailable_fields=True): # NOTE(rloo): Typically we set the value to None. However, # deploy_step is a FlexibleDictField. Setting it to None # causes it to be set to {} under-the-hood. So I am being # explicit about that here. step_is_set = self.obj_attr_is_set('deploy_step') if target_version >= (1, 26): if not step_is_set: self.deploy_step = {} elif step_is_set: if remove_unavailable_fields: delattr(self, 'deploy_step') elif self.deploy_step: self.deploy_step = {} def _convert_conductor_group_field(self, target_version, remove_unavailable_fields=True): # NOTE(jroll): The default conductor_group is "", not None is_set = self.obj_attr_is_set('conductor_group') if target_version >= (1, 27): if not is_set: self.conductor_group = '' elif is_set: if remove_unavailable_fields: delattr(self, 'conductor_group') elif self.conductor_group: self.conductor_group = '' # NOTE (yolanda): new method created to avoid repeating code in # _convert_to_version, and to avoid pep8 too complex error def _adjust_field_to_version(self, field_name, field_default_value, target_version, major, minor, remove_unavailable_fields): field_is_set = self.obj_attr_is_set(field_name) if target_version >= (major, minor): # target version supports the major/minor specified if not field_is_set: # set it to its default value if it is not set setattr(self, field_name, field_default_value) elif field_is_set: # target version does not support the field, and it is set if remove_unavailable_fields: # (De)serialising: remove unavailable fields delattr(self, field_name) elif getattr(self, field_name) is not field_default_value: # DB: set unavailable field to the default value setattr(self, field_name, field_default_value) def _convert_to_version(self, target_version, remove_unavailable_fields=True): """Convert to the target version. Convert the object to the target version. The target version may be the same, older, or newer than the version of the object. This is used for DB interactions as well as for serialization/deserialization. Version 1.22: rescue_interface field was added. Its default value is None. For versions prior to this, it should be set to None (or removed). Version 1.23: traits field was added. Its default value is None. For versions prior to this, it should be set to None (or removed). Version 1.24: bios_interface field was added. Its default value is None. For versions prior to this, it should be set to None (or removed). Version 1.25: fault field was added. For versions prior to this, it should be removed. Version 1.26: deploy_step field was added. For versions prior to this, it should be removed. Version 1.27: conductor_group field was added. For versions prior to this, it should be removed. Version 1.28: automated_clean was added. For versions prior to this, it should be set to None (or removed). Version 1.29: protected was added. For versions prior to this, it should be set to False (or removed). :param target_version: the desired version of the object :param remove_unavailable_fields: True to remove fields that are unavailable in the target version; set this to True when (de)serializing. False to set the unavailable fields to appropriate values; set this to False for DB interactions. """ target_version = versionutils.convert_version_to_tuple(target_version) # Convert the different fields depending on version fields = [('rescue_interface', 22), ('traits', 23), ('bios_interface', 24), ('automated_clean', 28), ('protected_reason', 29)] for name, minor in fields: self._adjust_field_to_version(name, None, target_version, 1, minor, remove_unavailable_fields) # NOTE(dtantsur): the default is False for protected self._adjust_field_to_version('protected', False, target_version, 1, 29, remove_unavailable_fields) self._convert_fault_field(target_version, remove_unavailable_fields) self._convert_deploy_step_field(target_version, remove_unavailable_fields) self._convert_conductor_group_field(target_version, remove_unavailable_fields)
class NodePayload(notification.NotificationPayloadBase): """Base class used for all notification payloads about a Node object.""" # NOTE: This payload does not include the Node fields "chassis_id", # "driver_info", "driver_internal_info", "instance_info", "raid_config", # "reservation", or "target_raid_config". These were excluded for reasons # including: # - increased complexity needed for creating the payload # - sensitive information in the fields that shouldn't be exposed to # external services # - being internal-only or hardware-related fields SCHEMA = { 'clean_step': ('node', 'clean_step'), 'conductor_group': ('node', 'conductor_group'), 'console_enabled': ('node', 'console_enabled'), 'created_at': ('node', 'created_at'), 'deploy_step': ('node', 'deploy_step'), 'driver': ('node', 'driver'), 'extra': ('node', 'extra'), 'inspection_finished_at': ('node', 'inspection_finished_at'), 'inspection_started_at': ('node', 'inspection_started_at'), 'instance_uuid': ('node', 'instance_uuid'), 'last_error': ('node', 'last_error'), 'maintenance': ('node', 'maintenance'), 'maintenance_reason': ('node', 'maintenance_reason'), 'fault': ('node', 'fault'), 'name': ('node', 'name'), 'bios_interface': ('node', 'bios_interface'), 'boot_interface': ('node', 'boot_interface'), 'console_interface': ('node', 'console_interface'), 'deploy_interface': ('node', 'deploy_interface'), 'inspect_interface': ('node', 'inspect_interface'), 'management_interface': ('node', 'management_interface'), 'network_interface': ('node', 'network_interface'), 'power_interface': ('node', 'power_interface'), 'raid_interface': ('node', 'raid_interface'), 'rescue_interface': ('node', 'rescue_interface'), 'storage_interface': ('node', 'storage_interface'), 'vendor_interface': ('node', 'vendor_interface'), 'power_state': ('node', 'power_state'), 'properties': ('node', 'properties'), 'protected': ('node', 'protected'), 'protected_reason': ('node', 'protected_reason'), 'provision_state': ('node', 'provision_state'), 'provision_updated_at': ('node', 'provision_updated_at'), 'resource_class': ('node', 'resource_class'), 'target_power_state': ('node', 'target_power_state'), 'target_provision_state': ('node', 'target_provision_state'), 'updated_at': ('node', 'updated_at'), 'uuid': ('node', 'uuid') } # Version 1.0: Initial version, based off of Node version 1.18. # Version 1.1: Type of network_interface changed to just nullable string # similar to version 1.20 of Node. # Version 1.2: Add nullable to console_enabled and maintenance. # Version 1.3: Add dynamic interfaces fields exposed via API. # Version 1.4: Add storage interface field exposed via API. # Version 1.5: Add rescue interface field exposed via API. # Version 1.6: Add traits field exposed via API. # Version 1.7: Add fault field exposed via API. # Version 1.8: Add bios interface field exposed via API. # Version 1.9: Add deploy_step field exposed via API. # Version 1.10: Add conductor_group field exposed via API. # Version 1.11: Add protected and protected_reason fields exposed via API. VERSION = '1.11' fields = { 'clean_step': object_fields.FlexibleDictField(nullable=True), 'conductor_group': object_fields.StringField(nullable=True), 'console_enabled': object_fields.BooleanField(nullable=True), 'created_at': object_fields.DateTimeField(nullable=True), 'deploy_step': object_fields.FlexibleDictField(nullable=True), 'driver': object_fields.StringField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'inspection_finished_at': object_fields.DateTimeField(nullable=True), 'inspection_started_at': object_fields.DateTimeField(nullable=True), 'instance_uuid': object_fields.UUIDField(nullable=True), 'last_error': object_fields.StringField(nullable=True), 'maintenance': object_fields.BooleanField(nullable=True), 'maintenance_reason': object_fields.StringField(nullable=True), 'fault': object_fields.StringField(nullable=True), 'bios_interface': object_fields.StringField(nullable=True), 'boot_interface': object_fields.StringField(nullable=True), 'console_interface': object_fields.StringField(nullable=True), 'deploy_interface': object_fields.StringField(nullable=True), 'inspect_interface': object_fields.StringField(nullable=True), 'management_interface': object_fields.StringField(nullable=True), 'network_interface': object_fields.StringField(nullable=True), 'power_interface': object_fields.StringField(nullable=True), 'raid_interface': object_fields.StringField(nullable=True), 'rescue_interface': object_fields.StringField(nullable=True), 'storage_interface': object_fields.StringField(nullable=True), 'vendor_interface': object_fields.StringField(nullable=True), 'name': object_fields.StringField(nullable=True), 'power_state': object_fields.StringField(nullable=True), 'properties': object_fields.FlexibleDictField(nullable=True), 'protected': object_fields.BooleanField(nullable=True), 'protected_reason': object_fields.StringField(nullable=True), 'provision_state': object_fields.StringField(nullable=True), 'provision_updated_at': object_fields.DateTimeField(nullable=True), 'resource_class': object_fields.StringField(nullable=True), 'target_power_state': object_fields.StringField(nullable=True), 'target_provision_state': object_fields.StringField(nullable=True), 'traits': object_fields.ListOfStringsField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField() } def __init__(self, node, **kwargs): super(NodePayload, self).__init__(**kwargs) self.populate_schema(node=node) # NOTE(mgoddard): Populate traits with a list of trait names, rather # than the TraitList object. if node.obj_attr_is_set('traits') and node.traits is not None: self.traits = node.traits.get_trait_names() else: self.traits = []
class IronicObject(ovo_base.VersionedObjectDictCompat): """Base class and object factory. This forms the base of all objects that can be remoted or instantiated via RPC. Simply defining a class that inherits from this base class will make it remotely instantiatable. Objects should implement the necessary "get" classmethod routines as well as "save" object methods as appropriate. """ # Version of this object (see rules above check_object_version()) VERSION = '1.0' # The fields present in this object as key:typefn pairs. For example: # # fields = { 'foo': int, # 'bar': str, # 'baz': lambda x: str(x).ljust(8), # } # # NOTE(danms): The base IronicObject class' fields will be inherited # by subclasses, but that is a special case. Objects inheriting from # other objects will not receive this merging of fields contents. fields = { 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), } obj_extra_fields = [] _attr_created_at_from_primitive = obj_utils.dt_deserializer _attr_updated_at_from_primitive = obj_utils.dt_deserializer _attr_created_at_to_primitive = obj_utils.dt_serializer('created_at') _attr_updated_at_to_primitive = obj_utils.dt_serializer('updated_at') def __init__(self, context, **kwargs): self._changed_fields = set() self._context = context self.update(kwargs) @classmethod def obj_name(cls): """Get canonical object name. This object name will be used over the wire for remote hydration. """ return cls.__name__ @classmethod def obj_class_from_name(cls, objname, objver): """Returns a class from the registry based on a name and version.""" if objname not in cls._obj_classes: LOG.error( _LE('Unable to instantiate unregistered object type ' '%(objtype)s'), dict(objtype=objname)) raise exception.UnsupportedObjectError(objtype=objname) latest = None compatible_match = None for objclass in cls._obj_classes[objname]: if objclass.VERSION == objver: return objclass version_bits = tuple([int(x) for x in objclass.VERSION.split(".")]) if latest is None: latest = version_bits elif latest < version_bits: latest = version_bits if versionutils.is_compatible(objver, objclass.VERSION): compatible_match = objclass if compatible_match: return compatible_match latest_ver = '%i.%i' % latest raise exception.IncompatibleObjectVersion(objname=objname, objver=objver, supported=latest_ver) def _attr_from_primitive(self, attribute, value): """Attribute deserialization dispatcher. This calls self._attr_foo_from_primitive(value) for an attribute foo with value, if it exists, otherwise it assumes the value is suitable for the attribute's setter method. """ handler = '_attr_%s_from_primitive' % attribute if hasattr(self, handler): return getattr(self, handler)(value) return value @classmethod def _obj_from_primitive(cls, context, objver, primitive): self = cls(context) self.VERSION = objver objdata = primitive['ironic_object.data'] changes = primitive.get('ironic_object.changes', []) for name in self.fields: if name in objdata: setattr(self, name, self._attr_from_primitive(name, objdata[name])) self._changed_fields = set([x for x in changes if x in self.fields]) return self @classmethod def obj_from_primitive(cls, primitive, context=None): """Simple base-case hydration. This calls self._attr_from_primitive() for each item in fields. """ if primitive['ironic_object.namespace'] != 'ironic': # NOTE(danms): We don't do anything with this now, but it's # there for "the future" raise exception.UnsupportedObjectError( objtype='%s.%s' % (primitive['ironic_object.namespace'], primitive['ironic_object.name'])) objname = primitive['ironic_object.name'] objver = primitive['ironic_object.version'] objclass = cls.obj_class_from_name(objname, objver) return objclass._obj_from_primitive(context, objver, primitive) def __deepcopy__(self, memo): """Efficiently make a deep copy of this object.""" # NOTE(danms): A naive deepcopy would copy more than we need, # and since we have knowledge of the volatile bits of the # object, we can be smarter here. Also, nested entities within # some objects may be uncopyable, so we can avoid those sorts # of issues by copying only our field data. nobj = self.__class__(self._context) for name in self.fields: if self.obj_attr_is_set(name): nval = copy.deepcopy(getattr(self, name), memo) setattr(nobj, name, nval) nobj._changed_fields = set(self._changed_fields) return nobj def obj_clone(self): """Create a copy.""" return copy.deepcopy(self) def _attr_to_primitive(self, attribute): """Attribute serialization dispatcher. This calls self._attr_foo_to_primitive() for an attribute foo, if it exists, otherwise it assumes the attribute itself is primitive-enough to be sent over the RPC wire. """ handler = '_attr_%s_to_primitive' % attribute if hasattr(self, handler): return getattr(self, handler)() else: return getattr(self, attribute) def obj_to_primitive(self): """Simple base-case dehydration. This calls self._attr_to_primitive() for each item in fields. """ primitive = dict() for name in self.fields: if hasattr(self, get_attrname(name)): primitive[name] = self._attr_to_primitive(name) obj = { 'ironic_object.name': self.obj_name(), 'ironic_object.namespace': 'ironic', 'ironic_object.version': self.VERSION, 'ironic_object.data': primitive } if self.obj_what_changed(): obj['ironic_object.changes'] = list(self.obj_what_changed()) return obj def obj_load_attr(self, attrname): """Load an additional attribute from the real object. This should use self._conductor, and cache any data that might be useful for future load operations. """ raise NotImplementedError( _("Cannot load '%(attrname)s' in the base class") % {'attrname': attrname}) def save(self, context): """Save the changed fields back to the store. This is optional for subclasses, but is presented here in the base class for consistency among those that do. """ raise NotImplementedError(_("Cannot save anything in the base class")) def obj_get_changes(self): """Returns a dict of changed fields and their new values.""" changes = {} for key in self.obj_what_changed(): changes[key] = self[key] return changes def obj_what_changed(self): """Returns a set of fields that have been modified.""" return self._changed_fields def obj_reset_changes(self, fields=None): """Reset the list of fields that have been changed. Note that this is NOT "revert to previous values" """ if fields: self._changed_fields -= set(fields) else: self._changed_fields.clear() def obj_attr_is_set(self, attrname): """Test object to see if attrname is present. Returns True if the named attribute has a value set, or False if not. Raises AttributeError if attrname is not a valid attribute for this object. """ if attrname not in self.obj_fields: raise AttributeError( _("%(objname)s object has no attribute '%(attrname)s'") % { 'objname': self.obj_name(), 'attrname': attrname }) return hasattr(self, get_attrname(attrname)) @property def obj_fields(self): return list(self.fields) + self.obj_extra_fields def as_dict(self): return dict( (k, getattr(self, k)) for k in self.fields if hasattr(self, k))
class IronicObject(object_base.VersionedObject): """Base class and object factory. This forms the base of all objects that can be remoted or instantiated via RPC. Simply defining a class that inherits from this base class will make it remotely instantiatable. Objects should implement the necessary "get" classmethod routines as well as "save" object methods as appropriate. """ OBJ_SERIAL_NAMESPACE = 'ironic_object' OBJ_PROJECT_NAMESPACE = 'ironic' # TODO(lintan) Refactor these fields and create PersistentObject and # TimeStampObject like Nova when it is necessary. fields = { 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), } def as_dict(self): """Return the object represented as a dict. The returned object is JSON-serialisable. """ def _attr_as_dict(field): """Return an attribute as a dict, handling nested objects.""" attr = getattr(self, field) if isinstance(attr, IronicObject): attr = attr.as_dict() return attr return dict((k, _attr_as_dict(k)) for k in self.fields if self.obj_attr_is_set(k)) def obj_refresh(self, loaded_object): """Applies updates for objects that inherit from base.IronicObject. Checks for updated attributes in an object. Updates are applied from the loaded object column by column in comparison with the current object. """ for field in self.fields: if (self.obj_attr_is_set(field) and self[field] != loaded_object[field]): self[field] = loaded_object[field] def _convert_to_version(self, target_version, remove_unavailable_fields=True): """Convert to the target version. Subclasses should redefine this method, to do the conversion of the object to the target version. Convert the object to the target version. The target version may be the same, older, or newer than the version of the object. This is used for DB interactions as well as for serialization/deserialization. The remove_unavailable_fields flag is used to distinguish these two cases: 1) For serialization/deserialization, we need to remove the unavailable fields, because the service receiving the object may not know about these fields. remove_unavailable_fields is set to True in this case. 2) For DB interactions, we need to set the unavailable fields to their appropriate values so that these fields are saved in the DB. (If they are not set, the VersionedObject magic will not know to save/update them to the DB.) remove_unavailable_fields is set to False in this case. :param target_version: the desired version of the object :param remove_unavailable_fields: True to remove fields that are unavailable in the target version; set this to True when (de)serializing. False to set the unavailable fields to appropriate values; set this to False for DB interactions. """ pass def convert_to_version(self, target_version, remove_unavailable_fields=True): """Convert this object to the target version. Convert the object to the target version. The target version may be the same, older, or newer than the version of the object. This is used for DB interactions as well as for serialization/deserialization. The remove_unavailable_fields flag is used to distinguish these two cases: 1) For serialization/deserialization, we need to remove the unavailable fields, because the service receiving the object may not know about these fields. remove_unavailable_fields is set to True in this case. 2) For DB interactions, we need to set the unavailable fields to their appropriate values so that these fields are saved in the DB. (If they are not set, the VersionedObject magic will not know to save/update them to the DB.) remove_unavailable_fields is set to False in this case. _convert_to_version() does the actual work. :param target_version: the desired version of the object :param remove_unavailable_fields: True to remove fields that are unavailable in the target version; set this to True when (de)serializing. False to set the unavailable fields to appropriate values; set this to False for DB interactions. """ if self.VERSION != target_version: self._convert_to_version( target_version, remove_unavailable_fields=remove_unavailable_fields) if remove_unavailable_fields: # NOTE(rloo): We changed the object, but don't keep track of # any of these changes, since it is inaccurate anyway (because # it doesn't keep track of any 'changed' unavailable fields). self.obj_reset_changes() # NOTE(rloo): self.__class__.VERSION is the latest version that # is supported by this service. self.VERSION is the version of # this object instance -- it may get set via e.g. the # serialization or deserialization process, or here. if (self.__class__.VERSION != target_version or self.VERSION != self.__class__.VERSION): self.VERSION = target_version @classmethod def get_target_version(cls): """Returns the target version for this object. This is the version in which the object should be manipulated, e.g. sent over the wire via RPC or saved in the DB. :returns: if pinned, returns the version of this object corresponding to the pin. Otherwise, returns the version of the object. :raises: ovo_exception.IncompatibleObjectVersion """ pin = CONF.pin_release_version if not pin: return cls.VERSION version_manifest = versions.RELEASE_MAPPING[pin]['objects'] pinned_version = version_manifest.get(cls.obj_name()) if pinned_version: pinned_version = pinned_version[0] if not versionutils.is_compatible(pinned_version, cls.VERSION): LOG.error( 'For object "%(objname)s", the target version ' '"%(target)s" is not compatible with its supported ' 'version "%(support)s". The value ("%(pin)s") of the ' '"pin_release_version" configuration option may be ' 'incorrect.', {'objname': cls.obj_name(), 'target': pinned_version, 'support': cls.VERSION, 'pin': pin}) raise ovo_exception.IncompatibleObjectVersion( objname=cls.obj_name(), objver=pinned_version, supported=cls.VERSION) return pinned_version return cls.VERSION @classmethod def supports_version(cls, version): """Return whether this object supports a particular version. Check the requested version against the object's target version. The target version may not be the latest version during an upgrade, when object versions are pinned. :param version: A tuple representing the version to check :returns: Whether the version is supported :raises: ovo_exception.IncompatibleObjectVersion """ target_version = cls.get_target_version() target_version = versionutils.convert_version_to_tuple(target_version) return target_version >= version def _set_from_db_object(self, context, db_object, fields=None): """Sets object fields. :param context: security context :param db_object: A DB entity of the object :param fields: list of fields to set on obj from values from db_object. """ fields = fields or self.fields for field in fields: setattr(self, field, db_object[field]) @staticmethod def _from_db_object(context, obj, db_object, fields=None): """Converts a database entity to a formal object. This always converts the database entity to the latest version of the object. Note that the latest version is available at object.__class__.VERSION. object.VERSION is the version of this particular object instance; it is possible that it is not the latest version. :param context: security context :param obj: An object of the class. :param db_object: A DB entity of the object :param fields: list of fields to set on obj from values from db_object. :return: The object of the class with the database entity added :raises: ovo_exception.IncompatibleObjectVersion """ objname = obj.obj_name() db_version = db_object['version'] if not versionutils.is_compatible(db_version, obj.__class__.VERSION): raise ovo_exception.IncompatibleObjectVersion( objname=objname, objver=db_version, supported=obj.__class__.VERSION) obj._set_from_db_object(context, db_object, fields) obj._context = context # NOTE(rloo). We now have obj, a versioned object that corresponds to # its DB representation. A versioned object has an internal attribute # ._changed_fields; this is a list of changed fields -- used, e.g., # when saving the object to the DB (only those changed fields are # saved to the DB). The obj.obj_reset_changes() clears this list # since we didn't actually make any modifications to the object that # we want saved later. obj.obj_reset_changes() if db_version != obj.__class__.VERSION: # convert to the latest version obj.VERSION = db_version obj.convert_to_version(obj.__class__.VERSION, remove_unavailable_fields=False) return obj @classmethod def _from_db_object_list(cls, context, db_objects): """Returns objects corresponding to database entities. Returns a list of formal objects of this class that correspond to the list of database entities. :param cls: the VersionedObject class of the desired object :param context: security context :param db_objects: A list of DB models of the object :returns: A list of objects corresponding to the database entities """ return [cls._from_db_object(context, cls(), db_obj) for db_obj in db_objects] def do_version_changes_for_db(self): """Change the object to the version needed for the database. If needed, this changes the object (modifies object fields) to be in the correct version for saving to the database. The version used to save the object in the DB is determined as follows: * If the object is pinned, we save the object in the pinned version. Since it is pinned, we must not save in a newer version, in case a rolling upgrade is happening and some services are still using the older version of ironic, with no knowledge of this newer version. * If the object isn't pinned, we save the object in the latest version. Because the object may be converted to a different object version, this method must only be called just before saving the object to the DB. :returns: a dictionary of changed fields and their new values (could be an empty dictionary). These are the fields/values of the object that would be saved to the DB. """ target_version = self.get_target_version() if target_version != self.VERSION: # Convert the object so we can save it in the target version. self.convert_to_version(target_version, remove_unavailable_fields=False) changes = self.obj_get_changes() # NOTE(rloo): Since this object doesn't keep track of the version that # is saved in the DB and we don't want to make a DB call # just to find out, we always update 'version' in the DB. changes['version'] = self.VERSION return changes
class NodePayload(notification.NotificationPayloadBase): """Base class used for all notification payloads about a Node object.""" # NOTE: This payload does not include the Node fields "chassis_id", # "driver_info", "driver_internal_info", "instance_info", "raid_config", # "reservation", or "target_raid_config". These were excluded for reasons # including: # - increased complexity needed for creating the payload # - sensitive information in the fields that shouldn't be exposed to # external services # - being internal-only or hardware-related fields SCHEMA = { 'clean_step': ('node', 'clean_step'), 'console_enabled': ('node', 'console_enabled'), 'created_at': ('node', 'created_at'), 'driver': ('node', 'driver'), 'extra': ('node', 'extra'), 'inspection_finished_at': ('node', 'inspection_finished_at'), 'inspection_started_at': ('node', 'inspection_started_at'), 'instance_uuid': ('node', 'instance_uuid'), 'last_error': ('node', 'last_error'), 'maintenance': ('node', 'maintenance'), 'maintenance_reason': ('node', 'maintenance_reason'), 'name': ('node', 'name'), 'boot_interface': ('node', 'boot_interface'), 'console_interface': ('node', 'console_interface'), 'deploy_interface': ('node', 'deploy_interface'), 'inspect_interface': ('node', 'inspect_interface'), 'management_interface': ('node', 'management_interface'), 'network_interface': ('node', 'network_interface'), 'power_interface': ('node', 'power_interface'), 'raid_interface': ('node', 'raid_interface'), 'vendor_interface': ('node', 'vendor_interface'), 'power_state': ('node', 'power_state'), 'properties': ('node', 'properties'), 'provision_state': ('node', 'provision_state'), 'provision_updated_at': ('node', 'provision_updated_at'), 'resource_class': ('node', 'resource_class'), 'target_power_state': ('node', 'target_power_state'), 'target_provision_state': ('node', 'target_provision_state'), 'updated_at': ('node', 'updated_at'), 'uuid': ('node', 'uuid') } # TODO(TheJulia): At a later point in time, once storage_interfaces # are able to be leveraged, we need to add the sotrage_interface # field to payload and increment the object versions for all objects # that inherit the NodePayload object. # Version 1.0: Initial version, based off of Node version 1.18. # Version 1.1: Type of network_interface changed to just nullable string # similar to version 1.20 of Node. # Version 1.2: Add nullable to console_enabled and maintenance. # Version 1.3: Add dynamic interfaces fields exposed via API. VERSION = '1.3' fields = { 'clean_step': object_fields.FlexibleDictField(nullable=True), 'console_enabled': object_fields.BooleanField(nullable=True), 'created_at': object_fields.DateTimeField(nullable=True), 'driver': object_fields.StringField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'inspection_finished_at': object_fields.DateTimeField(nullable=True), 'inspection_started_at': object_fields.DateTimeField(nullable=True), 'instance_uuid': object_fields.UUIDField(nullable=True), 'last_error': object_fields.StringField(nullable=True), 'maintenance': object_fields.BooleanField(nullable=True), 'maintenance_reason': object_fields.StringField(nullable=True), 'boot_interface': object_fields.StringField(nullable=True), 'console_interface': object_fields.StringField(nullable=True), 'deploy_interface': object_fields.StringField(nullable=True), 'inspect_interface': object_fields.StringField(nullable=True), 'management_interface': object_fields.StringField(nullable=True), 'network_interface': object_fields.StringField(nullable=True), 'power_interface': object_fields.StringField(nullable=True), 'raid_interface': object_fields.StringField(nullable=True), 'vendor_interface': object_fields.StringField(nullable=True), 'name': object_fields.StringField(nullable=True), 'power_state': object_fields.StringField(nullable=True), 'properties': object_fields.FlexibleDictField(nullable=True), 'provision_state': object_fields.StringField(nullable=True), 'provision_updated_at': object_fields.DateTimeField(nullable=True), 'resource_class': object_fields.StringField(nullable=True), 'target_power_state': object_fields.StringField(nullable=True), 'target_provision_state': object_fields.StringField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField() } def __init__(self, node, **kwargs): super(NodePayload, self).__init__(**kwargs) self.populate_schema(node=node)
class Node(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version # Version 1.1: Added instance_info # Version 1.2: Add get() and get_by_id() and make get_by_uuid() # only work with a uuid # Version 1.3: Add create() and destroy() # Version 1.4: Add get_by_instance_uuid() # Version 1.5: Add list() # Version 1.6: Add reserve() and release() # Version 1.7: Add conductor_affinity # Version 1.8: Add maintenance_reason # Version 1.9: Add driver_internal_info # Version 1.10: Add name and get_by_name() # Version 1.11: Add clean_step # Version 1.12: Add raid_config and target_raid_config # Version 1.13: Add touch_provisioning() # Version 1.14: Add _validate_property_values() and make create() # and save() validate the input of property values. # Version 1.15: Add get_by_port_addresses # Version 1.16: Add network_interface field # Version 1.17: Add resource_class field # Version 1.18: Add default setting for network_interface # Version 1.19: Add fields: boot_interface, console_interface, # deploy_interface, inspect_interface, management_interface, # power_interface, raid_interface, vendor_interface # Version 1.20: Type of network_interface changed to just nullable string # Version 1.21: Add storage_interface field VERSION = '1.21' dbapi = db_api.get_instance() fields = { 'id': object_fields.IntegerField(), 'uuid': object_fields.UUIDField(nullable=True), 'name': object_fields.StringField(nullable=True), 'chassis_id': object_fields.IntegerField(nullable=True), 'instance_uuid': object_fields.UUIDField(nullable=True), 'driver': object_fields.StringField(nullable=True), 'driver_info': object_fields.FlexibleDictField(nullable=True), 'driver_internal_info': object_fields.FlexibleDictField(nullable=True), # A clean step dictionary, indicating the current clean step # being executed, or None, indicating cleaning is not in progress # or has not yet started. 'clean_step': object_fields.FlexibleDictField(nullable=True), 'raid_config': object_fields.FlexibleDictField(nullable=True), 'target_raid_config': object_fields.FlexibleDictField(nullable=True), 'instance_info': object_fields.FlexibleDictField(nullable=True), 'properties': object_fields.FlexibleDictField(nullable=True), 'reservation': object_fields.StringField(nullable=True), # a reference to the id of the conductor service, not its hostname, # that has most recently performed some action which could require # local state to be maintained (eg, built a PXE config) 'conductor_affinity': object_fields.IntegerField(nullable=True), # One of states.POWER_ON|POWER_OFF|NOSTATE|ERROR 'power_state': object_fields.StringField(nullable=True), # Set to one of states.POWER_ON|POWER_OFF when a power operation # starts, and set to NOSTATE when the operation finishes # (successfully or unsuccessfully). 'target_power_state': object_fields.StringField(nullable=True), 'provision_state': object_fields.StringField(nullable=True), 'provision_updated_at': object_fields.DateTimeField(nullable=True), 'target_provision_state': object_fields.StringField(nullable=True), 'maintenance': object_fields.BooleanField(), 'maintenance_reason': object_fields.StringField(nullable=True), 'console_enabled': object_fields.BooleanField(), # Any error from the most recent (last) asynchronous transaction # that started but failed to finish. 'last_error': object_fields.StringField(nullable=True), # Used by nova to relate the node to a flavor 'resource_class': object_fields.StringField(nullable=True), 'inspection_finished_at': object_fields.DateTimeField(nullable=True), 'inspection_started_at': object_fields.DateTimeField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'boot_interface': object_fields.StringField(nullable=True), 'console_interface': object_fields.StringField(nullable=True), 'deploy_interface': object_fields.StringField(nullable=True), 'inspect_interface': object_fields.StringField(nullable=True), 'management_interface': object_fields.StringField(nullable=True), 'network_interface': object_fields.StringField(nullable=True), 'power_interface': object_fields.StringField(nullable=True), 'raid_interface': object_fields.StringField(nullable=True), 'storage_interface': object_fields.StringField(nullable=True), 'vendor_interface': object_fields.StringField(nullable=True), } def _validate_property_values(self, properties): """Check if the input of local_gb, cpus and memory_mb are valid. :param properties: a dict contains the node's information. """ if not properties: return invalid_msgs_list = [] for param in REQUIRED_INT_PROPERTIES: value = properties.get(param) if value is None: continue try: int_value = int(value) assert int_value >= 0 except (ValueError, AssertionError): msg = (('%(param)s=%(value)s') % {'param': param, 'value': value}) invalid_msgs_list.append(msg) if invalid_msgs_list: msg = (_('The following properties for node %(node)s ' 'should be non-negative integers, ' 'but provided values are: %(msgs)s') % {'node': self.uuid, 'msgs': ', '.join(invalid_msgs_list)}) raise exception.InvalidParameterValue(msg) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get(cls, context, node_id): """Find a node based on its id or uuid and return a Node object. :param node_id: the id *or* uuid of a node. :returns: a :class:`Node` object. """ if strutils.is_int_like(node_id): return cls.get_by_id(context, node_id) elif uuidutils.is_uuid_like(node_id): return cls.get_by_uuid(context, node_id) else: raise exception.InvalidIdentity(identity=node_id) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_id(cls, context, node_id): """Find a node based on its integer ID and return a Node object. :param cls: the :class:`Node` :param context: Security context :param node_id: the ID of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_id(node_id) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_uuid(cls, context, uuid): """Find a node based on UUID and return a Node object. :param cls: the :class:`Node` :param context: Security context :param uuid: the UUID of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_uuid(uuid) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_name(cls, context, name): """Find a node based on name and return a Node object. :param cls: the :class:`Node` :param context: Security context :param name: the logical name of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_name(name) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_instance_uuid(cls, context, instance_uuid): """Find a node based on the instance UUID and return a Node object. :param cls: the :class:`Node` :param context: Security context :param uuid: the UUID of the instance. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_instance(instance_uuid) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def list(cls, context, limit=None, marker=None, sort_key=None, sort_dir=None, filters=None): """Return a list of Node objects. :param cls: the :class:`Node` :param context: Security context. :param limit: maximum number of resources to return in a single result. :param marker: pagination marker for large data sets. :param sort_key: column to sort results by. :param sort_dir: direction to sort. "asc" or "desc". :param filters: Filters to apply. :returns: a list of :class:`Node` object. """ db_nodes = cls.dbapi.get_node_list(filters=filters, limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir) return cls._from_db_object_list(context, db_nodes) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def reserve(cls, context, tag, node_id): """Get and reserve a node. To prevent other ManagerServices from manipulating the given Node while a Task is performed, mark it reserved by this host. :param cls: the :class:`Node` :param context: Security context. :param tag: A string uniquely identifying the reservation holder. :param node_id: A node ID or UUID. :raises: NodeNotFound if the node is not found. :returns: a :class:`Node` object. """ db_node = cls.dbapi.reserve_node(tag, node_id) node = cls._from_db_object(context, cls(), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def release(cls, context, tag, node_id): """Release the reservation on a node. :param context: Security context. :param tag: A string uniquely identifying the reservation holder. :param node_id: A node id or uuid. :raises: NodeNotFound if the node is not found. """ cls.dbapi.release_node(tag, node_id) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def create(self, context=None): """Create a Node record in the DB. Column-wise updates will be made based on the result of self.what_changed(). If target_power_state is provided, it will be checked against the in-database copy of the node before updates are made. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) :raises: InvalidParameterValue if some property values are invalid. """ values = self.obj_get_changes() self._validate_property_values(values.get('properties')) db_node = self.dbapi.create_node(values) self._from_db_object(self._context, self, db_node) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def destroy(self, context=None): """Delete the Node from the DB. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) """ self.dbapi.destroy_node(self.uuid) self.obj_reset_changes() # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def save(self, context=None): """Save updates to this Node. Column-wise updates will be made based on the result of self.what_changed(). If target_power_state is provided, it will be checked against the in-database copy of the node before updates are made. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) :raises: InvalidParameterValue if some property values are invalid. """ updates = self.obj_get_changes() self._validate_property_values(updates.get('properties')) if 'driver' in updates and 'driver_internal_info' not in updates: # Clean driver_internal_info when changes driver self.driver_internal_info = {} updates = self.obj_get_changes() db_node = self.dbapi.update_node(self.uuid, updates) self._from_db_object(self._context, self, db_node) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def refresh(self, context=None): """Refresh the object by re-fetching from the DB. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) """ current = self.get_by_uuid(self._context, self.uuid) self.obj_refresh(current) self.obj_reset_changes() # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def touch_provisioning(self, context=None): """Touch the database record to mark the provisioning as alive.""" self.dbapi.touch_node_provisioning(self.id) @classmethod def get_by_port_addresses(cls, context, addresses): """Get a node by associated port addresses. :param cls: the :class:`Node` :param context: Security context. :param addresses: A list of port addresses. :raises: NodeNotFound if the node is not found. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_port_addresses(addresses) node = cls._from_db_object(context, cls(), db_node) return node
class Node(base.IronicObject, object_base.VersionedObjectDictCompat): # Version 1.0: Initial version # Version 1.1: Added instance_info # Version 1.2: Add get() and get_by_id() and make get_by_uuid() # only work with a uuid # Version 1.3: Add create() and destroy() # Version 1.4: Add get_by_instance_uuid() # Version 1.5: Add list() # Version 1.6: Add reserve() and release() # Version 1.7: Add conductor_affinity # Version 1.8: Add maintenance_reason # Version 1.9: Add driver_internal_info # Version 1.10: Add name and get_by_name() # Version 1.11: Add clean_step # Version 1.12: Add raid_config and target_raid_config # Version 1.13: Add touch_provisioning() VERSION = '1.13' dbapi = db_api.get_instance() fields = { 'id': object_fields.IntegerField(), 'uuid': object_fields.UUIDField(nullable=True), 'name': object_fields.StringField(nullable=True), 'chassis_id': object_fields.IntegerField(nullable=True), 'instance_uuid': object_fields.UUIDField(nullable=True), 'driver': object_fields.StringField(nullable=True), 'driver_info': object_fields.FlexibleDictField(nullable=True), 'driver_internal_info': object_fields.FlexibleDictField(nullable=True), # A clean step dictionary, indicating the current clean step # being executed, or None, indicating cleaning is not in progress # or has not yet started. 'clean_step': object_fields.FlexibleDictField(nullable=True), 'raid_config': object_fields.FlexibleDictField(nullable=True), 'target_raid_config': object_fields.FlexibleDictField(nullable=True), 'instance_info': object_fields.FlexibleDictField(nullable=True), 'properties': object_fields.FlexibleDictField(nullable=True), 'reservation': object_fields.StringField(nullable=True), # a reference to the id of the conductor service, not its hostname, # that has most recently performed some action which could require # local state to be maintained (eg, built a PXE config) 'conductor_affinity': object_fields.IntegerField(nullable=True), # One of states.POWER_ON|POWER_OFF|NOSTATE|ERROR 'power_state': object_fields.StringField(nullable=True), # Set to one of states.POWER_ON|POWER_OFF when a power operation # starts, and set to NOSTATE when the operation finishes # (successfully or unsuccessfully). 'target_power_state': object_fields.StringField(nullable=True), 'provision_state': object_fields.StringField(nullable=True), 'provision_updated_at': object_fields.DateTimeField(nullable=True), 'target_provision_state': object_fields.StringField(nullable=True), 'maintenance': object_fields.BooleanField(), 'maintenance_reason': object_fields.StringField(nullable=True), 'console_enabled': object_fields.BooleanField(), # Any error from the most recent (last) asynchronous transaction # that started but failed to finish. 'last_error': object_fields.StringField(nullable=True), 'inspection_finished_at': object_fields.DateTimeField(nullable=True), 'inspection_started_at': object_fields.DateTimeField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), } @staticmethod def _from_db_object(node, db_node): """Converts a database entity to a formal object.""" for field in node.fields: node[field] = db_node[field] node.obj_reset_changes() return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get(cls, context, node_id): """Find a node based on its id or uuid and return a Node object. :param node_id: the id *or* uuid of a node. :returns: a :class:`Node` object. """ if strutils.is_int_like(node_id): return cls.get_by_id(context, node_id) elif uuidutils.is_uuid_like(node_id): return cls.get_by_uuid(context, node_id) else: raise exception.InvalidIdentity(identity=node_id) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_id(cls, context, node_id): """Find a node based on its integer id and return a Node object. :param node_id: the id of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_id(node_id) node = Node._from_db_object(cls(context), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_uuid(cls, context, uuid): """Find a node based on uuid and return a Node object. :param uuid: the uuid of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_uuid(uuid) node = Node._from_db_object(cls(context), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_name(cls, context, name): """Find a node based on name and return a Node object. :param name: the logical name of a node. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_name(name) node = Node._from_db_object(cls(context), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def get_by_instance_uuid(cls, context, instance_uuid): """Find a node based on the instance uuid and return a Node object. :param uuid: the uuid of the instance. :returns: a :class:`Node` object. """ db_node = cls.dbapi.get_node_by_instance(instance_uuid) node = Node._from_db_object(cls(context), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def list(cls, context, limit=None, marker=None, sort_key=None, sort_dir=None, filters=None): """Return a list of Node objects. :param context: Security context. :param limit: maximum number of resources to return in a single result. :param marker: pagination marker for large data sets. :param sort_key: column to sort results by. :param sort_dir: direction to sort. "asc" or "desc". :param filters: Filters to apply. :returns: a list of :class:`Node` object. """ db_nodes = cls.dbapi.get_node_list(filters=filters, limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir) return [Node._from_db_object(cls(context), obj) for obj in db_nodes] # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def reserve(cls, context, tag, node_id): """Get and reserve a node. To prevent other ManagerServices from manipulating the given Node while a Task is performed, mark it reserved by this host. :param context: Security context. :param tag: A string uniquely identifying the reservation holder. :param node_id: A node id or uuid. :raises: NodeNotFound if the node is not found. :returns: a :class:`Node` object. """ db_node = cls.dbapi.reserve_node(tag, node_id) node = Node._from_db_object(cls(context), db_node) return node # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable_classmethod @classmethod def release(cls, context, tag, node_id): """Release the reservation on a node. :param context: Security context. :param tag: A string uniquely identifying the reservation holder. :param node_id: A node id or uuid. :raises: NodeNotFound if the node is not found. """ cls.dbapi.release_node(tag, node_id) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def create(self, context=None): """Create a Node record in the DB. Column-wise updates will be made based on the result of self.what_changed(). If target_power_state is provided, it will be checked against the in-database copy of the node before updates are made. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) """ values = self.obj_get_changes() db_node = self.dbapi.create_node(values) self._from_db_object(self, db_node) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def destroy(self, context=None): """Delete the Node from the DB. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) """ self.dbapi.destroy_node(self.uuid) self.obj_reset_changes() # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def save(self, context=None): """Save updates to this Node. Column-wise updates will be made based on the result of self.what_changed(). If target_power_state is provided, it will be checked against the in-database copy of the node before updates are made. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) """ updates = self.obj_get_changes() if 'driver' in updates and 'driver_internal_info' not in updates: # Clean driver_internal_info when changes driver self.driver_internal_info = {} updates = self.obj_get_changes() self.dbapi.update_node(self.uuid, updates) self.obj_reset_changes() # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def refresh(self, context=None): """Refresh the object by re-fetching from the DB. :param context: Security context. NOTE: This should only be used internally by the indirection_api. Unfortunately, RPC requires context as the first argument, even though we don't use it. A context should be set when instantiating the object, e.g.: Node(context) """ current = self.__class__.get_by_uuid(self._context, self.uuid) self.obj_refresh(current) # NOTE(xek): We don't want to enable RPC on this call just yet. Remotable # methods can be used in the future to replace current explicit RPC calls. # Implications of calling new remote procedures should be thought through. # @object_base.remotable def touch_provisioning(self, context=None): """Touch the database record to mark the provisioning as alive.""" self.dbapi.touch_node_provisioning(self.id)
class IronicObject(object_base.VersionedObject): """Base class and object factory. This forms the base of all objects that can be remoted or instantiated via RPC. Simply defining a class that inherits from this base class will make it remotely instantiatable. Objects should implement the necessary "get" classmethod routines as well as "save" object methods as appropriate. """ OBJ_SERIAL_NAMESPACE = 'ironic_object' OBJ_PROJECT_NAMESPACE = 'ironic' # TODO(lintan) Refactor these fields and create PersistentObject and # TimeStampObject like Nova when it is necessary. fields = { 'created_at': object_fields.DateTimeField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), } def as_dict(self): return dict( (k, getattr(self, k)) for k in self.fields if hasattr(self, k)) def obj_refresh(self, loaded_object): """Applies updates for objects that inherit from base.IronicObject. Checks for updated attributes in an object. Updates are applied from the loaded object column by column in comparison with the current object. """ for field in self.fields: if (self.obj_attr_is_set(field) and self[field] != loaded_object[field]): self[field] = loaded_object[field] def _convert_to_version(self, target_version): """Convert to the target version. Subclasses should redefine this method, to do the conversion of the object to the specified version. As a result of any conversion, the object changes (self.obj_what_changed()) should be retained. :param target_version: the desired version of the object """ pass def convert_to_version(self, target_version): """Convert this object to the target version. _convert_to_version() does the actual work. :param target_version: the desired version of the object """ self._convert_to_version(target_version) # NOTE(rloo): self.__class__.VERSION is the latest version that # is supported by this service. self.VERSION is the version of # this object instance -- it may get set via e.g. the # serialization or deserialization process, or here. if (self.__class__.VERSION != target_version or self.VERSION != self.__class__.VERSION): self.VERSION = target_version def get_target_version(self): """Returns the target version for this object. This is the version in which the object should be manipulated, e.g. sent over the wire via RPC or saved in the DB. :returns: if pinned, returns the version of this object corresponding to the pin. Otherwise, returns the version of the object. :raises: ovo_exception.IncompatibleObjectVersion """ pin = CONF.pin_release_version if not pin: return self.__class__.VERSION version_manifest = versions.RELEASE_MAPPING[pin]['objects'] pinned_version = version_manifest.get(self.obj_name()) if pinned_version: if not versionutils.is_compatible(pinned_version, self.__class__.VERSION): LOG.error( 'For object "%(objname)s", the target version ' '"%(target)s" is not compatible with its supported ' 'version "%(support)s". The value ("%(pin)s") of the ' '"pin_release_version" configuration option may be ' 'incorrect.', { 'objname': self.obj_name(), 'target': pinned_version, 'support': self.__class__.VERSION, 'pin': pin }) raise ovo_exception.IncompatibleObjectVersion( objname=self.obj_name(), objver=pinned_version, supported=self.__class__.VERSION) return pinned_version return self.__class__.VERSION def _set_from_db_object(self, context, db_object, fields=None): """Sets object fields. :param context: security context :param db_object: A DB entity of the object :param fields: list of fields to set on obj from values from db_object. """ fields = fields or self.fields for field in fields: self[field] = db_object[field] @staticmethod def _from_db_object(context, obj, db_object, fields=None): """Converts a database entity to a formal object. This always converts the database entity to the latest version of the object. Note that the latest version is available at object.__class__.VERSION. object.VERSION is the version of this particular object instance; it is possible that it is not the latest version. :param context: security context :param obj: An object of the class. :param db_object: A DB entity of the object :param fields: list of fields to set on obj from values from db_object. :return: The object of the class with the database entity added :raises: ovo_exception.IncompatibleObjectVersion """ objname = obj.obj_name() db_version = db_object['version'] if db_version is None: # NOTE(rloo): This can only happen after we've updated the DB # tables to include the 'version' column but haven't saved the # object to the DB since the new column was added. This column is # added in the Pike cycle, so if the version isn't set, use the # version associated with the most recent release, i.e. '8.0'. # The objects and RPC versions haven't changed between '8.0' and # Ocata, which is why it is fine to use Ocata. # Furthermore, if this is a new object that did not exist in the # most recent release, we assume it is version 1.0. # TODO(rloo): This entire if clause can be deleted in Queens # since the dbsync online migration populates all the versions # and it must be run to completion before upgrading to Queens. db_version = versions.RELEASE_MAPPING['ocata']['objects'].get( objname, '1.0') if not versionutils.is_compatible(db_version, obj.__class__.VERSION): raise ovo_exception.IncompatibleObjectVersion( objname=objname, objver=db_version, supported=obj.__class__.VERSION) obj._set_from_db_object(context, db_object, fields) obj._context = context # NOTE(rloo). We now have obj, a versioned object that corresponds to # its DB representation. A versioned object has an internal attribute # ._changed_fields; this is a list of changed fields -- used, e.g., # when saving the object to the DB (only those changed fields are # saved to the DB). The obj.obj_reset_changes() clears this list # since we didn't actually make any modifications to the object that # we want saved later. obj.obj_reset_changes() if db_version != obj.__class__.VERSION: # convert to the latest version obj.convert_to_version(obj.__class__.VERSION) if obj.get_target_version() == db_version: # pinned, so no need to keep these changes (we'll end up # converting back to db_version if obj is saved) obj.obj_reset_changes() else: # keep these changes around because they are needed # when/if saving to the DB in the latest version pass return obj @classmethod def _from_db_object_list(cls, context, db_objects): """Returns objects corresponding to database entities. Returns a list of formal objects of this class that correspond to the list of database entities. :param cls: the VersionedObject class of the desired object :param context: security context :param db_objects: A list of DB models of the object :returns: A list of objects corresponding to the database entities """ return [ cls._from_db_object(context, cls(), db_obj) for db_obj in db_objects ] def do_version_changes_for_db(self): """Change the object to the version needed for the database. If needed, this changes the object (modifies object fields) to be in the correct version for saving to the database. The version used to save the object in the DB is determined as follows: * If the object is pinned, we save the object in the pinned version. Since it is pinned, we must not save in a newer version, in case a rolling upgrade is happening and some services are still using the older version of ironic, with no knowledge of this newer version. * If the object isn't pinned, we save the object in the latest version. Because the object may be converted to a different object version, this method must only be called just before saving the object to the DB. :returns: a dictionary of changed fields and their new values (could be an empty dictionary). These are the fields/values of the object that would be saved to the DB. """ target_version = self.get_target_version() if target_version != self.VERSION: # Convert the object so we can save it in the target version. self.convert_to_version(target_version) db_version = target_version else: db_version = self.VERSION changes = self.obj_get_changes() # NOTE(rloo): Since this object doesn't keep track of the version that # is saved in the DB and we don't want to make a DB call # just to find out, we always update 'version' in the DB. changes['version'] = db_version return changes
class NodePayload(notification.NotificationPayloadBase): """Base class used for all notification payloads about a Node object.""" # NOTE: This payload does not include the Node fields "chassis_id", # "driver_info", "driver_internal_info", "instance_info", "raid_config", # "reservation", or "target_raid_config". These were excluded for reasons # including: # - increased complexity needed for creating the payload # - sensitive information in the fields that shouldn't be exposed to # external services # - being internal-only or hardware-related fields SCHEMA = { 'clean_step': ('node', 'clean_step'), 'console_enabled': ('node', 'console_enabled'), 'created_at': ('node', 'created_at'), 'driver': ('node', 'driver'), 'extra': ('node', 'extra'), 'inspection_finished_at': ('node', 'inspection_finished_at'), 'inspection_started_at': ('node', 'inspection_started_at'), 'instance_uuid': ('node', 'instance_uuid'), 'last_error': ('node', 'last_error'), 'maintenance': ('node', 'maintenance'), 'maintenance_reason': ('node', 'maintenance_reason'), 'name': ('node', 'name'), 'network_interface': ('node', 'network_interface'), 'power_state': ('node', 'power_state'), 'properties': ('node', 'properties'), 'provision_state': ('node', 'provision_state'), 'provision_updated_at': ('node', 'provision_updated_at'), 'resource_class': ('node', 'resource_class'), 'target_power_state': ('node', 'target_power_state'), 'target_provision_state': ('node', 'target_provision_state'), 'updated_at': ('node', 'updated_at'), 'uuid': ('node', 'uuid') } # Version 1.0: Initial version, based off of Node version 1.18. VERSION = '1.0' fields = { 'clean_step': object_fields.FlexibleDictField(nullable=True), 'console_enabled': object_fields.BooleanField(), 'created_at': object_fields.DateTimeField(nullable=True), 'driver': object_fields.StringField(nullable=True), 'extra': object_fields.FlexibleDictField(nullable=True), 'inspection_finished_at': object_fields.DateTimeField(nullable=True), 'inspection_started_at': object_fields.DateTimeField(nullable=True), 'instance_uuid': object_fields.UUIDField(nullable=True), 'last_error': object_fields.StringField(nullable=True), 'maintenance': object_fields.BooleanField(), 'maintenance_reason': object_fields.StringField(nullable=True), 'network_interface': object_fields.StringFieldThatAcceptsCallable(), 'name': object_fields.StringField(nullable=True), 'power_state': object_fields.StringField(nullable=True), 'properties': object_fields.FlexibleDictField(nullable=True), 'provision_state': object_fields.StringField(nullable=True), 'provision_updated_at': object_fields.DateTimeField(nullable=True), 'resource_class': object_fields.StringField(nullable=True), 'target_power_state': object_fields.StringField(nullable=True), 'target_provision_state': object_fields.StringField(nullable=True), 'updated_at': object_fields.DateTimeField(nullable=True), 'uuid': object_fields.UUIDField() } def __init__(self, node, **kwargs): super(NodePayload, self).__init__(**kwargs) self.populate_schema(node=node)