summaryrefslogtreecommitdiff
path: root/cloudinit/config
diff options
context:
space:
mode:
authordermotbradley <dermot_bradley@yahoo.com>2020-08-20 00:18:25 +0100
committerGitHub <noreply@github.com>2020-08-19 18:18:25 -0500
commit79a8ce7e714ae1686c10bff77612eab0f6eccc95 (patch)
tree5bf05e746bb91f6a21bd549a1fc579d2c9cd1940 /cloudinit/config
parentb749548a9eb43b34cce64f8688107645411abc8c (diff)
downloadcloud-init-git-79a8ce7e714ae1686c10bff77612eab0f6eccc95.tar.gz
Add Alpine Linux support. (#535)
Add new module cc_apk_configure for creating Alpine /etc/apk/repositories file. Modify cc_ca_certs, cc_ntp, cc_power_state_change, and cc_resolv_conf for Alpine. Add Alpine template files for Chrony and Busybox NTP support. Add Alpine template file for /etc/hosts.
Diffstat (limited to 'cloudinit/config')
-rw-r--r--cloudinit/config/cc_apk_configure.py263
-rw-r--r--cloudinit/config/cc_ca_certs.py22
-rw-r--r--cloudinit/config/cc_ntp.py61
-rw-r--r--cloudinit/config/cc_power_state_change.py57
-rw-r--r--cloudinit/config/cc_resolv_conf.py4
5 files changed, 370 insertions, 37 deletions
diff --git a/cloudinit/config/cc_apk_configure.py b/cloudinit/config/cc_apk_configure.py
new file mode 100644
index 00000000..84d7a0b6
--- /dev/null
+++ b/cloudinit/config/cc_apk_configure.py
@@ -0,0 +1,263 @@
+# Copyright (c) 2020 Dermot Bradley
+#
+# Author: Dermot Bradley <dermot_bradley@yahoo.com>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+"""Apk Configure: Configures apk repositories file."""
+
+from textwrap import dedent
+
+from cloudinit import log as logging
+from cloudinit import temp_utils
+from cloudinit import templater
+from cloudinit import util
+from cloudinit.config.schema import (
+ get_schema_doc, validate_cloudconfig_schema)
+from cloudinit.settings import PER_INSTANCE
+
+LOG = logging.getLogger(__name__)
+
+
+# If no mirror is specified then use this one
+DEFAULT_MIRROR = "https://alpine.global.ssl.fastly.net/alpine"
+
+
+REPOSITORIES_TEMPLATE = """\
+## template:jinja
+#
+# Created by cloud-init
+#
+# This file is written on first boot of an instance
+#
+
+{{ alpine_baseurl }}/{{ alpine_version }}/main
+{% if community_enabled -%}
+{{ alpine_baseurl }}/{{ alpine_version }}/community
+{% endif -%}
+{% if testing_enabled -%}
+{% if alpine_version != 'edge' %}
+#
+# Testing - using with non-Edge installation may cause problems!
+#
+{% endif %}
+{{ alpine_baseurl }}/edge/testing
+{% endif %}
+{% if local_repo != '' %}
+
+#
+# Local repo
+#
+{{ local_repo }}/{{ alpine_version }}
+{% endif %}
+
+"""
+
+
+frequency = PER_INSTANCE
+distros = ['alpine']
+schema = {
+ 'id': 'cc_apk_configure',
+ 'name': 'APK Configure',
+ 'title': 'Configure apk repositories file',
+ 'description': dedent("""\
+ This module handles configuration of the /etc/apk/repositories file.
+
+ .. note::
+ To ensure that apk configuration is valid yaml, any strings
+ containing special characters, especially ``:`` should be quoted.
+ """),
+ 'distros': distros,
+ 'examples': [
+ dedent("""\
+ # Keep the existing /etc/apk/repositories file unaltered.
+ apk_repos:
+ preserve_repositories: true
+ """),
+ dedent("""\
+ # Create repositories file for Alpine v3.12 main and community
+ # using default mirror site.
+ apk_repos:
+ alpine_repo:
+ community_enabled: true
+ version: 'v3.12'
+ """),
+ dedent("""\
+ # Create repositories file for Alpine Edge main, community, and
+ # testing using a specified mirror site and also a local repo.
+ apk_repos:
+ alpine_repo:
+ base_url: 'https://some-alpine-mirror/alpine'
+ community_enabled: true
+ testing_enabled: true
+ version: 'edge'
+ local_repo_base_url: 'https://my-local-server/local-alpine'
+ """),
+ ],
+ 'frequency': frequency,
+ 'type': 'object',
+ 'properties': {
+ 'apk_repos': {
+ 'type': 'object',
+ 'properties': {
+ 'preserve_repositories': {
+ 'type': 'boolean',
+ 'default': False,
+ 'description': dedent("""\
+ By default, cloud-init will generate a new repositories
+ file ``/etc/apk/repositories`` based on any valid
+ configuration settings specified within a apk_repos
+ section of cloud config. To disable this behavior and
+ preserve the repositories file from the pristine image,
+ set ``preserve_repositories`` to ``true``.
+
+ The ``preserve_repositories`` option overrides
+ all other config keys that would alter
+ ``/etc/apk/repositories``.
+ """)
+ },
+ 'alpine_repo': {
+ 'type': ['object', 'null'],
+ 'properties': {
+ 'base_url': {
+ 'type': 'string',
+ 'default': DEFAULT_MIRROR,
+ 'description': dedent("""\
+ The base URL of an Alpine repository, or
+ mirror, to download official packages from.
+ If not specified then it defaults to ``{}``
+ """.format(DEFAULT_MIRROR))
+ },
+ 'community_enabled': {
+ 'type': 'boolean',
+ 'default': False,
+ 'description': dedent("""\
+ Whether to add the Community repo to the
+ repositories file. By default the Community
+ repo is not included.
+ """)
+ },
+ 'testing_enabled': {
+ 'type': 'boolean',
+ 'default': False,
+ 'description': dedent("""\
+ Whether to add the Testing repo to the
+ repositories file. By default the Testing
+ repo is not included. It is only recommended
+ to use the Testing repo on a machine running
+ the ``Edge`` version of Alpine as packages
+ installed from Testing may have dependancies
+ that conflict with those in non-Edge Main or
+ Community repos."
+ """)
+ },
+ 'version': {
+ 'type': 'string',
+ 'description': dedent("""\
+ The Alpine version to use (e.g. ``v3.12`` or
+ ``edge``)
+ """)
+ },
+ },
+ 'required': ['version'],
+ 'minProperties': 1,
+ 'additionalProperties': False,
+ },
+ 'local_repo_base_url': {
+ 'type': 'string',
+ 'description': dedent("""\
+ The base URL of an Alpine repository containing
+ unofficial packages
+ """)
+ }
+ },
+ 'required': [],
+ 'minProperties': 1, # Either preserve_repositories or alpine_repo
+ 'additionalProperties': False,
+ }
+ }
+}
+
+__doc__ = get_schema_doc(schema)
+
+
+def handle(name, cfg, cloud, log, _args):
+ """
+ Call to handle apk_repos sections in cloud-config file.
+
+ @param name: The module name "apk-configure" from cloud.cfg
+ @param cfg: A nested dict containing the entire cloud config contents.
+ @param cloud: The CloudInit object in use.
+ @param log: Pre-initialized Python logger object to use for logging.
+ @param _args: Any module arguments from cloud.cfg
+ """
+
+ # If there is no "apk_repos" section in the configuration
+ # then do nothing.
+ apk_section = cfg.get('apk_repos')
+ if not apk_section:
+ LOG.debug(("Skipping module named %s,"
+ " no 'apk_repos' section found"), name)
+ return
+
+ validate_cloudconfig_schema(cfg, schema)
+
+ # If "preserve_repositories" is explicitly set to True in
+ # the configuration do nothing.
+ if util.get_cfg_option_bool(apk_section, 'preserve_repositories', False):
+ LOG.debug(("Skipping module named %s,"
+ " 'preserve_repositories' is set"), name)
+ return
+
+ # If there is no "alpine_repo" subsection of "apk_repos" present in the
+ # configuration then do nothing, as at least "version" is required to
+ # create valid repositories entries.
+ alpine_repo = apk_section.get('alpine_repo')
+ if not alpine_repo:
+ LOG.debug(("Skipping module named %s,"
+ " no 'alpine_repo' configuration found"), name)
+ return
+
+ # If there is no "version" value present in configuration then do nothing.
+ alpine_version = alpine_repo.get('version')
+ if not alpine_version:
+ LOG.debug(("Skipping module named %s,"
+ " 'version' not specified in alpine_repo"), name)
+ return
+
+ local_repo = apk_section.get('local_repo_base_url', '')
+
+ _write_repositories_file(alpine_repo, alpine_version, local_repo)
+
+
+def _write_repositories_file(alpine_repo, alpine_version, local_repo):
+ """
+ Write the /etc/apk/repositories file with the specified entries.
+
+ @param alpine_repo: A nested dict of the alpine_repo configuration.
+ @param alpine_version: A string of the Alpine version to use.
+ @param local_repo: A string containing the base URL of a local repo.
+ """
+
+ repo_file = '/etc/apk/repositories'
+
+ alpine_baseurl = alpine_repo.get('base_url', DEFAULT_MIRROR)
+
+ params = {'alpine_baseurl': alpine_baseurl,
+ 'alpine_version': alpine_version,
+ 'community_enabled': alpine_repo.get('community_enabled'),
+ 'testing_enabled': alpine_repo.get('testing_enabled'),
+ 'local_repo': local_repo}
+
+ tfile = temp_utils.mkstemp(prefix='template_name-', suffix=".tmpl")
+ template_fn = tfile[1] # Filepath is second item in tuple
+ util.write_file(template_fn, content=REPOSITORIES_TEMPLATE)
+
+ LOG.debug('Generating Alpine repository configuration file: %s',
+ repo_file)
+ templater.render_to_file(template_fn, repo_file, params)
+ # Clean up temporary template
+ util.del_file(template_fn)
+
+
+# vi: ts=4 expandtab
diff --git a/cloudinit/config/cc_ca_certs.py b/cloudinit/config/cc_ca_certs.py
index 910b78de..3c453d91 100644
--- a/cloudinit/config/cc_ca_certs.py
+++ b/cloudinit/config/cc_ca_certs.py
@@ -16,11 +16,16 @@ can be removed from the system with the configuration option
certificates must be specified using valid yaml. in order to specify a
multiline certificate, the yaml multiline list syntax must be used
+.. note::
+ For Alpine Linux the "remove-defaults" functionality works if the
+ ca-certificates package is installed but not if the
+ ca-certificates-bundle package is installed.
+
**Internal name:** ``cc_ca_certs``
**Module frequency:** per instance
-**Supported distros:** ubuntu, debian
+**Supported distros:** alpine, debian, ubuntu
**Config keys**::
@@ -45,7 +50,7 @@ CA_CERT_CONFIG = "/etc/ca-certificates.conf"
CA_CERT_SYSTEM_PATH = "/etc/ssl/certs/"
CA_CERT_FULL_PATH = os.path.join(CA_CERT_PATH, CA_CERT_FILENAME)
-distros = ['ubuntu', 'debian']
+distros = ['alpine', 'debian', 'ubuntu']
def update_ca_certs():
@@ -83,7 +88,7 @@ def add_ca_certs(certs):
util.write_file(CA_CERT_CONFIG, out, omode="wb")
-def remove_default_ca_certs():
+def remove_default_ca_certs(distro_name):
"""
Removes all default trusted CA certificates from the system. To actually
apply the change you must also call L{update_ca_certs}.
@@ -91,11 +96,14 @@ def remove_default_ca_certs():
util.delete_dir_contents(CA_CERT_PATH)
util.delete_dir_contents(CA_CERT_SYSTEM_PATH)
util.write_file(CA_CERT_CONFIG, "", mode=0o644)
- debconf_sel = "ca-certificates ca-certificates/trust_new_crts select no"
- subp.subp(('debconf-set-selections', '-'), debconf_sel)
+
+ if distro_name != 'alpine':
+ debconf_sel = (
+ "ca-certificates ca-certificates/trust_new_crts " + "select no")
+ subp.subp(('debconf-set-selections', '-'), debconf_sel)
-def handle(name, cfg, _cloud, log, _args):
+def handle(name, cfg, cloud, log, _args):
"""
Call to handle ca-cert sections in cloud-config file.
@@ -117,7 +125,7 @@ def handle(name, cfg, _cloud, log, _args):
# default trusted CA certs first.
if ca_cert_cfg.get("remove-defaults", False):
log.debug("Removing default certificates")
- remove_default_ca_certs()
+ remove_default_ca_certs(cloud.distro.name)
# If we are given any new trusted CA certs to add, add them.
if "trusted" in ca_cert_cfg:
diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py
index 7d3f73ff..3d7279d6 100644
--- a/cloudinit/config/cc_ntp.py
+++ b/cloudinit/config/cc_ntp.py
@@ -24,7 +24,8 @@ LOG = logging.getLogger(__name__)
frequency = PER_INSTANCE
NTP_CONF = '/etc/ntp.conf'
NR_POOL_SERVERS = 4
-distros = ['centos', 'debian', 'fedora', 'opensuse', 'rhel', 'sles', 'ubuntu']
+distros = ['alpine', 'centos', 'debian', 'fedora', 'opensuse', 'rhel',
+ 'sles', 'ubuntu']
NTP_CLIENT_CONFIG = {
'chrony': {
@@ -63,6 +64,17 @@ NTP_CLIENT_CONFIG = {
# This is Distro-specific configuration overrides of the base config
DISTRO_CLIENT_CONFIG = {
+ 'alpine': {
+ 'chrony': {
+ 'confpath': '/etc/chrony/chrony.conf',
+ 'service_name': 'chronyd',
+ },
+ 'ntp': {
+ 'confpath': '/etc/ntp.conf',
+ 'packages': [],
+ 'service_name': 'ntpd',
+ },
+ },
'debian': {
'chrony': {
'confpath': '/etc/chrony/chrony.conf',
@@ -114,11 +126,11 @@ schema = {
Handle ntp configuration. If ntp is not installed on the system and
ntp configuration is specified, ntp will be installed. If there is a
default ntp config file in the image or one is present in the
- distro's ntp package, it will be copied to ``/etc/ntp.conf.dist``
- before any changes are made. A list of ntp pools and ntp servers can
- be provided under the ``ntp`` config key. If no ntp ``servers`` or
- ``pools`` are provided, 4 pools will be used in the format
- ``{0-3}.{distro}.pool.ntp.org``."""),
+ distro's ntp package, it will be copied to a file with ``.dist``
+ appended to the filename before any changes are made. A list of ntp
+ pools and ntp servers can be provided under the ``ntp`` config key.
+ If no ntp ``servers`` or ``pools`` are provided, 4 pools will be used
+ in the format ``{0-3}.{distro}.pool.ntp.org``."""),
'distros': distros,
'examples': [
dedent("""\
@@ -171,7 +183,10 @@ schema = {
'description': dedent("""\
List of ntp pools. If both pools and servers are
empty, 4 default pool servers will be provided of
- the format ``{0-3}.{distro}.pool.ntp.org``.""")
+ the format ``{0-3}.{distro}.pool.ntp.org``. NOTE:
+ for Alpine Linux when using the Busybox NTP client
+ this setting will be ignored due to the limited
+ functionality of Busybox's ntpd.""")
},
'servers': {
'type': 'array',
@@ -364,21 +379,30 @@ def generate_server_names(distro):
"""
names = []
pool_distro = distro
- # For legal reasons x.pool.sles.ntp.org does not exist,
- # use the opensuse pool
+
if distro == 'sles':
+ # For legal reasons x.pool.sles.ntp.org does not exist,
+ # use the opensuse pool
pool_distro = 'opensuse'
+ elif distro == 'alpine':
+ # Alpine-specific pool (i.e. x.alpine.pool.ntp.org) does not exist
+ # so use general x.pool.ntp.org instead.
+ pool_distro = ''
+
for x in range(0, NR_POOL_SERVERS):
- name = "%d.%s.pool.ntp.org" % (x, pool_distro)
- names.append(name)
+ names.append(".".join(
+ [n for n in [str(x)] + [pool_distro] + ['pool.ntp.org'] if n]))
+
return names
-def write_ntp_config_template(distro_name, servers=None, pools=None,
- path=None, template_fn=None, template=None):
+def write_ntp_config_template(distro_name, service_name=None, servers=None,
+ pools=None, path=None, template_fn=None,
+ template=None):
"""Render a ntp client configuration for the specified client.
@param distro_name: string. The distro class name.
+ @param service_name: string. The name of the NTP client service.
@param servers: A list of strings specifying ntp servers. Defaults to empty
list.
@param pools: A list of strings specifying ntp pools. Defaults to empty
@@ -397,7 +421,14 @@ def write_ntp_config_template(distro_name, servers=None, pools=None,
if not pools:
pools = []
- if len(servers) == 0 and len(pools) == 0:
+ if (len(servers) == 0 and distro_name == 'alpine' and
+ service_name == 'ntpd'):
+ # Alpine's Busybox ntpd only understands "servers" configuration
+ # and not "pool" configuration.
+ servers = generate_server_names(distro_name)
+ LOG.debug(
+ 'Adding distro default ntp servers: %s', ','.join(servers))
+ elif len(servers) == 0 and len(pools) == 0:
pools = generate_server_names(distro_name)
LOG.debug(
'Adding distro default ntp pool servers: %s', ','.join(pools))
@@ -532,6 +563,8 @@ def handle(name, cfg, cloud, log, _args):
raise RuntimeError(msg)
write_ntp_config_template(cloud.distro.name,
+ service_name=ntp_client_config.get(
+ 'service_name'),
servers=ntp_cfg.get('servers', []),
pools=ntp_cfg.get('pools', []),
path=ntp_client_config.get('confpath'),
diff --git a/cloudinit/config/cc_power_state_change.py b/cloudinit/config/cc_power_state_change.py
index 41ffb46c..ab953a0d 100644
--- a/cloudinit/config/cc_power_state_change.py
+++ b/cloudinit/config/cc_power_state_change.py
@@ -22,9 +22,8 @@ The ``delay`` key specifies a duration to be added onto any shutdown command
used. Therefore, if a 5 minute delay and a 120 second shutdown are specified,
the maximum amount of time between cloud-init starting and the system shutting
down is 7 minutes, and the minimum amount of time is 5 minutes. The ``delay``
-key must have an argument in a form that the ``shutdown`` utility recognizes.
-The most common format is the form ``+5`` for 5 minutes. See ``man shutdown``
-for more options.
+key must have an argument in either the form ``+5`` for 5 minutes or ``now``
+for immediate shutdown.
Optionally, a command can be run to determine whether or not
the system should shut down. The command to be run should be specified in the
@@ -33,6 +32,10 @@ the system should shut down. The command to be run should be specified in the
``condition`` key is omitted or the command specified by the ``condition``
key returns 0.
+.. note::
+ With Alpine Linux any message value specified is ignored as Alpine's halt,
+ poweroff, and reboot commands do not support broadcasting a message.
+
**Internal name:** ``cc_power_state_change``
**Module frequency:** per instance
@@ -112,9 +115,9 @@ def check_condition(cond, log=None):
return False
-def handle(_name, cfg, _cloud, log, _args):
+def handle(_name, cfg, cloud, log, _args):
try:
- (args, timeout, condition) = load_power_state(cfg)
+ (args, timeout, condition) = load_power_state(cfg, cloud.distro.name)
if args is None:
log.debug("no power_state provided. doing nothing")
return
@@ -141,7 +144,19 @@ def handle(_name, cfg, _cloud, log, _args):
condition, execmd, [args, devnull_fp])
-def load_power_state(cfg):
+def convert_delay(delay, fmt=None, scale=None):
+ if not fmt:
+ fmt = "+%s"
+ if not scale:
+ scale = 1
+
+ if delay != "now":
+ delay = fmt % int(int(delay) * int(scale))
+
+ return delay
+
+
+def load_power_state(cfg, distro_name):
# returns a tuple of shutdown_command, timeout
# shutdown_command is None if no config found
pstate = cfg.get('power_state')
@@ -161,20 +176,34 @@ def load_power_state(cfg):
(','.join(opt_map.keys()), mode))
delay = pstate.get("delay", "now")
- # convert integer 30 or string '30' to '+30'
+ message = pstate.get("message")
+ scale = 1
+ fmt = "+%s"
+ command = ["shutdown", opt_map[mode]]
+
+ if distro_name == 'alpine':
+ # Convert integer 30 or string '30' to '1800' (seconds) as Alpine's
+ # halt/poweroff/reboot commands take seconds rather than minutes.
+ scale = 60
+ # No "+" in front of delay value as not supported by Alpine's commands.
+ fmt = "%s"
+ if delay == "now":
+ # Alpine's commands do not understand "now".
+ delay = "0"
+ command = [mode, "-d"]
+ # Alpine's commands don't support a message.
+ message = None
+
try:
- delay = "+%s" % int(delay)
+ delay = convert_delay(delay, fmt=fmt, scale=scale)
except ValueError:
- pass
-
- if delay != "now" and not re.match(r"\+[0-9]+", delay):
raise TypeError(
"power_state[delay] must be 'now' or '+m' (minutes)."
" found '%s'." % delay)
- args = ["shutdown", opt_map[mode], delay]
- if pstate.get("message"):
- args.append(pstate.get("message"))
+ args = command + [delay]
+ if message:
+ args.append(message)
try:
timeout = float(pstate.get('timeout', 30.0))
diff --git a/cloudinit/config/cc_resolv_conf.py b/cloudinit/config/cc_resolv_conf.py
index 69f4768a..519e66eb 100644
--- a/cloudinit/config/cc_resolv_conf.py
+++ b/cloudinit/config/cc_resolv_conf.py
@@ -30,7 +30,7 @@ are configured correctly.
**Module frequency:** per instance
-**Supported distros:** fedora, rhel, sles
+**Supported distros:** alpine, fedora, rhel, sles
**Config keys**::
@@ -55,7 +55,7 @@ LOG = logging.getLogger(__name__)
frequency = PER_INSTANCE
-distros = ['fedora', 'opensuse', 'rhel', 'sles']
+distros = ['alpine', 'fedora', 'opensuse', 'rhel', 'sles']
def generate_resolv_conf(template_fn, params, target_fname="/etc/resolv.conf"):