~hopem/charms/trusty/keystone/lp1476325

« back to all changes in this revision

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

  • Committer: Liam Young
  • Date: 2015-01-09 15:54:17 UTC
  • mfrom: (99 keystone-242900)
  • mto: This revision was merged to the branch mainline in revision 100.
  • Revision ID: liam.young@canonical.com-20150109155417-ev9c5l3hcx7lo4ec
Merged next in and resolved conflicts

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
import json
2
2
import os
3
3
import time
4
 
 
5
4
from base64 import b64decode
 
5
from subprocess import check_call
6
6
 
7
 
from subprocess import (
8
 
    check_call
9
 
)
 
7
import six
10
8
 
11
9
from charmhelpers.fetch import (
12
10
    apt_install,
13
11
    filter_installed_packages,
14
12
)
15
 
 
16
13
from charmhelpers.core.hookenv import (
17
14
    config,
 
15
    is_relation_made,
18
16
    local_unit,
19
17
    log,
20
18
    relation_get,
23
21
    relation_set,
24
22
    unit_get,
25
23
    unit_private_ip,
 
24
    charm_name,
 
25
    DEBUG,
 
26
    INFO,
 
27
    WARNING,
26
28
    ERROR,
27
 
    INFO
28
29
)
29
30
 
 
31
from charmhelpers.core.sysctl import create as sysctl_create
 
32
 
30
33
from charmhelpers.core.host import (
31
34
    mkdir,
32
 
    write_file
 
35
    write_file,
33
36
)
34
 
 
35
37
from charmhelpers.contrib.hahelpers.cluster import (
36
38
    determine_apache_port,
37
39
    determine_api_port,
38
40
    https,
39
 
    is_clustered
 
41
    is_clustered,
40
42
)
41
 
 
42
43
from charmhelpers.contrib.hahelpers.apache import (
43
44
    get_cert,
44
45
    get_ca_cert,
45
46
    install_ca_cert,
46
47
)
47
 
 
48
48
from charmhelpers.contrib.openstack.neutron import (
49
49
    neutron_plugin_attribute,
50
50
)
51
 
 
52
51
from charmhelpers.contrib.network.ip import (
53
52
    get_address_in_network,
54
53
    get_ipv6_addr,
55
54
    get_netmask_for_address,
56
55
    format_ipv6_addr,
57
 
    is_address_in_network
 
56
    is_address_in_network,
58
57
)
59
 
 
60
58
from charmhelpers.contrib.openstack.utils import get_host_ip
61
59
 
62
60
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
 
61
ADDRESS_TYPES = ['admin', 'internal', 'public']
63
62
 
64
63
 
65
64
class OSContextError(Exception):
67
66
 
68
67
 
69
68
def ensure_packages(packages):
70
 
    '''Install but do not upgrade required plugin packages'''
 
69
    """Install but do not upgrade required plugin packages."""
71
70
    required = filter_installed_packages(packages)
72
71
    if required:
73
72
        apt_install(required, fatal=True)
75
74
 
76
75
def context_complete(ctxt):
77
76
    _missing = []
78
 
    for k, v in ctxt.iteritems():
 
77
    for k, v in six.iteritems(ctxt):
79
78
        if v is None or v == '':
80
79
            _missing.append(k)
 
80
 
81
81
    if _missing:
82
 
        log('Missing required data: %s' % ' '.join(_missing), level='INFO')
 
82
        log('Missing required data: %s' % ' '.join(_missing), level=INFO)
83
83
        return False
 
84
 
84
85
    return True
85
86
 
86
87
 
87
88
def config_flags_parser(config_flags):
 
89
    """Parses config flags string into dict.
 
90
 
 
91
    The provided config_flags string may be a list of comma-separated values
 
92
    which themselves may be comma-separated list of values.
 
93
    """
88
94
    if config_flags.find('==') >= 0:
89
 
        log("config_flags is not in expected format (key=value)",
90
 
            level=ERROR)
 
95
        log("config_flags is not in expected format (key=value)", level=ERROR)
91
96
        raise OSContextError
 
97
 
92
98
    # strip the following from each value.
93
99
    post_strippers = ' ,'
94
100
    # we strip any leading/trailing '=' or ' ' from the string then
96
102
    split = config_flags.strip(' =').split('=')
97
103
    limit = len(split)
98
104
    flags = {}
99
 
    for i in xrange(0, limit - 1):
 
105
    for i in range(0, limit - 1):
100
106
        current = split[i]
101
107
        next = split[i + 1]
102
108
        vindex = next.rfind(',')
111
117
            # if this not the first entry, expect an embedded key.
112
118
            index = current.rfind(',')
113
119
            if index < 0:
114
 
                log("invalid config value(s) at index %s" % (i),
115
 
                    level=ERROR)
 
120
                log("Invalid config value(s) at index %s" % (i), level=ERROR)
116
121
                raise OSContextError
117
122
            key = current[index + 1:]
118
123
 
119
124
        # Add to collection.
120
125
        flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
 
126
 
121
127
    return flags
122
128
 
123
129
 
124
130
class OSContextGenerator(object):
 
131
    """Base class for all context generators."""
125
132
    interfaces = []
126
133
 
127
134
    def __call__(self):
133
140
 
