~junaidali/charms/trusty/plumgrid-edge/docker-oil

« back to all changes in this revision

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

  • Committer: bbaqar at plumgrid
  • Date: 2016-04-25 09:18:40 UTC
  • mfrom: (26.1.3 plumgrid-edge)
  • Revision ID: bbaqar@plumgrid.com-20160425091840-sirw6bbzalts677s
Merge: Liberty/Mitaka support

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.
24
22
import json
25
23
import os
26
24
import sys
 
25
import re
 
26
import itertools
 
27
import functools
27
28
 
28
29
import six
 
30
import tempfile
 
31
import traceback
 
32
import uuid
29
33
import yaml
30
34
 
31
35
from charmhelpers.contrib.network import ip
35
39
)
36
40
 
37
41
from charmhelpers.core.hookenv import (
 
42
    action_fail,
 
43
    action_set,
38
44
    config,
39
45
    log as juju_log,
40
46
    charm_dir,
 
47
    DEBUG,
41
48
    INFO,
 
49
    related_units,
42
50
    relation_ids,
43
 
    relation_set
 
51
    relation_set,
 
52
    status_set,
 
53
    hook_name
44
54
)
45
55
 
46
56
from charmhelpers.contrib.storage.linux.lvm import (
50
60
)
51
61
 
52
62
from charmhelpers.contrib.network.ip import (
53
 
    get_ipv6_addr
 
63
    get_ipv6_addr,
 
64
    is_ipv6,
 
65
    port_has_listener,
54
66
)
55
67
 
56
68
from charmhelpers.contrib.python.packages import (
58
70
    pip_install,
59
71
)
60
72
 
61
 
from charmhelpers.core.host import lsb_release, mounts, umount
 
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
)
62
82
from charmhelpers.fetch import apt_install, apt_cache, install_remote
63
83
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
64
84
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
69
89
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
70
90
                   'restricted main multiverse universe')
71
91
 
72
 
 
73
92
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
74
93
    ('oneiric', 'diablo'),
75
94
    ('precise', 'essex'),
80
99
    ('utopic', 'juno'),
81
100
    ('vivid', 'kilo'),
82
101
    ('wily', 'liberty'),
 
102
    ('xenial', 'mitaka'),
83
103
])
84
104
 
85
105
 
93
113
    ('2014.2', 'juno'),
94
114
    ('2015.1', 'kilo'),
95
115
    ('2015.2', 'liberty'),
 
116
    ('2016.1', 'mitaka'),
96
117
])
97
118
 
98
 
# The ugly duckling
 
119
# The ugly duckling - must list releases oldest to newest
99
120
SWIFT_CODENAMES = OrderedDict([
100
 
    ('1.4.3', 'diablo'),
101
 
    ('1.4.8', 'essex'),
102
 
    ('1.7.4', 'folsom'),
103
 
    ('1.8.0', 'grizzly'),
104
 
    ('1.7.7', 'grizzly'),
105
 
    ('1.7.6', 'grizzly'),
106
 
    ('1.10.0', 'havana'),
107
 
    ('1.9.1', 'havana'),
108
 
    ('1.9.0', 'havana'),
109
 
    ('1.13.1', 'icehouse'),
110
 
    ('1.13.0', 'icehouse'),
111
 
    ('1.12.0', 'icehouse'),
112
 
    ('1.11.0', 'icehouse'),
113
 
    ('2.0.0', 'juno'),
114
 
    ('2.1.0', 'juno'),
115
 
    ('2.2.0', 'juno'),
116
 
    ('2.2.1', 'kilo'),
117
 
    ('2.2.2', 'kilo'),
118
 
    ('2.3.0', 'liberty'),
 
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']),
119
141
])
120
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
    ]),
 
153
    'cinder-common': OrderedDict([
 
154
        ('7.0', 'liberty'),
 
155
        ('8.0', 'mitaka'),
 
156
    ]),
 
157
    'keystone': OrderedDict([
 
158
        ('8.0', 'liberty'),
 
159
        ('8.1', 'liberty'),
 
160
        ('9.0', 'mitaka'),
 
161
    ]),
 
162
    'horizon-common': OrderedDict([
 
163
        ('8.0', 'liberty'),
 
164
        ('9.0', 'mitaka'),
 
165
    ]),
 
166
    'ceilometer-common': OrderedDict([
 
167
        ('5.0', 'liberty'),
 
168
        ('6.0', 'mitaka'),
 
169
    ]),
 
170
    'heat-common': OrderedDict([
 
171
        ('5.0', 'liberty'),
 
172
        ('6.0', 'mitaka'),
 
173
    ]),
 
174
    'glance-common': OrderedDict([
 
175
        ('11.0', 'liberty'),
 
176
        ('12.0', 'mitaka'),
 
177
    ]),
 
178
    'openstack-dashboard': OrderedDict([
 
179
        ('8.0', 'liberty'),
 
180
        ('9.0', 'mitaka'),
 
181
    ]),
 
182
}
 
183
 
121
184
DEFAULT_LOOPBACK_SIZE = '5G'
122
185
 
123
186
 
167
230
        error_out(e)
168
231
 
169
232
 
170
 
def get_os_version_codename(codename):
 
233
def get_os_version_codename(codename, version_map=OPENSTACK_CODENAMES):
171
234
    '''Determine OpenStack version number from codename.'''
172
 
    for k, v in six.iteritems(OPENSTACK_CODENAMES):
 
235
    for k, v in six.iteritems(version_map):
173
236
        if v == codename:
174
237
            return k
175
238
    e = 'Could not derive OpenStack version for '\
177
240
    error_out(e)
178
241
 
179
242
 
 
243
def get_os_version_codename_swift(codename):
 
244
    '''Determine OpenStack version number of swift from codename.'''
 
245
    for k, v in six.iteritems(SWIFT_CODENAMES):
 
246
        if k == codename:
 
247
            return v[-1]
 
248
    e = 'Could not derive swift version for '\
 
