object: Introduce Resource and ResourceList objs

Introduce Resource and ResourceList objects, and add new resources
field to instance, also old_/new_ resources fields to migration_context,
Resource object can contain a generic resource information.

Change-Id: I095bb71d2a5e451f2827f646aa5b3ed31d2ccac3
Partially-Implements: blueprint virtual-persistent-memory
Co-Authored-By: He Jie Xu <hejie.xu@intel.com>
This commit is contained in:
LuyaoZhong 2019-09-11 04:49:00 +00:00
parent 6a4d6ec786
commit 384d783fc6
11 changed files with 301 additions and 10 deletions

View File

@ -65,6 +65,7 @@ def register_all():
__import__('nova.objects.request_spec')
__import__('nova.objects.tag')
__import__('nova.objects.quotas')
__import__('nova.objects.resource')
__import__('nova.objects.security_group')
__import__('nova.objects.security_group_rule')
__import__('nova.objects.selection')

View File

@ -31,6 +31,23 @@ from nova.objects import fields as obj_fields
from nova import utils
def all_things_equal(obj_a, obj_b):
if obj_b is None:
return False
for name in obj_a.fields:
set_a = name in obj_a
set_b = name in obj_b
if set_a != set_b:
return False
elif not set_a:
continue
if getattr(obj_a, name) != getattr(obj_b, name):
return False
return True
def get_attrname(name):
"""Return the mangled name of the attribute's underlying storage."""
# FIXME(danms): This is just until we use o.vo's class properties

View File

@ -90,6 +90,24 @@ IPV4Network = fields.IPV4Network
IPV6Network = fields.IPV6Network
class ResourceClass(fields.StringPattern):
PATTERN = r"^[A-Z0-9_]+$"
_REGEX = re.compile(PATTERN)
@staticmethod
def coerce(obj, attr, value):
if isinstance(value, six.string_types):
uppered = value.upper()
if ResourceClass._REGEX.match(uppered):
return uppered
raise ValueError(_("Malformed Resource Class %s") % value)
class ResourceClassField(AutoTypedField):
AUTO_TYPE = ResourceClass()
class SetOfStringsField(AutoTypedField):
AUTO_TYPE = Set(fields.String())

View File

