From 2d998c8df2048167c2db3694d3aae4605ff2625c Mon Sep 17 00:00:00 2001 From: Russell Bryant Date: Fri, 29 Aug 2014 14:56:10 +0000 Subject: [PATCH] Add InstancePCIRequests object Part of blueprint pci-passthrough-sriov Co-Authored-By: Dan Smith Co-Authored-By: Baodong (Robert) Li Change-Id: I125e4afdf98a7073abcb8117bbb7a3e3fe2f9830 --- nova/objects/__init__.py | 1 + nova/objects/instance_pci_requests.py | 123 +++++++++++++ .../objects/test_instance_pci_requests.py | 172 ++++++++++++++++++ 3 files changed, 296 insertions(+) create mode 100644 nova/objects/instance_pci_requests.py create mode 100644 nova/tests/objects/test_instance_pci_requests.py diff --git a/nova/objects/__init__.py b/nova/objects/__init__.py index cfcc898af30..bd2cefd1978 100644 --- a/nova/objects/__init__.py +++ b/nova/objects/__init__.py @@ -41,6 +41,7 @@ def register_all(): __import__('nova.objects.instance_group') __import__('nova.objects.instance_info_cache') __import__('nova.objects.instance_numa_topology') + __import__('nova.objects.instance_pci_requests') __import__('nova.objects.keypair') __import__('nova.objects.migration') __import__('nova.objects.network') diff --git a/nova/objects/instance_pci_requests.py b/nova/objects/instance_pci_requests.py new file mode 100644 index 00000000000..2eb1bdec053 --- /dev/null +++ b/nova/objects/instance_pci_requests.py @@ -0,0 +1,123 @@ +# 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 nova import db +from nova.objects import base +from nova.objects import fields +from nova.openstack.common import jsonutils + + +class InstancePCIRequest(base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'count': fields.IntegerField(), + 'spec': fields.ListOfDictOfNullableStringsField(), + 'alias_name': fields.StringField(), + # A stashed request related to a resize, not current + 'is_new': fields.BooleanField(default=False), + } + + def obj_load_attr(self, attr): + setattr(self, attr, None) + + # NOTE(danms): The dict that this object replaces uses a key of 'new' + # so we translate it here to our more appropropriately-named 'is_new'. + # This is not something that affects the obect version, so we could + # remove this later when all dependent code is fixed. + @property + def new(self): + return self.is_new + + +class InstancePCIRequests(base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'instance_uuid': fields.UUIDField(), + 'requests': fields.ListOfObjectsField('InstancePCIRequest'), + } + + @base.remotable_classmethod + def get_by_instance_uuid(cls, context, instance_uuid): + obj_pci_requests = cls(instance_uuid=instance_uuid) + obj_pci_requests.requests = [] + obj_pci_requests._context = context + + db_pci_requests = db.instance_extra_get_by_instance_uuid( + context, instance_uuid) + if db_pci_requests: + try: + requests = jsonutils.loads(db_pci_requests['pci_requests']) + except TypeError: + requests = [] + for request in requests: + request_obj = InstancePCIRequest( + count=request['count'], spec=request['spec'], + alias_name=request['alias_name'], is_new=request['is_new']) + request_obj.obj_reset_changes() + obj_pci_requests.requests.append(request_obj) + + obj_pci_requests.obj_reset_changes() + + return obj_pci_requests + + @classmethod + def get_by_instance_uuid_and_newness(cls, context, instance_uuid, is_new): + requests = cls.get_by_instance_uuid(context, instance_uuid) + requests.requests = [x for x in requests.requests + if x.new == is_new] + return requests + + @staticmethod + def _load_legacy_requests(sysmeta_value, is_new=False): + if sysmeta_value is None: + return [] + requests = [] + db_requests = jsonutils.loads(sysmeta_value) + for db_request in db_requests: + request = InstancePCIRequest( + count=db_request['count'], spec=db_request['spec'], + alias_name=db_request['alias_name'], is_new=is_new) + request.obj_reset_changes() + requests.append(request) + return requests + + @classmethod + def get_by_instance(cls, context, instance): + # NOTE (baoli): not all callers are passing instance as object yet. + # Therefore, use the dict syntax in this routine + if 'pci_requests' in instance['system_metadata']: + # NOTE(danms): This instance hasn't been converted to use + # instance_extra yet, so extract the data from sysmeta + sysmeta = instance['system_metadata'] + _requests = ( + cls._load_legacy_requests(sysmeta['pci_requests']) + + cls._load_legacy_requests(sysmeta.get('new_pci_requests'), + is_new=True)) + requests = cls(instance_uuid=instance['uuid'], requests=_requests) + requests.obj_reset_changes() + return requests + else: + return cls.get_by_instance_uuid(context, instance['uuid']) + + @base.remotable + def save(self, context): + blob = [{'count': x.count, + 'spec': x.spec, + 'alias_name': x.alias_name, + 'is_new': x.is_new} for x in self.requests] + requests = jsonutils.dumps(blob) + db.instance_extra_update_by_uuid(context, self.instance_uuid, + {'pci_requests': requests}) diff --git a/nova/tests/objects/test_instance_pci_requests.py b/nova/tests/objects/test_instance_pci_requests.py new file mode 100644 index 00000000000..00ca37e806b --- /dev/null +++ b/nova/tests/objects/test_instance_pci_requests.py @@ -0,0 +1,172 @@ +# 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 nova import objects +from nova.openstack.common import jsonutils +from nova.tests.objects import test_objects + + +FAKE_UUID = '79a53d6b-0893-4838-a971-15f4f382e7c2' + +# NOTE(danms): Yes, these are the same right now, but going forward, +# we have changes to make which will be reflected in the format +# in instance_extra, but not in system_metadata. +fake_pci_requests = [ + {'count': 2, + 'spec': [{'vendor_id': '8086', + 'device_id': '1502'}], + 'alias_name': 'alias_1', + 'is_new': False}, + {'count': 2, + 'spec': [{'vendor_id': '6502', + 'device_id': '07B5'}], + 'alias_name': 'alias_2', + 'is_new': True}, + ] + +fake_legacy_pci_requests = [ + {'count': 2, + 'spec': [{'vendor_id': '8086', + 'device_id': '1502'}], + 'alias_name': 'alias_1'}, + {'count': 1, + 'spec': [{'vendor_id': '6502', + 'device_id': '07B5'}], + 'alias_name': 'alias_2'}, + ] + + +class _TestInstancePCIRequests(object): + @mock.patch('nova.db.instance_extra_get_by_instance_uuid') + def test_get_by_instance_uuid(self, mock_get): + mock_get.return_value = { + 'instance_uuid': FAKE_UUID, + 'pci_requests': jsonutils.dumps(fake_pci_requests), + } + requests = objects.InstancePCIRequests.get_by_instance_uuid( + self.context, FAKE_UUID) + self.assertEqual(2, len(requests.requests)) + for index, request in enumerate(requests.requests): + self.assertEqual(fake_pci_requests[index]['alias_name'], + request.alias_name) + self.assertEqual(fake_pci_requests[index]['count'], + request.count) + self.assertEqual(fake_pci_requests[index]['spec'], + [dict(x.items()) for x in request.spec]) + + @mock.patch('nova.objects.InstancePCIRequests.get_by_instance_uuid') + def test_get_by_instance_uuid_and_newness(self, mock_get): + pcir = objects.InstancePCIRequests + mock_get.return_value = objects.InstancePCIRequests( + instance_uuid='fake-uuid', + requests=[objects.InstancePCIRequest(count=1, is_new=False), + objects.InstancePCIRequest(count=2, is_new=True)]) + old_req = pcir.get_by_instance_uuid_and_newness(self.context, + 'fake-uuid', + False) + mock_get.return_value = objects.InstancePCIRequests( + instance_uuid='fake-uuid', + requests=[objects.InstancePCIRequest(count=1, is_new=False), + objects.InstancePCIRequest(count=2, is_new=True)]) + new_req = pcir.get_by_instance_uuid_and_newness(self.context, + 'fake-uuid', + True) + self.assertEqual(1, old_req.requests[0].count) + self.assertEqual(2, new_req.requests[0].count) + + @mock.patch('nova.objects.InstancePCIRequests.get_by_instance_uuid') + def test_get_by_instance_current(self, mock_get): + instance = objects.Instance(uuid='fake-uuid', + system_metadata={}) + objects.InstancePCIRequests.get_by_instance(self.context, + instance) + mock_get.assert_called_once_with(self.context, 'fake-uuid') + + def test_get_by_instance_legacy(self): + fakesysmeta = { + 'pci_requests': jsonutils.dumps([fake_legacy_pci_requests[0]]), + 'new_pci_requests': jsonutils.dumps([fake_legacy_pci_requests[1]]), + } + instance = objects.Instance(uuid='fake-uuid', + system_metadata=fakesysmeta) + requests = objects.InstancePCIRequests.get_by_instance(self.context, + instance) + self.assertEqual(2, len(requests.requests)) + self.assertEqual('alias_1', requests.requests[0].alias_name) + self.assertFalse(requests.requests[0].is_new) + self.assertEqual('alias_2', requests.requests[1].alias_name) + self.assertTrue(requests.requests[1].is_new) + + @mock.patch('nova.db.instance_extra_update_by_uuid') + def test_save(self, mock_update): + requests = objects.InstancePCIRequests( + context=self.context, + instance_uuid=FAKE_UUID, + requests=[objects.InstancePCIRequest( + count=1, + spec=[{'foo': 'bar'}, {'baz': 'bat'}], + alias_name='alias_1', + is_new=False)]) + requests.save() + self.assertEqual(FAKE_UUID, mock_update.call_args_list[0][0][1]) + self.assertEqual( + [{'count': 1, 'is_new': False, + 'alias_name': 'alias_1', + 'spec': [{'foo': 'bar'}, {'baz': 'bat'}]}], + jsonutils.loads( + mock_update.call_args_list[0][0][2]['pci_requests'])) + + @mock.patch('nova.db.instance_extra_update_by_uuid') + @mock.patch('nova.db.instance_extra_get_by_instance_uuid') + def test_save_and_reload(self, mock_get, mock_update): + database = {} + + def _save(context, uuid, values): + database.setdefault(uuid, {'instance_uuid': uuid}) + database[uuid].update(values) + + def _get(context, uuid): + return database.get(uuid, {}) + + mock_update.side_effect = _save + mock_get.side_effect = _get + + requests = objects.InstancePCIRequests( + context=self.context, + instance_uuid=FAKE_UUID, + requests=[objects.InstancePCIRequest( + count=1, is_new=False, alias_name='alias_1', + spec=[{'foo': 'bar'}])]) + requests.save() + _requests = objects.InstancePCIRequests.get_by_instance_uuid( + self.context, FAKE_UUID) + + self.assertEqual(requests.instance_uuid, _requests.instance_uuid) + self.assertEqual(len(requests.requests), len(_requests.requests)) + self.assertEqual(requests.requests[0].alias_name, + _requests.requests[0].alias_name) + + def test_new_compatibility(self): + request = objects.InstancePCIRequest(is_new=False) + self.assertFalse(request.new) + + +class TestInstancePCIRequests(test_objects._LocalTest, + _TestInstancePCIRequests): + pass + + +class TestRemoteInstancePCIRequests(test_objects._RemoteTest, + _TestInstancePCIRequests): + pass