~brad-marshall/charms/trusty/apache2-wsgi/fix-haproxy-relations

« back to all changes in this revision

Viewing changes to hooks/lib/charmhelpers/contrib/openstack/context.py

  • Committer: Robin Winslow
  • Date: 2014-05-27 14:00:44 UTC
  • Revision ID: robin.winslow@canonical.com-20140527140044-8rpmb3wx4djzwa83
Add all files

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import json
 
2
import os
 
3
import time
 
4
 
 
5
from base64 import b64decode
 
6
 
 
7
from subprocess import (
 
8
    check_call
 
9
)
 
10
 
 
11
 
 
12
from charmhelpers.fetch import (
 
13
    apt_install,
 
14
    filter_installed_packages,
 
15
)
 
16
 
 
17
from charmhelpers.core.hookenv import (
 
18
    config,
 
19
    local_unit,
 
20
    log,
 
21
    relation_get,
 
22
    relation_ids,
 
23
    related_units,
 
24
    unit_get,
 
25
    unit_private_ip,
 
26
    ERROR,
 
27
)
 
28
 
 
29
from charmhelpers.contrib.hahelpers.cluster import (
 
30
    determine_apache_port,
 
31
    determine_api_port,
 
32
    https,
 
33
    is_clustered
 
34
)
 
35
 
 
36
from charmhelpers.contrib.hahelpers.apache import (
 
37
    get_cert,
 
38
    get_ca_cert,
 
39
)
 
40
 
 
41
from charmhelpers.contrib.openstack.neutron import (
 
42
    neutron_plugin_attribute,
 
43
)
 
44
 
 
45
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
 
46
 
 
47
 
 
48
class OSContextError(Exception):
 
49
    pass
 
50
 
 
51
 
 
52
def ensure_packages(packages):
 
53
    '''Install but do not upgrade required plugin packages'''
 
54
    required = filter_installed_packages(packages)
 
55
    if required:
 
56
        apt_install(required, fatal=True)
 
57
 
 
58
 
 
59
def context_complete(ctxt):
 
60
    _missing = []
 
61
    for k, v in ctxt.iteritems():
 
62
        if v is None or v == '':
 
63
            _missing.append(k)
 
64
    if _missing:
 
65
        log('Missing required data: %s' % ' '.join(_missing), level='INFO')
 
66
        return False
 
67
    return True
 
68
 
 
69
 
 
70
def config_flags_parser(config_flags):
 
71
    if config_flags.find('==') >= 0:
 
72
        log("config_flags is not in expected format (key=value)",
 
73
            level=ERROR)
 
74
        raise OSContextError
 
75
    # strip the following from each value.
 
76
    post_strippers = ' ,'
 
77
    # we strip any leading/trailing '=' or ' ' from the string then
 
78
    # split on '='.
 
79
    split = config_flags.strip(' =').split('=')
 
80
    limit = len(split)
 
81
    flags = {}
 
82
    for i in xrange(0, limit - 1):
 
83
        current = split[i]
 
84
        next = split[i + 1]
 
85
        vindex = next.rfind(',')
 
86
        if (i == limit - 2) or (vindex < 0):
 
87
            value = next
 
88
        else:
 
89
            value = next[:vindex]
 
90
 
 
91
        if i == 0:
 
92
            key = current
 
93
        else:
 
94
            # if this not the first entry, expect an embedded key.
 
95
            index = current.rfind(',')
 
96
            if index < 0:
 
97
                log("invalid config value(s) at index %s" % (i),
 
98
                    level=ERROR)
 
99
                raise OSContextError
 
100
            key = current[index + 1:]
 
101
 
 
102
        # Add to collection.
 
103
        flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
 
104
    return flags
 
105
 
 
106
 
 
107
class OSContextGenerator(object):
 
108
    interfaces = []
 
109
 
 
110
    def __call__(self):
 
111
        raise NotImplementedError
 
