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
This commit is contained in:
committed by
Gerrit Code Review
parent
4461f20bd6
commit
6e93fefade
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+80
@@ -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})
|
||||
Reference in New Issue
Block a user