diff --git a/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml b/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml new file mode 100644 index 000000000..a3e76872e --- /dev/null +++ b/releasenotes/notes/add-server-console-078ed2696e5b04d9.yaml @@ -0,0 +1,6 @@ +--- +features: + - Added get_server_console method to fetch the console + log from a Server. On clouds that do not expose this + feature, a debug line will be logged and an empty + string will be returned. diff --git a/shade/_tasks.py b/shade/_tasks.py index 238eafe20..09f1c78b6 100644 --- a/shade/_tasks.py +++ b/shade/_tasks.py @@ -152,6 +152,11 @@ class ServerListSecurityGroups(task_manager.Task): return client.nova_client.servers.list_security_group(**self.args) +class ServerConsoleGet(task_manager.Task): + def main(self, client): + return client.nova_client.servers.get_console_output(**self.args) + + class ServerGet(task_manager.Task): def main(self, client): return client.nova_client.servers.get(**self.args) diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index ec2a85fa8..f8c177c71 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1969,6 +1969,40 @@ class OpenStackCloud(object): return _utils._get_entity( self.search_security_groups, name_or_id, filters) + def get_server_console(self, server, length=None): + """Get the console log for a server. + + :param server: The server to fetch the console log for. Can be either + a server dict or the Name or ID of the server. + :param int length: The number of lines you would like to retrieve from + the end of the log. (optional, defaults to all) + + :returns: A string containing the text of the console log or an + empty string if the cloud does not support console logs. + :raises: OpenStackCloudException if an invalid server argument is given + or if something else unforseen happens + """ + + if not isinstance(server, dict): + server = self.get_server(server) + + if not server: + raise OpenStackCloudException( + "Console log requested for invalid server") + + try: + return self.manager.submitTask( + _tasks.ServerConsoleGet(server=server['id'], length=length), + raw=True) + except nova_exceptions.BadRequest: + return "" + except OpenStackCloudException: + raise + except Exception as e: + raise OpenStackCloudException( + "Unable to get console log for {server}: {exception}".format( + server=server['id'], exception=str(e))) + def get_server(self, name_or_id=None, filters=None, detailed=False): """Get a server by name or ID. diff --git a/shade/task_manager.py b/shade/task_manager.py index d2dce731d..cca56ad52 100644 --- a/shade/task_manager.py +++ b/shade/task_manager.py @@ -92,7 +92,7 @@ class Task(object): elif (not isinstance(self._result, bool) and not isinstance(self._result, int) and not isinstance(self._result, float) and - not isinstance(self._result, str) and + not isinstance(self._result, six.string_types) and not isinstance(self._result, set) and not isinstance(self._result, tuple) and not isinstance(self._result, types.GeneratorType)): diff --git a/shade/tests/functional/test_compute.py b/shade/tests/functional/test_compute.py index d17ebfaa1..92478b666 100644 --- a/shade/tests/functional/test_compute.py +++ b/shade/tests/functional/test_compute.py @@ -19,9 +19,12 @@ test_compute Functional tests for `shade` compute methods. """ +import six + from shade import exc from shade.tests.functional import base from shade.tests.functional.util import pick_flavor, pick_image +from shade import _utils class TestCompute(base.BaseFunctionalTestCase): @@ -67,6 +70,40 @@ class TestCompute(base.BaseFunctionalTestCase): self.demo_cloud.delete_server(self.server_name, wait=True)) self.assertIsNone(self.demo_cloud.get_server(self.server_name)) + def test_get_server_console(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + server = self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) + for _ in _utils._iterate_timeout( + 5, "Did not get more than 0 lines in the console log"): + log = self.demo_cloud.get_server_console(server=server) + self.assertTrue(isinstance(log, six.string_types)) + if len(log) > 0: + break + + def test_get_server_console_name_or_id(self): + self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) + self.demo_cloud.create_server( + name=self.server_name, + image=self.image, + flavor=self.flavor, + wait=True) + for _ in _utils._iterate_timeout( + 5, "Did not get more than 0 lines in the console log"): + log = self.demo_cloud.get_server_console(server=self.server_name) + self.assertTrue(isinstance(log, six.string_types)) + if len(log) > 0: + break + + def test_get_server_console_bad_server(self): + self.assertRaises( + exc.OpenStackCloudException, + self.demo_cloud.get_server_console, + server=self.server_name) + def test_create_and_delete_server_with_admin_pass(self): self.addCleanup(self._cleanup_servers_and_volumes, self.server_name) server = self.demo_cloud.create_server( diff --git a/shade/tests/unit/test_server_console.py b/shade/tests/unit/test_server_console.py new file mode 100644 index 000000000..2f5467912 --- /dev/null +++ b/shade/tests/unit/test_server_console.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# 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 mock +import novaclient.exceptions as nova_exceptions + +import shade +from shade.tests.unit import base +from shade.tests import fakes + + +class TestServerConsole(base.TestCase): + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_server_console_dict(self, mock_nova): + server = dict(id='12345') + self.cloud.get_server_console(server) + + mock_nova.servers.list.assert_not_called() + mock_nova.servers.get_console_output.assert_called_once_with( + server='12345', length=None) + + @mock.patch.object(shade.OpenStackCloud, 'has_service') + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_server_console_name_or_id(self, mock_nova, mock_has_service): + server = '12345' + + fake_server = fakes.FakeServer(server, '', 'ACTIVE') + mock_nova.servers.get.return_value = fake_server + mock_nova.servers.list.return_value = [fake_server] + mock_has_service.return_value = False + + self.cloud.get_server_console(server) + + mock_nova.servers.get_console_output.assert_called_once_with( + server='12345', length=None) + + @mock.patch.object(shade.OpenStackCloud, 'nova_client') + def test_get_server_console_no_console(self, mock_nova): + server = dict(id='12345') + exc = nova_exceptions.BadRequest( + 'There is no such action: os-getConsoleOutput') + mock_nova.servers.get_console_output.side_effect = exc + log = self.cloud.get_server_console(server) + + self.assertEqual('', log) + mock_nova.servers.list.assert_not_called() + mock_nova.servers.get_console_output.assert_called_once_with( + server='12345', length=None)