~canonical-hw-cert/charms/xenial/snappy-device-agent/trunk

1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
1
# Copyright 2014-2015 Canonical Limited.
2
#
3
# This file is part of charm-helpers.
4
#
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
8
#
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Lesser General Public License for more details.
13
#
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
17
# Common python helper functions used for OpenStack charms.
18
from collections import OrderedDict
19
from functools import wraps
20
21
import subprocess
22
import json
23
import os
24
import sys
14 by Paul Larson
Add support for xenial
25
import re
26
import itertools
27
import functools
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
28
29
import six
14 by Paul Larson
Add support for xenial
30
import tempfile
31
import traceback
32
import uuid
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
33
import yaml
34
14 by Paul Larson
Add support for xenial
35
from charmhelpers.contrib.network import ip
36
37
from charmhelpers.core import (
38
    unitdata,
39
)
40
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
41
from charmhelpers.core.hookenv import (
14 by Paul Larson
Add support for xenial
42
    action_fail,
43
    action_set,
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
44
    config,
45
    log as juju_log,
46
    charm_dir,
14 by Paul Larson
Add support for xenial
47
    DEBUG,
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
48
    INFO,
14 by Paul Larson
Add support for xenial
49
    related_units,
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
50
    relation_ids,
14 by Paul Larson
Add support for xenial
51
    relation_set,
52
    status_set,
53
    hook_name
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
54
)
55
56
from charmhelpers.contrib.storage.linux.lvm import (
57
    deactivate_lvm_volume_group,
58
    is_lvm_physical_volume,
59
    remove_lvm_physical_volume,
60
)
61
62
from charmhelpers.contrib.network.ip import (
14 by Paul Larson
Add support for xenial
63
    get_ipv6_addr,
64
    is_ipv6,
65
    port_has_listener,
66
)
67
68
from charmhelpers.contrib.python.packages import (
69
    pip_create_virtualenv,
70
    pip_install,
71
)
72
73
from charmhelpers.core.host import (
74
    lsb_release,
75
    mounts,
76
    umount,
77
    service_running,
78
    service_pause,
79
    service_resume,
80
    restart_on_change_helper,
81
)
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
82
from charmhelpers.fetch import apt_install, apt_cache, install_remote
83
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
84
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
85
86
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
87
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
88
89
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
90
                   'restricted main multiverse universe')
91
92
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
93
    ('oneiric', 'diablo'),
94
    ('precise', 'essex'),
95
    ('quantal', 'folsom'),
96
    ('raring', 'grizzly'),
97
    ('saucy', 'havana'),
98
    ('trusty', 'icehouse'),
99
    ('utopic', 'juno'),
100
    ('vivid', 'kilo'),
14 by Paul Larson
Add support for xenial
101
    ('wily', 'liberty'),
102
    ('xenial', 'mitaka'),
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
103
])
104
105
106
OPENSTACK_CODENAMES = OrderedDict([
107
    ('2011.2', 'diablo'),
108
    ('2012.1', 'essex'),
109
    ('2012.2', 'folsom'),
110
    ('2013.1', 'grizzly'),
111
    ('2013.2', 'havana'),
112
    ('2014.1', 'icehouse'),
113
    ('2014.2', 'juno'),
114
    ('2015.1', 'kilo'),
14 by Paul Larson
Add support for xenial
115
    ('2015.2', 'liberty'),
116
    ('2016.1', 'mitaka'),
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
117
])
118
14 by Paul Larson
Add support for xenial
119
# The ugly duckling - must list releases oldest to newest
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
120
SWIFT_CODENAMES = OrderedDict([
14 by Paul Larson
Add support for xenial
121
    ('diablo',
122
        ['1.4.3']),
123
    ('essex',
124
        ['1.4.8']),
125
    ('folsom',
126
        ['1.7.4']),
127
    ('grizzly',
128
        ['1.7.6', '1.7.7', '1.8.0']),
129
    ('havana',
130
        ['1.9.0', '1.9.1', '1.10.0']),
131
    ('icehouse',
132
        ['1.11.0', '1.12.0', '1.13.0', '1.13.1']),
133
    ('juno',
134
        ['2.0.0', '2.1.0', '2.2.0']),
135
    ('kilo',
136
        ['2.2.1', '2.2.2']),
137
    ('liberty',
138
        ['2.3.0', '2.4.0', '2.5.0']),
139
    ('mitaka',
140
        ['2.5.0', '2.6.0', '2.7.0']),
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
141
])
142
14 by Paul Larson
Add support for xenial
143
# >= Liberty version->codename mapping
144
PACKAGE_CODENAMES = {
145
    'nova-common': OrderedDict([
146
        ('12.0', 'liberty'),
147
        ('13.0', 'mitaka'),
148
    ]),
149
    'neutron-common': OrderedDict([
150
        ('7.0', 'liberty'),
151
        ('8.0', 'mitaka'),
152
        ('8.1', 'mitaka'),
153
    ]),
154
    'cinder-common': OrderedDict([
155
        ('7.0', 'liberty'),
156
        ('8.0', 'mitaka'),
157
    ]),
158
    'keystone': OrderedDict([
159
        ('8.0', 'liberty'),
160
        ('8.1', 'liberty'),
161
        ('9.0', 'mitaka'),
162
    ]),
163
    'horizon-common': OrderedDict([
164
        ('8.0', 'liberty'),
165
        ('9.0', 'mitaka'),
166
    ]),
167
    'ceilometer-common': OrderedDict([
168
        ('5.0', 'liberty'),
169
        ('6.0', 'mitaka'),
170
    ]),
171
    'heat-common': OrderedDict([
172
        ('5.0', 'liberty'),
173
        ('6.0', 'mitaka'),
174
    ]),
175
    'glance-common': OrderedDict([
176
        ('11.0', 'liberty'),
177
        ('12.0', 'mitaka'),
178
    ]),
179
    'openstack-dashboard': OrderedDict([
180
        ('8.0', 'liberty'),
181
        ('9.0', 'mitaka'),
182
    ]),
183
}
184
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
185
DEFAULT_LOOPBACK_SIZE = '5G'
186
187
188
def error_out(msg):
189
    juju_log("FATAL ERROR: %s" % msg, level='ERROR')
190
    sys.exit(1)
191
192
193
def get_os_codename_install_source(src):
194
    '''Derive OpenStack release codename from a given installation source.'''
195
    ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
196
    rel = ''
197
    if src is None:
198
        return rel
199
    if src in ['distro', 'distro-proposed']:
200
        try:
201
            rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
202
        except KeyError:
203
            e = 'Could not derive openstack release for '\
204
                'this Ubuntu release: %s' % ubuntu_rel
205
            error_out(e)
206
        return rel
207
208
    if src.startswith('cloud:'):
209
        ca_rel = src.split(':')[1]
210
        ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
211
        return ca_rel
212
213
    # Best guess match based on deb string provided
214
    if src.startswith('deb') or src.startswith('ppa'):
215
        for k, v in six.iteritems(OPENSTACK_CODENAMES):
216
            if v in src:
217
                return v
218
219
220
def get_os_version_install_source(src):
221
    codename = get_os_codename_install_source(src)
