~openstack-charmers-next/charms/wily/glance-simplestreams-sync/trunk

« back to all changes in this revision

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

  • Committer: james.page at ubuntu
  • Date: 2015-09-08 16:26:21 UTC
  • Revision ID: james.page@ubuntu.com-20150908162621-25gebqljve63h45a
Resync helpers

Show diffs side-by-side

added added

removed removed

Lines of Context:
20
20
# Authors:
21
21
#  Charm Helpers Developers <juju@lists.ubuntu.com>
22
22
 
 
23
from __future__ import print_function
 
24
import copy
 
25
from distutils.version import LooseVersion
 
26
from functools import wraps
 
27
import glob
23
28
import os
24
29
import json
25
30
import yaml
26
31
import subprocess
27
32
import sys
 
33
import errno
 
34
import tempfile
28
35
from subprocess import CalledProcessError
29
36
 
30
37
import six
56
63
 
57
64
    will cache the result of unit_get + 'test' for future calls.
58
65
    """
 
66
    @wraps(func)
59
67
    def wrapper(*args, **kwargs):
60
68
        global cache
61
69
        key = str((func, args, kwargs))
62
70
        try:
63
71
            return cache[key]
64
72
        except KeyError:
65
 
            res = func(*args, **kwargs)
66
 
            cache[key] = res
67
 
            return res
 
73
            pass  # Drop out of the exception handler scope.
 
74
        res = func(*args, **kwargs)
 
75
        cache[key] = res
 
76
        return res
 
77
    wrapper._wrapped = func
68
78
    return wrapper
69
79
 
70
80
 
87
97
    if not isinstance(message, six.string_types):
88
98
        message = repr(message)
89
99
    command += [message]
90
 
    subprocess.call(command)
 
100
    # Missing juju-log should not cause failures in unit tests
 
101
    # Send log output to stderr
 
102
    try:
 
103
        subprocess.call(command)
 
104
    except OSError as e:
 
105
        if e.errno == errno.ENOENT:
 
106
            if level:
 
107
                message = "{}: {}".format(level, message)
 
108
            message = "juju-log: {}".format(message)
 
109
            print(message, file=sys.stderr)
 
110
        else:
 
111
            raise
91
112
 
92
113
 
93
114
class Serializable(UserDict):
153
174
    return os.environ.get('JUJU_RELATION', None)
154
175
 
155
176
 
156
 
def relation_id():
157
 
    """The relation ID for the current relation hook"""
158
 
    return os.environ.get('JUJU_RELATION_ID', None)
 
177
@cached
 
178
def relation_id(relation_name=None, service_or_unit=None):
 
179
    """The relation ID for the current or a specified relation"""
 
180
    if not relation_name and not service_or_unit:
 
181
        return os.environ.get('JUJU_RELATION_ID', None)
 
182
    elif relation_name and service_or_unit:
 
183
        service_name = service_or_unit.split('/')[0]
 
184
        for relid in relation_ids(relation_name):
 
185
            remote_service = remote_service_name(relid)
 
186
            if remote_service == service_name:
 
187
                return relid
 
188
    else:
 
189
        raise ValueError('Must specify neither or both of relation_name and service_or_unit')
159
190
 
160
191
 
161
192
def local_unit():
165
196
 
166
197
def remote_unit():
167
198
    """The remote unit for the current relation hook"""
168
 
    return os.environ['JUJU_REMOTE_UNIT']
 
199
    return os.environ.get('JUJU_REMOTE_UNIT', None)
169
200
 
170
201
 
171
202
def service_name():
173
204
    return local_unit().split('/')[0]
174
205
 
175
206
 
 
207
@cached
 
208
def remote_service_name(relid=None):
 
209
    """The remote service name for a given relation-id (or the current relation)"""
 
210
    if relid is None:
 
211
        unit = remote_unit()
 
212
    else:
 
213
        units = related_units(relid)
 
214
        unit = units[0] if units else None
 
215
    return unit.split('/')[0] if unit else None
 
216
 
 
217
 
176
218
def hook_name():
177
219
    """The name of the currently executing hook"""
178
 
    return os.path.basename(sys.argv[0])
 
220
    return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
179
221
 
180
222
 
181
223
class Config(dict):
225
267
        self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
226
268
        if os.path.exists(self.path):
227
269
            self.load_previous()
228
 
 
229
 
    def __getitem__(self, key):
230
 
        """For regular dict lookups, check the current juju config first,
