From 9e0360212e348d8a8b35ec27e18d5f2ea69ccf7c Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 11 Jan 2023 14:14:28 +0100 Subject: [PATCH 01/21] init/configureNetworking: improve readability Signed-off-by: Yann Dirson --- init | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/init b/init index b995935b..465ac7c8 100755 --- a/init +++ b/init @@ -51,15 +51,15 @@ def configureNetworking(ui, device, config): nethw = netutil.scanConfiguration() netcfg = {} - for i in nethw: - if (device == i or device == nethw[i].hwaddr) and mode == 'static': - netcfg[i] = NetInterface(NetInterface.Static, nethw[i].hwaddr, - config_dict['ip'], config_dict['netmask'], - config_dict['gateway'], config_dict['dns'], - config_dict['domain'], config_dict['vlan']) + for devname, nic in nethw.items(): + if (device == devname or device == nic.hwaddr) and mode == 'static': + netcfg[devname] = NetInterface(NetInterface.Static, nic.hwaddr, + config_dict['ip'], config_dict['netmask'], + config_dict['gateway'], config_dict['dns'], + config_dict['domain'], config_dict['vlan']) else: - netcfg[i] = NetInterface(NetInterface.DHCP, nethw[i].hwaddr, - vlan=config_dict['vlan']) + netcfg[devname] = NetInterface(NetInterface.DHCP, nic.hwaddr, + vlan=config_dict['vlan']) netutil.writeNetInterfaceFiles(netcfg) netutil.writeResolverFile(netcfg, '/etc/resolv.conf') From af7aa43e9645e5ea43b07d95831cad2fe45d703c Mon Sep 17 00:00:00 2001 From: BenjiReis Date: Mon, 9 Jan 2023 11:41:58 +0100 Subject: [PATCH 02/21] writeRHStyleInterface: split writeIface() This will allow for more concise logic. --- netinterface.py | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/netinterface.py b/netinterface.py index 0cdcad47..8099d449 100644 --- a/netinterface.py +++ b/netinterface.py @@ -164,30 +164,31 @@ def writeRHStyleInterface(self, iface): """ Write a RedHat-style configuration entry for this interface to file object f using interface name iface. """ + def writeIface(iface_name): + f = open('/etc/sysconfig/network-scripts/ifcfg-%s' % iface_name, 'w') + f.write("DEVICE=%s\n" % iface_name) + f.write("ONBOOT=yes\n") + if self.mode == self.DHCP: + f.write("BOOTPROTO=dhcp\n") + f.write("PERSISTENT_DHCLIENT=1\n") + else: + # CA-11825: broadcast needs to be determined for non-standard networks + bcast = self.getBroadcast() + f.write("BOOTPROTO=none\n") + f.write("IPADDR=%s\n" % self.ipaddr) + if bcast is not None: + f.write("BROADCAST=%s\n" % bcast) + f.write("NETMASK=%s\n" % self.netmask) + if self.gateway: + f.write("GATEWAY=%s\n" % self.gateway) + if self.vlan: + f.write("VLAN=yes\n") + f.close() + assert self.modev6 is None assert self.mode iface_vlan = self.getInterfaceName(iface) - - f = open('/etc/sysconfig/network-scripts/ifcfg-%s' % iface_vlan, 'w') - f.write("DEVICE=%s\n" % iface_vlan) - f.write("ONBOOT=yes\n") - if self.mode == self.DHCP: - f.write("BOOTPROTO=dhcp\n") - f.write("PERSISTENT_DHCLIENT=1\n") - else: - # CA-11825: broadcast needs to be determined for non-standard networks - bcast = self.getBroadcast() - f.write("BOOTPROTO=none\n") - f.write("IPADDR=%s\n" % self.ipaddr) - if bcast is not None: - f.write("BROADCAST=%s\n" % bcast) - f.write("NETMASK=%s\n" % self.netmask) - if self.gateway: - f.write("GATEWAY=%s\n" % self.gateway) - if self.vlan: - f.write("VLAN=yes\n") - f.close() - + writeIface(iface_vlan) def waitUntilUp(self, iface): if not self.isStatic(): From 0e42c102f1d2c2f1e964d78190a1787370981e81 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 17 Jan 2023 15:05:08 +0100 Subject: [PATCH 03/21] writeIface: use "with" statement Always safer, and will facilitate further splitting --- netinterface.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/netinterface.py b/netinterface.py index 8099d449..6f247fdb 100644 --- a/netinterface.py +++ b/netinterface.py @@ -165,25 +165,24 @@ def writeRHStyleInterface(self, iface): file object f using interface name iface. """ def writeIface(iface_name): - f = open('/etc/sysconfig/network-scripts/ifcfg-%s' % iface_name, 'w') - f.write("DEVICE=%s\n" % iface_name) - f.write("ONBOOT=yes\n") - if self.mode == self.DHCP: - f.write("BOOTPROTO=dhcp\n") - f.write("PERSISTENT_DHCLIENT=1\n") - else: - # CA-11825: broadcast needs to be determined for non-standard networks - bcast = self.getBroadcast() - f.write("BOOTPROTO=none\n") - f.write("IPADDR=%s\n" % self.ipaddr) - if bcast is not None: - f.write("BROADCAST=%s\n" % bcast) - f.write("NETMASK=%s\n" % self.netmask) - if self.gateway: - f.write("GATEWAY=%s\n" % self.gateway) - if self.vlan: - f.write("VLAN=yes\n") - f.close() + with open('/etc/sysconfig/network-scripts/ifcfg-%s' % iface_name, 'w') as f: + f.write("DEVICE=%s\n" % iface_name) + f.write("ONBOOT=yes\n") + if self.mode == self.DHCP: + f.write("BOOTPROTO=dhcp\n") + f.write("PERSISTENT_DHCLIENT=1\n") + else: + # CA-11825: broadcast needs to be determined for non-standard networks + bcast = self.getBroadcast() + f.write("BOOTPROTO=none\n") + f.write("IPADDR=%s\n" % self.ipaddr) + if bcast is not None: + f.write("BROADCAST=%s\n" % bcast) + f.write("NETMASK=%s\n" % self.netmask) + if self.gateway: + f.write("GATEWAY=%s\n" % self.gateway) + if self.vlan: + f.write("VLAN=yes\n") assert self.modev6 is None assert self.mode From b893a8c934798c073839f16d90c8737c21903b52 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 17 Jan 2023 15:06:45 +0100 Subject: [PATCH 04/21] writeIface: split writing IP configuration for reuse Signed-off-by: Yann Dirson --- netinterface.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/netinterface.py b/netinterface.py index 6f247fdb..05f461e0 100644 --- a/netinterface.py +++ b/netinterface.py @@ -168,6 +168,11 @@ def writeIface(iface_name): with open('/etc/sysconfig/network-scripts/ifcfg-%s' % iface_name, 'w') as f: f.write("DEVICE=%s\n" % iface_name) f.write("ONBOOT=yes\n") + writeIpConfig(f) + if self.vlan: + f.write("VLAN=yes\n") + + def writeIpConfig(f): if self.mode == self.DHCP: f.write("BOOTPROTO=dhcp\n") f.write("PERSISTENT_DHCLIENT=1\n") @@ -181,8 +186,6 @@ def writeIface(iface_name): f.write("NETMASK=%s\n" % self.netmask) if self.gateway: f.write("GATEWAY=%s\n" % self.gateway) - if self.vlan: - f.write("VLAN=yes\n") assert self.modev6 is None assert self.mode From 474433853e715b51e48e244862ed43abeb5eaa3a Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 17 Jan 2023 15:17:08 +0100 Subject: [PATCH 05/21] fixup! writeIface: split writing IP configuration for reuse --- netinterface.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/netinterface.py b/netinterface.py index 05f461e0..66293dcb 100644 --- a/netinterface.py +++ b/netinterface.py @@ -173,19 +173,19 @@ def writeIface(iface_name): f.write("VLAN=yes\n") def writeIpConfig(f): - if self.mode == self.DHCP: - f.write("BOOTPROTO=dhcp\n") - f.write("PERSISTENT_DHCLIENT=1\n") - else: - # CA-11825: broadcast needs to be determined for non-standard networks - bcast = self.getBroadcast() - f.write("BOOTPROTO=none\n") - f.write("IPADDR=%s\n" % self.ipaddr) - if bcast is not None: - f.write("BROADCAST=%s\n" % bcast) - f.write("NETMASK=%s\n" % self.netmask) - if self.gateway: - f.write("GATEWAY=%s\n" % self.gateway) + if self.mode == self.DHCP: + f.write("BOOTPROTO=dhcp\n") + f.write("PERSISTENT_DHCLIENT=1\n") + else: + # CA-11825: broadcast needs to be determined for non-standard networks + bcast = self.getBroadcast() + f.write("BOOTPROTO=none\n") + f.write("IPADDR=%s\n" % self.ipaddr) + if bcast is not None: + f.write("BROADCAST=%s\n" % bcast) + f.write("NETMASK=%s\n" % self.netmask) + if self.gateway: + f.write("GATEWAY=%s\n" % self.gateway) assert self.modev6 is None assert self.mode From e78b633211d3090b312864f166827dd30377bb1f Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 12 Jan 2023 10:40:10 +0100 Subject: [PATCH 06/21] ifup: remove extraneous assert This check was already not very useful, as if an eth device is not in getNetifList(), the ifup command will just fail properly. But in the case of a bond interface, the bond device will not even exist before running ifup. Signed-off-by: Yann Dirson --- netutil.py | 1 - 1 file changed, 1 deletion(-) diff --git a/netutil.py b/netutil.py index 898fe0ea..00f0cb0c 100644 --- a/netutil.py +++ b/netutil.py @@ -111,7 +111,6 @@ def splitInterfaceVlan(interface): def ifup(interface): device, vlan = splitInterfaceVlan(interface) - assert device in getNetifList() interface_up[interface] = True return util.runCmd2(['ifup', interface]) From d0248651bdc3e7780184f5c2983f894b6d20af16 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 12 Jan 2023 11:25:03 +0100 Subject: [PATCH 07/21] getNetifList: remove eth hardcoding in interface sorting This is a prerequisite to allowing more than just eth* Signed-off-by: Yann Dirson --- netutil.py | 10 +++++----- tui/fcoe.py | 2 +- tui/network.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/netutil.py b/netutil.py index 00f0cb0c..c0c50322 100644 --- a/netutil.py +++ b/netutil.py @@ -47,6 +47,10 @@ def scanConfiguration(): return conf +def netifSortKey(interface): + m = re.match(r"([a-z]+)(\d+)((?:\..+)?)", interface) + return m.groups() + def getNetifList(include_vlan=False): all = os.listdir("/sys/class/net") @@ -54,12 +58,8 @@ def ethfilter(interface, include_vlan): return interface.startswith("eth") and (interface.isalnum() or (include_vlan and "." in interface)) - def rankValue(ethx): - iface, vlan = splitInterfaceVlan(ethx) - return (int(iface.strip('eth'))*10000 + (int(vlan) if vlan else -1)) - relevant = filter(lambda x: ethfilter(x, include_vlan), all) - relevant.sort(lambda l, r: rankValue(l) - rankValue(r)) + relevant.sort(key=netifSortKey) return relevant # writes an 'interfaces' style file given a network configuration object list diff --git a/tui/fcoe.py b/tui/fcoe.py index 5957ec85..f619ea99 100644 --- a/tui/fcoe.py +++ b/tui/fcoe.py @@ -34,7 +34,7 @@ def select_fcoe_ifaces(answers): return - netifs.sort(lambda l, r: int(l[3:]) - int(r[3:])) + netifs.sort(key=netutil.netifSortKey) def iface_details(context): tui.update_help_line([' ', ' ']) diff --git a/tui/network.py b/tui/network.py index faf87dc9..00ebebe3 100644 --- a/tui/network.py +++ b/tui/network.py @@ -140,7 +140,7 @@ def select_netif(text, conf, offer_existing=False, default=None): """ netifs = conf.keys() - netifs.sort(lambda l, r: int(l[3:]) - int(r[3:])) + netifs.sort(key=netutil.netifSortKey) if default not in netifs: # find first link that is up From 2b118bca3da6b23fc0dec9ea545fa23debad425f Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 12 Jan 2023 17:52:52 +0100 Subject: [PATCH 08/21] netutil.scanConfiguration: rework for extensibility Less complexity for the nominal case, and makes it easier to add different non-nominal cases. Signed-off-by: Yann Dirson --- netutil.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/netutil.py b/netutil.py index c0c50322..e5332326 100644 --- a/netutil.py +++ b/netutil.py @@ -42,8 +42,10 @@ def scanConfiguration(): for nic in all_devices_all_names().values(): name = nic.get("Kernel name", "") - if name in nics: - conf[name] = NIC(nic) + if name not in nics: + logger.log("scanConfiguration: {} not in nics".format(name)) + continue + conf[name] = NIC(nic) return conf From c182e60a09af64f5e12f4c4a6f983f2daee5e3e4 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 11 Jan 2023 14:13:13 +0100 Subject: [PATCH 09/21] doc: make it clear where we expect URLs Signed-off-by: Yann Dirson --- doc/parameters.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/parameters.txt b/doc/parameters.txt index ccd14470..3eea13e9 100644 --- a/doc/parameters.txt +++ b/doc/parameters.txt @@ -122,19 +122,19 @@ Installer Proceed with installation/upgrade. - --answerfile=ans + --answerfile=url Read answerfile and perform a non-interactive installation reporting status using screens. - --rt_answerfile=ans + --rt_answerfile=url Read answerfile and perform a non-interactive installation reporting status to the console as text. - --answerfile_generator=script + --answerfile_generator=url Retrieve script, run it and use the output of it as an answerfile. From d46565e5dc34129d6692487b96fdfc2b9f0bc624 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 11 Jan 2023 14:13:54 +0100 Subject: [PATCH 10/21] doc: fix description of --network_device default Signed-off-by: Yann Dirson --- doc/parameters.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/parameters.txt b/doc/parameters.txt index 3eea13e9..c0818e12 100644 --- a/doc/parameters.txt +++ b/doc/parameters.txt @@ -144,7 +144,8 @@ Installer Bring up networking on the given interface to allow access to answerfiles. - Default: all + Default: "all" if a non-local URL is specified for any of the above + answerfile parameters, else no device is started --map_netdev=eth:d|s:mac|pci[[index]]|ppn From 4fb8af857a1dc13f2c119025a26a9b17fb98e208 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Wed, 11 Jan 2023 15:30:25 +0100 Subject: [PATCH 11/21] doc: make --network_device no less visible than its deprecated counterpart Don't repeat all parameters on the deprecated version, as we're going to add more options. Signed-off-by: Yann Dirson --- doc/parameters.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/parameters.txt b/doc/parameters.txt index c0818e12..79c4fb8c 100644 --- a/doc/parameters.txt +++ b/doc/parameters.txt @@ -139,7 +139,8 @@ Installer Retrieve script, run it and use the output of it as an answerfile. - --answerfile_device[D]=eth|mac|all | --network_device=eth|mac|all + --network_device=eth|mac|all + --answerfile_device[D]=... Bring up networking on the given interface to allow access to answerfiles. @@ -147,6 +148,9 @@ Installer Default: "all" if a non-local URL is specified for any of the above answerfile parameters, else no device is started + --answerfile_device is a deprecated alias, and accepts the same + parameters as --network_device. + --map_netdev=eth:d|s:mac|pci[[index]]|ppn From c4ee1316a8ba34f4db6aa0d245c84b8da94a00bb Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 17 Jan 2023 15:10:27 +0100 Subject: [PATCH 12/21] WIP NIC,NetInterface: add support for bonding interfaces FIXME: - maybe support DebStyleInterface ? how would we test ? Note: addition of more args seems to call for passing a NIC to NetInterface instead, which in turn suggests to include a NIC instead of storing individual fields - but NIC is really too tied to biosdevname output, which bled into xcp.net.biosdevname API. --- netinterface.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++++- netutil.py | 1 + tui/network.py | 4 ++-- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/netinterface.py b/netinterface.py index 66293dcb..18973b12 100644 --- a/netinterface.py +++ b/netinterface.py @@ -24,7 +24,7 @@ class NetInterface: Autoconf = 3 def __init__(self, mode, hwaddr, ipaddr=None, netmask=None, gateway=None, - dns=None, domain=None, vlan=None): + dns=None, domain=None, vlan=None, bond_mode=None, bond_members=None): assert mode is None or mode == self.Static or mode == self.DHCP if ipaddr == '': ipaddr = None @@ -61,6 +61,13 @@ def __init__(self, mode, hwaddr, ipaddr=None, netmask=None, gateway=None, self.ipv6addr = None self.ipv6_gateway = None + self.bond_mode = bond_mode + if bond_mode is not None: + # Not `balance-slb` because it's openvswitch specific + assert bond_mode in ["lacp", "active-backup"] + assert bond_members is not None + self.bond_members = bond_members + def __repr__(self): hw = "hwaddr = '%s' " % self.hwaddr @@ -164,8 +171,42 @@ def writeRHStyleInterface(self, iface): """ Write a RedHat-style configuration entry for this interface to file object f using interface name iface. """ + def writeBondMember(index, member): + """ Write a RedHat-style configuration entry for a bond member. """ + + f = open('/etc/sysconfig/network-scripts/ifcfg-%s' % member, 'w') + f.write("NAME=%s-slave%d\n" % (iface, index)) + f.write("DEVICE=%s\n" % member) + f.write("ONBOOT=yes\n") + f.write("MASTER=%s\n" % iface) + f.write("SLAVE=yes\n") + f.write("BOOTPROTO=none\n") + f.write("Type=Ethernet\n") + f.close() + + def writeBondMaster(): + """ Write a RedHat-style configuration entry for a bond master. """ + + with open('/etc/sysconfig/network-scripts/ifcfg-%s' % iface, 'w') as f: + f.write("NAME=%s\n" % iface) + f.write("DEVICE=%s\n" % iface) + f.write("ONBOOT=yes\n") + f.write("Type=Bond\n") + f.write("NOZEROCONF=yes\n") + f.write("BONDING_MASTER=yes\n") + if self.bond_mode == "lacp": + f.write("BONDING_OPTS=\"mode=4 miimon=100\"\n") + elif self.bond_mode == "active-backup": + f.write("BONDING_OPTS=\"mode=1 miimon=100\"\n") + + if self.vlan: + f.write("BOOTPROTO=none\n") + else: + writeIpConfig(f) + def writeIface(iface_name): with open('/etc/sysconfig/network-scripts/ifcfg-%s' % iface_name, 'w') as f: + f.write("NAME=%s\n" % iface_name) f.write("DEVICE=%s\n" % iface_name) f.write("ONBOOT=yes\n") writeIpConfig(f) @@ -189,6 +230,16 @@ def writeIpConfig(f): assert self.modev6 is None assert self.mode + + if self.bond_mode is not None: + for idx, member in enumerate(self.bond_members): + writeBondMember(idx, member) + + writeBondMaster() + if not self.vlan: + return + + # No bound or bond + vlan iface_vlan = self.getInterfaceName(iface) writeIface(iface_vlan) diff --git a/netutil.py b/netutil.py index e5332326..6de4f667 100644 --- a/netutil.py +++ b/netutil.py @@ -20,6 +20,7 @@ def __init__(self, nic_dict): self.driver = "%s (%s)" % (nic_dict.get("Driver", ""), nic_dict.get("Driver version", "")) self.smbioslabel = nic_dict.get("SMBIOS Label", "") + self.bond_mode, self.bond_members = None, None def __repr__(self): return "" % (self.name, self.hwaddr) diff --git a/tui/network.py b/tui/network.py index 00ebebe3..70524328 100644 --- a/tui/network.py +++ b/tui/network.py @@ -126,11 +126,11 @@ def dhcp_change(): vlan_value = int(vlan_field.value()) if vlan_cb.selected() else None if bool(dhcp_rb.selected()): - answers = NetInterface(NetInterface.DHCP, nic.hwaddr, vlan=vlan_value) + answers = NetInterface(NetInterface.DHCP, nic.hwaddr, vlan=vlan_value, bond_mode=nic.bond_mode, bond_members=nic.bond_members) else: answers = NetInterface(NetInterface.Static, nic.hwaddr, ip_field.value(), subnet_field.value(), gateway_field.value(), - dns_field.value(), vlan=vlan_value) + dns_field.value(), vlan=vlan_value, bond_mode=nic.bond_mode, bond_members=nic.bond_members) return RIGHT_FORWARDS, answers def select_netif(text, conf, offer_existing=False, default=None): From 7f53e63ce87ef7aac329487a479cd8f24dd99d98 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 17 Jan 2023 13:54:17 +0100 Subject: [PATCH 13/21] fixup! WIP NIC,NetInterface: add support for bonding interfaces --- netinterface.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/netinterface.py b/netinterface.py index 18973b12..469cf3de 100644 --- a/netinterface.py +++ b/netinterface.py @@ -174,15 +174,14 @@ def writeRHStyleInterface(self, iface): def writeBondMember(index, member): """ Write a RedHat-style configuration entry for a bond member. """ - f = open('/etc/sysconfig/network-scripts/ifcfg-%s' % member, 'w') - f.write("NAME=%s-slave%d\n" % (iface, index)) - f.write("DEVICE=%s\n" % member) - f.write("ONBOOT=yes\n") - f.write("MASTER=%s\n" % iface) - f.write("SLAVE=yes\n") - f.write("BOOTPROTO=none\n") - f.write("Type=Ethernet\n") - f.close() + with open('/etc/sysconfig/network-scripts/ifcfg-%s' % member, 'w') as f: + f.write("NAME=%s-slave%d\n" % (iface, index)) + f.write("DEVICE=%s\n" % member) + f.write("ONBOOT=yes\n") + f.write("MASTER=%s\n" % iface) + f.write("SLAVE=yes\n") + f.write("BOOTPROTO=none\n") + f.write("Type=Ethernet\n") def writeBondMaster(): """ Write a RedHat-style configuration entry for a bond master. """ @@ -231,17 +230,17 @@ def writeIpConfig(f): assert self.modev6 is None assert self.mode - if self.bond_mode is not None: + iface_vlan = self.getInterfaceName(iface) + + if self.bond_mode: + # configuration of the bond interface for idx, member in enumerate(self.bond_members): writeBondMember(idx, member) - - writeBondMaster() - if not self.vlan: - return - - # No bound or bond + vlan - iface_vlan = self.getInterfaceName(iface) - writeIface(iface_vlan) + writeBondMaster() # ... includes IP config if not using VLAN ... + if self.vlan: + writeIface(iface_vlan) # ... but here when using VLAN + else: + writeIface(iface_vlan) def waitUntilUp(self, iface): if not self.isStatic(): From 6d63bebac3f3ea720744e66506aa57e1d6d90ca5 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 13 Jan 2023 12:08:09 +0100 Subject: [PATCH 14/21] init: add support for LACP bonding in --network_device (xcp-ng/xcp#350) Signed-off-by: Yann Dirson --- doc/parameters.txt | 5 ++++- init | 17 ++++++++++++++--- netutil.py | 29 +++++++++++++++++++++++++---- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/doc/parameters.txt b/doc/parameters.txt index 79c4fb8c..df5f1218 100644 --- a/doc/parameters.txt +++ b/doc/parameters.txt @@ -139,12 +139,15 @@ Installer Retrieve script, run it and use the output of it as an answerfile. - --network_device=eth|mac|all + --network_device=eth|mac|lacp:members=eth,eth[,eth]*|all --answerfile_device[D]=... Bring up networking on the given interface to allow access to answerfiles. + Specification of LACP bonding configures a bond0 interfaces with + given members. + Default: "all" if a non-local URL is specified for any of the above answerfile parameters, else no device is started diff --git a/init b/init index 465ac7c8..9df6c3a3 100755 --- a/init +++ b/init @@ -51,15 +51,26 @@ def configureNetworking(ui, device, config): nethw = netutil.scanConfiguration() netcfg = {} + + if device.startswith("lacp:"): + bond_mode, bond_config = device.split(":", 1) + device = "bond0" + assert bond_config.startswith("members=") + k, v = bond_config.split("=", 1) + bond_members = v.split(",") + netutil.configure_bonding_interface(nethw, device, "lacp", bond_members) + for devname, nic in nethw.items(): if (device == devname or device == nic.hwaddr) and mode == 'static': netcfg[devname] = NetInterface(NetInterface.Static, nic.hwaddr, config_dict['ip'], config_dict['netmask'], config_dict['gateway'], config_dict['dns'], - config_dict['domain'], config_dict['vlan']) + config_dict['domain'], config_dict['vlan'], + bond_mode=nic.bond_mode, bond_members=nic.bond_members) else: netcfg[devname] = NetInterface(NetInterface.DHCP, nic.hwaddr, - vlan=config_dict['vlan']) + vlan=config_dict['vlan'], + bond_mode=nic.bond_mode, bond_members=nic.bond_members) netutil.writeNetInterfaceFiles(netcfg) netutil.writeResolverFile(netcfg, '/etc/resolv.conf') @@ -67,7 +78,7 @@ def configureNetworking(ui, device, config): iface_to_start = [] if device == 'all': iface_to_start.extend(netcfg.keys()) - elif device.startswith('eth'): + elif device.startswith('eth') or device.startswith('bond'): if device in nethw: iface_to_start.append(device) else: diff --git a/netutil.py b/netutil.py index 6de4f667..4a13db02 100644 --- a/netutil.py +++ b/netutil.py @@ -20,10 +20,14 @@ def __init__(self, nic_dict): self.driver = "%s (%s)" % (nic_dict.get("Driver", ""), nic_dict.get("Driver version", "")) self.smbioslabel = nic_dict.get("SMBIOS Label", "") - self.bond_mode, self.bond_members = None, None + # those labels cannot come from biosdevname, but are added + # here to avoid adding another API + self.bond_mode = nic_dict.get("Bond mode", None) + self.bond_members = nic_dict.get("Bond members", None) def __repr__(self): - return "" % (self.name, self.hwaddr) + return "" % (self.name, self.hwaddr, + (", bonding=" + self.bond_mode) if self.bond_mode else "") def scanConfiguration(): """ Returns a dictionary of string -> NIC with a snapshot of the NIC @@ -58,8 +62,8 @@ def getNetifList(include_vlan=False): all = os.listdir("/sys/class/net") def ethfilter(interface, include_vlan): - return interface.startswith("eth") and (interface.isalnum() or - (include_vlan and "." in interface)) + return ((interface.startswith("eth") or interface.startswith("bond")) and + (interface.isalnum() or (include_vlan and "." in interface))) relevant = filter(lambda x: ethfilter(x, include_vlan), all) relevant.sort(key=netifSortKey) @@ -278,6 +282,23 @@ def as_xml(self): output += '\n' return output +def configure_bonding_interface(nethw, bond_iface, bond_mode, bond_members): + assert len(bond_members) >= 2 + for devname in bond_members: + assert devname in nethw + + # forge a NIC definition for the LACP interface + ref_devname = bond_members[0] + nethw[bond_iface] = NIC({"Kernel name": bond_iface, + "Assigned MAC": nethw[ref_devname].hwaddr, + "Bond mode": bond_mode, + "Bond members": bond_members, + }) + + # remove LACP member NIC definitions from the list + for devname in bond_members: + del nethw[devname] + ### EA-1069 import xcp.logger as LOG From 65d7d90dd3322d03a9be577e9e5c1565a41802dd Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Thu, 12 Jan 2023 17:55:24 +0100 Subject: [PATCH 15/21] netutil.scanConfiguration: allow reuse of commandline config Bonding configuration created from commandline cannot be properly passed to the UI, which needs it both for netinstall and host-management, so we have to re-discover the existing configuration to present it to the user. This includes identifying the configured bonding interface, and filtering out their member interfaces from available choices. This is not very satisfying, and we may want to improve the overall installer architecture to make this better in the future, but it would be way out of scope here. Signed-off-by: Yann Dirson --- netutil.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/netutil.py b/netutil.py index 4a13db02..366a1327 100644 --- a/netutil.py +++ b/netutil.py @@ -40,16 +40,50 @@ def scanConfiguration(): """ conf = {} nics = [] + slave_nics = [] for nif in getNetifList(): if nif not in diskutil.ibft_reserved_nics: nics.append(nif) + # identify any LACP interface previously configured (commandline) + SYSVIRTUALNET = "/sys/devices/virtual/net" + virtual_devs = os.listdir(SYSVIRTUALNET) + for iface in virtual_devs: + mode_file = os.path.join(SYSVIRTUALNET, iface, "bonding/mode") + if not os.path.exists(mode_file): + logger.log("scanConfiguration: no file %r" % mode_file) + continue # not a bonding iface + with open(mode_file) as fd: + lines = fd.readlines() + assert len(lines) == 1 + if lines[0].strip() != "802.3ad 4": + logger.log("scanConfiguration: wrong bonding mode %r != '802.3ad 4'" % lines[0]) + continue # not a LACP iface + with open(os.path.join(SYSVIRTUALNET, iface, "bonding/slaves")) as fd: + lines = fd.readlines() + assert len(lines) == 1 + bond_members = tuple(lines[0].strip().split()) + with open(os.path.join(SYSVIRTUALNET, iface, "address")) as fd: + lines = fd.readlines() + assert len(lines) == 1 + mac = lines[0].strip() + slave_nics.extend(bond_members) + conf[iface] = NIC({"Kernel name": iface, + "Assigned MAC": mac, + "Bond mode": "lacp", + "Bond members": bond_members, + }) + logger.log("scanConfiguration: bonding interfaces: {}, slaves: {}".format(conf.keys(), slave_nics)) + for nic in all_devices_all_names().values(): name = nic.get("Kernel name", "") if name not in nics: logger.log("scanConfiguration: {} not in nics".format(name)) continue + if name in slave_nics: + logger.log("scanConfiguration: {} in slave_nics".format(name)) + continue conf[name] = NIC(nic) return conf From 996bf7fa2a1f51332d39dd25360c719e000c8101 Mon Sep 17 00:00:00 2001 From: BenjiReis Date: Fri, 13 Jan 2023 10:03:30 +0100 Subject: [PATCH 16/21] configureNetworking: write bond configuration to firstboot.d --- backend.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend.py b/backend.py index 9c5d0d83..fd2e14e9 100644 --- a/backend.py +++ b/backend.py @@ -1503,6 +1503,10 @@ def configureNetworking(mounts, admin_iface, admin_bridge, admin_config, hn_conf print >>mc, "IPv6_GATEWAY='%s'" % admin_config.ipv6_gateway if admin_config.vlan: print >>mc, "VLAN='%d'" % admin_config.vlan + if admin_config.bond_mode is not None: + print >>mc, "BOND_MODE='%s'" % admin_config.bond_mode + print >>mc, "BOND_MEMBERS='%s'" % ','.join(admin_config.bond_members) + mc.close() if network_backend == constants.NETWORK_BACKEND_VSWITCH: From dfd647231022cb059d4d5dbb1335dfc2b94e7fe7 Mon Sep 17 00:00:00 2001 From: BenjiReis Date: Mon, 9 Jan 2023 11:22:41 +0100 Subject: [PATCH 17/21] tui.network: make lentry() and iface_details() more accessible We'll call them from upcoming lacp_bond_ui(). --- tui/network.py | 66 ++++++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/tui/network.py b/tui/network.py index 70524328..82778b13 100644 --- a/tui/network.py +++ b/tui/network.py @@ -133,6 +133,32 @@ def dhcp_change(): dns_field.value(), vlan=vlan_value, bond_mode=nic.bond_mode, bond_members=nic.bond_members) return RIGHT_FORWARDS, answers +def lentry(iface, conf): + key = iface + tag = netutil.linkUp(iface) and ' ' or ' [no link]' + text = "%s (%s)%s" % (iface, conf[iface].hwaddr, tag) + return (text, key) + +def iface_details(context, conf): + tui.update_help_line([' ', ' ']) + if context: + nic = conf[context] + + table = [ ("Name:", nic.name), + ("Driver:", nic.driver), + ("MAC Address:", nic.hwaddr), + ("PCI Details:", nic.pci_string) ] + if nic.smbioslabel != "": + table.append(("BIOS Label:", nic.smbioslabel)) + + snackutil.TableDialog(tui.screen, "Interface Details", *table) + else: + netifs_all = netutil.getNetifList(include_vlan=True) + details = map(lambda x: (x, netutil.ipaddr(x)), filter(netutil.interfaceUp, netifs_all)) + snackutil.TableDialog(tui.screen, "Networking Details", *details) + tui.screen.popHelpLine() + return True + def select_netif(text, conf, offer_existing=False, default=None): """ Display a screen that displays a choice of network interfaces to the user, with 'text' as the informative text as the data, and conf being the @@ -150,37 +176,14 @@ def select_netif(text, conf, offer_existing=False, default=None): default = iface break - def lentry(iface): - key = iface - tag = netutil.linkUp(iface) and ' ' or ' [no link]' - text = "%s (%s)%s" % (iface, conf[iface].hwaddr, tag) - return (text, key) - - def iface_details(context): - tui.update_help_line([' ', ' ']) - if context: - nic = conf[context] - - table = [ ("Name:", nic.name), - ("Driver:", nic.driver), - ("MAC Address:", nic.hwaddr), - ("PCI Details:", nic.pci_string) ] - if nic.smbioslabel != "": - table.append(("BIOS Label:", nic.smbioslabel)) - - snackutil.TableDialog(tui.screen, "Interface Details", *table) - else: - netifs_all = netutil.getNetifList(include_vlan=True) - details = map(lambda x: (x, netutil.ipaddr(x)), filter(netutil.interfaceUp, netifs_all)) - snackutil.TableDialog(tui.screen, "Networking Details", *details) - tui.screen.popHelpLine() - return True + def iface_details_with_conf(context): + return iface_details(context, conf) def update(listbox): old = listbox.current() for item in listbox.item2key.keys(): if item: - text, _ = lentry(item) + text, _ = lentry(item, conf) listbox.replace(text, item) listbox.setCurrent(old) return True @@ -193,12 +196,13 @@ def update(listbox): else: netif_list = [] if default: - def_iface = lentry(default) - netif_list += [lentry(x) for x in netifs] + def_iface = lentry(default, conf) + netif_list += [lentry(x, conf) for x in netifs] scroll, height = snackutil.scrollHeight(6, len(netif_list)) - rc, entry = snackutil.ListboxChoiceWindowEx(tui.screen, "Networking", text, netif_list, - ['Ok', 'Back'], 45, scroll, height, def_iface, help='selif:info', - hotkeys={'F5': iface_details}, timeout_ms=5000, timeout_cb=update) + rc, entry = snackutil.ListboxChoiceWindowEx( + tui.screen, "Networking", text, netif_list, + ['Ok', 'Back'], 45, scroll, height, def_iface, help='selif:info', + hotkeys={'F5': iface_details_with_conf}, timeout_ms=5000, timeout_cb=update) tui.screen.popHelpLine() From c169793baf10e1267c950ecbd3e6c619d87efe7e Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 13 Jan 2023 14:51:03 +0100 Subject: [PATCH 18/21] tui: don't hide NIC selection when there is only one Making things visible is good (esp. here it allows to check interface details), especially once we add the ability to create a bond interface. Signed-off-by: Yann Dirson --- tui/installer/__init__.py | 4 +--- tui/network.py | 9 ++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/tui/installer/__init__.py b/tui/installer/__init__.py index 7a8f588f..74d5db95 100644 --- a/tui/installer/__init__.py +++ b/tui/installer/__init__.py @@ -46,8 +46,6 @@ def upgrade_but_no_settings_predicate(answers): ('installation-to-overwrite' not in answers or \ not answers['installation-to-overwrite'].settingsAvailable()) - has_multiple_nics = lambda a: len(a['network-hardware'].keys()) > 1 - is_reinstall_fn = lambda a: a['install-type'] == constants.INSTALL_TYPE_REINSTALL is_clean_install_fn = lambda a: a['install-type'] == constants.INSTALL_TYPE_FRESH is_not_restore_fn = lambda a: a['install-type'] != constants.INSTALL_TYPE_RESTORE @@ -160,7 +158,7 @@ def out_of_order_pool_upgrade_fn(answers): Step(uis.get_root_password, predicates=[is_not_restore_fn, not_preserve_settings]), Step(uis.get_admin_interface, - predicates=[is_not_restore_fn, has_multiple_nics, not_preserve_settings]), + predicates=[is_not_restore_fn, not_preserve_settings]), Step(uis.get_admin_interface_configuration, predicates=[is_not_restore_fn, not_preserve_settings]), Step(uis.get_name_service_configuration, diff --git a/tui/network.py b/tui/network.py index 82778b13..b9d92cca 100644 --- a/tui/network.py +++ b/tui/network.py @@ -266,13 +266,8 @@ def specify_configuration(answers, txt, defaults): def_iface = defaults[interface_key] if config_key in defaults: def_conf = defaults[config_key] - if len(nethw.keys()) > 1 or netutil.networkingUp(): - seq = [ uicontroller.Step(select_interface, args=[def_iface, msg]), - uicontroller.Step(specify_configuration, args=[None, def_conf]) ] - else: - text = "%s Setup needs network access to continue.\n\nHow should networking be configured at this time?" % (version.PRODUCT_BRAND or version.PLATFORM_NAME) - conf_dict['interface'] = nethw.keys()[0] - seq = [ uicontroller.Step(specify_configuration, args=[text, def_conf]) ] + seq = [ uicontroller.Step(select_interface, args=[def_iface, msg]), + uicontroller.Step(specify_configuration, args=[None, def_conf]) ] direction = uicontroller.runSequence(seq, conf_dict) if direction == RIGHT_FORWARDS and 'config' in conf_dict: From 3db0eaf00934f06b746a2e6a228543621cd97ce8 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Fri, 13 Jan 2023 10:05:55 +0100 Subject: [PATCH 19/21] WIP tui: let user configure a bonding interface interactively Works at selectNetif level, and as such applies both to installer and host-management networt configs. FIXME: - don't show button when we have only one interface - give nice error popup if user selects less than 2 ifaces (or can we disable OK?) - allow bonding only eth interfaces - should include removing button if only bond0 is there --- tui/installer/screens.py | 6 +++-- tui/network.py | 53 +++++++++++++++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/tui/installer/screens.py b/tui/installer/screens.py index 8f5bd9c2..462a46c6 100644 --- a/tui/installer/screens.py +++ b/tui/installer/screens.py @@ -194,7 +194,9 @@ def get_admin_interface(answers): def get_admin_interface_configuration(answers): if 'net-admin-interface' not in answers: answers['net-admin-interface'] = answers['network-hardware'].keys()[0] - nic = answers['network-hardware'][answers['net-admin-interface']] + + net_admin_interface = answers['net-admin-interface'] + nic = answers['network-hardware'][net_admin_interface] defaults = None try: @@ -203,7 +205,7 @@ def get_admin_interface_configuration(answers): elif 'runtime-iface-configuration' in answers: all_dhcp, manual_config = answers['runtime-iface-configuration'] if not all_dhcp: - defaults = manual_config[answers['net-admin-interface']] + defaults = manual_config[net_admin_interface] except: pass diff --git a/tui/network.py b/tui/network.py index b9d92cca..8b676fbf 100644 --- a/tui/network.py +++ b/tui/network.py @@ -159,6 +159,48 @@ def iface_details(context, conf): tui.screen.popHelpLine() return True +def lacp_bond_ui(conf): + netifs = conf.keys() + netifs.sort(key=netutil.netifSortKey) + entries = [lentry(x, conf) for x in netifs] + + text = TextboxReflowed(54, "Select interfaces to create the bond on.") + buttons = ButtonBar(tui.screen, [('Create', 'create'), ('Back', 'back')]) + scroll, _ = snackutil.scrollHeight(3, len(entries)) + cbt = CheckboxTree(3, scroll) + for (c_text, c_item) in entries: + cbt.append(c_text, c_item, False) + gf = GridFormHelp(tui.screen, 'LACP Bond', '', 1, 4) + gf.add(text, 0, 0, padding=(0, 0, 0, 1)) + gf.add(cbt, 0, 1, padding=(0, 0, 0, 1)) + gf.add(buttons, 0, 3, growx=1) + gf.addHotKey('F5') + + tui.update_help_line([None, " more info"]) + loop = True + while loop: + rc = gf.run() + if rc == 'F5': + iface_details(cbt.getCurrent(), conf) + else: + loop = False + tui.screen.popWindow() + tui.screen.popHelpLine() + + button = buttons.buttonPressed(rc) + if button == 'create': + selected = cbt.getSelection() + txt = 'Create a LACP bond with members %s? This choice cannot be rolled back.' % ( + ", ".join(selected)) + title = 'LACP bond creation' + confirmation = snackutil.ButtonChoiceWindowEx(tui.screen, title, txt, + ('Ok', 'Cancel'), 40, default=1) + if confirmation == 'ok': + netutil.configure_bonding_interface(conf, 'bond0', "lacp", tuple(selected)) + + # always back to iface selection + return REPEAT_STEP, None + def select_netif(text, conf, offer_existing=False, default=None): """ Display a screen that displays a choice of network interfaces to the user, with 'text' as the informative text as the data, and conf being the @@ -199,14 +241,15 @@ def update(listbox): def_iface = lentry(default, conf) netif_list += [lentry(x, conf) for x in netifs] scroll, height = snackutil.scrollHeight(6, len(netif_list)) - rc, entry = snackutil.ListboxChoiceWindowEx( - tui.screen, "Networking", text, netif_list, - ['Ok', 'Back'], 45, scroll, height, def_iface, help='selif:info', - hotkeys={'F5': iface_details_with_conf}, timeout_ms=5000, timeout_cb=update) + rc, entry = snackutil.ListboxChoiceWindowEx(tui.screen, "Networking", text, netif_list, + ['Ok', 'Create LACP Bond', 'Back'], 45, scroll, height, def_iface, help='selif:info', + hotkeys={'F5': iface_details_with_conf}, timeout_ms=5000, timeout_cb=update) tui.screen.popHelpLine() if rc == 'back': return LEFT_BACKWARDS, None + if rc == 'create lacp bond': return lacp_bond_ui(conf) + return RIGHT_FORWARDS, entry def requireNetworking(answers, defaults=None, msg=None, keys=['net-admin-interface', 'net-admin-configuration']): @@ -252,6 +295,8 @@ def specify_configuration(answers, txt, defaults): if 'reuse-networking' in answers and answers['reuse-networking']: return RIGHT_FORWARDS + logger.log("answers['interface'] = {!r}".format(answers['interface'])) + # FIXME if this is a tuple we have a bond, what to do ? direction, conf = get_iface_configuration(nethw[answers['interface']], txt, defaults=defaults, include_dns=True) if direction == RIGHT_FORWARDS: From 6d39fd0fc0d74bf7457d73820ea60cce58724748 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 17 Jan 2023 13:59:05 +0100 Subject: [PATCH 20/21] net*: cleanups around vlan-interface Using 1 name for 2 concepts is a bad idea, especially in a single function. Also nukes one line of dead code. Signed-off-by: Yann Dirson --- netinterface.py | 2 +- netutil.py | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/netinterface.py b/netinterface.py index 469cf3de..01c56f36 100644 --- a/netinterface.py +++ b/netinterface.py @@ -90,7 +90,7 @@ def __repr__(self): ipv6 = "autoconf" else: ipv6 = "None" - vlan = ("vlan = '%d' " % self.vlan) if self.vlan else "" + vlan = (" vlan='%d' " % self.vlan) if self.vlan else "" return "" % (hw, vlan, ipv4, ipv6) diff --git a/netutil.py b/netutil.py index 366a1327..d82b6b9b 100644 --- a/netutil.py +++ b/netutil.py @@ -145,15 +145,14 @@ def writeResolverFile(configuration, filename): interface_up = {} # simple wrapper for calling the local ifup script: -def splitInterfaceVlan(interface): +def splitInterfaceVlan(interface_vlan): if "." in interface: return interface.split(".", 1) return interface, None -def ifup(interface): - device, vlan = splitInterfaceVlan(interface) - interface_up[interface] = True - return util.runCmd2(['ifup', interface]) +def ifup(interface_vlan): + interface_up[interface_vlan] = True + return util.runCmd2(['ifup', interface_vlan]) def ifdown(interface): if interface in interface_up: @@ -209,8 +208,8 @@ def networkingUp(): return False # make a string to help users identify a network interface: -def getPCIInfo(interface): - interface, vlan = splitInterfaceVlan(interface) +def getPCIInfo(interface_vlan): + interface, vlan = splitInterfaceVlan(interface_vlan) info = "" devpath = os.path.realpath('/sys/class/net/%s/device' % interface) slot = devpath[len(devpath) - 7:] @@ -232,8 +231,8 @@ def getPCIInfo(interface): return info -def getDriver(interface): - interface, vlan = splitInterfaceVlan(interface) +def getDriver(interface_vlan): + interface, vlan = splitInterfaceVlan(interface_vlan) return os.path.basename(os.path.realpath('/sys/class/net/%s/device/driver' % interface)) def __readOneLineFile__(filename): From 86090cd2b07cc6d51c37671edb4aecd3fd4c6672 Mon Sep 17 00:00:00 2001 From: Yann Dirson Date: Tue, 17 Jan 2023 17:11:49 +0100 Subject: [PATCH 21/21] WIP answerfile: LACP bonding support FIXME: parsed but not applied --- answerfile.py | 30 ++++++++++++++++++++++++++++++ doc/answerfile.txt | 8 ++++++++ 2 files changed, 38 insertions(+) diff --git a/answerfile.py b/answerfile.py index 490b49ec..f2ed8249 100644 --- a/answerfile.py +++ b/answerfile.py @@ -134,6 +134,7 @@ def parseFreshInstall(self): results['backup-existing-installation'] = False results.update(self.parseDisks()) + results.update(self.parseBonding()) results.update(self.parseInterface()) results.update(self.parseRootPassword()) results.update(self.parseNSConfig()) @@ -352,6 +353,35 @@ def parseFCoEInterface(self): return results + def parseBonding(self): + results = {} + nethw = netutil.scanConfiguration() + + nodes = getElementsByTagName(self.top_node, ['bonding']) + if len(nodes) > 1: + raise AnswerfileException(" must appear only once") + if nodes: + node = nodes[0] + bond_name = getStrAttribute(node, ['name'], mandatory=True) + if bond_name != "bond0": + raise AnswerfileException(" name must be 'bond0'") + + bond_mode = getStrAttribute(node, ['mode'], mandatory=True) + if bond_mode != "lacp": + raise AnswerfileException(" mode must be 'lacp'") + + bond_members_str = getStrAttribute(node, ['members'], mandatory=True) + bond_members = bond_members_str.split(",") + if len(bond_members) < 2: + raise AnswerfileException(" members must be at least two") + for member in bond_members: + if member not in nethw: + raise AnswerfileException(" member %r not in detected NICs" % (member,)) + + results['bonding'] = dict(name=bond_name, mode=bond_mode, members=bond_members) + + return results + def parseInterface(self): results = {} node = getElementsByTagName(self.top_node, ['admin-interface'], mandatory=True)[0] diff --git a/doc/answerfile.txt b/doc/answerfile.txt index 76221624..0acbf6cb 100644 --- a/doc/answerfile.txt +++ b/doc/answerfile.txt @@ -174,6 +174,14 @@ Common Elements Specifies additional devices to be included in the local SR. + + + Join given ethernet interfaces using LACP bonding. Name of + resulting interface must be "bond0". It can be then be used with + + + + | Specifies the initial management interface.