~xianghui/charms/trusty/hacluster/ha-scale

« back to all changes in this revision

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

  • Committer: Hui Xiang
  • Date: 2014-12-12 07:21:20 UTC
  • mfrom: (35.2.2 hacluster.next)
  • Revision ID: hui.xiang@canonical.com-20141212072120-wkaem1404zkwqbm7
Merge hacluster/next, add contraints.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
#!/usr/bin/python
 
2
 
 
3
# Common python helper functions used for OpenStack charms.
 
4
from collections import OrderedDict
 
5
 
 
6
import subprocess
 
7
import json
 
8
import os
 
9
import socket
 
10
import sys
 
11
 
 
12
from charmhelpers.core.hookenv import (
 
13
    config,
 
14
    log as juju_log,
 
15
    charm_dir,
 
16
    ERROR,
 
17
    INFO,
 
18
    relation_ids,
 
19
    relation_set
 
20
)
 
21
 
 
22
from charmhelpers.contrib.storage.linux.lvm import (
 
23
    deactivate_lvm_volume_group,
 
24
    is_lvm_physical_volume,
 
25
    remove_lvm_physical_volume,
 
26
)
 
27
 
 
28
from charmhelpers.contrib.network.ip import (
 
29
    get_ipv6_addr
 
30
)
 
31
 
 
32
from charmhelpers.core.host import lsb_release, mounts, umount
 
33
from charmhelpers.fetch import apt_install, apt_cache
 
34
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
 
35
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
 
36
 
 
37
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
 
38
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
 
39
 
 
40
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
 
41
                   'restricted main multiverse universe')
 
42
 
 
43
 
 
44
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
 
45
    ('oneiric', 'diablo'),
 
46
    ('precise', 'essex'),
 
47
    ('quantal', 'folsom'),
 
48
    ('raring', 'grizzly'),
 
49
    ('saucy', 'havana'),
 
50
    ('trusty', 'icehouse'),
 
51
    ('utopic', 'juno'),
 
52
])
 
53
 
 
54
 
 
55
OPENSTACK_CODENAMES = OrderedDict([
 
56
    ('2011.2', 'diablo'),
 
57
    ('2012.1', 'essex'),
 
58
    ('2012.2', 'folsom'),
 
59
    ('2013.1', 'grizzly'),
 
60
    ('2013.2', 'havana'),
 
61
    ('2014.1', 'icehouse'),
 
62
    ('2014.2', 'juno'),
 
63
])
 
64
 
 
65
# The ugly duckling
 
66
SWIFT_CODENAMES = OrderedDict([
 
67
    ('1.4.3', 'diablo'),
 
68
    ('1.4.8', 'essex'),
 
69
    ('1.7.4', 'folsom'),
 
70
    ('1.8.0', 'grizzly'),
 
71
    ('1.7.7', 'grizzly'),
 
72
    ('1.7.6', 'grizzly'),
 
73
    ('1.10.0', 'havana'),
 
74
    ('1.9.1', 'havana'),
 
75
    ('1.9.0', 'havana'),
 
76
    ('1.13.1', 'icehouse'),
 
77
    ('1.13.0', 'icehouse'),
 
78
    ('1.12.0', 'icehouse'),
 
79
    ('1.11.0', 'icehouse'),
 
80
    ('2.0.0', 'juno'),
 
81
    ('2.1.0', 'juno'),
 
82
    ('2.2.0', 'juno'),
 
83
])
 
84
 
 
85
DEFAULT_LOOPBACK_SIZE = '5G'
 
86
 
 
87
 
 
88
def error_out(msg):
 
89
    juju_log("FATAL ERROR: %s" % msg, level='ERROR')
 
90
    sys.exit(1)
 
91
 
 
92
 
 
93
def get_os_codename_install_source(src):
 
94
    '''Derive OpenStack release codename from a given installation source.'''
 
95
    ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
 
96
    rel = ''
 
97
    if src is None:
 
98
        return rel
 
99
    if src in ['distro', 'distro-proposed']:
 
100
        try:
 
101
            rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
 
102
        except KeyError:
 
103
            e = 'Could not derive openstack release for '\
 
104
                'this Ubuntu release: %s' % ubuntu_rel
 
105
            error_out(e)
 
106
        return rel
 
107
 
 
108
    if src.startswith('cloud:'):
 
109
        ca_rel = src.split(':')[1]
 
110
        ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
 
111
        return ca_rel
 
112
 
 
113
    # Best guess match based on deb string provided
 
114
    if src.startswith('deb') or src.startswith('ppa'):
 
115
        for k, v in OPENSTACK_CODENAMES.iteritems():
 
116
            if v in src:
 
117
                return v
 
