~thomir-deactivatedaccount/charms/trusty/tanuki-result-enum-worker/trunk

« back to all changes in this revision

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

  • Committer: Thomi Richards
  • Date: 2015-06-16 04:35:51 UTC
  • Revision ID: thomi.richards@canonical.com-20150616043551-6efyisjvm20vefq3
Add missing bits of charmhelpers.

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(self.interfaces[0]):
 
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 neutron_ctxt(self):
 
879
        if https():
 
880
            proto = 'https'
 
881
        else:
 
882
            proto = 'http'
 
883
 
 
884
        if is_clustered():
 
885
            host = config('vip')
 
886
        else:
 
887
            host = unit_get('private-address')
 
888
 
 
889
        ctxt = {'network_manager': self.network_manager,
 
890
                'neutron_url': '%s://%s:%s' % (proto, host, '9696')}
 
891
        return ctxt
 
892
 
 
893
    def __call__(self):
 
894
        self._ensure_packages()
 
895
 
 
896
        if self.network_manager not in ['quantum', 'neutron']:
 
897
            return {}
 
898
 
 
899
        if not self.plugin:
 
900
            return {}
 
901
 
 
902
        ctxt = self.neutron_ctxt()
 
903
 
 
904
        if self.plugin == 'ovs':
 
905
            ctxt.update(self.ovs_ctxt())
 
906
        elif self.plugin in ['nvp', 'nsx']:
 
907
            ctxt.update(self.nvp_ctxt())
 
908
        elif self.plugin == 'n1kv':
 
909
            ctxt.update(self.n1kv_ctxt())
 
910
        elif self.plugin == 'Calico':
 
911
            ctxt.update(self.calico_ctxt())
 
912
        elif self.plugin == 'vsp':
 
913
            ctxt.update(self.nuage_ctxt())
 
914
 
 
915
        alchemy_flags = config('neutron-alchemy-flags')
 
916
        if alchemy_flags:
 
917
            flags = config_flags_parser(alchemy_flags)
 
918
            ctxt['neutron_alchemy_flags'] = flags
 
919
 
 
920
        self._save_flag_file()
 
921
        return ctxt
 
922
 
 
923
 
 
924
class NeutronPortContext(OSContextGenerator):
 
925
    NIC_PREFIXES = ['eth', 'bond']
 
926
 
 
927
    def resolve_ports(self, ports):
 
928
        """Resolve NICs not yet bound to bridge(s)
 
929
 
 
930
        If hwaddress provided then returns resolved hwaddress otherwise NIC.
 
931
        """
 
932
        if not ports:
 
933
            return None
 
934
 
 
935
        hwaddr_to_nic = {}
 
936
        hwaddr_to_ip = {}
 
937
        for nic in list_nics(self.NIC_PREFIXES):
 
938
            hwaddr = get_nic_hwaddr(nic)
 
939
            hwaddr_to_nic[hwaddr] = nic
 
940
            addresses = get_ipv4_addr(nic, fatal=False)
 
941
            addresses += get_ipv6_addr(iface=nic, fatal=False)
 
942
            hwaddr_to_ip[hwaddr] = addresses
 
943
 
 
944
        resolved = []
 
945
        mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
 
946
        for entry in ports:
 
947
            if re.match(mac_regex, entry):
 
948
                # NIC is in known NICs and does NOT hace an IP address
 
949
                if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
 
950
                    # If the nic is part of a bridge then don't use it
 
951
                    if is_bridge_member(hwaddr_to_nic[entry]):
 
952
                        continue
 
953
 
 
954
                    # Entry is a MAC address for a valid interface that doesn't
 
955
                    # have an IP address assigned yet.
 
956
                    resolved.append(hwaddr_to_nic[entry])
 
957
            else:
 
958
                # If the passed entry is not a MAC address, assume it's a valid
 
959
                # interface, and that the user put it there on purpose (we can
 
960
                # trust it to be the real external network).
 
961
                resolved.append(entry)
 
962
 
 
963
        return resolved
 
964
 
 
965
 
 
966
class OSConfigFlagContext(OSContextGenerator):
 
967
    """Provides support for user-defined config flags.
 
968
 
 
969
    Users can define a comma-seperated list of key=value pairs
 
970
    in the charm configuration and apply them at any point in
 
971
    any file by using a template flag.
 
972
 
 
973
    Sometimes users might want config flags inserted within a
 
974
    specific section so this class allows users to specify the
 
975
    template flag name, allowing for multiple template flags
 
976
    (sections) within the same context.
 
977
 
 
978
    NOTE: the value of config-flags may be a comma-separated list of
 
979
          key=value pairs and some Openstack config files support
 
980
          comma-separated lists as values.
 
981
    """
 
982
 
 
983
    def __init__(self, charm_flag='config-flags',
 
984
                 template_flag='user_config_flags'):
 
