~niedbalski/charms/trusty/rabbitmq-server/fix-lp-1489053

« back to all changes in this revision

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

  • Committer: Liam Young
  • Date: 2015-09-09 13:12:47 UTC
  • mfrom: (110.1.4 rabbitmq-server)
  • Revision ID: liam.young@canonical.com-20150909131247-16hxw74o91c57kpg
[1chb1n, r=gnuoy] Refactor amulet tests, deprecate old tests

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright 2014-2015 Canonical Limited.
2
 
#
3
 
# This file is part of charm-helpers.
4
 
#
5
 
# charm-helpers is free software: you can redistribute it and/or modify
6
 
# it under the terms of the GNU Lesser General Public License version 3 as
7
 
# published by the Free Software Foundation.
8
 
#
9
 
# charm-helpers is distributed in the hope that it will be useful,
10
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
# GNU Lesser General Public License for more details.
13
 
#
14
 
# You should have received a copy of the GNU Lesser General Public License
15
 
# along with charm-helpers.  If not, see <http://www.gnu.org/licenses/>.
16
 
 
17
 
"Interactions with the Juju environment"
18
 
# Copyright 2013 Canonical Ltd.
19
 
#
20
 
# Authors:
21
 
#  Charm Helpers Developers <juju@lists.ubuntu.com>
22
 
 
23
 
from __future__ import print_function
24
 
import copy
25
 
from distutils.version import LooseVersion
26
 
from functools import wraps
27
 
import glob
28
 
import os
29
 
import json
30
 
import yaml
31
 
import subprocess
32
 
import sys
33
 
import errno
34
 
import tempfile
35
 
from subprocess import CalledProcessError
36
 
 
37
 
import six
38
 
if not six.PY3:
39
 
    from UserDict import UserDict
40
 
else:
41
 
    from collections import UserDict
42
 
 
43
 
CRITICAL = "CRITICAL"
44
 
ERROR = "ERROR"
45
 
WARNING = "WARNING"
46
 
INFO = "INFO"
47
 
DEBUG = "DEBUG"
48
 
MARKER = object()
49
 
 
50
 
cache = {}
51
 
 
52
 
 
53
 
def cached(func):
54
 
    """Cache return values for multiple executions of func + args
55
 
 
56
 
    For example::
57
 
 
58
 
        @cached
59
 
        def unit_get(attribute):
60
 
            pass
61
 
 
62
 
        unit_get('test')
63
 
 
64
 
    will cache the result of unit_get + 'test' for future calls.
65
 
    """
66
 
    @wraps(func)
67
 
    def wrapper(*args, **kwargs):
68
 
        global cache
69
 
        key = str((func, args, kwargs))
70
 
        try:
71
 
            return cache[key]
72
 
        except KeyError:
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
78
 
    return wrapper
79
 
 
80
 
 
81
 
def flush(key):
82
 
    """Flushes any entries from function cache where the
83
 
    key is found in the function+args """
84
 
    flush_list = []
85
 
    for item in cache:
86
 
        if key in item:
87
 
            flush_list.append(item)
88
 
    for item in flush_list:
89
 
        del cache[item]
90
 
 
91
 
 
92
 
def log(message, level=None):
93
 
    """Write a message to the juju log"""
94
 
    command = ['juju-log']
95
 
    if level:
96
 
        command += ['-l', level]
97
 
    if not isinstance(message, six.string_types):
98
 
        message = repr(message)
99
 
    command += [message]
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
112
 
 
113
 
 
114
 
class Serializable(UserDict):
115
 
    """Wrapper, an object that can be serialized to yaml or json"""
116
 
 
117
 
    def __init__(self, obj):
118
 
        # wrap the object
119
 
        UserDict.__init__(self)
120
 
        self.data = obj
121
 
 
122
 
    def __getattr__(self, attr):
123
 
        # See if this object has attribute.
124
 
        if attr in ("json", "yaml", "data"):
