libvirt: Define and emit DeviceRemovedEvent and DeviceRemovalFailedEvent

This patch registers for VIR_DOMAIN_EVENT_ID_DEVICE_REMOVED and
VIR_DOMAIN_EVENT_ID_DEVICE_REMOVAL_FAILED libvirt events and transforms
them to nova virt events.

This patch also extends the libvirt driver to have a driver specific
event handling function for these events instead of using the generic
virt driver event handler that passes all the existing lifecycle events
up to the compute manager.

This is part of the longer series trying to transform the existing
device detach handling to use libvirt events.

Co-Authored-By: Lee Yarwood <lyarwood@redhat.com>
Related-Bug: #1882521
Change-Id: I92eb27b710f16d69cf003712431fe225a014c3a8
This commit is contained in:
Lee Yarwood 2020-09-04 11:49:51 +01:00 committed by Balazs Gibizer
parent f8090825c1
commit 4a70fc9cfb
6 changed files with 165 additions and 5 deletions

View File

@ -6,5 +6,6 @@ nova/virt/driver.py
nova/virt/hardware.py
nova/virt/libvirt/__init__.py
nova/virt/libvirt/driver.py
nova/virt/libvirt/event.py
nova/virt/libvirt/host.py
nova/virt/libvirt/utils.py

View File

