~josvaz/charms/trusty/bip/client_side_ssl-with_helper-lp1604894

« back to all changes in this revision

Viewing changes to lib/charm-helpers/charmhelpers/core/hookenv.py

  • Committer: Jose Vazquez
  • Date: 2016-07-29 12:54:32 UTC
  • Revision ID: jose.vazquez@canonical.com-20160729125432-7gmm2tpyicju5jl4
Result of removing charmhelpers and re-syncing it

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# Licensed under the Apache License, Version 2.0 (the "License");
 
4
# you may not use this file except in compliance with the License.
 
5
# You may obtain a copy of the License at
 
6
#
 
7
#  http://www.apache.org/licenses/LICENSE-2.0
 
8
#
 
9
# Unless required by applicable law or agreed to in writing, software
 
10
# distributed under the License is distributed on an "AS IS" BASIS,
 
11
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 
12
# See the License for the specific language governing permissions and
 
13
# limitations under the License.
 
14
 
1
15
"Interactions with the Juju environment"
2
16
# Copyright 2013 Canonical Ltd.
3
17
#
4
18
# Authors:
5
19
#  Charm Helpers Developers <juju@lists.ubuntu.com>
6
20
 
 
21
from __future__ import print_function
 
22
import copy
 
23
from distutils.version import LooseVersion
 
24
from functools import wraps
 
25
import glob
7
26
import os
8
27
import json
9
28
import yaml
10
29
import subprocess
11
 
import UserDict
 
30
import sys
 
31
import errno
 
32
import tempfile
 
33
from subprocess import CalledProcessError
 
34
 
 
35
import six
 
36
if not six.PY3:
 
37
    from UserDict import UserDict
 
38
else:
 
39
    from collections import UserDict
12
40
 
13
41
CRITICAL = "CRITICAL"
14
42
ERROR = "ERROR"
21
49
 
22
50
 
23
51
def cached(func):
24
 
    ''' Cache return values for multiple executions of func + args
 
52
    """Cache return values for multiple executions of func + args
25
53
 
26
 
    For example:
 
54
    For example::
27
55
 
28
56
        @cached
29
57
        def unit_get(attribute):
32
60
        unit_get('test')
33
61
 
34
62
    will cache the result of unit_get + 'test' for future calls.
35
 
    '''
 
63
    """
 
64
    @wraps(func)
36
65
    def wrapper(*args, **kwargs):
37
66
        global cache
38
67
        key = str((func, args, kwargs))
39
68
        try:
40
69
            return cache[key]
41
70
        except KeyError:
42
 
            res = func(*args, **kwargs)
43
 
            cache[key] = res
44
 
            return res
 
71
            pass  # Drop out of the exception handler scope.
 
72
        res = func(*args, **kwargs)
 
73
        cache[key] = res
 
74
        return res
 
75
    wrapper._wrapped = func
45
76
    return wrapper
46
77
 
47
78
 
48
79
def flush(key):
49
 
    ''' Flushes any entries from function cache where the
50
 
    key is found in the function+args '''
 
80
    """Flushes any entries from function cache where the
 
81
    key is found in the function+args """
51
82
    flush_list = []
52
83
    for item in cache:
53
84
        if key in item:
57
88
 
58
89
 
59
90
def log(message, level=None):
60
 
    "Write a message to the juju log"
 
91
    """Write a message to the juju log"""
61
92
    command = ['juju-log']
62
93
    if level:
63
94
        command += ['-l', level]
 
95
    if not isinstance(message, six.string_types):
 
96
        message = repr(message)
64
97
    command += [message]
65
 
    subprocess.call(command)
66
 
 
67
 
 
68
 
class Serializable(UserDict.IterableUserDict):
69
 
    "Wrapper, an object that can be serialized to yaml or json"
 
98
    # Missing juju-log should not cause failures in unit tests
 
99
    # Send log output to stderr
 
100
    try:
 
101
        subprocess.call(command)
 
102
    except OSError as e:
 
103
        if e.errno == errno.ENOENT:
 
104
            if level:
 
105
                message = "{}: {}".format(level, message)
 
106
            message = "juju-log: {}".format(message)
 
107
            print(message, file=sys.stderr)
 
108
        else:
 
109
            raise
 
110
 
 
111
 
 
112
class Serializable(UserDict):
 
113
    """Wrapper, an object that can be serialized to yaml or json"""
70
114
 
71
115
    def __init__(self, obj):
72
116
        # wrap the object
73
 
        UserDict.IterableUserDict.__init__(self)
 
117
        UserDict.__init__(self)
74
118
        self.data = obj
75
119
 
76
120
    def __getattr__(self, attr):
96
140
        self.data = state
97
141
 
98
142
    def json(self):
99
 
        "Serialize the object to json"
 
143
        """Serialize the object to json"""
100
144
        return json.dumps(self.data)
101
145
 
102
146
    def yaml(self):
103
 
        "Serialize the object to yaml"
 
147
        """Serialize the object to yaml"""
104
148
        return yaml.dump(self.data)
105
149
 
106
150
 
119
163
 
120
164
 
121
165
def in_relation_hook():
122
 
    "Determine whether we're running in a relation hook"
 
166
    """Determine whether we're running in a relation hook"""