125
 
            return self.__dict__[attr]
126
 
        # Check for attribute in wrapped object.
127
 
        got = getattr(self.data, attr, MARKER)
128
 
        if got is not MARKER:
129
 
            return got
130
 
        # Proxy to the wrapped object via dict interface.
131
 
        try:
132
 
            return self.data[attr]
133
 
        except KeyError:
134
 
            raise AttributeError(attr)
135
 
 
136
 
    def __getstate__(self):
137
 
        # Pickle as a standard dictionary.
138
 
        return self.data
139
 
 
140
 
    def __setstate__(self, state):
141
 
        # Unpickle into our wrapper.
142
 
        self.data = state
143
 
 
144
 
    def json(self):
145
 
        """Serialize the object to json"""
146
 
        return json.dumps(self.data)
147
 
 
148
 
    def yaml(self):
149
 
        """Serialize the object to yaml"""
150
 
        return yaml.dump(self.data)
151
 
 
152
 
 
153
 
def execution_environment():
154
 
    """A convenient bundling of the current execution context"""
155
 
    context = {}
156
 
    context['conf'] = config()
157
 
    if relation_id():
158
 
        context['reltype'] = relation_type()
159
 
        context['relid'] = relation_id()
160
 
        context['rel'] = relation_get()
161
 
    context['unit'] = local_unit()
162
 
    context['rels'] = relations()
163
 
    context['env'] = os.environ
164
 
    return context
165
 
 
166
 
 
167
 
def in_relation_hook():
168
 
    """Determine whether we're running in a relation hook"""
169
 
    return 'JUJU_RELATION' in os.environ
170
 
 
171
 
 
172
 
def relation_type():
173
 
    """The scope for the current relation hook"""
174
 
    return os.environ.get('JUJU_RELATION', None)
175
 
 
176
 
 
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')
190
 
 
191
 
 
192
 
def local_unit():
193
 
    """Local unit ID"""
194
 
    return os.environ['JUJU_UNIT_NAME']
195
 
 
196
 
 
197
 
def remote_unit():
198
 
    """The remote unit for the current relation hook"""
199
 
    return os.environ.get('JUJU_REMOTE_UNIT', None)
200
 
 
201
 
 
202
 
def service_name():
203
 
    """The name service group this unit belongs to"""
204
 
    return local_unit().split('/')[0]
205
 
 
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
 
 
218
 
def hook_name():
219
 
    """The name of the currently executing hook"""
220
 
    return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
221
 
 
222
 
 
223
 
class Config(dict):
224
 
    """A dictionary representation of the charm's config.yaml, with some
225
 
    extra features:
226
 
 
227
 
    - See which values in the dictionary have changed since the previous hook.
228
 
    - For values that have changed, see what the previous value was.
229
 
    - Store arbitrary data for use in a later hook.
230
 
 
231
 
    NOTE: Do not instantiate this object directly - instead call
232
 
    ``hookenv.config()``, which will return an instance of :class:`Config`.
233
 
 
234
 
    Example usage::
235
 
 
236
 
        >>> # inside a hook
237
 
        >>> from charmhelpers.core import hookenv
238
 
        >>> config = hookenv.config()
239
 
        >>> config['foo']
240
 
        'bar'
241
 
        >>> # store a new key/value for later use
242
 
        >>> config['mykey'] = 'myval'
243
 
 
244
 
 
245
 
        >>> # user runs `juju set mycharm foo=baz`
246
 
        >>> # now we're inside subsequent config-changed hook
247
 
        >>> config = hookenv.config()
248
 
        >>> config['foo']
249
 
        'baz'
250
 
        >>> # test to see if this val has changed since last hook
251
 
        >>> config.changed('foo')
252
 
        True
253
 
        >>> # what was the previous value?
254
 
        >>> config.previous('foo')
255
 
        'bar'
256
 
        >>> # keys/values that we add are preserved across hooks
257
 
        >>> config['mykey']
258
 
        'myval'
259
 
 
260
 
    """