222
    return get_os_version_codename(codename)
223
224
225
def get_os_codename_version(vers):
226
    '''Determine OpenStack codename from version number.'''
227
    try:
228
        return OPENSTACK_CODENAMES[vers]
229
    except KeyError:
230
        e = 'Could not determine OpenStack codename for version %s' % vers
231
        error_out(e)
232
233
14 by Paul Larson
Add support for xenial
234
def get_os_version_codename(codename, version_map=OPENSTACK_CODENAMES):
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
235
    '''Determine OpenStack version number from codename.'''
14 by Paul Larson
Add support for xenial
236
    for k, v in six.iteritems(version_map):
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
237
        if v == codename:
238
            return k
239
    e = 'Could not derive OpenStack version for '\
240
        'codename: %s' % codename
241
    error_out(e)
242
243
14 by Paul Larson
Add support for xenial
244
def get_os_version_codename_swift(codename):
245
    '''Determine OpenStack version number of swift from codename.'''
246
    for k, v in six.iteritems(SWIFT_CODENAMES):
247
        if k == codename:
248
            return v[-1]
249
    e = 'Could not derive swift version for '\
250
        'codename: %s' % codename
251
    error_out(e)
252
253
254
def get_swift_codename(version):
255
    '''Determine OpenStack codename that corresponds to swift version.'''
256
    codenames = [k for k, v in six.iteritems(SWIFT_CODENAMES) if version in v]
257
    if len(codenames) > 1:
258
        # If more than one release codename contains this version we determine
259
        # the actual codename based on the highest available install source.
260
        for codename in reversed(codenames):
261
            releases = UBUNTU_OPENSTACK_RELEASE
262
            release = [k for k, v in six.iteritems(releases) if codename in v]
263
            ret = subprocess.check_output(['apt-cache', 'policy', 'swift'])
264
            if codename in ret or release[0] in ret:
265
                return codename
266
    elif len(codenames) == 1:
267
        return codenames[0]
268
    return None
269
270
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
271
def get_os_codename_package(package, fatal=True):
272
    '''Derive OpenStack release codename from an installed package.'''
273
    import apt_pkg as apt
274
275
    cache = apt_cache()
276
277
    try:
278
        pkg = cache[package]
279
    except:
280
        if not fatal:
281
            return None
282
        # the package is unknown to the current apt cache.
283
        e = 'Could not determine version of package with no installation '\
284
            'candidate: %s' % package
285
        error_out(e)
286
287
    if not pkg.current_ver:
288
        if not fatal:
289
            return None
290
        # package is known, but no version is currently installed.
291
        e = 'Could not determine version of uninstalled package: %s' % package
292
        error_out(e)
293
294
    vers = apt.upstream_version(pkg.current_ver.ver_str)
14 by Paul Larson
Add support for xenial
295
    if 'swift' in pkg.name:
296
        # Fully x.y.z match for swift versions
297
        match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)
298
    else:
299
        # x.y match only for 20XX.X
300
        # and ignore patch level for other packages
301
        match = re.match('^(\d+)\.(\d+)', vers)
302
303
    if match:
304
        vers = match.group(0)
305
306
    # >= Liberty independent project versions
307
    if (package in PACKAGE_CODENAMES and
308
            vers in PACKAGE_CODENAMES[package]):
309
        return PACKAGE_CODENAMES[package][vers]
310
    else:
311
        # < Liberty co-ordinated project versions
312
        try:
313
            if 'swift' in pkg.name:
314
                return get_swift_codename(vers)
315
            else:
316
                return OPENSTACK_CODENAMES[vers]
317
        except KeyError:
318
            if not fatal:
319
                return None
320
            e = 'Could not determine OpenStack codename for version %s' % vers
321
            error_out(e)
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
322
323
324
def get_os_version_package(pkg, fatal=True):
325
    '''Derive OpenStack version number from an installed package.'''
326
    codename = get_os_codename_package(pkg, fatal=fatal)
327
328
    if not codename:
329
        return None
330
331
    if 'swift' in pkg:
332
        vers_map = SWIFT_CODENAMES
14 by Paul Larson
Add support for xenial
333
        for cname, version in six.iteritems(vers_map):
334
            if cname == codename:
335
                return version[-1]
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
336
    else:
337
        vers_map = OPENSTACK_CODENAMES
14 by Paul Larson
Add support for xenial
338
        for version, cname in six.iteritems(vers_map):
339
            if cname == codename:
340
                return version
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
341
    # e = "Could not determine OpenStack version for package: %s" % pkg
342
    # error_out(e)
343
344
345
os_rel = None
346
347
348
def os_release(package, base='essex'):
349
    '''
350
    Returns OpenStack release codename from a cached global.
351
    If the codename can not be determined from either an installed package or
352
    the installation source, the earliest release supported by the charm should
353
    be returned.
354
    '''
355
    global os_rel
356
    if os_rel:
357
        return os_rel
358
    os_rel = (get_os_codename_package(package, fatal=False) or
359
              get_os_codename_install_source(config('openstack-origin')) or
360
              base)
361
    return os_rel
362
363
364
def import_key(keyid):
14 by Paul Larson
Add support for xenial
365
    key = keyid.strip()
366
    if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
367
            key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
368
        juju_log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
369
        juju_log("Importing ASCII Armor PGP key", level=DEBUG)
370
        with tempfile.NamedTemporaryFile() as keyfile:
371
            with open(keyfile.name, 'w') as fd:
372
                fd.write(key)
373
                fd.write("\n")
374
375
            cmd = ['apt-key', 'add', keyfile.name]
376
            try:
377
                subprocess.check_call(cmd)
378
            except subprocess.CalledProcessError:
379
                error_out("Error importing PGP key '%s'" % key)
380
    else:
381
        juju_log("PGP key found (looks like Radix64 format)", level=DEBUG)
382
        juju_log("Importing PGP key from keyserver", level=DEBUG)
