~james-page/charms/trusty/swift-proxy/trunk

« back to all changes in this revision

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

  • Committer: Liam Young
  • Date: 2015-12-01 10:56:46 UTC
  • mfrom: (126.2.1 stable.remote)
  • Revision ID: liam.young@canonical.com-20151201105646-xn47a71z6lwvc3at
[brad-marshall, r=gnuoy]  Expose haproxy server and client timeouts via config.

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
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
 
    """
253
 
    def __init__(self, source, target,
254
 
                 owner='root', group='root', perms=0o444,
255
 
                 on_change_action=None):
256
 
        self.source = source
257
 
        self.target = target
258
 
        self.owner = owner
259
 
        self.group = group
260
 
        self.perms = perms
261
 
        self.on_change_action = on_change_action
262
 
 
263
 
    def __call__(self, manager, service_name, event_name):
264
 
        pre_checksum = ''
265
 
        if self.on_change_action and os.path.isfile(self.target):
266
 
            pre_checksum = host.file_hash(self.target)
267
 
        service = manager.get_service(service_name)
268
 
        context = {}
269
 
        for ctx in service.get('required_data', []):
270
 
            context.update(ctx)
271
 
        templating.render(self.source, self.target, context,
272
 
                          self.owner, self.group, self.perms)
273
 
        if self.on_change_action:
274
 
            if pre_checksum == host.file_hash(self.target):
275
 
                hookenv.log(
276
 
                    'No change detected: {}'.format(self.target),
277
 
                    hookenv.DEBUG)
278
 
            else:
279
 
                self.on_change_action()
280
 
 
281
 
 
282
 
# Convenience aliases for templates
283
 
render_template = template = TemplateCallback