diff --git a/cloudinit/cmd/main.py b/cloudinit/cmd/main.py index 63186d341b8..aeea07ef9c6 100644 --- a/cloudinit/cmd/main.py +++ b/cloudinit/cmd/main.py @@ -35,7 +35,7 @@ from cloudinit.reporting import events from cloudinit.settings import (PER_INSTANCE, PER_ALWAYS, PER_ONCE, - CLOUD_CONFIG) + CLOUD_CONFIG, RUN_CLOUD_CONFIG) from cloudinit import atomic_helper @@ -626,7 +626,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 036baf85ee9..8bd12dd0159 100644 --- a/cloudinit/config/cc_package_update_upgrade_install.py +++ b/cloudinit/config/cc_package_update_upgrade_install.py @@ -57,8 +57,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(0, wait_attempts): @@ -113,7 +118,7 @@ def handle(_name, cfg, cloud, log, _args): "%s", REBOOT_FILE) # 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 990a6939248..4d686899122 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 @@ get_schema_doc, validate_cloudconfig_schema) from cloudinit.settings import PER_ALWAYS from cloudinit import subp +from cloudinit import temp_utils from cloudinit import util NOBLOCK = "noblock" @@ -81,7 +83,60 @@ def _resize_ufs(mount_point, devpth): return ('growfs', '-y', mount_point) +def _resize_zfs_growpart_for_illumos(devpth): + """Due to https://www.illumos.org/issues/14022, it is currently + necessary to grow the underlying partition before growing the + ZFS pool""" + + def prtvtoc(devpth): + sectorsize = sectorcount = None + partitions = {} + (out, _) = subp.subp(['/usr/sbin/prtvtoc', + f'/dev/dsk/{devpth}'], rcs=[0]) + for line in out.splitlines(): + if not sectorsize: + m = re.search(rf'^\*\s+(\d+)\sbytes\/sector$', out) + if m: + sectorsize = m.group(1) + continue + if not sectorcount: + m = re.search(rf'^\*\s+(\d+)\ssectors$', out) + if m: + sectorcount = m.group(1) + continue + if line.startswith('*'): + continue + (pid, tag, flags, start, count, end) = line.split() + partitions[pid] = { + 'id': pid, + 'tag': tag, + 'flags': flags, + 'start': start, + 'count': count, + 'end': end, + } + return (sectorsize, sectorcount, partitions) + + sectorsize, sectorcount, partitions = prtvtoc(devpth) + + import pprint + pprint.pprint(partitions) + + + #(_, tfile) = temp_utils.mkstemp(prefix='resize-', suffix="") + #util.write_file(tfile, content="partition\nexpand\nlabel\nquit\nquit\n") + #try: + # (out, _err) = subp.subp(['/usr/sbin/format', '-d', devpth, + # '-f', tfile]) + #except subp.ProcessExecutionError as e: + # LOG.warning(f"Failed to resize {devpth} for illumos") + + #util.del_file(tfile) + + def _resize_zfs(mount_point, devpth): + if util.is_illumos(): + _resize_zfs_growpart_for_illumos(devpth) return ('zpool', 'online', '-e', mount_point, devpth) @@ -108,6 +163,18 @@ def _can_skip_resize_ufs(mount_point, devpth): return False +def _can_skip_resize_zfs(zpool, devpth): + if util.is_illumos(): + # ZFS resize is temporarily disabled for illumos + return True + 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. @@ -121,7 +188,8 @@ def _can_skip_resize_ufs(mount_point, devpth): ] RESIZE_FS_PRECHECK_CMDS = { - 'ufs': _can_skip_resize_ufs + 'ufs': _can_skip_resize_ufs, + 'zfs': _can_skip_resize_zfs, } @@ -230,7 +298,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 3843aaf7743..3b9b51c0ff1 100755 --- a/cloudinit/config/cc_set_passwords.py +++ b/cloudinit/config/cc_set_passwords.py @@ -234,7 +234,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 cf6aad14b25..2c3568eb291 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -52,6 +52,7 @@ 'redhat': ['almalinux', 'amazon', 'centos', 'cloudlinux', 'eurolinux', 'fedora', 'openEuler', 'photon', 'rhel', 'rocky', '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..c09d6a81d7e --- /dev/null +++ b/cloudinit/distros/illumos.py @@ -0,0 +1,231 @@ +import 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 + 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 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 c291196a604..d453174039d 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 subp from cloudinit import net, util @@ -194,6 +195,33 @@ def try_set_link_up(self, devname: DeviceName) -> bool: raise NotImplementedError() +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 + + 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 bba3daf2fb1..55d95c9f609 100644 --- a/cloudinit/dmi.py +++ b/cloudinit/dmi.py @@ -1,7 +1,7 @@ # 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.util import is_container, is_FreeBSD +from cloudinit.util import is_container, is_FreeBSD, is_illumos from collections import namedtuple import os @@ -11,8 +11,8 @@ # 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')), } @@ -97,6 +113,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 @@ -139,6 +181,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 7558745f2c8..c9af50ab9c7 100644 --- a/cloudinit/net/__init__.py +++ b/cloudinit/net/__init__.py @@ -12,6 +12,7 @@ import os import re from typing import Any, Dict +from socket import inet_ntoa from cloudinit import subp from cloudinit import util @@ -47,6 +48,10 @@ def natural_sort_key(s, _nsre=re.compile('([0-9]+)')): for text in re.split(_nsre, s)] +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 @@ -105,7 +110,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. @@ -114,15 +144,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 @@ -333,6 +369,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* @@ -352,7 +395,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: @@ -375,17 +418,44 @@ 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(" dict: elif util.is_OpenBSD(): 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) @@ -891,6 +977,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}. @@ -1112,13 +1210,22 @@ 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.""" - subp.subp( - ['ip', '-family', 'inet', 'addr', 'del', - '%s/%s' % (address, prefix), 'dev', self.interface], - capture=True) + + if util.is_illumos(): + subp.subp( + ['/usr/sbin/ipadm', 'delete-addr', + f'{self.interface}/eph'], capture=True) + illumos_delete_unused_intf(self.interface) + else: + subp.subp( + ['ip', '-family', 'inet', 'addr', 'del', + '%s/%s' % (address, prefix), 'dev', self.interface], + capture=True) def _bringup_device(self): """Perform the ip comands to fully setup the device.""" @@ -1127,17 +1234,30 @@ def _bringup_device(self): 'Attempting setup of ephemeral network on %s with %s brd %s', self.interface, cidr, 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', self.interface, 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( ['ip', '-family', 'inet', 'link', 'set', 'dev', self.interface, @@ -1156,38 +1276,63 @@ def _bringup_static_routes(self): via_arg = [] if gateway != "0.0.0.0": via_arg = ['via', gateway] - subp.subp( - ['ip', '-4', 'route', 'add', 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', 'add', 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', self.interface, 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 11149548b6f..499097550f0 100644 --- a/cloudinit/net/activators.py +++ b/cloudinit/net/activators.py @@ -239,6 +239,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 = [ @@ -246,6 +266,7 @@ def bring_down_interface(device_name: str) -> bool: NetworkManagerActivator, NetplanActivator, NetworkdActivator, + illumosActivator, ] diff --git a/cloudinit/net/dhcp.py b/cloudinit/net/dhcp.py index 3f4b0418711..2be12f79623 100644 --- a/cloudinit/net/dhcp.py +++ b/cloudinit/net/dhcp.py @@ -15,7 +15,7 @@ from cloudinit.net import ( EphemeralIPv4Network, find_fallback_nic, get_devicelist, - has_url_connectivity) + has_url_connectivity, illumos_delete_unused_intf) from cloudinit.net.network_state import mask_and_ipv4_to_bcast_addr as bcip from cloudinit import temp_utils from cloudinit import subp @@ -68,6 +68,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: @@ -93,6 +97,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'], self.lease['fixed-address'], self.lease['subnet-mask']) @@ -159,6 +165,14 @@ def maybe_perform_dhcp_discovery(nic=None, dhcp_log_func=None): LOG.debug( 'Skip dhcp_discovery: nic %s not found in get_devicelist.', nic) return [] + + 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 822b45de6e3..98cfae4930c 100644 --- a/cloudinit/net/renderers.py +++ b/cloudinit/net/renderers.py @@ -4,6 +4,7 @@ from . import eni from . import freebsd +from . import illumos from . import netbsd from . import netplan from . import networkd @@ -15,6 +16,7 @@ NAME_TO_RENDERER = { "eni": eni, "freebsd": freebsd, + "illumos": illumos, "netbsd": netbsd, "netplan": netplan, "networkd": networkd, @@ -23,7 +25,7 @@ } DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd", - "netbsd", "openbsd", "networkd"] + "netbsd", "openbsd", "networkd", "illumos"] def search( diff --git a/cloudinit/netinfo.py b/cloudinit/netinfo.py index 628e2908f12..05b541dcf6b 100644 --- a/cloudinit/netinfo.py +++ b/cloudinit/netinfo.py @@ -392,9 +392,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 43c8fa24cfe..4ba3f4a80b3 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 93493fa01a4..d2f9bda5b96 100755 --- a/cloudinit/sources/DataSourceAzure.py +++ b/cloudinit/sources/DataSourceAzure.py @@ -247,6 +247,10 @@ def get_resource_disk_on_freebsd(port_id): return None +def get_resource_disk_on_illumos(port_id): + return None + + # update the FreeBSD specific information if util.is_FreeBSD(): DEFAULT_PRIMARY_NIC = 'hn0' @@ -261,6 +265,21 @@ def get_resource_disk_on_freebsd(port_id): # 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 = { 'agent_command': AGENT_START_BUILTIN, 'data_dir': AGENT_SEED_DIR, diff --git a/cloudinit/sources/DataSourceCloudStack.py b/cloudinit/sources/DataSourceCloudStack.py index 8cb0d5a7ac7..506e4c9efc9 100644 --- a/cloudinit/sources/DataSourceCloudStack.py +++ b/cloudinit/sources/DataSourceCloudStack.py @@ -13,13 +13,13 @@ # This file is part of cloud-init. See LICENSE file for license information. import os -from socket import inet_ntoa, getaddrinfo, gaierror +from socket import getaddrinfo, gaierror from struct import pack import time from cloudinit import ec2_utils as ec2 from cloudinit import log as logging -from cloudinit.net import dhcp +from cloudinit.net import dhcp, get_default_gateway from cloudinit import sources from cloudinit import url_helper as uhelp from cloudinit import subp @@ -168,19 +168,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 + 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.9 setup.py install --root=`pwd`/root --init-system=smf + +[ -n "$1" ] && "$@" + diff --git a/setup.py b/setup.py index 58fddf0f2a5..bdb78ce2a99 100755 --- a/setup.py +++ b/setup.py @@ -139,6 +139,7 @@ def render_tmpl(template, mode=None): render_tmpl(f, mode=0o755) for f in glob('systemd/*') if is_f(f) and is_generator(f)], 'upstart': [f for f in glob('upstart/*') if is_f(f)], + 'smf': [f for f in glob('smf/*') if is_f(f)], } INITSYS_ROOTS = { 'sysvinit': 'etc/rc.d/init.d', @@ -151,6 +152,7 @@ def render_tmpl(template, mode=None): 'systemd.generators': pkg_config_read('systemd', 'systemdsystemgeneratordir'), 'upstart': 'etc/init/', + 'smf': 'lib/svc/manifest/system/', } INITSYS_TYPES = sorted([f.partition(".")[0] for f in INITSYS_ROOTS.keys()]) @@ -220,9 +222,11 @@ 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: @@ -266,7 +270,7 @@ def finalize_options(self): (USR + '/share/doc/cloud-init/examples/seed', [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([ (ETC + '/NetworkManager/dispatcher.d/', ['tools/hook-network-manager']), diff --git a/smf/cloud-init.xml b/smf/cloud-init.xml new file mode 100755 index 00000000000..554714f8a03 --- /dev/null +++ b/smf/cloud-init.xml @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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.