On 16.09.2019 11:26, Peter Krempa wrote:
On Mon, Sep 09, 2019 at 14:33:07 +0300, Nikolay Shirokovskiy wrote:
> Now when code handling attaching/detaching usb hostdev is appropriately
> changed use it to handle host usb device udev add/del events.
>
> Signed-off-by: Nikolay Shirokovskiy <nshirokovskiy(a)virtuozzo.com>
> ---
> src/qemu/Makefile.inc.am | 2 +
> src/qemu/qemu_conf.h | 3 +
> src/qemu/qemu_domain.c | 2 +
> src/qemu/qemu_domain.h | 2 +
> src/qemu/qemu_driver.c | 351 ++++++++++++++++++++++++++++++++++++++-
> 5 files changed, 359 insertions(+), 1 deletion(-)
>
> diff --git a/src/qemu/Makefile.inc.am b/src/qemu/Makefile.inc.am
> index d16b315ebc..8be0dee396 100644
> --- a/src/qemu/Makefile.inc.am
> +++ b/src/qemu/Makefile.inc.am
> @@ -85,6 +85,7 @@ libvirt_driver_qemu_impl_la_CFLAGS = \
> -I$(srcdir)/conf \
> -I$(srcdir)/secret \
> $(AM_CFLAGS) \
> + $(UDEV_CFLAGS) \
> $(NULL)
> libvirt_driver_qemu_impl_la_LDFLAGS = $(AM_LDFLAGS)
> libvirt_driver_qemu_impl_la_LIBADD = \
> @@ -93,6 +94,7 @@ libvirt_driver_qemu_impl_la_LIBADD = \
> $(LIBNL_LIBS) \
> $(SELINUX_LIBS) \
> $(LIBXML_LIBS) \
> + $(UDEV_LIBS) \
> $(NULL)
> libvirt_driver_qemu_impl_la_SOURCES = $(QEMU_DRIVER_SOURCES)
>
> diff --git a/src/qemu/qemu_conf.h b/src/qemu/qemu_conf.h
> index 0cbddd7a9c..2e50bb0950 100644
> --- a/src/qemu/qemu_conf.h
> +++ b/src/qemu/qemu_conf.h
> @@ -294,6 +294,9 @@ struct _virQEMUDriver {
>
> /* Immutable pointer, self-locking APIs */
> virHashAtomicPtr migrationErrors;
> +
> + struct udev_monitor *udev_monitor;
> + int udev_watch;
> };
>
> virQEMUDriverConfigPtr virQEMUDriverConfigNew(bool privileged);
> diff --git a/src/qemu/qemu_domain.c b/src/qemu/qemu_domain.c
> index 657f3ecfe4..4784804d1e 100644
> --- a/src/qemu/qemu_domain.c
> +++ b/src/qemu/qemu_domain.c
> @@ -15034,6 +15034,8 @@ qemuProcessEventFree(struct qemuProcessEvent *event)
> case QEMU_PROCESS_EVENT_SERIAL_CHANGED:
> case QEMU_PROCESS_EVENT_BLOCK_JOB:
> case QEMU_PROCESS_EVENT_MONITOR_EOF:
> + case QEMU_PROCESS_EVENT_USB_REMOVED:
> + case QEMU_PROCESS_EVENT_USB_ADDED:
> VIR_FREE(event->data);
> break;
> case QEMU_PROCESS_EVENT_JOB_STATUS_CHANGE:
> diff --git a/src/qemu/qemu_domain.h b/src/qemu/qemu_domain.h
> index d097f23342..94aea62693 100644
> --- a/src/qemu/qemu_domain.h
> +++ b/src/qemu/qemu_domain.h
> @@ -521,6 +521,8 @@ typedef enum {
> QEMU_PROCESS_EVENT_MONITOR_EOF,
> QEMU_PROCESS_EVENT_PR_DISCONNECT,
> QEMU_PROCESS_EVENT_RDMA_GID_STATUS_CHANGED,
> + QEMU_PROCESS_EVENT_USB_REMOVED,
> + QEMU_PROCESS_EVENT_USB_ADDED,
>
> QEMU_PROCESS_EVENT_LAST
> } qemuProcessEventType;
> diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c
> index 2378a2e7d0..ce41b0a8d9 100644
> --- a/src/qemu/qemu_driver.c
> +++ b/src/qemu/qemu_driver.c
> @@ -34,6 +34,7 @@
> #include <sys/ioctl.h>
> #include <sys/un.h>
> #include <byteswap.h>
> +#include <libudev.h>
>
>
> #include "qemu_driver.h"
> @@ -719,6 +720,254 @@ qemuDomainFindMaxID(virDomainObjPtr vm,
> }
>
>
> +struct qemuUdevUSBRemoveData {
> + unsigned int bus;
> + unsigned int device;
> +};
> +
> +struct qemuUdevUSBAddData {
> + unsigned int vendor;
> + unsigned int product;
> +};
> +
> +struct qemuUdevUSBEventData {
> + union {
> + struct qemuUdevUSBRemoveData remove;
> + struct qemuUdevUSBAddData add;
> + } data;
> + bool found;
> + bool remove;
> +};
> +
> +static int
> +qemuUdevUSBHandleEvent(virDomainObjPtr vm, void *opaque)
> +{
> + struct qemuUdevUSBEventData *data = opaque;
> + struct qemuProcessEvent *event = NULL;
> + size_t i;
> +
> + if (data->found)
> + return 0;
> +
> + virObjectLock(vm);
> +
> + if (!virDomainObjIsActive(vm))
> + goto cleanup;
> +
> + for (i = 0; i < vm->def->nhostdevs; i++) {
> + virDomainHostdevDefPtr hostdev = vm->def->hostdevs[i];
> + virDomainHostdevSubsysUSBPtr usbsrc = &hostdev->source.subsys.u.usb;
> +
> + if (hostdev->source.subsys.type != VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_USB)
> + continue;
> +
> + if (data->remove) {
> + if (usbsrc->bus != data->data.remove.bus ||
> + usbsrc->device != data->data.remove.device)
> + continue;
> + } else {
> + if (usbsrc->vendor != data->data.add.vendor ||
> + usbsrc->product != data->data.add.product)
> + continue;
> + }
I don't see any XML change related to this in this series.
IMO it's unacceptable to re-plug ANY device by default and we must
introduce a flag where the admin gives explicit consent for a device to
be re-plugged on the host hotplug event.
You must note that these two instances may be long time apart and thus
the user might not notice that the device is re-grabbed by the VM.
Also due to the nature of USB devices it may be a completely different
device (e.g. a USB Flash drive of the same make/model but with different
data).
Allowing this by default would lead to confusion.
Fair enough.
> +
> + data->found = true;
> +
> + if (VIR_ALLOC(event) < 0)
> + goto cleanup;
> +
> + if (data->remove) {
> + struct qemuUdevUSBRemoveData *rm_data;
> +
> +
> + if (VIR_ALLOC(rm_data) < 0)
> + goto cleanup;
> +
> + *rm_data = data->data.remove;
> + event->data = rm_data;
> + event->eventType = QEMU_PROCESS_EVENT_USB_REMOVED;
> + } else {
> + struct qemuUdevUSBAddData *add_data;
> +
> + if (VIR_ALLOC(add_data) < 0)
> + goto cleanup;
> +
> + *add_data = data->data.add;
> + event->data = add_data;
> + event->eventType = QEMU_PROCESS_EVENT_USB_ADDED;
> + }
> +
> + event->vm = virObjectRef(vm);
> +
> + if (virThreadPoolSendJob(qemu_driver->workerPool, 0, event) < 0) {
> + virObjectUnref(vm);
> + goto cleanup;
> + }
> +
> + event = NULL;
> +
> + break;
> + }
> +
> + cleanup:
> + virObjectUnlock(vm);
> +
> + qemuProcessEventFree(event);
> +
> + return 0;
> +}
> +
> +
> +static void
> +qemuUdevEventHandleCallback(int watch ATTRIBUTE_UNUSED,
> + int fd ATTRIBUTE_UNUSED,
> + int events ATTRIBUTE_UNUSED,
> + void *data ATTRIBUTE_UNUSED)
> +{
> + struct qemuUdevUSBEventData event_data;
> + struct udev_device *dev = NULL;
> + const char *action;
> + const char *devtype;
> + const char *tmp;
> +
> + /* libvirtd daemon do not run event loop before full state drivers
> + * initialization. Also state drivers uninitialized only after
> + * full stop of event loop. In short driver initialization/uninitialization
> + * and handling events occurs in same main loop thread. Thus we
> + * don't need any locking here. */
> +
> + if (!(dev = udev_monitor_receive_device(qemu_driver->udev_monitor))) {
> + VIR_WARNINGS_NO_WLOGICALOP_EQUAL_EXPR
> + if (errno == EAGAIN || errno == EWOULDBLOCK) {
> + VIR_WARNINGS_RESET
> + return;
> + }
> +
> + virReportSystemError(errno, "%s",
> + _("failed to receive device from udev
monitor"));
> + return;
> + }
> +
> + devtype = udev_device_get_devtype(dev);
> +
> + if (STRNEQ_NULLABLE(devtype, "usb_device"))
> + goto cleanup;
> +
> + if (!(action = udev_device_get_action(dev))) {
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("failed to receive action from udev monitor"));
> + goto cleanup;
> + }
> +
> + if (STREQ(action, "remove")) {
> + struct qemuUdevUSBRemoveData *rm_data = &event_data.data.remove;
> +
> + if (!(tmp = udev_device_get_property_value(dev, "BUSNUM"))) {
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("failed to receive busnum from udev
monitor"));
> + goto cleanup;
> + }
> + if (virStrToLong_ui(tmp, NULL, 10, &rm_data->bus) < 0) {
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("Failed to convert busnum to int"));
> + goto cleanup;
> + }
> +
> + if (!(tmp = udev_device_get_property_value(dev, "DEVNUM"))) {
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("failed to receive devnum from udev
monitor"));
> + goto cleanup;
> + }
> + if (virStrToLong_ui(tmp, NULL, 10, &rm_data->device) < 0) {
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("Failed to convert devnum to int"));
> + goto cleanup;
> + }
> + event_data.remove = true;
> + } else if (STREQ(action, "add")) {
> + struct qemuUdevUSBAddData *add_data = &event_data.data.add;
> +
> + if (!(tmp = udev_device_get_property_value(dev, "ID_VENDOR_ID")))
{
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("failed to receive vendor from udev
monitor"));
> + goto cleanup;
> + }
> + if (virStrToLong_ui(tmp, NULL, 16, &add_data->vendor) < 0) {
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("Failed to convert vendor to int"));
> + goto cleanup;
> + }
> +
> + if (!(tmp = udev_device_get_property_value(dev, "ID_MODEL_ID")))
{
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("failed to receive product from udev
monitor"));
> + goto cleanup;
> + }
> + if (virStrToLong_ui(tmp, NULL, 16, &add_data->product) < 0) {
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("Failed to convert product to int"));
> + goto cleanup;
> + }
> + event_data.remove = false;
> + }
> +
> + event_data.found = false;
> + virDomainObjListForEach(qemu_driver->domains, qemuUdevUSBHandleEvent,
&event_data);
Is this executed from the event loop thread? If yes we can't do this as
qemuUdevUSBHandleEvent is taking domain locks and thus potentially
waiting indefinitely for any stuck VM.
Yes, this is executed in the event loop thread. But I guess we can take
domain lock here as this lock is intended to be grabbed only for short
periods of time by design (as stated in THREADS.txt). There are a lot
of qemu event handlers that grab domain lock (qemuProcessHandleMonitorEOF
for example). AFAIK we use offloading to thread pool if we need to
start a job which can take long time because of already running job.
And this is offloading is done in qemuUdevUSBHandleEvent.
> +
> + cleanup:
> + udev_device_unref(dev);
> +}
> +
> +
> +static int
> +qemuUdevInitialize(void)
> +{
> + struct udev *udev;
> +
> + if (!(udev = udev_new())) {
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("failed to create udev context"));
> + return -1;
> + }
> +
> + if (!(qemu_driver->udev_monitor = udev_monitor_new_from_netlink(udev,
"udev"))) {
> + udev_unref(udev);
> + virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
> + _("udev_monitor_new_from_netlink returned NULL"));
> + return -1;
> + }
> +
> + udev_monitor_enable_receiving(qemu_driver->udev_monitor);
> +
> + qemu_driver->udev_watch =
virEventAddHandle(udev_monitor_get_fd(qemu_driver->udev_monitor),
> + VIR_EVENT_HANDLE_READABLE,
> + qemuUdevEventHandleCallback, NULL,
NULL);
> +
> + if (qemu_driver->udev_watch < 0)
> + return -1;
> +
> + return 0;
> +}
> +
> +
> +static void
> +qemuUdevCleanup(void)
> +{
> + if (qemu_driver->udev_monitor) {
> + struct udev *udev = udev_monitor_get_udev(qemu_driver->udev_monitor);
> +
> + udev_monitor_unref(qemu_driver->udev_monitor);
> + udev_unref(udev);
> + qemu_driver->udev_monitor = NULL;
> + }
> +
> + if (qemu_driver->udev_watch > 0) {
> + virEventRemoveHandle(qemu_driver->udev_watch);
> + qemu_driver->udev_watch = 0;
> + }
> +}
> +
> +
> /**
> * qemuStateInitialize:
> *
> @@ -1030,6 +1279,9 @@ qemuStateInitialize(bool privileged,
> if (!(qemu_driver->closeCallbacks = virCloseCallbacksNew()))
> goto error;
>
> + if (qemuUdevInitialize() < 0)
> + goto error;
> +
> /* Get all the running persistent or transient configs first */
> if (virDomainObjListLoadAllConfigs(qemu_driver->domains,
> cfg->stateDir,
> @@ -1239,6 +1491,8 @@ qemuStateCleanup(void)
>
> virLockManagerPluginUnref(qemu_driver->lockManager);
>
> + qemuUdevCleanup();
> +
> virMutexDestroy(&qemu_driver->lock);
> VIR_FREE(qemu_driver);
>
> @@ -5011,7 +5265,96 @@ processRdmaGidStatusChangedEvent(virDomainObjPtr vm,
> }
>
>
> -static void qemuProcessEventHandler(void *data, void *opaque)
> +static void
> +processUSBAddedEvent(virQEMUDriverPtr driver,
> + virDomainObjPtr vm,
> + struct qemuUdevUSBAddData *data)
> +{
> + virDomainHostdevDefPtr hostdev;
> + virDomainHostdevSubsysUSBPtr usbsrc;
> + size_t i;
> +
> + if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_MODIFY) < 0)
> + return;
> +
> + if (!virDomainObjIsActive(vm)) {
> + VIR_DEBUG("Domain is not running");
> + goto cleanup;
> + }
> +
> + for (i = 0; i < vm->def->nhostdevs; i++) {
> + hostdev = vm->def->hostdevs[i];
> +
> + if (hostdev->source.subsys.type != VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_USB)
> + continue;
> +
> + usbsrc = &hostdev->source.subsys.u.usb;
> +
> + if (usbsrc->vendor == data->vendor && usbsrc->product ==
data->product)
> + break;
> + }
> +
> + if (i == vm->def->nhostdevs)
> + goto cleanup;
> +
> + if (qemuDomainAttachHostDevice(driver, vm, hostdev) < 0)
> + goto cleanup;
> +
> + cleanup:
> + qemuDomainObjEndJob(driver, vm);
> +}
> +
> +
> +static void
> +processUSBRemovedEvent(virQEMUDriverPtr driver,
> + virDomainObjPtr vm,
> + struct qemuUdevUSBRemoveData *data)
> +{
> + size_t i;
> + virDomainHostdevDefPtr hostdev;
> + virDomainDeviceDef dev = { .type = VIR_DOMAIN_DEVICE_HOSTDEV };
> +
> + if (qemuDomainObjBeginJob(driver, vm, QEMU_JOB_MODIFY) < 0)
> + return;
> +
> + if (!virDomainObjIsActive(vm)) {
> + VIR_DEBUG("Domain is not running");
> + goto cleanup;
> + }
> +
> + for (i = 0; i < vm->def->nhostdevs; i++) {
> + virDomainHostdevSubsysUSBPtr usbsrc;
> +
> + hostdev = vm->def->hostdevs[i];
> + if (hostdev->source.subsys.type != VIR_DOMAIN_HOSTDEV_SUBSYS_TYPE_USB)
> + continue;
> +
> + usbsrc = &hostdev->source.subsys.u.usb;
> +
> + /* don't mess with devices that don't use stable host addressing
> + * with respect to unplug/plug to host
> + */
> + if (!usbsrc->vendor || !usbsrc->product)
> + continue;
> +
> + if (usbsrc->bus == data->bus && usbsrc->device ==
data->device)
> + break;
> + }
> +
> + if (i == vm->def->nhostdevs)
> + goto cleanup;
> +
> + dev.data.hostdev = hostdev;
> + if (qemuDomainDetachDeviceLive(vm, &dev, driver, true, true) < 0)
AFAIK this will remove the definition form the XML so how is the re-plug
going to be facilitated then?
"[PATCH v2 02/11] qemu: support host usb device unplug" changes removing
logic to keep unplugged device in the libvirt config.
Nikolay
> + goto cleanup;
> +
> + cleanup:
> + qemuDomainObjEndJob(driver, vm);
> +}
> +
> +
> +static void
> +qemuProcessEventHandler(void *data, void *opaque)
> {
> struct qemuProcessEvent *processEvent = data;
> virDomainObjPtr vm = processEvent->vm;