~robert-ayres/charms/trusty/contrail-configuration/trunk

« back to all changes in this revision

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

  • Committer: Robert Ayres
  • Date: 2014-09-10 14:03:02 UTC
  • Revision ID: robert.ayres@canonical.com-20140910140302-bqu0wb61an4nhgfa
Initial charm

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