@ -109,6 +109,7 @@ from nova.virt.libvirt import blockinfo
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import designer
from nova.virt.libvirt import driver as libvirt_driver
from nova.virt.libvirt import event as libvirtevent
from nova.virt.libvirt import guest as libvirt_guest
from nova.virt.libvirt import host
from nova.virt.libvirt.host import SEV_KERNEL_PARAM_FILE
@ -27274,3 +27275,24 @@ class LibvirtPMEMNamespaceTests(test.NoDBTestCase):
</devices>
</domain>'''
self.assertXmlEqual(expected, guest.to_xml())
@ddt.ddt
class LibvirtDeviceRemoveEventTestCase(test.NoDBTestCase):
def setUp(self):
super().setUp()
self.useFixture(fakelibvirt.FakeLibvirtFixture())
@mock.patch.object(libvirt_driver.LOG, 'debug')
@mock.patch('nova.virt.driver.ComputeDriver.emit_event')
@ddt.data(
libvirtevent.DeviceRemovedEvent,
libvirtevent.DeviceRemovalFailedEvent)
def test_libvirt_device_removal_events(
self, event_type, mock_base_handles, mock_debug
):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
event = event_type(uuid=uuids.event, dev=mock.sentinel.dev_alias)
drvr.emit_event(event)
mock_base_handles.assert_not_called()
mock_debug.assert_not_called()

View File

@ -33,6 +33,7 @@ from nova.tests.unit.virt.libvirt import fake_libvirt_data
from nova.tests.unit.virt.libvirt import fakelibvirt
from nova.virt import event
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import event as libvirtevent
from nova.virt.libvirt import guest as libvirt_guest
from nova.virt.libvirt import host
@ -302,6 +303,42 @@ class HostTestCase(test.NoDBTestCase):
gt_mock.cancel.assert_called_once_with()
self.assertNotIn(uuid, hostimpl._events_delayed.keys())
def test_device_removed_event(self):
hostimpl = mock.MagicMock()
conn = mock.MagicMock()
fake_dom_xml = """
<domain type='kvm'>
<uuid>cef19ce0-0ca2-11df-855d-b19fbce37686</uuid>
</domain>
"""
dom = fakelibvirt.Domain(conn, fake_dom_xml, running=True)
host.Host._event_device_removed_callback(
conn, dom, dev='virtio-1', opaque=hostimpl)
expected_event = hostimpl._queue_event.call_args[0][0]
self.assertEqual(
libvirtevent.DeviceRemovedEvent, type(expected_event))
self.assertEqual(
'cef19ce0-0ca2-11df-855d-b19fbce37686', expected_event.uuid)
self.assertEqual('virtio-1', expected_event.dev)
def test_device_removal_failed(self):
hostimpl = mock.MagicMock()
conn = mock.MagicMock()
fake_dom_xml = """
<domain type='kvm'>
<uuid>cef19ce0-0ca2-11df-855d-b19fbce37686</uuid>
</domain>
"""
dom = fakelibvirt.Domain(conn, fake_dom_xml, running=True)
host.Host._event_device_removal_failed_callback(
conn, dom, dev='virtio-1', opaque=hostimpl)
expected_event = hostimpl._queue_event.call_args[0][0]
self.assertEqual(
libvirtevent.DeviceRemovalFailedEvent, type(expected_event))
self.assertEqual(
'cef19ce0-0ca2-11df-855d-b19fbce37686', expected_event.uuid)
self.assertEqual('virtio-1', expected_event.dev)
@mock.patch.object(fakelibvirt.virConnect, "domainEventRegisterAny")
@mock.patch.object(host.Host, "_connect")
def test_get_connection_serial(self, mock_conn, mock_event):

View File

@ -106,12 +106,14 @@ from nova.virt import configdrive
from nova.virt.disk import api as disk_api
from nova.virt.disk.vfs import guestfs
from nova.virt import driver
from nova.virt import event as virtevent
from nova.virt import hardware
from nova.virt.image import model as imgmodel
from nova.virt import images
from nova.virt.libvirt import blockinfo
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import designer
from nova.virt.libvirt import event as libvirtevent
from nova.virt.libvirt import guest as libvirt_guest
from nova.virt.libvirt import host
from nova.virt.libvirt import imagebackend
@ -1980,6 +1982,26 @@ class LibvirtDriver(driver.ComputeDriver):
block_device_info=block_device_info)
return xml
def emit_event(self, event: virtevent.InstanceEvent) -> None:
"""Handles libvirt specific events locally and dispatches the rest to
the compute manager.
"""
if isinstance(event, libvirtevent.LibvirtEvent):
# These are libvirt specific events handled here on the driver
# level instead of propagating them to the compute manager level
if isinstance(event, libvirtevent.DeviceEvent):
# TODO(gibi): handle it
pass
else:
LOG.debug(
"Received event %s from libvirt but no handler is "
"implemented for it in the libvirt driver so it is "
"ignored", event)
else:
# Let the generic driver code dispatch the event to the compute
# manager
super().emit_event(event)
def detach_volume(self, context, connection_info, instance, mountpoint,
encryption=None):
disk_dev = mountpoint.rpartition("/")[2]

View File

@ -0,0 +1,41 @@
# 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.
from nova.virt import event
class LibvirtEvent(event.InstanceEvent):
"""Base class for virt events that are specific to libvirt and therefore
handled in the libvirt driver level instead of propagatig it up to the
compute manager.
"""
class DeviceEvent(LibvirtEvent):
"""Base class for device related libvirt events"""
def __init__(self, uuid: str, dev: str, timestamp: float = None):
super().__init__(uuid, timestamp)
self.dev = dev
def __repr__(self) -> str:
return "<%s: %s, %s => %s>" % (
self.__class__.__name__,
self.timestamp,
self.uuid,
self.dev)
class DeviceRemovedEvent(DeviceEvent):
"""Libvirt sends this event after a successful device detach"""
class DeviceRemovalFailedEvent(DeviceEvent):
"""Libvirt sends this event after an unsuccessful device detach"""

View File

@ -56,6 +56,7 @@ from nova import rpc
from nova import utils
from nova.virt import event as virtevent
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt import event as libvirtevent
from nova.virt.libvirt import guest as libvirt_guest
from nova.virt.libvirt import migration as libvirt_migrate
from nova.virt.libvirt import utils as libvirt_utils
@ -196,6 +197,32 @@ class Host(object):
finally:
self._conn_event_handler_queue.task_done()
@staticmethod
def _event_device_removed_callback(conn, dom, dev, opaque):
"""Receives device removed events from libvirt.
NB: this method is executing in a native thread, not
an eventlet coroutine. It can only invoke other libvirt
APIs, or use self._queue_event(). Any use of logging APIs
in particular is forbidden.
"""
self = opaque
uuid = dom.UUIDString()
self._queue_event(libvirtevent.DeviceRemovedEvent(uuid, dev))
@staticmethod
def _event_device_removal_failed_callback(conn, dom, dev, opaque):
"""Receives device removed events from libvirt.
NB: this method is executing in a native thread, not
an eventlet coroutine. It can only invoke other libvirt
APIs, or use self._queue_event(). Any use of logging APIs
in particular is forbidden.
"""
self = opaque
uuid = dom.UUIDString()
self._queue_event(libvirtevent.DeviceRemovalFailedEvent(uuid, dev))
@staticmethod
def _event_lifecycle_callback(conn, dom, event, detail, opaque):
"""Receives lifecycle events from libvirt.
@ -330,9 +357,9 @@ class Host(object):
while not self._event_queue.empty():
try:
event_type = ty.Union[
virtevent.LifecycleEvent, ty.Mapping[str, ty.Any]]
virtevent.InstanceEvent, ty.Mapping[str, ty.Any]]
event: event_type = self._event_queue.get(block=False)
if isinstance(event, virtevent.LifecycleEvent):
if issubclass(type(event), virtevent.InstanceEvent):
# call possibly with delay
self._event_emit_delayed(event)
@ -366,10 +393,10 @@ class Host(object):
if event.uuid in self._events_delayed.keys():
self._events_delayed[event.uuid].cancel()
self._events_delayed.pop(event.uuid, None)
LOG.debug("Removed pending event for %s due to "
"lifecycle event", event.uuid)
LOG.debug("Removed pending event for %s due to event", event.uuid)
if event.transition == virtevent.EVENT_LIFECYCLE_STOPPED:
if (isinstance(event, virtevent.LifecycleEvent) and
event.transition == virtevent.EVENT_LIFECYCLE_STOPPED):
# Delay STOPPED event, as they may be followed by a STARTED
# event in case the instance is rebooting
id_ = greenthread.spawn_after(self._lifecycle_delay,
@ -443,6 +470,16 @@ class Host(object):
libvirt.VIR_DOMAIN_EVENT_ID_LIFECYCLE,
self._event_lifecycle_callback,
self)
wrapped_conn.domainEventRegisterAny(
None,
libvirt.VIR_DOMAIN_EVENT_ID_DEVICE_REMOVED,
self._event_device_removed_callback,
self)
wrapped_conn.domainEventRegisterAny(
None,
libvirt.VIR_DOMAIN_EVENT_ID_DEVICE_REMOVAL_FAILED,
self._event_device_removal_failed_callback,
self)
except Exception as e:
LOG.warning("URI %(uri)s does not support events: %(error)s",
{'uri': self._uri, 'error': e})