249
        'codename: %s' % codename
 
250
    error_out(e)
 
251
 
 
252
 
 
253
def get_swift_codename(version):
 
254
    '''Determine OpenStack codename that corresponds to swift version.'''
 
255
    codenames = [k for k, v in six.iteritems(SWIFT_CODENAMES) if version in v]
 
256
    if len(codenames) > 1:
 
257
        # If more than one release codename contains this version we determine
 
258
        # the actual codename based on the highest available install source.
 
259
        for codename in reversed(codenames):
 
260
            releases = UBUNTU_OPENSTACK_RELEASE
 
261
            release = [k for k, v in six.iteritems(releases) if codename in v]
 
262
            ret = subprocess.check_output(['apt-cache', 'policy', 'swift'])
 
263
            if codename in ret or release[0] in ret:
 
264
                return codename
 
265
    elif len(codenames) == 1:
 
266
        return codenames[0]
 
267
    return None
 
268
 
 
269
 
180
270
def get_os_codename_package(package, fatal=True):
181
271
    '''Derive OpenStack release codename from an installed package.'''
182
272
    import apt_pkg as apt
201
291
        error_out(e)
202
292
 
203
293
    vers = apt.upstream_version(pkg.current_ver.ver_str)
204
 
 
205
 
    try:
206
 
        if 'swift' in pkg.name:
207
 
            swift_vers = vers[:5]
208
 
            if swift_vers not in SWIFT_CODENAMES:
209
 
                # Deal with 1.10.0 upward
210
 
                swift_vers = vers[:6]
211
 
            return SWIFT_CODENAMES[swift_vers]
212
 
        else:
213
 
            vers = vers[:6]
214
 
            return OPENSTACK_CODENAMES[vers]
215
 
    except KeyError:
216
 
        e = 'Could not determine OpenStack codename for version %s' % vers
217
 
        error_out(e)
 
294
    if 'swift' in pkg.name:
 
295
        # Fully x.y.z match for swift versions
 
296
        match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)
 
297
    else:
 
298
        # x.y match only for 20XX.X
 
299
        # and ignore patch level for other packages
 
300
        match = re.match('^(\d+)\.(\d+)', vers)
 
301
 
 
302
    if match:
 
303
        vers = match.group(0)
 
304
 
 
305
    # >= Liberty independent project versions
 
306
    if (package in PACKAGE_CODENAMES and
 
307
            vers in PACKAGE_CODENAMES[package]):
 
308
        return PACKAGE_CODENAMES[package][vers]
 
309
    else:
 
310
        # < Liberty co-ordinated project versions
 
311
        try:
 
312
            if 'swift' in pkg.name:
 
313
                return get_swift_codename(vers)
 
314
            else:
 
315
                return OPENSTACK_CODENAMES[vers]
 
316
        except KeyError:
 
317
            if not fatal:
 
318
                return None
 
319
            e = 'Could not determine OpenStack codename for version %s' % vers
 
320
            error_out(e)
218
321
 
219
322
 
220
323
def get_os_version_package(pkg, fatal=True):
226
329
 
227
330
    if 'swift' in pkg:
228
331
        vers_map = SWIFT_CODENAMES
 
332
        for cname, version in six.iteritems(vers_map):
 
333
            if cname == codename:
 
334
                return version[-1]
229
335
    else:
230
336
        vers_map = OPENSTACK_CODENAMES
231
 
 
232
 
    for version, cname in six.iteritems(vers_map):
233
 
        if cname == codename:
234
 
            return version
 
337
        for version, cname in six.iteritems(vers_map):
 
338
            if cname == codename:
 
339
                return version
235
340
    # e = "Could not determine OpenStack version for package: %s" % pkg
236
341
    # error_out(e)
237
342
 
256
361
 
257
362
 
258
363
def import_key(keyid):
259
 
    cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 " \
260
 
          "--recv-keys %s" % keyid
261
 
    try:
262
 
        subprocess.check_call(cmd.split(' '))
263
 
    except subprocess.CalledProcessError:
264
 
        error_out("Error importing repo key %s" % keyid)
 
364
    key = keyid.strip()
 
365
    if (key.startswith('-----BEGIN PGP PUBLIC KEY BLOCK-----') and
 
366
            key.endswith('-----END PGP PUBLIC KEY BLOCK-----')):
 
367
        juju_log("PGP key found (looks like ASCII Armor format)", level=DEBUG)
 
368
        juju_log("Importing ASCII Armor PGP key", level=DEBUG)
 
369
        with tempfile.NamedTemporaryFile() as keyfile:
 
370
            with open(keyfile.name, 'w') as fd:
 
371
                fd.write(key)
 
372
                fd.write("\n")
 
373
 
 
374
            cmd = ['apt-key', 'add', keyfile.name]
 
375
            try:
 
376
                subprocess.check_call(cmd)
 
377
            except subprocess.CalledProcessError:
 
378
                error_out("Error importing PGP key '%s'" % key)
 
379
    else:
 
380
        juju_log("PGP key found (looks like Radix64 format)", level=DEBUG)
 
381
        juju_log("Importing PGP key from keyserver", level=DEBUG)
 
382
        cmd = ['apt-key', 'adv', '--keyserver',
 
383
               'hkp://keyserver.ubuntu.com:80', '--recv-keys', key]
 
384
        try:
 
385
            subprocess.check_call(cmd)
 
386
        except subprocess.CalledProcessError:
 
387
            error_out("Error importing PGP key '%s'" % key)
 
388
 
 
389
 
 
390
def get_source_and_pgp_key(input):
 
391
    """Look for a pgp key ID or ascii-armor key in the given input."""
 
392
    index = input.strip()
 
393
    index = input.rfind('|')
 
394
    if index < 0:
 
395
        return input, None
 
396
 
 
397
    key = input[index + 1:].strip('|')
 
398
    source = input[:index]
 
399
    return source, key
265
400
 
266
401
 
267
402
def configure_installation_source(rel):
273
408
        with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
