diff --git a/nova/exception.py b/nova/exception.py index c11ae05915f3..e5842f845dcd 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -624,6 +624,10 @@ class StorageRepositoryNotFound(NotFound): msg_fmt = _("Cannot find SR to read/write VDI.") +class InstanceMappingNotFound(NotFound): + msg_fmt = _("Instance %(uuid)s has no mapping to a cell.") + + class NetworkDuplicated(Invalid): msg_fmt = _("Network %(network_id)s is duplicated.") diff --git a/nova/objects/__init__.py b/nova/objects/__init__.py index 27c6ea20e71a..412fb913b47c 100644 --- a/nova/objects/__init__.py +++ b/nova/objects/__init__.py @@ -41,6 +41,7 @@ def register_all(): __import__('nova.objects.instance_fault') __import__('nova.objects.instance_group') __import__('nova.objects.instance_info_cache') + __import__('nova.objects.instance_mapping') __import__('nova.objects.instance_numa_topology') __import__('nova.objects.instance_pci_requests') __import__('nova.objects.keypair') diff --git a/nova/objects/instance_mapping.py b/nova/objects/instance_mapping.py new file mode 100644 index 000000000000..be6af6d5d2dc --- /dev/null +++ b/nova/objects/instance_mapping.py @@ -0,0 +1,136 @@ +# 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.db.sqlalchemy import api as db_api +from nova.db.sqlalchemy import api_models +from nova import exception +from nova import objects +from nova.objects import base +from nova.objects import fields + + +class InstanceMapping(base.NovaTimestampObject, base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.IntegerField(read_only=True), + 'instance_uuid': fields.UUIDField(), + 'cell_id': fields.IntegerField(), + 'project_id': fields.StringField(), + } + + @staticmethod + def _from_db_object(context, instance_mapping, db_instance_mapping): + for key in instance_mapping.fields: + setattr(instance_mapping, key, db_instance_mapping[key]) + instance_mapping.obj_reset_changes() + instance_mapping._context = context + return instance_mapping + + @staticmethod + def _get_by_instance_uuid_from_db(context, instance_uuid): + session = db_api.get_api_session() + + with session.begin(): + db_mapping = session.query( + api_models.InstanceMapping).filter_by( + instance_uuid=instance_uuid).first() + if not db_mapping: + raise exception.InstanceMappingNotFound(uuid=instance_uuid) + + return db_mapping + + @base.remotable_classmethod + def get_by_instance_uuid(cls, context, instance_uuid): + db_mapping = cls._get_by_instance_uuid_from_db(context, instance_uuid) + return cls._from_db_object(context, cls(), db_mapping) + + @staticmethod + def _create_in_db(context, updates): + session = db_api.get_api_session() + + db_mapping = api_models.InstanceMapping() + db_mapping.update(updates) + db_mapping.save(session) + return db_mapping + + @base.remotable + def create(self): + db_mapping = self._create_in_db(self._context, self.obj_get_changes()) + self._from_db_object(self._context, self, db_mapping) + + @staticmethod + def _save_in_db(context, instance_uuid, updates): + session = db_api.get_api_session() + + with session.begin(): + db_mapping = session.query( + api_models.InstanceMapping).filter_by( + instance_uuid=instance_uuid).first() + if not db_mapping: + raise exception.InstanceMappingNotFound(uuid=instance_uuid) + + db_mapping.update(updates) + session.add(db_mapping) + return db_mapping + + @base.remotable + def save(self): + changes = self.obj_get_changes() + db_mapping = self._save_in_db(self._context, self.instance_uuid, + changes) + self._from_db_object(self._context, self, db_mapping) + self.obj_reset_changes() + + @staticmethod + def _destroy_in_db(context, instance_uuid): + session = db_api.get_api_session() + + with session.begin(): + result = session.query(api_models.InstanceMapping).filter_by( + instance_uuid=instance_uuid).delete() + if not result: + raise exception.InstanceMappingNotFound(uuid=instance_uuid) + + @base.remotable + def destroy(self): + self._destroy_in_db(self._context, self.instance_uuid) + + +class InstanceMappingList(base.ObjectListBase, base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'objects': fields.ListOfObjectsField('InstanceMapping'), + } + child_versions = { + '1.0': '1.0', + } + + @staticmethod + def _get_by_project_id_from_db(context, project_id): + session = db_api.get_api_session() + + with session.begin(): + db_mappings = session.query(api_models.InstanceMapping).filter_by( + project_id=project_id).all() + + return db_mappings + + @base.remotable_classmethod + def get_by_project_id(cls, context, project_id): + db_mappings = cls._get_by_project_id_from_db(context, project_id) + + return base.obj_make_list(context, cls(), objects.InstanceMapping, + db_mappings) diff --git a/nova/tests/functional/db/test_instance_mapping.py b/nova/tests/functional/db/test_instance_mapping.py new file mode 100644 index 000000000000..e9ce03f4d728 --- /dev/null +++ b/nova/tests/functional/db/test_instance_mapping.py @@ -0,0 +1,96 @@ +# 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_utils import uuidutils + +from nova import context +from nova import exception +from nova.objects import instance_mapping +from nova import test +from nova.tests import fixtures + + +sample_mapping = {'instance_uuid': '', + 'cell_id': 3, + 'project_id': 'fake-project'} + + +def create_mapping(**kwargs): + args = sample_mapping.copy() + if 'instance_uuid' not in kwargs: + args['instance_uuid'] = uuidutils.generate_uuid() + args.update(kwargs) + ctxt = context.RequestContext('fake-user', 'fake-project') + return instance_mapping.InstanceMapping._create_in_db(ctxt, args) + + +class InstanceMappingTestCase(test.NoDBTestCase): + def setUp(self): + super(InstanceMappingTestCase, self).setUp() + self.useFixture(fixtures.Database(database='api')) + self.context = context.RequestContext('fake-user', 'fake-project') + self.mapping_obj = instance_mapping.InstanceMapping() + + def test_get_by_instance_uuid(self): + mapping = create_mapping() + db_mapping = self.mapping_obj._get_by_instance_uuid_from_db( + self.context, mapping['instance_uuid']) + for key in self.mapping_obj.fields.keys(): + self.assertEqual(db_mapping[key], mapping[key]) + + def test_get_by_instance_uuid_not_found(self): + self.assertRaises(exception.InstanceMappingNotFound, + self.mapping_obj._get_by_instance_uuid_from_db, self.context, + uuidutils.generate_uuid()) + + def test_save_in_db(self): + mapping = create_mapping() + self.mapping_obj._save_in_db(self.context, mapping['instance_uuid'], + {'cell_id': 42}) + db_mapping = self.mapping_obj._get_by_instance_uuid_from_db( + self.context, mapping['instance_uuid']) + self.assertNotEqual(db_mapping['cell_id'], mapping['cell_id']) + for key in [key for key in self.mapping_obj.fields.keys() + if key not in ['cell_id', 'updated_at']]: + self.assertEqual(db_mapping[key], mapping[key]) + + def test_destroy_in_db(self): + mapping = create_mapping() + self.mapping_obj._get_by_instance_uuid_from_db(self.context, + mapping['instance_uuid']) + self.mapping_obj._destroy_in_db(self.context, mapping['instance_uuid']) + self.assertRaises(exception.InstanceMappingNotFound, + self.mapping_obj._get_by_instance_uuid_from_db, self.context, + mapping['instance_uuid']) + + +class InstanceMappingListTestCase(test.NoDBTestCase): + def setUp(self): + super(InstanceMappingListTestCase, self).setUp() + self.useFixture(fixtures.Database(database='api')) + self.context = context.RequestContext('fake-user', 'fake-project') + self.list_obj = instance_mapping.InstanceMappingList() + + def test_get_by_project_id_from_db(self): + project_id = 'fake-project' + mappings = {} + mapping = create_mapping(project_id=project_id) + mappings[mapping['instance_uuid']] = mapping + mapping = create_mapping(project_id=project_id) + mappings[mapping['instance_uuid']] = mapping + + db_mappings = self.list_obj._get_by_project_id_from_db( + self.context, project_id) + for db_mapping in db_mappings: + mapping = mappings[db_mapping.instance_uuid] + for key in instance_mapping.InstanceMapping.fields.keys(): + self.assertEqual(db_mapping[key], mapping[key]) diff --git a/nova/tests/unit/objects/test_instance_mapping.py b/nova/tests/unit/objects/test_instance_mapping.py new file mode 100644 index 000000000000..d6f76b1d011d --- /dev/null +++ b/nova/tests/unit/objects/test_instance_mapping.py @@ -0,0 +1,122 @@ +# 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_utils import uuidutils + +from nova import objects +from nova.objects import instance_mapping +from nova.tests.unit.objects import test_objects + + +def get_db_mapping(**updates): + db_mapping = { + 'id': 1, + 'instance_uuid': uuidutils.generate_uuid(), + 'cell_id': 42, + 'project_id': 'fake-project', + 'created_at': None, + 'updated_at': None, + } + db_mapping.update(updates) + return db_mapping + + +class _TestInstanceMappingObject(object): + @mock.patch.object(instance_mapping.InstanceMapping, + '_get_by_instance_uuid_from_db') + def test_get_by_instance_uuid(self, uuid_from_db): + db_mapping = get_db_mapping() + uuid_from_db.return_value = db_mapping + + mapping_obj = objects.InstanceMapping().get_by_instance_uuid( + self.context, db_mapping['instance_uuid']) + uuid_from_db.assert_called_once_with(self.context, + db_mapping['instance_uuid']) + self.compare_obj(mapping_obj, db_mapping) + + @mock.patch.object(instance_mapping.InstanceMapping, '_create_in_db') + def test_create(self, create_in_db): + db_mapping = get_db_mapping() + uuid = db_mapping['instance_uuid'] + create_in_db.return_value = db_mapping + mapping_obj = objects.InstanceMapping(self.context) + mapping_obj.instance_uuid = uuid + mapping_obj.cell_id = db_mapping['cell_id'] + mapping_obj.project_id = db_mapping['project_id'] + + mapping_obj.create() + create_in_db.assert_called_once_with(self.context, + {'instance_uuid': uuid, + 'cell_id': db_mapping['cell_id'], + 'project_id': db_mapping['project_id']}) + self.compare_obj(mapping_obj, db_mapping) + + @mock.patch.object(instance_mapping.InstanceMapping, '_save_in_db') + def test_save(self, save_in_db): + db_mapping = get_db_mapping() + uuid = db_mapping['instance_uuid'] + save_in_db.return_value = db_mapping + mapping_obj = objects.InstanceMapping(self.context) + mapping_obj.instance_uuid = uuid + mapping_obj.cell_id = 3 + + mapping_obj.save() + save_in_db.assert_called_once_with(self.context, + db_mapping['instance_uuid'], + {'cell_id': 3, + 'instance_uuid': uuid}) + self.compare_obj(mapping_obj, db_mapping) + + @mock.patch.object(instance_mapping.InstanceMapping, '_destroy_in_db') + def test_destroy(self, destroy_in_db): + uuid = uuidutils.generate_uuid() + mapping_obj = objects.InstanceMapping(self.context) + mapping_obj.instance_uuid = uuid + + mapping_obj.destroy() + destroy_in_db.assert_called_once_with(self.context, uuid) + + +class TestInstanceMappingObject(test_objects._LocalTest, + _TestInstanceMappingObject): + pass + + +class TestRemoteInstanceMappingObject(test_objects._RemoteTest, + _TestInstanceMappingObject): + pass + + +class _TestInstanceMappingListObject(object): + @mock.patch.object(instance_mapping.InstanceMappingList, + '_get_by_project_id_from_db') + def test_get_by_project_id(self, project_id_from_db): + db_mapping = get_db_mapping() + project_id_from_db.return_value = [db_mapping] + + mapping_obj = objects.InstanceMappingList().get_by_project_id( + self.context, db_mapping['project_id']) + project_id_from_db.assert_called_once_with(self.context, + db_mapping['project_id']) + self.compare_obj(mapping_obj.objects[0], db_mapping) + + +class TestInstanceMappingListObject(test_objects._LocalTest, + _TestInstanceMappingListObject): + pass + + +class TestRemoteInstanceMappingListObject(test_objects._RemoteTest, + _TestInstanceMappingListObject): + pass diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index fb70852e92a6..0caf6e1f1052 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1209,6 +1209,8 @@ object_data = { 'InstanceGroupList': '1.6-c6b78f3c9d9080d33c08667e80589817', 'InstanceInfoCache': '1.5-ef7394dae46cff2dd560324555cb85cf', 'InstanceList': '1.16-8594a8f95e717e57ee57b4aba59c688e', + 'InstanceMapping': '1.0-d7cfc251f16c93df612af2b9de59e5b7', + 'InstanceMappingList': '1.0-3523d501c591640b483c5c1971ef9fd0', 'InstanceNUMACell': '1.2-5d2dfa36e9ecca9b63f24bf3bc958ea4', 'InstanceNUMATopology': '1.1-b6fab68a3f0f1dfab4c98a236d29839a', 'InstancePCIRequest': '1.1-e082d174f4643e5756ba098c47c1510f',