~cloud-init-dev/cloud-init/trunk

« back to all changes in this revision

Viewing changes to cloudinit/config/cc_snappy.py

  • Committer: Scott Moser
  • Date: 2016-08-10 15:06:15 UTC
  • Revision ID: smoser@ubuntu.com-20160810150615-ma2fv107w3suy1ma
README: Mention move of revision control to git.

cloud-init development has moved its revision control to git.
It is available at 
  https://code.launchpad.net/cloud-init

Clone with 
  git clone https://git.launchpad.net/cloud-init
or
  git clone git+ssh://git.launchpad.net/cloud-init

For more information see
  https://git.launchpad.net/cloud-init/tree/HACKING.rst

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# vi: ts=4 expandtab
2
 
#
3
 
"""
4
 
snappy modules allows configuration of snappy.
5
 
Example config:
6
 
  #cloud-config
7
 
  snappy:
8
 
    system_snappy: auto
9
 
    ssh_enabled: auto
10
 
    packages: [etcd, pkg2.smoser]
11
 
    config:
12
 
      pkgname:
13
 
        key2: value2
14
 
      pkg2:
15
 
        key1: value1
16
 
    packages_dir: '/writable/user-data/cloud-init/snaps'
17
 
 
18
 
 - ssh_enabled:
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).
24
 
 
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.
30
 
 
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.
34
 
 
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
43
 
 
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.
47
 
"""
48
 
 
49
 
from cloudinit import log as logging
50
 
from cloudinit.settings import PER_INSTANCE
51
 
from cloudinit import util
52
 
 
53
 
import glob
54
 
import os
55
 
import tempfile
56
 
 
57
 
LOG = logging.getLogger(__name__)
58
 
 
59
 
frequency = PER_INSTANCE
60
 
SNAPPY_CMD = "snappy"
61
 
NAMESPACE_DELIM = '.'
62
 
 
63
 
BUILTIN_CFG = {
64
 
    'packages': [],
65
 
    'packages_dir': '/writable/user-data/cloud-init/snaps',
66
 
    'ssh_enabled': "auto",
67
 
    'system_snappy': "auto",
68
 
    'config': {},
69
 
}
70
 
 
71
 
 
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)
78
 
 
79
 
 
80
 
def get_fs_package_ops(fspath):
81
 
    if not fspath:
82
 
        return []
83
 
    ops = []
84
 
    for snapfile in sorted(glob.glob(os.path.sep.join([fspath, '*.snap']))):
85
 
        (name, shortname, fname_noext) = parse_filename(snapfile)
86
 
        cfg = None
87
 
        for cand in (fname_noext, name, shortname):
88
 
            fpcand = os.path.sep.join([fspath, cand]) + ".config"
89
 
            if os.path.isfile(fpcand):
90
 
                cfg = fpcand
91
 
                break
92
 
        ops.append(makeop('install', name, config=None,
93
 
                   path=snapfile, cfgfile=cfg))
94
 
    return ops
95
 
 
96
 
 
97
 
def makeop(op, name, config=None, path=None, cfgfile=None):
98
 
    return({'op': op, 'name': name, 'config': config, 'path': path,
99
 
            'cfgfile': cfgfile})
100
 
 
101
 
 
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)
106
 
    if name in configs:
107
 
        return configs[name]
108
 
    return configs.get(name.partition(NAMESPACE_DELIM)[0])
109
 
 
110
 
 
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]
116
 
 
117
 
    if not packages:
118
 
        packages = []
119
 
    if not configs:
120
 
        configs = {}
121
 
 
122
 
    ops = []
123
 
    ops += get_fs_package_ops(fspath)
124
 
 
125
 
    for name in packages:
126
 
        ops.append(makeop('install', name, get_package_config(configs, name)))
127
 
 
128
 
    to_install = [f['name'] for f in ops]
129
 
    short_to_install = [f['name'].partition(NAMESPACE_DELIM)[0] for f in ops]
130
 
 
131
 
    for name in configs:
132
 
        if name in to_install:
133
 
            continue
134
 
        shortname = name.partition(NAMESPACE_DELIM)[0]
135
 
        if shortname in short_to_install:
136
 
            continue
137
 
        if name in installed or shortname in short_installed:
138
 
            ops.append(makeop('config', name,
139
 
                              config=get_package_config(configs, name)))
140
 
 
141
 
    # prefer config entries to filepath entries
142
 
    for op in ops:
143
 
        if op['op'] != 'install' or not op['cfgfile']:
144
 
            continue
145
 
        name = op['name']
146
 
        fromcfg = get_package_config(configs, op['name'])
147
 
        if fromcfg:
148
 
            LOG.debug("preferring configs[%(name)s] over '%(cfgfile)s'", op)