134
141
    def __init__(self,
135
142
                 database=None, user=None, relation_prefix=None, ssl_dir=None):
136
 
        '''
137
 
        Allows inspecting relation for settings prefixed with relation_prefix.
138
 
        This is useful for parsing access for multiple databases returned via
139
 
        the shared-db interface (eg, nova_password, quantum_password)
140
 
        '''
 
143
        """Allows inspecting relation for settings prefixed with
 
144
        relation_prefix. This is useful for parsing access for multiple
 
145
        databases returned via the shared-db interface (eg, nova_password,
 
146
        quantum_password)
 
147
        """
141
148
        self.relation_prefix = relation_prefix
142
149
        self.database = database
143
150
        self.user = user
147
154
        self.database = self.database or config('database')
148
155
        self.user = self.user or config('database-user')
149
156
        if None in [self.database, self.user]:
150
 
            log('Could not generate shared_db context. '
151
 
                'Missing required charm config options. '
152
 
                '(database name and user)')
 
157
            log("Could not generate shared_db context. Missing required charm "
 
158
                "config options. (database name and user)", level=ERROR)
153
159
            raise OSContextError
154
160
 
155
161
        ctxt = {}
202
208
    def __call__(self):
203
209
        self.database = self.database or config('database')
204
210
        if self.database is None:
205
 
            log('Could not generate postgresql_db context. '
206
 
                'Missing required charm config options. '
207
 
                '(database name)')
 
211
            log('Could not generate postgresql_db context. Missing required '
 
212
                'charm config options. (database name)', level=ERROR)
208
213
            raise OSContextError
 
214
 
209
215
        ctxt = {}
210
 
 
211
216
        for rid in relation_ids(self.interfaces[0]):
212
217
            for unit in related_units(rid):
213
 
                ctxt = {
214
 
                    'database_host': relation_get('host', rid=rid, unit=unit),
215
 
                    'database': self.database,
216
 
                    'database_user': relation_get('user', rid=rid, unit=unit),
217
 
                    'database_password': relation_get('password', rid=rid, unit=unit),
218
 
                    'database_type': 'postgresql',
219
 
                }
 
218
                rel_host = relation_get('host', rid=rid, unit=unit)
 
219
                rel_user = relation_get('user', rid=rid, unit=unit)
 
220
                rel_passwd = relation_get('password', rid=rid, unit=unit)
 
221
                ctxt = {'database_host': rel_host,
 
222
                        'database': self.database,
 
223
                        'database_user': rel_user,
 
224
                        'database_password': rel_passwd,
 
225
                        'database_type': 'postgresql'}
220
226
                if context_complete(ctxt):
221
227
                    return ctxt
 
228
 
222
229
        return {}
223
230
 
224
231
 
227
234
        ca_path = os.path.join(ssl_dir, 'db-client.ca')
228
235
        with open(ca_path, 'w') as fh:
229
236
            fh.write(b64decode(rdata['ssl_ca']))
 
237
 
230
238
        ctxt['database_ssl_ca'] = ca_path
231
239
    elif 'ssl_ca' in rdata:
232
 
        log("Charm not setup for ssl support but ssl ca found")
 
240
        log("Charm not setup for ssl support but ssl ca found", level=INFO)
233
241
        return ctxt
 
242
 
234
243
    if 'ssl_cert' in rdata:
235
244
        cert_path = os.path.join(
236
245
            ssl_dir, 'db-client.cert')
237
246
        if not os.path.exists(cert_path):
238
 
            log("Waiting 1m for ssl client cert validity")
 
247
            log("Waiting 1m for ssl client cert validity", level=INFO)
239
248
            time.sleep(60)
 
249
 
240
250
        with open(cert_path, 'w') as fh:
241
251
            fh.write(b64decode(rdata['ssl_cert']))
 
252
 
242
253
        ctxt['database_ssl_cert'] = cert_path
243
254
        key_path = os.path.join(ssl_dir, 'db-client.key')
244
255
        with open(key_path, 'w') as fh:
245
256
            fh.write(b64decode(rdata['ssl_key']))
 
257
 
246
258
        ctxt['database_ssl_key'] = key_path
 
259
 
247
260
    return ctxt
248
261
 
249
262
 
251
264
    interfaces = ['identity-service']
252
265
 
253
266
    def __call__(self):
254
 
        log('Generating template context for identity-service')
 
267
        log('Generating template context for identity-service', level=DEBUG)
255
268
        ctxt = {}
256
 
 
257
269
        for rid in relation_ids('identity-service'):
258
270
            for unit in related_units(rid):
259
271
                rdata = relation_get(rid=rid, unit=unit)
261
273
                serv_host = format_ipv6_addr(serv_host) or serv_host
262
274
                auth_host = rdata.get('auth_host')
263
275
                auth_host = format_ipv6_addr(auth_host) or auth_host
264
 
 
265
 
                ctxt = {
266
 
                    'service_port': rdata.get('service_port'),
267
 
                    'service_host': serv_host,
268
 
                    'auth_host': auth_host,
269
 
                    'auth_port': rdata.get('auth_port'),
270
 
                    'admin_tenant_name': rdata.get('service_tenant'),
271
 
                    'admin_user': rdata.get('service_username'),
272
 
                    'admin_password': rdata.get('service_password'),
273
 
                    'service_protocol':
274
 
                    rdata.get('service_protocol') or 'http',
275
 
                    'auth_protocol':
276
 
                    rdata.get('auth_protocol') or 'http',
277
 
                }
 