118
 
 
119
 
 
120
def get_os_version_install_source(src):
 
121
    codename = get_os_codename_install_source(src)
 
122
    return get_os_version_codename(codename)
 
123
 
 
124
 
 
125
def get_os_codename_version(vers):
 
126
    '''Determine OpenStack codename from version number.'''
 
127
    try:
 
128
        return OPENSTACK_CODENAMES[vers]
 
129
    except KeyError:
 
130
        e = 'Could not determine OpenStack codename for version %s' % vers
 
131
        error_out(e)
 
132
 
 
133
 
 
134
def get_os_version_codename(codename):
 
135
    '''Determine OpenStack version number from codename.'''
 
136
    for k, v in OPENSTACK_CODENAMES.iteritems():
 
137
        if v == codename:
 
138
            return k
 
139
    e = 'Could not derive OpenStack version for '\
 
140
        'codename: %s' % codename
 
141
    error_out(e)
 
142
 
 
143
 
 
144
def get_os_codename_package(package, fatal=True):
 
145
    '''Derive OpenStack release codename from an installed package.'''
 
146
    import apt_pkg as apt
 
147
 
 
148
    cache = apt_cache()
 
149
 
 
150
    try:
 
151
        pkg = cache[package]
 
152
    except:
 
153
        if not fatal:
 
154
            return None
 
155
        # the package is unknown to the current apt cache.
 
156
        e = 'Could not determine version of package with no installation '\
 
157
            'candidate: %s' % package
 
158
        error_out(e)
 
159
 
 
160
    if not pkg.current_ver:
 
161
        if not fatal:
 
162
            return None
 
163
        # package is known, but no version is currently installed.
 
164
        e = 'Could not determine version of uninstalled package: %s' % package
 
165
        error_out(e)
 
166
 
 
167
    vers = apt.upstream_version(pkg.current_ver.ver_str)
 
168
 
 
169
    try:
 
170
        if 'swift' in pkg.name:
 
171
            swift_vers = vers[:5]
 
172
            if swift_vers not in SWIFT_CODENAMES:
 
173
                # Deal with 1.10.0 upward
 
174
                swift_vers = vers[:6]
 
175
            return SWIFT_CODENAMES[swift_vers]
 
176
        else:
 
177
            vers = vers[:6]
 
178
            return OPENSTACK_CODENAMES[vers]
 
179
    except KeyError:
 
180
        e = 'Could not determine OpenStack codename for version %s' % vers
 
181
        error_out(e)
 
182
 
 
183
 
 
184
def get_os_version_package(pkg, fatal=True):
 
185
    '''Derive OpenStack version number from an installed package.'''
 
186
    codename = get_os_codename_package(pkg, fatal=fatal)
 
187
 
 
188
    if not codename:
 
189
        return None
 
190
 
 
191
    if 'swift' in pkg:
 
192
        vers_map = SWIFT_CODENAMES
 
193
    else:
 
194
        vers_map = OPENSTACK_CODENAMES
 
195
 
 
196
    for version, cname in vers_map.iteritems():
 
197
        if cname == codename:
 
198
            return version
 
199
    # e = "Could not determine OpenStack version for package: %s" % pkg
 
200
    # error_out(e)
 
201
 
 
202
 
 
203
os_rel = None
 
204
 
 
205
 
 
206
def os_release(package, base='essex'):
 
207
    '''
 
208
    Returns OpenStack release codename from a cached global.
 
209
    If the codename can not be determined from either an installed package or
 
210
    the installation source, the earliest release supported by the charm should
 
211
    be returned.
 
212
    '''
 
213
    global os_rel
 
214
    if os_rel:
 
215
        return os_rel
 
216
    os_rel = (get_os_codename_package(package, fatal=False) or
 
217
              get_os_codename_install_source(config('openstack-origin')) or
 
218
              base)
 
219
    return os_rel
 
220
 
 
221
 
 
222
def import_key(keyid):
 
223
    cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 " \
 
224
          "--recv-keys %s" % keyid
 
225
    try:
 
226
        subprocess.check_call(cmd.split(' '))
 
227
    except subprocess.CalledProcessError:
 
228
        error_out("Error importing repo key %s" % keyid)
 
229
 
 
230
 
 
231
def configure_installation_source(rel):
 
232
    '''Configure apt installation source.'''
 
233
    if rel == 'distro':
 
234
        return
 
235
    elif rel == 'distro-proposed':
 
236
        ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
 
237
        with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
 
238
            f.write(DISTRO_PROPOSED % ubuntu_rel)
 
239
    elif rel[:4] == "ppa:":
 
240
        src = rel
 
241
        subprocess.check_call(["add-apt-repository", "-y", src])
 
