--- ./cloudinit/distros/aix.py 2026-06-30 08:10:31.933047467 -0500 +++ ./cloudinit/distros/aix.py 2026-06-30 08:10:48.904321393 -0500 @@ -23,6 +23,7 @@ from cloudinit.distros import net_util from cloudinit.distros import rhel_util from cloudinit.distros import aix_util +from cloudinit.distros import aix_network_state from cloudinit.settings import PER_INSTANCE from cloudinit.distros.parsers.hostname import HostnameConf @@ -57,8 +58,19 @@ def apply_network_config_names(self, netconfig): LOG.debug("AIX does not rename network interface, netconfig=%s", netconfig) - def _write_network_state(self, settings): - raise NotImplementedError() + def _write_network_state(self, network_state): + """ + AIX implementation of _write_network_state. + + This directly processes the network_state object (similar to Linux), + bypassing the deprecated ENI text conversion path. + """ + LOG.debug("AIX: Using direct network_state processing") + dev_names = aix_network_state.write_network_state_aix( + network_state, + self.resolve_conf_fn + ) + return dev_names def _write_network(self, settings): print("aix.py _write_network settings=%s" % settings) --- ./cloudinit/distros/aix_util.py 2026-06-30 08:10:31.933047467 -0500 +++ ./cloudinit/distros/aix_util.py 2026-06-30 08:10:48.904321393 -0500 @@ -44,6 +44,47 @@ else: return devname +def bring_interface_down(aix_dev): + #Bring down an AIX network interface. + + LOG.debug("AIX: Checking state of interface %s", aix_dev) + current_state = get_if_attr(aix_dev, "state") + + if current_state == "down": + LOG.debug("AIX: Interface %s is already down", aix_dev) + time.sleep(1) + return True + + LOG.debug("AIX: Bringing down interface %s using chdev", aix_dev) + cmd = ["/usr/sbin/chdev", "-l", aix_dev, "-a", "state=down"] + try: + (_out, err) = subp.subp(cmd, rcs=[0, 1]) + time.sleep(1) + if len(err): + LOG.warning("AIX: Running %s resulted in stderr output: %s", cmd, err) + return True + except subp.ProcessExecutionError: + util.logexc(LOG, "AIX: Running interface command %s failed", cmd) + return False + +# Always bring interface UP using chdev. +def bring_interface_up(aix_dev): + LOG.debug("AIX: Bringing up interface %s using chdev", aix_dev) + cmd = ['/usr/sbin/chdev', '-l', aix_dev, '-a', 'state=up'] + try: + (_out, err) = subp.subp(cmd, rcs=[0, 1]) + time.sleep(1) + if len(err): + LOG.warning("AIX: Running %s resulted in stderr output: %s", cmd, err) + + return True + # Persist the bring-up command to /etc/rc.tcpip + #util.append_file("/etc/rc.tcpip", + # "%s\n" % (" ".join(bring_up_cmd))) + except subp.ProcessExecutionError: + util.logexc(LOG, "AIX: Running interface command %s failed", cmd) + return False + # Call chdev to add route def add_route(network, route): # First, delete the route if it exists on the system @@ -167,10 +208,10 @@ util.append_file(tmpf, "{\n") if info.get('netmask'): util.append_file(tmpf, " option 1 %s\n" % (info.get('netmask'))) - if interface == "en0": - if info.get('gateway'): - util.append_file(tmpf, " option 3 %s\n" % (info.get('gateway'))) + if info.get('gateway'): + util.append_file(tmpf, " option 3 %s\n" % (info.get('gateway'))) else: + # Reject gateway option for interfaces without a gateway util.append_file(tmpf, " reject 3\n") if info.get('address'): util.append_file(tmpf, " option 50 %s\n" % (info.get('address'))) --- ./cloudinit/distros/networking.py 2026-06-30 08:10:31.943062441 -0500 +++ ./cloudinit/distros/networking.py 2026-06-30 08:10:48.914326446 -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 2026-06-30 08:10:31.963090022 -0500 +++ ./cloudinit/net/__init__.py 2026-06-30 08:10:48.934347362 -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 2026-06-30 08:10:31.973095930 -0500 +++ ./cloudinit/sources/DataSourceConfigDrive.py 2026-06-30 08:10:48.954367147 -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 2026-06-30 08:10:31.872953258 -0500 +++ ./cloudinit/util.py 2026-06-30 08:10:48.854267485 -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-07-01 06:16:44.000262359 -0500 +++ ./cloudinit/distros/aix_network_state.py 2026-07-01 06:12:03.346824359 -0500 @@ -0,0 +1,330 @@ +# 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 (per-interface / per-network entries) + for subnet in subnets: + for ns in subnet.get('dns_nameservers', []): + if ns not in nameservers: + nameservers.append(ns) + + for sd in subnet.get('dns_search', []): + if sd not in searchservers: + searchservers.append(sd) + + # Collect global DNS from top-level network_data.json "services" entries. + # These are stored in network_state.dns_nameservers / dns_searchdomains + # after being converted to "nameserver" config entries by convert_net_json(). + for ns in network_state.dns_nameservers: + if ns not in nameservers: + nameservers.append(ns) + for sd in network_state.dns_searchdomains: + if sd not in searchservers: + searchservers.append(sd) + + # 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 + --- ./cloudinit/sources/helpers/openstack.py_orig 2026-07-01 06:13:10.580877683 -0500 +++ ./cloudinit/sources/helpers/openstack.py 2026-07-01 06:16:29.825473869 -0500 @@ -650,6 +650,26 @@ subnet["ipv4"] = True if network["type"] == "ipv6": subnet["ipv6"] = True + + # Look for either subnet or network specific DNS servers + # and add them as subnet level DNS entries. + # Subnet specific nameservers (from per-route services) + dns_nameservers = [ + service["address"] + for route in network.get("routes", []) + for service in route.get("services", []) + if service.get("type") == "dns" + ] + # Network specific nameservers + for service in network.get("services", []): + if service.get("type") != "dns": + continue + if service["address"] in dns_nameservers: + continue + dns_nameservers.append(service["address"]) + if dns_nameservers: + subnet["dns_nameservers"] = dns_nameservers + subnets.append(subnet) cfg.update({"subnets": subnets}) if link["type"] in ["bond"]: @@ -751,7 +771,7 @@ cfg["type"] = "infiniband" for service in services: - cfg = service + cfg = copy.deepcopy(service) cfg.update({"type": "nameserver"}) config.append(cfg)