Merge "[Native CephFS] Add messages for async ACL ops" into stable/ussuri
This commit is contained in:
commit
db1dcdd325
|
@ -512,6 +512,10 @@ class InvalidShareAccessLevel(Invalid):
|
||||||
message = _("Invalid or unsupported share access level: %(level)s.")
|
message = _("Invalid or unsupported share access level: %(level)s.")
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidShareAccessType(Invalid):
|
||||||
|
message = _("Invalid or unsupported share access type: %(type)s.")
|
||||||
|
|
||||||
|
|
||||||
class ShareBackendException(ManilaException):
|
class ShareBackendException(ManilaException):
|
||||||
message = _("Share backend error: %(msg)s.")
|
message = _("Share backend error: %(msg)s.")
|
||||||
|
|
||||||
|
|
|
@ -33,15 +33,19 @@ class Action(object):
|
||||||
DELETE = ('007', _('delete'))
|
DELETE = ('007', _('delete'))
|
||||||
EXTEND = ('008', _('extend'))
|
EXTEND = ('008', _('extend'))
|
||||||
SHRINK = ('009', _('shrink'))
|
SHRINK = ('009', _('shrink'))
|
||||||
ALL = (ALLOCATE_HOST,
|
UPDATE_ACCESS_RULES = ('010', _('update access rules'))
|
||||||
CREATE,
|
ALL = (
|
||||||
DELETE_ACCESS_RULES,
|
ALLOCATE_HOST,
|
||||||
PROMOTE,
|
CREATE,
|
||||||
UPDATE,
|
DELETE_ACCESS_RULES,
|
||||||
REVERT_TO_SNAPSHOT,
|
PROMOTE,
|
||||||
DELETE,
|
UPDATE,
|
||||||
EXTEND,
|
REVERT_TO_SNAPSHOT,
|
||||||
SHRINK)
|
DELETE,
|
||||||
|
EXTEND,
|
||||||
|
SHRINK,
|
||||||
|
UPDATE_ACCESS_RULES,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Detail(object):
|
class Detail(object):
|
||||||
|
@ -98,26 +102,39 @@ class Detail(object):
|
||||||
'019',
|
'019',
|
||||||
_("Share Driver does not support shrinking shares."
|
_("Share Driver does not support shrinking shares."
|
||||||
" Shrinking share operation failed."))
|
" Shrinking share operation failed."))
|
||||||
|
FORBIDDEN_CLIENT_ACCESS = (
|
||||||
|
'020',
|
||||||
|
_("Failed to grant access to client. The client ID used may be "
|
||||||
|
"forbidden. You may try again with a different client identifier."))
|
||||||
|
UNSUPPORTED_CLIENT_ACCESS = (
|
||||||
|
'021',
|
||||||
|
_("Failed to grant access to client. The access level or type may "
|
||||||
|
"be unsupported. You may try again with a different access level "
|
||||||
|
"or access type."))
|
||||||
|
|
||||||
ALL = (UNKNOWN_ERROR,
|
ALL = (
|
||||||
NO_VALID_HOST,
|
UNKNOWN_ERROR,
|
||||||
UNEXPECTED_NETWORK,
|
NO_VALID_HOST,
|
||||||
NO_SHARE_SERVER,
|
UNEXPECTED_NETWORK,
|
||||||
NO_ACTIVE_AVAILABLE_REPLICA,
|
NO_SHARE_SERVER,
|
||||||
NO_ACTIVE_REPLICA,
|
NO_ACTIVE_AVAILABLE_REPLICA,
|
||||||
FILTER_AVAILABILITY,
|
NO_ACTIVE_REPLICA,
|
||||||
FILTER_CAPABILITIES,
|
FILTER_AVAILABILITY,
|
||||||
FILTER_CAPACITY,
|
FILTER_CAPABILITIES,
|
||||||
FILTER_DRIVER,
|
FILTER_CAPACITY,
|
||||||
FILTER_IGNORE,
|
FILTER_DRIVER,
|
||||||
FILTER_JSON,
|
FILTER_IGNORE,
|
||||||
FILTER_RETRY,
|
FILTER_JSON,
|
||||||
FILTER_REPLICATION,
|
FILTER_RETRY,
|
||||||
DRIVER_FAILED_EXTEND,
|
FILTER_REPLICATION,
|
||||||
FILTER_CREATE_FROM_SNAPSHOT,
|
DRIVER_FAILED_EXTEND,
|
||||||
DRIVER_FAILED_CREATING_FROM_SNAP,
|
FILTER_CREATE_FROM_SNAPSHOT,
|
||||||
DRIVER_REFUSED_SHRINK,
|
DRIVER_FAILED_CREATING_FROM_SNAP,
|
||||||
DRIVER_FAILED_SHRINK)
|
DRIVER_REFUSED_SHRINK,
|
||||||
|
DRIVER_FAILED_SHRINK,
|
||||||
|
FORBIDDEN_CLIENT_ACCESS,
|
||||||
|
UNSUPPORTED_CLIENT_ACCESS,
|
||||||
|
)
|
||||||
|
|
||||||
# Exception and detail mappings
|
# Exception and detail mappings
|
||||||
EXCEPTION_DETAIL_MAPPINGS = {
|
EXCEPTION_DETAIL_MAPPINGS = {
|
||||||
|
|
|
@ -27,6 +27,8 @@ import six
|
||||||
from manila.common import constants
|
from manila.common import constants
|
||||||
from manila import exception
|
from manila import exception
|
||||||
from manila.i18n import _
|
from manila.i18n import _
|
||||||
|
from manila.message import api as message_api
|
||||||
|
from manila.message import message_field
|
||||||
from manila.share import driver
|
from manila.share import driver
|
||||||
from manila.share.drivers import ganesha
|
from manila.share.drivers import ganesha
|
||||||
from manila.share.drivers.ganesha import utils as ganesha_utils
|
from manila.share.drivers.ganesha import utils as ganesha_utils
|
||||||
|
@ -366,6 +368,7 @@ class NativeProtocolHelper(ganesha.NASHelperBase):
|
||||||
|
|
||||||
def __init__(self, execute, config, **kwargs):
|
def __init__(self, execute, config, **kwargs):
|
||||||
self.volume_client = kwargs.pop('ceph_vol_client')
|
self.volume_client = kwargs.pop('ceph_vol_client')
|
||||||
|
self.message_api = message_api.API()
|
||||||
super(NativeProtocolHelper, self).__init__(execute, config,
|
super(NativeProtocolHelper, self).__init__(execute, config,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
|
@ -395,8 +398,7 @@ class NativeProtocolHelper(ganesha.NASHelperBase):
|
||||||
|
|
||||||
def _allow_access(self, context, share, access, share_server=None):
|
def _allow_access(self, context, share, access, share_server=None):
|
||||||
if access['access_type'] != CEPHX_ACCESS_TYPE:
|
if access['access_type'] != CEPHX_ACCESS_TYPE:
|
||||||
raise exception.InvalidShareAccess(
|
raise exception.InvalidShareAccessType(type=access['access_type'])
|
||||||
reason=_("Only 'cephx' access type allowed."))
|
|
||||||
|
|
||||||
ceph_auth_id = access['access_to']
|
ceph_auth_id = access['access_to']
|
||||||
|
|
||||||
|
@ -409,7 +411,7 @@ class NativeProtocolHelper(ganesha.NASHelperBase):
|
||||||
error_message = (_('Ceph authentication ID %s must be different '
|
error_message = (_('Ceph authentication ID %s must be different '
|
||||||
'than the one the Manila service uses.') %
|
'than the one the Manila service uses.') %
|
||||||
ceph_auth_id)
|
ceph_auth_id)
|
||||||
raise exception.InvalidInput(message=error_message)
|
raise exception.InvalidShareAccess(reason=error_message)
|
||||||
|
|
||||||
if not getattr(self.volume_client, 'version', None):
|
if not getattr(self.volume_client, 'version', None):
|
||||||
if access['access_level'] == constants.ACCESS_LEVEL_RO:
|
if access['access_level'] == constants.ACCESS_LEVEL_RO:
|
||||||
|
@ -422,9 +424,18 @@ class NativeProtocolHelper(ganesha.NASHelperBase):
|
||||||
cephfs_share_path(share), ceph_auth_id)
|
cephfs_share_path(share), ceph_auth_id)
|
||||||
else:
|
else:
|
||||||
readonly = access['access_level'] == constants.ACCESS_LEVEL_RO
|
readonly = access['access_level'] == constants.ACCESS_LEVEL_RO
|
||||||
auth_result = self.volume_client.authorize(
|
try:
|
||||||
cephfs_share_path(share), ceph_auth_id, readonly=readonly,
|
auth_result = self.volume_client.authorize(
|
||||||
tenant_id=share['project_id'])
|
cephfs_share_path(share), ceph_auth_id, readonly=readonly,
|
||||||
|
tenant_id=share['project_id'])
|
||||||
|
except Exception as e:
|
||||||
|
if 'not allowed' in six.text_type(e).lower():
|
||||||
|
msg = ("Access to client %(client)s is not allowed. "
|
||||||
|
"Reason: %(reason)s")
|
||||||
|
msg_payload = {'client': ceph_auth_id, 'reason': e}
|
||||||
|
raise exception.InvalidShareAccess(
|
||||||
|
reason=msg % msg_payload)
|
||||||
|
raise
|
||||||
|
|
||||||
return auth_result['auth_key']
|
return auth_result['auth_key']
|
||||||
|
|
||||||
|
@ -443,7 +454,7 @@ class NativeProtocolHelper(ganesha.NASHelperBase):
|
||||||
|
|
||||||
def update_access(self, context, share, access_rules, add_rules,
|
def update_access(self, context, share, access_rules, add_rules,
|
||||||
delete_rules, share_server=None):
|
delete_rules, share_server=None):
|
||||||
access_keys = {}
|
access_updates = {}
|
||||||
|
|
||||||
if not (add_rules or delete_rules): # recovery/maintenance mode
|
if not (add_rules or delete_rules): # recovery/maintenance mode
|
||||||
add_rules = access_rules
|
add_rules = access_rules
|
||||||
|
@ -475,13 +486,48 @@ class NativeProtocolHelper(ganesha.NASHelperBase):
|
||||||
# access keys and ensure that after recovery, manila and the Ceph
|
# access keys and ensure that after recovery, manila and the Ceph
|
||||||
# backend are in sync.
|
# backend are in sync.
|
||||||
for rule in add_rules:
|
for rule in add_rules:
|
||||||
access_key = self._allow_access(context, share, rule)
|
try:
|
||||||
access_keys.update({rule['access_id']: {'access_key': access_key}})
|
access_key = self._allow_access(context, share, rule)
|
||||||
|
except (exception.InvalidShareAccessLevel,
|
||||||
|
exception.InvalidShareAccessType):
|
||||||
|
self.message_api.create(
|
||||||
|
context,
|
||||||
|
message_field.Action.UPDATE_ACCESS_RULES,
|
||||||
|
share['project_id'],
|
||||||
|
resource_type=message_field.Resource.SHARE,
|
||||||
|
resource_id=share['share_id'],
|
||||||
|
detail=message_field.Detail.UNSUPPORTED_CLIENT_ACCESS)
|
||||||
|
log_args = {'id': rule['access_id'],
|
||||||
|
'access_level': rule['access_level'],
|
||||||
|
'access_to': rule['access_to']}
|
||||||
|
LOG.exception("Failed to provide %(access_level)s access to "
|
||||||
|
"%(access_to)s (Rule ID: %(id)s). Setting rule "
|
||||||
|
"to 'error' state.", log_args)
|
||||||
|
access_updates.update({rule['access_id']: {'state': 'error'}})
|
||||||
|
except exception.InvalidShareAccess:
|
||||||
|
self.message_api.create(
|
||||||
|
context,
|
||||||
|
message_field.Action.UPDATE_ACCESS_RULES,
|
||||||
|
share['project_id'],
|
||||||
|
resource_type=message_field.Resource.SHARE,
|
||||||
|
resource_id=share['share_id'],
|
||||||
|
detail=message_field.Detail.FORBIDDEN_CLIENT_ACCESS)
|
||||||
|
log_args = {'id': rule['access_id'],
|
||||||
|
'access_level': rule['access_level'],
|
||||||
|
'access_to': rule['access_to']}
|
||||||
|
LOG.exception("Failed to provide %(access_level)s access to "
|
||||||
|
"%(access_to)s (Rule ID: %(id)s). Setting rule "
|
||||||
|
"to 'error' state.", log_args)
|
||||||
|
access_updates.update({rule['access_id']: {'state': 'error'}})
|
||||||
|
else:
|
||||||
|
access_updates.update({
|
||||||
|
rule['access_id']: {'access_key': access_key},
|
||||||
|
})
|
||||||
|
|
||||||
for rule in delete_rules:
|
for rule in delete_rules:
|
||||||
self._deny_access(context, share, rule)
|
self._deny_access(context, share, rule)
|
||||||
|
|
||||||
return access_keys
|
return access_updates
|
||||||
|
|
||||||
def get_configured_ip_versions(self):
|
def get_configured_ip_versions(self):
|
||||||
return [4]
|
return [4]
|
||||||
|
|
|
@ -387,7 +387,7 @@ class NativeProtocolHelperTestCase(test.TestCase):
|
||||||
super(NativeProtocolHelperTestCase, self).setUp()
|
super(NativeProtocolHelperTestCase, self).setUp()
|
||||||
self.fake_conf = configuration.Configuration(None)
|
self.fake_conf = configuration.Configuration(None)
|
||||||
self._context = context.get_admin_context()
|
self._context = context.get_admin_context()
|
||||||
self._share = fake_share.fake_share(share_proto='CEPHFS')
|
self._share = fake_share.fake_share_instance(share_proto='CEPHFS')
|
||||||
|
|
||||||
self.fake_conf.set_default('driver_handles_share_servers', False)
|
self.fake_conf.set_default('driver_handles_share_servers', False)
|
||||||
|
|
||||||
|
@ -470,7 +470,7 @@ class NativeProtocolHelperTestCase(test.TestCase):
|
||||||
tenant_id=self._share['project_id'])
|
tenant_id=self._share['project_id'])
|
||||||
|
|
||||||
def test_allow_access_wrong_type(self):
|
def test_allow_access_wrong_type(self):
|
||||||
self.assertRaises(exception.InvalidShareAccess,
|
self.assertRaises(exception.InvalidShareAccessType,
|
||||||
self._native_protocol_helper._allow_access,
|
self._native_protocol_helper._allow_access,
|
||||||
self._context, self._share, {
|
self._context, self._share, {
|
||||||
'access_level': constants.ACCESS_LEVEL_RW,
|
'access_level': constants.ACCESS_LEVEL_RW,
|
||||||
|
@ -479,7 +479,7 @@ class NativeProtocolHelperTestCase(test.TestCase):
|
||||||
})
|
})
|
||||||
|
|
||||||
def test_allow_access_same_cephx_id_as_manila_service(self):
|
def test_allow_access_same_cephx_id_as_manila_service(self):
|
||||||
self.assertRaises(exception.InvalidInput,
|
self.assertRaises(exception.InvalidShareAccess,
|
||||||
self._native_protocol_helper._allow_access,
|
self._native_protocol_helper._allow_access,
|
||||||
self._context, self._share, {
|
self._context, self._share, {
|
||||||
'access_level': constants.ACCESS_LEVEL_RW,
|
'access_level': constants.ACCESS_LEVEL_RW,
|
||||||
|
@ -487,6 +487,23 @@ class NativeProtocolHelperTestCase(test.TestCase):
|
||||||
'access_to': 'manila',
|
'access_to': 'manila',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def test_allow_access_to_preexisting_ceph_user(self):
|
||||||
|
|
||||||
|
vc = self._native_protocol_helper.volume_client
|
||||||
|
msg = ("auth ID: admin exists and not created by "
|
||||||
|
"ceph_volume_client. Not allowed to modify")
|
||||||
|
self.mock_object(vc, 'authorize',
|
||||||
|
mock.Mock(side_effect=Exception(msg)))
|
||||||
|
|
||||||
|
self.assertRaises(exception.InvalidShareAccess,
|
||||||
|
self._native_protocol_helper._allow_access,
|
||||||
|
self._context, self._share,
|
||||||
|
{
|
||||||
|
'access_level': constants.ACCESS_LEVEL_RW,
|
||||||
|
'access_type': 'cephx',
|
||||||
|
'access_to': 'admin'
|
||||||
|
})
|
||||||
|
|
||||||
def test_deny_access(self):
|
def test_deny_access(self):
|
||||||
vc = self._native_protocol_helper.volume_client
|
vc = self._native_protocol_helper.volume_client
|
||||||
self._native_protocol_helper._deny_access(self._context, self._share, {
|
self._native_protocol_helper._deny_access(self._context, self._share, {
|
||||||
|
@ -501,7 +518,6 @@ class NativeProtocolHelperTestCase(test.TestCase):
|
||||||
"alice", volume_path=driver.cephfs_share_path(self._share))
|
"alice", volume_path=driver.cephfs_share_path(self._share))
|
||||||
|
|
||||||
def test_update_access_add_rm(self):
|
def test_update_access_add_rm(self):
|
||||||
vc = self._native_protocol_helper.volume_client
|
|
||||||
alice = {
|
alice = {
|
||||||
'id': 'instance_mapping_id1',
|
'id': 'instance_mapping_id1',
|
||||||
'access_id': 'accessid1',
|
'access_id': 'accessid1',
|
||||||
|
@ -512,22 +528,66 @@ class NativeProtocolHelperTestCase(test.TestCase):
|
||||||
bob = {
|
bob = {
|
||||||
'id': 'instance_mapping_id2',
|
'id': 'instance_mapping_id2',
|
||||||
'access_id': 'accessid2',
|
'access_id': 'accessid2',
|
||||||
'access_level': 'rw',
|
'access_level': 'ro',
|
||||||
'access_type': 'cephx',
|
'access_type': 'cephx',
|
||||||
'access_to': 'bob'
|
'access_to': 'bob'
|
||||||
}
|
}
|
||||||
|
manila = {
|
||||||
|
'id': 'instance_mapping_id3',
|
||||||
|
'access_id': 'accessid3',
|
||||||
|
'access_level': 'ro',
|
||||||
|
'access_type': 'cephx',
|
||||||
|
'access_to': 'manila'
|
||||||
|
}
|
||||||
|
admin = {
|
||||||
|
'id': 'instance_mapping_id4',
|
||||||
|
'access_id': 'accessid4',
|
||||||
|
'access_level': 'rw',
|
||||||
|
'access_type': 'cephx',
|
||||||
|
'access_to': 'admin'
|
||||||
|
}
|
||||||
|
dabo = {
|
||||||
|
'id': 'instance_mapping_id5',
|
||||||
|
'access_id': 'accessid5',
|
||||||
|
'access_level': 'rwx',
|
||||||
|
'access_type': 'cephx',
|
||||||
|
'access_to': 'dabo'
|
||||||
|
}
|
||||||
|
|
||||||
|
allow_access_side_effects = [
|
||||||
|
'abc123',
|
||||||
|
exception.InvalidShareAccess(reason='not'),
|
||||||
|
exception.InvalidShareAccess(reason='allowed'),
|
||||||
|
exception.InvalidShareAccessLevel(level='rwx')
|
||||||
|
]
|
||||||
|
self.mock_object(self._native_protocol_helper.message_api, 'create')
|
||||||
|
self.mock_object(self._native_protocol_helper, '_deny_access')
|
||||||
|
self.mock_object(self._native_protocol_helper,
|
||||||
|
'_allow_access',
|
||||||
|
mock.Mock(side_effect=allow_access_side_effects))
|
||||||
|
|
||||||
access_updates = self._native_protocol_helper.update_access(
|
access_updates = self._native_protocol_helper.update_access(
|
||||||
self._context, self._share, access_rules=[alice],
|
self._context,
|
||||||
add_rules=[alice], delete_rules=[bob])
|
self._share,
|
||||||
|
access_rules=[alice, manila, admin, dabo],
|
||||||
|
add_rules=[alice, manila, admin, dabo],
|
||||||
|
delete_rules=[bob])
|
||||||
|
|
||||||
|
expected_access_updates = {
|
||||||
|
'accessid1': {'access_key': 'abc123'},
|
||||||
|
'accessid3': {'state': 'error'},
|
||||||
|
'accessid4': {'state': 'error'},
|
||||||
|
'accessid5': {'state': 'error'}
|
||||||
|
}
|
||||||
|
self.assertEqual(expected_access_updates, access_updates)
|
||||||
|
self._native_protocol_helper._allow_access.assert_has_calls(
|
||||||
|
[mock.call(self._context, self._share, alice),
|
||||||
|
mock.call(self._context, self._share, manila),
|
||||||
|
mock.call(self._context, self._share, admin)])
|
||||||
|
self._native_protocol_helper._deny_access.assert_called_once_with(
|
||||||
|
self._context, self._share, bob)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
{'accessid1': {'access_key': 'abc123'}}, access_updates)
|
3, self._native_protocol_helper.message_api.create.call_count)
|
||||||
vc.authorize.assert_called_once_with(
|
|
||||||
driver.cephfs_share_path(self._share), "alice", readonly=False,
|
|
||||||
tenant_id=self._share['project_id'])
|
|
||||||
vc.deauthorize.assert_called_once_with(
|
|
||||||
driver.cephfs_share_path(self._share), "bob")
|
|
||||||
|
|
||||||
@ddt.data(None, 1)
|
@ddt.data(None, 1)
|
||||||
def test_update_access_all(self, volume_client_version):
|
def test_update_access_all(self, volume_client_version):
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- |
|
||||||
|
New user messages now alert users of possible remediations during access
|
||||||
|
rule creation errors with CephFS shares. This includes hints to users to
|
||||||
|
not use cephx client users that are prohibited by CephFS or the share
|
||||||
|
driver. See `CVE-2020-27781
|
||||||
|
<https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-27781>`_ and
|
||||||
|
bug #1904015 <https://launchpad.net/bugs/1904015>`_ for more details.
|
Loading…
Reference in New Issue