--- ./cloudinit/distros/networking.py_orig 2026-05-14 02:10:13.595207250 -0500 +++ ./cloudinit/distros/networking.py 2026-05-14 02:11:13.994811123 -0500 @@ -196,6 +196,12 @@ print("try_set_link_up devname %s" % devname) return self.is_up(devname) + def get_interfaces_by_mac(self) -> dict: + """Get interfaces by MAC address for AIX using netstat and lsattr.""" + return net.get_interfaces_by_mac_on_aix( + blacklist_drivers=self.blacklist_drivers + ) + class BSDNetworking(Networking): """Implementation of networking functionality shared across BSDs.""" --- ./cloudinit/net/__init__.py_orig 2026-05-14 02:11:51.680426732 -0500 +++ ./cloudinit/net/__init__.py 2026-05-15 06:25:59.919964886 -0500 @@ -866,6 +866,10 @@ return get_interfaces_by_mac_on_openbsd( blacklist_drivers=blacklist_drivers ) + elif util.is_AIX(): + return get_interfaces_by_mac_on_aix( + blacklist_drivers=blacklist_drivers + ) else: return get_interfaces_by_mac_on_linux( blacklist_drivers=blacklist_drivers @@ -957,6 +961,69 @@ ) ret[ib_mac] = name return ret + +def get_interfaces_by_mac_on_aix(blacklist_drivers=None) -> dict: + """Build a dictionary of tuples {mac: name} for AIX. + + Uses lsdev to find all ethernet interfaces, then entstat to get their MACs. + This works for interfaces in any state (Defined, Available, Stopped). + """ + LOG.debug("AIX: get_interfaces_by_mac_on_aix() called") + ret = {} + + try: + # First, get list of all ethernet interfaces using lsdev + # Output format: + # en0 Defined Standard Ethernet Network Interface + # en1 Available Standard Ethernet Network Interface + (out, _) = subp.subp(["lsdev", "-Cc", "if"]) + LOG.debug("AIX: lsdev -Cc if output:\n%s", out) + + # Parse lsdev output to find en* interfaces + en_interfaces = [] + for line in out.splitlines(): + parts = line.split() + if len(parts) >= 2 and parts[0].startswith('en'): + ifname = parts[0] + en_interfaces.append(ifname) + LOG.debug("AIX: Found interface %s", ifname) + + # For each interface, use entstat to get its MAC address + for ifname in en_interfaces: + try: + # entstat shows detailed interface statistics including MAC + # Hardware Address: fa:64:d0:0d:0c:20 + (entstat_out, _) = subp.subp(["entstat", "-d", ifname]) + LOG.debug("AIX: entstat -d %s output:\n%s", ifname, entstat_out) + + # Look for "Hardware Address:" line + for line in entstat_out.splitlines(): + if "Hardware Address:" in line: + # Extract MAC address + parts = line.split(":") + if len(parts) >= 2: + # Get everything after "Hardware Address:" + mac_part = ":".join(parts[1:]).strip() + # MAC is already in colon notation from entstat + mac_address = mac_part.lower() + + if mac_address: + ret[mac_address] = ifname + LOG.debug("AIX: Found interface %s with MAC %s", + ifname, mac_address) + break + except subp.ProcessExecutionError as e: + LOG.debug("AIX: entstat failed for %s: %s", ifname, e) + continue + + except subp.ProcessExecutionError as e: + LOG.warning("AIX: Failed to get interfaces via lsdev: %s", e) + except Exception as e: + LOG.warning("AIX: Unexpected error getting interfaces by MAC: %s", e) + + LOG.debug("AIX: get_interfaces_by_mac_on_aix() returning: %s", ret) + return ret + def get_interfaces(blacklist_drivers=None) -> list: --- ./cloudinit/sources/DataSourceConfigDrive.py_orig 2026-05-14 06:34:29.141164381 -0500 +++ ./cloudinit/sources/DataSourceConfigDrive.py 2026-05-19 03:49:36.749173604 -0500 @@ -163,15 +163,14 @@ @property def network_config(self): if self._network_config is None: - if self.network_eni is not None: - LOG.debug("network config provided via converted eni data") - self._network_config = eni.convert_eni_data(self.network_eni) - print("self.network_eni=%s self._network_config=%s" % (self.network_eni, self._network_config)) - elif self.network_json not in (None, sources.UNSET): + if self.network_json not in (None, sources.UNSET): LOG.debug("network config provided via network_json") self._network_config = openstack.convert_net_json( self.network_json, known_macs=self.known_macs ) + elif self.network_eni is not None: + self._network_config = eni.convert_eni_data(self.network_eni) + LOG.debug("network config provided via converted eni data") else: LOG.debug("no network configuration available") return self._network_config --- ./cloudinit/util.py_orig 2026-05-14 07:07:43.639149663 -0500 +++ ./cloudinit/util.py 2026-05-15 07:05:57.939031500 -0500 @@ -447,6 +447,10 @@ def is_OpenBSD(): return system_info()["variant"] == "openbsd" +@lru_cache() +def is_AIX(): + return system_info()["system"].lower() == "aix" + def get_cfg_option_bool(yobj, key, default=False): if key not in yobj: return default --- /dev/null 2026-05-25 02:01:08.804223746 -0500 +++ ./cloudinit/distros/aix_network_state.py 2026-05-24 11:01:52.258458528 -0500 @@ -0,0 +1,320 @@ +# vi: ts=4 expandtab +# +# AIX-specific network state handling +# This module provides direct network_state processing for AIX, +# similar to how Linux distros use network renderers. + +import time +from cloudinit import log as logging +from cloudinit import subp, util +from cloudinit.distros import aix_util +from cloudinit.net.network_state import subnet_is_ipv6 + +LOG = logging.getLogger(__name__) + + +def write_network_state_aix(network_state, resolve_conf_fn): + """ + AIX-specific implementation to write network configuration directly + from network_state object, bypassing ENI text conversion. + + This is similar to how Linux distros use network renderers, but + adapted for AIX's chdev/ifconfig commands. + + Args: + network_state: NetworkState object containing interfaces, routes, etc. + resolve_conf_fn: Path to resolv.conf file + + Returns: + List of device names configured + """ + LOG.debug("AIX: Writing network state directly from network_state object") + + dev_names = [] + nameservers = [] + searchservers = [] + create_dhcp_file = True + run_dhcpcd = False + run_autoconf6 = False + ipv6_interface = None + + # First, make sure the services starts out uncommented in /etc/rc.tcpip + aix_util.disable_dhcpcd() + aix_util.disable_ndpd_host() + aix_util.disable_autoconf6() + aix_util.remove_resolve_conf_file(resolve_conf_fn) + + # Remove the chdev ipv6 entries present in the /etc/rc.tcpip from earlier runs. + # Read the content of the file and filter out lines containing both words + with open('/etc/rc.tcpip', "r") as infile: + lines = [line for line in infile + if "chdev" not in line and "anetaddr6" not in line + and "cloud-init" not in line] + + # Write the filtered lines back to the file + with open('/etc/rc.tcpip', "w") as outfile: + outfile.writelines(lines) + + # Build gateway map from routes using iter_routes() + gateway_map = {} + LOG.debug("AIX: Extracting gateways from network_state routes") + for route in network_state.iter_routes(): + LOG.debug("AIX: Processing route: %s", route) + # Look for default gateway routes (0.0.0.0/0 or ::/0) + if route.get('network') in ['0.0.0.0', '::'] or \ + route.get('destination') in ['0.0.0.0/0', '::/0', 'default']: + interface = route.get('interface') + gateway = route.get('gateway') + if interface and gateway: + gateway_map[interface] = gateway + LOG.debug("AIX: Found gateway %s for interface %s from top-level routes", + gateway, interface) + + # Process each interface using iter_interfaces() + for iface in network_state.iter_interfaces(): + iface_name = iface.get('name') + if iface_name == 'lo': + continue + + dev_names.append(iface_name) + aix_dev = aix_util.translate_devname(iface_name) + LOG.debug("AIX: Configuring interface %s (AIX device: %s)", + iface_name, aix_dev) + + # Get subnets for this interface + subnets = iface.get('subnets', []) + if not subnets: + LOG.debug("AIX: No subnets found for %s", iface_name) + continue + + # Static configuration + run_cmd = False + ipv6_present = False + chdev_cmd = ['/usr/sbin/chdev'] + + # Process each subnet + for subnet in subnets: + subnet_type = subnet.get('type') + + # Check for DHCP + if subnet_type in ['dhcp', 'dhcp4', 'dhcp6']: + # Build info dict from subnet data for config_dhcp + # config_dhcp expects a dict with 'address', 'netmask', 'gateway' keys + info = { + 'address': subnet.get('address'), + 'netmask': subnet.get('netmask'), + 'gateway': subnet.get('gateway') + } + aix_util.config_dhcp(aix_dev, info, create_dhcp_file) + create_dhcp_file = False + run_dhcpcd = True + continue + + chdev_cmd.extend(['-l', aix_dev]) + # Static IPv6 configuration + if subnet_type == 'static6' and subnet_is_ipv6(subnet): + ipv6_address = subnet.get('address') + if ipv6_address: + run_cmd = True + ipv6_present = True + run_autoconf6 = True + addr_parts = ipv6_address.split('/') + LOG.debug("AIX: Configuring IPv6 %s on %s", ipv6_address, aix_dev) + chdev_cmd.append('-anetaddr6=' + addr_parts[0]) + if len(addr_parts) > 1: + chdev_cmd.append('-aprefixlen=' + addr_parts[1]) + + if ipv6_interface is None: + ipv6_interface = aix_dev + else: + ipv6_interface = "any" + + # Static IPv4 configuration + elif subnet_type == 'static' and not subnet_is_ipv6(subnet): + ipv4_address = subnet.get('address') + ipv4_netmask = subnet.get('netmask') + if ipv4_address: + run_cmd = True + ipv6_present = False + LOG.debug("AIX: Configuring IPv4 %s/%s on %s", + ipv4_address, ipv4_netmask, aix_dev) + chdev_cmd.append('-anetaddr=' + ipv4_address) + if ipv4_netmask: + chdev_cmd.append('-anetmask=' + ipv4_netmask) + + # Check for gateway in subnet routes + subnet_routes = subnet.get('routes', []) + for route in subnet_routes: + LOG.debug("AIX: Processing subnet route: %s", route) + if route.get('network') in ['0.0.0.0', '::'] or \ + route.get('destination') in ['0.0.0.0/0', '::/0', 'default'] or \ + route.get('netmask') == '0.0.0.0': + gw = route.get('gateway') + if gw and iface_name not in gateway_map: + gateway_map[iface_name] = gw + LOG.debug("AIX: Found gateway %s for %s from subnet routes", + gw, iface_name) + + # Configure autoconf6 if needed + if run_autoconf6: + ifconfig_cmd = ['/etc/ifconfig', '-a'] + (cmd_output, _) = subp.subp(ifconfig_cmd, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of "+" ".join(ifconfig_cmd)) + print(cmd_output) + print("------------------------------------------------------------") + rmdev_cmd = ['/usr/sbin/rmdev', '-l', aix_dev] + (cmd_output, _) = subp.subp(rmdev_cmd, rcs=[0, 1]) + print(" ".join(rmdev_cmd) + " executed") + print(cmd_output) + time.sleep(2) + ifconfig_cmd = ['/etc/ifconfig', '-a'] + (cmd_output, _) = subp.subp(ifconfig_cmd, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of "+" ".join(ifconfig_cmd)) + print(cmd_output) + print("------------------------------------------------------------") + netstat_cmd = ['/usr/bin/netstat', '-rn'] + (cmd_output, _) = subp.subp(netstat_cmd, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of "+" ".join(netstat_cmd)) + print(cmd_output) + print("------------------------------------------------------------") + autconf6_cmd = ['/usr/sbin/autoconf6', '-6', '-R', '-i', aix_dev] + (cmd_output, _) = subp.subp(autconf6_cmd, rcs=[0, 1]) + print(" ".join(autconf6_cmd) + " executed") + if ipv6_present: + util.append_file("/etc/rc.tcpip", "%s\n" % ("/usr/sbin/autoconf6 -6 -R -i " + aix_dev + " #cloud-init")) + time.sleep(2) + netstat_cmd = ['/usr/bin/netstat', '-rn'] + (cmd_output, _) = subp.subp(netstat_cmd, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of "+" ".join(netstat_cmd)) + print(cmd_output) + print("------------------------------------------------------------") + + + + # Execute chdev command + if run_cmd: + try: + print("AIX: Running ", chdev_cmd) + subp.subp(chdev_cmd, logstring=chdev_cmd) + time.sleep(2) + + print(" ".join(chdev_cmd) + " executed") + netstat_cmd = ['/usr/bin/netstat', '-rn'] + (cmd_output, _) = subp.subp(netstat_cmd, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of "+" ".join(netstat_cmd)) + print(cmd_output) + print("------------------------------------------------------------") + + # Always persist chdev commands to /etc/rc.tcpip for reboot + util.append_file("/etc/rc.tcpip", + "%s\n" % (" ".join(chdev_cmd))) + except Exception as e: + LOG.error("AIX: Failed to configure %s: %s", aix_dev, e) + raise + + # MTU configuration + mtu = iface.get('mtu') + if mtu: + if int(mtu) > 1500: + subp.subp(["/etc/ifconfig", aix_dev, "down", "detach"], + capture=False, rcs=[0, 1]) + time.sleep(2) + aix_adapter = aix_util.logical_adpt_name(aix_dev) + subp.subp(["/usr/sbin/chdev", "-l", aix_adapter, + "-ajumbo_frames=yes"], capture=False, rcs=[0, 1]) + time.sleep(2) + + subp.subp(["/usr/sbin/chdev", "-l", aix_dev, + "-amtu=" + str(mtu)], capture=False, rcs=[0, 1]) + time.sleep(2) + + # This ensures clean state before bringing it up + aix_util.bring_interface_down(aix_dev) + # Always bring interface UP. + aix_util.bring_interface_up(aix_dev) + + # Add gateway from routes + gateway = gateway_map.get(iface_name) + if gateway: + LOG.debug("AIX: Adding gateway %s for %s", gateway, aix_dev) + if run_autoconf6: + aix_util.add_route("ipv6", gateway) + #Modified 'command' from list to string. And stopped using the routine subp.subp() + #Since, it has issues in passing multiple arguments at once. It considers them as + #a single byte stream and thus throws the invalid arg errors. + #startsrc_cmd = ['startsrc', '-s', 'ndpd-host', '-a "-g -p"'] + startsrc_cmd = 'startsrc -s ndpd-host -a "-g -p"' + (cmd_output, _) = subp.subp(startsrc_cmd, shell=True, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of " + startsrc_cmd) + print(cmd_output) + print("------------------------------------------------------------") + LOG.debug("AIX: Ran command: %s", startsrc_cmd) + #need this delay, because ndpd-host was showing active if checked as soon as startsrc is run + time.sleep(10) + tmp_cmd = "lssrc -s ndpd-host | grep ndpd-host" + (cmd_output, _) = subp.subp(tmp_cmd, shell=True, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of " + tmp_cmd) + print(cmd_output) + print("------------------------------------------------------------") + if "inoperative" in cmd_output: + LOG.debug( + "Running %s resulted in stderr output", + tmp_cmd + ) + LOG.debug("AIX: ndpd-host does not support option, p. Running it without option, p") + #startsrc_cmd = ['startsrc', '-s' 'ndpd-host' '-a' '-g -v'] + startsrc_cmd = 'startsrc -s ndpd-host -a "-g -v"' + (cmd_output, _) = subp.subp(startsrc_cmd, shell=True, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of " + startsrc_cmd) + print(cmd_output) + print("------------------------------------------------------------") + LOG.debug("AIX: Ran command: %s", startsrc_cmd) + print(startsrc_cmd + " executed") + # startsrc_cmd is already a string, use it directly + #util.append_file("/etc/rc.tcpip", "%s\n" % (startsrc_cmd + ' #cloud-init')) + util.append_file("/etc/rc.tcpip", "%s\n" % (startsrc_cmd + ' #cloud-init')) + netstat_cmd = ['/usr/bin/netstat', '-rn'] + (cmd_output, _) = subp.subp(netstat_cmd, rcs=[0, 1]) + print("------------------------------------------------------------") + print("Output of "+" ".join(netstat_cmd)) + print(cmd_output) + print("------------------------------------------------------------") + else: + aix_util.add_route("ipv4", gateway) + + run_autoconf6 = False + + # DNS configuration from subnets + for subnet in subnets: + dns_nameservers = subnet.get('dns_nameservers', []) + if dns_nameservers: + nameservers.extend(dns_nameservers) + + dns_search = subnet.get('dns_search', []) + if dns_search: + searchservers.extend(dns_search) + + # Enable services + if run_dhcpcd: + aix_util.enable_dhcpcd() +#As per the new tested order, we dont need these two steps here. +# if run_autoconf6: +# aix_util.enable_ndpd_host() +# aix_util.enable_autoconf6(ipv6_interface) + + # Write DNS configuration + if nameservers or searchservers: + aix_util.update_resolve_conf_file(resolve_conf_fn, + nameservers, searchservers) + + print("AIX: Returning", dev_names) + return dev_names +