231
 
        then the previous (saved) copy. This ensures that user-saved values
232
 
        will be returned by a dict lookup.
233
 
 
234
 
        """
235
 
        try:
236
 
            return dict.__getitem__(self, key)
237
 
        except KeyError:
238
 
            return (self._prev_dict or {})[key]
239
 
 
240
 
    def keys(self):
241
 
        prev_keys = []
242
 
        if self._prev_dict is not None:
243
 
            prev_keys = self._prev_dict.keys()
244
 
        return list(set(prev_keys + list(dict.keys(self))))
 
270
        atexit(self._implicit_save)
245
271
 
246
272
    def load_previous(self, path=None):
247
273
        """Load previous copy of config from disk.
260
286
        self.path = path or self.path
261
287
        with open(self.path) as f:
262
288
            self._prev_dict = json.load(f)
 
289
        for k, v in copy.deepcopy(self._prev_dict).items():
 
290
            if k not in self:
 
291
                self[k] = v
263
292
 
264
293
    def changed(self, key):
265
294
        """Return True if the current value for this key is different from
291
320
        instance.
292
321
 
293
322
        """
294
 
        if self._prev_dict:
295
 
            for k, v in six.iteritems(self._prev_dict):
296
 
                if k not in self:
297
 
                    self[k] = v
298
323
        with open(self.path, 'w') as f:
299
324
            json.dump(self, f)
300
325
 
 
326
    def _implicit_save(self):
 
327
        if self.implicit_save:
 
328
            self.save()
 
329
 
301
330
 
302
331
@cached
303
332
def config(scope=None):
340
369
    """Set relation information for the current unit"""
341
370
    relation_settings = relation_settings if relation_settings else {}
342
371
    relation_cmd_line = ['relation-set']
 
372
    accepts_file = "--file" in subprocess.check_output(
 
373
        relation_cmd_line + ["--help"], universal_newlines=True)
343
374
    if relation_id is not None:
344
375
        relation_cmd_line.extend(('-r', relation_id))
345
 
    for k, v in (list(relation_settings.items()) + list(kwargs.items())):
346
 
        if v is None:
347
 
            relation_cmd_line.append('{}='.format(k))
348
 
        else:
349
 
            relation_cmd_line.append('{}={}'.format(k, v))
350
 
    subprocess.check_call(relation_cmd_line)
 
376
    settings = relation_settings.copy()
 
377
    settings.update(kwargs)
 
378
    for key, value in settings.items():
 
379
        # Force value to be a string: it always should, but some call
 
380
        # sites pass in things like dicts or numbers.
 
381
        if value is not None:
 
382
            settings[key] = "{}".format(value)
 
383
    if accepts_file:
 
384
        # --file was introduced in Juju 1.23.2. Use it by default if
 
385
        # available, since otherwise we'll break if the relation data is
 
386
        # too big. Ideally we should tell relation-set to read the data from
 
387
        # stdin, but that feature is broken in 1.23.2: Bug #1454678.
 
388
        with tempfile.NamedTemporaryFile(delete=False) as settings_file:
 
389
            settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
 
390
        subprocess.check_call(
 
391
            relation_cmd_line + ["--file", settings_file.name])
 
392
        os.remove(settings_file.name)
 
393
    else:
 
394
        for key, value in settings.items():
 
395
            if value is None:
 
396
                relation_cmd_line.append('{}='.format(key))
 
397
            else:
 
398
                relation_cmd_line.append('{}={}'.format(key, value))
 
399
        subprocess.check_call(relation_cmd_line)
