Hyper-V: Fix instance event handler
The listener currently used by the instance event handler records all WMI instance object changes, even though we only care about power state changes. For this reason, we currently record the last emited power state change in order to see whether the power state actually changed before emiting a new change. This is problematic and unreliable. When the instance is created, it will be powered off. If anything changes, the current implementation will emit a power off event, having no previous recorded state. For this reason, the manager can consider that the the instance was unexpectedly powered off, calling the stop API, which is highly undesirable. This patch fixes the issue by constructing a WQL query used by the event listener in order to catch events only in case the instance actually transitioned into one of the states that we're interested in. Change-Id: I7efe33d8f4d8df44a09ac3c0ac3e29c2947fe67c Closes-Bug: #1463814
This commit is contained in:
@@ -47,7 +47,6 @@ CONF.register_opts(hyperv_opts, 'hyperv')
|
||||
|
||||
|
||||
class InstanceEventHandler(object):
|
||||
_MODIFICATION_EVENT = 'modification'
|
||||
# The event listener timeout is set to 0 in order to return immediately
|
||||
# and avoid blocking the thread.
|
||||
_WAIT_TIMEOUT = 0
|
||||
@@ -69,16 +68,14 @@ class InstanceEventHandler(object):
|
||||
def __init__(self, state_change_callback=None,
|
||||
running_state_callback=None):
|
||||
self._vmutils = utilsfactory.get_vmutils()
|
||||
self._listener = self._vmutils.get_vm_state_change_listener(
|
||||
event_type=self._MODIFICATION_EVENT,
|
||||
timeframe=CONF.hyperv.power_state_check_timeframe)
|
||||
self._listener = self._vmutils.get_vm_power_state_change_listener(
|
||||
timeframe=CONF.hyperv.power_state_check_timeframe,
|
||||
filtered_states=self._TRANSITION_MAP.keys())
|
||||
|
||||
self._polling_interval = CONF.hyperv.power_state_event_polling_interval
|
||||
self._state_change_callback = state_change_callback
|
||||
self._running_state_callback = running_state_callback
|
||||
|
||||
self._last_state = {}
|
||||
|
||||
def start_listener(self):
|
||||
eventlet.spawn_n(self._poll_events)
|
||||
|
||||
@@ -98,23 +95,9 @@ class InstanceEventHandler(object):
|
||||
eventlet.sleep(self._polling_interval)
|
||||
|
||||
def _dispatch_event(self, event):
|
||||
# Hyper-V generated GUID
|
||||
instance_guid = event.Name
|
||||
instance_state = self._vmutils.get_vm_power_state(event.EnabledState)
|
||||
instance_name = event.ElementName
|
||||
|
||||
# The event listener returns all instance related changes. We
|
||||
# ignore events that are not triggered by power state changes
|
||||
# as well as intermediary states.
|
||||
last_state = self._last_state.get(instance_guid)
|
||||
|
||||
state_unchanged = last_state == instance_state
|
||||
is_intermediary_state = instance_state not in self._TRANSITION_MAP
|
||||
self._last_state[instance_guid] = instance_state
|
||||
|
||||
if state_unchanged or is_intermediary_state:
|
||||
return
|
||||
|
||||
# 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)
|
||||
|
||||
@@ -83,6 +83,9 @@ class VMUtils(object):
|
||||
'Msvm_SyntheticEthernetPortSettingData'
|
||||
_AFFECTED_JOB_ELEMENT_CLASS = "Msvm_AffectedJobElement"
|
||||
_CIM_RES_ALLOC_SETTING_DATA_CLASS = 'Cim_ResourceAllocationSettingData'
|
||||
_COMPUTER_SYSTEM_CLASS = "Msvm_ComputerSystem"
|
||||
|
||||
_VM_ENABLED_STATE_PROP = "EnabledState"
|
||||
|
||||
_SHUTDOWN_COMPONENT = "Msvm_ShutdownComponent"
|
||||
_VIRTUAL_SYSTEM_CURRENT_SETTINGS = 3
|
||||
@@ -793,11 +796,41 @@ class VMUtils(object):
|
||||
raise NotImplementedError(_('RemoteFX is currently not supported by '
|
||||
'this driver on this version of Hyper-V'))
|
||||
|
||||
def get_vm_state_change_listener(self, event_type, timeframe):
|
||||
return self._conn.Msvm_ComputerSystem.watch_for(
|
||||
event_type,
|
||||
delay_secs=timeframe,
|
||||
fields=["EnabledState"])
|
||||
def get_vm_power_state_change_listener(self, timeframe, filtered_states):
|
||||
field = self._VM_ENABLED_STATE_PROP
|
||||
query = self._get_event_wql_query(cls=self._COMPUTER_SYSTEM_CLASS,
|
||||
field=field,
|
||||
timeframe=timeframe,
|
||||
filtered_states=filtered_states)
|
||||
return self._conn.Msvm_ComputerSystem.watch_for(raw_wql=query,
|
||||
fields=[field])
|
||||
|
||||
def _get_event_wql_query(self, cls, field,
|
||||
timeframe, filtered_states=None):
|
||||
"""Return a WQL query used for polling WMI events.
|
||||
|
||||
:param cls: the WMI class polled for events
|
||||
:param field: the field checked
|
||||
:param timeframe: check for events that occurred in
|
||||
the specified timeframe
|
||||
:param filtered_states: only catch events triggered when a WMI
|
||||
object transitioned into one of those
|
||||
states.
|
||||
"""
|
||||
query = ("SELECT %(field)s, TargetInstance "
|
||||
"FROM __InstanceModificationEvent "
|
||||
"WITHIN %(timeframe)s "
|
||||
"WHERE TargetInstance ISA '%(class)s' "
|
||||
"AND TargetInstance.%(field)s != "
|
||||
"PreviousInstance.%(field)s" %
|
||||
{'class': cls,
|
||||
'field': field,
|
||||
'timeframe': timeframe})
|
||||
if filtered_states:
|
||||
checks = ["TargetInstance.%s = '%s'" % (field, state)
|
||||
for state in filtered_states]
|
||||
query += " AND (%s)" % " OR ".join(checks)
|
||||
return query
|
||||
|
||||
def _get_instance_notes(self, vm_name):
|
||||
vm = self._lookup_vm_check(vm_name)
|
||||
|
||||
@@ -77,53 +77,29 @@ class EventHandlerTestCase(test_base.HyperVBaseTestCase):
|
||||
'_get_instance_uuid')
|
||||
@mock.patch.object(eventhandler.InstanceEventHandler, '_emit_event')
|
||||
def _test_dispatch_event(self, mock_emit_event, mock_get_uuid,
|
||||
missing_uuid=False,
|
||||
intermediary_state=False,
|
||||
power_state_changed=True):
|
||||
fake_power_state = (
|
||||
constants.HYPERV_VM_STATE_ENABLED if not intermediary_state
|
||||
else constants.HYPERV_VM_STATE_OTHER)
|
||||
last_power_state = None if power_state_changed else fake_power_state
|
||||
|
||||
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 = (
|
||||
fake_power_state)
|
||||
self._event_handler._last_state = {
|
||||
mock.sentinel.instance_guid: last_power_state
|
||||
}
|
||||
mock.sentinel.power_state)
|
||||
|
||||
event = mock.Mock()
|
||||
event.ElementName = mock.sentinel.instance_name
|
||||
# Hyper-V generated GUID
|
||||
event.Name = mock.sentinel.instance_guid
|
||||
event.EnabledState = mock.sentinel.enabled_state
|
||||
|
||||
self._event_handler._dispatch_event(event)
|
||||
|
||||
if not (missing_uuid or intermediary_state) and power_state_changed:
|
||||
if not missing_uuid:
|
||||
mock_emit_event.assert_called_once_with(
|
||||
mock.sentinel.instance_name,
|
||||
mock.sentinel.instance_uuid,
|
||||
fake_power_state)
|
||||
mock.sentinel.power_state)
|
||||
else:
|
||||
self.assertFalse(mock_emit_event.called)
|
||||
|
||||
# Make sure that the last power state record is always updated,
|
||||
# even in case of intermediary states.
|
||||
self.assertEqual(
|
||||
fake_power_state,
|
||||
self._event_handler._last_state[mock.sentinel.instance_guid])
|
||||
|
||||
def test_dispatch_event_new_final_state(self):
|
||||
self._test_dispatch_event()
|
||||
|
||||
def test_dispatch_event_intermediary_state(self):
|
||||
self._test_dispatch_event(intermediary_state=True)
|
||||
|
||||
def test_dispatch_event_unchanged_state(self):
|
||||
self._test_dispatch_event(power_state_changed=False)
|
||||
|
||||
def test_dispatch_event_missing_uuid(self):
|
||||
self._test_dispatch_event(missing_uuid=True)
|
||||
|
||||
|
||||
@@ -829,3 +829,50 @@ class VMUtilsTestCase(test.NoDBTestCase):
|
||||
notes = self._vmutils._get_instance_notes(mock.sentinel.vm_name)
|
||||
|
||||
self.assertEqual(notes[0], self._FAKE_VM_UUID)
|
||||
|
||||
def test_get_event_wql_query(self):
|
||||
cls = self._vmutils._COMPUTER_SYSTEM_CLASS
|
||||
field = self._vmutils._VM_ENABLED_STATE_PROP
|
||||
timeframe = 10
|
||||
filtered_states = [constants.HYPERV_VM_STATE_ENABLED,
|
||||
constants.HYPERV_VM_STATE_DISABLED]
|
||||
|
||||
expected_checks = ' OR '.join(
|
||||
["TargetInstance.%s = '%s'" % (field, state)
|
||||
for state in filtered_states])
|
||||
expected_query = (
|
||||
"SELECT %(field)s, TargetInstance "
|
||||
"FROM __InstanceModificationEvent "
|
||||
"WITHIN %(timeframe)s "
|
||||
"WHERE TargetInstance ISA '%(class)s' "
|
||||
"AND TargetInstance.%(field)s != "
|
||||
"PreviousInstance.%(field)s "
|
||||
"AND (%(checks)s)" %
|
||||
{'class': cls,
|
||||
'field': field,
|
||||
'timeframe': timeframe,
|
||||
'checks': expected_checks})
|
||||
|
||||
query = self._vmutils._get_event_wql_query(
|
||||
cls=cls, field=field, timeframe=timeframe,
|
||||
filtered_states=filtered_states)
|
||||
self.assertEqual(expected_query, query)
|
||||
|
||||
def test_get_vm_power_state_change_listener(self):
|
||||
with mock.patch.object(self._vmutils,
|
||||
'_get_event_wql_query') as mock_get_query:
|
||||
listener = self._vmutils.get_vm_power_state_change_listener(
|
||||
mock.sentinel.timeframe,
|
||||
mock.sentinel.filtered_states)
|
||||
|
||||
mock_get_query.assert_called_once_with(
|
||||
cls=self._vmutils._COMPUTER_SYSTEM_CLASS,
|
||||
field=self._vmutils._VM_ENABLED_STATE_PROP,
|
||||
timeframe=mock.sentinel.timeframe,
|
||||
filtered_states=mock.sentinel.filtered_states)
|
||||
watcher = self._vmutils._conn.Msvm_ComputerSystem.watch_for
|
||||
watcher.assert_called_once_with(
|
||||
raw_wql=mock_get_query.return_value,
|
||||
fields=[self._vmutils._VM_ENABLED_STATE_PROP])
|
||||
|
||||
self.assertEqual(watcher.return_value, listener)
|
||||
|
||||
Reference in New Issue
Block a user