~patrick-hetu/+junk/sentry

« back to all changes in this revision

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

  • Committer: Patrick Hetu
  • Date: 2014-07-14 16:12:10 UTC
  • Revision ID: patrick.hetu@gmail.com-20140714161210-a9f6o1b9knyvg4yf
initial commit

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"Interactions with the Juju environment"
 
2
# Copyright 2013 Canonical Ltd.
 
3
#
 
4
# Authors:
 
5
#  Charm Helpers Developers <juju@lists.ubuntu.com>
 
6
 
 
7
import os
 
8
import json
 
9
import yaml
 
10
import subprocess
 
11
import sys
 
12
import UserDict
 
13
from subprocess import CalledProcessError
 
14
 
 
15
CRITICAL = "CRITICAL"
 
16
ERROR = "ERROR"
 
17
WARNING = "WARNING"
 
18
INFO = "INFO"
 
19
DEBUG = "DEBUG"
 
20
MARKER = object()
 
21
 
 
22
cache = {}
 
23
 
 
24
 
 
25
def cached(func):
 
26
    """Cache return values for multiple executions of func + args
 
27
 
 
28
    For example::
 
29
 
 
30
        @cached
 
31
        def unit_get(attribute):
 
32
            pass
 
33
 
 
34
        unit_get('test')
 
35
 
 
36
    will cache the result of unit_get + 'test' for future calls.
 
37
    """
 
38
    def wrapper(*args, **kwargs):
 
39
        global cache
 
40
        key = str((func, args, kwargs))
 
41
        try:
 
42
            return cache[key]
 
43
        except KeyError:
 
44
            res = func(*args, **kwargs)
 
45
            cache[key] = res
 
46
            return res
 
47
    return wrapper
 
48
 
 
49
 
 
50
def flush(key):
 
51
    """Flushes any entries from function cache where the
 
52
    key is found in the function+args """
 
53
    flush_list = []
 
54
    for item in cache:
 
55
        if key in item:
 
56
            flush_list.append(item)
 
57
    for item in flush_list:
 
58
        del cache[item]
 
59
 
 
60
 
 
61
def log(message, level=None):
 
62
    """Write a message to the juju log"""
 
63
    command = ['juju-log']
 
64
    if level:
 
65
        command += ['-l', level]
 
66
    command += [message]
 
67
    subprocess.call(command)
 
68
 
 
69
 
 
70
class Serializable(UserDict.IterableUserDict):
 
71
    """Wrapper, an object that can be serialized to yaml or json"""
 
72
 
 
73
    def __init__(self, obj):
 
74
        # wrap the object
 
75
        UserDict.IterableUserDict.__init__(self)
 
76
        self.data = obj
 
77
 
 
78
    def __getattr__(self, attr):
 
79
        # See if this object has attribute.
 
80
        if attr in ("json", "yaml", "data"):
 
81
            return self.__dict__[attr]
 
82
        # Check for attribute in wrapped object.
 
83
        got = getattr(self.data, attr, MARKER)
 
84
        if got is not MARKER:
 
85
            return got
 
86
        # Proxy to the wrapped object via dict interface.
 
87
        try:
 
88
            return self.data[attr]
 
89
        except KeyError:
 
90
            raise AttributeError(attr)
 
91
 
 
92
    def __getstate__(self):
 
93
        # Pickle as a standard dictionary.
 
94
        return self.data
 
95
 
 
96
    def __setstate__(self, state):
 
97
        # Unpickle into our wrapper.
 
98
        self.data = state
 
99
 
 
100
    def json(self):
 
101
        """Serialize the object to json"""
 
102
        return json.dumps(self.data)
 
103
 
 
104
    def yaml(self):
 
105
        """Serialize the object to yaml"""
 
106
        return yaml.dump(self.data)
 
107
 
 
108
 
 
109
def execution_environment():
 
110
    """A convenient bundling of the current execution context"""
 
111
    context = {}
 
112
    context['conf'] = config()
 
113
    if relation_id():
 
114
        context['reltype'] = relation_type()
 
115
        context['relid'] = relation_id()
 
116
        context['rel'] = relation_get()
 
117
    context['unit'] = local_unit()
 
118
    context['rels'] = relations()
 
119
    context['env'] = os.environ
 
120
    return context
 
121
 
 
122
 
 
123
def in_relation_hook():
 
124
    """Determine whether we're running in a relation hook"""
 
125
    return 'JUJU_RELATION' in os.environ
 
126
 
 
127
 
 
128
def relation_type():
 
