Support for resource locks
Add OSC and SDK interfaces to create, view, update and delete resource locks. Depends-On: I146bc09e4e8a39797e22458ff6860346e11e592e Partially-Implements: bp allow-locking-shares-against-deletion Change-Id: Ib8586a4f80aa8c172d876c6745ae73b7bdaf4705
This commit is contained in:
parent
7a15a2a1ae
commit
1734b45fa5
@ -214,3 +214,10 @@ share servers
|
||||
|
||||
.. autoprogram-cliff:: openstack.share.v2
|
||||
:command: share server *
|
||||
|
||||
==============
|
||||
resource locks
|
||||
==============
|
||||
|
||||
.. autoprogram-cliff:: openstack.share.v2
|
||||
:command: share lock *
|
||||
|
@ -27,7 +27,7 @@ from manilaclient import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
MAX_VERSION = '2.79'
|
||||
MAX_VERSION = '2.81'
|
||||
MIN_VERSION = '2.0'
|
||||
DEPRECATED_VERSION = '1.0'
|
||||
_VERSIONED_METHOD_MAP = {}
|
||||
|
@ -79,6 +79,16 @@ SHARE_TRANSFER_SORT_KEY_VALUES = (
|
||||
'expires_at',
|
||||
)
|
||||
|
||||
RESOURCE_LOCK_SORT_KEY_VALUES = (
|
||||
'id',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'resource_id',
|
||||
'resource_type',
|
||||
'resource_action'
|
||||
'lock_reason',
|
||||
)
|
||||
|
||||
TASK_STATE_MIGRATION_SUCCESS = 'migration_success'
|
||||
TASK_STATE_MIGRATION_ERROR = 'migration_error'
|
||||
TASK_STATE_MIGRATION_CANCELLED = 'migration_cancelled'
|
||||
@ -131,3 +141,4 @@ GROUP_BOOL_SPECS = (
|
||||
REPLICA_GRADUATION_VERSION = '2.56'
|
||||
REPLICA_PRE_GRADUATION_VERSION = '2.55'
|
||||
SHARE_TRANSFER_VERSION = '2.77'
|
||||
RESOURCE_LOCK_VERSION = '2.81'
|
||||
|
411
manilaclient/osc/v2/resource_locks.py
Normal file
411
manilaclient/osc/v2/resource_locks.py
Normal file
@ -0,0 +1,411 @@
|
||||
# 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 openstackclient.identity import common as identity_common
|
||||
from osc_lib.command import command
|
||||
from osc_lib import exceptions
|
||||
from osc_lib import utils as osc_utils
|
||||
from oslo_utils import uuidutils
|
||||
|
||||
from manilaclient.common._i18n import _
|
||||
from manilaclient.common.apiclient import utils as apiutils
|
||||
from manilaclient.common import constants
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
LOCK_DETAIL_ATTRIBUTES = [
|
||||
'ID',
|
||||
'Resource Id',
|
||||
'Resource Type',
|
||||
'Resource Action',
|
||||
'Lock Context',
|
||||
'User Id',
|
||||
'Project Id',
|
||||
'Created At',
|
||||
'Updated At',
|
||||
'Lock Reason',
|
||||
]
|
||||
|
||||
LOCK_SUMMARY_ATTRIBUTES = [
|
||||
'ID',
|
||||
'Resource Id',
|
||||
'Resource Type',
|
||||
'Resource Action',
|
||||
]
|
||||
|
||||
RESOURCE_TYPE_MANAGERS = {
|
||||
'share': 'shares',
|
||||
}
|
||||
|
||||
|
||||
class CreateResourceLock(command.ShowOne):
|
||||
"""Create a new resource lock."""
|
||||
_description = _("Lock a resource action from occurring on a resource")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(CreateResourceLock, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'resource',
|
||||
metavar='<resource_name_or_id>',
|
||||
help='Name or ID of resource to lock.')
|
||||
parser.add_argument(
|
||||
'resource_type',
|
||||
metavar='<resource_type>',
|
||||
help='Type of the resource (e.g.: share, access).')
|
||||
parser.add_argument(
|
||||
'--resource-action',
|
||||
'--resource_action',
|
||||
metavar='<resource_action>',
|
||||
default='delete',
|
||||
help='Action to lock on the resource (default="delete")')
|
||||
parser.add_argument(
|
||||
'--lock-reason',
|
||||
'--lock_reason',
|
||||
'--reason',
|
||||
metavar='<lock_reason>',
|
||||
help='Reason for the resource lock.')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
share_client = self.app.client_manager.share
|
||||
resource_type = parsed_args.resource_type
|
||||
if resource_type not in RESOURCE_TYPE_MANAGERS:
|
||||
raise exceptions.CommandError(_("Unsupported resource type"))
|
||||
res_manager = RESOURCE_TYPE_MANAGERS[resource_type]
|
||||
|
||||
resource = osc_utils.find_resource(getattr(share_client, res_manager),
|
||||
parsed_args.resource)
|
||||
resource_lock = share_client.resource_locks.create(
|
||||
resource.id,
|
||||
resource_type,
|
||||
parsed_args.resource_action,
|
||||
parsed_args.lock_reason
|
||||
)
|
||||
|
||||
resource_lock._info.pop('links', None)
|
||||
|
||||
return self.dict2columns(resource_lock._info)
|
||||
|
||||
|
||||
class DeleteResourceLock(command.Command):
|
||||
"""Remove one or more resource locks."""
|
||||
_description = _("Remove one or more resource locks")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(DeleteResourceLock, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'lock',
|
||||
metavar='<lock>',
|
||||
nargs='+',
|
||||
help='ID(s) of the lock(s).')
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
share_client = self.app.client_manager.share
|
||||
failure_count = 0
|
||||
|
||||
for lock in parsed_args.lock:
|
||||
try:
|
||||
lock = apiutils.find_resource(
|
||||
share_client.resource_locks,
|
||||
lock
|
||||
)
|
||||
lock.delete()
|
||||
except Exception as e:
|
||||
failure_count += 1
|
||||
LOG.error(_(
|
||||
"Failed to delete %(lock)s: %(e)s"),
|
||||
{'lock': lock, 'e': e})
|
||||
|
||||
if failure_count > 0:
|
||||
raise exceptions.CommandError(_(
|
||||
"Unable to delete some or all of the specified locks."))
|
||||
|
||||
|
||||
class ListResourceLock(command.Lister):
|
||||
"""Lists all resource locks."""
|
||||
_description = _("Lists all resource locks")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ListResourceLock, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'--all-projects',
|
||||
action='store_true',
|
||||
help=_("Filter resource locks for all projects. (Admin only).")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--project',
|
||||
default=None,
|
||||
help=_("Filter resource locks for specific project by name or ID, "
|
||||
"combine with --all-projects (Admin only).")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--user',
|
||||
default=None,
|
||||
help=_("Filter resource locks for specific user by name or ID, "
|
||||
"combine with --all-projects to search across projects "
|
||||
"(Admin only).")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--id',
|
||||
metavar='<id>',
|
||||
default=None,
|
||||
help='Filter resource locks by ID. Default=None.')
|
||||
parser.add_argument(
|
||||
'--resource',
|
||||
'--resource-id',
|
||||
'--resource_id',
|
||||
default=None,
|
||||
metavar='<resource-id>',
|
||||
dest='resource',
|
||||
help=_("Filter resource locks for a resource by ID, specify "
|
||||
"--resource-type to look up by name.")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--resource-type',
|
||||
'--resource_type',
|
||||
default=None,
|
||||
metavar='<resource_type>',
|
||||
help=_("Filter resource locks by type of resource.")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--resource-action',
|
||||
'--resource_action',
|
||||
default=None,
|
||||
metavar='<resource_action>',
|
||||
help=_("Filter resource locks by resource action.")
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--lock-context',
|
||||
'--lock_context',
|
||||
'--context',
|
||||
default=None,
|
||||
choices=['user', 'admin', 'service'],
|
||||
metavar='<lock_context>',
|
||||
help=_("Filter resource locks by context.")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--since',
|
||||
default=None,
|
||||
metavar='<created_since>',
|
||||
help=_("Filter resource locks created since given date. "
|
||||
"The date format must be conforming to ISO8601. ")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--before',
|
||||
default=None,
|
||||
metavar='<created_before>',
|
||||
help=_("Filter resource locks created before given date. "
|
||||
"The date format must be conforming to ISO8601. ")
|
||||
)
|
||||
parser.add_argument(
|
||||
'--limit',
|
||||
metavar='<limit>',
|
||||
type=int,
|
||||
default=None,
|
||||
help=_("Number of resource locks to list. (Default=None)"))
|
||||
parser.add_argument(
|
||||
'--offset',
|
||||
metavar="<offset>",
|
||||
default=None,
|
||||
help='Starting position of resource lock records '
|
||||
'in a paginated list.')
|
||||
parser.add_argument(
|
||||
'--sort-key', '--sort_key',
|
||||
metavar='<sort_key>',
|
||||
type=str,
|
||||
default=None,
|
||||
choices=constants.RESOURCE_LOCK_SORT_KEY_VALUES,
|
||||
help='Key to be sorted, available keys are %(keys)s. '
|
||||
'Default=None.'
|
||||
% {'keys': constants.RESOURCE_LOCK_SORT_KEY_VALUES})
|
||||
parser.add_argument(
|
||||
'--sort-dir', '--sort_dir',
|
||||
metavar='<sort_dir>',
|
||||
type=str,
|
||||
default=None,
|
||||
choices=constants.SORT_DIR_VALUES,
|
||||
help='Sort direction, available values are %(values)s. '
|
||||
'OPTIONAL: Default=None.' % {
|
||||
'values': constants.SORT_DIR_VALUES})
|
||||
parser.add_argument(
|
||||
'--detailed',
|
||||
dest='detailed',
|
||||
metavar='<0|1>',
|
||||
nargs='?',
|
||||
type=int,
|
||||
const=1,
|
||||
default=0,
|
||||
help="Show detailed information about filtered resource locks.")
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
share_client = self.app.client_manager.share
|
||||
|
||||
columns = (
|
||||
LOCK_SUMMARY_ATTRIBUTES
|
||||
if not parsed_args.detailed
|
||||
else LOCK_DETAIL_ATTRIBUTES
|
||||
)
|
||||
|
||||
project_id = None
|
||||
user_id = None
|
||||
|
||||
if parsed_args.project:
|
||||
project_id = identity_common.find_project(
|
||||
identity_client,
|
||||
parsed_args.project,
|
||||
parsed_args.project_domain).id
|
||||
if parsed_args.user:
|
||||
user_id = identity_common.find_user(identity_client,
|
||||
parsed_args.user,
|
||||
parsed_args.user_domain).id
|
||||
# set all_projects when using project option
|
||||
all_projects = bool(parsed_args.project) or parsed_args.all_projects
|
||||
|
||||
resource_id = parsed_args.resource
|
||||
resource_type = parsed_args.resource_type
|
||||
if resource_type is not None:
|
||||
if resource_type not in RESOURCE_TYPE_MANAGERS:
|
||||
raise exceptions.CommandError(_("Unsupported resource type"))
|
||||
if resource_id is not None:
|
||||
res_manager = RESOURCE_TYPE_MANAGERS[resource_type]
|
||||
resource_id = osc_utils.find_resource(
|
||||
getattr(share_client, res_manager),
|
||||
parsed_args.resource
|
||||
).id
|
||||
elif resource_id and not uuidutils.is_uuid_like(resource_id):
|
||||
raise exceptions.CommandError(
|
||||
_("Provide resource ID or specify --resource-type."))
|
||||
|
||||
search_opts = {
|
||||
'all_projects': all_projects,
|
||||
'project_id': project_id,
|
||||
'user_id': user_id,
|
||||
'id': parsed_args.id,
|
||||
'resource_id': resource_id,
|
||||
'resource_type': parsed_args.resource_type,
|
||||
'resource_action': parsed_args.resource_action,
|
||||
'lock_context': parsed_args.lock_context,
|
||||
'created_before': parsed_args.before,
|
||||
'created_since': parsed_args.since,
|
||||
'limit': parsed_args.limit,
|
||||
'offset': parsed_args.offset,
|
||||
}
|
||||
|
||||
resource_locks = share_client.resource_locks.list(
|
||||
search_opts=search_opts,
|
||||
sort_key=parsed_args.sort_key,
|
||||
sort_dir=parsed_args.sort_dir
|
||||
)
|
||||
|
||||
return (columns, (osc_utils.get_item_properties
|
||||
(m, columns) for m in resource_locks))
|
||||
|
||||
|
||||
class ShowResourceLock(command.ShowOne):
|
||||
"""Show details about a resource lock."""
|
||||
_description = _("Show details about a resource lock")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(ShowResourceLock, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'lock',
|
||||
metavar='<lock>',
|
||||
help=_('ID of resource lock to show.'))
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
share_client = self.app.client_manager.share
|
||||
|
||||
resource_lock = apiutils.find_resource(
|
||||
share_client.resource_locks,
|
||||
parsed_args.lock)
|
||||
|
||||
return (
|
||||
LOCK_DETAIL_ATTRIBUTES,
|
||||
osc_utils.get_dict_properties(resource_lock._info,
|
||||
LOCK_DETAIL_ATTRIBUTES)
|
||||
)
|
||||
|
||||
|
||||
class SetResourceLock(command.Command):
|
||||
"""Set resource lock properties."""
|
||||
_description = _("Update resource lock properties")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(SetResourceLock, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'lock',
|
||||
metavar='<lock>',
|
||||
help='ID of lock to update.')
|
||||
parser.add_argument(
|
||||
'--resource-action',
|
||||
'--resource_action',
|
||||
metavar='<resource_action>',
|
||||
help='Resource action to set in the resource lock')
|
||||
parser.add_argument(
|
||||
'--lock-reason',
|
||||
'--lock_reason',
|
||||
'--reason',
|
||||
dest='lock_reason',
|
||||
help="Reason for the resource lock")
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
share_client = self.app.client_manager.share
|
||||
|
||||
update_kwargs = {}
|
||||
if parsed_args.resource_action is not None:
|
||||
update_kwargs['resource_action'] = parsed_args.resource_action
|
||||
if parsed_args.lock_reason is not None:
|
||||
update_kwargs['lock_reason'] = parsed_args.lock_reason
|
||||
if update_kwargs:
|
||||
share_client.resource_locks.update(
|
||||
parsed_args.lock,
|
||||
**update_kwargs
|
||||
)
|
||||
|
||||
|
||||
class UnsetResourceLock(command.Command):
|
||||
"""Unsets a property on a resource lock."""
|
||||
_description = _("Remove resource lock properties")
|
||||
|
||||
def get_parser(self, prog_name):
|
||||
parser = super(UnsetResourceLock, self).get_parser(prog_name)
|
||||
parser.add_argument(
|
||||
'lock',
|
||||
metavar='<lock>',
|
||||
help='ID of resource lock to update.')
|
||||
parser.add_argument(
|
||||
'--lock-reason',
|
||||
'--lock_reason',
|
||||
'--reason',
|
||||
dest='lock_reason',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Unset the lock reason. (Default=False)")
|
||||
return parser
|
||||
|
||||
def take_action(self, parsed_args):
|
||||
share_client = self.app.client_manager.share
|
||||
|
||||
if parsed_args.lock_reason:
|
||||
share_client.resource_locks.update(
|
||||
parsed_args.lock,
|
||||
lock_reason=None
|
||||
)
|
@ -416,3 +416,21 @@ class OSCClientTestBase(base.ClientTestBase):
|
||||
|
||||
check_result = self.dict_result('share', cmd)
|
||||
return check_result
|
||||
|
||||
def create_resource_lock(self, resource_id, resource_type='share',
|
||||
resource_action='delete', lock_reason=None,
|
||||
add_cleanup=True, client=None):
|
||||
|
||||
cmd = f'lock create {resource_id} {resource_type}'
|
||||
cmd += f' --resource-action {resource_action}'
|
||||
|
||||
if lock_reason:
|
||||
cmd += f' --reason "{lock_reason}"'
|
||||
|
||||
lock = self.dict_result('share', cmd, client=client)
|
||||
|
||||
if add_cleanup:
|
||||
self.addCleanup(self.openstack,
|
||||
'share lock delete %s' % lock['id'],
|
||||
client=client)
|
||||
return lock
|
||||
|
167
manilaclient/tests/functional/osc/test_resource_locks.py
Normal file
167
manilaclient/tests/functional/osc/test_resource_locks.py
Normal file
@ -0,0 +1,167 @@
|
||||
# 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 tempest.lib.common.utils import data_utils
|
||||
from tempest.lib import exceptions as lib_exc
|
||||
|
||||
from manilaclient.tests.functional.osc import base
|
||||
from manilaclient.tests.functional import utils
|
||||
|
||||
LOCK_DETAIL_ATTRIBUTES = [
|
||||
'ID',
|
||||
'Resource Id',
|
||||
'Resource Type',
|
||||
'Resource Action',
|
||||
'Lock Context',
|
||||
'User Id',
|
||||
'Project Id',
|
||||
'Created At',
|
||||
'Updated At',
|
||||
'Lock Reason',
|
||||
]
|
||||
|
||||
LOCK_SUMMARY_ATTRIBUTES = [
|
||||
'ID',
|
||||
'Resource Id',
|
||||
'Resource Type',
|
||||
'Resource Action',
|
||||
]
|
||||
|
||||
|
||||
@utils.skip_if_microversion_not_supported('2.81')
|
||||
class ResourceLockTests(base.OSCClientTestBase):
|
||||
"""Lock CLI test cases"""
|
||||
|
||||
def setUp(self):
|
||||
super(ResourceLockTests, self).setUp()
|
||||
self.share_type = self.create_share_type(
|
||||
name=data_utils.rand_name('lock_tests_type'))
|
||||
self.share = self.create_share(share_type=self.share_type['id'])
|
||||
|
||||
def test_lock_create_show_use_delete(self):
|
||||
"""Create a deletion lock on share, view it, try it and remove."""
|
||||
lock = self.create_resource_lock(self.share['id'],
|
||||
lock_reason='tigers rule',
|
||||
client=self.user_client,
|
||||
add_cleanup=False)
|
||||
|
||||
client_user_id = self.openstack(
|
||||
"token issue -c user_id -f value",
|
||||
client=self.user_client
|
||||
).strip()
|
||||
client_project_id = self.openstack(
|
||||
"token issue -c project_id -f value",
|
||||
client=self.user_client
|
||||
).strip()
|
||||
|
||||
self.assertEqual(self.share['id'], lock['resource_id'])
|
||||
self.assertEqual('delete', lock['resource_action'])
|
||||
self.assertEqual(client_user_id, lock['user_id'])
|
||||
self.assertEqual(client_project_id, lock['project_id'])
|
||||
self.assertEqual('user', lock['lock_context'])
|
||||
self.assertEqual('tigers rule', lock['lock_reason'])
|
||||
|
||||
lock_show = self.dict_result("share", f"lock show {lock['id']}")
|
||||
self.assertEqual(lock['id'], lock_show['ID'])
|
||||
self.assertEqual(lock['lock_context'], lock_show['Lock Context'])
|
||||
|
||||
# When a deletion lock exists, share deletion must fail
|
||||
self.assertRaises(lib_exc.CommandFailed,
|
||||
self.openstack,
|
||||
f"share delete {self.share['id']}")
|
||||
|
||||
# delete the lock, share will be deleted in cleanup stage
|
||||
self.openstack(f"share lock delete {lock['id']}",
|
||||
client=self.user_client)
|
||||
|
||||
self.assertRaises(lib_exc.CommandFailed,
|
||||
self.openstack,
|
||||
f"share lock show {lock['id']}")
|
||||
|
||||
def test_lock_list_filter_paginate(self):
|
||||
lock_1 = self.create_resource_lock(self.share['id'],
|
||||
lock_reason='tigers rule',
|
||||
client=self.user_client)
|
||||
lock_2 = self.create_resource_lock(self.share['id'],
|
||||
lock_reason='tigers still rule',
|
||||
client=self.user_client)
|
||||
lock_3 = self.create_resource_lock(self.share['id'],
|
||||
lock_reason='admins rule',
|
||||
client=self.admin_client)
|
||||
|
||||
locks = self.listing_result('share',
|
||||
f'lock list --resource {self.share["id"]}')
|
||||
|
||||
self.assertEqual(3, len(locks))
|
||||
self.assertEqual(sorted(LOCK_SUMMARY_ATTRIBUTES),
|
||||
sorted(locks[0].keys()))
|
||||
|
||||
locks = self.listing_result('share',
|
||||
'lock list --lock-context user '
|
||||
f' --resource {self.share["id"]}')
|
||||
self.assertEqual(2, len(locks))
|
||||
self.assertNotIn(lock_3['id'], [l['ID'] for l in locks])
|
||||
|
||||
locks = self.listing_result('share',
|
||||
'lock list --lock-context user'
|
||||
f' --resource {self.share["id"]}'
|
||||
' --sort-key created_at '
|
||||
' --sort-dir desc '
|
||||
' --limit 1')
|
||||
self.assertEqual(1, len(locks))
|
||||
self.assertIn(lock_2['id'], [l['ID'] for l in locks])
|
||||
self.assertNotIn(lock_1['id'], [l['ID'] for l in locks])
|
||||
self.assertNotIn(lock_3['id'], [l['ID'] for l in locks])
|
||||
|
||||
def test_lock_set_unset_lock_reason(self):
|
||||
lock = self.create_resource_lock(self.share['id'],
|
||||
client=self.user_client)
|
||||
self.assertEqual('None', lock['lock_reason'])
|
||||
|
||||
self.openstack('share lock set '
|
||||
f"--lock-reason 'updated reason' {lock['id']}")
|
||||
lock_show = self.dict_result("share", f"lock show {lock['id']}")
|
||||
self.assertEqual('updated reason', lock_show['Lock Reason'])
|
||||
|
||||
self.openstack(f"share lock unset --lock-reason {lock['id']}")
|
||||
lock_show = self.dict_result("share", f"lock show {lock['id']}")
|
||||
self.assertEqual('None', lock_show['Lock Reason'])
|
||||
|
||||
def test_lock_restrictions(self):
|
||||
"""A user can't update or delete a lock created by another user."""
|
||||
lock = self.create_resource_lock(self.share['id'],
|
||||
client=self.admin_client,
|
||||
add_cleanup=False)
|
||||
self.assertEqual('admin', lock['lock_context'])
|
||||
|
||||
self.assertRaises(lib_exc.CommandFailed,
|
||||
self.openstack,
|
||||
f"share lock set {lock['id']} "
|
||||
f"--reason 'i cannot do this'",
|
||||
client=self.user_client)
|
||||
self.assertRaises(lib_exc.CommandFailed,
|
||||
self.openstack,
|
||||
f"share lock unset {lock['id']} --reason",
|
||||
client=self.user_client)
|
||||
self.assertRaises(lib_exc.CommandFailed,
|
||||
self.openstack,
|
||||
f"share lock delete {lock['id']} ",
|
||||
client=self.user_client)
|
||||
|
||||
self.openstack(f'share lock set '
|
||||
f'--lock-reason "I can do this" '
|
||||
f'{lock["id"]}',
|
||||
client=self.admin_client)
|
||||
|
||||
self.openstack(f'share lock delete '
|
||||
f'{lock["id"]}',
|
||||
client=self.admin_client)
|
@ -61,6 +61,7 @@ class FakeShareClient(object):
|
||||
self.share_group_types = mock.Mock()
|
||||
self.share_group_type_access = mock.Mock()
|
||||
self.share_servers = mock.Mock()
|
||||
self.resource_locks = mock.Mock()
|
||||
|
||||
|
||||
class ManilaParseException(Exception):
|
||||
@ -1521,3 +1522,61 @@ class FakeShareServer(object):
|
||||
share_servers.append(
|
||||
FakeShareServer.create_one_server(attrs))
|
||||
return share_servers
|
||||
|
||||
|
||||
class FakeResourceLock(object):
|
||||
"""Fake a resource lock"""
|
||||
|
||||
@staticmethod
|
||||
def create_one_lock(attrs=None, methods=None):
|
||||
"""Create a fake resource lock
|
||||
|
||||
:param Dictionary attrs:
|
||||
A dictionary with all attributes
|
||||
:return:
|
||||
A FakeResource object, with id, resource and so on
|
||||
"""
|
||||
|
||||
attrs = attrs or {}
|
||||
methods = methods or {}
|
||||
now_time = datetime.datetime.now()
|
||||
delta_time = now_time + datetime.timedelta(minutes=5)
|
||||
|
||||
lock = {
|
||||
'id': str(uuid.uuid4()),
|
||||
'resource_id': str(uuid.uuid4()),
|
||||
'resource_type': 'share',
|
||||
'resource_action': 'delete',
|
||||
'created_at': now_time.isoformat(),
|
||||
'updated_at': delta_time.isoformat(),
|
||||
'project_id': uuid.uuid4().hex,
|
||||
'user_id': uuid.uuid4().hex,
|
||||
'lock_context': 'user',
|
||||
'lock_reason': 'created by func tests',
|
||||
}
|
||||
|
||||
lock.update(attrs)
|
||||
lock = osc_fakes.FakeResource(info=copy.deepcopy(
|
||||
lock),
|
||||
methods=methods,
|
||||
loaded=True)
|
||||
return lock
|
||||
|
||||
@staticmethod
|
||||
def create_locks(attrs=None, count=2):
|
||||
"""Create multiple fake locks.
|
||||
|
||||
:param Dictionary attrs:
|
||||
A dictionary with all attributes
|
||||
:param Integer count:
|
||||
The number of share transfers to be faked
|
||||
:return:
|
||||
A list of FakeResource objects
|
||||
"""
|
||||
|
||||
resource_locks = []
|
||||
for n in range(0, count):
|
||||
resource_locks.append(
|
||||
FakeResourceLock.create_one_lock(attrs))
|
||||
|
||||
return resource_locks
|
||||
|
342
manilaclient/tests/unit/osc/v2/test_resource_locks.py
Normal file
342
manilaclient/tests/unit/osc/v2/test_resource_locks.py
Normal file
@ -0,0 +1,342 @@
|
||||
# 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 unittest import mock
|
||||
|
||||
from osc_lib import exceptions
|
||||
from osc_lib import utils as oscutils
|
||||
|
||||
from manilaclient import api_versions
|
||||
from manilaclient.osc.v2 import resource_locks as osc_resource_locks
|
||||
from manilaclient.tests.unit.osc import osc_utils
|
||||
from manilaclient.tests.unit.osc.v2 import fakes as manila_fakes
|
||||
|
||||
DETAIL_COLUMNS = [
|
||||
'ID',
|
||||
'Resource Id',
|
||||
'Resource Type',
|
||||
'Resource Action',
|
||||
'Created At',
|
||||
'Updated At',
|
||||
'User Id',
|
||||
'Project Id',
|
||||
'Lock Reason',
|
||||
'Lock Context',
|
||||
]
|
||||
|
||||
SUMMARY_COLUMNS = [
|
||||
'ID',
|
||||
'Resource Id',
|
||||
'Resource Type',
|
||||
'Resource Action',
|
||||
]
|
||||
|
||||
|
||||
class TestResourceLock(manila_fakes.TestShare):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceLock, self).setUp()
|
||||
|
||||
self.shares_mock = self.app.client_manager.share.shares
|
||||
self.shares_mock.reset_mock()
|
||||
|
||||
self.locks_mock = self.app.client_manager.share.resource_locks
|
||||
self.locks_mock.reset_mock()
|
||||
|
||||
self.app.client_manager.share.api_version = api_versions.APIVersion(
|
||||
api_versions.MAX_VERSION)
|
||||
|
||||
|
||||
class TestResourceLockCreate(TestResourceLock):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceLockCreate, self).setUp()
|
||||
|
||||
self.share = manila_fakes.FakeShare.create_one_share()
|
||||
self.shares_mock.create.return_value = self.share
|
||||
|
||||
self.shares_mock.get.return_value = self.share
|
||||
|
||||
self.lock = manila_fakes.FakeResourceLock.create_one_lock(
|
||||
attrs={'resource_id': self.share.id})
|
||||
self.locks_mock.get.return_value = self.lock
|
||||
self.locks_mock.create.return_value = self.lock
|
||||
|
||||
self.cmd = osc_resource_locks.CreateResourceLock(self.app, None)
|
||||
|
||||
self.data = tuple(self.lock._info.values())
|
||||
self.columns = tuple(self.lock._info.keys())
|
||||
|
||||
def test_share_lock_create_missing_required_args(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
self.assertRaises(osc_utils.ParserException,
|
||||
self.check_parser, self.cmd, arglist, verifylist)
|
||||
|
||||
def test_share_lock_create(self):
|
||||
arglist = [
|
||||
'--resource-action', 'revert_to_snapshot',
|
||||
'--lock-reason', "you cannot go back in time",
|
||||
self.share.id,
|
||||
'share',
|
||||
]
|
||||
verifylist = [
|
||||
('resource', self.share.id),
|
||||
('resource_type', 'share'),
|
||||
('resource_action', 'revert_to_snapshot'),
|
||||
('lock_reason', 'you cannot go back in time')
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.locks_mock.create.assert_called_with(
|
||||
self.share.id,
|
||||
'share',
|
||||
'revert_to_snapshot',
|
||||
'you cannot go back in time',
|
||||
)
|
||||
|
||||
self.assertCountEqual(self.columns, columns)
|
||||
self.assertCountEqual(self.data, data)
|
||||
|
||||
|
||||
class TestResourceLockDelete(TestResourceLock):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceLockDelete, self).setUp()
|
||||
|
||||
self.lock = manila_fakes.FakeResourceLock.create_one_lock()
|
||||
|
||||
self.locks_mock.get.return_value = self.lock
|
||||
self.lock.delete = mock.Mock()
|
||||
|
||||
self.cmd = osc_resource_locks.DeleteResourceLock(self.app, None)
|
||||
|
||||
def test_share_lock_delete_missing_args(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
self.assertRaises(osc_utils.ParserException,
|
||||
self.check_parser, self.cmd, arglist, verifylist)
|
||||
|
||||
def test_share_lock_delete(self):
|
||||
arglist = [
|
||||
self.lock.id
|
||||
]
|
||||
verifylist = [
|
||||
('lock', [self.lock.id])
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.lock.delete.assert_called_once_with()
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_share_lock_delete_multiple(self):
|
||||
locks = manila_fakes.FakeResourceLock.create_locks(count=2)
|
||||
arglist = [
|
||||
locks[0].id,
|
||||
locks[1].id
|
||||
]
|
||||
verifylist = [
|
||||
('lock', [locks[0].id, locks[1].id])
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertEqual(self.lock.delete.call_count,
|
||||
len(locks))
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_share_lock_delete_exception(self):
|
||||
arglist = [
|
||||
self.lock.id
|
||||
]
|
||||
verifylist = [
|
||||
('lock', [self.lock.id])
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
self.lock.delete.side_effect = exceptions.CommandError()
|
||||
self.assertRaises(exceptions.CommandError,
|
||||
self.cmd.take_action,
|
||||
parsed_args)
|
||||
|
||||
|
||||
class TestResourceLockShow(TestResourceLock):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceLockShow, self).setUp()
|
||||
|
||||
self.lock = manila_fakes.FakeResourceLock.create_one_lock()
|
||||
self.locks_mock.get.return_value = self.lock
|
||||
|
||||
self.cmd = osc_resource_locks.ShowResourceLock(self.app, None)
|
||||
|
||||
self.data = self.lock._info.values()
|
||||
self.columns = list(self.lock._info.keys())
|
||||
|
||||
def test_share_lock_show_missing_args(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
self.assertRaises(osc_utils.ParserException,
|
||||
self.check_parser, self.cmd, arglist, verifylist)
|
||||
|
||||
def test_share_lock_show(self):
|
||||
arglist = [
|
||||
self.lock.id,
|
||||
]
|
||||
verifylist = [
|
||||
('lock', self.lock.id)
|
||||
]
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.locks_mock.get.assert_called_with(self.lock.id)
|
||||
self.assertEqual(len(self.columns), len(columns))
|
||||
self.assertCountEqual(sorted(self.data), sorted(data))
|
||||
|
||||
|
||||
class TestResourceLockList(TestResourceLock):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceLockList, self).setUp()
|
||||
|
||||
self.locks = manila_fakes.FakeResourceLock.create_locks(count=2)
|
||||
|
||||
self.locks_mock.list.return_value = self.locks
|
||||
|
||||
self.values = (oscutils.get_dict_properties(
|
||||
m._info, DETAIL_COLUMNS) for m in self.locks)
|
||||
|
||||
self.cmd = osc_resource_locks.ListResourceLock(self.app, None)
|
||||
|
||||
def test_share_lock_list(self):
|
||||
arglist = [
|
||||
'--detailed'
|
||||
]
|
||||
verifylist = [
|
||||
('detailed', True)
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
columns, data = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.locks_mock.list.assert_called_with(
|
||||
search_opts={
|
||||
'all_projects': False,
|
||||
'project_id': None,
|
||||
'user_id': None,
|
||||
'id': None,
|
||||
'resource_id': None,
|
||||
'resource_type': None,
|
||||
'resource_action': None,
|
||||
'lock_context': None,
|
||||
'created_before': None,
|
||||
'created_since': None,
|
||||
'limit': None,
|
||||
'offset': None,
|
||||
},
|
||||
sort_key=None,
|
||||
sort_dir=None
|
||||
)
|
||||
|
||||
self.assertEqual(sorted(DETAIL_COLUMNS), sorted(columns))
|
||||
actual_data = [sorted(d) for d in data]
|
||||
expected_data = [sorted(v) for v in self.values]
|
||||
self.assertEqual(actual_data, expected_data)
|
||||
|
||||
|
||||
class TestResourceLockSet(TestResourceLock):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceLockSet, self).setUp()
|
||||
|
||||
self.lock = manila_fakes.FakeResourceLock.create_one_lock()
|
||||
self.lock.update = mock.Mock()
|
||||
|
||||
self.locks_mock.get.return_value = self.lock
|
||||
|
||||
self.cmd = osc_resource_locks.SetResourceLock(self.app, None)
|
||||
|
||||
def test_share_lock_set_missing_args(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
self.assertRaises(osc_utils.ParserException,
|
||||
self.check_parser, self.cmd, arglist, verifylist)
|
||||
|
||||
def test_share_lock_set(self):
|
||||
arglist = [
|
||||
self.lock.id,
|
||||
'--resource-action', 'unmanage',
|
||||
]
|
||||
verifylist = [
|
||||
('lock', self.lock.id),
|
||||
('resource_action', 'unmanage')
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertIsNone(result)
|
||||
self.locks_mock.update.assert_called_with(self.lock.id,
|
||||
resource_action='unmanage')
|
||||
|
||||
|
||||
class TestResourceLockUnSet(TestResourceLock):
|
||||
|
||||
def setUp(self):
|
||||
super(TestResourceLockUnSet, self).setUp()
|
||||
|
||||
self.lock = manila_fakes.FakeResourceLock.create_one_lock()
|
||||
self.lock.update = mock.Mock()
|
||||
|
||||
self.locks_mock.get.return_value = self.lock
|
||||
|
||||
self.cmd = osc_resource_locks.UnsetResourceLock(self.app, None)
|
||||
|
||||
def test_share_lock_unset_missing_args(self):
|
||||
arglist = []
|
||||
verifylist = []
|
||||
|
||||
self.assertRaises(osc_utils.ParserException,
|
||||
self.check_parser, self.cmd, arglist, verifylist)
|
||||
|
||||
def test_share_lock_unset(self):
|
||||
arglist = [
|
||||
self.lock.id,
|
||||
'--lock-reason'
|
||||
]
|
||||
verifylist = [
|
||||
('lock', self.lock.id),
|
||||
('lock_reason', True)
|
||||
]
|
||||
|
||||
parsed_args = self.check_parser(self.cmd, arglist, verifylist)
|
||||
|
||||
result = self.cmd.take_action(parsed_args)
|
||||
|
||||
self.assertIsNone(result)
|
||||
self.locks_mock.update.assert_called_with(self.lock.id,
|
||||
lock_reason=None)
|
@ -23,6 +23,7 @@ from manilaclient.v2 import limits
|
||||
from manilaclient.v2 import messages
|
||||
from manilaclient.v2 import quota_classes
|
||||
from manilaclient.v2 import quotas
|
||||
from manilaclient.v2 import resource_locks
|
||||
from manilaclient.v2 import scheduler_stats
|
||||
from manilaclient.v2 import security_services
|
||||
from manilaclient.v2 import services
|
||||
@ -201,6 +202,8 @@ class Client(object):
|
||||
self.quota_classes = quota_classes.QuotaClassSetManager(self)
|
||||
self.quotas = quotas.QuotaSetManager(self)
|
||||
|
||||
self.resource_locks = resource_locks.ResourceLockManager(self)
|
||||
|
||||
self.shares = shares.ShareManager(self)
|
||||
self.share_export_locations = (
|
||||
share_export_locations.ShareExportLocationManager(self))
|
||||
|
130
manilaclient/v2/resource_locks.py
Normal file
130
manilaclient/v2/resource_locks.py
Normal file
@ -0,0 +1,130 @@
|
||||
# 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 manilaclient import api_versions
|
||||
from manilaclient import base
|
||||
from manilaclient.common import constants
|
||||
|
||||
|
||||
class ResourceLock(base.Resource):
|
||||
"""Lock a share resource action from being executed"""
|
||||
|
||||
def __repr__(self):
|
||||
return "<ResourceLock: %s>" % self.id
|
||||
|
||||
def delete(self):
|
||||
"""Delete this lock."""
|
||||
return self.manager.delete(self)
|
||||
|
||||
def update(self, **kwargs):
|
||||
"""Update this lock."""
|
||||
return self.manager.update(self, **kwargs)
|
||||
|
||||
|
||||
class ResourceLockManager(base.ManagerWithFind):
|
||||
"""Manage :class:`ResourceLock` resources."""
|
||||
resource_class = ResourceLock
|
||||
|
||||
@api_versions.wraps(constants.RESOURCE_LOCK_VERSION)
|
||||
def create(self, resource_id, resource_type,
|
||||
resource_action='delete', lock_reason=None):
|
||||
"""Creates a resource lock.
|
||||
|
||||
:param resource_id: The ID of the resource to lock
|
||||
:param resource_type: The type of the resource (e.g., "share",
|
||||
"access")
|
||||
:param resource_action: The functionality to lock (e.g., "delete",
|
||||
"view/delete")
|
||||
:param lock_reason: Lock description
|
||||
:rtype: :class:`ResourceLock`
|
||||
"""
|
||||
body = {
|
||||
'resource_lock': {
|
||||
'resource_id': resource_id,
|
||||
'resource_type': resource_type,
|
||||
'resource_action': resource_action,
|
||||
'lock_reason': lock_reason,
|
||||
}
|
||||
}
|
||||
|
||||
return self._create('/resource-locks', body, 'resource_lock')
|
||||
|
||||
@api_versions.wraps(constants.RESOURCE_LOCK_VERSION)
|
||||
def get(self, lock_id):
|
||||
"""Show details of a resource lock.
|
||||
|
||||
:param lock_id: The ID of the resource lock to display.
|
||||
:rtype: :class:`ResourceLock`
|
||||
"""
|
||||
return self._get("/resource-locks/%s" % lock_id, "resource_lock")
|
||||
|
||||
@api_versions.wraps(constants.RESOURCE_LOCK_VERSION)
|
||||
def list(self, search_opts=None, sort_key=None, sort_dir=None):
|
||||
"""Get a list of all resource locks.
|
||||
|
||||
:param search_opts: Filtering options as a dictionary.
|
||||
:param sort_key: Key to be sorted (i.e. 'created_at').
|
||||
:param sort_dir: Sort direction, should be 'desc' or 'asc'.
|
||||
:rtype: list of :class:`ResourceLock`
|
||||
"""
|
||||
search_opts = search_opts or {}
|
||||
|
||||
sort_key = sort_key or 'created_at'
|
||||
if sort_key in constants.RESOURCE_LOCK_SORT_KEY_VALUES:
|
||||
search_opts['sort_key'] = sort_key
|
||||
else:
|
||||
raise ValueError(
|
||||
'sort_key must be one of the following: %s.'
|
||||
% ', '.join(constants.RESOURCE_LOCK_SORT_KEY_VALUES))
|
||||
sort_dir = sort_dir or 'desc'
|
||||
if sort_dir in constants.SORT_DIR_VALUES:
|
||||
search_opts['sort_dir'] = sort_dir
|
||||
else:
|
||||
raise ValueError('sort_dir must be one of the following: %s.'
|
||||
% ', '.join(constants.SORT_DIR_VALUES))
|
||||
|
||||
query_string = self._build_query_string(search_opts)
|
||||
|
||||
path = "/resource-locks%s" % (query_string,)
|
||||
|
||||
return self._list(path, 'resource_locks')
|
||||
|
||||
@api_versions.wraps(constants.RESOURCE_LOCK_VERSION)
|
||||
def update(self, lock, **kwargs):
|
||||
"""Updates a resource lock.
|
||||
|
||||
:param lock: The :class:`ResourceLock` object, or a lock id to update.
|
||||
:param kwargs: "resource_action" and "lock_reason" are allowed kwargs
|
||||
:rtype: :class:`ResourceLock`
|
||||
"""
|
||||
if not kwargs:
|
||||
return
|
||||
|
||||
body = {'resource_lock': {}}
|
||||
|
||||
if 'lock_reason' in kwargs:
|
||||
body['resource_lock']['lock_reason'] = kwargs['lock_reason']
|
||||
if 'resource_action' in kwargs:
|
||||
body['resource_lock']['resource_action'] = (
|
||||
kwargs['resource_action']
|
||||
)
|
||||
|
||||
lock_id = base.getid(lock)
|
||||
return self._update("/resource-locks/%s" % lock_id, body)
|
||||
|
||||
@api_versions.wraps(constants.RESOURCE_LOCK_VERSION)
|
||||
def delete(self, lock):
|
||||
"""Delete a resource lock.
|
||||
|
||||
:param lock: The :class:`ResourceLock` object, or a lock id to delete.
|
||||
"""
|
||||
return self._delete("/resource-locks/%s" % base.getid(lock))
|
@ -0,0 +1,5 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Added SDK and OSC commands to create, view, update and delete resource
|
||||
locks, alongside support for API version 2.81.
|
@ -166,6 +166,12 @@ openstack.share.v2 =
|
||||
share_transfer_list = manilaclient.osc.v2.share_transfers:ListShareTransfer
|
||||
share_transfer_show = manilaclient.osc.v2.share_transfers:ShowShareTransfer
|
||||
share_transfer_accept = manilaclient.osc.v2.share_transfers:AcceptShareTransfer
|
||||
share_lock_create = manilaclient.osc.v2.resource_locks:CreateResourceLock
|
||||
share_lock_list = manilaclient.osc.v2.resource_locks:ListResourceLock
|
||||
share_lock_show = manilaclient.osc.v2.resource_locks:ShowResourceLock
|
||||
share_lock_set = manilaclient.osc.v2.resource_locks:SetResourceLock
|
||||
share_lock_unset = manilaclient.osc.v2.resource_locks:UnsetResourceLock
|
||||
share_lock_delete = manilaclient.osc.v2.resource_locks:DeleteResourceLock
|
||||
|
||||
[coverage:run]
|
||||
omit = manilaclient/tests/*
|
||||
|
Loading…
Reference in New Issue
Block a user