diff options
author | John Garbutt <john@johngarbutt.com> | 2013-05-23 17:42:51 +0100 |
---|---|---|
committer | Gerrit Code Review <review@openstack.org> | 2013-06-12 14:39:49 +0000 |
commit | 6e93fefade26ddb6282e423af51f806a08efc8b0 (patch) | |
tree | 1874de5751cc12efc0d72e3cecf8fceddb833d39 | |
parent | 4461f20bd6187ec02e00cd862d754df38523f9ef (diff) | |
download | nova-6e93fefade26ddb6282e423af51f806a08efc8b0.tar.gz nova-6e93fefade26ddb6282e423af51f806a08efc8b0.tar.xz nova-6e93fefade26ddb6282e423af51f806a08efc8b0.zip |
xenapi: implement get_console_output for XCP/XenServer
If an administrator has enabled the logging of guest
consoles on XCP/XenServer, this enables nova to return
the last MB of those logs to the user.
The management of the logs on the server is a little
tricky, and will be sorted in a later patch.
Change was based on this previous idea:
https://review.openstack.org/#/c/17959/
DocImpact
Part of blueprint xenapi-server-log
Change-Id: I23c83bcf8c648cc2714a0c78951acc29a16d5c31
-rw-r--r-- | nova/tests/virt/xenapi/test_vmops.py | 75 | ||||
-rw-r--r-- | nova/tests/virt/xenapi/test_xenapi.py | 11 | ||||
-rw-r--r-- | nova/virt/xenapi/fake.py | 8 | ||||
-rw-r--r-- | nova/virt/xenapi/vmops.py | 26 | ||||
-rw-r--r-- | plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec | 1 | ||||
-rwxr-xr-x | plugins/xenserver/xenapi/etc/xapi.d/plugins/console | 80 |
6 files changed, 195 insertions, 6 deletions
diff --git a/nova/tests/virt/xenapi/test_vmops.py b/nova/tests/virt/xenapi/test_vmops.py index 18a444f41..674d84882 100644 --- a/nova/tests/virt/xenapi/test_vmops.py +++ b/nova/tests/virt/xenapi/test_vmops.py @@ -18,8 +18,12 @@ from nova.compute import task_states from nova.compute import vm_mode +from nova import exception from nova import test +from nova.tests.virt.xenapi import stubs from nova.virt import fake +from nova.virt.xenapi import driver as xenapi_conn +from nova.virt.xenapi import fake as xenapi_fake from nova.virt.xenapi import vm_utils from nova.virt.xenapi import vmops @@ -166,3 +170,74 @@ class VMOpsTestCase(test.TestCase): self.assertTrue(self._vmops._is_xsm_sr_check_relaxed()) self.assertEqual(self.make_plugin_call_count, 1) + + +class GetConsoleOutputTestCase(stubs.XenAPITestBase): + def setUp(self): + super(GetConsoleOutputTestCase, self).setUp() + stubs.stubout_session(self.stubs, xenapi_fake.SessionBase) + self._session = xenapi_conn.XenAPISession('test_url', 'root', + 'test_pass', fake.FakeVirtAPI()) + self.vmops = vmops.VMOps(self._session, fake.FakeVirtAPI()) + self.vms = [] + + def tearDown(self): + super(GetConsoleOutputTestCase, self).tearDown() + for vm in self.vms: + xenapi_fake.destroy_vm(vm) + + def _create_vm(self, name, state): + vm = xenapi_fake.create_vm(name, state) + self.vms.append(vm) + return vm + + def test_get_console_output_works(self): + self.mox.StubOutWithMock(self.vmops, '_get_dom_id') + + instance = {"name": "dummy"} + self.vmops._get_dom_id(instance, check_rescue=True).AndReturn(42) + self.mox.ReplayAll() + + self.assertEqual("dom_id: 42", self.vmops.get_console_output(instance)) + + def test_get_console_output_throws_nova_exception(self): + self.mox.StubOutWithMock(self.vmops, '_get_dom_id') + + instance = {"name": "dummy"} + # dom_id=0 used to trigger exception in fake XenAPI + self.vmops._get_dom_id(instance, check_rescue=True).AndReturn(0) + self.mox.ReplayAll() + + self.assertRaises(exception.NovaException, + self.vmops.get_console_output, instance) + + def test_get_dom_id_works(self): + instance = {"name": "dummy"} + vm_ref = self._create_vm("dummy", "Running") + vm_rec = xenapi_fake.get_record("VM", vm_ref) + + self.assertEqual(vm_rec["domid"], self.vmops._get_dom_id(instance)) + + def test_get_dom_id_works_with_rescue_vm(self): + instance = {"name": "dummy"} + vm_ref = self._create_vm("dummy-rescue", "Running") + vm_rec = xenapi_fake.get_record("VM", vm_ref) + + self.assertEqual(vm_rec["domid"], + self.vmops._get_dom_id(instance, check_rescue=True)) + + def test_get_dom_id_raises_not_found(self): + instance = {"name": "dummy"} + vm_ref = self._create_vm("notdummy", "Running") + vm_rec = xenapi_fake.get_record("VM", vm_ref) + + self.assertRaises(exception.NotFound, + self.vmops._get_dom_id, instance) + + def test_get_dom_id_works_with_vmref(self): + instance = {"name": "dummy"} + vm_ref = self._create_vm("dummy", "Running") + vm_rec = xenapi_fake.get_record("VM", vm_ref) + + self.assertEqual(vm_rec["domid"], + self.vmops._get_dom_id(vm_ref=vm_ref)) diff --git a/nova/tests/virt/xenapi/test_xenapi.py b/nova/tests/virt/xenapi/test_xenapi.py index 7dcb1cec8..d99fdcb8e 100644 --- a/nova/tests/virt/xenapi/test_xenapi.py +++ b/nova/tests/virt/xenapi/test_xenapi.py @@ -1121,6 +1121,17 @@ class XenAPIVMTestCase(stubs.XenAPITestBase): conn.reboot(self.context, instance, None, "SOFT") + def test_get_console_output_succeeds(self): + + def fake_get_console_output(instance): + self.assertEqual("instance", instance) + return "console_log" + self.stubs.Set(self.conn._vmops, 'get_console_output', + fake_get_console_output) + + self.assertEqual(self.conn.get_console_output("instance"), + "console_log") + def _test_maintenance_mode(self, find_host, find_aggregate): real_call_xenapi = self.conn._session.call_xenapi instance = self._create_instance(spawn=True) diff --git a/nova/virt/xenapi/fake.py b/nova/virt/xenapi/fake.py index 69b0c2010..379671370 100644 --- a/nova/virt/xenapi/fake.py +++ b/nova/virt/xenapi/fake.py @@ -50,10 +50,12 @@ A fake XenAPI SDK. """ +import base64 import pickle import random import uuid from xml.sax import saxutils +import zlib import pprint @@ -608,6 +610,12 @@ class SessionBase(object): def _plugin_xenhost_host_uptime(self, method, args): return jsonutils.dumps({"uptime": "fake uptime"}) + def _plugin_console_get_console_log(self, method, args): + dom_id = args["dom_id"] + if dom_id == 0: + raise Failure('Guest does not have a console') + return base64.b64encode(zlib.compress("dom_id: %s" % dom_id)) + def host_call_plugin(self, _1, _2, plugin, method, args): func = getattr(self, '_plugin_%s_%s' % (plugin, method), None) if not func: diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 56a4b18d8..bf8aa3e52 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -19,9 +19,11 @@ Management class for VM-related functions (spawn, reboot, etc). """ +import base64 import functools import itertools import time +import zlib from eventlet import greenthread import netaddr @@ -1421,9 +1423,18 @@ class VMOps(object): return bw def get_console_output(self, instance): - """Return snapshot of console.""" - # TODO(armando-migliaccio): implement this to fix pylint! - return 'FAKE CONSOLE OUTPUT of instance' + """Return last few lines of instance console.""" + dom_id = self._get_dom_id(instance, check_rescue=True) + + try: + raw_console_data = self._session.call_plugin('console', + 'get_console_log', {'dom_id': dom_id}) + except self._session.XenAPI.Failure as exc: + LOG.exception(exc) + msg = _("Guest does not have a console available") + raise exception.NovaException(msg) + + return zlib.decompress(base64.b64decode(raw_console_data)) def get_vnc_console(self, instance): """Return connection info for a vnc console.""" @@ -1607,9 +1618,7 @@ class VMOps(object): """ args = {} if instance or vm_ref: - vm_ref = vm_ref or self._get_vm_opaque_ref(instance) - vm_rec = self._session.call_xenapi("VM.get_record", vm_ref) - args['dom_id'] = vm_rec['domid'] + args['dom_id'] = self._get_dom_id(instance, vm_ref) args.update(addl_args) try: return self._session.call_plugin(plugin, method, args) @@ -1630,6 +1639,11 @@ class VMOps(object): return {'returncode': 'error', 'message': err_msg} return None + def _get_dom_id(self, instance=None, vm_ref=None, check_rescue=False): + vm_ref = vm_ref or self._get_vm_opaque_ref(instance, check_rescue) + vm_rec = self._session.call_xenapi("VM.get_record", vm_ref) + return vm_rec['domid'] + def _add_to_param_xenstore(self, vm_ref, key, val): """ Takes a key/value pair and adds it to the xenstore parameter diff --git a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec index b93c7b071..85c2d1c05 100644 --- a/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec +++ b/plugins/xenserver/xenapi/contrib/rpmbuild/SPECS/openstack-xen-plugins.spec @@ -32,6 +32,7 @@ rm -rf $RPM_BUILD_ROOT /etc/xapi.d/plugins/bandwidth /etc/xapi.d/plugins/bittorrent /etc/xapi.d/plugins/config_file +/etc/xapi.d/plugins/console /etc/xapi.d/plugins/glance /etc/xapi.d/plugins/kernel /etc/xapi.d/plugins/migration diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/console b/plugins/xenserver/xenapi/etc/xapi.d/plugins/console new file mode 100755 index 000000000..afcb783f7 --- /dev/null +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/console @@ -0,0 +1,80 @@ +#!/usr/bin/python +# 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. + +""" +To configure this plugin, you must set the following xenstore key: +/local/logconsole/@ = "/var/log/xen/guest/console.%d" + +This can be done by running: +xenstore-write /local/logconsole/@ "/var/log/xen/guest/console.%d" + +WARNING: +You should ensure appropriate log rotation to ensure +guests are not able to consume too much Dom0 disk space, +and equally should not be able to stop other guests from logging. +Adding and removing the following xenstore key will reopen the log, +as will be required after a log rotate: +/local/logconsole/<dom_id> +""" + +import base64 +import logging +import os +import zlib + +import XenAPIPlugin + +import pluginlib_nova +pluginlib_nova.configure_logging("console") + +CONSOLE_LOG_DIR = '/var/log/xen/guest' +CONSOLE_LOG_FILE_PATTERN = CONSOLE_LOG_DIR + '/console.%d' + +MAX_CONSOLE_BYTES = 102400 +SEEK_SET = 0 +SEEK_END = 2 + + +def _last_bytes(file_like_object): + try: + file_like_object.seek(-MAX_CONSOLE_BYTES, SEEK_END) + except IOError, e: + if e.errno == 22: + file_like_object.seek(0, SEEK_SET) + else: + raise + return file_like_object.read() + + +def get_console_log(session, arg_dict): + try: + raw_dom_id = arg_dict['dom_id'] + except KeyError: + raise pluginlib_nova.PluginError("Missing dom_id") + try: + dom_id = int(raw_dom_id) + except ValueError: + raise pluginlib_nova.PluginError("Invalid dom_id") + + logfile = CONSOLE_LOG_FILE_PATTERN % dom_id + try: + log_content = pluginlib_nova.with_file(logfile, 'rb', _last_bytes) + except IOError, e: + msg = "Error reading console: %s" % e + logging.debug(msg) + raise pluginlib_nova.PluginError(msg) + return base64.b64encode(zlib.compress(log_content)) + + +if __name__ == "__main__": + XenAPIPlugin.dispatch({"get_console_log": get_console_log}) |