~chris-gondolin/charms/trusty/keystone/ldap-ca-cert

« back to all changes in this revision

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

  • Committer: billy.olsen at canonical
  • Date: 2015-08-31 17:35:57 UTC
  • mfrom: (170.1.39 stable.remote)
  • Revision ID: billy.olsen@canonical.com-20150831173557-0r0ftkapbitq0s20
[ack,r=billy-olsen,1chb1n,tealeg,adam-collard] Add pause/resume actions to keystone.

This changes introduces the pause and resume action set to the keystone charm. These
actions can be used to pause keystone services on a unit for maintenance activities.

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 re
 
20
import time
 
21
from base64 import b64decode
 
22
from subprocess import check_call
 
23
 
 
24
import six
 
25
import yaml
 
26
 
 
27
from charmhelpers.fetch import (
 
28
    apt_install,
 
29
    filter_installed_packages,
 
30
)
 
31
from charmhelpers.core.hookenv import (
 
32
    config,
 
33
    is_relation_made,
 
34
    local_unit,
 
35
    log,
 
36
    relation_get,
 
37
    relation_ids,
 
38
    related_units,
 
39
    relation_set,
 
40
    unit_get,
 
41
    unit_private_ip,
 
42
    charm_name,
 
43
    DEBUG,
 
44
    INFO,
 
45
    WARNING,
 
46
    ERROR,
 
47
)
 
48
 
 
49
from charmhelpers.core.sysctl import create as sysctl_create
 
50
from charmhelpers.core.strutils import bool_from_string
 
51
 
 
52
from charmhelpers.core.host import (
 
53
    get_bond_master,
 
54
    is_phy_iface,
 
55
    list_nics,
 
56
    get_nic_hwaddr,
 
57
    mkdir,
 
58
    write_file,
 
59
)
 
60
from charmhelpers.contrib.hahelpers.cluster import (
 
61
    determine_apache_port,
 
62
    determine_api_port,
 
63
    https,
 
64
    is_clustered,
 
65
)
 
66
from charmhelpers.contrib.hahelpers.apache import (
 
67
    get_cert,
 
68
    get_ca_cert,
 
69
    install_ca_cert,
 
70
)
 
71
from charmhelpers.contrib.openstack.neutron import (
 
72
    neutron_plugin_attribute,
 
73
    parse_data_port_mappings,
 
74
)
 
75
from charmhelpers.contrib.openstack.ip import (
 
76
    resolve_address,
 
77
    INTERNAL,
 
78
)
 
79
from charmhelpers.contrib.network.ip import (
 
80
    get_address_in_network,
 
81
    get_ipv4_addr,
 
82
    get_ipv6_addr,
 
83
    get_netmask_for_address,
 
84
    format_ipv6_addr,
 
85
    is_address_in_network,
 
86
    is_bridge_member,
 
87
)
 
88
from charmhelpers.contrib.openstack.utils import get_host_ip
 
89
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
 
90
ADDRESS_TYPES = ['admin', 'internal', 'public']
 
91
 
 
92
 
 
93
class OSContextError(Exception):
 
94
    pass
 
95
 
 
96
 
 
97
def ensure_packages(packages):
 
98
    """Install but do not upgrade required plugin packages."""
 
99
    required = filter_installed_packages(packages)
 
100
    if required:
 
101
        apt_install(required, fatal=True)
 
102
 
 
103
 
 
104
def context_complete(ctxt):
 
105
    _missing = []
 
106
    for k, v in six.iteritems(ctxt):
 
107
        if v is None or v == '':
 
108
            _missing.append(k)
 
109
 
 
110
    if _missing:
 
111
        log('Missing required data: %s' % ' '.join(_missing), level=INFO)
 
112
        return False
 
113
 
 
114
    return True
 
115
 
 
116
 
 
117
def config_flags_parser(config_flags):
 
118
    """Parses config flags string into dict.
 
119
 
 
120
    This parsing method supports a few different formats for the config
 
121
    flag values to be parsed:
 
122
 
 
123
      1. A string in the simple format of key=value pairs, with the possibility
 
124
         of specifying multiple key value pairs within the same string. For
 
125
         example, a string in the format of 'key1=value1, key2=value2' will
 
126
         return a dict of:
 
127
 
 
128
             {'key1': 'value1',
 
129
              'key2': 'value2'}.
 
130
 
 
131
      2. A string in the above format, but supporting a comma-delimited list
 
132
         of values for the same key. For example, a string in the format of
 
133
         'key1=value1, key2=value3,value4,value5' will return a dict of:
 
134
 
 
135
             {'key1', 'value1',
 
136
              'key2', 'value2,value3,value4'}
 
137
 
 
138
      3. A string containing a colon character (:) prior to an equal
 
139
         character (=) will be treated as yaml and parsed as such. This can be
 
140
         used to specify more complex key value pairs. For example,
 
141
         a string in the format of 'key1: subkey1=value1, subkey2=value2' will
 
142
         return a dict of:
 
143
 
 
144
             {'key1', 'subkey1=value1, subkey2=value2'}
 
145
 
 
146
    The provided config_flags string may be a list of comma-separated values
 
147
    which themselves may be comma-separated list of values.
 
148
    """
 
149
    # If we find a colon before an equals sign then treat it as yaml.
 
150
    # Note: limit it to finding the colon first since this indicates assignment
 
151
    # for inline yaml.
 
152
    colon = config_flags.find(':')
 
153
    equals = config_flags.find('=')
 
154
    if colon > 0:
 
155
        if colon < equals or equals < 0:
 
156
            return yaml.safe_load(config_flags)
 
157
 
 
158
    if config_flags.find('==') >= 0:
 
159
        log("config_flags is not in expected format (key=value)", level=ERROR)
 
160
        raise OSContextError
 
161
 
 
162
    # strip the following from each value.
 
163
    post_strippers = ' ,'
 
164
    # we strip any leading/trailing '=' or ' ' from the string then
 
165
    # split on '='.
 
166
    split = config_flags.strip(' =').split('=')
 
167
    limit = len(split)
 
168
    flags = {}
 
169
    for i in range(0, limit - 1):
 
170
        current = split[i]
 
171
        next = split[i + 1]
 
172
        vindex = next.rfind(',')
 
173
        if (i == limit - 2) or (vindex < 0):
 
174
            value = next
 
175
        else:
 
176
            value = next[:vindex]
 
177
 
 
178
        if i == 0:
 
179
            key = current
 
180
        else:
 
181
            # if this not the first entry, expect an embedded key.
 
182
            index = current.rfind(',')
 
183
            if index < 0:
 
184
                log("Invalid config value(s) at index %s" % (i), level=ERROR)
 
185
                raise OSContextError
 
186
            key = current[index + 1:]
 
187
 
 
188
        # Add to collection.
 
189
        flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
 
190
 
 
191
    return flags
 
192
 
 
193
 
 
194
class OSContextGenerator(object):
 
195
    """Base class for all context generators."""
 
196
    interfaces = []
 
197
 
 
198
    def __call__(self):
 