123
167
    return 'JUJU_RELATION' in os.environ
124
168
 
125
169
 
126
170
def relation_type():
127
 
    "The scope for the current relation hook"
 
171
    """The scope for the current relation hook"""
128
172
    return os.environ.get('JUJU_RELATION', None)
129
173
 
130
174
 
131
 
def relation_id():
132
 
    "The relation ID for the current relation hook"
133
 
    return os.environ.get('JUJU_RELATION_ID', None)
 
175
@cached
 
176
def relation_id(relation_name=None, service_or_unit=None):
 
177
    """The relation ID for the current or a specified relation"""
 
178
    if not relation_name and not service_or_unit:
 
179
        return os.environ.get('JUJU_RELATION_ID', None)
 
180
    elif relation_name and service_or_unit:
 
181
        service_name = service_or_unit.split('/')[0]
 
182
        for relid in relation_ids(relation_name):
 
183
            remote_service = remote_service_name(relid)
 
184
            if remote_service == service_name:
 
185
                return relid
 
186
    else:
 
187
        raise ValueError('Must specify neither or both of relation_name and service_or_unit')
134
188
 
135
189
 
136
190
def local_unit():
137
 
    "Local unit ID"
 
191
    """Local unit ID"""
138
192
    return os.environ['JUJU_UNIT_NAME']
139
193
 
140
194
 
141
195
def remote_unit():
142
 
    "The remote unit for the current relation hook"
143
 
    return os.environ['JUJU_REMOTE_UNIT']
 
196
    """The remote unit for the current relation hook"""
 
197
    return os.environ.get('JUJU_REMOTE_UNIT', None)
144
198
 
145
199
 
146
200
def service_name():
147
 
    "The name service group this unit belongs to"
 
201
    """The name service group this unit belongs to"""
148
202
    return local_unit().split('/')[0]
149
203
 
150
204
 
151
205
@cached
 
206
def remote_service_name(relid=None):
 
207
    """The remote service name for a given relation-id (or the current relation)"""
 
208
    if relid is None:
 
209
        unit = remote_unit()
 
210
    else:
 
211
        units = related_units(relid)
 
212
        unit = units[0] if units else None
 
213
    return unit.split('/')[0] if unit else None
 
214
 
 
215
 
 
216
def hook_name():
 
217
    """The name of the currently executing hook"""
 
218
    return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
 
219
 
 
220
 
 
221
class Config(dict):
 
222
    """A dictionary representation of the charm's config.yaml, with some
 
223
    extra features:
 
224
 
 
225
    - See which values in the dictionary have changed since the previous hook.
 
226
    - For values that have changed, see what the previous value was.
 
227
    - Store arbitrary data for use in a later hook.
 
228
 
 
229
    NOTE: Do not instantiate this object directly - instead call
 
230
    ``hookenv.config()``, which will return an instance of :class:`Config`.
 
231
 
 
232
    Example usage::
 
233
 
 
234
        >>> # inside a hook
 
235
        >>> from charmhelpers.core import hookenv
 
236
        >>> config = hookenv.config()
 
237
        >>> config['foo']
 
238
        'bar'
 
239
        >>> # store a new key/value for later use
 
240
        >>> config['mykey'] = 'myval'
 
241
 
 
242
 
 
243
        >>> # user runs `juju set mycharm foo=baz`
 
244
        >>> # now we're inside subsequent config-changed hook
 
245
        >>> config = hookenv.config()
 
246
        >>> config['foo']
 
247
        'baz'
 
248
        >>> # test to see if this val has changed since last hook
 
249
        >>> config.changed('foo')
 
250
        True
 
251
        >>> # what was the previous value?
 
252
        >>> config.previous('foo')
 
253
        'bar'
 
254
        >>> # keys/values that we add are preserved across hooks
 
255
        >>> config['mykey']
 
256
        'myval'
 
257
 
 
258
    """
 
259
    CONFIG_FILE_NAME = '.juju-persistent-config'
 
260
 
 
261
    def __init__(self, *args, **kw):
 
