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:
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)])
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user