~1chb1n/charms/trusty/nova-cloud-controller/15.10-stable-flip-tests-helper-syncs

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/core/hookenv.py

  • Committer: james.page at ubuntu
  • Date: 2015-08-10 16:36:50 UTC
  • Revision ID: james.page@ubuntu.com-20150810163650-bpjo4l0dru4txcji
Tags: 15.07
[gnuoy] 15.07 Charm release

Show diffs side-by-side

added added

removed removed

Lines of Context:
21
21
#  Charm Helpers Developers <juju@lists.ubuntu.com>
22
22
 
23
23
from __future__ import print_function
 
24
import copy
 
25
from distutils.version import LooseVersion
 
26
from functools import wraps
 
27
import glob
24
28
import os
25
29
import json
26
30
import yaml
27
31
import subprocess
28
32
import sys
29
33
import errno
 
34
import tempfile
30
35
from subprocess import CalledProcessError
31
36
 
 
37
try:
 
38
    from charmhelpers.cli import cmdline
 
39
except ImportError as e:
 
40
    # due to the anti-pattern of partially synching charmhelpers directly
 
41
    # into charms, it's possible that charmhelpers.cli is not available;
 
42
    # if that's the case, they don't really care about using the cli anyway,
 
43
    # so mock it out
 
44
    if str(e) == 'No module named cli':
 
45
        class cmdline(object):
 
46
            @classmethod
 
47
            def subcommand(cls, *args, **kwargs):
 
48
                def _wrap(func):
 
49
                    return func
 
50
                return _wrap
 
51
    else:
 
52
        raise
 
53
 
32
54
import six
33
55
if not six.PY3:
34
56
    from UserDict import UserDict
58
80
 
59
81
    will cache the result of unit_get + 'test' for future calls.
60
82
    """
 
83
    @wraps(func)
61
84
    def wrapper(*args, **kwargs):
62
85
        global cache
63
86
        key = str((func, args, kwargs))
64
87
        try:
65
88
            return cache[key]
66
89
        except KeyError:
67
 
            res = func(*args, **kwargs)
68
 
            cache[key] = res
69
 
            return res
 
90
            pass  # Drop out of the exception handler scope.
 
91
        res = func(*args, **kwargs)
 
92
        cache[key] = res
 
93
        return res
70
94
    return wrapper
71
95
 
72
96
 
166
190
    return os.environ.get('JUJU_RELATION', None)
167
191
 
168
192
 
169
 
def relation_id():
170
 
    """The relation ID for the current relation hook"""
171
 
    return os.environ.get('JUJU_RELATION_ID', None)
 
193
@cmdline.subcommand()
 
194
@cached
 
195
def relation_id(relation_name=None, service_or_unit=None):
 
196
    """The relation ID for the current or a specified relation"""
 
197
    if not relation_name and not service_or_unit:
 
198
        return os.environ.get('JUJU_RELATION_ID', None)
 
199
    elif relation_name and service_or_unit:
 
200
        service_name = service_or_unit.split('/')[0]
 
201
        for relid in relation_ids(relation_name):
 
202
            remote_service = remote_service_name(relid)
 
203
            if remote_service == service_name:
 
204
                return relid
 
205
    else:
 
206
        raise ValueError('Must specify neither or both of relation_name and service_or_unit')
172
207
 
173
208
 
174
209
def local_unit():
178
213
 
179
214
def remote_unit():
180
215
    """The remote unit for the current relation hook"""
181
 
    return os.environ['JUJU_REMOTE_UNIT']
182
 
 
183
 
 
 
216
    return os.environ.get('JUJU_REMOTE_UNIT', None)
 
217
 
 
218
 
 
219
@cmdline.subcommand()
184
220
def service_name():
185
221
    """The name service group this unit belongs to"""
186
222
    return local_unit().split('/')[0]
187
223
 
188
224
 
 
225
@cmdline.subcommand()
 
226
@cached
 
227
def remote_service_name(relid=None):
 
228
    """The remote service name for a given relation-id (or the current relation)"""
 
229
    if relid is None:
 
230
        unit = remote_unit()
 
231
    else:
 
232
        units = related_units(relid)
 
233
        unit = units[0] if units else None
 
234
    return unit.split('/')[0] if unit else None
 
235
 
 
236
 
189
237
def hook_name():
190
238
    """The name of the currently executing hook"""
191
 
    return os.path.basename(sys.argv[0])
 
239
    return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
192
240
 
193
241
 
194
242
class Config(dict):
238
286
        self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
239
287
        if os.path.exists(self.path):
240
288
            self.load_previous()
241
 
 
242
 
    def __getitem__(self, key):
243
 
        """For regular dict lookups, check the current juju config first,
244
 
        then the previous (saved) copy. This ensures that user-saved values
