Attach Manila shares via virtiofs (manila abstraction)

This patch is inspired by /nova/volume/cinder.py, it is an abstraction to
the manila service.

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
Depends-On: https://review.opendev.org/c/openstack/openstacksdk/+/889519

Change-Id: I44ab37ec2c15fcfc351c42216660bda39461b163
This commit is contained in:
René Ribaud 2022-06-16 16:22:48 +02:00
parent 9a1872d30e
commit 0f9001f06e
12 changed files with 978 additions and 5 deletions

View File

@ -40,6 +40,7 @@ from nova.conf import ironic
from nova.conf import key_manager
from nova.conf import keystone
from nova.conf import libvirt
from nova.conf import manila
from nova.conf import mks
from nova.conf import netconf
from nova.conf import neutron
@ -82,6 +83,7 @@ devices.register_opts(CONF)
ephemeral_storage.register_opts(CONF)
glance.register_opts(CONF)
guestfs.register_opts(CONF)
manila.register_opts(CONF)
mks.register_opts(CONF)
imagecache.register_opts(CONF)
ironic.register_opts(CONF)

58
nova/conf/manila.py Normal file
View File

@ -0,0 +1,58 @@
# 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 keystoneauth1 import loading as ks_loading
from oslo_config import cfg
from nova.conf import utils as confutils
DEFAULT_SERVICE_TYPE = 'shared-file-system'
manila_group = cfg.OptGroup(
'manila',
title='Manila Options',
help="Configuration options for the share-file-system service")
manila_opts = [
cfg.IntOpt('share_apply_policy_timeout',
default=10,
help="""
Timeout period for share policy application.
Maximum duration to await a response from the Manila service for the
application of a share policy before experiencing a timeout.
0 means do not wait (0s).
Possible values:
* A positive integer or 0 (default value is 10).
"""),
]
def register_opts(conf):
conf.register_group(manila_group)
conf.register_opts(manila_opts, group=manila_group)
ks_loading.register_session_conf_options(conf, manila_group.name)
ks_loading.register_auth_conf_options(conf, manila_group.name)
confutils.register_ksa_opts(conf, manila_group, DEFAULT_SERVICE_TYPE)
def list_opts():
return {
manila_group.name: (
manila_opts +
ks_loading.get_session_conf_options() +
ks_loading.get_auth_common_conf_options() +
ks_loading.get_auth_plugin_conf_options('v3password'))
}

View File

@ -115,7 +115,7 @@ class RequestContext(context.RequestContext):
self.service_catalog = [s for s in service_catalog
if s.get('type') in ('image', 'block-storage', 'volumev3',
'key-manager', 'placement', 'network',
'accelerator')]
'accelerator', 'sharev2')]
else:
# if list is empty or none
self.service_catalog = []

View File

@ -143,6 +143,10 @@ class CinderConnectionFailed(NovaException):
msg_fmt = _("Connection to cinder host failed: %(reason)s")
class ManilaConnectionFailed(NovaException):
msg_fmt = _("Connection to manila service failed: %(reason)s")
class UnsupportedCinderAPIVersion(NovaException):
msg_fmt = _('Nova does not support Cinder API version %(version)s')
@ -704,6 +708,14 @@ class ShareNotFound(NotFound):
msg_fmt = _("Share %(share_id)s could not be found.")
class ShareMappingAlreadyExists(NotFound):
msg_fmt = _("Share %(share_id)s already associated to this server.")
class ShareProtocolUnknown(NotFound):
msg_fmt = _("Share protocol %(share_proto)s is unknown.")
class ShareUmountError(NovaException):
msg_fmt = _("Share id %(share_id)s umount error "
"from server %(server_id)s.\n"
@ -716,6 +728,23 @@ class ShareMountError(NovaException):
"Reason: %(reason)s.")
class ShareAccessNotFound(NotFound):
msg_fmt = _("Share access from Manila could not be found for "
"share id %(share_id)s.")
class ShareAccessGrantError(NovaException):
msg_fmt = _("Share access could not be granted to "
"share id %(share_id)s.\n"
"Reason: %(reason)s.")
class ShareAccessRemovalError(NovaException):
msg_fmt = _("Share access could not be removed from "
"share id %(share_id)s.\n"
"Reason: %(reason)s.")
class VolumeTypeNotFound(NotFound):
msg_fmt = _("Volume type %(id_or_name)s could not be found.")