261
 
    CONFIG_FILE_NAME = '.juju-persistent-config'
262
 
 
263
 
    def __init__(self, *args, **kw):
264
 
        super(Config, self).__init__(*args, **kw)
265
 
        self.implicit_save = True
266
 
        self._prev_dict = None
267
 
        self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
268
 
        if os.path.exists(self.path):
269
 
            self.load_previous()
270
 
        atexit(self._implicit_save)
271
 
 
272
 
    def load_previous(self, path=None):
273
 
        """Load previous copy of config from disk.
274
 
 
275
 
        In normal usage you don't need to call this method directly - it
276
 
        is called automatically at object initialization.
277
 
 
278
 
        :param path:
279
 
 
280
 
            File path from which to load the previous config. If `None`,
281
 
            config is loaded from the default location. If `path` is
282
 
            specified, subsequent `save()` calls will write to the same
283
 
            path.
284
 
 
285
 
        """
286
 
        self.path = path or self.path
287
 
        with open(self.path) as f:
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
292
 
 
293
 
    def changed(self, key):
294
 
        """Return True if the current value for this key is different from
295
 
        the previous value.
296
 
 
297
 
        """
298
 
        if self._prev_dict is None:
299
 
            return True
300
 
        return self.previous(key) != self.get(key)
301
 
 
302
 
    def previous(self, key):
303
 
        """Return previous value for this key, or None if there
304
 
        is no previous value.
305
 
 
306
 
        """
307
 
        if self._prev_dict:
308
 
            return self._prev_dict.get(key)
309
 
        return None
310
 
 
311
 
    def save(self):
312
 
        """Save this config to disk.
313
 
 
314
 
        If the charm is using the :mod:`Services Framework <services.base>`
315
 
        or :meth:'@hook <Hooks.hook>' decorator, this
316
 
        is called automatically at the end of successful hook execution.
317
 
        Otherwise, it should be called directly by user code.
318
 
 
319
 
        To disable automatic saves, set ``implicit_save=False`` on this
320
 
        instance.
321
 
 
322
 
        """
323
 
        with open(self.path, 'w') as f:
324
 
            json.dump(self, f)
325
 
 
326
 
    def _implicit_save(self):
327
 
        if self.implicit_save:
328
 
            self.save()
329
 
 
330
 
 
331
 
@cached
332
 
def config(scope=None):
333
 
    """Juju charm configuration"""
334
 
    config_cmd_line = ['config-get']
335
 
    if scope is not None:
336
 
        config_cmd_line.append(scope)
337
 
    config_cmd_line.append('--format=json')
338
 
    try:
339
 
        config_data = json.loads(
340
 
            subprocess.check_output(config_cmd_line).decode('UTF-8'))
341
 
        if scope is not None:
342
 
            return config_data
343
 
        return Config(config_data)
344
 
    except ValueError:
345
 
        return None
346
 
 
347
 
 
348
 
@cached
349
 
def relation_get(attribute=None, unit=None, rid=None):
350
 
    """Get relation information"""
351
 
    _args = ['relation-get', '--format=json']
352
 
    if rid:
353
 
        _args.append('-r')
354
 
        _args.append(rid)
355
 
    _args.append(attribute or '-')
356
 
    if unit:
357
 
        _args.append(unit)
358
 
    try:
359
 
        return json.loads(subprocess.check_output(_args).decode('UTF-8'))
360
 
    except ValueError:
361
 
        return None
362
 
    except CalledProcessError as e:
363
 
        if e.returncode == 2:
364
 
            return None
365
 
        raise
366
 
 
367
 
 
368
 
def relation_set(relation_id=None, relation_settings=None, **kwargs):
369
 
    """Set relation information for the current unit"""
370
 
    relation_settings = relation_settings if relation_settings else {}
371
 
    relation_cmd_line = ['relation-set']