199
        raise NotImplementedError
 
200
 
 
201
 
 
202
class SharedDBContext(OSContextGenerator):
 
203
    interfaces = ['shared-db']
 
204
 
 
205
    def __init__(self,
 
206
                 database=None, user=None, relation_prefix=None, ssl_dir=None):
 
207
        """Allows inspecting relation for settings prefixed with
 
208
        relation_prefix. This is useful for parsing access for multiple
 
209
        databases returned via the shared-db interface (eg, nova_password,
 
210
        quantum_password)
 
211
        """
 
212
        self.relation_prefix = relation_prefix
 
213
        self.database = database
 
214
        self.user = user
 
215
        self.ssl_dir = ssl_dir
 
216
 
 
217
    def __call__(self):
 
218
        self.database = self.database or config('database')
 
219
        self.user = self.user or config('database-user')
 
220
        if None in [self.database, self.user]:
 
221
            log("Could not generate shared_db context. Missing required charm "
 
222
                "config options. (database name and user)", level=ERROR)
 
223
            raise OSContextError
 
224
 
 
225
        ctxt = {}
 
226
 
 
227
        # NOTE(jamespage) if mysql charm provides a network upon which
 
228
        # access to the database should be made, reconfigure relation
 
229
        # with the service units local address and defer execution
 
230
        access_network = relation_get('access-network')
 
231
        if access_network is not None:
 
232
            if self.relation_prefix is not None:
 
233
                hostname_key = "{}_hostname".format(self.relation_prefix)
 
234
            else:
 
235
                hostname_key = "hostname"
 
236
            access_hostname = get_address_in_network(access_network,
 
237
                                                     unit_get('private-address'))
 
238
            set_hostname = relation_get(attribute=hostname_key,
 
239
                                        unit=local_unit())
 
240
            if set_hostname != access_hostname:
 
241
                relation_set(relation_settings={hostname_key: access_hostname})
 
242
                return None  # Defer any further hook execution for now....
 
243
 
 
244
        password_setting = 'password'
 
245
        if self.relation_prefix:
 
246
            password_setting = self.relation_prefix + '_password'
 
247
 
 
248
        for rid in relation_ids(self.interfaces[0]):
 
249
            for unit in related_units(rid):
 
250
                rdata = relation_get(rid=rid, unit=unit)
 
251
                host = rdata.get('db_host')
 
252
                host = format_ipv6_addr(host) or host
 
253
                ctxt = {
 
254
                    'database_host': host,
 
255
                    'database': self.database,
 
256
                    'database_user': self.user,
 
257
                    'database_password': rdata.get(password_setting),
 
258
                    'database_type': 'mysql'
 
259
                }
 
260
                if context_complete(ctxt):
 
261
                    db_ssl(rdata, ctxt, self.ssl_dir)
 
262
                    return ctxt
 
263
        return {}
 
264
 
 
265
 
 
266
class PostgresqlDBContext(OSContextGenerator):
 
267
    interfaces = ['pgsql-db']
 
268
 
 
269
    def __init__(self, database=None):
 
270
        self.database = database
 
271
 
 
272
    def __call__(self):
 
273
        self.database = self.database or config('database')
 
274
        if self.database is None:
 
275
            log('Could not generate postgresql_db context. Missing required '
 
276
                'charm config options. (database name)', level=ERROR)
 
277
            raise OSContextError
 
278
 
 
279
        ctxt = {}
 
280
        for rid in relation_ids(self.interfaces[0]):
 
281
            for unit in related_units(rid):
 
282
                rel_host = relation_get('host', rid=rid, unit=unit)
 
283
                rel_user = relation_get('user', rid=rid, unit=unit)
 
284
                rel_passwd = relation_get('password', rid=rid, unit=unit)
 
285
                ctxt = {'database_host': rel_host,
 
286
                        'database': self.database,
 
287
                        'database_user': rel_user,
 
288
                        'database_password': rel_passwd,
 
289
                        'database_type': 'postgresql'}
 
290
                if context_complete(ctxt):
 
291
                    return ctxt
 
292
 
 
293
        return {}
 
294
 
 
295
 
 
296
def db_ssl(rdata, ctxt, ssl_dir):
 
297
    if 'ssl_ca' in rdata and ssl_dir:
 
298
        ca_path = os.path.join(ssl_dir, 'db-client.ca')
 
299
        with open(ca_path, 'w') as fh:
 
300
            fh.write(b64decode(rdata['ssl_ca']))
 
301
 
 
302
        ctxt['database_ssl_ca'] = ca_path
 
303
    elif 'ssl_ca' in rdata:
 
304
        log("Charm not setup for ssl support but ssl ca found", level=INFO)
 
305
        return ctxt
 
306
 
 
307
    if 'ssl_cert' in rdata:
 
308
        cert_path = os.path.join(
 
309
            ssl_dir, 'db-client.cert')
 
310
        if not os.path.exists(cert_path):
 
311
            log("Waiting 1m for ssl client cert validity", level=INFO)
 
312
            time.sleep(60)
 
313
 
 
314
        with open(cert_path, 'w') as fh:
 
315
            fh.write(b64decode(rdata['ssl_cert']))
 
316
 
 
317
        ctxt['database_ssl_cert'] = cert_path
 
318
        key_path = os.path.join(ssl_dir, 'db-client.key')
 
319
        with open(key_path, 'w') as fh:
 
320
            fh.write(b64decode(rdata['ssl_key']))
 
321
 
 
322
        ctxt['database_ssl_key'] = key_path
 
323
 
 
324
    return ctxt
 
325
 
 
326
 
 
327
class IdentityServiceContext(OSContextGenerator):
 
328
 
 
329
    def __init__(self, service=None, service_user=None, rel_name='identity-service'):
 
330
        self.service = service
 
331
        self.service_user = service_user
 
332
        self.rel_name = rel_name
 
333
        self.interfaces = [self.rel_name]
 
334
 
 
335
    def __call__(self):
 
336
        log('Generating template context for ' + self.rel_name, level=DEBUG)
 
337
        ctxt = {}
 
338
 
 
339
        if self.service and self.service_user:
 
340
            # This is required for pki token signing if we don't want /tmp to
 
341
            # be used.
 
342
            cachedir = '/var/cache/%s' % (self.service)
 
343
            if not os.path.isdir(cachedir):
 
344
                log("Creating service cache dir %s" % (cachedir), level=DEBUG)
 
345
                mkdir(path=cachedir, owner=self.service_user,
 
346
                      group=self.service_user, perms=0o700)
 
347
 
 
348
            ctxt['signing_dir'] = cachedir
 
349
 
 
350
        for rid in relation_ids(self.rel_name):
 
351
            for unit in related_units(rid):
 
352
                rdata = relation_get(rid=rid, unit=unit)
 
353
                serv_host = rdata.get('service_host')
 
354
                serv_host = format_ipv6_addr(serv_host) or serv_host
 
355
                auth_host = rdata.get('auth_host')
 
