Merge "Hyper-V: restart serial console workers after instance power change"

This commit is contained in:
Jenkins 2015-07-27 11:45:33 +00:00 committed by Gerrit Code Review
commit 25b378bf5c
10 changed files with 437 additions and 7 deletions

View File

@ -0,0 +1,158 @@
# Copyright 2015 Cloudbase Solutions Srl
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import eventlet
import mock
from nova import exception
from nova.tests.unit.virt.hyperv import test_base
from nova.virt.hyperv import constants
from nova.virt.hyperv import eventhandler
from nova.virt.hyperv import utilsfactory
class EventHandlerTestCase(test_base.HyperVBaseTestCase):
_FAKE_POLLING_INTERVAL = 3
_FAKE_EVENT_CHECK_TIMEFRAME = 15
@mock.patch.object(utilsfactory, 'get_vmutils')
def setUp(self, mock_get_vmutils):
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')
self.flags(
power_state_event_polling_interval=self._FAKE_POLLING_INTERVAL,
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)
@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,
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)
if not missing_uuid:
mock_emit_event.assert_called_once_with(
mock.sentinel.instance_name,
mock.sentinel.instance_uuid,
mock.sentinel.power_state)
else:
self.assertFalse(mock_emit_event.called)
def test_dispatch_event_new_final_state(self):
self._test_dispatch_event()
def test_dispatch_event_missing_uuid(self):
self._test_dispatch_event(missing_uuid=True)
@mock.patch.object(eventhandler.InstanceEventHandler, '_get_virt_event')
@mock.patch.object(eventlet, '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)
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.sentinel.instance_name,
mock.sentinel.instance_uuid)])
def _test_get_instance_uuid(self, instance_found=True,
missing_uuid=False):
if instance_found:
side_effect = (mock.sentinel.instance_uuid
if not missing_uuid else None, )
else:
side_effect = exception.NotFound
mock_get_uuid = self._event_handler._vmutils.get_instance_uuid
mock_get_uuid.side_effect = side_effect
instance_uuid = self._event_handler._get_instance_uuid(
mock.sentinel.instance_name)
expected_uuid = (mock.sentinel.instance_uuid
if instance_found and not missing_uuid else None)
self.assertEqual(expected_uuid, instance_uuid)
def test_get_nova_created_instance_uuid(self):
self._test_get_instance_uuid()
def test_get_deleted_instance_uuid(self):
self._test_get_instance_uuid(instance_found=False)
def test_get_instance_uuid_missing_notes(self):
self._test_get_instance_uuid(missing_uuid=True)
@mock.patch('nova.virt.event.LifecycleEvent')
def test_get_virt_event(self, mock_lifecycle_event):
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)
self.assertEqual(mock_lifecycle_event.return_value,
virt_event)
mock_lifecycle_event.assert_called_once_with(
uuid=mock.sentinel.instance_uuid,
transition=expected_transition)

View File

@ -781,13 +781,14 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
mock_set_vm_state.assert_called_once_with(
mock_instance, constants.HYPERV_VM_STATE_ENABLED)
def _test_power_off(self, timeout):
def _test_power_off(self, timeout, set_state_expected=True):
instance = fake_instance.fake_instance_obj(self.context)
with mock.patch.object(self._vmops, '_set_vm_state') as mock_set_state:
self._vmops.power_off(instance, timeout)
mock_set_state.assert_called_once_with(
instance, constants.HYPERV_VM_STATE_DISABLED)
if set_state_expected:
mock_set_state.assert_called_once_with(
instance, constants.HYPERV_VM_STATE_DISABLED)
def test_power_off_hard(self):
self._test_power_off(timeout=0)
@ -809,6 +810,11 @@ class VMOpsTestCase(test_base.HyperVBaseTestCase):
instance, 1, vmops.SHUTDOWN_TIME_INCREMENT)
self.assertFalse(mock_set_state.called)
@mock.patch("nova.virt.hyperv.vmops.VMOps._soft_shutdown")
def test_power_off_unexisting_instance(self, mock_soft_shutdown):
mock_soft_shutdown.side_effect = exception.NotFound
self._test_power_off(timeout=1, set_state_expected=False)
@mock.patch('nova.virt.hyperv.vmops.VMOps._set_vm_state')
def test_power_on(self, mock_set_vm_state):
mock_instance = fake_instance.fake_instance_obj(self.context)

