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:
Lucian Petrut
2015-06-10 15:03:54 +03:00
parent 055d8ee6a4
commit 490dfdd316
4 changed files with 92 additions and 53 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)