@ -56,10 +56,11 @@ _INSTANCE_OPTIONAL_NON_COLUMN_FIELDS = ['flavor', 'old_flavor',
# These are fields that are optional and in instance_extra
_INSTANCE_EXTRA_FIELDS = ['numa_topology', 'pci_requests',
'flavor', 'vcpu_model', 'migration_context',
'keypairs', 'device_metadata', 'trusted_certs']
'keypairs', 'device_metadata', 'trusted_certs',
'resources']
# These are fields that applied/drooped by migration_context
_MIGRATION_CONTEXT_ATTRS = ['numa_topology', 'pci_requests',
'pci_devices']
'pci_devices', 'resources']
# These are fields that can be specified as expected_attrs
INSTANCE_OPTIONAL_ATTRS = (_INSTANCE_OPTIONAL_JOINED_FIELDS +
@ -114,7 +115,8 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
# Version 2.4: Added trusted_certs
# Version 2.5: Added hard_delete kwarg in destroy
# Version 2.6: Added hidden
VERSION = '2.6'
# Version 2.7: Added resources
VERSION = '2.7'
fields = {
'id': fields.IntegerField(),
@ -217,6 +219,7 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
'keypairs': fields.ObjectField('KeyPairList'),
'trusted_certs': fields.ObjectField('TrustedCerts', nullable=True),
'hidden': fields.BooleanField(default=False),
'resources': fields.ObjectField('ResourceList', nullable=True),
}
obj_extra_fields = ['name']
@ -224,6 +227,8 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
def obj_make_compatible(self, primitive, target_version):
super(Instance, self).obj_make_compatible(primitive, target_version)
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (2, 7) and 'resources' in primitive:
del primitive['resources']
if target_version < (2, 6) and 'hidden' in primitive:
del primitive['hidden']
if target_version < (2, 4) and 'trusted_certs' in primitive:
@ -487,6 +492,12 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
db_inst['extra'].get('trusted_certs'))
else:
instance.trusted_certs = None
if 'resources' in expected_attrs:
if have_extra:
instance._load_resources(
db_inst['extra'].get('resources'))
else:
instance.resources = None
if any([x in expected_attrs for x in ('flavor',
'old_flavor',
'new_flavor')]):
@ -601,6 +612,13 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
trusted_certs.obj_to_primitive())
else:
updates['extra']['trusted_certs'] = None
resources = updates.pop('resources', None)
expected_attrs.append('resources')
if resources:
updates['extra']['resources'] = jsonutils.dumps(
resources.obj_to_primitive())
else:
updates['extra']['resources'] = None
db_inst = db.instance_create(self._context, updates)
self._from_db_object(self._context, self, db_inst, expected_attrs)
@ -983,6 +1001,16 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
self.trusted_certs = objects.TrustedCerts.obj_from_primitive(
jsonutils.loads(db_trusted_certs))
def _load_resources(self, db_resources=_NO_DATA_SENTINEL):
if db_resources is None:
self.resources = None
elif db_resources is _NO_DATA_SENTINEL:
self.resources = objects.ResourceList.get_by_instance_uuid(
self._context, self.uuid)
else:
self.resources = objects.ResourceList.obj_from_primitive(
jsonutils.loads(db_resources))
def apply_migration_context(self):
if self.migration_context:
self._set_migration_context_to_instance(prefix='new_')
@ -1098,6 +1126,8 @@ class Instance(base.NovaPersistentObject, base.NovaObject,
return self._load_keypairs()
elif attrname == 'trusted_certs':
return self._load_trusted_certs()
elif attrname == 'resources':
return self._load_resources()
elif attrname == 'security_groups':
self._load_security_groups()
elif attrname == 'pci_devices':

View File

@ -38,7 +38,8 @@ class MigrationContext(base.NovaPersistentObject, base.NovaObject):
# Version 1.0: Initial version
# Version 1.1: Add old/new pci_devices and pci_requests
VERSION = '1.1'
# Version 1.2: Add old/new resources
VERSION = '1.2'
fields = {
'instance_uuid': fields.UUIDField(),
@ -55,11 +56,18 @@ class MigrationContext(base.NovaPersistentObject, base.NovaObject):
nullable=True),
'old_pci_requests': fields.ObjectField('InstancePCIRequests',
nullable=True),
'new_resources': fields.ObjectField('ResourceList',
nullable=True),
'old_resources': fields.ObjectField('ResourceList',
nullable=True),
}
@classmethod
def obj_make_compatible(cls, primitive, target_version):
target_version = versionutils.convert_version_to_tuple(target_version)
if target_version < (1, 2):
primitive.pop('old_resources', None)
primitive.pop('new_resources', None)
if target_version < (1, 1):
primitive.pop('old_pci_devices', None)
primitive.pop('new_pci_devices', None)

85
nova/objects/resource.py Normal file
View File

@ -0,0 +1,85 @@
# Copyright 2019 Intel Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
from nova.db import api as db
from nova.objects import base
from nova.objects import fields
@base.NovaObjectRegistry.register
class ResourceMetadata(base.NovaObject):
# Version 1.0: Initial version
VERSION = "1.0"
# This is parent object of specific resources.
# And it's used to be a object field of Resource,
# that is to say Resource.metadata.
def __eq__(self, other):
return base.all_things_equal(self, other)
def __ne__(self, other):
return not (self == other)
@base.NovaObjectRegistry.register
class Resource(base.NovaObject):
# Version 1.0: Initial version
VERSION = "1.0"
fields = {
# UUID of resource provider
'provider_uuid': fields.UUIDField(),
# resource class of the Resource
'resource_class': fields.ResourceClassField(),
# identifier is used to identify resource, it is up to virt drivers
# for mdev, it will be a UUID, for vpmem, it's backend namespace name
'identifier': fields.StringField(),
# metadata is used to contain virt driver specific resource info
'metadata': fields.ObjectField('ResourceMetadata', subclasses=True),
}
def __eq__(self, other):
return base.all_things_equal(self, other)
def __ne__(self, other):
return not (self == other)
def __hash__(self):
metadata = self.metadata if 'metadata' in self else None
return hash((self.provider_uuid, self.resource_class,
self.identifier, metadata))
@base.NovaObjectRegistry.register
class ResourceList(base.ObjectListBase, base.NovaObject):
# Version 1.0: Initial version
VERSION = "1.0"
fields = {
'objects': fields.ListOfObjectsField('Resource'),
}
@base.remotable_classmethod
def get_by_instance_uuid(cls, context, instance_uuid):
db_extra = db.instance_extra_get_by_instance_uuid(
context, instance_uuid, columns=['resources'])
if not db_extra or db_extra['resources'] is None:
return None
primitive = jsonutils.loads(db_extra['resources'])
resources = cls.obj_from_primitive(primitive)
return resources

