Skip to content

Commit

Permalink
Add Alpine Linux support. (#535)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
dermotbradley authored Aug 19, 2020
1 parent b749548 commit 79a8ce7
Show file tree
Hide file tree
Showing 24 changed files with 1,068 additions and 106 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ get in contact with that distribution and send them our way!

| Supported OSes | Supported Public Clouds | Supported Private Clouds |
| --- | --- | --- |
| Ubuntu<br />SLES/openSUSE<br />RHEL/CentOS<br />Fedora<br />Gentoo Linux<br />Debian<br />ArchLinux<br />FreeBSD<br />NetBSD<br />OpenBSD<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />|
| Alpine Linux<br />ArchLinux<br />Debian<br />Fedora<br />FreeBSD<br />Gentoo Linux<br />NetBSD<br />OpenBSD<br />RHEL/CentOS<br />SLES/openSUSE<br />Ubuntu<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /> | Amazon Web Services<br />Microsoft Azure<br />Google Cloud Platform<br />Oracle Cloud Infrastructure<br />Softlayer<br />Rackspace Public Cloud<br />IBM Cloud<br />Digital Ocean<br />Bigstep<br />Hetzner<br />Joyent<br />CloudSigma<br />Alibaba Cloud<br />OVH<br />OpenNebula<br />Exoscale<br />Scaleway<br />CloudStack<br />AltCloud<br />SmartOS<br />HyperOne<br />Rootbox<br /> | Bare metal installs<br />OpenStack<br />LXD<br />KVM<br />Metal-as-a-Service (MAAS)<br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br /><br />|

## To start developing cloud-init

Expand Down
263 changes: 263 additions & 0 deletions cloudinit/config/cc_apk_configure.py
Original file line number Diff line number Diff line change
@@ -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
22 changes: 15 additions & 7 deletions cloudinit/config/cc_ca_certs.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,16 @@
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**::
Expand All @@ -45,7 +50,7 @@
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():
Expand Down Expand Up @@ -83,19 +88,22 @@ 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}.
"""
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.
Expand All @@ -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:
Expand Down
Loading

0 comments on commit 79a8ce7

Please sign in to comment.