diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index 9e9454ee646..52feac04f4b 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -45,7 +45,8 @@ ) from cloudinit.reporting import events from cloudinit.safeyaml import load -from cloudinit.settings import PER_INSTANCE, PER_ALWAYS, PER_ONCE, CLOUD_CONFIG +from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE, + CLOUD_CONFIG, RUN_CLOUD_CONFIG) # Welcome message template WELCOME_MSG_TPL = ( @@ -426,6 +427,15 @@ def main_init(name, args): _maybe_persist_instance_data(init) # Stage 6 iid = init.instancify() + if init.is_new_instance(): + util.multi_log(""" + +********************************************************* +* cloud-init is configuring this system, please wait... * +********************************************************* + +""", console=True, stderr=True, log=LOG) + LOG.debug( "[%s] %s will now be targeting instance id: %s. new=%s", mode, @@ -702,7 +712,7 @@ def status_wrapper(name, args, data_d=None, link_d=None): paths = read_cfg_paths() data_d = paths.get_cpath("data") if link_d is None: - link_d = os.path.normpath("/run/cloud-init") + link_d = os.path.dirname(os.path.normpath(RUN_CLOUD_CONFIG)) status_path = os.path.join(data_d, "status.json") status_link = os.path.join(link_d, "status.json") diff --git a/cloudinit/config/cc_package_update_upgrade_install.py b/cloudinit/config/cc_package_update_upgrade_install.py index a26e001d311..457e34b850b 100644 --- a/cloudinit/config/cc_package_update_upgrade_install.py +++ b/cloudinit/config/cc_package_update_upgrade_install.py @@ -76,8 +76,13 @@ def _multi_cfg_bool_get(cfg, *keys): return False -def _fire_reboot(wait_attempts=6, initial_sleep=1, backoff=2): - subp.subp(REBOOT_CMD) +def _fire_reboot(cloud, wait_attempts=6, initial_sleep=1, backoff=2): + try: + cmd = cloud.distro.shutdown_command(mode='reboot', delay='now', + message='Rebooting after package installation') + except: + cmd = REBOOT_CMD + subp.subp(cmd) start = time.time() wait_time = initial_sleep for _i in range(wait_attempts): @@ -135,7 +140,7 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: ) # Flush the above warning + anything else out... flush_loggers(LOG) - _fire_reboot() + _fire_reboot(cloud) except Exception as e: util.logexc(LOG, "Requested reboot did not happen!") errors.append(e) diff --git a/cloudinit/config/cc_resizefs.py b/cloudinit/config/cc_resizefs.py index dfcca4f0042..423bc0243df 100644 --- a/cloudinit/config/cc_resizefs.py +++ b/cloudinit/config/cc_resizefs.py @@ -11,6 +11,7 @@ import errno import logging import os +import re import stat from textwrap import dedent @@ -20,6 +21,7 @@ from cloudinit.config.schema import MetaSchema, get_meta_doc from cloudinit.distros import ALL_DISTROS from cloudinit.settings import PER_ALWAYS +from cloudinit import temp_utils NOBLOCK = "noblock" @@ -130,6 +132,15 @@ def _can_skip_resize_ufs(mount_point, devpth): return False +def _can_skip_resize_zfs(zpool, devpth): + try: + (out, _err) = subp.subp(['zpool', 'get', '-Hp', '-o', 'value', + 'expandsz', zpool]) + return out.strip() == '-' + except subp.ProcessExecutionError as e: + return False + + # Do not use a dictionary as these commands should be able to be used # for multiple filesystem types if possible, e.g. one command for # ext2, ext3 and ext4. @@ -143,7 +154,10 @@ def _can_skip_resize_ufs(mount_point, devpth): ("bcachefs", _resize_bcachefs), ] -RESIZE_FS_PRECHECK_CMDS = {"ufs": _can_skip_resize_ufs} +RESIZE_FS_PRECHECK_CMDS = { + "ufs": _can_skip_resize_ufs, + "zfs": _can_skip_resize_zfs, +} def can_skip_resize(fs_type, resize_what, devpth): @@ -267,7 +281,12 @@ def handle(name: str, cfg: Config, cloud: Cloud, args: list) -> None: info = "dev=%s mnt_point=%s path=%s" % (devpth, mount_point, resize_what) LOG.debug("resize_info: %s", info) - devpth = maybe_get_writable_device_path(devpth, info) + if util.is_illumos() and fs_type == 'zfs': + # On illumos ZFS, the devices are just bare words like 'c0t0d0' + # which can be used directly as arguments for the resize. + pass + else: + devpth = maybe_get_writable_device_path(devpth, info) if not devpth: return # devpath was not a writable block device diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 79e2623562f..a7be02ccb2f 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -91,6 +91,7 @@ ], "openeuler": ["openeuler"], "OpenCloudOS": ["OpenCloudOS", "TencentOS"], + "illumos": ["omnios"], } LOG = logging.getLogger(__name__) diff --git a/cloudinit/distros/illumos.py b/cloudinit/distros/illumos.py new file mode 100644 index 00000000000..642b16a84dd --- /dev/null +++ b/cloudinit/distros/illumos.py @@ -0,0 +1,323 @@ +import os, platform, logging + +from cloudinit import distros +from cloudinit import helpers +from cloudinit import net +from cloudinit import subp +from cloudinit import util +from .networking import illumosNetworking +from cloudinit.net import dhcp +from cloudinit.net.netops.illumos_netops import illumosNetOps + +LOG = logging.getLogger(__name__) + + +class Distro(distros.Distro): + networking_cls = illumosNetworking + net_ops = illumosNetOps + + hostname_conf_fn = "/etc/nodename" + hosts_fn = "/etc/inet/hosts" + tz_zone_dir = "/usr/share/lib/zoneinfo" + home_dir = '/home' + init_cmd = ['svcadm'] + + def __init__(self, name, cfg, paths): + super().__init__(name, cfg, paths) + self._runner = helpers.Runners(paths) + self.osfamily = 'illumos' + self.dhcp_client_priority = [dhcp.illumosDhcp] + self.networking_cls = illumosNetworking + self.net_ops = illumosNetOps + + shutdown_options_map = { + 'halt': ['-i', '0'], + 'poweroff': ['-i', '5'], + 'reboot': ['-i', '6'], + } + + def shutdown_command(self, *, mode, delay, message): + command = ['shutdown', '-y'] + command.extend(self.shutdown_options_map[mode]) + if delay == 'now': + delay = 0 + else: + try: + delay = int(delay) + except ValueError as e: + raise TypeError( + "power_state[delay] must be 'now' or '+m' (minutes)." + " found '%s'." % (delay,) + ) from e + + command.extend(['-g', str(delay)]) + if message: + command.append(message) + + return command + + def manage_service(self, action, service): + init_cmd = self.init_cmd + + if action == 'status': + cmd = ['svcs', '-H', '-o', 'state', service] + try: + (out, err) = subp.subp(cmd, capture=True) + except subp.ProcessExecutionError: + # Pass back to caller + raise + + # This emulates what the callers expect (since they still mostly + # assume Linux). Successful execution if the service is online, + # otherwise a non-zero exit code. + if out == "online": + return (None, None) + + # Callers do not actually check the exit status unless + # distro.uses_systemd() is True but exit code 3 would mean + # 'not running', so we use that. + raise subp.ProcessExecutionError( + cmd=cmd, stdout=out, stderr=err, exit_code=3 + ) + + cmds = {'stop': ['stop', service], + 'start': ['start', service], + 'enable': ['enable', service], + 'restart': ['restart', service], + 'reload': ['restart', service], + 'try-reload': ['restart', service], + } + cmd = list(init_cmd) + list(cmds[action]) + return subp.subp(cmd, capture=True) + + def generate_fallback_config(self): + return self.networking.generate_fallback_config() + + def _read_system_hostname(self): + sys_hostname = self._read_hostname(self.hostname_conf_fn) + return (self.hostname_conf_fn, sys_hostname) + + def _read_hostname(self, filename, default=None): + return util.load_file(filename).strip() + + def _write_hostname(self, hostname, filename): + content = hostname + '\n' + util.write_file(filename, content) + + def _write_profiles(self, user, profiles): + pfile = '/etc/user_attr.d/cloud-init-users' + + lines = [ + "", + "# User rules for %s" % user, + "%s::::type=normal;profiles=%s" % (user, profiles) + ] + content = "\n".join(lines) + "\n" + + if not os.path.exists(pfile): + contents = [ + util.make_header(), + content, + ] + try: + util.write_file(pfile, "\n".join(contents), 0o440) + except IOError as e: + util.logexc(LOG, "Failed to write user attr file %s", pfile) + raise e + else: + try: + util.append_file(pfile, content) + except IOError as e: + util.logexc(LOG, "Failed to append user attr file %s", pfile) + raise e + + self.manage_service('restart', 'system/rbac') + + def create_user(self, name, **kwargs): + super().create_user(name, **kwargs); + + # Configure profiles + if "profiles" in kwargs and kwargs["profiles"] is not False: + self._write_profiles(name, kwargs["profiles"]) + + def create_group(self, name, members=None): + group_add_cmd = ['groupadd', name] + + # Check if group exists, and then add it doesn't + if util.is_group(name): + LOG.warning("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(LOG, "Failed to create group %s", name) + + def add_user(self, name, **kwargs): + if util.is_user(name): + LOG.info("User %s already exists, skipping.", name) + return False + + useradd_cmd = ['useradd'] + + useradd_opts = { + 'homedir': '-d', + 'gecos': '-c', + 'primary_group': '-g', + 'groups': '-G', + 'shell': '-s', + 'inactive': '-f', + 'expiredate': '-e', + 'uid': '-u', + } + + if 'create_groups' in kwargs: + create_groups = kwargs.pop('create_groups') + else: + create_groups = True + + # support kwargs having groups=[list] or groups="g1,g2" + groups = kwargs.get('groups') + if groups: + if isinstance(groups, str): + groups = groups.split(",") + + # remove any white spaces in group names, most likely + # that came in as a string like: groups: group1, group2 + groups = [g.strip() for g in groups] + + # kwargs.items loop below wants a comma delimited string + # that can go right through to the command. + kwargs['groups'] = ",".join(groups) + + primary_group = kwargs.get('primary_group') + if primary_group: + groups.append(primary_group) + + if create_groups and groups: + for group in groups: + if not util.is_group(group): + self.create_group(group) + LOG.debug("created group '%s' for user '%s'", group, name) + + for key, val in kwargs.items(): + if key in useradd_opts and val and isinstance(val, str): + useradd_cmd.extend([useradd_opts[key], val]) + + if 'no_create_home' in kwargs or 'system' in kwargs: + pass + else: + useradd_cmd.extend(['-m', '-z', + '-d', '{home_dir}/{name}'.format( + home_dir=self.home_dir, name=name)]) + + useradd_cmd.append(name) + + # Run the command + LOG.info("Adding user %s", name) + try: + subp.subp(useradd_cmd) + except Exception: + util.logexc(LOG, "Failed to create user %s", name) + raise + # Set the password if it is provided + # For security consideration, only hashed passwd is assumed + passwd_val = kwargs.get('passwd', None) + if passwd_val is not None: + self.set_passwd(name, passwd_val, hashed=True) + + def expire_passwd(self, user): + try: + subp.subp(['passwd', '-f', user]) + except Exception: + util.logexc(LOG, "Failed to expire password for %s", user); + raise + + def lock_passwd(self, user): + try: + subp.subp(['passwd', '-N', user]) + except Exception: + util.logexc(LOG, 'Failed to disable password for user %s', user) + raise + + def set_passwd(self, user, passwd, hashed=False): + if hashed: + hashed_pw = passwd + else: + method = crypt.METHOD_SHA512 + hashed_pw = crypt.crypt( + passwd, + crypt.mksalt(method) + ) + + try: + subp.subp(['/usr/lib/passmgmt', '-m', '-p', hashed_pw, user], + logstring=f'/usr/lib/passmgmt -m -p {user}') + except Exception: + util.logexc(LOG, "Failed to set password for %s", user) + raise + + def install_packages(self, pkglist): + raise NotImplementedError() + + def package_command(self, command, args=None, pkgs=None): + raise NotImplementedError() + + def update_package_sources(self): + raise NotImplementedError() + + def _update_init(self, key, val, prefixes=None): + out_fn = '/etc/default/init' + + if prefixes is None: + prefixes = (f'{key}=') + + try: + content = util.load_file(out_fn).splitlines() + except OSError as err: + if err.errno != errno.ENOENT: + raise + content = [] + content = [a for a in content if not a.startswith(prefixes)] + LOG.debug(f'Setting {key}={val} in {out_fn}') + content.append(f'{key}={val}') + content.append('') + util.write_file(out_fn, "\n".join(content)) + + def apply_locale(self, locale, out_fn=None): + self._update_init('LC_ALL', locale, ('LC_', 'LANG')) + + def set_timezone(self, tz): + self._update_init('TZ', tz) + + def chpasswd(self, plist_in: list, hashed: bool): + for name, password in plist_in: + self.set_passwd(name, password, hashed=hashed) + + @staticmethod + def _dhcpattr(attr): + try: + (out, _err) = subp.subp(["/sbin/dhcpinfo", attr]) + return out.strip() + except: + return None + + @staticmethod + def obtain_dhcp_lease( + nic: str, + ) -> list: + instance = 'ephdhcp' + illumosNetOps._ipadm(nic, "create-if", rcs=[0,1]) + illumosNetOps._ipadm(nic, instance=instance, + cmd=["create-addr", "-T", "dhcp", "-w", "15"]) + + lease = [{ + 'interface': nic, + 'fixed-address': Distro._dhcpattr('Yiaddr'), + 'subnet-mask': Distro._dhcpattr('Subnet'), + 'router': Distro._dhcpattr('Router'), + }] + illumosNetOps._ipadm(nic, instance=instance, cmd="delete-addr") + return lease if lease[0]['fixed-address'] else None + +# vi: ts=4 sw=4 expandtab diff --git a/cloudinit/distros/networking.py b/cloudinit/distros/networking.py index c645c748a3f..a697be1e11f 100644 --- a/cloudinit/distros/networking.py +++ b/cloudinit/distros/networking.py @@ -1,6 +1,7 @@ import abc import logging import os +import re from cloudinit import net, subp, util from cloudinit.distros.parsers import ifconfig @@ -251,6 +252,48 @@ def is_renamed(self, devname: DeviceName) -> bool: return False +class illumosNetworking(Networking): + """Implementation of networking functionality for illumos.""" + + def is_physical(self, devname: DeviceName) -> bool: + raise NotImplementedError() + + def settle(self, *, exists=None) -> None: + """illumos has no equivalent to `udevadm settle`; noop.""" + + def try_set_link_up(self, devname: DeviceName) -> bool: + raise NotImplementedError() + + def generate_fallback_config( + self, *, blacklist_drivers=None, config_driver: bool = False + ): + nconf = {'config': [], 'version': 1} + (out, _) = subp.subp(['/usr/sbin/ipadm', 'show-addr']) + for mac, name in net.get_interfaces_by_mac().items(): + if re.search(rf'^{name}/', out, re.MULTILINE): + # Address already configured + continue + nconf['config'].append( + {'type': 'physical', 'name': name, + 'mac_address': mac, 'subnets': [{'type': 'dhcp'}]}) + return nconf + + def apply_network_config_names(self, netcfg: NetworkConfig) -> None: + """Read the network config and rename devices accordingly. + + Renames are only attempted for interfaces of type 'physical'. It is + expected that the network system will create other devices with the + correct name in place. + """ + + try: + self._rename_interfaces(self.extract_physdevs(netcfg)) + except RuntimeError as e: + raise RuntimeError( + "Failed to apply network config names: %s" % e + ) from e + + class LinuxNetworking(Networking): """Implementation of networking functionality common to Linux distros.""" diff --git a/cloudinit/distros/omnios.py b/cloudinit/distros/omnios.py new file mode 100644 index 00000000000..84b16d6a0d5 --- /dev/null +++ b/cloudinit/distros/omnios.py @@ -0,0 +1,70 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +import logging + +from cloudinit import subp +from cloudinit import util + +from cloudinit.distros import illumos +from cloudinit.settings import PER_INSTANCE + +LOG = logging.getLogger(__name__) + + +class Distro(illumos.Distro): + + def install_packages(self, pkglist): + self.update_package_sources() + (out, _) = self.package_command('install', args=['--parsable=0'], + pkgs=pkglist) + try: + j = util.load_json(out.splitlines()[0]) + except: + return + + for pkg in j['add-packages']: + LOG.info(f'Installed {pkg}') + + if j['be-name']: + LOG.info('Package installation requires reboot') + util.ensure_file('/var/run/reboot-required') + + def upgrade_packages(self): + self.update_package_sources() + (out, _) = self.package_command('update', '-f', args=['--parsable=0']) + try: + j = util.load_json(out.splitlines()[0]) + except: + return + + if j['be-name']: + LOG.info('Package update requires reboot') + util.ensure_file('/var/run/reboot-required') + + def package_command(self, command, args=None, pkgs=None): + + # Called directly from cc_package_update_upgrade_install + if command == 'upgrade': + self.upgrade_packages() + return + + cmd = ['pkg', command] + + if args and isinstance(args, str): + cmd.append(args) + elif args and isinstance(args, list): + cmd.extend(args) + + if pkgs: + pkglist = util.expand_package_list('%s@%s', pkgs) + if pkglist: + cmd.extend(pkglist) + + # Exit status 4 is "No changes were made, nothing to do" + return subp.subp(cmd, rcs=[0, 4]) + + def update_package_sources(self): + self._runner.run("update-sources", self.package_command, + ["refresh"], freq=PER_INSTANCE) + +# vi: ts=4 sw=4 expandtab diff --git a/cloudinit/dmi.py b/cloudinit/dmi.py index 3b8cca26c2c..6af0978c33d 100644 --- a/cloudinit/dmi.py +++ b/cloudinit/dmi.py @@ -6,15 +6,15 @@ from typing import Optional from cloudinit import subp -from cloudinit.util import is_container, is_FreeBSD +from cloudinit.util import is_container, is_FreeBSD, is_illumos LOG = logging.getLogger(__name__) # Path for DMI Data DMI_SYS_PATH = "/sys/class/dmi/id" -KernelNames = namedtuple("KernelNames", ["linux", "freebsd"]) -KernelNames.__new__.__defaults__ = (None, None) +KernelNames = namedtuple("KernelNames", ["linux", "freebsd", "illumos"]) +KernelNames.__new__.__defaults__ = (None, None, None) # FreeBSD's kenv(1) and Linux /sys/class/dmi/id/* both use different names from # dmidecode. The values are the same, and ultimately what we're interested in. @@ -22,41 +22,39 @@ # This is our canonical translation table. If we add more tools on other # platforms to find dmidecode's values, their keys need to be put in here. DMIDECODE_TO_KERNEL = { - "baseboard-asset-tag": KernelNames("board_asset_tag", "smbios.planar.tag"), - "baseboard-manufacturer": KernelNames( - "board_vendor", "smbios.planar.maker" - ), - "baseboard-product-name": KernelNames( - "board_name", "smbios.planar.product" - ), - "baseboard-serial-number": KernelNames( - "board_serial", "smbios.planar.serial" - ), - "baseboard-version": KernelNames("board_version", "smbios.planar.version"), - "bios-release-date": KernelNames("bios_date", "smbios.bios.reldate"), - "bios-vendor": KernelNames("bios_vendor", "smbios.bios.vendor"), - "bios-version": KernelNames("bios_version", "smbios.bios.version"), - "chassis-asset-tag": KernelNames( - "chassis_asset_tag", "smbios.chassis.tag" - ), - "chassis-manufacturer": KernelNames( - "chassis_vendor", "smbios.chassis.maker" - ), - "chassis-serial-number": KernelNames( - "chassis_serial", "smbios.chassis.serial" - ), - "chassis-version": KernelNames( - "chassis_version", "smbios.chassis.version" - ), - "system-manufacturer": KernelNames("sys_vendor", "smbios.system.maker"), - "system-product-name": KernelNames( - "product_name", "smbios.system.product" - ), - "system-serial-number": KernelNames( - "product_serial", "smbios.system.serial" - ), - "system-uuid": KernelNames("product_uuid", "smbios.system.uuid"), - "system-version": KernelNames("product_version", "smbios.system.version"), + "baseboard-asset-tag": KernelNames("board_asset_tag", "smbios.planar.tag", None), + "baseboard-manufacturer": KernelNames("board_vendor", "smbios.planar.maker", + (2, "Manufacturer")), + "baseboard-product-name": KernelNames("board_name", "smbios.planar.product", + (2, "Product")), + "baseboard-serial-number": KernelNames("board_serial", "smbios.planar.serial", + (2, "Serial Number")), + "baseboard-version": KernelNames("board_version", "smbios.planar.version", + (2, "Version")), + "bios-release-date": KernelNames("bios_date", "smbios.bios.reldate", + (0, "Release Date")), + "bios-vendor": KernelNames("bios_vendor", "smbios.bios.vendor", + (0, "Vendor")), + "bios-version": KernelNames("bios_version", "smbios.bios.version", + (0, "Version String")), + "chassis-asset-tag": KernelNames("chassis_asset_tag", "smbios.chassis.tag", + (3, "Asset Tag")), + "chassis-manufacturer": KernelNames("chassis_vendor", "smbios.chassis.maker", + (3, "Manufacturer")), + "chassis-serial-number": KernelNames("chassis_serial", "smbios.chassis.serial", + (3, "Serial Number")), + "chassis-version": KernelNames("chassis_version", "smbios.chassis.version", + (3, "Version")), + "system-manufacturer": KernelNames("sys_vendor", "smbios.system.maker", + (1, "Manufacturer")), + "system-product-name": KernelNames("product_name", "smbios.system.product", + (1, "Product")), + "system-serial-number": KernelNames("product_serial", "smbios.system.serial", + (1, "Serial Number")), + "system-uuid": KernelNames("product_uuid", "smbios.system.uuid", + (1, "UUID")), + "system-version": KernelNames("product_version", "smbios.system.version", + (1, "Version")), } @@ -120,6 +118,32 @@ def _read_kenv(key: str) -> Optional[str]: return None +def _read_smbios(key: str) -> Optional[str]: + """ + Reads dmi data from illumos' smbios(1) + """ + + kmap = DMIDECODE_TO_KERNEL.get(key) + if kmap is None or kmap.illumos is None: + return None + + (typ, key) = kmap.illumos + + LOG.debug(f"querying dmi data {typ}/{key}") + + cmd = ['smbios', '-t', str(typ)] + try: + import re + (out, _err) = subp.subp(cmd, rcs=[0]) + m = re.search(rf'^\s*{key}:\s*(.+)\s*$', out, re.MULTILINE) + if m: + return m.group(1) + except subp.ProcessExecutionError as e: + LOG.debug('failed smbios cmd: %s\n%s', cmd, e) + + return None + + def _call_dmidecode(key: str, dmidecode_path: str) -> Optional[str]: """ Calls out to dmidecode to get the data out. This is mostly for supporting @@ -162,6 +186,9 @@ def read_dmi_data(key: str) -> Optional[str]: if is_FreeBSD(): return _read_kenv(key) + if is_illumos(): + return _read_smbios(key) + syspath_value = _read_dmi_syspath(key) if syspath_value is not None: return syspath_value diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py index bf21633b1fb..c90a18f2c8c 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -13,6 +13,7 @@ import re from typing import Any, Callable, Dict, List, Optional, Tuple from urllib.parse import urlparse +from socket import inet_ntoa from cloudinit import subp, util from cloudinit.url_helper import UrlError, readurl @@ -54,6 +55,10 @@ def natural_sort_key(s, _nsre=re.compile("([0-9]+)")): ] +def zeropad_mac(mac): + return ':'.join(l.zfill(2) for l in mac.split(':')) + + def get_sys_class_path(): """Simple function to return the global SYS_CLASS_NET.""" return SYS_CLASS_NET @@ -124,7 +129,32 @@ def read_sys_net_int(iface, field): return None +def illumos_linkprop(devname, prop): + (out, _err) = subp.subp(['/usr/sbin/dladm', 'show-link', '-p', + '-o', prop, devname]) + return out.strip() + + +def illumos_intf_in_use(devname): + (out, _err) = subp.subp(['/usr/sbin/ipadm', 'show-addr', '-p', + '-o', 'addrobj']) + for addr in out.splitlines(): + if addr.startswith(f'{devname}/'): + return True + return False + + +def illumos_delete_unused_intf(devname): + if not illumos_intf_in_use(devname): + subp.subp(['/usr/sbin/ipadm', 'delete-if', devname]) + + def is_up(devname): + if util.is_illumos(): + # This function is used to check if the network is already up and + # therefore to avoid using ephemeral configuration. On illumos, + # check if there are any configured addresses. + return illumos_intf_in_use(devname) # The linux kernel says to consider devices in 'unknown' # operstate as up for the purposes of network configuration. See # Documentation/networking/operstates.txt in the kernel source. @@ -133,15 +163,21 @@ def is_up(devname): def is_bridge(devname): + if util.is_illumos(): + return illumos_linkprop(devname, 'CLASS') == "bridge" return os.path.exists(sys_dev_path(devname, "bridge")) def is_bond(devname): + if util.is_illumos(): + return illumos_linkprop(devname, 'CLASS') == "aggr" return os.path.exists(sys_dev_path(devname, "bonding")) def get_master(devname): """Return the master path for devname, or None if no master""" + if util.is_illumos(): + return None path = sys_dev_path(devname, path="master") if os.path.exists(path): return path @@ -357,6 +393,13 @@ def is_vlan(devname): def device_driver(devname): """Return the device driver for net device named 'devname'.""" + if util.is_illumos(): + try: + (out, _err) = subp.subp(['/usr/sbin/dladm', 'show-phys', + '-mp', '-o', 'CLIENT', devname]) + return out.strip().rstrip('1234567890') + except subp.ProcessExecutionError as e: + return None driver = None driver_path = sys_dev_path(devname, "device/driver") # driver is a symlink to the driver *dir* @@ -376,7 +419,7 @@ def device_devid(devname): def get_devicelist(): - if util.is_FreeBSD() or util.is_DragonFlyBSD(): + if util.is_FreeBSD() or util.is_DragonFlyBSD() or util.is_illumos(): return list(get_interfaces_by_mac().values()) try: @@ -399,6 +442,31 @@ def is_disabled_cfg(cfg): return cfg.get("config") == "disabled" +def get_default_gateway(): + """Returns the default gateway ip address in the dotted format.""" + if util.is_illumos(): + return get_default_gateway_on_illumos() + lines = util.load_file("/proc/net/route").splitlines() + for line in lines: + items = line.split("\t") + if items[1] == "00000000": + # Found the default route, get the gateway + gw = inet_ntoa(pack(" List[str]: """Get the list of network interfaces viable for networking. @@ -406,7 +474,7 @@ def find_candidate_nics() -> List[str]: """ if util.is_FreeBSD() or util.is_DragonFlyBSD(): return find_candidate_nics_on_freebsd() - elif util.is_NetBSD() or util.is_OpenBSD(): + elif util.is_NetBSD() or util.is_OpenBSD() or util.is_illumos(): return find_candidate_nics_on_netbsd_or_openbsd() else: return find_candidate_nics_on_linux() @@ -416,7 +484,7 @@ def find_fallback_nic() -> Optional[str]: """Get the name of the 'fallback' network device.""" if util.is_FreeBSD() or util.is_DragonFlyBSD(): return find_fallback_nic_on_freebsd() - elif util.is_NetBSD() or util.is_OpenBSD(): + elif util.is_NetBSD() or util.is_OpenBSD() or util.is_illumos(): return find_fallback_nic_on_netbsd_or_openbsd() else: return find_fallback_nic_on_linux() @@ -674,7 +742,7 @@ def _get_current_rename_info(check_downable=True): "up": is_up(name), } - if check_downable: + if check_downable and not util.is_illumos(): nmatch = re.compile(r"[0-9]+:\s+(\w+)[@:]") ipv6, _err = subp.subp( ["ip", "-6", "addr", "show", "permanent", "scope", "global"], @@ -719,13 +787,20 @@ def update_byname(bymac): return dict((data["name"], data) for data in cur_info.values()) def rename(cur, new): - subp.subp(["ip", "link", "set", cur, "name", new], capture=True) + if util.is_illumos(): + if not illumos_intf_in_use(cur): + subp.subp(["/usr/sbin/dladm", "rename-link", cur, new], + capture=True) + else: + subp.subp(["ip", "link", "set", cur, "name", new], capture=True) def down(name): - subp.subp(["ip", "link", "set", name, "down"], capture=True) + if not util.is_illumos(): + subp.subp(["ip", "link", "set", name, "down"], capture=True) def up(name): - subp.subp(["ip", "link", "set", name, "up"], capture=True) + if not util.is_illumos(): + subp.subp(["ip", "link", "set", name, "up"], capture=True) ops = [] errors = [] @@ -855,6 +930,12 @@ def find_entry(mac, driver, device_id): def get_interface_mac(ifname): """Returns the string value of an interface's MAC Address""" + if util.is_illumos(): + (out, _) = subp.subp(['/usr/sbin/dladm', 'show-phys', '-m', + '-o', 'ADDRESS', ifname]) + for line in out.splitlines(): + if ':' in line: + return zeropad_mac(line) path = "address" if os.path.isdir(sys_dev_path(ifname, "bonding_slave")): # for a bond slave, get the nic's hwaddress, not the address it @@ -884,6 +965,8 @@ def get_interfaces_by_mac() -> dict: return get_interfaces_by_mac_on_netbsd() elif util.is_OpenBSD(): return get_interfaces_by_mac_on_openbsd() + elif util.is_illumos(): + return get_interfaces_by_mac_on_illumos() else: return get_interfaces_by_mac_on_linux() @@ -955,6 +1038,18 @@ def get_interfaces_by_mac_on_openbsd() -> dict: return ret +def get_interfaces_by_mac_on_illumos() -> dict(): + ret = {} + (out, _) = subp.subp(['/usr/sbin/dladm', 'show-phys', '-m', + '-o', 'LINK,ADDRESS']) + for line in out.splitlines(): + (link, mac) = line.split() + if ':' not in mac: + continue + ret[zeropad_mac(mac)] = link + return ret + + def get_interfaces_by_mac_on_linux() -> dict: """Build a dictionary of tuples {mac: name}. diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py index e69da40d371..32d5842a730 100644 --- a/cloudinit/net/activators.py +++ b/cloudinit/net/activators.py @@ -213,6 +213,26 @@ def bring_down_interface(device_name: str) -> bool: return _alter_interface(cmd, device_name) +class illumosActivator(NetworkActivator): + @staticmethod + def available(target=None) -> bool: + return util.is_illumos() + + @staticmethod + def bring_up_interface(device_name: str) -> bool: + subp.subp(['/usr/sbin/ipadm', 'enable-if', '-t', device_name], + rcs=[0, 1]) + return True + + @staticmethod + def bring_down_interface(device_name: str) -> bool: + try: + subp.subp(['/usr/sbin/ipadm', 'disable-if', '-t', device_name]) + return True + except: + return False + + # This section is mostly copied and pasted from renderers.py. An abstract # version to encompass both seems overkill at this point DEFAULT_PRIORITY = [ @@ -220,6 +240,7 @@ def bring_down_interface(device_name: str) -> bool: "netplan", "network-manager", "networkd", + "illumos", ] NAME_TO_ACTIVATOR: Dict[str, Type[NetworkActivator]] = { @@ -227,6 +248,7 @@ def bring_down_interface(device_name: str) -> bool: "netplan": NetplanActivator, "network-manager": NetworkManagerActivator, "networkd": NetworkdActivator, + "illumos": illumosActivator, } diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py index 07c13390185..c7f70d0a0ed 100644 --- a/cloudinit/net/dhcp.py +++ b/cloudinit/net/dhcp.py @@ -25,6 +25,7 @@ get_interface_mac, is_ib_interface, ) +from cloudinit.settings import RUN_CLOUD_CONFIG LOG = logging.getLogger(__name__) @@ -195,6 +196,18 @@ def start_service(cls, dhcp_interface: str, distro): def stop_service(cls, dhcp_interface: str, distro): distro.manage_service("stop", cls.client_name, rcs=[0, 1]) +class illumosDhcp(DhcpClient): + client_name = "illumosdhcp" + + def dhcp_discovery( + self, + interface, + dhcp_log_func=None, + distro=None, + ): + LOG.debug("Performing a dhcp discovery on %s", interface) + return distro.obtain_dhcp_lease(interface) + class IscDhclient(DhcpClient): client_name = "dhclient" @@ -262,8 +275,8 @@ def dhcp_discovery( # We want to avoid running /sbin/dhclient-script because of # side-effects in # /etc/resolv.conf any any other vendor specific # scripts in /etc/dhcp/dhclient*hooks.d. - pid_file = "/run/dhclient.pid" - lease_file = "/run/dhclient.lease" + pid_file = os.path.join(RUN_CLOUD_CONFIG, "dhclient.pid") + lease_file = os.path.join(RUN_CLOUD_CONFIG, "dhclient.lease") config_file = None # this function waits for these files to exist, clean previous runs diff --git a/cloudinit/net/ephemeral.py b/cloudinit/net/ephemeral.py index 28c851cd706..da271a9e6d5 100644 --- a/cloudinit/net/ephemeral.py +++ b/cloudinit/net/ephemeral.py @@ -15,6 +15,8 @@ ) from cloudinit.subp import ProcessExecutionError +from cloudinit.util import is_illumos + LOG = logging.getLogger(__name__) diff --git a/cloudinit/net/illumos.py b/cloudinit/net/illumos.py new file mode 100644 index 00000000000..1781549c2db --- /dev/null +++ b/cloudinit/net/illumos.py @@ -0,0 +1,185 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +import logging, re + +from cloudinit import net +from cloudinit import subp +from cloudinit import util +from cloudinit.distros.parsers.resolv_conf import ResolvConf + +from . import renderer + +LOG = logging.getLogger(__name__) + +from pprint import pprint, pformat + + +class Renderer(renderer.Renderer): + resolv_conf_fn = '/etc/resolv.conf' + + def __init__(self, config=None): + super(Renderer, self).__init__() + + def _ipadm(self, device_name, cmd, rcs=[0], instance=None): + if type(cmd) == str: + cmd = [cmd] + + if instance is not None: + device_name += f'/{instance}' + + cmd.insert(0, '/usr/sbin/ipadm') + cmd.append(device_name) + + try: + subp.subp(cmd, rcs=rcs) + except subp.ProcessExecutionError as e: + LOG.error(f'ipadm command failed: {e}') + + + def _dladm(self, device_name, cmd, rcs=[0]): + if type(cmd) == str: + cmd = [cmd] + + cmd.insert(0, '/usr/sbin/dladm') + cmd.append(device_name) + + try: + subp.subp(cmd, rcs=rcs) + except subp.ProcessExecutionError as e: + LOG.error(f'dladm command failed: {e}') + + def _interfaces(self, settings): + ifname_by_mac = net.get_interfaces_by_mac() + interface_config = {} + + for interface in settings.iter_interfaces(): + device_name = interface.get("name") + device_mac = interface.get("mac_address") + if device_name and re.match(r'^lo\d+$', device_name): + continue + if device_mac not in ifname_by_mac: + LOG.info('Cannot find any device with MAC %s', device_mac) + elif device_mac and device_name: + cur_name = ifname_by_mac[device_mac] + if cur_name != device_name: + LOG.info(f'rename {cur_name} to {device_name}') + if net.illumos_intf_in_use(cur_name): + LOG.warning( + f'Interface {cur_name} is in use; cannot rename') + else: + self._ipadm(device_name, ['delete-if', cur_name], + rcs=[0, 1]) + self._dladm(device_name, ['rename-link', cur_name]) + device_name = cur_name + else: + device_name = ifname_by_mac[device_mac] + + LOG.info(f'Configuring interface {device_name}') + + interface_config[device_name] = 'DHCP' + + for subnet in interface.get("subnets", []): + if subnet.get('type') == 'static': + addr = subnet.get('address') + prefix = subnet.get('prefix') + LOG.debug('Configuring dev %s with %s/%s', device_name, + addr, prefix) + + interface_config[device_name] = { + 'address': addr, + 'netmask': prefix, + 'mtu': subnet.get('mtu') or interface.get('mtu'), + } + + dhcp_done = False + for device_name, v in interface_config.items(): + self._ipadm(device_name, ['create-if'], rcs=[0, 1]) + if v == 'DHCP': + self._ipadm(device_name, ['create-addr', '-T', 'dhcp', + '-w', '15'], instance='dhcp') + dhcp_done = True + else: + addr = v.get('address') + mask = v.get('netmask') + mtu = v.get('mtu') + if mtu: + self._dladm(device_name, ['set-linkprop', '-p', + f'mtu={mtu}']) + self._ipadm(device_name, ['create-addr', '-T', 'static', + '-a', f'local={addr}/{mask}'], instance='ci') + + if dhcp_done: + subp.subp(['/usr/sbin/svcadm', 'restart', 'network/service']) + + def _routes(self, settings): + routes = list(settings.iter_routes()) + for interface in settings.iter_interfaces(): + subnets = interface.get("subnets", []) + for subnet in subnets: + if subnet.get('type') != 'static': + continue + routes += subnet.get('routes', []) + gateway = subnet.get('gateway') + if gateway and len(gateway.split('.')) == 4: + util.write_file('/etc/defaultrouter', f"{gateway}\n") + routes.append({ + 'network': '0.0.0.0', + 'prefix': '0', + 'gateway': gateway}) + for route in routes: + network = route.get('network') + prefix = route.get('prefix') + gateway = route.get('gateway') + if not network: + LOG.debug('Skipping a bad route entry') + continue + + subp.subp(['route', '-p', 'add', '-net', f'{network}/{prefix}', + gateway], rcs=[0,1]) + + def _resolv_conf(self, settings): + nameservers = settings.dns_nameservers + searchdomains = settings.dns_searchdomains + for interface in settings.iter_interfaces(): + for subnet in interface.get("subnets", []): + if 'dns_nameservers' in subnet: + nameservers.extend(subnet['dns_nameservers']) + if 'dns_search' in subnet: + searchdomains.extend(subnet['dns_search']) + + try: + resolvconf = ResolvConf(util.load_file(self.resolv_conf_fn)) + except (IOError, FileNotFoundError): + util.logexc(LOG, "Failed to parse %s, use new empty file", + self.resolv_conf_fn) + resolvconf = ResolvConf('') + + resolvconf.parse() + + for server in nameservers: + try: + resolvconf.add_nameserver(server) + except ValueError: + util.logexc(LOG, "Failed to add nameserver %s", server) + + for domain in searchdomains: + try: + resolvconf.add_search_domain(domain) + except ValueError: + util.logexc(LOG, "Failed to add search domain %s", domain) + + util.write_file(self.resolv_conf_fn, str(resolvconf), 0o644) + + subp.subp(['/usr/sbin/svcadm', 'refresh', 'network/dns/client']) + + def render_network_state(self, network_state, templates=None, target=None): + if target: + self.target = target + self._interfaces(settings=network_state) + self._routes(settings=network_state) + self._resolv_conf(settings=network_state) + +def available(target=None): + return util.is_illumos() + +# vi: ts=4 sw=4 expandtab diff --git a/cloudinit/net/netops/illumos_netops.py b/cloudinit/net/netops/illumos_netops.py new file mode 100644 index 00000000000..3d3b7293b3a --- /dev/null +++ b/cloudinit/net/netops/illumos_netops.py @@ -0,0 +1,152 @@ +import logging, re + +from typing import Optional + +from cloudinit import subp +from cloudinit import net +from cloudinit import util +import cloudinit.net.netops as netops + +LOG = logging.getLogger(__name__) + +from pprint import pprint, pformat + +IPADM = "/usr/sbin/ipadm" +DLADM = "/usr/sbin/dladm" + +class illumosNetOps(netops.NetOps): + + @staticmethod + def _ipadm(device_name, cmd, rcs=[0], instance=None): + if type(cmd) == str: + cmd = [cmd] + + if instance is not None: + device_name += f'/{instance}' + + cmd.insert(0, IPADM) + cmd.append(device_name) + + try: + subp.subp(cmd, rcs=rcs) + except subp.ProcessExecutionError as e: + LOG.error(f'ipadm command failed: {e}') + + @staticmethod + def _dladm(device_name, cmd, rcs=[0]): + if type(cmd) == str: + cmd = [cmd] + + cmd.insert(0, DLADM) + cmd.append(device_name) + + try: + subp.subp(cmd, rcs=rcs) + except subp.ProcessExecutionError as e: + LOG.error(f'dladm command failed: {e}') + + @staticmethod + def _addrobj_exists(interface: str, instance: str) -> bool: + addrobj = f'{interface}/{instance}' + try: + (out, _err) = subp.subp( + [IPADM, "show-addr", "-po", "ADDROBJ", addrobj]) + return out.strip() == addrobj + except: + return False + + @staticmethod + def _find_address(interface: str, address: str) -> str: + try: + (out, _err) = subp.subp([IPADM, "show-addr", "-po", + "ADDROBJ,ADDR", f'{interface}/']) + for line in out.splitlines(): + (addrobj, addr) = line.split(':') + if addr == address: + return addrobj + return None + except: + return None + + @staticmethod + def _intf_in_use(devname): + (out, _err) = subp.subp([IPADM, 'show-addr', '-p', + '-o', 'addrobj']) + for addr in out.splitlines(): + if addr.startswith(f'{devname}/'): + return True + return False + + @staticmethod + def link_up(interface: str, family: Optional[str] = None): + illumosNetOps._ipadm(interface, ["enable-if", "-t"], rcs=[0,1]) + + @staticmethod + def link_down(interface: str, family: Optional[str] = None): + #illumosNetOps._ipadm(interface, ["disable-if", "-t"]) + pass + + @staticmethod + def add_route( + interface: str, + route: str, + *, + gateway: Optional[str] = None, + source_address: Optional[str] = None + ): + cmd = ["route", "-p", "add", "-inet"] + if not gateway or gateway == "0.0.0.0": + cmd.extend(["-iface", "-ifp", interface, "-host"]) + cmd.extend([route, gateway]) + subp.subp(cmd); + + @staticmethod + def append_route(interface: str, address: str, gateway: str): + return illumosNetOps.add_route(interface, route=address, + gateway=gateway) + + @staticmethod + def del_route( + interface: str, + address: str, + *, + gateway: Optional[str] = None, + source_address: Optional[str] = None + ): + cmd = ["route", "-p", "delete", "-inet"] + if not gateway or gateway == "0.0.0.0": + cmd.extend(["-iface", "-ifp", interface, "-host"]) + cmd.extend([route, gateway]) + subp.subp(cmd); + + @staticmethod + def get_default_route() -> str: + try: + (out, _) = subp.subp(['route', '-n', 'get', 'default']) + m = re.search(rf'^\s+gateway:\s+([0-9.]+)', out, re.MULTILINE) + if m: + return m.group(1) + except: + pass + return None + + @staticmethod + def add_addr(interface: str, address: str, broadcast: str): + # The interface may already exist, so allow rc 1 + illumosNetOps._ipadm(interface, "create-if", rcs=[0,1]) + inum = 0 + while illumosNetOps._addrobj_exists(interface, f'ci{inum}'): + inum += 1 + illumosNetOps._ipadm(interface, ["create-addr", "-T", "static", + "-a", f'local={address}'], instance=f'ci{inum}') + + @staticmethod + def del_addr(interface: str, address: str): + addrobj = illumosNetOps._find_address(interface, address) + if addrobj: + instance = addrobj.split('/')[-1] + illumosNetOps._ipadm(interface, "delete-addr", instance=instance) + if not illumosNetOps._intf_in_use(interface): + illumosNetOps._ipadm(interface, "delete-if", rcs=[0,1]) + +# vim:ts=4:sw=4:et:fdm=marker diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py index e201bfe4b62..5e206dca409 100644 --- a/cloudinit/net/renderers.py +++ b/cloudinit/net/renderers.py @@ -13,11 +13,13 @@ openbsd, renderer, sysconfig, + illumos, ) NAME_TO_RENDERER = { "eni": eni, "freebsd": freebsd, + "illumos": illumos, "netbsd": netbsd, "netplan": netplan, "network-manager": network_manager, @@ -35,6 +37,7 @@ "netbsd", "openbsd", "networkd", + "illumos", ] diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 9478efc35fd..0b7e4dfd07b 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -503,9 +503,71 @@ def _netdev_route_info_netstat(route_data): return routes +def _netdev_route_info_illumos(): + routes = {} + routes['ipv4'] = [] + routes['ipv6'] = [] + + try: + (route_data, _err) = subp.subp( ["netstat", "-rnv", "-f", "inet"]) + except subp.ProcessExecutionError: + pass + else: + entries = route_data.splitlines() + for line in entries: + if not line: + continue + toks = line.split() + if len(toks) < 9: + continue + + if toks[0].startswith(('IRE', 'Destination', '---')): + continue + + routes['ipv4'].append({ + 'destination': toks[0], + 'genmask': toks[1], + 'gateway': toks[2], + 'iface': toks[3], + 'ref': toks[5], + 'flags': toks[6], + 'metric': '1', + 'use': '1', + }) + + try: + (route_data, _err) = subp.subp( ["netstat", "-rnv", "-f", "inet6"]) + except subp.ProcessExecutionError: + pass + else: + entries = route_data.splitlines() + for line in entries: + if not line: + continue + toks = line.split() + if len(toks) < 8: + continue + + if toks[0].startswith(('IRE', 'Destination', '---')): + continue + routes['ipv6'].append({ + 'destination': toks[0], + 'gateway': toks[1], + 'iface': toks[2], + 'ref': toks[4], + 'flags': toks[5], + 'metric': '1', + 'use': '1', + }) + + return routes + + def route_info(): routes = {} - if subp.which("ip"): + if util.is_illumos(): + routes = _netdev_route_info_illumos() + elif subp.which("ip"): # Try iproute first of all (iproute_out, _err) = subp.subp(["ip", "-o", "route", "list"]) routes = _netdev_route_info_iproute(iproute_out) diff --git a/cloudinit/settings.py b/cloudinit/settings.py index 592e144dc07..fa308b603b6 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py @@ -8,6 +8,8 @@ # # 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" @@ -16,7 +18,10 @@ CLEAN_RUNPARTS_DIR = "/etc/cloud/clean.d" -RUN_CLOUD_CONFIG = "/run/cloud-init/cloud.cfg" +if platform.system() == "SunOS": + RUN_CLOUD_CONFIG = "/var/run/cloud-init/cloud.cfg" +else: + RUN_CLOUD_CONFIG = "/run/cloud-init/cloud.cfg" # What u get if no config is provided CFG_BUILTIN = { diff --git a/cloudinit/sources/DataSourceAzure.py b/cloudinit/sources/DataSourceAzure.py index 11c14e2001f..22f491cb27a 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -273,6 +273,10 @@ def get_resource_disk_on_freebsd(port_id) -> Optional[str]: return None +def get_resource_disk_on_illumos(port_id): + return None + + # update the FreeBSD specific information if util.is_FreeBSD(): DEFAULT_FS = "freebsd-ufs" @@ -285,6 +289,21 @@ def get_resource_disk_on_freebsd(port_id) -> Optional[str]: # TODO Find where platform entropy data is surfaced PLATFORM_ENTROPY_SOURCE = None +# update the illumos specific information +if util.is_illumos(): + DEFAULT_PRIMARY_NIC = 'hv_netvsc0' + LEASE_FILE = '/etc/dhcp/hv_netvsc0:1.dhc' + DEFAULT_FS = 'zfs' + res_disk = get_resource_disk_on_illumos(1) + if res_disk is not None: + LOG.debug("resource disk is not None") + RESOURCE_DISK_PATH = "/dev/" + res_disk + else: + LOG.debug("resource disk is None") + # TODO Find where platform entropy data is surfaced + PLATFORM_ENTROPY_SOURCE = None + + BUILTIN_DS_CONFIG = { "data_dir": AGENT_SEED_DIR, "disk_aliases": {"ephemeral0": RESOURCE_DISK_PATH}, diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py index fd2482a3fb6..1f534d93e71 100644 --- a/cloudinit/sources/DataSourceCloudStack.py +++ b/cloudinit/sources/DataSourceCloudStack.py @@ -19,6 +19,7 @@ from struct import pack from cloudinit import sources, subp +from cloudinit.net import dhcp, get_default_gateway from cloudinit import url_helper as uhelp from cloudinit import util from cloudinit.net import dhcp @@ -194,19 +195,6 @@ def get_data_server(): return addrinfo[0][4][0] # return IP -def get_default_gateway(): - # Returns the default gateway ip address in the dotted format. - lines = util.load_file("/proc/net/route").splitlines() - for line in lines: - items = line.split("\t") - if items[1] == "00000000": - # Found the default route, get the gateway - gw = inet_ntoa(pack(" /etc/defaultrouter + route -p add default 172.27.10.254 + echo nameserver 80.80.80.80 > /etc/resolv.conf + cp /etc/inet/hosts{.sav,} + userdel omnios + zfs destroy rpool/home/omnios + rm -rf /home/omnios + rm -f /etc/sudoers.d/90-cloud-init-users + rm -f /etc/user_attr.d/cloud-init-users + cloud-init clean -ls + touch /var/log/cloud-init.log + pkg uninstall cpuid pciutils + beadm destroy -Ffs omnios-r151039-1 + echo + cloud-init status +} + +function run { + clean + cloud-init init -l + cloud-init init + cloud-init modules --mode config + cloud-init modules --mode final +} + +[ "$1" = clean ] && rm -rf $PWD/root +python3 setup.py install --root=$PWD/root --init-system=smf + +[ -n "$1" ] && "$@" + diff --git a/setup.py b/setup.py index bff18362498..d6a64dfd075 100644 --- a/setup.py +++ b/setup.py @@ -151,6 +151,11 @@ def render_tmpl(template, mode=None, is_yaml=False): for f in glob("systemd/*") if is_f(f) and is_generator(f) ], + "smf": lambda: [ + render_tmpl(f, mode=0o755) + for f in glob("smf/*") + if is_f(f) + ], } INITSYS_ROOTS = { "sysvinit": "etc/rc.d/init.d", @@ -162,6 +167,7 @@ def render_tmpl(template, mode=None, is_yaml=False): "systemd.generators": pkg_config_read( "systemd", "systemdsystemgeneratordir" ), + "smf": "lib/svc/manifest/system/", } INITSYS_TYPES = sorted([f.partition(".")[0] for f in INITSYS_ROOTS.keys()]) @@ -235,10 +241,13 @@ def finalize_options(self): if self.init_system and isinstance(self.init_system, str): self.init_system = self.init_system.split(",") - if len(self.init_system) == 0 and not platform.system().endswith( - "BSD" - ): - self.init_system = ["systemd"] + if len(self.init_system) == 0: + if platform.system() == "SunOS": + self.init_system = ["smf"] + elif not platform.system().endswith( + "BSD" + ): + self.init_system = ["systemd"] bad = [f for f in self.init_system if f not in INITSYS_TYPES] if len(bad) != 0: @@ -294,7 +303,7 @@ def finalize_options(self): [f for f in glob("doc/examples/seed/*") if is_f(f)], ), ] -if not platform.system().endswith("BSD"): +if not platform.system().endswith("BSD") and platform.system() != "SunOS": RULES_PATH = pkg_config_read("udev", "udevdir") if not in_virtualenv(): RULES_PATH = "/" + RULES_PATH diff --git a/smf/cloud-init.xml b/smf/cloud-init.xml new file mode 100755 index 00000000000..6750b978ab0 --- /dev/null +++ b/smf/cloud-init.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/hosts.illumos.tmpl b/templates/hosts.illumos.tmpl new file mode 100644 index 00000000000..f557bd2f2ea --- /dev/null +++ b/templates/hosts.illumos.tmpl @@ -0,0 +1,26 @@ +## template:jinja +{# +This file /etc/cloud/templates/hosts.illumos.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 +-#} +# +# Internet host table +# +# Generated by cloud-init +# +# This 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 /etc/cloud/templates/hosts.illumos.tmpl +# b.) change or remove the value of 'manage_etc_hosts' in +# /etc/cloud/cloud.cfg or cloud-config from user-data +# +::1 localhost +{% if fqdn == hostname %} +127.0.0.1 localhost loghost {{hostname}} +{% else %} +127.0.0.1 localhost loghost {{fqdn}} {{hostname}} +{% endif %} + diff --git a/test/README b/test/README new file mode 100644 index 00000000000..cb2b6310048 --- /dev/null +++ b/test/README @@ -0,0 +1,3 @@ + +zadm set citest cloud-init=http://172.27.10.254/cloudinit/ + diff --git a/test/cloud-init/meta-data b/test/cloud-init/meta-data new file mode 100644 index 00000000000..78c08ae193f --- /dev/null +++ b/test/cloud-init/meta-data @@ -0,0 +1,2 @@ +instance-id: 40d84db8-bf08-48b9-c347-fb436d131bb1 +local-hostname: test diff --git a/test/cloud-init/meta-data~ b/test/cloud-init/meta-data~ new file mode 100644 index 00000000000..78c08ae193f --- /dev/null +++ b/test/cloud-init/meta-data~ @@ -0,0 +1,2 @@ +instance-id: 40d84db8-bf08-48b9-c347-fb436d131bb1 +local-hostname: test diff --git a/test/cloud-init/network-config b/test/cloud-init/network-config new file mode 100644 index 00000000000..7c86d6f7f7d --- /dev/null +++ b/test/cloud-init/network-config @@ -0,0 +1,14 @@ +ethernets: + bob3: + addresses: + - 10.0.0.5/25 + gateway4: 10.0.0.127 + match: + macaddress: 02:08:20:7c:68:7a + nameservers: + addresses: + - 1.1.1.1 + - 9.9.9.9 + search: omnios.org + set-name: bob3 +version: 2 diff --git a/test/cloud-init/user-data b/test/cloud-init/user-data new file mode 100644 index 00000000000..75c104ab31d --- /dev/null +++ b/test/cloud-init/user-data @@ -0,0 +1,20 @@ +#cloud-config +chpasswd: + expire: false +disable_root: false +hostname: test +password: $6$SEeDRaFR$CI8Y/wfMXioIWlrtTLs75iOA4m/./1Vu78d5Plhk6N/T.yctR/s8ojMIjIhyIJB8lwYJAlQXi5GBuh4O0gjY5/ +ssh-pwauth: true +ssh_authorized_keys: +- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKi3Xy6qla2g7wH5b1t+6nDi99D/Unl9Hqpi7j4acP8s + cinit +users: +- default +- name: root + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKi3Xy6qla2g7wH5b1t+6nDi99D/Unl9Hqpi7j4acP8s + cinit +packages: + - system/cpuid + - [system/pciutils, 3.7.0] +package_reboot_if_required: false diff --git a/test/cloud-init/user-data~ b/test/cloud-init/user-data~ new file mode 100644 index 00000000000..6827117c521 --- /dev/null +++ b/test/cloud-init/user-data~ @@ -0,0 +1,16 @@ +#cloud-config +chpasswd: + expire: false +disable_root: false +hostname: test +password: $6$SEeDRaFR$CI8Y/wfMXioIWlrtTLs75iOA4m/./1Vu78d5Plhk6N/T.yctR/s8ojMIjIhyIJB8lwYJAlQXi5GBuh4O0gjY5/ +ssh-pwauth: true +ssh_authorized_keys: +- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKi3Xy6qla2g7wH5b1t+6nDi99D/Unl9Hqpi7j4acP8s + cinit +users: +- default +- name: root + ssh_authorized_keys: + - ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIKi3Xy6qla2g7wH5b1t+6nDi99D/Unl9Hqpi7j4acP8s + cinit diff --git a/test/cloud-init/vendor-data b/test/cloud-init/vendor-data new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/cloudinit b/test/cloudinit new file mode 120000 index 00000000000..992a1c2a4ea --- /dev/null +++ b/test/cloudinit @@ -0,0 +1 @@ +cloud-init \ No newline at end of file diff --git a/tools/write-ssh-key-fingerprints b/tools/write-ssh-key-fingerprints index 9409257dba0..7fe6f7c5ea3 100755 --- a/tools/write-ssh-key-fingerprints +++ b/tools/write-ssh-key-fingerprints @@ -7,7 +7,10 @@ do_syslog() { # rhels' version of logger_opts does not support long # form of -s (--stderr), so use short form. - logger_opts="-s" + # illumos' logger does not support -s at all + if [ $(uname -s) != 'SunOS' ]; then + logger_opts="-s" + fi # Need to end the options list with "--" to ensure that any minus symbols # in the text passed to logger are not interpreted as logger options. diff --git a/updatereqs b/updatereqs new file mode 100755 index 00000000000..8bffc2cb361 --- /dev/null +++ b/updatereqs @@ -0,0 +1,81 @@ +#!/bin/ksh +# +# {{{ CDDL HEADER +# +# This file and its contents are supplied under the terms of the +# Common Development and Distribution License ("CDDL"), version 1.0. +# You may only use this file in accordance with the terms of version +# 1.0 of the CDDL. +# +# A full copy of the text of the CDDL should have accompanied this +# source. A copy of the CDDL is also available via the Internet at +# http://www.illumos.org/license/CDDL. +# }}} + +# Copyright 2023 OmniOS Community Edition (OmniOSce) Association. + +ROOT="`git rev-parse --show-toplevel`" +if [ -z "$ROOT" ]; then + echo "Cannot find root of checked out repository" + exit 1 +fi + +tmpd=`mktemp -d` +tmpf=`mktemp` +trap 'rm -rf $tmpd $tmpf $tmpf.*' EXIT + +echo "+ Installing 'pipreqs' to $tmpd" +python3 -mvenv $tmpd +. $tmpd/bin/activate +pip install --quiet --upgrade pip +pip install --quiet pipreqs + +function strip { + sed " + # Use core version + /setuptools/d + # Use core version (also needed by validate_pkg in gate) + /jsonschema/d + # Use versions from core + /attrs/d + /idna/d + /PyYAML/d + /six/d + /typing_extensions/d + " +} + +function freeze { + typeset tag="$1"; shift + typeset f="$1"; shift + typeset op="$1"; shift + + echo "+ $tag package set" + sed 's/^/ /' < $f + + rm -rf $tmpd + python3 -mvenv $tmpd + . $tmpd/bin/activate + echo "+ Building required package set ($tag)" + pip install --quiet --upgrade pip wheel + pip install -r $f + + echo "+ Generating freeze file ($tag)" + pip freeze | strip | sed " +1i\\ +#\\ +# This file was automatically produced by updatereqs\\ +# Generated on `date`\\ +# Do not edit directly\\ +# +" > $op +} + +echo "+ Evaluating requirements" +pipreqs --print --mode no-pin $ROOT/cloudinit | strip > $tmpf.core + +freeze core $tmpf.core $ROOT/frozen-requirements.txt +echo "+ Updated frozen-requirements.txt" + +# Vim hints +# vim:ts=4:sw=4:et:fdm=marker