diff --git a/doc/source/command-objects/server-event.rst b/doc/source/command-objects/server-event.rst new file mode 100644 index 0000000000..ef4685f8d6 --- /dev/null +++ b/doc/source/command-objects/server-event.rst @@ -0,0 +1,45 @@ +============ +server event +============ + +Server event is the event record that had been done on a server, include: event +type(create, delete, reboot and so on), event result(success, error), start +time, finish time and so on. These are important information for server +maintains. + +Compute v2 + +server event list +----------------- + +List recent events of a server + +.. program:: server event list +.. code:: bash + + openstack server event list + + +.. describe:: + + Server to list events (name or ID) + +server event show +----------------- + +Show server event details + +.. program:: server event show +.. code:: bash + + openstack server event show + + + +.. describe:: + + Server to show event details (name or ID) + +.. describe:: + + Request ID of the event to show (ID only) diff --git a/doc/source/commands.rst b/doc/source/commands.rst index a0c67cd4c8..f423618892 100644 --- a/doc/source/commands.rst +++ b/doc/source/commands.rst @@ -137,6 +137,7 @@ referring to both Compute and Volume quotas. * ``server``: (**Compute**) virtual machine instance * ``server backup``: (**Compute**) backup server disk image by using snapshot method * ``server dump``: (**Compute**) a dump file of a server created by features like kdump +* ``server event``: (**Compute**) events of a server * ``server group``: (**Compute**) a grouping of servers * ``server image``: (**Compute**) saved server disk image * ``service``: (**Identity**) a cloud service diff --git a/openstackclient/compute/v2/server_event.py b/openstackclient/compute/v2/server_event.py new file mode 100644 index 0000000000..ccb19ef722 --- /dev/null +++ b/openstackclient/compute/v2/server_event.py @@ -0,0 +1,117 @@ +# Copyright 2017 Huawei, Inc. All rights reserved. +# +# 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. +# + +"""Compute v2 Server operation event implementations""" + +import logging +import six + +from osc_lib.command import command +from osc_lib import utils + +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class ListServerEvent(command.Lister): + _description = _("List recent events of a server") + + def get_parser(self, prog_name): + parser = super(ListServerEvent, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server to list events (name or ID)'), + ) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output") + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + server_id = utils.find_resource(compute_client.servers, + parsed_args.server).id + data = compute_client.instance_action.list(server_id) + + if parsed_args.long: + columns = ( + 'request_id', + 'instance_uuid', + 'action', + 'start_time', + 'message', + 'project_id', + 'user_id', + ) + column_headers = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + 'Message', + 'Project ID', + 'User ID', + ) + else: + columns = ( + 'request_id', + 'instance_uuid', + 'action', + 'start_time', + ) + column_headers = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + ) + + return (column_headers, + (utils.get_item_properties( + s, columns, + ) for s in data)) + + +class ShowServerEvent(command.ShowOne): + _description = _("Show server event details") + + def get_parser(self, prog_name): + parser = super(ShowServerEvent, self).get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server to show event details (name or ID)'), + ) + parser.add_argument( + 'request_id', + metavar='', + help=_('Request ID of the event to show (ID only)'), + ) + return parser + + def take_action(self, parsed_args): + compute_client = self.app.client_manager.compute + server_id = utils.find_resource(compute_client.servers, + parsed_args.server).id + action_detail = compute_client.instance_action.get( + server_id, parsed_args.request_id) + + return zip(*sorted(six.iteritems(action_detail._info))) diff --git a/openstackclient/tests/functional/compute/v2/test_server_event.py b/openstackclient/tests/functional/compute/v2/test_server_event.py new file mode 100644 index 0000000000..6be5822f98 --- /dev/null +++ b/openstackclient/tests/functional/compute/v2/test_server_event.py @@ -0,0 +1,97 @@ +# Copyright 2017 Huawei, Inc. All rights reserved. +# +# 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 json +import uuid + +from openstackclient.tests.functional import base +from openstackclient.tests.functional.compute.v2 import test_server + + +class ServerEventTests(base.TestCase): + """Functional tests for server event.""" + + def setUp(self): + super(ServerEventTests, self).setUp() + _flavor = test_server.ServerTests.get_flavor() + _image = test_server.ServerTests.get_image() + _network = test_server.ServerTests.get_network() + self.server_name = uuid.uuid4().hex + cmd_output = json.loads(self.openstack( + 'server create -f json ' + + '--flavor ' + _flavor + ' ' + + '--image ' + _image + ' ' + + _network + ' ' + + '--wait ' + + self.server_name + )) + if not cmd_output: + self.fail('Server has not been created!') + self.addCleanup(self.openstack, 'server delete ' + self.server_name) + self.assertEqual(self.server_name, cmd_output['name']) + self.server_id = cmd_output.get('id') + + def test_server_event_list_and_show(self): + """Test list, show server event""" + # Test 'server event list' for creating + cmd_output = json.loads(self.openstack( + 'server event list -f json ' + self.server_name + )) + request_id = None + for each_event in cmd_output: + self.assertNotIn('Message', each_event) + self.assertNotIn('Project ID', each_event) + self.assertNotIn('User ID', each_event) + if each_event.get('Action') == 'create': + self.assertEqual(self.server_id, each_event.get('Server ID')) + request_id = each_event.get('Request ID') + break + self.assertIsNotNone(request_id) + # Test 'server event show' for creating + cmd_output = json.loads(self.openstack( + 'server event show -f json ' + self.server_name + ' ' + request_id + )) + self.assertEqual(self.server_id, cmd_output.get('instance_uuid')) + self.assertEqual(request_id, cmd_output.get('request_id')) + self.assertEqual('create', cmd_output.get('action')) + self.assertIsNotNone(cmd_output.get('events')) + self.assertIsInstance(cmd_output.get('events'), list) + + # Reboot server, trigger reboot event + self.openstack('server reboot --wait ' + self.server_name) + # Test 'server event list --long' for rebooting + cmd_output = json.loads(self.openstack( + 'server event list --long -f json ' + self.server_name + )) + request_id = None + for each_event in cmd_output: + self.assertIn('Message', each_event) + self.assertIn('Project ID', each_event) + self.assertIn('User ID', each_event) + if each_event.get('Action') == 'reboot': + request_id = each_event.get('Request ID') + self.assertEqual(self.server_id, each_event.get('Server ID')) + break + self.assertIsNotNone(request_id) + # Test 'server event show' for rebooting + cmd_output = json.loads(self.openstack( + 'server event show -f json ' + self.server_name + ' ' + request_id + )) + + self.assertEqual(self.server_id, cmd_output.get('instance_uuid')) + self.assertEqual(request_id, cmd_output.get('request_id')) + self.assertEqual('reboot', cmd_output.get('action')) + self.assertIsNotNone(cmd_output.get('events')) + self.assertIsInstance(cmd_output.get('events'), list) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 4fe735b6b4..bbb770bbba 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -204,6 +204,9 @@ class FakeComputev2Client(object): self.server_groups = mock.Mock() self.server_groups.resource_class = fakes.FakeResource(None, {}) + self.instance_action = mock.Mock() + self.instance_action.resource_class = fakes.FakeResource(None, {}) + self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] @@ -656,6 +659,47 @@ class FakeServer(object): return mock.Mock(side_effect=servers) +class FakeServerEvent(object): + """Fake one or more server event.""" + + @staticmethod + def create_one_server_event(attrs=None): + """Create a fake server event. + + :param attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id and other attributes + """ + attrs = attrs or {} + + # Set default attributes + server_event_info = { + "instance_uuid": "server-event-" + uuid.uuid4().hex, + "user_id": "user-id-" + uuid.uuid4().hex, + "start_time": "2017-02-27T07:47:13.000000", + "request_id": "req-" + uuid.uuid4().hex, + "action": "create", + "message": None, + "project_id": "project-id-" + uuid.uuid4().hex, + "events": [{ + "finish_time": "2017-02-27T07:47:25.000000", + "start_time": "2017-02-27T07:47:15.000000", + "traceback": None, + "event": "compute__do_build_and_run_instance", + "result": "Success" + }] + } + # Overwrite default attributes + server_event_info.update(attrs) + + server_event = fakes.FakeResource( + info=copy.deepcopy(server_event_info), + loaded=True, + ) + return server_event + + class FakeService(object): """Fake one or more services.""" diff --git a/openstackclient/tests/unit/compute/v2/test_server_event.py b/openstackclient/tests/unit/compute/v2/test_server_event.py new file mode 100644 index 0000000000..5c94891a14 --- /dev/null +++ b/openstackclient/tests/unit/compute/v2/test_server_event.py @@ -0,0 +1,167 @@ +# Copyright 2017 Huawei, Inc. All rights reserved. +# +# 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 openstackclient.compute.v2 import server_event +from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes + + +class TestServerEvent(compute_fakes.TestComputev2): + + fake_server = compute_fakes.FakeServer.create_one_server() + + def setUp(self): + super(TestServerEvent, self).setUp() + + self.servers_mock = self.app.client_manager.compute.servers + self.servers_mock.reset_mock() + self.events_mock = self.app.client_manager.compute.instance_action + self.events_mock.reset_mock() + + self.servers_mock.get.return_value = self.fake_server + + +class TestListServerEvent(TestServerEvent): + + fake_event = compute_fakes.FakeServerEvent.create_one_server_event() + + columns = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + ) + data = (( + fake_event.request_id, + fake_event.instance_uuid, + fake_event.action, + fake_event.start_time, + ), ) + + long_columns = ( + 'Request ID', + 'Server ID', + 'Action', + 'Start Time', + 'Message', + 'Project ID', + 'User ID', + ) + long_data = (( + fake_event.request_id, + fake_event.instance_uuid, + fake_event.action, + fake_event.start_time, + fake_event.message, + fake_event.project_id, + fake_event.user_id, + ), ) + + def setUp(self): + super(TestListServerEvent, self).setUp() + + self.events_mock.list.return_value = [self.fake_event, ] + self.cmd = server_event.ListServerEvent(self.app, None) + + def test_server_event_list(self): + arglist = [ + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('long', False), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_once_with(self.fake_server.name) + self.events_mock.list.assert_called_once_with(self.fake_server.id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, tuple(data)) + + def test_server_event_list_long(self): + arglist = [ + '--long', + self.fake_server.name, + ] + verifylist = [ + ('server', self.fake_server.name), + ('long', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_once_with(self.fake_server.name) + self.events_mock.list.assert_called_once_with(self.fake_server.id) + + self.assertEqual(self.long_columns, columns) + self.assertEqual(self.long_data, tuple(data)) + + +class TestShowServerEvent(TestServerEvent): + + fake_event = compute_fakes.FakeServerEvent.create_one_server_event() + + columns = ( + 'action', + 'events', + 'instance_uuid', + 'message', + 'project_id', + 'request_id', + 'start_time', + 'user_id', + ) + data = ( + fake_event.action, + fake_event.events, + fake_event.instance_uuid, + fake_event.message, + fake_event.project_id, + fake_event.request_id, + fake_event.start_time, + fake_event.user_id, + ) + + def setUp(self): + super(TestShowServerEvent, self).setUp() + + self.events_mock.get.return_value = self.fake_event + self.cmd = server_event.ShowServerEvent(self.app, None) + + def test_server_event_show(self): + arglist = [ + self.fake_server.name, + self.fake_event.request_id, + ] + verifylist = [ + ('server', self.fake_server.name), + ('request_id', self.fake_event.request_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.servers_mock.get.assert_called_once_with(self.fake_server.name) + self.events_mock.get.assert_called_once_with( + self.fake_server.id, self.fake_event.request_id) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/releasenotes/notes/bug-1642030-166b2b28c8adf22e.yaml b/releasenotes/notes/bug-1642030-166b2b28c8adf22e.yaml new file mode 100644 index 0000000000..200c73fbe8 --- /dev/null +++ b/releasenotes/notes/bug-1642030-166b2b28c8adf22e.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + Add ``server event`` list and show commands, that is similar to nova's + instance action commands. + + Server event is the event record that had been done on a server, + include: event type(create, delete, reboot and so on), + event result(success, error), start time, finish time and so on. + These are important information for server maintains. + [Bug `1642030 `_] diff --git a/setup.cfg b/setup.cfg index e18aa5c1e7..ea4691b32e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -135,6 +135,9 @@ openstack.compute.v2 = server_backup_create = openstackclient.compute.v2.server_backup:CreateServerBackup + server_event_list = openstackclient.compute.v2.server_event:ListServerEvent + server_event_show = openstackclient.compute.v2.server_event:ShowServerEvent + server_group_create = openstackclient.compute.v2.server_group:CreateServerGroup server_group_delete = openstackclient.compute.v2.server_group:DeleteServerGroup server_group_list = openstackclient.compute.v2.server_group:ListServerGroup