~gandelman-a/charms/precise/nova-compute/unused_func

« back to all changes in this revision

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

  • Committer: James Page
  • Date: 2013-10-15 12:04:13 UTC
  • mfrom: (46.1.83 nova-compute)
  • Revision ID: james.page@canonical.com-20131015120413-grclbw2ot5gbgp5r
Update of all Havana / Saucy / python-redux work:

* Full python rewrite using new OpenStack charm-helpers.

* Test coverage

* Havana support

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import json
 
2
import os
 
3
 
 
4
from base64 import b64decode
 
5
 
 
6
from subprocess import (
 
7
    check_call
 
8
)
 
9
 
 
10
 
 
11
from charmhelpers.fetch import (
 
12
    apt_install,
 
13
    filter_installed_packages,
 
14
)
 
15
 
 
16
from charmhelpers.core.hookenv import (
 
17
    config,
 
18
    local_unit,
 
19
    log,
 
20
    relation_get,
 
21
    relation_ids,
 
22
    related_units,
 
23
    unit_get,
 
24
    unit_private_ip,
 
25
    ERROR,
 
26
    WARNING,
 
27
)
 
28
 
 
29
from charmhelpers.contrib.hahelpers.cluster import (
 
30
    determine_api_port,
 
31
    determine_haproxy_port,
 
32
    https,
 
33
    is_clustered,
 
34
    peer_units,
 
35
)
 
36
 
 
37
from charmhelpers.contrib.hahelpers.apache import (
 
38
    get_cert,
 
39
    get_ca_cert,
 
40
)
 
41
 
 
42
from charmhelpers.contrib.openstack.neutron import (
 
43
    neutron_plugin_attribute,
 
44
)
 
45
 
 
46
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
 
47
 
 
48
 
 
49
class OSContextError(Exception):
 
50
    pass
 
51
 
 
52
 
 
53
def ensure_packages(packages):
 
54
    '''Install but do not upgrade required plugin packages'''
 
55
    required = filter_installed_packages(packages)
 
56
    if required:
 
57
        apt_install(required, fatal=True)
 
58
 
 
59
 
 
60
def context_complete(ctxt):
 
61
    _missing = []
 
62
    for k, v in ctxt.iteritems():
 
63
        if v is None or v == '':
 
64
            _missing.append(k)
 
65
    if _missing:
 
66
        log('Missing required data: %s' % ' '.join(_missing), level='INFO')
 
67
        return False
 
68
    return True
 
69
 
 
70
 
 
71
class OSContextGenerator(object):
 
72
    interfaces = []
 
73
 
 
74
    def __call__(self):
 
75
        raise NotImplementedError
 
76
 
 
77
 
 
78
class SharedDBContext(OSContextGenerator):
 
79
    interfaces = ['shared-db']
 
80
 
 
81
    def __init__(self, database=None, user=None, relation_prefix=None):
 
82
        '''
 
83
        Allows inspecting relation for settings prefixed with relation_prefix.
 
84
        This is useful for parsing access for multiple databases returned via
 
85
        the shared-db interface (eg, nova_password, quantum_password)
 
86
        '''
 
87
        self.relation_prefix = relation_prefix
 
88
        self.database = database
 
89
        self.user = user
 
90
 
 
91
    def __call__(self):
 
92
        self.database = self.database or config('database')
 
93
        self.user = self.user or config('database-user')
 
94
        if None in [self.database, self.user]:
 
95
            log('Could not generate shared_db context. '
 
96
                'Missing required charm config options. '
 
97
                '(database name and user)')
 
98
            raise OSContextError
 
99
        ctxt = {}
 
100
 
 
101
        password_setting = 'password'
 
102
        if self.relation_prefix:
 
103
            password_setting = self.relation_prefix + '_password'
 
104
 
 
105
        for rid in relation_ids('shared-db'):
 
106
            for unit in related_units(rid):
 
107
                passwd = relation_get(password_setting, rid=rid, unit=unit)
 