112
 
 
113
 
 
114
class SharedDBContext(OSContextGenerator):
 
115
    interfaces = ['shared-db']
 
116
 
 
117
    def __init__(self,
 
118
                 database=None, user=None, relation_prefix=None, ssl_dir=None):
 
119
        '''
 
120
        Allows inspecting relation for settings prefixed with relation_prefix.
 
121
        This is useful for parsing access for multiple databases returned via
 
122
        the shared-db interface (eg, nova_password, quantum_password)
 
123
        '''
 
124
        self.relation_prefix = relation_prefix
 
125
        self.database = database
 
126
        self.user = user
 
127
        self.ssl_dir = ssl_dir
 
128
 
 
129
    def __call__(self):
 
130
        self.database = self.database or config('database')
 
131
        self.user = self.user or config('database-user')
 
132
        if None in [self.database, self.user]:
 
133
            log('Could not generate shared_db context. '
 
134
                'Missing required charm config options. '
 
135
                '(database name and user)')
 
136
            raise OSContextError
 
137
        ctxt = {}
 
138
 
 
139
        password_setting = 'password'
 
140
        if self.relation_prefix:
 
141
            password_setting = self.relation_prefix + '_password'
 
142
 
 
143
        for rid in relation_ids('shared-db'):
 
144
            for unit in related_units(rid):
 
145
                rdata = relation_get(rid=rid, unit=unit)
 
146
                ctxt = {
 
147
                    'database_host': rdata.get('db_host'),
 
148
                    'database': self.database,
 
149
                    'database_user': self.user,
 
150
                    'database_password': rdata.get(password_setting),
 
151
                    'database_type': 'mysql'
 
152
                }
 
153
                if context_complete(ctxt):
 
154
                    db_ssl(rdata, ctxt, self.ssl_dir)
 
155
                    return ctxt
 
156
        return {}
 
157
 
 
158
 
 
159
class PostgresqlDBContext(OSContextGenerator):
 
160
    interfaces = ['pgsql-db']
 
161
 
 
162
    def __init__(self, database=None):
 
163
        self.database = database
 
164
 
 
165
    def __call__(self):
 
166
        self.database = self.database or config('database')
 
167
        if self.database is None:
 
168
            log('Could not generate postgresql_db context. '
 
169
                'Missing required charm config options. '
 
170
                '(database name)')
 
171
            raise OSContextError
 
172
        ctxt = {}
 
173
 
 
174
        for rid in relation_ids(self.interfaces[0]):
 
175
            for unit in related_units(rid):
 
176
                ctxt = {
 
177
                    'database_host': relation_get('host', rid=rid, unit=unit),
 
178
                    'database': self.database,
 
179
                    'database_user': relation_get('user', rid=rid, unit=unit),
 
180
                    'database_password': relation_get('password', rid=rid, unit=unit),
 
181
                    'database_type': 'postgresql',
 
182
                }
 
183
                if context_complete(ctxt):
 
184
                    return ctxt
 
185
        return {}
 
186
 
 
187
 
 
188
def db_ssl(rdata, ctxt, ssl_dir):
 
189
    if 'ssl_ca' in rdata and ssl_dir:
 
190
        ca_path = os.path.join(ssl_dir, 'db-client.ca')
 
191
        with open(ca_path, 'w') as fh:
 
192
            fh.write(b64decode(rdata['ssl_ca']))
 
193
        ctxt['database_ssl_ca'] = ca_path
 
194
    elif 'ssl_ca' in rdata:
 
195
        log("Charm not setup for ssl support but ssl ca found")
 
196
        return ctxt
 
197
    if 'ssl_cert' in rdata:
 
198
        cert_path = os.path.join(
 
199
            ssl_dir, 'db-client.cert')
 
200
        if not os.path.exists(cert_path):
 
201
            log("Waiting 1m for ssl client cert validity")
 
202
            time.sleep(60)
 
