|
Packit Service |
a04d08 |
# Copyright (C) 2018 Canonical Ltd.
|
|
Packit Service |
a04d08 |
#
|
|
Packit Service |
a04d08 |
# This file is part of cloud-init. See LICENSE file for license information.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
"""Snap: Install, configure and manage snapd and snap packages."""
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
import sys
|
|
Packit Service |
a04d08 |
from textwrap import dedent
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
from cloudinit import log as logging
|
|
Packit Service |
a04d08 |
from cloudinit.config.schema import (
|
|
Packit Service |
a04d08 |
get_schema_doc, validate_cloudconfig_schema)
|
|
Packit Service |
a04d08 |
from cloudinit.settings import PER_INSTANCE
|
|
Packit Service |
a04d08 |
from cloudinit.subp import prepend_base_command
|
|
Packit Service |
751c4a |
from cloudinit import subp
|
|
Packit Service |
a04d08 |
from cloudinit import util
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
distros = ['ubuntu']
|
|
Packit Service |
a04d08 |
frequency = PER_INSTANCE
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
LOG = logging.getLogger(__name__)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
schema = {
|
|
Packit Service |
a04d08 |
'id': 'cc_snap',
|
|
Packit Service |
a04d08 |
'name': 'Snap',
|
|
Packit Service |
a04d08 |
'title': 'Install, configure and manage snapd and snap packages',
|
|
Packit Service |
a04d08 |
'description': dedent("""\
|
|
Packit Service |
a04d08 |
This module provides a simple configuration namespace in cloud-init to
|
|
Packit Service |
a04d08 |
both setup snapd and install snaps.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
.. note::
|
|
Packit Service |
a04d08 |
Both ``assertions`` and ``commands`` values can be either a
|
|
Packit Service |
a04d08 |
dictionary or a list. If these configs are provided as a
|
|
Packit Service |
a04d08 |
dictionary, the keys are only used to order the execution of the
|
|
Packit Service |
a04d08 |
assertions or commands and the dictionary is merged with any
|
|
Packit Service |
a04d08 |
vendor-data snap configuration provided. If a list is provided by
|
|
Packit Service |
a04d08 |
the user instead of a dict, any vendor-data snap configuration is
|
|
Packit Service |
a04d08 |
ignored.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
The ``assertions`` configuration option is a dictionary or list of
|
|
Packit Service |
a04d08 |
properly-signed snap assertions which will run before any snap
|
|
Packit Service |
a04d08 |
``commands``. They will be added to snapd's assertion database by
|
|
Packit Service |
a04d08 |
invoking ``snap ack <aggregate_assertion_file>``.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
Snap ``commands`` is a dictionary or list of individual snap
|
|
Packit Service |
a04d08 |
commands to run on the target system. These commands can be used to
|
|
Packit Service |
a04d08 |
create snap users, install snaps and provide snap configuration.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
.. note::
|
|
Packit Service |
a04d08 |
If 'side-loading' private/unpublished snaps on an instance, it is
|
|
Packit Service |
a04d08 |
best to create a snap seed directory and seed.yaml manifest in
|
|
Packit Service |
a04d08 |
**/var/lib/snapd/seed/** which snapd automatically installs on
|
|
Packit Service |
a04d08 |
startup.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
**Development only**: The ``squashfuse_in_container`` boolean can be
|
|
Packit Service |
a04d08 |
set true to install squashfuse package when in a container to enable
|
|
Packit Service |
a04d08 |
snap installs. Default is false.
|
|
Packit Service |
a04d08 |
"""),
|
|
Packit Service |
a04d08 |
'distros': distros,
|
|
Packit Service |
a04d08 |
'examples': [dedent("""\
|
|
Packit Service |
a04d08 |
snap:
|
|
Packit Service |
a04d08 |
assertions:
|
|
Packit Service |
a04d08 |
00: |
|
|
Packit Service |
751c4a |
signed_assertion_blob_here
|
|
Packit Service |
a04d08 |
02: |
|
|
Packit Service |
751c4a |
signed_assertion_blob_here
|
|
Packit Service |
a04d08 |
commands:
|
|
Packit Service |
a04d08 |
00: snap create-user --sudoer --known <snap-user>@mydomain.com
|
|
Packit Service |
a04d08 |
01: snap install canonical-livepatch
|
|
Packit Service |
a04d08 |
02: canonical-livepatch enable <AUTH_TOKEN>
|
|
Packit Service |
a04d08 |
"""), dedent("""\
|
|
Packit Service |
a04d08 |
# LXC-based containers require squashfuse before snaps can be installed
|
|
Packit Service |
a04d08 |
snap:
|
|
Packit Service |
a04d08 |
commands:
|
|
Packit Service |
a04d08 |
00: apt-get install squashfuse -y
|
|
Packit Service |
a04d08 |
11: snap install emoj
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
"""), dedent("""\
|
|
Packit Service |
a04d08 |
# Convenience: the snap command can be omitted when specifying commands
|
|
Packit Service |
a04d08 |
# as a list and 'snap' will automatically be prepended.
|
|
Packit Service |
a04d08 |
# The following commands are equivalent:
|
|
Packit Service |
a04d08 |
snap:
|
|
Packit Service |
a04d08 |
commands:
|
|
Packit Service |
a04d08 |
00: ['install', 'vlc']
|
|
Packit Service |
a04d08 |
01: ['snap', 'install', 'vlc']
|
|
Packit Service |
a04d08 |
02: snap install vlc
|
|
Packit Service |
a04d08 |
03: 'snap install vlc'
|
|
Packit Service |
751c4a |
"""), dedent("""\
|
|
Packit Service |
751c4a |
# You can use a list of commands
|
|
Packit Service |
751c4a |
snap:
|
|
Packit Service |
751c4a |
commands:
|
|
Packit Service |
751c4a |
- ['install', 'vlc']
|
|
Packit Service |
751c4a |
- ['snap', 'install', 'vlc']
|
|
Packit Service |
751c4a |
- snap install vlc
|
|
Packit Service |
751c4a |
- 'snap install vlc'
|
|
Packit Service |
751c4a |
"""), dedent("""\
|
|
Packit Service |
751c4a |
# You can use a list of assertions
|
|
Packit Service |
751c4a |
snap:
|
|
Packit Service |
751c4a |
assertions:
|
|
Packit Service |
751c4a |
- signed_assertion_blob_here
|
|
Packit Service |
751c4a |
- |
|
|
Packit Service |
751c4a |
signed_assertion_blob_here
|
|
Packit Service |
a04d08 |
""")],
|
|
Packit Service |
a04d08 |
'frequency': PER_INSTANCE,
|
|
Packit Service |
a04d08 |
'type': 'object',
|
|
Packit Service |
a04d08 |
'properties': {
|
|
Packit Service |
a04d08 |
'snap': {
|
|
Packit Service |
a04d08 |
'type': 'object',
|
|
Packit Service |
a04d08 |
'properties': {
|
|
Packit Service |
a04d08 |
'assertions': {
|
|
Packit Service |
a04d08 |
'type': ['object', 'array'], # Array of strings or dict
|
|
Packit Service |
a04d08 |
'items': {'type': 'string'},
|
|
Packit Service |
a04d08 |
'additionalItems': False, # Reject items non-string
|
|
Packit Service |
a04d08 |
'minItems': 1,
|
|
Packit Service |
a04d08 |
'minProperties': 1,
|
|
Packit Service |
751c4a |
'uniqueItems': True,
|
|
Packit Service |
751c4a |
'additionalProperties': {'type': 'string'},
|
|
Packit Service |
a04d08 |
},
|
|
Packit Service |
a04d08 |
'commands': {
|
|
Packit Service |
a04d08 |
'type': ['object', 'array'], # Array of strings or dict
|
|
Packit Service |
a04d08 |
'items': {
|
|
Packit Service |
a04d08 |
'oneOf': [
|
|
Packit Service |
a04d08 |
{'type': 'array', 'items': {'type': 'string'}},
|
|
Packit Service |
a04d08 |
{'type': 'string'}]
|
|
Packit Service |
a04d08 |
},
|
|
Packit Service |
a04d08 |
'additionalItems': False, # Reject non-string & non-list
|
|
Packit Service |
a04d08 |
'minItems': 1,
|
|
Packit Service |
a04d08 |
'minProperties': 1,
|
|
Packit Service |
751c4a |
'additionalProperties': {
|
|
Packit Service |
751c4a |
'oneOf': [
|
|
Packit Service |
751c4a |
{'type': 'string'},
|
|
Packit Service |
751c4a |
{'type': 'array', 'items': {'type': 'string'}},
|
|
Packit Service |
751c4a |
],
|
|
Packit Service |
751c4a |
},
|
|
Packit Service |
a04d08 |
},
|
|
Packit Service |
a04d08 |
'squashfuse_in_container': {
|
|
Packit Service |
a04d08 |
'type': 'boolean'
|
|
Packit Service |
a04d08 |
}
|
|
Packit Service |
a04d08 |
},
|
|
Packit Service |
a04d08 |
'additionalProperties': False, # Reject keys not in schema
|
|
Packit Service |
a04d08 |
'required': [],
|
|
Packit Service |
a04d08 |
'minProperties': 1
|
|
Packit Service |
a04d08 |
}
|
|
Packit Service |
a04d08 |
}
|
|
Packit Service |
a04d08 |
}
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
__doc__ = get_schema_doc(schema) # Supplement python help()
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
SNAP_CMD = "snap"
|
|
Packit Service |
a04d08 |
ASSERTIONS_FILE = "/var/lib/cloud/instance/snapd.assertions"
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def add_assertions(assertions):
|
|
Packit Service |
a04d08 |
"""Import list of assertions.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
Import assertions by concatenating each assertion into a
|
|
Packit Service |
a04d08 |
string separated by a '\n'. Write this string to a instance file and
|
|
Packit Service |
a04d08 |
then invoke `snap ack /path/to/file` and check for errors.
|
|
Packit Service |
a04d08 |
If snap exits 0, then all assertions are imported.
|
|
Packit Service |
a04d08 |
"""
|
|
Packit Service |
a04d08 |
if not assertions:
|
|
Packit Service |
a04d08 |
return
|
|
Packit Service |
a04d08 |
LOG.debug('Importing user-provided snap assertions')
|
|
Packit Service |
a04d08 |
if isinstance(assertions, dict):
|
|
Packit Service |
a04d08 |
assertions = assertions.values()
|
|
Packit Service |
a04d08 |
elif not isinstance(assertions, list):
|
|
Packit Service |
a04d08 |
raise TypeError(
|
|
Packit Service |
a04d08 |
'assertion parameter was not a list or dict: {assertions}'.format(
|
|
Packit Service |
a04d08 |
assertions=assertions))
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
snap_cmd = [SNAP_CMD, 'ack']
|
|
Packit Service |
a04d08 |
combined = "\n".join(assertions)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
for asrt in assertions:
|
|
Packit Service |
a04d08 |
LOG.debug('Snap acking: %s', asrt.split('\n')[0:2])
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
util.write_file(ASSERTIONS_FILE, combined.encode('utf-8'))
|
|
Packit Service |
751c4a |
subp.subp(snap_cmd + [ASSERTIONS_FILE], capture=True)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def run_commands(commands):
|
|
Packit Service |
a04d08 |
"""Run the provided commands provided in snap:commands configuration.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
Commands are run individually. Any errors are collected and reported
|
|
Packit Service |
a04d08 |
after attempting all commands.
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
@param commands: A list or dict containing commands to run. Keys of a
|
|
Packit Service |
a04d08 |
dict will be used to order the commands provided as dict values.
|
|
Packit Service |
a04d08 |
"""
|
|
Packit Service |
a04d08 |
if not commands:
|
|
Packit Service |
a04d08 |
return
|
|
Packit Service |
a04d08 |
LOG.debug('Running user-provided snap commands')
|
|
Packit Service |
a04d08 |
if isinstance(commands, dict):
|
|
Packit Service |
a04d08 |
# Sort commands based on dictionary key
|
|
Packit Service |
a04d08 |
commands = [v for _, v in sorted(commands.items())]
|
|
Packit Service |
a04d08 |
elif not isinstance(commands, list):
|
|
Packit Service |
a04d08 |
raise TypeError(
|
|
Packit Service |
a04d08 |
'commands parameter was not a list or dict: {commands}'.format(
|
|
Packit Service |
a04d08 |
commands=commands))
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
fixed_snap_commands = prepend_base_command('snap', commands)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
cmd_failures = []
|
|
Packit Service |
a04d08 |
for command in fixed_snap_commands:
|
|
Packit Service |
a04d08 |
shell = isinstance(command, str)
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
751c4a |
subp.subp(command, shell=shell, status_cb=sys.stderr.write)
|
|
Packit Service |
751c4a |
except subp.ProcessExecutionError as e:
|
|
Packit Service |
a04d08 |
cmd_failures.append(str(e))
|
|
Packit Service |
a04d08 |
if cmd_failures:
|
|
Packit Service |
a04d08 |
msg = 'Failures running snap commands:\n{cmd_failures}'.format(
|
|
Packit Service |
a04d08 |
cmd_failures=cmd_failures)
|
|
Packit Service |
a04d08 |
util.logexc(LOG, msg)
|
|
Packit Service |
a04d08 |
raise RuntimeError(msg)
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
# RELEASE_BLOCKER: Once LP: #1628289 is released on xenial, drop this function.
|
|
Packit Service |
a04d08 |
def maybe_install_squashfuse(cloud):
|
|
Packit Service |
a04d08 |
"""Install squashfuse if we are in a container."""
|
|
Packit Service |
a04d08 |
if not util.is_container():
|
|
Packit Service |
a04d08 |
return
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
a04d08 |
cloud.distro.update_package_sources()
|
|
Packit Service |
a04d08 |
except Exception:
|
|
Packit Service |
a04d08 |
util.logexc(LOG, "Package update failed")
|
|
Packit Service |
a04d08 |
raise
|
|
Packit Service |
a04d08 |
try:
|
|
Packit Service |
a04d08 |
cloud.distro.install_packages(['squashfuse'])
|
|
Packit Service |
a04d08 |
except Exception:
|
|
Packit Service |
a04d08 |
util.logexc(LOG, "Failed to install squashfuse")
|
|
Packit Service |
a04d08 |
raise
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
def handle(name, cfg, cloud, log, args):
|
|
Packit Service |
a04d08 |
cfgin = cfg.get('snap', {})
|
|
Packit Service |
a04d08 |
if not cfgin:
|
|
Packit Service |
a04d08 |
LOG.debug(("Skipping module named %s,"
|
|
Packit Service |
a04d08 |
" no 'snap' key in configuration"), name)
|
|
Packit Service |
a04d08 |
return
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
validate_cloudconfig_schema(cfg, schema)
|
|
Packit Service |
a04d08 |
if util.is_true(cfgin.get('squashfuse_in_container', False)):
|
|
Packit Service |
a04d08 |
maybe_install_squashfuse(cloud)
|
|
Packit Service |
a04d08 |
add_assertions(cfgin.get('assertions', []))
|
|
Packit Service |
a04d08 |
run_commands(cfgin.get('commands', []))
|
|
Packit Service |
a04d08 |
|
|
Packit Service |
a04d08 |
# vi: ts=4 expandtab
|