108
                ctxt = {
 
109
                    'database_host': relation_get('db_host', rid=rid,
 
110
                                                  unit=unit),
 
111
                    'database': self.database,
 
112
                    'database_user': self.user,
 
113
                    'database_password': passwd,
 
114
                }
 
115
                if context_complete(ctxt):
 
116
                    return ctxt
 
117
        return {}
 
118
 
 
119
 
 
120
class IdentityServiceContext(OSContextGenerator):
 
121
    interfaces = ['identity-service']
 
122
 
 
123
    def __call__(self):
 
124
        log('Generating template context for identity-service')
 
125
        ctxt = {}
 
126
 
 
127
        for rid in relation_ids('identity-service'):
 
128
            for unit in related_units(rid):
 
129
                ctxt = {
 
130
                    'service_port': relation_get('service_port', rid=rid,
 
131
                                                 unit=unit),
 
132
                    'service_host': relation_get('service_host', rid=rid,
 
133
                                                 unit=unit),
 
134
                    'auth_host': relation_get('auth_host', rid=rid, unit=unit),
 
135
                    'auth_port': relation_get('auth_port', rid=rid, unit=unit),
 
136
                    'admin_tenant_name': relation_get('service_tenant',
 
137
                                                      rid=rid, unit=unit),
 
138
                    'admin_user': relation_get('service_username', rid=rid,
 
139
                                               unit=unit),
 
140
                    'admin_password': relation_get('service_password', rid=rid,
 
141
                                                   unit=unit),
 
142
                    # XXX: Hard-coded http.
 
143
                    'service_protocol': 'http',
 
144
                    'auth_protocol': 'http',
 
145
                }
 
146
                if context_complete(ctxt):
 
147
                    return ctxt
 
148
        return {}
 
149
 
 
150
 
 
151
class AMQPContext(OSContextGenerator):
 
152
    interfaces = ['amqp']
 
153
 
 
154
    def __call__(self):
 
155
        log('Generating template context for amqp')
 
156
        conf = config()
 
157
        try:
 
158
            username = conf['rabbit-user']
 
159
            vhost = conf['rabbit-vhost']
 
160
        except KeyError as e:
 
161
            log('Could not generate shared_db context. '
 
162
                'Missing required charm config options: %s.' % e)
 
163
            raise OSContextError
 
164
 
 
165
        ctxt = {}
 
166
        for rid in relation_ids('amqp'):
 
167
            for unit in related_units(rid):
 
168
                if relation_get('clustered', rid=rid, unit=unit):
 
169
                    ctxt['clustered'] = True
 
170
                    ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
 
171
                                                         unit=unit)
 
172
                else:
 
173
                    ctxt['rabbitmq_host'] = relation_get('private-address',
 
174
                                                         rid=rid, unit=unit)
 
175
                ctxt.update({
 
176
                    'rabbitmq_user': username,
 
177
                    'rabbitmq_password': relation_get('password', rid=rid,
 
178
                                                      unit=unit),
 
179
                    'rabbitmq_virtual_host': vhost,
 
180
                })
 
181
                if context_complete(ctxt):
 
182
                    # Sufficient information found = break out!
 
183
                    break
 
184
            # Used for active/active rabbitmq >= grizzly
 
185
            ctxt['rabbitmq_hosts'] = []
 
186
            for unit in related_units(rid):
 
187
                ctxt['rabbitmq_hosts'].append(relation_get('private-address',
 
188
                                                           rid=rid, unit=unit))
 
189
        if not context_complete(ctxt):
 
190
            return {}
 
191
        else:
 
192
            return ctxt
 
193
 
 
194
 
 
195
class CephContext(OSContextGenerator):
 
196
    interfaces = ['ceph']
 
197
 
 
198
    def __call__(self):
 
199
        '''This generates context for /etc/ceph/ceph.conf templates'''
 
200
        if not relation_ids('ceph'):
 
201
            return {}
 
202
        log('Generating template context for ceph')
 
203
        mon_hosts = []
 
204
        auth = None
 
205
        key = None
 
206
        for rid in relation_ids('ceph'):
 
