From 0680f6868272ce888f252202ecb5e4a48c7eb0df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Ribaud?= Date: 2022年6月16日 16:24:39 +0200 Subject: [PATCH] Attach Manila shares via virtiofs (objects) This patch introduce the ShareMapping and ShareMappingList top level objects and associated attach and detach methods that interact with the DB It also introduce ShareMappingLibvirt and ShareMappingLibvirtNFS children objects that will be used by the driver part. Manila is the OpenStack Shared Filesystems service. These series of patches implement changes required in Nova to allow the shares provided by Manila to be associated with and attached to instances using virtiofs. Implements: blueprint libvirt-virtiofs-attach-manila-shares Change-Id: Ibacfeaf7daa726bf0ee7802bacd3f70329fec624 --- nova/exception.py | 16 + nova/objects/__init__.py | 1 + nova/objects/fields.py | 27 ++ nova/objects/share_mapping.py | 143 +++++++++ nova/tests/unit/objects/test_objects.py | 2 + nova/tests/unit/objects/test_share_mapping.py | 286 ++++++++++++++++++ 6 files changed, 475 insertions(+) create mode 100644 nova/objects/share_mapping.py create mode 100644 nova/tests/unit/objects/test_share_mapping.py diff --git a/nova/exception.py b/nova/exception.py index a9df60304c41..48978d646a58 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 5f7e4382518c..4b51ea3e4665 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 cae1ea4a4d73..06a850913279 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 000000000000..6ddccdece943 --- /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 2568e099fc76..17ba1a23a515 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 000000000000..d9dd39d30b67 --- /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

AltStyle によって変換されたページ (->オリジナル) /