129
    """The scope for the current relation hook"""
 
130
    return os.environ.get('JUJU_RELATION', None)
 
131
 
 
132
 
 
133
def relation_id():
 
134
    """The relation ID for the current relation hook"""
 
135
    return os.environ.get('JUJU_RELATION_ID', None)
 
136
 
 
137
 
 
138
def local_unit():
 
139
    """Local unit ID"""
 
140
    return os.environ['JUJU_UNIT_NAME']
 
141
 
 
142
 
 
143
def remote_unit():
 
144
    """The remote unit for the current relation hook"""
 
145
    return os.environ['JUJU_REMOTE_UNIT']
 
146
 
 
147
 
 
148
def service_name():
 
149
    """The name service group this unit belongs to"""
 
150
    return local_unit().split('/')[0]
 
151
 
 
152
 
 
153
def hook_name():
 
154
    """The name of the currently executing hook"""
 
155
    return os.path.basename(sys.argv[0])
 
156
 
 
157
 
 
158
class Config(dict):
 
159
    """A Juju charm config dictionary that can write itself to
 
160
    disk (as json) and track which values have changed since
 
161
    the previous hook invocation.
 
162
 
 
163
    Do not instantiate this object directly - instead call
 
164
    ``hookenv.config()``
 
165
 
 
166
    Example usage::
 
167
 
 
168
        >>> # inside a hook
 
169
        >>> from charmhelpers.core import hookenv
 
170
        >>> config = hookenv.config()
 
171
        >>> config['foo']
 
172
        'bar'
 
173
        >>> config['mykey'] = 'myval'
 
174
        >>> config.save()
 
175
 
 
176
 
 
177
        >>> # user runs `juju set mycharm foo=baz`
 
178
        >>> # now we're inside subsequent config-changed hook
 
179
        >>> config = hookenv.config()
 
180
        >>> config['foo']
 
181
        'baz'
 
182
        >>> # test to see if this val has changed since last hook
 
183
        >>> config.changed('foo')
 
184
        True
 
185
        >>> # what was the previous value?
 
186
        >>> config.previous('foo')
 
187
        'bar'
 
188
        >>> # keys/values that we add are preserved across hooks
 
189
        >>> config['mykey']
 
190
        'myval'
 
191
        >>> # don't forget to save at the end of hook!
 
192
        >>> config.save()
 
193
 
 
194
    """
 
195
    CONFIG_FILE_NAME = '.juju-persistent-config'
 
196
 
 
197
    def __init__(self, *args, **kw):
 
198
        super(Config, self).__init__(*args, **kw)
 
199
        self._prev_dict = None
 
200
        self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
 
201
        if os.path.exists(self.path):
 
202
            self.load_previous()
 
203
 
 
204
    def load_previous(self, path=None):
 
205
        """Load previous copy of config from disk so that current values
 
206
        can be compared to previous values.
 
207
 
 
208
        :param path:
 
209
 
 
210
            File path from which to load the previous config. If `None`,
 
211
            config is loaded from the default location. If `path` is
 
212
            specified, subsequent `save()` calls will write to the same
 
213
            path.
 
214
 
 
215
        """
 
216
        self.path = path or self.path
 
217
        with open(self.path) as f:
 
218
            self._prev_dict = json.load(f)
 
219
 
 
220
    def changed(self, key):
 
221
        """Return true if the value for this key has changed since
 
222
        the last save.
 
223
 
 
224
        """
 
225
        if self._prev_dict is None:
 
226
            return True
 
227
        return self.previous(key) != self.get(key)
 
228
 
 
229
    def previous(self, key):
 
230
        """Return previous value for this key, or None if there
 
231
        is no "previous" value.
 
232
 
 
233
        """
 
234
        if self._prev_dict:
 
235
            return self._prev_dict.get(key)
 
236
        return None
 
237
 
 
238
    def save(self):
 
239
        """Save this config to disk.
 
240
 
 
241
        Preserves items in _prev_dict that do not exist in self.
 
242
 
 
243
        """
 
244
        if self._prev_dict:
 
245
            for k, v in self._prev_dict.iteritems():
 
246
                if k not in self:
 
247
                    self[k] = v
 
248
        with open(self.path, 'w') as f:
 
249
            json.dump(self, f)
 
250
 
 
251
 
 
252
@cached
 
253
def config(scope=None):
 
254
    """Juju charm configuration"""
 
255
    config_cmd_line = ['config-get']
 
256
    if scope is not None:
 
