1
# Copyright 2014-2015 Canonical Limited.
3
# This file is part of charm-helpers.
5
# charm-helpers is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU Lesser General Public License version 3 as
7
# published by the Free Software Foundation.
9
# charm-helpers is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public License
15
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
1
17
"Interactions with the Juju environment"
2
18
# Copyright 2013 Canonical Ltd.
5
21
# Charm Helpers Developers <juju@lists.ubuntu.com>
23
from __future__ import print_function
24
from functools import wraps
13
32
from subprocess import CalledProcessError
36
from UserDict import UserDict
38
from collections import UserDict
15
40
CRITICAL = "CRITICAL"
17
42
WARNING = "WARNING"
63
90
command = ['juju-log']
65
92
command += ['-l', level]
93
if not isinstance(message, six.string_types):
94
message = repr(message)
66
95
command += [message]
67
subprocess.call(command)
70
class Serializable(UserDict.IterableUserDict):
96
# Missing juju-log should not cause failures in unit tests
97
# Send log output to stderr
99
subprocess.call(command)
101
if e.errno == errno.ENOENT:
103
message = "{}: {}".format(level, message)
104
message = "juju-log: {}".format(message)
105
print(message, file=sys.stderr)
110
class Serializable(UserDict):
71
111
"""Wrapper, an object that can be serialized to yaml or json"""
73
113
def __init__(self, obj):
75
UserDict.IterableUserDict.__init__(self)
115
UserDict.__init__(self)
78
118
def __getattr__(self, attr):
316
363
"""Set relation information for the current unit"""
317
364
relation_settings = relation_settings if relation_settings else {}
318
365
relation_cmd_line = ['relation-set']
366
accepts_file = "--file" in subprocess.check_output(
367
relation_cmd_line + ["--help"], universal_newlines=True)
319
368
if relation_id is not None:
320
369
relation_cmd_line.extend(('-r', relation_id))
321
for k, v in (relation_settings.items() + kwargs.items()):
323
relation_cmd_line.append('{}='.format(k))
325
relation_cmd_line.append('{}={}'.format(k, v))
326
subprocess.check_call(relation_cmd_line)
370
settings = relation_settings.copy()
371
settings.update(kwargs)
372
for key, value in settings.items():
373
# Force value to be a string: it always should, but some call
374
# sites pass in things like dicts or numbers.
375
if value is not None:
376
settings[key] = "{}".format(value)
378
# --file was introduced in Juju 1.23.2. Use it by default if
379
# available, since otherwise we'll break if the relation data is
380
# too big. Ideally we should tell relation-set to read the data from
381
# stdin, but that feature is broken in 1.23.2: Bug #1454678.
382
with tempfile.NamedTemporaryFile(delete=False) as settings_file:
383
settings_file.write(yaml.safe_dump(settings).encode("utf-8"))
384
subprocess.check_call(
385
relation_cmd_line + ["--file", settings_file.name])
386
os.remove(settings_file.name)
388
for key, value in settings.items():
390
relation_cmd_line.append('{}='.format(key))
392
relation_cmd_line.append('{}={}'.format(key, value))
393
subprocess.check_call(relation_cmd_line)
327
394
# Flush cache of any relation-gets for local unit
328
395
flush(local_unit())
398
def relation_clear(r_id=None):
399
''' Clears any relation data already set on relation r_id '''
400
settings = relation_get(rid=r_id,
402
for setting in settings:
403
if setting not in ['public-address', 'private-address']:
404
settings[setting] = None
405
relation_set(relation_id=r_id,
332
410
def relation_ids(reltype=None):
333
411
"""A list of relation_ids"""
470
"""Get the current charm metadata.yaml contents as a python object"""
471
with open(os.path.join(charm_dir(), 'metadata.yaml')) as md:
472
return yaml.safe_load(md)
389
476
def relation_types():
390
477
"""Get a list of relation types supported by this charm"""
391
charmdir = os.environ.get('CHARM_DIR', '')
392
mdf = open(os.path.join(charmdir, 'metadata.yaml'))
393
md = yaml.safe_load(mdf)
395
480
for key in ('provides', 'requires', 'peers'):
396
481
section = md.get(key)
398
483
rel_types.extend(section.keys())
489
"""Get the name of the current charm as is specified on metadata.yaml"""
490
return metadata().get('name')
405
495
"""Get a nested dictionary of relation data for all related units"""
531
626
"""Return the root directory of the current charm"""
532
627
return os.environ.get('CHARM_DIR')
631
def action_get(key=None):
632
"""Gets the value of an action parameter, or all key/value param pairs"""
636
cmd.append('--format=json')
637
action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
641
def action_set(values):
642
"""Sets the values to be returned after the action finishes"""
644
for k, v in list(values.items()):
645
cmd.append('{}={}'.format(k, v))
646
subprocess.check_call(cmd)
649
def action_fail(message):
650
"""Sets the action status to failed and sets the error message.
652
The results set by action_set are preserved."""
653
subprocess.check_call(['action-fail', message])
656
def status_set(workload_state, message):
657
"""Set the workload state with a message
659
Use status-set to set the workload state with a message which is visible
660
to the user via juju status. If the status-set command is not found then
661
assume this is juju < 1.23 and juju-log the message unstead.
663
workload_state -- valid juju workload state.
664
message -- status update message
666
valid_states = ['maintenance', 'blocked', 'waiting', 'active']
667
if workload_state not in valid_states:
669
'{!r} is not a valid workload state'.format(workload_state)
671
cmd = ['status-set', workload_state, message]
673
ret = subprocess.call(cmd)
677
if e.errno != errno.ENOENT:
679
log_message = 'status-set failed: {} {}'.format(workload_state,
681
log(log_message, level='INFO')
685
"""Retrieve the previously set juju workload state
687
If the status-set command is not found then assume this is juju < 1.23 and
692
raw_status = subprocess.check_output(cmd, universal_newlines=True)
693
status = raw_status.rstrip()
696
if e.errno == errno.ENOENT:
702
def translate_exc(from_exc, to_exc):
703
def inner_translate_exc1(f):
704
def inner_translate_exc2(*args, **kwargs):
706
return f(*args, **kwargs)
710
return inner_translate_exc2
712
return inner_translate_exc1
715
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
717
"""Does the current unit hold the juju leadership
719
Uses juju to determine whether the current unit is the leader of its peers
721
cmd = ['is-leader', '--format=json']
722
return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
725
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
726
def leader_get(attribute=None):
727
"""Juju leader get value(s)"""
728
cmd = ['leader-get', '--format=json'] + [attribute or '-']
729
return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
732
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
733
def leader_set(settings=None, **kwargs):
734
"""Juju leader set value(s)"""
735
log("Juju leader-set '%s'" % (settings), level=DEBUG)
737
settings = settings or {}
738
settings.update(kwargs)
739
for k, v in settings.iteritems():
741
cmd.append('{}='.format(k))
743
cmd.append('{}={}'.format(k, v))
744
subprocess.check_call(cmd)