274
409
            f.write(DISTRO_PROPOSED % ubuntu_rel)
275
410
    elif rel[:4] == "ppa:":
276
 
        src = rel
 
411
        src, key = get_source_and_pgp_key(rel)
 
412
        if key:
 
413
            import_key(key)
 
414
 
277
415
        subprocess.check_call(["add-apt-repository", "-y", src])
278
416
    elif rel[:3] == "deb":
279
 
        l = len(rel.split('|'))
280
 
        if l == 2:
281
 
            src, key = rel.split('|')
282
 
            juju_log("Importing PPA key from keyserver for %s" % src)
 
417
        src, key = get_source_and_pgp_key(rel)
 
418
        if key:
283
419
            import_key(key)
284
 
        elif l == 1:
285
 
            src = rel
 
420
 
286
421
        with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
287
422
            f.write(src)
288
423
    elif rel[:6] == 'cloud:':
327
462
            'liberty': 'trusty-updates/liberty',
328
463
            'liberty/updates': 'trusty-updates/liberty',
329
464
            'liberty/proposed': 'trusty-proposed/liberty',
 
465
            'mitaka': 'trusty-updates/mitaka',
 
466
            'mitaka/updates': 'trusty-updates/mitaka',
 
467
            'mitaka/proposed': 'trusty-proposed/mitaka',
330
468
        }
331
469
 
332
470
        try:
392
530
    import apt_pkg as apt
393
531
    src = config('openstack-origin')
394
532
    cur_vers = get_os_version_package(package)
395
 
    available_vers = get_os_version_install_source(src)
 
533
    if "swift" in package:
 
534
        codename = get_os_codename_install_source(src)
 
535
        avail_vers = get_os_version_codename_swift(codename)
 
536
    else:
 
537
        avail_vers = get_os_version_install_source(src)
396
538
    apt.init()
397
 
    return apt.version_compare(available_vers, cur_vers) == 1
 
539
    if "swift" in package:
 
540
        major_cur_vers = cur_vers.split('.', 1)[0]
 
541
        major_avail_vers = avail_vers.split('.', 1)[0]
 
542
        major_diff = apt.version_compare(major_avail_vers, major_cur_vers)
 
543
        return avail_vers > cur_vers and (major_diff == 1 or major_diff == 0)
 
544
    return apt.version_compare(avail_vers, cur_vers) == 1
398
545
 
399
546
 
400
547
def ensure_block_device(block_device):
469
616
                                      relation_prefix=None):
470
617
    hosts = get_ipv6_addr(dynamic_only=False)
471
618
 
 
619
    if config('vip'):
 
620
        vips = config('vip').split()
 
621
        for vip in vips:
 
622
            if vip and is_ipv6(vip):
 
623
                hosts.append(vip)
 
624
 
472
625
    kwargs = {'database': database,
473
626
              'username': database_user,
474
627
              'hostname': json.dumps(hosts)}
517
670
    return yaml.load(projects_yaml)
518
671
 
519
672
 
520
 
def git_clone_and_install(projects_yaml, core_project, depth=1):
 
673
def git_clone_and_install(projects_yaml, core_project):
521
674
    """
522
675
    Clone/install all specified OpenStack repositories.
523
676
 
567
720
    for p in projects['repositories']:
568
721
        repo = p['repository']
569
722
        branch = p['branch']
 
723
        depth = '1'
 
724
        if 'depth' in p.keys():
 
725
            depth = p['depth']
570
726
        if p['name'] == 'requirements':
571
727
            repo_dir = _git_clone_and_install_single(repo, branch, depth,
572
728
                                                     parent_dir, http_proxy,
611
767
    """
612
768
    Clone and install a single git repository.
613
769
    """
614
 
    dest_dir = os.path.join(parent_dir, os.path.basename(repo))
615
 
 
616
770
    if not os.path.exists(parent_dir):
617
771
        juju_log('Directory already exists at {}. '
618
772
                 'No need to create directory.'.format(parent_dir))
619
773
        os.mkdir(parent_dir)
620
774
 
621
 
    if not os.path.exists(dest_dir):
622
 
        juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
623
 
        repo_dir = install_remote(repo, dest=parent_dir, branch=branch,
624
 
                                  depth=depth)
625
 
    else:
626
 
        repo_dir = dest_dir
 
775
    juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
 
776
    repo_dir = install_remote(
 
777
        repo, dest=parent_dir, branch=branch, depth=depth)
627
778
 
628
779
    venv = os.path.join(parent_dir, 'venv')
629
780
 
704
855
        return projects[key]
705
856
 
706
857
    return None
 
858
 
 
859
 
 
860
def os_workload_status(configs, required_interfaces, charm_func=None):
 
861
    """
 
862
    Decorator to set workload status based on complete contexts
 
863
    """
 
864
    def wrap(f):
 
865
        @wraps(f)
 
866
        def wrapped_f(*args, **kwargs):
 
867
            # Run the original function first
 
868
            f(*args, **kwargs)
 
869
            # Set workload status now that contexts have been
 
870
            # acted on
 
871
            set_os_workload_status(configs, required_interfaces, charm_func)
 
872
        return wrapped_f
 
873
    return wrap
 
874
 
 
875
 
 
876
def set_os_workload_status(configs, required_interfaces, charm_func=None,
 
877
                           services=None, ports=None):
 
878
    """Set the state of the workload status for the charm.
 
879
 
 
880
    This calls _determine_os_workload_status() to get the new state, message
 
881
    and sets the status using status_set()
 
882
 
 
883
    @param configs: a templating.OSConfigRenderer() object
 
884
    @param required_interfaces: {generic: [specific, specific2, ...]}
 
885
    @param charm_func: a callable function that returns state, message. The
 
886
                       signature is charm_func(configs) -> (state, message)
 
887
    @param services: list of strings OR dictionary specifying services/ports
 
888
    @param ports: OPTIONAL list of port numbers.
 
889
    @returns state, message: the new workload status, user message
 
