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:
Goutham Pacha Ravi 2023-07-07 20:10:15 -07:00
parent 7a15a2a1ae
commit 1734b45fa5
12 changed files with 1160 additions and 1 deletions

View File

@ -214,3 +214,10 @@ share servers
.. autoprogram-cliff:: openstack.share.v2
:command: share server *
==============
resource locks
==============
.. autoprogram-cliff:: openstack.share.v2
:command: share lock *

View File

@ -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 = {}

View File

@ -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'

View 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
)

View File

@ -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

View 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)

View File

@ -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

View 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)

View File

@ -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))

View 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))

View File

@ -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.

View File

@ -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/*