[PATCH 4/7] network: turn on autoaddr selection in bridge driver

Laine Stump posted 7 patches 3 months, 2 weeks ago
[PATCH 4/7] network: turn on autoaddr selection in bridge driver
Posted by Laine Stump 3 months, 2 weeks ago
When a network has an <ip autoaddr='yes'/> element, the network driver
will look for an "unused" subnet to assign to the network. The IP
address of the network bridge will then be the .1 of that network, and
any DHCP range, DHCP static hosts, and tftp boot server will be
changed to the new network (while retaining the same host bits).

* the range of subnets (and their prefix) have a hostwide
  configuration in /etc/libvirt/network.conf ("autoaddr_*").

* if the network config has a <dhcp> element with no <range>
  specified, then a range will automatically be added that starts at
  ${net}.2 and ends at ${net}.254 (adjusted for prefix - this is
  example is only if the prefix is 24).

* If a network has autoaddr="yes" and also has an IP address defined,
  then the network driver will check that IP address first, and then
  start looking for a new address if that initial try fails.

* If a new subnet is selected, this change of IP address will be saved
  to the network's persistent config (along with associated changes to
  other elements that need to be on the same subnet). This way the next
  time the network is started, it will attempt to use the same subnet
  that it had used the previous time.

Signed-off-by: Laine Stump <laine@redhat.com>
---
 src/network/bridge_driver.c | 137 ++++++++++++++++++++++++++++++++++--
 1 file changed, 133 insertions(+), 4 deletions(-)

