~hopem/charms/precise/ci-configurator/relations-cleanup

« back to all changes in this revision

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

[hopem,r=wolsen]

synced ~canonical-ci/charm-helpers/trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
8
8
import json
9
9
import yaml
10
10
import subprocess
 
11
import sys
11
12
import UserDict
 
13
from subprocess import CalledProcessError
12
14
 
13
15
CRITICAL = "CRITICAL"
14
16
ERROR = "ERROR"
21
23
 
22
24
 
23
25
def cached(func):
24
 
    ''' Cache return values for multiple executions of func + args
 
26
    """Cache return values for multiple executions of func + args
25
27
 
26
28
    For example:
27
29
 
32
34
        unit_get('test')
33
35
 
34
36
    will cache the result of unit_get + 'test' for future calls.
35
 
    '''
 
37
    """
36
38
    def wrapper(*args, **kwargs):
37
39
        global cache
38
40
        key = str((func, args, kwargs))
46
48
 
47
49
 
48
50
def flush(key):
49
 
    ''' Flushes any entries from function cache where the
50
 
    key is found in the function+args '''
 
51
    """Flushes any entries from function cache where the
 
52
    key is found in the function+args """
51
53
    flush_list = []
52
54
    for item in cache:
53
55
        if key in item:
57
59
 
58
60
 
59
61
def log(message, level=None):
60
 
    "Write a message to the juju log"
 
62
    """Write a message to the juju log"""
61
63
    command = ['juju-log']
62
64
    if level:
63
65
        command += ['-l', level]
66
68
 
67
69
 
68
70
class Serializable(UserDict.IterableUserDict):
69
 
    "Wrapper, an object that can be serialized to yaml or json"
 
71
    """Wrapper, an object that can be serialized to yaml or json"""
70
72
 
71
73
    def __init__(self, obj):
72
74
        # wrap the object
96
98
        self.data = state
97
99
 
98
100
    def json(self):
99
 
        "Serialize the object to json"
 
101
        """Serialize the object to json"""
100
102
        return json.dumps(self.data)
101
103
 
102
104
    def yaml(self):
103
 
        "Serialize the object to yaml"
 
105
        """Serialize the object to yaml"""
104
106
        return yaml.dump(self.data)
105
107
 
106
108
 
119
121
 
120
122
 
121
123
def in_relation_hook():
122
 
    "Determine whether we're running in a relation hook"
 
124
    """Determine whether we're running in a relation hook"""
123
125
    return 'JUJU_RELATION' in os.environ
124
126
 
125
127
 
126
128
def relation_type():
127
 
    "The scope for the current relation hook"
 
129
    """The scope for the current relation hook"""
128
130
    return os.environ.get('JUJU_RELATION', None)
129
131
 
130
132
 
131
133
def relation_id():
132
 
    "The relation ID for the current relation hook"
 
134
    """The relation ID for the current relation hook"""
133
135
    return os.environ.get('JUJU_RELATION_ID', None)
134
136
 
135
137
 
136
138
def local_unit():
137
 
    "Local unit ID"
 
139
    """Local unit ID"""
138
140
    return os.environ['JUJU_UNIT_NAME']
139
141
 
140
142
 
141
143
def remote_unit():
142
 
    "The remote unit for the current relation hook"
 
144
    """The remote unit for the current relation hook"""
143
145
    return os.environ['JUJU_REMOTE_UNIT']
144
146
 
145
147
 
146
148
def service_name():
147
 
    "The name service group this unit belongs to"
 
149
    """The name service group this unit belongs to"""
148
150
    return local_unit().split('/')[0]
149
151
 
150
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
 
151
252
@cached
152
253
def config(scope=None):
153
 
    "Juju charm configuration"
 
254
    """Juju charm configuration"""
154
255
    config_cmd_line = ['config-get']
155
256
    if scope is not None:
156
257
        config_cmd_line.append(scope)
157
258
    config_cmd_line.append('--format=json')
158
259
    try:
159
 
        return json.loads(subprocess.check_output(config_cmd_line))
 
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)
160
264
    except ValueError:
161
265
        return None
162
266
 
163
267
 
164
268
@cached
165
269
def relation_get(attribute=None, unit=None, rid=None):
 
270
    """Get relation information"""
166
271
    _args = ['relation-get', '--format=json']
167
272
    if rid:
168
273
        _args.append('-r')
174
279
        return json.loads(subprocess.check_output(_args))
175
280
    except ValueError:
176
281
        return None
 
282
    except CalledProcessError, e:
 
283
        if e.returncode == 2:
 
284
            return None
 
285
        raise
177
286
 
178
287
 
179
288
def relation_set(relation_id=None, relation_settings={}, **kwargs):
 
289
    """Set relation information for the current unit"""
180
290
    relation_cmd_line = ['relation-set']
181
291
    if relation_id is not None:
182
292
        relation_cmd_line.extend(('-r', relation_id))
192
302
 