245
 
        will be returned by a dict lookup.
246
 
 
247
 
        """
248
 
        try:
249
 
            return dict.__getitem__(self, key)
250
 
        except KeyError:
251
 
            return (self._prev_dict or {})[key]
252
 
 
253
 
    def keys(self):
254
 
        prev_keys = []
255
 
        if self._prev_dict is not None:
256
 
            prev_keys = self._prev_dict.keys()
257
 
        return list(set(prev_keys + list(dict.keys(self))))
 
289
        atexit(self._implicit_save)
258
290
 
259
291
    def load_previous(self, path=None):
260
292
        """Load previous copy of config from disk.
273
305
        self.path = path or self.path
274
306
        with open(self.path) as f:
275
307
            self._prev_dict = json.load(f)
 
308
        for k, v in copy.deepcopy(self._prev_dict).items():
 
309
            if k not in self:
 
310
                self[k] = v
276
311
 
277
312
    def changed(self, key):
278
313
        """Return True if the current value for this key is different from
304
339
        instance.
305
340
 
306
341
        """
307
 
        if self._prev_dict:
308
 
            for k, v in six.iteritems(self._prev_dict):
309
 
                if k not in self:
310
 
                    self[k] = v
311
342
        with open(self.path, 'w') as f:
312
343
            json.dump(self, f)
313
344
 
 
345
    def _implicit_save(self):
 
346
        if self.implicit_save:
 
347
            self.save()
 
348
 
314
349
 
315
350
@cached
316
351
def config(scope=None):
353
388
    """Set relation information for the current unit"""
354
389
    relation_settings = relation_settings if relation_settings else {}
355
390
    relation_cmd_line = ['relation-set']
 
391
    accepts_file = "--file" in subprocess.check_output(
 
392
        relation_cmd_line + ["--help"], universal_newlines=True)
356
393
    if relation_id is not None:
357
394
        relation_cmd_line.extend(('-r', relation_id))
358
 
    for k, v in (list(relation_settings.items()) + list(kwargs.items())):
359
 
        if v is None:
360
 
            relation_cmd_line.append('{}='.format(k))
361
 
        else:
362
 
            relation_cmd_line.append('{}={}'.format(k, v))
363
 
    subprocess.check_call(relation_cmd_line)
 
395
    settings = relation_settings.copy()
 
396
    settings.update(kwargs)
 
397
    for key, value in settings.items():
 
398
        # Force value to be a string: it always should, but some call
 
399
        # sites pass in things like dicts or numbers.
 
400
        if value is not None:
 
401
            settings[key] = "{}".format(value)
 
402
    if accepts_file:
 
403
        # --file was introduced in Juju 1.23.2. Use it by default if
 
404
        # available, since otherwise we'll break if the relation data is
 
405
        # too big. Ideally we should tell relation-set to read the data from
 
406
        # stdin, but that feature is broken in 1.23.2: Bug #1454678.
 
407
        with tempfile.NamedTemporaryFile(delete=False) as settings_file:
 
408
            settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
 
409
        subprocess.check_call(
 
410
            relation_cmd_line + ["--file", settings_file.name])
 
411
        os.remove(settings_file.name)
 
412
    else:
 
413
        for key, value in settings.items():
 
414
            if value is None:
 
415
                relation_cmd_line.append('{}='.format(key))
 
416
            else:
 
417
                relation_cmd_line.append('{}={}'.format(key, value))
 
418
        subprocess.check_call(relation_cmd_line)
364
419
    # Flush cache of any relation-gets for local unit
365
420
    flush(local_unit())
366
421
 
367
422
 
 
423
def relation_clear(r_id=None):
 
424
    ''' Clears any relation data already set on relation r_id '''
 
425
    settings = relation_get(rid=r_id,
 
426
                            unit=local_unit())
 
427
    for setting in settings:
 
428
        if setting not in ['public-address', 'private-address']:
 
429
            settings[setting] = None
 
430
    relation_set(relation_id=r_id,
 
431
                 **settings)
 
432
 
 
433
 
368
434
@cached
369
435
def relation_ids(reltype=None):
370
436
    """A list of relation_ids"""
444
510
 
445
511
 
446
512
@cached
 
513
def relation_to_interface(relation_name):
 
514
    """
 
515
    Given the name of a relation, return the interface that relation uses.
 
516
 
 
517
    :returns: The interface name, or ``None``.
 
518
    """
 
519
    return relation_to_role_and_interface(relation_name)[1]
 
520
 
 
521
 
 
522
@cached
 
523
def relation_to_role_and_interface(relation_name):
 
524
    """
 
525
    Given the name of a relation, return the role and the name of the interface
 
526
    that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
 
527
 
 
528
    :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
 
529
    """
 
530
    _metadata = metadata()
 