262
        super(Config, self).__init__(*args, **kw)
 
263
        self.implicit_save = True
 
264
        self._prev_dict = None
 
265
        self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
 
266
        if os.path.exists(self.path):
 
267
            self.load_previous()
 
268
        atexit(self._implicit_save)
 
269
 
 
270
    def load_previous(self, path=None):
 
271
        """Load previous copy of config from disk.
 
272
 
 
273
        In normal usage you don't need to call this method directly - it
 
274
        is called automatically at object initialization.
 
275
 
 
276
        :param path:
 
277
 
 
278
            File path from which to load the previous config. If `None`,
 
279
            config is loaded from the default location. If `path` is
 
280
            specified, subsequent `save()` calls will write to the same
 
281
            path.
 
282
 
 
283
        """
 
284
        self.path = path or self.path
 
285
        with open(self.path) as f:
 
286
            self._prev_dict = json.load(f)
 
287
        for k, v in copy.deepcopy(self._prev_dict).items():
 
288
            if k not in self:
 
289
                self[k] = v
 
290
 
 
291
    def changed(self, key):
 
292
        """Return True if the current value for this key is different from
 
293
        the previous value.
 
294
 
 
295
        """
 
296
        if self._prev_dict is None:
 
297
            return True
 
298
        return self.previous(key) != self.get(key)
 
299
 
 
300
    def previous(self, key):
 
301
        """Return previous value for this key, or None if there
 
302
        is no previous value.
 
303
 
 
304
        """
 
305
        if self._prev_dict:
 
306
            return self._prev_dict.get(key)
 
307
        return None
 
308
 
 
309
    def save(self):
 
310
        """Save this config to disk.
 
311
 
 
312
        If the charm is using the :mod:`Services Framework <services.base>`
 
313
        or :meth:'@hook <Hooks.hook>' decorator, this
 
314
        is called automatically at the end of successful hook execution.
 
315
        Otherwise, it should be called directly by user code.
 
316
 
 
317
        To disable automatic saves, set ``implicit_save=False`` on this
 
318
        instance.
 
319
 
 
320
        """
 
321
        with open(self.path, 'w') as f:
 
322
            json.dump(self, f)
 
323
 
 
324
    def _implicit_save(self):
 
325
        if self.implicit_save:
 
326
            self.save()
 
327
 
 
328
 
 
329
@cached
152
330
def config(scope=None):
153
 
    "Juju charm configuration"
 
331
    """Juju charm configuration"""
154
332
    config_cmd_line = ['config-get']
155
333
    if scope is not None:
156
334
        config_cmd_line.append(scope)
157
335
    config_cmd_line.append('--format=json')
158
336
    try:
159
 
        return json.loads(subprocess.check_output(config_cmd_line))
 
337
        config_data = json.loads(
 
338
            subprocess.check_output(config_cmd_line).decode('UTF-8'))
 
339
        if scope is not None:
 
340
            return config_data
 
341
        return Config(config_data)
160
342
    except ValueError:
161
343
        return None
162
344
 
163
345
 
164
346
@cached
165
347
def relation_get(attribute=None, unit=None, rid=None):
 
348
    """Get relation information"""
166
349
    _args = ['relation-get', '--format=json']
167
350
    if rid:
168
351
        _args.append('-r')
171
354
    if unit:
172
355
        _args.append(unit)
173
356
    try:
174
 
        return json.loads(subprocess.check_output(_args))
 
357
        return json.loads(subprocess.check_output(_args).decode('UTF-8'))
175
358
    except ValueError:
176
359
        return None
177
 
 
178
 
 
179
 
def relation_set(relation_id=None, relation_settings={}, **kwargs):
 
360
    except CalledProcessError as e:
 
361
        if e.returncode == 2:
 
362
            return None
 
363
        raise
 
364
 
 
365
 
 
366
def relation_set(relation_id=None, relation_settings=None, **kwargs):
 
367
    """Set relation information for the current unit"""
 
368
    relation_settings = relation_settings if relation_settings else {}
180
369
    relation_cmd_line = ['relation-set']
 
370
    accepts_file = "--file" in subprocess.check_output(
 
371
        relation_cmd_line + ["--help"], universal_newlines=True)
181
372
    if relation_id is not None:
182
373
        relation_cmd_line.extend(('-r', relation_id))
183
 
    for k, v in (relation_settings.items() + kwargs.items()):
184
 
        if v is None:
185
 
            relation_cmd_line.append('{}='.format(k))
186
 
        else:
187
 
            relation_cmd_line.append('{}={}'.format(k, v))
188
 
    subprocess.check_call(relation_cmd_line)
 
