1
"Interactions with the Juju environment"
2
# Copyright 2013 Canonical Ltd.
5
# Charm Helpers Developers <juju@lists.ubuntu.com>
12
from subprocess import CalledProcessError
25
"""Cache return values for multiple executions of func + args
30
def unit_get(attribute):
35
will cache the result of unit_get + 'test' for future calls.
37
def wrapper(*args, **kwargs):
39
key = str((func, args, kwargs))
43
res = func(*args, **kwargs)
50
"""Flushes any entries from function cache where the
51
key is found in the function+args """
55
flush_list.append(item)
56
for item in flush_list:
60
def log(message, level=None):
61
"""Write a message to the juju log"""
62
command = ['juju-log']
64
command += ['-l', level]
66
subprocess.call(command)
69
class Serializable(UserDict.IterableUserDict):
70
"""Wrapper, an object that can be serialized to yaml or json"""
72
def __init__(self, obj):
74
UserDict.IterableUserDict.__init__(self)
77
def __getattr__(self, attr):
78
# See if this object has attribute.
79
if attr in ("json", "yaml", "data"):
80
return self.__dict__[attr]
81
# Check for attribute in wrapped object.
82
got = getattr(self.data, attr, MARKER)
85
# Proxy to the wrapped object via dict interface.
87
return self.data[attr]
89
raise AttributeError(attr)
91
def __getstate__(self):
92
# Pickle as a standard dictionary.
95
def __setstate__(self, state):
96
# Unpickle into our wrapper.
100
"""Serialize the object to json"""
101
return json.dumps(self.data)
104
"""Serialize the object to yaml"""
105
return yaml.dump(self.data)
108
def execution_environment():
109
"""A convenient bundling of the current execution context"""
111
context['conf'] = config()
113
context['reltype'] = relation_type()
114
context['relid'] = relation_id()
115
context['rel'] = relation_get()
116
context['unit'] = local_unit()
117
context['rels'] = relations()
118
context['env'] = os.environ
122
def in_relation_hook():
123
"""Determine whether we're running in a relation hook"""
124
return 'JUJU_RELATION' in os.environ
128
"""The scope for the current relation hook"""
129
return os.environ.get('JUJU_RELATION', None)
133
"""The relation ID for the current relation hook"""
134
return os.environ.get('JUJU_RELATION_ID', None)
139
return os.environ['JUJU_UNIT_NAME']
143
"""The remote unit for the current relation hook"""
144
return os.environ['JUJU_REMOTE_UNIT']
148
"""The name service group this unit belongs to"""
149
return local_unit().split('/')[0]
153
def config(scope=None):
154
"""Juju charm configuration"""
155
config_cmd_line = ['config-get']
156
if scope is not None:
157
config_cmd_line.append(scope)
158
config_cmd_line.append('--format=json')
160
return json.loads(subprocess.check_output(config_cmd_line))
166
def relation_get(attribute=None, unit=None, rid=None):
167
"""Get relation information"""
168
_args = ['relation-get', '--format=json']
172
_args.append(attribute or '-')
176
return json.loads(subprocess.check_output(_args))
179
except CalledProcessError, e:
180
if e.returncode == 2:
185
def relation_set(relation_id=None, relation_settings={}, **kwargs):
186
"""Set relation information for the current unit"""
187
relation_cmd_line = ['relation-set']
188
if relation_id is not None:
189
relation_cmd_line.extend(('-r', relation_id))
190
for k, v in (relation_settings.items() + kwargs.items()):
192
relation_cmd_line.append('{}='.format(k))
194
relation_cmd_line.append('{}={}'.format(k, v))
195
subprocess.check_call(relation_cmd_line)
196
# Flush cache of any relation-gets for local unit
201
def relation_ids(reltype=None):
202
"""A list of relation_ids"""
203
reltype = reltype or relation_type()
204
relid_cmd_line = ['relation-ids', '--format=json']
205
if reltype is not None:
206
relid_cmd_line.append(reltype)
207
return json.loads(subprocess.check_output(relid_cmd_line)) or []
212
def related_units(relid=None):
213
"""A list of related units"""
214
relid = relid or relation_id()
215
units_cmd_line = ['relation-list', '--format=json']
216
if relid is not None:
217
units_cmd_line.extend(('-r', relid))
218
return json.loads(subprocess.check_output(units_cmd_line)) or []
222
def relation_for_unit(unit=None, rid=None):
223
"""Get the json represenation of a unit's relation"""
224
unit = unit or remote_unit()
225
relation = relation_get(unit=unit, rid=rid)
227
if key.endswith('-list'):
228
relation[key] = relation[key].split()
229
relation['__unit__'] = unit
234
def relations_for_id(relid=None):
235
"""Get relations of a specific relation ID"""
237
relid = relid or relation_ids()
238
for unit in related_units(relid):
239
unit_data = relation_for_unit(unit, relid)
240
unit_data['__relid__'] = relid
241
relation_data.append(unit_data)
246
def relations_of_type(reltype=None):
247
"""Get relations of a specific type"""
249
reltype = reltype or relation_type()
250
for relid in relation_ids(reltype):
251
for relation in relations_for_id(relid):
252
relation['__relid__'] = relid
253
relation_data.append(relation)
258
def relation_types():
259
"""Get a list of relation types supported by this charm"""
260
charmdir = os.environ.get('CHARM_DIR', '')
261
mdf = open(os.path.join(charmdir, 'metadata.yaml'))
262
md = yaml.safe_load(mdf)
264
for key in ('provides', 'requires', 'peers'):
265
section = md.get(key)
267
rel_types.extend(section.keys())
274
"""Get a nested dictionary of relation data for all related units"""
276
for reltype in relation_types():
278
for relid in relation_ids(reltype):
279
units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
280
for unit in related_units(relid):
281
reldata = relation_get(unit=unit, rid=relid)
282
units[unit] = reldata
283
relids[relid] = units
284
rels[reltype] = relids
289
def is_relation_made(relation, keys='private-address'):
291
Determine whether a relation is established by checking for
292
presence of key(s). If a list of keys is provided, they
293
must all be present for the relation to be identified as made
295
if isinstance(keys, str):
297
for r_id in relation_ids(relation):
298
for unit in related_units(r_id):
301
context[k] = relation_get(k, rid=r_id,
303
if None not in context.values():
308
def open_port(port, protocol="TCP"):
309
"""Open a service network port"""
310
_args = ['open-port']
311
_args.append('{}/{}'.format(port, protocol))
312
subprocess.check_call(_args)
315
def close_port(port, protocol="TCP"):
316
"""Close a service network port"""
317
_args = ['close-port']
318
_args.append('{}/{}'.format(port, protocol))
319
subprocess.check_call(_args)
323
def unit_get(attribute):
324
"""Get the unit ID for the remote unit"""
325
_args = ['unit-get', '--format=json', attribute]
327
return json.loads(subprocess.check_output(_args))
332
def unit_private_ip():
333
"""Get this unit's private IP address"""
334
return unit_get('private-address')
337
class UnregisteredHookError(Exception):
338
"""Raised when an undefined hook is called"""
343
"""A convenient handler for hook functions.
348
# register a hook, taking its name from the function name
353
# register a hook, providing a custom hook name
354
@hooks.hook("config-changed")
355
def config_changed():
358
if __name__ == "__main__":
359
# execute a hook based on the name the program is called by
360
hooks.execute(sys.argv)
364
super(Hooks, self).__init__()
367
def register(self, name, function):
368
"""Register a hook"""
369
self._hooks[name] = function
371
def execute(self, args):
372
"""Execute a registered hook based on args[0]"""
373
hook_name = os.path.basename(args[0])
374
if hook_name in self._hooks:
375
self._hooks[hook_name]()
377
raise UnregisteredHookError(hook_name)
379
def hook(self, *hook_names):
380
"""Decorator, registering them as hooks"""
381
def wrapper(decorated):
382
for hook_name in hook_names:
383
self.register(hook_name, decorated)
385
self.register(decorated.__name__, decorated)
386
if '_' in decorated.__name__:
388
decorated.__name__.replace('_', '-'), decorated)
394
"""Return the root directory of the current charm"""
395
return os.environ.get('CHARM_DIR')