~cjwatson/charms/precise/squid-forwardproxy/dns-v4-first

« back to all changes in this revision

Viewing changes to hooks/charmhelpers/core/services/helpers.py

  • Committer: Kit Randel
  • Date: 2015-12-17 22:37:59 UTC
  • mfrom: (27.2.42 squid-forwardproxy)
  • Revision ID: kit.randel@canonical.com-20151217223759-rmrf1i290ld1l0wn
Manage local unit state with charmhelpers unitdata.kv.

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
 
 
17
import os
 
18
import yaml
 
19
 
 
20
from charmhelpers.core import hookenv
 
21
from charmhelpers.core import host
 
22
from charmhelpers.core import templating
 
23
 
 
24
from charmhelpers.core.services.base import ManagerCallback
 
25
 
 
26
 
 
27
__all__ = ['RelationContext', 'TemplateCallback',
 
28
           'render_template', 'template']
 
29
 
 
30
 
 
31
class RelationContext(dict):
 
32
    """
 
33
    Base class for a context generator that gets relation data from juju.
 
34
 
 
35
    Subclasses must provide the attributes `name`, which is the name of the
 
36
    interface of interest, `interface`, which is the type of the interface of
 
37
    interest, and `required_keys`, which is the set of keys required for the
 
38
    relation to be considered complete.  The data for all interfaces matching
 
39
    the `name` attribute that are complete will used to populate the dictionary
 
40
    values (see `get_data`, below).
 
41
 
 
42
    The generated context will be namespaced under the relation :attr:`name`,
 
43
    to prevent potential naming conflicts.
 
44
 
 
45
    :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
 
46
    :param list additional_required_keys: Extend the list of :attr:`required_keys`
 
47
    """
 
48
    name = None
 
49
    interface = None
 
50
 
 
51
    def __init__(self, name=None, additional_required_keys=None):
 
52
        if not hasattr(self, 'required_keys'):
 
53
            self.required_keys = []
 
54
 
 
55
        if name is not None:
 
56
            self.name = name
 
57
        if additional_required_keys:
 
58
            self.required_keys.extend(additional_required_keys)
 
59
        self.get_data()
 
60
 
 
61
    def __bool__(self):
 
62
        """
 
63
        Returns True if all of the required_keys are available.
 
64
        """
 
65
        return self.is_ready()
 
66
 
 
67
    __nonzero__ = __bool__
 
68
 
 
69
    def __repr__(self):
 
70
        return super(RelationContext, self).__repr__()
 
71
 
 
72
    def is_ready(self):
 
73
        """
 
74
        Returns True if all of the `required_keys` are available from any units.
 
75
        """
 
76
        ready = len(self.get(self.name, [])) > 0
 
77
        if not ready:
 
78
            hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
 
79
        return ready
 
80
 
 
81
    def _is_ready(self, unit_data):
 
82
        """
 
83
        Helper method that tests a set of relation data and returns True if
 
84
        all of the `required_keys` are present.
 
85
        """
 
86
        return set(unit_data.keys()).issuperset(set(self.required_keys))
 
87
 
 
88
    def get_data(self):
 
89
        """
 
90
        Retrieve the relation data for each unit involved in a relation and,
 
91
        if complete, store it in a list under `self[self.name]`.  This
 
92
        is automatically called when the RelationContext is instantiated.
 
93
 
 
94
        The units are sorted lexographically first by the service ID, then by
 
95
        the unit ID.  Thus, if an interface has two other services, 'db:1'
 
96
        and 'db:2', with 'db:1' having two units, 'wordpress/0' and 'wordpress/1',
 
97
        and 'db:2' having one unit, 'mediawiki/0', all of which have a complete
 
98
        set of data, the relation data for the units will be stored in the
 
99
        order: 'wordpress/0', 'wordpress/1', 'mediawiki/0'.
 
100
 
 
101
        If you only care about a single unit on the relation, you can just
 
102
        access it as `{{ interface[0]['key'] }}`.  However, if you can at all
 
103
        support multiple units on a relation, you should iterate over the list,
 
104
        like::
 
105
 
 
106
            {% for unit in interface -%}
 
107
                {{ unit['key'] }}{% if not loop.last %},{% endif %}
 
108
            {%- endfor %}
 
109
 
 
110
        Note that since all sets of relation data from all related services and
 
111
        units are in a single list, if you need to know which service or unit a
 
112
        set of data came from, you'll need to extend this class to preserve
 
113
        that information.
 
114
        """
 
115
        if not hookenv.relation_ids(self.name):
 
116
            return
 
117
 
 
118
        ns = self.setdefault(self.name, [])
 
119
        for rid in sorted(hookenv.relation_ids(self.name)):
 
120
            for unit in sorted(hookenv.related_units(rid)):
 
121
                reldata = hookenv.relation_get(rid=rid, unit=unit)
 
122
                if self._is_ready(reldata):
 
123
                    ns.append(reldata)
 
124
 
 
125
    def provide_data(self):
 
126
        """
 
127
        Return data to be relation_set for this interface.
 
128
        """
 
129
        return {}
 
130
 
 
131
 
 
132
class MysqlRelation(RelationContext):
 
133
    """
 
134
    Relation context for the `mysql` interface.
 
135
 
 
136
    :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
 
137
    :param list additional_required_keys: Extend the list of :attr:`required_keys`
 
138
    """
 
139
    name = 'db'
 
140
    interface = 'mysql'
 
141
 
 
142
    def __init__(self, *args, **kwargs):
 
143
        self.required_keys = ['host', 'user', 'password', 'database']
 
144
        RelationContext.__init__(self, *args, **kwargs)
 
145
 
 
146
 
 
147
class HttpRelation(RelationContext):
 