985
        """
 
986
        :param charm_flag: config flags in charm configuration.
 
987
        :param template_flag: insert point for user-defined flags in template
 
988
                              file.
 
989
        """
 
990
        super(OSConfigFlagContext, self).__init__()
 
991
        self._charm_flag = charm_flag
 
992
        self._template_flag = template_flag
 
993
 
 
994
    def __call__(self):
 
995
        config_flags = config(self._charm_flag)
 
996
        if not config_flags:
 
997
            return {}
 
998
 
 
999
        return {self._template_flag:
 
1000
                config_flags_parser(config_flags)}
 
1001
 
 
1002
 
 
1003
class SubordinateConfigContext(OSContextGenerator):
 
1004
 
 
1005
    """
 
1006
    Responsible for inspecting relations to subordinates that
 
1007
    may be exporting required config via a json blob.
 
1008
 
 
1009
    The subordinate interface allows subordinates to export their
 
1010
    configuration requirements to the principle for multiple config
 
1011
    files and multiple serivces.  Ie, a subordinate that has interfaces
 
1012
    to both glance and nova may export to following yaml blob as json::
 
1013
 
 
1014
        glance:
 
1015
            /etc/glance/glance-api.conf:
 
1016
                sections:
 
1017
                    DEFAULT:
 
1018
                        - [key1, value1]
 
1019
            /etc/glance/glance-registry.conf:
 
1020
                    MYSECTION:
 
1021
                        - [key2, value2]
 
1022
        nova:
 
1023
            /etc/nova/nova.conf:
 
1024
                sections:
 
1025
                    DEFAULT:
 
1026
                        - [key3, value3]
 
1027
 
 
1028
 
 
1029
    It is then up to the principle charms to subscribe this context to
 
1030
    the service+config file it is interestd in.  Configuration data will
 
1031
    be available in the template context, in glance's case, as::
 
1032
 
 
1033
        ctxt = {
 
1034
            ... other context ...
 
1035
            'subordinate_config': {
 
1036
                'DEFAULT': {
 
1037
                    'key1': 'value1',
 
1038
                },
 
1039
                'MYSECTION': {
 
1040
                    'key2': 'value2',
 
1041
                },
 
1042
            }
 
1043
        }
 
1044
    """
 
1045
 
 
1046
    def __init__(self, service, config_file, interface):
 
1047
        """
 
1048
        :param service     : Service name key to query in any subordinate
 
1049
                             data found
 
1050
        :param config_file : Service's config file to query sections
 
1051
        :param interface   : Subordinate interface to inspect
 
1052
        """
 
1053
        self.service = service
 
1054
        self.config_file = config_file
 
1055
        self.interface = interface
 
1056
 
 
1057
    def __call__(self):
 
1058
        ctxt = {'sections': {}}
 
1059
        for rid in relation_ids(self.interface):
 
1060
            for unit in related_units(rid):
 
1061
                sub_config = relation_get('subordinate_configuration',
 
1062
                                          rid=rid, unit=unit)
 
1063
                if sub_config and sub_config != '':
 
1064
                    try:
 
1065
                        sub_config = json.loads(sub_config)
 
1066
                    except:
 
1067
                        log('Could not parse JSON from subordinate_config '
 
1068
                            'setting from %s' % rid, level=ERROR)
 
1069
                        continue
 
1070
 
 
1071
                    if self.service not in sub_config:
 
1072
                        log('Found subordinate_config on %s but it contained'
 
1073
                            'nothing for %s service' % (rid, self.service),
 
1074
                            level=INFO)
 
1075
                        continue
 
1076
 
 
1077
                    sub_config = sub_config[self.service]
 
1078
                    if self.config_file not in sub_config:
 
1079
                        log('Found subordinate_config on %s but it contained'
 
1080
                            'nothing for %s' % (rid, self.config_file),
 
1081
                            level=INFO)
 
1082
                        continue
 
1083
 
 
1084
                    sub_config = sub_config[self.config_file]
 
1085
                    for k, v in six.iteritems(sub_config):
 
1086
                        if k == 'sections':
 
1087
                            for section, config_dict in six.iteritems(v):
 
1088
                                log("adding section '%s'" % (section),
 
1089
                                    level=DEBUG)
 
1090
                                ctxt[k][section] = config_dict
 
1091
                        else:
 
1092
                            ctxt[k] = v
 
1093
 
 
1094
        log("%d section(s) found" % (len(ctxt['sections'])), level=DEBUG)
 
1095
        return ctxt
 
1096
 
 
1097
 
 
1098
class LogLevelContext(OSContextGenerator):
 
1099
 
 
1100
    def __call__(self):
 
1101
        ctxt = {}
 
1102
        ctxt['debug'] = \
 
1103
            False if config('debug') is None else config('debug')
 