374
    settings = relation_settings.copy()
 
375
    settings.update(kwargs)
 
376
    for key, value in settings.items():
 
377
        # Force value to be a string: it always should, but some call
 
378
        # sites pass in things like dicts or numbers.
 
379
        if value is not None:
 
380
            settings[key] = "{}".format(value)
 
381
    if accepts_file:
 
382
        # --file was introduced in Juju 1.23.2. Use it by default if
 
383
        # available, since otherwise we'll break if the relation data is
 
384
        # too big. Ideally we should tell relation-set to read the data from
 
385
        # stdin, but that feature is broken in 1.23.2: Bug #1454678.
 
386
        with tempfile.NamedTemporaryFile(delete=False) as settings_file:
 
387
            settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
 
388
        subprocess.check_call(
 
389
            relation_cmd_line + ["--file", settings_file.name])
 
390
        os.remove(settings_file.name)
 
391
    else:
 
392
        for key, value in settings.items():
 
393
            if value is None:
 
394
                relation_cmd_line.append('{}='.format(key))
 
395
            else:
 
396
                relation_cmd_line.append('{}={}'.format(key, value))
 
397
        subprocess.check_call(relation_cmd_line)
189
398
    # Flush cache of any relation-gets for local unit
190
399
    flush(local_unit())
191
400
 
192
401
 
 
402
def relation_clear(r_id=None):
 
403
    ''' Clears any relation data already set on relation r_id '''
 
404
    settings = relation_get(rid=r_id,
 
405
                            unit=local_unit())
 
406
    for setting in settings:
 
407
        if setting not in ['public-address', 'private-address']:
 
408
            settings[setting] = None
 
409
    relation_set(relation_id=r_id,
 
410
                 **settings)
 
411
 
 
412
 
193
413
@cached
194
414
def relation_ids(reltype=None):
195
 
    "A list of relation_ids"
 
415
    """A list of relation_ids"""
196
416
    reltype = reltype or relation_type()
197
417
    relid_cmd_line = ['relation-ids', '--format=json']
198
418
    if reltype is not None:
199
419
        relid_cmd_line.append(reltype)
200
 
        return json.loads(subprocess.check_output(relid_cmd_line)) or []
 