531
    for role in ('provides', 'requires', 'peer'):
 
532
        interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
 
533
        if interface:
 
534
            return role, interface
 
535
    return None, None
 
536
 
 
537
 
 
538
@cached
 
539
def role_and_interface_to_relations(role, interface_name):
 
540
    """
 
541
    Given a role and interface name, return a list of relation names for the
 
542
    current charm that use that interface under that role (where role is one
 
543
    of ``provides``, ``requires``, or ``peer``).
 
544
 
 
545
    :returns: A list of relation names.
 
546
    """
 
547
    _metadata = metadata()
 
548
    results = []
 
549
    for relation_name, relation in _metadata.get(role, {}).items():
 
550
        if relation['interface'] == interface_name:
 
551
            results.append(relation_name)
 
552
    return results
 
553
 
 
554
 
 
555
@cached
 
556
def interface_to_relations(interface_name):
 
557
    """
 
558
    Given an interface, return a list of relation names for the current
 
559
    charm that use that interface.
 
560
 
 
561
    :returns: A list of relation names.
 
562
    """
 
563
    results = []
 
564
    for role in ('provides', 'requires', 'peer'):
 
565
        results.extend(role_and_interface_to_relations(role, interface_name))
 
566
    return results
 
567
 
 
568
 
 
569
@cached
447
570
def charm_name():
448
571
    """Get the name of the current charm as is specified on metadata.yaml"""
449
572
    return metadata().get('name')
509
632
        return None
510
633
 
511
634
 
 
635
def unit_public_ip():
 
636
    """Get this unit's public IP address"""
 
637
    return unit_get('public-address')
 
638
 
 
639
 
512
640
def unit_private_ip():
513
641
    """Get this unit's private IP address"""
514
642
    return unit_get('private-address')
541
669
            hooks.execute(sys.argv)
542
670
    """
543
671
 
544
 
    def __init__(self, config_save=True):
 
672
    def __init__(self, config_save=None):
545
673
        super(Hooks, self).__init__()
546
674
        self._hooks = {}
547
 
        self._config_save = config_save
 
675
 
 
676
        # For unknown reasons, we allow the Hooks constructor to override
 
677
        # config().implicit_save.
 
678
        if config_save is not None:
 
679
            config().implicit_save = config_save
548
680
 
549
681
    def register(self, name, function):
550
682
        """Register a hook"""
552
684
 
553
685
    def execute(self, args):
554
686
        """Execute a registered hook based on args[0]"""
 
687
        _run_atstart()
555
688
        hook_name = os.path.basename(args[0])
556
689
        if hook_name in self._hooks:
557
 
            self._hooks[hook_name]()
558
 
            if self._config_save:
559
 
                cfg = config()
560
 
                if cfg.implicit_save:
561
 
                    cfg.save()
 
690
            try:
 
691
                self._hooks[hook_name]()
 
692
            except SystemExit as x:
 
693
                if x.code is None or x.code == 0:
 
694
                    _run_atexit()
 
695
                raise
 
696
            _run_atexit()
562
697
        else:
563
698
            raise UnregisteredHookError(hook_name)
564
699
 
605
740
 
606
741
    The results set by action_set are preserved."""
607
742
    subprocess.check_call(['action-fail', message])
 
743
 
 
744
 
 
745
def action_name():
 
746
    """Get the name of the currently executing action."""
 
747
    return os.environ.get('JUJU_ACTION_NAME')
 
748
 
 
749
 
 
750
def action_uuid():
 
751
    """Get the UUID of the currently executing action."""
 
752
    return os.environ.get('JUJU_ACTION_UUID')
 
753
 
 
754
 
 
755
def action_tag():
 
756
    """Get the tag for the currently executing action."""
 
757
    return os.environ.get('JUJU_ACTION_TAG')
 
758
 
 
759
 
 
760
def status_set(workload_state, message):
 
761
    """Set the workload state with a message
 
762
 
 
763
    Use status-set to set the workload state with a message which is visible
 
764
    to the user via juju status. If the status-set command is not found then
 
765
    assume this is juju < 1.23 and juju-log the message unstead.
 
766
 
 
767
    workload_state -- valid juju workload state.
 
768
    message        -- status update message
 
769
    """
 
770
    valid_states = ['maintenance', 'blocked', 'waiting', 'active']
 
771
    if workload_state not in valid_states:
 
772
        raise ValueError(
 
773
            '{!r} is not a valid workload state'.format(workload_state)
 
774
        )
 
775
    cmd = ['status-set', workload_state, message]
 
776
    try:
 
777
        ret = subprocess.call(cmd)
 
778
        if ret == 0:
 
779
            return
 
780
    except OSError as e:
 
781
        if e.errno != errno.ENOENT:
 
782
            raise
 
