1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
|
# Copyright (C) 2018 Canonical Ltd.
#
# This file is part of cloud-init. See LICENSE file for license information.
"""Snap: Install, configure and manage snapd and snap packages."""
import os
import sys
from logging import Logger
from textwrap import dedent
from cloudinit import log as logging
from cloudinit import subp, util
from cloudinit.cloud import Cloud
from cloudinit.config import Config
from cloudinit.config.schema import MetaSchema, get_meta_doc
from cloudinit.settings import PER_INSTANCE
from cloudinit.subp import prepend_base_command
distros = ["ubuntu"]
frequency = PER_INSTANCE
LOG = logging.getLogger(__name__)
meta: MetaSchema = {
"id": "cc_snap",
"name": "Snap",
"title": "Install, configure and manage snapd and snap packages",
"description": dedent(
"""\
This module provides a simple configuration namespace in cloud-init to
both setup snapd and install snaps.
.. note::
Both ``assertions`` and ``commands`` values can be either a
dictionary or a list. If these configs are provided as a
dictionary, the keys are only used to order the execution of the
assertions or commands and the dictionary is merged with any
vendor-data snap configuration provided. If a list is provided by
the user instead of a dict, any vendor-data snap configuration is
ignored.
The ``assertions`` configuration option is a dictionary or list of
properly-signed snap assertions which will run before any snap
``commands``. They will be added to snapd's assertion database by
invoking ``snap ack <aggregate_assertion_file>``.
Snap ``commands`` is a dictionary or list of individual snap
commands to run on the target system. These commands can be used to
create snap users, install snaps and provide snap configuration.
.. note::
If 'side-loading' private/unpublished snaps on an instance, it is
best to create a snap seed directory and seed.yaml manifest in
**/var/lib/snapd/seed/** which snapd automatically installs on
startup.
"""
),
"distros": distros,
"examples": [
dedent(
"""\
snap:
assertions:
00: |
signed_assertion_blob_here
02: |
signed_assertion_blob_here
commands:
00: snap create-user --sudoer --known <snap-user>@mydomain.com
01: snap install canonical-livepatch
02: canonical-livepatch enable <AUTH_TOKEN>
"""
),
dedent(
"""\
# Convenience: the snap command can be omitted when specifying commands
# as a list and 'snap' will automatically be prepended.
# The following commands are equivalent:
snap:
commands:
00: ['install', 'vlc']
01: ['snap', 'install', 'vlc']
02: snap install vlc
03: 'snap install vlc'
"""
),
dedent(
"""\
# You can use a list of commands
snap:
commands:
- ['install', 'vlc']
- ['snap', 'install', 'vlc']
- snap install vlc
- 'snap install vlc'
"""
),
dedent(
"""\
# You can use a list of assertions
snap:
assertions:
- signed_assertion_blob_here
- |
signed_assertion_blob_here
"""
),
],
"frequency": PER_INSTANCE,
"activate_by_schema_keys": ["snap"],
}
__doc__ = get_meta_doc(meta)
SNAP_CMD = "snap"
def add_assertions(assertions, assertions_file):
"""Import list of assertions.
Import assertions by concatenating each assertion into a
string separated by a '\n'. Write this string to a instance file and
then invoke `snap ack /path/to/file` and check for errors.
If snap exits 0, then all assertions are imported.
"""
if not assertions:
return
LOG.debug("Importing user-provided snap assertions")
if isinstance(assertions, dict):
assertions = assertions.values()
elif not isinstance(assertions, list):
raise TypeError(
"assertion parameter was not a list or dict: {assertions}".format(
assertions=assertions
)
)
snap_cmd = [SNAP_CMD, "ack", assertions_file]
combined = "\n".join(assertions)
for asrt in assertions:
LOG.debug("Snap acking: %s", asrt.split("\n")[0:2])
util.write_file(assertions_file, combined.encode("utf-8"))
subp.subp(snap_cmd, capture=True)
def run_commands(commands):
"""Run the provided commands provided in snap:commands configuration.
Commands are run individually. Any errors are collected and reported
after attempting all commands.
@param commands: A list or dict containing commands to run. Keys of a
dict will be used to order the commands provided as dict values.
"""
if not commands:
return
LOG.debug("Running user-provided snap commands")
if isinstance(commands, dict):
# Sort commands based on dictionary key
commands = [v for _, v in sorted(commands.items())]
elif not isinstance(commands, list):
raise TypeError(
"commands parameter was not a list or dict: {commands}".format(
commands=commands
)
)
fixed_snap_commands = prepend_base_command("snap", commands)
cmd_failures = []
for command in fixed_snap_commands:
shell = isinstance(command, str)
try:
subp.subp(command, shell=shell, status_cb=sys.stderr.write)
except subp.ProcessExecutionError as e:
cmd_failures.append(str(e))
if cmd_failures:
msg = "Failures running snap commands:\n{cmd_failures}".format(
cmd_failures=cmd_failures
)
util.logexc(LOG, msg)
raise RuntimeError(msg)
def handle(
name: str, cfg: Config, cloud: Cloud, log: Logger, args: list
) -> None:
cfgin = cfg.get("snap", {})
if not cfgin:
LOG.debug(
"Skipping module named %s, no 'snap' key in configuration", name
)
return
add_assertions(
cfgin.get("assertions", []),
os.path.join(cloud.paths.get_ipath_cur(), "snapd.assertions"),
)
run_commands(cfgin.get("commands", []))
# vi: ts=4 expandtab
|