383
        cmd = ['apt-key', 'adv', '--keyserver',
384
               'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
385
        try:
386
            subprocess.check_call(cmd)
387
        except subprocess.CalledProcessError:
388
            error_out("Error importing PGP key '%s'" % key)
389
390
391
def get_source_and_pgp_key(input):
392
    """Look for a pgp key ID or ascii-armor key in the given input."""
393
    index = input.strip()
394
    index = input.rfind('|')
395
    if index < 0:
396
        return input, None
397
398
    key = input[index + 1:].strip('|')
399
    source = input[:index]
400
    return source, key
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
401
402
403
def configure_installation_source(rel):
404
    '''Configure apt installation source.'''
405
    if rel == 'distro':
406
        return
407
    elif rel == 'distro-proposed':
408
        ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
409
        with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
410
            f.write(DISTRO_PROPOSED % ubuntu_rel)
411
    elif rel[:4] == "ppa:":
14 by Paul Larson
Add support for xenial
412
        src, key = get_source_and_pgp_key(rel)
413
        if key:
414
            import_key(key)
415
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
416
        subprocess.check_call(["add-apt-repository", "-y", src])
417
    elif rel[:3] == "deb":
14 by Paul Larson
Add support for xenial
418
        src, key = get_source_and_pgp_key(rel)
419
        if key:
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
420
            import_key(key)
14 by Paul Larson
Add support for xenial
421
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
422
        with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
423
            f.write(src)
424
    elif rel[:6] == 'cloud:':
425
        ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
426
        rel = rel.split(':')[1]
427
        u_rel = rel.split('-')[0]
428
        ca_rel = rel.split('-')[1]
429
430
        if u_rel != ubuntu_rel:
431
            e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
432
                'version (%s)' % (ca_rel, ubuntu_rel)
433
            error_out(e)
434
435
        if 'staging' in ca_rel:
436
            # staging is just a regular PPA.
437
            os_rel = ca_rel.split('/')[0]
438
            ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
439
            cmd = 'add-apt-repository -y %s' % ppa
440
            subprocess.check_call(cmd.split(' '))
441
            return
442
443
        # map charm config options to actual archive pockets.
444
        pockets = {
445
            'folsom': 'precise-updates/folsom',
446
            'folsom/updates': 'precise-updates/folsom',
447
            'folsom/proposed': 'precise-proposed/folsom',
448
            'grizzly': 'precise-updates/grizzly',
449
            'grizzly/updates': 'precise-updates/grizzly',
450
            'grizzly/proposed': 'precise-proposed/grizzly',
451
            'havana': 'precise-updates/havana',
452
            'havana/updates': 'precise-updates/havana',
453
            'havana/proposed': 'precise-proposed/havana',
454
            'icehouse': 'precise-updates/icehouse',
455
            'icehouse/updates': 'precise-updates/icehouse',
456
            'icehouse/proposed': 'precise-proposed/icehouse',
457
            'juno': 'trusty-updates/juno',
458
            'juno/updates': 'trusty-updates/juno',
459
            'juno/proposed': 'trusty-proposed/juno',
460
            'kilo': 'trusty-updates/kilo',
461
            'kilo/updates': 'trusty-updates/kilo',
462
            'kilo/proposed': 'trusty-proposed/kilo',
14 by Paul Larson
Add support for xenial
463
            'liberty': 'trusty-updates/liberty',
464
            'liberty/updates': 'trusty-updates/liberty',
465
            'liberty/proposed': 'trusty-proposed/liberty',
466
            'mitaka': 'trusty-updates/mitaka',
467
            'mitaka/updates': 'trusty-updates/mitaka',
468
            'mitaka/proposed': 'trusty-proposed/mitaka',
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
469
        }
470
471
        try:
472
            pocket = pockets[ca_rel]
473
        except KeyError:
474
            e = 'Invalid Cloud Archive release specified: %s' % rel
475
            error_out(e)
476
477
        src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
478
        apt_install('ubuntu-cloud-keyring', fatal=True)
479
480
        with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
481
            f.write(src)
482
    else:
483
        error_out("Invalid openstack-release specified: %s" % rel)
484
485
14 by Paul Larson
Add support for xenial
486
def config_value_changed(option):
487
    """
488
    Determine if config value changed since last call to this function.
489
    """
490
    hook_data = unitdata.HookData()
491
    with hook_data():
492
        db = unitdata.kv()
493
        current = config(option)
494
        saved = db.get(option)
495
        db.set(option, current)
496
        if saved is None:
497
            return False
498
        return current != saved
499
500
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
501
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
502
    """
503
    Write an rc file in the charm-delivered directory containing
504
    exported environment variables provided by env_vars. Any charm scripts run
505
    outside the juju hook environment can source this scriptrc to obtain
506
    updated config information necessary to perform health checks or
507
    service changes.
508
    """
509
    juju_rc_path = "%s/%s" % (charm_dir(), script_path)
510
    if not os.path.exists(os.path.dirname(juju_rc_path)):
511
        os.mkdir(os.path.dirname(juju_rc_path))
512
    with open(juju_rc_path, 'wb') as rc_script:
513
        rc_script.write(
514
            "#!/bin/bash\n")
515
        [rc_script.write('export %s=%s\n' % (u, p))
516
         for u, p in six.iteritems(env_vars) if u != "script_path"]
517
518
519
def openstack_upgrade_available(package):
520
    """
521
    Determines if an OpenStack upgrade is available from installation
522
    source, based on version of installed package.
523
524
    :param package: str: Name of installed package.
525
526
    :returns: bool:    : Returns True if configured installation source offers
527
                         a newer version of package.
528
529
    """
530
531
    import apt_pkg as apt
532
    src = config('openstack-origin')
533
    cur_vers = get_os_version_package(package)
14 by Paul Larson
Add support for xenial
534
    if "swift" in package:
535
        codename = get_os_codename_install_source(src)
536
        avail_vers = get_os_version_codename_swift(codename)
537
    else:
538
        avail_vers = get_os_version_install_source(src)
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
539
    apt.init()
14 by Paul Larson
Add support for xenial
540
    if "swift" in package:
541
        major_cur_vers = cur_vers.split('.', 1)[0]
542
        major_avail_vers = avail_vers.split('.', 1)[0]
543
        major_diff = apt.version_compare(major_avail_vers, major_cur_vers)
544
        return avail_vers > cur_vers and (major_diff == 1 or major_diff == 0)
545
    return apt.version_compare(avail_vers, cur_vers) == 1
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
546
547
548
def ensure_block_device(block_device):
549
    '''
550
    Confirm block_device, create as loopback if necessary.
551
552
    :param block_device: str: Full path of block device to ensure.
553
554
    :returns: str: Full path of ensured block device.
555
    '''
556
    _none = ['None', 'none', None]
557
    if (block_device in _none):
558
        error_out('prepare_storage(): Missing required input: block_device=%s.'
559
                  % block_device)
560
561
    if block_device.startswith('/dev/'):
562
        bdev = block_device
563
    elif block_device.startswith('/'):
564
        _bd = block_device.split('|')
565
        if len(_bd) == 2:
566
            bdev, size = _bd
567
        else:
568
            bdev = block_device
569
            size = DEFAULT_LOOPBACK_SIZE
570
        bdev = ensure_loopback_device(bdev, size)
571
    else:
572
        bdev = '/dev/%s' % block_device
573
574
    if not is_block_device(bdev):
575
        error_out('Failed to locate valid block device at %s' % bdev)
576
577
    return bdev
578
579
580
def clean_storage(block_device):
581
    '''
582
    Ensures a block device is clean.  That is:
583
        - unmounted
584
        - any lvm volume groups are deactivated
585
        - any lvm physical device signatures removed
586
        - partition table wiped
587
588
    :param block_device: str: Full path to block device to clean.
589
    '''
590
    for mp, d in mounts():
591
        if d == block_device:
592
            juju_log('clean_storage(): %s is mounted @ %s, unmounting.' %
593
                     (d, mp), level=INFO)
594
            umount(mp, persist=True)
595
596
    if is_lvm_physical_volume(block_device):
597
        deactivate_lvm_volume_group(block_device)
598
        remove_lvm_physical_volume(block_device)
599
    else:
600
        zap_disk(block_device)
601
14 by Paul Larson
Add support for xenial
602
is_ip = ip.is_ip
603
ns_query = ip.ns_query
604
get_host_ip = ip.get_host_ip
605
get_hostname = ip.get_hostname
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
606
607
608
def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):
609
    mm_map = {}