View File

@ -787,3 +787,64 @@ class VMUtilsTestCase(test.NoDBTestCase):
self._vmutils._conn.query.assert_called_once_with(expected_query)
self.assertEqual(expected_disks, ret_disks)
def _get_fake_instance_notes(self):
return self._FAKE_VM_UUID
def test_instance_notes(self):
self._lookup_vm()
mock_vm_settings = mock.Mock()
mock_vm_settings.Notes = self._get_fake_instance_notes()
self._vmutils._get_vm_setting_data = mock.Mock(
return_value=mock_vm_settings)
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)

View File

@ -150,6 +150,9 @@ class VMUtilsV2TestCase(test_vmutils.VMUtilsTestCase):
['ElementName', 'Notes'],
VirtualSystemType=self._vmutils._VIRTUAL_SYSTEM_TYPE_REALIZED)
def _get_fake_instance_notes(self):
return [self._FAKE_VM_UUID]
@mock.patch('nova.virt.hyperv.vmutilsv2.VMUtilsV2.check_ret_val')
@mock.patch('nova.virt.hyperv.vmutilsv2.VMUtilsV2._get_wmi_obj')
def _test_create_vm_obj(self, mock_get_wmi_obj, mock_check_ret_val,

View File

@ -20,6 +20,7 @@ Constants used in ops classes
from nova.compute import arch
from nova.compute import power_state
HYPERV_VM_STATE_OTHER = 1
HYPERV_VM_STATE_ENABLED = 2
HYPERV_VM_STATE_DISABLED = 3
HYPERV_VM_STATE_SHUTTING_DOWN = 4

View File

@ -23,6 +23,7 @@ from oslo_log import log as logging
from nova.i18n import _
from nova.virt import driver
from nova.virt.hyperv import eventhandler
from nova.virt.hyperv import hostops
from nova.virt.hyperv import livemigrationops
from nova.virt.hyperv import migrationops
@ -54,6 +55,10 @@ class HyperVDriver(driver.ComputeDriver):
def init_host(self, host):
self._vmops.restart_vm_log_writers()
event_handler = eventhandler.InstanceEventHandler(
state_change_callback=self.emit_event,
running_state_callback=self._vmops.log_vm_serial_output)
event_handler.start_listener()
def list_instance_uuids(self):
return self._vmops.list_instance_uuids()

View File

@ -0,0 +1,126 @@
# Copyright 2015 Cloudbase Solutions Srl
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import eventlet
import sys
if sys.platform == 'win32':
import wmi
from oslo_config import cfg
from oslo_log import log as logging
from nova import exception
from nova.i18n import _LW
from nova.virt import event as virtevent
from nova.virt.hyperv import constants
from nova.virt.hyperv import utilsfactory
LOG = logging.getLogger(__name__)
hyperv_opts = [
cfg.IntOpt('power_state_check_timeframe',
default=60,
help='The timeframe to be checked for instance power '
'state changes.'),
cfg.IntOpt('power_state_event_polling_interval',
default=2,
help='Instance power state change event polling frequency.'),
]
CONF = cfg.CONF
CONF.register_opts(hyperv_opts, 'hyperv')
class InstanceEventHandler(object):
# The event listener timeout is set to 0 in order to return immediately
# and avoid blocking the thread.
_WAIT_TIMEOUT = 0
_TRANSITION_MAP = {
constants.HYPERV_VM_STATE_ENABLED: virtevent.EVENT_LIFECYCLE_STARTED,
constants.HYPERV_VM_STATE_DISABLED: virtevent.EVENT_LIFECYCLE_STOPPED,
constants.HYPERV_VM_STATE_PAUSED: virtevent.EVENT_LIFECYCLE_PAUSED,
constants.HYPERV_VM_STATE_SUSPENDED:
virtevent.EVENT_LIFECYCLE_SUSPENDED
}
def __init__(self, state_change_callback=None,
running_state_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=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
def start_listener(self):
eventlet.spawn_n(self._poll_events)
def _poll_events(self):
while True:
try:
# Retrieve one by one all the events that occured 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
# 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)
def _emit_event(self, instance_name, instance_uuid, instance_state):
virt_event = self._get_virt_event(instance_uuid,
instance_state)
eventlet.spawn_n(self._state_change_callback, virt_event)
if instance_state == constants.HYPERV_VM_STATE_ENABLED:
eventlet.spawn_n(self._running_state_callback,
instance_name, instance_uuid)
def _get_instance_uuid(self, instance_name):
try:
instance_uuid = self._vmutils.get_instance_uuid(instance_name)
if not instance_uuid:
LOG.warn(_LW("Instance uuid could not be retrieved for "
"instance %s. Instance state change event "
"will be ignored."),
instance_name)
return instance_uuid
except exception.NotFound:
# The instance has been deleted.
pass
def _get_virt_event(self, instance_uuid, instance_state):
transition = self._TRANSITION_MAP[instance_state]
return virtevent.LifecycleEvent(uuid=instance_uuid,
transition=transition)

View File

@ -537,11 +537,22 @@ class VMOps(object):
LOG.debug("Power off instance", instance=instance)
if retry_interval <= 0:
retry_interval = SHUTDOWN_TIME_INCREMENT
if timeout and self._soft_shutdown(instance, timeout, retry_interval):
return
self._set_vm_state(instance,
constants.HYPERV_VM_STATE_DISABLED)
try:
if timeout and self._soft_shutdown(instance,
timeout,
retry_interval):
return
self._set_vm_state(instance,
constants.HYPERV_VM_STATE_DISABLED)
except exception.NotFound:
# The manager can call the stop API after recieving instance
# power off events. If this is triggered when the instance
# is being deleted, it might attempt to power off an unexisting
# instance. We'll just pass in this case.
LOG.debug("Instance not found. Skipping power off",
instance=instance)
def power_on(self, instance, block_device_info=None):
"""Power on the specified instance."""

View File

@ -27,6 +27,7 @@ if sys.platform == 'win32':
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import uuidutils
import six
from six.moves import range
@ -83,6 +84,9 @@ class VMUtils(object):
_SYNTHETIC_ETHERNET_PORT_SETTING_DATA_CLASS = \
'Msvm_SyntheticEthernetPortSettingData'
_AFFECTED_JOB_ELEMENT_CLASS = "Msvm_AffectedJobElement"
_COMPUTER_SYSTEM_CLASS = "Msvm_ComputerSystem"
_VM_ENABLED_STATE_PROP = "EnabledState"
_SHUTDOWN_COMPONENT = "Msvm_ShutdownComponent"
_VIRTUAL_SYSTEM_CURRENT_SETTINGS = 3
@ -766,3 +770,53 @@ class VMUtils(object):
if v.EnabledState == constants.HYPERV_VM_STATE_ENABLED]
return active_vm_names
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)
vmsettings = self._get_vm_setting_data(vm)
return [note for note in vmsettings.Notes.split('\n') if note]
def get_instance_uuid(self, vm_name):
instance_notes = self._get_instance_notes(vm_name)
if instance_notes and uuidutils.is_uuid_like(instance_notes[0]):
return instance_notes[0]
def get_vm_power_state(self, vm_enabled_state):
return self._enabled_states_map.get(vm_enabled_state,
constants.HYPERV_VM_STATE_OTHER)

View File

@ -321,3 +321,8 @@ class VMUtilsV2(vmutils.VMUtils):
if sasd.ResourceSubType == self._DVD_DISK_RES_SUB_TYPE]
return dvd_paths
def _get_instance_notes(self, vm_name):
vm = self._lookup_vm_check(vm_name)
vmsettings = self._get_vm_setting_data(vm)
return [note for note in vmsettings.Notes if note]