HyperV: Nova serial console access support

Hyper-V provides a solid interface for accessing serial ports via
named pipes, already employed in the Nova serial console log
implementation.

This patch makes use of this interface by implementing a simple TCP
socket proxy, providing access to instance serial console ports.

DocImpact

Implements: blueprint hyperv-serial-ports

Change-Id: I58c328391a80ee8b81f66b2e09a1bfa4b26e584c
This commit is contained in:
Lucian Petrut
2015-01-05 16:38:10 +02:00
parent f554c3f777
commit e215e6cba9
11 changed files with 189 additions and 370 deletions

View File

@@ -48,6 +48,7 @@ class HyperVDriverTestCase(test_base.HyperVBaseTestCase):
self.driver._livemigrationops = mock.MagicMock()
self.driver._migrationops = mock.MagicMock()
self.driver._rdpconsoleops = mock.MagicMock()
self.driver._serialconsoleops = mock.MagicMock()
@mock.patch.object(driver.utilsfactory, 'get_hostutils')
def test_check_minimum_windows_version(self, mock_get_hostutils):
@@ -112,10 +113,11 @@ class HyperVDriverTestCase(test_base.HyperVBaseTestCase):
def test_init_host(self, mock_InstanceEventHandler):
self.driver.init_host(mock.sentinel.host)
self.driver._vmops.restart_vm_log_writers.assert_called_once_with()
mock_start_console_handlers = (
self.driver._serialconsoleops.start_console_handlers)
mock_start_console_handlers.assert_called_once_with()
mock_InstanceEventHandler.assert_called_once_with(
state_change_callback=self.driver.emit_event,
running_state_callback=self.driver._vmops.log_vm_serial_output)
state_change_callback=self.driver.emit_event)
fake_event_handler = mock_InstanceEventHandler.return_value
fake_event_handler.start_listener.assert_called_once_with()
@@ -428,7 +430,19 @@ class HyperVDriverTestCase(test_base.HyperVBaseTestCase):
mock.sentinel.instance)
def test_get_console_output(self):
self.driver.get_console_output(
mock.sentinel.context, mock.sentinel.instance)
self.driver._vmops.get_console_output.assert_called_once_with(
mock.sentinel.instance)
mock_instance = fake_instance.fake_instance_obj(self.context)
self.driver.get_console_output(self.context, mock_instance)
mock_get_console_output = (
self.driver._serialconsoleops.get_console_output)
mock_get_console_output.assert_called_once_with(
mock_instance.name)
def test_get_serial_console(self):
mock_instance = fake_instance.fake_instance_obj(self.context)
self.driver.get_console_output(self.context, mock_instance)
mock_get_serial_console = (
self.driver._serialconsoleops.get_console_output)
mock_get_serial_console.assert_called_once_with(
mock_instance.name)

View File