276
                svc_protocol = rdata.get('service_protocol') or 'http'
 
277
                auth_protocol = rdata.get('auth_protocol') or 'http'
 
278
                ctxt = {'service_port': rdata.get('service_port'),
 
279
                        'service_host': serv_host,
 
280
                        'auth_host': auth_host,
 
281
                        'auth_port': rdata.get('auth_port'),
 
282
                        'admin_tenant_name': rdata.get('service_tenant'),
 
283
                        'admin_user': rdata.get('service_username'),
 
284
                        'admin_password': rdata.get('service_password'),
 
285
                        'service_protocol': svc_protocol,
 
286
                        'auth_protocol': auth_protocol}
278
287
                if context_complete(ctxt):
279
288
                    # NOTE(jamespage) this is required for >= icehouse
280
289
                    # so a missing value just indicates keystone needs
281
290
                    # upgrading
282
291
                    ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
283
292
                    return ctxt
 
293
 
284
294
        return {}
285
295
 
286
296
 
293
303
        self.interfaces = [rel_name]
294
304
 
295
305
    def __call__(self):
296
 
        log('Generating template context for amqp')
 
306
        log('Generating template context for amqp', level=DEBUG)
297
307
        conf = config()
298
 
        user_setting = 'rabbit-user'
299
 
        vhost_setting = 'rabbit-vhost'
300
308
        if self.relation_prefix:
301
 
            user_setting = self.relation_prefix + '-rabbit-user'
302
 
            vhost_setting = self.relation_prefix + '-rabbit-vhost'
 
309
            user_setting = '%s-rabbit-user' % (self.relation_prefix)
 
310
            vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
 
311
        else:
 
312
            user_setting = 'rabbit-user'
 
313
            vhost_setting = 'rabbit-vhost'
303
314
 
304
315
        try:
305
316
            username = conf[user_setting]
306
317
            vhost = conf[vhost_setting]
307
318
        except KeyError as e:
308
 
            log('Could not generate shared_db context. '
309
 
                'Missing required charm config options: %s.' % e)
 
319
            log('Could not generate shared_db context. Missing required charm '
 
320
                'config options: %s.' % e, level=ERROR)
310
321
            raise OSContextError
 
322
 
311
323
        ctxt = {}
312
324
        for rid in relation_ids(self.rel_name):
313
325
            ha_vip_only = False
321
333
                    host = relation_get('private-address', rid=rid, unit=unit)
322
334
                    host = format_ipv6_addr(host) or host
323
335
                    ctxt['rabbitmq_host'] = host
 
336
 