148
    """
 
149
    Relation context for the `http` interface.
 
150
 
 
151
    :param str name: Override the relation :attr:`name`, since it can vary from charm to charm
 
152
    :param list additional_required_keys: Extend the list of :attr:`required_keys`
 
153
    """
 
154
    name = 'website'
 
155
    interface = 'http'
 
156
 
 
157
    def __init__(self, *args, **kwargs):
 
158
        self.required_keys = ['host', 'port']
 
159
        RelationContext.__init__(self, *args, **kwargs)
 
160
 
 
161
    def provide_data(self):
 
162
        return {
 
163
            'host': hookenv.unit_get('private-address'),
 
164
            'port': 80,
 
165
        }
 
166
 
 
167
 
 
168
class RequiredConfig(dict):
 
169
    """
 
170
    Data context that loads config options with one or more mandatory options.
 
171
 
 
172
    Once the required options have been changed from their default values, all
 
173
    config options will be available, namespaced under `config` to prevent
 
174
    potential naming conflicts (for example, between a config option and a
 
175
    relation property).
 
176
 
 
177
    :param list *args: List of options that must be changed from their default values.
 
178
    """
 
179
 
 
180
    def __init__(self, *args):
 
181
        self.required_options = args
 
182
        self['config'] = hookenv.config()
 
183
        with open(os.path.join(hookenv.charm_dir(), 'config.yaml')) as fp:
 
184
            self.config = yaml.load(fp).get('options', {})
 
185
 
 
186
    def __bool__(self):
 
187
        for option in self.required_options:
 
188
            if option not in self['config']:
 
189
                return False
 
190
            current_value = self['config'][option]
 
191
            default_value = self.config[option].get('default')
 
192
            if current_value == default_value:
 
193
                return False
 
194
            if current_value in (None, '') and default_value in (None, ''):
 
195
                return False
 
196
        return True
 
197
 
 
198
    def __nonzero__(self):
 
199
        return self.__bool__()
 
200
 
 
201
 
 
202
class StoredContext(dict):
 
203
    """
 
204
    A data context that always returns the data that it was first created with.
 
205
 
 
206
    This is useful to do a one-time generation of things like passwords, that
 
207
    will thereafter use the same value that was originally generated, instead
 
208
    of generating a new value each time it is run.
 
209
    """
 
210
    def __init__(self, file_name, config_data):
 
211
        """
 
212
        If the file exists, populate `self` with the data from the file.
 
213
        Otherwise, populate with the given data and persist it to the file.
 
214
        """
 
215
        if os.path.exists(file_name):
 
216
            self.update(self.read_context(file_name))
 
217
        else:
 
218
            self.store_context(file_name, config_data)
 
219
            self.update(config_data)
 
220
 
 
221
    def store_context(self, file_name, config_data):
 
222
        if not os.path.isabs(file_name):
 
223
            file_name = os.path.join(hookenv.charm_dir(), file_name)
 
224
        with open(file_name, 'w') as file_stream:
 
225
            os.fchmod(file_stream.fileno(), 0o600)
 
226
            yaml.dump(config_data, file_stream)
 
227
 
 
228
    def read_context(self, file_name):
 
229
        if not os.path.isabs(file_name):
 
230
            file_name = os.path.join(hookenv.charm_dir(), file_name)
 
231
        with open(file_name, 'r') as file_stream:
 
232
            data = yaml.load(file_stream)
 
233
            if not data:
 
234
                raise OSError("%s is empty" % file_name)
 
235
            return data
 
236
 
 
237
 
 
238
class TemplateCallback(ManagerCallback):
 
239
    """
 
240
    Callback class that will render a Jinja2 template, for use as a ready
 
241
    action.
 
242
 
 
243
    :param str source: The template source file, relative to
 
244
        `$CHARM_DIR/templates`
 
245
 
 
246
    :param str target: The target to write the rendered template to (or None)
 
247
    :param str owner: The owner of the rendered file
 
248
    :param str group: The group of the rendered file
 
249
    :param int perms: The permissions of the rendered file
 
250
    :param partial on_change_action: functools partial to be executed when
 
251
                                     rendered file changes
 
252
    :param jinja2 loader template_loader: A jinja2 template loader
 
253
 
 
254
    :return str: The rendered template
 
255
    """
 
256
    def __init__(self, source, target,
 
257
                 owner='root', group='root', perms=0o444,
 
258
                 on_change_action=None, template_loader=None):
 
259
        self.source = source
 
260
        self.target = target
 
261
        self.owner = owner
 
262
        self.group = group
 
263
        self.perms = perms
 
264
        self.on_change_action = on_change_action
 
265
        self.template_loader = template_loader
 
266
 
 
267
    def __call__(self, manager, service_name, event_name):
 
268
        pre_checksum = ''
 
269
        if self.on_change_action and os.path.isfile(self.target):
 
270
            pre_checksum = host.file_hash(self.target)
 
271
        service = manager.get_service(service_name)
 
272
        context = {'ctx': {}}
 
273
        for ctx in service.get('required_data', []):
 
274
            context.update(ctx)
 
275
            context['ctx'].update(ctx)
 
276
 
 
277
        result = templating.render(self.source, self.target, context,
 
278
                                   self.owner, self.group, self.perms,
 
279
                                   template_loader=self.template_loader)
 
280
        if self.on_change_action:
 
281
            if pre_checksum == host.file_hash(self.target):
 
282
                hookenv.log(
 
283
                    'No change detected: {}'.format(self.target),
 
284
                    hookenv.DEBUG)
 
285
            else:
 
286
                self.on_change_action()
 
287
 
 
288
        return result
 
289
 
 
290
 
 
291
# Convenience aliases for templates
 
292
render_template = template = TemplateCallback