# vi: ts=4 expandtab
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""
Snappy
------
**Summary:** snappy modules allows configuration of snappy.
The below example config config would install ``etcd``, and then install
``pkg2.smoser`` with a ```` argument where ``config-file`` has
``config-blob`` inside it. If ``pkgname`` is installed already, then
``snappy config pkgname ``
will be called where ``file`` has ``pkgname-config-blob`` as its content.
Entries in ``config`` can be namespaced or non-namespaced for a package.
In either case, the config provided to snappy command is non-namespaced.
The package name is provided as it appears.
If ``packages_dir`` has files in it that end in ``.snap``, then they are
installed. Given 3 files:
- /foo.snap
- /foo.config
- /bar.snap
cloud-init will invoke:
- snappy install /foo.snap /foo.config
- snappy install /bar.snap
.. note::
that if provided a ``config`` entry for ``ubuntu-core``, then
cloud-init will invoke: snappy config ubuntu-core
Allowing you to configure ubuntu-core in this way.
The ``ssh_enabled`` key controls the system's ssh service. The default value
is ``auto``. Options are:
- **True:** enable ssh service
- **False:** disable ssh service
- **auto:** enable ssh service if either ssh keys have been provided
or user has requested password authentication (ssh_pwauth).
**Internal name:** ``cc_snappy``
**Module frequency:** per instance
**Supported distros:** ubuntu
**Config keys**::
#cloud-config
snappy:
system_snappy: auto
ssh_enabled: auto
packages: [etcd, pkg2.smoser]
config:
pkgname:
key2: value2
pkg2:
key1: value1
packages_dir: '/writable/user-data/cloud-init/snaps'
"""
from cloudinit import log as logging
from cloudinit.settings import PER_INSTANCE
from cloudinit import util
import glob
import os
import tempfile
LOG = logging.getLogger(__name__)
frequency = PER_INSTANCE
SNAPPY_CMD = "snappy"
NAMESPACE_DELIM = '.'
BUILTIN_CFG = {
'packages': [],
'packages_dir': '/writable/user-data/cloud-init/snaps',
'ssh_enabled': "auto",
'system_snappy': "auto",
'config': {},
}
distros = ['ubuntu']
def parse_filename(fname):
fname = os.path.basename(fname)
fname_noext = fname.rpartition(".")[0]
name = fname_noext.partition("_")[0]
shortname = name.partition(".")[0]
return(name, shortname, fname_noext)
def get_fs_package_ops(fspath):
if not fspath:
return []
ops = []
for snapfile in sorted(glob.glob(os.path.sep.join([fspath, '*.snap']))):
(name, shortname, fname_noext) = parse_filename(snapfile)
cfg = None
for cand in (fname_noext, name, shortname):
fpcand = os.path.sep.join([fspath, cand]) + ".config"
if os.path.isfile(fpcand):
cfg = fpcand
break
ops.append(makeop('install', name, config=None,
path=snapfile, cfgfile=cfg))
return ops
def makeop(op, name, config=None, path=None, cfgfile=None):
return({'op': op, 'name': name, 'config': config, 'path': path,
'cfgfile': cfgfile})
def get_package_config(configs, name):
# load the package's config from the configs dict.
# prefer full-name entry (config-example.canonical)
# over short name entry (config-example)
if name in configs:
return configs[name]
return configs.get(name.partition(NAMESPACE_DELIM)[0])
def get_package_ops(packages, configs, installed=None, fspath=None):
# get the install an config operations that should be done
if installed is None:
installed = read_installed_packages()
short_installed = [p.partition(NAMESPACE_DELIM)[0] for p in installed]
if not packages:
packages = []
if not configs:
configs = {}
ops = []
ops += get_fs_package_ops(fspath)
for name in packages:
ops.append(makeop('install', name, get_package_config(configs, name)))
to_install = [f['name'] for f in ops]
short_to_install = [f['name'].partition(NAMESPACE_DELIM)[0] for f in ops]
for name in configs:
if name in to_install:
continue
shortname = name.partition(NAMESPACE_DELIM)[0]
if shortname in short_to_install:
continue
if name in installed or shortname in short_installed:
ops.append(makeop('config', name,
config=get_package_config(configs, name)))
# prefer config entries to filepath entries
for op in ops:
if op['op'] != 'install' or not op['cfgfile']:
continue
name = op['name']
fromcfg = get_package_config(configs, op['name'])
if fromcfg:
LOG.debug("preferring configs[%(name)s] over '%(cfgfile)s'", op)
op['cfgfile'] = None
op['config'] = fromcfg
return ops
def render_snap_op(op, name, path=None, cfgfile=None, config=None):
if op not in ('install', 'config'):
raise ValueError("cannot render op '%s'" % op)
shortname = name.partition(NAMESPACE_DELIM)[0]
try:
cfg_tmpf = None
if config is not None:
# input to 'snappy config packagename' must have nested data. odd.
# config:
# packagename:
# config
# Note, however, we do not touch config files on disk.
nested_cfg = {'config': {shortname: config}}
(fd, cfg_tmpf) = tempfile.mkstemp()
os.write(fd, util.yaml_dumps(nested_cfg).encode())
os.close(fd)
cfgfile = cfg_tmpf
cmd = [SNAPPY_CMD, op]
if op == 'install':
if path:
cmd.append("--allow-unauthenticated")
cmd.append(path)
else:
cmd.append(name)
if cfgfile:
cmd.append(cfgfile)
elif op == 'config':
cmd += [name, cfgfile]
util.subp(cmd)
finally:
if cfg_tmpf:
os.unlink(cfg_tmpf)
def read_installed_packages():
ret = []
for (name, date, version, dev) in read_pkg_data():
if dev:
ret.append(NAMESPACE_DELIM.join([name, dev]))
else:
ret.append(name)
return ret
def read_pkg_data():
out, err = util.subp([SNAPPY_CMD, "list"])
pkg_data = []
for line in out.splitlines()[1:]:
toks = line.split(sep=None, maxsplit=3)
if len(toks) == 3:
(name, date, version) = toks
dev = None
else:
(name, date, version, dev) = toks
pkg_data.append((name, date, version, dev,))
return pkg_data
def disable_enable_ssh(enabled):
LOG.debug("setting enablement of ssh to: %s", enabled)
# do something here that would enable or disable
not_to_be_run = "/etc/ssh/sshd_not_to_be_run"
if enabled:
util.del_file(not_to_be_run)
# this is an indempotent operation
util.subp(["systemctl", "start", "ssh"])
else:
# this is an indempotent operation
util.subp(["systemctl", "stop", "ssh"])
util.write_file(not_to_be_run, "cloud-init\n")
def set_snappy_command():
global SNAPPY_CMD
if util.which("snappy-go"):
SNAPPY_CMD = "snappy-go"
elif util.which("snappy"):
SNAPPY_CMD = "snappy"
else:
SNAPPY_CMD = "snap"
LOG.debug("snappy command is '%s'", SNAPPY_CMD)
def handle(name, cfg, cloud, log, args):
cfgin = cfg.get('snappy')
if not cfgin:
cfgin = {}
mycfg = util.mergemanydict([cfgin, BUILTIN_CFG])
sys_snappy = str(mycfg.get("system_snappy", "auto"))
if util.is_false(sys_snappy):
LOG.debug("%s: System is not snappy. disabling", name)
return
if sys_snappy.lower() == "auto" and not(util.system_is_snappy()):
LOG.debug("%s: 'auto' mode, and system not snappy", name)
return
set_snappy_command()
pkg_ops = get_package_ops(packages=mycfg['packages'],
configs=mycfg['config'],
fspath=mycfg['packages_dir'])
fails = []
for pkg_op in pkg_ops:
try:
render_snap_op(**pkg_op)
except Exception as e:
fails.append((pkg_op, e,))
LOG.warn("'%s' failed for '%s': %s",
pkg_op['op'], pkg_op['name'], e)
# Default to disabling SSH
ssh_enabled = mycfg.get('ssh_enabled', "auto")
# If the user has not explicitly enabled or disabled SSH, then enable it
# when password SSH authentication is requested or there are SSH keys
if ssh_enabled == "auto":
user_ssh_keys = cloud.get_public_ssh_keys() or None
password_auth_enabled = cfg.get('ssh_pwauth', False)
if user_ssh_keys:
LOG.debug("Enabling SSH, ssh keys found in datasource")
ssh_enabled = True
elif cfg.get('ssh_authorized_keys'):
LOG.debug("Enabling SSH, ssh keys found in config")
elif password_auth_enabled:
LOG.debug("Enabling SSH, password authentication requested")
ssh_enabled = True
elif ssh_enabled not in (True, False):
LOG.warn("Unknown value '%s' in ssh_enabled", ssh_enabled)
disable_enable_ssh(ssh_enabled)
if fails:
raise Exception("failed to install/configure snaps")