diff --git a/src/network/bridge_driver.c b/src/network/bridge_driver.c
index b8e752f20d..cceeb5d941 100644
--- a/src/network/bridge_driver.c
+++ b/src/network/bridge_driver.c
@@ -1901,19 +1901,78 @@ networkAddRouteToBridge(virNetworkObj *obj,
 /* XXX: This function can be a lot more exhaustive, there are certainly
  *      other scenarios where we can ruin host network connectivity.
  * XXX: Using a proper library is preferred over parsing /proc
+ *
+ * check for any host route colliding exactly with each requested IPv4
+ * address/prefix, If a collision is found and autoaddr isn't set for
+ * the network, immediately return an error.
+
+ * if autoaddr *is* set, iteratively try subnets using the auto_addr*
+ * info from the system config (in /etc/libvirt/network.conf) until an
+ * "unused" subnet is found, then use that subnet, and rewrite the
+ * persistent network config so that next time we'll start trying
+ * using the same address we got this time.
+ *
+ * If all subnets in the configured autoaddr range are already in use,
+ * then return an error.
  */
 static int
-networkCheckIPAddrCollision(virNetworkDef *def, GHashTable *routes)
+networkCheckIPAddrCollision(virNetworkDriverState *driver,
+                            virNetworkObj *obj,
+                            GHashTable *routes)
 {
+    g_autoptr(virNetworkDriverConfig) cfg = virNetworkDriverGetConfig(driver);
+    uint32_t nextNet = ntohl(cfg->autoaddrStart.data.inet4.sin_addr.s_addr); /* next subnet to check */
+    uint32_t lastNet = ntohl(cfg->autoaddrEnd.data.inet4.sin_addr.s_addr);
+    virNetworkDef *def = virNetworkObjGetDef(obj);
     size_t i;
     virNetworkIPDef *ipdef;
 
     /* check all IPv4 addresses of the network for conflict with
      * existing routes
      */
-    for (i = 0; (ipdef = virNetworkDefGetIPByIndex(def, AF_INET, i)); i++) {
+    for (i = 0; (ipdef = virNetworkDefGetIPByIndex(def, AF_UNSPEC, i)); i++) {
+        virSocketAddr ip = ipdef->address; /* don't disturb the original (yet) */
         unsigned int prefix = virNetworkIPDefPrefix(ipdef);
-        const char *iface = networkSysRoutesTableFind(routes, &ipdef->address, prefix);
+        const char *iface;
+        bool done = false;
+        uint32_t updateNet = 0; /* new subnet in *host* byte order (or 0 if no change) */
+
+        if (VIR_SOCKET_ADDR_IS_FAMILY(&ip, AF_INET6))
+            continue; /* autoaddr isn't used on IPv6 addresses */
+
+        do {
+            if (VIR_SOCKET_ADDR_IS_FAMILY(&ip, AF_INET))
+                iface = networkSysRoutesTableFind(routes, &ip, prefix);
+            else
+                iface = "unset"; /* could be anything !NULL, just need to take upcoming else clause */
+
+            if (!iface || !ipdef->autoaddr) {
+
+                done = true;
+
+            } else {
+                g_autofree char *addrStr = NULL;
+
+                if (nextNet > lastNet) {
+                    /* we've tried all of the subnets and they're all used, so give up */
+                    virReportError(VIR_ERR_INTERNAL_ERROR,
+                                   _("could not find an unused subnet for autoaddr network '%1$s'"),
+                                   def->name);
+                    return -1;
+                }
+
+                /* this is what we'll try next time through the loop */
+                updateNet = nextNet;
+                virSocketAddrSetIPv4Addr(&ip, updateNet + 1); /* + 1 for host address */
+                prefix = cfg->autoaddrPrefix;
+
+                addrStr = virSocketAddrFormat(&ip);
+                VIR_INFO("autoaddr trying %s/%u", NULLSTR(addrStr), prefix);
+
+                /* get ready for the next "new" request (the loop *after* the next) */
+                nextNet = ((updateNet >> (32 - prefix)) + 1) << (32 - prefix);
+            }
+        } while (!done);
 
         if (iface) {
             g_autofree char *addrStr = virSocketAddrFormat(&ipdef->address);
@@ -1923,6 +1982,76 @@ networkCheckIPAddrCollision(virNetworkDef *def, GHashTable *routes)
                            NULLSTR(addrStr), prefix, iface);
             return -1;
         }
+
+        if (updateNet) {
+            /* address was changed via autoaddr */
+            g_autofree char *addrStr = virSocketAddrFormat(&ip);
+
+            VIR_INFO("autoaddr success: %s/%u", NULLSTR(addrStr), prefix);
+
+            /* put the newly found unused IP/prefix in the NetworkDef */
+            ipdef->address = ip;
+            ipdef->prefix = prefix;
+            memset(&ipdef->netmask, 0, sizeof(ipdef->netmask)); /*in case original had netmask */
+
+            /* if there is a <dhcp> element also add/update the DHCP
+             * range, tftp bootserver, and, static hosts (they will
+             * all have the network part of their address changed, but
+             * the host bits will remain the same.
+             */
+
+            if (ipdef->dhcp) {
+                size_t j;
+                uint32_t tmp;
+                /* in *host* order initially, since we need to use if for some arithmetic */
+                uint32_t hostmask = ~(0xffffffff << (32 - prefix));
+
+
+                if (!ipdef->nranges) {
+                    /* if there isn't any address range in the config,
+                     * we need to setup at least one
+                     */
+                    virNetworkDHCPRangeDef range = { 0 };
+
+                    /* just prime them with the host bits for .2 and
+                     * .254 (adjusted for prefix)
+                     */
+                    virSocketAddrSetIPv4Addr(&range.addr.start, updateNet + 2);
+                    virSocketAddrSetIPv4Addr(&range.addr.end, updateNet + hostmask - 1);
+                    VIR_APPEND_ELEMENT(ipdef->ranges, ipdef->nranges, range);
+                }
+
+                updateNet = htonl(updateNet);
+                hostmask = htonl(hostmask);
+
+                for (j = 0; j < ipdef->nranges; j++) {
+                    tmp = (ipdef->ranges[j].addr.start.data.inet4.sin_addr.s_addr & hostmask) | updateNet;
+                    virSocketAddrSetIPv4AddrNetOrder(&ipdef->ranges[j].addr.start, tmp);
+
+                    tmp = (ipdef->ranges[j].addr.end.data.inet4.sin_addr.s_addr & hostmask) | updateNet;
+                    virSocketAddrSetIPv4AddrNetOrder(&ipdef->ranges[j].addr.end, tmp);
+                }
+
+                for (j = 0; j < ipdef->nhosts; j++) {
+                    tmp = (ipdef->hosts[j].ip.data.inet4.sin_addr.s_addr & hostmask) | updateNet;
+                    virSocketAddrSetIPv4AddrNetOrder(&ipdef->hosts[j].ip, tmp);
+                }
+
+                if (VIR_SOCKET_ADDR_IS_FAMILY(&ipdef->bootserver, AF_INET)) {
+                    tmp = (ipdef->bootserver.data.inet4.sin_addr.s_addr & hostmask) | updateNet;
+                    virSocketAddrSetIPv4AddrNetOrder(&ipdef->bootserver, tmp);
+                }
+            }
+
+            /* if this network is persistent also update on disk (so
+             * that the discovered address is tried first the next
+             * time this network is started)
+             */
+            if (virNetworkObjIsPersistent(obj)
+                && virNetworkSaveConfig(cfg->networkConfigDir, def, driver->xmlopt) < 0) {
+                VIR_WARN("couldn't update autoaddr in network %1$s", def->name);
+            }
+        }
     }
 
     return 0;
@@ -1977,7 +2106,7 @@ networkStartNetworkVirtual(virNetworkDriverState *driver,
 
     if (routes) {
         /* Check to see if any network IP collides with an existing route on the host */
-        if (networkCheckIPAddrCollision(def, routes) < 0)
+        if (networkCheckIPAddrCollision(driver, obj, routes) < 0)
             return -1;
 
         /* also check for requested routes that collide with existing routes */
-- 
2.45.2