~ibmcharmers/ibmlayers/layer-ibm-spectrum-scale-client

« back to all changes in this revision

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

  • Committer: Shilpa Kaul
  • Date: 2017-01-31 10:20:59 UTC
  • Revision ID: shilkaul@in.ibm.com-20170131102059-kae8r4f6229l6ymu
Modified tests

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