890
    """
 
891
    state, message = _determine_os_workload_status(
 
892
        configs, required_interfaces, charm_func, services, ports)
 
893
    status_set(state, message)
 
894
 
 
895
 
 
896
def _determine_os_workload_status(
 
897
        configs, required_interfaces, charm_func=None,
 
898
        services=None, ports=None):
 
899
    """Determine the state of the workload status for the charm.
 
900
 
 
901
    This function returns the new workload status for the charm based
 
902
    on the state of the interfaces, the paused state and whether the
 
903
    services are actually running and any specified ports are open.
 
904
 
 
905
    This checks:
 
906
 
 
907
     1. if the unit should be paused, that it is actually paused.  If so the
 
908
        state is 'maintenance' + message, else 'broken'.
 
909
     2. that the interfaces/relations are complete.  If they are not then
 
910
        it sets the state to either 'broken' or 'waiting' and an appropriate
 
911
        message.
 
912
     3. If all the relation data is set, then it checks that the actual
 
913
        services really are running.  If not it sets the state to 'broken'.
 
914
 
 
915
    If everything is okay then the state returns 'active'.
 
916
 
 
917
    @param configs: a templating.OSConfigRenderer() object
 
918
    @param required_interfaces: {generic: [specific, specific2, ...]}
 
919
    @param charm_func: a callable function that returns state, message. The
 
920
                       signature is charm_func(configs) -> (state, message)
 
921
    @param services: list of strings OR dictionary specifying services/ports
 
922
    @param ports: OPTIONAL list of port numbers.
 
923
    @returns state, message: the new workload status, user message
 
924
    """
 
925
    state, message = _ows_check_if_paused(services, ports)
 
926
 
 
927
    if state is None:
 
928
        state, message = _ows_check_generic_interfaces(
 
929
            configs, required_interfaces)
 
930
 
 
931
    if state != 'maintenance' and charm_func:
 
932
        # _ows_check_charm_func() may modify the state, message
 
933
        state, message = _ows_check_charm_func(
 
934
            state, message, lambda: charm_func(configs))
 
935
 
 
936
    if state is None:
 
937
        state, message = _ows_check_services_running(services, ports)
 
938
 
 
939
    if state is None:
 
940
        state = 'active'
 
941
        message = "Unit is ready"
 
942
        juju_log(message, 'INFO')
 
943
 
 
944
    return state, message
 
945
 
 
946
 
 
947
def _ows_check_if_paused(services=None, ports=None):
 
948
    """Check if the unit is supposed to be paused, and if so check that the
 
949
    services/ports (if passed) are actually stopped/not being listened to.
 
950
 
 
951
    if the unit isn't supposed to be paused, just return None, None
 
952
 
 
953
    @param services: OPTIONAL services spec or list of service names.
 
954
    @param ports: OPTIONAL list of port numbers.
 
955
    @returns state, message or None, None
 
956
    """
 
957
    if is_unit_paused_set():
 
958
        state, message = check_actually_paused(services=services,
 
959
                                               ports=ports)
 
960
        if state is None:
 
961
            # we're paused okay, so set maintenance and return
 
962
            state = "maintenance"
 
963
            message = "Paused. Use 'resume' action to resume normal service."
 
964
        return state, message
 
965
    return None, None
 
966
 
 
967
 
 
968
def _ows_check_generic_interfaces(configs, required_interfaces):
 
969
    """Check the complete contexts to determine the workload status.
 
970
 
 
971
     - Checks for missing or incomplete contexts
 
972
     - juju log details of missing required data.
 
973
     - determines the correct workload status
 
974
     - creates an appropriate message for status_set(...)
 
975
 
 
976
    if there are no problems then the function returns None, None
 
977
 
 
978
    @param configs: a templating.OSConfigRenderer() object
 
979
    @params required_interfaces: {generic_interface: [specific_interface], }
 
980
    @returns state, message or None, None
 
981
    """
 
982
    incomplete_rel_data = incomplete_relation_data(configs,
 
983
                                                   required_interfaces)
 
984
    state = None
 
985
    message = None
 
986
    missing_relations = set()
 
987
    incomplete_relations = set()
 
988
 
 
989
    for generic_interface, relations_states in incomplete_rel_data.items():
 
990
        related_interface = None
 
991
        missing_data = {}
 
992
        # Related or not?
 
993
        for interface, relation_state in relations_states.items():
 
994
            if relation_state.get('related'):
 
995
                related_interface = interface
 
996
                missing_data = relation_state.get('missing_data')
 
997
                break
 
998
        # No relation ID for the generic_interface?
 
999
        if not related_interface:
 
1000
            juju_log("{} relation is missing and must be related for "
 
1001
                     "functionality. ".format(generic_interface), 'WARN')
 
1002
            state = 'blocked'
 
1003
            missing_relations.add(generic_interface)
 
1004
        else:
 
1005
            # Relation ID eists but no related unit
 
1006
            if not missing_data:
 
1007
                # Edge case - relation ID exists but departings
 
1008
                _hook_name = hook_name()
 
1009
                if (('departed' in _hook_name or 'broken' in _hook_name) and
 
1010
                        related_interface in _hook_name):
 
1011
                    state = 'blocked'
 
1012
                    missing_relations.add(generic_interface)
 
1013
                    juju_log("{} relation's interface, {}, "
 
1014
                             "relationship is departed or broken "
 
1015
                             "and is required for functionality."
 
1016
                             "".format(generic_interface, related_interface),
 
1017
                             "WARN")
 
1018
                # Normal case relation ID exists but no related unit
 
1019
                # (joining)
 
1020
                else:
 
1021
                    juju_log("{} relations's interface, {}, is related but has"
 
1022
                             " no units in the relation."
 
1023
                             "".format(generic_interface, related_interface),
 
1024
                             "INFO")
 
1025
            # Related unit exists and data missing on the relation
 
1026
            else:
 
1027
                juju_log("{} relation's interface, {}, is related awaiting "
 
1028
                         "the following data from the relationship: {}. "
 
1029
                         "".format(generic_interface, related_interface,
 
1030
                                   ", ".join(missing_data)), "INFO")
 
1031
            if state != 'blocked':
 
1032
                state = 'waiting'
 
1033
            if generic_interface not in missing_relations:
 
1034
                incomplete_relations.add(generic_interface)
 
1035
 
 
1036
    if missing_relations:
 
1037
        message = "Missing relations: {}".format(", ".join(missing_relations))
 
1038
        if incomplete_relations:
 
1039
            message += "; incomplete relations: {}" \
 
1040
                       "".format(", ".join(incomplete_relations))
 
1041
        state = 'blocked'
 
1042
    elif incomplete_relations:
 
1043
        message = "Incomplete relations: {}" \
 
1044
                  "".format(", ".join(incomplete_relations))
 
1045
        state = 'waiting'
 
1046
 
 
1047
    return state, message
 
1048
 
 
1049
 
 
1050
def _ows_check_charm_func(state, message, charm_func_with_configs):
 
1051
    """Run a custom check function for the charm to see if it wants to
 