@@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import eventlet
import mock
from os_win import constants
from os_win import exceptions as os_win_exc
@@ -33,7 +32,6 @@ class EventHandlerTestCase(test_base.HyperVBaseTestCase):
super(EventHandlerTestCase, self).setUp()
self._state_change_callback = mock.Mock()
self._running_state_callback = mock.Mock()
self.flags(
power_state_check_timeframe=self._FAKE_EVENT_CHECK_TIMEFRAME,
group='hyperv')
@@ -42,52 +40,21 @@ class EventHandlerTestCase(test_base.HyperVBaseTestCase):
group='hyperv')
self._event_handler = eventhandler.InstanceEventHandler(
self._state_change_callback,
self._running_state_callback)
@mock.patch.object(eventhandler, 'wmi', create=True)
@mock.patch.object(eventhandler.InstanceEventHandler, '_dispatch_event')
@mock.patch.object(eventlet, 'sleep')
def _test_poll_events(self, mock_sleep, mock_dispatch,
mock_wmi, event_found=True):
fake_listener = mock.Mock()
mock_wmi.x_wmi_timed_out = Exception
fake_listener.side_effect = (mock.sentinel.event if event_found
else mock_wmi.x_wmi_timed_out,
KeyboardInterrupt)
self._event_handler._listener = fake_listener
# This is supposed to run as a daemon, so we'll just cause an exception
# in order to be able to test the method.
self.assertRaises(KeyboardInterrupt,
self._event_handler._poll_events)
if event_found:
mock_dispatch.assert_called_once_with(mock.sentinel.event)
else:
mock_sleep.assert_called_once_with(self._FAKE_POLLING_INTERVAL)
def test_poll_having_events(self):
# Test case in which events were found in the checked interval
self._test_poll_events()
def test_poll_no_event_found(self):
self._test_poll_events(event_found=False)
self._state_change_callback)
self._event_handler._serial_console_ops = mock.Mock()
@mock.patch.object(eventhandler.InstanceEventHandler,
'_get_instance_uuid')
@mock.patch.object(eventhandler.InstanceEventHandler, '_emit_event')
def _test_dispatch_event(self, mock_emit_event, mock_get_uuid,
def _test_event_callback(self, mock_emit_event, mock_get_uuid,
missing_uuid=False):
mock_get_uuid.return_value = (
mock.sentinel.instance_uuid if not missing_uuid else None)
self._event_handler._vmutils.get_vm_power_state.return_value = (
mock.sentinel.power_state)
event = mock.Mock()
event.ElementName = mock.sentinel.instance_name
event.EnabledState = mock.sentinel.enabled_state
self._event_handler._dispatch_event(event)
self._event_handler._event_callback(mock.sentinel.instance_name,
mock.sentinel.power_state)
if not missing_uuid:
mock_emit_event.assert_called_once_with(
@@ -97,25 +64,41 @@ class EventHandlerTestCase(test_base.HyperVBaseTestCase):
else:
self.assertFalse(mock_emit_event.called)
def test_dispatch_event_new_final_state(self):
self._test_dispatch_event()
def test_event_callback_uuid_present(self):
self._test_event_callback()
def test_dispatch_event_missing_uuid(self):
self._test_dispatch_event(missing_uuid=True)
def test_event_callback_missing_uuid(self):
self._test_event_callback(missing_uuid=True)
@mock.patch.object(eventhandler.InstanceEventHandler, '_get_virt_event')
@mock.patch.object(utils, 'spawn_n')
def test_emit_event(self, mock_spawn, mock_get_event):
self._event_handler._emit_event(mock.sentinel.instance_name,
mock.sentinel.instance_uuid,
constants.HYPERV_VM_STATE_ENABLED)
mock.sentinel.instance_state)
virt_event = mock_get_event.return_value
mock_spawn.assert_has_calls(
[mock.call(self._state_change_callback, virt_event),
mock.call(self._running_state_callback,
mock.call(self._event_handler._handle_serial_console_workers,
mock.sentinel.instance_name,
mock.sentinel.instance_uuid)])
mock.sentinel.instance_state)])
def test_handle_serial_console_instance_running(self):
self._event_handler._handle_serial_console_workers(
mock.sentinel.instance_name,
constants.HYPERV_VM_STATE_ENABLED)
serialops = self._event_handler._serial_console_ops
serialops.start_console_handler.assert_called_once_with(
mock.sentinel.instance_name)
def test_handle_serial_console_instance_stopped(self):
self._event_handler._handle_serial_console_workers(
mock.sentinel.instance_name,
constants.HYPERV_VM_STATE_DISABLED)
serialops = self._event_handler._serial_console_ops
serialops.stop_console_handler.assert_called_once_with(
mock.sentinel.instance_name)
def _test_get_instance_uuid(self, instance_found=True,
missing_uuid=False):

View File

@@ -20,6 +20,7 @@ from oslo_config import cfg
from nova.tests.unit import fake_instance
from nova.tests.unit.virt.hyperv import test_base
from nova.virt.hyperv import livemigrationops
from nova.virt.hyperv import serialconsoleops
CONF = cfg.CONF
@@ -34,13 +35,15 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase):
self._livemigrops._livemigrutils = mock.MagicMock()
self._livemigrops._pathutils = mock.MagicMock()
@mock.patch('nova.virt.hyperv.vmops.VMOps.copy_vm_console_logs')
@mock.patch.object(serialconsoleops.SerialConsoleOps,
'stop_console_handler')
@mock.patch('nova.virt.hyperv.vmops.VMOps.copy_vm_dvd_disks')
def _test_live_migration(self, mock_get_vm_dvd_paths,
mock_copy_logs, side_effect):
mock_stop_console_handler, side_effect):
mock_instance = fake_instance.fake_instance_obj(self.context)
mock_post = mock.MagicMock()
mock_recover = mock.MagicMock()
mock_copy_logs = self._livemigrops._pathutils.copy_vm_console_logs
fake_dest = mock.sentinel.DESTINATION
self._livemigrops._livemigrutils.live_migrate_vm.side_effect = [
side_effect]
@@ -58,6 +61,8 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase):
post_method=mock_post,
recover_method=mock_recover)
mock_stop_console_handler.assert_called_once_with(
mock_instance.name)
mock_copy_logs.assert_called_once_with(mock_instance.name,
fake_dest)
mock_live_migr = self._livemigrops._livemigrutils.live_migrate_vm
@@ -108,12 +113,3 @@ class LiveMigrationOpsTestCase(test_base.HyperVBaseTestCase):
mock.sentinel.block_device_info)
self._livemigrops._pathutils.get_instance_dir.assert_called_once_with(
mock.sentinel.instance.name, create_dir=False, remove_dir=True)
@mock.patch('nova.virt.hyperv.vmops.VMOps.log_vm_serial_output')
def test_post_live_migration_at_destination(self, mock_log_vm):
mock_instance = fake_instance.fake_instance_obj(self.context)
self._livemigrops.post_live_migration_at_destination(
self.context, mock_instance, network_info=mock.sentinel.NET_INFO,
block_migration=mock.sentinel.BLOCK_INFO)
mock_log_vm.assert_called_once_with(mock_instance.name,
mock_instance.uuid)