0
nova/share/__init__.py Normal file
View File

331
nova/share/manila.py Normal file
View File

@ -0,0 +1,331 @@
# 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.
"""
Handles all requests relating to shares + manila.
"""
from dataclasses import dataclass
import functools
from typing import Optional
from openstack import exceptions as sdk_exc
from oslo_log import log as logging
import nova.conf
from nova import exception
from nova import utils
CONF = nova.conf.CONF
LOG = logging.getLogger(__name__)
MIN_SHARE_FILE_SYSTEM_MICROVERSION = "2.82"
def manilaclient(context):
"""Constructs a manila client object for making API requests.
:return: An openstack.proxy.Proxy object for the specified service_type.
:raise: ConfGroupForServiceTypeNotFound If no conf group name could be
found for the specified service_type.
:raise: ServiceUnavailable if the service is down
"""
return utils.get_sdk_adapter(
"shared-file-system",
check_service=True,
shared_file_system_api_version=MIN_SHARE_FILE_SYSTEM_MICROVERSION,
global_request_id=context.global_id
)
@dataclass(frozen=True)
class Share():
id: str
size: int
availability_zone: Optional[str]
created_at: str
status: str
name: Optional[str]
description: Optional[str]
project_id: str
snapshot_id: Optional[str]
share_network_id: Optional[str]
share_proto: str
export_location: str
metadata: dict
share_type: Optional[str]
is_public: bool
@classmethod
def from_manila_share(cls, manila_share, export_location):
return cls(
id=manila_share.id,
size=manila_share.size,
availability_zone=manila_share.availability_zone,
created_at=manila_share.created_at,
status=manila_share.status,
name=manila_share.name,
description=manila_share.description,
project_id=manila_share.project_id,
snapshot_id=manila_share.snapshot_id,
share_network_id=manila_share.share_network_id,
share_proto=manila_share.share_protocol,
export_location=export_location,
metadata=manila_share.metadata,
share_type=manila_share.share_type,
is_public=manila_share.is_public,
)
@dataclass(frozen=True)
class Access():
id: str
access_level: str
state: str
access_type: str
access_to: str
access_key: Optional[str]
@classmethod
def from_manila_access(cls, manila_access):
return cls(
id=manila_access.id,
access_level=manila_access.access_level,
state=manila_access.state,
access_type=manila_access.access_type,
access_to=manila_access.access_to,
access_key= getattr(manila_access, 'access_key', None)
)
@classmethod
def from_dict(cls, manila_access):
return cls(
id=manila_access['id'],
access_level=manila_access['access_level'],
state=manila_access['state'],
access_type=manila_access['access_type'],
access_to=manila_access['access_to'],
access_key=manila_access['access_key'],
)
def translate_sdk_exception(method):
"""Transforms a manila exception but keeps its traceback intact."""
@functools.wraps(method)
def wrapper(self, *args, **kwargs):
try:
res = method(self, *args, **kwargs)
except (exception.ServiceUnavailable,
exception.ConfGroupForServiceTypeNotFound) as exc:
raise exception.ManilaConnectionFailed(reason=str(exc)) from exc
except (sdk_exc.BadRequestException) as exc:
raise exception.InvalidInput(reason=str(exc)) from exc
except (sdk_exc.ForbiddenException) as exc:
raise exception.Forbidden(str(exc)) from exc
return res
return wrapper
def translate_share_exception(method):
"""Transforms the exception for the share but keeps its traceback intact.
"""
def wrapper(self, *args, **kwargs):
try:
res = method(self, *args, **kwargs)
except (sdk_exc.ResourceNotFound) as exc:
raise exception.ShareNotFound(
share_id=args[1], reason=exc) from exc
except (sdk_exc.BadRequestException) as exc:
raise exception.ShareNotFound(
share_id=args[1], reason=exc) from exc
return res
return translate_sdk_exception(wrapper)
def translate_allow_exception(method):
"""Transforms the exception for allow but keeps its traceback intact.
"""
def wrapper(self, *args, **kwargs):
try:
res = method(self, *args, **kwargs)
except (sdk_exc.BadRequestException) as exc:
raise exception.ShareAccessGrantError(
share_id=args[1], reason=exc) from exc
except (sdk_exc.ResourceNotFound) as exc:
raise exception.ShareNotFound(
share_id=args[1], reason=exc) from exc
return res
return translate_sdk_exception(wrapper)
def translate_deny_exception(method):
"""Transforms the exception for deny but keeps its traceback intact.
"""
def wrapper(self, *args, **kwargs):
try:
res = method(self, *args, **kwargs)
except (sdk_exc.BadRequestException) as exc:
raise exception.ShareAccessRemovalError(
share_id=args[1], reason=exc) from exc
except (sdk_exc.ResourceNotFound) as exc:
raise exception.ShareNotFound(
share_id=args[1], reason=exc) from exc
return res
return translate_sdk_exception(wrapper)
class API(object):
"""API for interacting with the share manager."""
@translate_share_exception
def get(self, context, share_id):
"""Get the details about a share given its ID.
:param share_id: the id of the share to get
:raises: ShareNotFound if the share_id specified is not available.
:returns: Share object.
"""
def filter_export_locations(export_locations):
# Return the preferred path otherwise choose the first one
paths = []
for export_location in export_locations:
if export_location.is_preferred:
return export_location.path
else:
paths.append(export_location.path)
return paths[0]
client = manilaclient(context)
LOG.debug("Get share id:'%s' data from manila", share_id)
share = client.get_share(share_id)
export_locations = client.export_locations(share.id)
export_location = filter_export_locations(export_locations)
return Share.from_manila_share(share, export_location)
@translate_share_exception
def get_access(
self,
context,
share_id,
access_type,
access_to,
):
"""Get share access
:param share_id: the id of the share to get
:param access_type: the type of access ("ip", "cert", "user")
:param access_to: ip:cidr or cert:cn or user:group or user name
:raises: ShareNotFound if the share_id specified is not available.
:returns: Access object or None if there is no access granted to this
share.
"""
LOG.debug("Get share access id for share id:'%s'",
share_id)
access_list = manilaclient(context).access_rules(share_id)
for access in access_list:
if (
access.access_type == access_type and
access.access_to == access_to
):
return Access.from_manila_access(access)
return None
@translate_allow_exception
def allow(
self,
context,
share_id,
access_type,
access_to,
access_level,
):
"""Allow share access
:param share_id: the id of the share
:param access_type: the type of access ("ip", "cert", "user")
:param access_to: ip:cidr or cert:cn or user:group or user name
:param access_level: "ro" for read only or "rw" for read/write
:raises: ShareNotFound if the share_id specified is not available.
:raises: BadRequest if the share already exists.
:raises: ShareAccessGrantError if the answer from manila allow API is
not the one expected.
"""
def check_manila_access_response(access):
if not (
isinstance(access, Access) and
access.access_type == access_type and
access.access_to == access_to and
access.access_level == access_level
):
raise exception.ShareAccessGrantError(share_id=share_id)
LOG.debug("Allow host access to share id:'%s'",
share_id)
access = manilaclient(context).create_access_rule(
share_id,
access_type=access_type,
access_to=access_to,
access_level=access_level,
lock_visibility=True,
lock_deletion=True,
lock_reason="Lock by nova",
)
access = Access.from_manila_access(access)
check_manila_access_response(access)
return access
@translate_deny_exception
def deny(
self,
context,
share_id,
access_type,
access_to,
):
"""Deny share access
:param share_id: the id of the share
:param access_type: the type of access ("ip", "cert", "user")
:param access_to: ip:cidr or cert:cn or user:group or user name
:raises: ShareAccessNotFound if the access_id specified is not
available.
:raises: ShareAccessRemovalError if the manila deny API does not
respond with a status code 202.
"""
client = manilaclient(context)
access = self.get_access(
context,
share_id,
access_type,
access_to,
)
if access:
LOG.debug("Deny host access to share id:'%s'", share_id)
resp = client.delete_access_rule(access.id, share_id)
if resp.status_code != 202:
raise exception.ShareAccessRemovalError(
share_id=share_id, reason=resp.reason
)
else:
raise exception.ShareAccessNotFound(share_id=share_id)