1104
        ctxt['verbose'] = \
 
1105
            False if config('verbose') is None else config('verbose')
 
1106
 
 
1107
        return ctxt
 
1108
 
 
1109
 
 
1110
class SyslogContext(OSContextGenerator):
 
1111
 
 
1112
    def __call__(self):
 
1113
        ctxt = {'use_syslog': config('use-syslog')}
 
1114
        return ctxt
 
1115
 
 
1116
 
 
1117
class BindHostContext(OSContextGenerator):
 
1118
 
 
1119
    def __call__(self):
 
1120
        if config('prefer-ipv6'):
 
1121
            return {'bind_host': '::'}
 
1122
        else:
 
1123
            return {'bind_host': '0.0.0.0'}
 
1124
 
 
1125
 
 
1126
class WorkerConfigContext(OSContextGenerator):
 
1127
 
 
1128
    @property
 
1129
    def num_cpus(self):
 
1130
        try:
 
1131
            from psutil import NUM_CPUS
 
1132
        except ImportError:
 
1133
            apt_install('python-psutil', fatal=True)
 
1134
            from psutil import NUM_CPUS
 
1135
 
 
1136
        return NUM_CPUS
 
1137
 
 
1138
    def __call__(self):
 
1139
        multiplier = config('worker-multiplier') or 0
 
1140
        ctxt = {"workers": self.num_cpus * multiplier}
 
1141
        return ctxt
 
1142
 
 
1143
 
 
1144
class ZeroMQContext(OSContextGenerator):
 
1145
    interfaces = ['zeromq-configuration']
 
1146
 
 
1147
    def __call__(self):
 
1148
        ctxt = {}
 
1149
        if is_relation_made('zeromq-configuration', 'host'):
 
1150
            for rid in relation_ids('zeromq-configuration'):
 
1151
                    for unit in related_units(rid):
 
1152
                        ctxt['zmq_nonce'] = relation_get('nonce', unit, rid)
 
1153
                        ctxt['zmq_host'] = relation_get('host', unit, rid)
 
1154
                        ctxt['zmq_redis_address'] = relation_get(
 
1155
                            'zmq_redis_address', unit, rid)
 
1156
 
 
1157
        return ctxt
 
1158
 
 
1159
 
 
1160
class NotificationDriverContext(OSContextGenerator):
 
1161
 
 
1162
    def __init__(self, zmq_relation='zeromq-configuration',
 
1163
                 amqp_relation='amqp'):
 
1164
        """
 
1165
        :param zmq_relation: Name of Zeromq relation to check
 
1166
        """
 
1167
        self.zmq_relation = zmq_relation
 
1168
        self.amqp_relation = amqp_relation
 
1169
 
 
1170
    def __call__(self):
 
1171
        ctxt = {'notifications': 'False'}
 
1172
        if is_relation_made(self.amqp_relation):
 
1173
            ctxt['notifications'] = "True"
 
1174
 
 
1175
        return ctxt
 
1176
 
 
1177
 
 
1178
class SysctlContext(OSContextGenerator):
 
1179
    """This context check if the 'sysctl' option exists on configuration
 
1180
    then creates a file with the loaded contents"""
 
1181
    def __call__(self):
 
1182
        sysctl_dict = config('sysctl')
 
1183
        if sysctl_dict:
 
