~jshieh/charms/trusty/xcat/1428813

« back to all changes in this revision

Viewing changes to charms/trusty/gpfs/lib/charmhelpers/core/services/helpers.py

  • Committer: Michael Chase-Salerno
  • Date: 2015-01-16 16:16:45 UTC
  • Revision ID: bratac@linux.vnet.ibm.com-20150116161645-wvh0sllwqgyilelw
Initial GPFS charm

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import os
 
2
import yaml
 
3
from charmhelpers.core import hookenv
 
4
from charmhelpers.core import templating
 
5
 
 
6
from charmhelpers.core.services.base import ManagerCallback
 
7
 
 
8
 
 
9
__all__ = ['RelationContext', 'TemplateCallback',
 
10
           'render_template', 'template']
 
11
 
 
12
 
 
13
class RelationContext(dict):
 
14
    """
 
15
    Base class for a context generator that gets relation data from juju.
 
16
 
 
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).
 
23
 
 
24
    The generated context will be namespaced under the relation :attr:`name`,
 
25
    to prevent potential naming conflicts.
 
26
 
 
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`
 
29
    """
 
30
    name = None
 
31
    interface = None
 
32
    required_keys = []
 
33
 
 
34
    def __init__(self, name=None, additional_required_keys=None):
 
35
        if name is not None:
 
36
            self.name = name
 
37
        if additional_required_keys is not None:
 
38
            self.required_keys.extend(additional_required_keys)
 
39
        self.get_data()
 
40
 
 
41
    def __bool__(self):
 
42
        """
 
43
        Returns True if all of the required_keys are available.
 
44
        """
 
45
        return self.is_ready()
 
46
 
 
47
    __nonzero__ = __bool__
 
48
 
 
49
    def __repr__(self):
 
50
        return super(RelationContext, self).__repr__()
 
51
 
 
52
    def is_ready(self):
 
53
        """
 
54
        Returns True if all of the `required_keys` are available from any units.
 
55
        """
 
56
        ready = len(self.get(self.name, [])) > 0
 
57
        if not ready:
 
58
            hookenv.log('Incomplete relation: {}'.format(self.__class__.__name__), hookenv.DEBUG)
 
59
        return ready
 
60
 
 
61
    def _is_ready(self, unit_data):
 
62
        """
 
63
        Helper method that tests a set of relation data and returns True if
 
64
        all of the `required_keys` are present.
 
65
        """
 
66
        return set(unit_data.keys()).issuperset(set(self.required_keys))
 
67
 
 
68
    def get_data(self):
 
69
        """
 
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.
 
73
 
 
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'.
 
80
 
 
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,
 
84
        like::
 
85
 
 
86
            {% for unit in interface -%}
 
87
                {{ unit['key'] }}{% if not loop.last %},{% endif %}
 
88
            {%- endfor %}
 
89
 
 
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
 
93
        that information.
 
94
        """
 
95
        if not hookenv.relation_ids(self.name):
 
96
            return
 
97
 
 
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):
 
103
                    ns.append(reldata)
 
104
 
 
105
    def provide_data(self):
 
106
        """
 
107
        Return data to be relation_set for this interface.
 
108
        """
 
109
        return {}
 
110
 
 
111
 
 
112
class MysqlRelation(RelationContext):
 
113
    """
 
114
    Relation context for the `mysql` interface.
 
115
 
 
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`
 
118
    """
 
119
    name = 'db'
 
120
    interface = 'mysql'
 
121
    required_keys = ['host', 'user', 'password', 'database']
 
122
 
 
123
 
 
124
class HttpRelation(RelationContext):
 
125
    """
 
126
    Relation context for the `http` interface.
 
127
 
 
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`
 
130
    """
 
131
    name = 'website'
 
132
    interface = 'http'
 
133
    required_keys = ['host', 'port']
 
134
 
 
135
    def provide_data(self):
 
136
        return {
 
137
            'host': hookenv.unit_get('private-address'),
 
138
            'port': 80,
 
139
        }
 
140
 
 
141
 
 
142
class RequiredConfig(dict):
 
143
    """
 
144
    Data context that loads config options with one or more mandatory options.
 
145
 
 
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
 
149
    relation property).
 
150
 
 
151
    :param list *args: List of options that must be changed from their default values.
 
152
    """
 
153
 
 
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', {})
 
159
 
 
160
    def __bool__(self):
 
161
        for option in self.required_options:
 
162
            if option not in self['config']:
 
163
                return False
 
164
            current_value = self['config'][option]
 
165
            default_value = self.config[option].get('default')
 
166
            if current_value == default_value:
 
167
                return False
 
168
            if current_value in (None, '') and default_value in (None, ''):
 
169
                return False
 
170
        return True
 
171
 
 
172
    def __nonzero__(self):
 
173
        return self.__bool__()
 
174
 
 
175
 
 
176
class StoredContext(dict):
 
177
    """
 
178
    A data context that always returns the data that it was first created with.
 
179
 
 
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.
 
183
    """
 
184
    def __init__(self, file_name, config_data):
 
185
        """
 
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.
 
188
        """
 
189
        if os.path.exists(file_name):
 
190
            self.update(self.read_context(file_name))
 
191
        else:
 
192
            self.store_context(file_name, config_data)
 
193
            self.update(config_data)
 
194
 
 
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)
 
201
 
 
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)
 
207
            if not data:
 
208
                raise OSError("%s is empty" % file_name)
 
209
            return data
 
210
 
 
211
 
 
212
class TemplateCallback(ManagerCallback):
 
213
    """
 
214
    Callback class that will render a Jinja2 template, for use as a ready
 
215
    action.
 
216
 
 
217
    :param str source: The template source file, relative to
 
218
    `$CHARM_DIR/templates`
 
219
 
 
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
 
224
    """
 
225
    def __init__(self, source, target,
 
226
                 owner='root', group='root', perms=0o444):
 
227
        self.source = source
 
228
        self.target = target
 
229
        self.owner = owner
 
230
        self.group = group
 
231
        self.perms = perms
 
232
 
 
233
    def __call__(self, manager, service_name, event_name):
 
234
        service = manager.get_service(service_name)
 
235
        context = {}
 
236
        for ctx in service.get('required_data', []):
 
237
            context.update(ctx)
 
238
        templating.render(self.source, self.target, context,
 
239
                          self.owner, self.group, self.perms)
 
240
 
 
241
 
 
242
# Convenience aliases for templates
 
243
render_template = template = TemplateCallback