3
# Common python helper functions used for OpenStack charms.
4
from collections import OrderedDict
12
from charmhelpers.core.hookenv import (
22
from charmhelpers.contrib.storage.linux.lvm import (
23
deactivate_lvm_volume_group,
24
is_lvm_physical_volume,
25
remove_lvm_physical_volume,
28
from charmhelpers.contrib.network.ip import (
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
37
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
38
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
40
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
41
'restricted main multiverse universe')
44
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
45
('oneiric', 'diablo'),
47
('quantal', 'folsom'),
48
('raring', 'grizzly'),
50
('trusty', 'icehouse'),
55
OPENSTACK_CODENAMES = OrderedDict([
59
('2013.1', 'grizzly'),
61
('2014.1', 'icehouse'),
66
SWIFT_CODENAMES = OrderedDict([
76
('1.13.1', 'icehouse'),
77
('1.13.0', 'icehouse'),
78
('1.12.0', 'icehouse'),
79
('1.11.0', 'icehouse'),
85
DEFAULT_LOOPBACK_SIZE = '5G'
89
juju_log("FATAL ERROR: %s" % msg, level='ERROR')
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']
99
if src in ['distro', 'distro-proposed']:
101
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
103
e = 'Could not derive openstack release for '\
104
'this Ubuntu release: %s' % ubuntu_rel
108
if src.startswith('cloud:'):
109
ca_rel = src.split(':')[1]
110
ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
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():
120
def get_os_version_install_source(src):
121
codename = get_os_codename_install_source(src)
122
return get_os_version_codename(codename)
125
def get_os_codename_version(vers):
126
'''Determine OpenStack codename from version number.'''
128
return OPENSTACK_CODENAMES[vers]
130
e = 'Could not determine OpenStack codename for version %s' % vers
134
def get_os_version_codename(codename):
135
'''Determine OpenStack version number from codename.'''
136
for k, v in OPENSTACK_CODENAMES.iteritems():
139
e = 'Could not derive OpenStack version for '\
140
'codename: %s' % codename
144
def get_os_codename_package(package, fatal=True):
145
'''Derive OpenStack release codename from an installed package.'''
146
import apt_pkg as apt
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
160
if not pkg.current_ver:
163
# package is known, but no version is currently installed.
164
e = 'Could not determine version of uninstalled package: %s' % package
167
vers = apt.upstream_version(pkg.current_ver.ver_str)
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]
178
return OPENSTACK_CODENAMES[vers]
180
e = 'Could not determine OpenStack codename for version %s' % vers
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)
192
vers_map = SWIFT_CODENAMES
194
vers_map = OPENSTACK_CODENAMES
196
for version, cname in vers_map.iteritems():
197
if cname == codename:
199
# e = "Could not determine OpenStack version for package: %s" % pkg
206
def os_release(package, base='essex'):
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
216
os_rel = (get_os_codename_package(package, fatal=False) or
217
get_os_codename_install_source(config('openstack-origin')) or
222
def import_key(keyid):
223
cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 " \
224
"--recv-keys %s" % keyid
226
subprocess.check_call(cmd.split(' '))
227
except subprocess.CalledProcessError:
228
error_out("Error importing repo key %s" % keyid)
231
def configure_installation_source(rel):
232
'''Configure apt installation source.'''
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:":
241
subprocess.check_call(["add-apt-repository", "-y", src])
242
elif rel[:3] == "deb":
243
l = len(rel.split('|'))
245
src, key = rel.split('|')
246
juju_log("Importing PPA key from keyserver for %s" % src)
250
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
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]
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)
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(' '))
271
# map charm config options to actual archive 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',
291
pocket = pockets[ca_rel]
293
e = 'Invalid Cloud Archive release specified: %s' % rel
296
src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
297
apt_install('ubuntu-cloud-keyring', fatal=True)
299
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
302
error_out("Invalid openstack-release specified: %s" % rel)
305
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
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
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:
319
[rc_script.write('export %s=%s\n' % (u, p))
320
for u, p in env_vars.iteritems() if u != "script_path"]
323
def openstack_upgrade_available(package):
325
Determines if an OpenStack upgrade is available from installation
326
source, based on version of installed package.
328
:param package: str: Name of installed package.
330
:returns: bool: : Returns True if configured installation source offers
331
a newer version of package.
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)
340
return apt.version_compare(available_vers, cur_vers) == 1
343
def ensure_block_device(block_device):
345
Confirm block_device, create as loopback if necessary.
347
:param block_device: str: Full path of block device to ensure.
349
:returns: str: Full path of ensured block device.
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)
356
if block_device.startswith('/dev/'):
358
elif block_device.startswith('/'):
359
_bd = block_device.split('|')
364
size = DEFAULT_LOOPBACK_SIZE
365
bdev = ensure_loopback_device(bdev, size)
367
bdev = '/dev/%s' % block_device
369
if not is_block_device(bdev):
370
error_out('Failed to locate valid block device at %s' % bdev,
376
def clean_storage(block_device):
378
Ensures a block device is clean. That is:
380
- any lvm volume groups are deactivated
381
- any lvm physical device signatures removed
382
- partition table wiped
384
:param block_device: str: Full path to block device to clean.
386
for mp, d in mounts():
387
if d == block_device:
388
juju_log('clean_storage(): %s is mounted @ %s, unmounting.' %
390
umount(mp, persist=True)
392
if is_lvm_physical_volume(block_device):
393
deactivate_lvm_volume_group(block_device)
394
remove_lvm_physical_volume(block_device)
396
zap_disk(block_device)
401
Returns True if address is a valid IP address.
404
# Test to see if already an IPv4 address
405
socket.inet_aton(address)
411
def ns_query(address):
415
apt_install('python-dnspython')
418
if isinstance(address, dns.name.Name):
420
elif isinstance(address, basestring):
425
answers = dns.resolver.query(address, rtype)
427
return str(answers[0])
431
def get_host_ip(hostname):
433
Resolves the IP for a given hostname, or returns
434
the input if it is already an IP.
439
return ns_query(hostname)
442
def get_hostname(address, fqdn=True):
444
Resolves hostname for given IP, or returns the input
445
if it is already a hostname.
449
import dns.reversename
451
apt_install('python-dnspython')
452
import dns.reversename
454
rev = dns.reversename.from_address(address)
455
result = ns_query(rev)
463
if result.endswith('.'):
468
return result.split('.')[0]
471
def sync_db_with_multi_ipv6_addresses(database, database_user,
472
relation_prefix=None):
473
hosts = get_ipv6_addr(dynamic_only=False)
475
kwargs = {'database': database,
476
'username': database_user,
477
'hostname': json.dumps(hosts)}
482
kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key]
485
for rid in relation_ids('shared-db'):
486
relation_set(relation_id=rid, **kwargs)