View File

@ -75,6 +75,7 @@ def fake_db_instance(**updates):
'vcpu_model': None,
'device_metadata': None,
'trusted_certs': None,
'resources': None,
},
'tags': [],
'services': []

View File

@ -145,11 +145,13 @@ class _TestInstanceObject(object):
exp_cols.remove('keypairs')
exp_cols.remove('device_metadata')
exp_cols.remove('trusted_certs')
exp_cols.remove('resources')
exp_cols = [exp_col for exp_col in exp_cols if 'flavor' not in exp_col]
exp_cols.extend(['extra', 'extra.numa_topology', 'extra.pci_requests',
'extra.flavor', 'extra.vcpu_model',
'extra.migration_context', 'extra.keypairs',
'extra.device_metadata', 'extra.trusted_certs'])
'extra.device_metadata', 'extra.trusted_certs',
'extra.resources'])
fake_topology = test_instance_numa.fake_db_topology['numa_topology']
fake_requests = jsonutils.dumps(test_instance_pci_requests.
@ -169,6 +171,11 @@ class _TestInstanceObject(object):
fake_keypairlist.obj_to_primitive())
fake_trusted_certs = jsonutils.dumps(
objects.TrustedCerts(ids=['123foo']).obj_to_primitive())
fake_resource = objects.Resource(
provider_uuid=uuids.rp, resource_class='CUSTOM_FOO',
identifier='foo')
fake_resources = jsonutils.dumps(objects.ResourceList(
objects=[fake_resource]).obj_to_primitive())
fake_service = {'created_at': None, 'updated_at': None,
'deleted_at': None, 'deleted': False, 'id': 123,
'host': 'fake-host', 'binary': 'nova-compute',
@ -188,6 +195,7 @@ class _TestInstanceObject(object):
'migration_context': fake_mig_context,
'keypairs': fake_keypairs,
'trusted_certs': fake_trusted_certs,
'resources': fake_resources,
})
mock_get.return_value = fake_instance
@ -203,6 +211,8 @@ class _TestInstanceObject(object):
self.assertEqual(123, inst.services[0].id)
self.assertEqual('foo', inst.keypairs[0].name)
self.assertEqual(['123foo'], inst.trusted_certs.ids)
self.assertEqual(fake_resource.identifier,
inst.resources[0].identifier)
mock_get.assert_called_once_with(self.context, 'uuid',
columns_to_join=exp_cols)
@ -968,6 +978,7 @@ class _TestInstanceObject(object):
'pci_requests': None,
'device_metadata': None,
'trusted_certs': None,
'resources': None,
}}
fake_inst = fake_instance.fake_db_instance(**vals)
mock_create.return_value = fake_inst
@ -998,6 +1009,7 @@ class _TestInstanceObject(object):
'pci_requests': None,
'device_metadata': None,
'trusted_certs': None,
'resources': None,
}}
fake_inst = fake_instance.fake_db_instance(**vals)
mock_create.return_value = fake_inst
@ -1015,6 +1027,7 @@ class _TestInstanceObject(object):
'pci_requests': None,
'device_metadata': None,
'trusted_certs': None,
'resources': None,
}
mock_create.return_value = self.fake_instance
inst = objects.Instance(context=self.context)
@ -1052,6 +1065,9 @@ class _TestInstanceObject(object):
spec=[])]),
vcpu_model=test_vcpu_model.fake_vcpumodel,
trusted_certs=objects.TrustedCerts(ids=['123foo']),
resources=objects.ResourceList(objects=[objects.Resource(
provider_uuid=uuids.rp, resource_class='CUSTOM_FOO',
identifier='foo')])
)
inst.create()
self.assertIsNotNone(inst.numa_topology)
@ -1069,6 +1085,7 @@ class _TestInstanceObject(object):
self.context, inst.uuid)
self.assertEqual('fake-model', vcpu_model.model)
self.assertEqual(['123foo'], inst.trusted_certs.ids)
self.assertEqual('foo', inst.resources[0].identifier)
def test_recreate_fails(self):
inst = objects.Instance(context=self.context,
@ -1107,6 +1124,7 @@ class _TestInstanceObject(object):
'pci_requests': None,
'device_metadata': None,
'trusted_certs': None,
'resources': None,
},
})
@ -1348,7 +1366,8 @@ class _TestInstanceObject(object):
inst.apply_migration_context()
attrs_type = {'numa_topology': objects.InstanceNUMATopology,
'pci_requests': objects.InstancePCIRequests,
'pci_devices': objects.PciDeviceList}
'pci_devices': objects.PciDeviceList,
'resources': objects.ResourceList}
for attr_name in instance._MIGRATION_CONTEXT_ATTRS:
value = getattr(inst, attr_name)
@ -1379,14 +1398,17 @@ class _TestInstanceObject(object):
pci_requests = objects.InstancePCIRequests(requests=[
objects.InstancePCIRequest(count=1, spec=[])])
pci_devices = pci_device.PciDeviceList()
resources = objects.ResourceList()
inst = instance.Instance(context=self.context, uuid=uuids.instance,
numa_topology=numa_topology,
pci_requests=pci_requests,
pci_devices=pci_devices)
pci_devices=pci_devices,
resources=resources)
expected_objs = {'numa_topology': numa_topology,
'pci_requests': pci_requests,
'pci_devices': pci_devices}
'pci_devices': pci_devices,
'resources': resources}
inst.migration_context = test_mig_ctxt.get_fake_migration_context_obj(
self.context)
with inst.mutated_migration_context():
@ -1491,6 +1513,15 @@ class _TestInstanceObject(object):
mock_get.assert_called_once_with(self.context, uuids.pci_devices)
self.assertEqual(fake_pci_devices, pci_devices)
@mock.patch('nova.objects.ResourceList.get_by_instance_uuid')
def test_load_resources(self, mock_get):
fake_resources = objects.ResourceList()
mock_get.return_value = fake_resources
inst = objects.Instance(context=self.context, uuid=uuids.resources)
resources = inst.resources
mock_get.assert_called_once_with(self.context, uuids.resources)
self.assertEqual(fake_resources, resources)
def test_get_with_extras(self):
pci_requests = objects.InstancePCIRequests(requests=[
objects.InstancePCIRequest(count=123, spec=[])])

