~landscape/charms/trusty/keystone-apt/trunk

« back to all changes in this revision

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

  • Committer: James Page
  • Date: 2014-04-16 08:20:08 UTC
  • mfrom: (52.2.30 keystone)
  • Revision ID: james.page@canonical.com-20140416082008-34w0nyak0y571tfp
[james-page,ivoks,hazmat,yolanda.robla,r=james-page,t=*]

Redux to used charm helpers
Support for Icehouse on 12.04 and 14.04
Support for Active/Active and SSL RabbitMQ
Support for SSL MySQL
Support for SSL endpoints
Support for PostgreSQL

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