203
        with open(cert_path, 'w') as fh:
 
204
            fh.write(b64decode(rdata['ssl_cert']))
 
205
        ctxt['database_ssl_cert'] = cert_path
 
206
        key_path = os.path.join(ssl_dir, 'db-client.key')
 
207
        with open(key_path, 'w') as fh:
 
208
            fh.write(b64decode(rdata['ssl_key']))
 
209
        ctxt['database_ssl_key'] = key_path
 
210
    return ctxt
 
211
 
 
212
 
 
213
class IdentityServiceContext(OSContextGenerator):
 
214
    interfaces = ['identity-service']
 
215
 
 
216
    def __call__(self):
 
217
        log('Generating template context for identity-service')
 
218
        ctxt = {}
 
219
 
 
220
        for rid in relation_ids('identity-service'):
 
221
            for unit in related_units(rid):
 
222
                rdata = relation_get(rid=rid, unit=unit)
 
223
                ctxt = {
 
224
                    'service_port': rdata.get('service_port'),
 
225
                    'service_host': rdata.get('service_host'),
 
226
                    'auth_host': rdata.get('auth_host'),
 
227
                    'auth_port': rdata.get('auth_port'),
 
228
                    'admin_tenant_name': rdata.get('service_tenant'),
 
229
                    'admin_user': rdata.get('service_username'),
 
230
                    'admin_password': rdata.get('service_password'),
 
231
                    'service_protocol':
 
232
                    rdata.get('service_protocol') or 'http',
 
233
                    'auth_protocol':
 
234
                    rdata.get('auth_protocol') or 'http',
 
235
                }
 
236
                if context_complete(ctxt):
 
237
                    # NOTE(jamespage) this is required for >= icehouse
 
238
                    # so a missing value just indicates keystone needs
 
239
                    # upgrading
 
240
                    ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
 
241
                    return ctxt
 
242
        return {}
 
243
 
 
244
 
 
245
class AMQPContext(OSContextGenerator):
 
246
    interfaces = ['amqp']
 
247
 
 
248
    def __init__(self, ssl_dir=None):
 
249
        self.ssl_dir = ssl_dir
 
250
 
 
251
    def __call__(self):
 
252
        log('Generating template context for amqp')
 
253
        conf = config()
 
254
        try:
 
255
            username = conf['rabbit-user']
 
256
            vhost = conf['rabbit-vhost']
 
257
        except KeyError as e:
 
258
            log('Could not generate shared_db context. '
 
259
                'Missing required charm config options: %s.' % e)
 
260
            raise OSContextError
 
261
        ctxt = {}
 
262
        for rid in relation_ids('amqp'):
 
263
            ha_vip_only = False
 
264
            for unit in related_units(rid):
 
265
                if relation_get('clustered', rid=rid, unit=unit):
 
266
                    ctxt['clustered'] = True
 
267
                    ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
 
268
                                                         unit=unit)
 
269
                else:
 
270
                    ctxt['rabbitmq_host'] = relation_get('private-address',
 
271
                                                         rid=rid, unit=unit)
 
272
                ctxt.update({
 
273
                    'rabbitmq_user': username,
 
274
                    'rabbitmq_password': relation_get('password', rid=rid,
 
275
                                                      unit=unit),
 
276
                    'rabbitmq_virtual_host': vhost,
 
277
                })
 
278
 
 
279
                ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
 
280
                if ssl_port:
 
281
                    ctxt['rabbit_ssl_port'] = ssl_port
 
282
                ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
 
283
                if ssl_ca:
 
284
                    ctxt['rabbit_ssl_ca'] = ssl_ca
 
285
 
 
286
                if relation_get('ha_queues', rid=rid, unit=unit) is not None:
 
287
                    ctxt['rabbitmq_ha_queues'] = True
 
288
 
 
289
                ha_vip_only = relation_get('ha-vip-only',
 
290
                                           rid=rid, unit=unit) is not None
 
