~johnsca/charms/trusty/cf-cloud-controller/refactor

« back to all changes in this revision

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

  • Committer: Benjamin Saller
  • Date: 2014-03-28 22:39:20 UTC
  • mto: This revision was merged to the branch mainline in revision 5.
  • Revision ID: benjamin.saller@canonical.com-20140328223920-tt7m5t44kwdjry57
wip

Show diffs side-by-side

added added

removed removed

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