356
                auth_host = format_ipv6_addr(auth_host) or auth_host
 
357
                svc_protocol = rdata.get('service_protocol') or 'http'
 
358
                auth_protocol = rdata.get('auth_protocol') or 'http'
 
359
                ctxt.update({'service_port': rdata.get('service_port'),
 
360
                             'service_host': serv_host,
 
361
                             'auth_host': auth_host,
 
362
                             'auth_port': rdata.get('auth_port'),
 
363
                             'admin_tenant_name': rdata.get('service_tenant'),
 
364
                             'admin_user': rdata.get('service_username'),
 
365
                             'admin_password': rdata.get('service_password'),
 
366
                             'service_protocol': svc_protocol,
 
367
                             'auth_protocol': auth_protocol})
 
368
 
 
369
                if context_complete(ctxt):
 
370
                    # NOTE(jamespage) this is required for >= icehouse
 
371
                    # so a missing value just indicates keystone needs
 
372
                    # upgrading
 
373
                    ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
 
374
                    return ctxt
 
375
 
 
376
        return {}
 
377
 
 
378
 
 
379
class AMQPContext(OSContextGenerator):
 
380
 
 
381
    def __init__(self, ssl_dir=None, rel_name='amqp', relation_prefix=None):
 
382
        self.ssl_dir = ssl_dir
 
383
        self.rel_name = rel_name
 
384
        self.relation_prefix = relation_prefix
 
385
        self.interfaces = [rel_name]
 
386
 
 
387
    def __call__(self):
 
388
        log('Generating template context for amqp', level=DEBUG)
 
389
        conf = config()
 
390
        if self.relation_prefix:
 
391
            user_setting = '%s-rabbit-user' % (self.relation_prefix)
 
392
            vhost_setting = '%s-rabbit-vhost' % (self.relation_prefix)
 
393
        else:
 
394
            user_setting = 'rabbit-user'
 
395
            vhost_setting = 'rabbit-vhost'
 
396
 
 
397
        try:
 
398
            username = conf[user_setting]
 
399
            vhost = conf[vhost_setting]
 
400
        except KeyError as e:
 
401
            log('Could not generate shared_db context. Missing required charm '
 
402
                'config options: %s.' % e, level=ERROR)
 
403
            raise OSContextError
 
404
 
 
405
        ctxt = {}
 
406
        for rid in relation_ids(self.rel_name):
 
407
            ha_vip_only = False
 
408
            for unit in related_units(rid):
 
409
                if relation_get('clustered', rid=rid, unit=unit):
 
410
                    ctxt['clustered'] = True
 
411
                    vip = relation_get('vip', rid=rid, unit=unit)
 
412
                    vip = format_ipv6_addr(vip) or vip
 
413
                    ctxt['rabbitmq_host'] = vip
 
414
                else:
 
415
                    host = relation_get('private-address', rid=rid, unit=unit)
 
416
                    host = format_ipv6_addr(host) or host
 
417
                    ctxt['rabbitmq_host'] = host
 
418
 
 
419
                ctxt.update({
 
420
                    'rabbitmq_user': username,
 
421
                    'rabbitmq_password': relation_get('password', rid=rid,
 
422
                                                      unit=unit),
 
423
                    'rabbitmq_virtual_host': vhost,
 
424
                })
 
425
 
 
426
                ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
 
427
                if ssl_port:
 
428
                    ctxt['rabbit_ssl_port'] = ssl_port
 
429
 
 
430
                ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
 
431
                if ssl_ca:
 
432
                    ctxt['rabbit_ssl_ca'] = ssl_ca
 
433
 
 
434
                if relation_get('ha_queues', rid=rid, unit=unit) is not None:
 
435
                    ctxt['rabbitmq_ha_queues'] = True
 
436
 
 
437
                ha_vip_only = relation_get('ha-vip-only',
 
438
                                           rid=rid, unit=unit) is not None
 
439
 
 
440
                if context_complete(ctxt):
 
441
                    if 'rabbit_ssl_ca' in ctxt:
 
442
                        if not self.ssl_dir:
 
443
                            log("Charm not setup for ssl support but ssl ca "
 
444
                                "found", level=INFO)
 
445
                            break
 
446
 
 
447
                        ca_path = os.path.join(
 
448
                            self.ssl_dir, 'rabbit-client-ca.pem')
 
449
                        with open(ca_path, 'w') as fh:
 
450
                            fh.write(b64decode(ctxt['rabbit_ssl_ca']))
 
451
                            ctxt['rabbit_ssl_ca'] = ca_path
 
452
 
 
453
                    # Sufficient information found = break out!
 
454
                    break
 
455
 
 
456
            # Used for active/active rabbitmq >= grizzly
 
457
            if (('clustered' not in ctxt or ha_vip_only) and
 
458
                    len(related_units(rid)) > 1):
 
459
                rabbitmq_hosts = []
 
460
                for unit in related_units(rid):
 
461
                    host = relation_get('private-address', rid=rid, unit=unit)
 
462
                    host = format_ipv6_addr(host) or host
 
463
                    rabbitmq_hosts.append(host)
 
464
 
 
465
                ctxt['rabbitmq_hosts'] = ','.join(sorted(rabbitmq_hosts))
 
466
 
 
467
        oslo_messaging_flags = conf.get('oslo-messaging-flags', None)
 
468
        if oslo_messaging_flags:
 
469
            ctxt['oslo_messaging_flags'] = config_flags_parser(
 
470
                oslo_messaging_flags)
 
471
 
 
472
        if not context_complete(ctxt):
 
473
            return {}
 
474
 
 
475
        return ctxt
 
476
 
 
477
 
 
478
class CephContext(OSContextGenerator):
 
479
    """Generates context for /etc/ceph/ceph.conf templates."""
 
480
    interfaces = ['ceph']
 
481
 
 
482
    def __call__(self):
 
483
        if not relation_ids('ceph'):
 
484
            return {}
 
485
 
 
486
        log('Generating template context for ceph', level=DEBUG)
 
487
        mon_hosts = []
 
488
        auth = None
 
489
        key = None
 
490
        use_syslog = str(config('use-syslog')).lower()
 
491
        for rid in relation_ids('ceph'):
 
492
            for unit in related_units(rid):
 
493
                auth = relation_get('auth', rid=rid, unit=unit)
 
494
                key = relation_get('key', rid=rid, unit=unit)
 
495
                ceph_pub_addr = relation_get('ceph-public-address', rid=rid,
 
496
                                             unit=unit)
 
497
                unit_priv_addr = relation_get('private-address', rid=rid,
 
498
                                              unit=unit)
 
499
                ceph_addr = ceph_pub_addr or unit_priv_addr
 
500
                ceph_addr = format_ipv6_addr(ceph_addr) or ceph_addr
 
501
                mon_hosts.append(ceph_addr)
 
502
 
 
503
        ctxt = {'mon_hosts': ' '.join(sorted(mon_hosts)),
 
504
                'auth': auth,
 
505
                'key': key,
 
506
                'use_syslog': use_syslog}
 