291
 
 
292
                if context_complete(ctxt):
 
293
                    if 'rabbit_ssl_ca' in ctxt:
 
294
                        if not self.ssl_dir:
 
295
                            log(("Charm not setup for ssl support "
 
296
                                 "but ssl ca found"))
 
297
                            break
 
298
                        ca_path = os.path.join(
 
299
                            self.ssl_dir, 'rabbit-client-ca.pem')
 
300
                        with open(ca_path, 'w') as fh:
 
301
                            fh.write(b64decode(ctxt['rabbit_ssl_ca']))
 
302
                            ctxt['rabbit_ssl_ca'] = ca_path
 
303
                    # Sufficient information found = break out!
 
304
                    break
 
305
            # Used for active/active rabbitmq >= grizzly
 
306
            if ('clustered' not in ctxt or ha_vip_only) \
 
307
                    and len(related_units(rid)) > 1:
 
308
                rabbitmq_hosts = []
 
309
                for unit in related_units(rid):
 
310
                    rabbitmq_hosts.append(relation_get('private-address',
 
311
                                                       rid=rid, unit=unit))
 
312
                ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
 
313
        if not context_complete(ctxt):
 
314
            return {}
 
315
        else:
 
316
            return ctxt
 
317
 
 
318
 
 
319
class CephContext(OSContextGenerator):
 
320
    interfaces = ['ceph']
 
321
 
 
322
    def __call__(self):
 
323
        '''This generates context for /etc/ceph/ceph.conf templates'''
 
324
        if not relation_ids('ceph'):
 
325
            return {}
 
326
 
 
327
        log('Generating template context for ceph')
 
328
 
 
329
        mon_hosts = []
 
330
        auth = None
 
331
        key = None
 
332
        use_syslog = str(config('use-syslog')).lower()
 
333
        for rid in relation_ids('ceph'):
 
334
            for unit in related_units(rid):
 
335
                mon_hosts.append(relation_get('private-address', rid=rid,
 
336
                                              unit=unit))
 
337
                auth = relation_get('auth', rid=rid, unit=unit)
 
338
                key = relation_get('key', rid=rid, unit=unit)
 
339
 
 
340
        ctxt = {
 
341
            'mon_hosts': ' '.join(mon_hosts),
 
342
            'auth': auth,
 
343
            'key': key,
 
344
            'use_syslog': use_syslog
 
345
        }
 
346
 
 
347
        if not os.path.isdir('/etc/ceph'):
 
348
            os.mkdir('/etc/ceph')
 
349
 
 
350
        if not context_complete(ctxt):
 
351
            return {}
 
352
 
 
353
        ensure_packages(['ceph-common'])
 
354
 
 
355
        return ctxt
 
356
 
 
357
 
 
358
class HAProxyContext(OSContextGenerator):
 
359
    interfaces = ['cluster']
 
360
 
 
361
    def __call__(self):
 
362
        '''
 
363
        Builds half a context for the haproxy template, which describes
 
364
        all peers to be included in the cluster.  Each charm needs to include
 
365
        its own context generator that describes the port mapping.
 
366
        '''
 
367
        if not relation_ids('cluster'):
 
368
            return {}
 
369
 
 
370
        cluster_hosts = {}
 
371
        l_unit = local_unit().replace('/', '-')
 
372
        cluster_hosts[l_unit] = unit_get('private-address')
 
373
 
 
374
        for rid in relation_ids('cluster'):
 
375
            for unit in related_units(rid):
 
376
                _unit = unit.replace('/', '-')
 
377
                addr = relation_get('private-address', rid=rid, unit=unit)
 
378
                cluster_hosts[_unit] = addr
 
379
 
 
380
        ctxt = {
 
381
            'units': cluster_hosts,
 
382
        }
 
383
        if len(cluster_hosts.keys()) > 1:
 
384
            # Enable haproxy when we have enough peers.
 