351
400
    # Flush cache of any relation-gets for local unit
352
401
    flush(local_unit())
353
402
 
354
403
 
 
404
def relation_clear(r_id=None):
 
405
    ''' Clears any relation data already set on relation r_id '''
 
406
    settings = relation_get(rid=r_id,
 
407
                            unit=local_unit())
 
408
    for setting in settings:
 
409
        if setting not in ['public-address', 'private-address']:
 
410
            settings[setting] = None
 
411
    relation_set(relation_id=r_id,
 
412
                 **settings)
 
413
 
 
414
 
355
415
@cached
356
416
def relation_ids(reltype=None):
357
417
    """A list of relation_ids"""
431
491
 
432
492
 
433
493
@cached
 
494
def relation_to_interface(relation_name):
 
495
    """
 
496
    Given the name of a relation, return the interface that relation uses.
 
497
 
 
498
    :returns: The interface name, or ``None``.
 
499
    """
 
500
    return relation_to_role_and_interface(relation_name)[1]
 
501
 
 
502
 
 
503
@cached
 
504
def relation_to_role_and_interface(relation_name):
 
505
    """
 
506
    Given the name of a relation, return the role and the name of the interface
 
507
    that relation uses (where role is one of ``provides``, ``requires``, or ``peer``).
 
508
 
 
509
    :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
 
510
    """
 
511
    _metadata = metadata()
 
512
    for role in ('provides', 'requires', 'peer'):
 
513
        interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
 
514
        if interface:
 
515
            return role, interface
 
516
    return None, None
 
517
 
 
518
 
 
519
@cached
 
520
def role_and_interface_to_relations(role, interface_name):
 
521
    """
 
522
    Given a role and interface name, return a list of relation names for the
 
523
    current charm that use that interface under that role (where role is one
 
524
    of ``provides``, ``requires``, or ``peer``).
 
525
 
 
526
    :returns: A list of relation names.
 
527
    """
 
528
    _metadata = metadata()
 
529
    results = []
 
530
    for relation_name, relation in _metadata.get(role, {}).items():
 
531
        if relation['interface'] == interface_name:
 
532
            results.append(relation_name)
 
533
    return results
 
534
 
 
535
 
 
536
@cached
 
537
def interface_to_relations(interface_name):
 
538
    """
 
539
    Given an interface, return a list of relation names for the current
 
540
    charm that use that interface.
 
541
 
 
542
    :returns: A list of relation names.
 
543
    """
 
544
    results = []
 
545
    for role in ('provides', 'requires', 'peer'):
 
546
        results.extend(role_and_interface_to_relations(role, interface_name))
 
547
    return results
 
548
 
 
549
 
 
550
@cached
434
551
def charm_name():
435
552
    """Get the name of the current charm as is specified on metadata.yaml"""
436
553
    return metadata().get('name')
496
613
        return None
497
614
 
498
615
 
 
616
def unit_public_ip():
 
617
    """Get this unit's public IP address"""
 
618
    return unit_get('public-address')
 
619
 
 
620
 
499
621
def unit_private_ip():
500
622
    """Get this unit's private IP address"""
501
623
    return unit_get('private-address')
528
650
            hooks.execute(sys.argv)