507
 
 
508
        if not os.path.isdir('/etc/ceph'):
 
509
            os.mkdir('/etc/ceph')
 
510
 
 
511
        if not context_complete(ctxt):
 
512
            return {}
 
513
 
 
514
        ensure_packages(['ceph-common'])
 
515
        return ctxt
 
516
 
 
517
 
 
518
class HAProxyContext(OSContextGenerator):
 
519
    """Provides half a context for the haproxy template, which describes
 
520
    all peers to be included in the cluster.  Each charm needs to include
 
521
    its own context generator that describes the port mapping.
 
522
    """
 
523
    interfaces = ['cluster']
 
524
 
 
525
    def __init__(self, singlenode_mode=False):
 
526
        self.singlenode_mode = singlenode_mode
 
527
 
 
528
    def __call__(self):
 
529
        if not relation_ids('cluster') and not self.singlenode_mode:
 
530
            return {}
 
531
 
 
532
        if config('prefer-ipv6'):
 
533
            addr = get_ipv6_addr(exc_list=[config('vip')])[0]
 
534
        else:
 
535
            addr = get_host_ip(unit_get('private-address'))
 
536
 
 
537
        l_unit = local_unit().replace('/', '-')
 
538
        cluster_hosts = {}
 
539
 
 
540
        # NOTE(jamespage): build out map of configured network endpoints
 
541
        # and associated backends
 
542
        for addr_type in ADDRESS_TYPES:
 
543
            cfg_opt = 'os-{}-network'.format(addr_type)
 
544
            laddr = get_address_in_network(config(cfg_opt))
 
545
            if laddr:
 
546
                netmask = get_netmask_for_address(laddr)
 
547
                cluster_hosts[laddr] = {'network': "{}/{}".format(laddr,
 
548
                                                                  netmask),
 
549
                                        'backends': {l_unit: laddr}}
 
550
                for rid in relation_ids('cluster'):
 
551
                    for unit in related_units(rid):
 
552
                        _laddr = relation_get('{}-address'.format(addr_type),
 
553
                                              rid=rid, unit=unit)
 
554
                        if _laddr:
 
555
                            _unit = unit.replace('/', '-')
 
556
                            cluster_hosts[laddr]['backends'][_unit] = _laddr
 
557
 
 
558
        # NOTE(jamespage) add backend based on private address - this
 
559
        # with either be the only backend or the fallback if no acls
 
560
        # match in the frontend
 
561
        cluster_hosts[addr] = {}
 
562
        netmask = get_netmask_for_address(addr)
 
563
        cluster_hosts[addr] = {'network': "{}/{}".format(addr, netmask),
 
564
                               'backends': {l_unit: addr}}
 
565
        for rid in relation_ids('cluster'):
 
566
            for unit in related_units(rid):
 
567
                _laddr = relation_get('private-address',
 
568
                                      rid=rid, unit=unit)
 
569
                if _laddr:
 
570
                    _unit = unit.replace('/', '-')
 
571
                    cluster_hosts[addr]['backends'][_unit] = _laddr
 
572
 
 
573
        ctxt = {
 
574
            'frontends': cluster_hosts,
 
575
            'default_backend': addr
 
576
        }
 
577
 
 
578
        if config('haproxy-server-timeout'):
 
579
            ctxt['haproxy_server_timeout'] = config('haproxy-server-timeout')
 
580
 
 
581
        if config('haproxy-client-timeout'):
 
582
            ctxt['haproxy_client_timeout'] = config('haproxy-client-timeout')
 
583
 
 
584
        if config('prefer-ipv6'):
 
585
            ctxt['ipv6'] = True
 
586
            ctxt['local_host'] = 'ip6-localhost'
 
587
            ctxt['haproxy_host'] = '::'
 
588
            ctxt['stat_port'] = ':::8888'
 
589
        else:
 
590
            ctxt['local_host'] = '127.0.0.1'
 
591
            ctxt['haproxy_host'] = '0.0.0.0'
 
592
            ctxt['stat_port'] = ':8888'
 
593
 
 
594
        for frontend in cluster_hosts:
 
595
            if (len(cluster_hosts[frontend]['backends']) > 1 or
 
596
                    self.singlenode_mode):
 
597
                # Enable haproxy when we have enough peers.
 
598
                log('Ensuring haproxy enabled in /etc/default/haproxy.',
 
599
                    level=DEBUG)
 
600
                with open('/etc/default/haproxy', 'w') as out:
 
601
                    out.write('ENABLED=1\n')
 
602
 
 
603
                return ctxt
 
604
 
 
605
        log('HAProxy context is incomplete, this unit has no peers.',
 
606
            level=INFO)
 
607
        return {}
 
608
 
 
609
 
 
610
class ImageServiceContext(OSContextGenerator):
 
611
    interfaces = ['image-service']
 
612
 
 
613
    def __call__(self):
 
614
        """Obtains the glance API server from the image-service relation.
 
615
        Useful in nova and cinder (currently).
 
616
        """
 
617
        log('Generating template context for image-service.', level=DEBUG)
 
618
        rids = relation_ids('image-service')
 
619
        if not rids:
 
620
            return {}
 
621
 
 
622
        for rid in rids:
 
623
            for unit in related_units(rid):
 
624
                api_server = relation_get('glance-api-server',
 
625
                                          rid=rid, unit=unit)
 
626
                if api_server:
 
627
                    return {'glance_api_servers': api_server}
 
628
 
 
629
        log("ImageService context is incomplete. Missing required relation "
 
630
            "data.", level=INFO)
 
631
        return {}
 
632
 
 
633
 
 
634
class ApacheSSLContext(OSContextGenerator):
 
635
    """Generates a context for an apache vhost configuration that configures
 
636
    HTTPS reverse proxying for one or many endpoints.  Generated context
 
637
    looks something like::
 
638
 
 
639
        {
 
640
            'namespace': 'cinder',
 
641
            'private_address': 'iscsi.mycinderhost.com',
 
642
            'endpoints': [(8776, 8766), (8777, 8767)]
 
643
        }
 
644
 
 
645
    The endpoints list consists of a tuples mapping external ports
 
646
    to internal ports.
 
647
    """
 
648
    interfaces = ['https']
 
649
 
 
650
    # charms should inherit this context and set external ports
 
651
    # and service namespace accordingly.
 
652
    external_ports = []
 
653
    service_namespace = None
 
654
 
 
655
    def enable_modules(self):
 
656
        cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
 
657
        check_call(cmd)
 
658
 
 
659
    def configure_cert(self, cn=None):
 
660
        ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
 
661
        mkdir(path=ssl_dir)
 
662
        cert, key = get_cert(cn)
 
663
        if cn:
 
664
            cert_filename = 'cert_{}'.format(cn)
 
665
            key_filename = 'key_{}'.format(cn)
 
666
        else:
 
667
            cert_filename = 'cert'
 
668
            key_filename = 'key'
 
669
 
 
670
        write_file(path=os.path.join(ssl_dir, cert_filename),
 
671
                   content=b64decode(cert))
 