385
            log('Ensuring haproxy enabled in /etc/default/haproxy.')
 
386
            with open('/etc/default/haproxy', 'w') as out:
 
387
                out.write('ENABLED=1\n')
 
388
            return ctxt
 
389
        log('HAProxy context is incomplete, this unit has no peers.')
 
390
        return {}
 
391
 
 
392
 
 
393
class ImageServiceContext(OSContextGenerator):
 
394
    interfaces = ['image-service']
 
395
 
 
396
    def __call__(self):
 
397
        '''
 
398
        Obtains the glance API server from the image-service relation.  Useful
 
399
        in nova and cinder (currently).
 
400
        '''
 
401
        log('Generating template context for image-service.')
 
402
        rids = relation_ids('image-service')
 
403
        if not rids:
 
404
            return {}
 
405
        for rid in rids:
 
406
            for unit in related_units(rid):
 
407
                api_server = relation_get('glance-api-server',
 
408
                                          rid=rid, unit=unit)
 
409
                if api_server:
 
410
                    return {'glance_api_servers': api_server}
 
411
        log('ImageService context is incomplete. '
 
412
            'Missing required relation data.')
 
413
        return {}
 
414
 
 
415
 
 
416
class ApacheSSLContext(OSContextGenerator):
 
417
 
 
418
    """
 
419
    Generates a context for an apache vhost configuration that configures
 
420
    HTTPS reverse proxying for one or many endpoints.  Generated context
 
421
    looks something like:
 
422
    {
 
423
        'namespace': 'cinder',
 
424
        'private_address': 'iscsi.mycinderhost.com',
 
425
        'endpoints': [(8776, 8766), (8777, 8767)]
 
426
    }
 
427
 
 
428
    The endpoints list consists of a tuples mapping external ports
 
429
    to internal ports.
 
430
    """
 
431
    interfaces = ['https']
 
432
 
 
433
    # charms should inherit this context and set external ports
 
434
    # and service namespace accordingly.
 
435
    external_ports = []
 
436
    service_namespace = None
 
437
 
 
438
    def enable_modules(self):
 
439
        cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
 
440
        check_call(cmd)
 
441
 
 
442
    def configure_cert(self):
 
443
        if not os.path.isdir('/etc/apache2/ssl'):
 
444
            os.mkdir('/etc/apache2/ssl')
 
445
        ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
 
446
        if not os.path.isdir(ssl_dir):
 
447
            os.mkdir(ssl_dir)
 
448
        cert, key = get_cert()
 
449
        with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
 
450
            cert_out.write(b64decode(cert))
 
451
        with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
 
452
            key_out.write(b64decode(key))
 
453
        ca_cert = get_ca_cert()
 
454
        if ca_cert:
 
455
            with open(CA_CERT_PATH, 'w') as ca_out:
 
456
                ca_out.write(b64decode(ca_cert))
 
457
            check_call(['update-ca-certificates'])
 
458
 
 
459
    def __call__(self):
 
460
        if isinstance(self.external_ports, basestring):
 
461
            self.external_ports = [self.external_ports]
 
462
        if (not self.external_ports or not https()):
 
463
            return {}
 
464
 
 
465
        self.configure_cert()
 
466
        self.enable_modules()
 
467
 
 
468
        ctxt = {
 
469
            'namespace': self.service_namespace,
 
470
            'private_address': unit_get('private-address'),
 
471
            'endpoints': []
 
472
        }
 
473
        if is_clustered():
 
474
            ctxt['private_address'] = config('vip')
 
475
        for api_port in self.external_ports:
 
476
            ext_port = determine_apache_port(api_port)
 
477
            int_port = determine_api_port(api_port)
 
478
            portmap = (int(ext_port), int(int_port))
 
479
            ctxt['endpoints'].append(portmap)
 
480
        return ctxt
 
481
 
 
482
 
 
483
class NeutronContext(OSContextGenerator):
 
484
    interfaces = []
 