View File

@@ -76,3 +76,27 @@ class PathUtilsTestCase(test_base.HyperVBaseTestCase):
self.assertRaises(exception.AdminRequired,
self._pathutils._get_instances_sub_dir,
fake_dir_name)
def test_copy_vm_console_logs(self):
fake_local_logs = [mock.sentinel.log_path,
mock.sentinel.archived_log_path]
fake_remote_logs = [mock.sentinel.remote_log_path,
mock.sentinel.remote_archived_log_path]
self._pathutils.exists = mock.Mock(return_value=True)
self._pathutils.copy = mock.Mock()
self._pathutils.get_vm_console_log_paths = mock.Mock(
side_effect=[fake_local_logs, fake_remote_logs])
self._pathutils.copy_vm_console_logs(mock.sentinel.instance_name,
mock.sentinel.dest_host)
self._pathutils.get_vm_console_log_paths.assert_has_calls(
[mock.call(mock.sentinel.instance_name),
mock.call(mock.sentinel.instance_name,
remote_server=mock.sentinel.dest_host)])
self._pathutils.copy.assert_has_calls([
mock.call(mock.sentinel.log_path,
mock.sentinel.remote_log_path),
mock.call(mock.sentinel.archived_log_path,
mock.sentinel.remote_archived_log_path)])

View File

