~openstack-charmers-next/charms/trusty/cisco-vpp/trunk

« back to all changes in this revision

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

  • Committer: Liam Young
  • Date: 2015-12-02 10:33:20 UTC
  • mfrom: (115.1.9 dhcp)
  • Revision ID: liam.young@canonical.com-20151202103320-0cjg1te17yovqyu3
[gnuoy, r=james-page] Adds support for serving dhcp and metadata requests to guests.

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
 
 
27
import six
 
28
import traceback
27
29
import uuid
28
 
 
29
 
import six
30
30
import yaml
31
31
 
32
32
from charmhelpers.contrib.network import ip
36
36
)
37
37
 
38
38
from charmhelpers.core.hookenv import (
 
39
    action_fail,
 
40
    action_set,
39
41
    config,
40
42
    log as juju_log,
41
43
    charm_dir,
42
44
    INFO,
 
45
    related_units,
43
46
    relation_ids,
44
 
    related_units,
45
47
    relation_set,
 
48
    status_set,
 
49
    hook_name
46
50
)
47
51
 
48
52
from charmhelpers.contrib.storage.linux.lvm import (
52
56
)
53
57
 
54
58
from charmhelpers.contrib.network.ip import (
55
 
    get_ipv6_addr
 
59
    get_ipv6_addr,
 
60
    is_ipv6,
56
61
)
57
62
 
58
63
from charmhelpers.contrib.python.packages import (
71
76
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
72
77
                   'restricted main multiverse universe')
73
78
 
74
 
 
75
79
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
76
80
    ('oneiric', 'diablo'),
77
81
    ('precise', 'essex'),
81
85
    ('trusty', 'icehouse'),
82
86
    ('utopic', 'juno'),
83
87
    ('vivid', 'kilo'),
 
88
    ('wily', 'liberty'),
84
89
])
85
90
 
86
91
 
93
98
    ('2014.1', 'icehouse'),
94
99
    ('2014.2', 'juno'),
95
100
    ('2015.1', 'kilo'),
 
101
    ('2015.2', 'liberty'),
96
102
])
97
103
 
98
104
# The ugly duckling
115
121
    ('2.2.0', 'juno'),
116
122
    ('2.2.1', 'kilo'),
117
123
    ('2.2.2', 'kilo'),
 
124
    ('2.3.0', 'liberty'),
 
125
    ('2.4.0', 'liberty'),
 
126
    ('2.5.0', 'liberty'),
118
127
])
119
128
 
 
129
# >= Liberty version->codename mapping
 
130
PACKAGE_CODENAMES = {
 
131
    'nova-common': OrderedDict([
 
132
        ('12.0.0', 'liberty'),
 
133
    ]),
 
134
    'neutron-common': OrderedDict([
 
135
        ('7.0.0', 'liberty'),
 
136
    ]),
 
137
    'cinder-common': OrderedDict([
 
138
        ('7.0.0', 'liberty'),
 
139
    ]),
 
140
    'keystone': OrderedDict([
 
141
        ('8.0.0', 'liberty'),
 
142
    ]),
 
143
    'horizon-common': OrderedDict([
 
144
        ('8.0.0', 'liberty'),
 
145
    ]),
 
146
    'ceilometer-common': OrderedDict([
 
147
        ('5.0.0', 'liberty'),
 
148
    ]),
 
149
    'heat-common': OrderedDict([
 
150
        ('5.0.0', 'liberty'),
 
151
    ]),
 
152
    'glance-common': OrderedDict([
 
153
        ('11.0.0', 'liberty'),
 
154
    ]),
 
155
    'openstack-dashboard': OrderedDict([
 
156
        ('8.0.0', 'liberty'),
 
157
    ]),
 
158
}
 
159
 
120
160
DEFAULT_LOOPBACK_SIZE = '5G'
121
161
 
122
162
 
166
206
        error_out(e)
167
207
 
168
208
 
169
 
def get_os_version_codename(codename):
 
209
def get_os_version_codename(codename, version_map=OPENSTACK_CODENAMES):
170
210
    '''Determine OpenStack version number from codename.'''
171
 
    for k, v in six.iteritems(OPENSTACK_CODENAMES):
 
211
    for k, v in six.iteritems(version_map):
172
212
        if v == codename:
173
213
            return k
174
214
    e = 'Could not derive OpenStack version for '\
200
240
        error_out(e)
201
241
 
202
242
    vers = apt.upstream_version(pkg.current_ver.ver_str)
 
243
    match = re.match('^(\d+)\.(\d+)\.(\d+)', vers)
 
244
    if match:
 
245
        vers = match.group(0)
203
246
 
204
 
    try:
205
 
        if 'swift' in pkg.name:
206
 
            swift_vers = vers[:5]
207
 
            if swift_vers not in SWIFT_CODENAMES:
208
 
                # Deal with 1.10.0 upward
209
 
                swift_vers = vers[:6]
210
 
            return SWIFT_CODENAMES[swift_vers]
211
 
        else:
212
 
            vers = vers[:6]
213
 
            return OPENSTACK_CODENAMES[vers]
214
 
    except KeyError:
215
 
        e = 'Could not determine OpenStack codename for version %s' % vers
216
 
        error_out(e)
 
247
    # >= Liberty independent project versions
 
248
    if (package in PACKAGE_CODENAMES and
 
249
            vers in PACKAGE_CODENAMES[package]):
 
250
        return PACKAGE_CODENAMES[package][vers]
 
251
    else:
 
252
        # < Liberty co-ordinated project versions
 
253
        try:
 
254
            if 'swift' in pkg.name:
 
255
                swift_vers = vers[:5]
 
256
                if swift_vers not in SWIFT_CODENAMES:
 
257
                    # Deal with 1.10.0 upward
 
258
                    swift_vers = vers[:6]
 
259
                return SWIFT_CODENAMES[swift_vers]
 
260
            else:
 
261
                vers = vers[:6]
 
262
                return OPENSTACK_CODENAMES[vers]
 
263
        except KeyError:
 
264
            if not fatal:
 
265
                return None
 
266
            e = 'Could not determine OpenStack codename for version %s' % vers
 
267
            error_out(e)
217
268
 
218
269
 
219
270
def get_os_version_package(pkg, fatal=True):
323
374
            'kilo': 'trusty-updates/kilo',
324
375
            'kilo/updates': 'trusty-updates/kilo',
325
376
            'kilo/proposed': 'trusty-proposed/kilo',
 
377
            'liberty': 'trusty-updates/liberty',
 
378
            'liberty/updates': 'trusty-updates/liberty',
 
379
            'liberty/proposed': 'trusty-proposed/liberty',
326
380
        }
327
381
 
328
382
        try:
388
442
    import apt_pkg as apt
389
443
    src = config('openstack-origin')
390
444
    cur_vers = get_os_version_package(package)
391
 
    available_vers = get_os_version_install_source(src)
 
445
    if "swift" in package:
 
446
        codename = get_os_codename_install_source(src)
 
447
        available_vers = get_os_version_codename(codename, SWIFT_CODENAMES)
 
448
    else:
 
449
        available_vers = get_os_version_install_source(src)
392
450
    apt.init()
393
451
    return apt.version_compare(available_vers, cur_vers) == 1
394
452
 
465
523
                                      relation_prefix=None):
466
524
    hosts = get_ipv6_addr(dynamic_only=False)
467
525
 
 
526
    if config('vip'):
 
527
        vips = config('vip').split()
 
528
        for vip in vips:
 
529
            if vip and is_ipv6(vip):
 
530
                hosts.append(vip)
 
531
 
468
532
    kwargs = {'database': database,
469
533
              'username': database_user,
470
534
              'hostname': json.dumps(hosts)}
518
582
    Clone/install all specified OpenStack repositories.
519
583
 
520
584
    The expected format of projects_yaml is:
 
585
 
521
586
        repositories:
522
587
          - {name: keystone,
523
588
             repository: 'git://git.openstack.org/openstack/keystone.git',
525
590
          - {name: requirements,
526
591
             repository: 'git://git.openstack.org/openstack/requirements.git',
527
592
             branch: 'stable/icehouse'}
 
593
 
528
594
        directory: /mnt/openstack-git
529
595
        http_proxy: squid-proxy-url
530
596
        https_proxy: squid-proxy-url
531
597
 
532
 
        The directory, http_proxy, and https_proxy keys are optional.
 
598
    The directory, http_proxy, and https_proxy keys are optional.
 
599
 
533
600
    """
534
601
    global requirements_dir
535
602
    parent_dir = '/mnt/openstack-git'
551
618
 
552
619
    pip_create_virtualenv(os.path.join(parent_dir, 'venv'))
553
620
 
 
621
    # Upgrade setuptools and pip from default virtualenv versions. The default
 
622
    # versions in trusty break master OpenStack branch deployments.
 
623
    for p in ['pip', 'setuptools']:
 
624
        pip_install(p, upgrade=True, proxy=http_proxy,
 
625
                    venv=os.path.join(parent_dir, 'venv'))
 
626
 
554
627
    for p in projects['repositories']:
555
628
        repo = p['repository']
556
629
        branch = p['branch']
612
685
    else:
613
686
        repo_dir = dest_dir
614
687
 
 
688
    venv = os.path.join(parent_dir, 'venv')
 
689
 
615
690
    if update_requirements:
616
691
        if not requirements_dir:
617
692
            error_out('requirements repo must be cloned before '
618
693
                      'updating from global requirements.')
619
 
        _git_update_requirements(repo_dir, requirements_dir)
 
694
        _git_update_requirements(venv, repo_dir, requirements_dir)
620
695
 
621
696
    juju_log('Installing git repo from dir: {}'.format(repo_dir))
622
697
    if http_proxy:
623
 
        pip_install(repo_dir, proxy=http_proxy,
624
 
                    venv=os.path.join(parent_dir, 'venv'))
 
698
        pip_install(repo_dir, proxy=http_proxy, venv=venv)
625
699
    else:
626
 
        pip_install(repo_dir,
627
 
                    venv=os.path.join(parent_dir, 'venv'))
 
700
        pip_install(repo_dir, venv=venv)
628
701
 
629
702
    return repo_dir
630
703
 
631
704
 
632
 
def _git_update_requirements(package_dir, reqs_dir):
 
705
def _git_update_requirements(venv, package_dir, reqs_dir):
633
706
    """
634
707
    Update from global requirements.
635
708
 
638
711
    """
639
712
    orig_dir = os.getcwd()
640
713
    os.chdir(reqs_dir)
641
 
    cmd = ['python', 'update.py', package_dir]
 
714
    python = os.path.join(venv, 'bin/python')
 
715
    cmd = [python, 'update.py', package_dir]
642
716
    try:
643
717
        subprocess.check_call(cmd)
644
718
    except subprocess.CalledProcessError:
645
719
        package = os.path.basename(package_dir)
646
 
        error_out("Error updating {} from global-requirements.txt".format(package))
 
720
        error_out("Error updating {} from "
 
721
                  "global-requirements.txt".format(package))
647
722
    os.chdir(orig_dir)
648
723
 
649
724
 
691
766
    return None
692
767
 
693
768
 
 
769
def os_workload_status(configs, required_interfaces, charm_func=None):
 
770
    """
 
771
    Decorator to set workload status based on complete contexts
 
772
    """
 
773
    def wrap(f):
 
774
        @wraps(f)
 
775
        def wrapped_f(*args, **kwargs):
 
776
            # Run the original function first
 
777
            f(*args, **kwargs)
 
778
            # Set workload status now that contexts have been
 
779
            # acted on
 
780
            set_os_workload_status(configs, required_interfaces, charm_func)
 
781
        return wrapped_f
 
782
    return wrap
 
783
 
 
784
 
 
785
def set_os_workload_status(configs, required_interfaces, charm_func=None):
 
786
    """
 
787
    Set workload status based on complete contexts.
 
788
    status-set missing or incomplete contexts
 
789
    and juju-log details of missing required data.
 
790
    charm_func is a charm specific function to run checking
 
791
    for charm specific requirements such as a VIP setting.
 
792
    """
 
793
    incomplete_rel_data = incomplete_relation_data(configs, required_interfaces)
 
794
    state = 'active'
 
795
    missing_relations = []
 
796
    incomplete_relations = []
 
797
    message = None
 
798
    charm_state = None
 
799
    charm_message = None
 
800
 
 
801
    for generic_interface in incomplete_rel_data.keys():
 
802
        related_interface = None
 
803
        missing_data = {}
 
804
        # Related or not?
 
805
        for interface in incomplete_rel_data[generic_interface]:
 
806
            if incomplete_rel_data[generic_interface][interface].get('related'):
 
807
                related_interface = interface
 
808
                missing_data = incomplete_rel_data[generic_interface][interface].get('missing_data')
 
809
        # No relation ID for the generic_interface
 
810
        if not related_interface:
 
811
            juju_log("{} relation is missing and must be related for "
 
812
                     "functionality. ".format(generic_interface), 'WARN')
 
813
            state = 'blocked'
 
814
            if generic_interface not in missing_relations:
 
815
                missing_relations.append(generic_interface)
 
816
        else:
 
817
            # Relation ID exists but no related unit
 
818
            if not missing_data:
 
819
                # Edge case relation ID exists but departing
 
820
                if ('departed' in hook_name() or 'broken' in hook_name()) \
 
821
                        and related_interface in hook_name():
 
822
                    state = 'blocked'
 
823
                    if generic_interface not in missing_relations:
 
824
                        missing_relations.append(generic_interface)
 
825
                    juju_log("{} relation's interface, {}, "
 
826
                             "relationship is departed or broken "
 
827
                             "and is required for functionality."
 
828
                             "".format(generic_interface, related_interface), "WARN")
 
829
                # Normal case relation ID exists but no related unit
 
830
                # (joining)
 
831
                else:
 
832
                    juju_log("{} relations's interface, {}, is related but has "
 
833
                             "no units in the relation."
 
834
                             "".format(generic_interface, related_interface), "INFO")
 
835
            # Related unit exists and data missing on the relation
 
836
            else:
 
837
                juju_log("{} relation's interface, {}, is related awaiting "
 
838
                         "the following data from the relationship: {}. "
 
839
                         "".format(generic_interface, related_interface,
 
840
                                   ", ".join(missing_data)), "INFO")
 
841
            if state != 'blocked':
 
842
                state = 'waiting'
 
843
            if generic_interface not in incomplete_relations \
 
844
                    and generic_interface not in missing_relations:
 
845
                incomplete_relations.append(generic_interface)
 
846
 
 
847
    if missing_relations:
 
848
        message = "Missing relations: {}".format(", ".join(missing_relations))
 
849
        if incomplete_relations:
 
850
            message += "; incomplete relations: {}" \
 
851
                       "".format(", ".join(incomplete_relations))
 
852
        state = 'blocked'
 
853
    elif incomplete_relations:
 
854
        message = "Incomplete relations: {}" \
 
855
                  "".format(", ".join(incomplete_relations))
 
856
        state = 'waiting'
 
857
 
 
858
    # Run charm specific checks
 
859
    if charm_func:
 
860
        charm_state, charm_message = charm_func(configs)
 
861
        if charm_state != 'active' and charm_state != 'unknown':
 
862
            state = workload_state_compare(state, charm_state)
 
863
            if message:
 
864
                charm_message = charm_message.replace("Incomplete relations: ",
 
865
                                                      "")
 
866
                message = "{}, {}".format(message, charm_message)
 
867
            else:
 
868
                message = charm_message
 
869
 
 
870
    # Set to active if all requirements have been met
 
871
    if state == 'active':
 
872
        message = "Unit is ready"
 
873
        juju_log(message, "INFO")
 
874
 
 
875
    status_set(state, message)
 
876
 
 
877
 
 
878
def workload_state_compare(current_workload_state, workload_state):
 
879
    """ Return highest priority of two states"""
 
880
    hierarchy = {'unknown': -1,
 
881
                 'active': 0,
 
882
                 'maintenance': 1,
 
883
                 'waiting': 2,
 
884
                 'blocked': 3,
 
885
                 }
 
886
 
 
887
    if hierarchy.get(workload_state) is None:
 
888
        workload_state = 'unknown'
 
889
    if hierarchy.get(current_workload_state) is None:
 
890
        current_workload_state = 'unknown'
 
891
 
 
892
    # Set workload_state based on hierarchy of statuses
 
893
    if hierarchy.get(current_workload_state) > hierarchy.get(workload_state):
 
894
        return current_workload_state
 
895
    else:
 
896
        return workload_state
 
897
 
 
898
 
 
899
def incomplete_relation_data(configs, required_interfaces):
 
900
    """
 
901
    Check complete contexts against required_interfaces
 
902
    Return dictionary of incomplete relation data.
 
903
 
 
904
    configs is an OSConfigRenderer object with configs registered
 
905
 
 
906
    required_interfaces is a dictionary of required general interfaces
 
907
    with dictionary values of possible specific interfaces.
 
908
    Example:
 
909
    required_interfaces = {'database': ['shared-db', 'pgsql-db']}
 
910
 
 
911
    The interface is said to be satisfied if anyone of the interfaces in the
 
912
    list has a complete context.
 
913
 
 
914
    Return dictionary of incomplete or missing required contexts with relation
 
915
    status of interfaces and any missing data points. Example:
 
916
        {'message':
 
917
             {'amqp': {'missing_data': ['rabbitmq_password'], 'related': True},
 
918
              'zeromq-configuration': {'related': False}},
 
919
         'identity':
 
920
             {'identity-service': {'related': False}},
 
921
         'database':
 
922
             {'pgsql-db': {'related': False},
 
923
              'shared-db': {'related': True}}}
 
924
    """
 
925
    complete_ctxts = configs.complete_contexts()
 
926
    incomplete_relations = []
 
927
    for svc_type in required_interfaces.keys():
 
928
        # Avoid duplicates
 
929
        found_ctxt = False
 
930
        for interface in required_interfaces[svc_type]:
 
931
            if interface in complete_ctxts:
 
932
                found_ctxt = True
 
933
        if not found_ctxt:
 
934
            incomplete_relations.append(svc_type)
 
935
    incomplete_context_data = {}
 
936
    for i in incomplete_relations:
 
937
        incomplete_context_data[i] = configs.get_incomplete_context_data(required_interfaces[i])
 
938
    return incomplete_context_data
 
939
 
 
940
 
 
941
def do_action_openstack_upgrade(package, upgrade_callback, configs):
 
942
    """Perform action-managed OpenStack upgrade.
 
943
 
 
944
    Upgrades packages to the configured openstack-origin version and sets
 
945
    the corresponding action status as a result.
 
946
 
 
947
    If the charm was installed from source we cannot upgrade it.
 
948
    For backwards compatibility a config flag (action-managed-upgrade) must
 
949
    be set for this code to run, otherwise a full service level upgrade will
 
950
    fire on config-changed.
 
951
 
 
952
    @param package: package name for determining if upgrade available
 
953
    @param upgrade_callback: function callback to charm's upgrade function
 
954
    @param configs: templating object derived from OSConfigRenderer class
 
955
 
 
956
    @return: True if upgrade successful; False if upgrade failed or skipped
 
957
    """
 
958
    ret = False
 
959
 
 
960
    if git_install_requested():
 
961
        action_set({'outcome': 'installed from source, skipped upgrade.'})
 
962
    else:
 
963
        if openstack_upgrade_available(package):
 
964
            if config('action-managed-upgrade'):
 
965
                juju_log('Upgrading OpenStack release')
 
966
 
 
967
                try:
 
968
                    upgrade_callback(configs=configs)
 
969
                    action_set({'outcome': 'success, upgrade completed.'})
 
970
                    ret = True
 
971
                except:
 
972
                    action_set({'outcome': 'upgrade failed, see traceback.'})
 
973
                    action_set({'traceback': traceback.format_exc()})
 
974
                    action_fail('do_openstack_upgrade resulted in an '
 
975
                                'unexpected error')
 
976
            else:
 
977
                action_set({'outcome': 'action-managed-upgrade config is '
 
978
                                       'False, skipped upgrade.'})
 
979
        else:
 
980
            action_set({'outcome': 'no upgrade available.'})
 
981
 
 
982
    return ret
 
983
 
 
984
 
694
985
def remote_restart(rel_name, remote_service=None):
695
986
    trigger = {
696
987
        'restart-trigger': str(uuid.uuid4()),
700
991
    for rid in relation_ids(rel_name):
701
992
        # This subordinate can be related to two seperate services using
702
993
        # different subordinate relations so only issue the restart if
703
 
        # thr principle is conencted down the relation we think it is
 
994
        # the principle is conencted down the relation we think it is
704
995
        if related_units(relid=rid):
705
996
            relation_set(relation_id=rid,
706
997
                         relation_settings=trigger,