1052
    change the state.  This is only run if not in 'maintenance' and
 
1053
    tests to see if the new state is more important that the previous
 
1054
    one determined by the interfaces/relations check.
 
1055
 
 
1056
    @param state: the previously determined state so far.
 
1057
    @param message: the user orientated message so far.
 
1058
    @param charm_func: a callable function that returns state, message
 
1059
    @returns state, message strings.
 
1060
    """
 
1061
    if charm_func_with_configs:
 
1062
        charm_state, charm_message = charm_func_with_configs()
 
1063
        if charm_state != 'active' and charm_state != 'unknown':
 
1064
            state = workload_state_compare(state, charm_state)
 
1065
            if message:
 
1066
                charm_message = charm_message.replace("Incomplete relations: ",
 
1067
                                                      "")
 
1068
                message = "{}, {}".format(message, charm_message)
 
1069
            else:
 
1070
                message = charm_message
 
1071
    return state, message
 
1072
 
 
1073
 
 
1074
def _ows_check_services_running(services, ports):
 
1075
    """Check that the services that should be running are actually running
 
1076
    and that any ports specified are being listened to.
 
1077
 
 
1078
    @param services: list of strings OR dictionary specifying services/ports
 
1079
    @param ports: list of ports
 
1080
    @returns state, message: strings or None, None
 
1081
    """
 
1082
    messages = []
 
1083
    state = None
 
1084
    if services is not None:
 
1085
        services = _extract_services_list_helper(services)
 
1086
        services_running, running = _check_running_services(services)
 
1087
        if not all(running):
 
1088
            messages.append(
 
1089
                "Services not running that should be: {}"
 
1090
                .format(", ".join(_filter_tuples(services_running, False))))
 
1091
            state = 'blocked'
 
1092
        # also verify that the ports that should be open are open
 
1093
        # NB, that ServiceManager objects only OPTIONALLY have ports
 
1094
        map_not_open, ports_open = (
 
1095
            _check_listening_on_services_ports(services))
 
1096
        if not all(ports_open):
 
1097
            # find which service has missing ports. They are in service
 
1098
            # order which makes it a bit easier.
 
1099
            message_parts = {service: ", ".join([str(v) for v in open_ports])
 
1100
                             for service, open_ports in map_not_open.items()}
 
1101
            message = ", ".join(
 
1102
                ["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
 
1103
            messages.append(
 
1104
                "Services with ports not open that should be: {}"
 
1105
                .format(message))
 
1106
            state = 'blocked'
 
1107
 
 
1108
    if ports is not None:
 
1109
        # and we can also check ports which we don't know the service for
 
1110
        ports_open, ports_open_bools = _check_listening_on_ports_list(ports)
 
1111
        if not all(ports_open_bools):
 
1112
            messages.append(
 
1113
                "Ports which should be open, but are not: {}"
 
1114
                .format(", ".join([str(p) for p, v in ports_open
 
1115
                                   if not v])))
 
1116
            state = 'blocked'
 
1117
 
 
1118
    if state is not None:
 
1119
        message = "; ".join(messages)
 
1120
        return state, message
 
1121
 
 
1122
    return None, None
 
1123
 
 
1124
 
 
1125
def _extract_services_list_helper(services):
 
1126
    """Extract a OrderedDict of {service: [ports]} of the supplied services
 
1127
    for use by the other functions.
 
1128
 
 
1129
    The services object can either be:
 
1130
      - None : no services were passed (an empty dict is returned)
 
1131
      - a list of strings
 
1132
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
 
1133
      - An array of [{'service': service_name, ...}, ...]
 
1134
 
 
1135
    @param services: see above
 
1136
    @returns OrderedDict(service: [ports], ...)
 
1137
    """
 
1138
    if services is None:
 
1139
        return {}
 
1140
    if isinstance(services, dict):
 
1141
        services = services.values()
 
1142
    # either extract the list of services from the dictionary, or if
 
1143
    # it is a simple string, use that. i.e. works with mixed lists.
 
1144
    _s = OrderedDict()
 
1145
    for s in services:
 
1146
        if isinstance(s, dict) and 'service' in s:
 
1147
            _s[s['service']] = s.get('ports', [])
 
1148
        if isinstance(s, str):
 
1149
            _s[s] = []
 
1150
    return _s
 
1151
 
 
1152
 
 
1153
def _check_running_services(services):
 
1154
    """Check that the services dict provided is actually running and provide
 
1155
    a list of (service, boolean) tuples for each service.
 
1156
 
 
1157
    Returns both a zipped list of (service, boolean) and a list of booleans
 
1158
    in the same order as the services.
 
1159
 
 
1160
    @param services: OrderedDict of strings: [ports], one for each service to
 
1161
                     check.
 
1162
    @returns [(service, boolean), ...], : results for checks
 
1163
             [boolean]                  : just the result of the service checks
 