485
 
 
486
    @property
 
487
    def plugin(self):
 
488
        return None
 
489
 
 
490
    @property
 
491
    def network_manager(self):
 
492
        return None
 
493
 
 
494
    @property
 
495
    def packages(self):
 
496
        return neutron_plugin_attribute(
 
497
            self.plugin, 'packages', self.network_manager)
 
498
 
 
499
    @property
 
500
    def neutron_security_groups(self):
 
501
        return None
 
502
 
 
503
    def _ensure_packages(self):
 
504
        [ensure_packages(pkgs) for pkgs in self.packages]
 
505
 
 
506
    def _save_flag_file(self):
 
507
        if self.network_manager == 'quantum':
 
508
            _file = '/etc/nova/quantum_plugin.conf'
 
509
        else:
 
510
            _file = '/etc/nova/neutron_plugin.conf'
 
511
        with open(_file, 'wb') as out:
 
512
            out.write(self.plugin + '\n')
 
513
 
 
514
    def ovs_ctxt(self):
 
515
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
516
                                          self.network_manager)
 
517
        config = neutron_plugin_attribute(self.plugin, 'config',
 
518
                                          self.network_manager)
 
519
        ovs_ctxt = {
 
520
            'core_plugin': driver,
 
521
            'neutron_plugin': 'ovs',
 
522
            'neutron_security_groups': self.neutron_security_groups,
 
523
            'local_ip': unit_private_ip(),
 
524
            'config': config
 
525
        }
 
526
 
 
527
        return ovs_ctxt
 
528
 
 
529
    def nvp_ctxt(self):
 
530
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
531
                                          self.network_manager)
 
532
        config = neutron_plugin_attribute(self.plugin, 'config',
 
533
                                          self.network_manager)
 
534
        nvp_ctxt = {
 
535
            'core_plugin': driver,
 
536
            'neutron_plugin': 'nvp',
 
537
            'neutron_security_groups': self.neutron_security_groups,
 
538
            'local_ip': unit_private_ip(),
 
539
            'config': config
 
540
        }
 
541
 
 
542
        return nvp_ctxt
 
543
 
 
544
    def neutron_ctxt(self):
 
545
        if https():
 
546
            proto = 'https'
 
547
        else:
 
548
            proto = 'http'
 
549
        if is_clustered():
 
550
            host = config('vip')
 
551
        else:
 
552
            host = unit_get('private-address')
 
553
        url = '%s://%s:%s' % (proto, host, '9696')
 
554
        ctxt = {
 
555
            'network_manager': self.network_manager,
 
556
            'neutron_url': url,
 
557
        }
 
558
        return ctxt
 
559
 
 
560
    def __call__(self):
 
561
        self._ensure_packages()
 
562
 
 
563
        if self.network_manager not in ['quantum', 'neutron']:
 
564
            return {}
 
565
 
 
566
        if not self.plugin:
 
567
            return {}
 
568
 
 
569
        ctxt = self.neutron_ctxt()
 
570
 
 
571
        if self.plugin == 'ovs':
 
572
            ctxt.update(self.ovs_ctxt())
 
573
        elif self.plugin == 'nvp':
 
574
            ctxt.update(self.nvp_ctxt())
 
575
 
 
576
        alchemy_flags = config('neutron-alchemy-flags')
 
577
        if alchemy_flags:
 
578
            flags = config_flags_parser(alchemy_flags)
 
579
            ctxt['neutron_alchemy_flags'] = flags
 
580
 
 
581
        self._save_flag_file()
 
582
        return ctxt
 
583
 
 
584
 
 
585
class OSConfigFlagContext(OSContextGenerator):
 
586
 
 
587
        """
 
588
        Responsible for adding user-defined config-flags in charm config to a
 
589
        template context.
 
590
 
 
591
        NOTE: the value of config-flags may be a comma-separated list of
 
592
              key=value pairs and some Openstack config files support
 
593
              comma-separated lists as values.
 
594
        """
 
