diff options
| author | Jake Dahn <jake@ansolabs.com> | 2011-10-24 23:56:05 +0000 |
|---|---|---|
| committer | Brian Waldon <brian.waldon@rackspace.com> | 2011-12-19 16:46:43 -0500 |
| commit | 2033aef54f3c43ba73847e34e1fdc6490fd6f851 (patch) | |
| tree | ff437b7663d9a4c30248067c42469c1253ccb8dd | |
| parent | 90668f8564e65f95a681baee847a384e484c46d8 (diff) | |
Add a console output action to servers
Relates to blueprint osapi-console-log and bug 876809. Adds equivalent
of euca-get-console-output to openstack api as an extension.
Change-Id: Ia71361ebbec820616a3007e216b0b9ff98d43541
| -rw-r--r-- | nova/api/openstack/v2/contrib/console_output.py | 60 | ||||
| -rw-r--r-- | nova/compute/api.py | 5 | ||||
| -rw-r--r-- | nova/compute/manager.py | 17 | ||||
| -rw-r--r-- | nova/tests/api/ec2/test_cloud.py | 3 | ||||
| -rw-r--r-- | nova/tests/api/openstack/v2/contrib/test_console_output.py | 69 | ||||
| -rw-r--r-- | nova/tests/api/openstack/v2/test_extensions.py | 1 | ||||
| -rw-r--r-- | nova/tests/test_compute.py | 15 | ||||
| -rw-r--r-- | nova/virt/fake.py | 2 |
8 files changed, 164 insertions, 8 deletions
diff --git a/nova/api/openstack/v2/contrib/console_output.py b/nova/api/openstack/v2/contrib/console_output.py new file mode 100644 index 000000000..32bd3a5fb --- /dev/null +++ b/nova/api/openstack/v2/contrib/console_output.py @@ -0,0 +1,60 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2011 OpenStack LLC. +# Copyright 2011 Grid Dynamics +# Copyright 2011 Eldar Nugaev, Kirill Shileev, Ilya Alekseyev +# +# 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 webob + +from nova import compute +from nova import exception +from nova import log as logging +from nova.api.openstack.v2 import extensions + + +LOG = logging.getLogger('nova.api.openstack.v2.contrib.console_output') + + +class Console_output(extensions.ExtensionDescriptor): + """Console log output support, with tailing ability.""" + + name = "Console_output" + alias = "os-console-output" + namespace = "http://docs.openstack.org/ext/os-console-output/api/v2" + updated = "2011-12-08T00:00:00+00:00" + + def __init__(self, ext_mgr): + self.compute_api = compute.API() + super(Console_output, self).__init__(ext_mgr) + + def get_console_output(self, input_dict, req, server_id): + """Get text console output.""" + context = req.environ['nova.context'] + length = input_dict['os-getConsoleOutput'].get('length') + try: + return self.compute_api.get_console_output(context, + server_id, + length) + except exception.ApiError, e: + raise webob.exc.HTTPBadRequest(explanation=e.message) + except exception.NotAuthorized, e: + raise webob.exc.HTTPUnauthorized() + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [extensions.ActionExtension("servers", "os-getConsoleOutput", + self.get_console_output)] + + return actions diff --git a/nova/compute/api.py b/nova/compute/api.py index 0fd55f97d..bfeb60a7a 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -1486,11 +1486,12 @@ class API(base.Base): 'hostignore', 'portignore')} - def get_console_output(self, context, instance): + def get_console_output(self, context, instance, tail_length=None): """Get console output for an an instance.""" return self._call_compute_message('get_console_output', context, - instance) + instance, + {'tail_length': tail_length}) def lock(self, context, instance): """Lock the given instance.""" diff --git a/nova/compute/manager.py b/nova/compute/manager.py index d6912bb56..1abcf842e 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -1377,15 +1377,30 @@ class ComputeManager(manager.SchedulerDependentManager): self.driver.inject_network_info(instance, network_info) @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) - def get_console_output(self, context, instance_uuid): + def get_console_output(self, context, instance_uuid, tail_length=None): """Send the console output for the given instance.""" context = context.elevated() instance_ref = self.db.instance_get_by_uuid(context, instance_uuid) LOG.audit(_("Get console output for instance %s"), instance_uuid, context=context) output = self.driver.get_console_output(instance_ref) + + if tail_length is not None: + output = self._tail_log(output, tail_length) + return output.decode('utf-8', 'replace').encode('ascii', 'replace') + def _tail_log(self, log, length): + try: + length = int(length) + except ValueError: + length = 0 + + if length == 0: + return '' + else: + return '\n'.join(log.split('\n')[-int(length):]) + @exception.wrap_exception(notifier=notifier, publisher_id=publisher_id()) def get_ajax_console(self, context, instance_uuid): """Return connection information for an ajax console.""" diff --git a/nova/tests/api/ec2/test_cloud.py b/nova/tests/api/ec2/test_cloud.py index b44bd2322..7487db4a6 100644 --- a/nova/tests/api/ec2/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py @@ -21,7 +21,6 @@ import copy import functools import os -from eventlet import greenthread from M2Crypto import BIO from M2Crypto import RSA @@ -1128,7 +1127,7 @@ class CloudTestCase(test.TestCase): output = self.cloud.get_console_output(context=self.context, instance_id=[instance_id]) self.assertEquals(base64.b64decode(output['output']), - 'FAKE CONSOLE?OUTPUT') + 'FAKE CONSOLE OUTPUT\nANOTHER\nLAST LINE') # TODO(soren): We need this until we can stop polling in the rpc code # for unit tests. rv = self.cloud.terminate_instances(self.context, [instance_id]) diff --git a/nova/tests/api/openstack/v2/contrib/test_console_output.py b/nova/tests/api/openstack/v2/contrib/test_console_output.py new file mode 100644 index 000000000..bfe48ecea --- /dev/null +++ b/nova/tests/api/openstack/v2/contrib/test_console_output.py @@ -0,0 +1,69 @@ +# Copyright 2011 Eldar Nugaev +# 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 webob + +from nova import compute +from nova import test +from nova.tests.api.openstack import fakes + + +def fake_text_console_tail(self, method, context, instance_id, params): + tail_length = params['tail_length'] + fixture = [str(i) for i in range(10)] + + if tail_length is None: + pass + elif tail_length == 0: + fixture = [] + else: + fixture = fixture[-int(tail_length):] + + return '\n'.join(fixture) + + +class ConsoleOutputExtensionTest(test.TestCase): + + def setUp(self): + super(ConsoleOutputExtensionTest, self).setUp() + + def test_get_text_console_instance_action(self): + self.stubs.Set(compute.API, '_call_compute_message', + fake_text_console_tail) + + body = {'os-getConsoleOutput': {}} + req = webob.Request.blank('/v1.1/123/servers/1/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + + def test_get_console_output_with_tail(self): + self.stubs.Set(compute.API, + '_call_compute_message', + fake_text_console_tail) + + body = {'os-getConsoleOutput': {'length': 3}} + req = webob.Request.blank('/v2/123/servers/1/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 200) diff --git a/nova/tests/api/openstack/v2/test_extensions.py b/nova/tests/api/openstack/v2/test_extensions.py index 650770f50..2c29f6eed 100644 --- a/nova/tests/api/openstack/v2/test_extensions.py +++ b/nova/tests/api/openstack/v2/test_extensions.py @@ -98,6 +98,7 @@ class ExtensionControllerTest(ExtensionTestCase): super(ExtensionControllerTest, self).setUp() self.ext_list = [ "AdminActions", + "Console_output", "Createserverext", "DeferredDelete", "DiskConfig", diff --git a/nova/tests/test_compute.py b/nova/tests/test_compute.py index b01344463..93eabae3d 100644 --- a/nova/tests/test_compute.py +++ b/nova/tests/test_compute.py @@ -588,9 +588,20 @@ class ComputeTestCase(BaseTestCase): instance = self._create_fake_instance() self.compute.run_instance(self.context, instance['uuid']) - console = self.compute.get_console_output(self.context, + output = self.compute.get_console_output(self.context, instance['uuid']) - self.assert_(console) + self.assertEqual(output, 'FAKE CONSOLE OUTPUT\nANOTHER\nLAST LINE') + self.compute.terminate_instance(self.context, instance['uuid']) + + def test_console_output_tail(self): + """Make sure we can get console output from instance""" + instance = self._create_fake_instance() + self.compute.run_instance(self.context, instance['uuid']) + + output = self.compute.get_console_output(self.context, + instance['uuid'], + tail_length=2) + self.assertEqual(output, 'ANOTHER\nLAST LINE') self.compute.terminate_instance(self.context, instance['uuid']) def test_ajax_console(self): diff --git a/nova/virt/fake.py b/nova/virt/fake.py index c1e32be90..d25fe112e 100644 --- a/nova/virt/fake.py +++ b/nova/virt/fake.py @@ -218,7 +218,7 @@ class FakeConnection(driver.ComputeDriver): return [0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L] def get_console_output(self, instance): - return 'FAKE CONSOLE\xffOUTPUT' + return 'FAKE CONSOLE OUTPUT\nANOTHER\nLAST LINE' def get_ajax_console(self, instance): return {'token': 'FAKETOKEN', |
