~canonical-ci-engineering/charms/trusty/core-image-publisher/trunk

« back to all changes in this revision

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

  • Committer: Celso Providelo
  • Date: 2015-03-25 04:13:43 UTC
  • Revision ID: celso.providelo@canonical.com-20150325041343-jw05jaz6jscs3c8f
fork of core-image-watcher

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
from charmhelpers.core import hookenv
 
20
from charmhelpers.core import templating
 
21
 
 
22
from charmhelpers.core.services.base import ManagerCallback
 
23
 
 
24
 
 
25
__all__ = ['RelationContext', 'TemplateCallback',
 
26
           'render_template', 'template']
 
27
 
 
28
 
 
29
class RelationContext(dict):
 
30
    """
 
31
    Base class for a context generator that gets relation data from juju.
 
32
 
 
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).
 
39
 
 
40
    The generated context will be namespaced under the relation :attr:`name`,
 
41
    to prevent potential naming conflicts.
 
42
 
 
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`
 
45
    """
 
46
    name = None
 
47
    interface = None
 
48
    required_keys = []
 
49
 
 
50
    def __init__(self, name=None, additional_required_keys=None):
 
51
        if name is not None:
 
52
            self.name = name
 
53
        if additional_required_keys is not None:
 
54
            self.required_keys.extend(additional_required_keys)
 
55
        self.get_data()
 
56
 
 
57
    def __bool__(self):
 
58
        """
 
59
        Returns True if all of the required_keys are available.
 
60
        """
 
61
        return self.is_ready()
 
62
 
 
63
    __nonzero__ = __bool__
 
64
 
 
65
    def __repr__(self):
 
66
        return super(RelationContext, self).__repr__()
 
67
 
 
68
    def is_ready(self):
 
69
        """
 
70
        Returns True if all of the `required_keys` are available from any units.
 
71
        """
 
72
        ready = len(self.get(self.name, [])) > 0
 
73
        if not ready:
 
74
            hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
 
75
        return ready
 
76
 
 
77
    def _is_ready(self, unit_data):
 
78
        """
 
79
        Helper method that tests a set of relation data and returns True if
 
80
        all of the `required_keys` are present.
 
81
        """
 
82
        return set(unit_data.keys()).issuperset(set(self.required_keys))
 
83
 
 
84
    def get_data(self):
 
85
        """
 
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.
 
89
 
 
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'.
 
96
 
 
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,
 
100
        like::
 
101
 
 
102
            {% for unit in interface -%}
 
103
                {{ unit['key'] }}{% if not loop.last %},{% endif %}
 
104
            {%- endfor %}
 
105
 
 
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
 
109
        that information.
 
110
        """
 
111
        if not hookenv.relation_ids(self.name):
 
112
            return
 
113
 
 
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):
 
119
                    ns.append(reldata)
 
120
 
 
121
    def provide_data(self):
 
122
        """
 
123
        Return data to be relation_set for this interface.
 
124
        """
 
125
        return {}
 
126
 
 
127
 
 
128
class MysqlRelation(RelationContext):
 
129
    """
 
130
    Relation context for the `mysql` interface.
 
131
 
 
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`
 
134
    """
 
135
    name = 'db'
 
136
    interface = 'mysql'
 
137
    required_keys = ['host', 'user', 'password', 'database']
 
138
 
 
139
 
 
140
class HttpRelation(RelationContext):
 
141
    """
 
142
    Relation context for the `http` interface.
 
143
 
 
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`
 
146
    """
 
147
    name = 'website'
 
148
    interface = 'http'
 
149
    required_keys = ['host', 'port']
 
150
 
 
151
    def provide_data(self):
 
152
        return {
 
153
            'host': hookenv.unit_get('private-address'),
 
154
            'port': 80,
 
155
        }
 
156
 
 
157
 
 
158
class RequiredConfig(dict):
 
159
    """
 
160
    Data context that loads config options with one or more mandatory options.
 
161
 
 
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
 
165
    relation property).
 
166
 
 
167
    :param list *args: List of options that must be changed from their default values.
 
168
    """
 
169
 
 
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', {})
 
175
 
 
176
    def __bool__(self):
 
177
        for option in self.required_options:
 
178
            if option not in self['config']:
 
179
                return False
 
180
            current_value = self['config'][option]
 
181
            default_value = self.config[option].get('default')
 
182
            if current_value == default_value:
 
183
                return False
 
184
            if current_value in (None, '') and default_value in (None, ''):
 
185
                return False
 
186
        return True
 
187
 
 
188
    def __nonzero__(self):
 
189
        return self.__bool__()
 
190
 
 
191
 
 
192
class StoredContext(dict):
 
193
    """
 
194
    A data context that always returns the data that it was first created with.
 
195
 
 
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.
 
199
    """
 
200
    def __init__(self, file_name, config_data):
 
201
        """
 
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.
 
204
        """
 
205
        if os.path.exists(file_name):
 
206
            self.update(self.read_context(file_name))
 
207
        else:
 
208
            self.store_context(file_name, config_data)
 
209
            self.update(config_data)
 
210
 
 
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)
 
217
 
 
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)
 
223
            if not data:
 
224
                raise OSError("%s is empty" % file_name)
 
225
            return data
 
226
 
 
227
 
 
228
class TemplateCallback(ManagerCallback):
 
229
    """
 
230
    Callback class that will render a Jinja2 template, for use as a ready
 
231
    action.
 
232
 
 
233
    :param str source: The template source file, relative to
 
234
    `$CHARM_DIR/templates`
 
235
 
 
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
 
240
    """
 
241
    def __init__(self, source, target,
 
242
                 owner='root', group='root', perms=0o444):
 
243
        self.source = source
 
244
        self.target = target
 
245
        self.owner = owner
 
246
        self.group = group
 
247
        self.perms = perms
 
248
 
 
249
    def __call__(self, manager, service_name, event_name):
 
250
        service = manager.get_service(service_name)
 
251
        context = {}
 
252
        for ctx in service.get('required_data', []):
 
253
            context.update(ctx)
 
254
        templating.render(self.source, self.target, context,
 
255
                          self.owner, self.group, self.perms)
 
256
 
 
257
 
 
258
# Convenience aliases for templates
 
259
render_template = template = TemplateCallback