257
        config_cmd_line.append(scope)
 
258
    config_cmd_line.append('--format=json')
 
259
    try:
 
260
        config_data = json.loads(subprocess.check_output(config_cmd_line))
 
261
        if scope is not None:
 
262
            return config_data
 
263
        return Config(config_data)
 
264
    except ValueError:
 
265
        return None
 
266
 
 
267
 
 
268
@cached
 
269
def relation_get(attribute=None, unit=None, rid=None):
 
270
    """Get relation information"""
 
271
    _args = ['relation-get', '--format=json']
 
272
    if rid:
 
273
        _args.append('-r')
 
274
        _args.append(rid)
 
275
    _args.append(attribute or '-')
 
276
    if unit:
 
277
        _args.append(unit)
 
278
    try:
 
279
        return json.loads(subprocess.check_output(_args))
 
280
    except ValueError:
 
281
        return None
 
282
    except CalledProcessError, e:
 
283
        if e.returncode == 2:
 
284
            return None
 
285
        raise
 
286
 
 
287
 
 
288
def relation_set(relation_id=None, relation_settings={}, **kwargs):
 
289
    """Set relation information for the current unit"""
 
290
    relation_cmd_line = ['relation-set']
 
291
    if relation_id is not None:
 
292
        relation_cmd_line.extend(('-r', relation_id))
 
293
    for k, v in (relation_settings.items() + kwargs.items()):
 
294
        if v is None:
 
295
            relation_cmd_line.append('{}='.format(k))
 
296
        else:
 
297
            relation_cmd_line.append('{}={}'.format(k, v))
 
298
    subprocess.check_call(relation_cmd_line)
 
299
    # Flush cache of any relation-gets for local unit
 
300
    flush(local_unit())
 
301
 
 
302
 
 
303
@cached
 
304
def relation_ids(reltype=None):
 
305
    """A list of relation_ids"""
 
306
    reltype = reltype or relation_type()
 
307
    relid_cmd_line = ['relation-ids', '--format=json']
 
308
    if reltype is not None:
 
309
        relid_cmd_line.append(reltype)
 
310
        return json.loads(subprocess.check_output(relid_cmd_line)) or []
 
311
    return []
 
312
 
 
313
 
 
314
@cached
 
315
def related_units(relid=None):
 
316
    """A list of related units"""
 
317
    relid = relid or relation_id()
 
318
    units_cmd_line = ['relation-list', '--format=json']
 
319
    if relid is not None:
 
320
        units_cmd_line.extend(('-r', relid))
 
321
    return json.loads(subprocess.check_output(units_cmd_line)) or []
 
322
 
 
323
 
 
324
@cached
 
325
def relation_for_unit(unit=None, rid=None):
 
326
    """Get the json represenation of a unit's relation"""
 
327
    unit = unit or remote_unit()
 
328
    relation = relation_get(unit=unit, rid=rid)
 
329
    for key in relation:
 
330
        if key.endswith('-list'):
 
331
            relation[key] = relation[key].split()
 
332
    relation['__unit__'] = unit
 
333
    return relation
 
334
 
 
335
 
 
336
@cached
 
337
def relations_for_id(relid=None):
 
338
    """Get relations of a specific relation ID"""
 
339
    relation_data = []
 
340
    relid = relid or relation_ids()
 
341
    for unit in related_units(relid):
 
342
        unit_data = relation_for_unit(unit, relid)
 
343
        unit_data['__relid__'] = relid
 
344
        relation_data.append(unit_data)
 
345
    return relation_data
 
346
 
 
347
 
 
348
@cached
 
349
def relations_of_type(reltype=None):
 
350
    """Get relations of a specific type"""
 
351
    relation_data = []
 
352
    reltype = reltype or relation_type()
 
353
    for relid in relation_ids(reltype):
 
354
        for relation in relations_for_id(relid):
 
355
            relation['__relid__'] = relid
 
356
            relation_data.append(relation)
 
357
    return relation_data
 
358
 
 
359
 
 
360
@cached
 
361
def relation_types():
 
362
    """Get a list of relation types supported by this charm"""
 
363
    charmdir = os.environ.get('CHARM_DIR', '')
 
364
    mdf = open(os.path.join(charmdir, 'metadata.yaml'))
 
365
    md = yaml.safe_load(mdf)
 
366
    rel_types = []
 
367
    for key in ('provides', 'requires', 'peers'):
 
368
        section = md.get(key)
 
369
        if section:
 
370
            rel_types.extend(section.keys())
 
