4
snappy modules allows configuration of snappy.
10
packages: [etcd, pkg2.smoser]
16
packages_dir: '/writable/user-data/cloud-init/snaps'
19
This controls the system's ssh service. The default value is 'auto'.
20
True: enable ssh service
21
False: disable ssh service
22
auto: enable ssh service if either ssh keys have been provided
23
or user has requested password authentication (ssh_pwauth).
25
- snap installation and config
26
The above would install 'etcd', and then install 'pkg2.smoser' with a
27
'<config-file>' argument where 'config-file' has 'config-blob' inside it.
28
If 'pkgname' is installed already, then 'snappy config pkgname <file>'
29
will be called where 'file' has 'pkgname-config-blob' as its content.
31
Entries in 'config' can be namespaced or non-namespaced for a package.
32
In either case, the config provided to snappy command is non-namespaced.
33
The package name is provided as it appears.
35
If 'packages_dir' has files in it that end in '.snap', then they are
36
installed. Given 3 files:
37
<packages_dir>/foo.snap
38
<packages_dir>/foo.config
39
<packages_dir>/bar.snap
40
cloud-init will invoke:
41
snappy install <packages_dir>/foo.snap <packages_dir>/foo.config
42
snappy install <packages_dir>/bar.snap
44
Note, that if provided a 'config' entry for 'ubuntu-core', then
45
cloud-init will invoke: snappy config ubuntu-core <config>
46
Allowing you to configure ubuntu-core in this way.
49
from cloudinit import log as logging
50
from cloudinit.settings import PER_INSTANCE
51
from cloudinit import util
57
LOG = logging.getLogger(__name__)
59
frequency = PER_INSTANCE
65
'packages_dir': '/writable/user-data/cloud-init/snaps',
66
'ssh_enabled': "auto",
67
'system_snappy': "auto",
72
def parse_filename(fname):
73
fname = os.path.basename(fname)
74
fname_noext = fname.rpartition(".")[0]
75
name = fname_noext.partition("_")[0]
76
shortname = name.partition(".")[0]
77
return(name, shortname, fname_noext)
80
def get_fs_package_ops(fspath):
84
for snapfile in sorted(glob.glob(os.path.sep.join([fspath, '*.snap']))):
85
(name, shortname, fname_noext) = parse_filename(snapfile)
87
for cand in (fname_noext, name, shortname):
88
fpcand = os.path.sep.join([fspath, cand]) + ".config"
89
if os.path.isfile(fpcand):
92
ops.append(makeop('install', name, config=None,
93
path=snapfile, cfgfile=cfg))
97
def makeop(op, name, config=None, path=None, cfgfile=None):
98
return({'op': op, 'name': name, 'config': config, 'path': path,
102
def get_package_config(configs, name):
103
# load the package's config from the configs dict.
104
# prefer full-name entry (config-example.canonical)
105
# over short name entry (config-example)
108
return configs.get(name.partition(NAMESPACE_DELIM)[0])
111
def get_package_ops(packages, configs, installed=None, fspath=None):
112
# get the install an config operations that should be done
113
if installed is None:
114
installed = read_installed_packages()
115
short_installed = [p.partition(NAMESPACE_DELIM)[0] for p in installed]
123
ops += get_fs_package_ops(fspath)
125
for name in packages:
126
ops.append(makeop('install', name, get_package_config(configs, name)))
128
to_install = [f['name'] for f in ops]
129
short_to_install = [f['name'].partition(NAMESPACE_DELIM)[0] for f in ops]
132
if name in to_install:
134
shortname = name.partition(NAMESPACE_DELIM)[0]
135
if shortname in short_to_install:
137
if name in installed or shortname in short_installed:
138
ops.append(makeop('config', name,
139
config=get_package_config(configs, name)))
141
# prefer config entries to filepath entries
143
if op['op'] != 'install' or not op['cfgfile']:
146
fromcfg = get_package_config(configs, op['name'])
148
LOG.debug("preferring configs[%(name)s] over '%(cfgfile)s'", op)
150
op['config'] = fromcfg
155
def render_snap_op(op, name, path=None, cfgfile=None, config=None):
156
if op not in ('install', 'config'):
157
raise ValueError("cannot render op '%s'" % op)
159
shortname = name.partition(NAMESPACE_DELIM)[0]
162
if config is not None:
163
# input to 'snappy config packagename' must have nested data. odd.
167
# Note, however, we do not touch config files on disk.
168
nested_cfg = {'config': {shortname: config}}
169
(fd, cfg_tmpf) = tempfile.mkstemp()
170
os.write(fd, util.yaml_dumps(nested_cfg).encode())
174
cmd = [SNAPPY_CMD, op]
177
cmd.append("--allow-unauthenticated")
184
cmd += [name, cfgfile]
193
def read_installed_packages():
195
for (name, date, version, dev) in read_pkg_data():
197
ret.append(NAMESPACE_DELIM.join([name, dev]))
204
out, err = util.subp([SNAPPY_CMD, "list"])
206
for line in out.splitlines()[1:]:
207
toks = line.split(sep=None, maxsplit=3)
209
(name, date, version) = toks
212
(name, date, version, dev) = toks
213
pkg_data.append((name, date, version, dev,))
217
def disable_enable_ssh(enabled):
218
LOG.debug("setting enablement of ssh to: %s", enabled)
219
# do something here that would enable or disable
220
not_to_be_run = "/etc/ssh/sshd_not_to_be_run"
222
util.del_file(not_to_be_run)
223
# this is an indempotent operation
224
util.subp(["systemctl", "start", "ssh"])
226
# this is an indempotent operation
227
util.subp(["systemctl", "stop", "ssh"])
228
util.write_file(not_to_be_run, "cloud-init\n")
231
def system_is_snappy():
232
# channel.ini is configparser loadable.
233
# snappy will move to using /etc/system-image/config.d/*.ini
234
# this is certainly not a perfect test, but good enough for now.
235
content = util.load_file("/etc/system-image/channel.ini", quiet=True)
236
if 'ubuntu-core' in content.lower():
238
if os.path.isdir("/etc/system-image/config.d/"):
243
def set_snappy_command():
245
if util.which("snappy-go"):
246
SNAPPY_CMD = "snappy-go"
248
SNAPPY_CMD = "snappy"
249
LOG.debug("snappy command is '%s'", SNAPPY_CMD)
252
def handle(name, cfg, cloud, log, args):
253
cfgin = cfg.get('snappy')
256
mycfg = util.mergemanydict([cfgin, BUILTIN_CFG])
258
sys_snappy = str(mycfg.get("system_snappy", "auto"))
259
if util.is_false(sys_snappy):
260
LOG.debug("%s: System is not snappy. disabling", name)
263
if sys_snappy.lower() == "auto" and not(system_is_snappy()):
264
LOG.debug("%s: 'auto' mode, and system not snappy", name)
269
pkg_ops = get_package_ops(packages=mycfg['packages'],
270
configs=mycfg['config'],
271
fspath=mycfg['packages_dir'])
274
for pkg_op in pkg_ops:
276
render_snap_op(**pkg_op)
277
except Exception as e:
278
fails.append((pkg_op, e,))
279
LOG.warn("'%s' failed for '%s': %s",
280
pkg_op['op'], pkg_op['name'], e)
282
# Default to disabling SSH
283
ssh_enabled = mycfg.get('ssh_enabled', "auto")
285
# If the user has not explicitly enabled or disabled SSH, then enable it
286
# when password SSH authentication is requested or there are SSH keys
287
if ssh_enabled == "auto":
288
user_ssh_keys = cloud.get_public_ssh_keys() or None
289
password_auth_enabled = cfg.get('ssh_pwauth', False)
291
LOG.debug("Enabling SSH, ssh keys found in datasource")
293
elif cfg.get('ssh_authorized_keys'):
294
LOG.debug("Enabling SSH, ssh keys found in config")
295
elif password_auth_enabled:
296
LOG.debug("Enabling SSH, password authentication requested")
298
elif ssh_enabled not in (True, False):
299
LOG.warn("Unknown value '%s' in ssh_enabled", ssh_enabled)
301
disable_enable_ssh(ssh_enabled)
304
raise Exception("failed to install/configure snaps")