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/>.
17
from six.moves import filter, map
18
from functools import wraps, partial
20
from charmhelpers.core import hookenv
21
from charms.reactive.bus import Handler
22
from charms.reactive.bus import get_states
23
from charms.reactive.bus import _action_id
24
from charms.reactive.relations import RelationBase
25
from charms.reactive.helpers import _hook
26
from charms.reactive.helpers import _when
27
from charms.reactive.helpers import any_file_changed
28
from charms.reactive.helpers import was_invoked
29
from charms.reactive.helpers import mark_invoked
32
def hook(*hook_patterns):
34
Register the decorated function to run when the current hook matches any of
35
the ``hook_patterns``.
37
The hook patterns can use the ``{interface:...}`` and ``{A,B,...}`` syntax
38
supported by :func:`~charms.reactive.bus.any_hook`.
40
If the hook is a relation hook, an instance of that relation class will be
41
passed in to the decorated function.
43
For example, to match any joined or changed hook for the relation providing
44
the ``mysql`` interface::
46
class MySQLRelation(RelationBase):
47
@hook('{provides:mysql}-relation-{joined,changed}')
48
def joined_or_changed(self):
51
Note that hook decorators **cannot** be combined with :func:`when` or
52
:func:`when_not` decorators.
54
def _register(action):
56
# use a generator to defer calling of hookenv.relation_type, for tests
57
rel = RelationBase.from_name(hookenv.relation_type())
61
handler = Handler.get(action)
62
handler.add_predicate(partial(_hook, hook_patterns))
63
handler.add_args(arg_gen())
68
def when(*desired_states):
70
Register the decorated function to run when all ``desired_states`` are active.
72
This decorator will pass zero or more relation instances to the handler, if
73
any of the states are associated with relations. If so, they will be passed
74
in in the same order that the states are given to the decorator.
76
Note that handlers whose conditions match are triggered at least once per
79
def _register(action):
80
handler = Handler.get(action)
81
handler.add_predicate(partial(_when, desired_states, False))
82
handler.add_args(filter(None, map(RelationBase.from_state, desired_states)))
83
handler.register_states(desired_states)
88
def when_not(*desired_states):
90
Register the decorated function to run when **not** all desired_states are active.
92
This decorator will never cause arguments to be passed to the handler.
94
Note that handlers whose conditions match are triggered at least once per
97
def _register(action):
98
handler = Handler.get(action)
99
handler.add_predicate(partial(_when, desired_states, True))
100
handler.register_states(desired_states)
105
def when_file_changed(*filenames, **kwargs):
107
Register the decorated function to run when one or more files have changed.
109
:param list filenames: The names of one or more files to check for changes.
110
:param str hash_type: The type of hash to use for determining if a file has
111
changed. Defaults to 'md5'. Must be given as a kwarg.
113
def _register(action):
114
handler = Handler.get(action)
115
handler.add_predicate(partial(any_file_changed, filenames, **kwargs))
120
def not_unless(*desired_states):
122
Assert that the decorated function can only be called if the desired_states
125
Note that, unlike :func:`when`, this does **not** trigger the decorated
126
function if the states match. It **only** raises an exception if the
127
function is called when the states do not match.
129
This is primarily for informational purposes and as a guard clause.
131
def _decorator(func):
133
def _wrapped(*args, **kwargs):
134
active_states = get_states()
135
missing_states = [state for state in desired_states if state not in active_states]
137
func_id = "%s:%s:%s" % (func.__code__.co_filename,
138
func.__code__.co_firstlineno,
139
func.__code__.co_name)
140
hookenv.log('%s called before state%s: %s' % (
142
's' if len(missing_states) > 1 else '',
143
', '.join(missing_states)), hookenv.WARNING)
144
return func(*args, **kwargs)
149
def only_once(action):
151
Ensure that the decorated function is only executed the first time it is called.
153
This can be used on reactive handlers to ensure that they are only triggered
154
once, even if their conditions continue to match on subsequent calls, even
155
across hook invocations.
158
def wrapper(*args, **kwargs):
159
action_id = _action_id(action)
160
if not was_invoked(action_id):
161
action(*args, **kwargs)
162
mark_invoked(action_id)