672
        write_file(path=os.path.join(ssl_dir, key_filename),
 
673
                   content=b64decode(key))
 
674
 
 
675
    def configure_ca(self):
 
676
        ca_cert = get_ca_cert()
 
677
        if ca_cert:
 
678
            install_ca_cert(b64decode(ca_cert))
 
679
 
 
680
    def canonical_names(self):
 
681
        """Figure out which canonical names clients will access this service.
 
682
        """
 
683
        cns = []
 
684
        for r_id in relation_ids('identity-service'):
 
685
            for unit in related_units(r_id):
 
686
                rdata = relation_get(rid=r_id, unit=unit)
 
687
                for k in rdata:
 
688
                    if k.startswith('ssl_key_'):
 
689
                        cns.append(k.lstrip('ssl_key_'))
 
690
 
 
691
        return sorted(list(set(cns)))
 
692
 
 
693
    def get_network_addresses(self):
 
694
        """For each network configured, return corresponding address and vip
 
695
           (if available).
 
696
 
 
697
        Returns a list of tuples of the form:
 
698
 
 
699
            [(address_in_net_a, vip_in_net_a),
 
700
             (address_in_net_b, vip_in_net_b),
 
701
             ...]
 
702
 
 
703
            or, if no vip(s) available:
 
704
 
 
705
            [(address_in_net_a, address_in_net_a),
 
706
             (address_in_net_b, address_in_net_b),
 
707
             ...]
 
708
        """
 
709
        addresses = []
 
710
        if config('vip'):
 
711
            vips = config('vip').split()
 
712
        else:
 
713
            vips = []
 
714
 
 
715
        for net_type in ['os-internal-network', 'os-admin-network',
 
716
                         'os-public-network']:
 
717
            addr = get_address_in_network(config(net_type),
 
718
                                          unit_get('private-address'))
 
719
            if len(vips) > 1 and is_clustered():
 
720
                if not config(net_type):
 
721
                    log("Multiple networks configured but net_type "
 
722
                        "is None (%s)." % net_type, level=WARNING)
 
723
                    continue
 
724
 
 
725
                for vip in vips:
 
726
                    if is_address_in_network(config(net_type), vip):
 
727
                        addresses.append((addr, vip))
 
728
                        break
 
729
 
 
730
            elif is_clustered() and config('vip'):
 
731
                addresses.append((addr, config('vip')))
 
732
            else:
 
733
                addresses.append((addr, addr))
 
734
 
 
735
        return sorted(addresses)
 
736
 
 
737
    def __call__(self):
 
738
        if isinstance(self.external_ports, six.string_types):
 
739
            self.external_ports = [self.external_ports]
 
740
 
 
741
        if not self.external_ports or not https():
 
742
            return {}
 
743
 
 
744
        self.configure_ca()
 
745
        self.enable_modules()
 
746
 
 
747
        ctxt = {'namespace': self.service_namespace,
 
748
                'endpoints': [],
 
749
                'ext_ports': []}
 
750
 
 
751
        cns = self.canonical_names()
 
752
        if cns:
 
753
            for cn in cns:
 
754
                self.configure_cert(cn)
 
755
        else:
 
756
            # Expect cert/key provided in config (currently assumed that ca
 
757
            # uses ip for cn)
 
758
            cn = resolve_address(endpoint_type=INTERNAL)
 
759
            self.configure_cert(cn)
 
760
 
 
761
        addresses = self.get_network_addresses()
 
762
        for address, endpoint in sorted(set(addresses)):
 
763
            for api_port in self.external_ports:
 
764
                ext_port = determine_apache_port(api_port,
 
765
                                                 singlenode_mode=True)
 
766
                int_port = determine_api_port(api_port, singlenode_mode=True)
 
767
                portmap = (address, endpoint, int(ext_port), int(int_port))
 
768
                ctxt['endpoints'].append(portmap)
 
769
                ctxt['ext_ports'].append(int(ext_port))
 
770
 
 
771
        ctxt['ext_ports'] = sorted(list(set(ctxt['ext_ports'])))
 
772
        return ctxt
 
773
 
 
774
 
 
775
class NeutronContext(OSContextGenerator):
 
776
    interfaces = []
 
777
 
 
778
    @property
 
779
    def plugin(self):
 
780
        return None
 
781
 
 
782
    @property
 
783
    def network_manager(self):
 
784
        return None
 
785
 
 
786
    @property
 
787
    def packages(self):
 
788
        return neutron_plugin_attribute(self.plugin, 'packages',
 
789
                                        self.network_manager)
 
790
 
 
791
    @property
 
792
    def neutron_security_groups(self):
 
793
        return None
 
794
 
 
795
    def _ensure_packages(self):
 
796
        for pkgs in self.packages:
 
797
            ensure_packages(pkgs)
 
798
 
 
799
    def _save_flag_file(self):
 
800
        if self.network_manager == 'quantum':
 
801
            _file = '/etc/nova/quantum_plugin.conf'
 
802
        else:
 
803
            _file = '/etc/nova/neutron_plugin.conf'
 
804
 
 
805
        with open(_file, 'wb') as out:
 
806
            out.write(self.plugin + '\n')
 
807
 
 
808
    def ovs_ctxt(self):
 
809
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
810
                                          self.network_manager)
 
811
        config = neutron_plugin_attribute(self.plugin, 'config',
 
812
                                          self.network_manager)
 
813
        ovs_ctxt = {'core_plugin': driver,
 
814
                    'neutron_plugin': 'ovs',
 
815
                    'neutron_security_groups': self.neutron_security_groups,
 
816
                    'local_ip': unit_private_ip(),
 
817
                    'config': config}
 
818
 
 
819
        return ovs_ctxt
 
820
 
 
821
    def nuage_ctxt(self):
 
822
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
823
                                          self.network_manager)
 
824
        config = neutron_plugin_attribute(self.plugin, 'config',
 
825
                                          self.network_manager)
 
826
        nuage_ctxt = {'core_plugin': driver,
 
827
                      'neutron_plugin': 'vsp',
 
828
                      'neutron_security_groups': self.neutron_security_groups,
 
829
                      'local_ip': unit_private_ip(),
 
830
                      'config': config}
 
831
 
 
832
        return nuage_ctxt
 
833
 
 
834
    def nvp_ctxt(self):
 
835
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
836
                                          self.network_manager)
 
837
        config = neutron_plugin_attribute(self.plugin, 'config',
 
838
                                          self.network_manager)
 
839
        nvp_ctxt = {'core_plugin': driver,
 
840
                    'neutron_plugin': 'nvp',
 
841
                    'neutron_security_groups': self.neutron_security_groups,
 
842
                    'local_ip': unit_private_ip(),
 
843
                    'config': config}
 
844
 
 
845
        return nvp_ctxt
 
846
 
 
847
    def n1kv_ctxt(self):
 
848
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
849
                                          self.network_manager)
 
850
        n1kv_config = neutron_plugin_attribute(self.plugin, 'config',
 
851
                                               self.network_manager)
 
