# This file is part of cloud-init. See LICENSE file for license information. # RELEASE_BLOCKER: Remove this deprecated module in 18.3 """ Snappy ------ **Summary:** snappy modules allows configuration of snappy. **Deprecated**: Use :ref:`snap` module instead. This module will not exist in cloud-init 18.3. 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 temp_utils from cloudinit import safeyaml from cloudinit import util import glob import os 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) = temp_utils.mkstemp() os.write(fd, safeyaml.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 log.warning( 'DEPRECATION: snappy module will be dropped in 18.3 release.' ' Use snap module instead') 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.warning("'%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.warning("Unknown value '%s' in ssh_enabled", ssh_enabled) disable_enable_ssh(ssh_enabled) if fails: raise Exception("failed to install/configure snaps") # vi: ts=4 expandtab