~canonical-ci-engineering/charms/trusty/core-result-checker/trunk

« back to all changes in this revision

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

  • Committer: Celso Providelo
  • Date: 2015-03-25 04:54:08 UTC
  • Revision ID: celso.providelo@canonical.com-20150325045408-q4knk469ig0ddg4u
forking core-image-watcher.

Show diffs side-by-side

added added

removed removed

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