324
337
                ctxt.update({
325
338
                    'rabbitmq_user': username,
326
339
                    'rabbitmq_password': relation_get('password', rid=rid,
331
344
                ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
332
345
                if ssl_port:
333
346
                    ctxt['rabbit_ssl_port'] = ssl_port
 
347
 
334
348
                ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
335
349
                if ssl_ca:
336
350
                    ctxt['rabbit_ssl_ca'] = ssl_ca
344
358
                if context_complete(ctxt):
345
359
                    if 'rabbit_ssl_ca' in ctxt:
346
360
                        if not self.ssl_dir:
347
 
                            log(("Charm not setup for ssl support "
348
 
                                 "but ssl ca found"))
 
361
                            log("Charm not setup for ssl support but ssl ca "
 
362
                                "found", level=INFO)
349
363
                            break
 
364
 
350
365
                        ca_path = os.path.join(
351
366
                            self.ssl_dir, 'rabbit-client-ca.pem')
352
367
                        with open(ca_path, 'w') as fh:
353
368
                            fh.write(b64decode(ctxt['rabbit_ssl_ca']))
354
369
                            ctxt['rabbit_ssl_ca'] = ca_path
 
370
 
355
371
                    # Sufficient information found = break out!
356
372
                    break
 
373
 
357
374
            # Used for active/active rabbitmq >= grizzly
358
 
            if ('clustered' not in ctxt or ha_vip_only) \
359
 
                    and len(related_units(rid)) > 1:
 
375
            if (('clustered' not in ctxt or ha_vip_only) and
 
376
                    len(related_units(rid)) > 1):
360
377
                rabbitmq_hosts = []
361
378
                for unit in related_units(rid):
362
379
                    host = relation_get('private-address', rid=rid, unit=unit)
363
380
                    host = format_ipv6_addr(host) or host
364
381
                    rabbitmq_hosts.append(host)
365
 
                ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
 
382
 
 
383
                ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
 
384
 
366
385
        if not context_complete(ctxt):
367
386
            return {}
368
 
        else:
369
 
            return ctxt
 
387
 
 
388
        return ctxt
370
389
 
371
390
 
372
391
class CephContext(OSContextGenerator):
 
392
    """Generates context for /etc/ceph/ceph.conf templates."""
373
393
    interfaces = ['ceph']
374
394
 
375
395
    def __call__(self):
376
 
        '''This generates context for /etc/ceph/ceph.conf templates'''
377
396
        if not relation_ids('ceph'):
378
397
            return {}
379
398
 
380
 
        log('Generating template context for ceph')
381
 
 
 
399
        log('Generating template context for ceph', level=DEBUG)
382
400
        mon_hosts = []
383
401
        auth = None
384
402
        key = None
387
405
            for unit in related_units(rid):
388
406
                auth = relation_get('auth', rid=rid, unit=unit)
389
407
                key = relation_get('key', rid=rid, unit=unit)
390
 
                ceph_addr = \
391
 
                    relation_get('ceph-public-address', rid=rid, unit=unit) or \
392
 
                    relation_get('private-address', rid=rid, unit=unit)
 
408
                ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
 
409
                                             unit=unit)
 
410
                unit_priv_addr = relation_get('private-address', rid=rid,
 
411
                                              unit=unit)
 
412
                ceph_addr = ceph_pub_addr or unit_priv_addr
393
413
                ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
394
414
                mon_hosts.append(ceph_addr)
395
415
 
396
 
        ctxt = {
397
 
            'mon_hosts': ' '.join(mon_hosts),
398
 
            'auth': auth,
399
 
            'key': key,
400
 
            'use_syslog': use_syslog
401
 
        }
 
416
        ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
 
417
                'auth': auth,
 
418
                'key': key,
 
419
                'use_syslog': use_syslog}
402
420
 
403
421
        if not os.path.isdir('/etc/ceph'):
404
422
            os.mkdir('/etc/ceph')
407
425
            return {}
408
426
 
409
427
        ensure_packages(['ceph-common'])
410
 
 
411
428
        return ctxt
412
429
 
413
430
 
414
 
ADDRESS_TYPES = ['admin', 'internal', 'public']
415
 
 
416
 
 
417
431
class HAProxyContext(OSContextGenerator):
 
432
    """Provides half a context for the haproxy template, which describes
 
433
    all peers to be included in the cluster.  Each charm needs to include
 
434
    its own context generator that describes the port mapping.
 
435
    """
418
436
    interfaces = ['cluster']
419
437
 
 
438
    def __init__(self, singlenode_mode=False):
 
439
        self.singlenode_mode = singlenode_mode
 
440
 
420
441
    def __call__(self):
421
 
        '''
422
 
        Builds half a context for the haproxy template, which describes
423
 
        all peers to be included in the cluster.  Each charm needs to include
424
 
        its own context generator that describes the port mapping.
425
 
        '''
426
 
        if not relation_ids('cluster'):
 
442
        if not relation_ids('cluster') and not self.singlenode_mode:
427
443
            return {}
428
444
 
429
 
        l_unit = local_unit().replace('/', '-')
430
 
 
431
445
        if config('prefer-ipv6'):
432
446
            addr = get_ipv6_addr(exc_list=[config('vip')])[0]
433
447
        else:
434
448
            addr = get_host_ip(unit_get('private-address'))
435
449
 
 
450
        l_unit = local_unit().replace('/', '-')
436
451
        cluster_hosts = {}
437
452
 
438
453
        # NOTE(jamespage): build out map of configured network endpoints
439
454
        # and associated backends
440
455
        for addr_type in ADDRESS_TYPES:
441
 
            laddr = get_address_in_network(
442
 
                config('os-{}-network'.format(addr_type)))
 
456
            cfg_opt = 'os-{}-network'.format(addr_type)
 
457
            laddr = get_address_in_network(config(cfg_opt))
443
458
            if laddr:
444
 
                cluster_hosts[laddr] = {}
445
 
                cluster_hosts[laddr]['network'] = "{}/{}".format(
446
 
                    laddr,
447
 
                    get_netmask_for_address(laddr)
448
 
                )
449
 
                cluster_hosts[laddr]['backends'] = {}
450
 
                cluster_hosts[laddr]['backends'][l_unit] = laddr
 
459
                netmask = get_netmask_for_address(laddr)
 
460
                cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
 
461
                                                                  netmask),
 
462
                                        'backends': {l_unit: laddr}}
451
463
                for rid in relation_ids('cluster'):
452
464
                    for unit in related_units(rid):
453
 
                        _unit = unit.replace('/', '-')
454
465
                        _laddr = relation_get('{}-address'.format(addr_type),
455
466
                                              rid=rid, unit=unit)
456
467
                        if _laddr:
 
468
                            _unit = unit.replace('/', '-')
457
469
                            cluster_hosts[laddr]['backends'][_unit] = _laddr
458
470
 
459
471
        # NOTE(jamespage) no split configurations found, just use
460
472
        # private addresses
461
473
        if not cluster_hosts:
462
 
            cluster_hosts[addr] = {}
463
 
            cluster_hosts[addr]['network'] = "{}/{}".format(
464
 
                addr,
465
 
                get_netmask_for_address(addr)
466
 
            )
467
 
            cluster_hosts[addr]['backends'] = {}
468
 
            cluster_hosts[addr]['backends'][l_unit] = addr
 
474
            netmask = get_netmask_for_address(addr)
 
475
            cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
 
476
                                   'backends': {l_unit: addr}}
469
477
            for rid in relation_ids('cluster'):
470
478
                for unit in related_units(rid):
471
 
                    _unit = unit.replace('/', '-')