610
    if os.path.isfile(mm_file):
611
        with open(mm_file, 'r') as f:
612
            mm_map = json.load(f)
613
    return mm_map
614
615
616
def sync_db_with_multi_ipv6_addresses(database, database_user,
617
                                      relation_prefix=None):
618
    hosts = get_ipv6_addr(dynamic_only=False)
619
14 by Paul Larson
Add support for xenial
620
    if config('vip'):
621
        vips = config('vip').split()
622
        for vip in vips:
623
            if vip and is_ipv6(vip):
624
                hosts.append(vip)
625
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
626
    kwargs = {'database': database,
627
              'username': database_user,
628
              'hostname': json.dumps(hosts)}
629
630
    if relation_prefix:
631
        for key in list(kwargs.keys()):
632
            kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key]
633
            del kwargs[key]
634
635
    for rid in relation_ids('shared-db'):
636
        relation_set(relation_id=rid, **kwargs)
637
638
639
def os_requires_version(ostack_release, pkg):
640
    """
641
    Decorator for hook to specify minimum supported release
642
    """
643
    def wrap(f):
644
        @wraps(f)
645
        def wrapped_f(*args):
646
            if os_release(pkg) < ostack_release:
647
                raise Exception("This hook is not supported on releases"
648
                                " before %s" % ostack_release)
649
            f(*args)
650
        return wrapped_f
651
    return wrap
652
653
654
def git_install_requested():
14 by Paul Larson
Add support for xenial
655
    """
656
    Returns true if openstack-origin-git is specified.
657
    """
658
    return config('openstack-origin-git') is not None
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
659
660
661
requirements_dir = None
662
663
14 by Paul Larson
Add support for xenial
664
def _git_yaml_load(projects_yaml):
665
    """
666
    Load the specified yaml into a dictionary.
667
    """
668
    if not projects_yaml:
669
        return None
670
671
    return yaml.load(projects_yaml)
672
673
674
def git_clone_and_install(projects_yaml, core_project):
675
    """
676
    Clone/install all specified OpenStack repositories.
677
678
    The expected format of projects_yaml is:
679
680
        repositories:
681
          - {name: keystone,
682
             repository: 'git://git.openstack.org/openstack/keystone.git',
683
             branch: 'stable/icehouse'}
684
          - {name: requirements,
685
             repository: 'git://git.openstack.org/openstack/requirements.git',
686
             branch: 'stable/icehouse'}
687
688
        directory: /mnt/openstack-git
689
        http_proxy: squid-proxy-url
690
        https_proxy: squid-proxy-url
691
692
    The directory, http_proxy, and https_proxy keys are optional.
693
694
    """
695
    global requirements_dir
696
    parent_dir = '/mnt/openstack-git'
697
    http_proxy = None
698
699
    projects = _git_yaml_load(projects_yaml)
700
    _git_validate_projects_yaml(projects, core_project)
701
702
    old_environ = dict(os.environ)
703
704
    if 'http_proxy' in projects.keys():
705
        http_proxy = projects['http_proxy']
706
        os.environ['http_proxy'] = projects['http_proxy']
707
    if 'https_proxy' in projects.keys():
708
        os.environ['https_proxy'] = projects['https_proxy']
709
710
    if 'directory' in projects.keys():
711
        parent_dir = projects['directory']
712
713
    pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
714
715
    # Upgrade setuptools and pip from default virtualenv versions. The default
716
    # versions in trusty break master OpenStack branch deployments.
717
    for p in ['pip', 'setuptools']:
718
        pip_install(p, upgrade=True, proxy=http_proxy,
719
                    venv=os.path.join(parent_dir, 'venv'))
720
721
    for p in projects['repositories']:
722
        repo = p['repository']
723
        branch = p['branch']
724
        depth = '1'
725
        if 'depth' in p.keys():
726
            depth = p['depth']
727
        if p['name'] == 'requirements':
728
            repo_dir = _git_clone_and_install_single(repo, branch, depth,
729
                                                     parent_dir, http_proxy,
730
                                                     update_requirements=False)
731
            requirements_dir = repo_dir
732
        else:
733
            repo_dir = _git_clone_and_install_single(repo, branch, depth,
734
                                                     parent_dir, http_proxy,
735
                                                     update_requirements=True)
736
737
    os.environ = old_environ
738
739
740
def _git_validate_projects_yaml(projects, core_project):
741
    """
742
    Validate the projects yaml.
743
    """
744
    _git_ensure_key_exists('repositories', projects)
745
746
    for project in projects['repositories']:
747
        _git_ensure_key_exists('name', project.keys())
748
        _git_ensure_key_exists('repository', project.keys())
749
        _git_ensure_key_exists('branch', project.keys())
750
751
    if projects['repositories'][0]['name'] != 'requirements':
752
        error_out('{} git repo must be specified first'.format('requirements'))
753
754
    if projects['repositories'][-1]['name'] != core_project:
755
        error_out('{} git repo must be specified last'.format(core_project))
756
757
758
def _git_ensure_key_exists(key, keys):
759
    """
760
    Ensure that key exists in keys.
761
    """
762
    if key not in keys:
763
        error_out('openstack-origin-git key \'{}\' is missing'.format(key))
764
765
766
def _git_clone_and_install_single(repo, branch, depth, parent_dir, http_proxy,
767
                                  update_requirements):
768
    """
769
    Clone and install a single git repository.
770
    """
771
    if not os.path.exists(parent_dir):
772
        juju_log('Directory already exists at {}. '
773
                 'No need to create directory.'.format(parent_dir))
774
        os.mkdir(parent_dir)
775
776
    juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
777
    repo_dir = install_remote(
778
        repo, dest=parent_dir, branch=branch, depth=depth)
779
780
    venv = os.path.join(parent_dir, 'venv')
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
781
782
    if update_requirements:
783
        if not requirements_dir:
784
            error_out('requirements repo must be cloned before '
785
                      'updating from global requirements.')
14 by Paul Larson
Add support for xenial
786
        _git_update_requirements(venv, repo_dir, requirements_dir)
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
787
788
    juju_log('Installing git repo from dir: {}'.format(repo_dir))
14 by Paul Larson
Add support for xenial
789
    if http_proxy:
790
        pip_install(repo_dir, proxy=http_proxy, venv=venv)
791
    else:
792
        pip_install(repo_dir, venv=venv)
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
793
794
    return repo_dir
795
796
14 by Paul Larson
Add support for xenial
797
def _git_update_requirements(venv, package_dir, reqs_dir):
798
    """
799
    Update from global requirements.
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
800
14 by Paul Larson
Add support for xenial
801
    Update an OpenStack git directory's requirements.txt and
802
    test-requirements.txt from global-requirements.txt.
803
    """
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
804
    orig_dir = os.getcwd()
805
    os.chdir(reqs_dir)
14 by Paul Larson
Add support for xenial
806
    python = os.path.join(venv, 'bin/python')