529
651
    """
530
652
 
531
 
    def __init__(self, config_save=True):
 
653
    def __init__(self, config_save=None):
532
654
        super(Hooks, self).__init__()
533
655
        self._hooks = {}
534
 
        self._config_save = config_save
 
656
 
 
657
        # For unknown reasons, we allow the Hooks constructor to override
 
658
        # config().implicit_save.
 
659
        if config_save is not None:
 
660
            config().implicit_save = config_save
535
661
 
536
662
    def register(self, name, function):
537
663
        """Register a hook"""
539
665
 
540
666
    def execute(self, args):
541
667
        """Execute a registered hook based on args[0]"""
 
668
        _run_atstart()
542
669
        hook_name = os.path.basename(args[0])
543
670
        if hook_name in self._hooks:
544
 
            self._hooks[hook_name]()
545
 
            if self._config_save:
546
 
                cfg = config()
547
 
                if cfg.implicit_save:
548
 
                    cfg.save()
 
671
            try:
 
672
                self._hooks[hook_name]()
 
673
            except SystemExit as x:
 
674
                if x.code is None or x.code == 0:
 
675
                    _run_atexit()
 
676
                raise
 
677
            _run_atexit()
549
678
        else:
550
679
            raise UnregisteredHookError(hook_name)
551
680
 
566
695
def charm_dir():
567
696
    """Return the root directory of the current charm"""
568
697
    return os.environ.get('CHARM_DIR')
 
698
 
 
699
 
 
700
@cached
 
701
def action_get(key=None):
 
702
    """Gets the value of an action parameter, or all key/value param pairs"""
 
703
    cmd = ['action-get']
 
704
    if key is not None:
 
705
        cmd.append(key)
 
706
    cmd.append('--format=json')
 
707
    action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
708
    return action_data
 
709
 
 
710
 
 
711
def action_set(values):
 
712
    """Sets the values to be returned after the action finishes"""
 
713
    cmd = ['action-set']
 
714
    for k, v in list(values.items()):
 
715
        cmd.append('{}={}'.format(k, v))
 
716
    subprocess.check_call(cmd)
 
717
 
 
718
 
 
719
def action_fail(message):
 
720
    """Sets the action status to failed and sets the error message.
 
721
 
 
722
    The results set by action_set are preserved."""
 
723
    subprocess.check_call(['action-fail', message])
 
724
 
 
725
 
 
726
def action_name():
 
727
    """Get the name of the currently executing action."""
 
728
    return os.environ.get('JUJU_ACTION_NAME')
 
729
 
 
730
 
 
731
def action_uuid():
 
732
    """Get the UUID of the currently executing action."""
 
733
    return os.environ.get('JUJU_ACTION_UUID')
 
734
 
 
735
 
 
736
def action_tag():
 
737
    """Get the tag for the currently executing action."""
 
738
    return os.environ.get('JUJU_ACTION_TAG')
 
739
 
 
740
 
 
741
def status_set(workload_state, message):
 
742
    """Set the workload state with a message
 
743
 
 
744
    Use status-set to set the workload state with a message which is visible
 
745
    to the user via juju status. If the status-set command is not found then
 
746
    assume this is juju < 1.23 and juju-log the message unstead.
 
747
 
 
748
    workload_state -- valid juju workload state.
 
749
    message        -- status update message
 
750
    """
 
751
    valid_states = ['maintenance', 'blocked', 'waiting', 'active']
 
752
    if workload_state not in valid_states:
 
753
        raise ValueError(
 
754
            '{!r} is not a valid workload state'.format(workload_state)
 
755
        )
 
756
    cmd = ['status-set', workload_state, message]
 
757
    try:
 
758
        ret = subprocess.call(cmd)
 
759
        if ret == 0:
 
760
            return
 
761
    except OSError as e:
 
762
        if e.errno != errno.ENOENT:
 
763
            raise
 
764
    log_message = 'status-set failed: {} {}'.format(workload_state,
 
765
                                                    message)
 
766
    log(log_message, level='INFO')
 
767
 
 
768
 
 
769
def status_get():
 
770
    """Retrieve the previously set juju workload state and message
 
771
 
 
772
    If the status-get command is not found then assume this is juju < 1.23 and
 
773
    return 'unknown', ""
 
774
 
 
775
    """
 
776
    cmd = ['status-get', "--format=json", "--include-data"]
 
777
    try:
 
778
        raw_status = subprocess.check_output(cmd)
 
779
    except OSError as e:
 
780
        if e.errno == errno.ENOENT:
 
781
            return ('unknown', "")
 
782
        else:
 
783
            raise
 
784
    else:
 
785
        status = json.loads(raw_status.decode("UTF-8"))
 
786
        return (status["status"], status["message"])
 
787
 
 
788
 
 
789
def translate_exc(from_exc, to_exc):
 
790
    def inner_translate_exc1(f):
 
791
        def inner_translate_exc2(*args, **kwargs):
 
792
            try:
 
793
                return f(*args, **kwargs)
 
794
            except from_exc:
 
795
                raise to_exc
 
796
 
 
797
        return inner_translate_exc2
 
798
 
 
799
    return inner_translate_exc1
 
800
 
 
801
 
 
802
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
803
def is_leader():
 
804
    """Does the current unit hold the juju leadership
 