242
    elif rel[:3] == "deb":
 
243
        l = len(rel.split('|'))
 
244
        if l == 2:
 
245
            src, key = rel.split('|')
 
246
            juju_log("Importing PPA key from keyserver for %s" % src)
 
247
            import_key(key)
 
248
        elif l == 1:
 
249
            src = rel
 
250
        with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
 
251
            f.write(src)
 
252
    elif rel[:6] == 'cloud:':
 
253
        ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
 
254
        rel = rel.split(':')[1]
 
255
        u_rel = rel.split('-')[0]
 
256
        ca_rel = rel.split('-')[1]
 
257
 
 
258
        if u_rel != ubuntu_rel:
 
259
            e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
 
260
                'version (%s)' % (ca_rel, ubuntu_rel)
 
261
            error_out(e)
 
262
 
 
263
        if 'staging' in ca_rel:
 
264
            # staging is just a regular PPA.
 
265
            os_rel = ca_rel.split('/')[0]
 
266
            ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
 
267
            cmd = 'add-apt-repository -y %s' % ppa
 
268
            subprocess.check_call(cmd.split(' '))
 
269
            return
 
270
 
 
271
        # map charm config options to actual archive pockets.
 
272
        pockets = {
 
273
            'folsom': 'precise-updates/folsom',
 
274
            'folsom/updates': 'precise-updates/folsom',
 
275
            'folsom/proposed': 'precise-proposed/folsom',
 
276
            'grizzly': 'precise-updates/grizzly',
 
277
            'grizzly/updates': 'precise-updates/grizzly',
 
278
            'grizzly/proposed': 'precise-proposed/grizzly',
 
279
            'havana': 'precise-updates/havana',
 
280
            'havana/updates': 'precise-updates/havana',
 
281
            'havana/proposed': 'precise-proposed/havana',
 
282
            'icehouse': 'precise-updates/icehouse',
 
283
            'icehouse/updates': 'precise-updates/icehouse',
 
284
            'icehouse/proposed': 'precise-proposed/icehouse',
 
285
            'juno': 'trusty-updates/juno',
 
286
            'juno/updates': 'trusty-updates/juno',
 
287
            'juno/proposed': 'trusty-proposed/juno',
 
288
        }
 
289
 
 
290
        try:
 
291
            pocket = pockets[ca_rel]
 
292
        except KeyError:
 
293
            e = 'Invalid Cloud Archive release specified: %s' % rel
 
294
            error_out(e)
 
295
 
 
296
        src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
 
297
        apt_install('ubuntu-cloud-keyring', fatal=True)
 
298
 
 
299
        with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
 
300
            f.write(src)
 
301
    else:
 
302
        error_out("Invalid openstack-release specified: %s" % rel)
 
303
 
 
304
 
 
305
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
 
306
    """
 
307
    Write an rc file in the charm-delivered directory containing
 
308
    exported environment variables provided by env_vars. Any charm scripts run
 
309
    outside the juju hook environment can source this scriptrc to obtain
 
310
    updated config information necessary to perform health checks or
 
311
    service changes.
 
312
    """
 
313
    juju_rc_path = "%s/%s" % (charm_dir(), script_path)
 
314
    if not os.path.exists(os.path.dirname(juju_rc_path)):
 
315
        os.mkdir(os.path.dirname(juju_rc_path))
 
316
    with open(juju_rc_path, 'wb') as rc_script:
 
317
        rc_script.write(
 
318
            "#!/bin/bash\n")
 
319
        [rc_script.write('export %s=%s\n' % (u, p))
 
320
         for u, p in env_vars.iteritems() if u != "script_path"]
 
321
 
 
322
 
 
323
def openstack_upgrade_available(package):
 
324
    """
 
325
    Determines if an OpenStack upgrade is available from installation
 
326
    source, based on version of installed package.
 
327
 
 
328
    :param package: str: Name of installed package.
 
329
 
 
330
    :returns: bool:    : Returns True if configured installation source offers
 
331
                         a newer version of package.
 
332
 
 
333
    """
 
334
 
 
335
    import apt_pkg as apt
 
336
    src = config('openstack-origin')
 
337
    cur_vers = get_os_version_package(package)
 
338
    available_vers = get_os_version_install_source(src)
 
339
    apt.init()
 
340
    return apt.version_compare(available_vers, cur_vers) == 1
 
341
 
 
342
 
 
343
def ensure_block_device(block_device):
 
344
    '''
 
345
    Confirm block_device, create as loopback if necessary.
 
346
 
 
347
    :param block_device: str: Full path of block device to ensure.
 
348
 
 
349
    :returns: str: Full path of ensured block device.
 
350
    '''
 
