diff options
author | zsdc <taras@vyos.io> | 2022-03-25 20:58:01 +0200 |
---|---|---|
committer | zsdc <taras@vyos.io> | 2022-03-25 21:42:00 +0200 |
commit | 31448cccedd8f841fb3ac7d0f2e3cdefe08a53ba (patch) | |
tree | 349631a02467dae0158f6f663cc8aa8537974a97 /cloudinit/distros | |
parent | 5c4b3943343a85fbe517e5ec1fc670b3a8566b4b (diff) | |
parent | 8537237d80a48c8f0cbf8e66aa4826bbc882b022 (diff) | |
download | vyos-cloud-init-31448cccedd8f841fb3ac7d0f2e3cdefe08a53ba.tar.gz vyos-cloud-init-31448cccedd8f841fb3ac7d0f2e3cdefe08a53ba.zip |
T2117: Cloud-init updated to 22.1
Merged with 22.1 tag from the upstream Cloud-init repository.
Our modules were slightly modified for compatibility with the new
version.
Diffstat (limited to 'cloudinit/distros')
39 files changed, 1520 insertions, 1405 deletions
diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 1e118472..76acd6a3 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -16,48 +16,53 @@ import stat import string import urllib.parse from io import StringIO +from typing import Any, Mapping, Type from cloudinit import importer from cloudinit import log as logging -from cloudinit import net -from cloudinit.net import eni -from cloudinit.net import network_state -from cloudinit.net import renderers -from cloudinit import persistence -from cloudinit import ssh_util -from cloudinit import type_utils -from cloudinit import subp -from cloudinit import util - -from cloudinit.features import \ - ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES - +from cloudinit import net, persistence, ssh_util, subp, type_utils, util from cloudinit.distros.parsers import hosts -from .networking import LinuxNetworking +from cloudinit.features import ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES +from cloudinit.net import activators, eni, network_state, renderers +from cloudinit.net.network_state import parse_net_config_data +from .networking import LinuxNetworking, Networking # Used when a cloud-config module can be run on all cloud-init distibutions. # The value 'all' is surfaced in module documentation for distro support. -ALL_DISTROS = 'all' +ALL_DISTROS = "all" OSFAMILIES = { - 'alpine': ['alpine'], - 'arch': ['arch'], - 'debian': ['debian', 'ubuntu'], - 'freebsd': ['freebsd'], - 'gentoo': ['gentoo'], - 'redhat': ['amazon', 'centos', 'fedora', 'rhel'], - 'suse': ['opensuse', 'sles'], + "alpine": ["alpine"], + "arch": ["arch"], + "debian": ["debian", "ubuntu"], + "freebsd": ["freebsd"], + "gentoo": ["gentoo"], + "redhat": [ + "almalinux", + "amazon", + "centos", + "cloudlinux", + "eurolinux", + "fedora", + "miraclelinux", + "openEuler", + "photon", + "rhel", + "rocky", + "virtuozzo", + ], + "suse": ["opensuse", "sles"], } LOG = logging.getLogger(__name__) # This is a best guess regex, based on current EC2 AZs on 2017-12-11. # It could break when Amazon adds new regions and new AZs. -_EC2_AZ_RE = re.compile('^[a-z][a-z]-(?:[a-z]+-)+[0-9][a-z]$') +_EC2_AZ_RE = re.compile("^[a-z][a-z]-(?:[a-z]+-)+[0-9][a-z]$") # Default NTP Client Configurations -PREFERRED_NTP_CLIENTS = ['chrony', 'systemd-timesyncd', 'ntp', 'ntpdate'] +PREFERRED_NTP_CLIENTS = ["chrony", "systemd-timesyncd", "ntp", "ntpdate"] # Letters/Digits/Hyphen characters, for use in domain name validation LDH_ASCII_CHARS = string.ascii_letters + string.digits + "-" @@ -70,21 +75,23 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): ci_sudoers_fn = "/etc/sudoers.d/90-cloud-init-users" hostname_conf_fn = "/etc/hostname" tz_zone_dir = "/usr/share/zoneinfo" - init_cmd = ['service'] # systemctl, service etc - renderer_configs = {} + init_cmd = ["service"] # systemctl, service etc + renderer_configs: Mapping[str, Mapping[str, Any]] = {} _preferred_ntp_clients = None - networking_cls = LinuxNetworking + networking_cls: Type[Networking] = LinuxNetworking # This is used by self.shutdown_command(), and can be overridden in # subclasses - shutdown_options_map = {'halt': '-H', 'poweroff': '-P', 'reboot': '-r'} + shutdown_options_map = {"halt": "-H", "poweroff": "-P", "reboot": "-r"} _ci_pkl_version = 1 + prefer_fqdn = False + resolve_conf_fn = "/etc/resolv.conf" def __init__(self, name, cfg, paths): self._paths = paths self._cfg = cfg self.name = name - self.networking = self.networking_cls() + self.networking: Networking = self.networking_cls() def _unpickle(self, ci_pkl_version: int) -> None: """Perform deserialization fixes for Distro.""" @@ -103,34 +110,38 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): raise NotImplementedError() def _write_network(self, settings): - raise RuntimeError( + """Deprecated. Remove if/when arch and gentoo support renderers.""" + raise NotImplementedError( "Legacy function '_write_network' was called in distro '%s'.\n" - "_write_network_config needs implementation.\n" % self.name) - - def _write_network_config(self, settings): - raise NotImplementedError() + "_write_network_config needs implementation.\n" % self.name + ) - def _supported_write_network_config(self, network_config): + def _write_network_state(self, network_state): priority = util.get_cfg_by_path( - self._cfg, ('network', 'renderers'), None) + self._cfg, ("network", "renderers"), None + ) name, render_cls = renderers.select(priority=priority) - LOG.debug("Selected renderer '%s' from priority list: %s", - name, priority) + LOG.debug( + "Selected renderer '%s' from priority list: %s", name, priority + ) renderer = render_cls(config=self.renderer_configs.get(name)) - renderer.render_network_config(network_config) - return [] + renderer.render_network_state(network_state) def _find_tz_file(self, tz): tz_file = os.path.join(self.tz_zone_dir, str(tz)) if not os.path.isfile(tz_file): - raise IOError(("Invalid timezone %s," - " no file found at %s") % (tz, tz_file)) + raise IOError( + "Invalid timezone %s, no file found at %s" % (tz, tz_file) + ) return tz_file def get_option(self, opt_name, default=None): return self._cfg.get(opt_name, default) + def set_option(self, opt_name, value=None): + self._cfg[opt_name] = value + def set_hostname(self, hostname, fqdn=None): writeable_hostname = self._select_hostname(hostname, fqdn) self._write_hostname(writeable_hostname, self.hostname_conf_fn) @@ -141,7 +152,7 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): return uses_systemd() @abc.abstractmethod - def package_command(self, cmd, args=None, pkgs=None): + def package_command(self, command, args=None, pkgs=None): raise NotImplementedError() @abc.abstractmethod @@ -164,10 +175,12 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # This resolves the package_mirrors config option # down to a single dict of {mirror_name: mirror_url} arch_info = self._get_arch_package_mirror_info(arch) - return _get_package_mirror_info(data_source=data_source, - mirror_info=arch_info) + return _get_package_mirror_info( + data_source=data_source, mirror_info=arch_info + ) def apply_network(self, settings, bring_up=True): + """Deprecated. Remove if/when arch and gentoo support renderers.""" # this applies network where 'settings' is interfaces(5) style # it is obsolete compared to apply_network_config # Write it out @@ -182,36 +195,62 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): return False def _apply_network_from_network_config(self, netconfig, bring_up=True): + """Deprecated. Remove if/when arch and gentoo support renderers.""" distro = self.__class__ - LOG.warning("apply_network_config is not currently implemented " - "for distribution '%s'. Attempting to use apply_network", - distro) - header = '\n'.join([ - "# Converted from network_config for distro %s" % distro, - "# Implementation of _write_network_config is needed." - ]) + LOG.warning( + "apply_network_config is not currently implemented " + "for distribution '%s'. Attempting to use apply_network", + distro, + ) + header = "\n".join( + [ + "# Converted from network_config for distro %s" % distro, + "# Implementation of _write_network_config is needed.", + ] + ) ns = network_state.parse_net_config_data(netconfig) contents = eni.network_state_to_eni( - ns, header=header, render_hwaddress=True) + ns, header=header, render_hwaddress=True + ) return self.apply_network(contents, bring_up=bring_up) def generate_fallback_config(self): return net.generate_fallback_config() - def apply_network_config(self, netconfig, bring_up=False): - # apply network config netconfig + def apply_network_config(self, netconfig, bring_up=False) -> bool: + """Apply the network config. + + If bring_up is True, attempt to bring up the passed in devices. If + devices is None, attempt to bring up devices returned by + _write_network_config. + + Returns True if any devices failed to come up, otherwise False. + """ # This method is preferred to apply_network which only takes # a much less complete network config format (interfaces(5)). + network_state = parse_net_config_data(netconfig) try: - dev_names = self._write_network_config(netconfig) + self._write_network_state(network_state) except NotImplementedError: # backwards compat until all distros have apply_network_config return self._apply_network_from_network_config( - netconfig, bring_up=bring_up) + netconfig, bring_up=bring_up + ) # Now try to bring them up if bring_up: - return self._bring_up_interfaces(dev_names) + LOG.debug("Bringing up newly configured network interfaces") + try: + network_activator = activators.select_activator() + except activators.NoActivatorException: + LOG.warning( + "No network activator found, not bringing up " + "network interfaces" + ) + return True + network_activator.bring_up_all_interfaces(network_state) + else: + LOG.debug("Not bringing up newly configured network interfaces") return False def apply_network_config_names(self, netconfig): @@ -248,17 +287,28 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # temporarily (until reboot so it should # not be depended on). Use the write # hostname functions for 'permanent' adjustments. - LOG.debug("Non-persistently setting the system hostname to %s", - hostname) + LOG.debug( + "Non-persistently setting the system hostname to %s", hostname + ) try: - subp.subp(['hostname', hostname]) + subp.subp(["hostname", hostname]) except subp.ProcessExecutionError: - util.logexc(LOG, "Failed to non-persistently adjust the system " - "hostname to %s", hostname) + util.logexc( + LOG, + "Failed to non-persistently adjust the system hostname to %s", + hostname, + ) def _select_hostname(self, hostname, fqdn): # Prefer the short hostname over the long # fully qualified domain name + if ( + util.get_cfg_option_bool( + self._cfg, "prefer_fqdn_over_hostname", self.prefer_fqdn + ) + and fqdn + ): + return fqdn if not hostname: return fqdn return hostname @@ -300,32 +350,39 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # If the system hostname is different than the previous # one or the desired one lets update it as well - if ((not sys_hostname) or (sys_hostname == prev_hostname and - sys_hostname != hostname)): + if (not sys_hostname) or ( + sys_hostname == prev_hostname and sys_hostname != hostname + ): update_files.append(sys_fn) # If something else has changed the hostname after we set it # initially, we should not overwrite those changes (we should # only be setting the hostname once per instance) - if (sys_hostname and prev_hostname and - sys_hostname != prev_hostname): - LOG.info("%s differs from %s, assuming user maintained hostname.", - prev_hostname_fn, sys_fn) + if sys_hostname and prev_hostname and sys_hostname != prev_hostname: + LOG.info( + "%s differs from %s, assuming user maintained hostname.", + prev_hostname_fn, + sys_fn, + ) return # Remove duplicates (incase the previous config filename) # is the same as the system config filename, don't bother # doing it twice update_files = set([f for f in update_files if f]) - LOG.debug("Attempting to update hostname to %s in %s files", - hostname, len(update_files)) + LOG.debug( + "Attempting to update hostname to %s in %s files", + hostname, + len(update_files), + ) for fn in update_files: try: self._write_hostname(hostname, fn) except IOError: - util.logexc(LOG, "Failed to write hostname %s to %s", hostname, - fn) + util.logexc( + LOG, "Failed to write hostname %s to %s", hostname, fn + ) # If the system hostname file name was provided set the # non-fqdn as the transient hostname. @@ -333,11 +390,11 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): self._apply_hostname(applying_hostname) def update_etc_hosts(self, hostname, fqdn): - header = '' + header = "" if os.path.exists(self.hosts_fn): eh = hosts.HostsConf(util.load_file(self.hosts_fn)) else: - eh = hosts.HostsConf('') + eh = hosts.HostsConf("") header = util.make_header(base="added") local_ip = self._get_localhost_ip() prev_info = eh.get_entry(local_ip) @@ -384,20 +441,11 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): return self._preferred_ntp_clients def _bring_up_interface(self, device_name): - cmd = ['ifup', device_name] - LOG.debug("Attempting to run bring up interface %s using command %s", - device_name, cmd) - try: - (_out, err) = subp.subp(cmd) - if len(err): - LOG.warning("Running %s resulted in stderr output: %s", - cmd, err) - return True - except subp.ProcessExecutionError: - util.logexc(LOG, "Running interface command %s failed", cmd) - return False + """Deprecated. Remove if/when arch and gentoo support renderers.""" + raise NotImplementedError def _bring_up_interfaces(self, device_names): + """Deprecated. Remove if/when arch and gentoo support renderers.""" am_failed = 0 for d in device_names: if not self._bring_up_interface(d): @@ -407,7 +455,7 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): return False def get_default_user(self): - return self.get_option('default_user') + return self.get_option("default_user") def add_user(self, name, **kwargs): """ @@ -423,43 +471,43 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): LOG.info("User %s already exists, skipping.", name) return - if 'create_groups' in kwargs: - create_groups = kwargs.pop('create_groups') + if "create_groups" in kwargs: + create_groups = kwargs.pop("create_groups") else: create_groups = True - useradd_cmd = ['useradd', name] - log_useradd_cmd = ['useradd', name] + useradd_cmd = ["useradd", name] + log_useradd_cmd = ["useradd", name] if util.system_is_snappy(): - useradd_cmd.append('--extrausers') - log_useradd_cmd.append('--extrausers') + useradd_cmd.append("--extrausers") + log_useradd_cmd.append("--extrausers") # Since we are creating users, we want to carefully validate the # inputs. If something goes wrong, we can end up with a system # that nobody can login to. useradd_opts = { - "gecos": '--comment', - "homedir": '--home', - "primary_group": '--gid', - "uid": '--uid', - "groups": '--groups', - "passwd": '--password', - "shell": '--shell', - "expiredate": '--expiredate', - "inactive": '--inactive', - "selinux_user": '--selinux-user', + "gecos": "--comment", + "homedir": "--home", + "primary_group": "--gid", + "uid": "--uid", + "groups": "--groups", + "passwd": "--password", + "shell": "--shell", + "expiredate": "--expiredate", + "inactive": "--inactive", + "selinux_user": "--selinux-user", } useradd_flags = { - "no_user_group": '--no-user-group', - "system": '--system', - "no_log_init": '--no-log-init', + "no_user_group": "--no-user-group", + "system": "--system", + "no_log_init": "--no-log-init", } - redact_opts = ['passwd'] + redact_opts = ["passwd"] # support kwargs having groups=[list] or groups="g1,g2" - groups = kwargs.get('groups') + groups = kwargs.get("groups") if groups: if isinstance(groups, str): groups = groups.split(",") @@ -470,9 +518,9 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # kwargs.items loop below wants a comma delimeted string # that can go right through to the command. - kwargs['groups'] = ",".join(groups) + kwargs["groups"] = ",".join(groups) - primary_group = kwargs.get('primary_group') + primary_group = kwargs.get("primary_group") if primary_group: groups.append(primary_group) @@ -490,7 +538,7 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # Redact certain fields from the logs if key in redact_opts: - log_useradd_cmd.extend([useradd_opts[key], 'REDACTED']) + log_useradd_cmd.extend([useradd_opts[key], "REDACTED"]) else: log_useradd_cmd.extend([useradd_opts[key], val]) @@ -500,12 +548,12 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # Don't create the home directory if directed so or if the user is a # system user - if kwargs.get('no_create_home') or kwargs.get('system'): - useradd_cmd.append('-M') - log_useradd_cmd.append('-M') + if kwargs.get("no_create_home") or kwargs.get("system"): + useradd_cmd.append("-M") + log_useradd_cmd.append("-M") else: - useradd_cmd.append('-m') - log_useradd_cmd.append('-m') + useradd_cmd.append("-m") + log_useradd_cmd.append("-m") # Run the command LOG.debug("Adding user %s", name) @@ -520,8 +568,8 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): Add a snappy user to the system using snappy tools """ - snapuser = kwargs.get('snapuser') - known = kwargs.get('known', False) + snapuser = kwargs.get("snapuser") + known = kwargs.get("known", False) create_user_cmd = ["snap", "create-user", "--sudoer", "--json"] if known: create_user_cmd.append("--known") @@ -530,11 +578,12 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): # Run the command LOG.debug("Adding snap user %s", name) try: - (out, err) = subp.subp(create_user_cmd, logstring=create_user_cmd, - capture=True) + (out, err) = subp.subp( + create_user_cmd, logstring=create_user_cmd, capture=True + ) LOG.debug("snap create-user returned: %s:%s", out, err) jobj = util.load_json(out) - username = jobj.get('username', None) + username = jobj.get("username", None) except Exception as e: util.logexc(LOG, "Failed to create snap user %s", name) raise e @@ -562,60 +611,66 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): """ # Add a snap user, if requested - if 'snapuser' in kwargs: + if "snapuser" in kwargs: return self.add_snap_user(name, **kwargs) # Add the user self.add_user(name, **kwargs) # Set password if plain-text password provided and non-empty - if 'plain_text_passwd' in kwargs and kwargs['plain_text_passwd']: - self.set_passwd(name, kwargs['plain_text_passwd']) + if "plain_text_passwd" in kwargs and kwargs["plain_text_passwd"]: + self.set_passwd(name, kwargs["plain_text_passwd"]) # Set password if hashed password is provided and non-empty - if 'hashed_passwd' in kwargs and kwargs['hashed_passwd']: - self.set_passwd(name, kwargs['hashed_passwd'], hashed=True) + if "hashed_passwd" in kwargs and kwargs["hashed_passwd"]: + self.set_passwd(name, kwargs["hashed_passwd"], hashed=True) # Default locking down the account. 'lock_passwd' defaults to True. # lock account unless lock_password is False. - if kwargs.get('lock_passwd', True): + if kwargs.get("lock_passwd", True): self.lock_passwd(name) # Configure sudo access - if 'sudo' in kwargs and kwargs['sudo'] is not False: - self.write_sudo_rules(name, kwargs['sudo']) + if "sudo" in kwargs and kwargs["sudo"] is not False: + self.write_sudo_rules(name, kwargs["sudo"]) # Import SSH keys - if 'ssh_authorized_keys' in kwargs: + if "ssh_authorized_keys" in kwargs: # Try to handle this in a smart manner. - keys = kwargs['ssh_authorized_keys'] + keys = kwargs["ssh_authorized_keys"] if isinstance(keys, str): keys = [keys] elif isinstance(keys, dict): keys = list(keys.values()) if keys is not None: if not isinstance(keys, (tuple, list, set)): - LOG.warning("Invalid type '%s' detected for" - " 'ssh_authorized_keys', expected list," - " string, dict, or set.", type(keys)) + LOG.warning( + "Invalid type '%s' detected for" + " 'ssh_authorized_keys', expected list," + " string, dict, or set.", + type(keys), + ) keys = [] else: keys = set(keys) or [] ssh_util.setup_user_keys(set(keys), name) - if 'ssh_redirect_user' in kwargs: - cloud_keys = kwargs.get('cloud_public_ssh_keys', []) + if "ssh_redirect_user" in kwargs: + cloud_keys = kwargs.get("cloud_public_ssh_keys", []) if not cloud_keys: LOG.warning( - 'Unable to disable SSH logins for %s given' - ' ssh_redirect_user: %s. No cloud public-keys present.', - name, kwargs['ssh_redirect_user']) + "Unable to disable SSH logins for %s given" + " ssh_redirect_user: %s. No cloud public-keys present.", + name, + kwargs["ssh_redirect_user"], + ) else: - redirect_user = kwargs['ssh_redirect_user'] + redirect_user = kwargs["ssh_redirect_user"] disable_option = ssh_util.DISABLE_USER_OPTS - disable_option = disable_option.replace('$USER', redirect_user) - disable_option = disable_option.replace('$DISABLE_USER', name) + disable_option = disable_option.replace("$USER", redirect_user) + disable_option = disable_option.replace("$DISABLE_USER", name) ssh_util.setup_user_keys( - set(cloud_keys), name, options=disable_option) + set(cloud_keys), name, options=disable_option + ) return True def lock_passwd(self, name): @@ -623,36 +678,36 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): Lock the password of a user, i.e., disable password logins """ # passwd must use short '-l' due to SLES11 lacking long form '--lock' - lock_tools = (['passwd', '-l', name], ['usermod', '--lock', name]) + lock_tools = (["passwd", "-l", name], ["usermod", "--lock", name]) try: cmd = next(tool for tool in lock_tools if subp.which(tool[0])) except StopIteration as e: - raise RuntimeError(( + raise RuntimeError( "Unable to lock user account '%s'. No tools available. " - " Tried: %s.") % (name, [c[0] for c in lock_tools]) + " Tried: %s." % (name, [c[0] for c in lock_tools]) ) from e try: subp.subp(cmd) except Exception as e: - util.logexc(LOG, 'Failed to disable password for user %s', name) + util.logexc(LOG, "Failed to disable password for user %s", name) raise e def expire_passwd(self, user): try: - subp.subp(['passwd', '--expire', user]) + subp.subp(["passwd", "--expire", user]) except Exception as e: util.logexc(LOG, "Failed to set 'expire' for %s", user) raise e def set_passwd(self, user, passwd, hashed=False): - pass_string = '%s:%s' % (user, passwd) - cmd = ['chpasswd'] + pass_string = "%s:%s" % (user, passwd) + cmd = ["chpasswd"] if hashed: # Need to use the short option name '-e' instead of '--encrypted' # (which would be more descriptive) since SLES 11 doesn't know # about long names. - cmd.append('-e') + cmd.append("-e") try: subp.subp(cmd, pass_string, logstring="chpasswd for %s" % user) @@ -662,10 +717,10 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): return True - def ensure_sudo_dir(self, path, sudo_base='/etc/sudoers'): + def ensure_sudo_dir(self, path, sudo_base="/etc/sudoers"): # Ensure the dir is included and that # it actually exists as a directory - sudoers_contents = '' + sudoers_contents = "" base_exists = False if os.path.exists(sudo_base): sudoers_contents = util.load_file(sudo_base) @@ -673,7 +728,7 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): found_include = False for line in sudoers_contents.splitlines(): line = line.strip() - include_match = re.search(r"^#includedir\s+(.*)$", line) + include_match = re.search(r"^[#|@]includedir\s+(.*)$", line) if not include_match: continue included_dir = include_match.group(1).strip() @@ -686,15 +741,23 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): if not found_include: try: if not base_exists: - lines = [('# See sudoers(5) for more information' - ' on "#include" directives:'), '', - util.make_header(base="added"), - "#includedir %s" % (path), ''] + lines = [ + "# See sudoers(5) for more information" + ' on "#include" directives:', + "", + util.make_header(base="added"), + "#includedir %s" % (path), + "", + ] sudoers_contents = "\n".join(lines) util.write_file(sudo_base, sudoers_contents, 0o440) else: - lines = ['', util.make_header(base="added"), - "#includedir %s" % (path), ''] + lines = [ + "", + util.make_header(base="added"), + "#includedir %s" % (path), + "", + ] sudoers_contents = "\n".join(lines) util.append_file(sudo_base, sudoers_contents) LOG.debug("Added '#includedir %s' to %s", path, sudo_base) @@ -708,7 +771,7 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): sudo_file = self.ci_sudoers_fn lines = [ - '', + "", "# User rules for %s" % user, ] if isinstance(rules, (list, tuple)): @@ -741,9 +804,9 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): raise e def create_group(self, name, members=None): - group_add_cmd = ['groupadd', name] + group_add_cmd = ["groupadd", name] if util.system_is_snappy(): - group_add_cmd.append('--extrausers') + group_add_cmd.append("--extrausers") if not members: members = [] @@ -761,11 +824,15 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): if len(members) > 0: for member in members: if not util.is_user(member): - LOG.warning("Unable to add group member '%s' to group '%s'" - "; user does not exist.", member, name) + LOG.warning( + "Unable to add group member '%s' to group '%s'" + "; user does not exist.", + member, + name, + ) continue - subp.subp(['usermod', '-a', '-G', name, member]) + subp.subp(["usermod", "-a", "-G", name, member]) LOG.info("Added user '%s' to group '%s'", member, name) def shutdown_command(self, *, mode, delay, message): @@ -784,6 +851,51 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): args.append(message) return args + def manage_service(self, action, service): + """ + Perform the requested action on a service. This handles the common + 'systemctl' and 'service' cases and may be overridden in subclasses + as necessary. + May raise ProcessExecutionError + """ + init_cmd = self.init_cmd + if self.uses_systemd() or "systemctl" in init_cmd: + init_cmd = ["systemctl"] + cmds = { + "stop": ["stop", service], + "start": ["start", service], + "enable": ["enable", service], + "restart": ["restart", service], + "reload": ["reload-or-restart", service], + "try-reload": ["reload-or-try-restart", service], + } + else: + cmds = { + "stop": [service, "stop"], + "start": [service, "start"], + "enable": [service, "start"], + "restart": [service, "restart"], + "reload": [service, "restart"], + "try-reload": [service, "restart"], + } + cmd = list(init_cmd) + list(cmds[action]) + return subp.subp(cmd, capture=True) + + def set_keymap(self, layout, model, variant, options): + if self.uses_systemd(): + subp.subp( + [ + "localectl", + "set-x11-keymap", + layout, + model, + variant, + options, + ] + ) + else: + raise NotImplementedError() + def _apply_hostname_transformations_to_url(url: str, transformations: list): """ @@ -837,7 +949,7 @@ def _sanitize_mirror_url(url: str): * Converts it to its IDN form (see below for details) * Replaces any non-Letters/Digits/Hyphen (LDH) characters in it with hyphens - * TODO: Remove any leading/trailing hyphens from each domain name label + * Removes any leading/trailing hyphens from each domain name label Before we replace any invalid domain name characters, we first need to ensure that any valid non-ASCII characters in the hostname will not be @@ -871,27 +983,25 @@ def _sanitize_mirror_url(url: str): # This is an IP address, not a hostname, so no need to apply the # transformations lambda hostname: None if net.is_ip_address(hostname) else hostname, - # Encode with IDNA to get the correct characters (as `bytes`), then # decode with ASCII so we return a `str` - lambda hostname: hostname.encode('idna').decode('ascii'), - + lambda hostname: hostname.encode("idna").decode("ascii"), # Replace any unacceptable characters with "-" - lambda hostname: ''.join( + lambda hostname: "".join( c if c in acceptable_chars else "-" for c in hostname ), - # Drop leading/trailing hyphens from each part of the hostname - lambda hostname: '.'.join( - part.strip('-') for part in hostname.split('.') + lambda hostname: ".".join( + part.strip("-") for part in hostname.split(".") ), ] return _apply_hostname_transformations_to_url(url, transformations) -def _get_package_mirror_info(mirror_info, data_source=None, - mirror_filter=util.search_for_mirror): +def _get_package_mirror_info( + mirror_info, data_source=None, mirror_filter=util.search_for_mirror +): # given a arch specific 'mirror_info' entry (from package_mirrors) # search through the 'search' entries, and fallback appropriately # return a dict with only {name: mirror} entries. @@ -900,7 +1010,7 @@ def _get_package_mirror_info(mirror_info, data_source=None, subst = {} if data_source and data_source.availability_zone: - subst['availability_zone'] = data_source.availability_zone + subst["availability_zone"] = data_source.availability_zone # ec2 availability zones are named cc-direction-[0-9][a-d] (us-east-1b) # the region is us-east-1. so region = az[0:-1] @@ -908,18 +1018,18 @@ def _get_package_mirror_info(mirror_info, data_source=None, ec2_region = data_source.availability_zone[0:-1] if ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES: - subst['ec2_region'] = "%s" % ec2_region + subst["ec2_region"] = "%s" % ec2_region elif data_source.platform_type == "ec2": - subst['ec2_region'] = "%s" % ec2_region + subst["ec2_region"] = "%s" % ec2_region if data_source and data_source.region: - subst['region'] = data_source.region + subst["region"] = data_source.region results = {} - for (name, mirror) in mirror_info.get('failsafe', {}).items(): + for (name, mirror) in mirror_info.get("failsafe", {}).items(): results[name] = mirror - for (name, searchlist) in mirror_info.get('search', {}).items(): + for (name, searchlist) in mirror_info.get("search", {}).items(): mirrors = [] for tmpl in searchlist: try: @@ -953,17 +1063,20 @@ def _get_arch_package_mirror_info(package_mirrors, arch): def fetch(name): - locs, looked_locs = importer.find_module(name, ['', __name__], ['Distro']) + locs, looked_locs = importer.find_module(name, ["", __name__], ["Distro"]) if not locs: - raise ImportError("No distribution found for distro %s (searched %s)" - % (name, looked_locs)) + raise ImportError( + "No distribution found for distro %s (searched %s)" + % (name, looked_locs) + ) mod = importer.import_module(locs[0]) - cls = getattr(mod, 'Distro') + cls = getattr(mod, "Distro") return cls -def set_etc_timezone(tz, tz_file=None, tz_conf="/etc/timezone", - tz_local="/etc/localtime"): +def set_etc_timezone( + tz, tz_file=None, tz_conf="/etc/timezone", tz_local="/etc/localtime" +): util.write_file(tz_conf, str(tz).rstrip() + "\n") # This ensures that the correct tz will be used for the system if tz_local and tz_file: @@ -980,7 +1093,7 @@ def set_etc_timezone(tz, tz_file=None, tz_conf="/etc/timezone", def uses_systemd(): try: - res = os.lstat('/run/systemd/system') + res = os.lstat("/run/systemd/system") return stat.S_ISDIR(res.st_mode) except Exception: return False diff --git a/cloudinit/distros/almalinux.py b/cloudinit/distros/almalinux.py new file mode 100644 index 00000000..3dc0a342 --- /dev/null +++ b/cloudinit/distros/almalinux.py @@ -0,0 +1,10 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + + +# vi: ts=4 expandtab diff --git a/cloudinit/distros/alpine.py b/cloudinit/distros/alpine.py index ca5bfe80..3d7d4891 100644 --- a/cloudinit/distros/alpine.py +++ b/cloudinit/distros/alpine.py @@ -6,13 +6,8 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import distros -from cloudinit import helpers -from cloudinit import subp -from cloudinit import util - +from cloudinit import distros, helpers, subp, util from cloudinit.distros.parsers.hostname import HostnameConf - from cloudinit.settings import PER_INSTANCE NETWORK_FILE_HEADER = """\ @@ -26,12 +21,11 @@ NETWORK_FILE_HEADER = """\ class Distro(distros.Distro): - init_cmd = ['rc-service'] # init scripts + init_cmd = ["rc-service"] # init scripts locale_conf_fn = "/etc/profile.d/locale.sh" network_conf_fn = "/etc/network/interfaces" renderer_configs = { - "eni": {"eni_path": network_conf_fn, - "eni_header": NETWORK_FILE_HEADER} + "eni": {"eni_path": network_conf_fn, "eni_header": NETWORK_FILE_HEADER} } def __init__(self, name, cfg, paths): @@ -40,13 +34,13 @@ class Distro(distros.Distro): # calls from repeatly happening (when they # should only happen say once per instance...) self._runner = helpers.Runners(paths) - self.default_locale = 'C.UTF-8' - self.osfamily = 'alpine' - cfg['ssh_svcname'] = 'sshd' + self.default_locale = "C.UTF-8" + self.osfamily = "alpine" + cfg["ssh_svcname"] = "sshd" def get_locale(self): """The default locale for Alpine Linux is different than - cloud-init's DataSource default. + cloud-init's DataSource default. """ return self.default_locale @@ -71,33 +65,20 @@ class Distro(distros.Distro): def install_packages(self, pkglist): self.update_package_sources() - self.package_command('add', pkgs=pkglist) - - def _write_network_config(self, netconfig): - return self._supported_write_network_config(netconfig) - - def _bring_up_interfaces(self, device_names): - use_all = False - for d in device_names: - if d == 'all': - use_all = True - if use_all: - return distros.Distro._bring_up_interface(self, '-a') - else: - return distros.Distro._bring_up_interfaces(self, device_names) + self.package_command("add", pkgs=pkglist) - def _write_hostname(self, your_hostname, out_fn): + def _write_hostname(self, hostname, filename): conf = None try: # Try to update the previous one # so lets see if we can read it first. - conf = self._read_hostname_conf(out_fn) + conf = self._read_hostname_conf(filename) except IOError: pass if not conf: - conf = HostnameConf('') - conf.set_hostname(your_hostname) - util.write_file(out_fn, str(conf), 0o644) + conf = HostnameConf("") + conf.set_hostname(hostname) + util.write_file(filename, str(conf), 0o644) def _read_system_hostname(self): sys_hostname = self._read_hostname(self.hostname_conf_fn) @@ -129,7 +110,7 @@ class Distro(distros.Distro): if pkgs is None: pkgs = [] - cmd = ['apk'] + cmd = ["apk"] # Redirect output cmd.append("--quiet") @@ -141,25 +122,32 @@ class Distro(distros.Distro): if command: cmd.append(command) - pkglist = util.expand_package_list('%s-%s', pkgs) + if command == "upgrade": + cmd.extend(["--update-cache", "--available"]) + + pkglist = util.expand_package_list("%s-%s", pkgs) cmd.extend(pkglist) # Allow the output of this to flow outwards (ie not be captured) subp.subp(cmd, capture=False) def update_package_sources(self): - self._runner.run("update-sources", self.package_command, - ["update"], freq=PER_INSTANCE) + self._runner.run( + "update-sources", + self.package_command, + ["update"], + freq=PER_INSTANCE, + ) @property def preferred_ntp_clients(self): """Allow distro to determine the preferred ntp client list""" if not self._preferred_ntp_clients: - self._preferred_ntp_clients = ['chrony', 'ntp'] + self._preferred_ntp_clients = ["chrony", "ntp"] return self._preferred_ntp_clients - def shutdown_command(self, mode='poweroff', delay='now', message=None): + def shutdown_command(self, mode="poweroff", delay="now", message=None): # called from cc_power_state_change.load_power_state # Alpine has halt/poweroff/reboot, with the following specifics: # - we use them rather than the generic "shutdown" @@ -173,7 +161,7 @@ class Distro(distros.Distro): # halt/poweroff/reboot commands take seconds rather than minutes. if delay == "now": # Alpine's commands do not understand "now". - command += ['0'] + command += ["0"] else: try: command.append(str(int(delay) * 60)) @@ -185,4 +173,5 @@ class Distro(distros.Distro): return command + # vi: ts=4 expandtab diff --git a/cloudinit/distros/amazon.py b/cloudinit/distros/amazon.py index 5fcec952..a3573547 100644 --- a/cloudinit/distros/amazon.py +++ b/cloudinit/distros/amazon.py @@ -14,7 +14,6 @@ from cloudinit.distros import rhel class Distro(rhel.Distro): - def update_package_sources(self): return None diff --git a/cloudinit/distros/arch.py b/cloudinit/distros/arch.py index 967be168..0bdfef83 100644 --- a/cloudinit/distros/arch.py +++ b/cloudinit/distros/arch.py @@ -4,33 +4,29 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import distros -from cloudinit import helpers -from cloudinit import log as logging -from cloudinit import util -from cloudinit import subp +import os +from cloudinit import distros, helpers +from cloudinit import log as logging +from cloudinit import subp, util from cloudinit.distros import net_util from cloudinit.distros.parsers.hostname import HostnameConf - from cloudinit.net.renderers import RendererNotFoundError - from cloudinit.settings import PER_INSTANCE -import os - LOG = logging.getLogger(__name__) class Distro(distros.Distro): - locale_conf_fn = "/etc/locale.gen" + locale_gen_fn = "/etc/locale.gen" network_conf_dir = "/etc/netctl" - resolve_conf_fn = "/etc/resolv.conf" - init_cmd = ['systemctl'] # init scripts + init_cmd = ["systemctl"] # init scripts renderer_configs = { - "netplan": {"netplan_path": "/etc/netplan/50-cloud-init.yaml", - "netplan_header": "# generated by cloud-init\n", - "postcmds": True} + "netplan": { + "netplan_path": "/etc/netplan/50-cloud-init.yaml", + "netplan_header": "# generated by cloud-init\n", + "postcmds": True, + } } def __init__(self, name, cfg, paths): @@ -39,83 +35,94 @@ class Distro(distros.Distro): # calls from repeatly happening (when they # should only happen say once per instance...) self._runner = helpers.Runners(paths) - self.osfamily = 'arch' - cfg['ssh_svcname'] = 'sshd' + self.osfamily = "arch" + cfg["ssh_svcname"] = "sshd" def apply_locale(self, locale, out_fn=None): - if not out_fn: - out_fn = self.locale_conf_fn - subp.subp(['locale-gen', '-G', locale], capture=False) - # "" provides trailing newline during join + if out_fn is not None and out_fn != "/etc/locale.conf": + LOG.warning( + "Invalid locale_configfile %s, only supported " + "value is /etc/locale.conf", + out_fn, + ) lines = [ util.make_header(), - 'LANG="%s"' % (locale), + # Hard-coding the charset isn't ideal, but there is no other way. + "%s UTF-8" % (locale), "", ] - util.write_file(out_fn, "\n".join(lines)) + util.write_file(self.locale_gen_fn, "\n".join(lines)) + subp.subp(["locale-gen"], capture=False) + # In the future systemd can handle locale-gen stuff: + # https://github.com/systemd/systemd/pull/9864 + subp.subp(["localectl", "set-locale", locale], capture=False) def install_packages(self, pkglist): self.update_package_sources() - self.package_command('', pkgs=pkglist) + self.package_command("", pkgs=pkglist) - def _write_network_config(self, netconfig): + def _write_network_state(self, network_state): try: - return self._supported_write_network_config(netconfig) + super()._write_network_state(network_state) except RendererNotFoundError as e: # Fall back to old _write_network raise NotImplementedError from e def _write_network(self, settings): entries = net_util.translate_network(settings) - LOG.debug("Translated ubuntu style network settings %s into %s", - settings, entries) + LOG.debug( + "Translated ubuntu style network settings %s into %s", + settings, + entries, + ) return _render_network( - entries, resolv_conf=self.resolve_conf_fn, + entries, + resolv_conf=self.resolve_conf_fn, conf_dir=self.network_conf_dir, - enable_func=self._enable_interface) + enable_func=self._enable_interface, + ) def _enable_interface(self, device_name): - cmd = ['netctl', 'reenable', device_name] + cmd = ["netctl", "reenable", device_name] try: (_out, err) = subp.subp(cmd) if len(err): - LOG.warning("Running %s resulted in stderr output: %s", - cmd, err) + LOG.warning( + "Running %s resulted in stderr output: %s", cmd, err + ) except subp.ProcessExecutionError: util.logexc(LOG, "Running interface command %s failed", cmd) def _bring_up_interface(self, device_name): - cmd = ['netctl', 'restart', device_name] - LOG.debug("Attempting to run bring up interface %s using command %s", - device_name, cmd) + cmd = ["netctl", "restart", device_name] + LOG.debug( + "Attempting to run bring up interface %s using command %s", + device_name, + cmd, + ) try: (_out, err) = subp.subp(cmd) if len(err): - LOG.warning("Running %s resulted in stderr output: %s", - cmd, err) + LOG.warning( + "Running %s resulted in stderr output: %s", cmd, err + ) return True except subp.ProcessExecutionError: util.logexc(LOG, "Running interface command %s failed", cmd) return False - def _bring_up_interfaces(self, device_names): - for d in device_names: - if not self._bring_up_interface(d): - return False - return True - - def _write_hostname(self, your_hostname, out_fn): + def _write_hostname(self, hostname, filename): conf = None try: # Try to update the previous one # so lets see if we can read it first. - conf = self._read_hostname_conf(out_fn) + conf = self._read_hostname_conf(filename) except IOError: pass if not conf: - conf = HostnameConf('') - conf.set_hostname(your_hostname) - util.write_file(out_fn, str(conf), omode="w", mode=0o644) + conf = HostnameConf("") + conf.set_hostname(hostname) + util.write_file(filename, str(conf), omode="w", mode=0o644) def _read_system_hostname(self): sys_hostname = self._read_hostname(self.hostname_conf_fn) @@ -137,6 +144,21 @@ class Distro(distros.Distro): return default return hostname + # hostname (inetutils) isn't installed per default on arch, so we use + # hostnamectl which is installed per default (systemd). + def _apply_hostname(self, hostname): + LOG.debug( + "Non-persistently setting the system hostname to %s", hostname + ) + try: + subp.subp(["hostnamectl", "--transient", "set-hostname", hostname]) + except subp.ProcessExecutionError: + util.logexc( + LOG, + "Failed to non-persistently adjust the system hostname to %s", + hostname, + ) + def set_timezone(self, tz): distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) @@ -144,7 +166,7 @@ class Distro(distros.Distro): if pkgs is None: pkgs = [] - cmd = ['pacman', "-Sy", "--quiet", "--noconfirm"] + cmd = ["pacman", "-Sy", "--quiet", "--noconfirm"] # Redirect output if args and isinstance(args, str): @@ -152,22 +174,30 @@ class Distro(distros.Distro): elif args and isinstance(args, list): cmd.extend(args) + if command == "upgrade": + command = "-u" if command: cmd.append(command) - pkglist = util.expand_package_list('%s-%s', pkgs) + pkglist = util.expand_package_list("%s-%s", pkgs) cmd.extend(pkglist) # Allow the output of this to flow outwards (ie not be captured) subp.subp(cmd, capture=False) def update_package_sources(self): - self._runner.run("update-sources", self.package_command, - ["-y"], freq=PER_INSTANCE) - - -def _render_network(entries, target="/", conf_dir="etc/netctl", - resolv_conf="etc/resolv.conf", enable_func=None): + self._runner.run( + "update-sources", self.package_command, ["-y"], freq=PER_INSTANCE + ) + + +def _render_network( + entries, + target="/", + conf_dir="etc/netctl", + resolv_conf="etc/resolv.conf", + enable_func=None, +): """Render the translate_network format into netctl files in target. Paths will be rendered under target. """ @@ -178,29 +208,27 @@ def _render_network(entries, target="/", conf_dir="etc/netctl", conf_dir = subp.target_path(target, conf_dir) for (dev, info) in entries.items(): - if dev == 'lo': + if dev == "lo": # no configuration should be rendered for 'lo' continue devs.append(dev) net_fn = os.path.join(conf_dir, dev) net_cfg = { - 'Connection': 'ethernet', - 'Interface': dev, - 'IP': info.get('bootproto'), - 'Address': "%s/%s" % (info.get('address'), - info.get('netmask')), - 'Gateway': info.get('gateway'), - 'DNS': info.get('dns-nameservers', []), + "Connection": "ethernet", + "Interface": dev, + "IP": info.get("bootproto"), + "Address": "%s/%s" % (info.get("address"), info.get("netmask")), + "Gateway": info.get("gateway"), + "DNS": info.get("dns-nameservers", []), } util.write_file(net_fn, convert_netctl(net_cfg)) - if enable_func and info.get('auto'): + if enable_func and info.get("auto"): enable_func(dev) - if 'dns-nameservers' in info: - nameservers.extend(info['dns-nameservers']) + if "dns-nameservers" in info: + nameservers.extend(info["dns-nameservers"]) if nameservers: - util.write_file(resolv_conf, - convert_resolv_conf(nameservers)) + util.write_file(resolv_conf, convert_resolv_conf(nameservers)) return devs @@ -217,17 +245,18 @@ def convert_netctl(settings): if val is None: val = "" elif isinstance(val, (tuple, list)): - val = "(" + ' '.join("'%s'" % v for v in val) + ")" + val = "(" + " ".join("'%s'" % v for v in val) + ")" result.append("%s=%s\n" % (key, val)) - return ''.join(result) + return "".join(result) def convert_resolv_conf(settings): """Returns a settings string formatted for resolv.conf.""" - result = '' + result = "" if isinstance(settings, list): for ns in settings: - result = result + 'nameserver %s\n' % ns + result = result + "nameserver %s\n" % ns return result + # vi: ts=4 expandtab diff --git a/cloudinit/distros/bsd.py b/cloudinit/distros/bsd.py index f717a667..1b4498b3 100644 --- a/cloudinit/distros/bsd.py +++ b/cloudinit/distros/bsd.py @@ -1,12 +1,10 @@ import platform -from cloudinit import distros -from cloudinit.distros import bsd_utils -from cloudinit import helpers +from cloudinit import distros, helpers from cloudinit import log as logging -from cloudinit import net -from cloudinit import subp -from cloudinit import util +from cloudinit import net, subp, util +from cloudinit.distros import bsd_utils + from .networking import BSDNetworking LOG = logging.getLogger(__name__) @@ -14,12 +12,12 @@ LOG = logging.getLogger(__name__) class BSD(distros.Distro): networking_cls = BSDNetworking - hostname_conf_fn = '/etc/rc.conf' + hostname_conf_fn = "/etc/rc.conf" rc_conf_fn = "/etc/rc.conf" # This differs from the parent Distro class, which has -P for # poweroff. - shutdown_options_map = {'halt': '-H', 'poweroff': '-p', 'reboot': '-r'} + shutdown_options_map = {"halt": "-H", "poweroff": "-p", "reboot": "-r"} # Set in BSD distro subclasses group_add_cmd_prefix = [] @@ -35,7 +33,7 @@ class BSD(distros.Distro): # calls from repeatly happening (when they # should only happen say once per instance...) self._runner = helpers.Runners(paths) - cfg['ssh_svcname'] = 'sshd' + cfg["ssh_svcname"] = "sshd" self.osfamily = platform.system().lower() def _read_system_hostname(self): @@ -43,13 +41,13 @@ class BSD(distros.Distro): return (self.hostname_conf_fn, sys_hostname) def _read_hostname(self, filename, default=None): - return bsd_utils.get_rc_config_value('hostname') + return bsd_utils.get_rc_config_value("hostname") def _get_add_member_to_group_cmd(self, member_name, group_name): - raise NotImplementedError('Return list cmd to add member to group') + raise NotImplementedError("Return list cmd to add member to group") def _write_hostname(self, hostname, filename): - bsd_utils.set_rc_config_value('hostname', hostname, fn='/etc/rc.conf') + bsd_utils.set_rc_config_value("hostname", hostname, fn="/etc/rc.conf") def create_group(self, name, members=None): if util.is_group(name): @@ -66,45 +64,55 @@ class BSD(distros.Distro): members = [] for member in members: if not util.is_user(member): - LOG.warning("Unable to add group member '%s' to group '%s'" - "; user does not exist.", member, name) + LOG.warning( + "Unable to add group member '%s' to group '%s'" + "; user does not exist.", + member, + name, + ) continue try: subp.subp(self._get_add_member_to_group_cmd(member, name)) LOG.info("Added user '%s' to group '%s'", member, name) except Exception: - util.logexc(LOG, "Failed to add user '%s' to group '%s'", - member, name) + util.logexc( + LOG, "Failed to add user '%s' to group '%s'", member, name + ) def generate_fallback_config(self): - nconf = {'config': [], 'version': 1} + nconf = {"config": [], "version": 1} for mac, name in net.get_interfaces_by_mac().items(): - nconf['config'].append( - {'type': 'physical', 'name': name, - 'mac_address': mac, 'subnets': [{'type': 'dhcp'}]}) + nconf["config"].append( + { + "type": "physical", + "name": name, + "mac_address": mac, + "subnets": [{"type": "dhcp"}], + } + ) return nconf def install_packages(self, pkglist): self.update_package_sources() - self.package_command('install', pkgs=pkglist) + self.package_command("install", pkgs=pkglist) def _get_pkg_cmd_environ(self): """Return environment vars used in *BSD package_command operations""" - raise NotImplementedError('BSD subclasses return a dict of env vars') + raise NotImplementedError("BSD subclasses return a dict of env vars") def package_command(self, command, args=None, pkgs=None): if pkgs is None: pkgs = [] - if command == 'install': + if command == "install": cmd = self.pkg_cmd_install_prefix - elif command == 'remove': + elif command == "remove": cmd = self.pkg_cmd_remove_prefix - elif command == 'update': + elif command == "update": if not self.pkg_cmd_update_prefix: return cmd = self.pkg_cmd_update_prefix - elif command == 'upgrade': + elif command == "upgrade": if not self.pkg_cmd_upgrade_prefix: return cmd = self.pkg_cmd_upgrade_prefix @@ -114,20 +122,17 @@ class BSD(distros.Distro): elif args and isinstance(args, list): cmd.extend(args) - pkglist = util.expand_package_list('%s-%s', pkgs) + pkglist = util.expand_package_list("%s-%s", pkgs) cmd.extend(pkglist) # Allow the output of this to flow outwards (ie not be captured) subp.subp(cmd, env=self._get_pkg_cmd_environ(), capture=False) - def _write_network_config(self, netconfig): - return self._supported_write_network_config(netconfig) - def set_timezone(self, tz): distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) def apply_locale(self, locale, out_fn=None): - LOG.debug('Cannot set the locale.') + LOG.debug("Cannot set the locale.") def apply_network_config_names(self, netconfig): - LOG.debug('Cannot rename network interface.') + LOG.debug("Cannot rename network interface.") diff --git a/cloudinit/distros/bsd_utils.py b/cloudinit/distros/bsd_utils.py index 079d0d53..00cd0662 100644 --- a/cloudinit/distros/bsd_utils.py +++ b/cloudinit/distros/bsd_utils.py @@ -18,31 +18,31 @@ def _unquote(value): return value -def get_rc_config_value(key, fn='/etc/rc.conf'): - key_prefix = '{}='.format(key) +def get_rc_config_value(key, fn="/etc/rc.conf"): + key_prefix = "{}=".format(key) for line in util.load_file(fn).splitlines(): if line.startswith(key_prefix): - value = line.replace(key_prefix, '') + value = line.replace(key_prefix, "") return _unquote(value) -def set_rc_config_value(key, value, fn='/etc/rc.conf'): +def set_rc_config_value(key, value, fn="/etc/rc.conf"): lines = [] done = False value = shlex.quote(value) original_content = util.load_file(fn) for line in original_content.splitlines(): - if '=' in line: - k, v = line.split('=', 1) + if "=" in line: + k, v = line.split("=", 1) if k == key: v = value done = True - lines.append('='.join([k, v])) + lines.append("=".join([k, v])) else: lines.append(line) if not done: - lines.append('='.join([key, value])) - new_content = '\n'.join(lines) + '\n' + lines.append("=".join([key, value])) + new_content = "\n".join(lines) + "\n" if new_content != original_content: util.write_file(fn, new_content) diff --git a/cloudinit/distros/centos.py b/cloudinit/distros/centos.py index edb3165d..3dc0a342 100644 --- a/cloudinit/distros/centos.py +++ b/cloudinit/distros/centos.py @@ -6,4 +6,5 @@ from cloudinit.distros import rhel class Distro(rhel.Distro): pass + # vi: ts=4 expandtab diff --git a/cloudinit/distros/cloudlinux.py b/cloudinit/distros/cloudlinux.py new file mode 100644 index 00000000..3dc0a342 --- /dev/null +++ b/cloudinit/distros/cloudlinux.py @@ -0,0 +1,10 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + + +# vi: ts=4 expandtab diff --git a/cloudinit/distros/debian.py b/cloudinit/distros/debian.py index 844aaf21..6dc1ad40 100644 --- a/cloudinit/distros/debian.py +++ b/cloudinit/distros/debian.py @@ -7,27 +7,29 @@ # Author: Joshua Harlow <harlowja@yahoo-inc.com> # # This file is part of cloud-init. See LICENSE file for license information. - +import fcntl import os +import time -from cloudinit import distros -from cloudinit import helpers +from cloudinit import distros, helpers from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util - +from cloudinit import subp, util from cloudinit.distros.parsers.hostname import HostnameConf - from cloudinit.settings import PER_INSTANCE LOG = logging.getLogger(__name__) -APT_GET_COMMAND = ('apt-get', '--option=Dpkg::Options::=--force-confold', - '--option=Dpkg::options::=--force-unsafe-io', - '--assume-yes', '--quiet') +APT_LOCK_WAIT_TIMEOUT = 30 +APT_GET_COMMAND = ( + "apt-get", + "--option=Dpkg::Options::=--force-confold", + "--option=Dpkg::options::=--force-unsafe-io", + "--assume-yes", + "--quiet", +) APT_GET_WRAPPER = { - 'command': 'eatmydata', - 'enabled': 'auto', + "command": "eatmydata", + "enabled": "auto", } NETWORK_FILE_HEADER = """\ @@ -41,19 +43,36 @@ NETWORK_FILE_HEADER = """\ NETWORK_CONF_FN = "/etc/network/interfaces.d/50-cloud-init" LOCALE_CONF_FN = "/etc/default/locale" +# The frontend lock needs to be acquired first followed by the order that +# apt uses. /var/lib/apt/lists is locked independently of that install chain, +# and only locked during update, so you can acquire it either order. +# Also update does not acquire the dpkg frontend lock. +# More context: +# https://github.com/canonical/cloud-init/pull/1034#issuecomment-986971376 +APT_LOCK_FILES = [ + "/var/lib/dpkg/lock-frontend", + "/var/lib/dpkg/lock", + "/var/cache/apt/archives/lock", + "/var/lib/apt/lists/lock", +] + class Distro(distros.Distro): hostname_conf_fn = "/etc/hostname" network_conf_fn = { "eni": "/etc/network/interfaces.d/50-cloud-init", - "netplan": "/etc/netplan/50-cloud-init.yaml" + "netplan": "/etc/netplan/50-cloud-init.yaml", } renderer_configs = { - "eni": {"eni_path": network_conf_fn["eni"], - "eni_header": NETWORK_FILE_HEADER}, - "netplan": {"netplan_path": network_conf_fn["netplan"], - "netplan_header": NETWORK_FILE_HEADER, - "postcmds": True} + "eni": { + "eni_path": network_conf_fn["eni"], + "eni_header": NETWORK_FILE_HEADER, + }, + "netplan": { + "netplan_path": network_conf_fn["netplan"], + "netplan_header": NETWORK_FILE_HEADER, + "postcmds": True, + }, } def __init__(self, name, cfg, paths): @@ -62,8 +81,8 @@ class Distro(distros.Distro): # calls from repeatly happening (when they # should only happen say once per instance...) self._runner = helpers.Runners(paths) - self.osfamily = 'debian' - self.default_locale = 'en_US.UTF-8' + self.osfamily = "debian" + self.default_locale = "en_US.UTF-8" self.system_locale = None def get_locale(self): @@ -74,25 +93,29 @@ class Distro(distros.Distro): self.system_locale = read_system_locale() # Return system_locale setting if valid, else use default locale - return (self.system_locale if self.system_locale else - self.default_locale) + return ( + self.system_locale if self.system_locale else self.default_locale + ) - def apply_locale(self, locale, out_fn=None, keyname='LANG'): + def apply_locale(self, locale, out_fn=None, keyname="LANG"): """Apply specified locale to system, regenerate if specified locale - differs from system default.""" + differs from system default.""" if not out_fn: out_fn = LOCALE_CONF_FN if not locale: - raise ValueError('Failed to provide locale value.') + raise ValueError("Failed to provide locale value.") # Only call locale regeneration if needed # Update system locale config with specified locale if needed distro_locale = self.get_locale() conf_fn_exists = os.path.exists(out_fn) sys_locale_unset = False if self.system_locale else True - need_regen = (locale.lower() != distro_locale.lower() or - not conf_fn_exists or sys_locale_unset) + need_regen = ( + locale.lower() != distro_locale.lower() + or not conf_fn_exists + or sys_locale_unset + ) need_conf = not conf_fn_exists or need_regen or sys_locale_unset if need_regen: @@ -100,7 +123,10 @@ class Distro(distros.Distro): else: LOG.debug( "System has '%s=%s' requested '%s', skipping regeneration.", - keyname, self.system_locale, locale) + keyname, + self.system_locale, + locale, + ) if need_conf: update_locale_conf(locale, out_fn, keyname=keyname) @@ -109,34 +135,24 @@ class Distro(distros.Distro): def install_packages(self, pkglist): self.update_package_sources() - self.package_command('install', pkgs=pkglist) + self.package_command("install", pkgs=pkglist) - def _write_network_config(self, netconfig): + def _write_network_state(self, network_state): _maybe_remove_legacy_eth0() - return self._supported_write_network_config(netconfig) - - def _bring_up_interfaces(self, device_names): - use_all = False - for d in device_names: - if d == 'all': - use_all = True - if use_all: - return distros.Distro._bring_up_interface(self, '--all') - else: - return distros.Distro._bring_up_interfaces(self, device_names) + return super()._write_network_state(network_state) - def _write_hostname(self, your_hostname, out_fn): + def _write_hostname(self, hostname, filename): conf = None try: # Try to update the previous one # so lets see if we can read it first. - conf = self._read_hostname_conf(out_fn) + conf = self._read_hostname_conf(filename) except IOError: pass if not conf: - conf = HostnameConf('') - conf.set_hostname(your_hostname) - util.write_file(out_fn, str(conf), 0o644) + conf = HostnameConf("") + conf.set_hostname(hostname) + util.write_file(filename, str(conf), 0o644) def _read_system_hostname(self): sys_hostname = self._read_hostname(self.hostname_conf_fn) @@ -165,18 +181,90 @@ class Distro(distros.Distro): def set_timezone(self, tz): distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) + def _apt_lock_available(self, lock_files=None): + """Determines if another process holds any apt locks. + + If all locks are clear, return True else False. + """ + if lock_files is None: + lock_files = APT_LOCK_FILES + for lock in lock_files: + if not os.path.exists(lock): + # Only wait for lock files that already exist + continue + with open(lock, "w") as handle: + try: + fcntl.lockf(handle, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError: + return False + return True + + def _wait_for_apt_command( + self, short_cmd, subp_kwargs, timeout=APT_LOCK_WAIT_TIMEOUT + ): + """Wait for apt install to complete. + + short_cmd: Name of command like "upgrade" or "install" + subp_kwargs: kwargs to pass to subp + """ + start_time = time.time() + LOG.debug("Waiting for apt lock") + while time.time() - start_time < timeout: + if not self._apt_lock_available(): + time.sleep(1) + continue + LOG.debug("apt lock available") + try: + # Allow the output of this to flow outwards (not be captured) + log_msg = "apt-%s [%s]" % ( + short_cmd, + " ".join(subp_kwargs["args"]), + ) + return util.log_time( + logfunc=LOG.debug, + msg=log_msg, + func=subp.subp, + kwargs=subp_kwargs, + ) + except subp.ProcessExecutionError: + # Even though we have already waited for the apt lock to be + # available, it is possible that the lock was acquired by + # another process since the check. Since apt doesn't provide + # a meaningful error code to check and checking the error + # text is fragile and subject to internationalization, we + # can instead check the apt lock again. If the apt lock is + # still available, given the length of an average apt + # transaction, it is extremely unlikely that another process + # raced us when we tried to acquire it, so raise the apt + # error received. If the lock is unavailable, just keep waiting + if self._apt_lock_available(): + raise + LOG.debug("Another process holds apt lock. Waiting...") + time.sleep(1) + raise TimeoutError("Could not get apt lock") + def package_command(self, command, args=None, pkgs=None): + """Run the given package command. + + On Debian, this will run apt-get (unless APT_GET_COMMAND is set). + + command: The command to run, like "upgrade" or "install" + args: Arguments passed to apt itself in addition to + any specified in APT_GET_COMMAND + pkgs: Apt packages that the command will apply to + """ if pkgs is None: pkgs = [] e = os.environ.copy() - # See: http://manpages.ubuntu.com/manpages/xenial/man7/debconf.7.html - e['DEBIAN_FRONTEND'] = 'noninteractive' + # See: http://manpages.ubuntu.com/manpages/bionic/man7/debconf.7.html + e["DEBIAN_FRONTEND"] = "noninteractive" wcfg = self.get_option("apt_get_wrapper", APT_GET_WRAPPER) cmd = _get_wrapper_prefix( - wcfg.get('command', APT_GET_WRAPPER['command']), - wcfg.get('enabled', APT_GET_WRAPPER['enabled'])) + wcfg.get("command", APT_GET_WRAPPER["command"]), + wcfg.get("enabled", APT_GET_WRAPPER["enabled"]), + ) cmd.extend(list(self.get_option("apt_get_command", APT_GET_COMMAND))) @@ -187,35 +275,46 @@ class Distro(distros.Distro): subcmd = command if command == "upgrade": - subcmd = self.get_option("apt_get_upgrade_subcommand", - "dist-upgrade") + subcmd = self.get_option( + "apt_get_upgrade_subcommand", "dist-upgrade" + ) cmd.append(subcmd) - pkglist = util.expand_package_list('%s=%s', pkgs) + pkglist = util.expand_package_list("%s=%s", pkgs) cmd.extend(pkglist) - # Allow the output of this to flow outwards (ie not be captured) - util.log_time(logfunc=LOG.debug, - msg="apt-%s [%s]" % (command, ' '.join(cmd)), - func=subp.subp, - args=(cmd,), kwargs={'env': e, 'capture': False}) + self._wait_for_apt_command( + short_cmd=command, + subp_kwargs={"args": cmd, "env": e, "capture": False}, + ) def update_package_sources(self): - self._runner.run("update-sources", self.package_command, - ["update"], freq=PER_INSTANCE) + self._runner.run( + "update-sources", + self.package_command, + ["update"], + freq=PER_INSTANCE, + ) def get_primary_arch(self): return util.get_dpkg_architecture() + def set_keymap(self, layout, model, variant, options): + # Let localectl take care of updating /etc/default/keyboard + distros.Distro.set_keymap(self, layout, model, variant, options) + # Workaround for localectl not applying new settings instantly + # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=926037 + self.manage_service("restart", "console-setup") + def _get_wrapper_prefix(cmd, mode): if isinstance(cmd, str): cmd = [str(cmd)] - if (util.is_true(mode) or - (str(mode).lower() == "auto" and cmd[0] and - subp.which(cmd[0]))): + if util.is_true(mode) or ( + str(mode).lower() == "auto" and cmd[0] and subp.which(cmd[0]) + ): return cmd else: return [] @@ -223,13 +322,13 @@ def _get_wrapper_prefix(cmd, mode): def _maybe_remove_legacy_eth0(path="/etc/network/interfaces.d/eth0.cfg"): """Ubuntu cloud images previously included a 'eth0.cfg' that had - hard coded content. That file would interfere with the rendered - configuration if it was present. + hard coded content. That file would interfere with the rendered + configuration if it was present. - if the file does not exist do nothing. - If the file exists: - - with known content, remove it and warn - - with unknown content, leave it and warn + if the file does not exist do nothing. + If the file exists: + - with known content, remove it and warn + - with unknown content, leave it and warn """ if not os.path.exists(path): @@ -239,24 +338,25 @@ def _maybe_remove_legacy_eth0(path="/etc/network/interfaces.d/eth0.cfg"): try: contents = util.load_file(path) known_contents = ["auto eth0", "iface eth0 inet dhcp"] - lines = [f.strip() for f in contents.splitlines() - if not f.startswith("#")] + lines = [ + f.strip() for f in contents.splitlines() if not f.startswith("#") + ] if lines == known_contents: util.del_file(path) msg = "removed %s with known contents" % path else: - msg = (bmsg + " '%s' exists with user configured content." % path) + msg = bmsg + " '%s' exists with user configured content." % path except Exception: msg = bmsg + " %s exists, but could not be read." % path LOG.warning(msg) -def read_system_locale(sys_path=LOCALE_CONF_FN, keyname='LANG'): +def read_system_locale(sys_path=LOCALE_CONF_FN, keyname="LANG"): """Read system default locale setting, if present""" sys_val = "" if not sys_path: - raise ValueError('Invalid path: %s' % sys_path) + raise ValueError("Invalid path: %s" % sys_path) if os.path.exists(sys_path): locale_content = util.load_file(sys_path) @@ -266,16 +366,22 @@ def read_system_locale(sys_path=LOCALE_CONF_FN, keyname='LANG'): return sys_val -def update_locale_conf(locale, sys_path, keyname='LANG'): +def update_locale_conf(locale, sys_path, keyname="LANG"): """Update system locale config""" - LOG.debug('Updating %s with locale setting %s=%s', - sys_path, keyname, locale) + LOG.debug( + "Updating %s with locale setting %s=%s", sys_path, keyname, locale + ) subp.subp( - ['update-locale', '--locale-file=' + sys_path, - '%s=%s' % (keyname, locale)], capture=False) + [ + "update-locale", + "--locale-file=" + sys_path, + "%s=%s" % (keyname, locale), + ], + capture=False, + ) -def regenerate_locale(locale, sys_path, keyname='LANG'): +def regenerate_locale(locale, sys_path, keyname="LANG"): """ Run locale-gen for the provided locale and set the default system variable `keyname` appropriately in the provided `sys_path`. @@ -286,13 +392,13 @@ def regenerate_locale(locale, sys_path, keyname='LANG'): # C # C.UTF-8 # POSIX - if locale.lower() in ['c', 'c.utf-8', 'posix']: - LOG.debug('%s=%s does not require rengeneration', keyname, locale) + if locale.lower() in ["c", "c.utf-8", "posix"]: + LOG.debug("%s=%s does not require rengeneration", keyname, locale) return # finally, trigger regeneration - LOG.debug('Generating locales for %s', locale) - subp.subp(['locale-gen', locale], capture=False) + LOG.debug("Generating locales for %s", locale) + subp.subp(["locale-gen", locale], capture=False) # vi: ts=4 expandtab diff --git a/cloudinit/distros/dragonflybsd.py b/cloudinit/distros/dragonflybsd.py new file mode 100644 index 00000000..0d02bee0 --- /dev/null +++ b/cloudinit/distros/dragonflybsd.py @@ -0,0 +1,12 @@ +# Copyright (C) 2020-2021 Gonéri Le Bouder +# +# This file is part of cloud-init. See LICENSE file for license information. + +import cloudinit.distros.freebsd + + +class Distro(cloudinit.distros.freebsd.Distro): + home_dir = "/home" + + +# vi: ts=4 expandtab diff --git a/cloudinit/distros/eurolinux.py b/cloudinit/distros/eurolinux.py new file mode 100644 index 00000000..3dc0a342 --- /dev/null +++ b/cloudinit/distros/eurolinux.py @@ -0,0 +1,10 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + + +# vi: ts=4 expandtab diff --git a/cloudinit/distros/fedora.py b/cloudinit/distros/fedora.py index 0fe1fbca..39203225 100644 --- a/cloudinit/distros/fedora.py +++ b/cloudinit/distros/fedora.py @@ -14,4 +14,5 @@ from cloudinit.distros import rhel class Distro(rhel.Distro): pass + # vi: ts=4 expandtab diff --git a/cloudinit/distros/freebsd.py b/cloudinit/distros/freebsd.py index dde34d41..513abdc2 100644 --- a/cloudinit/distros/freebsd.py +++ b/cloudinit/distros/freebsd.py @@ -10,53 +10,54 @@ from io import StringIO import cloudinit.distros.bsd from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util +from cloudinit import subp, util from cloudinit.settings import PER_INSTANCE LOG = logging.getLogger(__name__) class Distro(cloudinit.distros.bsd.BSD): - usr_lib_exec = '/usr/local/lib' - login_conf_fn = '/etc/login.conf' - login_conf_fn_bak = '/etc/login.conf.orig' - ci_sudoers_fn = '/usr/local/etc/sudoers.d/90-cloud-init-users' - group_add_cmd_prefix = ['pw', 'group', 'add'] + """ + Distro subclass for FreeBSD. + + (N.B. DragonFlyBSD inherits from this class.) + """ + + usr_lib_exec = "/usr/local/lib" + login_conf_fn = "/etc/login.conf" + login_conf_fn_bak = "/etc/login.conf.orig" + ci_sudoers_fn = "/usr/local/etc/sudoers.d/90-cloud-init-users" + group_add_cmd_prefix = ["pw", "group", "add"] pkg_cmd_install_prefix = ["pkg", "install"] pkg_cmd_remove_prefix = ["pkg", "remove"] pkg_cmd_update_prefix = ["pkg", "update"] pkg_cmd_upgrade_prefix = ["pkg", "upgrade"] - - def _select_hostname(self, hostname, fqdn): - # Should be FQDN if available. See rc.conf(5) in FreeBSD - if fqdn: - return fqdn - return hostname + prefer_fqdn = True # See rc.conf(5) in FreeBSD + home_dir = "/usr/home" def _get_add_member_to_group_cmd(self, member_name, group_name): - return ['pw', 'usermod', '-n', member_name, '-G', group_name] + return ["pw", "usermod", "-n", member_name, "-G", group_name] def add_user(self, name, **kwargs): if util.is_user(name): LOG.info("User %s already exists, skipping.", name) return False - pw_useradd_cmd = ['pw', 'useradd', '-n', name] - log_pw_useradd_cmd = ['pw', 'useradd', '-n', name] + pw_useradd_cmd = ["pw", "useradd", "-n", name] + log_pw_useradd_cmd = ["pw", "useradd", "-n", name] pw_useradd_opts = { - "homedir": '-d', - "gecos": '-c', - "primary_group": '-g', - "groups": '-G', - "shell": '-s', - "inactive": '-E', + "homedir": "-d", + "gecos": "-c", + "primary_group": "-g", + "groups": "-G", + "shell": "-s", + "inactive": "-E", } pw_useradd_flags = { - "no_user_group": '--no-user-group', - "system": '--system', - "no_log_init": '--no-log-init', + "no_user_group": "--no-user-group", + "system": "--system", + "no_log_init": "--no-log-init", } for key, val in kwargs.items(): @@ -67,14 +68,19 @@ class Distro(cloudinit.distros.bsd.BSD): pw_useradd_cmd.append(pw_useradd_flags[key]) log_pw_useradd_cmd.append(pw_useradd_flags[key]) - if 'no_create_home' in kwargs or 'system' in kwargs: - pw_useradd_cmd.append('-d/nonexistent') - log_pw_useradd_cmd.append('-d/nonexistent') + if "no_create_home" in kwargs or "system" in kwargs: + pw_useradd_cmd.append("-d/nonexistent") + log_pw_useradd_cmd.append("-d/nonexistent") else: - pw_useradd_cmd.append('-d/usr/home/%s' % name) - pw_useradd_cmd.append('-m') - log_pw_useradd_cmd.append('-d/usr/home/%s' % name) - log_pw_useradd_cmd.append('-m') + pw_useradd_cmd.append( + "-d{home_dir}/{name}".format(home_dir=self.home_dir, name=name) + ) + pw_useradd_cmd.append("-m") + log_pw_useradd_cmd.append( + "-d{home_dir}/{name}".format(home_dir=self.home_dir, name=name) + ) + + log_pw_useradd_cmd.append("-m") # Run the command LOG.info("Adding user %s", name) @@ -85,13 +91,13 @@ class Distro(cloudinit.distros.bsd.BSD): raise # Set the password if it is provided # For security consideration, only hashed passwd is assumed - passwd_val = kwargs.get('passwd', None) + 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(['pw', 'usermod', user, '-p', '01-Jan-1970']) + subp.subp(["pw", "usermod", user, "-p", "01-Jan-1970"]) except Exception: util.logexc(LOG, "Failed to set pw expiration for %s", user) raise @@ -103,15 +109,18 @@ class Distro(cloudinit.distros.bsd.BSD): hash_opt = "-h" try: - subp.subp(['pw', 'usermod', user, hash_opt, '0'], - data=passwd, logstring="chpasswd for %s" % user) + subp.subp( + ["pw", "usermod", user, hash_opt, "0"], + data=passwd, + logstring="chpasswd for %s" % user, + ) except Exception: util.logexc(LOG, "Failed to set password for %s", user) raise def lock_passwd(self, name): try: - subp.subp(['pw', 'usermod', name, '-h', '-']) + subp.subp(["pw", "usermod", name, "-h", "-"]) except Exception: util.logexc(LOG, "Failed to lock user %s", name) raise @@ -120,8 +129,9 @@ class Distro(cloudinit.distros.bsd.BSD): # Adjust the locales value to the new value newconf = StringIO() for line in util.load_file(self.login_conf_fn).splitlines(): - newconf.write(re.sub(r'^default:', - r'default:lang=%s:' % locale, line)) + newconf.write( + re.sub(r"^default:", r"default:lang=%s:" % locale, line) + ) newconf.write("\n") # Make a backup of login.conf. @@ -132,15 +142,16 @@ class Distro(cloudinit.distros.bsd.BSD): try: LOG.debug("Running cap_mkdb for %s", locale) - subp.subp(['cap_mkdb', self.login_conf_fn]) + subp.subp(["cap_mkdb", self.login_conf_fn]) except subp.ProcessExecutionError: # cap_mkdb failed, so restore the backup. util.logexc(LOG, "Failed to apply locale %s", locale) try: util.copy(self.login_conf_fn_bak, self.login_conf_fn) except IOError: - util.logexc(LOG, "Failed to restore %s backup", - self.login_conf_fn) + util.logexc( + LOG, "Failed to restore %s backup", self.login_conf_fn + ) def apply_network_config_names(self, netconfig): # This is handled by the freebsd network renderer. It writes in @@ -152,12 +163,16 @@ class Distro(cloudinit.distros.bsd.BSD): def _get_pkg_cmd_environ(self): """Return environment vars used in *BSD package_command operations""" e = os.environ.copy() - e['ASSUME_ALWAYS_YES'] = 'YES' + e["ASSUME_ALWAYS_YES"] = "YES" return e def update_package_sources(self): self._runner.run( - "update-sources", self.package_command, - ["update"], freq=PER_INSTANCE) + "update-sources", + self.package_command, + ["update"], + freq=PER_INSTANCE, + ) + # vi: ts=4 expandtab diff --git a/cloudinit/distros/gentoo.py b/cloudinit/distros/gentoo.py index e9b82602..4eb76da8 100644 --- a/cloudinit/distros/gentoo.py +++ b/cloudinit/distros/gentoo.py @@ -6,26 +6,27 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import distros -from cloudinit import helpers +from cloudinit import distros, helpers from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util - +from cloudinit import subp, util from cloudinit.distros import net_util from cloudinit.distros.parsers.hostname import HostnameConf - from cloudinit.settings import PER_INSTANCE LOG = logging.getLogger(__name__) class Distro(distros.Distro): - locale_conf_fn = '/etc/locale.gen' - network_conf_fn = '/etc/conf.d/net' - resolve_conf_fn = '/etc/resolv.conf' - hostname_conf_fn = '/etc/conf.d/hostname' - init_cmd = ['rc-service'] # init scripts + locale_conf_fn = "/etc/env.d/02locale" + locale_gen_fn = "/etc/locale.gen" + network_conf_fn = "/etc/conf.d/net" + hostname_conf_fn = "/etc/conf.d/hostname" + init_cmd = ["rc-service"] # init scripts + default_locale = "en_US.UTF-8" + + # C.UTF8 makes sense to generate, but is not selected + # Add /etc/locale.gen entries to this list to support more locales + locales = ["C.UTF8 UTF-8", "en_US.UTF-8 UTF-8"] def __init__(self, name, cfg, paths): distros.Distro.__init__(self, name, cfg, paths) @@ -33,97 +34,121 @@ class Distro(distros.Distro): # calls from repeatly happening (when they # should only happen say once per instance...) self._runner = helpers.Runners(paths) - self.osfamily = 'gentoo' + self.osfamily = "gentoo" # Fix sshd restarts - cfg['ssh_svcname'] = '/etc/init.d/sshd' - - def apply_locale(self, locale, out_fn=None): - if not out_fn: - out_fn = self.locale_conf_fn - subp.subp(['locale-gen', '-G', locale], capture=False) - # "" provides trailing newline during join - lines = [ - util.make_header(), - 'LANG="%s"' % locale, - "", - ] - util.write_file(out_fn, "\n".join(lines)) + cfg["ssh_svcname"] = "/etc/init.d/sshd" + if distros.uses_systemd(): + LOG.error("Cloud-init does not support systemd with gentoo") + + def apply_locale(self, _, out_fn=None): + """rc-only - not compatible with systemd + + Locales need to be added to /etc/locale.gen and generated prior + to selection. Default to en_US.UTF-8 for simplicity. + """ + util.write_file(self.locale_gen_fn, "\n".join(self.locales), mode=644) + + # generate locales + subp.subp(["locale-gen"], capture=False) + + # select locale + subp.subp( + ["eselect", "locale", "set", self.default_locale], capture=False + ) def install_packages(self, pkglist): self.update_package_sources() - self.package_command('', pkgs=pkglist) + self.package_command("", pkgs=pkglist) def _write_network(self, settings): entries = net_util.translate_network(settings) - LOG.debug("Translated ubuntu style network settings %s into %s", - settings, entries) + LOG.debug( + "Translated ubuntu style network settings %s into %s", + settings, + entries, + ) dev_names = entries.keys() nameservers = [] for (dev, info) in entries.items(): - if 'dns-nameservers' in info: - nameservers.extend(info['dns-nameservers']) - if dev == 'lo': + if "dns-nameservers" in info: + nameservers.extend(info["dns-nameservers"]) + if dev == "lo": continue - net_fn = self.network_conf_fn + '.' + dev - dns_nameservers = info.get('dns-nameservers') + net_fn = self.network_conf_fn + "." + dev + dns_nameservers = info.get("dns-nameservers") if isinstance(dns_nameservers, (list, tuple)): - dns_nameservers = str(tuple(dns_nameservers)).replace(',', '') + dns_nameservers = str(tuple(dns_nameservers)).replace(",", "") # eth0, {'auto': True, 'ipv6': {}, 'bootproto': 'dhcp'} # lo, {'dns-nameservers': ['10.0.1.3'], 'ipv6': {}, 'auto': True} - results = '' - if info.get('bootproto') == 'dhcp': + results = "" + if info.get("bootproto") == "dhcp": results += 'config_{name}="dhcp"'.format(name=dev) else: results += ( 'config_{name}="{ip_address} netmask {netmask}"\n' 'mac_{name}="{hwaddr}"\n' - ).format(name=dev, ip_address=info.get('address'), - netmask=info.get('netmask'), - hwaddr=info.get('hwaddress')) - results += 'routes_{name}="default via {gateway}"\n'.format( + ).format( name=dev, - gateway=info.get('gateway') + ip_address=info.get("address"), + netmask=info.get("netmask"), + hwaddr=info.get("hwaddress"), ) - if info.get('dns-nameservers'): + results += 'routes_{name}="default via {gateway}"\n'.format( + name=dev, gateway=info.get("gateway") + ) + if info.get("dns-nameservers"): results += 'dns_servers_{name}="{dnsservers}"\n'.format( - name=dev, - dnsservers=dns_nameservers) + name=dev, dnsservers=dns_nameservers + ) util.write_file(net_fn, results) self._create_network_symlink(dev) - if info.get('auto'): - cmd = ['rc-update', 'add', 'net.{name}'.format(name=dev), - 'default'] + if info.get("auto"): + cmd = [ + "rc-update", + "add", + "net.{name}".format(name=dev), + "default", + ] try: (_out, err) = subp.subp(cmd) if len(err): - LOG.warning("Running %s resulted in stderr output: %s", - cmd, err) + LOG.warning( + "Running %s resulted in stderr output: %s", + cmd, + err, + ) except subp.ProcessExecutionError: - util.logexc(LOG, "Running interface command %s failed", - cmd) + util.logexc( + LOG, "Running interface command %s failed", cmd + ) if nameservers: - util.write_file(self.resolve_conf_fn, - convert_resolv_conf(nameservers)) + util.write_file( + self.resolve_conf_fn, convert_resolv_conf(nameservers) + ) return dev_names @staticmethod def _create_network_symlink(interface_name): - file_path = '/etc/init.d/net.{name}'.format(name=interface_name) + file_path = "/etc/init.d/net.{name}".format(name=interface_name) if not util.is_link(file_path): - util.sym_link('/etc/init.d/net.lo', file_path) + util.sym_link("/etc/init.d/net.lo", file_path) def _bring_up_interface(self, device_name): - cmd = ['/etc/init.d/net.%s' % device_name, 'restart'] - LOG.debug("Attempting to run bring up interface %s using command %s", - device_name, cmd) + cmd = ["/etc/init.d/net.%s" % device_name, "restart"] + LOG.debug( + "Attempting to run bring up interface %s using command %s", + device_name, + cmd, + ) try: (_out, err) = subp.subp(cmd) if len(err): - LOG.warning("Running %s resulted in stderr output: %s", - cmd, err) + LOG.warning( + "Running %s resulted in stderr output: %s", cmd, err + ) return True except subp.ProcessExecutionError: util.logexc(LOG, "Running interface command %s failed", cmd) @@ -132,40 +157,41 @@ class Distro(distros.Distro): def _bring_up_interfaces(self, device_names): use_all = False for d in device_names: - if d == 'all': + if d == "all": use_all = True if use_all: # Grab device names from init scripts - cmd = ['ls', '/etc/init.d/net.*'] + cmd = ["ls", "/etc/init.d/net.*"] try: (_out, err) = subp.subp(cmd) if len(err): - LOG.warning("Running %s resulted in stderr output: %s", - cmd, err) + LOG.warning( + "Running %s resulted in stderr output: %s", cmd, err + ) except subp.ProcessExecutionError: util.logexc(LOG, "Running interface command %s failed", cmd) return False - devices = [x.split('.')[2] for x in _out.split(' ')] + devices = [x.split(".")[2] for x in _out.split(" ")] return distros.Distro._bring_up_interfaces(self, devices) else: return distros.Distro._bring_up_interfaces(self, device_names) - def _write_hostname(self, your_hostname, out_fn): + def _write_hostname(self, hostname, filename): conf = None try: # Try to update the previous one # so lets see if we can read it first. - conf = self._read_hostname_conf(out_fn) + conf = self._read_hostname_conf(filename) except IOError: pass if not conf: - conf = HostnameConf('') + conf = HostnameConf("") # Many distro's format is the hostname by itself, and that is the # way HostnameConf works but gentoo expects it to be in # hostname="the-actual-hostname" - conf.set_hostname('hostname="%s"' % your_hostname) - util.write_file(out_fn, str(conf), 0o644) + conf.set_hostname('hostname="%s"' % hostname) + util.write_file(filename, str(conf), 0o644) def _read_system_hostname(self): sys_hostname = self._read_hostname(self.hostname_conf_fn) @@ -195,7 +221,7 @@ class Distro(distros.Distro): if pkgs is None: pkgs = [] - cmd = list('emerge') + cmd = list("emerge") # Redirect output cmd.append("--quiet") @@ -207,23 +233,28 @@ class Distro(distros.Distro): if command: cmd.append(command) - pkglist = util.expand_package_list('%s-%s', pkgs) + pkglist = util.expand_package_list("%s-%s", pkgs) cmd.extend(pkglist) # Allow the output of this to flow outwards (ie not be captured) subp.subp(cmd, capture=False) def update_package_sources(self): - self._runner.run("update-sources", self.package_command, - ["-u", "world"], freq=PER_INSTANCE) + self._runner.run( + "update-sources", + self.package_command, + ["-u", "world"], + freq=PER_INSTANCE, + ) def convert_resolv_conf(settings): """Returns a settings string formatted for resolv.conf.""" - result = '' + result = "" if isinstance(settings, list): for ns in settings: - result += 'nameserver %s\n' % ns + result += "nameserver %s\n" % ns return result + # vi: ts=4 expandtab diff --git a/cloudinit/distros/miraclelinux.py b/cloudinit/distros/miraclelinux.py new file mode 100644 index 00000000..3dc0a342 --- /dev/null +++ b/cloudinit/distros/miraclelinux.py @@ -0,0 +1,10 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + + +# vi: ts=4 expandtab diff --git a/cloudinit/distros/net_util.py b/cloudinit/distros/net_util.py index edfcd99d..e37fb19b 100644 --- a/cloudinit/distros/net_util.py +++ b/cloudinit/distros/net_util.py @@ -68,7 +68,9 @@ # } from cloudinit.net.network_state import ( - net_prefix_to_ipv4_mask, mask_and_ipv4_to_bcast_addr) + mask_and_ipv4_to_bcast_addr, + net_prefix_to_ipv4_mask, +) def translate_network(settings): @@ -86,7 +88,7 @@ def translate_network(settings): ifaces = [] consume = {} for (cmd, args) in entries: - if cmd == 'iface': + if cmd == "iface": if consume: ifaces.append(consume) consume = {} @@ -96,19 +98,19 @@ def translate_network(settings): # Check if anything left over to consume absorb = False for (cmd, args) in consume.items(): - if cmd == 'iface': + if cmd == "iface": absorb = True if absorb: ifaces.append(consume) # Now translate real_ifaces = {} for info in ifaces: - if 'iface' not in info: + if "iface" not in info: continue - iface_details = info['iface'].split(None) + iface_details = info["iface"].split(None) # Check if current device *may* have an ipv6 IP use_ipv6 = False - if 'inet6' in iface_details: + if "inet6" in iface_details: use_ipv6 = True dev_name = None if len(iface_details) >= 1: @@ -118,55 +120,54 @@ def translate_network(settings): if not dev_name: continue iface_info = {} - iface_info['ipv6'] = {} + iface_info["ipv6"] = {} if len(iface_details) >= 3: proto_type = iface_details[2].strip().lower() # Seems like this can be 'loopback' which we don't # really care about - if proto_type in ['dhcp', 'static']: - iface_info['bootproto'] = proto_type + if proto_type in ["dhcp", "static"]: + iface_info["bootproto"] = proto_type # These can just be copied over if use_ipv6: - for k in ['address', 'gateway']: + for k in ["address", "gateway"]: if k in info: val = info[k].strip().lower() if val: - iface_info['ipv6'][k] = val + iface_info["ipv6"][k] = val else: - for k in ['netmask', 'address', 'gateway', 'broadcast']: + for k in ["netmask", "address", "gateway", "broadcast"]: if k in info: val = info[k].strip().lower() if val: iface_info[k] = val # handle static ip configurations using # ipaddress/prefix-length format - if 'address' in iface_info: - if 'netmask' not in iface_info: + if "address" in iface_info: + if "netmask" not in iface_info: # check if the address has a network prefix - addr, _, prefix = iface_info['address'].partition('/') + addr, _, prefix = iface_info["address"].partition("/") if prefix: - iface_info['netmask'] = ( - net_prefix_to_ipv4_mask(prefix)) - iface_info['address'] = addr + iface_info["netmask"] = net_prefix_to_ipv4_mask(prefix) + iface_info["address"] = addr # if we set the netmask, we also can set the broadcast - iface_info['broadcast'] = ( - mask_and_ipv4_to_bcast_addr( - iface_info['netmask'], addr)) + iface_info["broadcast"] = mask_and_ipv4_to_bcast_addr( + iface_info["netmask"], addr + ) # Name server info provided?? - if 'dns-nameservers' in info: - iface_info['dns-nameservers'] = info['dns-nameservers'].split() + if "dns-nameservers" in info: + iface_info["dns-nameservers"] = info["dns-nameservers"].split() # Name server search info provided?? - if 'dns-search' in info: - iface_info['dns-search'] = info['dns-search'].split() + if "dns-search" in info: + iface_info["dns-search"] = info["dns-search"].split() # Is any mac address spoofing going on?? - if 'hwaddress' in info: - hw_info = info['hwaddress'].lower().strip() + if "hwaddress" in info: + hw_info = info["hwaddress"].lower().strip() hw_split = hw_info.split(None, 1) - if len(hw_split) == 2 and hw_split[0].startswith('ether'): + if len(hw_split) == 2 and hw_split[0].startswith("ether"): hw_addr = hw_split[1] if hw_addr: - iface_info['hwaddress'] = hw_addr + iface_info["hwaddress"] = hw_addr # If ipv6 is enabled, device will have multiple IPs, so we need to # update the dictionary instead of overwriting it... if dev_name in real_ifaces: @@ -179,13 +180,14 @@ def translate_network(settings): if not args: continue dev_name = args[0].strip().lower() - if cmd == 'auto': + if cmd == "auto": # Seems like auto can be like 'auto eth0 eth0:1' so just get the # first part out as the device name if dev_name in real_ifaces: - real_ifaces[dev_name]['auto'] = True - if cmd == 'iface' and 'inet6' in args: - real_ifaces[dev_name]['inet6'] = True + real_ifaces[dev_name]["auto"] = True + if cmd == "iface" and "inet6" in args: + real_ifaces[dev_name]["inet6"] = True return real_ifaces + # vi: ts=4 expandtab diff --git a/cloudinit/distros/netbsd.py b/cloudinit/distros/netbsd.py index f1a9b182..9c38ae51 100644 --- a/cloudinit/distros/netbsd.py +++ b/cloudinit/distros/netbsd.py @@ -8,8 +8,7 @@ import platform import cloudinit.distros.bsd from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util +from cloudinit import subp, util LOG = logging.getLogger(__name__) @@ -21,42 +20,42 @@ class NetBSD(cloudinit.distros.bsd.BSD): (N.B. OpenBSD inherits from this class.) """ - ci_sudoers_fn = '/usr/pkg/etc/sudoers.d/90-cloud-init-users' + ci_sudoers_fn = "/usr/pkg/etc/sudoers.d/90-cloud-init-users" group_add_cmd_prefix = ["groupadd"] def __init__(self, name, cfg, paths): super().__init__(name, cfg, paths) if os.path.exists("/usr/pkg/bin/pkgin"): - self.pkg_cmd_install_prefix = ['pkgin', '-y', 'install'] - self.pkg_cmd_remove_prefix = ['pkgin', '-y', 'remove'] - self.pkg_cmd_update_prefix = ['pkgin', '-y', 'update'] - self.pkg_cmd_upgrade_prefix = ['pkgin', '-y', 'full-upgrade'] + self.pkg_cmd_install_prefix = ["pkgin", "-y", "install"] + self.pkg_cmd_remove_prefix = ["pkgin", "-y", "remove"] + self.pkg_cmd_update_prefix = ["pkgin", "-y", "update"] + self.pkg_cmd_upgrade_prefix = ["pkgin", "-y", "full-upgrade"] else: - self.pkg_cmd_install_prefix = ['pkg_add', '-U'] - self.pkg_cmd_remove_prefix = ['pkg_delete'] + self.pkg_cmd_install_prefix = ["pkg_add", "-U"] + self.pkg_cmd_remove_prefix = ["pkg_delete"] def _get_add_member_to_group_cmd(self, member_name, group_name): - return ['usermod', '-G', group_name, member_name] + return ["usermod", "-G", group_name, member_name] def add_user(self, name, **kwargs): if util.is_user(name): LOG.info("User %s already exists, skipping.", name) return False - adduser_cmd = ['useradd'] - log_adduser_cmd = ['useradd'] + adduser_cmd = ["useradd"] + log_adduser_cmd = ["useradd"] adduser_opts = { - "homedir": '-d', - "gecos": '-c', - "primary_group": '-g', - "groups": '-G', - "shell": '-s', + "homedir": "-d", + "gecos": "-c", + "primary_group": "-g", + "groups": "-G", + "shell": "-s", } adduser_flags = { - "no_user_group": '--no-user-group', - "system": '--system', - "no_log_init": '--no-log-init', + "no_user_group": "--no-user-group", + "system": "--system", + "no_log_init": "--no-log-init", } for key, val in kwargs.items(): @@ -67,9 +66,9 @@ class NetBSD(cloudinit.distros.bsd.BSD): adduser_cmd.append(adduser_flags[key]) log_adduser_cmd.append(adduser_flags[key]) - if 'no_create_home' not in kwargs or 'system' not in kwargs: - adduser_cmd += ['-m'] - log_adduser_cmd += ['-m'] + if "no_create_home" not in kwargs or "system" not in kwargs: + adduser_cmd += ["-m"] + log_adduser_cmd += ["-m"] adduser_cmd += [name] log_adduser_cmd += [name] @@ -83,29 +82,28 @@ class NetBSD(cloudinit.distros.bsd.BSD): raise # Set the password if it is provided # For security consideration, only hashed passwd is assumed - passwd_val = kwargs.get('passwd', None) + passwd_val = kwargs.get("passwd", None) if passwd_val is not None: self.set_passwd(name, passwd_val, hashed=True) def set_passwd(self, user, passwd, hashed=False): if hashed: hashed_pw = passwd - elif not hasattr(crypt, 'METHOD_BLOWFISH'): + elif not hasattr(crypt, "METHOD_BLOWFISH"): # crypt.METHOD_BLOWFISH comes with Python 3.7 which is available # on NetBSD 7 and 8. - LOG.error(( - 'Cannot set non-encrypted password for user %s. ' - 'Python >= 3.7 is required.'), user) + LOG.error( + "Cannot set non-encrypted password for user %s. " + "Python >= 3.7 is required.", + user, + ) return else: method = crypt.METHOD_BLOWFISH # pylint: disable=E1101 - hashed_pw = crypt.crypt( - passwd, - crypt.mksalt(method) - ) + hashed_pw = crypt.crypt(passwd, crypt.mksalt(method)) try: - subp.subp(['usermod', '-p', hashed_pw, user]) + subp.subp(["usermod", "-p", hashed_pw, user]) except Exception: util.logexc(LOG, "Failed to set password for %s", user) raise @@ -113,40 +111,42 @@ class NetBSD(cloudinit.distros.bsd.BSD): def force_passwd_change(self, user): try: - subp.subp(['usermod', '-F', user]) + subp.subp(["usermod", "-F", user]) except Exception: util.logexc(LOG, "Failed to set pw expiration for %s", user) raise def lock_passwd(self, name): try: - subp.subp(['usermod', '-C', 'yes', name]) + subp.subp(["usermod", "-C", "yes", name]) except Exception: util.logexc(LOG, "Failed to lock user %s", name) raise def unlock_passwd(self, name): try: - subp.subp(['usermod', '-C', 'no', name]) + subp.subp(["usermod", "-C", "no", name]) except Exception: util.logexc(LOG, "Failed to unlock user %s", name) raise def apply_locale(self, locale, out_fn=None): - LOG.debug('Cannot set the locale.') + LOG.debug("Cannot set the locale.") def apply_network_config_names(self, netconfig): - LOG.debug('NetBSD cannot rename network interface.') + LOG.debug("NetBSD cannot rename network interface.") def _get_pkg_cmd_environ(self): """Return env vars used in NetBSD package_command operations""" os_release = platform.release() os_arch = platform.machine() e = os.environ.copy() - e['PKG_PATH'] = ( - 'http://cdn.netbsd.org/pub/pkgsrc/' - 'packages/NetBSD/%s/%s/All' - ) % (os_arch, os_release) + e[ + "PKG_PATH" + ] = "http://cdn.netbsd.org/pub/pkgsrc/packages/NetBSD/%s/%s/All" % ( + os_arch, + os_release, + ) return e def update_package_sources(self): @@ -156,4 +156,5 @@ class NetBSD(cloudinit.distros.bsd.BSD): class Distro(NetBSD): pass + # vi: ts=4 expandtab diff --git a/cloudinit/distros/networking.py b/cloudinit/distros/networking.py index c291196a..b24b6233 100644 --- a/cloudinit/distros/networking.py +++ b/cloudinit/distros/networking.py @@ -1,10 +1,9 @@ import abc import logging import os +from typing import List, Optional -from cloudinit import subp -from cloudinit import net, util - +from cloudinit import net, subp, util LOG = logging.getLogger(__name__) @@ -24,7 +23,7 @@ class Networking(metaclass=abc.ABCMeta): """ def __init__(self): - self.blacklist_drivers = None + self.blacklist_drivers: Optional[List[str]] = None def _get_current_rename_info(self) -> dict: return net._get_current_rename_info() @@ -73,7 +72,8 @@ class Networking(metaclass=abc.ABCMeta): def get_interfaces_by_mac(self) -> dict: return net.get_interfaces_by_mac( - blacklist_drivers=self.blacklist_drivers) + blacklist_drivers=self.blacklist_drivers + ) def get_master(self, devname: DeviceName): return net.get_master(devname) @@ -225,7 +225,7 @@ class LinuxNetworking(Networking): def try_set_link_up(self, devname: DeviceName) -> bool: """Try setting the link to up explicitly and return if it is up. - Not guaranteed to bring the interface up. The caller is expected to - add wait times before retrying.""" - subp.subp(['ip', 'link', 'set', devname, 'up']) + Not guaranteed to bring the interface up. The caller is expected to + add wait times before retrying.""" + subp.subp(["ip", "link", "set", devname, "up"]) return self.is_up(devname) diff --git a/cloudinit/distros/openEuler.py b/cloudinit/distros/openEuler.py new file mode 100644 index 00000000..3dc0a342 --- /dev/null +++ b/cloudinit/distros/openEuler.py @@ -0,0 +1,10 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + + +# vi: ts=4 expandtab diff --git a/cloudinit/distros/openbsd.py b/cloudinit/distros/openbsd.py index 720c9cf3..ccdb8799 100644 --- a/cloudinit/distros/openbsd.py +++ b/cloudinit/distros/openbsd.py @@ -7,28 +7,27 @@ import platform import cloudinit.distros.netbsd from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util +from cloudinit import subp, util LOG = logging.getLogger(__name__) class Distro(cloudinit.distros.netbsd.NetBSD): - hostname_conf_fn = '/etc/myname' + hostname_conf_fn = "/etc/myname" def _read_hostname(self, filename, default=None): return util.load_file(self.hostname_conf_fn) def _write_hostname(self, hostname, filename): - content = hostname + '\n' + content = hostname + "\n" util.write_file(self.hostname_conf_fn, content) def _get_add_member_to_group_cmd(self, member_name, group_name): - return ['usermod', '-G', group_name, member_name] + return ["usermod", "-G", group_name, member_name] def lock_passwd(self, name): try: - subp.subp(['usermod', '-p', '*', name]) + subp.subp(["usermod", "-p", "*", name]) except Exception: util.logexc(LOG, "Failed to lock user %s", name) raise @@ -41,11 +40,10 @@ class Distro(cloudinit.distros.netbsd.NetBSD): os_release = platform.release() os_arch = platform.machine() e = os.environ.copy() - e['PKG_PATH'] = ( - 'ftp://ftp.openbsd.org/pub/OpenBSD/{os_release}/' - 'packages/{os_arch}/').format( - os_arch=os_arch, os_release=os_release - ) + e["PKG_PATH"] = ( + "ftp://ftp.openbsd.org/pub/OpenBSD/{os_release}/" + "packages/{os_arch}/" + ).format(os_arch=os_arch, os_release=os_release) return e diff --git a/cloudinit/distros/opensuse.py b/cloudinit/distros/opensuse.py index 7ca0ef99..00ed1514 100644 --- a/cloudinit/distros/opensuse.py +++ b/cloudinit/distros/opensuse.py @@ -8,69 +8,61 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import distros - -from cloudinit.distros.parsers.hostname import HostnameConf - -from cloudinit import helpers -from cloudinit import subp -from cloudinit import util - +from cloudinit import distros, helpers, subp, util from cloudinit.distros import rhel_util as rhutil +from cloudinit.distros.parsers.hostname import HostnameConf from cloudinit.settings import PER_INSTANCE class Distro(distros.Distro): - clock_conf_fn = '/etc/sysconfig/clock' - hostname_conf_fn = '/etc/HOSTNAME' - init_cmd = ['service'] - locale_conf_fn = '/etc/sysconfig/language' - network_conf_fn = '/etc/sysconfig/network/config' - network_script_tpl = '/etc/sysconfig/network/ifcfg-%s' - resolve_conf_fn = '/etc/resolv.conf' - route_conf_tpl = '/etc/sysconfig/network/ifroute-%s' - systemd_hostname_conf_fn = '/etc/hostname' - systemd_locale_conf_fn = '/etc/locale.conf' - tz_local_fn = '/etc/localtime' + clock_conf_fn = "/etc/sysconfig/clock" + hostname_conf_fn = "/etc/HOSTNAME" + init_cmd = ["service"] + locale_conf_fn = "/etc/sysconfig/language" + network_conf_fn = "/etc/sysconfig/network/config" + network_script_tpl = "/etc/sysconfig/network/ifcfg-%s" + route_conf_tpl = "/etc/sysconfig/network/ifroute-%s" + systemd_hostname_conf_fn = "/etc/hostname" + systemd_locale_conf_fn = "/etc/locale.conf" + tz_local_fn = "/etc/localtime" renderer_configs = { - 'sysconfig': { - 'control': 'etc/sysconfig/network/config', - 'flavor': 'suse', - 'iface_templates': '%(base)s/network/ifcfg-%(name)s', - 'netrules_path': ( - 'etc/udev/rules.d/85-persistent-net-cloud-init.rules'), - 'route_templates': { - 'ipv4': '%(base)s/network/ifroute-%(name)s', - 'ipv6': '%(base)s/network/ifroute-%(name)s', - } + "sysconfig": { + "control": "etc/sysconfig/network/config", + "flavor": "suse", + "iface_templates": "%(base)s/network/ifcfg-%(name)s", + "netrules_path": ( + "etc/udev/rules.d/85-persistent-net-cloud-init.rules" + ), + "route_templates": { + "ipv4": "%(base)s/network/ifroute-%(name)s", + "ipv6": "%(base)s/network/ifroute-%(name)s", + }, } } def __init__(self, name, cfg, paths): distros.Distro.__init__(self, name, cfg, paths) self._runner = helpers.Runners(paths) - self.osfamily = 'suse' - cfg['ssh_svcname'] = 'sshd' + self.osfamily = "suse" + cfg["ssh_svcname"] = "sshd" if self.uses_systemd(): - self.init_cmd = ['systemctl'] - cfg['ssh_svcname'] = 'sshd.service' + self.init_cmd = ["systemctl"] + cfg["ssh_svcname"] = "sshd.service" def apply_locale(self, locale, out_fn=None): if self.uses_systemd(): if not out_fn: out_fn = self.systemd_locale_conf_fn - locale_cfg = {'LANG': locale} + locale_cfg = {"LANG": locale} else: if not out_fn: out_fn = self.locale_conf_fn - locale_cfg = {'RC_LANG': locale} + locale_cfg = {"RC_LANG": locale} rhutil.update_sysconfig_file(out_fn, locale_cfg) def install_packages(self, pkglist): self.package_command( - 'install', - args='--auto-agree-with-licenses', - pkgs=pkglist + "install", args="--auto-agree-with-licenses", pkgs=pkglist ) def package_command(self, command, args=None, pkgs=None): @@ -78,11 +70,11 @@ class Distro(distros.Distro): pkgs = [] # No user interaction possible, enable non-interactive mode - cmd = ['zypper', '--non-interactive'] + cmd = ["zypper", "--non-interactive"] # Command is the operation, such as install - if command == 'upgrade': - command = 'update' + if command == "upgrade": + command = "update" cmd.append(command) # args are the arguments to the command, not global options @@ -91,7 +83,7 @@ class Distro(distros.Distro): elif args and isinstance(args, list): cmd.extend(args) - pkglist = util.expand_package_list('%s-%s', pkgs) + pkglist = util.expand_package_list("%s-%s", pkgs) cmd.extend(pkglist) # Allow the output of this to flow outwards (ie not be captured) @@ -107,27 +99,25 @@ class Distro(distros.Distro): else: # Adjust the sysconfig clock zone setting clock_cfg = { - 'TIMEZONE': str(tz), + "TIMEZONE": str(tz), } rhutil.update_sysconfig_file(self.clock_conf_fn, clock_cfg) # This ensures that the correct tz will be used for the system util.copy(tz_file, self.tz_local_fn) def update_package_sources(self): - self._runner.run("update-sources", self.package_command, - ['refresh'], freq=PER_INSTANCE) - - def _bring_up_interfaces(self, device_names): - if device_names and 'all' in device_names: - raise RuntimeError(('Distro %s can not translate ' - 'the device name "all"') % (self.name)) - return distros.Distro._bring_up_interfaces(self, device_names) + self._runner.run( + "update-sources", + self.package_command, + ["refresh"], + freq=PER_INSTANCE, + ) def _read_hostname(self, filename, default=None): - if self.uses_systemd() and filename.endswith('/previous-hostname'): + if self.uses_systemd() and filename.endswith("/previous-hostname"): return util.load_file(filename).strip() elif self.uses_systemd(): - (out, _err) = subp.subp(['hostname']) + (out, _err) = subp.subp(["hostname"]) if len(out): return out else: @@ -157,26 +147,23 @@ class Distro(distros.Distro): host_fn = self.hostname_conf_fn return (host_fn, self._read_hostname(host_fn)) - def _write_hostname(self, hostname, out_fn): - if self.uses_systemd() and out_fn.endswith('/previous-hostname'): - util.write_file(out_fn, hostname) + def _write_hostname(self, hostname, filename): + if self.uses_systemd() and filename.endswith("/previous-hostname"): + util.write_file(filename, hostname) elif self.uses_systemd(): - subp.subp(['hostnamectl', 'set-hostname', str(hostname)]) + subp.subp(["hostnamectl", "set-hostname", str(hostname)]) else: conf = None try: # Try to update the previous one # so lets see if we can read it first. - conf = self._read_hostname_conf(out_fn) + conf = self._read_hostname_conf(filename) except IOError: pass if not conf: - conf = HostnameConf('') + conf = HostnameConf("") conf.set_hostname(hostname) - util.write_file(out_fn, str(conf), 0o644) - - def _write_network_config(self, netconfig): - return self._supported_write_network_config(netconfig) + util.write_file(filename, str(conf), 0o644) @property def preferred_ntp_clients(self): @@ -184,22 +171,28 @@ class Distro(distros.Distro): # Allow distro to determine the preferred ntp client list if not self._preferred_ntp_clients: - distro_info = util.system_info()['dist'] + distro_info = util.system_info()["dist"] name = distro_info[0] - major_ver = int(distro_info[1].split('.')[0]) + major_ver = int(distro_info[1].split(".")[0]) # This is horribly complicated because of a case of # "we do not care if versions should be increasing syndrome" - if ( - (major_ver >= 15 and 'openSUSE' not in name) or - (major_ver >= 15 and 'openSUSE' in name and major_ver != 42) + if (major_ver >= 15 and "openSUSE" not in name) or ( + major_ver >= 15 and "openSUSE" in name and major_ver != 42 ): - self._preferred_ntp_clients = ['chrony', - 'systemd-timesyncd', 'ntp'] + self._preferred_ntp_clients = [ + "chrony", + "systemd-timesyncd", + "ntp", + ] else: - self._preferred_ntp_clients = ['ntp', - 'systemd-timesyncd', 'chrony'] + self._preferred_ntp_clients = [ + "ntp", + "systemd-timesyncd", + "chrony", + ] return self._preferred_ntp_clients + # vi: ts=4 expandtab diff --git a/cloudinit/distros/parsers/__init__.py b/cloudinit/distros/parsers/__init__.py index 6b5b6dde..5bea2ae1 100644 --- a/cloudinit/distros/parsers/__init__.py +++ b/cloudinit/distros/parsers/__init__.py @@ -9,10 +9,11 @@ def chop_comment(text, comment_chars): comment_locations = [text.find(c) for c in comment_chars] comment_locations = [c for c in comment_locations if c != -1] if not comment_locations: - return (text, '') + return (text, "") min_comment = min(comment_locations) before_comment = text[0:min_comment] comment = text[min_comment:] return (before_comment, comment) + # vi: ts=4 expandtab diff --git a/cloudinit/distros/parsers/hostname.py b/cloudinit/distros/parsers/hostname.py index e74c083c..61674082 100644 --- a/cloudinit/distros/parsers/hostname.py +++ b/cloudinit/distros/parsers/hostname.py @@ -23,11 +23,11 @@ class HostnameConf(object): self.parse() contents = StringIO() for (line_type, components) in self._contents: - if line_type == 'blank': + if line_type == "blank": contents.write("%s\n" % (components[0])) - elif line_type == 'all_comment': + elif line_type == "all_comment": contents.write("%s\n" % (components[0])) - elif line_type == 'hostname': + elif line_type == "hostname": (hostname, tail) = components contents.write("%s%s\n" % (hostname, tail)) # Ensure trailing newline @@ -40,7 +40,7 @@ class HostnameConf(object): def hostname(self): self.parse() for (line_type, components) in self._contents: - if line_type == 'hostname': + if line_type == "hostname": return components[0] return None @@ -51,28 +51,28 @@ class HostnameConf(object): self.parse() replaced = False for (line_type, components) in self._contents: - if line_type == 'hostname': + if line_type == "hostname": components[0] = str(your_hostname) replaced = True if not replaced: - self._contents.append(('hostname', [str(your_hostname), ''])) + self._contents.append(("hostname", [str(your_hostname), ""])) def _parse(self, contents): entries = [] hostnames_found = set() for line in contents.splitlines(): if not len(line.strip()): - entries.append(('blank', [line])) + entries.append(("blank", [line])) continue - (head, tail) = chop_comment(line.strip(), '#') + (head, tail) = chop_comment(line.strip(), "#") if not len(head): - entries.append(('all_comment', [line])) + entries.append(("all_comment", [line])) continue - entries.append(('hostname', [head, tail])) + entries.append(("hostname", [head, tail])) hostnames_found.add(head) if len(hostnames_found) > 1: - raise IOError("Multiple hostnames (%s) found!" - % (hostnames_found)) + raise IOError("Multiple hostnames (%s) found!" % (hostnames_found)) return entries + # vi: ts=4 expandtab diff --git a/cloudinit/distros/parsers/hosts.py b/cloudinit/distros/parsers/hosts.py index 54e4e934..e43880af 100644 --- a/cloudinit/distros/parsers/hosts.py +++ b/cloudinit/distros/parsers/hosts.py @@ -25,7 +25,7 @@ class HostsConf(object): self.parse() options = [] for (line_type, components) in self._contents: - if line_type == 'option': + if line_type == "option": (pieces, _tail) = components if len(pieces) and pieces[0] == ip: options.append(pieces[1:]) @@ -35,7 +35,7 @@ class HostsConf(object): self.parse() n_entries = [] for (line_type, components) in self._contents: - if line_type != 'option': + if line_type != "option": n_entries.append((line_type, components)) continue else: @@ -48,35 +48,37 @@ class HostsConf(object): def add_entry(self, ip, canonical_hostname, *aliases): self.parse() - self._contents.append(('option', - ([ip, canonical_hostname] + list(aliases), ''))) + self._contents.append( + ("option", ([ip, canonical_hostname] + list(aliases), "")) + ) def _parse(self, contents): entries = [] for line in contents.splitlines(): if not len(line.strip()): - entries.append(('blank', [line])) + entries.append(("blank", [line])) continue - (head, tail) = chop_comment(line.strip(), '#') + (head, tail) = chop_comment(line.strip(), "#") if not len(head): - entries.append(('all_comment', [line])) + entries.append(("all_comment", [line])) continue - entries.append(('option', [head.split(None), tail])) + entries.append(("option", [head.split(None), tail])) return entries def __str__(self): self.parse() contents = StringIO() for (line_type, components) in self._contents: - if line_type == 'blank': + if line_type == "blank": contents.write("%s\n" % (components[0])) - elif line_type == 'all_comment': + elif line_type == "all_comment": contents.write("%s\n" % (components[0])) - elif line_type == 'option': + elif line_type == "option": (pieces, tail) = components pieces = [str(p) for p in pieces] pieces = "\t".join(pieces) contents.write("%s%s\n" % (pieces, tail)) return contents.getvalue() + # vi: ts=4 expandtab diff --git a/cloudinit/distros/parsers/networkmanager_conf.py b/cloudinit/distros/parsers/networkmanager_conf.py index ac51f122..4b669b0f 100644 --- a/cloudinit/distros/parsers/networkmanager_conf.py +++ b/cloudinit/distros/parsers/networkmanager_conf.py @@ -13,9 +13,9 @@ import configobj class NetworkManagerConf(configobj.ConfigObj): def __init__(self, contents): - configobj.ConfigObj.__init__(self, contents, - interpolation=False, - write_empty_values=False) + configobj.ConfigObj.__init__( + self, contents, interpolation=False, write_empty_values=False + ) def set_section_keypair(self, section_name, key, value): if section_name not in self.sections: diff --git a/cloudinit/distros/parsers/resolv_conf.py b/cloudinit/distros/parsers/resolv_conf.py index 62929d03..0ef4e147 100644 --- a/cloudinit/distros/parsers/resolv_conf.py +++ b/cloudinit/distros/parsers/resolv_conf.py @@ -6,9 +6,9 @@ from io import StringIO -from cloudinit.distros.parsers import chop_comment from cloudinit import log as logging from cloudinit import util +from cloudinit.distros.parsers import chop_comment LOG = logging.getLogger(__name__) @@ -26,12 +26,12 @@ class ResolvConf(object): @property def nameservers(self): self.parse() - return self._retr_option('nameserver') + return self._retr_option("nameserver") @property def local_domain(self): self.parse() - dm = self._retr_option('domain') + dm = self._retr_option("domain") if dm: return dm[0] return None @@ -39,7 +39,7 @@ class ResolvConf(object): @property def search_domains(self): self.parse() - current_sds = self._retr_option('search') + current_sds = self._retr_option("search") flat_sds = [] for sdlist in current_sds: for sd in sdlist.split(None): @@ -51,11 +51,11 @@ class ResolvConf(object): self.parse() contents = StringIO() for (line_type, components) in self._contents: - if line_type == 'blank': + if line_type == "blank": contents.write("\n") - elif line_type == 'all_comment': + elif line_type == "all_comment": contents.write("%s\n" % (components[0])) - elif line_type == 'option': + elif line_type == "option": (cfg_opt, cfg_value, comment_tail) = components line = "%s %s" % (cfg_opt, cfg_value) if len(comment_tail): @@ -66,7 +66,7 @@ class ResolvConf(object): def _retr_option(self, opt_name): found = [] for (line_type, components) in self._contents: - if line_type == 'option': + if line_type == "option": (cfg_opt, cfg_value, _comment_tail) = components if cfg_opt == opt_name: found.append(cfg_value) @@ -74,27 +74,29 @@ class ResolvConf(object): def add_nameserver(self, ns): self.parse() - current_ns = self._retr_option('nameserver') + current_ns = self._retr_option("nameserver") new_ns = list(current_ns) new_ns.append(str(ns)) new_ns = util.uniq_list(new_ns) if len(new_ns) == len(current_ns): return current_ns if len(current_ns) >= 3: - LOG.warning("ignoring nameserver %r: adding would " - "exceed the maximum of " - "'3' name servers (see resolv.conf(5))", ns) + LOG.warning( + "ignoring nameserver %r: adding would " + "exceed the maximum of " + "'3' name servers (see resolv.conf(5))", + ns, + ) return current_ns[:3] - self._remove_option('nameserver') + self._remove_option("nameserver") for n in new_ns: - self._contents.append(('option', ['nameserver', n, ''])) + self._contents.append(("option", ["nameserver", n, ""])) return new_ns def _remove_option(self, opt_name): - def remove_opt(item): line_type, components = item - if line_type != 'option': + if line_type != "option": return False (cfg_opt, _cfg_value, _comment_tail) = components if cfg_opt != opt_name: @@ -116,23 +118,26 @@ class ResolvConf(object): return new_sds if len(flat_sds) >= 6: # Hard restriction on only 6 search domains - raise ValueError(("Adding %r would go beyond the " - "'6' maximum search domains") % (search_domain)) + raise ValueError( + "Adding %r would go beyond the '6' maximum search domains" + % (search_domain) + ) s_list = " ".join(new_sds) if len(s_list) > 256: # Some hard limit on 256 chars total - raise ValueError(("Adding %r would go beyond the " - "256 maximum search list character limit") - % (search_domain)) - self._remove_option('search') - self._contents.append(('option', ['search', s_list, ''])) + raise ValueError( + "Adding %r would go beyond the " + "256 maximum search list character limit" % (search_domain) + ) + self._remove_option("search") + self._contents.append(("option", ["search", s_list, ""])) return flat_sds @local_domain.setter def local_domain(self, domain): self.parse() - self._remove_option('domain') - self._contents.append(('option', ['domain', str(domain), ''])) + self._remove_option("domain") + self._contents.append(("option", ["domain", str(domain), ""])) return domain def _parse(self, contents): @@ -140,24 +145,30 @@ class ResolvConf(object): for (i, line) in enumerate(contents.splitlines()): sline = line.strip() if not sline: - entries.append(('blank', [line])) + entries.append(("blank", [line])) continue - (head, tail) = chop_comment(line, ';#') + (head, tail) = chop_comment(line, ";#") if not len(head.strip()): - entries.append(('all_comment', [line])) + entries.append(("all_comment", [line])) continue if not tail: - tail = '' + tail = "" try: (cfg_opt, cfg_values) = head.split(None, 1) except (IndexError, ValueError) as e: raise IOError( "Incorrectly formatted resolv.conf line %s" % (i + 1) ) from e - if cfg_opt not in ['nameserver', 'domain', - 'search', 'sortlist', 'options']: + if cfg_opt not in [ + "nameserver", + "domain", + "search", + "sortlist", + "options", + ]: raise IOError("Unexpected resolv.conf option %s" % (cfg_opt)) entries.append(("option", [cfg_opt, cfg_values, tail])) return entries + # vi: ts=4 expandtab diff --git a/cloudinit/distros/parsers/sys_conf.py b/cloudinit/distros/parsers/sys_conf.py index dee4c551..4132734c 100644 --- a/cloudinit/distros/parsers/sys_conf.py +++ b/cloudinit/distros/parsers/sys_conf.py @@ -20,7 +20,7 @@ import configobj # See: http://pubs.opengroup.org/onlinepubs/000095399/basedefs/xbd_chap08.html # or look at the 'param_expand()' function in the subst.c file in the bash # source tarball... -SHELL_VAR_RULE = r'[a-zA-Z_]+[a-zA-Z0-9_]*' +SHELL_VAR_RULE = r"[a-zA-Z_]+[a-zA-Z0-9_]*" SHELL_VAR_REGEXES = [ # Basic variables re.compile(r"\$" + SHELL_VAR_RULE), @@ -48,10 +48,11 @@ class SysConf(configobj.ConfigObj): ``configobj.ConfigObj.__init__`` (i.e. "a filename, file like object, or list of lines"). """ + def __init__(self, contents): - configobj.ConfigObj.__init__(self, contents, - interpolation=False, - write_empty_values=True) + configobj.ConfigObj.__init__( + self, contents, interpolation=False, write_empty_values=True + ) def __str__(self): contents = self.write() @@ -66,11 +67,13 @@ class SysConf(configobj.ConfigObj): if not isinstance(value, str): raise ValueError('Value "%s" is not a string' % (value)) if len(value) == 0: - return '' + return "" quot_func = None if value[0] in ['"', "'"] and value[-1] in ['"', "'"]: if len(value) == 1: - quot_func = (lambda x: self._get_single_quote(x) % x) + quot_func = ( + lambda x: self._get_single_quote(x) % x + ) # noqa: E731 else: # Quote whitespace if it isn't the start + end of a shell command if value.strip().startswith("$(") and value.strip().endswith(")"): @@ -82,11 +85,13 @@ class SysConf(configobj.ConfigObj): # leave it alone since the pipes.quote function likes # to use single quotes which won't get expanded... if re.search(r"[\n\"']", value): - quot_func = (lambda x: - self._get_triple_quote(x) % x) + quot_func = ( + lambda x: self._get_triple_quote(x) % x + ) # noqa: E731 else: - quot_func = (lambda x: - self._get_single_quote(x) % x) + quot_func = ( + lambda x: self._get_single_quote(x) % x + ) # noqa: E731 else: quot_func = pipes.quote if not quot_func: @@ -99,10 +104,13 @@ class SysConf(configobj.ConfigObj): val = self._decode_element(self._quote(this_entry)) key = self._decode_element(self._quote(entry)) cmnt = self._decode_element(comment) - return '%s%s%s%s%s' % (indent_string, - key, - self._a_to_u('='), - val, - cmnt) + return "%s%s%s%s%s" % ( + indent_string, + key, + self._a_to_u("="), + val, + cmnt, + ) + # vi: ts=4 expandtab diff --git a/cloudinit/distros/photon.py b/cloudinit/distros/photon.py new file mode 100644 index 00000000..14cefe90 --- /dev/null +++ b/cloudinit/distros/photon.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +# vi: ts=4 expandtab +# +# Copyright (C) 2021 VMware Inc. +# +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit import distros, helpers +from cloudinit import log as logging +from cloudinit import net, subp, util +from cloudinit.distros import rhel_util as rhutil +from cloudinit.settings import PER_INSTANCE + +LOG = logging.getLogger(__name__) + + +class Distro(distros.Distro): + systemd_hostname_conf_fn = "/etc/hostname" + network_conf_dir = "/etc/systemd/network/" + systemd_locale_conf_fn = "/etc/locale.conf" + resolve_conf_fn = "/etc/systemd/resolved.conf" + + renderer_configs = { + "networkd": { + "resolv_conf_fn": resolve_conf_fn, + "network_conf_dir": network_conf_dir, + } + } + + # Should be fqdn if we can use it + prefer_fqdn = True + + def __init__(self, name, cfg, paths): + distros.Distro.__init__(self, name, cfg, paths) + # This will be used to restrict certain + # calls from repeatly happening (when they + # should only happen say once per instance...) + self._runner = helpers.Runners(paths) + self.osfamily = "photon" + self.init_cmd = ["systemctl"] + + def exec_cmd(self, cmd, capture=True): + LOG.debug("Attempting to run: %s", cmd) + try: + (out, err) = subp.subp(cmd, capture=capture) + if err: + LOG.warning( + "Running %s resulted in stderr output: %s", cmd, err + ) + return True, out, err + return False, out, err + except subp.ProcessExecutionError: + util.logexc(LOG, "Command %s failed", cmd) + return True, None, None + + def generate_fallback_config(self): + key = "disable_fallback_netcfg" + disable_fallback_netcfg = self._cfg.get(key, True) + LOG.debug("%s value is: %s", key, disable_fallback_netcfg) + + if not disable_fallback_netcfg: + return net.generate_fallback_config() + + LOG.info( + "Skipping generate_fallback_config. Rely on PhotonOS default " + "network config" + ) + return None + + def apply_locale(self, locale, out_fn=None): + # This has a dependancy on glibc-i18n, user need to manually install it + # and enable the option in cloud.cfg + if not out_fn: + out_fn = self.systemd_locale_conf_fn + + locale_cfg = { + "LANG": locale, + } + + rhutil.update_sysconfig_file(out_fn, locale_cfg) + + # rhutil will modify /etc/locale.conf + # For locale change to take effect, reboot is needed or we can restart + # systemd-localed. This is equivalent of localectl + cmd = ["systemctl", "restart", "systemd-localed"] + self.exec_cmd(cmd) + + def install_packages(self, pkglist): + # self.update_package_sources() + self.package_command("install", pkgs=pkglist) + + def _write_hostname(self, hostname, filename): + if filename and filename.endswith("/previous-hostname"): + util.write_file(filename, hostname) + else: + ret, _out, err = self.exec_cmd( + ["hostnamectl", "set-hostname", str(hostname)] + ) + if ret: + LOG.warning( + ( + "Error while setting hostname: %s\nGiven hostname: %s", + err, + hostname, + ) + ) + + def _read_system_hostname(self): + sys_hostname = self._read_hostname(self.systemd_hostname_conf_fn) + return (self.systemd_hostname_conf_fn, sys_hostname) + + def _read_hostname(self, filename, default=None): + if filename and filename.endswith("/previous-hostname"): + return util.load_file(filename).strip() + + _ret, out, _err = self.exec_cmd(["hostname", "-f"]) + return out.strip() if out else default + + def _get_localhost_ip(self): + return "127.0.1.1" + + def set_timezone(self, tz): + distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) + + def package_command(self, command, args=None, pkgs=None): + if not pkgs: + pkgs = [] + + cmd = ["tdnf", "-y"] + if args and isinstance(args, str): + cmd.append(args) + elif args and isinstance(args, list): + cmd.extend(args) + + cmd.append(command) + + pkglist = util.expand_package_list("%s-%s", pkgs) + cmd.extend(pkglist) + + ret, _out, err = self.exec_cmd(cmd) + if ret: + LOG.error("Error while installing packages: %s", err) + + def update_package_sources(self): + self._runner.run( + "update-sources", + self.package_command, + ["makecache"], + freq=PER_INSTANCE, + ) diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index c72f7c17..84744ece 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -8,12 +8,9 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit import distros -from cloudinit import helpers +from cloudinit import distros, helpers from cloudinit import log as logging -from cloudinit import subp -from cloudinit import util - +from cloudinit import subp, util from cloudinit.distros import rhel_util from cloudinit.settings import PER_INSTANCE @@ -22,48 +19,48 @@ LOG = logging.getLogger(__name__) def _make_sysconfig_bool(val): if val: - return 'yes' + return "yes" else: - return 'no' + return "no" class Distro(distros.Distro): # See: https://access.redhat.com/documentation/en-US/Red_Hat_Enterprise_Linux/7/html/Networking_Guide/sec-Network_Configuration_Using_sysconfig_Files.html # noqa clock_conf_fn = "/etc/sysconfig/clock" - locale_conf_fn = '/etc/sysconfig/i18n' - systemd_locale_conf_fn = '/etc/locale.conf' + locale_conf_fn = "/etc/sysconfig/i18n" + systemd_locale_conf_fn = "/etc/locale.conf" network_conf_fn = "/etc/sysconfig/network" hostname_conf_fn = "/etc/sysconfig/network" systemd_hostname_conf_fn = "/etc/hostname" - network_script_tpl = '/etc/sysconfig/network-scripts/ifcfg-%s' - resolve_conf_fn = "/etc/resolv.conf" + network_script_tpl = "/etc/sysconfig/network-scripts/ifcfg-%s" tz_local_fn = "/etc/localtime" usr_lib_exec = "/usr/libexec" renderer_configs = { - 'sysconfig': { - 'control': 'etc/sysconfig/network', - 'iface_templates': '%(base)s/network-scripts/ifcfg-%(name)s', - 'route_templates': { - 'ipv4': '%(base)s/network-scripts/route-%(name)s', - 'ipv6': '%(base)s/network-scripts/route6-%(name)s' - } + "sysconfig": { + "control": "etc/sysconfig/network", + "iface_templates": "%(base)s/network-scripts/ifcfg-%(name)s", + "route_templates": { + "ipv4": "%(base)s/network-scripts/route-%(name)s", + "ipv6": "%(base)s/network-scripts/route6-%(name)s", + }, } } + # Should be fqdn if we can use it + # See: https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/5/html/deployment_guide/ch-sysconfig # noqa: E501 + prefer_fqdn = True + def __init__(self, name, cfg, paths): distros.Distro.__init__(self, name, cfg, paths) # This will be used to restrict certain # calls from repeatly happening (when they # should only happen say once per instance...) self._runner = helpers.Runners(paths) - self.osfamily = 'redhat' - cfg['ssh_svcname'] = 'sshd' + self.osfamily = "redhat" + cfg["ssh_svcname"] = "sshd" def install_packages(self, pkglist): - self.package_command('install', pkgs=pkglist) - - def _write_network_config(self, netconfig): - return self._supported_write_network_config(netconfig) + self.package_command("install", pkgs=pkglist) def apply_locale(self, locale, out_fn=None): if self.uses_systemd(): @@ -74,29 +71,22 @@ class Distro(distros.Distro): if not out_fn: out_fn = self.locale_conf_fn locale_cfg = { - 'LANG': locale, + "LANG": locale, } rhel_util.update_sysconfig_file(out_fn, locale_cfg) - def _write_hostname(self, hostname, out_fn): + def _write_hostname(self, hostname, filename): # systemd will never update previous-hostname for us, so # we need to do it ourselves - if self.uses_systemd() and out_fn.endswith('/previous-hostname'): - util.write_file(out_fn, hostname) + if self.uses_systemd() and filename.endswith("/previous-hostname"): + util.write_file(filename, hostname) elif self.uses_systemd(): - subp.subp(['hostnamectl', 'set-hostname', str(hostname)]) + subp.subp(["hostnamectl", "set-hostname", str(hostname)]) else: host_cfg = { - 'HOSTNAME': hostname, + "HOSTNAME": hostname, } - rhel_util.update_sysconfig_file(out_fn, host_cfg) - - def _select_hostname(self, hostname, fqdn): - # Should be fqdn if we can use it - # See: https://www.centos.org/docs/5/html/Deployment_Guide-en-US/ch-sysconfig.html#s2-sysconfig-network # noqa - if fqdn: - return fqdn - return hostname + rhel_util.update_sysconfig_file(filename, host_cfg) def _read_system_hostname(self): if self.uses_systemd(): @@ -106,27 +96,21 @@ class Distro(distros.Distro): return (host_fn, self._read_hostname(host_fn)) def _read_hostname(self, filename, default=None): - if self.uses_systemd() and filename.endswith('/previous-hostname'): + if self.uses_systemd() and filename.endswith("/previous-hostname"): return util.load_file(filename).strip() elif self.uses_systemd(): - (out, _err) = subp.subp(['hostname']) + (out, _err) = subp.subp(["hostname"]) if len(out): return out else: return default else: (_exists, contents) = rhel_util.read_sysconfig_file(filename) - if 'HOSTNAME' in contents: - return contents['HOSTNAME'] + if "HOSTNAME" in contents: + return contents["HOSTNAME"] else: return default - def _bring_up_interfaces(self, device_names): - if device_names and 'all' in device_names: - raise RuntimeError(('Distro %s can not translate ' - 'the device name "all"') % (self.name)) - return distros.Distro._bring_up_interfaces(self, device_names) - def set_timezone(self, tz): tz_file = self._find_tz_file(tz) if self.uses_systemd(): @@ -137,7 +121,7 @@ class Distro(distros.Distro): else: # Adjust the sysconfig clock zone setting clock_cfg = { - 'ZONE': str(tz), + "ZONE": str(tz), } rhel_util.update_sysconfig_file(self.clock_conf_fn, clock_cfg) # This ensures that the correct tz will be used for the system @@ -147,18 +131,18 @@ class Distro(distros.Distro): if pkgs is None: pkgs = [] - if subp.which('dnf'): - LOG.debug('Using DNF for package management') - cmd = ['dnf'] + if subp.which("dnf"): + LOG.debug("Using DNF for package management") + cmd = ["dnf"] else: - LOG.debug('Using YUM for package management') + LOG.debug("Using YUM for package management") # the '-t' argument makes yum tolerant of errors on the command # line with regard to packages. # # For example: if you request to install foo, bar and baz and baz # is installed; yum won't error out complaining that baz is already # installed. - cmd = ['yum', '-t'] + cmd = ["yum", "-t"] # Determines whether or not yum prompts for confirmation # of critical actions. We don't want to prompt... cmd.append("-y") @@ -170,14 +154,19 @@ class Distro(distros.Distro): cmd.append(command) - pkglist = util.expand_package_list('%s-%s', pkgs) + pkglist = util.expand_package_list("%s-%s", pkgs) cmd.extend(pkglist) # Allow the output of this to flow outwards (ie not be captured) subp.subp(cmd, capture=False) def update_package_sources(self): - self._runner.run("update-sources", self.package_command, - ["makecache"], freq=PER_INSTANCE) + self._runner.run( + "update-sources", + self.package_command, + ["makecache"], + freq=PER_INSTANCE, + ) + # vi: ts=4 expandtab diff --git a/cloudinit/distros/rhel_util.py b/cloudinit/distros/rhel_util.py index d71394b4..c96f93b5 100644 --- a/cloudinit/distros/rhel_util.py +++ b/cloudinit/distros/rhel_util.py @@ -8,10 +8,9 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.distros.parsers.sys_conf import SysConf - from cloudinit import log as logging from cloudinit import util +from cloudinit.distros.parsers.sys_conf import SysConf LOG = logging.getLogger(__name__) @@ -49,4 +48,5 @@ def read_sysconfig_file(fn): contents = [] return (exists, SysConf(contents)) + # vi: ts=4 expandtab diff --git a/cloudinit/distros/rocky.py b/cloudinit/distros/rocky.py new file mode 100644 index 00000000..3dc0a342 --- /dev/null +++ b/cloudinit/distros/rocky.py @@ -0,0 +1,10 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + + +# vi: ts=4 expandtab diff --git a/cloudinit/distros/sles.py b/cloudinit/distros/sles.py index f3bfb9c2..484214e7 100644 --- a/cloudinit/distros/sles.py +++ b/cloudinit/distros/sles.py @@ -10,4 +10,5 @@ from cloudinit.distros import opensuse class Distro(opensuse.Distro): pass + # vi: ts=4 expandtab diff --git a/cloudinit/distros/tests/__init__.py b/cloudinit/distros/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 --- a/cloudinit/distros/tests/__init__.py +++ /dev/null diff --git a/cloudinit/distros/tests/test_init.py b/cloudinit/distros/tests/test_init.py deleted file mode 100644 index db534654..00000000 --- a/cloudinit/distros/tests/test_init.py +++ /dev/null @@ -1,156 +0,0 @@ -# Copyright (C) 2020 Canonical Ltd. -# -# Author: Daniel Watkins <oddbloke@ubuntu.com> -# -# This file is part of cloud-init. See LICENSE file for license information. -"""Tests for cloudinit/distros/__init__.py""" - -from unittest import mock - -import pytest - -from cloudinit.distros import _get_package_mirror_info, LDH_ASCII_CHARS - - -# Define a set of characters we would expect to be replaced -INVALID_URL_CHARS = [ - chr(x) for x in range(127) if chr(x) not in LDH_ASCII_CHARS -] -for separator in [":", ".", "/", "#", "?", "@", "[", "]"]: - # Remove from the set characters that either separate hostname parts (":", - # "."), terminate hostnames ("/", "#", "?", "@"), or cause Python to be - # unable to parse URLs ("[", "]"). - INVALID_URL_CHARS.remove(separator) - - -class TestGetPackageMirrorInfo: - """ - Tests for cloudinit.distros._get_package_mirror_info. - - These supplement the tests in tests/unittests/test_distros/test_generic.py - which are more focused on testing a single production-like configuration. - These tests are more focused on specific aspects of the unit under test. - """ - - @pytest.mark.parametrize('mirror_info,expected', [ - # Empty info gives empty return - ({}, {}), - # failsafe values used if present - ({'failsafe': {'primary': 'http://value', 'security': 'http://other'}}, - {'primary': 'http://value', 'security': 'http://other'}), - # search values used if present - ({'search': {'primary': ['http://value'], - 'security': ['http://other']}}, - {'primary': ['http://value'], 'security': ['http://other']}), - # failsafe values used if search value not present - ({'search': {'primary': ['http://value']}, - 'failsafe': {'security': 'http://other'}}, - {'primary': ['http://value'], 'security': 'http://other'}) - ]) - def test_get_package_mirror_info_failsafe(self, mirror_info, expected): - """ - Test the interaction between search and failsafe inputs - - (This doesn't test the case where the mirror_filter removes all search - options; test_failsafe_used_if_all_search_results_filtered_out covers - that.) - """ - assert expected == _get_package_mirror_info(mirror_info, - mirror_filter=lambda x: x) - - def test_failsafe_used_if_all_search_results_filtered_out(self): - """Test the failsafe option used if all search options eliminated.""" - mirror_info = { - 'search': {'primary': ['http://value']}, - 'failsafe': {'primary': 'http://other'} - } - assert {'primary': 'http://other'} == _get_package_mirror_info( - mirror_info, mirror_filter=lambda x: False) - - @pytest.mark.parametrize('allow_ec2_mirror, platform_type', [ - (True, 'ec2') - ]) - @pytest.mark.parametrize('availability_zone,region,patterns,expected', ( - # Test ec2_region alone - ('fk-fake-1f', None, ['http://EC2-%(ec2_region)s/ubuntu'], - ['http://ec2-fk-fake-1/ubuntu']), - # Test availability_zone alone - ('fk-fake-1f', None, ['http://AZ-%(availability_zone)s/ubuntu'], - ['http://az-fk-fake-1f/ubuntu']), - # Test region alone - (None, 'fk-fake-1', ['http://RG-%(region)s/ubuntu'], - ['http://rg-fk-fake-1/ubuntu']), - # Test that ec2_region is not available for non-matching AZs - ('fake-fake-1f', None, - ['http://EC2-%(ec2_region)s/ubuntu', - 'http://AZ-%(availability_zone)s/ubuntu'], - ['http://az-fake-fake-1f/ubuntu']), - # Test that template order maintained - (None, 'fake-region', - ['http://RG-%(region)s-2/ubuntu', 'http://RG-%(region)s-1/ubuntu'], - ['http://rg-fake-region-2/ubuntu', 'http://rg-fake-region-1/ubuntu']), - # Test that non-ASCII hostnames are IDNA encoded; - # "IDNA-ТεЅТ̣".encode('idna') == b"xn--idna--4kd53hh6aba3q" - (None, 'ТεЅТ̣', ['http://www.IDNA-%(region)s.com/ubuntu'], - ['http://www.xn--idna--4kd53hh6aba3q.com/ubuntu']), - # Test that non-ASCII hostnames with a port are IDNA encoded; - # "IDNA-ТεЅТ̣".encode('idna') == b"xn--idna--4kd53hh6aba3q" - (None, 'ТεЅТ̣', ['http://www.IDNA-%(region)s.com:8080/ubuntu'], - ['http://www.xn--idna--4kd53hh6aba3q.com:8080/ubuntu']), - # Test that non-ASCII non-hostname parts of URLs are unchanged - (None, 'ТεЅТ̣', ['http://www.example.com/%(region)s/ubuntu'], - ['http://www.example.com/ТεЅТ̣/ubuntu']), - # Test that IPv4 addresses are unchanged - (None, 'fk-fake-1', ['http://192.168.1.1:8080/%(region)s/ubuntu'], - ['http://192.168.1.1:8080/fk-fake-1/ubuntu']), - # Test that IPv6 addresses are unchanged - (None, 'fk-fake-1', - ['http://[2001:67c:1360:8001::23]/%(region)s/ubuntu'], - ['http://[2001:67c:1360:8001::23]/fk-fake-1/ubuntu']), - # Test that unparseable URLs are filtered out of the mirror list - (None, 'inv[lid', - ['http://%(region)s.in.hostname/should/be/filtered', - 'http://but.not.in.the.path/%(region)s'], - ['http://but.not.in.the.path/inv[lid']), - (None, '-some-region-', - ['http://-lead-ing.%(region)s.trail-ing-.example.com/ubuntu'], - ['http://lead-ing.some-region.trail-ing.example.com/ubuntu']), - ) + tuple( - # Dynamically generate a test case for each non-LDH - # (Letters/Digits/Hyphen) ASCII character, testing that it is - # substituted with a hyphen - (None, 'fk{0}fake{0}1'.format(invalid_char), - ['http://%(region)s/ubuntu'], ['http://fk-fake-1/ubuntu']) - for invalid_char in INVALID_URL_CHARS - )) - def test_valid_substitution(self, - allow_ec2_mirror, - platform_type, - availability_zone, - region, - patterns, - expected): - """Test substitution works as expected.""" - flag_path = "cloudinit.distros." \ - "ALLOW_EC2_MIRRORS_ON_NON_AWS_INSTANCE_TYPES" - - m_data_source = mock.Mock( - availability_zone=availability_zone, - region=region, - platform_type=platform_type - ) - mirror_info = {'search': {'primary': patterns}} - - with mock.patch(flag_path, allow_ec2_mirror): - ret = _get_package_mirror_info( - mirror_info, - data_source=m_data_source, - mirror_filter=lambda x: x - ) - print(allow_ec2_mirror) - print(platform_type) - print(availability_zone) - print(region) - print(patterns) - print(expected) - assert {'primary': expected} == ret diff --git a/cloudinit/distros/tests/test_networking.py b/cloudinit/distros/tests/test_networking.py deleted file mode 100644 index ec508f4d..00000000 --- a/cloudinit/distros/tests/test_networking.py +++ /dev/null @@ -1,223 +0,0 @@ -from unittest import mock - -import pytest - -from cloudinit import net -from cloudinit.distros.networking import ( - BSDNetworking, - LinuxNetworking, - Networking, -) - -# See https://docs.pytest.org/en/stable/example -# /parametrize.html#parametrizing-conditional-raising -from contextlib import ExitStack as does_not_raise - - -@pytest.yield_fixture -def generic_networking_cls(): - """Returns a direct Networking subclass which errors on /sys usage. - - This enables the direct testing of functionality only present on the - ``Networking`` super-class, and provides a check on accidentally using /sys - in that context. - """ - - class TestNetworking(Networking): - def is_physical(self, *args, **kwargs): - raise NotImplementedError - - def settle(self, *args, **kwargs): - raise NotImplementedError - - def try_set_link_up(self, *args, **kwargs): - raise NotImplementedError - - error = AssertionError("Unexpectedly used /sys in generic networking code") - with mock.patch( - "cloudinit.net.get_sys_class_path", side_effect=error, - ): - yield TestNetworking - - -@pytest.yield_fixture -def sys_class_net(tmpdir): - sys_class_net_path = tmpdir.join("sys/class/net") - sys_class_net_path.ensure_dir() - with mock.patch( - "cloudinit.net.get_sys_class_path", - return_value=sys_class_net_path.strpath + "/", - ): - yield sys_class_net_path - - -class TestBSDNetworkingIsPhysical: - def test_raises_notimplementederror(self): - with pytest.raises(NotImplementedError): - BSDNetworking().is_physical("eth0") - - -class TestLinuxNetworkingIsPhysical: - def test_returns_false_by_default(self, sys_class_net): - assert not LinuxNetworking().is_physical("eth0") - - def test_returns_false_if_devname_exists_but_not_physical( - self, sys_class_net - ): - devname = "eth0" - sys_class_net.join(devname).mkdir() - assert not LinuxNetworking().is_physical(devname) - - def test_returns_true_if_device_is_physical(self, sys_class_net): - devname = "eth0" - device_dir = sys_class_net.join(devname) - device_dir.mkdir() - device_dir.join("device").write("") - - assert LinuxNetworking().is_physical(devname) - - -class TestBSDNetworkingTrySetLinkUp: - def test_raises_notimplementederror(self): - with pytest.raises(NotImplementedError): - BSDNetworking().try_set_link_up("eth0") - - -@mock.patch("cloudinit.net.is_up") -@mock.patch("cloudinit.distros.networking.subp.subp") -class TestLinuxNetworkingTrySetLinkUp: - def test_calls_subp_return_true(self, m_subp, m_is_up): - devname = "eth0" - m_is_up.return_value = True - is_success = LinuxNetworking().try_set_link_up(devname) - - assert (mock.call(['ip', 'link', 'set', devname, 'up']) == - m_subp.call_args_list[-1]) - assert is_success - - def test_calls_subp_return_false(self, m_subp, m_is_up): - devname = "eth0" - m_is_up.return_value = False - is_success = LinuxNetworking().try_set_link_up(devname) - - assert (mock.call(['ip', 'link', 'set', devname, 'up']) == - m_subp.call_args_list[-1]) - assert not is_success - - -class TestBSDNetworkingSettle: - def test_settle_doesnt_error(self): - # This also implicitly tests that it doesn't use subp.subp - BSDNetworking().settle() - - -@pytest.mark.usefixtures("sys_class_net") -@mock.patch("cloudinit.distros.networking.util.udevadm_settle", autospec=True) -class TestLinuxNetworkingSettle: - def test_no_arguments(self, m_udevadm_settle): - LinuxNetworking().settle() - - assert [mock.call(exists=None)] == m_udevadm_settle.call_args_list - - def test_exists_argument(self, m_udevadm_settle): - LinuxNetworking().settle(exists="ens3") - - expected_path = net.sys_dev_path("ens3") - assert [ - mock.call(exists=expected_path) - ] == m_udevadm_settle.call_args_list - - -class TestNetworkingWaitForPhysDevs: - @pytest.fixture - def wait_for_physdevs_netcfg(self): - """This config is shared across all the tests in this class.""" - - def ethernet(mac, name, driver=None, device_id=None): - v2_cfg = {"set-name": name, "match": {"macaddress": mac}} - if driver: - v2_cfg["match"].update({"driver": driver}) - if device_id: - v2_cfg["match"].update({"device_id": device_id}) - - return v2_cfg - - physdevs = [ - ["aa:bb:cc:dd:ee:ff", "eth0", "virtio", "0x1000"], - ["00:11:22:33:44:55", "ens3", "e1000", "0x1643"], - ] - netcfg = { - "version": 2, - "ethernets": {args[1]: ethernet(*args) for args in physdevs}, - } - return netcfg - - def test_skips_settle_if_all_present( - self, generic_networking_cls, wait_for_physdevs_netcfg, - ): - networking = generic_networking_cls() - with mock.patch.object( - networking, "get_interfaces_by_mac" - ) as m_get_interfaces_by_mac: - m_get_interfaces_by_mac.side_effect = iter( - [{"aa:bb:cc:dd:ee:ff": "eth0", "00:11:22:33:44:55": "ens3"}] - ) - with mock.patch.object( - networking, "settle", autospec=True - ) as m_settle: - networking.wait_for_physdevs(wait_for_physdevs_netcfg) - assert 0 == m_settle.call_count - - def test_calls_udev_settle_on_missing( - self, generic_networking_cls, wait_for_physdevs_netcfg, - ): - networking = generic_networking_cls() - with mock.patch.object( - networking, "get_interfaces_by_mac" - ) as m_get_interfaces_by_mac: - m_get_interfaces_by_mac.side_effect = iter( - [ - { - "aa:bb:cc:dd:ee:ff": "eth0" - }, # first call ens3 is missing - { - "aa:bb:cc:dd:ee:ff": "eth0", - "00:11:22:33:44:55": "ens3", - }, # second call has both - ] - ) - with mock.patch.object( - networking, "settle", autospec=True - ) as m_settle: - networking.wait_for_physdevs(wait_for_physdevs_netcfg) - m_settle.assert_called_with(exists="ens3") - - @pytest.mark.parametrize( - "strict,expectation", - [(True, pytest.raises(RuntimeError)), (False, does_not_raise())], - ) - def test_retrying_and_strict_behaviour( - self, - strict, - expectation, - generic_networking_cls, - wait_for_physdevs_netcfg, - ): - networking = generic_networking_cls() - with mock.patch.object( - networking, "get_interfaces_by_mac" - ) as m_get_interfaces_by_mac: - m_get_interfaces_by_mac.return_value = {} - - with mock.patch.object( - networking, "settle", autospec=True - ) as m_settle: - with expectation: - networking.wait_for_physdevs( - wait_for_physdevs_netcfg, strict=strict - ) - - assert ( - 5 * len(wait_for_physdevs_netcfg["ethernets"]) - == m_settle.call_count - ) diff --git a/cloudinit/distros/ubuntu.py b/cloudinit/distros/ubuntu.py index 2a1f93d9..ec6470a9 100644 --- a/cloudinit/distros/ubuntu.py +++ b/cloudinit/distros/ubuntu.py @@ -9,41 +9,44 @@ # # This file is part of cloud-init. See LICENSE file for license information. -from cloudinit.distros import debian -from cloudinit.distros import PREFERRED_NTP_CLIENTS -from cloudinit import util - import copy +from cloudinit import util +from cloudinit.distros import PREFERRED_NTP_CLIENTS, debian -class Distro(debian.Distro): +class Distro(debian.Distro): def __init__(self, name, cfg, paths): super(Distro, self).__init__(name, cfg, paths) # Ubuntu specific network cfg locations self.network_conf_fn = { "eni": "/etc/network/interfaces.d/50-cloud-init.cfg", - "netplan": "/etc/netplan/50-cloud-init.yaml" + "netplan": "/etc/netplan/50-cloud-init.yaml", } self.renderer_configs = { - "eni": {"eni_path": self.network_conf_fn["eni"], - "eni_header": debian.NETWORK_FILE_HEADER}, - "netplan": {"netplan_path": self.network_conf_fn["netplan"], - "netplan_header": debian.NETWORK_FILE_HEADER, - "postcmds": True} + "eni": { + "eni_path": self.network_conf_fn["eni"], + "eni_header": debian.NETWORK_FILE_HEADER, + }, + "netplan": { + "netplan_path": self.network_conf_fn["netplan"], + "netplan_header": debian.NETWORK_FILE_HEADER, + "postcmds": True, + }, } @property def preferred_ntp_clients(self): """The preferred ntp client is dependent on the version.""" if not self._preferred_ntp_clients: - (_name, _version, codename) = util.system_info()['dist'] + (_name, _version, codename) = util.system_info()["dist"] # Xenial cloud-init only installed ntp, UbuntuCore has timesyncd. if codename == "xenial" and not util.system_is_snappy(): - self._preferred_ntp_clients = ['ntp'] + self._preferred_ntp_clients = ["ntp"] else: - self._preferred_ntp_clients = ( - copy.deepcopy(PREFERRED_NTP_CLIENTS)) + self._preferred_ntp_clients = copy.deepcopy( + PREFERRED_NTP_CLIENTS + ) return self._preferred_ntp_clients diff --git a/cloudinit/distros/ug_util.py b/cloudinit/distros/ug_util.py index 08446a95..72766392 100755 --- a/cloudinit/distros/ug_util.py +++ b/cloudinit/distros/ug_util.py @@ -10,92 +10,80 @@ # This file is part of cloud-init. See LICENSE file for license information. from cloudinit import log as logging -from cloudinit import type_utils -from cloudinit import util +from cloudinit import type_utils, util LOG = logging.getLogger(__name__) -# Normalizes a input group configuration -# which can be a comma seperated list of -# group names, or a list of group names -# or a python dictionary of group names -# to a list of members of that group. +# Normalizes an input group configuration which can be: +# Comma seperated string or a list or a dictionary # -# The output is a dictionary of group -# names => members of that group which -# is the standard form used in the rest -# of cloud-init +# Returns dictionary of group names => members of that group which is the +# standard form used in the rest of cloud-init def _normalize_groups(grp_cfg): if isinstance(grp_cfg, str): grp_cfg = grp_cfg.strip().split(",") + if isinstance(grp_cfg, list): c_grp_cfg = {} for i in grp_cfg: if isinstance(i, dict): for k, v in i.items(): - if k not in c_grp_cfg: - if isinstance(v, list): - c_grp_cfg[k] = list(v) - elif isinstance(v, str): - c_grp_cfg[k] = [v] - else: - raise TypeError("Bad group member type %s" % - type_utils.obj_name(v)) + if not isinstance(v, (list, str)): + raise TypeError( + "Bad group member type %s" + % (type_utils.obj_name(v)) + ) + + if isinstance(v, list): + c_grp_cfg.setdefault(k, []).extend(v) else: - if isinstance(v, list): - c_grp_cfg[k].extend(v) - elif isinstance(v, str): - c_grp_cfg[k].append(v) - else: - raise TypeError("Bad group member type %s" % - type_utils.obj_name(v)) + c_grp_cfg.setdefault(k, []).append(v) elif isinstance(i, str): if i not in c_grp_cfg: c_grp_cfg[i] = [] else: - raise TypeError("Unknown group name type %s" % - type_utils.obj_name(i)) + raise TypeError( + "Unknown group name type %s" % (type_utils.obj_name(i)) + ) grp_cfg = c_grp_cfg + groups = {} if isinstance(grp_cfg, dict): - for (grp_name, grp_members) in grp_cfg.items(): + for grp_name, grp_members in grp_cfg.items(): groups[grp_name] = util.uniq_merge_sorted(grp_members) else: - raise TypeError(("Group config must be list, dict " - " or string types only and not %s") % - type_utils.obj_name(grp_cfg)) + raise TypeError( + "Group config must be list, dict or string type only but found %s" + % (type_utils.obj_name(grp_cfg)) + ) return groups -# Normalizes a input group configuration -# which can be a comma seperated list of -# user names, or a list of string user names -# or a list of dictionaries with components -# that define the user config + 'name' (if -# a 'name' field does not exist then the -# default user is assumed to 'own' that -# configuration. +# Normalizes an input group configuration which can be: a list or a dictionary +# +# components that define the user config + 'name' (if a 'name' field does not +# exist then the default user is assumed to 'own' that configuration.) # -# The output is a dictionary of user -# names => user config which is the standard -# form used in the rest of cloud-init. Note -# the default user will have a special config -# entry 'default' which will be marked as true -# all other users will be marked as false. +# Returns a dictionary of user names => user config which is the standard form +# used in the rest of cloud-init. Note the default user will have a special +# config entry 'default' which will be marked true and all other users will be +# marked false. def _normalize_users(u_cfg, def_user_cfg=None): if isinstance(u_cfg, dict): ad_ucfg = [] - for (k, v) in u_cfg.items(): + for k, v in u_cfg.items(): if isinstance(v, (bool, int, float, str)): if util.is_true(v): ad_ucfg.append(str(k)) elif isinstance(v, dict): - v['name'] = k + v["name"] = k ad_ucfg.append(v) else: - raise TypeError(("Unmappable user value type %s" - " for key %s") % (type_utils.obj_name(v), k)) + raise TypeError( + "Unmappable user value type %s for key %s" + % (type_utils.obj_name(v), k) + ) u_cfg = ad_ucfg elif isinstance(u_cfg, str): u_cfg = util.uniq_merge_sorted(u_cfg) @@ -107,181 +95,157 @@ def _normalize_users(u_cfg, def_user_cfg=None): if u and u not in users: users[u] = {} elif isinstance(user_config, dict): - if 'name' in user_config: - n = user_config.pop('name') - prev_config = users.get(n) or {} - users[n] = util.mergemanydict([prev_config, - user_config]) - else: - # Assume the default user then - prev_config = users.get('default') or {} - users['default'] = util.mergemanydict([prev_config, - user_config]) + n = user_config.pop("name", "default") + prev_config = users.get(n) or {} + users[n] = util.mergemanydict([prev_config, user_config]) else: - raise TypeError(("User config must be dictionary/list " - " or string types only and not %s") % - type_utils.obj_name(user_config)) + raise TypeError( + "User config must be dictionary/list or string " + " types only and not %s" % (type_utils.obj_name(user_config)) + ) # Ensure user options are in the right python friendly format if users: c_users = {} - for (uname, uconfig) in users.items(): + for uname, uconfig in users.items(): c_uconfig = {} - for (k, v) in uconfig.items(): - k = k.replace('-', '_').strip() + for k, v in uconfig.items(): + k = k.replace("-", "_").strip() if k: c_uconfig[k] = v c_users[uname] = c_uconfig users = c_users - # Fixup the default user into the real - # default user name and replace it... + # Fix the default user into the actual default user name and replace it. def_user = None - if users and 'default' in users: - def_config = users.pop('default') + if users and "default" in users: + def_config = users.pop("default") if def_user_cfg: - # Pickup what the default 'real name' is - # and any groups that are provided by the - # default config + # Pickup what the default 'real name' is and any groups that are + # provided by the default config def_user_cfg = def_user_cfg.copy() - def_user = def_user_cfg.pop('name') - def_groups = def_user_cfg.pop('groups', []) - # Pickup any config + groups for that user name - # that we may have previously extracted + def_user = def_user_cfg.pop("name") + def_groups = def_user_cfg.pop("groups", []) + # Pick any config + groups for the user name that we may have + # extracted previously parsed_config = users.pop(def_user, {}) - parsed_groups = parsed_config.get('groups', []) - # Now merge our extracted groups with - # anything the default config provided + parsed_groups = parsed_config.get("groups", []) + # Now merge the extracted groups with the default config provided users_groups = util.uniq_merge_sorted(parsed_groups, def_groups) - parsed_config['groups'] = ",".join(users_groups) - # The real config for the default user is the - # combination of the default user config provided - # by the distro, the default user config provided - # by the above merging for the user 'default' and - # then the parsed config from the user's 'real name' - # which does not have to be 'default' (but could be) - users[def_user] = util.mergemanydict([def_user_cfg, - def_config, - parsed_config]) - - # Ensure that only the default user that we - # found (if any) is actually marked as being - # the default user - if users: - for (uname, uconfig) in users.items(): - if def_user and uname == def_user: - uconfig['default'] = True - else: - uconfig['default'] = False + parsed_config["groups"] = ",".join(users_groups) + # The real config for the default user is the combination of the + # default user config provided by the distro, the default user + # config provided by the above merging for the user 'default' and + # then the parsed config from the user's 'real name' which does not + # have to be 'default' (but could be) + users[def_user] = util.mergemanydict( + [def_user_cfg, def_config, parsed_config] + ) + + # Ensure that only the default user that we found (if any) is actually + # marked as the default user + for uname, uconfig in users.items(): + uconfig["default"] = uname == def_user if def_user else False return users -# Normalizes a set of user/users and group -# dictionary configuration into a useable -# format that the rest of cloud-init can -# understand using the default user -# provided by the input distrobution (if any) -# to allow for mapping of the 'default' user. +# Normalizes a set of user/users and group dictionary configuration into an +# usable format so that the rest of cloud-init can understand using the default +# user provided by the input distribution (if any) to allow mapping of the +# 'default' user. # # Output is a dictionary of group names -> [member] (list) # and a dictionary of user names -> user configuration (dict) # -# If 'user' exists it will override -# the 'users'[0] entry (if a list) otherwise it will -# just become an entry in the returned dictionary (no override) +# If 'user' exists, it will override +# The 'users'[0] entry (if a list) otherwise it will just become an entry in +# the returned dictionary (no override) def normalize_users_groups(cfg, distro): if not cfg: cfg = {} - users = {} - groups = {} - if 'groups' in cfg: - groups = _normalize_groups(cfg['groups']) - # Handle the previous style of doing this where the first user # overrides the concept of the default user if provided in the user: XYZ # format. old_user = {} - if 'user' in cfg and cfg['user']: - old_user = cfg['user'] - # Translate it into the format that is more useful - # going forward + if "user" in cfg and cfg["user"]: + old_user = cfg["user"] + # Translate it into a format that will be more useful going forward if isinstance(old_user, str): - old_user = { - 'name': old_user, - } - if not isinstance(old_user, dict): - LOG.warning(("Format for 'user' key must be a string or dictionary" - " and not %s"), type_utils.obj_name(old_user)) + old_user = {"name": old_user} + elif not isinstance(old_user, dict): + LOG.warning( + "Format for 'user' key must be a string or dictionary" + " and not %s", + type_utils.obj_name(old_user), + ) old_user = {} - # If no old user format, then assume the distro - # provides what the 'default' user maps to, but notice - # that if this is provided, we won't automatically inject - # a 'default' user into the users list, while if a old user - # format is provided we will. + # If no old user format, then assume the distro provides what the 'default' + # user maps to, but notice that if this is provided, we won't automatically + # inject a 'default' user into the users list, while if an old user format + # is provided we will. distro_user_config = {} try: distro_user_config = distro.get_default_user() except NotImplementedError: - LOG.warning(("Distro has not implemented default user " - "access. No distribution provided default user" - " will be normalized.")) - - # Merge the old user (which may just be an empty dict when not - # present with the distro provided default user configuration so - # that the old user style picks up all the distribution specific - # attributes (if any) + LOG.warning( + "Distro has not implemented default user access. No " + "distribution provided default user will be normalized." + ) + + # Merge the old user (which may just be an empty dict when not present) + # with the distro provided default user configuration so that the old user + # style picks up all the distribution specific attributes (if any) default_user_config = util.mergemanydict([old_user, distro_user_config]) - base_users = cfg.get('users', []) + base_users = cfg.get("users", []) if not isinstance(base_users, (list, dict, str)): - LOG.warning(("Format for 'users' key must be a comma separated string" - " or a dictionary or a list and not %s"), - type_utils.obj_name(base_users)) + LOG.warning( + "Format for 'users' key must be a comma separated string" + " or a dictionary or a list but found %s", + type_utils.obj_name(base_users), + ) base_users = [] if old_user: - # Ensure that when user: is provided that this user - # always gets added (as the default user) + # When 'user:' is provided, it should be made as the default user if isinstance(base_users, list): - # Just add it on at the end... - base_users.append({'name': 'default'}) + base_users.append({"name": "default"}) elif isinstance(base_users, dict): - base_users['default'] = dict(base_users).get('default', True) + base_users["default"] = dict(base_users).get("default", True) elif isinstance(base_users, str): - # Just append it on to be re-parsed later base_users += ",default" + groups = {} + if "groups" in cfg: + groups = _normalize_groups(cfg["groups"]) + users = _normalize_users(base_users, default_user_config) return (users, groups) -# Given a user dictionary config it will -# extract the default user name and user config -# from that list and return that tuple or -# return (None, None) if no default user is -# found in the given input +# Given a user dictionary config, extract the default user name and user config +# and return them or return (None, None) if no default user is found def extract_default(users, default_name=None, default_config=None): if not users: - users = {} + return (default_name, default_config) def safe_find(entry): config = entry[1] - if not config or 'default' not in config: + if not config or "default" not in config: return False - else: - return config['default'] + return config["default"] - tmp_users = users.items() - tmp_users = dict(filter(safe_find, tmp_users)) + tmp_users = dict(filter(safe_find, users.items())) if not tmp_users: return (default_name, default_config) - else: - name = list(tmp_users)[0] - config = tmp_users[name] - config.pop('default', None) - return (name, config) + + name = list(tmp_users)[0] + config = tmp_users[name] + config.pop("default", None) + return (name, config) + # vi: ts=4 expandtab diff --git a/cloudinit/distros/virtuozzo.py b/cloudinit/distros/virtuozzo.py new file mode 100644 index 00000000..3dc0a342 --- /dev/null +++ b/cloudinit/distros/virtuozzo.py @@ -0,0 +1,10 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + + +# vi: ts=4 expandtab |