807
    cmd = [python, 'update.py', package_dir]
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
808
    try:
14 by Paul Larson
Add support for xenial
809
        subprocess.check_call(cmd)
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
810
    except subprocess.CalledProcessError:
811
        package = os.path.basename(package_dir)
14 by Paul Larson
Add support for xenial
812
        error_out("Error updating {} from "
813
                  "global-requirements.txt".format(package))
1 by Paul Larson
snappy-device-agent charm for deploying provisioning kits to work with SPI
814
    os.chdir(orig_dir)
14 by Paul Larson
Add support for xenial
815
816
817
def git_pip_venv_dir(projects_yaml):
818
    """
819
    Return the pip virtualenv path.
820
    """
821
    parent_dir = '/mnt/openstack-git'
822
823
    projects = _git_yaml_load(projects_yaml)
824
825
    if 'directory' in projects.keys():
826
        parent_dir = projects['directory']
827
828
    return os.path.join(parent_dir, 'venv')
829
830
831
def git_src_dir(projects_yaml, project):
832
    """
833
    Return the directory where the specified project's source is located.
834
    """
835
    parent_dir = '/mnt/openstack-git'
836
837
    projects = _git_yaml_load(projects_yaml)
838
839
    if 'directory' in projects.keys():
840
        parent_dir = projects['directory']
841
842
    for p in projects['repositories']:
843
        if p['name'] == project:
844
            return os.path.join(parent_dir, os.path.basename(p['repository']))
845
846
    return None
847
848
849
def git_yaml_value(projects_yaml, key):
850
    """
851
    Return the value in projects_yaml for the specified key.
852
    """
853
    projects = _git_yaml_load(projects_yaml)
854
855
    if key in projects.keys():
856
        return projects[key]
857
858
    return None
859
860
861
def os_workload_status(configs, required_interfaces, charm_func=None):
862
    """
863
    Decorator to set workload status based on complete contexts
864
    """
865
    def wrap(f):
866
        @wraps(f)
867
        def wrapped_f(*args, **kwargs):
868
            # Run the original function first
869
            f(*args, **kwargs)
870
            # Set workload status now that contexts have been
871
            # acted on
872
            set_os_workload_status(configs, required_interfaces, charm_func)
873
        return wrapped_f
874
    return wrap
875
876
877
def set_os_workload_status(configs, required_interfaces, charm_func=None,
878
                           services=None, ports=None):
879
    """Set the state of the workload status for the charm.
880
881
    This calls _determine_os_workload_status() to get the new state, message
882
    and sets the status using status_set()
883
884
    @param configs: a templating.OSConfigRenderer() object
885
    @param required_interfaces: {generic: [specific, specific2, ...]}
886
    @param charm_func: a callable function that returns state, message. The
887
                       signature is charm_func(configs) -> (state, message)
888
    @param services: list of strings OR dictionary specifying services/ports
889
    @param ports: OPTIONAL list of port numbers.
890
    @returns state, message: the new workload status, user message
891
    """
892
    state, message = _determine_os_workload_status(
893
        configs, required_interfaces, charm_func, services, ports)
894
    status_set(state, message)
895
896
897
def _determine_os_workload_status(
898
        configs, required_interfaces, charm_func=None,
899
        services=None, ports=None):
900
    """Determine the state of the workload status for the charm.
901
902
    This function returns the new workload status for the charm based
903
    on the state of the interfaces, the paused state and whether the
904
    services are actually running and any specified ports are open.
905
906
    This checks:
907
908
     1. if the unit should be paused, that it is actually paused.  If so the
909
        state is 'maintenance' + message, else 'broken'.
910
     2. that the interfaces/relations are complete.  If they are not then
911
        it sets the state to either 'broken' or 'waiting' and an appropriate
912
        message.
913
     3. If all the relation data is set, then it checks that the actual
914
        services really are running.  If not it sets the state to 'broken'.
915
916
    If everything is okay then the state returns 'active'.
917
918
    @param configs: a templating.OSConfigRenderer() object
919
    @param required_interfaces: {generic: [specific, specific2, ...]}
920
    @param charm_func: a callable function that returns state, message. The
921
                       signature is charm_func(configs) -> (state, message)
922
    @param services: list of strings OR dictionary specifying services/ports
923
    @param ports: OPTIONAL list of port numbers.
924
    @returns state, message: the new workload status, user message
925
    """
926
    state, message = _ows_check_if_paused(services, ports)
927
928
    if state is None:
929
        state, message = _ows_check_generic_interfaces(
930
            configs, required_interfaces)
931
932
    if state != 'maintenance' and charm_func:
933
        # _ows_check_charm_func() may modify the state, message
934
        state, message = _ows_check_charm_func(
935
            state, message, lambda: charm_func(configs))
936
937
    if state is None:
938
        state, message = _ows_check_services_running(services, ports)
939
940
    if state is None:
941
        state = 'active'
942
        message = "Unit is ready"
943
        juju_log(message, 'INFO')
944
945
    return state, message
946
947
948
def _ows_check_if_paused(services=None, ports=None):
949
    """Check if the unit is supposed to be paused, and if so check that the
950
    services/ports (if passed) are actually stopped/not being listened to.
951
952
    if the unit isn't supposed to be paused, just return None, None
953
954
    @param services: OPTIONAL services spec or list of service names.
955
    @param ports: OPTIONAL list of port numbers.
956
    @returns state, message or None, None
957
    """
958
    if is_unit_paused_set():
959
        state, message = check_actually_paused(services=services,
960
                                               ports=ports)
961
        if state is None:
962
            # we're paused okay, so set maintenance and return
963
            state = "maintenance"
964
            message = "Paused. Use 'resume' action to resume normal service."
965
        return state, message
966
    return None, None
967
968
969
def _ows_check_generic_interfaces(configs, required_interfaces):
970
    """Check the complete contexts to determine the workload status.
971
972
     - Checks for missing or incomplete contexts
973
     - juju log details of missing required data.
974
     - determines the correct workload status
975
     - creates an appropriate message for status_set(...)
976
977
    if there are no problems then the function returns None, None
978
979
    @param configs: a templating.OSConfigRenderer() object
980
    @params required_interfaces: {generic_interface: [specific_interface], }
981
    @returns state, message or None, None
982
    """
983
    incomplete_rel_data = incomplete_relation_data(configs,
984
                                                   required_interfaces)
985
    state = None
986
    message = None
987
    missing_relations = set()
988
    incomplete_relations = set()
989
990
    for generic_interface, relations_states in incomplete_rel_data.items():
991
        related_interface = None
992
        missing_data = {}
993
        # Related or not?
994
        for interface, relation_state in relations_states.items():
995
            if relation_state.get('related'):
996
                related_interface = interface
997
                missing_data = relation_state.get('missing_data')
998
                break
999
        # No relation ID for the generic_interface?
1000
        if not related_interface:
1001
            juju_log("{} relation is missing and must be related for "
1002
                     "functionality. ".format(generic_interface), 'WARN')
1003
            state = 'blocked'
1004
            missing_relations.add(generic_interface)
1005
        else:
1006
            # Relation ID eists but no related unit
