~lazypower/charms/trusty/nexentaedge-swift-gw/metadata-typo

« back to all changes in this revision

Viewing changes to charms/trusty/nedge-swift-gw/hooks/charmhelpers/core/hookenv.py

  • Committer: anton.skriptsov at nexenta
  • Date: 2015-11-11 22:13:28 UTC
  • Revision ID: anton.skriptsov@nexenta.com-20151111221328-78ddkdelwzq5e410
nedge-swift-gw initial

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
 
771
 
 
772
    If the status-set command is not found then assume this is juju < 1.23 and
 
773
    return 'unknown'
 
774
    """
 
775
    cmd = ['status-get']
 
776
    try:
 
777
        raw_status = subprocess.check_output(cmd, universal_newlines=True)
 
778
        status = raw_status.rstrip()
 
779
        return status
 
780
    except OSError as e:
 
781
        if e.errno == errno.ENOENT:
 
782
            return 'unknown'
 
783
        else:
 
784
            raise
 
785
 
 
786
 
 
787
def translate_exc(from_exc, to_exc):
 
788
    def inner_translate_exc1(f):
 
789
        def inner_translate_exc2(*args, **kwargs):
 
790
            try:
 
791
                return f(*args, **kwargs)
 
792
            except from_exc:
 
793
                raise to_exc
 
794
 
 
795
        return inner_translate_exc2
 
796
 
 
797
    return inner_translate_exc1
 
798
 
 
799
 
 
800
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
801
def is_leader():
 
802
    """Does the current unit hold the juju leadership
 
803
 
 
804
    Uses juju to determine whether the current unit is the leader of its peers
 
805
    """
 
806
    cmd = ['is-leader', '--format=json']
 
807
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
808
 
 
809
 
 
810
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
811
def leader_get(attribute=None):
 
812
    """Juju leader get value(s)"""
 
813
    cmd = ['leader-get', '--format=json'] + [attribute or '-']
 
814
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
815
 
 
816
 
 
817
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
818
def leader_set(settings=None, **kwargs):
 
819
    """Juju leader set value(s)"""
 
820
    # Don't log secrets.
 
821
    # log("Juju leader-set '%s'" % (settings), level=DEBUG)
 
822
    cmd = ['leader-set']
 
823
    settings = settings or {}
 
824
    settings.update(kwargs)
 
825
    for k, v in settings.items():
 
826
        if v is None:
 
827
            cmd.append('{}='.format(k))
 
828
        else:
 
829
            cmd.append('{}={}'.format(k, v))
 
830
    subprocess.check_call(cmd)
 
831
 
 
832
 
 
833
@cached
 
834
def juju_version():
 
835
    """Full version string (eg. '1.23.3.1-trusty-amd64')"""
 
836
    # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
 
837
    jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
 
838
    return subprocess.check_output([jujud, 'version'],
 
839
                                   universal_newlines=True).strip()
 
840
 
 
841
 
 
842
@cached
 
843
def has_juju_version(minimum_version):
 
844
    """Return True if the Juju version is at least the provided version"""
 
845
    return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
 
846
 
 
847
 
 
848
_atexit = []
 
849
_atstart = []
 
850
 
 
851
 
 
852
def atstart(callback, *args, **kwargs):
 
853
    '''Schedule a callback to run before the main hook.
 
854
 
 
855
    Callbacks are run in the order they were added.
 
856
 
 
857
    This is useful for modules and classes to perform initialization
 
858
    and inject behavior. In particular:
 
859
 
 
860
        - Run common code before all of your hooks, such as logging
 
861
          the hook name or interesting relation data.
 
862
        - Defer object or module initialization that requires a hook
 
863
          context until we know there actually is a hook context,
 
864
          making testing easier.
 
865
        - Rather than requiring charm authors to include boilerplate to
 
866
          invoke your helper's behavior, have it run automatically if
 
867
          your object is instantiated or module imported.
 
868
 
 
869
    This is not at all useful after your hook framework as been launched.
 
870
    '''
 
871
    global _atstart
 
872
    _atstart.append((callback, args, kwargs))
 
873
 
 
874
 
 
875
def atexit(callback, *args, **kwargs):
 
876
    '''Schedule a callback to run on successful hook completion.
 
877
 
 
878
    Callbacks are run in the reverse order that they were added.'''
 
879
    _atexit.append((callback, args, kwargs))
 
880
 
 
881
 
 
882
def _run_atstart():
 
883
    '''Hook frameworks must invoke this before running the main hook body.'''
 
884
    global _atstart
 
885
    for callback, args, kwargs in _atstart:
 
886
        callback(*args, **kwargs)
 
887
    del _atstart[:]
 
888
 
 
889
 
 
890
def _run_atexit():
 
891
    '''Hook frameworks must invoke this after the main hook body has
 
892
    successfully completed. Do not invoke it if the hook fails.'''
 
893
    global _atexit
 
894
    for callback, args, kwargs in reversed(_atexit):
 
895
        callback(*args, **kwargs)
 
896
    del _atexit[:]