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

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/contrib/openstack/utils.py

  • Committer: Paul Larson
  • Date: 2016-05-16 20:27:32 UTC
  • Revision ID: paul.larson@canonical.com-20160516202732-9r4nkyl2f91w9xo3
Add support for xenial

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/python
2
 
 
3
1
# Copyright 2014-2015 Canonical Limited.
4
2
#
5
3
# This file is part of charm-helpers.
23
21
import subprocess
24
22
import json
25
23
import os
26
 
import socket
27
24
import sys
 
25
import re
 
26
import itertools
 
27
import functools
28
28
 
29
29
import six
 
30
import tempfile
 
31
import traceback
 
32
import uuid
30
33
import yaml
31
34
 
 
35
from charmhelpers.contrib.network import ip
 
36
 
 
37
from charmhelpers.core import (
 
38
    unitdata,
 
39
)
 
40
 
32
41
from charmhelpers.core.hookenv import (
 
42
    action_fail,
 
43
    action_set,
33
44
    config,
34
45
    log as juju_log,
35
46
    charm_dir,
 
47
    DEBUG,
36
48
    INFO,
 
49
    related_units,
37
50
    relation_ids,
38
 
    relation_set
 
51
    relation_set,
 
52
    status_set,
 
53
    hook_name
39
54
)
40
55
 
41
56
from charmhelpers.contrib.storage.linux.lvm import (
45
60
)
46
61
 
47
62
from charmhelpers.contrib.network.ip import (
48
 
    get_ipv6_addr
49
 
)
50
 
 
51
 
from charmhelpers.core.host import lsb_release, mounts, umount
 
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
)
52
82
from charmhelpers.fetch import apt_install, apt_cache, install_remote
53
 
from charmhelpers.contrib.python.packages import pip_install
54
83
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
55
84
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
56
85
 
60
89
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
61
90
                   'restricted main multiverse universe')
62
91
 
63
 
 
64
92
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
65
93
    ('oneiric', 'diablo'),
66
94
    ('precise', 'essex'),
70
98
    ('trusty', 'icehouse'),
71
99
    ('utopic', 'juno'),
72
100
    ('vivid', 'kilo'),
 
101
    ('wily', 'liberty'),
 
102
    ('xenial', 'mitaka'),
73
103
])
74
104
 
75
105
 
82
112
    ('2014.1', 'icehouse'),
83
113
    ('2014.2', 'juno'),
84
114
    ('2015.1', 'kilo'),
 
115
    ('2015.2', 'liberty'),
 
116
    ('2016.1', 'mitaka'),
85
117
])
86
118
 
87
 
# The ugly duckling
 
119
# The ugly duckling - must list releases oldest to newest
88
120
SWIFT_CODENAMES = OrderedDict([
89
 
    ('1.4.3', 'diablo'),
90
 
    ('1.4.8', 'essex'),
91
 
    ('1.7.4', 'folsom'),
92
 
    ('1.8.0', 'grizzly'),
93
 
    ('1.7.7', 'grizzly'),
94
 
    ('1.7.6', 'grizzly'),
95
 
    ('1.10.0', 'havana'),
96
 
    ('1.9.1', 'havana'),
97
 
    ('1.9.0', 'havana'),
98
 
    ('1.13.1', 'icehouse'),
99
 
    ('1.13.0', 'icehouse'),
100
 
    ('1.12.0', 'icehouse'),
101
 
    ('1.11.0', 'icehouse'),
102
 
    ('2.0.0', 'juno'),
103
 
    ('2.1.0', 'juno'),
104
 
    ('2.2.0', 'juno'),
105
 
    ('2.2.1', 'kilo'),
 
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']),
106
141
])
107
142
 
 
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
 
108
185
DEFAULT_LOOPBACK_SIZE = '5G'
109
186
 
110
187
 
154
231
        error_out(e)
155
232
 
156
233
 
157
 
def get_os_version_codename(codename):
 
234
def get_os_version_codename(codename, version_map=OPENSTACK_CODENAMES):
158
235
    '''Determine OpenStack version number from codename.'''
