This is the final patch to resolve:
https://bugzilla.redhat.com/show_bug.cgi?id=805071
This patch finalizes support live change of any/all hostside config of
a network device as part of the public virDomainUpdateDeviceFlags
API. The changes are done without detaching the device from the guest,
so hopefully disruptions are kept to a minimum.
Two new functions are created for the task -
qemuDomainDetachNetHostSide() and qemuDomainAttachHostSide() - which
detach and reattach just the host side of a network device, while
leaving the PCI device on the guest side still attached. Calling these
two functions in sequence allows us to completely change the host side
of the device, including type of tap device, where the tap is
connected, bandwidth/vlan/virtualport settings, etc.
These functions had their starts as cut-pastes of
qemuDomainDetachNetDevice and qemuDomainAttachNetDevice. To avoid the
headaches of duplicated code we may later want those older functions
to call the new functions, but that should wait until the new code
has had some real world testing - better to keep it in a lesser role
for now, with the existing code path unchanged.
For now, the two new functions are only used by qemuDomainChangeNet()
in circumstances that previously would have resulted in an UNSUPPORTED
error, but now will instead perform a device reconnect using the
modified netdev object.
One potentially ugly bit is that if the Attach of the new config
fails, we will have already detached the old config; in order to
recover as well as possible under the circumstances, we try to
re-attach the old config, which could fail for the same (or similar)
reason, leaving a discrepancy between the domain object and the actual
state of the domain. There really isn't any good way out of this
unfortunately.
---
src/qemu/qemu_hotplug.c | 338 +++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 332 insertions(+), 6 deletions(-)
diff --git a/src/qemu/qemu_hotplug.c b/src/qemu/qemu_hotplug.c
index 5284eb7..57a2392 100644
--- a/src/qemu/qemu_hotplug.c
+++ b/src/qemu/qemu_hotplug.c
@@ -33,6 +33,7 @@
#include "domain_audit.h"
#include "domain_nwfilter.h"
#include "logging.h"
+#include "datatypes.h"
#include "virterror_internal.h"
#include "memory.h"
#include "pci.h"
@@ -646,7 +647,6 @@ error:
return -1;
}
-
/* XXX conn required for network -> bridge resolution */
int qemuDomainAttachNetDevice(virConnectPtr conn,
struct qemud_driver *driver,
@@ -1249,6 +1249,304 @@ static virDomainNetDefPtr *qemuDomainFindNet(virDomainObjPtr vm,
return NULL;
}
+/* qemuDomainDetachNetHostSide:
+ *
+ * detach only the Host side of the netdev. Leave the Guest side
+ * attached. The intent is for this to *only* be called just prior to
+ * calling qemuDomainAttachNetHostSide() to reconnect to a different
+ * host side source (or with different settings).
+ *
+ * The device is *not* removed from the domain's list of nets, and
+ * the "actual device" is not released.
+ *
+ * Returns 0 on success, -1 on failure.
+ */
+static int
+qemuDomainDetachNetHostSide(struct qemud_driver *driver,
+ virDomainObjPtr vm,
+ virDomainNetDefPtr detach)
+{
+ int ret = -1;
+ qemuDomainObjPrivatePtr priv = vm->privateData;
+ int vlan;
+ char *hostnet_name = NULL;
+ virNetDevVPortProfilePtr vport = NULL;
+
+ if (virDomainNetGetActualType(detach) == VIR_DOMAIN_NET_TYPE_HOSTDEV) {
+ /* it's not possible to detach just the host side of a hostdev */
+ virReportError(VIR_ERR_NO_SUPPORT, "%s",
+ _("cannot detach only host side of a hostdev network
device"));
+ goto cleanup;
+ }
+
+ if ((vlan = qemuDomainNetVLAN(detach)) < 0) {
+ virReportError(VIR_ERR_OPERATION_FAILED,
+ "%s", _("unable to determine original
VLAN"));
+ goto cleanup;
+ }
+
+ if (virAsprintf(&hostnet_name, "host%s", detach->info.alias) < 0)
{
+ virReportOOMError();
+ goto cleanup;
+ }
+
+ qemuDomainObjEnterMonitorWithDriver(driver, vm);
+ if (qemuCapsGet(priv->caps, QEMU_CAPS_NETDEV) &&
+ qemuCapsGet(priv->caps, QEMU_CAPS_DEVICE)) {
+ ret = qemuMonitorRemoveNetdev(priv->mon, hostnet_name);
+ } else {
+ ret = qemuMonitorRemoveHostNetwork(priv->mon, vlan, hostnet_name);
+ }
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ virDomainAuditNet(vm, detach, NULL, "detach", ret == 0);
+ if (ret < 0)
+ goto cleanup;
+
+ virDomainConfNWFilterTeardown(detach);
+
+ if (virDomainNetGetActualType(detach) == VIR_DOMAIN_NET_TYPE_DIRECT) {
+ ignore_value(virNetDevMacVLanDeleteWithVPortProfile(
+ detach->ifname, &detach->mac,
+ virDomainNetGetActualDirectDev(detach),
+ virDomainNetGetActualDirectMode(detach),
+ virDomainNetGetActualVirtPortProfile(detach),
+ driver->stateDir));
+ }
+
+ if ((driver->macFilter) && (detach->ifname != NULL)) {
+ if ((errno = networkDisallowMacOnPort(driver,
+ detach->ifname,
+ &detach->mac))) {
+ virReportSystemError(errno,
+ _("failed to remove ebtables rule on '%s'"),
+ detach->ifname);
+ }
+ }
+
+ vport = virDomainNetGetActualVirtPortProfile(detach);
+ if (vport && vport->virtPortType == VIR_NETDEV_VPORT_PROFILE_OPENVSWITCH)
+ ignore_value(virNetDevOpenvswitchRemovePort(
+ virDomainNetGetActualBridgeName(detach),
+ detach->ifname));
+ ret = 0;
+cleanup:
+ if (!ret)
+ networkReleaseActualDevice(detach);
+ VIR_FREE(hostnet_name);
+ return ret;
+}
+
+/* qemuDomainAttachNetHostSide:
+ *
+ * attach only the Host side of the netdev. Assume that the Guest side
+ * is already attached. The intent is for this to *only* be called
+ * just after calling qemuDomainDetachNetHostSide() to reconnect to a
+ * different host side source (or with different settings).
+ *
+ * The device is assumed to be already in the domain's list of nets,
+ * and the "actualDevice" is already allocated. Also, it isn't removed
+ * from the domain nets list in case of failure.
+ *
+ * Returns 0 on success, -1 on failure.
+ *
+ * XXX: conn required for network -> bridge resolution
+*/
+static int
+qemuDomainAttachNetHostSide(virConnectPtr conn,
+ struct qemud_driver *driver,
+ virDomainObjPtr vm,
+ virDomainNetDefPtr net)
+{
+ qemuDomainObjPrivatePtr priv = vm->privateData;
+ char *tapfd_name = NULL;
+ int tapfd = -1;
+ char *vhostfd_name = NULL;
+ int vhostfd = -1;
+ char *netstr = NULL;
+ virNetDevVPortProfilePtr vport = NULL;
+ int ret = -1;
+ int vlan;
+ bool iface_connected = false;
+ int actualType;
+ char *netdev_name = NULL;
+
+ actualType = virDomainNetGetActualType(net);
+
+ if (actualType == VIR_DOMAIN_NET_TYPE_HOSTDEV) {
+ /* We can't attach just one side of one of these devices */
+ virReportError(VIR_ERR_NO_SUPPORT, "%s",
+ _("cannot attach only host side of a hostdev network
device"));
+ goto cleanup;
+ }
+
+ if (!qemuCapsGet(priv->caps, QEMU_CAPS_HOST_NET_ADD)) {
+ virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s",
+ _("installed qemu version does not support
host_net_add"));
+ goto cleanup;
+ }
+
+ if (actualType == VIR_DOMAIN_NET_TYPE_BRIDGE ||
+ actualType == VIR_DOMAIN_NET_TYPE_NETWORK) {
+ /*
+ * If type=bridge then we attempt to allocate the tap fd here
+ * only if running as root or if "-netdev bridge" (using the
+ * qemu setuid network helper) is not supported.
+ */
+ if (actualType == VIR_DOMAIN_NET_TYPE_NETWORK ||
+ driver->privileged ||
+ (!qemuCapsGet (priv->caps, QEMU_CAPS_NETDEV_BRIDGE))) {
+ if ((tapfd = qemuNetworkIfaceConnect(vm->def, conn, driver,
+ net, priv->caps)) < 0) {
+ goto cleanup;
+ }
+ iface_connected = true;
+ if (qemuOpenVhostNet(vm->def, net, priv->caps, &vhostfd) < 0)
+ goto cleanup;
+ }
+ } else if (actualType == VIR_DOMAIN_NET_TYPE_DIRECT) {
+ if ((tapfd = qemuPhysIfaceConnect(vm->def, driver, net, priv->caps,
+ VIR_NETDEV_VPORT_PROFILE_OP_CREATE)) < 0)
+ goto cleanup;
+ iface_connected = true;
+ if (qemuOpenVhostNet(vm->def, net, priv->caps, &vhostfd) < 0)
+ goto cleanup;
+ }
+
+ if (qemuCapsGet(priv->caps, QEMU_CAPS_NETDEV) &&
+ qemuCapsGet(priv->caps, QEMU_CAPS_DEVICE)) {
+ vlan = -1;
+ } else {
+ vlan = qemuDomainNetVLAN(net);
+ if (vlan < 0) {
+ virReportError(VIR_ERR_CONFIG_UNSUPPORTED, "%s",
+ _("Unable to attach network devices without
vlan"));
+ goto cleanup;
+ }
+ }
+
+ if (tapfd != -1) {
+ if (virAsprintf(&tapfd_name, "fd-%s", net->info.alias) < 0)
+ goto no_memory;
+ }
+
+ if (vhostfd != -1) {
+ if (virAsprintf(&vhostfd_name, "vhostfd-%s", net->info.alias)
< 0)
+ goto no_memory;
+ }
+
+ if (qemuCapsGet(priv->caps, QEMU_CAPS_NETDEV) &&
+ qemuCapsGet(priv->caps, QEMU_CAPS_DEVICE)) {
+ if (!(netstr = qemuBuildHostNetStr(net, driver, priv->caps,
+ ',', -1, tapfd_name,
+ vhostfd_name)))
+ goto cleanup;
+ } else {
+ if (!(netstr = qemuBuildHostNetStr(net, driver, priv->caps,
+ ' ', vlan, tapfd_name,
+ vhostfd_name)))
+ goto cleanup;
+ }
+
+ qemuDomainObjEnterMonitorWithDriver(driver, vm);
+ if (qemuCapsGet(priv->caps, QEMU_CAPS_NETDEV) &&
+ qemuCapsGet(priv->caps, QEMU_CAPS_DEVICE)) {
+ if (qemuMonitorAddNetdev(priv->mon, netstr, tapfd, tapfd_name,
+ vhostfd, vhostfd_name) < 0) {
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ virDomainAuditNet(vm, NULL, net, "attach", false);
+ goto cleanup;
+ }
+ } else {
+ if (qemuMonitorAddHostNetwork(priv->mon, netstr, tapfd, tapfd_name,
+ vhostfd, vhostfd_name) < 0) {
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ virDomainAuditNet(vm, NULL, net, "attach", false);
+ goto cleanup;
+ }
+ }
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+
+ VIR_FORCE_CLOSE(tapfd);
+ VIR_FORCE_CLOSE(vhostfd);
+
+ if (!virDomainObjIsActive(vm)) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("guest unexpectedly quit"));
+ goto cleanup;
+ }
+
+ /* set link state */
+ if (net->linkstate == VIR_DOMAIN_NET_INTERFACE_LINK_STATE_DOWN) {
+ if (!net->info.alias) {
+ virReportError(VIR_ERR_OPERATION_FAILED, "%s",
+ _("device alias not found: cannot set link state to
down"));
+ } else {
+ qemuDomainObjEnterMonitorWithDriver(driver, vm);
+
+ if (qemuCapsGet(priv->caps, QEMU_CAPS_NETDEV)) {
+ if (qemuMonitorSetLink(priv->mon, net->info.alias,
+ VIR_DOMAIN_NET_INTERFACE_LINK_STATE_DOWN) < 0)
{
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ virDomainAuditNet(vm, NULL, net, "attach", false);
+ goto try_remove;
+ }
+ } else {
+ virReportError(VIR_ERR_OPERATION_FAILED, "%s",
+ _("setting of link state not supported: Link is
up"));
+ }
+
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ }
+ /* link set to down */
+ }
+
+ virDomainAuditNet(vm, NULL, net, "attach", true);
+
+ ret = 0;
+cleanup:
+ if (ret < 0 && iface_connected) {
+ virDomainConfNWFilterTeardown(net);
+
+ vport = virDomainNetGetActualVirtPortProfile(net);
+ if (vport && vport->virtPortType ==
VIR_NETDEV_VPORT_PROFILE_OPENVSWITCH)
+ ignore_value(virNetDevOpenvswitchRemovePort(
+ virDomainNetGetActualBridgeName(net), net->ifname));
+ }
+
+ VIR_FREE(netstr);
+ VIR_FREE(tapfd_name);
+ VIR_FORCE_CLOSE(tapfd);
+ VIR_FREE(vhostfd_name);
+ VIR_FORCE_CLOSE(vhostfd);
+ VIR_FREE(netdev_name);
+
+ return ret;
+
+try_remove:
+ if (!virDomainObjIsActive(vm))
+ goto cleanup;
+
+ if (virAsprintf(&netdev_name, "host%s", net->info.alias) < 0)
+ goto no_memory;
+
+ qemuDomainObjEnterMonitorWithDriver(driver, vm);
+ if (vlan < 0) {
+ if (qemuMonitorRemoveNetdev(priv->mon, netdev_name) < 0)
+ VIR_WARN("Failed to remove network backend for netdev %s",
+ netdev_name);
+ } else {
+ if (qemuMonitorRemoveHostNetwork(priv->mon, vlan, netdev_name) < 0)
+ VIR_WARN("Failed to remove network backend for vlan %d, net %s",
+ vlan, netdev_name);
+ }
+ qemuDomainObjExitMonitorWithDriver(driver, vm);
+ goto cleanup;
+
+no_memory:
+ virReportOOMError();
+ goto cleanup;
+}
+
static
int qemuDomainChangeNetBridge(virDomainObjPtr vm,
virDomainNetDefPtr olddev,
@@ -1330,7 +1628,7 @@ cleanup:
int
qemuDomainChangeNet(struct qemud_driver *driver,
virDomainObjPtr vm,
- virDomainPtr dom ATTRIBUTE_UNUSED,
+ virDomainPtr dom,
virDomainDeviceDefPtr dev)
{
virDomainNetDefPtr newdev =
dev->data.net;
@@ -1584,10 +1882,38 @@ qemuDomainChangeNet(struct qemud_driver *driver,
/* FINALLY - actual perform the required actions */
if (needReconnect) {
- virReportError(VIR_ERR_NO_SUPPORT,
- _("unable to change config on '%s' network
type"),
- virDomainNetTypeToString(newdev->type));
- goto cleanup;
+ /* disconnect using the old config */
+ if (qemuDomainDetachNetHostSide(driver, vm, olddev) < 0)
+ goto cleanup;
+
+ /* reconnect using the new config (note that the new
+ * actualDevice was already allocated above)
+ */
+ if (qemuDomainAttachNetHostSide(dom->conn, driver, vm, newdev) < 0) {
+ virErrorPtr err;
+
+ /* try to reconnect olddev */
+ err = virSaveLastError();
+ ignore_value(qemuDomainAttachNetHostSide(dom->conn, driver, vm, newdev));
+ virSetError(err);
+ virFreeError(err);
+ goto cleanup;
+ }
+
+ /* we're successfully attached using the new config, so put it
+ * in place in the domain nets list, and discard the old config
+ */
+ networkReleaseActualDevice(olddev);
+ virDomainNetDefFree(olddev);
+ /* move newdev into the nets list, and NULL it out from the
+ * virDomainDeviceDef that we were given so that the caller
+ * won't delete it on return. */
+ *olddevslot = newdev;
+ newdev =
dev->data.net = NULL;
+ dev->type = VIR_DOMAIN_DEVICE_NONE;
+
+ needChangeBridge = false;
+ needLinkStateChange = false;
}
if (needChangeBridge && qemuDomainChangeNetBridge(vm, olddev, newdev) <
0)
--
1.7.11.4