diff --git a/nova/exception.py b/nova/exception.py index a9df60304c..48978d646a 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -700,6 +700,22 @@ class VolumeNotFound(NotFound): msg_fmt = _("Volume %(volume_id)s could not be found.") +class ShareNotFound(NotFound): + msg_fmt = _("Share %(share_id)s could not be found.") + + +class ShareUmountError(NovaException): + msg_fmt = _("Share id %(share_id)s umount error " + "from server %(server_id)s.\n" + "Reason: %(reason)s.") + + +class ShareMountError(NovaException): + msg_fmt = _("Share id %(share_id)s mount error " + "from server %(server_id)s.\n" + "Reason: %(reason)s.") + + class VolumeTypeNotFound(NotFound): msg_fmt = _("Volume type %(id_or_name)s could not be found.") diff --git a/nova/objects/__init__.py b/nova/objects/__init__.py index 5f7e438251..4b51ea3e46 100644 --- a/nova/objects/__init__.py +++ b/nova/objects/__init__.py @@ -69,3 +69,4 @@ def register_all(): __import__('nova.objects.virt_cpu_topology') __import__('nova.objects.virtual_interface') __import__('nova.objects.volume_usage') + __import__('nova.objects.share_mapping') diff --git a/nova/objects/fields.py b/nova/objects/fields.py index cae1ea4a4d..06a8509132 100644 --- a/nova/objects/fields.py +++ b/nova/objects/fields.py @@ -526,6 +526,25 @@ class RNGModel(BaseNovaEnum): ALL = (VIRTIO,) +class ShareMappingStatus(BaseNovaEnum): + """Represents the possible status of a share mapping""" + + ACTIVE = "active" + INACTIVE = "inactive" + ERROR = "error" + + ALL = (ACTIVE, INACTIVE, ERROR) + + +class ShareMappingProto(BaseNovaEnum): + """Represents the possible protocol used by a share mapping""" + + NFS = "NFS" + CEPHFS = "CEPHFS" + + ALL = (NFS, CEPHFS) + + class TPMModel(BaseNovaEnum): TIS = "tpm-tis" @@ -1287,6 +1306,14 @@ class RNGModelField(BaseEnumField): AUTO_TYPE = RNGModel() +class ShareMappingStatusField(BaseEnumField): + AUTO_TYPE = ShareMappingStatus() + + +class ShareMappingProtoField(BaseEnumField): + AUTO_TYPE = ShareMappingProto() + + class TPMModelField(BaseEnumField): AUTO_TYPE = TPMModel() diff --git a/nova/objects/share_mapping.py b/nova/objects/share_mapping.py new file mode 100644 index 0000000000..6ddccdece9 --- /dev/null +++ b/nova/objects/share_mapping.py @@ -0,0 +1,143 @@ +# 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 logging +from nova.db.main import api as db +from nova import exception +from nova.objects import base +from nova.objects import fields + +LOG = logging.getLogger(__name__) + + +@base.NovaObjectRegistry.register +class ShareMapping(base.NovaTimestampObject, base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + + fields = { + 'id': fields.IntegerField(read_only=True), + 'uuid': fields.UUIDField(nullable=False), + 'instance_uuid': fields.UUIDField(nullable=False), + 'share_id': fields.UUIDField(nullable=False), + 'status': fields.ShareMappingStatusField(), + 'tag': fields.StringField(nullable=False), + 'export_location': fields.StringField(nullable=False), + 'share_proto': fields.ShareMappingProtoField() + } + + @staticmethod + def _from_db_object(context, share_mapping, db_share_mapping): + for field in share_mapping.fields: + setattr(share_mapping, field, db_share_mapping[field]) + share_mapping._context = context + share_mapping.obj_reset_changes() + return share_mapping + + @base.remotable + def save(self): + db_share_mapping = db.share_mapping_update( + self._context, self.uuid, self.instance_uuid, self.share_id, + self.status, self.tag, self.export_location, self.share_proto) + self._from_db_object(self._context, self, db_share_mapping) + + def create(self): + LOG.info( + "Associate share '%s' to instance '%s'.", + self.share_id, self.instance_uuid) + + self.save() + + @base.remotable + def delete(self): + LOG.info( + "Dissociate share '%s' from instance '%s'.", + self.share_id, + self.instance_uuid, + ) + db.share_mapping_delete_by_instance_uuid_and_share_id( + self._context, self.instance_uuid, self.share_id + ) + + def attach(self): + LOG.info( + "Share '%s' about to be attached to instance '%s'.", + self.share_id, self.instance_uuid) + + self.status = fields.ShareMappingStatus.ACTIVE + self.save() + + def detach(self): + LOG.info( + "Share '%s' about to be detached from instance '%s'.", + self.share_id, + self.instance_uuid, + ) + self.status = fields.ShareMappingStatus.INACTIVE + self.save() + + @base.remotable_classmethod + def get_by_instance_uuid_and_share_id( + # This query returns only one element as a share can be + # associated only one time to an instance. + # Note: the REST API prevent the user to create duplicate share + # mapping by raising an exception.ShareMappingAlreadyExists. + cls, context, instance_uuid, share_id): + share_mapping = ShareMapping(context) + db_share_mapping = db.share_mapping_get_by_instance_uuid_and_share_id( + context, instance_uuid, share_id) + if not db_share_mapping: + raise exception.ShareNotFound(share_id=share_id) + return ShareMapping._from_db_object( + context, + share_mapping, + db_share_mapping) + + def get_share_host_provider(self): + if not self.export_location: + return None + if self.share_proto == 'NFS': + rhost, _ = self.export_location.strip().split(':') + else: + raise NotImplementedError() + return rhost + + +@base.NovaObjectRegistry.register +class ShareMappingList(base.ObjectListBase, base.NovaObject): + # Version 1.0: Initial version + VERSION = '1.0' + fields = { + 'objects': fields.ListOfObjectsField('ShareMapping'), + } + + @base.remotable_classmethod + def get_by_instance_uuid(cls, context, instance_uuid): + db_share_mappings = db.share_mapping_get_by_instance_uuid( + context, instance_uuid) + return base.obj_make_list( + context, cls(context), ShareMapping, db_share_mappings) + + @base.remotable_classmethod + def get_by_share_id(cls, context, share_id): + db_share_mappings = db.share_mapping_get_by_share_id( + context, share_id) + return base.obj_make_list( + context, cls(context), ShareMapping, db_share_mappings) + + def attach_all(self): + for share in self: + share.attach() + + def detach_all(self): + for share in self: + share.detach() diff --git a/nova/tests/unit/objects/test_objects.py b/nova/tests/unit/objects/test_objects.py index 2568e099fc..17ba1a23a5 100644 --- a/nova/tests/unit/objects/test_objects.py +++ b/nova/tests/unit/objects/test_objects.py @@ -1168,6 +1168,8 @@ object_data = { 'Selection': '1.1-548e3c2f04da2a61ceaf9c4e1589f264', 'Service': '1.22-8a740459ab9bf258a19c8fcb875c2d9a', 'ServiceList': '1.19-5325bce13eebcbf22edc9678285270cc', + 'ShareMapping': '1.0-5ed0db9b97582e84d582c0b8488aa5df', + 'ShareMappingList': '1.0-634980d5efdf3656e28c8dec3d862ab9', 'Tag': '1.1-8b8d7d5b48887651a0e01241672e2963', 'TagList': '1.1-55231bdb671ecf7641d6a2e9109b5d8e', 'TaskLog': '1.0-78b0534366f29aa3eebb01860fbe18fe', diff --git a/nova/tests/unit/objects/test_share_mapping.py b/nova/tests/unit/objects/test_share_mapping.py new file mode 100644 index 0000000000..d9dd39d30b --- /dev/null +++ b/nova/tests/unit/objects/test_share_mapping.py @@ -0,0 +1,286 @@ +# 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 datetime + +from copy import deepcopy +from nova.db.main import api as db +from nova.db.main import models +from nova import exception +from nova import objects +from nova.objects import share_mapping as sm +from nova.tests.unit.objects import test_objects +from oslo_utils.fixture import uuidsentinel as uuids + +from unittest import mock + + +fake_share_mapping = { + 'created_at': datetime.datetime(2022, 8, 25, 10, 5, 4), + 'updated_at': None, + 'id': 1, + 'uuid': uuids.share_mapping, + 'instance_uuid': uuids.instance, + 'share_id': uuids.share, + 'status': 'inactive', + 'tag': 'fake_tag', + 'export_location': '192.168.122.152:/manila/share', + 'share_proto': 'NFS', + } + +fake_share_mapping2 = { + 'created_at': datetime.datetime(2022, 8, 26, 10, 5, 4), + 'updated_at': None, + 'id': 2, + 'uuid': uuids.share_mapping2, + 'instance_uuid': uuids.instance, + 'share_id': uuids.share2, + 'status': 'inactive', + 'tag': 'fake_tag2', + 'export_location': '192.168.122.152:/manila/share2', + 'share_proto': 'NFS', + } + +fake_share_mapping_attached = deepcopy(fake_share_mapping) +fake_share_mapping_attached['status'] = 'active' + + +class _TestShareMapping(object): + def _compare_obj(self, share_mapping, fake_share_mapping): + self.compare_obj( + share_mapping, + fake_share_mapping, + allow_missing=['deleted', 'deleted_at']) + + @mock.patch( + 'nova.db.main.api.share_mapping_update', + return_value=fake_share_mapping) + def test_save(self, mock_upd): + share_mapping = objects.ShareMapping(self.context) + share_mapping.uuid = uuids.share_mapping + share_mapping.instance_uuid = uuids.instance + share_mapping.share_id = uuids.share + share_mapping.status = 'inactive' + share_mapping.tag = 'fake_tag' + share_mapping.export_location = '192.168.122.152:/manila/share' + share_mapping.share_proto = 'NFS' + share_mapping.save() + mock_upd.assert_called_once_with( + self.context, + uuids.share_mapping, + uuids.instance, + uuids.share, + 'inactive', + 'fake_tag', + '192.168.122.152:/manila/share', + 'NFS' + ) + self._compare_obj(share_mapping, fake_share_mapping) + + def test_get_share_host_provider(self): + share_mapping = objects.ShareMapping(self.context) + share_mapping.uuid = uuids.share_mapping + share_mapping.instance_uuid = uuids.instance + share_mapping.share_id = uuids.share + share_mapping.status = 'inactive' + share_mapping.tag = 'fake_tag' + share_mapping.export_location = '192.168.122.152:/manila/share' + share_mapping.share_proto = 'NFS' + share_host_provider = share_mapping.get_share_host_provider() + self.assertEqual(share_host_provider, '192.168.122.152') + + def test_get_share_host_provider_not_defined(self): + share_mapping = objects.ShareMapping(self.context) + share_mapping.uuid = uuids.share_mapping + share_mapping.instance_uuid = uuids.instance + share_mapping.share_id = uuids.share + share_mapping.status = 'inactive' + share_mapping.tag = 'fake_tag' + share_mapping.export_location = '' + share_mapping.share_proto = 'NFS' + share_host_provider = share_mapping.get_share_host_provider() + self.assertIsNone(share_host_provider) + + @mock.patch( + 'nova.db.main.api.share_mapping_update', + return_value=fake_share_mapping_attached) + def test_create(self, mock_upd): + share_mapping = objects.ShareMapping(self.context) + share_mapping.uuid = uuids.share_mapping + share_mapping.instance_uuid = uuids.instance + share_mapping.share_id = uuids.share + share_mapping.status = 'inactive' + share_mapping.tag = 'fake_tag' + share_mapping.export_location = '192.168.122.152:/manila/share' + share_mapping.share_proto = 'NFS' + share_mapping.create() + mock_upd.assert_called_once_with( + self.context, + uuids.share_mapping, + uuids.instance, + uuids.share, + 'inactive', + 'fake_tag', + '192.168.122.152:/manila/share', + 'NFS' + ) + self._compare_obj(share_mapping, fake_share_mapping_attached) + + @mock.patch( + 'nova.db.main.api.share_mapping_update', + return_value=fake_share_mapping_attached) + def test_attach(self, mock_upd): + share_mapping = objects.ShareMapping(self.context) + share_mapping.uuid = uuids.share_mapping + share_mapping.instance_uuid = uuids.instance + share_mapping.share_id = uuids.share + share_mapping.status = 'inactive' + share_mapping.tag = 'fake_tag' + share_mapping.export_location = '192.168.122.152:/manila/share' + share_mapping.share_proto = 'NFS' + share_mapping.attach() + mock_upd.assert_called_once_with( + self.context, + uuids.share_mapping, + uuids.instance, + uuids.share, + 'active', + 'fake_tag', + + '192.168.122.152:/manila/share', + 'NFS' + ) + self._compare_obj(share_mapping, fake_share_mapping_attached) + + @mock.patch( + 'nova.db.main.api.share_mapping_update', + return_value=fake_share_mapping) + def test_detach(self, mock_upd): + share_mapping = objects.ShareMapping(self.context) + share_mapping.uuid = uuids.share_mapping + share_mapping.instance_uuid = uuids.instance + share_mapping.share_id = uuids.share + share_mapping.status = 'active' + share_mapping.tag = 'fake_tag' + share_mapping.export_location = '192.168.122.152:/manila/share' + share_mapping.share_proto = 'NFS' + share_mapping.detach() + mock_upd.assert_called_once_with( + self.context, + uuids.share_mapping, + uuids.instance, + uuids.share, + 'inactive', + 'fake_tag', + + '192.168.122.152:/manila/share', + 'NFS' + ) + self._compare_obj(share_mapping, fake_share_mapping) + + @mock.patch( + 'nova.db.main.api.share_mapping_delete_by_instance_uuid_and_share_id') + def test_delete(self, mock_del): + share_mapping = objects.ShareMapping(self.context) + share_mapping.uuid = uuids.share_mapping + share_mapping.instance_uuid = uuids.instance + share_mapping.share_id = uuids.share + share_mapping.status = 'inactive' + share_mapping.tag = 'fake_tag' + share_mapping.export_location = '192.168.122.152:/manila/share' + share_mapping.share_proto = 'NFS' + share_mapping.delete() + mock_del.assert_called_once_with( + self.context, uuids.instance, uuids.share) + + def test_get_by_instance_uuid_and_share_id(self): + + fake_db_sm = models.ShareMapping() + fake_db_sm.id = 1 + fake_db_sm.created_at = datetime.datetime(2022, 8, 25, 10, 5, 4) + fake_db_sm.uuid = fake_share_mapping['uuid'] + fake_db_sm.instance_uuid = fake_share_mapping['instance_uuid'] + fake_db_sm.share_id = fake_share_mapping['share_id'] + fake_db_sm.status = fake_share_mapping['status'] + fake_db_sm.tag = fake_share_mapping['tag'] + fake_db_sm.export_location = fake_share_mapping['export_location'] + fake_db_sm.share_proto = fake_share_mapping['share_proto'] + + with mock.patch( + 'nova.db.main.api.share_mapping_get_by_instance_uuid_and_share_id', + return_value=fake_db_sm + ) as mock_get: + + share_mapping = sm.ShareMapping.get_by_instance_uuid_and_share_id( + self.context, + uuids.instance, + uuids.share + ) + + mock_get.assert_called_once_with( + self.context, uuids.instance, uuids.share) + + self._compare_obj(share_mapping, fake_share_mapping) + + @mock.patch( + 'nova.db.main.api.share_mapping_get_by_instance_uuid_and_share_id', + return_value=None) + def test_get_by_instance_uuid_and_share_id_not_found(self, mock_get): + self.assertRaises(exception.ShareNotFound, + sm.ShareMapping.get_by_instance_uuid_and_share_id, + self.context, + uuids.instance, + uuids.share) + mock_get.assert_called_once_with( + self.context, uuids.instance, uuids.share) + + +class _TestShareMappingList(object): + def test_get_by_instance_uuid(self): + with mock.patch.object( + db, 'share_mapping_get_by_instance_uuid' + ) as get: + get.return_value = [fake_share_mapping] + share_mappings = sm.ShareMappingList.get_by_instance_uuid( + self.context, uuids.instance + ) + + self.assertIsInstance(share_mappings, sm.ShareMappingList) + self.assertEqual(1, len(share_mappings)) + self.assertIsInstance(share_mappings[0], sm.ShareMapping) + + def test_get_by_share_id(self): + with mock.patch.object(db, 'share_mapping_get_by_share_id') as get: + get.return_value = [fake_share_mapping] + share_mappings = sm.ShareMappingList.get_by_share_id( + self.context, uuids.share + ) + + self.assertIsInstance(share_mappings, sm.ShareMappingList) + self.assertEqual(1, len(share_mappings)) + self.assertIsInstance(share_mappings[0], sm.ShareMapping) + + +class TestShareMapping(test_objects._LocalTest, _TestShareMapping): + pass + + +class TestRemoteShareMapping(test_objects._RemoteTest, _TestShareMapping): + pass + + +class TestShareMappingList(test_objects._LocalTest, _TestShareMappingList): + pass + + +class TestRemoteShareMappingList( + test_objects._RemoteTest, _TestShareMappingList): + pass