~junaidali/charms/trusty/plumgrid-director/pg-restart

« back to all changes in this revision

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

  • Committer: bbaqar at plumgrid
  • Date: 2015-05-19 21:05:20 UTC
  • Revision ID: bbaqar@plumgrid.com-20150519210520-8nymqswwgdu7qygg
PLUMgrid director initial charm

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