207
            for unit in related_units(rid):
 
208
                mon_hosts.append(relation_get('private-address', rid=rid,
 
209
                                              unit=unit))
 
210
                auth = relation_get('auth', rid=rid, unit=unit)
 
211
                key = relation_get('key', rid=rid, unit=unit)
 
212
 
 
213
        ctxt = {
 
214
            'mon_hosts': ' '.join(mon_hosts),
 
215
            'auth': auth,
 
216
            'key': key,
 
217
        }
 
218
 
 
219
        if not os.path.isdir('/etc/ceph'):
 
220
            os.mkdir('/etc/ceph')
 
221
 
 
222
        if not context_complete(ctxt):
 
223
            return {}
 
224
 
 
225
        ensure_packages(['ceph-common'])
 
226
 
 
227
        return ctxt
 
228
 
 
229
 
 
230
class HAProxyContext(OSContextGenerator):
 
231
    interfaces = ['cluster']
 
232
 
 
233
    def __call__(self):
 
234
        '''
 
235
        Builds half a context for the haproxy template, which describes
 
236
        all peers to be included in the cluster.  Each charm needs to include
 
237
        its own context generator that describes the port mapping.
 
238
        '''
 
239
        if not relation_ids('cluster'):
 
240
            return {}
 
241
 
 
242
        cluster_hosts = {}
 
243
        l_unit = local_unit().replace('/', '-')
 
244
        cluster_hosts[l_unit] = unit_get('private-address')
 
245
 
 
246
        for rid in relation_ids('cluster'):
 
247
            for unit in related_units(rid):
 
248
                _unit = unit.replace('/', '-')
 
249
                addr = relation_get('private-address', rid=rid, unit=unit)
 
250
                cluster_hosts[_unit] = addr
 
251
 
 
252
        ctxt = {
 
253
            'units': cluster_hosts,
 
254
        }
 
255
        if len(cluster_hosts.keys()) > 1:
 
256
            # Enable haproxy when we have enough peers.
 
257
            log('Ensuring haproxy enabled in /etc/default/haproxy.')
 
258
            with open('/etc/default/haproxy', 'w') as out:
 
259
                out.write('ENABLED=1\n')
 
260
            return ctxt
 
261
        log('HAProxy context is incomplete, this unit has no peers.')
 
262
        return {}
 
263
 
 
264
 
 
265
class ImageServiceContext(OSContextGenerator):
 
266
    interfaces = ['image-service']
 
267
 
 
268
    def __call__(self):
 
269
        '''
 
270
        Obtains the glance API server from the image-service relation.  Useful
 
271
        in nova and cinder (currently).
 
272
        '''
 
273
        log('Generating template context for image-service.')
 
274
        rids = relation_ids('image-service')
 
275
        if not rids:
 
276
            return {}
 
277
        for rid in rids:
 
278
            for unit in related_units(rid):
 
279
                api_server = relation_get('glance-api-server',
 
280
                                          rid=rid, unit=unit)
 
281
                if api_server:
 
282
                    return {'glance_api_servers': api_server}
 
283
        log('ImageService context is incomplete. '
 
284
            'Missing required relation data.')
 
285
        return {}
 
286
 
 
287
 
 
288
class ApacheSSLContext(OSContextGenerator):
 
289
    """
 
290
    Generates a context for an apache vhost configuration that configures
 
291
    HTTPS reverse proxying for one or many endpoints.  Generated context
 
292
    looks something like:
 
293
    {
 
294
        'namespace': 'cinder',
 
295
        'private_address': 'iscsi.mycinderhost.com',
 
296
        'endpoints': [(8776, 8766), (8777, 8767)]
 
297
    }
 
298
 
 
299
    The endpoints list consists of a tuples mapping external ports
 
300
    to internal ports.
 
301
    """
 
302
    interfaces = ['https']
 
303
 
 
304
    # charms should inherit this context and set external ports
 
305
    # and service namespace accordingly.
 
306
    external_ports = []
 
307
    service_namespace = None
 