1007
            if not missing_data:
1008
                # Edge case - relation ID exists but departings
1009
                _hook_name = hook_name()
1010
                if (('departed' in _hook_name or 'broken' in _hook_name) and
1011
                        related_interface in _hook_name):
1012
                    state = 'blocked'
1013
                    missing_relations.add(generic_interface)
1014
                    juju_log("{} relation's interface, {}, "
1015
                             "relationship is departed or broken "
1016
                             "and is required for functionality."
1017
                             "".format(generic_interface, related_interface),
1018
                             "WARN")
1019
                # Normal case relation ID exists but no related unit
1020
                # (joining)
1021
                else:
1022
                    juju_log("{} relations's interface, {}, is related but has"
1023
                             " no units in the relation."
1024
                             "".format(generic_interface, related_interface),
1025
                             "INFO")
1026
            # Related unit exists and data missing on the relation
1027
            else:
1028
                juju_log("{} relation's interface, {}, is related awaiting "
1029
                         "the following data from the relationship: {}. "
1030
                         "".format(generic_interface, related_interface,
1031
                                   ", ".join(missing_data)), "INFO")
1032
            if state != 'blocked':
1033
                state = 'waiting'
1034
            if generic_interface not in missing_relations:
1035
                incomplete_relations.add(generic_interface)
1036
1037
    if missing_relations:
1038
        message = "Missing relations: {}".format(", ".join(missing_relations))
1039
        if incomplete_relations:
1040
            message += "; incomplete relations: {}" \
1041
                       "".format(", ".join(incomplete_relations))
1042
        state = 'blocked'
1043
    elif incomplete_relations:
1044
        message = "Incomplete relations: {}" \
1045
                  "".format(", ".join(incomplete_relations))
1046
        state = 'waiting'
1047
1048
    return state, message
1049
1050
1051
def _ows_check_charm_func(state, message, charm_func_with_configs):
1052
    """Run a custom check function for the charm to see if it wants to
1053
    change the state.  This is only run if not in 'maintenance' and
1054
    tests to see if the new state is more important that the previous
1055
    one determined by the interfaces/relations check.
1056
1057
    @param state: the previously determined state so far.
1058
    @param message: the user orientated message so far.
1059
    @param charm_func: a callable function that returns state, message
1060
    @returns state, message strings.
1061
    """
1062
    if charm_func_with_configs:
1063
        charm_state, charm_message = charm_func_with_configs()
1064
        if charm_state != 'active' and charm_state != 'unknown':
1065
            state = workload_state_compare(state, charm_state)
1066
            if message:
1067
                charm_message = charm_message.replace("Incomplete relations: ",
1068
                                                      "")
1069
                message = "{}, {}".format(message, charm_message)
1070
            else:
1071
                message = charm_message
1072
    return state, message
1073
1074
1075
def _ows_check_services_running(services, ports):
1076
    """Check that the services that should be running are actually running
1077
    and that any ports specified are being listened to.
1078
1079
    @param services: list of strings OR dictionary specifying services/ports
1080
    @param ports: list of ports
1081
    @returns state, message: strings or None, None
1082
    """
1083
    messages = []
1084
    state = None
1085
    if services is not None:
1086
        services = _extract_services_list_helper(services)
1087
        services_running, running = _check_running_services(services)
1088
        if not all(running):
1089
            messages.append(
1090
                "Services not running that should be: {}"
1091
                .format(", ".join(_filter_tuples(services_running, False))))
1092
            state = 'blocked'
1093
        # also verify that the ports that should be open are open
1094
        # NB, that ServiceManager objects only OPTIONALLY have ports
1095
        map_not_open, ports_open = (
1096
            _check_listening_on_services_ports(services))
1097
        if not all(ports_open):
1098
            # find which service has missing ports. They are in service
1099
            # order which makes it a bit easier.
1100
            message_parts = {service: ", ".join([str(v) for v in open_ports])
1101
                             for service, open_ports in map_not_open.items()}
