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
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)
132
"The relation ID for the current relation hook"
133
return os.environ.get('JUJU_RELATION_ID', None)
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:
187
raise ValueError('Must specify neither or both of relation_name and service_or_unit')
136
190
def local_unit():
138
192
return os.environ['JUJU_UNIT_NAME']
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)
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]
206
def remote_service_name(relid=None):
207
"""The remote service name for a given relation-id (or the current relation)"""
211
units = related_units(relid)
212
unit = units[0] if units else None
213
return unit.split('/')[0] if unit else None
217
"""The name of the currently executing hook"""
218
return os.environ.get('JUJU_HOOK_NAME', os.path.basename(sys.argv[0]))
222
"""A dictionary representation of the charm's config.yaml, with some
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.
229
NOTE: Do not instantiate this object directly - instead call
230
``hookenv.config()``, which will return an instance of :class:`Config`.
235
>>> from charmhelpers.core import hookenv
236
>>> config = hookenv.config()
239
>>> # store a new key/value for later use
240
>>> config['mykey'] = 'myval'
243
>>> # user runs `juju set mycharm foo=baz`
244
>>> # now we're inside subsequent config-changed hook
245
>>> config = hookenv.config()
248
>>> # test to see if this val has changed since last hook
249
>>> config.changed('foo')
251
>>> # what was the previous value?
252
>>> config.previous('foo')
254
>>> # keys/values that we add are preserved across hooks
259
CONFIG_FILE_NAME = '.juju-persistent-config'
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):
268
atexit(self._implicit_save)
270
def load_previous(self, path=None):
271
"""Load previous copy of config from disk.
273
In normal usage you don't need to call this method directly - it
274
is called automatically at object initialization.
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
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():
291
def changed(self, key):
292
"""Return True if the current value for this key is different from
296
if self._prev_dict is None:
298
return self.previous(key) != self.get(key)
300
def previous(self, key):
301
"""Return previous value for this key, or None if there
302
is no previous value.
306
return self._prev_dict.get(key)
310
"""Save this config to disk.
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.
317
To disable automatic saves, set ``implicit_save=False`` on this
321
with open(self.path, 'w') as f:
324
def _implicit_save(self):
325
if self.implicit_save:
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')
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:
341
return Config(config_data)
160
342
except ValueError:
165
347
def relation_get(attribute=None, unit=None, rid=None):
348
"""Get relation information"""
166
349
_args = ['relation-get', '--format=json']
168
351
_args.append('-r')
172
355
_args.append(unit)
174
return json.loads(subprocess.check_output(_args))
357
return json.loads(subprocess.check_output(_args).decode('UTF-8'))
175
358
except ValueError:
179
def relation_set(relation_id=None, relation_settings={}, **kwargs):
360
except CalledProcessError as e:
361
if e.returncode == 2:
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()):
185
relation_cmd_line.append('{}='.format(k))
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)
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)
392
for key, value in settings.items():
394
relation_cmd_line.append('{}='.format(key))
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())
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,
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,
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 []
421
subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
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 []
433
subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
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:
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)
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"""
257
484
for key in ('provides', 'requires', 'peers'):
258
485
section = md.get(key)
260
487
rel_types.extend(section.keys())
492
def peer_relation_id():
493
'''Get the peers relation id if a peers relation has been joined, else None.'''
495
section = md.get('peers')
498
relids = relation_ids(key)
505
def relation_to_interface(relation_name):
507
Given the name of a relation, return the interface that relation uses.
509
:returns: The interface name, or ``None``.
511
return relation_to_role_and_interface(relation_name)[1]
515
def relation_to_role_and_interface(relation_name):
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``).
520
:returns: A tuple containing ``(role, interface)``, or ``(None, None)``.
522
_metadata = metadata()
523
for role in ('provides', 'requires', 'peers'):
524
interface = _metadata.get(role, {}).get(relation_name, {}).get('interface')
526
return role, interface
531
def role_and_interface_to_relations(role, interface_name):
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``).
537
:returns: A list of relation names.
539
_metadata = metadata()
541
for relation_name, relation in _metadata.get(role, {}).items():
542
if relation['interface'] == interface_name:
543
results.append(relation_name)
548
def interface_to_relations(interface_name):
550
Given an interface, return a list of relation names for the current
551
charm that use that interface.
553
:returns: A list of relation names.
556
for role in ('provides', 'requires', 'peers'):
557
results.extend(role_and_interface_to_relations(role, interface_name))
563
"""Get the name of the current charm as is specified on metadata.yaml"""
564
return metadata().get('name')
569
"""Get a nested dictionary of relation data for all related units"""
268
571
for reltype in relation_types():
295
618
def unit_get(attribute):
619
"""Get the unit ID for the remote unit"""
296
620
_args = ['unit-get', '--format=json', attribute]
298
return json.loads(subprocess.check_output(_args))
622
return json.loads(subprocess.check_output(_args).decode('UTF-8'))
299
623
except ValueError:
627
def unit_public_ip():
628
"""Get this unit's public IP address"""
629
return unit_get('public-address')
303
632
def unit_private_ip():
633
"""Get this unit's private IP address"""
304
634
return unit_get('private-address')
638
def storage_get(attribute=None, storage_id=None):
639
"""Get storage attributes"""
640
_args = ['storage-get', '--format=json']
642
_args.extend(('-s', storage_id))
644
_args.append(attribute)
646
return json.loads(subprocess.check_output(_args).decode('UTF-8'))
652
def storage_list(storage_name=None):
653
"""List the storage IDs for the unit"""
654
_args = ['storage-list', '--format=json']
656
_args.append(storage_name)
658
return json.loads(subprocess.check_output(_args).decode('UTF-8'))
663
if e.errno == errno.ENOENT:
664
# storage-list does not exist
307
669
class UnregisteredHookError(Exception):
670
"""Raised when an undefined hook is called"""
311
674
class Hooks(object):
675
"""A convenient handler for hook functions.
681
# register a hook, taking its name from the function name
684
pass # your code here
686
# register a hook, providing a custom hook name
687
@hooks.hook("config-changed")
688
def config_changed():
689
pass # your code here
691
if __name__ == "__main__":
692
# execute a hook based on the name the program is called by
693
hooks.execute(sys.argv)
696
def __init__(self, config_save=None):
313
697
super(Hooks, self).__init__()
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
316
705
def register(self, name, function):
706
"""Register a hook"""
317
707
self._hooks[name] = function
319
709
def execute(self, args):
710
"""Execute a registered hook based on args[0]"""
320
712
hook_name = os.path.basename(args[0])
321
713
if hook_name in self._hooks:
322
self._hooks[hook_name]()
715
self._hooks[hook_name]()
716
except SystemExit as x:
717
if x.code is None or x.code == 0:
324
722
raise UnregisteredHookError(hook_name)
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)
739
"""Return the root directory of the current charm"""
340
740
return os.environ.get('CHARM_DIR')
744
def action_get(key=None):
745
"""Gets the value of an action parameter, or all key/value param pairs"""
749
cmd.append('--format=json')
750
action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
754
def action_set(values):
755
"""Sets the values to be returned after the action finishes"""
757
for k, v in list(values.items()):
758
cmd.append('{}={}'.format(k, v))
759
subprocess.check_call(cmd)
762
def action_fail(message):
763
"""Sets the action status to failed and sets the error message.
765
The results set by action_set are preserved."""
766
subprocess.check_call(['action-fail', message])
770
"""Get the name of the currently executing action."""
771
return os.environ.get('JUJU_ACTION_NAME')
775
"""Get the UUID of the currently executing action."""
776
return os.environ.get('JUJU_ACTION_UUID')
780
"""Get the tag for the currently executing action."""
781
return os.environ.get('JUJU_ACTION_TAG')
784
def status_set(workload_state, message):
785
"""Set the workload state with a message
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.
791
workload_state -- valid juju workload state.
792
message -- status update message
794
valid_states = ['maintenance', 'blocked', 'waiting', 'active']
795
if workload_state not in valid_states:
797
'{!r} is not a valid workload state'.format(workload_state)
799
cmd = ['status-set', workload_state, message]
801
ret = subprocess.call(cmd)
805
if e.errno != errno.ENOENT:
807
log_message = 'status-set failed: {} {}'.format(workload_state,
809
log(log_message, level='INFO')
813
"""Retrieve the previously set juju workload state and message
815
If the status-get command is not found then assume this is juju < 1.23 and
819
cmd = ['status-get', "--format=json", "--include-data"]
821
raw_status = subprocess.check_output(cmd)
823
if e.errno == errno.ENOENT:
824
return ('unknown', "")
828
status = json.loads(raw_status.decode("UTF-8"))
829
return (status["status"], status["message"])
832
def translate_exc(from_exc, to_exc):
833
def inner_translate_exc1(f):
835
def inner_translate_exc2(*args, **kwargs):
837
return f(*args, **kwargs)
841
return inner_translate_exc2
843
return inner_translate_exc1
846
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
848
"""Does the current unit hold the juju leadership
850
Uses juju to determine whether the current unit is the leader of its peers
852
cmd = ['is-leader', '--format=json']
853
return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
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'))
863
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
864
def leader_set(settings=None, **kwargs):
865
"""Juju leader set value(s)"""
867
# log("Juju leader-set '%s'" % (settings), level=DEBUG)
869
settings = settings or {}
870
settings.update(kwargs)
871
for k, v in settings.items():
873
cmd.append('{}='.format(k))
875
cmd.append('{}={}'.format(k, v))
876
subprocess.check_call(cmd)
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]:
886
subprocess.check_call(cmd)
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
895
cmd = ['payload-unregister']
896
for x in [klass, pid]:
898
subprocess.check_call(cmd)
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]:
910
subprocess.check_call(cmd)
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.
917
<name> must match a name of defined resource in metadata.yaml
919
returns either a path or False if resource not available
924
cmd = ['resource-get', name]
926
return subprocess.check_output(cmd).decode('UTF-8')
927
except subprocess.CalledProcessError:
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()
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)
950
def atstart(callback, *args, **kwargs):
951
'''Schedule a callback to run before the main hook.
953
Callbacks are run in the order they were added.
955
This is useful for modules and classes to perform initialization
956
and inject behavior. In particular:
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.
967
This is not at all useful after your hook framework as been launched.
970
_atstart.append((callback, args, kwargs))
973
def atexit(callback, *args, **kwargs):
974
'''Schedule a callback to run on successful hook completion.
976
Callbacks are run in the reverse order that they were added.'''
977
_atexit.append((callback, args, kwargs))
981
'''Hook frameworks must invoke this before running the main hook body.'''
983
for callback, args, kwargs in _atstart:
984
callback(*args, **kwargs)
989
'''Hook frameworks must invoke this after the main hook body has
990
successfully completed. Do not invoke it if the hook fails.'''
992
for callback, args, kwargs in reversed(_atexit):
993
callback(*args, **kwargs)
997
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
998
def network_get_primary_address(binding):
1000
Retrieve the primary network address for a named binding
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
1006
cmd = ['network-get', '--primary-address', binding]
1007
return subprocess.check_output(cmd).decode('UTF-8').strip()