~celebdor/charms/trusty/neutron-agents-midonet/trunk

« back to all changes in this revision

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

  • Committer: Antoni Segura Puimedon
  • Date: 2016-01-25 15:57:58 UTC
  • Revision ID: toni@midokura.com-20160125155758-ni7pxb7vryhgckpj
Sync dependencies

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