From 6d8be2b2dff1f774408dac9ae02f4e52baeb3c38 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Sat, 23 Oct 2021 15:56:29 +0100 Subject: [PATCH] compute: Add support for instance actions These become server actions, in keeping with our preference for using "server" rather than "instance". This is a read-only API so relatively easy to implement. Change-Id: I97d885cbaf99862cff801816c306e2d95b44e7ce Signed-off-by: Stephen Finucane --- openstack/compute/v2/_proxy.py | 44 +++++++++ openstack/compute/v2/server_action.py | 88 ++++++++++++++++++ openstack/tests/unit/compute/v2/test_proxy.py | 24 +++++ .../unit/compute/v2/test_server_actions.py | 91 +++++++++++++++++++ 4 files changed, 247 insertions(+) create mode 100644 openstack/compute/v2/server_action.py create mode 100644 openstack/tests/unit/compute/v2/test_server_actions.py diff --git a/openstack/compute/v2/_proxy.py b/openstack/compute/v2/_proxy.py index cab0a2619..f1e6de584 100644 --- a/openstack/compute/v2/_proxy.py +++ b/openstack/compute/v2/_proxy.py @@ -23,6 +23,7 @@ from openstack.compute.v2 import limits from openstack.compute.v2 import migration as _migration from openstack.compute.v2 import quota_set as _quota_set from openstack.compute.v2 import server as _server +from openstack.compute.v2 import server_action as _server_action from openstack.compute.v2 import server_diagnostics as _server_diagnostics from openstack.compute.v2 import server_group as _server_group from openstack.compute.v2 import server_interface as _server_interface @@ -2010,6 +2011,49 @@ class Proxy(proxy.Proxy): query = {} return res.commit(self, **query) + # ========== Server actions ========== + + def get_server_action(self, server_action, server, ignore_missing=True): + """Get a single server action + + :param server_action: The value can be the ID of a server action or a + :class:`~openstack.compute.v2.server_action.ServerAction` instance. + :param server: This parameter need to be specified when ServerAction ID + is given as value. It can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server` instance that the + action is associated with. + :param bool ignore_missing: When set to ``False`` + :class:`~openstack.exceptions.ResourceNotFound` will be raised when + the server action does not exist. When set to ``True``, no + exception will be set when attempting to retrieve a non-existent + server action. + + :returns: One :class:`~openstack.compute.v2.server_action.ServerAction` + :raises: :class:`~openstack.exceptions.ResourceNotFound` when no + resource can be found. + """ + server_id = self._get_uri_attribute(server_action, server, 'server_id') + server_action = resource.Resource._get_id(server_action) + + return self._get( + _server_action.ServerAction, + server_id=server_id, + action_id=server_action, + ignore_missing=ignore_missing, + ) + + def server_actions(self, server): + """Return a generator of server actions + + :param server: The server can be either the ID of a server or a + :class:`~openstack.compute.v2.server.Server`. + + :returns: A generator of ServerAction objects + :rtype: :class:`~openstack.compute.v2.server_action.ServerAction` + """ + server_id = resource.Resource._get_id(server) + return self._list(_server_action.ServerAction, server_id=server_id) + # ========== Utilities ========== def wait_for_server( diff --git a/openstack/compute/v2/server_action.py b/openstack/compute/v2/server_action.py new file mode 100644 index 000000000..fff96ae88 --- /dev/null +++ b/openstack/compute/v2/server_action.py @@ -0,0 +1,88 @@ +# 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 openstack import resource + + +class ServerActionEvent(resource.Resource): + + # Added the 'details' field in 2.84 + _max_microversion = '2.84' + + #: The name of the event + event = resource.Body('event') + #: The date and time when the event was started. The date and time stamp + #: format is ISO 8601 + start_time = resource.Body('start_time') + #: The date and time when the event finished. The date and time stamp + #: format is ISO 8601 + finish_time = resource.Body('finish_time') + #: The result of the event + result = resource.Body('result') + #: The traceback stack if an error occurred in this event. + #: This is only visible to cloud admins by default. + traceback = resource.Body('traceback') + #: The name of the host on which the event occurred. + #: This is only visible to cloud admins by default. + host = resource.Body('host') + #: An obfuscated hashed host ID string, or the empty string if there is no + #: host for the event. This is a hashed value so will not actually look + #: like a hostname, and is hashed with data from the project_id, so the + #: same physical host as seen by two different project_ids will be + #: different. This is useful when within the same project you need to + #: determine if two events occurred on the same or different physical + #: hosts. + host_id = resource.Body('hostId') + #: Details of the event. May be unset. + details = resource.Body('details') + + +class ServerAction(resource.Resource): + resource_key = 'instanceAction' + resources_key = 'instanceActions' + base_path = '/servers/{server_id}/os-instance-actions' + + # capabilities + allow_fetch = True + allow_list = True + + # Properties + + #: The ID of the server that this action relates to. + server_id = resource.URI('server_id') + + #: The name of the action. + action = resource.Body('action') + # FIXME(stephenfin): This conflicts since there is a server ID in the URI + # *and* in the body. We need a field that handles both or we need to use + # different names. + # #: The ID of the server that this action relates to. + # server_id = resource.Body('instance_uuid') + #: The ID of the request that this action related to. + request_id = resource.Body('request_id') + #: The ID of the user which initiated the server action. + user_id = resource.Body('user_id') + #: The ID of the project that this server belongs to. + project_id = resource.Body('project_id') + start_time = resource.Body('start_time') + #: The related error message for when an action fails. + message = resource.Body('message') + #: Events + events = resource.Body('events', type=list, list_type=ServerActionEvent) + + # events.details field added in 2.84 + _max_microversion = '2.84' + + _query_mapping = resource.QueryParameters( + changes_since="changes-since", + changes_before="changes-before", + ) diff --git a/openstack/tests/unit/compute/v2/test_proxy.py b/openstack/tests/unit/compute/v2/test_proxy.py index 303831485..bf697cff6 100644 --- a/openstack/tests/unit/compute/v2/test_proxy.py +++ b/openstack/tests/unit/compute/v2/test_proxy.py @@ -23,6 +23,7 @@ from openstack.compute.v2 import keypair from openstack.compute.v2 import migration from openstack.compute.v2 import quota_set from openstack.compute.v2 import server +from openstack.compute.v2 import server_action from openstack.compute.v2 import server_group from openstack.compute.v2 import server_interface from openstack.compute.v2 import server_ip @@ -1243,3 +1244,26 @@ class TestQuota(TestComputeProxy): quota_set.QuotaSet, 'qs', a='b' ) + + +class TestServerAction(TestComputeProxy): + + def test_server_action_get(self): + self._verify( + 'openstack.proxy.Proxy._get', + self.proxy.get_server_action, + method_args=['action_id'], + method_kwargs={'server': 'server_id'}, + expected_args=[server_action.ServerAction], + expected_kwargs={ + 'action_id': 'action_id', 'server_id': 'server_id', + }, + ) + + def test_server_actions(self): + self.verify_list( + self.proxy.server_actions, + server_action.ServerAction, + method_kwargs={'server': 'server_a'}, + expected_kwargs={'server_id': 'server_a'}, + ) diff --git a/openstack/tests/unit/compute/v2/test_server_actions.py b/openstack/tests/unit/compute/v2/test_server_actions.py new file mode 100644 index 000000000..bb34e2661 --- /dev/null +++ b/openstack/tests/unit/compute/v2/test_server_actions.py @@ -0,0 +1,91 @@ +# 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 openstack.compute.v2 import server_action +from openstack.tests.unit import base + +EXAMPLE = { + 'action': 'stop', + 'events': [ + { + 'event': 'compute_stop_instance', + 'finish_time': '2018-04-25T01:26:36.790544', + 'host': 'compute', + 'hostId': '2091634baaccdc4c5a1d57069c833e402921df696b7f970791b12ec6', # noqa: E501 + 'result': 'Success', + 'start_time': '2018-04-25T01:26:36.539271', + 'traceback': None, + 'details': None + } + ], + 'instance_uuid': '4bf3473b-d550-4b65-9409-292d44ab14a2', + 'message': None, + 'project_id': '6f70656e737461636b20342065766572', + 'request_id': 'req-0d819d5c-1527-4669-bdf0-ffad31b5105b', + 'start_time': '2018-04-25T01:26:36.341290', + 'updated_at': '2018-04-25T01:26:36.790544', + 'user_id': 'admin', +} + + +class TestServerAction(base.TestCase): + + def setUp(self): + super().setUp() + self.resp = mock.Mock() + self.resp.body = None + self.resp.json = mock.Mock(return_value=self.resp.body) + self.resp.status_code = 200 + self.sess = mock.Mock() + self.sess.post = mock.Mock(return_value=self.resp) + + def test_basic(self): + sot = server_action.ServerAction() + self.assertEqual('instanceAction', sot.resource_key) + self.assertEqual('instanceActions', sot.resources_key) + self.assertEqual( + '/servers/{server_id}/os-instance-actions', + sot.base_path, + ) + self.assertTrue(sot.allow_fetch) + self.assertTrue(sot.allow_list) + self.assertFalse(sot.allow_create) + self.assertFalse(sot.allow_commit) + self.assertFalse(sot.allow_delete) + + self.assertDictEqual( + { + 'changes_before': 'changes-before', + 'changes_since': 'changes-since', + 'limit': 'limit', + 'marker': 'marker', + }, + sot._query_mapping._mapping, + ) + + def test_make_it(self): + sot = server_action.ServerAction(**EXAMPLE) + self.assertEqual(EXAMPLE['action'], sot.action) + # FIXME: This isn't populated since it conflicts with the server_id URI + # argument + # self.assertEqual(EXAMPLE['instance_uuid'], sot.server_id) + self.assertEqual(EXAMPLE['message'], sot.message) + self.assertEqual(EXAMPLE['project_id'], sot.project_id) + self.assertEqual(EXAMPLE['request_id'], sot.request_id) + self.assertEqual(EXAMPLE['start_time'], sot.start_time) + self.assertEqual(EXAMPLE['user_id'], sot.user_id) + self.assertEqual( + [server_action.ServerActionEvent(**e) for e in EXAMPLE['events']], + sot.events, + )