3
# Copyright 2014-2015 Canonical Limited.
5
# This file is part of charm-helpers.
7
# charm-helpers is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU Lesser General Public License version 3 as
9
# published by the Free Software Foundation.
11
# charm-helpers is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU Lesser General Public License for more details.
16
# You should have received a copy of the GNU Lesser General Public License
17
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
19
# Common python helper functions used for OpenStack charms.
20
from collections import OrderedDict
21
from functools import wraps
31
from charmhelpers.contrib.network import ip
33
from charmhelpers.core import (
37
from charmhelpers.core.hookenv import (
46
from charmhelpers.contrib.storage.linux.lvm import (
47
deactivate_lvm_volume_group,
48
is_lvm_physical_volume,
49
remove_lvm_physical_volume,
52
from charmhelpers.contrib.network.ip import (
56
from charmhelpers.core.host import lsb_release, mounts, umount
57
from charmhelpers.fetch import apt_install, apt_cache, install_remote
58
from charmhelpers.contrib.python.packages import pip_install
59
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
60
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
62
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
63
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
65
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
66
'restricted main multiverse universe')
69
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
70
('oneiric', 'diablo'),
72
('quantal', 'folsom'),
73
('raring', 'grizzly'),
75
('trusty', 'icehouse'),
81
OPENSTACK_CODENAMES = OrderedDict([
85
('2013.1', 'grizzly'),
87
('2014.1', 'icehouse'),
93
SWIFT_CODENAMES = OrderedDict([
100
('1.10.0', 'havana'),
103
('1.13.1', 'icehouse'),
104
('1.13.0', 'icehouse'),
105
('1.12.0', 'icehouse'),
106
('1.11.0', 'icehouse'),
114
DEFAULT_LOOPBACK_SIZE = '5G'
118
juju_log("FATAL ERROR: %s" % msg, level='ERROR')
122
def get_os_codename_install_source(src):
123
'''Derive OpenStack release codename from a given installation source.'''
124
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
128
if src in ['distro', 'distro-proposed']:
130
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
132
e = 'Could not derive openstack release for '\
133
'this Ubuntu release: %s' % ubuntu_rel
137
if src.startswith('cloud:'):
138
ca_rel = src.split(':')[1]
139
ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
142
# Best guess match based on deb string provided
143
if src.startswith('deb') or src.startswith('ppa'):
144
for k, v in six.iteritems(OPENSTACK_CODENAMES):
149
def get_os_version_install_source(src):
150
codename = get_os_codename_install_source(src)
151
return get_os_version_codename(codename)
154
def get_os_codename_version(vers):
155
'''Determine OpenStack codename from version number.'''
157
return OPENSTACK_CODENAMES[vers]
159
e = 'Could not determine OpenStack codename for version %s' % vers
163
def get_os_version_codename(codename):
164
'''Determine OpenStack version number from codename.'''
165
for k, v in six.iteritems(OPENSTACK_CODENAMES):
168
e = 'Could not derive OpenStack version for '\
169
'codename: %s' % codename
173
def get_os_codename_package(package, fatal=True):
174
'''Derive OpenStack release codename from an installed package.'''
175
import apt_pkg as apt
184
# the package is unknown to the current apt cache.
185
e = 'Could not determine version of package with no installation '\
186
'candidate: %s' % package
189
if not pkg.current_ver:
192
# package is known, but no version is currently installed.
193
e = 'Could not determine version of uninstalled package: %s' % package
196
vers = apt.upstream_version(pkg.current_ver.ver_str)
199
if 'swift' in pkg.name:
200
swift_vers = vers[:5]
201
if swift_vers not in SWIFT_CODENAMES:
202
# Deal with 1.10.0 upward
203
swift_vers = vers[:6]
204
return SWIFT_CODENAMES[swift_vers]
207
return OPENSTACK_CODENAMES[vers]
209
e = 'Could not determine OpenStack codename for version %s' % vers
213
def get_os_version_package(pkg, fatal=True):
214
'''Derive OpenStack version number from an installed package.'''
215
codename = get_os_codename_package(pkg, fatal=fatal)
221
vers_map = SWIFT_CODENAMES
223
vers_map = OPENSTACK_CODENAMES
225
for version, cname in six.iteritems(vers_map):
226
if cname == codename:
228
# e = "Could not determine OpenStack version for package: %s" % pkg
235
def os_release(package, base='essex'):
237
Returns OpenStack release codename from a cached global.
238
If the codename can not be determined from either an installed package or
239
the installation source, the earliest release supported by the charm should
245
os_rel = (get_os_codename_package(package, fatal=False) or
246
get_os_codename_install_source(config('openstack-origin')) or
251
def import_key(keyid):
252
cmd = "apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 " \
253
"--recv-keys %s" % keyid
255
subprocess.check_call(cmd.split(' '))
256
except subprocess.CalledProcessError:
257
error_out("Error importing repo key %s" % keyid)
260
def configure_installation_source(rel):
261
'''Configure apt installation source.'''
264
elif rel == 'distro-proposed':
265
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
266
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
267
f.write(DISTRO_PROPOSED % ubuntu_rel)
268
elif rel[:4] == "ppa:":
270
subprocess.check_call(["add-apt-repository", "-y", src])
271
elif rel[:3] == "deb":
272
l = len(rel.split('|'))
274
src, key = rel.split('|')
275
juju_log("Importing PPA key from keyserver for %s" % src)
279
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
281
elif rel[:6] == 'cloud:':
282
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
283
rel = rel.split(':')[1]
284
u_rel = rel.split('-')[0]
285
ca_rel = rel.split('-')[1]
287
if u_rel != ubuntu_rel:
288
e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
289
'version (%s)' % (ca_rel, ubuntu_rel)
292
if 'staging' in ca_rel:
293
# staging is just a regular PPA.
294
os_rel = ca_rel.split('/')[0]
295
ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
296
cmd = 'add-apt-repository -y %s' % ppa
297
subprocess.check_call(cmd.split(' '))
300
# map charm config options to actual archive pockets.
302
'folsom': 'precise-updates/folsom',
303
'folsom/updates': 'precise-updates/folsom',
304
'folsom/proposed': 'precise-proposed/folsom',
305
'grizzly': 'precise-updates/grizzly',
306
'grizzly/updates': 'precise-updates/grizzly',
307
'grizzly/proposed': 'precise-proposed/grizzly',
308
'havana': 'precise-updates/havana',
309
'havana/updates': 'precise-updates/havana',
310
'havana/proposed': 'precise-proposed/havana',
311
'icehouse': 'precise-updates/icehouse',
312
'icehouse/updates': 'precise-updates/icehouse',
313
'icehouse/proposed': 'precise-proposed/icehouse',
314
'juno': 'trusty-updates/juno',
315
'juno/updates': 'trusty-updates/juno',
316
'juno/proposed': 'trusty-proposed/juno',
317
'kilo': 'trusty-updates/kilo',
318
'kilo/updates': 'trusty-updates/kilo',
319
'kilo/proposed': 'trusty-proposed/kilo',
323
pocket = pockets[ca_rel]
325
e = 'Invalid Cloud Archive release specified: %s' % rel
328
src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
329
apt_install('ubuntu-cloud-keyring', fatal=True)
331
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
334
error_out("Invalid openstack-release specified: %s" % rel)
337
def config_value_changed(option):
339
Determine if config value changed since last call to this function.
341
hook_data = unitdata.HookData()
344
current = config(option)
345
saved = db.get(option)
346
db.set(option, current)
349
return current != saved
352
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
354
Write an rc file in the charm-delivered directory containing
355
exported environment variables provided by env_vars. Any charm scripts run
356
outside the juju hook environment can source this scriptrc to obtain
357
updated config information necessary to perform health checks or
360
juju_rc_path = "%s/%s" % (charm_dir(), script_path)
361
if not os.path.exists(os.path.dirname(juju_rc_path)):
362
os.mkdir(os.path.dirname(juju_rc_path))
363
with open(juju_rc_path, 'wb') as rc_script:
366
[rc_script.write('export %s=%s\n' % (u, p))
367
for u, p in six.iteritems(env_vars) if u != "script_path"]
370
def openstack_upgrade_available(package):
372
Determines if an OpenStack upgrade is available from installation
373
source, based on version of installed package.
375
:param package: str: Name of installed package.
377
:returns: bool: : Returns True if configured installation source offers
378
a newer version of package.
382
import apt_pkg as apt
383
src = config('openstack-origin')
384
cur_vers = get_os_version_package(package)
385
available_vers = get_os_version_install_source(src)
387
return apt.version_compare(available_vers, cur_vers) == 1
390
def ensure_block_device(block_device):
392
Confirm block_device, create as loopback if necessary.
394
:param block_device: str: Full path of block device to ensure.
396
:returns: str: Full path of ensured block device.
398
_none = ['None', 'none', None]
399
if (block_device in _none):
400
error_out('prepare_storage(): Missing required input: block_device=%s.'
403
if block_device.startswith('/dev/'):
405
elif block_device.startswith('/'):
406
_bd = block_device.split('|')
411
size = DEFAULT_LOOPBACK_SIZE
412
bdev = ensure_loopback_device(bdev, size)
414
bdev = '/dev/%s' % block_device
416
if not is_block_device(bdev):
417
error_out('Failed to locate valid block device at %s' % bdev)
422
def clean_storage(block_device):
424
Ensures a block device is clean. That is:
426
- any lvm volume groups are deactivated
427
- any lvm physical device signatures removed
428
- partition table wiped
430
:param block_device: str: Full path to block device to clean.
432
for mp, d in mounts():
433
if d == block_device:
434
juju_log('clean_storage(): %s is mounted @ %s, unmounting.' %
436
umount(mp, persist=True)
438
if is_lvm_physical_volume(block_device):
439
deactivate_lvm_volume_group(block_device)
440
remove_lvm_physical_volume(block_device)
442
zap_disk(block_device)
445
ns_query = ip.ns_query
446
get_host_ip = ip.get_host_ip
447
get_hostname = ip.get_hostname
450
def get_matchmaker_map(mm_file='/etc/oslo/matchmaker_ring.json'):
452
if os.path.isfile(mm_file):
453
with open(mm_file, 'r') as f:
454
mm_map = json.load(f)
458
def sync_db_with_multi_ipv6_addresses(database, database_user,
459
relation_prefix=None):
460
hosts = get_ipv6_addr(dynamic_only=False)
462
kwargs = {'database': database,
463
'username': database_user,
464
'hostname': json.dumps(hosts)}
467
for key in list(kwargs.keys()):
468
kwargs["%s_%s" % (relation_prefix, key)] = kwargs[key]
471
for rid in relation_ids('shared-db'):
472
relation_set(relation_id=rid, **kwargs)
475
def os_requires_version(ostack_release, pkg):
477
Decorator for hook to specify minimum supported release
481
def wrapped_f(*args):
482
if os_release(pkg) < ostack_release:
483
raise Exception("This hook is not supported on releases"
484
" before %s" % ostack_release)
490
def git_install_requested():
492
Returns true if openstack-origin-git is specified.
494
return config('openstack-origin-git') is not None
497
requirements_dir = None
500
def git_clone_and_install(projects_yaml, core_project):
502
Clone/install all specified OpenStack repositories.
504
The expected format of projects_yaml is:
507
repository: 'git://git.openstack.org/openstack/keystone.git',
508
branch: 'stable/icehouse'}
509
- {name: requirements,
510
repository: 'git://git.openstack.org/openstack/requirements.git',
511
branch: 'stable/icehouse'}
512
directory: /mnt/openstack-git
513
http_proxy: http://squid.internal:3128
514
https_proxy: https://squid.internal:3128
516
The directory, http_proxy, and https_proxy keys are optional.
518
global requirements_dir
519
parent_dir = '/mnt/openstack-git'
521
if not projects_yaml:
524
projects = yaml.load(projects_yaml)
525
_git_validate_projects_yaml(projects, core_project)
527
old_environ = dict(os.environ)
529
if 'http_proxy' in projects.keys():
530
os.environ['http_proxy'] = projects['http_proxy']
531
if 'https_proxy' in projects.keys():
532
os.environ['https_proxy'] = projects['https_proxy']
534
if 'directory' in projects.keys():
535
parent_dir = projects['directory']
537
for p in projects['repositories']:
538
repo = p['repository']
540
if p['name'] == 'requirements':
541
repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,
542
update_requirements=False)
543
requirements_dir = repo_dir
545
repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,
546
update_requirements=True)
548
os.environ = old_environ
551
def _git_validate_projects_yaml(projects, core_project):
553
Validate the projects yaml.
555
_git_ensure_key_exists('repositories', projects)
557
for project in projects['repositories']:
558
_git_ensure_key_exists('name', project.keys())
559
_git_ensure_key_exists('repository', project.keys())
560
_git_ensure_key_exists('branch', project.keys())
562
if projects['repositories'][0]['name'] != 'requirements':
563
error_out('{} git repo must be specified first'.format('requirements'))
565
if projects['repositories'][-1]['name'] != core_project:
566
error_out('{} git repo must be specified last'.format(core_project))
569
def _git_ensure_key_exists(key, keys):
571
Ensure that key exists in keys.
574
error_out('openstack-origin-git key \'{}\' is missing'.format(key))
577
def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements):
579
Clone and install a single git repository.
581
dest_dir = os.path.join(parent_dir, os.path.basename(repo))
583
if not os.path.exists(parent_dir):
584
juju_log('Directory already exists at {}. '
585
'No need to create directory.'.format(parent_dir))
588
if not os.path.exists(dest_dir):
589
juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
590
repo_dir = install_remote(repo, dest=parent_dir, branch=branch)
594
if update_requirements:
595
if not requirements_dir:
596
error_out('requirements repo must be cloned before '
597
'updating from global requirements.')
598
_git_update_requirements(repo_dir, requirements_dir)
600
juju_log('Installing git repo from dir: {}'.format(repo_dir))
601
pip_install(repo_dir)
606
def _git_update_requirements(package_dir, reqs_dir):
608
Update from global requirements.
610
Update an OpenStack git directory's requirements.txt and
611
test-requirements.txt from global-requirements.txt.
613
orig_dir = os.getcwd()
615
cmd = ['python', 'update.py', package_dir]
617
subprocess.check_call(cmd)
618
except subprocess.CalledProcessError:
619
package = os.path.basename(package_dir)
620
error_out("Error updating {} from global-requirements.txt".format(package))
624
def git_src_dir(projects_yaml, project):
626
Return the directory where the specified project's source is located.
628
parent_dir = '/mnt/openstack-git'
630
if not projects_yaml:
633
projects = yaml.load(projects_yaml)
635
if 'directory' in projects.keys():
636
parent_dir = projects['directory']
638
for p in projects['repositories']:
639
if p['name'] == project:
640
return os.path.join(parent_dir, os.path.basename(p['repository']))