159
 
    for k, v in six.iteritems(OPENSTACK_CODENAMES):
 
236
    for k, v in six.iteritems(version_map):
160
237
        if v == codename:
161
238
            return k
162
239
    e = 'Could not derive OpenStack version for '\
164
241
    error_out(e)
165
242
 
166
243
 
 
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
 
167
271
def get_os_codename_package(package, fatal=True):
168
272
    '''Derive OpenStack release codename from an installed package.'''
169
273
    import apt_pkg as apt
188
292
        error_out(e)
189
293
 
190
294
    vers = apt.upstream_version(pkg.current_ver.ver_str)
191
 
 
192
 
    try:
193
 
        if 'swift' in pkg.name:
194
 
            swift_vers = vers[:5]
195
 
            if swift_vers not in SWIFT_CODENAMES:
196
 
                # Deal with 1.10.0 upward
197
 
                swift_vers = vers[:6]
198
 
            return SWIFT_CODENAMES[swift_vers]
199
 
        else:
200
 
            vers = vers[:6]
201
 
            return OPENSTACK_CODENAMES[vers]
202
 
    except KeyError:
203
 
        e = 'Could not determine OpenStack codename for version %s' % vers
204
 
        error_out(e)
 
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)
205
322
 
206
323
 
207
324
def get_os_version_package(pkg, fatal=True):
213
330
 
214
331
    if 'swift' in pkg:
215
332
        vers_map = SWIFT_CODENAMES
 
333
        for cname, version in six.iteritems(vers_map):
 
334
            if cname == codename:
 
335
                return version[-1]
216
336
    else:
217
337
        vers_map = OPENSTACK_CODENAMES
218
 
 
219
 
    for version, cname in six.iteritems(vers_map):
220
 
        if cname == codename:
221
 
            return version
 
338
        for version, cname in six.iteritems(vers_map):
 
339
            if cname == codename:
 
340
                return version
222
341
    # e = "Could not determine OpenStack version for package: %s" % pkg
223
342
    # error_out(e)
224
343
 
243
362
 
244
363
 
245
364
def import_key(keyid):
246
 
    cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 " \
247
 
          "--recv-keys %s" % keyid
248
 
    try:
249
 
        subprocess.check_call(cmd.split(' '))
250
 
    except subprocess.CalledProcessError:
251
 
        error_out("Error importing repo key %s" % keyid)
 
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
252
401
 
253
402
 
254
403
def configure_installation_source(rel):
260
409
        with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
261
410
            f.write(DISTRO_PROPOSED % ubuntu_rel)
262
411
    elif rel[:4] == "ppa:":
263
 
        src = rel
 
412
        src, key = get_source_and_pgp_key(rel)
 
413
        if key:
 
414
            import_key(key)
 
415
 
264
416
        subprocess.check_call(["add-apt-repository", "-y", src])
265
417
    elif rel[:3] == "deb":
266
 
        l = len(rel.split('|'))
267
 
        if l == 2:
268
 
            src, key = rel.split('|')
269
 
            juju_log("Importing PPA key from keyserver for %s" % src)
 
418
        src, key = get_source_and_pgp_key(rel)
 
419
        if key:
270
420
            import_key(key)
271
 
        elif l == 1:
272
 
            src = rel
 
421
 
273
422
        with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
274
423
            f.write(src)
275
424
    elif rel[:6] == 'cloud:':
311
460
            'kilo': 'trusty-updates/kilo',
312
461
            'kilo/updates': 'trusty-updates/kilo',
313
462
            'kilo/proposed': 'trusty-proposed/kilo',
 
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',
314
469
        }
315
470
 
316
471
        try:
328
483
        error_out("Invalid openstack-release specified: %s" % rel)
329
484
 
330
485
 
 
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
 