472
479
                    _laddr = relation_get('private-address',
473
480
                                          rid=rid, unit=unit)
474
481
                    if _laddr:
 
482
                        _unit = unit.replace('/', '-')
475
483
                        cluster_hosts[addr]['backends'][_unit] = _laddr
476
484
 
477
 
        ctxt = {
478
 
            'frontends': cluster_hosts,
479
 
        }
 
485
        ctxt = {'frontends': cluster_hosts}
480
486
 
481
487
        if config('haproxy-server-timeout'):
482
488
            ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
 
489
 
483
490
        if config('haproxy-client-timeout'):
484
491
            ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
485
492
 
486
493
        if config('prefer-ipv6'):
 
494
            ctxt['ipv6'] = True
487
495
            ctxt['local_host'] = 'ip6-localhost'
488
496
            ctxt['haproxy_host'] = '::'
489
497
            ctxt['stat_port'] = ':::8888'
493
501
            ctxt['stat_port'] = ':8888'
494
502
 
495
503
        for frontend in cluster_hosts:
496
 
            if len(cluster_hosts[frontend]['backends']) > 1:
 
504
            if (len(cluster_hosts[frontend]['backends']) > 1 or
 
505
                    self.singlenode_mode):
497
506
                # Enable haproxy when we have enough peers.
498
 
                log('Ensuring haproxy enabled in /etc/default/haproxy.')
 
507
                log('Ensuring haproxy enabled in /etc/default/haproxy.',
 
508
                    level=DEBUG)
499
509
                with open('/etc/default/haproxy', 'w') as out:
500
510
                    out.write('ENABLED=1\n')
 
511
 
501
512
                return ctxt
502
 
        log('HAProxy context is incomplete, this unit has no peers.')
 
513
 
 
514
        log('HAProxy context is incomplete, this unit has no peers.',
 
515
            level=INFO)
503
516
        return {}
504
517
 
505
518
 
507
520
    interfaces = ['image-service']
508
521
 
509
522
    def __call__(self):
510
 
        '''
511
 
        Obtains the glance API server from the image-service relation.  Useful
512
 
        in nova and cinder (currently).
513
 
        '''
514
 
        log('Generating template context for image-service.')
 
523
        """Obtains the glance API server from the image-service relation.
 
524
        Useful in nova and cinder (currently).
 
525
        """
 
526
        log('Generating template context for image-service.', level=DEBUG)
515
527
        rids = relation_ids('image-service')
516
528
        if not rids:
517
529
            return {}
 
530
 
518
531
        for rid in rids:
519
532
            for unit in related_units(rid):
520
533
                api_server = relation_get('glance-api-server',
521
534
                                          rid=rid, unit=unit)
522
535
                if api_server:
523
536
                    return {'glance_api_servers': api_server}
524
 
        log('ImageService context is incomplete. '
525
 
            'Missing required relation data.')
 
537
 
 
538
        log("ImageService context is incomplete. Missing required relation "
 
539
            "data.", level=INFO)
526
540
        return {}
527
541
 
528
542
 
529
543
class ApacheSSLContext(OSContextGenerator):
530
 
 
531
 
    """
532
 
    Generates a context for an apache vhost configuration that configures
 
544
    """Generates a context for an apache vhost configuration that configures
533
545
    HTTPS reverse proxying for one or many endpoints.  Generated context
534
546
    looks something like::
535
547
 
563
575
        else:
564
576
            cert_filename = 'cert'
565
577
            key_filename = 'key'
 
578
 
566
579
        write_file(path=os.path.join(ssl_dir, cert_filename),
567
580
                   content=b64decode(cert))