852
        n1kv_user_config_flags = config('n1kv-config-flags')
 
853
        restrict_policy_profiles = config('n1kv-restrict-policy-profiles')
 
854
        n1kv_ctxt = {'core_plugin': driver,
 
855
                     'neutron_plugin': 'n1kv',
 
856
                     'neutron_security_groups': self.neutron_security_groups,
 
857
                     'local_ip': unit_private_ip(),
 
858
                     'config': n1kv_config,
 
859
                     'vsm_ip': config('n1kv-vsm-ip'),
 
860
                     'vsm_username': config('n1kv-vsm-username'),
 
861
                     'vsm_password': config('n1kv-vsm-password'),
 
862
                     'restrict_policy_profiles': restrict_policy_profiles}
 
863
 
 
864
        if n1kv_user_config_flags:
 
865
            flags = config_flags_parser(n1kv_user_config_flags)
 
866
            n1kv_ctxt['user_config_flags'] = flags
 
867
 
 
868
        return n1kv_ctxt
 
869
 
 
870
    def calico_ctxt(self):
 
871
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
872
                                          self.network_manager)
 
873
        config = neutron_plugin_attribute(self.plugin, 'config',
 
874
                                          self.network_manager)
 
875
        calico_ctxt = {'core_plugin': driver,
 
876
                       'neutron_plugin': 'Calico',
 
877
                       'neutron_security_groups': self.neutron_security_groups,
 
878
                       'local_ip': unit_private_ip(),
 
879
                       'config': config}
 
880
 
 
881
        return calico_ctxt
 
882
 
 
883
    def neutron_ctxt(self):
 
884
        if https():
 
885
            proto = 'https'
 
886
        else:
 
887
            proto = 'http'
 
888
 
 
889
        if is_clustered():
 
890
            host = config('vip')
 
891
        else:
 
892
            host = unit_get('private-address')
 