331
501
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
332
502
    """
333
503
    Write an rc file in the charm-delivered directory containing
361
531
    import apt_pkg as apt
362
532
    src = config('openstack-origin')
363
533
    cur_vers = get_os_version_package(package)
364
 
    available_vers = get_os_version_install_source(src)
 
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)
365
539
    apt.init()
366
 
    return apt.version_compare(available_vers, cur_vers) == 1
 
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
367
546
 
368
547
 
369
548
def ensure_block_device(block_device):
420
599
    else:
421
600
        zap_disk(block_device)
422
601
 
423
 
 
424
 
def is_ip(address):
425
 
    """
426
 
    Returns True if address is a valid IP address.
427
 
    """
428
 
    try:
429
 
        # Test to see if already an IPv4 address
430
 
        socket.inet_aton(address)
431
 
        return True
432
 
    except socket.error:
433
 
        return False
434
 
 
435
 
 
436
 
def ns_query(address):
437
 
    try:
438
 
        import dns.resolver
439
 
    except ImportError:
440
 
        apt_install('python-dnspython')
441
 
        import dns.resolver
442
 
 
443
 
    if isinstance(address, dns.name.Name):
444
 
        rtype = 'PTR'
445
 
    elif isinstance(address, six.string_types):
446
 
        rtype = 'A'
447
 
    else:
448
 
        return None
449
 
 
450
 
    answers = dns.resolver.query(address, rtype)
451
 
    if answers:
452
 
        return str(answers[0])
453
 
    return None
454
 
 
455
 
 
456
 
def get_host_ip(hostname):
457
 
    """
458
 
    Resolves the IP for a given hostname, or returns
459
 
    the input if it is already an IP.
460
 
    """
461
 
    if is_ip(hostname):
462
 
        return hostname
463
 
 
464
 
    return ns_query(hostname)
465
 
 
466
 
 
467
 
def get_hostname(address, fqdn=True):
468
 
    """
469
 
    Resolves hostname for given IP, or returns the input
470
 
    if it is already a hostname.
471
 
    """
472
 
    if is_ip(address):
473
 
        try:
474
 
            import dns.reversename
475
 
        except ImportError:
476
 
            apt_install('python-dnspython')
477
 
            import dns.reversename
478
 
 
479
 
        rev = dns.reversename.from_address(address)
480
 
        result = ns_query(rev)
481
 
        if not result:
482
 
            return None
483
 
    else:
484
 
        result = address
485
 
 
486
 
    if fqdn:
487
 
        # strip trailing .
488
 
        if result.endswith('.'):
489
 
            return result[:-1]
490
 
        else:
491
 
            return result
492
 
    else:
493
 
        return result.split('.')[0]
 
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
494
606
 
495
607
 
496
608
def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):
505
617
                                      relation_prefix=None):
506
618
    hosts = get_ipv6_addr(dynamic_only=False)
507
619
 
 
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
 
508
626
    kwargs = {'database': database,
509
627
              'username': database_user,
510
628
              'hostname': json.dumps(hosts)}
534
652
 
535
653
 
536
654
def git_install_requested():
537
 
    """Returns true if openstack-origin-git is specified."""
538
 
    return config('openstack-origin-git') != "None"
 
655
    """
 
656
    Returns true if openstack-origin-git is specified.
 