568
581
        write_file(path=os.path.join(ssl_dir, key_filename),
574
587
            install_ca_cert(b64decode(ca_cert))
575
588
 
576
589
    def canonical_names(self):
577
 
        '''Figure out which canonical names clients will access this service'''
 
590
        """Figure out which canonical names clients will access this service.
 
591
        """
578
592
        cns = []
579
593
        for r_id in relation_ids('identity-service'):
580
594
            for unit in related_units(r_id):
582
596
                for k in rdata:
583
597
                    if k.startswith('ssl_key_'):
584
598
                        cns.append(k.lstrip('ssl_key_'))
585
 
        return list(set(cns))
 
599
 
 
600
        return sorted(list(set(cns)))
 
601
 
 
602
    def get_network_addresses(self):
 
603
        """For each network configured, return corresponding address and vip
 
604
           (if available).
 
605
 
 
606
        Returns a list of tuples of the form:
 
607
 
 
608
            [(address_in_net_a, vip_in_net_a),
 
609
             (address_in_net_b, vip_in_net_b),
 
610
             ...]
 
611
 
 
612
            or, if no vip(s) available:
 
613
 
 
614
            [(address_in_net_a, address_in_net_a),
 
615
             (address_in_net_b, address_in_net_b),
 
616
             ...]
 
617
        """
 
618
        addresses = []
 
619
        if config('vip'):
 
620
            vips = config('vip').split()
 
621
        else:
 
622
            vips = []
 
623
 
 
624
        for net_type in ['os-internal-network', 'os-admin-network',
 
625
                         'os-public-network']:
 
626
            addr = get_address_in_network(config(net_type),
 
627
                                          unit_get('private-address'))
 
628
            if len(vips) > 1 and is_clustered():
 
629
                if not config(net_type):
 
630
                    log("Multiple networks configured but net_type "
 
631
                        "is None (%s)." % net_type, level=WARNING)
 
632
                    continue
 
633
 
 
634
                for vip in vips:
 
635
                    if is_address_in_network(config(net_type), vip):
 
636
                        addresses.append((addr, vip))
 
637
                        break
 
638
 
 
639
            elif is_clustered() and config('vip'):
 
640
                addresses.append((addr, config('vip')))
 
641
            else:
 
642
                addresses.append((addr, addr))
 
643
 
 
644
        return sorted(addresses)
586
645
 
587
646
    def __call__(self):
588
 
        if isinstance(self.external_ports, basestring):
 
647
        if isinstance(self.external_ports, six.string_types):
589
648
            self.external_ports = [self.external_ports]
590
 
        if (not self.external_ports or not https()):
 
649
 
 
650
        if not self.external_ports or not https():
591
651
            return {}
592
652
 
593
653
        self.configure_ca()
594
654
        self.enable_modules()
595
655
 
596
 
        ctxt = {
597
 
            'namespace': self.service_namespace,
598
 
            'endpoints': [],
599
 
            'ext_ports': []
600
 
        }
 
656
        ctxt = {'namespace': self.service_namespace,
 
657
                'endpoints': [],
 
658
                'ext_ports': []}
601
659
 
602
660
        for cn in self.canonical_names():
603
661
            self.configure_cert(cn)
604
662
 
605
 
        addresses = []
606
 
        vips = []
607
 
        if config('vip'):
608
 
            vips = config('vip').split()
609
 
 
610
 
        for network_type in ['os-internal-network',
611
 
                             'os-admin-network',
612
 
                             'os-public-network']:
613
 
            address = get_address_in_network(config(network_type),
614
 
                                             unit_get('private-address'))
615
 
            if len(vips) > 0 and is_clustered():
616
 
                for vip in vips:
617
 
                    if is_address_in_network(config(network_type),
618
 
                                             vip):
619
 
                        addresses.append((address, vip))
620
 
                        break
621
 
            elif is_clustered():
622
 
                addresses.append((address, config('vip')))
623
 
            else:
624
 
                addresses.append((address, address))
625
 
 
626
 
        for address, endpoint in set(addresses):
 
663
        addresses = self.get_network_addresses()
 
664
        for address, endpoint in sorted(set(addresses)):
627
665
            for api_port in self.external_ports:
628
666
                ext_port = determine_apache_port(api_port)
629
667
                int_port = determine_api_port(api_port)
630
668
                portmap = (address, endpoint, int(ext_port), int(int_port))
631
669
                ctxt['endpoints'].append(portmap)
632
670
                ctxt['ext_ports'].append(int(ext_port))
633
 
        ctxt['ext_ports'] = list(set(ctxt['ext_ports']))
 
671
 
 
672
        ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
634
673
        return ctxt
635
674
 
636
675
 
647
686
 
648
687
    @property
649
688
    def packages(self):
650
 
        return neutron_plugin_attribute(
651
 
            self.plugin, 'packages', self.network_manager)
 
689
        return neutron_plugin_attribute(self.plugin, 'packages',
 
690
                                        self.network_manager)
652
691
 
653
692
    @property
654
693
    def neutron_security_groups(self):
655
694
        return None
656
695
 
657
696
    def _ensure_packages(self):
658
 
        [ensure_packages(pkgs) for pkgs in self.packages]
 
697
        for pkgs in self.packages:
 
698
            ensure_packages(pkgs)
659
699
 
660
700
    def _save_flag_file(self):
661
701
        if self.network_manager == 'quantum':
662
702
            _file = '/etc/nova/quantum_plugin.conf'
663
703
        else:
664
704
            _file = '/etc/nova/neutron_plugin.conf'
 
705
 
665
706
        with open(_file, 'wb') as out:
666
707
            out.write(self.plugin + '\n')
667
708
 
670
711
                                          self.network_manager)
671
712
        config = neutron_plugin_attribute(self.plugin, 'config',
672
713
                                          self.network_manager)
673
 
        ovs_ctxt = {
674
 
            'core_plugin': driver,
675
 
            'neutron_plugin': 'ovs',
676
 
            'neutron_security_groups': self.neutron_security_groups,
677
 
            'local_ip': unit_private_ip(),
678
 
            'config': config
679
 
        }
 
714
        ovs_ctxt = {'core_plugin': driver,
 
715
                    'neutron_plugin': 'ovs',
 
716
                    'neutron_security_groups': self.neutron_security_groups,
 
717
                    'local_ip': unit_private_ip(),
 
718
                    'config': config}
680
719
 
681
720
        return ovs_ctxt
682
721
 
685
724
                                          self.network_manager)
686
725
        config = neutron_plugin_attribute(self.plugin, 'config',
687
726
                                          self.network_manager)
688
 
        nvp_ctxt = {
689
 
            'core_plugin': driver,
690
 
            'neutron_plugin': 'nvp',
691
 
            'neutron_security_groups': self.neutron_security_groups,
692
 
            'local_ip': unit_private_ip(),
693
 
            'config': config
694
 
        }
 
