Merge "Attach Manila shares via virtiofs (objects)"
This commit is contained in:
@@ -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.")
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user