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 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229
|
# 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 sys
from textwrap import dedent
from cloudinit import log as logging
from cloudinit.config.schema import (
get_schema_doc, validate_cloudconfig_schema)
from cloudinit.settings import PER_INSTANCE
from cloudinit.subp import prepend_base_command
from cloudinit import util
distros = ['ubuntu']
frequency = PER_INSTANCE
LOG = logging.getLogger(__name__)
schema = {
'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.
**Development only**: The ``squashfuse_in_container`` boolean can be
set true to install squashfuse package when in a container to enable
snap installs. Default is false.
"""),
'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("""\
# LXC-based containers require squashfuse before snaps can be installed
snap:
commands:
00: apt-get install squashfuse -y
11: snap install emoj
"""), 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'
""")],
'frequency': PER_INSTANCE,
'type': 'object',
'properties': {
'snap': {
'type': 'object',
'properties': {
'assertions': {
'type': ['object', 'array'], # Array of strings or dict
'items': {'type': 'string'},
'additionalItems': False, # Reject items non-string
'minItems': 1,
'minProperties': 1,
'uniqueItems': True
},
'commands': {
'type': ['object', 'array'], # Array of strings or dict
'items': {
'oneOf': [
{'type': 'array', 'items': {'type': 'string'}},
{'type': 'string'}]
},
'additionalItems': False, # Reject non-string & non-list
'minItems': 1,
'minProperties': 1,
},
'squashfuse_in_container': {
'type': 'boolean'
}
},
'additionalProperties': False, # Reject keys not in schema
'required': [],
'minProperties': 1
}
}
}
# TODO schema for 'assertions' and 'commands' are too permissive at the moment.
# Once python-jsonschema supports schema draft 6 add support for arbitrary
# object keys with 'patternProperties' constraint to validate string values.
__doc__ = get_schema_doc(schema) # Supplement python help()
SNAP_CMD = "snap"
ASSERTIONS_FILE = "/var/lib/cloud/instance/snapd.assertions"
def add_assertions(assertions):
"""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']
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'))
util.subp(snap_cmd + [ASSERTIONS_FILE], 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:
util.subp(command, shell=shell, status_cb=sys.stderr.write)
except util.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)
# RELEASE_BLOCKER: Once LP: #1628289 is released on xenial, drop this function.
def maybe_install_squashfuse(cloud):
"""Install squashfuse if we are in a container."""
if not util.is_container():
return
try:
cloud.distro.update_package_sources()
except Exception:
util.logexc(LOG, "Package update failed")
raise
try:
cloud.distro.install_packages(['squashfuse'])
except Exception:
util.logexc(LOG, "Failed to install squashfuse")
raise
def handle(name, cfg, cloud, log, args):
cfgin = cfg.get('snap', {})
if not cfgin:
LOG.debug(("Skipping module named %s,"
" no 'snap' key in configuration"), name)
return
validate_cloudconfig_schema(cfg, schema)
if util.is_true(cfgin.get('squashfuse_in_container', False)):
maybe_install_squashfuse(cloud)
add_assertions(cfgin.get('assertions', []))
run_commands(cfgin.get('commands', []))
# vi: ts=4 expandtab
|