@@ -21,8 +21,6 @@ from os_win import exceptions as os_win_exc
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_utils import units
import six
import testtools
from nova import exception
from nova import objects
@@ -64,6 +62,7 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
self._vmops._vhdutils = mock.MagicMock()
self._vmops._pathutils = mock.MagicMock()
self._vmops._hostutils = mock.MagicMock()
self._vmops._serial_console_ops = mock.MagicMock()
@mock.patch('nova.network.is_neutron')
@mock.patch('nova.virt.hyperv.vmops.importutils.import_object')
@@ -385,7 +384,9 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
@mock.patch('nova.virt.hyperv.volumeops.VolumeOps'
'.attach_volumes')
@mock.patch.object(vmops.VMOps, '_attach_drive')
def _test_create_instance(self, mock_attach_drive, mock_attach_volumes,
@mock.patch.object(vmops.VMOps, '_create_vm_com_port_pipes')
def _test_create_instance(self, mock_create_pipes,
mock_attach_drive, mock_attach_volumes,
fake_root_path, fake_ephemeral_path,
enable_instance_metrics,
vm_gen=constants.VM_GEN_1):
@@ -429,6 +430,13 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
mock_attach_volumes.assert_called_once_with(mock.sentinel.DEV_INFO,
mock_instance.name,
ebs_root)
expected_port_settings = {
constants.DEFAULT_SERIAL_CONSOLE_PORT:
constants.SERIAL_PORT_TYPE_RW}
mock_create_pipes.assert_called_once_with(
mock_instance, expected_port_settings)
self._vmops._vmutils.create_nic.assert_called_once_with(
mock_instance.name, mock.sentinel.ID, mock.sentinel.ADDRESS)
mock_vif_driver.plug.assert_called_once_with(mock_instance,
@@ -812,6 +820,9 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
with mock.patch.object(self._vmops, '_set_vm_state') as mock_set_state:
self._vmops.power_off(instance, timeout)
serialops = self._vmops._serial_console_ops
serialops.stop_console_handler.assert_called_once_with(
instance.name)
if set_state_expected:
mock_set_state.assert_called_once_with(
instance, os_win_const.HYPERV_VM_STATE_DISABLED)
@@ -832,6 +843,9 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
self._vmops.power_off(instance, 1, 0)
serialops = self._vmops._serial_console_ops
serialops.stop_console_handler.assert_called_once_with(
instance.name)
mock_soft_shutdown.assert_called_once_with(
instance, 1, vmops.SHUTDOWN_TIME_INCREMENT)
self.assertFalse(mock_set_state.called)
@@ -865,22 +879,12 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
mock_set_vm_state.assert_called_once_with(
mock_instance, os_win_const.HYPERV_VM_STATE_ENABLED)
@mock.patch.object(vmops.VMOps, 'log_vm_serial_output')
@mock.patch.object(vmops.VMOps, '_delete_vm_console_log')
def _test_set_vm_state(self, mock_delete_vm_console_log,
mock_log_vm_output, state):
def _test_set_vm_state(self, state):
mock_instance = fake_instance.fake_instance_obj(self.context)
self._vmops._set_vm_state(mock_instance, state)
self._vmops._vmutils.set_vm_state.assert_called_once_with(
mock_instance.name, state)
if state in (os_win_const.HYPERV_VM_STATE_DISABLED,
os_win_const.HYPERV_VM_STATE_REBOOT):
mock_delete_vm_console_log.assert_called_once_with(mock_instance)
if state in (os_win_const.HYPERV_VM_STATE_ENABLED,
os_win_const.HYPERV_VM_STATE_REBOOT):
mock_log_vm_output.assert_called_once_with(mock_instance.name,
mock_instance.uuid)
def test_set_vm_state_disabled(self):
self._test_set_vm_state(state=os_win_const.HYPERV_VM_STATE_DISABLED)
@@ -924,159 +928,25 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
mock.sentinel.FAKE_VM_NAME, vmops.SHUTDOWN_TIME_INCREMENT)
self.assertFalse(result)
@mock.patch.object(vmops.ioutils, 'IOThread')
def _test_log_vm_serial_output(self, mock_io_thread,
worker_running=False,
worker_exists=False):
self._vmops._pathutils.get_vm_console_log_paths.return_value = (
mock.sentinel.log_path, )
fake_instance_uuid = 'fake-uuid'
fake_existing_worker = mock.Mock()
fake_existing_worker.is_active.return_value = worker_running
fake_log_writers = {fake_instance_uuid: fake_existing_worker}
self._vmops._vm_log_writers = (
fake_log_writers if worker_exists else {})
self._vmops.log_vm_serial_output(mock.sentinel.instance_name,
fake_instance_uuid)
if not (worker_exists and worker_running):
expected_pipe_path = r'\\.\pipe\%s' % fake_instance_uuid
expected_current_worker = mock_io_thread.return_value
expected_current_worker.start.assert_called_once_with()
mock_io_thread.assert_called_once_with(
expected_pipe_path, mock.sentinel.log_path,
self._vmops._MAX_CONSOLE_LOG_FILE_SIZE)
else:
expected_current_worker = fake_existing_worker
self.assertEqual(expected_current_worker,
self._vmops._vm_log_writers[fake_instance_uuid])
def test_log_vm_serial_output_unexisting_worker(self):
self._test_log_vm_serial_output()
def test_log_vm_serial_output_worker_stopped(self):
self._test_log_vm_serial_output(worker_exists=True)
def test_log_vm_serial_output_worker_running(self):
self._test_log_vm_serial_output(worker_exists=True,
worker_running=True)
def test_copy_vm_console_logs(self):
fake_local_paths = (mock.sentinel.FAKE_PATH,
mock.sentinel.FAKE_PATH_ARCHIVED)
fake_remote_paths = (mock.sentinel.FAKE_REMOTE_PATH,
mock.sentinel.FAKE_REMOTE_PATH_ARCHIVED)
self._vmops._pathutils.get_vm_console_log_paths.side_effect = [
fake_local_paths, fake_remote_paths]
self._vmops._pathutils.exists.side_effect = [True, False]
self._vmops.copy_vm_console_logs(mock.sentinel.FAKE_VM_NAME,
mock.sentinel.FAKE_DEST)
calls = [mock.call(mock.sentinel.FAKE_VM_NAME),
mock.call(mock.sentinel.FAKE_VM_NAME,
remote_server=mock.sentinel.FAKE_DEST)]
self._vmops._pathutils.get_vm_console_log_paths.assert_has_calls(calls)
calls = [mock.call(mock.sentinel.FAKE_PATH),
mock.call(mock.sentinel.FAKE_PATH_ARCHIVED)]
self._vmops._pathutils.exists.assert_has_calls(calls)
self._vmops._pathutils.copy.assert_called_once_with(
mock.sentinel.FAKE_PATH, mock.sentinel.FAKE_REMOTE_PATH)
@mock.patch.object(vmops.ioutils, 'IOThread')
def test_log_vm_serial_output(self, fake_iothread):
self._vmops._pathutils.get_vm_console_log_paths.return_value = [
mock.sentinel.FAKE_PATH]
self._vmops.log_vm_serial_output(mock.sentinel.FAKE_VM_NAME,
self.FAKE_UUID)
pipe_path = r'\\.\pipe\%s' % self.FAKE_UUID
fake_iothread.assert_called_once_with(
pipe_path, mock.sentinel.FAKE_PATH,
self._vmops._MAX_CONSOLE_LOG_FILE_SIZE)
fake_iothread.return_value.start.assert_called_once_with()
@testtools.skip('mock_open in 1.2 read only works once 1475661')
@mock.patch("os.path.exists")
def test_get_console_output(self, fake_path_exists):
def test_create_vm_com_port_pipes(self):
mock_instance = fake_instance.fake_instance_obj(self.context)
mock_serial_ports = {
1: constants.SERIAL_PORT_TYPE_RO,
2: constants.SERIAL_PORT_TYPE_RW
}
fake_path_exists.return_value = True
self._vmops._pathutils.get_vm_console_log_paths.return_value = (
mock.sentinel.FAKE_PATH, mock.sentinel.FAKE_PATH_ARCHIVED)
self._vmops._create_vm_com_port_pipes(mock_instance,
mock_serial_ports)
expected_calls = []
for port_number, port_type in mock_serial_ports.items():
expected_pipe = r'\\.\pipe\%s_%s' % (mock_instance.uuid,
port_type)
expected_calls.append(mock.call(mock_instance.name,
port_number,
expected_pipe))
with mock.patch('nova.virt.hyperv.vmops.open',
mock.mock_open(read_data=self.FAKE_LOG),
create=True):
instance_log = self._vmops.get_console_output(mock_instance)
# get_vm_console_log_paths returns 2 paths.
self.assertEqual(self.FAKE_LOG * 2, instance_log)
expected_calls = [mock.call(mock.sentinel.FAKE_PATH_ARCHIVED),
mock.call(mock.sentinel.FAKE_PATH)]
fake_path_exists.assert_has_calls(expected_calls, any_order=False)
@mock.patch.object(six.moves.builtins, 'open')
@mock.patch("os.path.exists")
def test_get_console_output_exception(self, fake_path_exists, fake_open):
fake_vm = mock.MagicMock()
fake_open.side_effect = IOError
fake_path_exists.return_value = True
self._vmops._pathutils.get_vm_console_log_paths.return_value = (
mock.sentinel.fake_console_log_path,
mock.sentinel.fake_console_log_archived)
with mock.patch('nova.virt.hyperv.vmops.open', fake_open, create=True):
self.assertRaises(exception.ConsoleLogOutputException,
self._vmops.get_console_output,
fake_vm)
@mock.patch.object(vmops.fileutils, 'delete_if_exists')
def test_delete_vm_console_log(self, mock_delete_if_exists):
mock_instance = fake_instance.fake_instance_obj(self.context)
self._vmops._pathutils.get_vm_console_log_paths.return_value = (
mock.sentinel.FAKE_PATH, )
mock_log_writer = mock.MagicMock()
self._vmops._vm_log_writers[mock_instance['uuid']] = mock_log_writer
self._vmops._delete_vm_console_log(mock_instance)
mock_log_writer.join.assert_called_once_with()
mock_delete_if_exists.assert_called_once_with(mock.sentinel.FAKE_PATH)
def test_create_vm_com_port_pipe(self):
mock_instance = fake_instance.fake_instance_obj(self.context)
pipe_path = r'\\.\pipe\%s' % mock_instance['uuid']
self._vmops._create_vm_com_port_pipe(mock_instance)
get_vm_serial_port = self._vmops._vmutils.get_vm_serial_port_connection
get_vm_serial_port.assert_called_once_with(mock_instance['name'],
update_connection=pipe_path)
@mock.patch.object(vmops.VMOps, "log_vm_serial_output")
@mock.patch("os.path.basename")
@mock.patch("os.path.exists")
def test_restart_vm_log_writers(self, mock_exists, mock_basename,
mock_log_vm_output):
self._vmops._vmutils.get_active_instances.return_value = [
mock.sentinel.FAKE_VM_NAME, mock.sentinel.FAKE_VM_NAME_OTHER]
mock_exists.side_effect = [True, False]
self._vmops.restart_vm_log_writers()
calls = [mock.call(mock.sentinel.FAKE_VM_NAME),
mock.call(mock.sentinel.FAKE_VM_NAME_OTHER)]
self._vmops._pathutils.get_instance_dir.assert_has_calls(calls)
get_vm_serial_port = self._vmops._vmutils.get_vm_serial_port_connection
get_vm_serial_port.assert_called_once_with(mock.sentinel.FAKE_VM_NAME)
mock_log_vm_output.assert_called_once_with(mock.sentinel.FAKE_VM_NAME,
mock_basename.return_value)
mock_set_conn = self._vmops._vmutils.set_vm_serial_port_connection
mock_set_conn.assert_has_calls(expected_calls)
def test_list_instance_uuids(self):
fake_uuid = '4f54fb69-d3a2-45b7-bb9b-b6e6b3d893b3'

View File

@@ -72,3 +72,7 @@ SERIAL_CONSOLE_BUFFER_SIZE = 4 * units.Ki
SERIAL_PORT_TYPE_RO = 'ro'
SERIAL_PORT_TYPE_RW = 'rw'
# The default serial console port number used for
# logging and interactive sessions.
DEFAULT_SERIAL_CONSOLE_PORT = 1

View File

@@ -34,6 +34,7 @@ from nova.virt.hyperv import hostops
from nova.virt.hyperv import livemigrationops
from nova.virt.hyperv import migrationops
from nova.virt.hyperv import rdpconsoleops
from nova.virt.hyperv import serialconsoleops
from nova.virt.hyperv import snapshotops
from nova.virt.hyperv import vmops
from nova.virt.hyperv import volumeops
@@ -111,6 +112,7 @@ class HyperVDriver(driver.ComputeDriver):
self._livemigrationops = livemigrationops.LiveMigrationOps()
self._migrationops = migrationops.MigrationOps()
self._rdpconsoleops = rdpconsoleops.RDPConsoleOps()
self._serialconsoleops = serialconsoleops.SerialConsoleOps()
def _check_minimum_windows_version(self):
if not utilsfactory.get_hostutils().check_min_windows_version(6, 2):
@@ -124,10 +126,9 @@ class HyperVDriver(driver.ComputeDriver):
raise exception.HypervisorTooOld(version='6.2')
def init_host(self, host):
self._vmops.restart_vm_log_writers()
self._serialconsoleops.start_console_handlers()
event_handler = eventhandler.InstanceEventHandler(
state_change_callback=self.emit_event,
running_state_callback=self._vmops.log_vm_serial_output)
state_change_callback=self.emit_event)
event_handler.start_listener()
def list_instance_uuids(self):
@@ -320,8 +321,11 @@ class HyperVDriver(driver.ComputeDriver):
def get_rdp_console(self, context, instance):
return self._rdpconsoleops.get_rdp_console(instance)
def get_serial_console(self, context, instance):
return self._serialconsoleops.get_serial_console(instance.name)
def get_console_output(self, context, instance):
return self._vmops.get_console_output(instance)
return self._serialconsoleops.get_console_output(instance.name)
def attach_interface(self, instance, image_meta, vif):
return self._vmops.attach_interface(instance, vif)