371
    mdf.close()
 
372
    return rel_types
 
373
 
 
374
 
 
375
@cached
 
376
def relations():
 
377
    """Get a nested dictionary of relation data for all related units"""
 
378
    rels = {}
 
379
    for reltype in relation_types():
 
380
        relids = {}
 
381
        for relid in relation_ids(reltype):
 
382
            units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
 
383
            for unit in related_units(relid):
 
384
                reldata = relation_get(unit=unit, rid=relid)
 
385
                units[unit] = reldata
 
386
            relids[relid] = units
 
387
        rels[reltype] = relids
 
388
    return rels
 
389
 
 
390
 
 
391
@cached
 
392
def is_relation_made(relation, keys='private-address'):
 
393
    '''
 
394
    Determine whether a relation is established by checking for
 
395
    presence of key(s).  If a list of keys is provided, they
 
396
    must all be present for the relation to be identified as made
 
397
    '''
 
398
    if isinstance(keys, str):
 
399
        keys = [keys]
 
400
    for r_id in relation_ids(relation):
 
401
        for unit in related_units(r_id):
 
402
            context = {}
 
403
            for k in keys:
 
404
                context[k] = relation_get(k, rid=r_id,
 
405
                                          unit=unit)
 
406
            if None not in context.values():
 
407
                return True
 
408
    return False
 
409
 
 
410
 
 
411
def open_port(port, protocol="TCP"):
 
412
    """Open a service network port"""
 
413
    _args = ['open-port']
 
414
    _args.append('{}/{}'.format(port, protocol))
 
415
    subprocess.check_call(_args)
 
416
 
 
417
 
 
418
def close_port(port, protocol="TCP"):
 
419
    """Close a service network port"""
 
420
    _args = ['close-port']
 
421
    _args.append('{}/{}'.format(port, protocol))
 
422
    subprocess.check_call(_args)
 
423
 
 
424
 
 
425
@cached
 
426
def unit_get(attribute):
 
427
    """Get the unit ID for the remote unit"""
 
428
    _args = ['unit-get', '--format=json', attribute]
 
429
    try:
 
430
        return json.loads(subprocess.check_output(_args))
 
431
    except ValueError:
 
432
        return None
 
433
 
 
434
 
 
435
def unit_private_ip():
 
436
    """Get this unit's private IP address"""
 
437
    return unit_get('private-address')
 
438
 
 
439
 
 
440
class UnregisteredHookError(Exception):
 
441
    """Raised when an undefined hook is called"""
 
442
    pass
 
443
 
 
444
 
 
445
class Hooks(object):
 
446
    """A convenient handler for hook functions.
 
447
 
 
448
    Example::
 
449
 
 
450
        hooks = Hooks()
 
451
 
 
452
        # register a hook, taking its name from the function name
 
453
        @hooks.hook()
 
454
        def install():
 
455
            pass  # your code here
 
456
 
 
457
        # register a hook, providing a custom hook name
 
458
        @hooks.hook("config-changed")
 
459
        def config_changed():
 
460
            pass  # your code here
 
461
 
 
462
        if __name__ == "__main__":
 
463
            # execute a hook based on the name the program is called by
 
464
            hooks.execute(sys.argv)
 
465
    """
 
466
 
 
467
    def __init__(self):
 
468
        super(Hooks, self).__init__()
 
469
        self._hooks = {}
 
470
 
 
471
    def register(self, name, function):
 
472
        """Register a hook"""
 
473
        self._hooks[name] = function
 
474
 
 
475
    def execute(self, args):
 
476
        """Execute a registered hook based on args[0]"""
 
477
        hook_name = os.path.basename(args[0])
 
478
        if hook_name in self._hooks:
 
479
            self._hooks[hook_name]()
 
480
        else:
 
481
            raise UnregisteredHookError(hook_name)
 
482
 
 
483
    def hook(self, *hook_names):
 
484
        """Decorator, registering them as hooks"""
 
485
        def wrapper(decorated):
 
486
            for hook_name in hook_names:
 
487
                self.register(hook_name, decorated)
 
488
            else:
 
489
                self.register(decorated.__name__, decorated)
 
490
                if '_' in decorated.__name__:
 
491
                    self.register(
 
492
                        decorated.__name__.replace('_', '-'), decorated)
 
493
            return decorated
 
494
        return wrapper
 
495
 
 
496
 
 
497
def charm_dir():
 
498
    """Return the root directory of the current charm"""
 
499
    return os.environ.get('CHARM_DIR')