diff --git a/compute_hyperv/nova/driver.py b/compute_hyperv/nova/driver.py index 2195aaf4..0e4198f2 100644 --- a/compute_hyperv/nova/driver.py +++ b/compute_hyperv/nova/driver.py @@ -126,6 +126,7 @@ class HyperVDriver(driver.ComputeDriver): self._imagecache = imagecache.ImageCache() self._image_api = image.API() self._pathutils = pathutils.PathUtils() + self._event_handler = eventhandler.InstanceEventHandler() def _check_minimum_windows_version(self): hostutils = utilsfactory.get_hostutils() @@ -151,13 +152,19 @@ class HyperVDriver(driver.ComputeDriver): def init_host(self, host): self._serialconsoleops.start_console_handlers() - event_handler = eventhandler.InstanceEventHandler( - state_change_callback=self.emit_event) - event_handler.start_listener() + + self._set_event_handler_callbacks() + self._event_handler.start_listener() instance_path = self._pathutils.get_instances_dir() self._pathutils.check_create_dir(instance_path) + def _set_event_handler_callbacks(self): + # Subclasses may override this. + self._event_handler.add_callback(self.emit_event) + self._event_handler.add_callback( + self._vmops.instance_state_change_callback) + def list_instance_uuids(self): return self._vmops.list_instance_uuids() diff --git a/compute_hyperv/nova/eventhandler.py b/compute_hyperv/nova/eventhandler.py index eb64a4db..c0fe4d79 100644 --- a/compute_hyperv/nova/eventhandler.py +++ b/compute_hyperv/nova/eventhandler.py @@ -20,7 +20,6 @@ from os_win import utilsfactory from oslo_log import log as logging import compute_hyperv.nova.conf -from compute_hyperv.nova import serialconsoleops from compute_hyperv.nova import vmops LOG = logging.getLogger(__name__) @@ -28,6 +27,13 @@ LOG = logging.getLogger(__name__) CONF = compute_hyperv.nova.conf.CONF +class HyperVLifecycleEvent(virtevent.LifecycleEvent): + def __init__(self, uuid, name, transition, timestamp=None): + super(HyperVLifecycleEvent, self).__init__(uuid, transition, timestamp) + + self.name = name + + class InstanceEventHandler(object): _TRANSITION_MAP = { constants.HYPERV_VM_STATE_ENABLED: virtevent.EVENT_LIFECYCLE_STARTED, @@ -37,7 +43,7 @@ class InstanceEventHandler(object): virtevent.EVENT_LIFECYCLE_SUSPENDED } - def __init__(self, state_change_callback=None): + def __init__(self): self._vmutils = utilsfactory.get_vmutils() self._listener = self._vmutils.get_vm_power_state_change_listener( timeframe=CONF.hyperv.power_state_check_timeframe, @@ -46,13 +52,16 @@ class InstanceEventHandler(object): get_handler=True) self._vmops = vmops.VMOps() - self._serial_console_ops = serialconsoleops.SerialConsoleOps() - self._state_change_callback = state_change_callback + + self._callbacks = [] + + def add_callback(self, callback): + self._callbacks.append(callback) def start_listener(self): - utils.spawn_n(self._listener, self._event_callback) + utils.spawn_n(self._listener, self._handle_event) - def _event_callback(self, instance_name, instance_power_state): + def _handle_event(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._vmops.get_instance_uuid(instance_name) @@ -69,27 +78,15 @@ class InstanceEventHandler(object): def _emit_event(self, instance_name, instance_uuid, instance_state): virt_event = self._get_virt_event(instance_uuid, + instance_name, instance_state) - utils.spawn_n(self._state_change_callback, virt_event) - should_enable_metrics = ( - CONF.hyperv.enable_instance_metrics_collection and - instance_state == constants.HYPERV_VM_STATE_ENABLED) - if should_enable_metrics: - utils.spawn_n(self._vmops.configure_instance_metrics, - instance_name, - enable_network_metrics=True) + for callback in self._callbacks: + utils.spawn_n(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: - self._serial_console_ops.start_console_handler(instance_name) - else: - self._serial_console_ops.stop_console_handler(instance_name) - - def _get_virt_event(self, instance_uuid, instance_state): + def _get_virt_event(self, instance_uuid, instance_name, instance_state): transition = self._TRANSITION_MAP[instance_state] - return virtevent.LifecycleEvent(uuid=instance_uuid, - transition=transition) + return HyperVLifecycleEvent( + uuid=instance_uuid, + name=instance_name, + transition=transition) diff --git a/compute_hyperv/nova/vmops.py b/compute_hyperv/nova/vmops.py index 0d98947d..9e812473 100644 --- a/compute_hyperv/nova/vmops.py +++ b/compute_hyperv/nova/vmops.py @@ -30,6 +30,7 @@ from nova import objects from nova.objects import fields from nova import utils from nova.virt import configdrive +from nova.virt import event as virtevent from nova.virt import hardware from os_win import constants as os_win_const from os_win import exceptions as os_win_exc @@ -1466,3 +1467,15 @@ class VMOps(object): "its uuid. It may have been deleted meanwhile.", instance_name) ctxt.reraise = expect_existing + + def instance_state_change_callback(self, event): + if event.transition in (virtevent.EVENT_LIFECYCLE_STARTED, + virtevent.EVENT_LIFECYCLE_RESUMED): + # We can handle the following operations concurrently. + utils.spawn_n(self._serial_console_ops.start_console_handler, + event.name) + utils.spawn_n(self.configure_instance_metrics, + event.name, + enable_network_metrics=True) + else: + self._serial_console_ops.stop_console_handler(event.name) diff --git a/compute_hyperv/tests/unit/test_driver.py b/compute_hyperv/tests/unit/test_driver.py index 6e1964d5..169652c1 100644 --- a/compute_hyperv/tests/unit/test_driver.py +++ b/compute_hyperv/tests/unit/test_driver.py @@ -35,6 +35,7 @@ from compute_hyperv.tests.unit import test_base class HyperVDriverTestCase(test_base.HyperVBaseTestCase): _autospec_classes = [ + driver.eventhandler.InstanceEventHandler, driver.hostops.HostOps, driver.volumeops.VolumeOps, driver.vmops.VMOps, @@ -124,9 +125,7 @@ class HyperVDriverTestCase(test_base.HyperVBaseTestCase): # original frame will contain the 'foo' variable. self.assertEqual('foofoo', trace.tb_frame.f_locals['foo']) - @mock.patch.object(driver.eventhandler, 'InstanceEventHandler') - def test_init_host(self, mock_InstanceEventHandler): - + def test_init_host(self): mock_get_inst_dir = self.driver._pathutils.get_instances_dir mock_get_inst_dir.return_value = mock.sentinel.FAKE_DIR @@ -135,10 +134,10 @@ class HyperVDriverTestCase(test_base.HyperVBaseTestCase): 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) - fake_event_handler = mock_InstanceEventHandler.return_value - fake_event_handler.start_listener.assert_called_once_with() + self.driver._event_handler.add_callback.assert_has_calls( + [mock.call(self.driver.emit_event), + mock.call(self.driver._vmops.instance_state_change_callback)]) + self.driver._event_handler.start_listener.assert_called_once_with() mock_get_inst_dir.assert_called_once_with() self.driver._pathutils.check_create_dir.assert_called_once_with( diff --git a/compute_hyperv/tests/unit/test_eventhandler.py b/compute_hyperv/tests/unit/test_eventhandler.py index e1a64e1b..4c71ac60 100644 --- a/compute_hyperv/tests/unit/test_eventhandler.py +++ b/compute_hyperv/tests/unit/test_eventhandler.py @@ -13,9 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. +import ddt import mock from nova import utils -from nova.virt import driver from os_win import constants from compute_hyperv.nova import eventhandler @@ -23,20 +23,14 @@ from compute_hyperv.nova import vmops from compute_hyperv.tests.unit import test_base +@ddt.ddt class EventHandlerTestCase(test_base.HyperVBaseTestCase): - - _autospec_classes = [ - eventhandler.serialconsoleops.SerialConsoleOps, - ] - _FAKE_POLLING_INTERVAL = 3 _FAKE_EVENT_CHECK_TIMEFRAME = 15 def setUp(self): super(EventHandlerTestCase, self).setUp() - self._state_change_callback = mock.Mock( - autospec=driver.ComputeDriver.emit_event) self.flags( power_state_check_timeframe=self._FAKE_EVENT_CHECK_TIMEFRAME, group='hyperv') @@ -44,20 +38,19 @@ class EventHandlerTestCase(test_base.HyperVBaseTestCase): power_state_event_polling_interval=self._FAKE_POLLING_INTERVAL, group='hyperv') - self._event_handler = eventhandler.InstanceEventHandler( - self._state_change_callback) + self._event_handler = eventhandler.InstanceEventHandler() + @ddt.data(True, False) @mock.patch.object(vmops.VMOps, 'get_instance_uuid') @mock.patch.object(eventhandler.InstanceEventHandler, '_emit_event') - def _test_event_callback(self, mock_emit_event, mock_get_uuid, - missing_uuid=False): + def test_handle_event(self, missing_uuid, mock_emit_event, mock_get_uuid): 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) - self._event_handler._event_callback(mock.sentinel.instance_name, - mock.sentinel.power_state) + self._event_handler._handle_event(mock.sentinel.instance_name, + mock.sentinel.power_state) if not missing_uuid: mock_emit_event.assert_called_once_with( @@ -67,60 +60,33 @@ class EventHandlerTestCase(test_base.HyperVBaseTestCase): else: self.assertFalse(mock_emit_event.called) - def test_event_callback_uuid_present(self): - self._test_event_callback() - - 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): + @mock.patch.object(utils, 'spawn_n', + lambda f, *args, **kwargs: f(*args, **kwargs)) + def test_emit_event(self, mock_get_event): state = constants.HYPERV_VM_STATE_ENABLED - self.flags(enable_instance_metrics_collection=True, - group="hyperv") + callbacks = [mock.Mock(), mock.Mock()] + + for cbk in callbacks: + self._event_handler.add_callback(cbk) self._event_handler._emit_event(mock.sentinel.instance_name, mock.sentinel.instance_uuid, state) - virt_event = mock_get_event.return_value - mock_spawn.assert_has_calls( - [mock.call(self._state_change_callback, virt_event), - mock.call(self._event_handler._vmops.configure_instance_metrics, - mock.sentinel.instance_name, - enable_network_metrics=True), - mock.call(self._event_handler._handle_serial_console_workers, - mock.sentinel.instance_name, - state)]) + for cbk in callbacks: + cbk.assert_called_once_with(mock_get_event.return_value) - 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) - - @mock.patch('nova.virt.event.LifecycleEvent') - def test_get_virt_event(self, mock_lifecycle_event): + def test_get_virt_event(self): instance_state = constants.HYPERV_VM_STATE_ENABLED expected_transition = self._event_handler._TRANSITION_MAP[ instance_state] virt_event = self._event_handler._get_virt_event( - mock.sentinel.instance_uuid, instance_state) + mock.sentinel.instance_uuid, + mock.sentinel.instance_name, + instance_state) - self.assertEqual(mock_lifecycle_event.return_value, - virt_event) - mock_lifecycle_event.assert_called_once_with( - uuid=mock.sentinel.instance_uuid, - transition=expected_transition) + self.assertEqual(mock.sentinel.instance_name, virt_event.name) + self.assertEqual(mock.sentinel.instance_uuid, virt_event.uuid) + self.assertEqual(expected_transition, virt_event.transition) diff --git a/compute_hyperv/tests/unit/test_vmops.py b/compute_hyperv/tests/unit/test_vmops.py index 48c880bd..136bd14c 100644 --- a/compute_hyperv/tests/unit/test_vmops.py +++ b/compute_hyperv/tests/unit/test_vmops.py @@ -22,6 +22,8 @@ from nova import exception from nova import objects from nova.objects import fields from nova.tests.unit.objects import test_virtual_interface +from nova import utils +from nova.virt import event as virtevent from nova.virt import hardware from os_win import constants as os_win_const from os_win import exceptions as os_win_exc @@ -31,6 +33,7 @@ from oslo_utils import units import compute_hyperv.nova.conf from compute_hyperv.nova import constants +from compute_hyperv.nova import eventhandler from compute_hyperv.nova import vmops from compute_hyperv.tests import fake_instance from compute_hyperv.tests.unit import test_base @@ -2420,3 +2423,25 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase): self._vmops.get_instance_uuid, mock.sentinel.instance_name, expect_existing=True) + + @ddt.data(virtevent.EVENT_LIFECYCLE_STARTED, + virtevent.EVENT_LIFECYCLE_STOPPED) + @mock.patch.object(vmops.VMOps, 'configure_instance_metrics') + @mock.patch.object(utils, 'spawn_n', + lambda f, *args, **kwargs: f(*args, **kwargs)) + def test_instance_state_change_callback(self, transition, + mock_configure_metrics): + event = eventhandler.HyperVLifecycleEvent( + mock.sentinel.uuid, + mock.sentinel.name, + transition) + + self._vmops.instance_state_change_callback(event) + + serialops = self._vmops._serial_console_ops + if transition == virtevent.EVENT_LIFECYCLE_STARTED: + serialops.start_console_handler.assert_called_once_with(event.name) + mock_configure_metrics.assert_called_once_with( + event.name, enable_network_metrics=True) + else: + serialops.stop_console_handler.assert_called_once_with(event.name)