diff --git a/novaclient/__init__.py b/novaclient/__init__.py index 2103d5cea..66520e666 100644 --- a/novaclient/__init__.py +++ b/novaclient/__init__.py @@ -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.57") +API_MAX_VERSION = api_versions.APIVersion("2.58") diff --git a/novaclient/tests/functional/v2/test_instance_action.py b/novaclient/tests/functional/v2/test_instance_action.py index 728dbb0bc..a318b6e7e 100644 --- a/novaclient/tests/functional/v2/test_instance_action.py +++ b/novaclient/tests/functional/v2/test_instance_action.py @@ -10,6 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import time + +from oslo_utils import timeutils from oslo_utils import uuidutils import six from tempest.lib import exceptions @@ -57,3 +60,56 @@ class TestInstanceActionCLI(base.ClientTestBase): # ensure that obtained action is "create". self.assertEqual("create", self._get_value_from_the_table(output, "action")) + + +class TestInstanceActionCLIV258(TestInstanceActionCLI): + """Instance action functional tests for v2.58 nova-api microversion.""" + + COMPUTE_API_VERSION = "2.58" + + def test_list_instance_action_with_marker_and_limit(self): + server = self._create_server() + server.stop() + # The actions are sorted by created_at in descending order, + # and now we have two actions: create and stop. + output = self.nova("instance-action-list %s --limit 1" % server.id) + marker_req = self._get_column_value_from_single_row_table( + output, "Request_ID") + action = self._get_list_of_values_from_single_column_table( + output, "Action") + # The stop action was most recently created so it's what + # we get back when limit=1. + self.assertEqual(action, ['stop']) + + output = self.nova("instance-action-list %s --limit 1 " + "--marker %s" % (server.id, marker_req)) + action = self._get_list_of_values_from_single_column_table( + output, "Action") + self.assertEqual(action, ['create']) + + def test_list_instance_action_with_changes_since(self): + # Ignore microseconds to make this a deterministic test. + before_create = timeutils.utcnow().replace(microsecond=0).isoformat() + server = self._create_server() + time.sleep(2) + before_stop = timeutils.utcnow().replace(microsecond=0).isoformat() + server.stop() + + create_output = self.nova( + "instance-action-list %s --changes-since %s" % + (server.id, before_create)) + action = self._get_list_of_values_from_single_column_table( + create_output, "Action") + # The actions are sorted by created_at in descending order. + self.assertEqual(action, ['create', 'stop']) + + stop_output = self.nova("instance-action-list %s --changes-since %s" % + (server.id, before_stop)) + action = self._get_list_of_values_from_single_column_table( + stop_output, "Action") + # Provide detailed debug information if this fails. + self.assertEqual(action, ['stop'], + 'Expected to find the stop action with ' + '--changes-since=%s but got: %s\n\n' + 'First instance-action-list output: %s' % + (before_stop, stop_output, create_output)) diff --git a/novaclient/tests/unit/v2/fakes.py b/novaclient/tests/unit/v2/fakes.py index 11789ec19..7c8f95285 100644 --- a/novaclient/tests/unit/v2/fakes.py +++ b/novaclient/tests/unit/v2/fakes.py @@ -1948,26 +1948,31 @@ class FakeSessionClient(base_client.SessionClient): return (200, FAKE_RESPONSE_HEADERS, {}) def get_servers_1234_os_instance_actions(self, **kw): - return (200, FAKE_RESPONSE_HEADERS, { - "instanceActions": - [{"instance_uuid": "1234", + action = {"instance_uuid": "1234", "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", "start_time": "2013-03-25T13:45:09.000000", "request_id": "req-abcde12345", "action": "create", "message": None, - "project_id": "04019601fe3648c0abd4f4abfb9e6106"}]}) + "project_id": "04019601fe3648c0abd4f4abfb9e6106"} + if self.api_version >= api_versions.APIVersion('2.58'): + # This is intentionally different from the start_time. + action['updated_at'] = '2013-03-25T13:50:09.000000' + return (200, FAKE_RESPONSE_HEADERS, { + "instanceActions": [action]}) def get_servers_1234_os_instance_actions_req_abcde12345(self, **kw): + action = {"instance_uuid": "1234", + "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", + "start_time": "2013-03-25T13:45:09.000000", + "request_id": "req-abcde12345", + "action": "create", + "message": None, + "project_id": "04019601fe3648c0abd4f4abfb9e6106"} + if self.api_version >= api_versions.APIVersion('2.58'): + action['updated_at'] = '2013-03-25T13:45:09.000000' return (200, FAKE_RESPONSE_HEADERS, { - "instanceAction": - {"instance_uuid": "1234", - "user_id": "b968c25e04ab405f9fe4e6ca54cce9a5", - "start_time": "2013-03-25T13:45:09.000000", - "request_id": "req-abcde12345", - "action": "create", - "message": None, - "project_id": "04019601fe3648c0abd4f4abfb9e6106"}}) + "instanceAction": action}) def post_servers_uuid1_action(self, **kw): return 202, {}, {} diff --git a/novaclient/tests/unit/v2/test_instance_actions.py b/novaclient/tests/unit/v2/test_instance_actions.py index 8eed17197..e26bf7a50 100644 --- a/novaclient/tests/unit/v2/test_instance_actions.py +++ b/novaclient/tests/unit/v2/test_instance_actions.py @@ -16,6 +16,7 @@ from novaclient import api_versions from novaclient.tests.unit import utils from novaclient.tests.unit.v2 import fakes +from novaclient.v2 import instance_action class InstanceActionExtensionTests(utils.TestCase): @@ -39,3 +40,24 @@ class InstanceActionExtensionTests(utils.TestCase): self.cs.assert_called( 'GET', '/servers/%s/os-instance-actions/%s' % (server_uuid, request_id)) + + +class InstanceActionExtensionV258Tests(InstanceActionExtensionTests): + def setUp(self): + super(InstanceActionExtensionV258Tests, self).setUp() + self.cs.api_version = api_versions.APIVersion("2.58") + + def test_list_instance_actions_with_limit_marker_params(self): + server_uuid = '1234' + marker = '12140183-c814-4ddf-8453-6df43028aa67' + + ias = self.cs.instance_action.list( + server_uuid, marker=marker, limit=10, + changes_since='2016-02-29T06:23:22') + self.assert_request_id(ias, fakes.FAKE_REQUEST_ID_LIST) + self.cs.assert_called( + 'GET', + '/servers/%s/os-instance-actions?changes-since=%s&limit=10&' + 'marker=%s' % (server_uuid, '2016-02-29T06%3A23%3A22', marker)) + for ia in ias: + self.assertIsInstance(ia, instance_action.InstanceAction) diff --git a/novaclient/tests/unit/v2/test_shell.py b/novaclient/tests/unit/v2/test_shell.py index fb21eb5e7..8f416cddd 100644 --- a/novaclient/tests/unit/v2/test_shell.py +++ b/novaclient/tests/unit/v2/test_shell.py @@ -2976,6 +2976,49 @@ class ShellTest(utils.TestCase): 'GET', '/servers/1234/os-instance-actions/req-abcde12345') + def test_instance_action_list_marker_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --marker %s' + self.assertRaises(SystemExit, self.run_command, + cmd % FAKE_UUID_1, api_version='2.57') + + def test_instance_action_list_limit_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --limit 10' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.57') + + def test_instance_action_list_changes_since_pre_v258_not_allowed(self): + cmd = 'instance-action-list sample-server --changes-since ' \ + '2016-02-29T06:23:22' + self.assertRaises(SystemExit, self.run_command, + cmd, api_version='2.57') + + def test_instance_action_list_limit_marker_v258(self): + out = self.run_command('instance-action-list sample-server --limit 10 ' + '--marker %s' % FAKE_UUID_1, + api_version='2.58')[0] + # Assert that the updated_at value is in the output. + self.assertIn('2013-03-25T13:50:09.000000', out) + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions?' + 'limit=10&marker=%s' % FAKE_UUID_1) + + def test_instance_action_list_with_changes_since_v258(self): + self.run_command('instance-action-list sample-server ' + '--changes-since 2016-02-29T06:23:22', + api_version='2.58') + self.assert_called( + 'GET', + '/servers/1234/os-instance-actions?' + 'changes-since=2016-02-29T06%3A23%3A22') + + def test_instance_action_list_with_changes_since_invalid_value_v258(self): + ex = self.assertRaises( + exceptions.CommandError, self.run_command, + 'instance-action-list sample-server --changes-since 0123456789', + api_version='2.58') + self.assertIn('Invalid changes-since value', six.text_type(ex)) + def test_cell_show(self): self.run_command('cell-show child_cell') self.assert_called('GET', '/os-cells/child_cell') diff --git a/novaclient/v2/instance_action.py b/novaclient/v2/instance_action.py index 5531d35fc..b8316c2d7 100644 --- a/novaclient/v2/instance_action.py +++ b/novaclient/v2/instance_action.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from novaclient import api_versions from novaclient import base @@ -32,9 +33,37 @@ class InstanceActionManager(base.ManagerWithFind): return self._get("/servers/%s/os-instance-actions/%s" % (base.getid(server), request_id), 'instanceAction') + @api_versions.wraps("2.0", "2.57") def list(self, server): """ Get a list of actions performed on a server. + + :param server: The :class:`Server` (or its ID) """ return self._list('/servers/%s/os-instance-actions' % base.getid(server), 'instanceActions') + + @api_versions.wraps("2.58") + def list(self, server, marker=None, limit=None, changes_since=None): + """ + Get a list of actions performed on a server. + + :param server: The :class:`Server` (or its ID) + :param marker: Begin returning actions that appear later in the action + list than that represented by this action request id + (optional). + :param limit: Maximum number of actions to return. (optional). + :param changes_since: List only instance actions changed after a + certain point of time. The provided time should + be an ISO 8061 formatted time. ex + 2016-03-04T06:27:59Z . (optional). + """ + opts = {} + if marker: + opts['marker'] = marker + if limit: + opts['limit'] = limit + if changes_since: + opts['changes-since'] = changes_since + return self._list('/servers/%s/os-instance-actions' % + base.getid(server), 'instanceActions', filters=opts) diff --git a/novaclient/v2/shell.py b/novaclient/v2/shell.py index ecd72a260..56828e9fb 100644 --- a/novaclient/v2/shell.py +++ b/novaclient/v2/shell.py @@ -4864,6 +4864,7 @@ def do_instance_action(cs, args): utils.print_dict(action) +@api_versions.wraps("2.0", "2.57") @utils.arg( 'server', metavar='', @@ -4887,6 +4888,56 @@ def do_instance_action_list(cs, args): sortby_index=3) +@api_versions.wraps("2.58") +@utils.arg( + 'server', + metavar='', + help=_('Name or UUID of the server to list actions for. Only UUID can be ' + 'used to list actions on a deleted server.')) +@utils.arg( + '--marker', + dest='marker', + metavar='', + default=None, + help=_('The last instance action of the previous page; displays list of ' + 'actions after "marker".')) +@utils.arg( + '--limit', + dest='limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of instance actions to display. Note that there ' + 'is a configurable max limit on the server, and the limit that is ' + 'used will be the minimum between what is requested here and what ' + 'is configured in the server.')) +@utils.arg( + '--changes-since', + dest='changes_since', + metavar='', + default=None, + help=_('List only instance actions changed after a certain point of ' + 'time. The provided time should be an ISO 8061 formatted time. ' + 'ex 2016-03-04T06:27:59Z.')) +def do_instance_action_list(cs, args): + """List actions on a server.""" + server = _find_server(cs, args.server, raise_if_notfound=False) + if args.changes_since: + try: + timeutils.parse_isotime(args.changes_since) + except ValueError: + raise exceptions.CommandError(_('Invalid changes-since value: %s') + % args.changes_since) + actions = cs.instance_action.list(server, marker=args.marker, + limit=args.limit, + changes_since=args.changes_since) + # TODO(yikun): Output a "Marker" column if there is a next link? + utils.print_list(actions, + ['Action', 'Request_ID', 'Message', 'Start_Time', + 'Updated_At'], + sortby_index=3) + + def do_list_extensions(cs, _args): """ List all the os-api extensions that are available. diff --git a/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml b/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml new file mode 100644 index 000000000..b2fb6a3da --- /dev/null +++ b/releasenotes/notes/microversion-v2_58-327c1031ebfe4a3a.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added support for microversion v2.58 which introduces pagination support + for instance actions with the help of new optional parameters ``limit``, + ``marker``, and also adds the new filter ``changes-since``. Users can use + ``changes-since`` filter to filter the results based on the last time the + instance action was updated.