~ibm-charms/charms/trusty/nova-compute-power/trunk

« back to all changes in this revision

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

  • Committer: james.page at ubuntu
  • Date: 2015-06-17 09:08:47 UTC
  • mfrom: (74.2.46 nova-compute-power)
  • Revision ID: james.page@ubuntu.com-20150617090847-ork7zgglosiiqs7y
Merge updates for OpenStack Kilo release

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2014-2015 Canonical Limited.
 
2
#
 
3
# This file is part of charm-helpers.
 
4
#
 
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.
 
8
#
 
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.
 
13
#
 
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/>.
 
16
 
1
17
"Interactions with the Juju environment"
2
18
# Copyright 2013 Canonical Ltd.
3
19
#
4
20
# Authors:
5
21
#  Charm Helpers Developers <juju@lists.ubuntu.com>
6
22
 
 
23
from __future__ import print_function
 
24
from functools import wraps
7
25
import os
8
26
import json
9
27
import yaml
10
28
import subprocess
11
29
import sys
12
 
import UserDict
 
30
import errno
 
31
import tempfile
13
32
from subprocess import CalledProcessError
14
33
 
 
34
import six
 
35
if not six.PY3:
 
36
    from UserDict import UserDict
 
37
else:
 
38
    from collections import UserDict
 
39
 
15
40
CRITICAL = "CRITICAL"
16
41
ERROR = "ERROR"
17
42
WARNING = "WARNING"
35
60
 
36
61
    will cache the result of unit_get + 'test' for future calls.
37
62
    """
 
63
    @wraps(func)
38
64
    def wrapper(*args, **kwargs):
39
65
        global cache
40
66
        key = str((func, args, kwargs))
41
67
        try:
42
68
            return cache[key]
43
69
        except KeyError:
44
 
            res = func(*args, **kwargs)
45
 
            cache[key] = res
46
 
            return res
 
70
            pass  # Drop out of the exception handler scope.
 
71
        res = func(*args, **kwargs)
 
72
        cache[key] = res
 
73
        return res
47
74
    return wrapper
48
75
 
49
76
 
63
90
    command = ['juju-log']
64
91
    if level:
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)
68
 
 
69
 
 
70
 
class Serializable(UserDict.IterableUserDict):
 
96
    # Missing juju-log should not cause failures in unit tests
 
97
    # Send log output to stderr
 
98
    try:
 
99
        subprocess.call(command)
 
100
    except OSError as e:
 
101
        if e.errno == errno.ENOENT:
 
102
            if level:
 
103
                message = "{}: {}".format(level, message)
 
104
            message = "juju-log: {}".format(message)
 
105
            print(message, file=sys.stderr)
 
106
        else:
 
107
            raise
 
108
 
 
109
 
 
110
class Serializable(UserDict):
71
111
    """Wrapper, an object that can be serialized to yaml or json"""
72
112
 
73
113
    def __init__(self, obj):
74
114
        # wrap the object
75
 
        UserDict.IterableUserDict.__init__(self)
 
115
        UserDict.__init__(self)
76
116
        self.data = obj
77
117
 
78
118
    def __getattr__(self, attr):
142
182
 
143
183
def remote_unit():
144
184
    """The remote unit for the current relation hook"""
145
 
    return os.environ['JUJU_REMOTE_UNIT']
 
185
    return os.environ.get('JUJU_REMOTE_UNIT', None)
146
186
 
147
187
 
148
188
def service_name():
214
254
        except KeyError:
215
255
            return (self._prev_dict or {})[key]
216
256
 
 
257
    def get(self, key, default=None):
 
258
        try:
 
259
            return self[key]
 
260
        except KeyError:
 
261
            return default
 
262
 
217
263
    def keys(self):
218
264
        prev_keys = []
219
265
        if self._prev_dict is not None:
220
266
            prev_keys = self._prev_dict.keys()
221
 
        return list(set(prev_keys + dict.keys(self)))
 
267
        return list(set(prev_keys + list(dict.keys(self))))
222
268
 
223
269
    def load_previous(self, path=None):
224
270
        """Load previous copy of config from disk.