727
        nvp_ctxt = {'core_plugin': driver,
 
728
                    'neutron_plugin': 'nvp',
 
729
                    'neutron_security_groups': self.neutron_security_groups,
 
730
                    'local_ip': unit_private_ip(),
 
731
                    'config': config}
695
732
 
696
733
        return nvp_ctxt
697
734
 
700
737
                                          self.network_manager)
701
738
        n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
702
739
                                               self.network_manager)
703
 
        n1kv_ctxt = {
704
 
            'core_plugin': driver,
705
 
            'neutron_plugin': 'n1kv',
706
 
            'neutron_security_groups': self.neutron_security_groups,
707
 
            'local_ip': unit_private_ip(),
708
 
            'config': n1kv_config,
709
 
            'vsm_ip': config('n1kv-vsm-ip'),
710
 
            'vsm_username': config('n1kv-vsm-username'),
711
 
            'vsm_password': config('n1kv-vsm-password'),
712
 
            'restrict_policy_profiles': config(
713
 
                'n1kv_restrict_policy_profiles'),
714
 
        }
 
740
        n1kv_user_config_flags = config('n1kv-config-flags')
 
741
        restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
 
742
        n1kv_ctxt = {'core_plugin': driver,
 
743
                     'neutron_plugin': 'n1kv',
 
744
                     'neutron_security_groups': self.neutron_security_groups,
 
745
                     'local_ip': unit_private_ip(),
 
746
                     'config': n1kv_config,
 
747
                     'vsm_ip': config('n1kv-vsm-ip'),
 
748
                     'vsm_username': config('n1kv-vsm-username'),
 
749
                     'vsm_password': config('n1kv-vsm-password'),
 
750
                     'restrict_policy_profiles': restrict_policy_profiles}
 
751
 
 
752
        if n1kv_user_config_flags:
 
753
            flags = config_flags_parser(n1kv_user_config_flags)
 
754
            n1kv_ctxt['user_config_flags'] = flags
715
755
 
716
756
        return n1kv_ctxt
717
757
 
 
758
    def calico_ctxt(self):
 
759
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
760
                                          self.network_manager)
 
761
        config = neutron_plugin_attribute(self.plugin, 'config',
 
762
                                          self.network_manager)
 
763
        calico_ctxt = {'core_plugin': driver,
 
764
                       'neutron_plugin': 'Calico',
 
765
                       'neutron_security_groups': self.neutron_security_groups,
 
766
                       'local_ip': unit_private_ip(),
 
767
                       'config': config}
 
768
 
 
769
        return calico_ctxt
 
770
 
718
771
    def neutron_ctxt(self):
719
772
        if https():
720
773
            proto = 'https'
721
774
        else:
722
775
            proto = 'http'
 
776
 
723
777
        if is_clustered():
724
778
            host = config('vip')
725
779
        else:
726
780
            host = unit_get('private-address')
727
 
        url = '%s://%s:%s' % (proto, host, '9696')
728
 
        ctxt = {
729
 
            'network_manager': self.network_manager,
730
 
            'neutron_url': url,
731
 
        }
 
781
 
 
782
        ctxt = {'network_manager': self.network_manager,
 
783
                'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
732
784
        return ctxt
733
785
 
734
786
    def __call__(self):
748
800
            ctxt.update(self.nvp_ctxt())
749
801
        elif self.plugin == 'n1kv':
750
802
            ctxt.update(self.n1kv_ctxt())
 
803
        elif self.plugin == 'Calico':
 
804
            ctxt.update(self.calico_ctxt())
751
805
 
752
806
        alchemy_flags = config('neutron-alchemy-flags')
753
807
        if alchemy_flags:
759
813
 
760
814
 
761
815
class OSConfigFlagContext(OSContextGenerator):
762
 
 
763
 
    """
764
 
    Responsible for adding user-defined config-flags in charm config to a
765
 
    template context.
 
816
    """Provides support for user-defined config flags.
 
817
 
 
818
    Users can define a comma-seperated list of key=value pairs
 
819
    in the charm configuration and apply them at any point in
 
820
    any file by using a template flag.
 
821
 
 
822
    Sometimes users might want config flags inserted within a
 
823
    specific section so this class allows users to specify the
 
824
    template flag name, allowing for multiple template flags
 
825
    (sections) within the same context.
766
826
 
767
827
    NOTE: the value of config-flags may be a comma-separated list of
768
828
          key=value pairs and some Openstack config files support
769
829
          comma-separated lists as values.
770
830
    """
771
831
 
 
832
    def __init__(self, charm_flag='config-flags',
 
833
                 template_flag='user_config_flags'):
 
834
        """
 
835
        :param charm_flag: config flags in charm configuration.
 
836
        :param template_flag: insert point for user-defined flags in template
 
837
                              file.
 
838
        """
 
839
        super(OSConfigFlagContext, self).__init__()
 
840
        self._charm_flag = charm_flag
 
841
        self._template_flag = template_flag
 
842
 
772
843
    def __call__(self):
773
 
        config_flags = config('config-flags')
 
844
        config_flags = config(self._charm_flag)
774
845
        if not config_flags:
775
846
            return {}
776
847
 
777
 
        flags = config_flags_parser(config_flags)
778
 
        return {'user_config_flags': flags}
 
848
        return {self._template_flag:
 
849
                config_flags_parser(config_flags)}
779
850
 
780
851
 
781
852
class SubordinateConfigContext(OSContextGenerator):
819
890
                },
820
891
            }
821
892
        }
822
 
 
823
893
    """
824
894
 
825
895
    def __init__(self, service, config_file, interface):
849
919
 
850
920
                    if self.service not in sub_config:
851
921
                        log('Found subordinate_config on %s but it contained'
852
 
                            'nothing for %s service' % (rid, self.service))
 
922
                            'nothing for %s service' % (rid, self.service),
 
923
                            level=INFO)
