Exemple #1
0
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)
Exemple #2
0
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)
Exemple #3
0
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)
Exemple #4
0
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)
Exemple #5
0
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)
Exemple #6
0
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)
Exemple #8
0
 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)
Exemple #9
0
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)
Exemple #10
0
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)
Exemple #11
0
 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)
Exemple #12
0
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
        ]
Exemple #13
0
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)
Exemple #14
0
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 = []
Exemple #15
0
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))
Exemple #16
0
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
Exemple #17
0
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)
Exemple #18
0
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
Exemple #19
0
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)
Exemple #20
0
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
Exemple #21
0
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)