View File

@ -22,6 +22,7 @@ from .glance import GlanceFixture # noqa: F401, H304
from .libvirt import LibvirtFixture # noqa: F401, H304
from .libvirt_imagebackend import \
LibvirtImageBackendFixture # noqa: F401, H304
from .manila import ManilaFixture # noqa: F401, H304
from .neutron import NeutronFixture # noqa: F401, H304
from .notifications import NotificationFixture # noqa: F401, H304
from .nova import * # noqa: F401, F403, H303, H304

111
nova/tests/fixtures/manila.py vendored Normal file
View File

@ -0,0 +1,111 @@
# 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 fixtures
import nova
from oslo_config import cfg
from oslo_log import log as logging
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
class ManilaShare():
def __init__(self, share_id, proto="NFS"):
self.id = share_id
self.size = 1
self.availability_zone = "nova"
self.created_at = "2015-09-18T10:25:24.000000"
self.status = "available"
self.name = "share_London"
self.description = "My custom share London"
self.project_id = "6a6a9c9eee154e9cb8cec487b98d36ab"
self.snapshot_id = None
self.share_network_id = "713df749-aac0-4a54-af52-10f6c991e80c"
self.share_protocol = proto
self.metadata = {"project": "my_app", "aim": "doc"}
self.share_type = "25747776-08e5-494f-ab40-a64b9d20d8f7"
self.volume_type = "default"
self.is_public = True
class ManilaAccess():
def __init__(self, access_type="ip"):
self.access_level = "rw"
self.state = "active"
self.id = "507bf114-36f2-4f56-8cf4-857985ca87c1"
if access_type == "ip":
self.access_type = "ip"
self.access_to = "192.168.0.1"
self.access_key = None
elif access_type == "cephx":
self.access_type = "cephx"
self.access_to = "nova"
self.access_key = "mykey"
class ManilaFixture(fixtures.Fixture):
"""Fixture that mocks Manila APIs used by nova/share/manila.py"""
def setUp(self):
super().setUp()
self.share_access = set()
self.mock_get = self.useFixture(fixtures.MockPatch(
'nova.share.manila.API.get',
side_effect=self.fake_get)).mock
self.mock_get_access = self.useFixture(fixtures.MockPatch(
'nova.share.manila.API.get_access',
side_effect=self.fake_get_access)).mock
self.mock_allow = self.useFixture(fixtures.MockPatch(
'nova.share.manila.API.allow',
side_effect=self.fake_allow)).mock
self.mock_deny = self.useFixture(fixtures.MockPatch(
'nova.share.manila.API.deny',
side_effect=self.fake_deny)).mock
def fake_get(self, context, share_id):
manila_share = ManilaShare(share_id)
export_location = "10.0.0.50:/mnt/foo"
return nova.share.manila.Share.from_manila_share(
manila_share, export_location
)
def fake_get_cephfs(self, context, share_id):
manila_share = ManilaShare(share_id, "CEPHFS")
export_location = "10.0.0.50:/mnt/foo"
return nova.share.manila.Share.from_manila_share(
manila_share, export_location
)
def fake_get_access(self, context, share_id, access_type, access_to):
if share_id not in self.share_access:
return None
else:
access = ManilaAccess()
return nova.share.manila.Access.from_manila_access(access)
def fake_get_access_cephfs(
self, context, share_id, access_type, access_to
):
access = ManilaAccess(access_type="cephx")
return access
def fake_allow(
self, context, share_id, access_type, access_to, access_level
):
self.share_access.add(share_id)
self.fake_get_access(context, share_id, access_type, access_to)
def fake_deny(self, context, share_id, access_type, access_to):
self.share_access.discard(share_id)
return 202