657
    """
 
658
    return config('openstack-origin-git') is not None
539
659
 
540
660
 
541
661
requirements_dir = None
542
662
 
543
663
 
544
 
def git_clone_and_install(file_name, core_project):
545
 
    """Clone/install all OpenStack repos specified in yaml config file."""
546
 
    global requirements_dir
547
 
 
548
 
    if file_name == "None":
549
 
        return
550
 
 
551
 
    yaml_file = os.path.join(charm_dir(), file_name)
552
 
 
553
 
    # clone/install the requirements project first
554
 
    installed = _git_clone_and_install_subset(yaml_file,
555
 
                                              whitelist=['requirements'])
556
 
    if 'requirements' not in installed:
557
 
        error_out('requirements git repository must be specified')
558
 
 
559
 
    # clone/install all other projects except requirements and the core project
560
 
    blacklist = ['requirements', core_project]
561
 
    _git_clone_and_install_subset(yaml_file, blacklist=blacklist,
562
 
                                  update_requirements=True)
563
 
 
564
 
    # clone/install the core project
565
 
    whitelist = [core_project]
566
 
    installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist,
567
 
                                              update_requirements=True)
568
 
    if core_project not in installed:
569
 
        error_out('{} git repository must be specified'.format(core_project))
570
 
 
571
 
 
572
 
def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[],
573
 
                                  update_requirements=False):
574
 
    """Clone/install subset of OpenStack repos specified in yaml config file."""
575
 
    global requirements_dir
576
 
    installed = []
577
 
 
578
 
    with open(yaml_file, 'r') as fd:
579
 
        projects = yaml.load(fd)
580
 
        for proj, val in projects.items():
581
 
            # The project subset is chosen based on the following 3 rules:
582
 
            # 1) If project is in blacklist, we don't clone/install it, period.
583
 
            # 2) If whitelist is empty, we clone/install everything else.
584
 
            # 3) If whitelist is not empty, we clone/install everything in the
585
 
            #    whitelist.
586
 
            if proj in blacklist:
587
 
                continue
588
 
            if whitelist and proj not in whitelist:
589
 
                continue
590
 
            repo = val['repository']
591
 
            branch = val['branch']
592
 
            repo_dir = _git_clone_and_install_single(repo, branch,
593
 
                                                     update_requirements)
594
 
            if proj == 'requirements':
595
 
                requirements_dir = repo_dir
596
 
            installed.append(proj)
597
 
    return installed
598
 
 
599
 
 
600
 
def _git_clone_and_install_single(repo, branch, update_requirements=False):
601
 
    """Clone and install a single git repository."""
602
 
    dest_parent_dir = "/mnt/openstack-git/"
603
 
    dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo))
604
 
 
605
 
    if not os.path.exists(dest_parent_dir):
606
 
        juju_log('Host dir not mounted at {}. '
607
 
                 'Creating directory there instead.'.format(dest_parent_dir))
608
 
        os.mkdir(dest_parent_dir)
609
 
 
610
 
    if not os.path.exists(dest_dir):
611
 
        juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
612
 
        repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch)
613
 
    else:
614
 
        repo_dir = dest_dir
 
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')
615
781
 
616
782
    if update_requirements:
617
783
        if not requirements_dir:
618
784
            error_out('requirements repo must be cloned before '
619
785
                      'updating from global requirements.')
620
 
        _git_update_requirements(repo_dir, requirements_dir)
 
786
        _git_update_requirements(venv, repo_dir, requirements_dir)
621
787
 
622
788
    juju_log('Installing git repo from dir: {}'.format(repo_dir))
623
 
    pip_install(repo_dir)
 
789
    if http_proxy:
 
790
        pip_install(repo_dir, proxy=http_proxy, venv=venv)
 
791
    else:
 
792
        pip_install(repo_dir, venv=venv)
624
793
 
625
794
    return repo_dir
626
795
 
627
796
 
628
 
def _git_update_requirements(package_dir, reqs_dir):
629
 
    """Update from global requirements.
 
797
def _git_update_requirements(venv, package_dir, reqs_dir):
 
798
    """
 
799
    Update from global requirements.
630
800
 
631
 
       Update an OpenStack git directory's requirements.txt and
632
 
       test-requirements.txt from global-requirements.txt."""
 
801
    Update an OpenStack git directory's requirements.txt and
 
802
    test-requirements.txt from global-requirements.txt.
 
803
    """
633
804
    orig_dir = os.getcwd()
634
805
    os.chdir(reqs_dir)
635
 
    cmd = "python update.py {}".format(package_dir)
 
806
    python = os.path.join(venv, 'bin/python')
 
807
    cmd = [python, 'update.py', package_dir]
636
808
    try:
637
 
        subprocess.check_call(cmd.split(' '))
 
809
        subprocess.check_call(cmd)
638
810
    except subprocess.CalledProcessError:
639
811
        package = os.path.basename(package_dir)
640
 
        error_out("Error updating {} from global-requirements.txt".format(package))
 
812
        error_out("Error updating {} from "
 
813
                  "global-requirements.txt".format(package))
641
814
    os.chdir(orig_dir)
 
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