595
 
 
596
        def __call__(self):
 
597
            config_flags = config('config-flags')
 
598
            if not config_flags:
 
599
                return {}
 
600
 
 
601
            flags = config_flags_parser(config_flags)
 
602
            return {'user_config_flags': flags}
 
603
 
 
604
 
 
605
class SubordinateConfigContext(OSContextGenerator):
 
606
 
 
607
    """
 
608
    Responsible for inspecting relations to subordinates that
 
609
    may be exporting required config via a json blob.
 
610
 
 
611
    The subordinate interface allows subordinates to export their
 
612
    configuration requirements to the principle for multiple config
 
613
    files and multiple serivces.  Ie, a subordinate that has interfaces
 
614
    to both glance and nova may export to following yaml blob as json:
 
615
 
 
616
        glance:
 
617
            /etc/glance/glance-api.conf:
 
618
                sections:
 
619
                    DEFAULT:
 
620
                        - [key1, value1]
 
621
            /etc/glance/glance-registry.conf:
 
622
                    MYSECTION:
 
623
                        - [key2, value2]
 
624
        nova:
 
625
            /etc/nova/nova.conf:
 
626
                sections:
 
627
                    DEFAULT:
 
628
                        - [key3, value3]
 
629
 
 
630
 
 
631
    It is then up to the principle charms to subscribe this context to
 
632
    the service+config file it is interestd in.  Configuration data will
 
633
    be available in the template context, in glance's case, as:
 
634
        ctxt = {
 
635
            ... other context ...
 
636
            'subordinate_config': {
 
637
                'DEFAULT': {
 
638
                    'key1': 'value1',
 
639
                },
 
640
                'MYSECTION': {
 
641
                    'key2': 'value2',
 
642
                },
 
643
            }
 
644
        }
 
645
 
 
646
    """
 
647
 
 
648
    def __init__(self, service, config_file, interface):
 
649
        """
 
650
        :param service     : Service name key to query in any subordinate
 
651
                             data found
 
652
        :param config_file : Service's config file to query sections
 
653
        :param interface   : Subordinate interface to inspect
 
654
        """
 
655
        self.service = service
 
656
        self.config_file = config_file
 
657
        self.interface = interface
 
658
 
 
659
    def __call__(self):
 
660
        ctxt = {}
 
661
        for rid in relation_ids(self.interface):
 
662
            for unit in related_units(rid):
 
663
                sub_config = relation_get('subordinate_configuration',
 
664
                                          rid=rid, unit=unit)
 
665
                if sub_config and sub_config != '':
 
666
                    try:
 
667
                        sub_config = json.loads(sub_config)
 
668
                    except:
 
669
                        log('Could not parse JSON from subordinate_config '
 
670
                            'setting from %s' % rid, level=ERROR)
 
671
                        continue
 
672
 
 
673
                    if self.service not in sub_config:
 
674
                        log('Found subordinate_config on %s but it contained'
 
675
                            'nothing for %s service' % (rid, self.service))
 
676
                        continue
 
677
 
 
678
                    sub_config = sub_config[self.service]
 
679
                    if self.config_file not in sub_config:
 
680
                        log('Found subordinate_config on %s but it contained'
 
681
                            'nothing for %s' % (rid, self.config_file))
 
682
                        continue
 
683
 
 
684
                    sub_config = sub_config[self.config_file]
 
685
                    for k, v in sub_config.iteritems():
 
686
                        ctxt[k] = v
 
687
 
 
688
        if not ctxt:
 
689
            ctxt['sections'] = {}
 
690
 
 
691
        return ctxt
 
692
 
 
693
 
 
694
class SyslogContext(OSContextGenerator):
 
695
 
 
696
    def __call__(self):
 
697
        ctxt = {
 
698
            'use_syslog': config('use-syslog')
 
699
        }
 
700
        return ctxt