269
315
 
270
316
        """
271
317
        if self._prev_dict:
272
 
            for k, v in self._prev_dict.iteritems():
 
318
            for k, v in six.iteritems(self._prev_dict):
273
319
                if k not in self:
274
320
                    self[k] = v
275
321
        with open(self.path, 'w') as f:
284
330
        config_cmd_line.append(scope)
285
331
    config_cmd_line.append('--format=json')
286
332
    try:
287
 
        config_data = json.loads(subprocess.check_output(config_cmd_line))
 
333
        config_data = json.loads(
 
334
            subprocess.check_output(config_cmd_line).decode('UTF-8'))
288
335
        if scope is not None:
289
336
            return config_data
290
337
        return Config(config_data)
303
350
    if unit:
304
351
        _args.append(unit)
305
352
    try:
306
 
        return json.loads(subprocess.check_output(_args))
 
353
        return json.loads(subprocess.check_output(_args).decode('UTF-8'))
307
354
    except ValueError:
308
355
        return None
309
 
    except CalledProcessError, e:
 
356
    except CalledProcessError as e:
310
357
        if e.returncode == 2:
311
358
            return None
312
359
        raise
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()):
322
 
        if v is None:
323
 
            relation_cmd_line.append('{}='.format(k))
324
 
        else:
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)
 
377
    if accepts_file:
 
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)
 
387
    else:
 
388
        for key, value in settings.items():
 
389
            if value is None:
 
390
                relation_cmd_line.append('{}='.format(key))
 
391
            else:
 
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())
329
396
 
330
397
 
 
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,
 
401
                            unit=local_unit())
 
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,
 
406
                 **settings)
 
407
 
 
408
 
331
409
@cached
332
410
def relation_ids(reltype=None):
333
411
    """A list of relation_ids"""
335
413
    relid_cmd_line = ['relation-ids', '--format=json']
336
414
    if reltype is not None:
337
415
        relid_cmd_line.append(reltype)
338
 
        return json.loads(subprocess.check_output(relid_cmd_line)) or []
 
416
        return json.loads(
 
417
            subprocess.check_output(relid_cmd_line).decode('UTF-8')) or []
339
418
    return []
340
419
 
341
420
 
346
425
    units_cmd_line = ['relation-list', '--format=json']
347
426
    if relid is not None:
348
427
        units_cmd_line.extend(('-r', relid))
349
 
    return json.loads(subprocess.check_output(units_cmd_line)) or []
 
428
    return json.loads(
 
429
        subprocess.check_output(units_cmd_line).decode('UTF-8')) or []
350
430
 
351
431
 
352
432
@cached
386
466
 
387
467
 
388
468
@cached
 
469
def metadata():
 
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)
 
473
 
 
474
 
 
475
@cached
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)
394
478
    rel_types = []
 
479
    md = metadata()
395
480
    for key in ('provides', 'requires', 'peers'):
396
481
        section = md.get(key)
397
482
        if section:
398
483
            rel_types.extend(section.keys())
399
 
    mdf.close()
400
484
    return rel_types
401
485
 
402
486
 
403
487
@cached
 
488
def charm_name():
 
489
    """Get the name of the current charm as is specified on metadata.yaml"""
 
490
    return metadata().get('name')
 
491
 
 
492
 
 
493
@cached
404
494
def relations():
405
495
    """Get a nested dictionary of relation data for all related units"""
406
496
    rels = {}
455
545
    """Get the unit ID for the remote unit"""
456
546
    _args = ['unit-get', '--format=json', attribute]
457
547
    try:
458
 
        return json.loads(subprocess.check_output(_args))
 
548
        return json.loads(subprocess.check_output(_args).decode('UTF-8'))
459
549
    except ValueError:
460
550
        return None
461
551
 
462
552
 
 
553
def unit_public_ip():
 
554
    """Get this unit's public IP address"""
 
555
    return unit_get('public-address')
 
556
 
 
557
 
