~narindergupta/opnfv/neutron-gateway

« back to all changes in this revision

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

  • Committer: Narinder Gupta (for canonical email id)
  • Date: 2017-02-15 16:35:39 UTC
  • Revision ID: narinder.gupta@canonical.com-20170215163539-3mm1k5mo1dxl4r5x
first draft version.

Show diffs side-by-side

added added

removed removed

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