193
303
@cached
194
304
def relation_ids(reltype=None):
195
 
    "A list of relation_ids"
 
305
    """A list of relation_ids"""
196
306
    reltype = reltype or relation_type()
197
307
    relid_cmd_line = ['relation-ids', '--format=json']
198
308
    if reltype is not None:
203
313
 
204
314
@cached
205
315
def related_units(relid=None):
206
 
    "A list of related units"
 
316
    """A list of related units"""
207
317
    relid = relid or relation_id()
208
318
    units_cmd_line = ['relation-list', '--format=json']
209
319
    if relid is not None:
213
323
 
214
324
@cached
215
325
def relation_for_unit(unit=None, rid=None):
216
 
    "Get the json represenation of a unit's relation"
 
326
    """Get the json represenation of a unit's relation"""
217
327
    unit = unit or remote_unit()
218
328
    relation = relation_get(unit=unit, rid=rid)
219
329
    for key in relation:
225
335
 
226
336
@cached
227
337
def relations_for_id(relid=None):
228
 
    "Get relations of a specific relation ID"
 
338
    """Get relations of a specific relation ID"""
229
339
    relation_data = []
230
340
    relid = relid or relation_ids()
231
341
    for unit in related_units(relid):
237
347
 
238
348
@cached
239
349
def relations_of_type(reltype=None):
240
 
    "Get relations of a specific type"
 
350
    """Get relations of a specific type"""
241
351
    relation_data = []
242
352
    reltype = reltype or relation_type()
243
353
    for relid in relation_ids(reltype):
249
359
 
250
360
@cached
251
361
def relation_types():
252
 
    "Get a list of relation types supported by this charm"
 
362
    """Get a list of relation types supported by this charm"""
253
363
    charmdir = os.environ.get('CHARM_DIR', '')
254
364
    mdf = open(os.path.join(charmdir, 'metadata.yaml'))
255
365
    md = yaml.safe_load(mdf)
264
374
 
265
375
@cached
266
376
def relations():
 
377
    """Get a nested dictionary of relation data for all related units"""
267
378
    rels = {}
268
379
    for reltype in relation_types():
269
380
        relids = {}
277
388
    return rels
278
389
 
279
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
 
280
411
def open_port(port, protocol="TCP"):
281
 
    "Open a service network port"
 
412
    """Open a service network port"""
282
413
    _args = ['open-port']
283
414
    _args.append('{}/{}'.format(port, protocol))
284
415
    subprocess.check_call(_args)
285
416
 
286
417
 
287
418
def close_port(port, protocol="TCP"):
288
 
    "Close a service network port"
 
419
    """Close a service network port"""
289
420
    _args = ['close-port']
290
421
    _args.append('{}/{}'.format(port, protocol))
291
422
    subprocess.check_call(_args)
293
424
 
294
425
@cached
295
426
def unit_get(attribute):
 
427
    """Get the unit ID for the remote unit"""
296
428
    _args = ['unit-get', '--format=json', attribute]
297
429
    try:
298
430
        return json.loads(subprocess.check_output(_args))
301
433
 
302
434
 
303
435
def unit_private_ip():
 
436
    """Get this unit's private IP address"""
304
437
    return unit_get('private-address')
305
438
 
306
439
 
307
440
class UnregisteredHookError(Exception):
 
441
    """Raised when an undefined hook is called"""
308
442
    pass
309
443
 
310
444
 
311
445
class Hooks(object):
 
446
    """A convenient handler for hook functions.
 
447
 
 
448
    Example:
 
449
        hooks = Hooks()
 
450
 
 
451
        # register a hook, taking its name from the function name
 
452
        @hooks.hook()
 
453
        def install():
 
454
            ...
 
455
 
 
456
        # register a hook, providing a custom hook name
 
457
        @hooks.hook("config-changed")
 
458
        def config_changed():
 
459
            ...
 
460
 
 
461
        if __name__ == "__main__":
 
462
            # execute a hook based on the name the program is called by
 
463
            hooks.execute(sys.argv)
 
464
    """
 
465
 
312
466
    def __init__(self):
313
467
        super(Hooks, self).__init__()
314
468
        self._hooks = {}
315
469
 
316
470
    def register(self, name, function):
 
471
        """Register a hook"""
317
472
        self._hooks[name] = function
318
473
 
319
474
    def execute(self, args):
 
475
        """Execute a registered hook based on args[0]"""
320
476
        hook_name = os.path.basename(args[0])
321
477
        if hook_name in self._hooks:
322
478
            self._hooks[hook_name]()
324
480
            raise UnregisteredHookError(hook_name)
325
481
 
326
482
    def hook(self, *hook_names):
 
483
        """Decorator, registering them as hooks"""
327
484
        def wrapper(decorated):
328
485
            for hook_name in hook_names:
329
486
                self.register(hook_name, decorated)
337
494
 
338
495
 
339
496
def charm_dir():
 
497
    """Return the root directory of the current charm"""
340
498
    return os.environ.get('CHARM_DIR')