805
 
 
806
    Uses juju to determine whether the current unit is the leader of its peers
 
807
    """
 
808
    cmd = ['is-leader', '--format=json']
 
809
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
810
 
 
811
 
 
812
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
813
def leader_get(attribute=None):
 
814
    """Juju leader get value(s)"""
 
815
    cmd = ['leader-get', '--format=json'] + [attribute or '-']
 
816
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
817
 
 
818
 
 
819
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
820
def leader_set(settings=None, **kwargs):
 
821
    """Juju leader set value(s)"""
 
822
    # Don't log secrets.
 
823
    # log("Juju leader-set '%s'" % (settings), level=DEBUG)
 
824
    cmd = ['leader-set']
 
825
    settings = settings or {}
 
826
    settings.update(kwargs)
 
827
    for k, v in settings.items():
 
828
        if v is None:
 
829
            cmd.append('{}='.format(k))
 
830
        else:
 
831
            cmd.append('{}={}'.format(k, v))
 
832
    subprocess.check_call(cmd)
 
833
 
 
834
 
 
835
@cached
 
836
def juju_version():
 
837
    """Full version string (eg. '1.23.3.1-trusty-amd64')"""
 
838
    # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
 
839
    jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
 
840
    return subprocess.check_output([jujud, 'version'],
 
841
                                   universal_newlines=True).strip()
 
842
 
 
843
 
 
844
@cached
 
845
def has_juju_version(minimum_version):
 
846
    """Return True if the Juju version is at least the provided version"""
 
847
    return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
 
848
 
 
849
 
 
850
_atexit = []
 
851
_atstart = []
 
852
 
 
853
 
 
854
def atstart(callback, *args, **kwargs):
 
855
    '''Schedule a callback to run before the main hook.
 
856
 
 
857
    Callbacks are run in the order they were added.
 
858
 
 
859
    This is useful for modules and classes to perform initialization
 
860
    and inject behavior. In particular:
 
861
 
 
862
        - Run common code before all of your hooks, such as logging
 
863
          the hook name or interesting relation data.
 
864
        - Defer object or module initialization that requires a hook
 
865
          context until we know there actually is a hook context,
 
866
          making testing easier.
 
867
        - Rather than requiring charm authors to include boilerplate to
 
868
          invoke your helper's behavior, have it run automatically if
 
869
          your object is instantiated or module imported.
 
870
 
 
871
    This is not at all useful after your hook framework as been launched.
 
872
    '''
 
873
    global _atstart
 
874
    _atstart.append((callback, args, kwargs))
 
875
 
 
876
 
 
877
def atexit(callback, *args, **kwargs):
 
878
    '''Schedule a callback to run on successful hook completion.
 
879
 
 
880
    Callbacks are run in the reverse order that they were added.'''
 
881
    _atexit.append((callback, args, kwargs))
 
882
 
 
883
 
 
884
def _run_atstart():
 
885
    '''Hook frameworks must invoke this before running the main hook body.'''
 
886
    global _atstart
 
887
    for callback, args, kwargs in _atstart:
 
888
        callback(*args, **kwargs)
 
889
    del _atstart[:]
 
890
 
 
891
 
 
892
def _run_atexit():
 
893
    '''Hook frameworks must invoke this after the main hook body has
 
894
    successfully completed. Do not invoke it if the hook fails.'''
 
895
    global _atexit
 
896
    for callback, args, kwargs in reversed(_atexit):
 
897
        callback(*args, **kwargs)
 
898
    del _atexit[:]