460 lines
13 KiB
Python
460 lines
13 KiB
Python
#!/usr/bin/python
|
|
|
|
# Common python helper functions used for OpenStack charms.
|
|
from collections import OrderedDict
|
|
|
|
import subprocess
|
|
import os
|
|
import socket
|
|
import sys
|
|
|
|
from charmhelpers.core.hookenv import (
|
|
config,
|
|
log as juju_log,
|
|
charm_dir,
|
|
ERROR,
|
|
INFO
|
|
)
|
|
|
|
from charmhelpers.contrib.storage.linux.lvm import (
|
|
deactivate_lvm_volume_group,
|
|
is_lvm_physical_volume,
|
|
remove_lvm_physical_volume,
|
|
)
|
|
|
|
from charmhelpers.core.host import lsb_release, mounts, umount
|
|
from charmhelpers.fetch import apt_install, apt_cache
|
|
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
|
|
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
|
|
|
|
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
|
|
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
|
|
|
|
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
|
|
'restricted main multiverse universe')
|
|
|
|
|
|
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
|
|
('oneiric', 'diablo'),
|
|
('precise', 'essex'),
|
|
('quantal', 'folsom'),
|
|
('raring', 'grizzly'),
|
|
('saucy', 'havana'),
|
|
('trusty', 'icehouse'),
|
|
('utopic', 'juno'),
|
|
])
|
|
|
|
|
|
OPENSTACK_CODENAMES = OrderedDict([
|
|
('2011.2', 'diablo'),
|
|
('2012.1', 'essex'),
|
|
('2012.2', 'folsom'),
|
|
('2013.1', 'grizzly'),
|
|
('2013.2', 'havana'),
|
|
('2014.1', 'icehouse'),
|
|
('2014.2', 'juno'),
|
|
])
|
|
|
|
# The ugly duckling
|
|
SWIFT_CODENAMES = OrderedDict([
|
|
('1.4.3', 'diablo'),
|
|
('1.4.8', 'essex'),
|
|
('1.7.4', 'folsom'),
|
|
('1.8.0', 'grizzly'),
|
|
('1.7.7', 'grizzly'),
|
|
('1.7.6', 'grizzly'),
|
|
('1.10.0', 'havana'),
|
|
('1.9.1', 'havana'),
|
|
('1.9.0', 'havana'),
|
|
('1.13.1', 'icehouse'),
|
|
('1.13.0', 'icehouse'),
|
|
('1.12.0', 'icehouse'),
|
|
('1.11.0', 'icehouse'),
|
|
('2.0.0', 'juno'),
|
|
])
|
|
|
|
DEFAULT_LOOPBACK_SIZE = '5G'
|
|
|
|
|
|
def error_out(msg):
|
|
juju_log("FATAL ERROR: %s" % msg, level='ERROR')
|
|
sys.exit(1)
|
|
|
|
|
|
def get_os_codename_install_source(src):
|
|
'''Derive OpenStack release codename from a given installation source.'''
|
|
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
|
rel = ''
|
|
if src is None:
|
|
return rel
|
|
if src in ['distro', 'distro-proposed']:
|
|
try:
|
|
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
|
|
except KeyError:
|
|
e = 'Could not derive openstack release for '\
|
|
'this Ubuntu release: %s' % ubuntu_rel
|
|
error_out(e)
|
|
return rel
|
|
|
|
if src.startswith('cloud:'):
|
|
ca_rel = src.split(':')[1]
|
|
ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
|
|
return ca_rel
|
|
|
|
# Best guess match based on deb string provided
|
|
if src.startswith('deb') or src.startswith('ppa'):
|
|
for k, v in OPENSTACK_CODENAMES.iteritems():
|
|
if v in src:
|
|
return v
|
|
|
|
|
|
def get_os_version_install_source(src):
|
|
codename = get_os_codename_install_source(src)
|
|
return get_os_version_codename(codename)
|
|
|
|
|
|
def get_os_codename_version(vers):
|
|
'''Determine OpenStack codename from version number.'''
|
|
try:
|
|
return OPENSTACK_CODENAMES[vers]
|
|
except KeyError:
|
|
e = 'Could not determine OpenStack codename for version %s' % vers
|
|
error_out(e)
|
|
|
|
|
|
def get_os_version_codename(codename):
|
|
'''Determine OpenStack version number from codename.'''
|
|
for k, v in OPENSTACK_CODENAMES.iteritems():
|
|
if v == codename:
|
|
return k
|
|
e = 'Could not derive OpenStack version for '\
|
|
'codename: %s' % codename
|
|
error_out(e)
|
|
|
|
|
|
def get_os_codename_package(package, fatal=True):
|
|
'''Derive OpenStack release codename from an installed package.'''
|
|
import apt_pkg as apt
|
|
|
|
cache = apt_cache()
|
|
|
|
try:
|
|
pkg = cache[package]
|
|
except:
|
|
if not fatal:
|
|
return None
|
|
# the package is unknown to the current apt cache.
|
|
e = 'Could not determine version of package with no installation '\
|
|
'candidate: %s' % package
|
|
error_out(e)
|
|
|
|
if not pkg.current_ver:
|
|
if not fatal:
|
|
return None
|
|
# package is known, but no version is currently installed.
|
|
e = 'Could not determine version of uninstalled package: %s' % package
|
|
error_out(e)
|
|
|
|
vers = apt.upstream_version(pkg.current_ver.ver_str)
|
|
|
|
try:
|
|
if 'swift' in pkg.name:
|
|
swift_vers = vers[:5]
|
|
if swift_vers not in SWIFT_CODENAMES:
|
|
# Deal with 1.10.0 upward
|
|
swift_vers = vers[:6]
|
|
return SWIFT_CODENAMES[swift_vers]
|
|
else:
|
|
vers = vers[:6]
|
|
return OPENSTACK_CODENAMES[vers]
|
|
except KeyError:
|
|
e = 'Could not determine OpenStack codename for version %s' % vers
|
|
error_out(e)
|
|
|
|
|
|
def get_os_version_package(pkg, fatal=True):
|
|
'''Derive OpenStack version number from an installed package.'''
|
|
codename = get_os_codename_package(pkg, fatal=fatal)
|
|
|
|
if not codename:
|
|
return None
|
|
|
|
if 'swift' in pkg:
|
|
vers_map = SWIFT_CODENAMES
|
|
else:
|
|
vers_map = OPENSTACK_CODENAMES
|
|
|
|
for version, cname in vers_map.iteritems():
|
|
if cname == codename:
|
|
return version
|
|
# e = "Could not determine OpenStack version for package: %s" % pkg
|
|
# error_out(e)
|
|
|
|
|
|
os_rel = None
|
|
|
|
|
|
def os_release(package, base='essex'):
|
|
'''
|
|
Returns OpenStack release codename from a cached global.
|
|
If the codename can not be determined from either an installed package or
|
|
the installation source, the earliest release supported by the charm should
|
|
be returned.
|
|
'''
|
|
global os_rel
|
|
if os_rel:
|
|
return os_rel
|
|
os_rel = (get_os_codename_package(package, fatal=False) or
|
|
get_os_codename_install_source(config('openstack-origin')) or
|
|
base)
|
|
return os_rel
|
|
|
|
|
|
def import_key(keyid):
|
|
cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 " \
|
|
"--recv-keys %s" % keyid
|
|
try:
|
|
subprocess.check_call(cmd.split(' '))
|
|
except subprocess.CalledProcessError:
|
|
error_out("Error importing repo key %s" % keyid)
|
|
|
|
|
|
def configure_installation_source(rel):
|
|
'''Configure apt installation source.'''
|
|
if rel == 'distro':
|
|
return
|
|
elif rel == 'distro-proposed':
|
|
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
|
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
|
|
f.write(DISTRO_PROPOSED % ubuntu_rel)
|
|
elif rel[:4] == "ppa:":
|
|
src = rel
|
|
subprocess.check_call(["add-apt-repository", "-y", src])
|
|
elif rel[:3] == "deb":
|
|
l = len(rel.split('|'))
|
|
if l == 2:
|
|
src, key = rel.split('|')
|
|
juju_log("Importing PPA key from keyserver for %s" % src)
|
|
import_key(key)
|
|
elif l == 1:
|
|
src = rel
|
|
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
|
|
f.write(src)
|
|
elif rel[:6] == 'cloud:':
|
|
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
|
rel = rel.split(':')[1]
|
|
u_rel = rel.split('-')[0]
|
|
ca_rel = rel.split('-')[1]
|
|
|
|
if u_rel != ubuntu_rel:
|
|
e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
|
|
'version (%s)' % (ca_rel, ubuntu_rel)
|
|
error_out(e)
|
|
|
|
if 'staging' in ca_rel:
|
|
# staging is just a regular PPA.
|
|
os_rel = ca_rel.split('/')[0]
|
|
ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
|
|
cmd = 'add-apt-repository -y %s' % ppa
|
|
subprocess.check_call(cmd.split(' '))
|
|
return
|
|
|
|
# map charm config options to actual archive pockets.
|
|
pockets = {
|
|
'folsom': 'precise-updates/folsom',
|
|
'folsom/updates': 'precise-updates/folsom',
|
|
'folsom/proposed': 'precise-proposed/folsom',
|
|
'grizzly': 'precise-updates/grizzly',
|
|
'grizzly/updates': 'precise-updates/grizzly',
|
|
'grizzly/proposed': 'precise-proposed/grizzly',
|
|
'havana': 'precise-updates/havana',
|
|
'havana/updates': 'precise-updates/havana',
|
|
'havana/proposed': 'precise-proposed/havana',
|
|
'icehouse': 'precise-updates/icehouse',
|
|
'icehouse/updates': 'precise-updates/icehouse',
|
|
'icehouse/proposed': 'precise-proposed/icehouse',
|
|
'juno': 'trusty-updates/juno',
|
|
'juno/updates': 'trusty-updates/juno',
|
|
'juno/proposed': 'trusty-proposed/juno',
|
|
}
|
|
|
|
try:
|
|
pocket = pockets[ca_rel]
|
|
except KeyError:
|
|
e = 'Invalid Cloud Archive release specified: %s' % rel
|
|
error_out(e)
|
|
|
|
src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
|
|
apt_install('ubuntu-cloud-keyring', fatal=True)
|
|
|
|
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
|
|
f.write(src)
|
|
else:
|
|
error_out("Invalid openstack-release specified: %s" % rel)
|
|
|
|
|
|
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
|
|
"""
|
|
Write an rc file in the charm-delivered directory containing
|
|
exported environment variables provided by env_vars. Any charm scripts run
|
|
outside the juju hook environment can source this scriptrc to obtain
|
|
updated config information necessary to perform health checks or
|
|
service changes.
|
|
"""
|
|
juju_rc_path = "%s/%s" % (charm_dir(), script_path)
|
|
if not os.path.exists(os.path.dirname(juju_rc_path)):
|
|
os.mkdir(os.path.dirname(juju_rc_path))
|
|
with open(juju_rc_path, 'wb') as rc_script:
|
|
rc_script.write(
|
|
"#!/bin/bash\n")
|
|
[rc_script.write('export %s=%s\n' % (u, p))
|
|
for u, p in env_vars.iteritems() if u != "script_path"]
|
|
|
|
|
|
def openstack_upgrade_available(package):
|
|
"""
|
|
Determines if an OpenStack upgrade is available from installation
|
|
source, based on version of installed package.
|
|
|
|
:param package: str: Name of installed package.
|
|
|
|
:returns: bool: : Returns True if configured installation source offers
|
|
a newer version of package.
|
|
|
|
"""
|
|
|
|
import apt_pkg as apt
|
|
src = config('openstack-origin')
|
|
cur_vers = get_os_version_package(package)
|
|
available_vers = get_os_version_install_source(src)
|
|
apt.init()
|
|
return apt.version_compare(available_vers, cur_vers) == 1
|
|
|
|
|
|
def ensure_block_device(block_device):
|
|
'''
|
|
Confirm block_device, create as loopback if necessary.
|
|
|
|
:param block_device: str: Full path of block device to ensure.
|
|
|
|
:returns: str: Full path of ensured block device.
|
|
'''
|
|
_none = ['None', 'none', None]
|
|
if (block_device in _none):
|
|
error_out('prepare_storage(): Missing required input: '
|
|
'block_device=%s.' % block_device, level=ERROR)
|
|
|
|
if block_device.startswith('/dev/'):
|
|
bdev = block_device
|
|
elif block_device.startswith('/'):
|
|
_bd = block_device.split('|')
|
|
if len(_bd) == 2:
|
|
bdev, size = _bd
|
|
else:
|
|
bdev = block_device
|
|
size = DEFAULT_LOOPBACK_SIZE
|
|
bdev = ensure_loopback_device(bdev, size)
|
|
else:
|
|
bdev = '/dev/%s' % block_device
|
|
|
|
if not is_block_device(bdev):
|
|
error_out('Failed to locate valid block device at %s' % bdev,
|
|
level=ERROR)
|
|
|
|
return bdev
|
|
|
|
|
|
def clean_storage(block_device):
|
|
'''
|
|
Ensures a block device is clean. That is:
|
|
- unmounted
|
|
- any lvm volume groups are deactivated
|
|
- any lvm physical device signatures removed
|
|
- partition table wiped
|
|
|
|
:param block_device: str: Full path to block device to clean.
|
|
'''
|
|
for mp, d in mounts():
|
|
if d == block_device:
|
|
juju_log('clean_storage(): %s is mounted @ %s, unmounting.' %
|
|
(d, mp), level=INFO)
|
|
umount(mp, persist=True)
|
|
|
|
if is_lvm_physical_volume(block_device):
|
|
deactivate_lvm_volume_group(block_device)
|
|
remove_lvm_physical_volume(block_device)
|
|
else:
|
|
zap_disk(block_device)
|
|
|
|
|
|
def is_ip(address):
|
|
"""
|
|
Returns True if address is a valid IP address.
|
|
"""
|
|
try:
|
|
# Test to see if already an IPv4 address
|
|
socket.inet_aton(address)
|
|
return True
|
|
except socket.error:
|
|
return False
|
|
|
|
|
|
def ns_query(address):
|
|
try:
|
|
import dns.resolver
|
|
except ImportError:
|
|
apt_install('python-dnspython')
|
|
import dns.resolver
|
|
|
|
if isinstance(address, dns.name.Name):
|
|
rtype = 'PTR'
|
|
elif isinstance(address, basestring):
|
|
rtype = 'A'
|
|
else:
|
|
return None
|
|
|
|
answers = dns.resolver.query(address, rtype)
|
|
if answers:
|
|
return str(answers[0])
|
|
return None
|
|
|
|
|
|
def get_host_ip(hostname):
|
|
"""
|
|
Resolves the IP for a given hostname, or returns
|
|
the input if it is already an IP.
|
|
"""
|
|
if is_ip(hostname):
|
|
return hostname
|
|
|
|
return ns_query(hostname)
|
|
|
|
|
|
def get_hostname(address, fqdn=True):
|
|
"""
|
|
Resolves hostname for given IP, or returns the input
|
|
if it is already a hostname.
|
|
"""
|
|
if is_ip(address):
|
|
try:
|
|
import dns.reversename
|
|
except ImportError:
|
|
apt_install('python-dnspython')
|
|
import dns.reversename
|
|
|
|
rev = dns.reversename.from_address(address)
|
|
result = ns_query(rev)
|
|
if not result:
|
|
return None
|
|
else:
|
|
result = address
|
|
|
|
if fqdn:
|
|
# strip trailing .
|
|
if result.endswith('.'):
|
|
return result[:-1]
|
|
else:
|
|
return result
|
|
else:
|
|
return result.split('.')[0]
|