View File

@ -34,6 +34,8 @@ fake_migration_context_obj.new_pci_requests = (
objects.InstancePCIRequests(requests=[
objects.InstancePCIRequest(count=123, spec=[])]))
fake_migration_context_obj.old_pci_requests = None
fake_migration_context_obj.new_resources = objects.ResourceList()
fake_migration_context_obj.old_resources = None
fake_db_context = {
'created_at': None,
@ -96,6 +98,10 @@ class _TestMigrationContext(object):
mig_context.new_pci_requests.__class__)
self.assertIsInstance(expected_mig_context.old_pci_requests,
mig_context.old_pci_requests.__class__)
self.assertIsInstance(expected_mig_context. new_resources,
mig_context.new_resources.__class__)
self.assertIsInstance(expected_mig_context.old_resources,
mig_context.old_resources.__class__)
else:
self.assertIsNone(mig_context)

View File

@ -1070,7 +1070,7 @@ object_data = {
'IDEDeviceBus': '1.0-29d4c9f27ac44197f01b6ac1b7e16502',
'ImageMeta': '1.8-642d1b2eb3e880a367f37d72dd76162d',
'ImageMetaProps': '1.24-f92fa09d54185499da98f5430524964e',
'Instance': '2.6-5fefbcb483703c85e4d328b887c8af33',
'Instance': '2.7-d187aec68cad2e4d8b8a03a68e4739ce',
'InstanceAction': '1.2-9a5abc87fdd3af46f45731960651efb5',
'InstanceActionEvent': '1.3-c749e1b3589e7117c81cb2aa6ac438d5',
'InstanceActionEventList': '1.1-13d92fb953030cdbfee56481756e02be',
@ -1095,7 +1095,7 @@ object_data = {
'LibvirtLiveMigrateData': '1.9-7082cc7dd48ca49df71fe3846521b2f3',
'MemoryDiagnostics': '1.0-2c995ae0f2223bb0f8e523c5cc0b83da',
'Migration': '1.7-b77066a88d08bdb0b05d7bc18780c55a',
'MigrationContext': '1.1-9fb17b0b521370957a884636499df52d',
'MigrationContext': '1.2-89f10a83999f852a489962ae37d8a026',
'MigrationList': '1.4-983a9c29d4f1e747ce719dc9063b729b',
'MonitorMetric': '1.1-53b1db7c4ae2c531db79761e7acc52ba',
'MonitorMetricList': '1.1-15ecf022a68ddbb8c2a6739cfc9f8f5e',
@ -1120,6 +1120,9 @@ object_data = {
'QuotasNoOp': '1.3-347a039fc7cfee7b225b68b5181e0733',
'RequestGroup': '1.3-0458d350a8ec9d0673f9be5640a990ce',
'RequestSpec': '1.12-25010470f219af9b6163f2a457a513f5',
'Resource': '1.0-d8a2abbb380da583b995fd118f6a8953',
'ResourceList': '1.0-4a53826625cc280e15fae64a575e0879',
'ResourceMetadata': '1.0-77509ea1ea0dd750d5864b9bd87d3f9d',
'S3ImageMapping': '1.0-7dd7366a890d82660ed121de9092276e',
'SCSIDeviceBus': '1.0-61c1e89a00901069ab1cf2991681533b',
'SchedulerLimits': '1.0-249c4bd8e62a9b327b7026b7f19cc641',

View File

@ -0,0 +1,91 @@
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import mock
from oslo_serialization import jsonutils
from oslo_utils.fixture import uuidsentinel as uuids
import six
from nova.objects import resource
from nova.tests.unit.objects import test_objects
fake_resources = resource.ResourceList(objects=[
resource.Resource(provider_uuid=uuids.rp, resource_class='CUSTOM_RESOURCE',
identifier='foo'),
resource.Resource(provider_uuid=uuids.rp, resource_class='CUSTOM_RESOURCE',
identifier='bar')])
fake_instance_extras = {
'resources': jsonutils.dumps(fake_resources.obj_to_primitive())
}
class TestResourceObject(test_objects._LocalTest):
def _test_set_malformed_resource_class(self, rc):
try:
resource.Resource(provider_uuid=uuids.rp,
resource_class=rc,
identifier='foo')
except ValueError as e:
self.assertEqual('Malformed Resource Class %s' % rc,
six.text_type(e))
else:
self.fail('Check malformed resource class failed.')
def _test_set_formed_resource_class(self, rc):
resource.Resource(provider_uuid=uuids.rp,
resource_class=rc,
identifier='foo')
def test_set_malformed_resource_classes(self):
malformed_resource_classes = ['!', ';', ' ']
for rc in malformed_resource_classes:
self._test_set_malformed_resource_class(rc)
def test_set_formed_resource_classes(self):
formed_resource_classes = ['resource', 'RESOURCE', '0123']
for rc in formed_resource_classes:
self._test_set_formed_resource_class(rc)
def test_equal_without_metadata(self):
resource_0 = resource.Resource(provider_uuid=uuids.rp,
resource_class='bar',
identifier='foo')
resource_1 = resource.Resource(provider_uuid=uuids.rp,
resource_class='bar',
identifier='foo')
self.assertEqual(resource_0, resource_1)
def test_not_equal_without_matadata(self):
self.assertNotEqual(fake_resources[0], fake_resources[1])
class _TestResourceListObject(object):
@mock.patch('nova.db.api.instance_extra_get_by_instance_uuid')
def test_get_by_instance_uuid(self, mock_get):
mock_get.return_value = fake_instance_extras
resources = resource.ResourceList.get_by_instance_uuid(
self.context, 'fake_uuid')
for i in range(len(resources)):
self.assertEqual(resources[i].identifier,
fake_resources[i].identifier)
class TestResourceListObject(test_objects._LocalTest,
_TestResourceListObject):
pass
class TestRemoteResourceListObject(test_objects._RemoteTest,
_TestResourceListObject):
pass