View File

@@ -13,13 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import eventlet
import sys
if sys.platform == 'win32':
import wmi
from os_win import constants
from os_win import exceptions as os_win_exc
from os_win import utilsfactory
@@ -29,6 +22,7 @@ import nova.conf
from nova.i18n import _LW
from nova import utils
from nova.virt import event as virtevent
from nova.virt.hyperv import serialconsoleops
LOG = logging.getLogger(__name__)
@@ -48,53 +42,42 @@ class InstanceEventHandler(object):
virtevent.EVENT_LIFECYCLE_SUSPENDED
}
def __init__(self, state_change_callback=None,
running_state_callback=None):
def __init__(self, state_change_callback=None):
self._vmutils = utilsfactory.get_vmutils()
self._listener = self._vmutils.get_vm_power_state_change_listener(
timeframe=CONF.hyperv.power_state_check_timeframe,
filtered_states=list(self._TRANSITION_MAP.keys()))
event_timeout=CONF.hyperv.power_state_event_polling_interval,
filtered_states=list(self._TRANSITION_MAP.keys()),
get_handler=True)
self._polling_interval = CONF.hyperv.power_state_event_polling_interval
self._serial_console_ops = serialconsoleops.SerialConsoleOps()
self._state_change_callback = state_change_callback
self._running_state_callback = running_state_callback
def start_listener(self):
utils.spawn_n(self._poll_events)
def _poll_events(self):
while True:
try:
# Retrieve one by one all the events that occurred in
# the checked interval.
event = self._listener(self._WAIT_TIMEOUT)
self._dispatch_event(event)
continue
except wmi.x_wmi_timed_out:
# If no events were triggered in the checked interval,
# a timeout exception is raised. We'll just ignore it.
pass
eventlet.sleep(self._polling_interval)
def _dispatch_event(self, event):
instance_state = self._vmutils.get_vm_power_state(event.EnabledState)
instance_name = event.ElementName
utils.spawn_n(self._listener, self._event_callback)
def _event_callback(self, instance_name, instance_power_state):
# Instance uuid set by Nova. If this is missing, we assume that
# the instance was not created by Nova and ignore the event.
instance_uuid = self._get_instance_uuid(instance_name)
if instance_uuid:
self._emit_event(instance_name, instance_uuid, instance_state)
self._emit_event(instance_name,
instance_uuid,
instance_power_state)
def _emit_event(self, instance_name, instance_uuid, instance_state):
virt_event = self._get_virt_event(instance_uuid,
instance_state)
utils.spawn_n(self._state_change_callback, virt_event)
utils.spawn_n(self._handle_serial_console_workers,
instance_name, instance_state)
def _handle_serial_console_workers(self, instance_name, instance_state):
if instance_state == constants.HYPERV_VM_STATE_ENABLED:
utils.spawn_n(self._running_state_callback,
instance_name, instance_uuid)
self._serial_console_ops.start_console_handler(instance_name)
else:
self._serial_console_ops.stop_console_handler(instance_name)
def _get_instance_uuid(self, instance_name):
try:

View File

@@ -25,6 +25,7 @@ import nova.conf
from nova.objects import migrate_data as migrate_data_obj
from nova.virt.hyperv import imagecache
from nova.virt.hyperv import pathutils
from nova.virt.hyperv import serialconsoleops
from nova.virt.hyperv import vmops
from nova.virt.hyperv import volumeops
@@ -38,6 +39,7 @@ class LiveMigrationOps(object):
self._pathutils = pathutils.PathUtils()
self._vmops = vmops.VMOps()
self._volumeops = volumeops.VolumeOps()
self._serial_console_ops = serialconsoleops.SerialConsoleOps()
self._imagecache = imagecache.ImageCache()
self._vmutils = utilsfactory.get_vmutils()
@@ -48,8 +50,13 @@ class LiveMigrationOps(object):
instance_name = instance_ref["name"]
try:
self._vmops.copy_vm_console_logs(instance_name, dest)
self._vmops.copy_vm_dvd_disks(instance_name, dest)
# We must make sure that the console log workers are stopped,
# otherwise we won't be able to delete / move VM log files.
self._serial_console_ops.stop_console_handler(instance_name)
self._pathutils.copy_vm_console_logs(instance_name, dest)
self._livemigrutils.live_migrate_vm(instance_name,
dest)
except Exception:
@@ -85,8 +92,6 @@ class LiveMigrationOps(object):
network_info, block_migration):
LOG.debug("post_live_migration_at_destination called",
instance=instance_ref)
self._vmops.log_vm_serial_output(instance_ref['name'],
instance_ref['uuid'])
def check_can_live_migrate_destination(self, ctxt, instance_ref,
src_compute_info, dst_compute_info,

View File

@@ -127,8 +127,19 @@ class PathUtils(pathutils.PathUtils):
return self._get_instances_sub_dir(dir_name, create_dir=True,
remove_dir=True)
def get_vm_console_log_paths(self, vm_name, remote_server=None):
instance_dir = self.get_instance_dir(vm_name,
def get_vm_console_log_paths(self, instance_name, remote_server=None):
instance_dir = self.get_instance_dir(instance_name,
remote_server)
console_log_path = os.path.join(instance_dir, 'console.log')
return console_log_path, console_log_path + '.1'
def copy_vm_console_logs(self, instance_name, dest_host):
local_log_paths = self.get_vm_console_log_paths(
instance_name)
remote_log_paths = self.get_vm_console_log_paths(
instance_name, remote_server=dest_host)
for local_log_path, remote_log_path in zip(local_log_paths,
remote_log_paths):
if self.exists(local_log_path):
self.copy(local_log_path, remote_log_path)

View File

@@ -24,17 +24,14 @@ import time
from eventlet import timeout as etimeout
from os_win import constants as os_win_const
from os_win import exceptions as os_win_exc
from os_win.utils.io import ioutils
from os_win import utilsfactory
from oslo_concurrency import processutils
from oslo_log import log as logging
from oslo_service import loopingcall
from oslo_utils import excutils
from oslo_utils import fileutils
from oslo_utils import importutils
from oslo_utils import units
from oslo_utils import uuidutils
import six
from nova.api.metadata import base as instance_metadata
import nova.conf
@@ -46,6 +43,7 @@ from nova.virt import hardware
from nova.virt.hyperv import constants
from nova.virt.hyperv import imagecache
from nova.virt.hyperv import pathutils
from nova.virt.hyperv import serialconsoleops
from nova.virt.hyperv import volumeops
LOG = logging.getLogger(__name__)
@@ -105,9 +103,9 @@ class VMOps(object):
self._pathutils = pathutils.PathUtils()
self._volumeops = volumeops.VolumeOps()
self._imagecache = imagecache.ImageCache()
self._serial_console_ops = serialconsoleops.SerialConsoleOps()
self._vif_driver = None
self._load_vif_driver_class()
self._vm_log_writers = {}
def _load_vif_driver_class(self):
try:
@@ -244,7 +242,8 @@ class VMOps(object):
try:
self.create_instance(instance, network_info, block_device_info,
root_vhd_path, eph_vhd_path, vm_gen)
root_vhd_path, eph_vhd_path,
vm_gen)
if configdrive.required_by(instance):
configdrive_path = self._create_config_drive(instance,
@@ -293,6 +292,16 @@ class VMOps(object):
instance_name,
ebs_root)
# For the moment, we use COM port 1 when getting the serial console
# log as well as interactive sessions. In the future, the way in which
# we consume instance serial ports may become configurable.
#
# Note that Hyper-V instances will always have 2 COM ports
serial_ports = {
constants.DEFAULT_SERIAL_CONSOLE_PORT:
constants.SERIAL_PORT_TYPE_RW}
self._create_vm_com_port_pipes(instance, serial_ports)
for vif in network_info:
LOG.debug('Creating nic for instance', instance=instance)
self._vmutils.create_nic(instance_name,
@@ -303,8 +312,6 @@ class VMOps(object):
if CONF.hyperv.enable_instance_metrics_collection:
self._metricsutils.enable_vm_metrics_collection(instance_name)
self._create_vm_com_port_pipe(instance)
def _attach_drive(self, instance_name, path, drive_addr, ctrl_disk_addr,
controller_type, drive_type=constants.DISK):
if controller_type == constants.CTRL_TYPE_SCSI:
@@ -504,6 +511,11 @@ class VMOps(object):
def power_off(self, instance, timeout=0, retry_interval=0):
"""Power off the specified instance."""
LOG.debug("Power off instance", instance=instance)
# We must make sure that the console log workers are stopped,
# otherwise we won't be able to delete or move the VM log files.
self._serial_console_ops.stop_console_handler(instance.name)
if retry_interval <= 0:
retry_interval = SHUTDOWN_TIME_INCREMENT
@@ -535,19 +547,10 @@ class VMOps(object):
def _set_vm_state(self, instance, req_state):
instance_name = instance.name
instance_uuid = instance.uuid
try:
self._vmutils.set_vm_state(instance_name, req_state)
if req_state in (os_win_const.HYPERV_VM_STATE_DISABLED,
os_win_const.HYPERV_VM_STATE_REBOOT):
self._delete_vm_console_log(instance)
if req_state in (os_win_const.HYPERV_VM_STATE_ENABLED,
os_win_const.HYPERV_VM_STATE_REBOOT):
self.log_vm_serial_output(instance_name,
instance_uuid)
LOG.debug("Successfully changed state of VM %(instance_name)s"
" to: %(req_state)s", {'instance_name': instance_name,
'req_state': req_state})
@@ -596,89 +599,11 @@ class VMOps(object):
"""Resume guest state when a host is booted."""
self.power_on(instance, block_device_info)
def log_vm_serial_output(self, instance_name, instance_uuid):
# Uses a 'thread' that will run in background, reading
# the console output from the according named pipe and
# write it to a file.
console_log_path = self._pathutils.get_vm_console_log_paths(
instance_name)[0]
pipe_path = r'\\.\pipe\%s' % instance_uuid
@utils.synchronized(pipe_path)
def log_serial_output():
vm_log_writer = self._vm_log_writers.get(instance_uuid)
if vm_log_writer and vm_log_writer.is_active():
LOG.debug("Instance %s log writer is already running.",
instance_name)
else:
vm_log_writer = ioutils.IOThread(
pipe_path, console_log_path,
self._MAX_CONSOLE_LOG_FILE_SIZE)
vm_log_writer.start()
self._vm_log_writers[instance_uuid] = vm_log_writer
log_serial_output()
def get_console_output(self, instance):
console_log_paths = (
self._pathutils.get_vm_console_log_paths(instance.name))
try:
instance_log = ''
# Start with the oldest console log file.
for console_log_path in console_log_paths[::-1]:
if os.path.exists(console_log_path):
with open(console_log_path, 'rb') as fp:
instance_log += fp.read()
return instance_log
except IOError as err:
raise exception.ConsoleLogOutputException(
instance_id=instance.uuid, reason=six.text_type(err))
def _delete_vm_console_log(self, instance):
console_log_files = self._pathutils.get_vm_console_log_paths(
instance.name)
vm_log_writer = self._vm_log_writers.get(instance.uuid)
if vm_log_writer:
vm_log_writer.join()
for log_file in console_log_files:
fileutils.delete_if_exists(log_file)
def copy_vm_console_logs(self, vm_name, dest_host):
local_log_paths = self._pathutils.get_vm_console_log_paths(
vm_name)
remote_log_paths = self._pathutils.get_vm_console_log_paths(
vm_name, remote_server=dest_host)
for local_log_path, remote_log_path in zip(local_log_paths,
remote_log_paths):
if self._pathutils.exists(local_log_path):
self._pathutils.copy(local_log_path,
remote_log_path)
def _create_vm_com_port_pipe(self, instance):
# Creates a pipe to the COM 0 serial port of the specified vm.
pipe_path = r'\\.\pipe\%s' % instance.uuid
self._vmutils.get_vm_serial_port_connection(
instance.name, update_connection=pipe_path)
def restart_vm_log_writers(self):
# Restart the VM console log writers after nova compute restarts.
active_instances = self._vmutils.get_active_instances()
for instance_name in active_instances:
instance_path = self._pathutils.get_instance_dir(instance_name)
# Skip instances that are not created by Nova
if not os.path.exists(instance_path):
continue
vm_serial_conn = self._vmutils.get_vm_serial_port_connection(
instance_name)
if vm_serial_conn:
instance_uuid = os.path.basename(vm_serial_conn)
self.log_vm_serial_output(instance_name, instance_uuid)
def _create_vm_com_port_pipes(self, instance, serial_ports):
for port_number, port_type in serial_ports.items():
pipe_path = r'\\.\pipe\%s_%s' % (instance.uuid, port_type)
self._vmutils.set_vm_serial_port_connection(
instance.name, port_number, pipe_path)
def copy_vm_dvd_disks(self, vm_name, dest_host):
dvd_disk_paths = self._vmutils.get_vm_dvd_disk_paths(vm_name)