Microversion 2.73: Support adding the reason behind a server lock

This patch adds a new parameter ``--reason`` to ``nova lock``
command and ``--locked`` filtering/sorting parameter to
``nova list`` command. This can help users to provide a reason
when locking the server and to filter/sort instances based on their
locked or value from 2.73 microversion.

Implements blueprint add-locked-reason
Depends-On: https://review.opendev.org/#/c/648662/

Change-Id: I438e6db2dd5000ba388d0a0f1c8ab74b96b47a71
This commit is contained in:
Surya Seetharaman 2019-03-29 11:34:04 +01:00
parent fe4138aea4
commit a1ac69c69a
8 changed files with 135 additions and 3 deletions

View File

@ -25,4 +25,4 @@ API_MIN_VERSION = api_versions.APIVersion("2.1")
# when client supported the max version, and bumped sequentially, otherwise
# the client may break due to server side new version may include some
# backward incompatible change.
API_MAX_VERSION = api_versions.APIVersion("2.72")
API_MAX_VERSION = api_versions.APIVersion("2.73")

View File

@ -454,6 +454,8 @@ class V1(Base):
pass
elif action == 'migrate':
return None
elif action == 'lock':
return None
elif action == 'rebuild':
body = body[action]
adminPass = body.get('adminPass', 'randompassword')

View File

@ -774,7 +774,7 @@ class FakeSessionClient(base_client.SessionClient):
none_actions = ['revertResize', 'os-stop', 'os-start',
'forceDelete', 'restore', 'pause', 'unpause', 'unlock',
'unrescue', 'resume', 'suspend', 'lock', 'shelve',
'unrescue', 'resume', 'suspend', 'shelve',
'shelveOffload', 'unshelve', 'resetNetwork']
type_actions = ['os-getVNCConsole', 'os-getSPICEConsole',
'os-getRDPConsole']
@ -836,6 +836,22 @@ class FakeSessionClient(base_client.SessionClient):
# host can be optional
expected.add('host')
assert set(body[action].keys()) == expected
elif action == 'lock':
if self.api_version < api_versions.APIVersion("2.73"):
assert body[action] is None
else:
# In 2.73 and above, we allow body to be one of these:
# a) {'lock': None}
# b) {'lock': {}}
# c) {'lock': {locked_reason': 'blah'}}
if body[action] is not None:
expected = set()
if 'locked_reason' in body[action].keys():
# reason can be optional
expected.add('locked_reason')
assert set(body[action].keys()) == expected
else:
assert body[action] is None
elif action == 'rebuild':
body = body[action]
adminPass = body.get('adminPass', 'randompassword')

View File

@ -1703,3 +1703,26 @@ class ServersV268Test(ServersV267Test):
ex = self.assertRaises(TypeError, self.cs.servers.live_migrate,
host='hostname', force=True)
self.assertIn('force', six.text_type(ex))
class ServersV273Test(ServersV268Test):
api_version = "2.73"
def test_lock_server(self):
s = self.cs.servers.get(1234)
ret = s.lock()
self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers/1234/action',
{'lock': None})
ret = s.lock(reason='zombie-apocalypse')
self.assert_request_id(ret, fakes.FAKE_REQUEST_ID_LIST)
self.assert_called('POST', '/servers/1234/action',
{'lock': {'locked_reason': 'zombie-apocalypse'}})
def test_lock_server_pre_273_fails_with_reason(self):
self.cs.api_version = api_versions.APIVersion('2.72')
s = self.cs.servers.get(1234)
e = self.assertRaises(TypeError,
s.lock, reason='blah')
self.assertIn("unexpected keyword argument 'reason'", six.text_type(e))

View File

