From 5af8f5b72789c4cc714c6dce6fb0be5ecb4e1afa Mon Sep 17 00:00:00 2001 From: Andy Fiddaman Date: Wed, 26 Jan 2022 16:00:05 +0000 Subject: [PATCH] Add support for illumos/OmniOS --- cloudinit/cmd/main.py | 14 +- .../cc_package_update_upgrade_install.py | 11 +- cloudinit/config/cc_resizefs.py | 23 +- cloudinit/config/cc_set_passwords.py | 2 +- cloudinit/distros/__init__.py | 1 + cloudinit/distros/illumos.py | 289 ++++++++++++++++ cloudinit/distros/networking.py | 43 +++ cloudinit/distros/omnios.py | 69 ++++ cloudinit/dmi.py | 85 +++-- cloudinit/net/__init__.py | 310 +++++++++++++----- cloudinit/net/activators.py | 21 ++ cloudinit/net/dhcp.py | 14 + cloudinit/net/illumos.py | 186 +++++++++++ cloudinit/net/renderers.py | 3 + cloudinit/netinfo.py | 64 +++- cloudinit/settings.py | 7 +- cloudinit/sources/DataSourceAzure.py | 19 ++ cloudinit/sources/DataSourceCloudStack.py | 14 +- cloudinit/temp_utils.py | 6 +- cloudinit/util.py | 120 ++++++- config/cloud.cfg.tmpl | 22 +- mk | 39 +++ setup.py | 15 +- smf/cloud-init.xml | 138 ++++++++ templates/hosts.illumos.tmpl | 26 ++ tools/write-ssh-key-fingerprints | 5 +- 26 files changed, 1405 insertions(+), 141 deletions(-) create mode 100644 cloudinit/distros/illumos.py create mode 100644 cloudinit/distros/omnios.py create mode 100644 cloudinit/net/illumos.py create mode 100755 mk create mode 100755 smf/cloud-init.xml create mode 100644 templates/hosts.illumos.tmpl diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index fcdaf72513b..22d8be0c91b 100755 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -41,7 +41,8 @@ from cloudinit import reporting from cloudinit.reporting import events -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) from cloudinit import atomic_helper @@ -394,6 +395,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, @@ -663,7 +673,7 @@ def status_wrapper(name, args, data_d=None, link_d=None): if data_d is None: data_d = os.path.normpath("/var/lib/cloud/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 5198305e430..e744c832e73 100644 --- a/cloudinit/config/cc_package_update_upgrade_install.py +++ b/cloudinit/config/cc_package_update_upgrade_install.py @@ -59,8 +59,13 @@ def _multi_cfg_bool_get(cfg, *keys): return False -def _fire_reboot(log, wait_attempts=6, initial_sleep=1, backoff=2): - subp.subp(REBOOT_CMD) +def _fire_reboot(log, 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): @@ -118,7 +123,7 @@ def handle(_name, cfg, cloud, log, _args): ) # Flush the above warning + anything else out... logging.flushLoggers(log) - _fire_reboot(log) + _fire_reboot(log, 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 39da1b5a8aa..959528461b3 100644 --- a/cloudinit/config/cc_resizefs.py +++ b/cloudinit/config/cc_resizefs.py @@ -10,6 +10,7 @@ import errno import os +import re import stat from textwrap import dedent @@ -17,6 +18,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" @@ -106,6 +108,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. @@ -118,7 +129,10 @@ def _can_skip_resize_ufs(mount_point, devpth): ("hammer2", _resize_hammer2), ] -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): @@ -239,7 +253,12 @@ def handle(name, cfg, _cloud, log, args): 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, log) + 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, log) if not devpth: return # devpath was not a writable block device diff --git a/cloudinit/config/cc_set_passwords.py b/cloudinit/config/cc_set_passwords.py index 3c8b378bf91..7f7975e0d9e 100644 --- a/cloudinit/config/cc_set_passwords.py +++ b/cloudinit/config/cc_set_passwords.py @@ -281,7 +281,7 @@ def rand_user_password(pwlen=20): def chpasswd(distro, plist_in, hashed=False): - if util.is_BSD(): + if util.is_BSD() or util.is_illumos(): for pentry in plist_in.splitlines(): u, p = pentry.split(":") distro.set_passwd(u, p, hashed=hashed) diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index b034e2c848d..2a13244a15f 100644 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -53,6 +53,7 @@ "virtuozzo", ], "suse": ["opensuse", "sles"], + "illumos": ["omnios"], } LOG = logging.getLogger(__name__) diff --git a/cloudinit/distros/illumos.py b/cloudinit/distros/illumos.py new file mode 100644 index 00000000000..85d808aa3e9 --- /dev/null +++ b/cloudinit/distros/illumos.py @@ -0,0 +1,289 @@ +import os, platform + +from cloudinit import distros +from cloudinit import helpers +from cloudinit import log as logging +from cloudinit import net +from cloudinit import subp +from cloudinit import util +from .networking import illumosNetworking + +LOG = logging.getLogger(__name__) + + +class Distro(distros.Distro): + networking_cls = illumosNetworking + + 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' + + 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 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 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) + + +# vim:ts=4:sw=4:et:fdm=marker diff --git a/cloudinit/distros/networking.py b/cloudinit/distros/networking.py index f14d678dc68..624c7da1c68 100644 --- a/cloudinit/distros/networking.py +++ b/cloudinit/distros/networking.py @@ -2,6 +2,7 @@ import logging import os from typing import List, Optional +import re from cloudinit import net, subp, util @@ -207,6 +208,48 @@ def apply_network_config_names(self, netcfg: NetworkConfig) -> None: pass +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..5e3363f6e4f --- /dev/null +++ b/cloudinit/distros/omnios.py @@ -0,0 +1,69 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit import log as 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 3a999d41c6d..cb137b55fef 100644 --- a/cloudinit/dmi.py +++ b/cloudinit/dmi.py @@ -4,15 +4,15 @@ from cloudinit import log as logging 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" -kdmi = namedtuple("KernelNames", ["linux", "freebsd"]) -kdmi.__new__.defaults__ = (None, None) +kdmi = namedtuple("KernelNames", ["linux", "freebsd", "illumos"]) +kdmi.__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. @@ -20,23 +20,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": kdmi("board_asset_tag", "smbios.planar.tag"), - "baseboard-manufacturer": kdmi("board_vendor", "smbios.planar.maker"), - "baseboard-product-name": kdmi("board_name", "smbios.planar.product"), - "baseboard-serial-number": kdmi("board_serial", "smbios.planar.serial"), - "baseboard-version": kdmi("board_version", "smbios.planar.version"), - "bios-release-date": kdmi("bios_date", "smbios.bios.reldate"), - "bios-vendor": kdmi("bios_vendor", "smbios.bios.vendor"), - "bios-version": kdmi("bios_version", "smbios.bios.version"), - "chassis-asset-tag": kdmi("chassis_asset_tag", "smbios.chassis.tag"), - "chassis-manufacturer": kdmi("chassis_vendor", "smbios.chassis.maker"), - "chassis-serial-number": kdmi("chassis_serial", "smbios.chassis.serial"), - "chassis-version": kdmi("chassis_version", "smbios.chassis.version"), - "system-manufacturer": kdmi("sys_vendor", "smbios.system.maker"), - "system-product-name": kdmi("product_name", "smbios.system.product"), - "system-serial-number": kdmi("product_serial", "smbios.system.serial"), - "system-uuid": kdmi("product_uuid", "smbios.system.uuid"), - "system-version": kdmi("product_version", "smbios.system.version"), + "baseboard-asset-tag": kdmi("board_asset_tag", "smbios.planar.tag", None), + "baseboard-manufacturer": kdmi("board_vendor", "smbios.planar.maker", + (2, "Manufacturer")), + "baseboard-product-name": kdmi("board_name", "smbios.planar.product", + (2, "Product")), + "baseboard-serial-number": kdmi("board_serial", "smbios.planar.serial", + (2, "Serial Number")), + "baseboard-version": kdmi("board_version", "smbios.planar.version", + (2, "Version")), + "bios-release-date": kdmi("bios_date", "smbios.bios.reldate", + (0, "Release Date")), + "bios-vendor": kdmi("bios_vendor", "smbios.bios.vendor", + (0, "Vendor")), + "bios-version": kdmi("bios_version", "smbios.bios.version", + (0, "Version String")), + "chassis-asset-tag": kdmi("chassis_asset_tag", "smbios.chassis.tag", + (3, "Asset Tag")), + "chassis-manufacturer": kdmi("chassis_vendor", "smbios.chassis.maker", + (3, "Manufacturer")), + "chassis-serial-number": kdmi("chassis_serial", "smbios.chassis.serial", + (3, "Serial Number")), + "chassis-version": kdmi("chassis_version", "smbios.chassis.version", + (3, "Version")), + "system-manufacturer": kdmi("sys_vendor", "smbios.system.maker", + (1, "Manufacturer")), + "system-product-name": kdmi("product_name", "smbios.system.product", + (1, "Product")), + "system-serial-number": kdmi("product_serial", "smbios.system.serial", + (1, "Serial Number")), + "system-uuid": kdmi("product_uuid", "smbios.system.uuid", + (1, "UUID")), + "system-version": kdmi("product_version", "smbios.system.version", + (1, "Version")), } @@ -101,6 +117,32 @@ def _read_kenv(key): return None +def _read_smbios(key): + """ + 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, dmidecode_path): """ Calls out to dmidecode to get the data out. This is mostly for supporting @@ -143,6 +185,9 @@ def read_dmi_data(key): 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 3297a3185ee..412ec666468 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 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 @@ -353,6 +389,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* @@ -372,7 +415,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: @@ -395,6 +438,32 @@ 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]: @@ -404,7 +473,7 @@ def find_candidate_nics( """ if util.is_FreeBSD() or util.is_DragonFlyBSD(): return find_candidate_nics_on_freebsd(blacklist_drivers) - 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(blacklist_drivers) else: return find_candidate_nics_on_linux(blacklist_drivers) @@ -416,7 +485,7 @@ def find_fallback_nic( """Get the name of the 'fallback' network device.""" if util.is_FreeBSD() or util.is_DragonFlyBSD(): return find_fallback_nic_on_freebsd(blacklist_drivers) - 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(blacklist_drivers) else: return find_fallback_nic_on_linux(blacklist_drivers) @@ -706,7 +775,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"], @@ -751,13 +820,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 = [] @@ -887,6 +963,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 @@ -922,6 +1004,10 @@ def get_interfaces_by_mac(blacklist_drivers=None) -> dict: return get_interfaces_by_mac_on_openbsd( blacklist_drivers=blacklist_drivers ) + elif util.is_illumos(): + return get_interfaces_by_mac_on_illumos( + blacklist_drivers=blacklist_drivers + ) else: return get_interfaces_by_mac_on_linux( blacklist_drivers=blacklist_drivers @@ -988,6 +1074,18 @@ def get_interfaces_by_mac_on_openbsd(blacklist_drivers=None) -> dict: return ret +def get_interfaces_by_mac_on_illumos(blacklist_drivers=None) -> 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(blacklist_drivers=None) -> dict: """Build a dictionary of tuples {mac: name}. @@ -1392,9 +1490,18 @@ def __exit__(self, excp_type, excp_value, excp_traceback): """Teardown anything we set up.""" for cmd in self.cleanup_cmds: subp.subp(cmd, capture=True) + if util.is_illumos(): + illumos_delete_unused_intf(self.interface) def _delete_address(self, address, prefix): """Perform the ip command to remove the specified address.""" + if util.is_illumos(): + subp.subp( + ['/usr/sbin/ipadm', 'delete-addr', + f'{self.interface}/eph'], capture=True) + illumos_delete_unused_intf(self.interface) + return + subp.subp( [ "ip", @@ -1419,24 +1526,33 @@ def _bringup_device(self): self.broadcast, ) try: - subp.subp( - [ - "ip", - "-family", - "inet", - "addr", - "add", - cidr, - "broadcast", - self.broadcast, - "dev", - self.interface, - ], - capture=True, - update_env={"LANG": "C"}, - ) + if util.is_illumos(): + subp.subp(['/usr/sbin/ipadm', 'create-if', self.interface], + rcs=[0,1]) + subp.subp( + ['/usr/sbin/ipadm', 'create-addr', '-t', '-T', 'static', + '-a', f'local={cidr}', f'{self.interface}/eph'], + capture=True) + else: + subp.subp( + [ + "ip", + "-family", + "inet", + "addr", + "add", + cidr, + "broadcast", + self.broadcast, + "dev", + self.interface, + ], + capture=True, + update_env={"LANG": "C"}, + ) except subp.ProcessExecutionError as e: - if "File exists" not in e.stderr: + if ("File exists" not in e.stderr and + "object already exists" not in e.stderr): raise LOG.debug( "Skip ephemeral network setup, %s already has address %s", @@ -1444,6 +1560,10 @@ def _bringup_device(self): self.ip, ) else: + if util.is_illumos(): + self.cleanup_cmds.append( + ['/usr/sbin/ipadm', 'delete-addr', f'{self.interface}/eph']) + return # Address creation success, bring up device and queue cleanup subp.subp( [ @@ -1490,23 +1610,34 @@ def _bringup_static_routes(self): via_arg = [] if gateway != "0.0.0.0": via_arg = ["via", gateway] - subp.subp( - ["ip", "-4", "route", "append", net_address] - + via_arg - + ["dev", self.interface], - capture=True, - ) - self.cleanup_cmds.insert( - 0, - ["ip", "-4", "route", "del", net_address] - + via_arg - + ["dev", self.interface], - ) + if util.is_illumos(): + subp.subp( + ['route', 'add', '-inet', net_address, gateway], + capture=True) + self.cleanup_cmds.insert( + 0, ['route', 'delete', '-inet', net_address, gateway]) + else: + subp.subp( + ["ip", "-4", "route", "append", net_address] + + via_arg + + ["dev", self.interface], + capture=True, + ) + self.cleanup_cmds.insert( + 0, + ["ip", "-4", "route", "del", net_address] + + via_arg + + ["dev", self.interface], + ) def _bringup_router(self): """Perform the ip commands to fully setup the router if needed.""" # Check if a default route exists and exit if it does - out, _ = subp.subp(["ip", "route", "show", "0.0.0.0/0"], capture=True) + if util.is_illumos(): + out, _ = subp.subp(['netstat', '-rnc', '-f', 'inet'], capture=True) + else: + out, _ = subp.subp(["ip", "route", "show", "0.0.0.0/0"], + capture=True) if "default" in out: LOG.debug( "Skip ephemeral route setup. %s already has default route: %s", @@ -1514,55 +1645,68 @@ def _bringup_router(self): out.strip(), ) return - subp.subp( - [ - "ip", - "-4", - "route", - "add", - self.router, - "dev", - self.interface, - "src", - self.ip, - ], - capture=True, - ) - self.cleanup_cmds.insert( - 0, - [ - "ip", - "-4", - "route", - "del", - self.router, - "dev", - self.interface, - "src", - self.ip, - ], - ) - subp.subp( - [ - "ip", - "-4", - "route", - "add", - "default", - "via", - self.router, - "dev", - self.interface, - ], - capture=True, - ) - self.cleanup_cmds.insert( - 0, ["ip", "-4", "route", "del", "default", "dev", self.interface] - ) + if util.is_illumos(): + subp.subp( + ['route', 'add', '-inet', '-host', '-iface', + self.router, self.ip], capture=True) + self.cleanup_cmdds.insert( + 0, ['route' 'delete', '-inet', '-host', '-iface', + self.router, self.ip]) + subp.subp( + ['route', 'add', '-inet', 'default', self.router], + capture=True) + self.cleanup_cmdds.insert( + 0, ['route' 'delete', '-inet', 'default', self.router]) + else: + subp.subp( + [ + "ip", + "-4", + "route", + "add", + self.router, + "dev", + self.interface, + "src", + self.ip, + ], + capture=True, + ) + self.cleanup_cmds.insert( + 0, + [ + "ip", + "-4", + "route", + "del", + self.router, + "dev", + self.interface, + "src", + self.ip, + ], + ) + subp.subp( + [ + "ip", + "-4", + "route", + "add", + "default", + "via", + self.router, + "dev", + self.interface, + ], + capture=True, + ) + self.cleanup_cmds.insert( + 0, ["ip", "-4", "route", "del", "default", "dev", self.interface] + ) class RendererNotFoundError(RuntimeError): pass -# vi: ts=4 expandtab +# vi: ts=4 sw=4 expandtab diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py index f2cc078f2e3..81f27b605c6 100644 --- a/cloudinit/net/activators.py +++ b/cloudinit/net/activators.py @@ -251,6 +251,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 = [ @@ -258,6 +278,7 @@ def bring_down_interface(device_name: str) -> bool: NetplanActivator, NetworkManagerActivator, NetworkdActivator, + illumosActivator, ] diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py index 53f8c6864f3..8365d616ba4 100644 --- a/cloudinit/net/dhcp.py +++ b/cloudinit/net/dhcp.py @@ -21,6 +21,7 @@ get_devicelist, has_url_connectivity, mask_and_ipv4_to_bcast_addr, + illumos_delete_unused_intf, ) LOG = logging.getLogger(__name__) @@ -80,6 +81,10 @@ def __exit__(self, excp_type, excp_value, excp_traceback): def clean_network(self): """Exit _ephipv4 context to teardown of ip configuration performed.""" + if util.is_illumos(): + subp.subp(['/usr/sbin/ipadm', 'delete-addr', '-r', + self.lease['if']]) + illumos_delete_unused_intf(self.lease['nic']) if self.lease: self.lease = None if not self._ephipv4: @@ -101,6 +106,8 @@ def obtain_lease(self): if not leases: raise NoDHCPLeaseError() self.lease = leases[-1] + if util.is_illumos(): + return LOG.debug( "Received dhcp lease on %s for %s/%s", self.lease["interface"], @@ -176,6 +183,13 @@ def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None): "Skip dhcp_discovery: nic %s not found in get_devicelist.", nic ) raise NoDHCPLeaseInterfaceError() + if util.is_illumos(): + subp.subp(['/usr/sbin/ipadm', 'create-if', nic], rcs=[0,1]) + subp.subp(['/usr/sbin/ipadm', 'create-addr', '-T', 'dhcp', + '-w', '15', f'{nic}/ephdhcp']) + subp.subp(['/usr/sbin/svcadm', 'restart', 'network/service']) + return [{'nic': nic, 'if': f'{nic}/ephdhcp'}] + dhclient_path = subp.which("dhclient") if not dhclient_path: LOG.debug("Skip dhclient configuration: No dhclient command found.") diff --git a/cloudinit/net/illumos.py b/cloudinit/net/illumos.py new file mode 100644 index 00000000000..4c02d26fac6 --- /dev/null +++ b/cloudinit/net/illumos.py @@ -0,0 +1,186 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +import re + +from cloudinit import log as logging +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=None, instance=None): + if rcs is None: + rcs = [0] + + 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 rcs is None: + rcs = [0] + + 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/renderers.py b/cloudinit/net/renderers.py index 7edc34b55a7..6e6b3dd31bd 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 5eeeb9675b3..9c6693c9d15 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -501,9 +501,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 ecc1403bd9b..15f9af37afe 100644 --- a/cloudinit/settings.py +++ b/cloudinit/settings.py @@ -8,13 +8,18 @@ # # 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" -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 d1bec85c84d..ab6f05c5529 100644 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -268,6 +268,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" @@ -280,6 +284,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 32cc078bd6d..8e814654c80 100644 --- a/cloudinit/sources/DataSourceCloudStack.py +++ b/cloudinit/sources/DataSourceCloudStack.py @@ -19,6 +19,7 @@ from cloudinit import log as logging 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 +} + +function run { + clean + cloud-init init -l + cloud-init init + cloud-init modules --mode config + cloud-init modules --mode final +} + +python3 setup.py install --root=`pwd`/root --init-system=smf + +[ -n "$1" ] && "$@" + diff --git a/setup.py b/setup.py index 7ba0ee8c39f..37d94d6dfa4 100644 --- a/setup.py +++ b/setup.py @@ -158,6 +158,7 @@ def render_tmpl(template, mode=None): for f in glob("systemd/*") if is_f(f) and is_generator(f) ], + "smf": [f for f in glob("smf/*") if is_f(f)], } INITSYS_ROOTS = { "sysvinit": "etc/rc.d/init.d", @@ -170,6 +171,7 @@ def render_tmpl(template, mode=None): "systemd.generators": pkg_config_read( "systemd", "systemdsystemgeneratordir" ), + "smf": "lib/svc/manifest/system/", } INITSYS_TYPES = sorted([f.partition(".")[0] for f in INITSYS_ROOTS.keys()]) @@ -243,10 +245,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: @@ -302,7 +307,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": data_files.extend( [ ( 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/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.