308
 
 
309
    def enable_modules(self):
 
310
        cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
 
311
        check_call(cmd)
 
312
 
 
313
    def configure_cert(self):
 
314
        if not os.path.isdir('/etc/apache2/ssl'):
 
315
            os.mkdir('/etc/apache2/ssl')
 
316
        ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
 
317
        if not os.path.isdir(ssl_dir):
 
318
            os.mkdir(ssl_dir)
 
319
        cert, key = get_cert()
 
320
        with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
 
321
            cert_out.write(b64decode(cert))
 
322
        with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
 
323
            key_out.write(b64decode(key))
 
324
        ca_cert = get_ca_cert()
 
325
        if ca_cert:
 
326
            with open(CA_CERT_PATH, 'w') as ca_out:
 
327
                ca_out.write(b64decode(ca_cert))
 
328
            check_call(['update-ca-certificates'])
 
329
 
 
330
    def __call__(self):
 
331
        if isinstance(self.external_ports, basestring):
 
332
            self.external_ports = [self.external_ports]
 
333
        if (not self.external_ports or not https()):
 
334
            return {}
 
335
 
 
336
        self.configure_cert()
 
337
        self.enable_modules()
 
338
 
 
339
        ctxt = {
 
340
            'namespace': self.service_namespace,
 
341
            'private_address': unit_get('private-address'),
 
342
            'endpoints': []
 
343
        }
 
344
        for ext_port in self.external_ports:
 
345
            if peer_units() or is_clustered():
 
346
                int_port = determine_haproxy_port(ext_port)
 
347
            else:
 
348
                int_port = determine_api_port(ext_port)
 
349
            portmap = (int(ext_port), int(int_port))
 
350
            ctxt['endpoints'].append(portmap)
 
351
        return ctxt
 
352
 
 
353
 
 
354
class NeutronContext(object):
 
355
    interfaces = []
 
356
 
 
357
    @property
 
358
    def plugin(self):
 
359
        return None
 
360
 
 
361
    @property
 
362
    def network_manager(self):
 
363
        return None
 
364
 
 
365
    @property
 
366
    def packages(self):
 
367
        return neutron_plugin_attribute(
 
368
            self.plugin, 'packages', self.network_manager)
 
369
 
 
370
    @property
 
371
    def neutron_security_groups(self):
 
372
        return None
 
373
 
 
374
    def _ensure_packages(self):
 
375
        [ensure_packages(pkgs) for pkgs in self.packages]
 
376
 
 
377
    def _save_flag_file(self):
 
378
        if self.network_manager == 'quantum':
 
379
            _file = '/etc/nova/quantum_plugin.conf'
 
380
        else:
 
381
            _file = '/etc/nova/neutron_plugin.conf'
 
382
        with open(_file, 'wb') as out:
 
383
            out.write(self.plugin + '\n')
 
384
 
 
385
    def ovs_ctxt(self):
 
386
        driver = neutron_plugin_attribute(self.plugin, 'driver',
 
387
                                          self.network_manager)
 
388
 
 
389
        ovs_ctxt = {
 
390
            'core_plugin': driver,
 
391
            'neutron_plugin': 'ovs',
 
392
            'neutron_security_groups': self.neutron_security_groups,
 
393
            'local_ip': unit_private_ip(),
 
394
        }
 
395
 
 
396
        return ovs_ctxt
 
397
 
 
398
    def __call__(self):
 
399
        self._ensure_packages()
 
400
 
 
401
        if self.network_manager not in ['quantum', 'neutron']:
 
402
            return {}
 
403
 
 
404
        if not self.plugin:
 
405
            return {}
 
406
 
 
407
        ctxt = {'network_manager': self.network_manager}
 
408
 
 
409
        if self.plugin == 'ovs':
 
410
            ctxt.update(self.ovs_ctxt())
 
411
 
 
412
        self._save_flag_file()
 
413
        return ctxt
 
414
 
 
415
 
 
416
class OSConfigFlagContext(OSContextGenerator):
 
