From eb04f0d46908594ba3db98ed8c62f2d9c4483f2c Mon Sep 17 00:00:00 2001 From: AkashKTripathi Date: Mon, 18 Dec 2023 23:36:55 +0530 Subject: [PATCH] Support for AIX --- cloudinit/cmd/main.py | 7 +- cloudinit/config/cc_disable_ec2_metadata.py | 2 +- cloudinit/config/cc_growpart.py | 4 + cloudinit/config/cc_grub_dpkg.py | 3 + cloudinit/config/cc_keys_to_console.py | 5 + cloudinit/config/cc_power_state_change.py | 8 + cloudinit/config/cc_resizefs.py | 4 + cloudinit/config/cc_resolv_conf.py | 11 +- cloudinit/config/cc_restore_volume_groups.py | 235 ++++++++++ cloudinit/config/cc_set_hostname_from_dns.py | 86 ++++ .../cc_set_multipath_hcheck_interval.py | 71 +++ cloudinit/config/cc_set_passwords.py | 41 +- cloudinit/config/cc_ssh_import_id.py | 2 +- cloudinit/config/cc_update_bootlist.py | 205 +++++++++ cloudinit/config/cc_write_files.py | 5 + cloudinit/distros/__init__.py | 27 ++ cloudinit/distros/aix.py | 416 +++++++++++++++++ cloudinit/distros/aix_util.py | 432 ++++++++++++++++++ cloudinit/distros/net_util.py | 2 +- cloudinit/distros/networking.py | 15 + cloudinit/helpers.py | 15 +- cloudinit/netinfo.py | 135 +++++- cloudinit/settings.py | 14 +- cloudinit/sources/DataSourceConfigDrive.py | 42 +- cloudinit/sources/DataSourceNoCloud.py | 33 +- cloudinit/sources/__init__.py | 17 +- cloudinit/util.py | 26 +- config/cloud.cfg | 102 +++++ config/cloud.cfg.tmpl | 4 +- packages/aix/cloud-init.spec | 176 +++++++ setup.py | 4 + sysvinit/aix/cloud-config | 108 +++++ sysvinit/aix/cloud-final | 108 +++++ sysvinit/aix/cloud-init | 108 +++++ sysvinit/aix/cloud-init-local | 111 +++++ templates/hosts.aix.tmpl | 19 + .../unittests/main_fix_dhcp_default_route.py | 50 ++ tests/unittests/test_aix_apply_network.py | 59 +++ tests/unittests/test_apply_network.py | 86 ++++ tests/unittests/test_apply_network_2.py | 86 ++++ tests/unittests/test_cc_chef.0.7.7.py | 60 +++ tests/unittests/test_cc_chef.py | 59 +++ tests/unittests/test_cc_locale.py | 45 ++ tests/unittests/test_cc_power_state_change.py | 41 ++ tests/unittests/test_cc_resolv_conf.py | 46 ++ tests/unittests/test_cc_runcmd.py | 43 ++ tests/unittests/test_cc_set_hostname.py | 42 ++ tests/unittests/test_cc_set_passwords.py | 44 ++ tests/unittests/test_cc_ssh.py | 47 ++ tests/unittests/test_cc_ssh_import_id.py | 49 ++ tests/unittests/test_cc_timezone.py | 42 ++ tests/unittests/test_cc_update_etc_hosts.py | 40 ++ tests/unittests/test_cc_users_groups.py | 54 +++ tests/unittests/test_cc_write_files.py | 42 ++ tests/unittests/test_dhcp_apply_network.py | 117 +++++ tests/unittests/test_ipv6_apply_network.py | 128 ++++++ tests/unittests/test_netinfo_aix.py | 21 + tests/unittests/test_yaml_output.py | 83 ++++ tools/create_pvid_to_vg_mappings.sh | 15 + 59 files changed, 3821 insertions(+), 81 deletions(-) create mode 100644 cloudinit/config/cc_restore_volume_groups.py create mode 100644 cloudinit/config/cc_set_hostname_from_dns.py create mode 100644 cloudinit/config/cc_set_multipath_hcheck_interval.py create mode 100644 cloudinit/config/cc_update_bootlist.py create mode 100644 cloudinit/distros/aix.py create mode 100644 cloudinit/distros/aix_util.py create mode 100644 config/cloud.cfg create mode 100644 packages/aix/cloud-init.spec create mode 100644 sysvinit/aix/cloud-config create mode 100644 sysvinit/aix/cloud-final create mode 100644 sysvinit/aix/cloud-init create mode 100644 sysvinit/aix/cloud-init-local create mode 100644 templates/hosts.aix.tmpl create mode 100644 tests/unittests/main_fix_dhcp_default_route.py create mode 100644 tests/unittests/test_aix_apply_network.py create mode 100644 tests/unittests/test_apply_network.py create mode 100644 tests/unittests/test_apply_network_2.py create mode 100644 tests/unittests/test_cc_chef.0.7.7.py create mode 100644 tests/unittests/test_cc_chef.py create mode 100644 tests/unittests/test_cc_locale.py create mode 100644 tests/unittests/test_cc_power_state_change.py create mode 100644 tests/unittests/test_cc_resolv_conf.py create mode 100644 tests/unittests/test_cc_runcmd.py create mode 100644 tests/unittests/test_cc_set_hostname.py create mode 100644 tests/unittests/test_cc_set_passwords.py create mode 100644 tests/unittests/test_cc_ssh.py create mode 100644 tests/unittests/test_cc_ssh_import_id.py create mode 100644 tests/unittests/test_cc_timezone.py create mode 100644 tests/unittests/test_cc_update_etc_hosts.py create mode 100644 tests/unittests/test_cc_users_groups.py create mode 100644 tests/unittests/test_cc_write_files.py create mode 100644 tests/unittests/test_dhcp_apply_network.py create mode 100644 tests/unittests/test_ipv6_apply_network.py create mode 100644 tests/unittests/test_netinfo_aix.py create mode 100644 tests/unittests/test_yaml_output.py create mode 100644 tools/create_pvid_to_vg_mappings.sh diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index 7bb1c6e0210..eb641217ca2 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -386,7 +386,12 @@ def main_init(name, args): LOG.debug("manual cache clean found from marker: %s", mfile) existing = "trust" - init.purge_cache() + # rm_instance_lnk is now set to True. Else, cache will be used in case + # instead of optical disk to fetch network config. This check make sure + # that customer always uses latest config data even if the instance-id + # is matching + init.purge_cache(True) + # Stage 5 bring_up_interfaces = _should_bring_up_interfaces(init, args) diff --git a/cloudinit/config/cc_disable_ec2_metadata.py b/cloudinit/config/cc_disable_ec2_metadata.py index 4ad789c0bd2..da0a0f75534 100644 --- a/cloudinit/config/cc_disable_ec2_metadata.py +++ b/cloudinit/config/cc_disable_ec2_metadata.py @@ -18,7 +18,7 @@ from cloudinit.distros import ALL_DISTROS from cloudinit.settings import PER_ALWAYS -REJECT_CMD_IF = ["route", "add", "-host", "169.254.169.254", "reject"] +REJECT_CMD_IF = ["route", "add", "-host", "169.254.169.254", "127.0.0.1", "reject"] REJECT_CMD_IP = ["ip", "route", "add", "prohibit", "169.254.169.254"] LOG = logging.getLogger(__name__) diff --git a/cloudinit/config/cc_growpart.py b/cloudinit/config/cc_growpart.py index 9a08a067c23..9b5db6c8bc1 100644 --- a/cloudinit/config/cc_growpart.py +++ b/cloudinit/config/cc_growpart.py @@ -15,6 +15,7 @@ import os.path import re import stat +import platform from abc import ABC, abstractmethod from contextlib import suppress from pathlib import Path @@ -601,6 +602,9 @@ def resize_devices(resizer, devices): def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: + if platform.system().lower() == "aix": + return + if "growpart" not in cfg: LOG.debug( "No 'growpart' entry in cfg. Using default: %s", DEFAULT_CONFIG diff --git a/cloudinit/config/cc_grub_dpkg.py b/cloudinit/config/cc_grub_dpkg.py index a7e2fbfa58f..04b3571ae0f 100644 --- a/cloudinit/config/cc_grub_dpkg.py +++ b/cloudinit/config/cc_grub_dpkg.py @@ -141,6 +141,9 @@ def is_efi_booted() -> bool: def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: + if _cloud.distro.name not in distros: + return + mycfg = cfg.get("grub_dpkg", cfg.get("grub-dpkg", {})) if not mycfg: mycfg = {} diff --git a/cloudinit/config/cc_keys_to_console.py b/cloudinit/config/cc_keys_to_console.py index 88a09c43ed5..2a3726a77b3 100644 --- a/cloudinit/config/cc_keys_to_console.py +++ b/cloudinit/config/cc_keys_to_console.py @@ -10,6 +10,7 @@ import logging import os +import platform from textwrap import dedent from cloudinit import subp, util @@ -19,6 +20,7 @@ from cloudinit.settings import PER_INSTANCE # This is a tool that cloud init provides +HELPER_TOOL_TPL_AIX = "/opt/freeware/lib/cloud-init/write-ssh-key-fingerprints" HELPER_TOOL_TPL = "%s/cloud-init/write-ssh-key-fingerprints" distros = ["all"] @@ -72,6 +74,9 @@ def _get_helper_tool_path(distro): + if platform.system().lower() == "aix": + return HELPER_TOOL_TPL_AIX + try: base_lib = distro.usr_lib_exec except AttributeError: diff --git a/cloudinit/config/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py index 72e6634206e..968f950c68a 100644 --- a/cloudinit/config/cc_power_state_change.py +++ b/cloudinit/config/cc_power_state_change.py @@ -12,6 +12,7 @@ import re import subprocess import time +import platform from textwrap import dedent from cloudinit import subp, util @@ -24,6 +25,7 @@ frequency = PER_INSTANCE EXIT_FAIL = 254 +AIX = 0 MODULE_DESCRIPTION = """\ This module handles shutdown/reboot after all config modules have been run. By @@ -94,6 +96,9 @@ def givecmdline(pid): line = output.splitlines()[1] m = re.search(r"\d+ (\w|\.|-)+\s+(/\w.+)", line) return m.group(2) + elif AIX: + (ps_out, _err) = subp.subp(["/usr/bin/ps", "-p", str(pid), "-oargs="], rcs=[0, 1]) + return ps_out.strip() else: return util.load_file("/proc/%s/cmdline" % pid) except IOError: @@ -125,6 +130,9 @@ def check_condition(cond): def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: + if platform.system().lower() == "aix": + global AIX + AIX = 1 try: (args, timeout, condition) = load_power_state(cfg, cloud.distro) if args is None: diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py index dfcca4f0042..e2be60de6a7 100644 --- a/cloudinit/config/cc_resizefs.py +++ b/cloudinit/config/cc_resizefs.py @@ -12,6 +12,7 @@ import logging import os import stat +import platform from textwrap import dedent from cloudinit import subp, util @@ -235,6 +236,9 @@ def maybe_get_writable_device_path(devpath, info): def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: + if platform.system().lower() == "aix": + return + if len(args) != 0: resize_root = args[0] else: diff --git a/cloudinit/config/cc_resolv_conf.py b/cloudinit/config/cc_resolv_conf.py index aa88919cc33..6ada517e80d 100644 --- a/cloudinit/config/cc_resolv_conf.py +++ b/cloudinit/config/cc_resolv_conf.py @@ -9,6 +9,7 @@ """Resolv Conf: configure resolv.conf""" import logging +import platform from textwrap import dedent from cloudinit import templater, util @@ -66,6 +67,7 @@ "opensuse-tumbleweed", "photon", "rhel", + "aix", "sle_hpc", "sle-micro", "sles", @@ -119,7 +121,14 @@ def generate_resolv_conf(template_fn, params, target_fname): params["flags"] = flags LOG.debug("Writing resolv.conf from template %s", template_fn) - templater.render_to_file(template_fn, target_fname, params) + if platform.system().lower() == "aix": + templater.render_to_file(template_fn, '/etc/resolv.conf', params) + else: + # Network Manager likes to overwrite the resolv.conf file, so make sure + # it is immutable after write + subp.subp(["chattr", "-i", target_fname]) + templater.render_to_file(template_fn, target_fname, params) + subp.subp(["chattr", "+i", target_fname]) def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: diff --git a/cloudinit/config/cc_restore_volume_groups.py b/cloudinit/config/cc_restore_volume_groups.py new file mode 100644 index 00000000000..95408e4951b --- /dev/null +++ b/cloudinit/config/cc_restore_volume_groups.py @@ -0,0 +1,235 @@ +# ================================================================= +# +# (c) Copyright IBM Corp. 2015 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# ================================================================= + +import os +import re + +from cloudinit.settings import PER_INSTANCE +from cloudinit import util, subp + +frequency = PER_INSTANCE + + +IMPORTVG = "/usr/sbin/importvg" +LSPV = "/usr/sbin/lspv" +# an example of lspv output is shown below (where space = any whitespace): +# +LSVG = "/usr/sbin/lsvg" +MOUNT = "/usr/sbin/mount" + +MAPPING_FILE = '/opt/freeware/etc/cloud/pvid_to_vg_mappings' + + +def handle(name, _cfg, _cloud, log, _args): + log.debug('Attempting to restore non-rootVG volume groups.') + + if not os.access(MAPPING_FILE, os.R_OK): + log.warn('Physical volume ID to volume group mapping file "%s" does ' + 'not exist or permission to read the file does not exist. ' + 'Ensure that the file exists and the permissions for it are ' + 'correct.' % MAPPING_FILE) + return + + with open(MAPPING_FILE, 'r') as f: + mapping_file_lines = f.readlines() + + pvid_to_vg_map = parse_mapping_file(log, mapping_file_lines) + + for physical_volume_id, volume_group_name in list(pvid_to_vg_map.items()): + if volume_group_name.lower() == 'none': + # skip any physical volumes that were not associated with any + # volume group on the captured system + log.warn('The physical volume with ID "%s" was associated ' + 'with a volume group labeled "%s" on the ' + 'captured system. This physical volume will not ' + 'be associated with a volume group.' % + (physical_volume_id, volume_group_name)) + continue + + # Run lspv for each captured physical volume ID, so that any + # changes caused by importvg are picked up the next time + # through the loop. The effect of this is that importvg is + # only run once per volume group: "The PhysicalVolume parameter + # specifies only one physical volume to identify the volume group; any + # remaining physical volumes (those belonging to the same volume group) + # are found the importvg command and included in the import". + pv = get_physical_volume_from_lspv(log, physical_volume_id) + + if pv is None: + log.warn('The physical volume with ID "%s" was not found, so it ' + 'cannot be associated with volume group "%s".' % + (physical_volume_id, volume_group_name)) + continue + + if (pv['volume_group'] is None and not pv['active']): + # If the volume group is not set and the disk is not active, + # then set it. + set_volume_group(log, pv['hdisk'], volume_group_name) + + # Ensure that all volume groups captured are now present + expected_volume_groups = set(vg for vg in list(pvid_to_vg_map.values())) + existing_volume_groups = get_existing_volume_groups(log) + for group in expected_volume_groups: + if group not in existing_volume_groups: + msg = 'Volume group "%s" is not present.' % group + log.error(msg) + raise Exception(msg) + + # Ensure that all disks get mounted that are marked in /etc/filesystems as + # auto-mounting. These disks would normally get auto-mounted at system + # startup, but not after importvg. Note that some errors may appear for + # filesystems that are already mounted. + try: + out = subp.subp([MOUNT, "all"])[0] + log.debug(out) + except subp.ProcessExecutionError as e: + log.debug('Attempting to mount disks marked as auto-mounting resulted ' + 'in errors. This is likely due to attempting to mount ' + 'filesystems that are already mounted, therefore ' + 'ignoring: %s.' % e) + + # Clean up the mapping file + os.remove(MAPPING_FILE) + + +def parse_mapping_file(log, lines): + ''' + Parses the lines, skipping any blank lines, expecting the each line to be + a volume group name, whitespace, then a physical volume ID. E.g.: + + + + + Note that the order does not matter. Physical volume IDs that are "none" + and volume group names that are "rootvg" will raise exceptions as the + script generating the mapping file should not include those entries. + + Returns a dictionary with keys of physical volume IDs mapping to their + corresponding volume group name. + ''' + pvid_to_vg_map = {} + + for line in lines: + if line.strip() == '': + continue + + split_line = line.strip().split() + if len(split_line) != 2: + msg = ('Physical volume ID to volume group mapping file contains ' + 'lines in an invalid format. Each line should contain a ' + 'volume group name, a single space, then a physical volume ' + 'ID. Invalid line: "%s".' % line.strip()) + log.error(msg) + raise Exception(msg) + + volume_group_name, physical_volume_id = tuple(line.split()) + if physical_volume_id.lower() == 'none': + msg = ('Physical volume ID parsed as "%s", but there should be no ' + 'entries in the mapping file like this.' % + physical_volume_id) + log.error(msg) + raise Exception(msg) + if volume_group_name.lower() == 'rootvg': + msg = ('Volume group name parsed as "%s", but there should be no ' + 'entries in the mapping file like this.' % + volume_group_name) + log.error(msg) + raise Exception(msg) + + pvid_to_vg_map[physical_volume_id] = volume_group_name + + return pvid_to_vg_map + + +def get_physical_volume_from_lspv(log, physical_volume_id): + ''' + The output of the lspv command for a specific physical volume ID is + returned from this method as a dictionary representing the physical volume + ID. If the lspv command output does not contain any output corresponding + to the given physical volume ID, then None is returned. + + The dictionary returned is of the following format: + {'hdisk': , + 'physical_volume_id': , + 'volume_group': , + 'active': } + ''' + try: + env = os.environ + env['LANG'] = 'C' + lspv_out = subp.subp([LSPV], env=env)[0].strip() + except subp.ProcessExecutionError: + util.logexc(log, 'Failed to run lspv command.') + raise + + lspv_out_specific_pvid = re.findall(r'.*%s.*' % physical_volume_id, + lspv_out) + if len(lspv_out_specific_pvid) < 1: + return None + + lspv_specific_pvid = lspv_out_specific_pvid[0].split() + + if len(lspv_specific_pvid) < 3: + msg = ('Output from lspv does not match the expected format. The ' + 'expected output is of of the form " ' + ' ". The ' + 'actual output was: "%s".' % lspv_out_specific_pvid) + log.error(msg) + raise Exception(msg) + + volume_group = lspv_specific_pvid[2] + if volume_group.lower() == 'none': + volume_group = None + + physical_volume = { + 'hdisk': lspv_specific_pvid[0], + 'physical_volume_id': lspv_specific_pvid[1], + 'volume_group': volume_group, + 'active': 'active' in lspv_specific_pvid + } + + return physical_volume + + +def set_volume_group(log, hdisk, volume_group_name): + ''' + Uses the importvg command to set the volume group for the given hdisk. + ''' + try: + out = subp.subp([IMPORTVG, "-y", volume_group_name, hdisk])[0] + log.debug(out) + except subp.ProcessExecutionError: + util.logexc(log, 'Failed to set the volume group for disk ' + '%s.' % hdisk) + raise + + +def get_existing_volume_groups(log): + ''' + Uses the lsvg command to get all existing volume groups. + ''' + volume_groups = [] + try: + env = os.environ + env['LANG'] = 'C' + lsvg_out = subp.subp([LSVG], env=env)[0].strip() + volume_groups = lsvg_out.split('\n') + volume_groups = [vg.strip() for vg in volume_groups] + except subp.ProcessExecutionError: + util.logexc(log, 'Failed to run lsvg command.') + raise + return volume_groups diff --git a/cloudinit/config/cc_set_hostname_from_dns.py b/cloudinit/config/cc_set_hostname_from_dns.py new file mode 100644 index 00000000000..5e52da7a985 --- /dev/null +++ b/cloudinit/config/cc_set_hostname_from_dns.py @@ -0,0 +1,86 @@ +# ================================================================= +# Licensed Materials - Property of IBM +# +# (c) Copyright IBM Corp. 2015 All Rights Reserved +# +# US Government Users Restricted Rights - Use, duplication or +# disclosure restricted by GSA ADP Schedule Contract with IBM Corp. +# ================================================================= + +from cloudinit.settings import PER_INSTANCE +from cloudinit import util +from cloudinit import netinfo +import socket + + +frequency = PER_INSTANCE + + +def handle(name, _cfg, _cloud, log, _args): + default_interface = 'eth0' + system_info = util.system_info() + if 'aix' in system_info['platform'].lower(): + default_interface = 'en0' + + interface = util.get_cfg_option_str(_cfg, + 'set_hostname_from_interface', + default=default_interface) + log.debug('Setting hostname based on interface %s' % interface) + set_hostname = False + fqdn = None + # Look up the IP address on the interface + # and then reverse lookup the hostname in DNS + info = netinfo.netdev_info() + if interface in info: + set_short = util.get_cfg_option_bool(_cfg, "set_dns_shortname", False) + if 'ipv4' in info[interface] and info[interface]['ipv4']: + # Handle IPv4 address from network_eni format + ipv4 = info[interface]['ipv4'] + set_hostname =_set_hostname(_cfg, _cloud, log, + ipv4[0]['ip'], set_short) + elif 'addr' in info[interface] and info[interface]['addr']: + # Handle IPv4 address + set_hostname =_set_hostname(_cfg, _cloud, log, + info[interface]['addr'], set_short) + elif 'addr6' in info[interface] and info[interface]['addr6']: + # Handle IPv6 addresses + for ipaddr in info[interface]['addr6']: + ipaddr = ipaddr.split('/')[0] + set_hostname = _set_hostname(_cfg, _cloud, log, ipaddr, + set_short) + if set_hostname: + break + else: + log.warning('Interface %s was not found on the system. ' + 'Interfaces found on system: %s' % (interface, + list(info.keys()))) + + # Reverse lookup failed, fall back to cc_set_hostname way. + if not set_hostname: + (short_hostname, fqdn) = util.get_hostname_fqdn(_cfg, _cloud) + try: + log.info('Fall back to setting hostname on VM as %s' % fqdn) + _cloud.distro.set_hostname(short_hostname, fqdn=fqdn) + except Exception: + util.logexc(log, "Failed to set the hostname to %s", fqdn) + raise + + +def _set_hostname(_cfg, _cloud, log, ipaddr, set_short): + log.debug('ipaddr: %s' % ipaddr) + try: + addrinfo = socket.getaddrinfo(ipaddr, None, 0, socket.SOCK_STREAM) + log.debug('addrinfo: %s' % addrinfo) + if addrinfo: + (fqdn, port) = socket.getnameinfo(addrinfo[0][4], + socket.NI_NAMEREQD) + if fqdn: + log.info('Setting hostname on VM as %s' % fqdn) + hostname = fqdn.split('.')[0] if set_short else fqdn + _cloud.distro.set_hostname(hostname, fqdn=hostname) + return True + except socket.error: + log.warning('No hostname found for IP address %s' % ipaddr) + except socket.gaierror: + log.warning('Unable to resolve hostname for IP address %s' % ipaddr) + return False diff --git a/cloudinit/config/cc_set_multipath_hcheck_interval.py b/cloudinit/config/cc_set_multipath_hcheck_interval.py new file mode 100644 index 00000000000..391b1eef37a --- /dev/null +++ b/cloudinit/config/cc_set_multipath_hcheck_interval.py @@ -0,0 +1,71 @@ +# ================================================================= +# +# (c) Copyright IBM Corp. 2015 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# ================================================================= + +import os + +from cloudinit.settings import PER_INSTANCE +from cloudinit import util, subp + +frequency = PER_INSTANCE + +LSPATH = '/usr/sbin/lspath' +CHDEV = '/usr/sbin/chdev' + +DEFAULT_INTERVAL = 60 + + +def handle(name, _cfg, _cloud, log, _args): + hcheck_interval = util.get_cfg_option_str(_cfg, + 'multipath_hcheck_interval', + default=DEFAULT_INTERVAL) + try: + hcheck_interval = int(hcheck_interval) + except ValueError: + log.warn('The hcheck interval for multipath specified in the ' + 'cloud.cfg file, "%s", could not be converted to an integer. ' + 'Ensure that the interval is specified as an integer. ' + 'The default interval of %d seconds is being used.' % + (hcheck_interval, DEFAULT_INTERVAL)) + hcheck_interval = DEFAULT_INTERVAL + + log.debug('Attempting to set the multipath hcheck interval to %d' % + hcheck_interval) + + hdisks = [] + try: + hdisks = subp.subp([LSPATH, '-F', 'name'])[0].strip().split('\n') + hdisks = set(hdisks) + except subp.ProcessExecutionError: + util.logexc(log, 'Failed to get paths to multipath device.') + raise + + if len(hdisks) < 1: + raise Exception('Failed to find any paths to multipath device.') + + # Permanently change the hcheck interval for each disk + for hdisk in hdisks: + try: + env = os.environ + env['LANG'] = 'C' + out = subp.subp([CHDEV, '-l', hdisk, '-a', + 'hcheck_interval=%d' % hcheck_interval, '-P'], + env=env)[0] + log.debug(out) + except subp.ProcessExecutionError: + util.logexc(log, 'Failed to permanently change the hcheck ' + 'interval for hdisk "%s".' % hdisk) + raise diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py index 24d8267ad23..4c25dfe9767 100644 --- a/cloudinit/config/cc_set_passwords.py +++ b/cloudinit/config/cc_set_passwords.py @@ -9,6 +9,7 @@ import logging import re +import platform from string import ascii_letters, digits from textwrap import dedent from typing import List @@ -154,18 +155,26 @@ def handle_ssh_pwauth(pw_auth, distro: Distro): return if distro.uses_systemd(): - state = subp.subp( - [ - "systemctl", - "show", - "--property", - "ActiveState", - "--value", - service, - ] - ).stdout.strip() - if state.lower() in ["active", "activating", "reloading"]: - _restart_ssh_daemon(distro, service) + if platform.system().lower() == "aix": + cmd = ['/usr/bin/stopsrc', '-s', 'sshd'] + # Allow 0 and 1 return codes since it will return 1 if sshd is + # currently down. + subp.subp(cmd, rcs=[0, 1]) + cmd = ["/usr/bin/startsrc", "-s", "sshd"] + subp.subp(cmd) + else: + state = subp.subp( + [ + "systemctl", + "show", + "--property", + "ActiveState", + "--value", + service, + ] + ).stdout.strip() + if state.lower() in ["active", "activating", "reloading"]: + _restart_ssh_daemon(distro, service) else: _restart_ssh_daemon(distro, service) @@ -291,8 +300,12 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: expired_users = [] for u in users_to_expire: try: - distro.expire_passwd(u) - expired_users.append(u) + if platform.system().lower() == "aix": + subp.subp(["/usr/bin/pwdadm", "-f", "ADMCHG", u]) + expired_users.append(u) + else: + distro.expire_passwd(u) + expired_users.append(u) except Exception as e: errors.append(e) util.logexc(LOG, "Failed to set 'expire' for %s", u) diff --git a/cloudinit/config/cc_ssh_import_id.py b/cloudinit/config/cc_ssh_import_id.py index a4ca1b981f7..047d14544a9 100644 --- a/cloudinit/config/cc_ssh_import_id.py +++ b/cloudinit/config/cc_ssh_import_id.py @@ -19,7 +19,7 @@ from cloudinit.settings import PER_INSTANCE # https://launchpad.net/ssh-import-id -distros = ["alpine", "cos", "debian", "ubuntu"] +distros = ["alpine", "cos", "debian", "ubuntu", "aix"] SSH_IMPORT_ID_BINARY = "ssh-import-id" MODULE_DESCRIPTION = """\ diff --git a/cloudinit/config/cc_update_bootlist.py b/cloudinit/config/cc_update_bootlist.py new file mode 100644 index 00000000000..1f70e0b51a3 --- /dev/null +++ b/cloudinit/config/cc_update_bootlist.py @@ -0,0 +1,205 @@ +# ================================================================= +# +# (c) Copyright IBM Corp. 2015 +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# ================================================================= + +import re + +from cloudinit.settings import PER_INSTANCE +from cloudinit import util, subp + +frequency = PER_INSTANCE + +BOOTINFO = "/usr/sbin/bootinfo" +BOOTLIST = "/usr/sbin/bootlist" +BOOTLIST_AIX = '/usr/bin/bootlist' +LSPROP = '/usr/sbin/lsprop' +CHOSEN_DEVICE_TREE = "/proc/device-tree/chosen" +OFPATHNAME = "/usr/sbin/ofpathname" +CPUINFO = "/proc/cpuinfo" +QEMU_STRING = 'emulated by qemu' + + +def handle(name, _cfg, _cloud, log, _args): + log.debug('Attempting to configure the boot list.') + system_info = util.system_info() + + if 'aix' in system_info['platform'].lower(): + try: + boot_devices = subp.subp([BOOTINFO, + "-b"])[0].strip().split('\n') + out = run_bootlist_command(log, mode='normal', fmt='logical', + boot_devices=boot_devices, + cmd_location=BOOTLIST_AIX) + log.debug(out) + return + except subp.ProcessExecutionError: + util.logexc(log, 'Failed to set the bootlist.') + raise + + if is_powerkvm(log): + log.debug('Not configuring the boot list since this VM is running on ' + 'PowerKVM.') + return + + architecture = system_info['uname'][4] + if 'ppc' not in architecture: + return + + orig_normal_bootlist = run_bootlist_command(log, mode='normal', + fmt='ofpath').split('\n') + orig_service_bootlist = run_bootlist_command(log, mode='service', + fmt='ofpath').split('\n') + + (dist, vers) = system_info['dist'][:2] + major_release = (int)(vers.split('.')[0]) + device_paths = [] + if dist.startswith('Red Hat Enterprise Linux'): + log.debug('RHEL version: %s' % vers) + if major_release == 6: + device_paths = get_device_paths_from_file(log, '/etc/yaboot.conf') + else: + device_paths = [get_last_booted_device(log)] + elif dist.startswith('SUSE Linux Enterprise'): + log.debug('SLES version: %s' % vers) + if major_release == 11: + device_paths = get_device_paths_from_file(log, '/etc/lilo.conf') + else: + device_paths = [get_last_booted_device(log)] + elif dist.startswith('Ubuntu'): + log.debug('Ubuntu version: %s' % vers) + device_paths = [get_last_booted_device(log)] + else: + raise NotImplementedError('Not yet implemented for (%s, %s)' % + (dist, vers)) + + # Running the bootlist command using the ofpath format requires ofpathname + # to work properly. On RHEL 6.4, ofpathname may fail if the 'bc' package + # is not installed, causing bootlist to have some strange behavior when + # setting the bootlist. In order to avoid setting an invalid bootlist, we + # will fail if ofpathname does not work properly. + # Example: `bootlist -m both -o` returns: + # ofpathname: 'bc' command not found. Please, install 'bc' package + try: + subp.subp([OFPATHNAME]) + except subp.ProcessExecutionError: + util.logexc(log, 'The ofpathname command returned errors. Since the ' + 'bootlist command relies on ofpathname, these errors need ' + 'to be resolved.') + raise + + if len(device_paths) > 0: + out = run_bootlist_command(log, mode='both', fmt='ofpath', + boot_devices=device_paths) + log.debug(out) + + successful = (verify_bootlist(log, 'normal', orig_normal_bootlist) and + verify_bootlist(log, 'service', orig_service_bootlist)) + if not successful: + msg = 'Failed to update the bootlist properly.' + log.error(msg) + raise Exception(msg) + + +def get_device_paths_from_file(log, conf_file): + device_paths = [] + try: + with open(conf_file, 'r') as f: + conf_contents = f.read() + device_paths = [s.strip() for s in re.findall(r'^boot.*=(.*)', conf_contents, + re.MULTILINE)] + return device_paths + except: + util.logexc(log, 'Failed to get device paths from conf file.') + raise + + if len(device_paths) < 1: + msg = 'No device paths were found in the conf file.' + log.error(msg) + raise Exception(msg) + + +def get_last_booted_device(log): + try: + lsprop_out = subp.subp([LSPROP, CHOSEN_DEVICE_TREE])[0].strip() + bootpath_matches = re.findall(r'^bootpath.*\"(.*)\"', lsprop_out, + re.MULTILINE) + if len(bootpath_matches) < 1: + raise Exception('Did not find a bootpath entry in the lsprop ' + 'output:\n%s' % lsprop_out) + device_in_ofpath_format = bootpath_matches[0].split(',')[0] + device_in_logical_format = subp.subp([OFPATHNAME, '-l', + device_in_ofpath_format])[0] + + return device_in_logical_format.strip() + except subp.ProcessExecutionError: + util.logexc(log, 'Failed to get the last booted device.') + raise + + +def run_bootlist_command(log, mode, fmt, boot_devices=[], + cmd_location=BOOTLIST): + if fmt == "logical": + fmt = "-o" + elif fmt == "ofpath": + fmt = "-r" + + cmd = [cmd_location, "-m", mode, fmt] + cmd += boot_devices + + try: + out = subp.subp(cmd)[0] + return out.strip() + except: + util.logexc(log, 'Bootlist command failed.') + raise + + +def verify_bootlist(log, mode, orig_bootlist): + successful = True + new_bootlist = run_bootlist_command(log, mode=mode, fmt='logical') + log.debug('%s mode boot list in ofpath format before ' + 'configuration: %s' % (mode.capitalize(), orig_bootlist)) + log.debug('%s mode boot list in logical format after ' + 'configuration: %s' % (mode.capitalize(), new_bootlist)) + if new_bootlist.startswith('ofpathname'): + successful = False + log.warn('The updated %s mode bootlist is not correct. Attempting to ' + 'revert the change.' % mode) + run_bootlist_command(log, mode=mode, fmt='ofpath', + boot_devices=orig_bootlist) + + # Check if the reversion was successful + reverted_bootlist = run_bootlist_command(log, mode=mode, fmt='logical') + if 'ofpathname' in reverted_bootlist: + log.warn('Failed to revert the change, so attempting to use the ' + 'last booted device as the bootlist.') + run_bootlist_command(log, mode=mode, fmt='ofpath', + boot_devices=[get_last_booted_device(log)]) + last_chance_bootlist = run_bootlist_command(log, mode=mode, + fmt='logical') + if 'ofpathname' not in last_chance_bootlist: + successful = True + + return successful + + +def is_powerkvm(log): + try: + out = subp.subp(["cat", CPUINFO])[0] + return QEMU_STRING in out.lower() + except: + util.logexc(log, 'Failed to determine if VM is running on PowerKVM.') + raise diff --git a/cloudinit/config/cc_write_files.py b/cloudinit/config/cc_write_files.py index 8bab6d76e7a..a195dfa0119 100644 --- a/cloudinit/config/cc_write_files.py +++ b/cloudinit/config/cc_write_files.py @@ -9,6 +9,7 @@ import base64 import logging import os +import platform from textwrap import dedent from cloudinit import util @@ -119,6 +120,10 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: + if platform.system().lower() == "aix": + global DEFAULT_OWNER + DEFAULT_OWNER = "root:system" + file_list = cfg.get("write_files", []) filtered_files = [ f diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 79e2623562f..39167d377a8 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -91,6 +91,7 @@ ], "openeuler": ["openeuler"], "OpenCloudOS": ["OpenCloudOS", "TencentOS"], + "aix": ["aix"], } LOG = logging.getLogger(__name__) @@ -300,6 +301,9 @@ def _find_tz_file(self, tz): ) return tz_file + def get_init_cmd(self): + return self.init_cmd + def get_option(self, opt_name, default=None): return self._cfg.get(opt_name, default) @@ -364,6 +368,7 @@ def apply_network(self, settings, bring_up=True): # pylint: enable=assignment-from-no-return # Now try to bring them up if bring_up: + self._bring_down_interfaces(dev_names) return self._bring_up_interfaces(dev_names) return False @@ -625,6 +630,28 @@ def _bring_up_interfaces(self, device_names): return True return False + def _bring_down_interface(self, device_name): + cmd = ["ifdown", device_name] + LOG.debug("Attempting to run bring down interface %s using command %s", + device_name, cmd) + try: + (_out, err) = subp.subp(cmd) + if len(err): + LOG.warn("Running %s resulted in stderr output: %s", cmd, err) + return True + except subp.ProcessExecutionError: + util.logexc(LOG, "Running interface command %s failed", cmd) + return False + + def _bring_down_interfaces(self, device_names): + am_failed = 0 + for d in device_names: + if not self._bring_down_interface(d): + am_failed += 1 + if am_failed == 0: + return True + return False + def get_default_user(self): return self.get_option("default_user") diff --git a/cloudinit/distros/aix.py b/cloudinit/distros/aix.py new file mode 100644 index 00000000000..9d6f12dea89 --- /dev/null +++ b/cloudinit/distros/aix.py @@ -0,0 +1,416 @@ +# vi: ts=4 expandtab +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import time +import subprocess +from cloudinit import distros +from cloudinit import helpers +from cloudinit import log as logging +from cloudinit import util, subp +from cloudinit import ssh_util + +from cloudinit.distros import net_util +from cloudinit.distros import rhel_util +from cloudinit.distros import aix_util +from cloudinit.settings import PER_INSTANCE + +from cloudinit.distros.parsers.hostname import HostnameConf +from cloudinit.distros.networking import AIXNetworking + +LOG = logging.getLogger(__name__) + + +class Distro(distros.Distro): + hostname_conf_fn = "/etc/hosts" + resolve_conf_fn = "/etc/resolv.conf" + networking_cls = AIXNetworking + + def __init__(self, name, cfg, paths): + distros.Distro.__init__(self, name, cfg, paths) + # This will be used to restrict certain + # calls from repeatly happening (when they + # should only happen say once per instance...) + self._runner = helpers.Runners(paths) + self.osfamily = "aix" + + def install_packages(self, pkglist): + self.package_command("install", pkgs=pkglist) + + def apply_network(self, settings, bring_up=True): + # Write it out + dev_names = self._write_network(settings) + LOG.debug("AIX: always bring up interfaces %s", dev_names) + self._bring_down_interfaces(dev_names) + return self._bring_up_interfaces(dev_names) + + 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(self, settings): + print("aix.py _write_network settings=%s" % settings) + entries = net_util.translate_network(settings) + aix_util.remove_resolve_conf_file(self.resolve_conf_fn) + print("Translated ubuntu style network settings %s into %s" % (settings, entries)) + + # Make the intermediate format as the rhel format... + nameservers = [] + searchservers = [] + dev_names = list(entries.keys()) + 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() + + # 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) + + for (dev, info) in list(entries.items()): + print("dev %s info %s" % (dev, info)) + + for (dev, info) in list(entries.items()): + run_cmd = 0 + ipv6_present = 0 + chdev_cmd = ['/usr/sbin/chdev'] + + if dev not in 'lo': + aix_dev = aix_util.translate_devname(dev) + print("dev %s aix_dev %s" % (dev, aix_dev)) + if info.get('bootproto') == 'dhcp': + aix_util.config_dhcp(aix_dev, info, create_dhcp_file) + create_dhcp_file = False + run_dhcpcd = True + else: + chdev_cmd.extend(['-l', aix_dev]) + + ipv6_info = info.get("ipv6") + if ipv6_info is not None and len(ipv6_info) > 0: + run_cmd = 1 + ipv6_present = 1 + run_autoconf6 = True + ipv6_address = ipv6_info.get("address") + ipv6_netmask = ipv6_info.get("netmask") + if ipv6_address is not None: + ipv6_addr = ipv6_address.split('/') + chdev_cmd.append('-anetaddr6=' + ipv6_addr[0]) + if ipv6_netmask is not None: + chdev_cmd.append('-aprefixlen=' + ipv6_netmask) + + if ipv6_interface is None: + ipv6_interface = aix_dev + else: + ipv6_interface = "any" + + else: + run_cmd = 1 + ipv6_present = 0 + ipv4_address = info.get("address") + ipv4_netmask = info.get("netmask") + if ipv4_address is not None: + chdev_cmd.append('-anetaddr=' + ipv4_address) + if ipv4_netmask is not None: + chdev_cmd.append('-anetmask=' + ipv4_netmask) + + if run_autoconf6: + command = "/usr/sbin/autoconf6 -i en0" + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + time.sleep(2) + command = "stopsrc -s ndpd-host ; sleep 3 ; startsrc -s ndpd-host ;" + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + + if run_cmd: + try: + print("running ", chdev_cmd) + subp.subp(chdev_cmd, logstring=chdev_cmd) + time.sleep(2) + + if ipv6_present: + util.append_file("/etc/rc.tcpip", "%s\n" % (" ".join(chdev_cmd))) + + except Exception as e: + raise e + + if 'mtu' in info: + if info['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=" + info['mtu']], capture=False, rcs=[0, 1]) + time.sleep(2) + if aix_dev == "en0": + if run_autoconf6 is True: + aix_util.add_route("ipv6", ipv6_info.get('gateway')) + #aix_util.disable_ndpd_host() + #aix_util.enable_ndpd_host() + util.append_file("/etc/rc.tcpip", "%s\n" % ("stopsrc -s ndpd-host ; sleep 3 ; startsrc -s ndpd-host ; #To remove default gateway for cloud-init ")) + util.append_file("/etc/rc.tcpip", "%s\n" % (" ".join(chdev_cmd))) + else: + aix_util.add_route("ipv4", info.get('gateway')) + + if 'dns-nameservers' in info: + nameservers.extend(info['dns-nameservers']) + if 'dns-search' in info: + searchservers.extend(info['dns-search']) + + if run_dhcpcd: + aix_util.enable_dhcpcd() + if run_autoconf6: + aix_util.enable_ndpd_host() + aix_util.enable_autoconf6(ipv6_interface) + + if ((nameservers and len(nameservers) > 0) or ( + searchservers and len(searchservers) > 0)): + aix_util.update_resolve_conf_file(self.resolve_conf_fn, nameservers, searchservers) + print("returning ", dev_names) + return dev_names + + def apply_locale(self, locale, out_fn=None): + subp.subp(["/usr/bin/chlang", "-M", str(locale)]) + + def _write_hostname(self, hostname, out_fn): + # Permanently change the hostname for inet0 device in the ODM + subp.subp(["/usr/sbin/chdev", "-l", "inet0", "-a", "hostname=" + str(hostname)]) + + shortname = hostname.split('.')[0] + # Change the node for the uname process + subp.subp(["/usr/bin/uname", "-S", str(shortname)[0:32]]) + + def _read_system_hostname(self): + host_fn = self.hostname_conf_fn + return (host_fn, self._read_hostname(host_fn)) + + def _read_hostname(self, filename, default=None): + (out, _err) = subp.subp(["/usr/bin/hostname"]) + if len(out): + return out + else: + return default + + def _bring_up_interface(self, device_name): + if device_name in 'lo': + return True + + cmd = ["/usr/sbin/chdev", "-l", aix_util.translate_devname(device_name), "-a", "state=up"] + LOG.debug("Attempting to run bring up interface %s using command %s", device_name, cmd) + try: + (_out, err) = subp.subp(cmd) + time.sleep(1) + if len(err): + LOG.warn("Running %s resulted in stderr output: %s", cmd, err) + return True + except subp.ProcessExecutionError: + util.logexc(LOG, "Running interface command %s failed", cmd) + return False + + def _bring_up_interfaces(self, device_names): + if device_names and 'all' in device_names: + raise RuntimeError(('Distro %s can not translate the device name "all"') % (self.name)) + for d in device_names: + if not self._bring_up_interface(d): + return False + return True + + def _bring_down_interface(self, device_name): + if device_name in 'lo': + return True + + interface = aix_util.translate_devname(device_name) + LOG.debug("Attempting to run bring down interface %s device_name %s", interface, device_name) + if aix_util.get_if_attr(interface, "state") == "down": + time.sleep(1) + return True + else: + cmd = ["/usr/sbin/chdev", "-l", interface, "-a", "state=down"] + LOG.debug("Attempting to run bring down interface %s using command %s", device_name, cmd) + try: + (_out, err) = subp.subp(cmd, rcs=[0, 1]) + time.sleep(1) + if len(err): + LOG.warn("Running %s resulted in stderr output: %s", cmd, err) + return True + except subp.ProcessExecutionError: + util.logexc(LOG, "Running interface command %s failed", cmd) + return False + + def _bring_down_interfaces(self, device_names): + if device_names and 'all' in device_names: + raise RuntimeError(('Distro %s can not translate the device name "all"') % (self.name)) + am_failed = 0 + for d in device_names: + if not self._bring_down_interface(d): + am_failed += 1 + if am_failed == 0: + return True + return False + + def set_timezone(self, tz): + cmd = ["/usr/bin/chtz", tz] + subp.subp(cmd) + + def package_command(self, command, args=None, pkgs=None): + if pkgs is None: + pkgs = [] + + if subp.which("dnf"): + LOG.debug("Using DNF for package management") + cmd = ["dnf"] + else: + LOG.debug("Using YUM for package management") + cmd = ["yum", "-t"] + # Determines whether or not yum prompts for confirmation + # of critical actions. We don't want to prompt... + cmd.append("-y") + + if args and isinstance(args, str): + cmd.append(args) + elif args and isinstance(args, list): + cmd.extend(args) + + cmd.append(command) + + pkglist = util.expand_package_list("%s-%s", pkgs) + cmd.extend(pkglist) + + # Allow the output of this to flow outwards (ie not be captured) + subp.subp(cmd, capture=False) + + def update_package_sources(self): + self._runner.run("update-sources", self.package_command, + ["makecache"], freq=PER_INSTANCE) + + def add_user(self, name, **kwargs): + if util.is_user(name): + LOG.info("User %s already exists, skipping.", name) + return False + + adduser_cmd = ["/usr/sbin/useradd"] + log_adduser_cmd = ["/usr/sbin/useradd"] + + adduser_opts = { + "homedir": '-d', + "gecos": '-c', + "primary_group": '-g', + "groups": '-G', + "shell": '-s', + "expiredate" : '-e', + } + + redact_opts = ["passwd"] + + for key, val in list(kwargs.items()): + if key in adduser_opts and val and isinstance(val, str): + adduser_cmd.extend([adduser_opts[key], val]) + + # Redact certain fields from the logs + if key in redact_opts: + log_adduser_cmd.extend([adduser_opts[key], 'REDACTED']) + else: + log_adduser_cmd.extend([adduser_opts[key], val]) + + if 'no_create_home' in kwargs or 'system' in kwargs: + adduser_cmd.append('-d/nonexistent') + log_adduser_cmd.append('-d/nonexistent') + else: + adduser_cmd.append('-m') + log_adduser_cmd.append('-m') + + adduser_cmd.append(name) + log_adduser_cmd.append(name) + + # Run the command + LOG.debug("Adding user %s", name) + try: + subp.subp(adduser_cmd, logstring=log_adduser_cmd) + except Exception as e: + util.logexc(LOG, "Failed to create user %s", name) + raise e + + def create_user(self, name, **kwargs): + """ + Creates users for the system using the GNU passwd tools. This + will work on an GNU system. This should be overriden on + distros where useradd is not desirable or not available. + """ + # Add the user + self.add_user(name, **kwargs) + + # Set password if plain-text password provided and non-empty + if 'plain_text_passwd' in kwargs and kwargs['plain_text_passwd']: + self.set_passwd(name, kwargs['plain_text_passwd']) + + # Default locking down the account. 'lock_passwd' defaults to True. + # lock account unless lock_password is False. + if kwargs.get('lock_passwd', True): + self.lock_passwd(name) + + # Configure sudo access + if 'sudo' in kwargs: + self.write_sudo_rules(name, kwargs['sudo']) + + # Import SSH keys + if 'ssh_authorized_keys' in kwargs: + keys = set(kwargs['ssh_authorized_keys']) or [] + ssh_util.setup_user_keys(keys, name, options=None) + return True + + def lock_passwd(self, name): + """ + Lock the password of a user, i.e., disable password logins + """ + try: + # Need to use the short option name '-l' instead of '--lock' + # (which would be more descriptive) since SLES 11 doesn't know + # about long names. + subp.subp(["/usr/bin/chuser", "account_locked=true", name]) + except Exception as e: + util.logexc(LOG, 'Failed to disable password for user %s', name) + raise e + + def create_group(self, name, members): + group_add_cmd = ['/usr/bin/mkgroup', name] + + # Check if group exists, and then add it doesn't + if util.is_group(name): + LOG.warn("Skipping creation of existing group '%s'" % name) + else: + try: + subp.subp(group_add_cmd) + LOG.info("Created new group %s" % name) + except Exception: + util.logexc("Failed to create group %s", name) + + # Add members to the group, if so defined + if len(members) > 0: + for member in members: + if not util.is_user(member): + LOG.warn("Unable to add group member '%s' to group '%s'; user does not exist.", member, name) + continue + + subp.subp(["/usr/sbin/usermod", "-G", name, member]) + LOG.info("Added user '%s' to group '%s'" % (member, name)) diff --git a/cloudinit/distros/aix_util.py b/cloudinit/distros/aix_util.py new file mode 100644 index 00000000000..a7bb61a567c --- /dev/null +++ b/cloudinit/distros/aix_util.py @@ -0,0 +1,432 @@ +# vi: ts=4 expandtab +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import os +import re +import contextlib +import subprocess +import time + +from cloudinit.distros.parsers.resolv_conf import ResolvConf + +from cloudinit import log as logging +from cloudinit import util, subp, temp_utils + + +LOG = logging.getLogger(__name__) + + +# Translate Linux ethernet device name ie. eth0 to AIX form ie. en0 +def translate_devname(devname): + device = re.compile('eth[0-9]+') + if device.match(devname): + return devname.replace('th', 'n') + else: + return devname + +# Translate AIX interface name ie. en0 to logical adapter name ie. ent0 +def logical_adpt_name(devname): + device = re.compile('en[0-9]+') + if device.match(devname): + return devname.replace('n', 'nt') + else: + return devname + +# Call chdev to add route +def add_route(network, route): + # First, delete the route if it exists on the system + del_route(network, route) + + # Add the route if there isn't already a default route + cmd = ['/usr/sbin/chdev', '-l', 'inet0'] + + if route: + if network == 'ipv4': + cmd.extend(["-aroute=" + "net,-hopcount,0,,0," + route]) + elif network == 'ipv6': + cmd.extend(["-arout6=" + "net,-hopcount,0,,,::," + route]) + cmd_string=' '.join(cmd) + subp.subp(cmd, capture=False, rcs=[0, 1]) + time.sleep(2) + + util.append_file("/etc/rc.tcpip", "%s\n" % (" ".join(cmd))) + if network == 'ipv6': + print("netstat -rn") + command = "/usr/bin/netstat -rn | grep default" + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + print(output) + print("netstat -in") + command = "/usr/bin/netstat -in " + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + print(output) + cmd_out = subprocess.check_output(cmd_string, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + + try: + print(cmd_string) + cmd_out = subprocess.check_output(cmd_string, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + print(cmd_out) + time.sleep(2) + command = "/usr/bin/netstat -rn | grep default" + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + print(output) + # Run the command and capture its output + command = "netstat -rn | grep -v link | grep default" + output = subprocess.check_output(command, shell=True, text=True) + + + except subprocess.CalledProcessError as e: + cmd_out= subprocess.check_output(cmd_string, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + print(cmd_out) + time.sleep(2) + else: + # Split the output into lines and count them + lines = output.split('\n') + non_empty_lines = [line for line in lines if line.strip() != ""] + line_count = len(non_empty_lines) + print("line_count :", line_count) + # Check if there are more than one line + if line_count > 1: + print("stopsrc -s ndpd-host ; sleep 3 ; startsrc -s ndpd-host ;") + command = "stopsrc -s ndpd-host ; sleep 3 ; startsrc -s ndpd-host ;" + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + else: + cmd_out= subprocess.check_output(cmd_string, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + print(cmd_out) + time.sleep(2) + command = "/usr/bin/netstat -rn | grep default" + output = subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT, universal_newlines=True) + print(output) + + + +# Call chdev to delete default route +def del_route(network, route): + # if route exists, delete it + route_out = get_route(network) + print("del_route: network %s route %s route_out %s" % (network, route, route_out)) + if route_out is not None: + cmd = ['/usr/sbin/chdev', '-l', 'inet0'] + if network == 'ipv4' and route in route_out.split(","): + cmd.append("-adelroute=\"" + route_out + "\"") + elif network == 'ipv6' and route in route_out.split(","): + cmd.append("-adelrout6=\"" + route_out + "\"") + + if len(cmd) > 3: + subprocess.call(cmd, stdout=open(os.devnull, "w"), stderr=subprocess.STDOUT) + time.sleep(1) + + +# Return the default route +def get_route(network): + # First, delete the route + if network == "ipv4": + cmd = ["/usr/sbin/lsattr", "-El", "inet0", "-a", "route", "-F", "value"] + elif network == "ipv6": + cmd = ["/usr/sbin/lsattr", "-El", "inet0", "-a", "rout6", "-F", "value"] + (out, err) = subp.subp(cmd) + time.sleep(1) + out = out.strip() + if len(out): + return out + else: + return None + + +# Enable the autoconf6 daemon in /etc/rc.tcpip +def enable_autoconf6(device_name): + cmd = ["/usr/sbin/chrctcp", "-c", "autoconf6", "-f", "interface=" + device_name] + subp.subp(cmd, capture=False) + start_autoconf6(device_name) + + +# Disable the autoconf6 daemon in /etc/rc.tcpip +def disable_autoconf6(): + cmd = ["/usr/sbin/chrctcp", "-d", "autoconf6"] + subp.subp(cmd, capture=False) + + +# Configure the IPv6 network interfaces +def start_autoconf6(device_name): + if device_name == "any": + cmd = ["/usr/sbin/autoconf6", "-A"] + else: + cmd = ["/usr/sbin/autoconf6", "-i", device_name] + subp.subp(cmd, capture=False) + + +# Enable the ndpd-host daemon in /etc/rc.tcpip and start the service +def enable_ndpd_host(): + cmd = ["/usr/sbin/chrctcp", "-S", "-a", "ndpd-host"] + subp.subp(cmd, capture=False) + + +# Disable the ndpd-host daemon in /etc/rc.tcpip and stop the daemon +def disable_ndpd_host(): + cmd = ["/usr/sbin/chrctcp", "-S", "-d", "ndpd-host"] + subp.subp(cmd, capture=False) + + +# Enable the dhcpcd daemon in /etc/rc.tcpip and start the service +def enable_dhcpcd(): + cmd = ["/usr/sbin/chrctcp", "-S", "-a", "dhcpcd"] + subp.subp(cmd, capture=False) + + +# Disable the dhcpcd daemon in /etc/rc.tcpip and stop the service +def disable_dhcpcd(): + cmd = ["/usr/sbin/chrctcp", "-S", "-d", "dhcpcd"] + subp.subp(cmd, capture=False) + + +# +# Update the /etc/dhcpcd.ini file with the following from +# the info dictionary +# +# option 1 : Subnet Mask +# option 3 : Routers (ip addresses) +# option 50 : Requested IP Address +# +def update_dhcp(tmpf, interface, info): + util.append_file(tmpf, "interface %s\n" % interface) + 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'))) + else: + util.append_file(tmpf, " reject 3\n") + if info.get('address'): + util.append_file(tmpf, " option 50 %s\n" % (info.get('address'))) + util.append_file(tmpf, "}\n\n") + + +# +# Parse the /etc/dhcpcd.ini file and update it with network information +# from the info dictionary produce by aix.py -> _write_network() +# +# create = True, create a new /etc/dhcpcd.ini file +# = False, go to the end and update /etc/dhcpcd.ini +# +def config_dhcp(interface, info, create=True): + infile = "/etc/dhcpcd.ini" + eat = 0 + updated = 0 + + if interface is not None: + with open(infile, 'r+') as f, temp_utils.tempdir() as tmpd: + tmpf = "%s/dhcpcd.ini" % tmpd + for line in f.readlines(): + if create is False: + util.append_file(tmpf, line) + else: + if eat == 0 and not line.startswith("interface "): + util.append_file(tmpf, line) + elif eat == 0 and line.startswith("interface "): + eat = 1 + elif eat == 1 and re.match("{", line.strip()): + eat = 2 + elif eat == 2: + update_dhcp(tmpf, interface, info) + updated = 1 + eat = 3 + if create is False: + update_dhcp(tmpf, interface, info) + else: + if updated == 0: + update_dhcp(tmpf, interface, info) + + util.copy(tmpf, infile) + + +# Return the device using the lsdev command output +def find_devs_with(path=None): + """ + find devices matching given criteria (via lsdev) + """ + lsdev_cmd = ['/usr/sbin/lsdev'] + options = [] + if path: + options.append("-Cl") + options.append(path) + cmd = lsdev_cmd + options + + (out, _err) = subp.subp(cmd) + entries = [] + for line in out.splitlines(): + line = line.strip().split()[0] + if line: + entries.append(line) + return entries + + +def mount_cb(device, callback, data=None, rw=False, mtype=None, sync=True): + """ + Mount the device, call method 'callback' passing the directory + in which it was mounted, then unmount. Return whatever 'callback' + returned. If data != None, also pass data to callback. + """ + mounted = mounts() + with temp_utils.tempdir() as tmpd: + #with util.tempdir() as tmpd: + umount = False + devname="/dev/" + device + if device in mounted: + mountpoint = mounted[device]["mountpoint"] + elif devname in mounted: + mountpoint = mounted[devname]["mountpoint"] + else: + try: + mountcmd = ["/usr/sbin/mount"] + mountopts = [] + if rw: + mountopts.append('rw') + else: + mountopts.append('ro') + if sync: + # This seems like the safe approach to do + # (ie where this is on by default) + mountopts.append("sync") + if mountopts: + mountcmd.extend(["-o", ",".join(mountopts)]) + if mtype: + mountcmd.extend(['-t', mtype]) + + if "/cd" in devname: + mountcmd.append('-vcdrfs') + mountcmd.append(devname) + else: + mountcmd.append(device) + + mountcmd.append(tmpd) + subp.subp(mountcmd) + umount = tmpd # This forces it to be unmounted (when set) + mountpoint = tmpd + except (IOError, OSError) as exc: + raise util.MountFailedError(("Failed mounting %s to %s due to: %s") % (device, tmpd, exc)) + # Be nice and ensure it ends with a slash + if not mountpoint.endswith("/"): + mountpoint += "/" + + with unmounter(umount): + if data is None: + ret = callback(mountpoint) + else: + ret = callback(mountpoint, data) + return ret + + +def mounts(): + mounted = {} + try: + # Go through mounts to see what is already mounted + (mountoutput, _err) = subp.subp("/usr/sbin/mount") + mount_locs = mountoutput.splitlines() + mountre = r'\s+(/dev/[\S]+)\s+(/\S*)\s+(\S+)\s+(\S+ \d+ \d+:\d+) (\S+(,\S+)?)' + for mpline in mount_locs: + # AIX: /dev/hd4 524288 142672 73% 10402 38% / + try: + m = re.search(mountre, mpline) + dev = m.group(1) + mp = m.group(2) + fstype = m.group(3) + date = m.group(4) + opts = m.group(5).split(",")[0] + except: + continue + # If the name of the mount point contains spaces these + # can be escaped as '\040', so undo that.. + mp = mp.replace("\\040", " ") + mounted[dev] = { + 'fstype': fstype, + 'mountpoint': mp, + 'opts': opts, + 'date': date, + } + print("Fetched %s mounts" % mounted) + except (IOError, OSError): + print("Failed fetching mount points") + return mounted + + +@contextlib.contextmanager +def unmounter(umount): + try: + yield umount + finally: + if umount: + umount_cmd = ["/usr/sbin/umount", umount] + subp.subp(umount_cmd) + + +# Helper function to write the resolv.conf file +def write_resolv_conf_file(fn, r_conf): + util.write_file(fn, str(r_conf), 0o644) + + +# Helper function to write /etc/resolv.conf +def update_resolve_conf_file(fn, dns_servers, search_servers): + try: + r_conf = ResolvConf(util.load_file(fn)) + r_conf.parse() + empty = False + except IOError: + LOG.info("Failed at parsing %s creating an empty instance", fn) + r_conf = ResolvConf('') + r_conf.parse() + empty = True + if dns_servers: + for s in dns_servers: + try: + r_conf.add_nameserver(s) + empty = False + except ValueError: + util.logexc(LOG, "Failed at adding nameserver %s", s) + if search_servers: + for s in search_servers: + try: + r_conf.add_search_domain(s) + except ValueError: + util.logexc(LOG, "Failed at adding search domain %s", s) + if empty is False: + write_resolv_conf_file(fn, r_conf) + + +# Overwrite the existing conf file so the resolv.conf +# is a replacement versus an update to eliminate unwanted +# existing changes from previous capture data +def remove_resolve_conf_file(fn): + util.del_file(fn) + + +def get_mask(interface): + netmask = get_if_attr(interface, "netmask") + if netmask is None: + return "-" + else: + return netmask + + +# +# Return the value of an attribute for an interface +# The attr argument comes from the lsattr command device attribute +# +def get_if_attr(interface, attr): + (lsattr_out, _err) = subp.subp(["/usr/sbin/lsattr", "-El", interface, "-a", attr, "-F", "value"], rcs=[0, 255]) + + if not lsattr_out or lsattr_out[0] == '\n': + return None + else: + return lsattr_out.strip() diff --git a/cloudinit/distros/net_util.py b/cloudinit/distros/net_util.py index 254e7d85f6d..b952132d952 100644 --- a/cloudinit/distros/net_util.py +++ b/cloudinit/distros/net_util.py @@ -132,7 +132,7 @@ def translate_network(settings): if val: iface_info["ipv6"][k] = val else: - for k in ["netmask", "address", "gateway", "broadcast"]: + for k in ["netmask", "address", "gateway", "broadcast", "mtu"]: if k in info: val = info[k].strip().lower() if val: diff --git a/cloudinit/distros/networking.py b/cloudinit/distros/networking.py index e63d2177d3e..134ce954417 100644 --- a/cloudinit/distros/networking.py +++ b/cloudinit/distros/networking.py @@ -173,6 +173,21 @@ def try_set_link_up(self, devname: DeviceName) -> bool: """Try setting the link to up explicitly and return if it is up.""" +class AIXNetworking(Networking): + """Implementation of networking functionality shared across AIXs""" + + def is_physical(self, devname: DeviceName) -> bool: + return "en" in devname + + def settle(self, *, exists=None) -> None: + """AIX has no equivalent to `udevadm settle`; noop.""" + + def try_set_link_up(self, devname: DeviceName) -> bool: + subp.subp(["ifconfig", devname, "up"]) + print("try_set_link_up devname %s" % devname) + return self.is_up(devname) + + class BSDNetworking(Networking): """Implementation of networking functionality shared across BSDs.""" diff --git a/cloudinit/helpers.py b/cloudinit/helpers.py index 9eefafa1b5e..20130f8548f 100644 --- a/cloudinit/helpers.py +++ b/cloudinit/helpers.py @@ -11,6 +11,7 @@ import contextlib import logging import os +import platform from configparser import NoOptionError, NoSectionError, RawConfigParser from io import StringIO from time import time @@ -306,7 +307,10 @@ class Paths(persistence.CloudInitPickleMixin): def __init__(self, path_cfgs: dict, ds=None): self.cfgs = path_cfgs # Populate all the initial paths - self.cloud_dir: str = path_cfgs.get("cloud_dir", "/var/lib/cloud") + if platform.system().lower() == "aix": + self.cloud_dir: str = path_cfgs.get("cloud_dir", "/opt/freeware/var/lib/cloud") + else: + self.cloud_dir: str = path_cfgs.get("cloud_dir", "/var/lib/cloud") self.run_dir: str = path_cfgs.get("run_dir", "/run/cloud-init") self.instance_link: str = os.path.join(self.cloud_dir, "instance") self.boot_finished: str = os.path.join( @@ -314,9 +318,12 @@ def __init__(self, path_cfgs: dict, ds=None): ) self.seed_dir: str = os.path.join(self.cloud_dir, "seed") # This one isn't joined, since it should just be read-only - template_dir: str = path_cfgs.get( - "templates_dir", "/etc/cloud/templates/" - ) + if platform.system().lower() == "aix": + template_dir: str = path_cfgs.get("templates_dir", "/opt/freeware/etc/cloud/templates/") + else: + template_dir: str = path_cfgs.get( + "templates_dir", "/etc/cloud/templates/" + ) self.template_tpl: str = os.path.join(template_dir, "%s.tmpl") self.lookups = { "boothooks": "boothooks", diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 9478efc35fd..3dcccc04cc2 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -11,6 +11,9 @@ import json import logging import re +import cloudinit.distros.aix_util as aix_util +import os +import platform from copy import copy, deepcopy from ipaddress import IPv4Network @@ -232,7 +235,10 @@ def _netdev_info_ifconfig(ifconfig_data): if len(line) == 0: continue if line[0] not in ("\t", " "): - curdev = line.split()[0] + if platform.system().lower() == "aix": + curdev = line.split()[0].replace(':', '') + else: + curdev = line.split()[0] # current ifconfig pops a ':' on the end of the device if curdev.endswith(":"): curdev = curdev[:-1] @@ -244,7 +250,7 @@ def _netdev_info_ifconfig(ifconfig_data): # If the output of ifconfig doesn't contain the required info in the # obvious place, use a regex filter to be sure. elif len(toks) > 1: - if re.search(r"flags=\d+ # # This file is part of cloud-init. See LICENSE file for license information. +import platform # Set and read for determining the cloud config file location CFG_ENV_NAME = "CLOUD_CFG" # This is expected to be a yaml formatted file -CLOUD_CONFIG = "/etc/cloud/cloud.cfg" +if platform.system().lower() == "aix": + CLOUD_CONFIG = "/opt/freeware/etc/cloud/cloud.cfg" +else: + CLOUD_CONFIG = "/etc/cloud/cloud.cfg" CLEAN_RUNPARTS_DIR = "/etc/cloud/clean.d" @@ -54,13 +58,13 @@ ], "def_log_file": "/var/log/cloud-init.log", "log_cfgs": [], - "syslog_fix_perms": ["syslog:adm", "root:adm", "root:wheel", "root:root"], + "syslog_fix_perms": ["root:system", "syslog:adm", "root:adm", "root:wheel", "root:root"], "system_info": { "paths": { - "cloud_dir": "/var/lib/cloud", - "templates_dir": "/etc/cloud/templates/", + "cloud_dir": "/var/lib/cloud", "/opt/freeware/var/lib/cloud", + "templates_dir": "/etc/cloud/templates/", "/opt/freeware/etc/cloud/templates/", }, - "distro": "ubuntu", + "distro": "ubuntu", "aix", "network": {"renderers": None}, }, "vendor_data": {"enabled": True, "prefix": []}, diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py index c6191f95604..34fd5cd5cb4 100644 --- a/cloudinit/sources/DataSourceConfigDrive.py +++ b/cloudinit/sources/DataSourceConfigDrive.py @@ -8,12 +8,14 @@ import logging import os +import platform from cloudinit import sources, subp, util from cloudinit.event import EventScope, EventType from cloudinit.net import eni from cloudinit.sources.DataSourceIBMCloud import get_ibm_platform from cloudinit.sources.helpers import openstack +from cloudinit.distros import aix_util LOG = logging.getLogger(__name__) @@ -26,9 +28,12 @@ FS_TYPES = ("vfat", "iso9660") LABEL_TYPES = ("config-2", "CONFIG-2") POSSIBLE_MOUNTS = ("sr", "cd") -OPTICAL_DEVICES = tuple( - ("/dev/%s%s" % (z, i) for z in POSSIBLE_MOUNTS for i in range(2)) -) +if platform.system().lower() == "aix": + OPTICAL_DEVICES = tuple(('cd%s' % i for i in range(0, 2))) +else: + OPTICAL_DEVICES = tuple( + ("/dev/%s%s" % (z, i) for z in POSSIBLE_MOUNTS for i in range(2)) + ) class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource): @@ -84,9 +89,14 @@ def _get_data(self): if dev.startswith("/dev/cd"): mtype = "cd9660" try: - results = util.mount_cb( - dev, read_config_drive, mtype=mtype - ) + if platform.system().lower() == "aix": + results = aix_util.mount_cb( + dev, read_config_drive, mtype=mtype + ) + else: + results = util.mount_cb( + dev, read_config_drive, mtype=mtype + ) found = dev except openstack.NonReadable: pass @@ -268,20 +278,26 @@ def find_candidate_devs(probe_optical=True, dslist=None): dslist = [] # query optical drive to get it in blkid cache for 2.6 kernels + by_fstype = [] if probe_optical: for device in OPTICAL_DEVICES: try: - util.find_devs_with(path=device) + if platform.system().lower() == "aix": + by_fstype.extend(aix_util.find_devs_with(device)) + else: + util.find_devs_with(path=device) except subp.ProcessExecutionError: pass - by_fstype = [] - for fs_type in FS_TYPES: - by_fstype.extend(util.find_devs_with("TYPE=%s" % (fs_type))) + if platform.system().lower() == "aix": + devices = by_fstype + else: + for fs_type in FS_TYPES: + by_fstype.extend(util.find_devs_with("TYPE=%s" % (fs_type))) - by_label = [] - for label in LABEL_TYPES: - by_label.extend(util.find_devs_with("LABEL=%s" % (label))) + by_label = [] + for label in LABEL_TYPES: + by_label.extend(util.find_devs_with("LABEL=%s" % (label))) # give preference to "last available disk" (vdb over vda) # note, this is not a perfect rendition of that. diff --git a/cloudinit/sources/DataSourceNoCloud.py b/cloudinit/sources/DataSourceNoCloud.py index 55a16638788..187d53e1e20 100644 --- a/cloudinit/sources/DataSourceNoCloud.py +++ b/cloudinit/sources/DataSourceNoCloud.py @@ -11,9 +11,11 @@ import errno import logging import os +import platform from cloudinit import dmi, sources, util from cloudinit.net import eni +from cloudinit.distros import aix_util LOG = logging.getLogger(__name__) @@ -37,15 +39,17 @@ def __str__(self): return "%s [seed=%s][dsmode=%s]" % (root, self.seed, self.dsmode) def _get_devices(self, label): - fslist = util.find_devs_with("TYPE=vfat") - fslist.extend(util.find_devs_with("TYPE=iso9660")) - - label_list = util.find_devs_with("LABEL=%s" % label.upper()) - label_list.extend(util.find_devs_with("LABEL=%s" % label.lower())) - label_list.extend(util.find_devs_with("LABEL_FATBOOT=%s" % label)) - - devlist = list(set(fslist) & set(label_list)) - devlist.sort(reverse=True) + if platform.system().lower() == "aix": + devlist = aix_util.find_devs_with("cd0") + devlist.extend(aix_util.find_devs_with("cd1")) + else: + fslist = util.find_devs_with("TYPE=vfat") + fslist.extend(util.find_devs_with("TYPE=iso9660")) + label_list = util.find_devs_with("LABEL=%s" % label.upper()) + label_list.extend(util.find_devs_with("LABEL=%s" % label.lower())) + label_list.extend(util.find_devs_with("LABEL_FATBOOT=%s" % label)) + devlist = list(set(fslist) & set(label_list)) + devlist.sort(reverse=True) return devlist def _get_data(self): @@ -123,9 +127,14 @@ def _pp2d_callback(mp, data): LOG.debug("Attempting to use data from %s", dev) try: - seeded = util.mount_cb( - dev, _pp2d_callback, pp2d_kwargs - ) + if platform.system().lower() == "aix": + seeded = aix_util.mount_cb( + dev, _pp2d_callback, pp2d_kwargs + ) + else: + seeded = util.mount_cb( + dev, _pp2d_callback, pp2d_kwargs + ) except ValueError: LOG.warning( "device %s with label=%s not a valid seed.", diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py index c207b5ed6df..7a5bfb99a28 100644 --- a/cloudinit/sources/__init__.py +++ b/cloudinit/sources/__init__.py @@ -757,13 +757,16 @@ def device_name_to_device(self, _name): return None def get_locale(self): - """Default locale is en_US.UTF-8, but allow distros to override""" - locale = self.default_locale - try: - locale = self.distro.get_locale() - except NotImplementedError: - pass - return locale + if self.distro.name == "aix": + return 'en_US' + else: + """Default locale is en_US.UTF-8, but allow distros to override""" + locale = self.default_locale + try: + locale = self.distro.get_locale() + except NotImplementedError: + pass + return locale @property def availability_zone(self): diff --git a/cloudinit/util.py b/cloudinit/util.py index 6401a196f7a..59b0623aa97 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -618,6 +618,9 @@ def get_linux_distro(): elif is_BSD(): distro_name = platform.system().lower() distro_version = platform.release() + elif platform.system().lower() == "aix": + distro_name = "aix" + distro_version = platform.release() else: dist = ("", "", "") try: @@ -690,6 +693,7 @@ def _get_variant(info): "netbsd", "openbsd", "dragonfly", + "aix", ): variant = system @@ -967,10 +971,16 @@ def fetch_ssl_details(paths=None): ssl_details = {} # Lookup in these locations for ssl key/cert files if not paths: - ssl_cert_paths = [ - "/var/lib/cloud/data/ssl", - "/var/lib/cloud/instance/data/ssl", - ] + if platform.system().lower() == "aix": + ssl_cert_paths = [ + "/opt/freeware/var/lib/cloud/data/ssl", + "/opt/freeware/var/lib/cloud/instance/data/ssl", + ] + else: + ssl_cert_paths = [ + "/var/lib/cloud/data/ssl", + "/var/lib/cloud/instance/data/ssl", + ] else: ssl_cert_paths = [ os.path.join(paths.get_ipath_cur("data"), "ssl"), @@ -2127,6 +2137,14 @@ def uptime(): contents = load_file("/proc/uptime") if contents: uptime_str = contents.split()[0] + elif os.path.exists("/usr/sbin/acct/fwtmp"): # for AIX support + method = '/usr/sbin/acct/fwtmp' + raw_contents = subprocess.run(['/usr/sbin/acct/fwtmp < /var/adm/wtmp | /usr/bin/grep "system boot" 2>/dev/null'], shell=True, check=True, stdout=subprocess.PIPE, universal_newlines=True) + contents = raw_contents.stdout + if contents: + bootup = contents.splitlines()[-1].split()[6] + now = time.time() + uptime_str = now - float(bootup) else: method = "ctypes" # This is the *BSD codepath diff --git a/config/cloud.cfg b/config/cloud.cfg new file mode 100644 index 00000000000..4a234e2fd12 --- /dev/null +++ b/config/cloud.cfg @@ -0,0 +1,102 @@ +# The top level settings are used as module +# and system configuration. + +# A set of users which may be applied and/or used by various modules +# when a 'default' entry is found it will reference the 'default_user' +# from the distro configuration specified below +users: + - default + +# If this is set, 'root' will not be able to ssh in and they +# will get a message to login instead as the above $user (ubuntu) +disable_root: false + +# Allow SSH password authorization +ssh_pwauth: true + +# Delete existing SSH host keys +ssh_deletekeys: true + +# Regen rsa and dsa host keys +ssh_genkeytypes: ['rsa', 'dsa'] + +# This will cause the set+update hostname module to not operate (if true) +preserve_hostname: false + +datasource_list: ['ConfigDrive'] + +# Example datasource config +# datasource: +# Ec2: +# metadata_urls: [ 'blah.com' ] +# timeout: 5 # (defaults to 50 seconds) +# max_wait: 10 # (defaults to 120 seconds) + +# The modules that run in the 'init' stage +cloud_init_modules: + - migrator + - seed_random + - bootcmd + - write-files + - set_hostname + - update_hostname + - update_etc_hosts + - ca-certs + - rsyslog + - users-groups + - ssh + - restore-volume-groups + - set-multipath-hcheck-interval + - update-bootlist + - reset-rmc + +# The modules that run in the 'config' stage +cloud_config_modules: +# Emit the cloud config ready event +# this can be used by upstart jobs for 'start on cloud-config'. + - emit_upstart + - disk_setup + - mounts + - ssh-import-id + - locale + - set-passwords + - package-update-upgrade-install + - landscape + - timezone + - puppet + - chef + - salt-minion + - mcollective + - disable-ec2-metadata + - runcmd + - byobu + +# The modules that run in the 'final' stage +cloud_final_modules: + - rightscale_userdata + - scripts-vendor + - scripts-per-once + - scripts-per-boot + - scripts-per-instance + - scripts-user + - ssh-authkey-fingerprints + - keys-to-console + - phone-home + - final-message + - power-state-change + +# System and/or distro specific settings +# (not accessible to handlers/transforms) +system_info: + # This will affect which distro class gets used + distro: aix + # Default user name + that default users groups (if added/used) + default_user: + name: root + lock_passwd: false + # Other config here will be given to the distro class and/or path classes + paths: + cloud_dir: /opt/freeware/var/lib/cloud/ + templates_dir: /opt/freeware/etc/cloud/templates/ + upstart_dir: /etc/rc.d/init.d/ + ssh_svcname: ssh diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index 598d78b4672..9fac95cb62d 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -51,14 +51,14 @@ manage_etc_hosts: false # If this is set, 'root' will not be able to ssh in and they # will get a message to login instead as the default $user -{% if variant in ["freebsd", "photon"] %} +{% if variant in ["freebsd", "photon", "aix"] %} disable_root: false {% else %} disable_root: true {% endif %} {%- if variant in ["alpine", "amazon", "fedora", "OpenCloudOS", "openeuler", - "openmandriva", "photon", "TencentOS"] or is_rhel %} + "openmandriva", "photon", "TencentOS", "aix"] or is_rhel %} {% if is_rhel %} mount_default_fields: [~, ~, 'auto', 'defaults,nofail,x-systemd.requires=cloud-init.service,_netdev', '0', '2'] diff --git a/packages/aix/cloud-init.spec b/packages/aix/cloud-init.spec new file mode 100644 index 00000000000..dc88397780a --- /dev/null +++ b/packages/aix/cloud-init.spec @@ -0,0 +1,176 @@ +Name: cloud-init +Version: 22.1 +Release: 0.0 +License: GPL-3.0 +Summary: Cloud node initialization tool +Url: http://launchpad.net/cloud-init/ +Group: System/Management +Source0: %{name}-%{version}.tar.gz +BuildRoot: %{_tmppath}/%{name}-%{version}-build +%define docdir %{_defaultdocdir}/%{name} + +%{!?python_sitelib: %global python_sitelib %(python -c "from distutils.sysconfig import get_python_lib; +print get_python_lib()")} + +%{!?python3_sitelib: %global python3_sitelib %(/opt/freeware/bin/python3 -c "from distutils.sysconfig import get_python_lib;print(get_python_lib())")} + +%define initsys aix + +%description +Cloud-init is an init script that initializes a cloud node (VM) +according to the fetched configuration data from the admin node. + +The RPM packages can be obtained from the following website: +ftp://ftp.software.ibm.com/aix/freeSoftware/aixtoolbox/RPMS/ppc/ + +%package doc +Summary: Cloud node initialization tool - Documentation +Group: System/Management + +%description doc +Cloud-init is an init script that initializes a cloud node (VM) +according to the fetched configuration data from the admin node. + +Documentation and examples for cloud-init tools + +%package test +Summary: Cloud node initialization tool - Testsuite +Group: System/Management +Requires: cloud-init = %{version} + +%description test +Cloud-init is an init script that initializes a cloud node (VM) +according to the fetched configuration data from the admin node. + +Unit tests for the cloud-init tools + +%prep +%setup -q + +echo "......finding directories......" +echo "%{buildroot}" +echo "%{_tmppath}" +echo "%{_defaultdocdir}" +echo "%{_localstatedir}" +echo "%{docdir}" +echo "%{python_sitelib}" +echo "%{_prefix}" +echo "%{initsys}" +echo "%{python3_sitelib}" +echo "\n------done------" + + +%build +/opt/freeware/bin/python3 setup.py build + +%install +/opt/freeware/bin/python3 setup.py install --root=%{buildroot} --prefix=%{_prefix} --install-lib=%{python3_sitelib} --init-system=%{initsys} + +#find %{buildroot} -name ".gitignore" -type f -exec rm -f {} \; +find %{buildroot} -name ".placeholder" -type f -exec rm -f {} \; + +# from debian install script +for x in "%{buildroot}%{_bindir}/"*.py; do + [ -f "${x}" ] && mv "${x}" "${x%.py}" +done +/usr/bin/mkdir -p %{buildroot}%{_localstatedir}/lib/cloud + +# move documentation +/usr/bin/mkdir -p %{buildroot}%{_defaultdocdir} +%define aixshare usr/share + +rm -rf %{buildroot}%{docdir} +mv -f %{buildroot}/%{aixshare}/doc/%{name} %{buildroot}%{docdir} +/usr/bin/mkdir -p %{buildroot}/%{_sysconfdir}/cloud/ + +/usr/bin/mkdir -p %{buildroot}/%_prefix/%_lib +cp -r %{buildroot}/usr/lib/cloud-init %{buildroot}/%_prefix/%_lib + +# copy the LICENSE +cp LICENSE %{buildroot}%{docdir} + + +# remove debian/ubuntu specific profile.d file (bnc#779553) +rm -f %{buildroot}%{_sysconfdir}/profile.d/Z99-cloud-locale-test.sh + +# Remove non-AIX templates +rm -f %{buildroot}/etc/cloud/templates/*.debian.* +rm -f %{buildroot}/etc/cloud/templates/*.redhat.* +rm -f %{buildroot}/etc/cloud/templates/*.ubuntu.* +rm -f %{buildroot}/etc/cloud/templates/*.suse.* + + +# Move everything from %{buildroot}/etc/cloud to %{buildroot}/%{_prefix}/etc/cloud +# so we can build it as a package and installed to /opt/freeware +rm -rf %{buildroot}/%{_prefix}/etc/cloud/* +mv -f %{buildroot}/etc/cloud/* %{buildroot}/%{_prefix}/etc/cloud + +# move aix sysvinit scripts into the "right" place +%define _initddir /etc/rc.d/init.d +/usr/bin/mkdir -p %{buildroot}/%{_initddir} +/usr/bin/mkdir -p %{buildroot}/%{_sbindir} +OLDPATH="%{buildroot}%{_initddir}" +for iniF in *; do + ln -sf "%{_initddir}/${iniF}" "%{buildroot}/%{_sbindir}/rc${iniF}" +done +cd $OLDPATH + +# remove duplicate files +/opt/freeware/bin/fdupes %{buildroot}%{python3_sitelib} + +%post +/usr/bin/ln -sf /etc/rc.d/init.d/cloud-init-local /etc/rc.d/rc2.d/S01cloud-init-local +/usr/bin/ln -sf /etc/rc.d/init.d/cloud-init /etc/rc.d/rc2.d/S02cloud-init +/usr/bin/ln -sf /etc/rc.d/init.d/cloud-config /etc/rc.d/rc2.d/S03cloud-config +/usr/bin/ln -sf /etc/rc.d/init.d/cloud-final /etc/rc.d/rc2.d/S04cloud-final +if [[ `/usr/sbin/lsattr -El sys0 -a clouddev >/dev/null 2>&1; echo $?` -eq 0 ]]; then + /usr/lib/boot/bootutil -c 2>/dev/null + /usr/sbin/chdev -l sys0 -a clouddev=1 >/dev/null 2>&1 +else + /usr/sbin/chdev -l sys0 -a ghostdev=1 >/dev/null 2>&1 +fi + +%preun +if [ "$1" = 0 ]; then + rm -rf /opt/freeware/var/lib/cloud/* + rm -rf /run/cloud-init +fi + +%postun +if [ "$1" = 0 ]; then + rm /etc/rc.d/rc2.d/S01cloud-init-local + rm /etc/rc.d/rc2.d/S02cloud-init + rm /etc/rc.d/rc2.d/S03cloud-config + rm /etc/rc.d/rc2.d/S04cloud-final +fi + +%files +%define py_ver 3.7 +%defattr(-,root,root) +# do not mark as doc or we get conflicts with the doc package +%{docdir}/LICENSE +%{_bindir}/cloud-init* +%config(noreplace) %{_prefix}/etc/cloud/ +%{python3_sitelib}/* +%{_prefix}/lib/cloud-init +%attr(0755, root, root) %{_initddir}/cloud-config +%attr(0755, root, root) %{_initddir}/cloud-init +%attr(0755, root, root) %{_initddir}/cloud-init-local +%attr(0755, root, root) %{_initddir}/cloud-final + +%dir %attr(0755, root, root) %{_localstatedir}/lib/cloud +%dir %{docdir} + + +%files doc +%defattr(-,root,root) +%{docdir}/examples/* +%{docdir}/*.txt +%dir %{docdir}/examples + +%files test +%defattr(-,root,root) +%{python3_sitelib}/tests/* +%dir %{python3_sitelib}/tests + +%changelog diff --git a/setup.py b/setup.py index e40d730e550..168acdcae12 100644 --- a/setup.py +++ b/setup.py @@ -141,6 +141,7 @@ def render_tmpl(template, mode=None, is_yaml=False): "sysvinit_openrc": lambda: [ f for f in glob("sysvinit/gentoo/*") if is_f(f) ], + "aix": [f for f in glob("sysvinit/aix/*") if is_f(f)], "systemd": lambda: [ render_tmpl(f) for f in ( @@ -164,6 +165,7 @@ def render_tmpl(template, mode=None, is_yaml=False): "sysvinit_openbsd": "etc/rc.d", "sysvinit_deb": "etc/init.d", "sysvinit_openrc": "etc/init.d", + "aix": "etc/rc.d/init.d", "systemd": pkg_config_read("systemd", "systemdsystemunitdir"), "systemd.generators": pkg_config_read( "systemd", "systemdsystemgeneratordir" @@ -275,6 +277,7 @@ def finalize_options(self): data_files = [ (ETC + "/cloud", [render_tmpl("config/cloud.cfg.tmpl", is_yaml=True)]), (ETC + "/cloud/clean.d", glob("config/clean.d/*")), + (ETC + "/cloud", glob("config/*.cfg")), (ETC + "/cloud/cloud.cfg.d", glob("config/cloud.cfg.d/*")), (ETC + "/cloud/templates", glob("templates/*")), ( @@ -284,6 +287,7 @@ def finalize_options(self): "tools/hook-hotplug", "tools/uncloud-init", "tools/write-ssh-key-fingerprints", + "tools/create_pvid_to_vg_mappings.sh", ], ), ( diff --git a/sysvinit/aix/cloud-config b/sysvinit/aix/cloud-config new file mode 100644 index 00000000000..ccc0e8090fc --- /dev/null +++ b/sysvinit/aix/cloud-config @@ -0,0 +1,108 @@ +#!/opt/freeware/bin/bash +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# This file is part of cloud-init. See LICENSE file for license information. + +# See: http://wiki.debian.org/LSBInitScripts +# See: http://tiny.cc/czvbgw +# See: http://www.novell.com/coolsolutions/feature/15380.html +# Also based on dhcpd in RHEL (for comparison) + +### BEGIN INIT INFO +# Provides: cloud-config +# Required-Start: cloud-init cloud-init-local +# Should-Start: $time +# Required-Stop: +# Should-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: The config cloud-init job +# Description: Start cloud-init and runs the config phase +# and any associated config modules as desired. +### END INIT INFO + +# Return values acc. to LSB for all commands but status: +# 0 - success +# 1 - generic or unspecified error +# 2 - invalid or excess argument(s) +# 3 - unimplemented feature (e.g. "reload") +# 4 - user had insufficient privileges +# 5 - program is not installed +# 6 - program is not configured +# 7 - program is not running +# 8--199 - reserved (8--99 LSB, 100--149 distrib, 150--199 appl) +# +# Note that starting an already running service, stopping +# or restarting a not-running service as well as the restart +# with force-reload (in case signaling is not supported) are +# considered a success. + +RETVAL=0 + +prog="cloud-init" +cloud_init="/opt/freeware/bin/cloud-init" +conf="/opt/freeware/etc/cloud/cloud.cfg" + +# If there exist sysconfig/default variable override files use it... +[ -f /etc/sysconfig/cloud-init ] && . /etc/sysconfig/cloud-init +[ -f /etc/default/cloud-init ] && . /etc/default/cloud-init + +start() { + [ -x $cloud_init ] || return 5 + [ -f $conf ] || return 6 + + echo -n $"Starting $prog: " + $cloud_init $CLOUDINITARGS modules --mode config + RETVAL=$? + return $RETVAL +} + +stop() { + echo -n $"Shutting down $prog: " + # No-op + RETVAL=7 + return $RETVAL +} + +case "$1" in + start) + start + RETVAL=$? + ;; + stop) + stop + RETVAL=$? + ;; + restart|try-restart|condrestart) + ## Stop the service and regardless of whether it was + ## running or not, start it again. + # + ## Note: try-restart is now part of LSB (as of 1.9). + ## RH has a similar command named condrestart. + start + RETVAL=$? + ;; + reload|force-reload) + # It does not support reload + RETVAL=3 + ;; + status) + echo -n $"Checking for service $prog:" + # Return value is slightly different for the status command: + # 0 - service up and running + # 1 - service dead, but /var/run/ pid file exists + # 2 - service dead, but /var/lock/ lock file exists + # 3 - service not running (unused) + # 4 - service status unknown :-( + # 5--199 reserved (5--99 LSB, 100--149 distro, 150--199 appl.) + RETVAL=3 + ;; + *) + echo "Usage: $0 {start|stop|status|try-restart|condrestart|restart|force-reload|reload}" + RETVAL=3 + ;; +esac + +exit $RETVAL diff --git a/sysvinit/aix/cloud-final b/sysvinit/aix/cloud-final new file mode 100644 index 00000000000..1e79811a9d0 --- /dev/null +++ b/sysvinit/aix/cloud-final @@ -0,0 +1,108 @@ +#!/opt/freeware/bin/bash +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# This file is part of cloud-init. See LICENSE file for license information. + +# See: http://wiki.debian.org/LSBInitScripts +# See: http://tiny.cc/czvbgw +# See: http://www.novell.com/coolsolutions/feature/15380.html +# Also based on dhcpd in RHEL (for comparison) + +### BEGIN INIT INFO +# Provides: cloud-final +# Required-Start: $all cloud-config +# Should-Start: $time +# Required-Stop: +# Should-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: The final cloud-init job +# Description: Start cloud-init and runs the final phase +# and any associated final modules as desired. +### END INIT INFO + +# Return values acc. to LSB for all commands but status: +# 0 - success +# 1 - generic or unspecified error +# 2 - invalid or excess argument(s) +# 3 - unimplemented feature (e.g. "reload") +# 4 - user had insufficient privileges +# 5 - program is not installed +# 6 - program is not configured +# 7 - program is not running +# 8--199 - reserved (8--99 LSB, 100--149 distrib, 150--199 appl) +# +# Note that starting an already running service, stopping +# or restarting a not-running service as well as the restart +# with force-reload (in case signaling is not supported) are +# considered a success. + +RETVAL=0 + +prog="cloud-init" +cloud_init="/opt/freeware/bin/cloud-init" +conf="/opt/freeware/etc/cloud/cloud.cfg" + +# If there exist sysconfig/default variable override files use it... +[ -f /etc/sysconfig/cloud-init ] && . /etc/sysconfig/cloud-init +[ -f /etc/default/cloud-init ] && . /etc/default/cloud-init + +start() { + [ -x $cloud_init ] || return 5 + [ -f $conf ] || return 6 + + echo -n $"Starting $prog: " + $cloud_init $CLOUDINITARGS modules --mode final + RETVAL=$? + return $RETVAL +} + +stop() { + echo -n $"Shutting down $prog: " + # No-op + RETVAL=7 + return $RETVAL +} + +case "$1" in + start) + start + RETVAL=$? + ;; + stop) + stop + RETVAL=$? + ;; + restart|try-restart|condrestart) + ## Stop the service and regardless of whether it was + ## running or not, start it again. + # + ## Note: try-restart is now part of LSB (as of 1.9). + ## RH has a similar command named condrestart. + start + RETVAL=$? + ;; + reload|force-reload) + # It does not support reload + RETVAL=3 + ;; + status) + echo -n $"Checking for service $prog:" + # Return value is slightly different for the status command: + # 0 - service up and running + # 1 - service dead, but /var/run/ pid file exists + # 2 - service dead, but /var/lock/ lock file exists + # 3 - service not running (unused) + # 4 - service status unknown :-( + # 5--199 reserved (5--99 LSB, 100--149 distro, 150--199 appl.) + RETVAL=3 + ;; + *) + echo "Usage: $0 {start|stop|status|try-restart|condrestart|restart|force-reload|reload}" + RETVAL=3 + ;; +esac + +exit $RETVAL diff --git a/sysvinit/aix/cloud-init b/sysvinit/aix/cloud-init new file mode 100644 index 00000000000..f087be22c7b --- /dev/null +++ b/sysvinit/aix/cloud-init @@ -0,0 +1,108 @@ +#!/opt/freeware/bin/bash +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# This file is part of cloud-init. See LICENSE file for license information. + +# See: http://wiki.debian.org/LSBInitScripts +# See: http://tiny.cc/czvbgw +# See: http://www.novell.com/coolsolutions/feature/15380.html +# Also based on dhcpd in RHEL (for comparison) + +### BEGIN INIT INFO +# Provides: cloud-init +# Required-Start: $local_fs $network $named $remote_fs cloud-init-local +# Should-Start: $time +# Required-Stop: +# Should-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: The initial cloud-init job (net and fs contingent) +# Description: Start cloud-init and runs the initialization phase +# and any associated initial modules as desired. +### END INIT INFO + +# Return values acc. to LSB for all commands but status: +# 0 - success +# 1 - generic or unspecified error +# 2 - invalid or excess argument(s) +# 3 - unimplemented feature (e.g. "reload") +# 4 - user had insufficient privileges +# 5 - program is not installed +# 6 - program is not configured +# 7 - program is not running +# 8--199 - reserved (8--99 LSB, 100--149 distrib, 150--199 appl) +# +# Note that starting an already running service, stopping +# or restarting a not-running service as well as the restart +# with force-reload (in case signaling is not supported) are +# considered a success. + +RETVAL=0 + +prog="cloud-init" +cloud_init="/opt/freeware/bin/cloud-init" +conf="/opt/freeware/etc/cloud/cloud.cfg" + +# If there exist sysconfig/default variable override files use it... +[ -f /etc/sysconfig/cloud-init ] && . /etc/sysconfig/cloud-init +[ -f /etc/default/cloud-init ] && . /etc/default/cloud-init + +start() { + [ -x $cloud_init ] || return 5 + [ -f $conf ] || return 6 + + echo -n $"Starting $prog: " + $cloud_init $CLOUDINITARGS init + RETVAL=$? + return $RETVAL +} + +stop() { + echo -n $"Shutting down $prog: " + # No-op + RETVAL=7 + return $RETVAL +} + +case "$1" in + start) + start + RETVAL=$? + ;; + stop) + stop + RETVAL=$? + ;; + restart|try-restart|condrestart) + ## Stop the service and regardless of whether it was + ## running or not, start it again. + # + ## Note: try-restart is now part of LSB (as of 1.9). + ## RH has a similar command named condrestart. + start + RETVAL=$? + ;; + reload|force-reload) + # It does not support reload + RETVAL=3 + ;; + status) + echo -n $"Checking for service $prog:" + # Return value is slightly different for the status command: + # 0 - service up and running + # 1 - service dead, but /var/run/ pid file exists + # 2 - service dead, but /var/lock/ lock file exists + # 3 - service not running (unused) + # 4 - service status unknown :-( + # 5--199 reserved (5--99 LSB, 100--149 distro, 150--199 appl.) + RETVAL=3 + ;; + *) + echo "Usage: $0 {start|stop|status|try-restart|condrestart|restart|force-reload|reload}" + RETVAL=3 + ;; +esac + +exit $RETVAL diff --git a/sysvinit/aix/cloud-init-local b/sysvinit/aix/cloud-init-local new file mode 100644 index 00000000000..aa1577c91f3 --- /dev/null +++ b/sysvinit/aix/cloud-init-local @@ -0,0 +1,111 @@ +#!/opt/freeware/bin/bash +# Copyright (C) 2012 Yahoo! Inc. +# +# Author: Joshua Harlow +# +# This file is part of cloud-init. See LICENSE file for license information. + +# See: http://wiki.debian.org/LSBInitScripts +# See: http://tiny.cc/czvbgw +# See: http://www.novell.com/coolsolutions/feature/15380.html +# Also based on dhcpd in RHEL (for comparison) + +# Bring this up before network, S10 +#chkconfig: 2345 09 91 + +### BEGIN INIT INFO +# Provides: cloud-init-local +# Required-Start: $local_fs +# Should-Start: $time +# Required-Stop: +# Should-Stop: +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: The initial cloud-init job (local fs contingent) +# Description: Start cloud-init and runs the initialization phases +# and any associated initial modules as desired. +### END INIT INFO + +# Return values acc. to LSB for all commands but status: +# 0 - success +# 1 - generic or unspecified error +# 2 - invalid or excess argument(s) +# 3 - unimplemented feature (e.g. "reload") +# 4 - user had insufficient privileges +# 5 - program is not installed +# 6 - program is not configured +# 7 - program is not running +# 8--199 - reserved (8--99 LSB, 100--149 distrib, 150--199 appl) +# +# Note that starting an already running service, stopping +# or restarting a not-running service as well as the restart +# with force-reload (in case signaling is not supported) are +# considered a success. + +RETVAL=0 + +prog="cloud-init" +cloud_init="/opt/freeware/bin/cloud-init" +conf="/opt/freeware/etc/cloud/cloud.cfg" + +# If there exist sysconfig/default variable override files use it... +[ -f /etc/sysconfig/cloud-init ] && . /etc/sysconfig/cloud-init +[ -f /etc/default/cloud-init ] && . /etc/default/cloud-init + +start() { + [ -x $cloud_init ] || return 5 + [ -f $conf ] || return 6 + + echo -n $"Starting $prog: " + $cloud_init $CLOUDINITARGS init --local + RETVAL=$? + return $RETVAL +} + +stop() { + echo -n $"Shutting down $prog: " + # No-op + RETVAL=7 + return $RETVAL +} + +case "$1" in + start) + start + RETVAL=$? + ;; + stop) + stop + RETVAL=$? + ;; + restart|try-restart|condrestart) + ## Stop the service and regardless of whether it was + ## running or not, start it again. + # + ## Note: try-restart is now part of LSB (as of 1.9). + ## RH has a similar command named condrestart. + start + RETVAL=$? + ;; + reload|force-reload) + # It does not support reload + RETVAL=3 + ;; + status) + echo -n $"Checking for service $prog:" + # Return value is slightly different for the status command: + # 0 - service up and running + # 1 - service dead, but /var/run/ pid file exists + # 2 - service dead, but /var/lock/ lock file exists + # 3 - service not running (unused) + # 4 - service status unknown :-( + # 5--199 reserved (5--99 LSB, 100--149 distro, 150--199 appl.) + RETVAL=3 + ;; + *) + echo "Usage: $0 {start|stop|status|try-restart|condrestart|restart|force-reload|reload}" + RETVAL=3 + ;; +esac + +exit $RETVAL diff --git a/templates/hosts.aix.tmpl b/templates/hosts.aix.tmpl new file mode 100644 index 00000000000..805dcd16592 --- /dev/null +++ b/templates/hosts.aix.tmpl @@ -0,0 +1,19 @@ +## This file (/etc/cloud/templates/hosts.tmpl) is only utilized +## if enabled in cloud-config. Specifically, in order to enable it +## you need to add the following to config: +## manage_etc_hosts: True +## +## Note, double-hash commented lines will not appear in /etc/hosts +# +# Your system has configured 'manage_etc_hosts' as True. +# As a result, if you wish for changes to this file to persist +# then you will need to either +# a.) make changes to the master file in /opt/freeware/etc/cloud/templates/hosts.aix.tmpl +# b.) change or remove the value of 'manage_etc_hosts' in +# /opt/freeware/etc/cloud/cloud.cfg or cloud-config from user-data +# +# The following lines are desirable for IPv4 capable hosts +127.0.0.1 loopback localhost + +# The following lines are desirable for IPv6 capable hosts +::1 loopback localhost diff --git a/tests/unittests/main_fix_dhcp_default_route.py b/tests/unittests/main_fix_dhcp_default_route.py new file mode 100644 index 00000000000..e3ce3ca5e4a --- /dev/null +++ b/tests/unittests/main_fix_dhcp_default_route.py @@ -0,0 +1,50 @@ +#!/usr/bin/python3 + +import sys +from cloudinit import distros +from cloudinit import helpers +from cloudinit import settings + +print("PATH = {}".format(sys.path)) + +#sys.path.insert(0,'/gsa/ausgsa/home/s/t/sttran/PROJECTS/CLOUD-INIT/WORKSPACE') + +print("PATH = {}".format(sys.path)) + +#from cloudinit.distros import aix + +hostname_conf_fn = "/tmp/hosts" +resolve_conf_fn = "/tmp/resolv.conf" + + +BASE_NET_CFG = ''' +auto lo +iface lo inet loopback + +auto eth0 +iface eth0 inet static + address 9.3.148.132 + netmask 255.255.254.0 + broadcast 9.3.255.255 + gateway 9.3.149.1 + dns-nameservers 9.3.1.200 9.0.128.50 + dns-search aus.stglabs.ibm.com + +auto eth1 +iface eth1 inet dhcp + dns-search aus.stglabs.ibm.com +''' + + +# +# MAIN EXECUTION HERE +# +# True for bring_up +# +cls = distros.fetch('aix') +cfg = settings.CFG_BUILTIN +cfg['system_info']['distro'] = 'aix' +paths = helpers.Paths({}) +aix = cls('aix', cfg, paths) + +aix.apply_network(BASE_NET_CFG, True) diff --git a/tests/unittests/test_aix_apply_network.py b/tests/unittests/test_aix_apply_network.py new file mode 100644 index 00000000000..e548a2e86e4 --- /dev/null +++ b/tests/unittests/test_aix_apply_network.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 + + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_runcmd + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers +from cloudinit import settings + + +print("PATH = {}".format(sys.path)) + +#from cloudinit.distros import aix + + +hostname_conf_fn = "/tmp/hosts" +resolve_conf_fn = "/tmp/resolv.conf" + + +BASE_NET_CFG = ''' +auto lo +iface lo inet loopback + +iface eth1 inet6 static + address 2001::2 + hwaddres ether ca:6a:b2:42:97:02 + netmask 64 + mtu 9600 + pre-up [ $(ifconfig eth1 | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}') = "ca:6a:b2:42:97:02" ] + dns-search aus.stglabs.ibm.com +''' +#iface eth2 inet6 static +# address 2001::20 +# netmask 64 +# gateway 2001::20 + + + +# +# MAIN EXECUTION HERE +# +# True for bring_up +# +cls = distros.fetch('aix') +cfg = settings.CFG_BUILTIN +cfg['system_info']['distro'] = 'aix' +paths = helpers.Paths({}) +aix = cls('aix', cfg, paths) + +aix.apply_network(BASE_NET_CFG, True) + + + diff --git a/tests/unittests/test_apply_network.py b/tests/unittests/test_apply_network.py new file mode 100644 index 00000000000..ad4bfb5742d --- /dev/null +++ b/tests/unittests/test_apply_network.py @@ -0,0 +1,86 @@ +#!/usr/bin/python3 + +import subprocess +import re +import os +from StringIO import StringIO + +hostname_conf_fn = "/tmp/hosts" +resolve_conf_fn = "/tmp/resolv.conf" + + +BASE_NET_CFG = ''' +auto lo +iface lo inet loopback + +auto eth1 +iface eth1 inet static + address 192.168.1.5 + netmask 255.255.255.0 + network 192.168.1.0 + broadcast 192.168.1.255 + gateway 192.168.1.254 + +#auto eth1 +#iface eth1 inet dhcp + +dns-nameservers 9.3.1.200 +dns-search aus.stglabs.ibm.com +''' + +class ProcessExecutionError(IOError): + + MESSAGE_TMPL = ('%(description)s\n' + 'Command: %(cmd)s\n' + 'Exit code: %(exit_code)s\n' + 'Reason: %(reason)s\n' + 'Stdout: %(stdout)r\n' + 'Stderr: %(stderr)r') + + def __init__(self, stdout=None, stderr=None, + exit_code=None, cmd=None, + description=None, reason=None): + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : begin") + if not cmd: + self.cmd = '-' + else: + self.cmd = cmd + + if not description: + self.description = 'Unexpected error while running command.' + else: + self.description = description + + if not isinstance(exit_code, (long, int)): + self.exit_code = '-' + else: + self.exit_code = exit_code + + if not stderr: + self.stderr = '' + else: + self.stderr = stderr + + if not stdout: + self.stdout = '' + else: + self.stdout = stdout + + if reason: + self.reason = reason + else: + self.reason = '-' + + message = self.MESSAGE_TMPL % { + 'description': self.description, + 'cmd': self.cmd, + 'exit_code': self.exit_code, + 'stdout': self.stdout, + 'stderr': self.stderr, + 'reason': self.reason, + } + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : message=%s" % message) + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : end") + IOError.__init__(self, message) + + diff --git a/tests/unittests/test_apply_network_2.py b/tests/unittests/test_apply_network_2.py new file mode 100644 index 00000000000..ad4467a03b2 --- /dev/null +++ b/tests/unittests/test_apply_network_2.py @@ -0,0 +1,86 @@ +#!/usr/bin/python3 + +import subprocess +import re +import os +import socket +from StringIO import StringIO + +hostname_conf_fn = "/tmp/hosts" +resolve_conf_fn = "/tmp/resolv.conf" + + +BASE_NET_CFG = ''' +auto lo +iface lo inet loopback + +auto eth1 +iface eth1 inet static + address 192.168.1.5 + netmask 255.255.255.0 + network 192.168.1.0 + broadcast 192.168.1.255 + gateway 192.168.1.254 + +#auto eth1 +#iface eth1 inet dhcp + +dns-nameservers 9.3.1.200 +dns-search aus.stglabs.ibm.com +''' + +class ProcessExecutionError(IOError): + + MESSAGE_TMPL = ('%(description)s\n' + 'Command: %(cmd)s\n' + 'Exit code: %(exit_code)s\n' + 'Reason: %(reason)s\n' + 'Stdout: %(stdout)r\n' + 'Stderr: %(stderr)r') + + def __init__(self, stdout=None, stderr=None, + exit_code=None, cmd=None, + description=None, reason=None): + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : begin") + if not cmd: + self.cmd = '-' + else: + self.cmd = cmd + + if not description: + self.description = 'Unexpected error while running command.' + else: + self.description = description + + if not isinstance(exit_code, (long, int)): + self.exit_code = '-' + else: + self.exit_code = exit_code + + if not stderr: + self.stderr = '' + else: + self.stderr = stderr + + if not stdout: + self.stdout = '' + else: + self.stdout = stdout + + if reason: + self.reason = reason + else: + self.reason = '-' + + message = self.MESSAGE_TMPL % { + 'description': self.description, + 'cmd': self.cmd, + 'exit_code': self.exit_code, + 'stdout': self.stdout, + 'stderr': self.stderr, + 'reason': self.reason, + } + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : message=%s" % message) + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : end") + IOError.__init__(self, message) + diff --git a/tests/unittests/test_cc_chef.0.7.7.py b/tests/unittests/test_cc_chef.0.7.7.py new file mode 100644 index 00000000000..ad586ae469d --- /dev/null +++ b/tests/unittests/test_cc_chef.0.7.7.py @@ -0,0 +1,60 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_chef + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'chef': { + 'install_type': 'gems', + 'force_install': True, + 'server_url': 'https://sysmgt-hmc1.austin.ibm.com', + 'node_name': 'isotopes02', + 'environment': 'production', + 'exec': True, + 'validation_name': "yourorg-validator", + 'validation_key': '/etc/chef/validation.pem', + 'validation_cert': '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA3Y8rAIkjch0DXttMGBGNNb0iehNY/RDTK9j5mESfc3tW4JcKkC5iXxViL3WThxZkntbmy1yXeLfJxrSh0FlaPCLzpwMzcdS8/Y031hT7Durz8LPeq3IjiPJMErdmvLgewJsbv18qqQl1yPAAuGQ5zYf40qBR0WW2azGBnkASa9ai38WzcMFPPeJ4vQGdoc9rmD/Q7ah5DkiuuMM5z//FlhzGdZGWrxlvaAl/Xki2ZmlDsMcgc6q0hbDd6HahWKX5SF50iMBmL1j4SHRG0Ntjz7OXkDFAl5l5YQYJOJaqzXjJKtIMV79XFK72sYYu/gtBdiP34NSHHGTiPrQDDHbvWwIDAQABAoIBABVdqRf0IabvhVOwcjYf+y4jfx+mnf5JkRO5aNh2RaotSsN9zVb6IiJpPX62J/PvBOUMdFVIKJNLpfmzkac19q218Sk59cwUZ+VLqQbMHynhHoUn02FVMHgUZaGobg/k8ZJBYvuhgcurTeCCxI8Dm09mvWgSbdFzrZPIwmcwZpZffoXmXxPSPBNXVa3u9eSWDtpO/P/Mrq0VAFrZZVRg3ovdKOX2NjnyFdrYAqNUuESfRKnNVuuHaX+hxvW4oAFriduK8xM/pDSNCiIdfnD0AnG/OEFsZB6HQpyFpFR/TdC2GP37IBUVvnCbJTGvqSHDDLXR8xTj9HJXcp07MN8FyoECgYEA8GY9D1D8fT8YFktE/omNag2Mgb3aHkbjqBk8CNO7bPiQb5vnIwja6lZ+IuWVWjmZDVL2CSoKpDHdIIYNL8xzr+bLIszyRAL4FGGyIUKfLvyRHmHeRT4Eew+w4f8rYeTti6B7LdIl4hIq2dRy1x+MiYCxz4TV0JvtGAj+VhqvFJ8CgYEA6+/wIr00TEKz+9S1Vl0e3oeMIJKSyoFAMBuRjcEJ5gioqa5HcljIks2UiWESKT6np2qymdhGbBKPbBFqgf3Lbgk9LYNPc6oPSGQuW2kxsvioOaQXIcsv1+0w0zViHiIRARoNEGrKkerL0SoAwhmge1N5kwTdSBHPFTM0x8BBT8UCgYEA70UDLxRvSgWbZs0h7apgyxaTK6sXxpzOCEidfTeoS4yWzc9BXZh5s1XFE9yoK3Y6hI12/qYOk2Bh8/YYd+OpnYE72/ZahyDhY//c+MfDglO16KSGQyq38PgsGLQNrNDbMebX00JfnERyy/5tEvp+uXkTATX4Tjpz4EFLS84hRocCgYEA38EwozF+zKgh2z3yMBKmOPKh8S4wqn6Dqlwq4R3mzlL96dYPiiErLxZqvRLjT1xNUZf+A6s5tjqv7BRkRx2zdQqsC2LR0ebBEa14zVZpPMtXdzroeTMij4wx1sx03hD+wWW8aApvTI05eId2Kp51NSCIVuaxGS1SkE98ycfJ6OUCgYA4zgurTeB6VZF7VO408zX3cCmUlbYeNgEA1DsHcWAjZUdV2iYj/xxoO04neMq+NtvR0cHgru2xSdy2WmZusOCAsaNkJchMevklhVU1rGMIfghJgdSu7RMXSMUBY3wGpTmiyRcsCu4SUUV8UDCi2yqPAeXwr8Z8qN+3K33sC0LjUQ==\n-----END RSA PRIVATE KEY-----\n', + 'run_list': ['recipe[apache2]', 'role[db]'], + 'initial_attributes': { + 'apache': { + 'prefork': { + 'maxclients': 100, + }, + }, + 'keepalive': 'off' + }, + 'omnibus_url': 'https://www.opscode.com/chef/install.sh', + 'output': { 'all': '| tee -a /var/log/cloud-init-output.log'}, + }, +} + +cc = _get_cloud('aix') + +cc_chef.handle('cc_chef', cfg, cc, LOG, []) diff --git a/tests/unittests/test_cc_chef.py b/tests/unittests/test_cc_chef.py new file mode 100644 index 00000000000..2761bb67987 --- /dev/null +++ b/tests/unittests/test_cc_chef.py @@ -0,0 +1,59 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_chef + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'chef': { + 'install_type': 'gems', + 'force_install': True, + 'server_url': 'https://sysmgt-hmc1.austin.ibm.com', + 'node_name': 'isotopes02', + 'environment': 'production', + 'validation_name': "yourorg-validator", + 'validation_cert': '-----BEGIN RSA PRIVATE KEY-----\nBLAHBLAH\n-----END RSA PRIVATE KEY-----\n', + 'validation_key': '-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA3Y8rAIkjch0DXttMGBGNNb0iehNY/RDTK9j5mESfc3tW4JcKkC5iXxViL3WThxZkntbmy1yXeLfJxrSh0FlaPCLzpwMzcdS8/Y031hT7Durz8LPeq3IjiPJMErdmvLgewJsbv18qqQl1yPAAuGQ5zYf40qBR0WW2azGBnkASa9ai38WzcMFPPeJ4vQGdoc9rmD/Q7ah5DkiuuMM5z//FlhzGdZGWrxlvaAl/Xki2ZmlDsMcgc6q0hbDd6HahWKX5SF50iMBmL1j4SHRG0Ntjz7OXkDFAl5l5YQYJOJaqzXjJKtIMV79XFK72sYYu/gtBdiP34NSHHGTiPrQDDHbvWwIDAQABAoIBABVdqRf0IabvhVOwcjYf+y4jfx+mnf5JkRO5aNh2RaotSsN9zVb6IiJpPX62J/PvBOUMdFVIKJNLpfmzkac19q218Sk59cwUZ+VLqQbMHynhHoUn02FVMHgUZaGobg/k8ZJBYvuhgcurTeCCxI8Dm09mvWgSbdFzrZPIwmcwZpZffoXmXxPSPBNXVa3u9eSWDtpO/P/Mrq0VAFrZZVRg3ovdKOX2NjnyFdrYAqNUuESfRKnNVuuHaX+hxvW4oAFriduK8xM/pDSNCiIdfnD0AnG/OEFsZB6HQpyFpFR/TdC2GP37IBUVvnCbJTGvqSHDDLXR8xTj9HJXcp07MN8FyoECgYEA8GY9D1D8fT8YFktE/omNag2Mgb3aHkbjqBk8CNO7bPiQb5vnIwja6lZ+IuWVWjmZDVL2CSoKpDHdIIYNL8xzr+bLIszyRAL4FGGyIUKfLvyRHmHeRT4Eew+w4f8rYeTti6B7LdIl4hIq2dRy1x+MiYCxz4TV0JvtGAj+VhqvFJ8CgYEA6+/wIr00TEKz+9S1Vl0e3oeMIJKSyoFAMBuRjcEJ5gioqa5HcljIks2UiWESKT6np2qymdhGbBKPbBFqgf3Lbgk9LYNPc6oPSGQuW2kxsvioOaQXIcsv1+0w0zViHiIRARoNEGrKkerL0SoAwhmge1N5kwTdSBHPFTM0x8BBT8UCgYEA70UDLxRvSgWbZs0h7apgyxaTK6sXxpzOCEidfTeoS4yWzc9BXZh5s1XFE9yoK3Y6hI12/qYOk2Bh8/YYd+OpnYE72/ZahyDhY//c+MfDglO16KSGQyq38PgsGLQNrNDbMebX00JfnERyy/5tEvp+uXkTATX4Tjpz4EFLS84hRocCgYEA38EwozF+zKgh2z3yMBKmOPKh8S4wqn6Dqlwq4R3mzlL96dYPiiErLxZqvRLjT1xNUZf+A6s5tjqv7BRkRx2zdQqsC2LR0ebBEa14zVZpPMtXdzroeTMij4wx1sx03hD+wWW8aApvTI05eId2Kp51NSCIVuaxGS1SkE98ycfJ6OUCgYA4zgurTeB6VZF7VO408zX3cCmUlbYeNgEA1DsHcWAjZUdV2iYj/xxoO04neMq+NtvR0cHgru2xSdy2WmZusOCAsaNkJchMevklhVU1rGMIfghJgdSu7RMXSMUBY3wGpTmiyRcsCu4SUUV8UDCi2yqPAeXwr8Z8qN+3K33sC0LjUQ==\n-----END RSA PRIVATE KEY-----\n', + 'run_list': ['recipe[apache2]', 'role[db]'], + 'initial_attributes': { + 'apache': { + 'prefork': { + 'maxclients': 100, + }, + }, + 'keepalive': 'off' + }, + 'omnibus_url': 'https://www.opscode.com/chef/install.sh', + 'output': { 'all': '| tee -a /var/log/cloud-init-output.log'}, + }, +} + +cc = _get_cloud('aix') + +cc_chef.handle('cc_chef', cfg, cc, LOG, []) diff --git a/tests/unittests/test_cc_locale.py b/tests/unittests/test_cc_locale.py new file mode 100644 index 00000000000..73a63826048 --- /dev/null +++ b/tests/unittests/test_cc_locale.py @@ -0,0 +1,45 @@ +#!/usr/bin/python3 + +import sys +import os + + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_locale + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging + +LOG = logging.getLogger(__name__) + +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'locale': 'en_US', +} + + + +cc = _get_cloud('aix') + +cc_locale.handle('cc_locale', cfg, cc, LOG, []) + +# Check /etc/environment for the timezone value diff --git a/tests/unittests/test_cc_power_state_change.py b/tests/unittests/test_cc_power_state_change.py new file mode 100644 index 00000000000..248b14f7090 --- /dev/null +++ b/tests/unittests/test_cc_power_state_change.py @@ -0,0 +1,41 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_power_state_change + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + + +cfg = { + 'power_state': {'mode': 'reboot', 'message': 'REBOOTING'} + +} +cc = _get_cloud('aix') + +cc_power_state_change.handle('cc_power_state_change', cfg, cc, LOG, []) + diff --git a/tests/unittests/test_cc_resolv_conf.py b/tests/unittests/test_cc_resolv_conf.py new file mode 100644 index 00000000000..aeb2366e937 --- /dev/null +++ b/tests/unittests/test_cc_resolv_conf.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 + +import sys +import os + + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_resolv_conf + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging + +LOG = logging.getLogger(__name__) + +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'manage_resolv_conf': True, + 'resolv_conf': { + 'nameservers': ['9.3.1.200'], + 'domain': 'aus.stglabs.ibm.com', + 'searchdomains': ['aus.stglabs.ibm.com'] + } +} + +cc = _get_cloud('aix') + +cc_resolv_conf.handle('cc_resolv_conf', cfg, cc, LOG, []) diff --git a/tests/unittests/test_cc_runcmd.py b/tests/unittests/test_cc_runcmd.py new file mode 100644 index 00000000000..9bf51361dc3 --- /dev/null +++ b/tests/unittests/test_cc_runcmd.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_runcmd + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None, 'instance-id': 'b302d9a1-ea2a-4f1f-930a-c0d0aa1dc5cf'}}, + 'runcmd': ["echo 'Instance has been configured by cloud-init.' | wall"], + 'instance-id': {'default': 'iid-datasource-none'}, +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths(cfg, {'datasource': {'NoCloud': {'fs_label': None, 'instance-id': 'b302d9a1-ea2a-4f1f-930a-c0d0aa1dc5cf'}}}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({'NoCloud': {'fs_label': None, 'instance-id': 'b302d9a1-ea2a-4f1f-930a-c0d0aa1dc5cf'}}, d, paths) + cc = cloud.Cloud(ds, paths, cfg, d, None) + return cc + + +#cfg = { +# 'instance-id': {'default': 'iid-datasource-none'}, +# 'datasource': {'NoCloud': {'fs_label': None}, 'instance-id': 'b302d9a1-ea2a-4f1f-930a-c0d0aa1dc5cf'}, +# 'runcmd': ["echo 'Instance has been configured by cloud-init.' | wall"], +#} +cc = _get_cloud('aix') + +cc_runcmd.handle('cc_runcmd', cfg, cc, LOG, []) diff --git a/tests/unittests/test_cc_set_hostname.py b/tests/unittests/test_cc_set_hostname.py new file mode 100644 index 00000000000..c048a82fced --- /dev/null +++ b/tests/unittests/test_cc_set_hostname.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_set_hostname + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging + +LOG = logging.getLogger(__name__) + +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'preserve_hostname': 'True', +} + +cc = _get_cloud('aix') +#cc_set_hostname_bytes = str.encode('cc_set_hostname') + +cc_set_hostname.handle('cc_set_hostname', cfg, cc, LOG, []) + diff --git a/tests/unittests/test_cc_set_passwords.py b/tests/unittests/test_cc_set_passwords.py new file mode 100644 index 00000000000..8446a917a29 --- /dev/null +++ b/tests/unittests/test_cc_set_passwords.py @@ -0,0 +1,44 @@ +#!/usr/bin/python3 + + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_set_passwords + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'chpasswd': { + 'expire': False, + 'list': 'root:What1niceday!\n' + } +} + +cc = _get_cloud('aix') + +# Test changing password on the user +cc_set_passwords.handle('cc_set_passwords', cfg, cc, LOG, []) diff --git a/tests/unittests/test_cc_ssh.py b/tests/unittests/test_cc_ssh.py new file mode 100644 index 00000000000..e947ca53f1d --- /dev/null +++ b/tests/unittests/test_cc_ssh.py @@ -0,0 +1,47 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_ssh + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'users': ['default', { + 'sudo': 'ALL=(ALL) NOPASSWD:ALL', + 'gecos': 'Mr. Demo', + #'name': 'foobar', + 'name': 'demo', + #'groups': 'staff', + 'groups': 'group2', + 'ssh-import-id': 'demo', + }] +} + +cc = _get_cloud('aix') + +cc_ssh.handle('cc_ssh', cfg, cc, LOG, []) diff --git a/tests/unittests/test_cc_ssh_import_id.py b/tests/unittests/test_cc_ssh_import_id.py new file mode 100644 index 00000000000..bc167381668 --- /dev/null +++ b/tests/unittests/test_cc_ssh_import_id.py @@ -0,0 +1,49 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_ssh_import_id + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'users': ['default', { + 'sudo': 'ALL=(ALL) NOPASSWD:ALL', + 'gecos': 'Mr Demo', + #'name': 'foobar', + 'name': 'demo', + #'groups': 'staff', + 'groups': 'group2', + #'ssh-import-id': 'foobar', + 'ssh-import-id': 'demo', + }], + 'packages': 'ssh-import-id', +} + +cc = _get_cloud('aix') + +cc_ssh_import_id.handle('cc_ssh_import_id', cfg, cc, LOG, []) diff --git a/tests/unittests/test_cc_timezone.py b/tests/unittests/test_cc_timezone.py new file mode 100644 index 00000000000..961c1d728f0 --- /dev/null +++ b/tests/unittests/test_cc_timezone.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_timezone + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + + +cfg = { + 'timezone': 'CST6CDT', +} +cc = _get_cloud('aix') + +# Create a dummy timezone file +cc_timezone.handle('cc_timezone', cfg, cc, LOG, []) + +# Check /etc/environment for the timezone value diff --git a/tests/unittests/test_cc_update_etc_hosts.py b/tests/unittests/test_cc_update_etc_hosts.py new file mode 100644 index 00000000000..28a66ae29e1 --- /dev/null +++ b/tests/unittests/test_cc_update_etc_hosts.py @@ -0,0 +1,40 @@ +#!/usr/bin/python3 +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_update_etc_hosts + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging + +LOG = logging.getLogger(__name__) + +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + +cfg = { + 'manage_etc_hosts': 'True', +} + +cc = _get_cloud('aix') + +cc_update_etc_hosts.handle('cc_update_etc_hosts', cfg, cc, LOG, []) + diff --git a/tests/unittests/test_cc_users_groups.py b/tests/unittests/test_cc_users_groups.py new file mode 100644 index 00000000000..d5ee02ea087 --- /dev/null +++ b/tests/unittests/test_cc_users_groups.py @@ -0,0 +1,54 @@ +#!/usr/bin/python3 + +import sys +import os +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_users_groups + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + + +cfg = { + 'users': [{ + 'ssh-authorized-keys': [ + 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCYWbhUTfKRfgtTUU1/u2KVNXkj1cWoBCW730W15x3lXZfgdKIjxYXzbSOrWJamj9CFOzIcy3CZ4CqAaIDb37SGY0mhfcPtgL1R2DVCsppbkv/V34omxGn2G//SK6GtirMhN4d6ze4PSDqvd6sotEDK6Z7TwPuUhxGh7Z2T/0OC98DSYHiUmshBQbml4qynnsl74ybhXzm4e/pk3HF9WZnAQEOxQxWnWLAOtnpllgNB93S6/AOiMXAObUq1vaUPOZKgBsmXB7heYLveU0bXucYODzrYGwL5gYWGsUMvukGV8DgCySym3VivOe0gnhX/FN6vZ8589y6dZvRlMXtQZ/bgGYWAMiNyusafYFiUhBlrT+K5H37kr4F9p0Ytul/MC800c9txlGiqoKSBhn6dDBL9mC7hnHQgqXP3ZOnWDWvkOqySTigu/UTtf0n5KHkD0q5BwDkXgRdQsCF9Ov3bJTnSr+H3r6znXnhxtMkCyQzFCZ8h5XdKHrIABVm65yodmKE= root@idevp9-lp2', + ], + 'shell': '/bin/ksh', + 'name': 'demo', + 'groups': 'group2', + 'sudo': ['ALL=(ALL) NOPASSWD:ALL'], + 'runcmd': ['touch /tmp/test.txt']}], + 'groups': [ + 'group1', + {'group2': ['demo']} + ], +} +cc = _get_cloud('aix') + +# Check /etc/security/login.cfg to see if the 'shell' such as /bin/bash is a valid option +#cc_str= str(cc_users_groups) +#cc_users_groups_byte= cc_str.encode() +#print(type(cc_users_groups_byte)) +cc_users_groups.handle('cc_users_groups', cfg, cc, LOG, []) diff --git a/tests/unittests/test_cc_write_files.py b/tests/unittests/test_cc_write_files.py new file mode 100644 index 00000000000..7428b83a84e --- /dev/null +++ b/tests/unittests/test_cc_write_files.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 + +import sys +import os + +# DECLARE AFTER THIS FOR TESTING CLOUDINIT + +from cloudinit.config import cc_write_files + +from cloudinit import cloud +from cloudinit import distros +from cloudinit import helpers + +from cloudinit.sources import DataSourceNoCloud + +from cloudinit import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +LOG = logging.getLogger(__name__) + +logging.setupLogging(cfg) + +def _get_cloud(distro): + paths = helpers.Paths({}) + cls = distros.fetch(distro) + d = cls(distro, {}, paths) + ds = DataSourceNoCloud.DataSourceNoCloud({}, d, paths) + cc = cloud.Cloud(ds, paths, {}, d, None) + return cc + + +cfg = { + 'write_files': [{ + 'content': 'Here is a line.\nAnother line is here.\n', + 'path': '/tmp/test.txt' + }], +} +cc = _get_cloud('aix') + +cc_write_files.handle('cc_write_files', cfg, cc, LOG, []) diff --git a/tests/unittests/test_dhcp_apply_network.py b/tests/unittests/test_dhcp_apply_network.py new file mode 100644 index 00000000000..ebe1d7af9a6 --- /dev/null +++ b/tests/unittests/test_dhcp_apply_network.py @@ -0,0 +1,117 @@ +#!/usr/bin/python3 + + +import subprocess +import re +import os +import time +import tempfile +import shutil +import contextlib +from StringIO import StringIO + +hostname_conf_fn = "/tmp/hosts" +resolve_conf_fn = "/tmp/resolv.conf" + +# +#BASE_NET_CFG = ''' +#auto lo +#iface lo inet loopback +# +#auto eth0 +#iface eth0 inet static +# address 9.3.148.132 +# netmask 255.255.254.0 +# network 9.3.148.0 +# broadcast 9.3.148.255 +# gateway 9.3.149.1 +# +#iface eth1 inet6 static +# address 2001::2 +# netmask 64 +# gateway 2001::2 +# +#dns-nameservers 9.3.1.200 +#dns-search aus.stglabs.ibm.com +#''' + +BASE_NET_CFG = ''' +auto lo +iface lo inet loopback + +iface eth0 inet static + address 9.3.148.132 + netmask 255.255.254.0 + network 9.3.148.0 + broadcast 9.3.148.255 + gateway 9.3.149.1 + +iface eth1 inet6 static + address 2001::2 + netmask 64 + gateway 2001::2 + +iface eth2 inet dhcp + address 30.0.0.66 + gateway 9.3.149.1 + netmask 255.255.255.0 + +dns-nameservers 9.3.1.200 +dns-search aus.stglabs.ibm.com +''' + +class ProcessExecutionError(IOError): + + MESSAGE_TMPL = ('%(description)s\n' + 'Command: %(cmd)s\n' + 'Exit code: %(exit_code)s\n' + 'Reason: %(reason)s\n' + 'Stdout: %(stdout)r\n' + 'Stderr: %(stderr)r') + + def __init__(self, stdout=None, stderr=None, + exit_code=None, cmd=None, + description=None, reason=None): + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : begin") + if not cmd: + self.cmd = '-' + else: + self.cmd = cmd + + if not description: + self.description = 'Unexpected error while running command.' + else: + self.description = description + + if not isinstance(exit_code, (long, int)): + self.exit_code = '-' + else: + self.exit_code = exit_code + + if not stderr: + self.stderr = '' + else: + self.stderr = stderr + + if not stdout: + self.stdout = '' + else: + self.stdout = stdout + + if reason: + self.reason = reason + else: + self.reason = '-' + + message = self.MESSAGE_TMPL % { + 'description': self.description, + 'cmd': self.cmd, + 'exit_code': self.exit_code, + 'stdout': self.stdout, + 'stderr': self.stderr, + 'reason': self.reason, + } + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : message=%s" % message) + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : end") + IOError.__init__(self, message) + diff --git a/tests/unittests/test_ipv6_apply_network.py b/tests/unittests/test_ipv6_apply_network.py new file mode 100644 index 00000000000..4f3b49d94cc --- /dev/null +++ b/tests/unittests/test_ipv6_apply_network.py @@ -0,0 +1,128 @@ +#!/usr/bin/python3 + + +import subprocess +import re +import os +import time +import tempfile +import shutil +import contextlib +from StringIO import StringIO + +hostname_conf_fn = "/tmp/hosts" +resolve_conf_fn = "/tmp/resolv.conf" + +# +#BASE_NET_CFG = ''' +#auto lo +#iface lo inet loopback +# +#auto eth0 +#iface eth0 inet static +# address 9.3.148.132 +# hwaddres ether ca:6a:b2:42:97:02 +# netmask 255.255.254.0 +# network 9.3.148.0 +# broadcast 9.3.148.255 +# pre-up [ $(ifconfig eth0 | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}') = "ca:6a:b2:42:97:02" ] +# gateway 9.3.149.1 +# dns-search aus.stglabs.ibm.com +# +#iface eth1 inet6 static +# address 2001::2 +# hwaddres ether ca:6a:b2:42:97:02 +# netmask 64 +# gateway 2001::2 +# pre-up [ $(ifconfig eth1 | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}') = "ca:6a:b2:42:97:02" ] +# dns-search aus.stglabs.ibm.com +# +#iface eth2 inet dhcp +# address 30.0.0.66 +# gateway 9.3.149.1 +# netmask 255.255.255.0 +# +#iface eth3 inet dhcp +# address 30.0.0.70 +# gateway 9.3.149.1 +# netmask 255.255.255.0 +# +#dns-nameservers 9.3.1.200 +#dns-search aus.stglabs.ibm.com +#''' + +BASE_NET_CFG = ''' +auto lo +iface lo inet loopback + +iface eth1 inet6 static + address 2001::2 + hwaddres ether ca:6a:b2:42:97:02 + netmask 64 + gateway 2001::2 + pre-up [ $(ifconfig eth1 | grep -o -E '([[:xdigit:]]{1,2}:){5}[[:xdigit:]]{1,2}') = "ca:6a:b2:42:97:02" ] + dns-search aus.stglabs.ibm.com + +iface eth2 inet6 static + address 2001::20 + netmask 64 + gateway 2001::20 + +''' + +class ProcessExecutionError(IOError): + + MESSAGE_TMPL = ('%(description)s\n' + 'Command: %(cmd)s\n' + 'Exit code: %(exit_code)s\n' + 'Reason: %(reason)s\n' + 'Stdout: %(stdout)r\n' + 'Stderr: %(stderr)r') + + def __init__(self, stdout=None, stderr=None, + exit_code=None, cmd=None, + description=None, reason=None): + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : begin") + if not cmd: + self.cmd = '-' + else: + self.cmd = cmd + + if not description: + self.description = 'Unexpected error while running command.' + else: + self.description = description + + if not isinstance(exit_code, (long, int)): + self.exit_code = '-' + else: + self.exit_code = exit_code + + if not stderr: + self.stderr = '' + else: + self.stderr = stderr + + if not stdout: + self.stdout = '' + else: + self.stdout = stdout + + if reason: + self.reason = reason + else: + self.reason = '-' + + message = self.MESSAGE_TMPL % { + 'description': self.description, + 'cmd': self.cmd, + 'exit_code': self.exit_code, + 'stdout': self.stdout, + 'stderr': self.stderr, + 'reason': self.reason, + } + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : message=%s" % message) + print("SCOTT::DEBUG :: /usr/opt/python3/lib/python3.9/site-packages/cloudinit/util.py : CLASS ProcessExecutionError : __init__() : end") + IOError.__init__(self, message) + + diff --git a/tests/unittests/test_netinfo_aix.py b/tests/unittests/test_netinfo_aix.py new file mode 100644 index 00000000000..a03ec3f5dd3 --- /dev/null +++ b/tests/unittests/test_netinfo_aix.py @@ -0,0 +1,21 @@ +#!/usr/bin/python3 +import sys +import os +import itertools as it +import functools + +print("HERE") + +from cloudinit import netinfo + +from cloudinit import log as logging +#from distutils import log as logging +cfg = { + 'datasource': {'NoCloud': {'fs_label': None}} +} + +logging.setupLogging(cfg) + + + +print("%s\n" % (netinfo.debug_info())) diff --git a/tests/unittests/test_yaml_output.py b/tests/unittests/test_yaml_output.py new file mode 100644 index 00000000000..00d24ef566b --- /dev/null +++ b/tests/unittests/test_yaml_output.py @@ -0,0 +1,83 @@ +#!/usr/bin/python3 + +import yaml +document = """ + chpasswd: + list: | + root1:nimfvt1 + user2:password1 + expire: False + + users: + - default + - name: foobar + gecos: Foo B. Bar + groups: staff + sudo: ALL=(ALL) NOPASSWD:ALL + + manage_resolv_conf: True + resolv_conf: + nameservers: + - 9.3.1.200 + - 9.0.128.50 + - 9.0.130.50 + searchdomains: + - austin.ibm.com + - aus.stglabs.ibm.com + domain: austin.ibm.com + + chef: + install_type: "packages" + force_install: False + server_url: "https://tranwin.austin.ibm.com" + node_name: "isotopes02" + environment: "production" + exec: True + validation_name: "yourorg-validator" + validation_key: "/etc/chef/validation.pem" + validation_cert: | + -----BEGIN RSA PRIVATE KEY----- + YOUR-ORGS-VALIDATION-KEY-HERE + -----END RSA PRIVATE KEY----- + run_list: + - "recipe[apache2]" + - "role[db]" + initial_attributes: + apache: + prefork: + maxclients: 100 + keepalive: "off" + + omnibus_url: "https://www.opscode.com/chef/install.sh" + output: { 'all': '| tee -a /var/log/cloud-init-output.log'} + + write_files: + - path: /tmp/test.txt + content: | + Here is a line. + Another line is here. + + users: + - name: demo + groups: sudo + shell: /bin/bash + sudo: ['ALL=(ALL) NOPASSWD:ALL'] + ssh-authorized-keys: + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDf0q4PyG0doiBQYV7OlOxbRjle026hJPBWD+eKHWuVXIpAiQlSElEBqQn0pOqNJZ3IBCvSLnrdZTUph4czNC4885AArS9NkyM7lK27Oo8RV888jWc8hsx4CD2uNfkuHL+NI5xPB/QT3Um2Zi7GRkIwIgNPN5uqUtXvjgA+i1CS0Ku4ld8vndXvr504jV9BMQoZrXEST3YlriOb8Wf7hYqphVMpF3b+8df96Pxsj0+iZqayS9wFcL8ITPApHi0yVwS8TjxEtI3FDpCbf7Y/DmTGOv49+AWBkFhS2ZwwGTX65L61PDlTSAzL+rPFmHaQBHnsli8U9N6E4XHDEOjbSMRX user@example.com + - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDcthLR0qW6y1eWtlmgUE/DveL4XCaqK6PQlWzi445v6vgh7emU4R5DmAsz+plWooJL40dDLCwBt9kEcO/vYzKY9DdHnX8dveMTJNU/OJAaoB1fV6ePvTOdQ6F3SlF2uq77xYTOqBiWjqF+KMDeB+dQ+eGyhuI/z/aROFP6pdkRyEikO9YkVMPyomHKFob+ZKPI4t7TwUi7x1rZB1GsKgRoFkkYu7gvGak3jEWazsZEeRxCgHgAV7TDm05VAWCrnX/+RzsQ/1DecwSzsP06DGFWZYjxzthhGTvH/W5+KFyMvyA+tZV4i1XM+CIv/Ma/xahwqzQkIaKUwsldPPu00jRN user@desktop + runcmd: + - touch /tmp/test.txt + + groups: + - group1 + - group2: [demo] + + runcmd: + - echo 'Instance has been configured by cloud-init.' | wall + + instance-id: + default: iid-dsconfigdrive +""" + +print(yaml.load(document)) + diff --git a/tools/create_pvid_to_vg_mappings.sh b/tools/create_pvid_to_vg_mappings.sh new file mode 100644 index 00000000000..5705f22be21 --- /dev/null +++ b/tools/create_pvid_to_vg_mappings.sh @@ -0,0 +1,15 @@ +#!/usr/bin/sh + +############# +# Save disk mappings to be restored during deploy +# Each line of the mapping file contains " " as output by the lspv command. +############# + +CLOUD_DIR="/opt/freeware/etc/cloud" +PVID_VG_MAPPING_FILE="$CLOUD_DIR/pvid_to_vg_mappings" +if [ ! -d "$CLOUD_DIR" ] +then + /usr/bin/mkdir -p $CLOUD_DIR +fi + +LANG=C; /usr/sbin/lspv | /usr/bin/awk '{if ($2 != "none" && $3 != "rootvg") print $3 " " $2}' > $PVID_VG_MAPPING_FILE