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`
50
def __init__(self, name=None, additional_required_keys=None):
53
if additional_required_keys is not None:
54
self.required_keys.extend(additional_required_keys)
59
Returns True if all of the required_keys are available.
61
return self.is_ready()
63
__nonzero__ = __bool__
66
return super(RelationContext, self).__repr__()
70
Returns True if all of the `required_keys` are available from any units.
72
ready = len(self.get(self.name, [])) > 0
74
hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
77
def _is_ready(self, unit_data):
79
Helper method that tests a set of relation data and returns True if
80
all of the `required_keys` are present.
82
return set(unit_data.keys()).issuperset(set(self.required_keys))
86
Retrieve the relation data for each unit involved in a relation and,
87
if complete, store it in a list under `self[self.name]`. This
88
is automatically called when the RelationContext is instantiated.
90
The units are sorted lexographically first by the service ID, then by
91
the unit ID. Thus, if an interface has two other services, 'db:1'
92
and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
93
and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
94
set of data, the relation data for the units will be stored in the
95
order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
97
If you only care about a single unit on the relation, you can just
98
access it as `{{ interface[0]['key'] }}`. However, if you can at all
99
support multiple units on a relation, you should iterate over the list,
102
{% for unit in interface -%}
103
{{ unit['key'] }}{% if not loop.last %},{% endif %}
106
Note that since all sets of relation data from all related services and
107
units are in a single list, if you need to know which service or unit a
108
set of data came from, you'll need to extend this class to preserve
111
if not hookenv.relation_ids(self.name):
114
ns = self.setdefault(self.name, [])
115
for rid in sorted(hookenv.relation_ids(self.name)):
116
for unit in sorted(hookenv.related_units(rid)):
117
reldata = hookenv.relation_get(rid=rid, unit=unit)
118
if self._is_ready(reldata):
121
def provide_data(self):
123
Return data to be relation_set for this interface.
128
class MysqlRelation(RelationContext):
130
Relation context for the `mysql` interface.
132
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
133
:param list additional_required_keys: Extend the list of :attr:`required_keys`
137
required_keys = ['host', 'user', 'password', 'database']
140
class HttpRelation(RelationContext):
142
Relation context for the `http` interface.
144
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
145
:param list additional_required_keys: Extend the list of :attr:`required_keys`
149
required_keys = ['host', 'port']
151
def provide_data(self):
153
'host': hookenv.unit_get('private-address'),
158
class RequiredConfig(dict):
160
Data context that loads config options with one or more mandatory options.
162
Once the required options have been changed from their default values, all
163
config options will be available, namespaced under `config` to prevent
164
potential naming conflicts (for example, between a config option and a
167
:param list *args: List of options that must be changed from their default values.
170
def __init__(self, *args):
171
self.required_options = args
172
self['config'] = hookenv.config()
173
with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
174
self.config = yaml.load(fp).get('options', {})
177
for option in self.required_options:
178
if option not in self['config']:
180
current_value = self['config'][option]
181
default_value = self.config[option].get('default')
182
if current_value == default_value:
184
if current_value in (None, '') and default_value in (None, ''):
188
def __nonzero__(self):
189
return self.__bool__()
192
class StoredContext(dict):
194
A data context that always returns the data that it was first created with.
196
This is useful to do a one-time generation of things like passwords, that
197
will thereafter use the same value that was originally generated, instead
198
of generating a new value each time it is run.
200
def __init__(self, file_name, config_data):
202
If the file exists, populate `self` with the data from the file.
203
Otherwise, populate with the given data and persist it to the file.
205
if os.path.exists(file_name):
206
self.update(self.read_context(file_name))
208
self.store_context(file_name, config_data)
209
self.update(config_data)
211
def store_context(self, file_name, config_data):
212
if not os.path.isabs(file_name):
213
file_name = os.path.join(hookenv.charm_dir(), file_name)
214
with open(file_name, 'w') as file_stream:
215
os.fchmod(file_stream.fileno(), 0o600)
216
yaml.dump(config_data, file_stream)
218
def read_context(self, file_name):
219
if not os.path.isabs(file_name):
220
file_name = os.path.join(hookenv.charm_dir(), file_name)
221
with open(file_name, 'r') as file_stream:
222
data = yaml.load(file_stream)
224
raise OSError("%s is empty" % file_name)
228
class TemplateCallback(ManagerCallback):
230
Callback class that will render a Jinja2 template, for use as a ready
233
:param str source: The template source file, relative to
234
`$CHARM_DIR/templates`
236
:param str target: The target to write the rendered template to
237
:param str owner: The owner of the rendered file
238
:param str group: The group of the rendered file
239
:param int perms: The permissions of the rendered file
241
def __init__(self, source, target,
242
owner='root', group='root', perms=0o444):
249
def __call__(self, manager, service_name, event_name):
250
service = manager.get_service(service_name)
252
for ctx in service.get('required_data', []):
254
templating.render(self.source, self.target, context,
255
self.owner, self.group, self.perms)
258
# Convenience aliases for templates
259
render_template = template = TemplateCallback