351
    _none = ['None', 'none', None]
 
352
    if (block_device in _none):
 
353
        error_out('prepare_storage(): Missing required input: '
 
354
                  'block_device=%s.' % block_device, level=ERROR)
 
355
 
 
356
    if block_device.startswith('/dev/'):
 
357
        bdev = block_device
 
358
    elif block_device.startswith('/'):
 
359
        _bd = block_device.split('|')
 
360
        if len(_bd) == 2:
 
361
            bdev, size = _bd
 
362
        else:
 
363
            bdev = block_device
 
364
            size = DEFAULT_LOOPBACK_SIZE
 
365
        bdev = ensure_loopback_device(bdev, size)
 
366
    else:
 
367
        bdev = '/dev/%s' % block_device
 
368
 
 
369
    if not is_block_device(bdev):
 
370
        error_out('Failed to locate valid block device at %s' % bdev,
 
371
                  level=ERROR)
 
372
 
 
373
    return bdev
 
374
 
 
375
 
 
376
def clean_storage(block_device):
 
377
    '''
 
378
    Ensures a block device is clean.  That is:
 
379
        - unmounted
 
380
        - any lvm volume groups are deactivated
 
381
        - any lvm physical device signatures removed
 
382
        - partition table wiped
 
383
 
 
384
    :param block_device: str: Full path to block device to clean.
 
385
    '''
 
386
    for mp, d in mounts():
 
387
        if d == block_device:
 
388
            juju_log('clean_storage(): %s is mounted @ %s, unmounting.' %
 
389
                     (d, mp), level=INFO)
 
390
            umount(mp, persist=True)
 
391
 
 
392
    if is_lvm_physical_volume(block_device):
 
393
        deactivate_lvm_volume_group(block_device)
 
394
        remove_lvm_physical_volume(block_device)
 
395
    else:
 
396
        zap_disk(block_device)
 
397
 
 
398
 
 
399
def is_ip(address):
 
400
    """
 
401
    Returns True if address is a valid IP address.
 
402
    """
 
403
    try:
 
404
        # Test to see if already an IPv4 address
 
405
        socket.inet_aton(address)
 
406
        return True
 
407
    except socket.error:
 
408
        return False
 
409
 
 
410
 
 
411
def ns_query(address):
 
412
    try:
 
413
        import dns.resolver
 
414
    except ImportError:
 
415
        apt_install('python-dnspython')
 
416
        import dns.resolver
 
417
 
 
418
    if isinstance(address, dns.name.Name):
 
419
        rtype = 'PTR'
 
420
    elif isinstance(address, basestring):
 
421
        rtype = 'A'
 
422
    else:
 
423
        return None
 
424
 
 
425
    answers = dns.resolver.query(address, rtype)
 
426
    if answers:
 
427
        return str(answers[0])
 
428
    return None
 
429
 
 
430
 
 
431
def get_host_ip(hostname):
 
432
    """
 
433
    Resolves the IP for a given hostname, or returns
 
434
    the input if it is already an IP.
 
435
    """
 
436
    if is_ip(hostname):
 
437
        return hostname
 
438
 
 
439
    return ns_query(hostname)
 
440
 
 
441
 
 
442
def get_hostname(address, fqdn=True):
 
443
    """
 
444
    Resolves hostname for given IP, or returns the input
 
445
    if it is already a hostname.
 
446
    """
 
447
    if is_ip(address):
 
448
        try:
 
449
            import dns.reversename
 
450
        except ImportError:
 
451
            apt_install('python-dnspython')
 
452
            import dns.reversename
 
453
 
 
454
        rev = dns.reversename.from_address(address)
 
455
        result = ns_query(rev)
 
456
        if not result:
 
457
            return None
 
458
    else:
 
459
        result = address
 
460
 
 
461
    if fqdn:
 
462
        # strip trailing .
 
463
        if result.endswith('.'):
 
464
            return result[:-1]
 
465
        else:
 
466
            return result
 
467
    else:
 
468
        return result.split('.')[0]
 
469
 
 
470
 
 
471
def sync_db_with_multi_ipv6_addresses(database, database_user,
 
472
                                      relation_prefix=None):
 
473
    hosts = get_ipv6_addr(dynamic_only=False)
 
474
 
 
475
    kwargs = {'database': database,
 
476
              'username': database_user,
 
477
              'hostname': json.dumps(hosts)}
 
478
 
 
479
    if relation_prefix:
 
480
        keys = kwargs.keys()
 
481
        for key in keys:
 
482
            kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key]
 
483
            del kwargs[key]
 
484
 
 
485
    for rid in relation_ids('shared-db'):
 
486
        relation_set(relation_id=rid, **kwargs)