783
    log_message = 'status-set failed: {} {}'.format(workload_state,
 
784
                                                    message)
 
785
    log(log_message, level='INFO')
 
786
 
 
787
 
 
788
def status_get():
 
789
    """Retrieve the previously set juju workload state
 
790
 
 
791
    If the status-set command is not found then assume this is juju < 1.23 and
 
792
    return 'unknown'
 
793
    """
 
794
    cmd = ['status-get']
 
795
    try:
 
796
        raw_status = subprocess.check_output(cmd, universal_newlines=True)
 
797
        status = raw_status.rstrip()
 
798
        return status
 
799
    except OSError as e:
 
800
        if e.errno == errno.ENOENT:
 
801
            return 'unknown'
 
802
        else:
 
803
            raise
 
804
 
 
805
 
 
806
def translate_exc(from_exc, to_exc):
 
807
    def inner_translate_exc1(f):
 
808
        def inner_translate_exc2(*args, **kwargs):
 
809
            try:
 
810
                return f(*args, **kwargs)
 
811
            except from_exc:
 
812
                raise to_exc
 
813
 
 
814
        return inner_translate_exc2
 
815
 
 
816
    return inner_translate_exc1
 
817
 
 
818
 
 
819
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
820
def is_leader():
 
821
    """Does the current unit hold the juju leadership
 
822
 
 
823
    Uses juju to determine whether the current unit is the leader of its peers
 
824
    """
 
825
    cmd = ['is-leader', '--format=json']
 
826
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
827
 
 
828
 
 
829
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
830
def leader_get(attribute=None):
 
831
    """Juju leader get value(s)"""
 
832
    cmd = ['leader-get', '--format=json'] + [attribute or '-']
 
833
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
834
 
 
835
 
 
836
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
837
def leader_set(settings=None, **kwargs):
 
838
    """Juju leader set value(s)"""
 
839
    # Don't log secrets.
 
840
    # log("Juju leader-set '%s'" % (settings), level=DEBUG)
 
841
    cmd = ['leader-set']
 
842
    settings = settings or {}
 
843
    settings.update(kwargs)
 
844
    for k, v in settings.items():
 
845
        if v is None:
 
846
            cmd.append('{}='.format(k))
 
847
        else:
 
848
            cmd.append('{}={}'.format(k, v))
 
849
    subprocess.check_call(cmd)
 
850
 
 
851
 
 
852
@cached
 
853
def juju_version():
 
854
    """Full version string (eg. '1.23.3.1-trusty-amd64')"""
 
855
    # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
 
856
    jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
 
857
    return subprocess.check_output([jujud, 'version'],
 
858
                                   universal_newlines=True).strip()
 
859
 
 
860
 
 
861
@cached
 
862
def has_juju_version(minimum_version):
 
863
    """Return True if the Juju version is at least the provided version"""
 
864
    return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
 
865
 
 
866
 
 
867
_atexit = []
 
868
_atstart = []
 
869
 
 
870
 
 
871
def atstart(callback, *args, **kwargs):
 
872
    '''Schedule a callback to run before the main hook.
 
873
 
 
874
    Callbacks are run in the order they were added.
 
875
 
 
876
    This is useful for modules and classes to perform initialization
 
877
    and inject behavior. In particular:
 
878
 
 
879
        - Run common code before all of your hooks, such as logging
 
880
          the hook name or interesting relation data.
 
881
        - Defer object or module initialization that requires a hook
 
882
          context until we know there actually is a hook context,
 
883
          making testing easier.
 
884
        - Rather than requiring charm authors to include boilerplate to
 
885
          invoke your helper's behavior, have it run automatically if
 
886
          your object is instantiated or module imported.
 
887
 
 
888
    This is not at all useful after your hook framework as been launched.
 
889
    '''
 
890
    global _atstart
 
891
    _atstart.append((callback, args, kwargs))
 
892
 
 
893
 
 
894
def atexit(callback, *args, **kwargs):
 
895
    '''Schedule a callback to run on successful hook completion.
 
896
 
 
897
    Callbacks are run in the reverse order that they were added.'''
 
898
    _atexit.append((callback, args, kwargs))
 
899
 
 
900
 
 
901
def _run_atstart():
 
902
    '''Hook frameworks must invoke this before running the main hook body.'''
 
903
    global _atstart
 
904
    for callback, args, kwargs in _atstart:
 
905
        callback(*args, **kwargs)
 
906
    del _atstart[:]
 
907
 
 
908
 
 
909
def _run_atexit():
 
910
    '''Hook frameworks must invoke this after the main hook body has
 
911
    successfully completed. Do not invoke it if the hook fails.'''
 
912
    global _atexit
 
913
    for callback, args, kwargs in reversed(_atexit):
 
914
        callback(*args, **kwargs)
 
915
    del _atexit[:]