1164
    """
 
1165
    services_running = [service_running(s) for s in services]
 
1166
    return list(zip(services, services_running)), services_running
 
1167
 
 
1168
 
 
1169
def _check_listening_on_services_ports(services, test=False):
 
1170
    """Check that the unit is actually listening (has the port open) on the
 
1171
    ports that the service specifies are open. If test is True then the
 
1172
    function returns the services with ports that are open rather than
 
1173
    closed.
 
1174
 
 
1175
    Returns an OrderedDict of service: ports and a list of booleans
 
1176
 
 
1177
    @param services: OrderedDict(service: [port, ...], ...)
 
1178
    @param test: default=False, if False, test for closed, otherwise open.
 
1179
    @returns OrderedDict(service: [port-not-open, ...]...), [boolean]
 
1180
    """
 
1181
    test = not(not(test))  # ensure test is True or False
 
1182
    all_ports = list(itertools.chain(*services.values()))
 
1183
    ports_states = [port_has_listener('0.0.0.0', p) for p in all_ports]
 
1184
    map_ports = OrderedDict()
 
1185
    matched_ports = [p for p, opened in zip(all_ports, ports_states)
 
1186
                     if opened == test]  # essentially opened xor test
 
1187
    for service, ports in services.items():
 
1188
        set_ports = set(ports).intersection(matched_ports)
 
1189
        if set_ports:
 
1190
            map_ports[service] = set_ports
 
1191
    return map_ports, ports_states
 
1192
 
 
1193
 
 
1194
def _check_listening_on_ports_list(ports):
 
1195
    """Check that the ports list given are being listened to
 
1196
 
 
1197
    Returns a list of ports being listened to and a list of the
 
1198
    booleans.
 
1199
 
 
1200
    @param ports: LIST or port numbers.
 
1201
    @returns [(port_num, boolean), ...], [boolean]
 
1202
    """
 
1203
    ports_open = [port_has_listener('0.0.0.0', p) for p in ports]
 
1204
    return zip(ports, ports_open), ports_open
 
1205
 
 
1206
 
 
1207
def _filter_tuples(services_states, state):
 
1208
    """Return a simple list from a list of tuples according to the condition
 
1209
 
 
1210
    @param services_states: LIST of (string, boolean): service and running
 
1211
           state.
 
1212
    @param state: Boolean to match the tuple against.
 
1213
    @returns [LIST of strings] that matched the tuple RHS.
 
1214
    """
 
1215
    return [s for s, b in services_states if b == state]
 
1216
 
 
1217
 
 
1218
def workload_state_compare(current_workload_state, workload_state):
 
1219
    """ Return highest priority of two states"""
 
1220
    hierarchy = {'unknown': -1,
 
1221
                 'active': 0,
 
1222
                 'maintenance': 1,
 
1223
                 'waiting': 2,
 
1224
                 'blocked': 3,
 
1225
                 }
 
1226
 
 
1227
    if hierarchy.get(workload_state) is None:
 
1228
        workload_state = 'unknown'
 
1229
    if hierarchy.get(current_workload_state) is None:
 
1230
        current_workload_state = 'unknown'
 
1231
 
 
1232
    # Set workload_state based on hierarchy of statuses
 
1233
    if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
 
1234
        return current_workload_state
 
1235
    else:
 
1236
        return workload_state
 
1237
 
 
1238
 
 
1239
def incomplete_relation_data(configs, required_interfaces):
 
1240
    """Check complete contexts against required_interfaces
 
1241
    Return dictionary of incomplete relation data.
 
1242
 
 
1243
    configs is an OSConfigRenderer object with configs registered
 
1244
 
 
1245
    required_interfaces is a dictionary of required general interfaces
 
1246
    with dictionary values of possible specific interfaces.
 
1247
    Example:
 
1248
    required_interfaces = {'database': ['shared-db', 'pgsql-db']}
 
1249
 
 
1250
    The interface is said to be satisfied if anyone of the interfaces in the
 
1251
    list has a complete context.
 
1252
 
 
1253
    Return dictionary of incomplete or missing required contexts with relation
 
1254
    status of interfaces and any missing data points. Example:
 
1255
        {'message':
 
1256
             {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
 
1257
              'zeromq-configuration': {'related': False}},
 
1258
         'identity':
 
1259
             {'identity-service': {'related': False}},
 
1260
         'database':
 
1261
             {'pgsql-db': {'related': False},
 
1262
              'shared-db': {'related': True}}}
 
1263
    """
 
1264
    complete_ctxts = configs.complete_contexts()
 
1265
    incomplete_relations = [
 
1266
        svc_type
 
1267
        for svc_type, interfaces in required_interfaces.items()
 
1268
        if not set(interfaces).intersection(complete_ctxts)]
 
1269
    return {
 
1270
        i: configs.get_incomplete_context_data(required_interfaces[i])
 
1271
        for i in incomplete_relations}
 
1272
 
 
1273
 
 
1274
def do_action_openstack_upgrade(package, upgrade_callback, configs):
 
1275
    """Perform action-managed OpenStack upgrade.
 
1276
 
 
1277
    Upgrades packages to the configured openstack-origin version and sets
 
1278
    the corresponding action status as a result.
 
1279
 
 
1280
    If the charm was installed from source we cannot upgrade it.
 
1281
    For backwards compatibility a config flag (action-managed-upgrade) must
 
1282
    be set for this code to run, otherwise a full service level upgrade will
 
1283
    fire on config-changed.
 
1284
 
 
1285
    @param package: package name for determining if upgrade available
 
1286
    @param upgrade_callback: function callback to charm's upgrade function
 
1287
    @param configs: templating object derived from OSConfigRenderer class
 
1288
 
 
1289
    @return: True if upgrade successful; False if upgrade failed or skipped
 