372
 
    accepts_file = "--file" in subprocess.check_output(
373
 
        relation_cmd_line + ["--help"], universal_newlines=True)
374
 
    if relation_id is not None:
375
 
        relation_cmd_line.extend(('-r', relation_id))
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)
400
 
    # Flush cache of any relation-gets for local unit
401
 
    flush(local_unit())
402
 
 
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
 
 
415
 
@cached
416
 
def relation_ids(reltype=None):
417
 
    """A list of relation_ids"""
418
 
    reltype = reltype or relation_type()
419
 
    relid_cmd_line = ['relation-ids', '--format=json']
420
 
    if reltype is not None:
421
 
        relid_cmd_line.append(reltype)
422
 
        return json.loads(
423
 
            subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
424
 
    return []
425
 
 
426
 
 
427
 
@cached
428
 
def related_units(relid=None):
429
 
    """A list of related units"""
430
 
    relid = relid or relation_id()
431
 
    units_cmd_line = ['relation-list', '--format=json']
432
 
    if relid is not None:
433
 
        units_cmd_line.extend(('-r', relid))
434
 
    return json.loads(
435
 
        subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
436
 
 
437
 
 
438
 
@cached
439
 
def relation_for_unit(unit=None, rid=None):
440
 
    """Get the json represenation of a unit's relation"""
441
 
    unit = unit or remote_unit()
442
 
    relation = relation_get(unit=unit, rid=rid)
443
 
    for key in relation:
444
 
        if key.endswith('-list'):
445
 
            relation[key] = relation[key].split()
446
 
    relation['__unit__'] = unit
447
 
    return relation
448
 
 
449
 
 
450
 
@cached
451
 
def relations_for_id(relid=None):
452
 
    """Get relations of a specific relation ID"""
453
 
    relation_data = []
454
 
    relid = relid or relation_ids()
455
 
    for unit in related_units(relid):
456
 
        unit_data = relation_for_unit(unit, relid)
457
 
        unit_data['__relid__'] = relid
458
 
        relation_data.append(unit_data)
459
 
    return relation_data
460
 
 
461
 
 
462
 
@cached
463
 
def relations_of_type(reltype=None):
464
 
    """Get relations of a specific type"""
465
 
    relation_data = []
466
 
    reltype = reltype or relation_type()
467
 
    for relid in relation_ids(reltype):
468
 
        for relation in relations_for_id(relid):
469
 
            relation['__relid__'] = relid
470
 
            relation_data.append(relation)
471
 
    return relation_data
472
 
 
473
 
 
474
 
@cached
475
 
def metadata():
476
 
    """Get the current charm metadata.yaml contents as a python object"""
477
 
    with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
478
 
        return yaml.safe_load(md)
479
 
 
480
 
 
481
 
@cached
482
 
def relation_types():
483
 
    """Get a list of relation types supported by this charm"""
484
 
    rel_types = []
485
 
    md = metadata()
486
 
    for key in ('provides', 'requires', 'peers'):
487
 
        section = md.get(key)
488
 
        if section:
489
 
            rel_types.extend(section.keys())
490
 
    return rel_types
491
 
 
492
 
 
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
551
 
def charm_name():
552
 
    """Get the name of the current charm as is specified on metadata.yaml"""
553
 
    return metadata().get('name')
554
 
 
555
 
 
556
 
@cached
557
 
def relations():
558
 
    """Get a nested dictionary of relation data for all related units"""
559
 
    rels = {}
560
 
    for reltype in relation_types():
561
 
        relids = {}
562
 
        for relid in relation_ids(reltype):
563
 
            units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
564
 
            for unit in related_units(relid):
565
 
                reldata = relation_get(unit=unit, rid=relid)
566
 
                units[unit] = reldata
567
 
            relids[relid] = units
568
 
        rels[reltype] = relids
569
 
    return rels
570
 
 
571
 
 
572
 
@cached
573
 
def is_relation_made(relation, keys='private-address'):
574
 
    '''
575
 
    Determine whether a relation is established by checking for
576
 
    presence of key(s).  If a list of keys is provided, they
577
 
    must all be present for the relation to be identified as made
578
 
    '''
579
 
    if isinstance(keys, str):
580
 
        keys = [keys]
581
 
    for r_id in relation_ids(relation):
582
 
        for unit in related_units(r_id):
583
 
            context = {}
584
 
            for k in keys:
585
 
                context[k] = relation_get(k, rid=r_id,
586
 
                                          unit=unit)
587
 
            if None not in context.values():
588
 
                return True
589
 
    return False
590
 
 
591
 
 
592
 
def open_port(port, protocol="TCP"):
593
 
    """Open a service network port"""
594
 
    _args = ['open-port']
595
 
    _args.append('{}/{}'.format(port, protocol))
596
 
    subprocess.check_call(_args)
597
 
 
598
 
 
599
 
def close_port(port, protocol="TCP"):
600
 
    """Close a service network port"""
601
 
    _args = ['close-port']
602
 
    _args.append('{}/{}'.format(port, protocol))
603
 
    subprocess.check_call(_args)
604
 
 
605
 
 
606
 
@cached
607
 
def unit_get(attribute):
608
 
    """Get the unit ID for the remote unit"""
609
 
    _args = ['unit-get', '--format=json', attribute]
610
 
    try:
611
 
        return json.loads(subprocess.check_output(_args).decode('UTF-8'))
612
 
    except ValueError:
613
 
        return None
614
 
 
615
 
 
616
 
def unit_public_ip():
617
 
    """Get this unit's public IP address"""
618
 
    return unit_get('public-address')
619
 
 
620
 
 
621
 
def unit_private_ip():
622
 
    """Get this unit's private IP address"""
623
 
    return unit_get('private-address')
624
 
 
625
 
 
626
 
class UnregisteredHookError(Exception):
627
 
    """Raised when an undefined hook is called"""
628
 
    pass
629
 
 
630
 
 
631
 
class Hooks(object):
632
 
    """A convenient handler for hook functions.
633
 
 
634
 
    Example::
635
 
 
636
 
        hooks = Hooks()
637
 
 
638
 
        # register a hook, taking its name from the function name
639
 
        @hooks.hook()
640
 
        def install():
641
 
            pass  # your code here
642
 
 
643
 
        # register a hook, providing a custom hook name
644
 
        @hooks.hook("config-changed")
645
 
        def config_changed():
646
 
            pass  # your code here
647
 
 
648
 
        if __name__ == "__main__":
649
 
            # execute a hook based on the name the program is called by
650
 
            hooks.execute(sys.argv)
651
 
    """
652
 
 
653
 
    def __init__(self, config_save=None):
654
 
        super(Hooks, self).__init__()
655
 
        self._hooks = {}
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
661
 
 
662
 
    def register(self, name, function):
663
 
        """Register a hook"""
664
 
        self._hooks[name] = function
665
 
 
666
 
    def execute(self, args):
667
 
        """Execute a registered hook based on args[0]"""
668
 
        _run_atstart()
669
 
        hook_name = os.path.basename(args[0])
670
 
        if hook_name in self._hooks:
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()
678
 
        else:
679
 
            raise UnregisteredHookError(hook_name)
680
 
 
681
 
    def hook(self, *hook_names):
682
 
        """Decorator, registering them as hooks"""
683
 
        def wrapper(decorated):
684
 
            for hook_name in hook_names:
685
 
                self.register(hook_name, decorated)
686
 
            else:
687
 
                self.register(decorated.__name__, decorated)
688
 
                if '_' in decorated.__name__:
689
 
                    self.register(
690
 
                        decorated.__name__.replace('_', '-'), decorated)
691
 
            return decorated
692
 
        return wrapper
693
 
 
694
 
 
695
 
def charm_dir():
696
 
    """Return the root directory of the current charm"""
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[:]