149
 
            op['cfgfile'] = None
150
 
            op['config'] = fromcfg
151
 
 
152
 
    return ops
153
 
 
154
 
 
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)
158
 
 
159
 
    shortname = name.partition(NAMESPACE_DELIM)[0]
160
 
    try:
161
 
        cfg_tmpf = None
162
 
        if config is not None:
163
 
            # input to 'snappy config packagename' must have nested data. odd.
164
 
            # config:
165
 
            #   packagename:
166
 
            #      config
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())
171
 
            os.close(fd)
172
 
            cfgfile = cfg_tmpf
173
 
 
174
 
        cmd = [SNAPPY_CMD, op]
175
 
        if op == 'install':
176
 
            if path:
177
 
                cmd.append("--allow-unauthenticated")
178
 
                cmd.append(path)
179
 
            else:
180
 
                cmd.append(name)
181
 
            if cfgfile:
182
 
                cmd.append(cfgfile)
183
 
        elif op == 'config':
184
 
            cmd += [name, cfgfile]
185
 
 
186
 
        util.subp(cmd)
187
 
 
188
 
    finally:
189
 
        if cfg_tmpf:
190
 
            os.unlink(cfg_tmpf)
191
 
 
192
 
 
193
 
def read_installed_packages():
194
 
    ret = []
195
 
    for (name, date, version, dev) in read_pkg_data():
196
 
        if dev:
197
 
            ret.append(NAMESPACE_DELIM.join([name, dev]))
198
 
        else:
199
 
            ret.append(name)
200
 
    return ret
201
 
 
202
 
 
203
 
def read_pkg_data():
204
 
    out, err = util.subp([SNAPPY_CMD, "list"])
205
 
    pkg_data = []
206
 
    for line in out.splitlines()[1:]:
207
 
        toks = line.split(sep=None, maxsplit=3)
208
 
        if len(toks) == 3:
209
 
            (name, date, version) = toks
210
 
            dev = None
211
 
        else:
212
 
            (name, date, version, dev) = toks
213
 
        pkg_data.append((name, date, version, dev,))
214
 
    return pkg_data
215
 
 
216
 
 
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"
221
 
    if enabled:
222
 
        util.del_file(not_to_be_run)
223
 
        # this is an indempotent operation
224
 
        util.subp(["systemctl", "start", "ssh"])
225
 
    else:
226
 
        # this is an indempotent operation
227
 
        util.subp(["systemctl", "stop", "ssh"])
228
 
        util.write_file(not_to_be_run, "cloud-init\n")
229
 
 
230
 
 
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():
237
 
        return True
238
 
    if os.path.isdir("/etc/system-image/config.d/"):
239
 
        return True
240
 
    return False
241
 
 
242
 
 
243
 
def set_snappy_command():
244
 
    global SNAPPY_CMD
245
 
    if util.which("snappy-go"):
246
 
        SNAPPY_CMD = "snappy-go"
247
 
    else:
248
 
        SNAPPY_CMD = "snappy"
249
 
    LOG.debug("snappy command is '%s'", SNAPPY_CMD)
250
 
 
251
 
 
252
 
def handle(name, cfg, cloud, log, args):
253
 
    cfgin = cfg.get('snappy')
254
 
    if not cfgin:
255
 
        cfgin = {}
256
 
    mycfg = util.mergemanydict([cfgin, BUILTIN_CFG])
257
 
 
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)
261
 
        return
262
 
 
263
 
    if sys_snappy.lower() == "auto" and not(system_is_snappy()):
264
 
        LOG.debug("%s: 'auto' mode, and system not snappy", name)
265
 
        return
266
 
 
267
 
    set_snappy_command()
268
 
 
269
 
    pkg_ops = get_package_ops(packages=mycfg['packages'],
270
 
                              configs=mycfg['config'],
271
 
                              fspath=mycfg['packages_dir'])
272
 
 
273
 
    fails = []
274
 
    for pkg_op in pkg_ops:
275
 
        try:
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)
281
 
 
282
 
    # Default to disabling SSH
283
 
    ssh_enabled = mycfg.get('ssh_enabled', "auto")
284
 
 
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)
290
 
        if user_ssh_keys:
291
 
            LOG.debug("Enabling SSH, ssh keys found in datasource")
292
 
            ssh_enabled = True
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")
297
 
            ssh_enabled = True
298
 
    elif ssh_enabled not in (True, False):
299
 
        LOG.warn("Unknown value '%s' in ssh_enabled", ssh_enabled)
300
 
 
301
 
    disable_enable_ssh(ssh_enabled)
302
 
 
303
 
    if fails:
304
 
        raise Exception("failed to install/configure snaps")