420
        return json.loads(
 
421
            subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
201
422
    return []
202
423
 
203
424
 
204
425
@cached
205
426
def related_units(relid=None):
206
 
    "A list of related units"
 
427
    """A list of related units"""
207
428
    relid = relid or relation_id()
208
429
    units_cmd_line = ['relation-list', '--format=json']
209
430
    if relid is not None:
210
431
        units_cmd_line.extend(('-r', relid))
211
 
    return json.loads(subprocess.check_output(units_cmd_line)) or []
 
432
    return json.loads(
 
433
        subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
212
434
 
213
435
 
214
436
@cached
215
437
def relation_for_unit(unit=None, rid=None):
216
 
    "Get the json represenation of a unit's relation"
 
438
    """Get the json represenation of a unit's relation"""
217
439
    unit = unit or remote_unit()
218
440
    relation = relation_get(unit=unit, rid=rid)
219
441
    for key in relation:
225
447
 
226
448
@cached
227
449
def relations_for_id(relid=None):
228
 
    "Get relations of a specific relation ID"
 
450
    """Get relations of a specific relation ID"""
229
451
    relation_data = []
230
452
    relid = relid or relation_ids()
231
453
    for unit in related_units(relid):
237
459
 
238
460
@cached
239
461
def relations_of_type(reltype=None):
240
 
    "Get relations of a specific type"
 
462
    """Get relations of a specific type"""
241
463
    relation_data = []
242
464
    reltype = reltype or relation_type()
243
465
    for relid in relation_ids(reltype):
248
470
 
249
471
 
250
472
@cached
 
473
def metadata():
 
474
    """Get the current charm metadata.yaml contents as a python object"""
 
475
    with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
 
476
        return yaml.safe_load(md)
 
477
 
 
478
 
 
479
@cached
251
480
def relation_types():
252
 
    "Get a list of relation types supported by this charm"
253
 
    charmdir = os.environ.get('CHARM_DIR', '')
254
 
    mdf = open(os.path.join(charmdir, 'metadata.yaml'))
255
 
    md = yaml.safe_load(mdf)
 
481
    """Get a list of relation types supported by this charm"""
256
482
    rel_types = []
 
483
    md = metadata()
257
484
    for key in ('provides', 'requires', 'peers'):
258
485
        section = md.get(key)
259
486
        if section:
260
487
            rel_types.extend(section.keys())
261
 
    mdf.close()
262
488
    return rel_types
263
489
 
264
490
 
265
491
@cached
 
492
def peer_relation_id():
 
493
    '''Get the peers relation id if a peers relation has been joined, else None.'''
 
494
    md = metadata()
 
495
    section = md.get('peers')
 
496
    if section:
 
497
        for key in section:
 
498
            relids = relation_ids(key)
 
499
            if relids:
 
500
                return relids[0]
 
501
    return None
 
502
 
 
503
 
 
504
@cached
 
505
def relation_to_interface(relation_name):
 
506
    """
 
507
    Given the name of a relation, return the interface that relation uses.
 
508
 
 
509
    :returns: The interface name, or ``None``.
 
510
    """
 
511
    return relation_to_role_and_interface(relation_name)[1]
 
512
 
 
513
 
 
514
@cached
 
515
def relation_to_role_and_interface(relation_name):
 
516
    """
 
517
    Given the name of a relation, return the role and the name of the interface
 
518
    that relation uses (where role is one of ``provides``, ``requires``, or ``peers``).
 
519
 
 
520
    :returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
 
521
    """
 
522
    _metadata = metadata()
 
523
    for role in ('provides', 'requires', 'peers'):
 
524
        interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
 
525
        if interface:
 
526
            return role, interface
 
527
    return None, None
 
528
 
 
529
 
 
530
@cached
 
531
def role_and_interface_to_relations(role, interface_name):
 
532
    """
 
533
    Given a role and interface name, return a list of relation names for the
 
534
    current charm that use that interface under that role (where role is one
 
535
    of ``provides``, ``requires``, or ``peers``).
 
536
 
 
537
    :returns: A list of relation names.
 
538
    """
 
539
    _metadata = metadata()
 
540
    results = []
 
541
    for relation_name, relation in _metadata.get(role, {}).items():
 
542
        if relation['interface'] == interface_name:
 
543
            results.append(relation_name)
 
544
    return results
 
545
 
 
546
 
 
547
@cached
 
548
def interface_to_relations(interface_name):
 
549
    """
 
550
    Given an interface, return a list of relation names for the current
 
551
    charm that use that interface.
 
552
 
 
553
    :returns: A list of relation names.
 
554
    """
 
555
    results = []
 
556
    for role in ('provides', 'requires', 'peers'):
 
557
        results.extend(role_and_interface_to_relations(role, interface_name))
 
558
    return results
 
559
 
 
560
 
 
561
@cached
 
562
def charm_name():
 
563
    """Get the name of the current charm as is specified on metadata.yaml"""
 
564
    return metadata().get('name')
 
565
 
 
566
 
 
567
@cached
266
568
def relations():
 
569
    """Get a nested dictionary of relation data for all related units"""
267
570
    rels = {}
268
571
    for reltype in relation_types():
269
572
        relids = {}
277
580
    return rels
278
581
 
279
582
 
 
583
@cached
 
584
def is_relation_made(relation, keys='private-address'):
 
585
    '''
 
586
    Determine whether a relation is established by checking for
 
587
    presence of key(s).  If a list of keys is provided, they
 
588
    must all be present for the relation to be identified as made
 
589
    '''
 
590
    if isinstance(keys, str):
 
591
        keys = [keys]
 
592
    for r_id in relation_ids(relation):
 
593
        for unit in related_units(r_id):
 
594
            context = {}
 
595
            for k in keys:
 
596
                context[k] = relation_get(k, rid=r_id,
 
597
                                          unit=unit)
 
598
            if None not in context.values():
 
599
                return True
 
600
    return False
 
601
 
 
602
 
280
603
def open_port(port, protocol="TCP"):
281
 
    "Open a service network port"
 
604
    """Open a service network port"""
282
605
    _args = ['open-port']
283
606
    _args.append('{}/{}'.format(port, protocol))
284
607
    subprocess.check_call(_args)
285
608
 
286
609
 
287
610
def close_port(port, protocol="TCP"):
288
 
    "Close a service network port"
 
611
    """Close a service network port"""
289
612
    _args = ['close-port']
290
613
    _args.append('{}/{}'.format(port, protocol))
291
614
    subprocess.check_call(_args)
293
616
 
294
617
@cached
295
618
def unit_get(attribute):
 
619
    """Get the unit ID for the remote unit"""
296
620
    _args = ['unit-get', '--format=json', attribute]
297
621
    try:
298
 
        return json.loads(subprocess.check_output(_args))
 
622
        return json.loads(subprocess.check_output(_args).decode('UTF-8'))
299
623
    except ValueError:
300
624
        return None
301
625
 
302
626
 
 
627
def unit_public_ip():
 
628
    """Get this unit's public IP address"""
 
629
    return unit_get('public-address')
 
630
 
 
631
 
303
632
def unit_private_ip():
 
633
    """Get this unit's private IP address"""
304
634
    return unit_get('private-address')
305
635
 
306
636
 
 
637
@cached
 
638
def storage_get(attribute=None, storage_id=None):
 
639
    """Get storage attributes"""
 
640
    _args = ['storage-get', '--format=json']
 
641
    if storage_id:
 
642
        _args.extend(('-s', storage_id))
 
643
    if attribute:
 
644
        _args.append(attribute)
 
645
    try:
 
646
        return json.loads(subprocess.check_output(_args).decode('UTF-8'))
 
647
    except ValueError:
 
648
        return None
 
649
 
 
650
 
 
651
@cached
 
652
def storage_list(storage_name=None):
 
653
    """List the storage IDs for the unit"""
 
654
    _args = ['storage-list', '--format=json']
 
655
    if storage_name:
 
656
        _args.append(storage_name)
 
657
    try:
 
658
        return json.loads(subprocess.check_output(_args).decode('UTF-8'))
 
659
    except ValueError:
 
660
        return None
 
661
    except OSError as e:
 
662
        import errno
 
663
        if e.errno == errno.ENOENT:
 
664
            # storage-list does not exist
 
665
            return []
 
666
        raise
 
667
 
 
668
 
307
669
class UnregisteredHookError(Exception):
 
670
    """Raised when an undefined hook is called"""
308
671
    pass
309
672
 
310
673
 
311
674
class Hooks(object):
312
 
    def __init__(self):
 
675
    """A convenient handler for hook functions.
 
676
 
 
677
    Example::
 
678
 
 
679
        hooks = Hooks()
 
680
 
 
681
        # register a hook, taking its name from the function name
 
682
        @hooks.hook()
 
683
        def install():
 
684
            pass  # your code here
 
685
 
 
686
        # register a hook, providing a custom hook name
 
687
        @hooks.hook("config-changed")
 
688
        def config_changed():
 
689
            pass  # your code here
 
690
 
 
691
        if __name__ == "__main__":
 
692
            # execute a hook based on the name the program is called by
 
693
            hooks.execute(sys.argv)
 
694
    """
 
695
 
 
696
    def __init__(self, config_save=None):
313
697
        super(Hooks, self).__init__()
314
698
        self._hooks = {}
315
699
 
 
700
        # For unknown reasons, we allow the Hooks constructor to override
 
701
        # config().implicit_save.
 
702
        if config_save is not None:
 
703
            config().implicit_save = config_save
 
704
 
316
705
    def register(self, name, function):
 
706
        """Register a hook"""
317
707
        self._hooks[name] = function
318
708
 
319
709
    def execute(self, args):
 
710
        """Execute a registered hook based on args[0]"""
 
711
        _run_atstart()
320
712
        hook_name = os.path.basename(args[0])
321
713
        if hook_name in self._hooks:
322
 
            self._hooks[hook_name]()
 
714
            try:
 
715
                self._hooks[hook_name]()
 
716
            except SystemExit as x:
 
717
                if x.code is None or x.code == 0:
 
718
                    _run_atexit()
 
719
                raise
 
720
            _run_atexit()
323
721
        else:
324
722
            raise UnregisteredHookError(hook_name)
325
723
 
326
724
    def hook(self, *hook_names):
 
725
        """Decorator, registering them as hooks"""
327
726
        def wrapper(decorated):
328
727
            for hook_name in hook_names:
329
728
                self.register(hook_name, decorated)
337
736
 
338
737
 
339
738
def charm_dir():
 
739
    """Return the root directory of the current charm"""
340
740
    return os.environ.get('CHARM_DIR')
 
741
 
 
742
 
 
743
@cached
 
744
def action_get(key=None):
 
745
    """Gets the value of an action parameter, or all key/value param pairs"""
 
746
    cmd = ['action-get']
 
747
    if key is not None:
 
748
        cmd.append(key)
 
749
    cmd.append('--format=json')
 
750
    action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
751
    return action_data
 
752
 
 
753
 
 
754
def action_set(values):
 
755
    """Sets the values to be returned after the action finishes"""
 
756
    cmd = ['action-set']
 
757
    for k, v in list(values.items()):
 
758
        cmd.append('{}={}'.format(k, v))
 
759
    subprocess.check_call(cmd)
 
760
 
 
761
 
 
762
def action_fail(message):
 
763
    """Sets the action status to failed and sets the error message.
 
764
 
 
765
    The results set by action_set are preserved."""
 
766
    subprocess.check_call(['action-fail', message])
 
767
 
 
768
 
 
769
def action_name():
 
770
    """Get the name of the currently executing action."""
 
771
    return os.environ.get('JUJU_ACTION_NAME')
 
772
 
 
773
 
 
774
def action_uuid():
 
775
    """Get the UUID of the currently executing action."""
 
776
    return os.environ.get('JUJU_ACTION_UUID')
 
777
 
 
778
 
 
779
def action_tag():
 
780
    """Get the tag for the currently executing action."""
 
781
    return os.environ.get('JUJU_ACTION_TAG')
 
782
 
 
783
 
 
784
def status_set(workload_state, message):
 
785
    """Set the workload state with a message
 
786
 
 
787
    Use status-set to set the workload state with a message which is visible
 
788
    to the user via juju status. If the status-set command is not found then
 
789
    assume this is juju < 1.23 and juju-log the message unstead.
 
790
 
 
791
    workload_state -- valid juju workload state.
 
792
    message        -- status update message
 
793
    """
 
794
    valid_states = ['maintenance', 'blocked', 'waiting', 'active']
 
795
    if workload_state not in valid_states:
 
796
        raise ValueError(
 
797
            '{!r} is not a valid workload state'.format(workload_state)
 
798
        )
 
799
    cmd = ['status-set', workload_state, message]
 
800
    try:
 
801
        ret = subprocess.call(cmd)
 
802
        if ret == 0:
 
803
            return
 
804
    except OSError as e:
 
805
        if e.errno != errno.ENOENT:
 
806
            raise
 
807
    log_message = 'status-set failed: {} {}'.format(workload_state,
 
808
                                                    message)
 
809
    log(log_message, level='INFO')
 
810
 
 
811
 
 
812
def status_get():
 
813
    """Retrieve the previously set juju workload state and message
 
814
 
 
815
    If the status-get command is not found then assume this is juju < 1.23 and
 
816
    return 'unknown', ""
 
817
 
 
818
    """
 
819
    cmd = ['status-get', "--format=json", "--include-data"]
 
820
    try:
 
821
        raw_status = subprocess.check_output(cmd)
 
822
    except OSError as e:
 
823
        if e.errno == errno.ENOENT:
 
824
            return ('unknown', "")
 
825
        else:
 
826
            raise
 
827
    else:
 
828
        status = json.loads(raw_status.decode("UTF-8"))
 
829
        return (status["status"], status["message"])
 
830
 
 
831
 
 
832
def translate_exc(from_exc, to_exc):
 
833
    def inner_translate_exc1(f):
 
834
        @wraps(f)
 
835
        def inner_translate_exc2(*args, **kwargs):
 
836
            try:
 
837
                return f(*args, **kwargs)
 
838
            except from_exc:
 
839
                raise to_exc
 
840
 
 
841
        return inner_translate_exc2
 
842
 
 
843
    return inner_translate_exc1
 
844
 
 
845
 
 
846
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
847
def is_leader():
 
848
    """Does the current unit hold the juju leadership
 
849
 
 
850
    Uses juju to determine whether the current unit is the leader of its peers
 
851
    """
 
852
    cmd = ['is-leader', '--format=json']
 
853
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
854
 
 
855
 
 
856
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
857
def leader_get(attribute=None):
 
858
    """Juju leader get value(s)"""
 
859
    cmd = ['leader-get', '--format=json'] + [attribute or '-']
 
860
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
861
 
 
862
 
 
863
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
864
def leader_set(settings=None, **kwargs):
 
865
    """Juju leader set value(s)"""
 
866
    # Don't log secrets.
 
867
    # log("Juju leader-set '%s'" % (settings), level=DEBUG)
 
868
    cmd = ['leader-set']
 
869
    settings = settings or {}
 
870
    settings.update(kwargs)
 
871
    for k, v in settings.items():
 
872
        if v is None:
 
873
            cmd.append('{}='.format(k))
 
874
        else:
 
875
            cmd.append('{}={}'.format(k, v))
 
876
    subprocess.check_call(cmd)
 
877
 
 
878
 
 
879
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
880
def payload_register(ptype, klass, pid):
 
881
    """ is used while a hook is running to let Juju know that a
 
882
        payload has been started."""
 
883
    cmd = ['payload-register']
 
884
    for x in [ptype, klass, pid]:
 
885
        cmd.append(x)
 
886
    subprocess.check_call(cmd)
 
887
 
 
888
 
 
889
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
890
def payload_unregister(klass, pid):
 
891
    """ is used while a hook is running to let Juju know
 
892
    that a payload has been manually stopped. The <class> and <id> provided
 
893
    must match a payload that has been previously registered with juju using
 
894
    payload-register."""
 
895
    cmd = ['payload-unregister']
 
896
    for x in [klass, pid]:
 
897
        cmd.append(x)
 
898
    subprocess.check_call(cmd)
 
899
 
 
900
 
 
901
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
902
def payload_status_set(klass, pid, status):
 
903
    """is used to update the current status of a registered payload.
 
904
    The <class> and <id> provided must match a payload that has been previously
 
905
    registered with juju using payload-register. The <status> must be one of the
 
906
    follow: starting, started, stopping, stopped"""
 
907
    cmd = ['payload-status-set']
 
908
    for x in [klass, pid, status]:
 
909
        cmd.append(x)
 
910
    subprocess.check_call(cmd)
 
911
 
 
912
 
 
913
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
914
def resource_get(name):
 
915
    """used to fetch the resource path of the given name.
 
916
 
 
917
    <name> must match a name of defined resource in metadata.yaml
 
918
 
 
919
    returns either a path or False if resource not available
 
920
    """
 
921
    if not name:
 
922
        return False
 
923
 
 
924
    cmd = ['resource-get', name]
 
925
    try:
 
926
        return subprocess.check_output(cmd).decode('UTF-8')
 
927
    except subprocess.CalledProcessError:
 
928
        return False
 
929
 
 
930
 
 
931
@cached
 
932
def juju_version():
 
933
    """Full version string (eg. '1.23.3.1-trusty-amd64')"""
 
934
    # Per https://bugs.launchpad.net/juju-core/+bug/1455368/comments/1
 
935
    jujud = glob.glob('/var/lib/juju/tools/machine-*/jujud')[0]
 
936
    return subprocess.check_output([jujud, 'version'],
 
937
                                   universal_newlines=True).strip()
 
938
 
 
939
 
 
940
@cached
 
941
def has_juju_version(minimum_version):
 
942
    """Return True if the Juju version is at least the provided version"""
 
943
    return LooseVersion(juju_version()) >= LooseVersion(minimum_version)
 
944
 
 
945
 
 
946
_atexit = []
 
947
_atstart = []
 
948
 
 
949
 
 
950
def atstart(callback, *args, **kwargs):
 
951
    '''Schedule a callback to run before the main hook.
 
952
 
 
953
    Callbacks are run in the order they were added.
 
954
 
 
955
    This is useful for modules and classes to perform initialization
 
956
    and inject behavior. In particular:
 
957
 
 
958
        - Run common code before all of your hooks, such as logging
 
959
          the hook name or interesting relation data.
 
960
        - Defer object or module initialization that requires a hook
 
961
          context until we know there actually is a hook context,
 
962
          making testing easier.
 
963
        - Rather than requiring charm authors to include boilerplate to
 
964
          invoke your helper's behavior, have it run automatically if
 
965
          your object is instantiated or module imported.
 
966
 
 
967
    This is not at all useful after your hook framework as been launched.
 
968
    '''
 
969
    global _atstart
 
970
    _atstart.append((callback, args, kwargs))
 
971
 
 
972
 
 
973
def atexit(callback, *args, **kwargs):
 
974
    '''Schedule a callback to run on successful hook completion.
 
975
 
 
976
    Callbacks are run in the reverse order that they were added.'''
 
977
    _atexit.append((callback, args, kwargs))
 
978
 
 
979
 
 
980
def _run_atstart():
 
981
    '''Hook frameworks must invoke this before running the main hook body.'''
 
982
    global _atstart
 
983
    for callback, args, kwargs in _atstart:
 
984
        callback(*args, **kwargs)
 
985
    del _atstart[:]
 
986
 
 
987
 
 
988
def _run_atexit():
 
989
    '''Hook frameworks must invoke this after the main hook body has
 
990
    successfully completed. Do not invoke it if the hook fails.'''
 
991
    global _atexit
 
992
    for callback, args, kwargs in reversed(_atexit):
 
993
        callback(*args, **kwargs)
 
994
    del _atexit[:]
 
995
 
 
996
 
 
997
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
998
def network_get_primary_address(binding):
 
999
    '''
 
1000
    Retrieve the primary network address for a named binding
 
1001
 
 
1002
    :param binding: string. The name of a relation of extra-binding
 
1003
    :return: string. The primary IP address for the named binding
 
1004
    :raise: NotImplementedError if run on Juju < 2.0
 
1005
    '''
 
1006
    cmd = ['network-get', '--primary-address', binding]
 
1007
    return subprocess.check_output(cmd).decode('UTF-8').strip()