1290
    """
 
1291
    ret = False
 
1292
 
 
1293
    if git_install_requested():
 
1294
        action_set({'outcome': 'installed from source, skipped upgrade.'})
 
1295
    else:
 
1296
        if openstack_upgrade_available(package):
 
1297
            if config('action-managed-upgrade'):
 
1298
                juju_log('Upgrading OpenStack release')
 
1299
 
 
1300
                try:
 
1301
                    upgrade_callback(configs=configs)
 
1302
                    action_set({'outcome': 'success, upgrade completed.'})
 
1303
                    ret = True
 
1304
                except:
 
1305
                    action_set({'outcome': 'upgrade failed, see traceback.'})
 
1306
                    action_set({'traceback': traceback.format_exc()})
 
1307
                    action_fail('do_openstack_upgrade resulted in an '
 
1308
                                'unexpected error')
 
1309
            else:
 
1310
                action_set({'outcome': 'action-managed-upgrade config is '
 
1311
                                       'False, skipped upgrade.'})
 
1312
        else:
 
1313
            action_set({'outcome': 'no upgrade available.'})
 
1314
 
 
1315
    return ret
 
1316
 
 
1317
 
 
1318
def remote_restart(rel_name, remote_service=None):
 
1319
    trigger = {
 
1320
        'restart-trigger': str(uuid.uuid4()),
 
1321
    }
 
1322
    if remote_service:
 
1323
        trigger['remote-service'] = remote_service
 
1324
    for rid in relation_ids(rel_name):
 
1325
        # This subordinate can be related to two seperate services using
 
1326
        # different subordinate relations so only issue the restart if
 
1327
        # the principle is conencted down the relation we think it is
 
1328
        if related_units(relid=rid):
 
1329
            relation_set(relation_id=rid,
 
1330
                         relation_settings=trigger,
 
1331
                         )
 
1332
 
 
1333
 
 
1334
def check_actually_paused(services=None, ports=None):
 
1335
    """Check that services listed in the services object and and ports
 
1336
    are actually closed (not listened to), to verify that the unit is
 
1337
    properly paused.
 
1338
 
 
1339
    @param services: See _extract_services_list_helper
 
1340
    @returns status, : string for status (None if okay)
 
1341
             message : string for problem for status_set
 
1342
    """
 
1343
    state = None
 
1344
    message = None
 
1345
    messages = []
 
1346
    if services is not None:
 
1347
        services = _extract_services_list_helper(services)
 
1348
        services_running, services_states = _check_running_services(services)
 
1349
        if any(services_states):
 
1350
            # there shouldn't be any running so this is a problem
 
1351
            messages.append("these services running: {}"
 
1352
                            .format(", ".join(
 
1353
                                _filter_tuples(services_running, True))))
 
1354
            state = "blocked"
 
1355
        ports_open, ports_open_bools = (
 
1356
            _check_listening_on_services_ports(services, True))
 
1357
        if any(ports_open_bools):
 
1358
            message_parts = {service: ", ".join([str(v) for v in open_ports])
 
1359
                             for service, open_ports in ports_open.items()}
 
1360
            message = ", ".join(
 
1361
                ["{}: [{}]".format(s, sp) for s, sp in message_parts.items()])
 
1362
            messages.append(
 
1363
                "these service:ports are open: {}".format(message))
 
1364
            state = 'blocked'
 
1365
    if ports is not None:
 
1366
        ports_open, bools = _check_listening_on_ports_list(ports)
 
1367
        if any(bools):
 
1368
            messages.append(
 
1369
                "these ports which should be closed, but are open: {}"
 
1370
                .format(", ".join([str(p) for p, v in ports_open if v])))
 
1371
            state = 'blocked'
 
1372
    if messages:
 
1373
        message = ("Services should be paused but {}"
 
1374
                   .format(", ".join(messages)))
 
1375
    return state, message
 
1376
 
 
1377
 
 
1378
def set_unit_paused():
 
1379
    """Set the unit to a paused state in the local kv() store.
 
1380
    This does NOT actually pause the unit
 
1381
    """
 
1382
    with unitdata.HookData()() as t:
 
1383
        kv = t[0]
 
1384
        kv.set('unit-paused', True)
 
1385
 
 
1386
 
 
1387
def clear_unit_paused():
 
1388
    """Clear the unit from a paused state in the local kv() store
 
1389
    This does NOT actually restart any services - it only clears the
 
1390
    local state.
 
1391
    """
 
1392
    with unitdata.HookData()() as t:
 
1393
        kv = t[0]
 
1394
        kv.set('unit-paused', False)
 
1395
 
 
1396
 
 
1397
def is_unit_paused_set():
 
1398
    """Return the state of the kv().get('unit-paused').
 
1399
    This does NOT verify that the unit really is paused.
 
1400
 
 
1401
    To help with units that don't have HookData() (testing)
 
1402
    if it excepts, return False
 
1403
    """
 
1404
    try:
 
1405
        with unitdata.HookData()() as t:
 
1406
            kv = t[0]
 
1407
            # transform something truth-y into a Boolean.
 
1408
            return not(not(kv.get('unit-paused')))
 
1409
    except:
 
1410
        return False
 
1411
 
 
1412
 
 
1413
def pause_unit(assess_status_func, services=None, ports=None,
 
1414
               charm_func=None):
 
1415
    """Pause a unit by stopping the services and setting 'unit-paused'
 
1416
    in the local kv() store.
 
1417
 
 
1418
    Also checks that the services have stopped and ports are no longer
 
1419
    being listened to.
 
1420
 
 
1421
    An optional charm_func() can be called that can either raise an
 
1422
    Exception or return non None, None to indicate that the unit
 
1423
    didn't pause cleanly.
 
1424
 
 
1425
    The signature for charm_func is:
 
1426
    charm_func() -> message: string
 
1427
 
 
1428
    charm_func() is executed after any services are stopped, if supplied.
 
1429
 
 
1430
    The services object can either be:
 
1431
      - None : no services were passed (an empty dict is returned)
 
1432
      - a list of strings
 
1433
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
 
1434
      - An array of [{'service': service_name, ...}, ...]
 
1435
 
 
1436
    @param assess_status_func: (f() -> message: string | None) or None
 