417
        '''
 
418
        Responsible adding user-defined config-flags in charm config to a
 
419
        to a template context.
 
420
        '''
 
421
        def __call__(self):
 
422
            config_flags = config('config-flags')
 
423
            if not config_flags or config_flags in ['None', '']:
 
424
                return {}
 
425
            config_flags = config_flags.split(',')
 
426
            flags = {}
 
427
            for flag in config_flags:
 
428
                if '=' not in flag:
 
429
                    log('Improperly formatted config-flag, expected k=v '
 
430
                        'got %s' % flag, level=WARNING)
 
431
                    continue
 
432
                k, v = flag.split('=')
 
433
                flags[k.strip()] = v
 
434
            ctxt = {'user_config_flags': flags}
 
435
            return ctxt
 
436
 
 
437
 
 
438
class SubordinateConfigContext(OSContextGenerator):
 
439
    """
 
440
    Responsible for inspecting relations to subordinates that
 
441
    may be exporting required config via a json blob.
 
442
 
 
443
    The subordinate interface allows subordinates to export their
 
444
    configuration requirements to the principle for multiple config
 
445
    files and multiple serivces.  Ie, a subordinate that has interfaces
 
446
    to both glance and nova may export to following yaml blob as json:
 
447
 
 
448
        glance:
 
449
            /etc/glance/glance-api.conf:
 
450
                sections:
 
451
                    DEFAULT:
 
452
                        - [key1, value1]
 
453
            /etc/glance/glance-registry.conf:
 
454
                    MYSECTION:
 
455
                        - [key2, value2]
 
456
        nova:
 
457
            /etc/nova/nova.conf:
 
458
                sections:
 
459
                    DEFAULT:
 
460
                        - [key3, value3]
 
461
 
 
462
 
 
463
    It is then up to the principle charms to subscribe this context to
 
464
    the service+config file it is interestd in.  Configuration data will
 
465
    be available in the template context, in glance's case, as:
 
466
        ctxt = {
 
467
            ... other context ...
 
468
            'subordinate_config': {
 
469
                'DEFAULT': {
 
470
                    'key1': 'value1',
 
471
                },
 
472
                'MYSECTION': {
 
473
                    'key2': 'value2',
 
474
                },
 
475
            }
 
476
        }
 
477
 
 
478
    """
 
479
    def __init__(self, service, config_file, interface):
 
480
        """
 
481
        :param service     : Service name key to query in any subordinate
 
482
                             data found
 
483
        :param config_file : Service's config file to query sections
 
484
        :param interface   : Subordinate interface to inspect
 
485
        """
 
486
        self.service = service
 
487
        self.config_file = config_file
 
488
        self.interface = interface
 
489
 
 
490
    def __call__(self):
 
491
        ctxt = {}
 
492
        for rid in relation_ids(self.interface):
 
493
            for unit in related_units(rid):
 
494
                sub_config = relation_get('subordinate_configuration',
 
495
                                          rid=rid, unit=unit)
 
496
                if sub_config and sub_config != '':
 
497
                    try:
 
498
                        sub_config = json.loads(sub_config)
 
499
                    except:
 
500
                        log('Could not parse JSON from subordinate_config '
 
501
                            'setting from %s' % rid, level=ERROR)
 
502
                        continue
 
503
 
 
504
                    if self.service not in sub_config:
 
505
                        log('Found subordinate_config on %s but it contained'
 
506
                            'nothing for %s service' % (rid, self.service))
 
507
                        continue
 
508
 
 
509
                    sub_config = sub_config[self.service]
 
510
                    if self.config_file not in sub_config:
 
511
                        log('Found subordinate_config on %s but it contained'
 
512
                            'nothing for %s' % (rid, self.config_file))
 
513
                        continue
 
514
 
 
515
                    sub_config = sub_config[self.config_file]
 
516
                    for k, v in sub_config.iteritems():
 
517
                        ctxt[k] = v
 
518
 
 
519
        if not ctxt:
 
520
            ctxt['sections'] = {}
 
521
 
 
522
        return ctxt