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/>.
19
from charmhelpers.core import hookenv
20
from charmhelpers.core import templating
22
from charmhelpers.core.services.base import ManagerCallback
25
__all__ = ['RelationContext', 'TemplateCallback',
26
'render_template', 'template']
29
class RelationContext(dict):
31
Base class for a context generator that gets relation data from juju.
33
Subclasses must provide the attributes `name`, which is the name of the
34
interface of interest, `interface`, which is the type of the interface of
35
interest, and `required_keys`, which is the set of keys required for the
36
relation to be considered complete. The data for all interfaces matching
37
the `name` attribute that are complete will used to populate the dictionary
38
values (see `get_data`, below).
40
The generated context will be namespaced under the relation :attr:`name`,
41
to prevent potential naming conflicts.
43
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
44
:param list additional_required_keys: Extend the list of :attr:`required_keys`
49
def __init__(self, name=None, additional_required_keys=None):
50
if not hasattr(self, 'required_keys'):
51
self.required_keys = []
55
if additional_required_keys:
56
self.required_keys.extend(additional_required_keys)
61
Returns True if all of the required_keys are available.
63
return self.is_ready()
65
__nonzero__ = __bool__
68
return super(RelationContext, self).__repr__()
72
Returns True if all of the `required_keys` are available from any units.
74
ready = len(self.get(self.name, [])) > 0
76
hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
79
def _is_ready(self, unit_data):
81
Helper method that tests a set of relation data and returns True if
82
all of the `required_keys` are present.
84
return set(unit_data.keys()).issuperset(set(self.required_keys))
88
Retrieve the relation data for each unit involved in a relation and,
89
if complete, store it in a list under `self[self.name]`. This
90
is automatically called when the RelationContext is instantiated.
92
The units are sorted lexographically first by the service ID, then by
93
the unit ID. Thus, if an interface has two other services, 'db:1'
94
and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
95
and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
96
set of data, the relation data for the units will be stored in the
97
order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
99
If you only care about a single unit on the relation, you can just
100
access it as `{{ interface[0]['key'] }}`. However, if you can at all
101
support multiple units on a relation, you should iterate over the list,
104
{% for unit in interface -%}
105
{{ unit['key'] }}{% if not loop.last %},{% endif %}
108
Note that since all sets of relation data from all related services and
109
units are in a single list, if you need to know which service or unit a
110
set of data came from, you'll need to extend this class to preserve
113
if not hookenv.relation_ids(self.name):
116
ns = self.setdefault(self.name, [])
117
for rid in sorted(hookenv.relation_ids(self.name)):
118
for unit in sorted(hookenv.related_units(rid)):
119
reldata = hookenv.relation_get(rid=rid, unit=unit)
120
if self._is_ready(reldata):
123
def provide_data(self):
125
Return data to be relation_set for this interface.
130
class MysqlRelation(RelationContext):
132
Relation context for the `mysql` interface.
134
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
135
:param list additional_required_keys: Extend the list of :attr:`required_keys`
140
def __init__(self, *args, **kwargs):
141
self.required_keys = ['host', 'user', 'password', 'database']
142
RelationContext.__init__(self, *args, **kwargs)
145
class HttpRelation(RelationContext):
147
Relation context for the `http` interface.
149
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
150
:param list additional_required_keys: Extend the list of :attr:`required_keys`
155
def __init__(self, *args, **kwargs):
156
self.required_keys = ['host', 'port']
157
RelationContext.__init__(self, *args, **kwargs)
159
def provide_data(self):
161
'host': hookenv.unit_get('private-address'),
166
class RequiredConfig(dict):
168
Data context that loads config options with one or more mandatory options.
170
Once the required options have been changed from their default values, all
171
config options will be available, namespaced under `config` to prevent
172
potential naming conflicts (for example, between a config option and a
175
:param list *args: List of options that must be changed from their default values.
178
def __init__(self, *args):
179
self.required_options = args
180
self['config'] = hookenv.config()
181
with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
182
self.config = yaml.load(fp).get('options', {})
185
for option in self.required_options:
186
if option not in self['config']:
188
current_value = self['config'][option]
189
default_value = self.config[option].get('default')
190
if current_value == default_value:
192
if current_value in (None, '') and default_value in (None, ''):
196
def __nonzero__(self):
197
return self.__bool__()
200
class StoredContext(dict):
202
A data context that always returns the data that it was first created with.
204
This is useful to do a one-time generation of things like passwords, that
205
will thereafter use the same value that was originally generated, instead
206
of generating a new value each time it is run.
208
def __init__(self, file_name, config_data):
210
If the file exists, populate `self` with the data from the file.
211
Otherwise, populate with the given data and persist it to the file.
213
if os.path.exists(file_name):
214
self.update(self.read_context(file_name))
216
self.store_context(file_name, config_data)
217
self.update(config_data)
219
def store_context(self, file_name, config_data):
220
if not os.path.isabs(file_name):
221
file_name = os.path.join(hookenv.charm_dir(), file_name)
222
with open(file_name, 'w') as file_stream:
223
os.fchmod(file_stream.fileno(), 0o600)
224
yaml.dump(config_data, file_stream)
226
def read_context(self, file_name):
227
if not os.path.isabs(file_name):
228
file_name = os.path.join(hookenv.charm_dir(), file_name)
229
with open(file_name, 'r') as file_stream:
230
data = yaml.load(file_stream)
232
raise OSError("%s is empty" % file_name)
236
class TemplateCallback(ManagerCallback):
238
Callback class that will render a Jinja2 template, for use as a ready
241
:param str source: The template source file, relative to
242
`$CHARM_DIR/templates`
244
:param str target: The target to write the rendered template to
245
:param str owner: The owner of the rendered file
246
:param str group: The group of the rendered file
247
:param int perms: The permissions of the rendered file
249
def __init__(self, source, target,
250
owner='root', group='root', perms=0o444):
257
def __call__(self, manager, service_name, event_name):
258
service = manager.get_service(service_name)
260
for ctx in service.get('required_data', []):
262
templating.render(self.source, self.target, context,
263
self.owner, self.group, self.perms)
266
# Convenience aliases for templates
267
render_template = template = TemplateCallback