mirror of
https://github.com/libvirt/libvirt.git
synced 2025-02-25 18:55:26 -06:00
The lookup didn't do anything apart from comparing the sysfs paths anyway since that's what makes each mdev unique. The most ridiculous usage of the old logic was in virHostdevReAttachMediatedDevices where in order to drop an mdev hostdev from the list of active devices we first had to create a new mdev and use it in the lookup call. Why couldn't we have used the hostdev directly? Because the hostdev and mdev structures are incompatible. The way mdevs are currently removed is via a write to a specific sysfs attribute. If you do it while the machine which has the mdev assigned is running, the write call may block (with a new enough kernel, with older kernels it would return a write error!) until the device is no longer in use which is when the QEMU process exits. The interesting part here comes afterwards when we're cleaning up and call virHostdevReAttachMediatedDevices. The domain doesn't exist anymore, so the list of active hostdevs needs to be updated and the respective hostdevs removed from the list, but remember we had to create an mdev object in the memory in order to find it in the list first which will fail because the write to sysfs had already removed the mdev instance from the host system. And so the next time you try to start the same domain you'll get: "Requested operation is not valid: mediated device <path> is in use by driver QEMU, domain <name>" Fixes: https://gitlab.com/libvirt/libvirt/-/issues/119 Signed-off-by: Erik Skultety <eskultet@redhat.com> Reviewed-by: Ján Tomko <jtomko@redhat.com>
593 lines
14 KiB
C
593 lines
14 KiB
C
/*
|
|
* virmdev.c: helper APIs for managing host mediated devices
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see
|
|
* <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include <config.h>
|
|
|
|
#include "virmdev.h"
|
|
#include "virlog.h"
|
|
#include "virerror.h"
|
|
#include "virfile.h"
|
|
#include "virstring.h"
|
|
#include "viralloc.h"
|
|
|
|
#define VIR_FROM_THIS VIR_FROM_NONE
|
|
|
|
#define MDEV_SYSFS_DEVICES "/sys/bus/mdev/devices/"
|
|
|
|
VIR_LOG_INIT("util.mdev");
|
|
|
|
struct _virMediatedDevice {
|
|
char *path; /* sysfs path */
|
|
virMediatedDeviceModelType model;
|
|
|
|
char *used_by_drvname;
|
|
char *used_by_domname;
|
|
};
|
|
|
|
struct _virMediatedDeviceList {
|
|
virObjectLockable parent;
|
|
|
|
size_t count;
|
|
virMediatedDevicePtr *devs;
|
|
};
|
|
|
|
VIR_ENUM_IMPL(virMediatedDeviceModel,
|
|
VIR_MDEV_MODEL_TYPE_LAST,
|
|
"vfio-pci",
|
|
"vfio-ccw",
|
|
"vfio-ap",
|
|
);
|
|
|
|
static virClassPtr virMediatedDeviceListClass;
|
|
|
|
static void
|
|
virMediatedDeviceListDispose(void *obj);
|
|
|
|
static int
|
|
virMediatedOnceInit(void)
|
|
{
|
|
if (!VIR_CLASS_NEW(virMediatedDeviceList, virClassForObjectLockable()))
|
|
return -1;
|
|
|
|
return 0;
|
|
}
|
|
|
|
VIR_ONCE_GLOBAL_INIT(virMediated);
|
|
|
|
#ifdef __linux__
|
|
|
|
static int
|
|
virMediatedDeviceGetSysfsDeviceAPI(virMediatedDevicePtr dev,
|
|
char **device_api)
|
|
{
|
|
g_autofree char *buf = NULL;
|
|
g_autofree char *file = NULL;
|
|
char *tmp = NULL;
|
|
|
|
file = g_strdup_printf("%s/mdev_type/device_api", dev->path);
|
|
|
|
/* TODO - make this a generic method to access sysfs files for various
|
|
* kinds of devices
|
|
*/
|
|
if (!virFileExists(file)) {
|
|
virReportSystemError(errno, _("failed to read '%s'"), file);
|
|
return -1;
|
|
}
|
|
|
|
if (virFileReadAll(file, 1024, &buf) < 0)
|
|
return -1;
|
|
|
|
if ((tmp = strchr(buf, '\n')))
|
|
*tmp = '\0';
|
|
|
|
*device_api = buf;
|
|
buf = NULL;
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static int
|
|
virMediatedDeviceCheckModel(virMediatedDevicePtr dev,
|
|
virMediatedDeviceModelType model)
|
|
{
|
|
g_autofree char *dev_api = NULL;
|
|
int actual_model;
|
|
|
|
if (virMediatedDeviceGetSysfsDeviceAPI(dev, &dev_api) < 0)
|
|
return -1;
|
|
|
|
/* safeguard in case we've got an older libvirt which doesn't know newer
|
|
* device_api models yet
|
|
*/
|
|
if ((actual_model = virMediatedDeviceModelTypeFromString(dev_api)) < 0) {
|
|
virReportError(VIR_ERR_INTERNAL_ERROR,
|
|
_("device API '%s' not supported yet"),
|
|
dev_api);
|
|
return -1;
|
|
}
|
|
|
|
if (actual_model != model) {
|
|
virReportError(VIR_ERR_INTERNAL_ERROR,
|
|
_("invalid device API '%s' for device %s: "
|
|
"device only supports '%s'"),
|
|
virMediatedDeviceModelTypeToString(model),
|
|
dev->path, dev_api);
|
|
return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
virMediatedDevicePtr
|
|
virMediatedDeviceNew(const char *uuidstr, virMediatedDeviceModelType model)
|
|
{
|
|
g_autoptr(virMediatedDevice) dev = NULL;
|
|
g_autofree char *sysfspath = NULL;
|
|
|
|
if (!(sysfspath = virMediatedDeviceGetSysfsPath(uuidstr)))
|
|
return NULL;
|
|
|
|
if (!virFileExists(sysfspath)) {
|
|
virReportError(VIR_ERR_DEVICE_MISSING,
|
|
_("mediated device '%s' not found"), uuidstr);
|
|
return NULL;
|
|
}
|
|
|
|
dev = g_new0(virMediatedDevice, 1);
|
|
|
|
dev->path = g_steal_pointer(&sysfspath);
|
|
|
|
/* Check whether the user-provided model corresponds with the actually
|
|
* supported mediated device's API.
|
|
*/
|
|
if (virMediatedDeviceCheckModel(dev, model))
|
|
return NULL;
|
|
|
|
dev->model = model;
|
|
return g_steal_pointer(&dev);
|
|
}
|
|
|
|
#else
|
|
|
|
virMediatedDevicePtr
|
|
virMediatedDeviceNew(const char *uuidstr G_GNUC_UNUSED,
|
|
virMediatedDeviceModelType model G_GNUC_UNUSED)
|
|
{
|
|
virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
|
|
_("mediated devices are not supported on non-linux "
|
|
"platforms"));
|
|
return NULL;
|
|
}
|
|
|
|
#endif /* __linux__ */
|
|
|
|
void
|
|
virMediatedDeviceFree(virMediatedDevicePtr dev)
|
|
{
|
|
if (!dev)
|
|
return;
|
|
VIR_FREE(dev->path);
|
|
VIR_FREE(dev->used_by_drvname);
|
|
VIR_FREE(dev->used_by_domname);
|
|
VIR_FREE(dev);
|
|
}
|
|
|
|
|
|
const char *
|
|
virMediatedDeviceGetPath(virMediatedDevicePtr dev)
|
|
{
|
|
return dev->path;
|
|
}
|
|
|
|
|
|
/* Returns an absolute canonicalized path to the device used to control the
|
|
* mediated device's IOMMU group (e.g. "/dev/vfio/15"). Caller is responsible
|
|
* for freeing the result.
|
|
*/
|
|
char *
|
|
virMediatedDeviceGetIOMMUGroupDev(const char *uuidstr)
|
|
{
|
|
g_autofree char *result_path = NULL;
|
|
g_autofree char *result_file = NULL;
|
|
g_autofree char *iommu_path = NULL;
|
|
g_autofree char *dev_path = virMediatedDeviceGetSysfsPath(uuidstr);
|
|
|
|
if (!dev_path)
|
|
return NULL;
|
|
|
|
iommu_path = g_strdup_printf("%s/iommu_group", dev_path);
|
|
|
|
if (!virFileExists(iommu_path)) {
|
|
virReportSystemError(errno, _("failed to access '%s'"), iommu_path);
|
|
return NULL;
|
|
}
|
|
|
|
if (virFileResolveLink(iommu_path, &result_path) < 0) {
|
|
virReportSystemError(errno, _("failed to resolve '%s'"), iommu_path);
|
|
return NULL;
|
|
}
|
|
|
|
result_file = g_path_get_basename(result_path);
|
|
|
|
return g_strdup_printf("/dev/vfio/%s", result_file);
|
|
}
|
|
|
|
|
|
int
|
|
virMediatedDeviceGetIOMMUGroupNum(const char *uuidstr)
|
|
{
|
|
g_autofree char *vfio_path = NULL;
|
|
g_autofree char *group_num_str = NULL;
|
|
unsigned int group_num = -1;
|
|
|
|
if (!(vfio_path = virMediatedDeviceGetIOMMUGroupDev(uuidstr)))
|
|
return -1;
|
|
|
|
group_num_str = g_path_get_basename(vfio_path);
|
|
ignore_value(virStrToLong_ui(group_num_str, NULL, 10, &group_num));
|
|
|
|
return group_num;
|
|
}
|
|
|
|
|
|
void
|
|
virMediatedDeviceGetUsedBy(virMediatedDevicePtr dev,
|
|
const char **drvname, const char **domname)
|
|
{
|
|
*drvname = dev->used_by_drvname;
|
|
*domname = dev->used_by_domname;
|
|
}
|
|
|
|
|
|
int
|
|
virMediatedDeviceSetUsedBy(virMediatedDevicePtr dev,
|
|
const char *drvname,
|
|
const char *domname)
|
|
{
|
|
VIR_FREE(dev->used_by_drvname);
|
|
VIR_FREE(dev->used_by_domname);
|
|
dev->used_by_drvname = g_strdup(drvname);
|
|
dev->used_by_domname = g_strdup(domname);
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
virMediatedDeviceListPtr
|
|
virMediatedDeviceListNew(void)
|
|
{
|
|
virMediatedDeviceListPtr list;
|
|
|
|
if (virMediatedInitialize() < 0)
|
|
return NULL;
|
|
|
|
if (!(list = virObjectLockableNew(virMediatedDeviceListClass)))
|
|
return NULL;
|
|
|
|
return list;
|
|
}
|
|
|
|
|
|
static void
|
|
virMediatedDeviceListDispose(void *obj)
|
|
{
|
|
virMediatedDeviceListPtr list = obj;
|
|
size_t i;
|
|
|
|
for (i = 0; i < list->count; i++) {
|
|
virMediatedDeviceFree(list->devs[i]);
|
|
list->devs[i] = NULL;
|
|
}
|
|
|
|
list->count = 0;
|
|
VIR_FREE(list->devs);
|
|
}
|
|
|
|
|
|
/* The reason for @dev to be double pointer is that VIR_APPEND_ELEMENT clears
|
|
* the pointer and we need to clear the original not a copy on the stack
|
|
*/
|
|
int
|
|
virMediatedDeviceListAdd(virMediatedDeviceListPtr list,
|
|
virMediatedDevicePtr *dev)
|
|
{
|
|
if (virMediatedDeviceListFind(list, (*dev)->path)) {
|
|
virReportError(VIR_ERR_INTERNAL_ERROR,
|
|
_("device %s is already in use"), (*dev)->path);
|
|
return -1;
|
|
}
|
|
return VIR_APPEND_ELEMENT(list->devs, list->count, *dev);
|
|
}
|
|
|
|
|
|
virMediatedDevicePtr
|
|
virMediatedDeviceListGet(virMediatedDeviceListPtr list,
|
|
ssize_t idx)
|
|
{
|
|
if (idx < 0 || idx >= list->count)
|
|
return NULL;
|
|
|
|
return list->devs[idx];
|
|
}
|
|
|
|
|
|
size_t
|
|
virMediatedDeviceListCount(virMediatedDeviceListPtr list)
|
|
{
|
|
return list->count;
|
|
}
|
|
|
|
|
|
virMediatedDevicePtr
|
|
virMediatedDeviceListStealIndex(virMediatedDeviceListPtr list,
|
|
ssize_t idx)
|
|
{
|
|
virMediatedDevicePtr ret;
|
|
|
|
if (idx < 0 || idx >= list->count)
|
|
return NULL;
|
|
|
|
ret = list->devs[idx];
|
|
VIR_DELETE_ELEMENT(list->devs, idx, list->count);
|
|
return ret;
|
|
}
|
|
|
|
|
|
virMediatedDevicePtr
|
|
virMediatedDeviceListSteal(virMediatedDeviceListPtr list,
|
|
virMediatedDevicePtr dev)
|
|
{
|
|
int idx = virMediatedDeviceListFindIndex(list, dev->path);
|
|
|
|
return virMediatedDeviceListStealIndex(list, idx);
|
|
}
|
|
|
|
|
|
void
|
|
virMediatedDeviceListDel(virMediatedDeviceListPtr list,
|
|
virMediatedDevicePtr dev)
|
|
{
|
|
virMediatedDeviceFree(virMediatedDeviceListSteal(list, dev));
|
|
}
|
|
|
|
|
|
int
|
|
virMediatedDeviceListFindIndex(virMediatedDeviceListPtr list,
|
|
const char *sysfspath)
|
|
{
|
|
size_t i;
|
|
|
|
for (i = 0; i < list->count; i++) {
|
|
virMediatedDevicePtr dev = list->devs[i];
|
|
if (STREQ(sysfspath, dev->path))
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
|
|
virMediatedDevicePtr
|
|
virMediatedDeviceListFind(virMediatedDeviceListPtr list,
|
|
const char *sysfspath)
|
|
{
|
|
int idx;
|
|
|
|
if ((idx = virMediatedDeviceListFindIndex(list, sysfspath)) >= 0)
|
|
return list->devs[idx];
|
|
else
|
|
return NULL;
|
|
}
|
|
|
|
|
|
bool
|
|
virMediatedDeviceIsUsed(virMediatedDevicePtr dev,
|
|
virMediatedDeviceListPtr list)
|
|
{
|
|
const char *drvname, *domname;
|
|
virMediatedDevicePtr tmp = NULL;
|
|
|
|
if ((tmp = virMediatedDeviceListFind(list, dev->path))) {
|
|
virMediatedDeviceGetUsedBy(tmp, &drvname, &domname);
|
|
virReportError(VIR_ERR_OPERATION_INVALID,
|
|
_("mediated device %s is in use by "
|
|
"driver %s, domain %s"),
|
|
tmp->path, drvname, domname);
|
|
}
|
|
|
|
return !!tmp;
|
|
}
|
|
|
|
|
|
char *
|
|
virMediatedDeviceGetSysfsPath(const char *uuidstr)
|
|
{
|
|
return g_strdup_printf(MDEV_SYSFS_DEVICES "%s", uuidstr);
|
|
}
|
|
|
|
|
|
int
|
|
virMediatedDeviceListMarkDevices(virMediatedDeviceListPtr dst,
|
|
virMediatedDeviceListPtr src,
|
|
const char *drvname,
|
|
const char *domname)
|
|
{
|
|
int ret = -1;
|
|
size_t count = virMediatedDeviceListCount(src);
|
|
size_t i, j;
|
|
|
|
virObjectLock(dst);
|
|
for (i = 0; i < count; i++) {
|
|
virMediatedDevicePtr mdev = virMediatedDeviceListGet(src, i);
|
|
|
|
if (virMediatedDeviceIsUsed(mdev, dst) ||
|
|
virMediatedDeviceSetUsedBy(mdev, drvname, domname) < 0)
|
|
goto rollback;
|
|
|
|
/* Copy mdev references to the driver list:
|
|
* - caller is responsible for NOT freeing devices in @src on success
|
|
* - we're responsible for performing a rollback on failure
|
|
*/
|
|
VIR_DEBUG("Add '%s' to list of active mediated devices used by '%s'",
|
|
mdev->path, domname);
|
|
if (virMediatedDeviceListAdd(dst, &mdev) < 0)
|
|
goto rollback;
|
|
|
|
}
|
|
|
|
ret = 0;
|
|
cleanup:
|
|
virObjectUnlock(dst);
|
|
return ret;
|
|
|
|
rollback:
|
|
for (j = 0; j < i; j++) {
|
|
virMediatedDevicePtr tmp = virMediatedDeviceListGet(src, j);
|
|
virMediatedDeviceListSteal(dst, tmp);
|
|
}
|
|
goto cleanup;
|
|
}
|
|
|
|
|
|
void
|
|
virMediatedDeviceTypeFree(virMediatedDeviceTypePtr type)
|
|
{
|
|
if (!type)
|
|
return;
|
|
|
|
VIR_FREE(type->id);
|
|
VIR_FREE(type->name);
|
|
VIR_FREE(type->device_api);
|
|
VIR_FREE(type);
|
|
}
|
|
|
|
|
|
int
|
|
virMediatedDeviceTypeReadAttrs(const char *sysfspath,
|
|
virMediatedDeviceTypePtr *type)
|
|
{
|
|
g_autoptr(virMediatedDeviceType) tmp = NULL;
|
|
|
|
#define MDEV_GET_SYSFS_ATTR(attr, dst, cb, optional) \
|
|
do { \
|
|
int rc; \
|
|
if ((rc = cb(dst, "%s/%s", sysfspath, attr)) < 0) { \
|
|
if (rc != -2 || !optional) \
|
|
return -1; \
|
|
} \
|
|
} while (0)
|
|
|
|
tmp = g_new0(virMediatedDeviceType, 1);
|
|
|
|
tmp->id = g_path_get_basename(sysfspath);
|
|
|
|
/* @name sysfs attribute is optional, so getting ENOENT is fine */
|
|
MDEV_GET_SYSFS_ATTR("name", &tmp->name, virFileReadValueString, true);
|
|
MDEV_GET_SYSFS_ATTR("device_api", &tmp->device_api,
|
|
virFileReadValueString, false);
|
|
MDEV_GET_SYSFS_ATTR("available_instances", &tmp->available_instances,
|
|
virFileReadValueUint, false);
|
|
|
|
#undef MDEV_GET_SYSFS_ATTR
|
|
|
|
*type = g_steal_pointer(&tmp);
|
|
|
|
return 0;
|
|
}
|
|
|
|
virMediatedDeviceAttrPtr virMediatedDeviceAttrNew(void)
|
|
{
|
|
return g_new0(virMediatedDeviceAttr, 1);
|
|
}
|
|
|
|
void virMediatedDeviceAttrFree(virMediatedDeviceAttrPtr attr)
|
|
{
|
|
g_free(attr->name);
|
|
g_free(attr->value);
|
|
g_free(attr);
|
|
}
|
|
|
|
|
|
#ifdef __linux__
|
|
|
|
ssize_t
|
|
virMediatedDeviceGetMdevTypes(const char *sysfspath,
|
|
virMediatedDeviceTypePtr **types,
|
|
size_t *ntypes)
|
|
{
|
|
ssize_t ret = -1;
|
|
int dirret = -1;
|
|
g_autoptr(DIR) dir = NULL;
|
|
struct dirent *entry;
|
|
g_autofree char *types_path = NULL;
|
|
g_autoptr(virMediatedDeviceType) mdev_type = NULL;
|
|
virMediatedDeviceTypePtr *mdev_types = NULL;
|
|
size_t nmdev_types = 0;
|
|
size_t i;
|
|
|
|
types_path = g_strdup_printf("%s/mdev_supported_types", sysfspath);
|
|
|
|
if ((dirret = virDirOpenIfExists(&dir, types_path)) < 0)
|
|
goto cleanup;
|
|
|
|
if (dirret == 0) {
|
|
ret = 0;
|
|
goto cleanup;
|
|
}
|
|
|
|
while ((dirret = virDirRead(dir, &entry, types_path)) > 0) {
|
|
g_autofree char *tmppath = NULL;
|
|
/* append the type id to the path and read the attributes from there */
|
|
tmppath = g_strdup_printf("%s/%s", types_path, entry->d_name);
|
|
|
|
if (virMediatedDeviceTypeReadAttrs(tmppath, &mdev_type) < 0)
|
|
goto cleanup;
|
|
|
|
if (VIR_APPEND_ELEMENT(mdev_types, nmdev_types, mdev_type) < 0)
|
|
goto cleanup;
|
|
}
|
|
|
|
if (dirret < 0)
|
|
goto cleanup;
|
|
|
|
*types = g_steal_pointer(&mdev_types);
|
|
*ntypes = nmdev_types;
|
|
nmdev_types = 0;
|
|
ret = 0;
|
|
cleanup:
|
|
for (i = 0; i < nmdev_types; i++)
|
|
virMediatedDeviceTypeFree(mdev_types[i]);
|
|
VIR_FREE(mdev_types);
|
|
return ret;
|
|
}
|
|
|
|
#else
|
|
static const char *unsupported = N_("not supported on non-linux platforms");
|
|
|
|
ssize_t
|
|
virMediatedDeviceGetMdevTypes(const char *sysfspath G_GNUC_UNUSED,
|
|
virMediatedDeviceTypePtr **types G_GNUC_UNUSED,
|
|
size_t *ntypes G_GNUC_UNUSED)
|
|
{
|
|
virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _(unsupported));
|
|
return -1;
|
|
}
|
|
|
|
#endif /* __linux__ */
|