463
558
def unit_private_ip():
464
559
    """Get this unit's private IP address"""
465
560
    return unit_get('private-address')
530
625
def charm_dir():
531
626
    """Return the root directory of the current charm"""
532
627
    return os.environ.get('CHARM_DIR')
 
628
 
 
629
 
 
630
@cached
 
631
def action_get(key=None):
 
632
    """Gets the value of an action parameter, or all key/value param pairs"""
 
633
    cmd = ['action-get']
 
634
    if key is not None:
 
635
        cmd.append(key)
 
636
    cmd.append('--format=json')
 
637
    action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
638
    return action_data
 
639
 
 
640
 
 
641
def action_set(values):
 
642
    """Sets the values to be returned after the action finishes"""
 
643
    cmd = ['action-set']
 
644
    for k, v in list(values.items()):
 
645
        cmd.append('{}={}'.format(k, v))
 
646
    subprocess.check_call(cmd)
 
647
 
 
648
 
 
649
def action_fail(message):
 
650
    """Sets the action status to failed and sets the error message.
 
651
 
 
652
    The results set by action_set are preserved."""
 
653
    subprocess.check_call(['action-fail', message])
 
654
 
 
655
 
 
656
def status_set(workload_state, message):
 
657
    """Set the workload state with a message
 
658
 
 
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.
 
662
 
 
663
    workload_state -- valid juju workload state.
 
664
    message        -- status update message
 
665
    """
 
666
    valid_states = ['maintenance', 'blocked', 'waiting', 'active']
 
667
    if workload_state not in valid_states:
 
668
        raise ValueError(
 
669
            '{!r} is not a valid workload state'.format(workload_state)
 
670
        )
 
671
    cmd = ['status-set', workload_state, message]
 
672
    try:
 
673
        ret = subprocess.call(cmd)
 
674
        if ret == 0:
 
675
            return
 
676
    except OSError as e:
 
677
        if e.errno != errno.ENOENT:
 
678
            raise
 
679
    log_message = 'status-set failed: {} {}'.format(workload_state,
 
680
                                                    message)
 
681
    log(log_message, level='INFO')
 
682
 
 
683
 
 
684
def status_get():
 
685
    """Retrieve the previously set juju workload state
 
686
 
 
687
    If the status-set command is not found then assume this is juju < 1.23 and
 
688
    return 'unknown'
 
689
    """
 
690
    cmd = ['status-get']
 
691
    try:
 
692
        raw_status = subprocess.check_output(cmd, universal_newlines=True)
 
693
        status = raw_status.rstrip()
 
694
        return status
 
695
    except OSError as e:
 
696
        if e.errno == errno.ENOENT:
 
697
            return 'unknown'
 
698
        else:
 
699
            raise
 
700
 
 
701
 
 
702
def translate_exc(from_exc, to_exc):
 
703
    def inner_translate_exc1(f):
 
704
        def inner_translate_exc2(*args, **kwargs):
 
705
            try:
 
706
                return f(*args, **kwargs)
 
707
            except from_exc:
 
708
                raise to_exc
 
709
 
 
710
        return inner_translate_exc2
 
711
 
 
712
    return inner_translate_exc1
 
713
 
 
714
 
 
715
@translate_exc(from_exc=OSError, to_exc=NotImplementedError)
 
716
def is_leader():
 
717
    """Does the current unit hold the juju leadership
 
718
 
 
719
    Uses juju to determine whether the current unit is the leader of its peers
 
720
    """
 
721
    cmd = ['is-leader', '--format=json']
 
722
    return json.loads(subprocess.check_output(cmd).decode('UTF-8'))
 
723
 
 
724
 
 
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'))
 
730
 
 
731
 
 
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)
 
736
    cmd = ['leader-set']
 
737
    settings = settings or {}
 
738
    settings.update(kwargs)
 
739
    for k, v in settings.iteritems():
 
740
        if v is None:
 
741
            cmd.append('{}='.format(k))
 
742
        else:
 
743
            cmd.append('{}={}'.format(k, v))
 
744
    subprocess.check_call(cmd)