View File

View File

@ -0,0 +1,432 @@
# 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 requests import Response
import fixtures
import nova.conf
from nova import context as nova_context
from nova import exception
from nova.share import manila
from nova import test
from openstack import exceptions as sdk_exc
from openstack.shared_file_system.v2 import (
share_access_rule as sdk_share_access_rule
)
from openstack.shared_file_system.v2 import (
share_export_locations as sdk_share_export_locations
)
from openstack.shared_file_system.v2 import share as sdk_share
from openstack import utils
from unittest import mock
from nova.tests.unit.api.openstack import fakes
CONF = nova.conf.CONF
def stub_share(share_id):
share = sdk_share.Share()
share.id = share_id
share.size = 1
share.availability_zone = "nova"
share.created_at = "2015-09-18T10:25:24.000000"
share.status = "available"
share.name = "share_London"
share.description = "My custom share London"
share.project_id = "16e1ab15c35a457e9c2b2aa189f544e1"
share.snapshot_id = None
share.share_network_id = "713df749-aac0-4a54-af52-10f6c991e80c"
share.share_protocol = "NFS"
share.metadata = {
"project": "my_app",
"aim": "doc"
}
share.share_type = "25747776-08e5-494f-ab40-a64b9d20d8f7"
share.is_public = True
share.share_server_id = "e268f4aa-d571-43dd-9ab3-f49ad06ffaef"
share.host = "manila2@generic1#GENERIC1"
share.location = utils.Munch(
{
"cloud": "envvars",
"region_name": "RegionOne",
"zone": "manila-zone-0",
"project": utils.Munch(
{
"id": "bce4fcc3bd0d4c598f610cb45ec5c5ba",
"name": "demo",
"domain_id": "default",
"domain_name": None,
}
),
}
)
return share
def stub_export_locations():
export_locations = []
export_location = sdk_share_export_locations.ShareExportLocation()
export_location.id = "b6bd76ce-12a2-42a9-a30a-8a43b503867d"
export_location.path = (
"10.0.0.3:/shares/share-e1c2d35e-fe67-4028-ad7a-45f668732b1d"
)
export_location.is_preferred = True
export_location.share_instance_id = (
"e1c2d35e-fe67-4028-ad7a-45f668732b1d"
)
export_location.is_admin = True
export_location.share_instance_id = "e1c2d35e-fe67-4028-ad7a-45f668732b1d"
export_location.location = utils.Munch(
{
"cloud": "envvars",
"region_name": "RegionOne",
"zone": None,
"project": utils.Munch(
{
"id": "bce4fcc3bd0d4c598f610cb45ec5c5ba",
"name": "demo",
"domain_id": "default",
"domain_name": None,
}
),
}
)
export_locations.append(export_location)
for item in export_locations:
yield item
def stub_access_list():
access_list = []
access_list.append(stub_access())
for access in access_list:
yield access
def stub_access():
access = sdk_share_access_rule.ShareAccessRule()
access.id = "a25b2df3-90bd-4add-afa6-5f0dbbd50452"
access.access_level = "rw"
access.access_to = "0.0.0.0/0"
access.access_type = "ip"
access.state = "active"
access.access_key = None
access.created_at = "2023-07-21T15:20:01.812350"
access.updated_at = "2023-07-21T15:20:01.812350"
access.metadata = {}
access.location = utils.Munch(
{
"cloud": "envvars",
"region_name": "RegionOne",
"zone": None,
"project": utils.Munch(
{
"id": "bce4fcc3bd0d4c598f610cb45ec5c5ba",
"name": "demo",
"domain_id": "default",
"domain_name": None,
}
),
}
)
return access
class BaseManilaTestCase(object):
project_id = fakes.FAKE_PROJECT_ID
def setUp(self):
super(BaseManilaTestCase, self).setUp()
self.mock_get_confgrp = self.useFixture(fixtures.MockPatch(
'nova.utils._get_conf_group')).mock
self.mock_get_auth_sess = self.useFixture(fixtures.MockPatch(
'nova.utils._get_auth_and_session')).mock
self.mock_get_auth_sess.return_value = (None, mock.sentinel.session)
self.service_type = 'shared-file-system'
self.mock_connection = self.useFixture(
fixtures.MockPatch(
"nova.utils.connection.Connection", side_effect=self.fake_conn
)
).mock
# We need to stub the CONF global in nova.utils to assert that the
# Connection constructor picks it up.
self.mock_conf = self.useFixture(fixtures.MockPatch(
'nova.utils.CONF')).mock
self.api = manila.API()
self.context = nova_context.RequestContext(
user_id="fake_user", project_id=self.project_id
)
def fake_conn(self, *args, **kwargs):
class FakeConnection(object):
def __init__(self):
self.shared_file_system = FakeConnectionShareV2Proxy()
class FakeConnectionShareV2Proxy(object):
def __init__(self):
pass
def get_share(self, share_id):
if share_id == 'nonexisting':
raise sdk_exc.ResourceNotFound
return stub_share(share_id)
def export_locations(self, share_id):
return stub_export_locations()
def access_rules(self, share_id):
if share_id == 'nonexisting':
raise sdk_exc.ResourceNotFound
if share_id == 'nonexisting2':
raise sdk_exc.ResourceNotFound
if share_id == '4567':
return []
return stub_access_list()
def create_access_rule(self, share_id, **kwargs):
if share_id == '2345':
raise sdk_exc.BadRequestException
if share_id == 'nonexisting':
raise sdk_exc.ResourceNotFound
return stub_access()
def delete_access_rule(self, access_id, share_id):
if share_id == 'nonexisting':
raise sdk_exc.ResourceNotFound
res = Response()
res.status_code = 202
res.reason = "Internal error"
if share_id == '2345':
res.status_code = 500
return res
return FakeConnection()
def create_client(self, context):
return manila.manilaclient(context)
def test_client(self):
client = self.create_client(self.context)
self.assertTrue(hasattr(client, 'get_share'))
self.assertTrue(hasattr(client, 'export_locations'))
self.assertTrue(hasattr(client, 'access_rules'))
self.assertTrue(hasattr(client, 'create_access_rule'))
self.assertTrue(hasattr(client, 'delete_access_rule'))
class ManilaTestCase(BaseManilaTestCase, test.NoDBTestCase):
def test_get_fails_non_existing_share(self):
"""Tests that we fail if trying to get an
non existing share.
"""
exc = self.assertRaises(
exception.ShareNotFound, self.api.get, self.context, "nonexisting"
)
self.assertIn("Share nonexisting could not be found.", exc.message)
def test_get_share(self):
"""Tests that we manage to get a share.
"""
share = self.api.get(self.context, '1234')
self.assertIsInstance(share, manila.Share)
self.assertEqual('1234', share.id)
self.assertEqual(1, share.size)
self.assertEqual('nova', share.availability_zone)
self.assertEqual('2015-09-18T10:25:24.000000',
share.created_at)
self.assertEqual('available', share.status)
self.assertEqual('share_London', share.name)
self.assertEqual('My custom share London',
share.description)
self.assertEqual('16e1ab15c35a457e9c2b2aa189f544e1',
share.project_id)
self.assertIsNone(share.snapshot_id)
self.assertEqual(
'713df749-aac0-4a54-af52-10f6c991e80c',
share.share_network_id)
self.assertEqual('NFS', share.share_proto)
self.assertEqual(share.export_location,
"10.0.0.3:/shares/"
"share-e1c2d35e-fe67-4028-ad7a-45f668732b1d"
)
self.assertEqual({"project": "my_app", "aim": "doc"},
share.metadata)
self.assertEqual(
'25747776-08e5-494f-ab40-a64b9d20d8f7',
share.share_type)
self.assertTrue(share.is_public)
def test_get_access_fails_non_existing_share(self):
"""Tests that we fail if trying to get an access on a
non existing share.
"""
exc = self.assertRaises(
exception.ShareNotFound,
self.api.get_access,
self.context,
"nonexisting",
"ip",
"0.0.0.0/0",
)
self.assertIn("Share nonexisting could not be found.", exc.message)
exc = self.assertRaises(
exception.ShareNotFound,
self.api.get_access,
self.context,
"nonexisting2",
"ip",
"0.0.0.0/0",
)
self.assertIn("Share nonexisting2 could not be found.", exc.message)
def test_get_access(self):
"""Tests that we manage to get an access id based on access_type and
access_to parameters.
"""
access = self.api.get_access(self.context, '1234', 'ip', '0.0.0.0/0')
self.assertEqual('a25b2df3-90bd-4add-afa6-5f0dbbd50452', access.id)
self.assertEqual('rw', access.access_level)
self.assertEqual('active', access.state)
self.assertEqual('ip', access.access_type)
self.assertEqual('0.0.0.0/0', access.access_to)
self.assertIsNone(access.access_key)
def test_get_access_not_existing(self):
"""Tests that we get None if the access id does not exist.
"""
access = self.api.get_access(
self.context, "1234", "ip", "192.168.0.1/32"
)
self.assertIsNone(access)
def test_allow_access_fails_non_existing_share(self):
"""Tests that we fail if trying to allow an
non existing share.
"""
exc = self.assertRaises(
exception.ShareNotFound,
self.api.allow,
self.context,
"nonexisting",
"ip",
"0.0.0.0/0",
"rw",
)
self.assertIn("Share nonexisting could not be found.", exc.message)
def test_allow_access(self):
"""Tests that we manage to allow access to a share.
"""
access = self.api.allow(self.context, '1234', 'ip', '0.0.0.0/0', 'rw')
self.assertEqual('a25b2df3-90bd-4add-afa6-5f0dbbd50452', access.id)
self.assertEqual('rw', access.access_level)
self.assertEqual('active', access.state)
self.assertEqual('ip', access.access_type)
self.assertEqual('0.0.0.0/0', access.access_to)
self.assertIsNone(access.access_key)
def test_allow_access_fails_already_exists(self):
"""Tests that we have an exception is the share already exists.
"""
exc = self.assertRaises(
exception.ShareAccessGrantError,
self.api.allow,
self.context,
'2345',
'ip',
'0.0.0.0/0',
'rw'
)
self.assertIn(
'Share access could not be granted to share',
exc.message)
def test_deny_access_fails_non_existing_share(self):
"""Tests that we fail if trying to deny an
non existing share.
"""
exc = self.assertRaises(
exception.ShareNotFound,
self.api.deny,
self.context,
"nonexisting",
"ip",
"0.0.0.0/0",
)
self.assertIn("Share nonexisting could not be found.", exc.message)
def test_deny_access(self):
"""Tests that we manage to deny access to a share.
"""
self.api.deny(
self.context,
'1234',
'ip',
'0.0.0.0/0'
)
def test_deny_access_fails_id_missing(self):
"""Tests that we fail if something wrong happens calling deny method.
"""
exc = self.assertRaises(exception.ShareAccessRemovalError,
self.api.deny,
self.context,
'2345',
'ip',
'0.0.0.0/0'
)
self.assertIn(
'Share access could not be removed from',
exc.message)
self.assertEqual(
500,
exc.code)
def test_deny_access_fails_access_not_found(self):
"""Tests that we fail if access is missing.
"""
exc = self.assertRaises(exception.ShareAccessNotFound,
self.api.deny,
self.context,
'4567',
'ip',
'0.0.0.0/0'
)
self.assertIn(
'Share access from Manila could not be found',
exc.message)
self.assertEqual(
404,
exc.code)

