3
from charmhelpers.core import hookenv
4
from charmhelpers.core import templating
6
from charmhelpers.core.services.base import ManagerCallback
9
__all__ = ['RelationContext', 'TemplateCallback',
10
'render_template', 'template']
13
class RelationContext(dict):
15
Base class for a context generator that gets relation data from juju.
17
Subclasses must provide the attributes `name`, which is the name of the
18
interface of interest, `interface`, which is the type of the interface of
19
interest, and `required_keys`, which is the set of keys required for the
20
relation to be considered complete. The data for all interfaces matching
21
the `name` attribute that are complete will used to populate the dictionary
22
values (see `get_data`, below).
24
The generated context will be namespaced under the relation :attr:`name`,
25
to prevent potential naming conflicts.
27
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
28
:param list additional_required_keys: Extend the list of :attr:`required_keys`
34
def __init__(self, name=None, additional_required_keys=None):
37
if additional_required_keys is not None:
38
self.required_keys.extend(additional_required_keys)
43
Returns True if all of the required_keys are available.
45
return self.is_ready()
47
__nonzero__ = __bool__
50
return super(RelationContext, self).__repr__()
54
Returns True if all of the `required_keys` are available from any units.
56
ready = len(self.get(self.name, [])) > 0
58
hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
61
def _is_ready(self, unit_data):
63
Helper method that tests a set of relation data and returns True if
64
all of the `required_keys` are present.
66
return set(unit_data.keys()).issuperset(set(self.required_keys))
70
Retrieve the relation data for each unit involved in a relation and,
71
if complete, store it in a list under `self[self.name]`. This
72
is automatically called when the RelationContext is instantiated.
74
The units are sorted lexographically first by the service ID, then by
75
the unit ID. Thus, if an interface has two other services, 'db:1'
76
and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
77
and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
78
set of data, the relation data for the units will be stored in the
79
order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
81
If you only care about a single unit on the relation, you can just
82
access it as `{{ interface[0]['key'] }}`. However, if you can at all
83
support multiple units on a relation, you should iterate over the list,
86
{% for unit in interface -%}
87
{{ unit['key'] }}{% if not loop.last %},{% endif %}
90
Note that since all sets of relation data from all related services and
91
units are in a single list, if you need to know which service or unit a
92
set of data came from, you'll need to extend this class to preserve
95
if not hookenv.relation_ids(self.name):
98
ns = self.setdefault(self.name, [])
99
for rid in sorted(hookenv.relation_ids(self.name)):
100
for unit in sorted(hookenv.related_units(rid)):
101
reldata = hookenv.relation_get(rid=rid, unit=unit)
102
if self._is_ready(reldata):
105
def provide_data(self):
107
Return data to be relation_set for this interface.
112
class MysqlRelation(RelationContext):
114
Relation context for the `mysql` interface.
116
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
117
:param list additional_required_keys: Extend the list of :attr:`required_keys`
121
required_keys = ['host', 'user', 'password', 'database']
124
class HttpRelation(RelationContext):
126
Relation context for the `http` interface.
128
:param str name: Override the relation :attr:`name`, since it can vary from charm to charm
129
:param list additional_required_keys: Extend the list of :attr:`required_keys`
133
required_keys = ['host', 'port']
135
def provide_data(self):
137
'host': hookenv.unit_get('private-address'),
142
class RequiredConfig(dict):
144
Data context that loads config options with one or more mandatory options.
146
Once the required options have been changed from their default values, all
147
config options will be available, namespaced under `config` to prevent
148
potential naming conflicts (for example, between a config option and a
151
:param list *args: List of options that must be changed from their default values.
154
def __init__(self, *args):
155
self.required_options = args
156
self['config'] = hookenv.config()
157
with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
158
self.config = yaml.load(fp).get('options', {})
161
for option in self.required_options:
162
if option not in self['config']:
164
current_value = self['config'][option]
165
default_value = self.config[option].get('default')
166
if current_value == default_value:
168
if current_value in (None, '') and default_value in (None, ''):
172
def __nonzero__(self):
173
return self.__bool__()
176
class StoredContext(dict):
178
A data context that always returns the data that it was first created with.
180
This is useful to do a one-time generation of things like passwords, that
181
will thereafter use the same value that was originally generated, instead
182
of generating a new value each time it is run.
184
def __init__(self, file_name, config_data):
186
If the file exists, populate `self` with the data from the file.
187
Otherwise, populate with the given data and persist it to the file.
189
if os.path.exists(file_name):
190
self.update(self.read_context(file_name))
192
self.store_context(file_name, config_data)
193
self.update(config_data)
195
def store_context(self, file_name, config_data):
196
if not os.path.isabs(file_name):
197
file_name = os.path.join(hookenv.charm_dir(), file_name)
198
with open(file_name, 'w') as file_stream:
199
os.fchmod(file_stream.fileno(), 0o600)
200
yaml.dump(config_data, file_stream)
202
def read_context(self, file_name):
203
if not os.path.isabs(file_name):
204
file_name = os.path.join(hookenv.charm_dir(), file_name)
205
with open(file_name, 'r') as file_stream:
206
data = yaml.load(file_stream)
208
raise OSError("%s is empty" % file_name)
212
class TemplateCallback(ManagerCallback):
214
Callback class that will render a Jinja2 template, for use as a ready
217
:param str source: The template source file, relative to
218
`$CHARM_DIR/templates`
220
:param str target: The target to write the rendered template to
221
:param str owner: The owner of the rendered file
222
:param str group: The group of the rendered file
223
:param int perms: The permissions of the rendered file
225
def __init__(self, source, target,
226
owner='root', group='root', perms=0o444):
233
def __call__(self, manager, service_name, event_name):
234
service = manager.get_service(service_name)
236
for ctx in service.get('required_data', []):
238
templating.render(self.source, self.target, context,
239
self.owner, self.group, self.perms)
242
# Convenience aliases for templates
243
render_template = template = TemplateCallback