1184
            sysctl_create(sysctl_dict,
 
1185
                          '/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
 
1186
        return {'sysctl': sysctl_dict}
 
1187
 
 
1188
 
 
1189
class NeutronAPIContext(OSContextGenerator):
 
1190
    '''
 
1191
    Inspects current neutron-plugin-api relation for neutron settings. Return
 
1192
    defaults if it is not present.
 
1193
    '''
 
1194
    interfaces = ['neutron-plugin-api']
 
1195
 
 
1196
    def __call__(self):
 
1197
        self.neutron_defaults = {
 
1198
            'l2_population': {
 
1199
                'rel_key': 'l2-population',
 
1200
                'default': False,
 
1201
            },
 
1202
            'overlay_network_type': {
 
1203
                'rel_key': 'overlay-network-type',
 
1204
                'default': 'gre',
 
1205
            },
 
1206
            'neutron_security_groups': {
 
1207
                'rel_key': 'neutron-security-groups',
 
1208
                'default': False,
 
1209
            },
 
1210
            'network_device_mtu': {
 
1211
                'rel_key': 'network-device-mtu',
 
1212
                'default': None,
 
1213
            },
 
1214
            'enable_dvr': {
 
1215
                'rel_key': 'enable-dvr',
 
1216
                'default': False,
 
1217
            },
 
1218
            'enable_l3ha': {
 
1219
                'rel_key': 'enable-l3ha',
 
1220
                'default': False,
 
1221
            },
 
1222
        }
 
1223
        ctxt = self.get_neutron_options({})
 
1224
        for rid in relation_ids('neutron-plugin-api'):
 
1225
            for unit in related_units(rid):
 
1226
                rdata = relation_get(rid=rid, unit=unit)
 
1227
                if 'l2-population' in rdata:
 
1228
                    ctxt.update(self.get_neutron_options(rdata))
 
1229
 
 
1230
        return ctxt
 
1231
 
 
1232
    def get_neutron_options(self, rdata):
 
1233
        settings = {}
 
1234
        for nkey in self.neutron_defaults.keys():
 
1235
            defv = self.neutron_defaults[nkey]['default']
 
1236
            rkey = self.neutron_defaults[nkey]['rel_key']
 
1237
            if rkey in rdata.keys():
 
1238
                if type(defv) is bool:
 
1239
                    settings[nkey] = bool_from_string(rdata[rkey])
 
1240
                else:
 
1241
                    settings[nkey] = rdata[rkey]
 
1242
            else:
 
1243
                settings[nkey] = defv
 
1244
        return settings
 
1245
 
 
1246
 
 
1247
class ExternalPortContext(NeutronPortContext):
 
1248
 
 
1249
    def __call__(self):
 
1250
        ctxt = {}
 
1251
        ports = config('ext-port')
 
1252
        if ports:
 
1253
            ports = [p.strip() for p in ports.split()]
 
1254
            ports = self.resolve_ports(ports)
 
1255
            if ports:
 
1256
                ctxt = {"ext_port": ports[0]}
 
1257
                napi_settings = NeutronAPIContext()()
 
1258
                mtu = napi_settings.get('network_device_mtu')
 
1259
                if mtu:
 
1260
                    ctxt['ext_port_mtu'] = mtu
 
1261
 
 
1262
        return ctxt
 
1263
 
 
1264
 
 
1265
class DataPortContext(NeutronPortContext):
 
1266
 
 
1267
    def __call__(self):
 
1268
        ports = config('data-port')
 
1269
        if ports:
 
1270
            portmap = parse_data_port_mappings(ports)
 
1271
            ports = portmap.values()
 
1272
            resolved = self.resolve_ports(ports)
 
1273
            normalized = {get_nic_hwaddr(port): port for port in resolved
 
1274
                          if port not in ports}
 
1275
            normalized.update({port: port for port in resolved
 
1276
                               if port in ports})
 
1277
            if resolved:
 
1278
                return {bridge: normalized[port] for bridge, port in
 
1279
                        six.iteritems(portmap) if port in normalized.keys()}
 
1280
 
 
1281
        return None
 
1282
 
 
1283
 
 
1284
class PhyNICMTUContext(DataPortContext):
 
1285
 
 
1286
    def __call__(self):
 
1287
        ctxt = {}
 
1288
        mappings = super(PhyNICMTUContext, self).__call__()
 
1289
        if mappings and mappings.values():
 
1290
            ports = mappings.values()
 
1291
            napi_settings = NeutronAPIContext()()
 
1292
            mtu = napi_settings.get('network_device_mtu')
 
1293
            if mtu:
 
1294
                ctxt["devs"] = '\\n'.join(ports)
 
1295
                ctxt['mtu'] = mtu
 
1296
 
 
1297
        return ctxt
 
1298
 
 
1299
 
 
1300
class NetworkServiceContext(OSContextGenerator):
 
1301
 
 
1302
    def __init__(self, rel_name='quantum-network-service'):
 
1303
        self.rel_name = rel_name
 
1304
        self.interfaces = [rel_name]
 
1305
 
 
1306
    def __call__(self):
 
1307
        for rid in relation_ids(self.rel_name):
 
1308
            for unit in related_units(rid):
 
1309
                rdata = relation_get(rid=rid, unit=unit)
 
1310
                ctxt = {
 
1311
                    'keystone_host': rdata.get('keystone_host'),
 
1312
                    'service_port': rdata.get('service_port'),
 
1313
                    'auth_port': rdata.get('auth_port'),
 
1314
                    'service_tenant': rdata.get('service_tenant'),
 
1315
                    'service_username': rdata.get('service_username'),
 
1316
                    'service_password': rdata.get('service_password'),
 
1317
                    'quantum_host': rdata.get('quantum_host'),
 
1318
                    'quantum_port': rdata.get('quantum_port'),
 
1319
                    'quantum_url': rdata.get('quantum_url'),
 
1320
                    'region': rdata.get('region'),
 
1321
                    'service_protocol':
 
1322
                    rdata.get('service_protocol') or 'http',
 
1323
                    'auth_protocol':
 
1324
                    rdata.get('auth_protocol') or 'http',
 
1325
                }
 
1326
                if context_complete(ctxt):
 
1327
                    return ctxt
 
1328
        return {}