From 4a70fc9cfb1d4354969c75e12fb94e1681e28ab7 Mon Sep 17 00:00:00 2001 From: Lee Yarwood Date: Fri, 4 Sep 2020 11:49:51 +0100 Subject: [PATCH] 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 Related-Bug: #1882521 Change-Id: I92eb27b710f16d69cf003712431fe225a014c3a8 --- mypy-files.txt | 1 + nova/tests/unit/virt/libvirt/test_driver.py | 22 ++++++++++ nova/tests/unit/virt/libvirt/test_host.py | 37 ++++++++++++++++ nova/virt/libvirt/driver.py | 22 ++++++++++ nova/virt/libvirt/event.py | 41 ++++++++++++++++++ nova/virt/libvirt/host.py | 47 ++++++++++++++++++--- 6 files changed, 165 insertions(+), 5 deletions(-) create mode 100644 nova/virt/libvirt/event.py diff --git a/mypy-files.txt b/mypy-files.txt index d21c70f05f7d..ab4ca9ed3f07 100644 --- a/mypy-files.txt +++ b/mypy-files.txt @@ -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 diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 954b33176a23..ec2835cdbbc9 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -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): ''' 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() diff --git a/nova/tests/unit/virt/libvirt/test_host.py b/nova/tests/unit/virt/libvirt/test_host.py index 634e9597b14b..ce60e2315247 100644 --- a/nova/tests/unit/virt/libvirt/test_host.py +++ b/nova/tests/unit/virt/libvirt/test_host.py @@ -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 = """ + + cef19ce0-0ca2-11df-855d-b19fbce37686 + + """ + 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 = """ + + cef19ce0-0ca2-11df-855d-b19fbce37686 + + """ + 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): diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index 6accf1ef14c1..7af56f7a3725 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -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] diff --git a/nova/virt/libvirt/event.py b/nova/virt/libvirt/event.py new file mode 100644 index 000000000000..a8691e612f9a --- /dev/null +++ b/nova/virt/libvirt/event.py @@ -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""" diff --git a/nova/virt/libvirt/host.py b/nova/virt/libvirt/host.py index 586d2c5fdc32..3cec73365243 100644 --- a/nova/virt/libvirt/host.py +++ b/nova/virt/libvirt/host.py @@ -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})