893
 
 
894
        ctxt = {'network_manager': self.network_manager,
 
895
                'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
 
896
        return ctxt
 
897
 
 
898
    def __call__(self):
 
899
        if self.network_manager not in ['quantum', 'neutron']:
 
900
            return {}
 
901
 
 
902
        if not self.plugin:
 
903
            return {}
 
904
 
 
905
        ctxt = self.neutron_ctxt()
 
906
 
 
907
        if self.plugin == 'ovs':
 
908
            ctxt.update(self.ovs_ctxt())
 
909
        elif self.plugin in ['nvp', 'nsx']:
 
910
            ctxt.update(self.nvp_ctxt())
 
911
        elif self.plugin == 'n1kv':
 
912
            ctxt.update(self.n1kv_ctxt())
 
913
        elif self.plugin == 'Calico':
 
914
            ctxt.update(self.calico_ctxt())
 
915
        elif self.plugin == 'vsp':
 
916
            ctxt.update(self.nuage_ctxt())
 
917
 
 
918
        alchemy_flags = config('neutron-alchemy-flags')
 
919
        if alchemy_flags:
 
920
            flags = config_flags_parser(alchemy_flags)
 
921
            ctxt['neutron_alchemy_flags'] = flags
 
922
 
 
923
        self._save_flag_file()
 
924
        return ctxt
 
925
 
 
926
 
 
927
class NeutronPortContext(OSContextGenerator):
 
928
 
 
929
    def resolve_ports(self, ports):
 
930
        """Resolve NICs not yet bound to bridge(s)
 
931
 
 
932
        If hwaddress provided then returns resolved hwaddress otherwise NIC.
 
933
        """
 
934
        if not ports:
 
935
            return None
 
936
 
 
937
        hwaddr_to_nic = {}
 
938
        hwaddr_to_ip = {}
 
939
        for nic in list_nics():
 
940
            # Ignore virtual interfaces (bond masters will be identified from
 
941
            # their slaves)
 
942
            if not is_phy_iface(nic):
 
943
                continue
 
944
 
 
945
            _nic = get_bond_master(nic)
 
946
            if _nic:
 
947
                log("Replacing iface '%s' with bond master '%s'" % (nic, _nic),
 
948
                    level=DEBUG)
 
949
                nic = _nic
 
950
 
 
951
            hwaddr = get_nic_hwaddr(nic)
 
952
            hwaddr_to_nic[hwaddr] = nic
 
953
            addresses = get_ipv4_addr(nic, fatal=False)
 
954
            addresses += get_ipv6_addr(iface=nic, fatal=False)
 
955
            hwaddr_to_ip[hwaddr] = addresses
 
956
 
 
957
        resolved = []
 
958
        mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
 
959
        for entry in ports:
 
960
            if re.match(mac_regex, entry):
 
961
                # NIC is in known NICs and does NOT hace an IP address
 
962
                if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
 
963
                    # If the nic is part of a bridge then don't use it
 
964
                    if is_bridge_member(hwaddr_to_nic[entry]):
 
965
                        continue
 
966
 
 
967
                    # Entry is a MAC address for a valid interface that doesn't
 
968
                    # have an IP address assigned yet.
 
969
                    resolved.append(hwaddr_to_nic[entry])
 
970
            else:
 
971
                # If the passed entry is not a MAC address, assume it's a valid
 
972
                # interface, and that the user put it there on purpose (we can
 
973
                # trust it to be the real external network).
 
974
                resolved.append(entry)
 
975
 
 
976
        # Ensure no duplicates
 
977
        return list(set(resolved))
 
978
 
 
979
 
 
980
class OSConfigFlagContext(OSContextGenerator):
 
981
    """Provides support for user-defined config flags.
 
982
 
 
983
    Users can define a comma-seperated list of key=value pairs
 
984
    in the charm configuration and apply them at any point in
 
985
    any file by using a template flag.
 
986
 
 
987
    Sometimes users might want config flags inserted within a
 
988
    specific section so this class allows users to specify the
 
989
    template flag name, allowing for multiple template flags
 
990
    (sections) within the same context.
 
991
 
 
992
    NOTE: the value of config-flags may be a comma-separated list of
 
993
          key=value pairs and some Openstack config files support
 
994
          comma-separated lists as values.
 
995
    """
 
996
 
 
997
    def __init__(self, charm_flag='config-flags',
 
998
                 template_flag='user_config_flags'):
 
999
        """
 
1000
        :param charm_flag: config flags in charm configuration.
 
1001
        :param template_flag: insert point for user-defined flags in template
 
1002
                              file.
 
1003
        """
 
1004
        super(OSConfigFlagContext, self).__init__()
 
1005
        self._charm_flag = charm_flag
 
1006
        self._template_flag = template_flag
 
1007
 
 
1008
    def __call__(self):
 
1009
        config_flags = config(self._charm_flag)
 
1010
        if not config_flags:
 
1011
            return {}
 
1012
 
 
1013
        return {self._template_flag:
 
1014
                config_flags_parser(config_flags)}
 
1015
 
 
1016
 
 
1017
class SubordinateConfigContext(OSContextGenerator):
 
1018
 
 
1019
    """
 
1020
    Responsible for inspecting relations to subordinates that
 
1021
    may be exporting required config via a json blob.
 
1022
 
 
1023
    The subordinate interface allows subordinates to export their
 
1024
    configuration requirements to the principle for multiple config
 
1025
    files and multiple serivces.  Ie, a subordinate that has interfaces
 
1026
    to both glance and nova may export to following yaml blob as json::
 
1027
 
 
1028
        glance:
 
1029
            /etc/glance/glance-api.conf:
 
1030
                sections:
 
1031
                    DEFAULT:
 
1032
                        - [key1, value1]
 
1033
            /etc/glance/glance-registry.conf:
 
1034
                    MYSECTION:
 
1035
                        - [key2, value2]
 
1036
        nova:
 
1037
            /etc/nova/nova.conf:
 
1038
                sections:
 
1039
                    DEFAULT:
 
1040
                        - [key3, value3]
 
1041
 
 
1042
 
 
1043
    It is then up to the principle charms to subscribe this context to
 
1044
    the service+config file it is interestd in.  Configuration data will
 
1045
    be available in the template context, in glance's case, as::
 
1046
 
 
1047
        ctxt = {
 
1048
            ... other context ...
 
1049
            'subordinate_config': {
 
1050
                'DEFAULT': {
 
1051
                    'key1': 'value1',
 
1052
                },
 
1053
                'MYSECTION': {
 
1054
                    'key2': 'value2',
 
1055
                },
 
1056
            }
 
1057
        }
 
1058
    """
 
1059
 
 
1060
    def __init__(self, service, config_file, interface):
 
1061
        """
 
1062
        :param service     : Service name key to query in any subordinate
 
1063
                             data found
 
1064
        :param config_file : Service's config file to query sections
 
1065
        :param interface   : Subordinate interface to inspect
 
1066
        """
 
1067
        self.config_file = config_file
 
1068
        if isinstance(service, list):
 
1069
            self.services = service
 
1070
        else:
 
1071
            self.services = [service]
 
1072
        if isinstance(interface, list):
 
1073
            self.interfaces = interface
 
1074
        else:
 
1075
            self.interfaces = [interface]
 
1076
 
 
1077
    def __call__(self):
 
1078
        ctxt = {'sections': {}}
 
1079
        rids = []
 
1080
        for interface in self.interfaces:
 
1081
            rids.extend(relation_ids(interface))
 
1082
        for rid in rids:
 
1083
            for unit in related_units(rid):
 
1084
                sub_config = relation_get('subordinate_configuration',
 
1085
                                          rid=rid, unit=unit)
 
1086
                if sub_config and sub_config != '':
 
1087
                    try:
 
1088
                        sub_config = json.loads(sub_config)
 
1089
                    except:
 
1090
                        log('Could not parse JSON from subordinate_config '
 
1091
                            'setting from %s' % rid, level=ERROR)
 
1092
                        continue
 
1093
 
 
1094
                    for service in self.services:
 
1095
                        if service not in sub_config:
 
1096
                            log('Found subordinate_config on %s but it contained'
 
1097
                                'nothing for %s service' % (rid, service),
 
1098
                                level=INFO)
 
1099
                            continue
 
1100
 
 
1101
                        sub_config = sub_config[service]
 
1102
                        if self.config_file not in sub_config:
 
1103
                            log('Found subordinate_config on %s but it contained'
 
1104
                                'nothing for %s' % (rid, self.config_file),
 
1105
                                level=INFO)
 
1106
                            continue
 
1107
 
 
1108
                        sub_config = sub_config[self.config_file]
 
1109
                        for k, v in six.iteritems(sub_config):
 
1110
                            if k == 'sections':
 
1111
                                for section, config_list in six.iteritems(v):
 
1112
                                    log("adding section '%s'" % (section),
 
1113
                                        level=DEBUG)
 
1114
                                    if ctxt[k].get(section):
 
1115
                                        ctxt[k][section].extend(config_list)
 
1116
                                    else:
 
1117
                                        ctxt[k][section] = config_list
 
1118
                            else:
 
1119
                                ctxt[k] = v
 
1120
        log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
 
1121
        return ctxt
 
1122
 
 
1123
 
 
1124
class LogLevelContext(OSContextGenerator):
 
1125
 
 
1126
    def __call__(self):
 
1127
        ctxt = {}
 
1128
        ctxt['debug'] = \
 
1129
            False if config('debug') is None else config('debug')
 
1130
        ctxt['verbose'] = \
 
1131
            False if config('verbose') is None else config('verbose')
 
1132
 
 
1133
        return ctxt
 
1134
 
 
1135
 
 
1136
class SyslogContext(OSContextGenerator):
 
1137
 
 
1138
    def __call__(self):
 
1139
        ctxt = {'use_syslog': config('use-syslog')}
 
1140
        return ctxt
 
1141
 
 
1142
 
 
1143
class BindHostContext(OSContextGenerator):
 
1144
 
 
1145
    def __call__(self):
 
1146
        if config('prefer-ipv6'):
 
1147
            return {'bind_host': '::'}
 
1148
        else:
 
1149
            return {'bind_host': '0.0.0.0'}
 
1150
 
 
1151
 
 
1152
class WorkerConfigContext(OSContextGenerator):
 
1153
 
 
1154
    @property
 
1155
    def num_cpus(self):
 
1156
        try:
 
1157
            from psutil import NUM_CPUS
 
1158
        except ImportError:
 
1159
            apt_install('python-psutil', fatal=True)
 
1160
            from psutil import NUM_CPUS
 
1161
 
 
1162
        return NUM_CPUS
 
1163
 
 
1164
    def __call__(self):
 
1165
        multiplier = config('worker-multiplier') or 0
 
1166
        ctxt = {"workers": self.num_cpus * multiplier}
 
1167
        return ctxt
 
1168
 
 
1169
 
 
1170
class ZeroMQContext(OSContextGenerator):
 
1171
    interfaces = ['zeromq-configuration']
 
1172
 
 
1173
    def __call__(self):
 
1174
        ctxt = {}
 
1175
        if is_relation_made('zeromq-configuration', 'host'):
 
1176
            for rid in relation_ids('zeromq-configuration'):
 
1177
                    for unit in related_units(rid):
 
1178
                        ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
 
1179
                        ctxt['zmq_host'] = relation_get('host', unit, rid)
 
1180
                        ctxt['zmq_redis_address'] = relation_get(
 
1181
                            'zmq_redis_address', unit, rid)
 
1182
 
 
1183
        return ctxt
 
1184
 
 
1185
 
 
1186
class NotificationDriverContext(OSContextGenerator):
 
1187
 
 
1188
    def __init__(self, zmq_relation='zeromq-configuration',
 
1189
                 amqp_relation='amqp'):
 
1190
        """
 
1191
        :param zmq_relation: Name of Zeromq relation to check
 
1192
        """
 
1193
        self.zmq_relation = zmq_relation
 
1194
        self.amqp_relation = amqp_relation
 
1195
 
 
1196
    def __call__(self):
 
1197
        ctxt = {'notifications': 'False'}
 
1198
        if is_relation_made(self.amqp_relation):
 
1199
            ctxt['notifications'] = "True"
 
1200
 
 
1201
        return ctxt
 
1202
 
 
1203
 
 
1204
class SysctlContext(OSContextGenerator):
 
1205
    """This context check if the 'sysctl' option exists on configuration
 
1206
    then creates a file with the loaded contents"""
 
1207
    def __call__(self):
 
1208
        sysctl_dict = config('sysctl')
 
1209
        if sysctl_dict:
 
1210
            sysctl_create(sysctl_dict,
 
1211
                          '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
 
1212
        return {'sysctl': sysctl_dict}
 
1213
 
 
1214
 
 
1215
class NeutronAPIContext(OSContextGenerator):
 
1216
    '''
 
1217
    Inspects current neutron-plugin-api relation for neutron settings. Return
 
1218
    defaults if it is not present.
 
1219
    '''
 
1220
    interfaces = ['neutron-plugin-api']
 
1221
 
 
1222
    def __call__(self):
 
1223
        self.neutron_defaults = {
 
1224
            'l2_population': {
 
1225
                'rel_key': 'l2-population',
 
1226
                'default': False,
 
1227
            },
 
1228
            'overlay_network_type': {
 
1229
                'rel_key': 'overlay-network-type',
 
1230
                'default': 'gre',
 
1231
            },
 
1232
            'neutron_security_groups': {
 
1233
                'rel_key': 'neutron-security-groups',
 
1234
                'default': False,
 
1235
            },
 
1236
            'network_device_mtu': {
 
1237
                'rel_key': 'network-device-mtu',
 
1238
                'default': None,
 
1239
            },
 
1240
            'enable_dvr': {
 
1241
                'rel_key': 'enable-dvr',
 
1242
                'default': False,
 
1243
            },
 
1244
            'enable_l3ha': {
 
1245
                'rel_key': 'enable-l3ha',
 
1246
                'default': False,
 
1247
            },
 
1248
        }
 
1249
        ctxt = self.get_neutron_options({})
 
1250
        for rid in relation_ids('neutron-plugin-api'):
 
1251
            for unit in related_units(rid):
 
1252
                rdata = relation_get(rid=rid, unit=unit)
 
1253
                if 'l2-population' in rdata:
 
1254
                    ctxt.update(self.get_neutron_options(rdata))
 
1255
 
 
1256
        return ctxt
 
1257
 
 
1258
    def get_neutron_options(self, rdata):
 
1259
        settings = {}
 
1260
        for nkey in self.neutron_defaults.keys():
 
1261
            defv = self.neutron_defaults[nkey]['default']
 
1262
            rkey = self.neutron_defaults[nkey]['rel_key']
 
1263
            if rkey in rdata.keys():
 
1264
                if type(defv) is bool:
 
1265
                    settings[nkey] = bool_from_string(rdata[rkey])
 
1266
                else:
 
1267
                    settings[nkey] = rdata[rkey]
 
1268
            else:
 
1269
                settings[nkey] = defv
 
1270
        return settings
 
1271
 
 
1272
 
 
1273
class ExternalPortContext(NeutronPortContext):
 
1274
 
 
1275
    def __call__(self):
 
1276
        ctxt = {}
 
1277
        ports = config('ext-port')
 
1278
        if ports:
 
1279
            ports = [p.strip() for p in ports.split()]
 
1280
            ports = self.resolve_ports(ports)
 
1281
            if ports:
 
1282
                ctxt = {"ext_port": ports[0]}
 
1283
                napi_settings = NeutronAPIContext()()
 
1284
                mtu = napi_settings.get('network_device_mtu')
 
1285
                if mtu:
 
1286
                    ctxt['ext_port_mtu'] = mtu
 
1287
 
 
1288
        return ctxt
 
1289
 
 
1290
 
 
1291
class DataPortContext(NeutronPortContext):
 
1292
 
 
1293
    def __call__(self):
 
1294
        ports = config('data-port')
 
1295
        if ports:
 
1296
            # Map of {port/mac:bridge}
 
1297
            portmap = parse_data_port_mappings(ports)
 
1298
            ports = portmap.keys()
 
1299
            # Resolve provided ports or mac addresses and filter out those
 
1300
            # already attached to a bridge.
 
1301
            resolved = self.resolve_ports(ports)
 
1302
            # FIXME: is this necessary?
 
1303
            normalized = {get_nic_hwaddr(port): port for port in resolved
 
1304
                          if port not in ports}
 
1305
            normalized.update({port: port for port in resolved
 
1306
                               if port in ports})
 
1307
            if resolved:
 
1308
                return {bridge: normalized[port] for port, bridge in
 
1309
                        six.iteritems(portmap) if port in normalized.keys()}
 
1310
 
 
1311
        return None
 
1312
 
 
1313
 
 
1314
class PhyNICMTUContext(DataPortContext):
 
1315
 
 
1316
    def __call__(self):
 
1317
        ctxt = {}
 
1318
        mappings = super(PhyNICMTUContext, self).__call__()
 
1319
        if mappings and mappings.values():
 
1320
            ports = mappings.values()
 
1321
            napi_settings = NeutronAPIContext()()
 
1322
            mtu = napi_settings.get('network_device_mtu')
 
1323
            if mtu:
 
1324
                ctxt["devs"] = '\\n'.join(ports)
 
1325
                ctxt['mtu'] = mtu
 
1326
 
 
1327
        return ctxt
 
1328
 
 
1329
 
 
1330
class NetworkServiceContext(OSContextGenerator):
 
1331
 
 
1332
    def __init__(self, rel_name='quantum-network-service'):
 
1333
        self.rel_name = rel_name
 
1334
        self.interfaces = [rel_name]
 
1335
 
 
1336
    def __call__(self):
 
1337
        for rid in relation_ids(self.rel_name):
 
1338
            for unit in related_units(rid):
 
1339
                rdata = relation_get(rid=rid, unit=unit)
 
1340
                ctxt = {
 
1341
                    'keystone_host': rdata.get('keystone_host'),
 
1342
                    'service_port': rdata.get('service_port'),
 
1343
                    'auth_port': rdata.get('auth_port'),
 
1344
                    'service_tenant': rdata.get('service_tenant'),
 
1345
                    'service_username': rdata.get('service_username'),
 
1346
                    'service_password': rdata.get('service_password'),
 
1347
                    'quantum_host': rdata.get('quantum_host'),
 
1348
                    'quantum_port': rdata.get('quantum_port'),
 
1349
                    'quantum_url': rdata.get('quantum_url'),
 
1350
                    'region': rdata.get('region'),
 
1351
                    'service_protocol':
 
1352
                    rdata.get('service_protocol') or 'http',
 
1353
                    'auth_protocol':
 
1354
                    rdata.get('auth_protocol') or 'http',
 
1355
                }
 
1356
                if context_complete(ctxt):
 
1357
                    return ctxt
 
1358
        return {}