853
924
                        continue
854
925
 
855
926
                    sub_config = sub_config[self.service]
856
927
                    if self.config_file not in sub_config:
857
928
                        log('Found subordinate_config on %s but it contained'
858
 
                            'nothing for %s' % (rid, self.config_file))
 
929
                            'nothing for %s' % (rid, self.config_file),
 
930
                            level=INFO)
859
931
                        continue
860
932
 
861
933
                    sub_config = sub_config[self.config_file]
862
 
                    for k, v in sub_config.iteritems():
 
934
                    for k, v in six.iteritems(sub_config):
863
935
                        if k == 'sections':
864
 
                            for section, config_dict in v.iteritems():
865
 
                                log("adding section '%s'" % (section))
 
936
                            for section, config_dict in six.iteritems(v):
 
937
                                log("adding section '%s'" % (section),
 
938
                                    level=DEBUG)
866
939
                                ctxt[k][section] = config_dict
867
940
                        else:
868
941
                            ctxt[k] = v
869
942
 
870
 
        log("%d section(s) found" % (len(ctxt['sections'])), level=INFO)
871
 
 
 
943
        log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
872
944
        return ctxt
873
945
 
874
946
 
880
952
            False if config('debug') is None else config('debug')
881
953
        ctxt['verbose'] = \
882
954
            False if config('verbose') is None else config('verbose')
 
955
 
883
956
        return ctxt
884
957
 
885
958
 
886
959
class SyslogContext(OSContextGenerator):
887
960
 
888
961
    def __call__(self):
889
 
        ctxt = {
890
 
            'use_syslog': config('use-syslog')
891
 
        }
 
962
        ctxt = {'use_syslog': config('use-syslog')}
892
963
        return ctxt
893
964
 
894
965
 
896
967
 
897
968
    def __call__(self):
898
969
        if config('prefer-ipv6'):
899
 
            return {
900
 
                'bind_host': '::'
901
 
            }
 
970
            return {'bind_host': '::'}
902
971
        else:
903
 
            return {
904
 
                'bind_host': '0.0.0.0'
905
 
            }
 
972
            return {'bind_host': '0.0.0.0'}
906
973
 
907
974
 
908
975
class WorkerConfigContext(OSContextGenerator):
914
981
        except ImportError:
915
982
            apt_install('python-psutil', fatal=True)
916
983
            from psutil import NUM_CPUS
 
984
 
917
985
        return NUM_CPUS
918
986
 
919
987
    def __call__(self):
920
 
        multiplier = config('worker-multiplier') or 1
921
 
        ctxt = {
922
 
            "workers": self.num_cpus * multiplier
923
 
        }
924
 
        return ctxt
 
988
        multiplier = config('worker-multiplier') or 0
 
989
        ctxt = {"workers": self.num_cpus * multiplier}
 
990
        return ctxt
 
991
 
 
992
 
 
993
class ZeroMQContext(OSContextGenerator):
 
994
    interfaces = ['zeromq-configuration']
 
995
 
 
996
    def __call__(self):
 
997
        ctxt = {}
 
998
        if is_relation_made('zeromq-configuration', 'host'):
 
999
            for rid in relation_ids('zeromq-configuration'):
 
1000
                    for unit in related_units(rid):
 
1001
                        ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
 
1002
                        ctxt['zmq_host'] = relation_get('host', unit, rid)
 
1003
 
 
1004
        return ctxt
 
1005
 
 
1006
 
 
1007
class NotificationDriverContext(OSContextGenerator):
 
1008
 
 
1009
    def __init__(self, zmq_relation='zeromq-configuration',
 
1010
                 amqp_relation='amqp'):
 
1011
        """
 
1012
        :param zmq_relation: Name of Zeromq relation to check
 
1013
        """
 
1014
        self.zmq_relation = zmq_relation
 
1015
        self.amqp_relation = amqp_relation
 
1016
 
 
1017
    def __call__(self):
 
1018
        ctxt = {'notifications': 'False'}
 
1019
        if is_relation_made(self.amqp_relation):
 
1020
            ctxt['notifications'] = "True"
 
1021
 
 
1022
        return ctxt
 
1023
 
 
1024
 
 
1025
class SysctlContext(OSContextGenerator):
 
1026
    """This context check if the 'sysctl' option exists on configuration
 
1027
    then creates a file with the loaded contents"""
 
1028
    def __call__(self):
 
1029
        sysctl_dict = config('sysctl')
 
1030
        if sysctl_dict:
 
1031
            sysctl_create(sysctl_dict,
 
1032
                          '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
 
1033
        return {'sysctl': sysctl_dict}