View File

@ -965,7 +965,9 @@ def get_ksa_adapter(service_type, ksa_auth=None, ksa_session=None,
min_version=min_version, max_version=max_version, raise_exc=False)
def get_sdk_adapter(service_type, check_service=False, conf_group=None):
def get_sdk_adapter(
service_type, check_service=False, conf_group=None, **kwargs
):
"""Construct an openstacksdk-brokered Adapter for a given service type.
We expect to find a conf group whose name corresponds to the service_type's
@ -978,6 +980,9 @@ def get_sdk_adapter(service_type, check_service=False, conf_group=None):
service is alive, raising ServiceUnavailable if it is not.
:param conf_group: String name of the conf group to use, otherwise the name
of the service_type will be used.
:param kwargs: Additional arguments to pass to the Adapter constructor.
Mainly used to pass microversion to a specific service,
e.g. shared_file_system_api_version="2.82".
:return: An openstack.proxy.Proxy object for the specified service_type.
:raise: ConfGroupForServiceTypeNotFound If no conf group name could be
found for the specified service_type.
@ -988,12 +993,16 @@ def get_sdk_adapter(service_type, check_service=False, conf_group=None):
try:
conn = connection.Connection(
session=sess, oslo_conf=CONF, service_types={service_type},
strict_proxies=check_service)
strict_proxies=check_service, **kwargs)
except sdk_exc.ServiceDiscoveryException as e:
raise exception.ServiceUnavailable(
_("The %(service_type)s service is unavailable: %(error)s") %
{'service_type': service_type, 'error': str(e)})
return getattr(conn, service_type)
# The replace('-', '_') below is to handle service names that use
# hyphens and SDK attributes that use underscores.
# e.g. service name --> sdk attribute
# 'shared-file-system' --> 'shared_file_system'
return getattr(conn, service_type.replace('-', '_'))
def get_endpoint(ksa_adapter):

View File

@ -62,5 +62,5 @@ retrying>=1.3.3 # Apache-2.0
os-service-types>=1.7.0 # Apache-2.0
python-dateutil>=2.7.0 # BSD
futurist>=1.8.0 # Apache-2.0
openstacksdk>=0.35.0 # Apache-2.0
openstacksdk>=4.0.0 # Apache-2.0
PyYAML>=5.1 # MIT