@ -2092,6 +2092,24 @@ class ShellTest(utils.TestCase):
self.run_command('lock sample-server')
self.assert_called('POST', '/servers/1234/action', {'lock': None})
def test_lock_pre_v273(self):
exp = self.assertRaises(SystemExit,
self.run_command,
'lock sample-server --reason zombies',
api_version='2.72')
self.assertIn('2', six.text_type(exp))
def test_lock_v273(self):
self.run_command('lock sample-server',
api_version='2.73')
self.assert_called('POST', '/servers/1234/action',
{'lock': None})
self.run_command('lock sample-server --reason zombies',
api_version='2.73')
self.assert_called('POST', '/servers/1234/action',
{'lock': {'locked_reason': 'zombies'}})
def test_unlock(self):
self.run_command('unlock sample-server')
self.assert_called('POST', '/servers/1234/action', {'unlock': None})
@ -4280,6 +4298,22 @@ class ShellTest(utils.TestCase):
self.assert_called('GET', '/servers/9015', pos=2)
self.assert_called('GET', '/v2/images/%s' % FAKE_UUID_2, pos=3)
def test_list_pre_v273(self):
exp = self.assertRaises(SystemExit,
self.run_command,
'list --locked t',
api_version='2.72')
self.assertEqual(2, exp.code)
def test_list_v273(self):
self.run_command('list --locked t', api_version='2.73')
self.assert_called('GET', '/servers/detail?locked=t')
def test_list_v273_with_sort_key_dir(self):
self.run_command('list --sort locked:asc', api_version='2.73')
self.assert_called(
'GET', '/servers/detail?sort_dir=asc&sort_key=locked')
class PollForStatusTestCase(utils.TestCase):
@mock.patch("novaclient.v2.shell.time")

View File

@ -214,6 +214,7 @@ class Server(base.Resource):
"""
return self.manager.unpause(self)
@api_versions.wraps("2.0", "2.72")
def lock(self):
"""
Lock -- Lock the instance from certain operations.
@ -222,6 +223,16 @@ class Server(base.Resource):
"""
return self.manager.lock(self)
@api_versions.wraps("2.73")
def lock(self, reason=None):
"""
Lock -- Lock the instance from certain operations.
:param reason: (Optional) The lock reason.
:returns: An instance of novaclient.base.TupleWithMeta
"""
return self.manager.lock(self, reason=reason)
def unlock(self):
"""
Unlock -- Remove instance lock.
@ -1097,6 +1108,7 @@ class ServerManager(base.BootingManagerWithFind):
"""
return self._action('unpause', server, None)
@api_versions.wraps("2.0", "2.72")
def lock(self, server):
"""
Lock the server.
@ -1106,6 +1118,22 @@ class ServerManager(base.BootingManagerWithFind):
"""
return self._action('lock', server, None)
@api_versions.wraps("2.73")
def lock(self, server, reason=None):
"""
Lock the server.
:param server: The :class:`Server` (or its ID) to lock
:param reason: (Optional) The lock reason.
:returns: An instance of novaclient.base.TupleWithMeta
"""
info = None
if reason:
info = {'locked_reason': reason}
return self._action('lock', server, info)
def unlock(self, server):
"""
Unlock the server.

View File

@ -1563,6 +1563,13 @@ def _print_flavor(flavor):
"case is 'NOT(t1 OR t2)'. Tags must be separated by commas: "
"--not-tags-any <tag1,tag2>"),
start_version="2.26")
@utils.arg(
'--locked',
dest='locked',
metavar='<locked>',
default=None,
help=_('Display servers based on their locked value'),
start_version="2.73")
def do_list(cs, args):
"""List servers."""
imageid = None
@ -1639,6 +1646,11 @@ def do_list(cs, args):
raise exceptions.CommandError(_('Invalid changes-before value: %s')
% search_opts['changes-before'])
# In microversion 2.73 we added ``locked`` option in server details.
have_added_locked = cs.api_version >= api_versions.APIVersion('2.73')
if have_added_locked and args.locked:
search_opts['locked'] = args.locked
servers = cs.servers.list(detailed=detailed,
search_opts=search_opts,
sort_keys=sort_keys,
@ -2155,12 +2167,23 @@ def do_start(cs, args):
_("Unable to start the specified server(s)."))
# From microversion 2.73, we can specify a reason for locking the server.
@utils.arg('server', metavar='<server>', help=_('Name or ID of server.'))
@utils.arg(
'--reason',
metavar='<reason>',
help=_('Reason for locking the server.'),
start_version='2.73')
def do_lock(cs, args):
"""Lock a server. A normal (non-admin) user will not be able to execute
actions on a locked server.
"""
_find_server(cs, args.server).lock()
update_kwargs = {}
if 'reason' in args and args.reason is not None:
update_kwargs['reason'] = args.reason
server = _find_server(cs, args.server)
server.lock(**update_kwargs)
@utils.arg('server', metavar='<server>', help=_('Name or ID of server.'))

View File

@ -0,0 +1,6 @@
---
features:
- Added a ``--reason`` option to ``nova lock`` command that enables users
to specify a reason when locking a server and a ``locked``
filtering/sorting option to ``nova list`` command which enables users to
filter/sort servers based on their ``locked`` value in microversion 2.73.