1102
            message = ", ".join(
1103
                ["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1104
            messages.append(
1105
                "Services with ports not open that should be: {}"
1106
                .format(message))
1107
            state = 'blocked'
1108
1109
    if ports is not None:
1110
        # and we can also check ports which we don't know the service for
1111
        ports_open, ports_open_bools = _check_listening_on_ports_list(ports)
1112
        if not all(ports_open_bools):
1113
            messages.append(
1114
                "Ports which should be open, but are not: {}"
1115
                .format(", ".join([str(p) for p, v in ports_open
1116
                                   if not v])))
1117
            state = 'blocked'
1118
1119
    if state is not None:
1120
        message = "; ".join(messages)
1121
        return state, message
1122
1123
    return None, None
1124
1125
1126
def _extract_services_list_helper(services):
1127
    """Extract a OrderedDict of {service: [ports]} of the supplied services
1128
    for use by the other functions.
1129
1130
    The services object can either be:
1131
      - None : no services were passed (an empty dict is returned)
1132
      - a list of strings
1133
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1134
      - An array of [{'service': service_name, ...}, ...]
1135
1136
    @param services: see above
1137
    @returns OrderedDict(service: [ports], ...)
1138
    """
1139
    if services is None:
1140
        return {}
1141
    if isinstance(services, dict):
1142
        services = services.values()
1143
    # either extract the list of services from the dictionary, or if
1144
    # it is a simple string, use that. i.e. works with mixed lists.
1145
    _s = OrderedDict()
1146
    for s in services:
1147
        if isinstance(s, dict) and 'service' in s:
1148
            _s[s['service']] = s.get('ports', [])
1149
        if isinstance(s, str):
1150
            _s[s] = []
1151
    return _s
1152
1153
1154
def _check_running_services(services):
1155
    """Check that the services dict provided is actually running and provide
1156
    a list of (service, boolean) tuples for each service.
1157
1158
    Returns both a zipped list of (service, boolean) and a list of booleans
1159
    in the same order as the services.
1160
1161
    @param services: OrderedDict of strings: [ports], one for each service to
1162
                     check.
1163
    @returns [(service, boolean), ...], : results for checks
1164
             [boolean]                  : just the result of the service checks
1165
    """
1166
    services_running = [service_running(s) for s in services]
1167
    return list(zip(services, services_running)), services_running
1168
1169
1170
def _check_listening_on_services_ports(services, test=False):
1171
    """Check that the unit is actually listening (has the port open) on the
1172
    ports that the service specifies are open. If test is True then the
1173
    function returns the services with ports that are open rather than
1174
    closed.
1175
1176
    Returns an OrderedDict of service: ports and a list of booleans
1177
1178
    @param services: OrderedDict(service: [port, ...], ...)
1179
    @param test: default=False, if False, test for closed, otherwise open.
1180
    @returns OrderedDict(service: [port-not-open, ...]...), [boolean]
1181
    """
1182
    test = not(not(test))  # ensure test is True or False
1183
    all_ports = list(itertools.chain(*services.values()))
1184
    ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports]
1185
    map_ports = OrderedDict()
1186
    matched_ports = [p for p, opened in zip(all_ports, ports_states)
1187
                     if opened == test]  # essentially opened xor test
1188
    for service, ports in services.items():
1189
        set_ports = set(ports).intersection(matched_ports)
1190
        if set_ports:
1191
            map_ports[service] = set_ports
1192
    return map_ports, ports_states
1193
1194
1195
def _check_listening_on_ports_list(ports):
1196
    """Check that the ports list given are being listened to
1197
1198
    Returns a list of ports being listened to and a list of the
1199
    booleans.
1200
1201
    @param ports: LIST or port numbers.
1202
    @returns [(port_num, boolean), ...], [boolean]
1203
    """
1204
    ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
1205
    return zip(ports, ports_open), ports_open
1206
1207
1208
def _filter_tuples(services_states, state):
1209
    """Return a simple list from a list of tuples according to the condition
1210
1211
    @param services_states: LIST of (string, boolean): service and running
1212
           state.
1213
    @param state: Boolean to match the tuple against.
1214
    @returns [LIST of strings] that matched the tuple RHS.
1215
    """
1216
    return [s for s, b in services_states if b == state]
1217
1218
1219
def workload_state_compare(current_workload_state, workload_state):
1220
    """ Return highest priority of two states"""
1221
    hierarchy = {'unknown': -1,
1222
                 'active': 0,
1223
                 'maintenance': 1,
1224
                 'waiting': 2,
1225
                 'blocked': 3,
1226
                 }
1227
1228
    if hierarchy.get(workload_state) is None:
1229
        workload_state = 'unknown'
1230
    if hierarchy.get(current_workload_state) is None:
1231
        current_workload_state = 'unknown'
1232
1233
    # Set workload_state based on hierarchy of statuses
1234
    if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
1235
        return current_workload_state
1236
    else:
1237
        return workload_state
1238
1239
1240
def incomplete_relation_data(configs, required_interfaces):
1241
    """Check complete contexts against required_interfaces
1242
    Return dictionary of incomplete relation data.
1243
1244
    configs is an OSConfigRenderer object with configs registered
1245
1246
    required_interfaces is a dictionary of required general interfaces
1247
    with dictionary values of possible specific interfaces.
1248
    Example:
1249
    required_interfaces = {'database': ['shared-db', 'pgsql-db']}
1250
1251
    The interface is said to be satisfied if anyone of the interfaces in the
1252
    list has a complete context.
1253
1254
    Return dictionary of incomplete or missing required contexts with relation
1255
    status of interfaces and any missing data points. Example:
1256
        {'message':
1257
             {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
1258
              'zeromq-configuration': {'related': False}},
1259
         'identity':
1260
             {'identity-service': {'related': False}},
1261
         'database':
1262
             {'pgsql-db': {'related': False},
1263
              'shared-db': {'related': True}}}
1264
    """
1265
    complete_ctxts = configs.complete_contexts()
1266
    incomplete_relations = [
1267
        svc_type
1268
        for svc_type, interfaces in required_interfaces.items()
1269
        if not set(interfaces).intersection(complete_ctxts)]
1270
    return {
1271
        i: configs.get_incomplete_context_data(required_interfaces[i])
1272
        for i in incomplete_relations}
1273
1274
1275
def do_action_openstack_upgrade(package, upgrade_callback, configs):
1276
    """Perform action-managed OpenStack upgrade.
1277
1278
    Upgrades packages to the configured openstack-origin version and sets
1279
    the corresponding action status as a result.
1280
1281
    If the charm was installed from source we cannot upgrade it.
1282
    For backwards compatibility a config flag (action-managed-upgrade) must
1283
    be set for this code to run, otherwise a full service level upgrade will
1284
    fire on config-changed.
1285
1286
    @param package: package name for determining if upgrade available
1287
    @param upgrade_callback: function callback to charm's upgrade function
1288
    @param configs: templating object derived from OSConfigRenderer class
1289
1290
    @return: True if upgrade successful; False if upgrade failed or skipped
1291
    """
1292
    ret = False
1293
1294
    if git_install_requested():
1295
        action_set({'outcome': 'installed from source, skipped upgrade.'})
1296
    else:
1297
        if openstack_upgrade_available(package):
1298
            if config('action-managed-upgrade'):
1299
                juju_log('Upgrading OpenStack release')
1300
1301
                try:
1302
                    upgrade_callback(configs=configs)
1303
                    action_set({'outcome': 'success, upgrade completed.'})
1304
                    ret = True
1305
                except:
1306
                    action_set({'outcome': 'upgrade failed, see traceback.'})
1307
                    action_set({'traceback': traceback.format_exc()})
1308
                    action_fail('do_openstack_upgrade resulted in an '
1309
                                'unexpected error')
1310
            else:
1311
                action_set({'outcome': 'action-managed-upgrade config is '
1312
                                       'False, skipped upgrade.'})
1313
        else:
1314
            action_set({'outcome': 'no upgrade available.'})
1315
1316
    return ret
1317
1318
1319
def remote_restart(rel_name, remote_service=None):
1320
    trigger = {
1321
        'restart-trigger': str(uuid.uuid4()),
1322
    }
1323
    if remote_service:
1324
        trigger['remote-service'] = remote_service
1325
    for rid in relation_ids(rel_name):
1326
        # This subordinate can be related to two seperate services using
1327
        # different subordinate relations so only issue the restart if
1328
        # the principle is conencted down the relation we think it is
1329
        if related_units(relid=rid):
1330
            relation_set(relation_id=rid,
1331
                         relation_settings=trigger,
1332
                         )
1333
1334
1335
def check_actually_paused(services=None, ports=None):
1336
    """Check that services listed in the services object and and ports
1337
    are actually closed (not listened to), to verify that the unit is
1338
    properly paused.
1339
1340
    @param services: See _extract_services_list_helper
1341
    @returns status, : string for status (None if okay)
1342
             message : string for problem for status_set
1343
    """
1344
    state = None
1345
    message = None
1346
    messages = []
1347
    if services is not None:
1348
        services = _extract_services_list_helper(services)
1349
        services_running, services_states = _check_running_services(services)
1350
        if any(services_states):
1351
            # there shouldn't be any running so this is a problem
1352
            messages.append("these services running: {}"
1353
                            .format(", ".join(
1354
                                _filter_tuples(services_running, True))))
1355
            state = "blocked"
1356
        ports_open, ports_open_bools = (
1357
            _check_listening_on_services_ports(services, True))
1358
        if any(ports_open_bools):
1359
            message_parts = {service: ", ".join([str(v) for v in open_ports])
1360
                             for service, open_ports in ports_open.items()}
1361
            message = ", ".join(
1362
                ["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
1363
            messages.append(
1364
                "these service:ports are open: {}".format(message))
1365
            state = 'blocked'
1366
    if ports is not None:
1367
        ports_open, bools = _check_listening_on_ports_list(ports)
1368
        if any(bools):
1369
            messages.append(
1370
                "these ports which should be closed, but are open: {}"
1371
                .format(", ".join([str(p) for p, v in ports_open if v])))
1372
            state = 'blocked'
1373
    if messages:
1374
        message = ("Services should be paused but {}"
1375
                   .format(", ".join(messages)))
1376
    return state, message
1377
1378
1379
def set_unit_paused():
1380
    """Set the unit to a paused state in the local kv() store.
1381
    This does NOT actually pause the unit
1382
    """
1383
    with unitdata.HookData()() as t:
1384
        kv = t[0]
1385
        kv.set('unit-paused', True)
1386
1387
1388
def clear_unit_paused():
1389
    """Clear the unit from a paused state in the local kv() store
1390
    This does NOT actually restart any services - it only clears the
1391
    local state.
1392
    """
1393
    with unitdata.HookData()() as t:
1394
        kv = t[0]
1395
        kv.set('unit-paused', False)
1396
1397
1398
def is_unit_paused_set():
1399
    """Return the state of the kv().get('unit-paused').
1400
    This does NOT verify that the unit really is paused.
1401
1402
    To help with units that don't have HookData() (testing)
1403
    if it excepts, return False
1404
    """
1405
    try:
1406
        with unitdata.HookData()() as t:
1407
            kv = t[0]
1408
            # transform something truth-y into a Boolean.
1409
            return not(not(kv.get('unit-paused')))
1410
    except:
1411
        return False
1412
1413
1414
def pause_unit(assess_status_func, services=None, ports=None,
1415
               charm_func=None):
1416
    """Pause a unit by stopping the services and setting 'unit-paused'
1417
    in the local kv() store.
1418
1419
    Also checks that the services have stopped and ports are no longer
1420
    being listened to.
1421
1422
    An optional charm_func() can be called that can either raise an
1423
    Exception or return non None, None to indicate that the unit
1424
    didn't pause cleanly.
1425
1426
    The signature for charm_func is:
1427
    charm_func() -> message: string
1428
1429
    charm_func() is executed after any services are stopped, if supplied.
1430
1431
    The services object can either be:
1432
      - None : no services were passed (an empty dict is returned)
1433
      - a list of strings
1434
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1435
      - An array of [{'service': service_name, ...}, ...]
1436
1437
    @param assess_status_func: (f() -> message: string | None) or None
1438
    @param services: OPTIONAL see above
1439
    @param ports: OPTIONAL list of port
1440
    @param charm_func: function to run for custom charm pausing.
1441
    @returns None
1442
    @raises Exception(message) on an error for action_fail().
1443
    """
1444
    services = _extract_services_list_helper(services)
1445
    messages = []
1446
    if services:
1447
        for service in services.keys():
1448
            stopped = service_pause(service)
1449
            if not stopped:
1450
                messages.append("{} didn't stop cleanly.".format(service))
1451
    if charm_func:
1452
        try:
1453
            message = charm_func()
1454
            if message:
1455
                messages.append(message)
1456
        except Exception as e:
1457
            message.append(str(e))
1458
    set_unit_paused()
1459
    if assess_status_func:
1460
        message = assess_status_func()
1461
        if message:
1462
            messages.append(message)
1463
    if messages:
1464
        raise Exception("Couldn't pause: {}".format("; ".join(messages)))
1465
1466
1467
def resume_unit(assess_status_func, services=None, ports=None,
1468
                charm_func=None):
1469
    """Resume a unit by starting the services and clearning 'unit-paused'
1470
    in the local kv() store.
1471
1472
    Also checks that the services have started and ports are being listened to.
1473
1474
    An optional charm_func() can be called that can either raise an
1475
    Exception or return non None to indicate that the unit
1476
    didn't resume cleanly.
1477
1478
    The signature for charm_func is:
1479
    charm_func() -> message: string
1480
1481
    charm_func() is executed after any services are started, if supplied.
1482
1483
    The services object can either be:
1484
      - None : no services were passed (an empty dict is returned)
1485
      - a list of strings
1486
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
1487
      - An array of [{'service': service_name, ...}, ...]
1488
1489
    @param assess_status_func: (f() -> message: string | None) or None
1490
    @param services: OPTIONAL see above
1491
    @param ports: OPTIONAL list of port
1492
    @param charm_func: function to run for custom charm resuming.
1493
    @returns None
1494
    @raises Exception(message) on an error for action_fail().
1495
    """
1496
    services = _extract_services_list_helper(services)
1497
    messages = []
1498
    if services:
1499
        for service in services.keys():
1500
            started = service_resume(service)
1501
            if not started:
1502
                messages.append("{} didn't start cleanly.".format(service))
1503
    if charm_func:
1504
        try:
1505
            message = charm_func()
1506
            if message:
1507
                messages.append(message)
1508
        except Exception as e:
1509
            message.append(str(e))
1510
    clear_unit_paused()
1511
    if assess_status_func:
1512
        message = assess_status_func()
1513
        if message:
1514
            messages.append(message)
1515
    if messages:
1516
        raise Exception("Couldn't resume: {}".format("; ".join(messages)))
1517
1518
1519
def make_assess_status_func(*args, **kwargs):
1520
    """Creates an assess_status_func() suitable for handing to pause_unit()
1521
    and resume_unit().
1522
1523
    This uses the _determine_os_workload_status(...) function to determine
1524
    what the workload_status should be for the unit.  If the unit is
1525
    not in maintenance or active states, then the message is returned to
1526
    the caller.  This is so an action that doesn't result in either a
1527
    complete pause or complete resume can signal failure with an action_fail()
1528
    """
1529
    def _assess_status_func():
1530
        state, message = _determine_os_workload_status(*args, **kwargs)
1531
        status_set(state, message)
1532
        if state not in ['maintenance', 'active']:
1533
            return message
1534
        return None
1535
1536
    return _assess_status_func
1537
1538
1539
def pausable_restart_on_change(restart_map, stopstart=False,
1540
                               restart_functions=None):
1541
    """A restart_on_change decorator that checks to see if the unit is
1542
    paused. If it is paused then the decorated function doesn't fire.
1543
1544
    This is provided as a helper, as the @restart_on_change(...) decorator
1545
    is in core.host, yet the openstack specific helpers are in this file
1546
    (contrib.openstack.utils).  Thus, this needs to be an optional feature
1547
    for openstack charms (or charms that wish to use the openstack
1548
    pause/resume type features).
1549
1550
    It is used as follows:
1551
1552
        from contrib.openstack.utils import (
1553
            pausable_restart_on_change as restart_on_change)
1554
1555
        @restart_on_change(restart_map, stopstart=<boolean>)
1556
        def some_hook(...):
1557
            pass
1558
1559
    see core.utils.restart_on_change() for more details.
1560
1561
    @param f: the function to decorate
1562
    @param restart_map: the restart map {conf_file: [services]}
1563
    @param stopstart: DEFAULT false; whether to stop, start or just restart
1564
    @returns decorator to use a restart_on_change with pausability
1565
    """
1566
    def wrap(f):
1567
        @functools.wraps(f)
1568
        def wrapped_f(*args, **kwargs):
1569
            if is_unit_paused_set():
1570
                return f(*args, **kwargs)
1571
            # otherwise, normal restart_on_change functionality
1572
            return restart_on_change_helper(
1573
                (lambda: f(*args, **kwargs)), restart_map, stopstart,
1574
                restart_functions)
1575
        return wrapped_f
1576
    return wrap