1437
    @param services: OPTIONAL see above
 
1438
    @param ports: OPTIONAL list of port
 
1439
    @param charm_func: function to run for custom charm pausing.
 
1440
    @returns None
 
1441
    @raises Exception(message) on an error for action_fail().
 
1442
    """
 
1443
    services = _extract_services_list_helper(services)
 
1444
    messages = []
 
1445
    if services:
 
1446
        for service in services.keys():
 
1447
            stopped = service_pause(service)
 
1448
            if not stopped:
 
1449
                messages.append("{} didn't stop cleanly.".format(service))
 
1450
    if charm_func:
 
1451
        try:
 
1452
            message = charm_func()
 
1453
            if message:
 
1454
                messages.append(message)
 
1455
        except Exception as e:
 
1456
            message.append(str(e))
 
1457
    set_unit_paused()
 
1458
    if assess_status_func:
 
1459
        message = assess_status_func()
 
1460
        if message:
 
1461
            messages.append(message)
 
1462
    if messages:
 
1463
        raise Exception("Couldn't pause: {}".format("; ".join(messages)))
 
1464
 
 
1465
 
 
1466
def resume_unit(assess_status_func, services=None, ports=None,
 
1467
                charm_func=None):
 
1468
    """Resume a unit by starting the services and clearning 'unit-paused'
 
1469
    in the local kv() store.
 
1470
 
 
1471
    Also checks that the services have started and ports are being listened to.
 
1472
 
 
1473
    An optional charm_func() can be called that can either raise an
 
1474
    Exception or return non None to indicate that the unit
 
1475
    didn't resume cleanly.
 
1476
 
 
1477
    The signature for charm_func is:
 
1478
    charm_func() -> message: string
 
1479
 
 
1480
    charm_func() is executed after any services are started, if supplied.
 
1481
 
 
1482
    The services object can either be:
 
1483
      - None : no services were passed (an empty dict is returned)
 
1484
      - a list of strings
 
1485
      - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
 
1486
      - An array of [{'service': service_name, ...}, ...]
 
1487
 
 
1488
    @param assess_status_func: (f() -> message: string | None) or None
 
1489
    @param services: OPTIONAL see above
 
1490
    @param ports: OPTIONAL list of port
 
1491
    @param charm_func: function to run for custom charm resuming.
 
1492
    @returns None
 
1493
    @raises Exception(message) on an error for action_fail().
 
1494
    """
 
1495
    services = _extract_services_list_helper(services)
 
1496
    messages = []
 
1497
    if services:
 
1498
        for service in services.keys():
 
1499
            started = service_resume(service)
 
1500
            if not started:
 
1501
                messages.append("{} didn't start cleanly.".format(service))
 
1502
    if charm_func:
 
1503
        try:
 
1504
            message = charm_func()
 
1505
            if message:
 
1506
                messages.append(message)
 
1507
        except Exception as e:
 
1508
            message.append(str(e))
 
1509
    clear_unit_paused()
 
1510
    if assess_status_func:
 
1511
        message = assess_status_func()
 
1512
        if message:
 
1513
            messages.append(message)
 
1514
    if messages:
 
1515
        raise Exception("Couldn't resume: {}".format("; ".join(messages)))
 
1516
 
 
1517
 
 
1518
def make_assess_status_func(*args, **kwargs):
 
1519
    """Creates an assess_status_func() suitable for handing to pause_unit()
 
1520
    and resume_unit().
 
1521
 
 
1522
    This uses the _determine_os_workload_status(...) function to determine
 
1523
    what the workload_status should be for the unit.  If the unit is
 
1524
    not in maintenance or active states, then the message is returned to
 
1525
    the caller.  This is so an action that doesn't result in either a
 
1526
    complete pause or complete resume can signal failure with an action_fail()
 
1527
    """
 
1528
    def _assess_status_func():
 
1529
        state, message = _determine_os_workload_status(*args, **kwargs)
 
1530
        status_set(state, message)
 
1531
        if state not in ['maintenance', 'active']:
 
1532
            return message
 
1533
        return None
 
1534
 
 
1535
    return _assess_status_func
 
1536
 
 
1537
 
 
1538
def pausable_restart_on_change(restart_map, stopstart=False,
 
1539
                               restart_functions=None):
 
1540
    """A restart_on_change decorator that checks to see if the unit is
 
1541
    paused. If it is paused then the decorated function doesn't fire.
 
1542
 
 
1543
    This is provided as a helper, as the @restart_on_change(...) decorator
 
1544
    is in core.host, yet the openstack specific helpers are in this file
 
1545
    (contrib.openstack.utils).  Thus, this needs to be an optional feature
 
1546
    for openstack charms (or charms that wish to use the openstack
 
1547
    pause/resume type features).
 
1548
 
 
1549
    It is used as follows:
 
1550
 
 
1551
        from contrib.openstack.utils import (
 
1552
            pausable_restart_on_change as restart_on_change)
 
1553
 
 
1554
        @restart_on_change(restart_map, stopstart=<boolean>)
 
1555
        def some_hook(...):
 
1556
            pass
 
1557
 
 
1558
    see core.utils.restart_on_change() for more details.
 
1559
 
 
1560
    @param f: the function to decorate
 
1561
    @param restart_map: the restart map {conf_file: [services]}
 
1562
    @param stopstart: DEFAULT false; whether to stop, start or just restart
 
1563
    @returns decorator to use a restart_on_change with pausability
 
1564
    """
 
1565
    def wrap(f):
 
1566
        @functools.wraps(f)
 
1567
        def wrapped_f(*args, **kwargs):
 
1568
            if is_unit_paused_set():
 
1569
                return f(*args, **kwargs)
 
1570
            # otherwise, normal restart_on_change functionality
 
1571
            return restart_on_change_helper(
 
1572
                (lambda: f(*args, **kwargs)), restart_map, stopstart,
 
1573
                restart_functions)
 
1574
        return wrapped_f
 
1575
    return wrap