1
"Interactions with the Juju environment"
2
# Copyright 2013 Canonical Ltd.
5
# Charm Helpers Developers <juju@lists.ubuntu.com>
13
from subprocess import CalledProcessError
26
"""Cache return values for multiple executions of func + args
31
def unit_get(attribute):
36
will cache the result of unit_get + 'test' for future calls.
38
def wrapper(*args, **kwargs):
40
key = str((func, args, kwargs))
44
res = func(*args, **kwargs)
51
"""Flushes any entries from function cache where the
52
key is found in the function+args """
56
flush_list.append(item)
57
for item in flush_list:
61
def log(message, level=None):
62
"""Write a message to the juju log"""
63
command = ['juju-log']
65
command += ['-l', level]
67
subprocess.call(command)
70
class Serializable(UserDict.IterableUserDict):
71
"""Wrapper, an object that can be serialized to yaml or json"""
73
def __init__(self, obj):
75
UserDict.IterableUserDict.__init__(self)
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)
86
# Proxy to the wrapped object via dict interface.
88
return self.data[attr]
90
raise AttributeError(attr)
92
def __getstate__(self):
93
# Pickle as a standard dictionary.
96
def __setstate__(self, state):
97
# Unpickle into our wrapper.
101
"""Serialize the object to json"""
102
return json.dumps(self.data)
105
"""Serialize the object to yaml"""
106
return yaml.dump(self.data)
109
def execution_environment():
110
"""A convenient bundling of the current execution context"""
112
context['conf'] = config()
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
123
def in_relation_hook():
124
"""Determine whether we're running in a relation hook"""
125
return 'JUJU_RELATION' in os.environ
129
"""The scope for the current relation hook"""
130
return os.environ.get('JUJU_RELATION', None)
134
"""The relation ID for the current relation hook"""
135
return os.environ.get('JUJU_RELATION_ID', None)
140
return os.environ['JUJU_UNIT_NAME']
144
"""The remote unit for the current relation hook"""
145
return os.environ['JUJU_REMOTE_UNIT']
149
"""The name service group this unit belongs to"""
150
return local_unit().split('/')[0]
154
"""The name of the currently executing hook"""
155
return os.path.basename(sys.argv[0])
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.
163
Do not instantiate this object directly - instead call
169
>>> from charmhelpers.core import hookenv
170
>>> config = hookenv.config()
173
>>> config['mykey'] = 'myval'
177
>>> # user runs `juju set mycharm foo=baz`
178
>>> # now we're inside subsequent config-changed hook
179
>>> config = hookenv.config()
182
>>> # test to see if this val has changed since last hook
183
>>> config.changed('foo')
185
>>> # what was the previous value?
186
>>> config.previous('foo')
188
>>> # keys/values that we add are preserved across hooks
191
>>> # don't forget to save at the end of hook!
195
CONFIG_FILE_NAME = '.juju-persistent-config'
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):
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.
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
216
self.path = path or self.path
217
with open(self.path) as f:
218
self._prev_dict = json.load(f)
220
def changed(self, key):
221
"""Return true if the value for this key has changed since
225
if self._prev_dict is None:
227
return self.previous(key) != self.get(key)
229
def previous(self, key):
230
"""Return previous value for this key, or None if there
231
is no "previous" value.
235
return self._prev_dict.get(key)
239
"""Save this config to disk.
241
Preserves items in _prev_dict that do not exist in self.
245
for k, v in self._prev_dict.iteritems():
248
with open(self.path, 'w') as f:
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')
260
config_data = json.loads(subprocess.check_output(config_cmd_line))
261
if scope is not None:
263
return Config(config_data)
269
def relation_get(attribute=None, unit=None, rid=None):
270
"""Get relation information"""
271
_args = ['relation-get', '--format=json']
275
_args.append(attribute or '-')
279
return json.loads(subprocess.check_output(_args))
282
except CalledProcessError, e:
283
if e.returncode == 2:
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()):
295
relation_cmd_line.append('{}='.format(k))
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
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 []
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 []
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)
330
if key.endswith('-list'):
331
relation[key] = relation[key].split()
332
relation['__unit__'] = unit
337
def relations_for_id(relid=None):
338
"""Get relations of a specific relation ID"""
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)
349
def relations_of_type(reltype=None):
350
"""Get relations of a specific type"""
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)
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)
367
for key in ('provides', 'requires', 'peers'):
368
section = md.get(key)
370
rel_types.extend(section.keys())
377
"""Get a nested dictionary of relation data for all related units"""
379
for reltype in relation_types():
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
392
def is_relation_made(relation, keys='private-address'):
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
398
if isinstance(keys, str):
400
for r_id in relation_ids(relation):
401
for unit in related_units(r_id):
404
context[k] = relation_get(k, rid=r_id,
406
if None not in context.values():
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)
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)
426
def unit_get(attribute):
427
"""Get the unit ID for the remote unit"""
428
_args = ['unit-get', '--format=json', attribute]
430
return json.loads(subprocess.check_output(_args))
435
def unit_private_ip():
436
"""Get this unit's private IP address"""
437
return unit_get('private-address')
440
class UnregisteredHookError(Exception):
441
"""Raised when an undefined hook is called"""
446
"""A convenient handler for hook functions.
452
# register a hook, taking its name from the function name
455
pass # your code here
457
# register a hook, providing a custom hook name
458
@hooks.hook("config-changed")
459
def config_changed():
460
pass # your code here
462
if __name__ == "__main__":
463
# execute a hook based on the name the program is called by
464
hooks.execute(sys.argv)
468
super(Hooks, self).__init__()
471
def register(self, name, function):
472
"""Register a hook"""
473
self._hooks[name] = function
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]()
481
raise UnregisteredHookError(hook_name)
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)
489
self.register(decorated.__name__, decorated)
490
if '_' in decorated.__name__:
492
decorated.__name__.replace('_', '-'), decorated)
498
"""Return the root directory of the current charm"""
499
return os.environ.get('CHARM_DIR')