Update of all Havana / Saucy / python-redux work:

* Full python rewrite using new OpenStack charm-helpers.

* Test coverage

* Havana support
This commit is contained in:
James Page 2013-10-15 13:04:13 +01:00
commit 812fafd5b7
88 changed files with 6108 additions and 1860 deletions

6
.coveragerc Normal file
View File

@ -0,0 +1,6 @@
[report]
# Regexes for lines to exclude from consideration
exclude_lines =
if __name__ == .__main__.:
include=
hooks/nova_*

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>nova-compute</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

9
.pydevproject Normal file
View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/nova-compute/hooks</path>
<path>/nova-compute/unit_tests</path>
</pydev_pathproperty>
</pydev_project>

14
Makefile Normal file
View File

@ -0,0 +1,14 @@
#!/usr/bin/make
PYTHON := /usr/bin/env python
lint:
@flake8 --exclude hooks/charmhelpers hooks
@flake8 --exclude hooks/charmhelpers unit_tests
@charm proof
test:
@echo Starting tests...
@$(PYTHON) /usr/bin/nosetests --nologcapture --with-coverage unit_tests
sync:
@charm-helper-sync -c charm-helpers.yaml

12
charm-helpers.yaml Normal file
View File

@ -0,0 +1,12 @@
branch: lp:charm-helpers
destination: hooks/charmhelpers
include:
- core
- fetch
- contrib.openstack|inc=*
- contrib.storage
- contrib.hahelpers:
- apache
- cluster
- contrib.network.ovs
- payload.execd

View File

@ -26,14 +26,22 @@ options:
default: nova
type: string
decsription: Rabbitmq vhost
db-user:
database-user:
default: nova
type: string
description: Username for database access
nova-db:
database:
default: nova
type: string
description: Database name
neutron-database-user:
default: neutron
type: string
description: Username for Neutron database access (if enabled)
neutron-database:
default: neutron
type: string
description: Database name for Neutron (if enabled)
virt-type:
default: kvm
type: string
@ -71,7 +79,6 @@ options:
type: string
description: Network interface on which to build bridge
config-flags:
default: None
type: string
description: Comma separated list of key=value config flags to be set in nova.conf.
nagios_context:

0
hooks/__init__.py Normal file
View File

1
hooks/amqp-relation-broken Symbolic link
View File

@ -0,0 +1 @@
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

1
hooks/ceph-relation-broken Symbolic link
View File

@ -0,0 +1 @@
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

View File

View File

@ -0,0 +1,58 @@
#
# Copyright 2012 Canonical Ltd.
#
# This file is sourced from lp:openstack-charm-helpers
#
# Authors:
# James Page <james.page@ubuntu.com>
# Adam Gandelman <adamg@ubuntu.com>
#
import subprocess
from charmhelpers.core.hookenv import (
config as config_get,
relation_get,
relation_ids,
related_units as relation_list,
log,
INFO,
)
def get_cert():
cert = config_get('ssl_cert')
key = config_get('ssl_key')
if not (cert and key):
log("Inspecting identity-service relations for SSL certificate.",
level=INFO)
cert = key = None
for r_id in relation_ids('identity-service'):
for unit in relation_list(r_id):
if not cert:
cert = relation_get('ssl_cert',
rid=r_id, unit=unit)
if not key:
key = relation_get('ssl_key',
rid=r_id, unit=unit)
return (cert, key)
def get_ca_cert():
ca_cert = None
log("Inspecting identity-service relations for CA SSL certificate.",
level=INFO)
for r_id in relation_ids('identity-service'):
for unit in relation_list(r_id):
if not ca_cert:
ca_cert = relation_get('ca_cert',
rid=r_id, unit=unit)
return ca_cert
def install_ca_cert(ca_cert):
if ca_cert:
with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
'w') as crt:
crt.write(ca_cert)
subprocess.check_call(['update-ca-certificates', '--fresh'])

View File

@ -0,0 +1,294 @@
#
# Copyright 2012 Canonical Ltd.
#
# This file is sourced from lp:openstack-charm-helpers
#
# Authors:
# James Page <james.page@ubuntu.com>
# Adam Gandelman <adamg@ubuntu.com>
#
import commands
import os
import shutil
import time
from subprocess import (
check_call,
check_output,
CalledProcessError
)
from charmhelpers.core.hookenv import (
relation_get,
relation_ids,
related_units,
log,
INFO,
ERROR
)
from charmhelpers.fetch import (
apt_install,
)
from charmhelpers.core.host import (
mount,
mounts,
service_start,
service_stop,
umount,
)
KEYRING = '/etc/ceph/ceph.client.%s.keyring'
KEYFILE = '/etc/ceph/ceph.client.%s.key'
CEPH_CONF = """[global]
auth supported = %(auth)s
keyring = %(keyring)s
mon host = %(mon_hosts)s
"""
def running(service):
# this local util can be dropped as soon the following branch lands
# in lp:charm-helpers
# https://code.launchpad.net/~gandelman-a/charm-helpers/service_running/
try:
output = check_output(['service', service, 'status'])
except CalledProcessError:
return False
else:
if ("start/running" in output or "is running" in output):
return True
else:
return False
def install():
ceph_dir = "/etc/ceph"
if not os.path.isdir(ceph_dir):
os.mkdir(ceph_dir)
apt_install('ceph-common', fatal=True)
def rbd_exists(service, pool, rbd_img):
(rc, out) = commands.getstatusoutput('rbd list --id %s --pool %s' %
(service, pool))
return rbd_img in out
def create_rbd_image(service, pool, image, sizemb):
cmd = [
'rbd',
'create',
image,
'--size',
str(sizemb),
'--id',
service,
'--pool',
pool
]
check_call(cmd)
def pool_exists(service, name):
(rc, out) = commands.getstatusoutput("rados --id %s lspools" % service)
return name in out
def create_pool(service, name):
cmd = [
'rados',
'--id',
service,
'mkpool',
name
]
check_call(cmd)
def keyfile_path(service):
return KEYFILE % service
def keyring_path(service):
return KEYRING % service
def create_keyring(service, key):
keyring = keyring_path(service)
if os.path.exists(keyring):
log('ceph: Keyring exists at %s.' % keyring, level=INFO)
cmd = [
'ceph-authtool',
keyring,
'--create-keyring',
'--name=client.%s' % service,
'--add-key=%s' % key
]
check_call(cmd)
log('ceph: Created new ring at %s.' % keyring, level=INFO)
def create_key_file(service, key):
# create a file containing the key
keyfile = keyfile_path(service)
if os.path.exists(keyfile):
log('ceph: Keyfile exists at %s.' % keyfile, level=INFO)
fd = open(keyfile, 'w')
fd.write(key)
fd.close()
log('ceph: Created new keyfile at %s.' % keyfile, level=INFO)
def get_ceph_nodes():
hosts = []
for r_id in relation_ids('ceph'):
for unit in related_units(r_id):
hosts.append(relation_get('private-address', unit=unit, rid=r_id))
return hosts
def configure(service, key, auth):
create_keyring(service, key)
create_key_file(service, key)
hosts = get_ceph_nodes()
mon_hosts = ",".join(map(str, hosts))
keyring = keyring_path(service)
with open('/etc/ceph/ceph.conf', 'w') as ceph_conf:
ceph_conf.write(CEPH_CONF % locals())
modprobe_kernel_module('rbd')
def image_mapped(image_name):
(rc, out) = commands.getstatusoutput('rbd showmapped')
return image_name in out
def map_block_storage(service, pool, image):
cmd = [
'rbd',
'map',
'%s/%s' % (pool, image),
'--user',
service,
'--secret',
keyfile_path(service),
]
check_call(cmd)
def filesystem_mounted(fs):
return fs in [f for m, f in mounts()]
def make_filesystem(blk_device, fstype='ext4', timeout=10):
count = 0
e_noent = os.errno.ENOENT
while not os.path.exists(blk_device):
if count >= timeout:
log('ceph: gave up waiting on block device %s' % blk_device,
level=ERROR)
raise IOError(e_noent, os.strerror(e_noent), blk_device)
log('ceph: waiting for block device %s to appear' % blk_device,
level=INFO)
count += 1
time.sleep(1)
else:
log('ceph: Formatting block device %s as filesystem %s.' %
(blk_device, fstype), level=INFO)
check_call(['mkfs', '-t', fstype, blk_device])
def place_data_on_ceph(service, blk_device, data_src_dst, fstype='ext4'):
# mount block device into /mnt
mount(blk_device, '/mnt')
# copy data to /mnt
try:
copy_files(data_src_dst, '/mnt')
except:
pass
# umount block device
umount('/mnt')
_dir = os.stat(data_src_dst)
uid = _dir.st_uid
gid = _dir.st_gid
# re-mount where the data should originally be
mount(blk_device, data_src_dst, persist=True)
# ensure original ownership of new mount.
cmd = ['chown', '-R', '%s:%s' % (uid, gid), data_src_dst]
check_call(cmd)
# TODO: re-use
def modprobe_kernel_module(module):
log('ceph: Loading kernel module', level=INFO)
cmd = ['modprobe', module]
check_call(cmd)
cmd = 'echo %s >> /etc/modules' % module
check_call(cmd, shell=True)
def copy_files(src, dst, symlinks=False, ignore=None):
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
shutil.copytree(s, d, symlinks, ignore)
else:
shutil.copy2(s, d)
def ensure_ceph_storage(service, pool, rbd_img, sizemb, mount_point,
blk_device, fstype, system_services=[]):
"""
To be called from the current cluster leader.
Ensures given pool and RBD image exists, is mapped to a block device,
and the device is formatted and mounted at the given mount_point.
If formatting a device for the first time, data existing at mount_point
will be migrated to the RBD device before being remounted.
All services listed in system_services will be stopped prior to data
migration and restarted when complete.
"""
# Ensure pool, RBD image, RBD mappings are in place.
if not pool_exists(service, pool):
log('ceph: Creating new pool %s.' % pool, level=INFO)
create_pool(service, pool)
if not rbd_exists(service, pool, rbd_img):
log('ceph: Creating RBD image (%s).' % rbd_img, level=INFO)
create_rbd_image(service, pool, rbd_img, sizemb)
if not image_mapped(rbd_img):
log('ceph: Mapping RBD Image as a Block Device.', level=INFO)
map_block_storage(service, pool, rbd_img)
# make file system
# TODO: What happens if for whatever reason this is run again and
# the data is already in the rbd device and/or is mounted??
# When it is mounted already, it will fail to make the fs
# XXX: This is really sketchy! Need to at least add an fstab entry
# otherwise this hook will blow away existing data if its executed
# after a reboot.
if not filesystem_mounted(mount_point):
make_filesystem(blk_device, fstype)
for svc in system_services:
if running(svc):
log('Stopping services %s prior to migrating data.' % svc,
level=INFO)
service_stop(svc)
place_data_on_ceph(service, blk_device, mount_point, fstype)
for svc in system_services:
service_start(svc)

View File

@ -0,0 +1,183 @@
#
# Copyright 2012 Canonical Ltd.
#
# Authors:
# James Page <james.page@ubuntu.com>
# Adam Gandelman <adamg@ubuntu.com>
#
import subprocess
import os
from socket import gethostname as get_unit_hostname
from charmhelpers.core.hookenv import (
log,
relation_ids,
related_units as relation_list,
relation_get,
config as config_get,
INFO,
ERROR,
unit_get,
)
class HAIncompleteConfig(Exception):
pass
def is_clustered():
for r_id in (relation_ids('ha') or []):
for unit in (relation_list(r_id) or []):
clustered = relation_get('clustered',
rid=r_id,
unit=unit)
if clustered:
return True
return False
def is_leader(resource):
cmd = [
"crm", "resource",
"show", resource
]
try:
status = subprocess.check_output(cmd)
except subprocess.CalledProcessError:
return False
else:
if get_unit_hostname() in status:
return True
else:
return False
def peer_units():
peers = []
for r_id in (relation_ids('cluster') or []):
for unit in (relation_list(r_id) or []):
peers.append(unit)
return peers
def oldest_peer(peers):
local_unit_no = int(os.getenv('JUJU_UNIT_NAME').split('/')[1])
for peer in peers:
remote_unit_no = int(peer.split('/')[1])
if remote_unit_no < local_unit_no:
return False
return True
def eligible_leader(resource):
if is_clustered():
if not is_leader(resource):
log('Deferring action to CRM leader.', level=INFO)
return False
else:
peers = peer_units()
if peers and not oldest_peer(peers):
log('Deferring action to oldest service unit.', level=INFO)
return False
return True
def https():
'''
Determines whether enough data has been provided in configuration
or relation data to configure HTTPS
.
returns: boolean
'''
if config_get('use-https') == "yes":
return True
if config_get('ssl_cert') and config_get('ssl_key'):
return True
for r_id in relation_ids('identity-service'):
for unit in relation_list(r_id):
rel_state = [
relation_get('https_keystone', rid=r_id, unit=unit),
relation_get('ssl_cert', rid=r_id, unit=unit),
relation_get('ssl_key', rid=r_id, unit=unit),
relation_get('ca_cert', rid=r_id, unit=unit),
]
# NOTE: works around (LP: #1203241)
if (None not in rel_state) and ('' not in rel_state):
return True
return False
def determine_api_port(public_port):
'''
Determine correct API server listening port based on
existence of HTTPS reverse proxy and/or haproxy.
public_port: int: standard public port for given service
returns: int: the correct listening port for the API service
'''
i = 0
if len(peer_units()) > 0 or is_clustered():
i += 1
if https():
i += 1
return public_port - (i * 10)
def determine_haproxy_port(public_port):
'''
Description: Determine correct proxy listening port based on public IP +
existence of HTTPS reverse proxy.
public_port: int: standard public port for given service
returns: int: the correct listening port for the HAProxy service
'''
i = 0
if https():
i += 1
return public_port - (i * 10)
def get_hacluster_config():
'''
Obtains all relevant configuration from charm configuration required
for initiating a relation to hacluster:
ha-bindiface, ha-mcastport, vip, vip_iface, vip_cidr
returns: dict: A dict containing settings keyed by setting name.
raises: HAIncompleteConfig if settings are missing.
'''
settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'vip_iface', 'vip_cidr']
conf = {}
for setting in settings:
conf[setting] = config_get(setting)
missing = []
[missing.append(s) for s, v in conf.iteritems() if v is None]
if missing:
log('Insufficient config data to configure hacluster.', level=ERROR)
raise HAIncompleteConfig
return conf
def canonical_url(configs, vip_setting='vip'):
'''
Returns the correct HTTP URL to this host given the state of HTTPS
configuration and hacluster.
:configs : OSTemplateRenderer: A config tempating object to inspect for
a complete https context.
:vip_setting: str: Setting in charm config that specifies
VIP address.
'''
scheme = 'http'
if 'https' in configs.complete_contexts():
scheme = 'https'
if is_clustered():
addr = config_get(vip_setting)
else:
addr = unit_get('private-address')
return '%s://%s' % (scheme, addr)

View File

@ -0,0 +1,72 @@
''' Helpers for interacting with OpenvSwitch '''
import subprocess
import os
from charmhelpers.core.hookenv import (
log, WARNING
)
from charmhelpers.core.host import (
service
)
def add_bridge(name):
''' Add the named bridge to openvswitch '''
log('Creating bridge {}'.format(name))
subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-br", name])
def del_bridge(name):
''' Delete the named bridge from openvswitch '''
log('Deleting bridge {}'.format(name))
subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-br", name])
def add_bridge_port(name, port):
''' Add a port to the named openvswitch bridge '''
log('Adding port {} to bridge {}'.format(port, name))
subprocess.check_call(["ovs-vsctl", "--", "--may-exist", "add-port",
name, port])
subprocess.check_call(["ip", "link", "set", port, "up"])
def del_bridge_port(name, port):
''' Delete a port from the named openvswitch bridge '''
log('Deleting port {} from bridge {}'.format(port, name))
subprocess.check_call(["ovs-vsctl", "--", "--if-exists", "del-port",
name, port])
subprocess.check_call(["ip", "link", "set", port, "down"])
def set_manager(manager):
''' Set the controller for the local openvswitch '''
log('Setting manager for local ovs to {}'.format(manager))
subprocess.check_call(['ovs-vsctl', 'set-manager',
'ssl:{}'.format(manager)])
CERT_PATH = '/etc/openvswitch/ovsclient-cert.pem'
def get_certificate():
''' Read openvswitch certificate from disk '''
if os.path.exists(CERT_PATH):
log('Reading ovs certificate from {}'.format(CERT_PATH))
with open(CERT_PATH, 'r') as cert:
full_cert = cert.read()
begin_marker = "-----BEGIN CERTIFICATE-----"
end_marker = "-----END CERTIFICATE-----"
begin_index = full_cert.find(begin_marker)
end_index = full_cert.rfind(end_marker)
if end_index == -1 or begin_index == -1:
raise RuntimeError("Certificate does not contain valid begin"
" and end markers.")
full_cert = full_cert[begin_index:(end_index + len(end_marker))]
return full_cert
else:
log('Certificate not found', level=WARNING)
return None
def full_restart():
''' Full restart and reload of openvswitch '''
service('force-reload-kmod', 'openvswitch-switch')

View File

@ -0,0 +1,522 @@
import json
import os
from base64 import b64decode
from subprocess import (
check_call
)
from charmhelpers.fetch import (
apt_install,
filter_installed_packages,
)
from charmhelpers.core.hookenv import (
config,
local_unit,
log,
relation_get,
relation_ids,
related_units,
unit_get,
unit_private_ip,
ERROR,
WARNING,
)
from charmhelpers.contrib.hahelpers.cluster import (
determine_api_port,
determine_haproxy_port,
https,
is_clustered,
peer_units,
)
from charmhelpers.contrib.hahelpers.apache import (
get_cert,
get_ca_cert,
)
from charmhelpers.contrib.openstack.neutron import (
neutron_plugin_attribute,
)
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
class OSContextError(Exception):
pass
def ensure_packages(packages):
'''Install but do not upgrade required plugin packages'''
required = filter_installed_packages(packages)
if required:
apt_install(required, fatal=True)
def context_complete(ctxt):
_missing = []
for k, v in ctxt.iteritems():
if v is None or v == '':
_missing.append(k)
if _missing:
log('Missing required data: %s' % ' '.join(_missing), level='INFO')
return False
return True
class OSContextGenerator(object):
interfaces = []
def __call__(self):
raise NotImplementedError
class SharedDBContext(OSContextGenerator):
interfaces = ['shared-db']
def __init__(self, database=None, user=None, relation_prefix=None):
'''
Allows inspecting relation for settings prefixed with relation_prefix.
This is useful for parsing access for multiple databases returned via
the shared-db interface (eg, nova_password, quantum_password)
'''
self.relation_prefix = relation_prefix
self.database = database
self.user = user
def __call__(self):
self.database = self.database or config('database')
self.user = self.user or config('database-user')
if None in [self.database, self.user]:
log('Could not generate shared_db context. '
'Missing required charm config options. '
'(database name and user)')
raise OSContextError
ctxt = {}
password_setting = 'password'
if self.relation_prefix:
password_setting = self.relation_prefix + '_password'
for rid in relation_ids('shared-db'):
for unit in related_units(rid):
passwd = relation_get(password_setting, rid=rid, unit=unit)
ctxt = {
'database_host': relation_get('db_host', rid=rid,
unit=unit),
'database': self.database,
'database_user': self.user,
'database_password': passwd,
}
if context_complete(ctxt):
return ctxt
return {}
class IdentityServiceContext(OSContextGenerator):
interfaces = ['identity-service']
def __call__(self):
log('Generating template context for identity-service')
ctxt = {}
for rid in relation_ids('identity-service'):
for unit in related_units(rid):
ctxt = {
'service_port': relation_get('service_port', rid=rid,
unit=unit),
'service_host': relation_get('service_host', rid=rid,
unit=unit),
'auth_host': relation_get('auth_host', rid=rid, unit=unit),
'auth_port': relation_get('auth_port', rid=rid, unit=unit),
'admin_tenant_name': relation_get('service_tenant',
rid=rid, unit=unit),
'admin_user': relation_get('service_username', rid=rid,
unit=unit),
'admin_password': relation_get('service_password', rid=rid,
unit=unit),
# XXX: Hard-coded http.
'service_protocol': 'http',
'auth_protocol': 'http',
}
if context_complete(ctxt):
return ctxt
return {}
class AMQPContext(OSContextGenerator):
interfaces = ['amqp']
def __call__(self):
log('Generating template context for amqp')
conf = config()
try:
username = conf['rabbit-user']
vhost = conf['rabbit-vhost']
except KeyError as e:
log('Could not generate shared_db context. '
'Missing required charm config options: %s.' % e)
raise OSContextError
ctxt = {}
for rid in relation_ids('amqp'):
for unit in related_units(rid):
if relation_get('clustered', rid=rid, unit=unit):
ctxt['clustered'] = True
ctxt['rabbitmq_host'] = relation_get('vip', rid=rid,
unit=unit)
else:
ctxt['rabbitmq_host'] = relation_get('private-address',
rid=rid, unit=unit)
ctxt.update({
'rabbitmq_user': username,
'rabbitmq_password': relation_get('password', rid=rid,
unit=unit),
'rabbitmq_virtual_host': vhost,
})
if context_complete(ctxt):
# Sufficient information found = break out!
break
# Used for active/active rabbitmq >= grizzly
ctxt['rabbitmq_hosts'] = []
for unit in related_units(rid):
ctxt['rabbitmq_hosts'].append(relation_get('private-address',
rid=rid, unit=unit))
if not context_complete(ctxt):
return {}
else:
return ctxt
class CephContext(OSContextGenerator):
interfaces = ['ceph']
def __call__(self):
'''This generates context for /etc/ceph/ceph.conf templates'''
if not relation_ids('ceph'):
return {}
log('Generating template context for ceph')
mon_hosts = []
auth = None
key = None
for rid in relation_ids('ceph'):
for unit in related_units(rid):
mon_hosts.append(relation_get('private-address', rid=rid,
unit=unit))
auth = relation_get('auth', rid=rid, unit=unit)
key = relation_get('key', rid=rid, unit=unit)
ctxt = {
'mon_hosts': ' '.join(mon_hosts),
'auth': auth,
'key': key,
}
if not os.path.isdir('/etc/ceph'):
os.mkdir('/etc/ceph')
if not context_complete(ctxt):
return {}
ensure_packages(['ceph-common'])
return ctxt
class HAProxyContext(OSContextGenerator):
interfaces = ['cluster']
def __call__(self):
'''
Builds half a context for the haproxy template, which describes
all peers to be included in the cluster. Each charm needs to include
its own context generator that describes the port mapping.
'''
if not relation_ids('cluster'):
return {}
cluster_hosts = {}
l_unit = local_unit().replace('/', '-')
cluster_hosts[l_unit] = unit_get('private-address')
for rid in relation_ids('cluster'):
for unit in related_units(rid):
_unit = unit.replace('/', '-')
addr = relation_get('private-address', rid=rid, unit=unit)
cluster_hosts[_unit] = addr
ctxt = {
'units': cluster_hosts,
}
if len(cluster_hosts.keys()) > 1:
# Enable haproxy when we have enough peers.
log('Ensuring haproxy enabled in /etc/default/haproxy.')
with open('/etc/default/haproxy', 'w') as out:
out.write('ENABLED=1\n')
return ctxt
log('HAProxy context is incomplete, this unit has no peers.')
return {}
class ImageServiceContext(OSContextGenerator):
interfaces = ['image-service']
def __call__(self):
'''
Obtains the glance API server from the image-service relation. Useful
in nova and cinder (currently).
'''
log('Generating template context for image-service.')
rids = relation_ids('image-service')
if not rids:
return {}
for rid in rids:
for unit in related_units(rid):
api_server = relation_get('glance-api-server',
rid=rid, unit=unit)
if api_server:
return {'glance_api_servers': api_server}
log('ImageService context is incomplete. '
'Missing required relation data.')
return {}
class ApacheSSLContext(OSContextGenerator):
"""
Generates a context for an apache vhost configuration that configures
HTTPS reverse proxying for one or many endpoints. Generated context
looks something like:
{
'namespace': 'cinder',
'private_address': 'iscsi.mycinderhost.com',
'endpoints': [(8776, 8766), (8777, 8767)]
}
The endpoints list consists of a tuples mapping external ports
to internal ports.
"""
interfaces = ['https']
# charms should inherit this context and set external ports
# and service namespace accordingly.
external_ports = []
service_namespace = None
def enable_modules(self):
cmd = ['a2enmod', 'ssl', 'proxy', 'proxy_http']
check_call(cmd)
def configure_cert(self):
if not os.path.isdir('/etc/apache2/ssl'):
os.mkdir('/etc/apache2/ssl')
ssl_dir = os.path.join('/etc/apache2/ssl/', self.service_namespace)
if not os.path.isdir(ssl_dir):
os.mkdir(ssl_dir)
cert, key = get_cert()
with open(os.path.join(ssl_dir, 'cert'), 'w') as cert_out:
cert_out.write(b64decode(cert))
with open(os.path.join(ssl_dir, 'key'), 'w') as key_out:
key_out.write(b64decode(key))
ca_cert = get_ca_cert()
if ca_cert:
with open(CA_CERT_PATH, 'w') as ca_out:
ca_out.write(b64decode(ca_cert))
check_call(['update-ca-certificates'])
def __call__(self):
if isinstance(self.external_ports, basestring):
self.external_ports = [self.external_ports]
if (not self.external_ports or not https()):
return {}
self.configure_cert()
self.enable_modules()
ctxt = {
'namespace': self.service_namespace,
'private_address': unit_get('private-address'),
'endpoints': []
}
for ext_port in self.external_ports:
if peer_units() or is_clustered():
int_port = determine_haproxy_port(ext_port)
else:
int_port = determine_api_port(ext_port)
portmap = (int(ext_port), int(int_port))
ctxt['endpoints'].append(portmap)
return ctxt
class NeutronContext(object):
interfaces = []
@property
def plugin(self):
return None
@property
def network_manager(self):
return None
@property
def packages(self):
return neutron_plugin_attribute(
self.plugin, 'packages', self.network_manager)
@property
def neutron_security_groups(self):
return None
def _ensure_packages(self):
[ensure_packages(pkgs) for pkgs in self.packages]
def _save_flag_file(self):
if self.network_manager == 'quantum':
_file = '/etc/nova/quantum_plugin.conf'
else:
_file = '/etc/nova/neutron_plugin.conf'
with open(_file, 'wb') as out:
out.write(self.plugin + '\n')
def ovs_ctxt(self):
driver = neutron_plugin_attribute(self.plugin, 'driver',
self.network_manager)
ovs_ctxt = {
'core_plugin': driver,
'neutron_plugin': 'ovs',
'neutron_security_groups': self.neutron_security_groups,
'local_ip': unit_private_ip(),
}
return ovs_ctxt
def __call__(self):
self._ensure_packages()
if self.network_manager not in ['quantum', 'neutron']:
return {}
if not self.plugin:
return {}
ctxt = {'network_manager': self.network_manager}
if self.plugin == 'ovs':
ctxt.update(self.ovs_ctxt())
self._save_flag_file()
return ctxt
class OSConfigFlagContext(OSContextGenerator):
'''
Responsible adding user-defined config-flags in charm config to a
to a template context.
'''
def __call__(self):
config_flags = config('config-flags')
if not config_flags or config_flags in ['None', '']:
return {}
config_flags = config_flags.split(',')
flags = {}
for flag in config_flags:
if '=' not in flag:
log('Improperly formatted config-flag, expected k=v '
'got %s' % flag, level=WARNING)
continue
k, v = flag.split('=')
flags[k.strip()] = v
ctxt = {'user_config_flags': flags}
return ctxt
class SubordinateConfigContext(OSContextGenerator):
"""
Responsible for inspecting relations to subordinates that
may be exporting required config via a json blob.
The subordinate interface allows subordinates to export their
configuration requirements to the principle for multiple config
files and multiple serivces. Ie, a subordinate that has interfaces
to both glance and nova may export to following yaml blob as json:
glance:
/etc/glance/glance-api.conf:
sections:
DEFAULT:
- [key1, value1]
/etc/glance/glance-registry.conf:
MYSECTION:
- [key2, value2]
nova:
/etc/nova/nova.conf:
sections:
DEFAULT:
- [key3, value3]
It is then up to the principle charms to subscribe this context to
the service+config file it is interestd in. Configuration data will
be available in the template context, in glance's case, as:
ctxt = {
... other context ...
'subordinate_config': {
'DEFAULT': {
'key1': 'value1',
},
'MYSECTION': {
'key2': 'value2',
},
}
}
"""
def __init__(self, service, config_file, interface):
"""
:param service : Service name key to query in any subordinate
data found
:param config_file : Service's config file to query sections
:param interface : Subordinate interface to inspect
"""
self.service = service
self.config_file = config_file
self.interface = interface
def __call__(self):
ctxt = {}
for rid in relation_ids(self.interface):
for unit in related_units(rid):
sub_config = relation_get('subordinate_configuration',
rid=rid, unit=unit)
if sub_config and sub_config != '':
try:
sub_config = json.loads(sub_config)
except:
log('Could not parse JSON from subordinate_config '
'setting from %s' % rid, level=ERROR)
continue
if self.service not in sub_config:
log('Found subordinate_config on %s but it contained'
'nothing for %s service' % (rid, self.service))
continue
sub_config = sub_config[self.service]
if self.config_file not in sub_config:
log('Found subordinate_config on %s but it contained'
'nothing for %s' % (rid, self.config_file))
continue
sub_config = sub_config[self.config_file]
for k, v in sub_config.iteritems():
ctxt[k] = v
if not ctxt:
ctxt['sections'] = {}
return ctxt

View File

@ -0,0 +1,117 @@
# Various utilies for dealing with Neutron and the renaming from Quantum.
from subprocess import check_output
from charmhelpers.core.hookenv import (
config,
log,
ERROR,
)
from charmhelpers.contrib.openstack.utils import os_release
def headers_package():
"""Ensures correct linux-headers for running kernel are installed,
for building DKMS package"""
kver = check_output(['uname', '-r']).strip()
return 'linux-headers-%s' % kver
# legacy
def quantum_plugins():
from charmhelpers.contrib.openstack import context
return {
'ovs': {
'config': '/etc/quantum/plugins/openvswitch/'
'ovs_quantum_plugin.ini',
'driver': 'quantum.plugins.openvswitch.ovs_quantum_plugin.'
'OVSQuantumPluginV2',
'contexts': [
context.SharedDBContext(user=config('neutron-database-user'),
database=config('neutron-database'),
relation_prefix='neutron')],
'services': ['quantum-plugin-openvswitch-agent'],
'packages': [[headers_package(), 'openvswitch-datapath-dkms'],
['quantum-plugin-openvswitch-agent']],
},
'nvp': {
'config': '/etc/quantum/plugins/nicira/nvp.ini',
'driver': 'quantum.plugins.nicira.nicira_nvp_plugin.'
'QuantumPlugin.NvpPluginV2',
'services': [],
'packages': [],
}
}
def neutron_plugins():
from charmhelpers.contrib.openstack import context
return {
'ovs': {
'config': '/etc/neutron/plugins/openvswitch/'
'ovs_neutron_plugin.ini',
'driver': 'neutron.plugins.openvswitch.ovs_neutron_plugin.'
'OVSNeutronPluginV2',
'contexts': [
context.SharedDBContext(user=config('neutron-database-user'),
database=config('neutron-database'),
relation_prefix='neutron')],
'services': ['neutron-plugin-openvswitch-agent'],
'packages': [[headers_package(), 'openvswitch-datapath-dkms'],
['quantum-plugin-openvswitch-agent']],
},
'nvp': {
'config': '/etc/neutron/plugins/nicira/nvp.ini',
'driver': 'neutron.plugins.nicira.nicira_nvp_plugin.'
'NeutronPlugin.NvpPluginV2',
'services': [],
'packages': [],
}
}
def neutron_plugin_attribute(plugin, attr, net_manager=None):
manager = net_manager or network_manager()
if manager == 'quantum':
plugins = quantum_plugins()
elif manager == 'neutron':
plugins = neutron_plugins()
else:
log('Error: Network manager does not support plugins.')
raise Exception
try:
_plugin = plugins[plugin]
except KeyError:
log('Unrecognised plugin for %s: %s' % (manager, plugin), level=ERROR)
raise Exception
try:
return _plugin[attr]
except KeyError:
return None
def network_manager():
'''
Deals with the renaming of Quantum to Neutron in H and any situations
that require compatability (eg, deploying H with network-manager=quantum,
upgrading from G).
'''
release = os_release('nova-common')
manager = config('network-manager').lower()
if manager not in ['quantum', 'neutron']:
return manager
if release in ['essex']:
# E does not support neutron
log('Neutron networking not supported in Essex.', level=ERROR)
raise Exception
elif release in ['folsom', 'grizzly']:
# neutron is named quantum in F and G
return 'quantum'
else:
# ensure accurate naming for all releases post-H
return 'neutron'

View File

@ -0,0 +1,2 @@
# dummy __init__.py to fool syncer into thinking this is a syncable python
# module

View File

@ -0,0 +1,11 @@
###############################################################################
# [ WARNING ]
# cinder configuration file maintained by Juju
# local changes may be overwritten.
###############################################################################
{% if auth -%}
[global]
auth_supported = {{ auth }}
keyring = /etc/ceph/$cluster.$name.keyring
mon host = {{ mon_hosts }}
{% endif -%}

View File

@ -0,0 +1,37 @@
global
log 127.0.0.1 local0
log 127.0.0.1 local1 notice
maxconn 20000
user haproxy
group haproxy
spread-checks 0
defaults
log global
mode http
option httplog
option dontlognull
retries 3
timeout queue 1000
timeout connect 1000
timeout client 30000
timeout server 30000
listen stats :8888
mode http
stats enable
stats hide-version
stats realm Haproxy\ Statistics
stats uri /
stats auth admin:password
{% if units -%}
{% for service, ports in service_ports.iteritems() -%}
listen {{ service }} 0.0.0.0:{{ ports[0] }}
balance roundrobin
option tcplog
{% for unit, address in units.iteritems() -%}
server {{ unit }} {{ address }}:{{ ports[1] }} check
{% endfor %}
{% endfor -%}
{% endif -%}

View File

@ -0,0 +1,23 @@
{% if endpoints -%}
{% for ext, int in endpoints -%}
Listen {{ ext }}
NameVirtualHost *:{{ ext }}
<VirtualHost *:{{ ext }}>
ServerName {{ private_address }}
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert
SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key
ProxyPass / http://localhost:{{ int }}/
ProxyPassReverse / http://localhost:{{ int }}/
ProxyPreserveHost on
</VirtualHost>
<Proxy *>
Order deny,allow
Allow from all
</Proxy>
<Location />
Order allow,deny
Allow from all
</Location>
{% endfor -%}
{% endif -%}

View File

@ -0,0 +1,23 @@
{% if endpoints -%}
{% for ext, int in endpoints -%}
Listen {{ ext }}
NameVirtualHost *:{{ ext }}
<VirtualHost *:{{ ext }}>
ServerName {{ private_address }}
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/{{ namespace }}/cert
SSLCertificateKeyFile /etc/apache2/ssl/{{ namespace }}/key
ProxyPass / http://localhost:{{ int }}/
ProxyPassReverse / http://localhost:{{ int }}/
ProxyPreserveHost on
</VirtualHost>
<Proxy *>
Order deny,allow
Allow from all
</Proxy>
<Location />
Order allow,deny
Allow from all
</Location>
{% endfor -%}
{% endif -%}

View File

@ -0,0 +1,280 @@
import os
from charmhelpers.fetch import apt_install
from charmhelpers.core.hookenv import (
log,
ERROR,
INFO
)
from charmhelpers.contrib.openstack.utils import OPENSTACK_CODENAMES
try:
from jinja2 import FileSystemLoader, ChoiceLoader, Environment, exceptions
except ImportError:
# python-jinja2 may not be installed yet, or we're running unittests.
FileSystemLoader = ChoiceLoader = Environment = exceptions = None
class OSConfigException(Exception):
pass
def get_loader(templates_dir, os_release):
"""
Create a jinja2.ChoiceLoader containing template dirs up to
and including os_release. If directory template directory
is missing at templates_dir, it will be omitted from the loader.
templates_dir is added to the bottom of the search list as a base
loading dir.
A charm may also ship a templates dir with this module
and it will be appended to the bottom of the search list, eg:
hooks/charmhelpers/contrib/openstack/templates.
:param templates_dir: str: Base template directory containing release
sub-directories.
:param os_release : str: OpenStack release codename to construct template
loader.
:returns : jinja2.ChoiceLoader constructed with a list of
jinja2.FilesystemLoaders, ordered in descending
order by OpenStack release.
"""
tmpl_dirs = [(rel, os.path.join(templates_dir, rel))
for rel in OPENSTACK_CODENAMES.itervalues()]
if not os.path.isdir(templates_dir):
log('Templates directory not found @ %s.' % templates_dir,
level=ERROR)
raise OSConfigException
# the bottom contains tempaltes_dir and possibly a common templates dir
# shipped with the helper.
loaders = [FileSystemLoader(templates_dir)]
helper_templates = os.path.join(os.path.dirname(__file__), 'templates')
if os.path.isdir(helper_templates):
loaders.append(FileSystemLoader(helper_templates))
for rel, tmpl_dir in tmpl_dirs:
if os.path.isdir(tmpl_dir):
loaders.insert(0, FileSystemLoader(tmpl_dir))
if rel == os_release:
break
log('Creating choice loader with dirs: %s' %
[l.searchpath for l in loaders], level=INFO)
return ChoiceLoader(loaders)
class OSConfigTemplate(object):
"""
Associates a config file template with a list of context generators.
Responsible for constructing a template context based on those generators.
"""
def __init__(self, config_file, contexts):
self.config_file = config_file
if hasattr(contexts, '__call__'):
self.contexts = [contexts]
else:
self.contexts = contexts
self._complete_contexts = []
def context(self):
ctxt = {}
for context in self.contexts:
_ctxt = context()
if _ctxt:
ctxt.update(_ctxt)
# track interfaces for every complete context.
[self._complete_contexts.append(interface)
for interface in context.interfaces
if interface not in self._complete_contexts]
return ctxt
def complete_contexts(self):
'''
Return a list of interfaces that have atisfied contexts.
'''
if self._complete_contexts:
return self._complete_contexts
self.context()
return self._complete_contexts
class OSConfigRenderer(object):
"""
This class provides a common templating system to be used by OpenStack
charms. It is intended to help charms share common code and templates,
and ease the burden of managing config templates across multiple OpenStack
releases.
Basic usage:
# import some common context generates from charmhelpers
from charmhelpers.contrib.openstack import context
# Create a renderer object for a specific OS release.
configs = OSConfigRenderer(templates_dir='/tmp/templates',
openstack_release='folsom')
# register some config files with context generators.
configs.register(config_file='/etc/nova/nova.conf',
contexts=[context.SharedDBContext(),
context.AMQPContext()])
configs.register(config_file='/etc/nova/api-paste.ini',
contexts=[context.IdentityServiceContext()])
configs.register(config_file='/etc/haproxy/haproxy.conf',
contexts=[context.HAProxyContext()])
# write out a single config
configs.write('/etc/nova/nova.conf')
# write out all registered configs
configs.write_all()
Details:
OpenStack Releases and template loading
---------------------------------------
When the object is instantiated, it is associated with a specific OS
release. This dictates how the template loader will be constructed.
The constructed loader attempts to load the template from several places
in the following order:
- from the most recent OS release-specific template dir (if one exists)
- the base templates_dir
- a template directory shipped in the charm with this helper file.
For the example above, '/tmp/templates' contains the following structure:
/tmp/templates/nova.conf
/tmp/templates/api-paste.ini
/tmp/templates/grizzly/api-paste.ini
/tmp/templates/havana/api-paste.ini
Since it was registered with the grizzly release, it first seraches
the grizzly directory for nova.conf, then the templates dir.
When writing api-paste.ini, it will find the template in the grizzly
directory.
If the object were created with folsom, it would fall back to the
base templates dir for its api-paste.ini template.
This system should help manage changes in config files through
openstack releases, allowing charms to fall back to the most recently
updated config template for a given release
The haproxy.conf, since it is not shipped in the templates dir, will
be loaded from the module directory's template directory, eg
$CHARM/hooks/charmhelpers/contrib/openstack/templates. This allows
us to ship common templates (haproxy, apache) with the helpers.
Context generators
---------------------------------------
Context generators are used to generate template contexts during hook
execution. Doing so may require inspecting service relations, charm
config, etc. When registered, a config file is associated with a list
of generators. When a template is rendered and written, all context
generates are called in a chain to generate the context dictionary
passed to the jinja2 template. See context.py for more info.
"""
def __init__(self, templates_dir, openstack_release):
if not os.path.isdir(templates_dir):
log('Could not locate templates dir %s' % templates_dir,
level=ERROR)
raise OSConfigException
self.templates_dir = templates_dir
self.openstack_release = openstack_release
self.templates = {}
self._tmpl_env = None
if None in [Environment, ChoiceLoader, FileSystemLoader]:
# if this code is running, the object is created pre-install hook.
# jinja2 shouldn't get touched until the module is reloaded on next
# hook execution, with proper jinja2 bits successfully imported.
apt_install('python-jinja2')
def register(self, config_file, contexts):
"""
Register a config file with a list of context generators to be called
during rendering.
"""
self.templates[config_file] = OSConfigTemplate(config_file=config_file,
contexts=contexts)
log('Registered config file: %s' % config_file, level=INFO)
def _get_tmpl_env(self):
if not self._tmpl_env:
loader = get_loader(self.templates_dir, self.openstack_release)
self._tmpl_env = Environment(loader=loader)
def _get_template(self, template):
self._get_tmpl_env()
template = self._tmpl_env.get_template(template)
log('Loaded template from %s' % template.filename, level=INFO)
return template
def render(self, config_file):
if config_file not in self.templates:
log('Config not registered: %s' % config_file, level=ERROR)
raise OSConfigException
ctxt = self.templates[config_file].context()
_tmpl = os.path.basename(config_file)
try:
template = self._get_template(_tmpl)
except exceptions.TemplateNotFound:
# if no template is found with basename, try looking for it
# using a munged full path, eg:
# /etc/apache2/apache2.conf -> etc_apache2_apache2.conf
_tmpl = '_'.join(config_file.split('/')[1:])
try:
template = self._get_template(_tmpl)
except exceptions.TemplateNotFound as e:
log('Could not load template from %s by %s or %s.' %
(self.templates_dir, os.path.basename(config_file), _tmpl),
level=ERROR)
raise e
log('Rendering from template: %s' % _tmpl, level=INFO)
return template.render(ctxt)
def write(self, config_file):
"""
Write a single config file, raises if config file is not registered.
"""
if config_file not in self.templates:
log('Config not registered: %s' % config_file, level=ERROR)
raise OSConfigException
_out = self.render(config_file)
with open(config_file, 'wb') as out:
out.write(_out)
log('Wrote template %s.' % config_file, level=INFO)
def write_all(self):
"""
Write out all registered config files.
"""
[self.write(k) for k in self.templates.iterkeys()]
def set_release(self, openstack_release):
"""
Resets the template environment and generates a new template loader
based on a the new openstack release.
"""
self._tmpl_env = None
self.openstack_release = openstack_release
self._get_tmpl_env()
def complete_contexts(self):
'''
Returns a list of context interfaces that yield a complete context.
'''
interfaces = []
[interfaces.extend(i.complete_contexts())
for i in self.templates.itervalues()]
return interfaces

View File

@ -0,0 +1,365 @@
#!/usr/bin/python
# Common python helper functions used for OpenStack charms.
from collections import OrderedDict
import apt_pkg as apt
import subprocess
import os
import socket
import sys
from charmhelpers.core.hookenv import (
config,
log as juju_log,
charm_dir,
)
from charmhelpers.core.host import (
lsb_release,
)
from charmhelpers.fetch import (
apt_install,
)
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
('oneiric', 'diablo'),
('precise', 'essex'),
('quantal', 'folsom'),
('raring', 'grizzly'),
('saucy', 'havana'),
])
OPENSTACK_CODENAMES = OrderedDict([
('2011.2', 'diablo'),
('2012.1', 'essex'),
('2012.2', 'folsom'),
('2013.1', 'grizzly'),
('2013.2', 'havana'),
('2014.1', 'icehouse'),
])
# 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'),
])
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 == 'distro':
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.'''
apt.init()
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 keyserver.ubuntu.com " \
"--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[: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',
}
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.
"""
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 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'
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):
"""
Resolves hostname for given IP, or returns the input
if it is already a hostname.
"""
if not is_ip(address):
return 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
# strip trailing .
if result.endswith('.'):
return result[:-1]
return result

View File

@ -0,0 +1,359 @@
#
# Copyright 2012 Canonical Ltd.
#
# This file is sourced from lp:openstack-charm-helpers
#
# Authors:
# James Page <james.page@ubuntu.com>
# Adam Gandelman <adamg@ubuntu.com>
#
import os
import shutil
import json
import time
from subprocess import (
check_call,
check_output,
CalledProcessError
)
from charmhelpers.core.hookenv import (
relation_get,
relation_ids,
related_units,
log,
INFO,
WARNING,
ERROR
)
from charmhelpers.core.host import (
mount,
mounts,
service_start,
service_stop,
service_running,
umount,
)
from charmhelpers.fetch import (
apt_install,
)
KEYRING = '/etc/ceph/ceph.client.{}.keyring'
KEYFILE = '/etc/ceph/ceph.client.{}.key'
CEPH_CONF = """[global]
auth supported = {auth}
keyring = {keyring}
mon host = {mon_hosts}
"""
def install():
''' Basic Ceph client installation '''
ceph_dir = "/etc/ceph"
if not os.path.exists(ceph_dir):
os.mkdir(ceph_dir)
apt_install('ceph-common', fatal=True)
def rbd_exists(service, pool, rbd_img):
''' Check to see if a RADOS block device exists '''
try:
out = check_output(['rbd', 'list', '--id', service,
'--pool', pool])
except CalledProcessError:
return False
else:
return rbd_img in out
def create_rbd_image(service, pool, image, sizemb):
''' Create a new RADOS block device '''
cmd = [
'rbd',
'create',
image,
'--size',
str(sizemb),
'--id',
service,
'--pool',
pool
]
check_call(cmd)
def pool_exists(service, name):
''' Check to see if a RADOS pool already exists '''
try:
out = check_output(['rados', '--id', service, 'lspools'])
except CalledProcessError:
return False
else:
return name in out
def get_osds(service):
'''
Return a list of all Ceph Object Storage Daemons
currently in the cluster
'''
return json.loads(check_output(['ceph', '--id', service,
'osd', 'ls', '--format=json']))
def create_pool(service, name, replicas=2):
''' Create a new RADOS pool '''
if pool_exists(service, name):
log("Ceph pool {} already exists, skipping creation".format(name),
level=WARNING)
return
# Calculate the number of placement groups based
# on upstream recommended best practices.
pgnum = (len(get_osds(service)) * 100 / replicas)
cmd = [
'ceph', '--id', service,
'osd', 'pool', 'create',
name, str(pgnum)
]
check_call(cmd)
cmd = [
'ceph', '--id', service,
'osd', 'pool', 'set', name,
'size', str(replicas)
]
check_call(cmd)
def delete_pool(service, name):
''' Delete a RADOS pool from ceph '''
cmd = [
'ceph', '--id', service,
'osd', 'pool', 'delete',
name, '--yes-i-really-really-mean-it'
]
check_call(cmd)
def _keyfile_path(service):
return KEYFILE.format(service)
def _keyring_path(service):
return KEYRING.format(service)
def create_keyring(service, key):
''' Create a new Ceph keyring containing key'''
keyring = _keyring_path(service)
if os.path.exists(keyring):
log('ceph: Keyring exists at %s.' % keyring, level=WARNING)
return
cmd = [
'ceph-authtool',
keyring,
'--create-keyring',
'--name=client.{}'.format(service),
'--add-key={}'.format(key)
]
check_call(cmd)
log('ceph: Created new ring at %s.' % keyring, level=INFO)
def create_key_file(service, key):
''' Create a file containing key '''
keyfile = _keyfile_path(service)
if os.path.exists(keyfile):
log('ceph: Keyfile exists at %s.' % keyfile, level=WARNING)
return
with open(keyfile, 'w') as fd:
fd.write(key)
log('ceph: Created new keyfile at %s.' % keyfile, level=INFO)
def get_ceph_nodes():
''' Query named relation 'ceph' to detemine current nodes '''
hosts = []
for r_id in relation_ids('ceph'):
for unit in related_units(r_id):
hosts.append(relation_get('private-address', unit=unit, rid=r_id))
return hosts
def configure(service, key, auth):
''' Perform basic configuration of Ceph '''
create_keyring(service, key)
create_key_file(service, key)
hosts = get_ceph_nodes()
with open('/etc/ceph/ceph.conf', 'w') as ceph_conf:
ceph_conf.write(CEPH_CONF.format(auth=auth,
keyring=_keyring_path(service),
mon_hosts=",".join(map(str, hosts))))
modprobe('rbd')
def image_mapped(name):
''' Determine whether a RADOS block device is mapped locally '''
try:
out = check_output(['rbd', 'showmapped'])
except CalledProcessError:
return False
else:
return name in out
def map_block_storage(service, pool, image):
''' Map a RADOS block device for local use '''
cmd = [
'rbd',
'map',
'{}/{}'.format(pool, image),
'--user',
service,
'--secret',
_keyfile_path(service),
]
check_call(cmd)
def filesystem_mounted(fs):
''' Determine whether a filesytems is already mounted '''
return fs in [f for f, m in mounts()]
def make_filesystem(blk_device, fstype='ext4', timeout=10):
''' Make a new filesystem on the specified block device '''
count = 0
e_noent = os.errno.ENOENT
while not os.path.exists(blk_device):
if count >= timeout:
log('ceph: gave up waiting on block device %s' % blk_device,
level=ERROR)
raise IOError(e_noent, os.strerror(e_noent), blk_device)
log('ceph: waiting for block device %s to appear' % blk_device,
level=INFO)
count += 1
time.sleep(1)
else:
log('ceph: Formatting block device %s as filesystem %s.' %
(blk_device, fstype), level=INFO)
check_call(['mkfs', '-t', fstype, blk_device])
def place_data_on_block_device(blk_device, data_src_dst):
''' Migrate data in data_src_dst to blk_device and then remount '''
# mount block device into /mnt
mount(blk_device, '/mnt')
# copy data to /mnt
copy_files(data_src_dst, '/mnt')
# umount block device
umount('/mnt')
# Grab user/group ID's from original source
_dir = os.stat(data_src_dst)
uid = _dir.st_uid
gid = _dir.st_gid
# re-mount where the data should originally be
# TODO: persist is currently a NO-OP in core.host
mount(blk_device, data_src_dst, persist=True)
# ensure original ownership of new mount.
os.chown(data_src_dst, uid, gid)
# TODO: re-use
def modprobe(module):
''' Load a kernel module and configure for auto-load on reboot '''
log('ceph: Loading kernel module', level=INFO)
cmd = ['modprobe', module]
check_call(cmd)
with open('/etc/modules', 'r+') as modules:
if module not in modules.read():
modules.write(module)
def copy_files(src, dst, symlinks=False, ignore=None):
''' Copy files from src to dst '''
for item in os.listdir(src):
s = os.path.join(src, item)
d = os.path.join(dst, item)
if os.path.isdir(s):
shutil.copytree(s, d, symlinks, ignore)
else:
shutil.copy2(s, d)
def ensure_ceph_storage(service, pool, rbd_img, sizemb, mount_point,
blk_device, fstype, system_services=[]):
"""
NOTE: This function must only be called from a single service unit for
the same rbd_img otherwise data loss will occur.
Ensures given pool and RBD image exists, is mapped to a block device,
and the device is formatted and mounted at the given mount_point.
If formatting a device for the first time, data existing at mount_point
will be migrated to the RBD device before being re-mounted.
All services listed in system_services will be stopped prior to data
migration and restarted when complete.
"""
# Ensure pool, RBD image, RBD mappings are in place.
if not pool_exists(service, pool):
log('ceph: Creating new pool {}.'.format(pool))
create_pool(service, pool)
if not rbd_exists(service, pool, rbd_img):
log('ceph: Creating RBD image ({}).'.format(rbd_img))
create_rbd_image(service, pool, rbd_img, sizemb)
if not image_mapped(rbd_img):
log('ceph: Mapping RBD Image {} as a Block Device.'.format(rbd_img))
map_block_storage(service, pool, rbd_img)
# make file system
# TODO: What happens if for whatever reason this is run again and
# the data is already in the rbd device and/or is mounted??
# When it is mounted already, it will fail to make the fs
# XXX: This is really sketchy! Need to at least add an fstab entry
# otherwise this hook will blow away existing data if its executed
# after a reboot.
if not filesystem_mounted(mount_point):
make_filesystem(blk_device, fstype)
for svc in system_services:
if service_running(svc):
log('ceph: Stopping services {} prior to migrating data.'
.format(svc))
service_stop(svc)
place_data_on_block_device(blk_device, mount_point)
for svc in system_services:
log('ceph: Starting service {} after migrating data.'
.format(svc))
service_start(svc)
def ensure_ceph_keyring(service, user=None, group=None):
'''
Ensures a ceph keyring is created for a named service
and optionally ensures user and group ownership.
Returns False if no ceph key is available in relation state.
'''
key = None
for rid in relation_ids('ceph'):
for unit in related_units(rid):
key = relation_get('key', rid=rid, unit=unit)
if key:
break
if not key:
return False
create_keyring(service=service, key=key)
keyring = _keyring_path(service)
if user and group:
check_call(['chown', '%s.%s' % (user, group), keyring])
return True

View File

@ -0,0 +1,62 @@
import os
import re
from subprocess import (
check_call,
check_output,
)
##################################################
# loopback device helpers.
##################################################
def loopback_devices():
'''
Parse through 'losetup -a' output to determine currently mapped
loopback devices. Output is expected to look like:
/dev/loop0: [0807]:961814 (/tmp/my.img)
:returns: dict: a dict mapping {loopback_dev: backing_file}
'''
loopbacks = {}
cmd = ['losetup', '-a']
devs = [d.strip().split(' ') for d in
check_output(cmd).splitlines() if d != '']
for dev, _, f in devs:
loopbacks[dev.replace(':', '')] = re.search('\((\S+)\)', f).groups()[0]
return loopbacks
def create_loopback(file_path):
'''
Create a loopback device for a given backing file.
:returns: str: Full path to new loopback device (eg, /dev/loop0)
'''
file_path = os.path.abspath(file_path)
check_call(['losetup', '--find', file_path])
for d, f in loopback_devices().iteritems():
if f == file_path:
return d
def ensure_loopback_device(path, size):
'''
Ensure a loopback device exists for a given backing file path and size.
If it a loopback device is not mapped to file, a new one will be created.
TODO: Confirm size of found loopback device.
:returns: str: Full path to the ensured loopback device (eg, /dev/loop0)
'''
for d, f in loopback_devices().iteritems():
if f == path:
return d
if not os.path.exists(path):
cmd = ['truncate', '--size', size, path]
check_call(cmd)
return create_loopback(path)

View File

@ -0,0 +1,88 @@
from subprocess import (
CalledProcessError,
check_call,
check_output,
Popen,
PIPE,
)
##################################################
# LVM helpers.
##################################################
def deactivate_lvm_volume_group(block_device):
'''
Deactivate any volume gruop associated with an LVM physical volume.
:param block_device: str: Full path to LVM physical volume
'''
vg = list_lvm_volume_group(block_device)
if vg:
cmd = ['vgchange', '-an', vg]
check_call(cmd)
def is_lvm_physical_volume(block_device):
'''
Determine whether a block device is initialized as an LVM PV.
:param block_device: str: Full path of block device to inspect.
:returns: boolean: True if block device is a PV, False if not.
'''
try:
check_output(['pvdisplay', block_device])
return True
except CalledProcessError:
return False
def remove_lvm_physical_volume(block_device):
'''
Remove LVM PV signatures from a given block device.
:param block_device: str: Full path of block device to scrub.
'''
p = Popen(['pvremove', '-ff', block_device],
stdin=PIPE)
p.communicate(input='y\n')
def list_lvm_volume_group(block_device):
'''
List LVM volume group associated with a given block device.
Assumes block device is a valid LVM PV.
:param block_device: str: Full path of block device to inspect.
:returns: str: Name of volume group associated with block device or None
'''
vg = None
pvd = check_output(['pvdisplay', block_device]).splitlines()
for l in pvd:
if l.strip().startswith('VG Name'):
vg = ' '.join(l.split()).split(' ').pop()
return vg
def create_lvm_physical_volume(block_device):
'''
Initialize a block device as an LVM physical volume.
:param block_device: str: Full path of block device to initialize.
'''
check_call(['pvcreate', block_device])
def create_lvm_volume_group(volume_group, block_device):
'''
Create an LVM volume group backed by a given block device.
Assumes block device has already been initialized as an LVM PV.
:param volume_group: str: Name of volume group to create.
:block_device: str: Full path of PV-initialized block device.
'''
check_call(['vgcreate', volume_group, block_device])

View File

@ -0,0 +1,25 @@
from os import stat
from stat import S_ISBLK
from subprocess import (
check_call
)
def is_block_device(path):
'''
Confirm device at path is a valid block device node.
:returns: boolean: True if path is a block device, False if not.
'''
return S_ISBLK(stat(path).st_mode)
def zap_disk(block_device):
'''
Clear a block device of partition table. Relies on sgdisk, which is
installed as pat of the 'gdisk' package in Ubuntu.
:param block_device: str: Full path of block device to clean.
'''
check_call(['sgdisk', '--zap-all', block_device])

View File

View File

@ -0,0 +1,340 @@
"Interactions with the Juju environment"
# Copyright 2013 Canonical Ltd.
#
# Authors:
# Charm Helpers Developers <juju@lists.ubuntu.com>
import os
import json
import yaml
import subprocess
import UserDict
CRITICAL = "CRITICAL"
ERROR = "ERROR"
WARNING = "WARNING"
INFO = "INFO"
DEBUG = "DEBUG"
MARKER = object()
cache = {}
def cached(func):
''' Cache return values for multiple executions of func + args
For example:
@cached
def unit_get(attribute):
pass
unit_get('test')
will cache the result of unit_get + 'test' for future calls.
'''
def wrapper(*args, **kwargs):
global cache
key = str((func, args, kwargs))
try:
return cache[key]
except KeyError:
res = func(*args, **kwargs)
cache[key] = res
return res
return wrapper
def flush(key):
''' Flushes any entries from function cache where the
key is found in the function+args '''
flush_list = []
for item in cache:
if key in item:
flush_list.append(item)
for item in flush_list:
del cache[item]
def log(message, level=None):
"Write a message to the juju log"
command = ['juju-log']
if level:
command += ['-l', level]
command += [message]
subprocess.call(command)
class Serializable(UserDict.IterableUserDict):
"Wrapper, an object that can be serialized to yaml or json"
def __init__(self, obj):
# wrap the object
UserDict.IterableUserDict.__init__(self)
self.data = obj
def __getattr__(self, attr):
# See if this object has attribute.
if attr in ("json", "yaml", "data"):
return self.__dict__[attr]
# Check for attribute in wrapped object.
got = getattr(self.data, attr, MARKER)
if got is not MARKER:
return got
# Proxy to the wrapped object via dict interface.
try:
return self.data[attr]
except KeyError:
raise AttributeError(attr)
def __getstate__(self):
# Pickle as a standard dictionary.
return self.data
def __setstate__(self, state):
# Unpickle into our wrapper.
self.data = state
def json(self):
"Serialize the object to json"
return json.dumps(self.data)
def yaml(self):
"Serialize the object to yaml"
return yaml.dump(self.data)
def execution_environment():
"""A convenient bundling of the current execution context"""
context = {}
context['conf'] = config()
if relation_id():
context['reltype'] = relation_type()
context['relid'] = relation_id()
context['rel'] = relation_get()
context['unit'] = local_unit()
context['rels'] = relations()
context['env'] = os.environ
return context
def in_relation_hook():
"Determine whether we're running in a relation hook"
return 'JUJU_RELATION' in os.environ
def relation_type():
"The scope for the current relation hook"
return os.environ.get('JUJU_RELATION', None)
def relation_id():
"The relation ID for the current relation hook"
return os.environ.get('JUJU_RELATION_ID', None)
def local_unit():
"Local unit ID"
return os.environ['JUJU_UNIT_NAME']
def remote_unit():
"The remote unit for the current relation hook"
return os.environ['JUJU_REMOTE_UNIT']
def service_name():
"The name service group this unit belongs to"
return local_unit().split('/')[0]
@cached
def config(scope=None):
"Juju charm configuration"
config_cmd_line = ['config-get']
if scope is not None:
config_cmd_line.append(scope)
config_cmd_line.append('--format=json')
try:
return json.loads(subprocess.check_output(config_cmd_line))
except ValueError:
return None
@cached
def relation_get(attribute=None, unit=None, rid=None):
_args = ['relation-get', '--format=json']
if rid:
_args.append('-r')
_args.append(rid)
_args.append(attribute or '-')
if unit:
_args.append(unit)
try:
return json.loads(subprocess.check_output(_args))
except ValueError:
return None
def relation_set(relation_id=None, relation_settings={}, **kwargs):
relation_cmd_line = ['relation-set']
if relation_id is not None:
relation_cmd_line.extend(('-r', relation_id))
for k, v in (relation_settings.items() + kwargs.items()):
if v is None:
relation_cmd_line.append('{}='.format(k))
else:
relation_cmd_line.append('{}={}'.format(k, v))
subprocess.check_call(relation_cmd_line)
# Flush cache of any relation-gets for local unit
flush(local_unit())
@cached
def relation_ids(reltype=None):
"A list of relation_ids"
reltype = reltype or relation_type()
relid_cmd_line = ['relation-ids', '--format=json']
if reltype is not None:
relid_cmd_line.append(reltype)
return json.loads(subprocess.check_output(relid_cmd_line)) or []
return []
@cached
def related_units(relid=None):
"A list of related units"
relid = relid or relation_id()
units_cmd_line = ['relation-list', '--format=json']
if relid is not None:
units_cmd_line.extend(('-r', relid))
return json.loads(subprocess.check_output(units_cmd_line)) or []
@cached
def relation_for_unit(unit=None, rid=None):
"Get the json represenation of a unit's relation"
unit = unit or remote_unit()
relation = relation_get(unit=unit, rid=rid)
for key in relation:
if key.endswith('-list'):
relation[key] = relation[key].split()
relation['__unit__'] = unit
return relation
@cached
def relations_for_id(relid=None):
"Get relations of a specific relation ID"
relation_data = []
relid = relid or relation_ids()
for unit in related_units(relid):
unit_data = relation_for_unit(unit, relid)
unit_data['__relid__'] = relid
relation_data.append(unit_data)
return relation_data
@cached
def relations_of_type(reltype=None):
"Get relations of a specific type"
relation_data = []
reltype = reltype or relation_type()
for relid in relation_ids(reltype):
for relation in relations_for_id(relid):
relation['__relid__'] = relid
relation_data.append(relation)
return relation_data
@cached
def relation_types():
"Get a list of relation types supported by this charm"
charmdir = os.environ.get('CHARM_DIR', '')
mdf = open(os.path.join(charmdir, 'metadata.yaml'))
md = yaml.safe_load(mdf)
rel_types = []
for key in ('provides', 'requires', 'peers'):
section = md.get(key)
if section:
rel_types.extend(section.keys())
mdf.close()
return rel_types
@cached
def relations():
rels = {}
for reltype in relation_types():
relids = {}
for relid in relation_ids(reltype):
units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
for unit in related_units(relid):
reldata = relation_get(unit=unit, rid=relid)
units[unit] = reldata
relids[relid] = units
rels[reltype] = relids
return rels
def open_port(port, protocol="TCP"):
"Open a service network port"
_args = ['open-port']
_args.append('{}/{}'.format(port, protocol))
subprocess.check_call(_args)
def close_port(port, protocol="TCP"):
"Close a service network port"
_args = ['close-port']
_args.append('{}/{}'.format(port, protocol))
subprocess.check_call(_args)
@cached
def unit_get(attribute):
_args = ['unit-get', '--format=json', attribute]
try:
return json.loads(subprocess.check_output(_args))
except ValueError:
return None
def unit_private_ip():
return unit_get('private-address')
class UnregisteredHookError(Exception):
pass
class Hooks(object):
def __init__(self):
super(Hooks, self).__init__()
self._hooks = {}
def register(self, name, function):
self._hooks[name] = function
def execute(self, args):
hook_name = os.path.basename(args[0])
if hook_name in self._hooks:
self._hooks[hook_name]()
else:
raise UnregisteredHookError(hook_name)
def hook(self, *hook_names):
def wrapper(decorated):
for hook_name in hook_names:
self.register(hook_name, decorated)
else:
self.register(decorated.__name__, decorated)
if '_' in decorated.__name__:
self.register(
decorated.__name__.replace('_', '-'), decorated)
return decorated
return wrapper
def charm_dir():
return os.environ.get('CHARM_DIR')

View File

@ -0,0 +1,241 @@
"""Tools for working with the host system"""
# Copyright 2012 Canonical Ltd.
#
# Authors:
# Nick Moffitt <nick.moffitt@canonical.com>
# Matthew Wedgwood <matthew.wedgwood@canonical.com>
import os
import pwd
import grp
import random
import string
import subprocess
import hashlib
from collections import OrderedDict
from hookenv import log
def service_start(service_name):
return service('start', service_name)
def service_stop(service_name):
return service('stop', service_name)
def service_restart(service_name):
return service('restart', service_name)
def service_reload(service_name, restart_on_failure=False):
service_result = service('reload', service_name)
if not service_result and restart_on_failure:
service_result = service('restart', service_name)
return service_result
def service(action, service_name):
cmd = ['service', service_name, action]
return subprocess.call(cmd) == 0
def service_running(service):
try:
output = subprocess.check_output(['service', service, 'status'])
except subprocess.CalledProcessError:
return False
else:
if ("start/running" in output or "is running" in output):
return True
else:
return False
def adduser(username, password=None, shell='/bin/bash', system_user=False):
"""Add a user"""
try:
user_info = pwd.getpwnam(username)
log('user {0} already exists!'.format(username))
except KeyError:
log('creating user {0}'.format(username))
cmd = ['useradd']
if system_user or password is None:
cmd.append('--system')
else:
cmd.extend([
'--create-home',
'--shell', shell,
'--password', password,
])
cmd.append(username)
subprocess.check_call(cmd)
user_info = pwd.getpwnam(username)
return user_info
def add_user_to_group(username, group):
"""Add a user to a group"""
cmd = [
'gpasswd', '-a',
username,
group
]
log("Adding user {} to group {}".format(username, group))
subprocess.check_call(cmd)
def rsync(from_path, to_path, flags='-r', options=None):
"""Replicate the contents of a path"""
options = options or ['--delete', '--executability']
cmd = ['/usr/bin/rsync', flags]
cmd.extend(options)
cmd.append(from_path)
cmd.append(to_path)
log(" ".join(cmd))
return subprocess.check_output(cmd).strip()
def symlink(source, destination):
"""Create a symbolic link"""
log("Symlinking {} as {}".format(source, destination))
cmd = [
'ln',
'-sf',
source,
destination,
]
subprocess.check_call(cmd)
def mkdir(path, owner='root', group='root', perms=0555, force=False):
"""Create a directory"""
log("Making dir {} {}:{} {:o}".format(path, owner, group,
perms))
uid = pwd.getpwnam(owner).pw_uid
gid = grp.getgrnam(group).gr_gid
realpath = os.path.abspath(path)
if os.path.exists(realpath):
if force and not os.path.isdir(realpath):
log("Removing non-directory file {} prior to mkdir()".format(path))
os.unlink(realpath)
else:
os.makedirs(realpath, perms)
os.chown(realpath, uid, gid)
def write_file(path, content, owner='root', group='root', perms=0444):
"""Create or overwrite a file with the contents of a string"""
log("Writing file {} {}:{} {:o}".format(path, owner, group, perms))
uid = pwd.getpwnam(owner).pw_uid
gid = grp.getgrnam(group).gr_gid
with open(path, 'w') as target:
os.fchown(target.fileno(), uid, gid)
os.fchmod(target.fileno(), perms)
target.write(content)
def mount(device, mountpoint, options=None, persist=False):
'''Mount a filesystem'''
cmd_args = ['mount']
if options is not None:
cmd_args.extend(['-o', options])
cmd_args.extend([device, mountpoint])
try:
subprocess.check_output(cmd_args)
except subprocess.CalledProcessError, e:
log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
return False
if persist:
# TODO: update fstab
pass
return True
def umount(mountpoint, persist=False):
'''Unmount a filesystem'''
cmd_args = ['umount', mountpoint]
try:
subprocess.check_output(cmd_args)
except subprocess.CalledProcessError, e:
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
return False
if persist:
# TODO: update fstab
pass
return True
def mounts():
'''List of all mounted volumes as [[mountpoint,device],[...]]'''
with open('/proc/mounts') as f:
# [['/mount/point','/dev/path'],[...]]
system_mounts = [m[1::-1] for m in [l.strip().split()
for l in f.readlines()]]
return system_mounts
def file_hash(path):
''' Generate a md5 hash of the contents of 'path' or None if not found '''
if os.path.exists(path):
h = hashlib.md5()
with open(path, 'r') as source:
h.update(source.read()) # IGNORE:E1101 - it does have update
return h.hexdigest()
else:
return None
def restart_on_change(restart_map):
''' Restart services based on configuration files changing
This function is used a decorator, for example
@restart_on_change({
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
})
def ceph_client_changed():
...
In this example, the cinder-api and cinder-volume services
would be restarted if /etc/ceph/ceph.conf is changed by the
ceph_client_changed function.
'''
def wrap(f):
def wrapped_f(*args):
checksums = {}
for path in restart_map:
checksums[path] = file_hash(path)
f(*args)
restarts = []
for path in restart_map:
if checksums[path] != file_hash(path):
restarts += restart_map[path]
for service_name in list(OrderedDict.fromkeys(restarts)):
service('restart', service_name)
return wrapped_f
return wrap
def lsb_release():
'''Return /etc/lsb-release in a dict'''
d = {}
with open('/etc/lsb-release', 'r') as lsb:
for l in lsb:
k, v = l.split('=')
d[k.strip()] = v.strip()
return d
def pwgen(length=None):
'''Generate a random pasword.'''
if length is None:
length = random.choice(range(35, 45))
alphanumeric_chars = [
l for l in (string.letters + string.digits)
if l not in 'l0QD1vAEIOUaeiou']
random_chars = [
random.choice(alphanumeric_chars) for _ in range(length)]
return(''.join(random_chars))

View File

@ -0,0 +1,209 @@
import importlib
from yaml import safe_load
from charmhelpers.core.host import (
lsb_release
)
from urlparse import (
urlparse,
urlunparse,
)
import subprocess
from charmhelpers.core.hookenv import (
config,
log,
)
import apt_pkg
CLOUD_ARCHIVE = """# Ubuntu Cloud Archive
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
"""
PROPOSED_POCKET = """# Proposed
deb http://archive.ubuntu.com/ubuntu {}-proposed main universe multiverse restricted
"""
def filter_installed_packages(packages):
"""Returns a list of packages that require installation"""
apt_pkg.init()
cache = apt_pkg.Cache()
_pkgs = []
for package in packages:
try:
p = cache[package]
p.current_ver or _pkgs.append(package)
except KeyError:
log('Package {} has no installation candidate.'.format(package),
level='WARNING')
_pkgs.append(package)
return _pkgs
def apt_install(packages, options=None, fatal=False):
"""Install one or more packages"""
options = options or []
cmd = ['apt-get', '-y']
cmd.extend(options)
cmd.append('install')
if isinstance(packages, basestring):
cmd.append(packages)
else:
cmd.extend(packages)
log("Installing {} with options: {}".format(packages,
options))
if fatal:
subprocess.check_call(cmd)
else:
subprocess.call(cmd)
def apt_update(fatal=False):
"""Update local apt cache"""
cmd = ['apt-get', 'update']
if fatal:
subprocess.check_call(cmd)
else:
subprocess.call(cmd)
def apt_purge(packages, fatal=False):
"""Purge one or more packages"""
cmd = ['apt-get', '-y', 'purge']
if isinstance(packages, basestring):
cmd.append(packages)
else:
cmd.extend(packages)
log("Purging {}".format(packages))
if fatal:
subprocess.check_call(cmd)
else:
subprocess.call(cmd)
def add_source(source, key=None):
if ((source.startswith('ppa:') or
source.startswith('http:'))):
subprocess.check_call(['add-apt-repository', '--yes', source])
elif source.startswith('cloud:'):
apt_install(filter_installed_packages(['ubuntu-cloud-keyring']),
fatal=True)
pocket = source.split(':')[-1]
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
apt.write(CLOUD_ARCHIVE.format(pocket))
elif source == 'proposed':
release = lsb_release()['DISTRIB_CODENAME']
with open('/etc/apt/sources.list.d/proposed.list', 'w') as apt:
apt.write(PROPOSED_POCKET.format(release))
if key:
subprocess.check_call(['apt-key', 'import', key])
class SourceConfigError(Exception):
pass
def configure_sources(update=False,
sources_var='install_sources',
keys_var='install_keys'):
"""
Configure multiple sources from charm configuration
Example config:
install_sources:
- "ppa:foo"
- "http://example.com/repo precise main"
install_keys:
- null
- "a1b2c3d4"
Note that 'null' (a.k.a. None) should not be quoted.
"""
sources = safe_load(config(sources_var))
keys = safe_load(config(keys_var))
if isinstance(sources, basestring) and isinstance(keys, basestring):
add_source(sources, keys)
else:
if not len(sources) == len(keys):
msg = 'Install sources and keys lists are different lengths'
raise SourceConfigError(msg)
for src_num in range(len(sources)):
add_source(sources[src_num], keys[src_num])
if update:
apt_update(fatal=True)
# The order of this list is very important. Handlers should be listed in from
# least- to most-specific URL matching.
FETCH_HANDLERS = (
'charmhelpers.fetch.archiveurl.ArchiveUrlFetchHandler',
'charmhelpers.fetch.bzrurl.BzrUrlFetchHandler',
)
class UnhandledSource(Exception):
pass
def install_remote(source):
"""
Install a file tree from a remote source
The specified source should be a url of the form:
scheme://[host]/path[#[option=value][&...]]
Schemes supported are based on this modules submodules
Options supported are submodule-specific"""
# We ONLY check for True here because can_handle may return a string
# explaining why it can't handle a given source.
handlers = [h for h in plugins() if h.can_handle(source) is True]
installed_to = None
for handler in handlers:
try:
installed_to = handler.install(source)
except UnhandledSource:
pass
if not installed_to:
raise UnhandledSource("No handler found for source {}".format(source))
return installed_to
def install_from_config(config_var_name):
charm_config = config()
source = charm_config[config_var_name]
return install_remote(source)
class BaseFetchHandler(object):
"""Base class for FetchHandler implementations in fetch plugins"""
def can_handle(self, source):
"""Returns True if the source can be handled. Otherwise returns
a string explaining why it cannot"""
return "Wrong source type"
def install(self, source):
"""Try to download and unpack the source. Return the path to the
unpacked files or raise UnhandledSource."""
raise UnhandledSource("Wrong source type {}".format(source))
def parse_url(self, url):
return urlparse(url)
def base_url(self, url):
"""Return url without querystring or fragment"""
parts = list(self.parse_url(url))
parts[4:] = ['' for i in parts[4:]]
return urlunparse(parts)
def plugins(fetch_handlers=None):
if not fetch_handlers:
fetch_handlers = FETCH_HANDLERS
plugin_list = []
for handler_name in fetch_handlers:
package, classname = handler_name.rsplit('.', 1)
try:
handler_class = getattr(importlib.import_module(package), classname)
plugin_list.append(handler_class())
except (ImportError, AttributeError):
# Skip missing plugins so that they can be ommitted from
# installation if desired
log("FetchHandler {} not found, skipping plugin".format(handler_name))
return plugin_list

View File

@ -0,0 +1,48 @@
import os
import urllib2
from charmhelpers.fetch import (
BaseFetchHandler,
UnhandledSource
)
from charmhelpers.payload.archive import (
get_archive_handler,
extract,
)
from charmhelpers.core.host import mkdir
class ArchiveUrlFetchHandler(BaseFetchHandler):
"""Handler for archives via generic URLs"""
def can_handle(self, source):
url_parts = self.parse_url(source)
if url_parts.scheme not in ('http', 'https', 'ftp', 'file'):
return "Wrong source type"
if get_archive_handler(self.base_url(source)):
return True
return False
def download(self, source, dest):
# propogate all exceptions
# URLError, OSError, etc
response = urllib2.urlopen(source)
try:
with open(dest, 'w') as dest_file:
dest_file.write(response.read())
except Exception as e:
if os.path.isfile(dest):
os.unlink(dest)
raise e
def install(self, source):
url_parts = self.parse_url(source)
dest_dir = os.path.join(os.environ.get('CHARM_DIR'), 'fetched')
if not os.path.exists(dest_dir):
mkdir(dest_dir, perms=0755)
dld_file = os.path.join(dest_dir, os.path.basename(url_parts.path))
try:
self.download(source, dld_file)
except urllib2.URLError as e:
raise UnhandledSource(e.reason)
except OSError as e:
raise UnhandledSource(e.strerror)
return extract(dld_file)

View File

@ -0,0 +1,49 @@
import os
from charmhelpers.fetch import (
BaseFetchHandler,
UnhandledSource
)
from charmhelpers.core.host import mkdir
try:
from bzrlib.branch import Branch
except ImportError:
from charmhelpers.fetch import apt_install
apt_install("python-bzrlib")
from bzrlib.branch import Branch
class BzrUrlFetchHandler(BaseFetchHandler):
"""Handler for bazaar branches via generic and lp URLs"""
def can_handle(self, source):
url_parts = self.parse_url(source)
if url_parts.scheme not in ('bzr+ssh', 'lp'):
return False
else:
return True
def branch(self, source, dest):
url_parts = self.parse_url(source)
# If we use lp:branchname scheme we need to load plugins
if not self.can_handle(source):
raise UnhandledSource("Cannot handle {}".format(source))
if url_parts.scheme == "lp":
from bzrlib.plugin import load_plugins
load_plugins()
try:
remote_branch = Branch.open(source)
remote_branch.bzrdir.sprout(dest).open_branch()
except Exception as e:
raise e
def install(self, source):
url_parts = self.parse_url(source)
branch_name = url_parts.path.strip("/").split("/")[-1]
dest_dir = os.path.join(os.environ.get('CHARM_DIR'), "fetched", branch_name)
if not os.path.exists(dest_dir):
mkdir(dest_dir, perms=0755)
try:
self.branch(source, dest_dir)
except OSError as e:
raise UnhandledSource(e.strerror)
return dest_dir

View File

@ -0,0 +1 @@
"Tools for working with files injected into a charm just before deployment."

View File

@ -0,0 +1,50 @@
#!/usr/bin/env python
import os
import sys
import subprocess
from charmhelpers.core import hookenv
def default_execd_dir():
return os.path.join(os.environ['CHARM_DIR'], 'exec.d')
def execd_module_paths(execd_dir=None):
"""Generate a list of full paths to modules within execd_dir."""
if not execd_dir:
execd_dir = default_execd_dir()
if not os.path.exists(execd_dir):
return
for subpath in os.listdir(execd_dir):
module = os.path.join(execd_dir, subpath)
if os.path.isdir(module):
yield module
def execd_submodule_paths(command, execd_dir=None):
"""Generate a list of full paths to the specified command within exec_dir.
"""
for module_path in execd_module_paths(execd_dir):
path = os.path.join(module_path, command)
if os.access(path, os.X_OK) and os.path.isfile(path):
yield path
def execd_run(command, execd_dir=None, die_on_error=False, stderr=None):
"""Run command for each module within execd_dir which defines it."""
for submodule_path in execd_submodule_paths(command, execd_dir):
try:
subprocess.check_call(submodule_path, shell=True, stderr=stderr)
except subprocess.CalledProcessError as e:
hookenv.log("Error ({}) running {}. Output: {}".format(
e.returncode, e.cmd, e.output))
if die_on_error:
sys.exit(e.returncode)
def execd_preinstall(execd_dir=None):
"""Run charm-pre-install for each module within execd_dir."""
execd_run('charm-pre-install', execd_dir=execd_dir)

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -0,0 +1 @@
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -0,0 +1 @@
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -1 +0,0 @@
nova-compute-relations

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -1,43 +0,0 @@
#!/bin/bash -e
# Essex-specific functions
nova_set_or_update() {
# Set a config option in nova.conf or api-paste.ini, depending
# Defaults to updating nova.conf
local key=$1
local value=$2
local conf_file=$3
local pattern=""
local nova_conf=${NOVA_CONF:-/etc/nova/nova.conf}
local api_conf=${API_CONF:-/etc/nova/api-paste.ini}
local libvirtd_conf=${LIBVIRTD_CONF:-/etc/libvirt/libvirtd.conf}
[[ -z $key ]] && juju-log "$CHARM set_or_update: value $value missing key" && exit 1
[[ -z $value ]] && juju-log "$CHARM set_or_update: key $key missing value" && exit 1
[[ -z "$conf_file" ]] && conf_file=$nova_conf
case "$conf_file" in
"$nova_conf") match="\-\-$key="
pattern="--$key="
out=$pattern
;;
"$api_conf"|"$libvirtd_conf") match="^$key = "
pattern="$match"
out="$key = "
;;
*) error_out "ERROR: set_or_update: Invalid conf_file ($conf_file)"
esac
cat $conf_file | grep "$match$value" >/dev/null &&
juju-log "$CHARM: $key=$value already in set in $conf_file" \
&& return 0
if cat $conf_file | grep "$match" >/dev/null ; then
juju-log "$CHARM: Updating $conf_file, $key=$value"
sed -i "s|\($pattern\).*|\1$value|" $conf_file
else
juju-log "$CHARM: Setting new option $key=$value in $conf_file"
echo "$out$value" >>$conf_file
fi
CONFIG_CHANGED=True
}

View File

@ -1,135 +0,0 @@
#!/bin/bash -e
# Folsom-specific functions
nova_set_or_update() {
# Set a config option in nova.conf or api-paste.ini, depending
# Defaults to updating nova.conf
local key="$1"
local value="$2"
local conf_file="$3"
local section="${4:-DEFAULT}"
local nova_conf=${NOVA_CONF:-/etc/nova/nova.conf}
local api_conf=${API_CONF:-/etc/nova/api-paste.ini}
local quantum_conf=${QUANTUM_CONF:-/etc/quantum/quantum.conf}
local quantum_api_conf=${QUANTUM_API_CONF:-/etc/quantum/api-paste.ini}
local quantum_plugin_conf=${QUANTUM_PLUGIN_CONF:-/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini}
local libvirtd_conf=${LIBVIRTD_CONF:-/etc/libvirt/libvirtd.conf}
[[ -z $key ]] && juju-log "$CHARM: set_or_update: value $value missing key" && exit 1
[[ -z $value ]] && juju-log "$CHARM: set_or_update: key $key missing value" && exit 1
[[ -z "$conf_file" ]] && conf_file=$nova_conf
local pattern=""
case "$conf_file" in
"$nova_conf") match="^$key="
pattern="$key="
out=$pattern
;;
"$api_conf"|"$quantum_conf"|"$quantum_api_conf"|"$quantum_plugin_conf"| \
"$libvirtd_conf")
match="^$key = "
pattern="$match"
out="$key = "
;;
*) juju-log "$CHARM ERROR: set_or_update: Invalid conf_file ($conf_file)"
esac
cat $conf_file | grep "$match$value" >/dev/null &&
juju-log "$CHARM: $key=$value already in set in $conf_file" \
&& return 0
case $conf_file in
"$quantum_conf"|"$quantum_api_conf"|"$quantum_plugin_conf")
python -c "
import ConfigParser
config = ConfigParser.RawConfigParser()
config.read('$conf_file')
config.set('$section','$key','$value')
with open('$conf_file', 'wb') as configfile:
config.write(configfile)
"
;;
*)
if cat $conf_file | grep "$match" >/dev/null ; then
juju-log "$CHARM: Updating $conf_file, $key=$value"
sed -i "s|\($pattern\).*|\1$value|" $conf_file
else
juju-log "$CHARM: Setting new option $key=$value in $conf_file"
echo "$out$value" >>$conf_file
fi
;;
esac
CONFIG_CHANGED="True"
}
# Upgrade Helpers
nova_pre_upgrade() {
# Pre-upgrade helper. Caller should pass the version of OpenStack we are
# upgrading from.
return 0 # Nothing to do here, yet.
}
nova_post_upgrade() {
# Post-upgrade helper. Caller should pass the version of OpenStack we are
# upgrading from.
local upgrade_from="$1"
juju-log "$CHARM: Running post-upgrade hook: $upgrade_from -> folsom."
# We only support essex -> folsom, currently.
[[ "$upgrade_from" != "essex" ]] &&
error_out "Unsupported upgrade: $upgrade_from -> folsom"
# This may be dangerous, if we are upgrading a number of units at once
# and they all begin the same migration concurrently. Migrate only from
# the cloud controller(s).
if [[ "$CHARM" == "nova-cloud-controller" ]] ; then
juju-log "$CHARM: Migrating nova database."
/usr/bin/nova-manage db sync
# Trigger a service restart on all other nova nodes.
trigger_remote_service_restarts
fi
# Packaging currently takes care of converting the Essex gflags format
# to .ini, but we need to update the api-paste.ini manually. It can be
# updated directly from keystone, via the identity-service relation,
# if it exists. Only services that require keystone credentials will
# have modified api-paste.ini, and only those services will have a .dpkg-dist
# version present.
local r_id=$(relation-ids identity-service)
if [[ -n "$r_id" ]] && [[ -e "$CONF_DIR/api-paste.ini.dpkg-dist" ]] ; then
# Backup the last api config, update the stock packaged version
# with our current Keystone info.
mv $API_CONF $CONF_DIR/api-paste.ini.juju-last
mv $CONF_DIR/api-paste.ini.dpkg-dist $CONF_DIR/api-paste.ini
unit=$(relation-list -r $r_id | head -n1)
# Note, this should never be called from an relation hook, only config-changed.
export JUJU_REMOTE_UNIT=$unit
service_port=$(relation-get -r $r_id service_port)
auth_port=$(relation-get -r $r_id auth_port)
service_username=$(relation-get -r $r_id service_username)
service_password=$(relation-get -r $r_id service_password)
service_tenant=$(relation-get -r $r_id service_tenant)
keystone_host=$(relation-get -r $r_id private-address)
unset JUJU_REMOTE_UNIT
juju-log "$CHARM: Updating new api-paste.ini with keystone data from $unit:$r_id"
set_or_update "service_host" "$keystone_host" "$API_CONF"
set_or_update "service_port" "$service_port" "$API_CONF"
set_or_update "auth_host" "$keystone_host" "$API_CONF"
set_or_update "auth_port" "$auth_port" "$API_CONF"
set_or_update "auth_uri" "http://$keystone_host:$service_port/" "$API_CONF"
set_or_update "admin_tenant_name" "$service_tenant" "$API_CONF"
set_or_update "admin_user" "$service_username" "$API_CONF"
set_or_update "admin_password" "$service_password" "$API_CONF"
fi
# TEMPORARY
# RC3 packaging in cloud archive doesn't have this in postinst. Do it here
sed -e "s,^root_helper=.\+,rootwrap_config=/etc/nova/rootwrap.conf," -i /etc/nova/nova.conf
juju-log "$CHARM: Post-upgrade hook complete: $upgrade_from -> folsom."
}

View File

@ -1,97 +0,0 @@
#!/bin/bash -e
# Folsom-specific functions
nova_set_or_update() {
# TODO: This needs to be shared among folsom, grizzly and beyond.
# Set a config option in nova.conf or api-paste.ini, depending
# Defaults to updating nova.conf
local key="$1"
local value="$2"
local conf_file="$3"
local section="${4:-DEFAULT}"
local nova_conf=${NOVA_CONF:-/etc/nova/nova.conf}
local api_conf=${API_CONF:-/etc/nova/api-paste.ini}
local quantum_conf=${QUANTUM_CONF:-/etc/quantum/quantum.conf}
local quantum_api_conf=${QUANTUM_API_CONF:-/etc/quantum/api-paste.ini}
local quantum_plugin_conf=${QUANTUM_PLUGIN_CONF:-/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini}
local libvirtd_conf=${LIBVIRTD_CONF:-/etc/libvirt/libvirtd.conf}
[[ -z $key ]] && juju-log "$CHARM: set_or_update: value $value missing key" && exit 1
[[ -z $value ]] && juju-log "$CHARM: set_or_update: key $key missing value" && exit 1
[[ -z "$conf_file" ]] && conf_file=$nova_conf
local pattern=""
case "$conf_file" in
"$nova_conf") match="^$key="
pattern="$key="
out=$pattern
;;
"$api_conf"|"$quantum_conf"|"$quantum_api_conf"|"$quantum_plugin_conf"| \
"$libvirtd_conf")
match="^$key = "
pattern="$match"
out="$key = "
;;
*) juju-log "$CHARM ERROR: set_or_update: Invalid conf_file ($conf_file)"
esac
cat $conf_file | grep "$match$value" >/dev/null &&
juju-log "$CHARM: $key=$value already in set in $conf_file" \
&& return 0
case $conf_file in
"$quantum_conf"|"$quantum_api_conf"|"$quantum_plugin_conf")
python -c "
import ConfigParser
config = ConfigParser.RawConfigParser()
config.read('$conf_file')
config.set('$section','$key','$value')
with open('$conf_file', 'wb') as configfile:
config.write(configfile)
"
;;
*)
if cat $conf_file | grep "$match" >/dev/null ; then
juju-log "$CHARM: Updating $conf_file, $key=$value"
sed -i "s|\($pattern\).*|\1$value|" $conf_file
else
juju-log "$CHARM: Setting new option $key=$value in $conf_file"
echo "$out$value" >>$conf_file
fi
;;
esac
CONFIG_CHANGED="True"
}
# Upgrade Helpers
nova_pre_upgrade() {
# Pre-upgrade helper. Caller should pass the version of OpenStack we are
# upgrading from.
return 0 # Nothing to do here, yet.
}
nova_post_upgrade() {
# Post-upgrade helper. Caller should pass the version of OpenStack we are
# upgrading from.
local upgrade_from="$1"
juju-log "$CHARM: Running post-upgrade hook: $upgrade_from -> grizzly."
# We only support folsom -> grizzly, currently.
[[ "$upgrade_from" != "folsom" ]] &&
error_out "Unsupported upgrade: $upgrade_from -> grizzly"
# This may be dangerous, if we are upgrading a number of units at once
# and they all begin the same migration concurrently. Migrate only from
# the cloud controller(s).
if [[ "$CHARM" == "nova-cloud-controller" ]] ; then
juju-log "$CHARM: Migrating nova database."
/usr/bin/nova-manage db sync
# Trigger a service restart on all other nova nodes.
trigger_remote_service_restarts
fi
juju-log "$CHARM: Post-upgrade hook complete: $upgrade_from -> grizzly."
}

View File

@ -1,148 +0,0 @@
#!/bin/bash -e
# Common utility functions used across all nova charms.
CONFIG_CHANGED=False
HOOKS_DIR="$CHARM_DIR/hooks"
# Load the common OpenStack helper library.
if [[ -e $HOOKS_DIR/lib/openstack-common ]] ; then
. $HOOKS_DIR/lib/openstack-common
else
juju-log "Couldn't load $HOOKS_DIR/lib/opentack-common." && exit 1
fi
set_or_update() {
# Update config flags in nova.conf or api-paste.ini.
# Config layout changed in Folsom, so this is now OpenStack release specific.
local rel=$(get_os_codename_package "nova-common")
. $HOOKS_DIR/lib/nova/$rel
nova_set_or_update $@
}
function set_config_flags() {
# Set user-defined nova.conf flags from deployment config
juju-log "$CHARM: Processing config-flags."
flags=$(config-get config-flags)
if [[ "$flags" != "None" && -n "$flags" ]] ; then
for f in $(echo $flags | sed -e 's/,/ /g') ; do
k=$(echo $f | cut -d= -f1)
v=$(echo $f | cut -d= -f2)
set_or_update "$k" "$v"
done
fi
}
configure_volume_service() {
local svc="$1"
local cur_vers="$(get_os_codename_package "nova-common")"
case "$svc" in
"cinder")
set_or_update "volume_api_class" "nova.volume.cinder.API" ;;
"nova-volume")
# nova-volume only supported before grizzly.
[[ "$cur_vers" == "essex" ]] || [[ "$cur_vers" == "folsom" ]] &&
set_or_update "volume_api_class" "nova.volume.api.API"
;;
*) juju-log "$CHARM ERROR - configure_volume_service: Invalid service $svc"
return 1 ;;
esac
}
function configure_network_manager {
local manager="$1"
echo "$CHARM: configuring $manager network manager"
case $1 in
"FlatManager")
set_or_update "network_manager" "nova.network.manager.FlatManager"
;;
"FlatDHCPManager")
set_or_update "network_manager" "nova.network.manager.FlatDHCPManager"
if [[ "$CHARM" == "nova-compute" ]] ; then
local flat_interface=$(config-get flat-interface)
local ec2_host=$(relation-get ec2_host)
set_or_update flat_inteface "$flat_interface"
set_or_update ec2_dmz_host "$ec2_host"
# Ensure flat_interface has link.
if ip link show $flat_interface >/dev/null 2>&1 ; then
ip link set $flat_interface up
fi
# work around (LP: #1035172)
if [[ -e /dev/vhost-net ]] ; then
iptables -A POSTROUTING -t mangle -p udp --dport 68 -j CHECKSUM \
--checksum-fill
fi
fi
;;
"Quantum")
local local_ip=$(get_ip `unit-get private-address`)
[[ -n $local_ip ]] || {
juju-log "Unable to resolve local IP address"
exit 1
}
set_or_update "network_api_class" "nova.network.quantumv2.api.API"
set_or_update "quantum_auth_strategy" "keystone"
set_or_update "core_plugin" "$QUANTUM_CORE_PLUGIN" "$QUANTUM_CONF"
set_or_update "bind_host" "0.0.0.0" "$QUANTUM_CONF"
if [ "$QUANTUM_PLUGIN" == "ovs" ]; then
set_or_update "tenant_network_type" "gre" $QUANTUM_PLUGIN_CONF "OVS"
set_or_update "enable_tunneling" "True" $QUANTUM_PLUGIN_CONF "OVS"
set_or_update "tunnel_id_ranges" "1:1000" $QUANTUM_PLUGIN_CONF "OVS"
set_or_update "local_ip" "$local_ip" $QUANTUM_PLUGIN_CONF "OVS"
fi
;;
*) juju-log "ERROR: Invalid network manager $1" && exit 1 ;;
esac
}
function trigger_remote_service_restarts() {
# Trigger a service restart on all other nova nodes that have a relation
# via the cloud-controller interface.
# possible relations to other nova services.
local relations="cloud-compute nova-volume-service"
for rel in $relations; do
local r_ids=$(relation-ids $rel)
for r_id in $r_ids ; do
juju-log "$CHARM: Triggering a service restart on relation $r_id."
relation-set -r $r_id restart-trigger=$(uuid)
done
done
}
do_openstack_upgrade() {
# update openstack components to those provided by a new installation source
# it is assumed the calling hook has confirmed that the upgrade is sane.
local rel="$1"
shift
local packages=$@
orig_os_rel=$(get_os_codename_package "nova-common")
new_rel=$(get_os_codename_install_source "$rel")
# Backup the config directory.
local stamp=$(date +"%Y%m%d%M%S")
tar -pcf /var/lib/juju/$CHARM-backup-$stamp.tar $CONF_DIR
# load the release helper library for pre/post upgrade hooks specific to the
# release we are upgrading to.
. $HOOKS_DIR/lib/nova/$new_rel
# new release specific pre-upgrade hook
nova_pre_upgrade "$orig_os_rel"
# Setup apt repository access and kick off the actual package upgrade.
configure_install_source "$rel"
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get --option Dpkg::Options::=--force-confold -y \
install --no-install-recommends $packages
# new release sepcific post-upgrade hook
nova_post_upgrade "$orig_os_rel"
}

View File

@ -1,781 +0,0 @@
#!/bin/bash -e
# Common utility functions used across all OpenStack charms.
error_out() {
juju-log "$CHARM ERROR: $@"
exit 1
}
function service_ctl_status {
# Return 0 if a service is running, 1 otherwise.
local svc="$1"
local status=$(service $svc status | cut -d/ -f1 | awk '{ print $2 }')
case $status in
"start") return 0 ;;
"stop") return 1 ;;
*) error_out "Unexpected status of service $svc: $status" ;;
esac
}
function service_ctl {
# control a specific service, or all (as defined by $SERVICES)
# service restarts will only occur depending on global $CONFIG_CHANGED,
# which should be updated in charm's set_or_update().
local config_changed=${CONFIG_CHANGED:-True}
if [[ $1 == "all" ]] ; then
ctl="$SERVICES"
else
ctl="$1"
fi
action="$2"
if [[ -z "$ctl" ]] || [[ -z "$action" ]] ; then
error_out "ERROR service_ctl: Not enough arguments"
fi
for i in $ctl ; do
case $action in
"start")
service_ctl_status $i || service $i start ;;
"stop")
service_ctl_status $i && service $i stop || return 0 ;;
"restart")
if [[ "$config_changed" == "True" ]] ; then
service_ctl_status $i && service $i restart || service $i start
fi
;;
esac
if [[ $? != 0 ]] ; then
juju-log "$CHARM: service_ctl ERROR - Service $i failed to $action"
fi
done
# all configs should have been reloaded on restart of all services, reset
# flag if its being used.
if [[ "$action" == "restart" ]] && [[ -n "$CONFIG_CHANGED" ]] &&
[[ "$ctl" == "all" ]]; then
CONFIG_CHANGED="False"
fi
}
function configure_install_source {
# Setup and configure installation source based on a config flag.
local src="$1"
# Default to installing from the main Ubuntu archive.
[[ $src == "distro" ]] || [[ -z "$src" ]] && return 0
. /etc/lsb-release
# standard 'ppa:someppa/name' format.
if [[ "${src:0:4}" == "ppa:" ]] ; then
juju-log "$CHARM: Configuring installation from custom src ($src)"
add-apt-repository -y "$src" || error_out "Could not configure PPA access."
return 0
fi
# standard 'deb http://url/ubuntu main' entries. gpg key ids must
# be appended to the end of url after a |, ie:
# 'deb http://url/ubuntu main|$GPGKEYID'
if [[ "${src:0:3}" == "deb" ]] ; then
juju-log "$CHARM: Configuring installation from custom src URL ($src)"
if echo "$src" | grep -q "|" ; then
# gpg key id tagged to end of url folloed by a |
url=$(echo $src | cut -d'|' -f1)
key=$(echo $src | cut -d'|' -f2)
juju-log "$CHARM: Importing repository key: $key"
apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" || \
juju-log "$CHARM WARN: Could not import key from keyserver: $key"
else
juju-log "$CHARM No repository key specified."
url="$src"
fi
echo "$url" > /etc/apt/sources.list.d/juju_deb.list
return 0
fi
# Cloud Archive
if [[ "${src:0:6}" == "cloud:" ]] ; then
# current os releases supported by the UCA.
local cloud_archive_versions="folsom grizzly"
local ca_rel=$(echo $src | cut -d: -f2)
local u_rel=$(echo $ca_rel | cut -d- -f1)
local os_rel=$(echo $ca_rel | cut -d- -f2 | cut -d/ -f1)
[[ "$u_rel" != "$DISTRIB_CODENAME" ]] &&
error_out "Cannot install from Cloud Archive pocket $src " \
"on this Ubuntu version ($DISTRIB_CODENAME)!"
valid_release=""
for rel in $cloud_archive_versions ; do
if [[ "$os_rel" == "$rel" ]] ; then
valid_release=1
juju-log "Installing OpenStack ($os_rel) from the Ubuntu Cloud Archive."
fi
done
if [[ -z "$valid_release" ]] ; then
error_out "OpenStack release ($os_rel) not supported by "\
"the Ubuntu Cloud Archive."
fi
# CA staging repos are standard PPAs.
if echo $ca_rel | grep -q "staging" ; then
add-apt-repository -y ppa:ubuntu-cloud-archive/${os_rel}-staging
return 0
fi
# the others are LP-external deb repos.
case "$ca_rel" in
"$u_rel-$os_rel"|"$u_rel-$os_rel/updates") pocket="$u_rel-updates/$os_rel" ;;
"$u_rel-$os_rel/proposed") pocket="$u_rel-proposed/$os_rel" ;;
"$u_rel-$os_rel"|"$os_rel/updates") pocket="$u_rel-updates/$os_rel" ;;
"$u_rel-$os_rel/proposed") pocket="$u_rel-proposed/$os_rel" ;;
*) error_out "Invalid Cloud Archive repo specified: $src"
esac
apt-get -y install ubuntu-cloud-keyring
entry="deb http://ubuntu-cloud.archive.canonical.com/ubuntu $pocket main"
echo "$entry" \
>/etc/apt/sources.list.d/ubuntu-cloud-archive-$DISTRIB_CODENAME.list
return 0
fi
error_out "Invalid installation source specified in config: $src"
}
get_os_codename_install_source() {
# derive the openstack release provided by a supported installation source.
local rel="$1"
local codename="unknown"
. /etc/lsb-release
# map ubuntu releases to the openstack version shipped with it.
if [[ "$rel" == "distro" ]] ; then
case "$DISTRIB_CODENAME" in
"oneiric") codename="diablo" ;;
"precise") codename="essex" ;;
"quantal") codename="folsom" ;;
"raring") codename="grizzly" ;;
esac
fi
# derive version from cloud archive strings.
if [[ "${rel:0:6}" == "cloud:" ]] ; then
rel=$(echo $rel | cut -d: -f2)
local u_rel=$(echo $rel | cut -d- -f1)
local ca_rel=$(echo $rel | cut -d- -f2)
if [[ "$u_rel" == "$DISTRIB_CODENAME" ]] ; then
case "$ca_rel" in
"folsom"|"folsom/updates"|"folsom/proposed"|"folsom/staging")
codename="folsom" ;;
"grizzly"|"grizzly/updates"|"grizzly/proposed"|"grizzly/staging")
codename="grizzly" ;;
esac
fi
fi
# have a guess based on the deb string provided
if [[ "${rel:0:3}" == "deb" ]] || \
[[ "${rel:0:3}" == "ppa" ]] ; then
CODENAMES="diablo essex folsom grizzly havana"
for cname in $CODENAMES; do
if echo $rel | grep -q $cname; then
codename=$cname
fi
done
fi
echo $codename
}
get_os_codename_package() {
local pkg_vers=$(dpkg -l | grep "$1" | awk '{ print $3 }') || echo "none"
pkg_vers=$(echo $pkg_vers | cut -d: -f2) # epochs
case "${pkg_vers:0:6}" in
"2011.2") echo "diablo" ;;
"2012.1") echo "essex" ;;
"2012.2") echo "folsom" ;;
"2013.1") echo "grizzly" ;;
"2013.2") echo "havana" ;;
esac
}
get_os_version_codename() {
case "$1" in
"diablo") echo "2011.2" ;;
"essex") echo "2012.1" ;;
"folsom") echo "2012.2" ;;
"grizzly") echo "2013.1" ;;
"havana") echo "2013.2" ;;
esac
}
get_ip() {
dpkg -l | grep -q python-dnspython || {
apt-get -y install python-dnspython 2>&1 > /dev/null
}
hostname=$1
python -c "
import dns.resolver
import socket
try:
# Test to see if already an IPv4 address
socket.inet_aton('$hostname')
print '$hostname'
except socket.error:
try:
answers = dns.resolver.query('$hostname', 'A')
if answers:
print answers[0].address
except dns.resolver.NXDOMAIN:
pass
"
}
# Common storage routines used by cinder, nova-volume and swift-storage.
clean_storage() {
# if configured to overwrite existing storage, we unmount the block-dev
# if mounted and clear any previous pv signatures
local block_dev="$1"
juju-log "Cleaining storage '$block_dev'"
if grep -q "^$block_dev" /proc/mounts ; then
mp=$(grep "^$block_dev" /proc/mounts | awk '{ print $2 }')
juju-log "Unmounting $block_dev from $mp"
umount "$mp" || error_out "ERROR: Could not unmount storage from $mp"
fi
if pvdisplay "$block_dev" >/dev/null 2>&1 ; then
juju-log "Removing existing LVM PV signatures from $block_dev"
# deactivate any volgroups that may be built on this dev
vg=$(pvdisplay $block_dev | grep "VG Name" | awk '{ print $3 }')
if [[ -n "$vg" ]] ; then
juju-log "Deactivating existing volume group: $vg"
vgchange -an "$vg" ||
error_out "ERROR: Could not deactivate volgroup $vg. Is it in use?"
fi
echo "yes" | pvremove -ff "$block_dev" ||
error_out "Could not pvremove $block_dev"
else
juju-log "Zapping disk of all GPT and MBR structures"
sgdisk --zap-all $block_dev ||
error_out "Unable to zap $block_dev"
fi
}
function get_block_device() {
# given a string, return full path to the block device for that
# if input is not a block device, find a loopback device
local input="$1"
case "$input" in
/dev/*) [[ ! -b "$input" ]] && error_out "$input does not exist."
echo "$input"; return 0;;
/*) :;;
*) [[ ! -b "/dev/$input" ]] && error_out "/dev/$input does not exist."
echo "/dev/$input"; return 0;;
esac
# this represents a file
# support "/path/to/file|5G"
local fpath size oifs="$IFS"
if [ "${input#*|}" != "${input}" ]; then
size=${input##*|}
fpath=${input%|*}
else
fpath=${input}
size=5G
fi
## loop devices are not namespaced. This is bad for containers.
## it means that the output of 'losetup' may have the given $fpath
## in it, but that may not represent this containers $fpath, but
## another containers. To address that, we really need to
## allow some uniq container-id to be expanded within path.
## TODO: find a unique container-id that will be consistent for
## this container throughout its lifetime and expand it
## in the fpath.
# fpath=${fpath//%{id}/$THAT_ID}
local found=""
# parse through 'losetup -a' output, looking for this file
# output is expected to look like:
# /dev/loop0: [0807]:961814 (/tmp/my.img)
found=$(losetup -a |
awk 'BEGIN { found=0; }
$3 == f { sub(/:$/,"",$1); print $1; found=found+1; }
END { if( found == 0 || found == 1 ) { exit(0); }; exit(1); }' \
f="($fpath)")
if [ $? -ne 0 ]; then
echo "multiple devices found for $fpath: $found" 1>&2
return 1;
fi
[ -n "$found" -a -b "$found" ] && { echo "$found"; return 1; }
if [ -n "$found" ]; then
echo "confused, $found is not a block device for $fpath";
return 1;
fi
# no existing device was found, create one
mkdir -p "${fpath%/*}"
truncate --size "$size" "$fpath" ||
{ echo "failed to create $fpath of size $size"; return 1; }
found=$(losetup --find --show "$fpath") ||
{ echo "failed to setup loop device for $fpath" 1>&2; return 1; }
echo "$found"
return 0
}
HAPROXY_CFG=/etc/haproxy/haproxy.cfg
HAPROXY_DEFAULT=/etc/default/haproxy
##########################################################################
# Description: Configures HAProxy services for Openstack API's
# Parameters:
# Space delimited list of service:port:mode combinations for which
# haproxy service configuration should be generated for. The function
# assumes the name of the peer relation is 'cluster' and that every
# service unit in the peer relation is running the same services.
#
# Services that do not specify :mode in parameter will default to http.
#
# Example
# configure_haproxy cinder_api:8776:8756:tcp nova_api:8774:8764:http
##########################################################################
configure_haproxy() {
local address=`unit-get private-address`
local name=${JUJU_UNIT_NAME////-}
cat > $HAPROXY_CFG << EOF
global
log 127.0.0.1 local0
log 127.0.0.1 local1 notice
maxconn 20000
user haproxy
group haproxy
spread-checks 0
defaults
log global
mode http
option httplog
option dontlognull
retries 3
timeout queue 1000
timeout connect 1000
timeout client 30000
timeout server 30000
listen stats :8888
mode http
stats enable
stats hide-version
stats realm Haproxy\ Statistics
stats uri /
stats auth admin:password
EOF
for service in $@; do
local service_name=$(echo $service | cut -d : -f 1)
local haproxy_listen_port=$(echo $service | cut -d : -f 2)
local api_listen_port=$(echo $service | cut -d : -f 3)
local mode=$(echo $service | cut -d : -f 4)
[[ -z "$mode" ]] && mode="http"
juju-log "Adding haproxy configuration entry for $service "\
"($haproxy_listen_port -> $api_listen_port)"
cat >> $HAPROXY_CFG << EOF
listen $service_name 0.0.0.0:$haproxy_listen_port
balance roundrobin
mode $mode
option ${mode}log
server $name $address:$api_listen_port check
EOF
local r_id=""
local unit=""
for r_id in `relation-ids cluster`; do
for unit in `relation-list -r $r_id`; do
local unit_name=${unit////-}
local unit_address=`relation-get -r $r_id private-address $unit`
if [ -n "$unit_address" ]; then
echo " server $unit_name $unit_address:$api_listen_port check" \
>> $HAPROXY_CFG
fi
done
done
done
echo "ENABLED=1" > $HAPROXY_DEFAULT
service haproxy restart
}
##########################################################################
# Description: Query HA interface to determine is cluster is configured
# Returns: 0 if configured, 1 if not configured
##########################################################################
is_clustered() {
local r_id=""
local unit=""
for r_id in $(relation-ids ha); do
if [ -n "$r_id" ]; then
for unit in $(relation-list -r $r_id); do
clustered=$(relation-get -r $r_id clustered $unit)
if [ -n "$clustered" ]; then
juju-log "Unit is haclustered"
return 0
fi
done
fi
done
juju-log "Unit is not haclustered"
return 1
}
##########################################################################
# Description: Return a list of all peers in cluster relations
##########################################################################
peer_units() {
local peers=""
local r_id=""
for r_id in $(relation-ids cluster); do
peers="$peers $(relation-list -r $r_id)"
done
echo $peers
}
##########################################################################
# Description: Determines whether the current unit is the oldest of all
# its peers - supports partial leader election
# Returns: 0 if oldest, 1 if not
##########################################################################
oldest_peer() {
peers=$1
local l_unit_no=$(echo $JUJU_UNIT_NAME | cut -d / -f 2)
for peer in $peers; do
echo "Comparing $JUJU_UNIT_NAME with peers: $peers"
local r_unit_no=$(echo $peer | cut -d / -f 2)
if (($r_unit_no<$l_unit_no)); then
juju-log "Not oldest peer; deferring"
return 1
fi
done
juju-log "Oldest peer; might take charge?"
return 0
}
##########################################################################
# Description: Determines whether the current service units is the
# leader within a) a cluster of its peers or b) across a
# set of unclustered peers.
# Parameters: CRM resource to check ownership of if clustered
# Returns: 0 if leader, 1 if not
##########################################################################
eligible_leader() {
if is_clustered; then
if ! is_leader $1; then
juju-log 'Deferring action to CRM leader'
return 1
fi
else
peers=$(peer_units)
if [ -n "$peers" ] && ! oldest_peer "$peers"; then
juju-log 'Deferring action to oldest service unit.'
return 1
fi
fi
return 0
}
##########################################################################
# Description: Query Cluster peer interface to see if peered
# Returns: 0 if peered, 1 if not peered
##########################################################################
is_peered() {
local r_id=$(relation-ids cluster)
if [ -n "$r_id" ]; then
if [ -n "$(relation-list -r $r_id)" ]; then
juju-log "Unit peered"
return 0
fi
fi
juju-log "Unit not peered"
return 1
}
##########################################################################
# Description: Determines whether host is owner of clustered services
# Parameters: Name of CRM resource to check ownership of
# Returns: 0 if leader, 1 if not leader
##########################################################################
is_leader() {
hostname=`hostname`
if [ -x /usr/sbin/crm ]; then
if crm resource show $1 | grep -q $hostname; then
juju-log "$hostname is cluster leader."
return 0
fi
fi
juju-log "$hostname is not cluster leader."
return 1
}
##########################################################################
# Description: Determines whether enough data has been provided in
# configuration or relation data to configure HTTPS.
# Parameters: None
# Returns: 0 if HTTPS can be configured, 1 if not.
##########################################################################
https() {
local r_id=""
if [[ -n "$(config-get ssl_cert)" ]] &&
[[ -n "$(config-get ssl_key)" ]] ; then
return 0
fi
for r_id in $(relation-ids identity-service) ; do
for unit in $(relation-list -r $r_id) ; do
if [[ "$(relation-get -r $r_id https_keystone $unit)" == "True" ]] &&
[[ -n "$(relation-get -r $r_id ssl_cert $unit)" ]] &&
[[ -n "$(relation-get -r $r_id ssl_key $unit)" ]] &&
[[ -n "$(relation-get -r $r_id ca_cert $unit)" ]] ; then
return 0
fi
done
done
return 1
}
##########################################################################
# Description: For a given number of port mappings, configures apache2
# HTTPs local reverse proxying using certficates and keys provided in
# either configuration data (preferred) or relation data. Assumes ports
# are not in use (calling charm should ensure that).
# Parameters: Variable number of proxy port mappings as
# $internal:$external.
# Returns: 0 if reverse proxy(s) have been configured, 0 if not.
##########################################################################
enable_https() {
local port_maps="$@"
local http_restart=""
juju-log "Enabling HTTPS for port mappings: $port_maps."
# allow overriding of keystone provided certs with those set manually
# in config.
local cert=$(config-get ssl_cert)
local key=$(config-get ssl_key)
local ca_cert=""
if [[ -z "$cert" ]] || [[ -z "$key" ]] ; then
juju-log "Inspecting identity-service relations for SSL certificate."
local r_id=""
cert=""
key=""
ca_cert=""
for r_id in $(relation-ids identity-service) ; do
for unit in $(relation-list -r $r_id) ; do
[[ -z "$cert" ]] && cert="$(relation-get -r $r_id ssl_cert $unit)"
[[ -z "$key" ]] && key="$(relation-get -r $r_id ssl_key $unit)"
[[ -z "$ca_cert" ]] && ca_cert="$(relation-get -r $r_id ca_cert $unit)"
done
done
[[ -n "$cert" ]] && cert=$(echo $cert | base64 -di)
[[ -n "$key" ]] && key=$(echo $key | base64 -di)
[[ -n "$ca_cert" ]] && ca_cert=$(echo $ca_cert | base64 -di)
else
juju-log "Using SSL certificate provided in service config."
fi
[[ -z "$cert" ]] || [[ -z "$key" ]] &&
juju-log "Expected but could not find SSL certificate data, not "\
"configuring HTTPS!" && return 1
apt-get -y install apache2
a2enmod ssl proxy proxy_http | grep -v "To activate the new configuration" &&
http_restart=1
mkdir -p /etc/apache2/ssl/$CHARM
echo "$cert" >/etc/apache2/ssl/$CHARM/cert
echo "$key" >/etc/apache2/ssl/$CHARM/key
if [[ -n "$ca_cert" ]] ; then
juju-log "Installing Keystone supplied CA cert."
echo "$ca_cert" >/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt
update-ca-certificates --fresh
# XXX TODO: Find a better way of exporting this?
if [[ "$CHARM" == "nova-cloud-controller" ]] ; then
[[ -e /var/www/keystone_juju_ca_cert.crt ]] &&
rm -rf /var/www/keystone_juju_ca_cert.crt
ln -s /usr/local/share/ca-certificates/keystone_juju_ca_cert.crt \
/var/www/keystone_juju_ca_cert.crt
fi
fi
for port_map in $port_maps ; do
local ext_port=$(echo $port_map | cut -d: -f1)
local int_port=$(echo $port_map | cut -d: -f2)
juju-log "Creating apache2 reverse proxy vhost for $port_map."
cat >/etc/apache2/sites-available/${CHARM}_${ext_port} <<END
Listen $ext_port
NameVirtualHost *:$ext_port
<VirtualHost *:$ext_port>
ServerName $(unit-get private-address)
SSLEngine on
SSLCertificateFile /etc/apache2/ssl/$CHARM/cert
SSLCertificateKeyFile /etc/apache2/ssl/$CHARM/key
ProxyPass / http://localhost:$int_port/
ProxyPassReverse / http://localhost:$int_port/
ProxyPreserveHost on
</VirtualHost>
<Proxy *>
Order deny,allow
Allow from all
</Proxy>
<Location />
Order allow,deny
Allow from all
</Location>
END
a2ensite ${CHARM}_${ext_port} | grep -v "To activate the new configuration" &&
http_restart=1
done
if [[ -n "$http_restart" ]] ; then
service apache2 restart
fi
}
##########################################################################
# Description: Ensure HTTPS reverse proxying is disabled for given port
# mappings.
# Parameters: Variable number of proxy port mappings as
# $internal:$external.
# Returns: 0 if reverse proxy is not active for all portmaps, 1 on error.
##########################################################################
disable_https() {
local port_maps="$@"
local http_restart=""
juju-log "Ensuring HTTPS disabled for $port_maps."
( [[ ! -d /etc/apache2 ]] || [[ ! -d /etc/apache2/ssl/$CHARM ]] ) && return 0
for port_map in $port_maps ; do
local ext_port=$(echo $port_map | cut -d: -f1)
local int_port=$(echo $port_map | cut -d: -f2)
if [[ -e /etc/apache2/sites-available/${CHARM}_${ext_port} ]] ; then
juju-log "Disabling HTTPS reverse proxy for $CHARM $port_map."
a2dissite ${CHARM}_${ext_port} | grep -v "To activate the new configuration" &&
http_restart=1
fi
done
if [[ -n "$http_restart" ]] ; then
service apache2 restart
fi
}
##########################################################################
# Description: Ensures HTTPS is either enabled or disabled for given port
# mapping.
# Parameters: Variable number of proxy port mappings as
# $internal:$external.
# Returns: 0 if HTTPS reverse proxy is in place, 1 if it is not.
##########################################################################
setup_https() {
# configure https via apache reverse proxying either
# using certs provided by config or keystone.
[[ -z "$CHARM" ]] &&
error_out "setup_https(): CHARM not set."
if ! https ; then
disable_https $@
else
enable_https $@
fi
}
##########################################################################
# Description: Determine correct API server listening port based on
# existence of HTTPS reverse proxy and/or haproxy.
# Paremeters: The standard public port for given service.
# Returns: The correct listening port for API service.
##########################################################################
determine_api_port() {
local public_port="$1"
local i=0
( [[ -n "$(peer_units)" ]] || is_clustered >/dev/null 2>&1 ) && i=$[$i + 1]
https >/dev/null 2>&1 && i=$[$i + 1]
echo $[$public_port - $[$i * 10]]
}
##########################################################################
# Description: Determine correct proxy listening port based on public IP +
# existence of HTTPS reverse proxy.
# Paremeters: The standard public port for given service.
# Returns: The correct listening port for haproxy service public address.
##########################################################################
determine_haproxy_port() {
local public_port="$1"
local i=0
https >/dev/null 2>&1 && i=$[$i + 1]
echo $[$public_port - $[$i * 10]]
}
##########################################################################
# Description: Print the value for a given config option in an OpenStack
# .ini style configuration file.
# Parameters: File path, option to retrieve, optional
# section name (default=DEFAULT)
# Returns: Prints value if set, prints nothing otherwise.
##########################################################################
local_config_get() {
# return config values set in openstack .ini config files.
# default placeholders starting (eg, %AUTH_HOST%) treated as
# unset values.
local file="$1"
local option="$2"
local section="$3"
[[ -z "$section" ]] && section="DEFAULT"
python -c "
import ConfigParser
config = ConfigParser.RawConfigParser()
config.read('$file')
try:
value = config.get('$section', '$option')
except:
print ''
exit(0)
if value.startswith('%'): exit(0)
print value
"
}
##########################################################################
# Description: Creates an rc file exporting environment variables to a
# script_path local to the charm's installed directory.
# 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
#
# Parameters:
# An array of '=' delimited ENV_VAR:value combinations to export.
# If optional script_path key is not provided in the array, script_path
# defaults to scripts/scriptrc
##########################################################################
function save_script_rc {
if [ ! -n "$JUJU_UNIT_NAME" ]; then
echo "Error: Missing JUJU_UNIT_NAME environment variable"
exit 1
fi
# our default unit_path
unit_path="$CHARM_DIR/scripts/scriptrc"
echo $unit_path
tmp_rc="/tmp/${JUJU_UNIT_NAME/\//-}rc"
echo "#!/bin/bash" > $tmp_rc
for env_var in "${@}"
do
if `echo $env_var | grep -q script_path`; then
# well then we need to reset the new unit-local script path
unit_path="$CHARM_DIR/${env_var/script_path=/}"
else
echo "export $env_var" >> $tmp_rc
fi
done
chmod 755 $tmp_rc
mv $tmp_rc $unit_path
}

View File

@ -1,309 +0,0 @@
#!/bin/bash -e
CHARM="nova-compute"
PACKAGES="nova-compute python-keystone genisoimage"
SERVICES="nova-compute"
CONF_DIR="/etc/nova"
NOVA_CONF=$(config-get nova-config)
API_CONF="/etc/nova/api-paste.ini"
QUANTUM_CONF="/etc/quantum/quantum.conf"
LIBVIRTD_CONF="/etc/libvirt/libvirtd.conf"
HOOKS_DIR="$CHARM_DIR/hooks"
MULTI_HOST=$(config-get multi-host)
if [ -f /etc/nova/nm.conf ]; then
NET_MANAGER=$(cat /etc/nova/nm.conf)
fi
case $NET_MANAGER in
"Quantum")
QUANTUM_PLUGIN=$(cat /etc/nova/quantum_plugin.conf)
case $QUANTUM_PLUGIN in
"ovs")
SERVICES="$SERVICES quantum-plugin-openvswitch-agent"
QUANTUM_PLUGIN_CONF="/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini"
;;
"nvp")
QUANTUM_PLUGIN_CONF="/etc/quantum/plugins/nicira/nvp.ini"
;;
*)
juju-log "Unrecognised plugin for quantum: $QUANTUM_PLUGIN" && exit 1
;;
esac
;;
"FlatManager"|"FlatDHCPManager")
if [[ "$MULTI_HOST" == "yes" ]] ; then
SERVICES="$SERVICES nova-api nova-network"
fi
;;
esac
if [[ -e $HOOKS_DIR/lib/nova/nova-common ]] ; then
. $HOOKS_DIR/lib/nova/nova-common
else
juju-log "$CHARM: Couldn't load $HOOKS_DIR/lib/nova-common" && exit 1
fi
determine_compute_package() {
# determines the appropriate nova-compute package to install
# for the configured virt-type.
local virt_type="$1"
local compute_pkg=""
case $virt_type in
"kvm") compute_pkg="nova-compute-kvm";;
"qemu") compute_pkg="nova-compute-qemu";;
"xen") compute_pkg="nova-compute-xen";;
"uml") compute_pkg="nova-compute-uml";;
"lxc") compute_pkg="nova-compute-lxc";;
*) error_out "ERROR: Unsupported virt_type=$virt_type";;
esac
echo "$compute_pkg"
}
function setup_bridge {
# XXX This is required by nova-network and will likely move somewhere else
# once we can split these services up into seperate formulas.
br=$1
ip=$2
netmask=$3
[[ -z $br ]] && br="br100"
[[ -z $ip ]] && ip="11.0.0.1"
[[ -z $netmask ]] && netmask="255.255.255.0"
apt-get -y install bridge-utils augeas-lenses augeas-tools
echo "Configuring bridge $br ($ip $netmask)"
context="/files/etc/network/interfaces"
augtool <<EOF
set $context/auto[child::1 = "$br"]/1 $br
set $context/iface[. = '$br'] $br
set $context/iface[. = '$br']/family inet
set $context/iface[. = '$br']/method static
set $context/iface[. = '$br']/address $ip
set $context/iface[. = '$br']/netmask $netmask
set $context/iface[. = '$br']/bridge_ports none
save
EOF
ifdown -a ; ifup -a
}
function configure_network_manager {
# needed by the nova-network bits
# to be expanded later to cover flatDhcp and VLAN
echo "$0: configuring $1 network manager"
local net_manager=$1
local quantum_plugin=$2
local network_bridge=$(config-get bridge-interface)
local private_address=$(get_ip `unit-get private-address`)
# Check to ensure we can actually resolve
# the local unit IP address
[[ -n $private_address ]] || {
juju-log "Unable to resolve local IP address"
exit 1
}
case $net_manager in
"FlatManager"|"FlatDHCPManager")
if [[ "$MULTI_HOST" == "yes" ]] ; then
apt-get -y install nova-api nova-network
SERVICES="$SERVICES nova-api nova-network"
fi
[[ -n $net_manager ]] && echo $net_manager > /etc/nova/nm.conf
;;&
"FlatManager")
local bridge_ip=$(config-get bridge-ip)
local bridge_netmask=$(config-get bridge-netmask)
setup_bridge $network_bridge $bridge_ip $bridge_netmask
set_or_update network_manager nova.network.manager.FlatManager
set_or_update flat_network_bridge $network_bridge
;;
"FlatDHCPManager")
local flat_interface=$(config-get flat-interface)
local ec2_host=$(relation-get ec2_host)
[[ -z $ec2_host ]] && juju-log "nova-compute: Missing ec2_host" \
&& exit 0
set_or_update network_manager nova.network.manager.FlatDHCPManager
# the interface on which bridge is built
set_or_update flat_interface $flat_interface
# address of API server to forward requests
set_or_update ec2_dmz_host $ec2_host
;;
"Quantum")
local keystone_host="$(relation-get keystone_host)"
local auth_port="$(relation-get auth_port)"
local quantum_url="$(relation-get quantum_url)"
local quantum_admin_tenant_name="$(relation-get service_tenant)"
local quantum_admin_username="$(relation-get service_username)"
local quantum_admin_password="$(relation-get service_password)"
local quantum_security_groups="$(relation-get quantum_security_groups)"
# might end up here before nova-c-c has processed keystone hooks
[[ -z "$keystone_host" ]] ||
[[ -z "$auth_port" ]] ||
[[ -z "$quantum_url" ]] ||
[[ -z "$quantum_admin_tenant_name" ]] ||
[[ -z "$quantum_admin_username" ]] ||
[[ -z "$quantum_admin_password" ]] &&
juju-log "nova-compute: Missing required data for Quantum config." &&
exit 0
local cur=$(get_os_codename_package "nova-common")
local vers=$(get_os_version_codename $cur)
[[ "$quantum_security_groups" == "yes" ]] &&
dpkg --compare-versions $vers lt '2013.1' &&
juju-log "Unable to use quantum security groups with < grizzly" &&
exit 1
set_or_update "network_api_class" "nova.network.quantumv2.api.API"
set_or_update "quantum_auth_strategy" "keystone"
set_or_update "quantum_url" "$quantum_url"
set_or_update "quantum_admin_tenant_name" "$quantum_admin_tenant_name"
set_or_update "quantum_admin_username" "$quantum_admin_username"
set_or_update "quantum_admin_password" "$quantum_admin_password"
set_or_update "quantum_admin_auth_url" \
"http://$keystone_host:$auth_port/v2.0"
if dpkg --compare-versions $vers gt '2012.2'; then
# Grizzly onwards supports metadata proxy so forcing use of config
# drive is not required.
set_or_update "force_config_drive" "False"
else
set_or_update "force_config_drive" "True"
fi
case $quantum_plugin in
"ovs")
apt-get -y install openvswitch-datapath-dkms
apt-get -y install quantum-plugin-openvswitch-agent
local quantum_plugin_conf="/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini"
set_or_update "core_plugin" "quantum.plugins.openvswitch.ovs_quantum_plugin.OVSQuantumPluginV2" "$QUANTUM_CONF"
if dpkg --compare-versions $vers gt '2012.2'; then
set_or_update "libvirt_vif_driver" "nova.virt.libvirt.vif.LibvirtGenericVIFDriver"
else
set_or_update "libvirt_vif_driver" "nova.virt.libvirt.vif.LibvirtHybridOVSBridgeDriver"
fi
set_or_update "libvirt_use_virtio_for_bridges" "True"
set_or_update "tenant_network_type" "gre" $quantum_plugin_conf "OVS"
set_or_update "enable_tunneling" "True" $quantum_plugin_conf "OVS"
set_or_update "tunnel_id_ranges" "1:1000" $quantum_plugin_conf "OVS"
set_or_update "local_ip" "$private_address" $quantum_plugin_conf "OVS"
if [ "$quantum_security_groups" == "yes" ]; then
set_or_update "security_group_api" "quantum"
set_or_update "firewall_driver" "nova.virt.firewall.NoopFirewallDriver"
set_or_update "firewall_driver" \
"quantum.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver" \
$quantum_plugin_conf "SECURITYGROUP"
fi
SERVICES="$SERVICES quantum-plugin-openvswitch-agent"
;;
esac
set_or_update "bind_host" "0.0.0.0" "$QUANTUM_CONF"
[[ -n $net_manager ]] && echo $net_manager > /etc/nova/nm.conf
[[ -n $quantum_plugin ]] && echo $quantum_plugin > /etc/nova/quantum_plugin.conf
;;
*) echo "ERROR: Invalid network manager $1" && exit 1 ;;
esac
}
BR_INT="br-int"
function configure_quantum_bridge {
if ! ovs-vsctl show | grep -q "Bridge $BR_INT"; then
ovs-vsctl add-br $BR_INT
fi
}
function initialize_ssh_keys {
# generate ssh keypair for root if one does not exist or
# the pari is not complete.
local pub="/root/.ssh/id_rsa"
local priv="/root/.ssh/id_rsa.pub"
if [[ -e $pub ]] &&
[[ -e $priv ]] ; then
juju-log "$CHARM: SSH credentials already exist for root."
return 0
fi
juju-log "$CHARM: Initializing new SSH key pair for live migration."
[[ -e $pub ]] && mv $pub $pub.$(date +"%s")
[[ -e $priv ]] && mv $priv $priv.$(date +"%s")
local keyname=$(echo $JUJU_UNIT_NAME | sed -e 's,/,-,g')
echo -e "\n" | ssh-keygen -C "$keyname" -N ""
}
function libvirt_tcp_listening {
# toggle libvirtd's tcp listening in both /etc/default/libvirt-bin
# and /etc/libvirt/libvirtd.conf.
local toggle="$1"
juju-log "$CHARM: Configuring libvirt tcp listening: $toggle."
local cur_opts=$(grep "^libvirtd_opts" /etc/default/libvirt-bin |
cut -d= -f2 | sed -e 's/\"//g')
local new_opts=""
if [[ "$toggle" == "on" ]] ; then
if [[ -z "$cur_opts" ]] ; then
echo "libvirtd_opts=\"-d -l\"" >>/etc/default/libvirt-bin
elif ! echo "$cur_opts" | grep -q "\-l" ; then
new_opts="$cur_opts -l"
sed -i "s|\(libvirtd_opts=\).*|\1\"$new_opts\"|" /etc/default/libvirt-bin
fi
set_or_update "listen_tcp" 1 $LIBVIRTD_CONF
elif [[ "$toggle" == "off" ]] ; then
if echo "$cur_opts" | grep -q "\-l" ; then
new_opts=$(echo $cur_opts | sed -e 's/\-l//g')
fi
set_or_update "listen_tcp" 0 $LIBVIRTD_CONF
fi
[[ -n "$new_opts" ]] &&
sed -i "s|\(libvirtd_opts=\).*|\1\"$new_opts\"|" /etc/default/libvirt-bin
return 0
}
function configure_migration {
local enable_migration=$(config-get enable-live-migration)
if [[ "$enable_migration" != "True" ]] &&
[[ "$enable_migraiton" != "true" ]] ; then
libvirt_tcp_listening "off"
return $?
fi
libvirt_tcp_listening "on"
case "$(config-get migration-auth-type)" in
"none"|"None")
set_or_update "listen_tls" 0 $LIBVIRTD_CONF
set_or_update "auth_tcp" "\"none\"" $LIBVIRTD_CONF
;;
"ssh")
set_or_update "listen_tls" 0 $LIBVIRTD_CONF
set_or_update "live_migration_uri" "qemu+ssh://%s/system" $NOVA_CONF
initialize_ssh_keys
# check in with nova-c-c and register our new key.
for id in $(relation-ids cloud-compute) ; do
compute_joined $id
done
service_ctl nova-compute restart ;;
"sasl") return 0 ;;
esac
}
function configure_libvirt {
cat > /etc/libvirt/qemu.conf << EOF
# File installed by Juju nova-compute charm
cgroup_device_acl = [
"/dev/null", "/dev/full", "/dev/zero",
"/dev/random", "/dev/urandom",
"/dev/ptmx", "/dev/kvm", "/dev/kqemu",
"/dev/rtc", "/dev/hpet", "/dev/net/tun",
]
EOF
configure_migration
service libvirt-bin restart
}
function migration_enabled {
local migration="$(config-get enable-live-migration)"
[[ "$migration" == "true" ]] || [[ "$migration" == "True" ]] && return 0
return 1
}

View File

@ -1,329 +0,0 @@
#!/bin/bash -e
HOOKS_DIR="$CHARM_DIR/hooks"
ARG0=${0##*/}
if [[ -e $HOOKS_DIR/nova-compute-common ]] ; then
. $HOOKS_DIR/nova-compute-common
else
juju-log "ERROR: Could not load nova-compute-common from $HOOKS_DIR"
fi
function install_hook {
[ -d exec.d ] && ( for f in exec.d/*/charm-pre-install; do [ -x $f ] && /bin/sh -c "$f";done )
local virt_type=$(config-get virt-type)
local compute_pkg=$(determine_compute_package "$virt_type")
apt-get -y install python-software-properties || exit 1
configure_install_source "$(config-get openstack-origin)"
apt-get update || exit 1
apt-get -y install $compute_pkg $PACKAGES || exit 1
service_ctl all stop
set_or_update "auth_strategy" "keystone"
configure_libvirt
}
function upgrade_hook {
[ -d exec.d ] && ( for f in exec.d/*/charm-pre-install; do [ -x $f ] && /bin/sh -c "$f";done )
}
function config_changed() {
# Determine whether or not we should do an upgrade, based on whether or not
# the version offered in openstack-origin is greater than what is installed.
local install_src=$(config-get openstack-origin)
local cur=$(get_os_codename_package "nova-common")
local available=$(get_os_codename_install_source "$install_src")
if dpkg --compare-versions $(get_os_version_codename "$cur") lt \
$(get_os_version_codename "$available") ; then
juju-log "$CHARM: Upgrading OpenStack release: $cur -> $available."
do_openstack_upgrade "$install_src" $PACKAGES
fi
# set this here until its fixed in grizzly packaging. (adam_g)
[[ "$cur" == "grizzly" ]] &&
set_or_update "compute_driver" "libvirt.LibvirtDriver"
configure_libvirt
set_config_flags
service_ctl all restart
}
function amqp_joined {
# we request a username on the rabbit queue
# and store it in nova.conf. our response is its IP + PASSWD
# but we configure that in _changed
local rabbit_user=$(config-get rabbit-user)
local rabbit_vhost=$(config-get rabbit-vhost)
juju-log "$CHARM - amqp_joined: requesting credentials for $rabbit_user"
relation-set username=$rabbit_user
relation-set vhost=$rabbit_vhost
}
function amqp_changed {
# server creates our credentials and tells us where
# to connect. for now, using default vhost '/'
local rabbit_host=$(relation-get private-address)
local rabbit_password=$(relation-get password)
if [[ -z $rabbit_host ]] || \
[[ -z $rabbit_password ]] ; then
juju-log "$CHARM - amqp_changed: rabbit_host||rabbit_password not set."
exit 0
fi
# if the rabbitmq service is clustered among nodes with hacluster,
# point to its vip instead of its private-address.
local clustered=$(relation-get clustered)
if [[ -n "$clustered" ]] ; then
juju-log "$CHARM - ampq_changed: Configuring for "\
"access to haclustered rabbitmq service."
local vip=$(relation-get vip)
[[ -z "$vip" ]] && juju-log "$CHARM - amqp_changed: Clustered but no vip."\
&& exit 0
rabbit_host="$vip"
fi
local rabbit_user=$(config-get rabbit-user)
local rabbit_vhost=$(config-get rabbit-vhost)
juju-log "$CHARM - amqp_changed: Setting rabbit config in nova.conf: " \
"$rabbit_user@$rabbit_host/$rabbit_vhost"
set_or_update rabbit_host $rabbit_host
set_or_update rabbit_userid $rabbit_user
set_or_update rabbit_password $rabbit_password
set_or_update rabbit_virtual_host $rabbit_vhost
if [ "$NET_MANAGER" == "Quantum" ]; then
set_or_update rabbit_host "$rabbit_host" "$QUANTUM_CONF"
set_or_update rabbit_userid "$rabbit_user" "$QUANTUM_CONF"
set_or_update rabbit_password "$rabbit_password" "$QUANTUM_CONF"
set_or_update rabbit_virtual_host "$rabbit_vhost" "$QUANTUM_CONF"
fi
service_ctl all restart
}
function db_joined {
# tell mysql provider which database we want. it will create it and give us
# credentials
local nova_db=$(config-get nova-db)
local db_user=$(config-get db-user)
local hostname=$(unit-get private-address)
juju-log "$CHARM - db_joined: requesting database access to $nova_db for "\
"$db_user@$hostname"
relation-set nova_database=$nova_db nova_username=$db_user nova_hostname=$hostname
if [ "$NET_MANAGER" == "Quantum" ]; then
relation-set quantum_database=quantum quantum_username=quantum quantum_hostname=$hostname
fi
}
function db_changed {
local db_host=`relation-get db_host`
local db_password=`relation-get nova_password`
if [[ -z $db_host ]] || [[ -z $db_password ]] ; then
juju-log "$CHARM - db_changed: db_host||db_password set, will retry."
exit 0
fi
local nova_db=$(config-get nova-db)
local db_user=$(config-get db-user)
juju-log "$CHARM - db_changed: Configuring nova.conf for access to $nova_db"
set_or_update sql_connection "mysql://$db_user:$db_password@$db_host/$nova_db"
if [ "$NET_MANAGER" == "Quantum" ]; then
local quantum_db_password=`relation-get quantum_password`
set_or_update sql_connection "mysql://quantum:$quantum_db_password@$db_host/quantum?charset=utf8" \
$QUANTUM_PLUGIN_CONF "DATABASE"
fi
service_ctl all restart
}
function image-service_changed {
local api_server=`relation-get glance-api-server`
if [[ -z $api_server ]] ; then
echo "image-service_changed: api_server not yet set. Exit 0 and retry"
exit 0
fi
if [[ "$(get_os_codename_package nova-common)" == "essex" ]] ; then
# essex needs glance_api_servers urls stripped of protocol.
api_server="$(echo $api_server | awk '{gsub(/http:\/\/|https:\/\//,"")}1')"
fi
set_or_update glance_api_servers $api_server
service_ctl all restart
}
function compute_joined {
migration_enabled || return 0
local relid="$1"
[[ -n "$relid" ]] && relid="-r $relid"
migration_auth="$(config-get migration-auth-type)"
case "$migration_auth" in
"none"|"None") return 0 ;;
"ssh") relation-set $relid ssh_public_key="$(cat /root/.ssh/id_rsa.pub)" ;;
esac
relation-set $relid migration_auth_type="$migration_auth"
}
function compute_changed {
# nova-c-c will inform us of the configured network manager. nova-compute
# needs to configure itself accordingly.
network_manager=`relation-get network_manager`
if [[ -n "$network_manager" ]] ; then
if [ "$network_manager" == "Quantum" ]; then
configure_network_manager "$network_manager" "$(relation-get quantum_plugin)"
configure_quantum_bridge
# Quantum also needs access to the quantum database
# depending on add-relation order, this relation
# may already be present so ask it for credentials if so
r_ids="$(relation-ids shared-db)"
for id in $r_ids ; do
relation-set -r $id \
quantum_database=quantum \
quantum_username=quantum \
quantum_hostname=$(unit-get private-address)
done
# Rabbit MQ relation may also already be in place
# shared vhost with nova so just grab settings and
# configure. need to be sure to use VIP if clustered.
local rabbit_clustered="" rabbit_vip="" rabbit_host="" rabbit_password=""
r_ids="$(relation-ids amqp)"
for id in $r_ids ; do
for unit in $(relation-list -r $id) ; do
[[ -z "$rabbit_clustered" ]] &&
rabbit_clustered=$(relation-get -r $id clustered $unit)
[[ -z "$rabbit_vip" ]] && rabbit_vip=$(relation-get -r $id vip $unit)
[[ -z "$rabbit_password" ]] &&
rabbit_password=$(relation-get -r $id password $unit)
rabbit_host=$(relation-get -r $id private-address $unit)
done
done
if [[ -n "$rabbit_clustered" ]] ; then
rabbit_host="$rabbit_vip"
fi
if [[ -n $rabbit_host ]] && \
[[ -n $rabbit_password ]]; then
set_or_update rabbit_host "$rabbit_host" "$QUANTUM_CONF"
set_or_update rabbit_userid "$(config-get rabbit-user)" "$QUANTUM_CONF"
set_or_update rabbit_password "$rabbit_password" "$QUANTUM_CONF"
set_or_update rabbit_virtual_host "$(config-get rabbit-vhost)" "$QUANTUM_CONF"
fi
else
configure_network_manager "$network_manager"
fi
fi
# nova-c-c informs us of what volume service has been deployed.
volume_service=`relation-get volume_service`
[[ -n "$volume_service" ]] && configure_volume_service "$volume_service"
if migration_enabled ; then
case "$(config-get migration-auth-type)" in
"ssh")
local known_hosts="$(relation-get known_hosts)"
local authorized_keys="$(relation-get authorized_keys)"
if [[ -n "$known_hosts" ]] &&
[[ -n "$authorized_keys" ]] ; then
juju-log "$CHARM: Saving new known_hosts+authorized_keys file."
echo "$known_hosts" | base64 -di >/root/.ssh/known_hosts
echo "$authorized_keys" | base64 -di >/root/.ssh/authorized_keys
fi
;;
esac
fi
# If Keytone is configured manage SSL certs, nova-compute needs a copy
# of its CA installed.
local ca_cert="$(relation-get ca_cert)"
if [[ -n "$ca_cert" ]] ; then
juju-log "Installing Keystone CA certificate."
ca_cert="$(echo $ca_cert | base64 -di)"
echo "$ca_cert" >/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt
update-ca-certificates
fi
# restart on all changed events. nova-c-c may send out a uuid to trigger
# remote restarts of services here (after db migrations, for instance)
service_ctl all restart
}
function ceph_joined {
mkdir -p /etc/ceph
apt-get -y install ceph-common || exit 1
}
function ceph_changed {
SERVICE_NAME=`echo $JUJU_UNIT_NAME | cut -d / -f 1`
KEYRING=/etc/ceph/ceph.client.$SERVICE_NAME.keyring
KEY=`relation-get key`
if [ -n "$KEY" ]; then
# But only once
if [ ! -f $KEYRING ]; then
ceph-authtool $KEYRING \
--create-keyring --name=client.$SERVICE_NAME \
--add-key="$KEY"
chmod +r $KEYRING
fi
else
# No key - bail for the time being
exit 0
fi
MONS=`relation-list`
mon_hosts=""
for mon in $MONS; do
mon_hosts="$mon_hosts $(get_ip $(relation-get private-address $mon)):6789"
done
cat > /etc/ceph/ceph.conf << EOF
[global]
auth supported = $(relation-get auth)
keyring = /etc/ceph/\$cluster.\$name.keyring
mon host = $mon_hosts
EOF
if [ ! -f /etc/ceph/secret.xml ]; then
# This is just a label and it must be consistent across
# nova-compute nodes to support live migration.
UUID="514c9fca-8cbe-11e2-9c52-3bc8c7819472"
cat > /etc/ceph/secret.xml << EOF
<secret ephemeral='no' private='no'>
<uuid>$UUID</uuid>
<usage type='ceph'>
<name>client.$SERVICE_NAME secret</name>
</usage>
</secret>
EOF
# Create secret for libvirt usage
# note that this does limit ceph usage to
# KVM only at this point in time.
virsh secret-define --file /etc/ceph/secret.xml
virsh secret-set-value --secret $UUID --base64 $KEY
set_or_update rbd_user $SERVICE_NAME
set_or_update rbd_secret_uuid $UUID
set_or_update rbd_pool nova
service_ctl all restart
fi
}
case $ARG0 in
"install") install_hook ;;
"upgrade-charm") upgrade_hook ;;
"start"|"stop") exit 0 ;;
"config-changed") config_changed ;;
"amqp-relation-joined") amqp_joined ;;
"amqp-relation-changed") amqp_changed ;;
"shared-db-relation-joined") db_joined ;;
"shared-db-relation-changed") db_changed ;;
"image-service-relation-joined") exit 0 ;;
"image-service-relation-changed") image-service_changed ;;
"identity-service-relation-joined") keystone_joined ;;
"identity-service-relation-changed") exit 0 ;;
"ceph-relation-joined") ceph_joined;;
"ceph-relation-changed") ceph_changed;;
"cloud-compute-relation-joined" ) compute_joined ;;
"cloud-compute-relation-changed") compute_changed ;;
esac

View File

@ -0,0 +1,326 @@
from charmhelpers.contrib.openstack import context
from charmhelpers.core.host import service_running, service_start
from charmhelpers.fetch import apt_install, filter_installed_packages
from charmhelpers.core.hookenv import (
config,
log,
relation_get,
relation_ids,
related_units,
service_name,
unit_get,
ERROR,
)
from charmhelpers.contrib.openstack.utils import get_host_ip, os_release
from charmhelpers.contrib.network.ovs import add_bridge
# This is just a label and it must be consistent across
# nova-compute nodes to support live migration.
CEPH_SECRET_UUID = '514c9fca-8cbe-11e2-9c52-3bc8c7819472'
OVS_BRIDGE = 'br-int'
def _save_flag_file(path, data):
'''
Saves local state about plugin or manager to specified file.
'''
# Wonder if we can move away from this now?
if data is None:
return
with open(path, 'wb') as out:
out.write(data)
# compatability functions to help with quantum -> neutron transition
def _network_manager():
from nova_compute_utils import network_manager as manager
return manager()
def _neutron_security_groups():
'''
Inspects current cloud-compute relation and determine if nova-c-c has
instructed us to use neutron security groups.
'''
for rid in relation_ids('cloud-compute'):
for unit in related_units(rid):
groups = [
relation_get('neutron_security_groups',
rid=rid, unit=unit),
relation_get('quantum_security_groups',
rid=rid, unit=unit)
]
if ('yes' in groups or 'Yes' in groups):
return True
return False
def _neutron_plugin():
from nova_compute_utils import neutron_plugin
return neutron_plugin()
def _neutron_url(rid, unit):
# supports legacy relation settings.
return (relation_get('neutron_url', rid=rid, unit=unit) or
relation_get('quantum_url', rid=rid, unit=unit))
class NovaComputeLibvirtContext(context.OSContextGenerator):
'''
Determines various libvirt and nova options depending on live migration
configuration.
'''
interfaces = []
def __call__(self):
# distro defaults
ctxt = {
# /etc/default/libvirt-bin
'libvirtd_opts': '-d',
# /etc/libvirt/libvirtd.conf (
'listen_tls': 0,
}
# enable tcp listening if configured for live migration.
if config('enable-live-migration'):
ctxt['libvirtd_opts'] += ' -l'
if config('migration-auth-type') in ['none', 'None', 'ssh']:
ctxt['listen_tls'] = 0
if config('migration-auth-type') == 'ssh':
# nova.conf
ctxt['live_migration_uri'] = 'qemu+ssh://%s/system'
return ctxt
class NovaComputeVirtContext(context.OSContextGenerator):
interfaces = []
def __call__(self):
return {}
class NovaComputeCephContext(context.CephContext):
def __call__(self):
ctxt = super(NovaComputeCephContext, self).__call__()
if not ctxt:
return {}
svc = service_name()
# secret.xml
ctxt['ceph_secret_uuid'] = CEPH_SECRET_UUID
# nova.conf
ctxt['service_name'] = svc
ctxt['rbd_user'] = svc
ctxt['rbd_secret_uuid'] = CEPH_SECRET_UUID
ctxt['rbd_pool'] = 'nova'
return ctxt
class CloudComputeContext(context.OSContextGenerator):
'''
Generates main context for writing nova.conf and quantum.conf templates
from a cloud-compute relation changed hook. Mainly used for determinig
correct network and volume service configuration on the compute node,
as advertised by the cloud-controller.
Note: individual quantum plugin contexts are handled elsewhere.
'''
interfaces = ['cloud-compute']
def _ensure_packages(self, packages):
'''Install but do not upgrade required packages'''
required = filter_installed_packages(packages)
if required:
apt_install(required, fatal=True)
@property
def network_manager(self):
return _network_manager()
@property
def volume_service(self):
volume_service = None
for rid in relation_ids('cloud-compute'):
for unit in related_units(rid):
volume_service = relation_get('volume_service',
rid=rid, unit=unit)
return volume_service
def flat_dhcp_context(self):
ec2_host = None
for rid in relation_ids('cloud-compute'):
for unit in related_units(rid):
ec2_host = relation_get('ec2_host', rid=rid, unit=unit)
if not ec2_host:
return {}
if config('multi-host').lower() == 'yes':
self._ensure_packages(['nova-api', 'nova-network'])
return {
'flat_interface': config('flat-interface'),
'ec2_dmz_host': ec2_host,
}
def neutron_context(self):
# generate config context for neutron or quantum. these get converted
# directly into flags in nova.conf
# NOTE: Its up to release templates to set correct driver
def _legacy_quantum(ctxt):
# rename neutron flags to support legacy quantum.
renamed = {}
for k, v in ctxt.iteritems():
k = k.replace('neutron', 'quantum')
renamed[k] = v
return renamed
neutron_ctxt = {'neutron_url': None}
for rid in relation_ids('cloud-compute'):
for unit in related_units(rid):
rel = {'rid': rid, 'unit': unit}
url = _neutron_url(**rel)
if not url:
# only bother with units that have a neutron url set.
continue
neutron_ctxt = {
'neutron_auth_strategy': 'keystone',
'keystone_host': relation_get(
'auth_host', **rel),
'auth_port': relation_get(
'auth_port', **rel),
'neutron_admin_tenant_name': relation_get(
'service_tenant_name', **rel),
'neutron_admin_username': relation_get(
'service_username', **rel),
'neutron_admin_password': relation_get(
'service_password', **rel),
'neutron_plugin': _neutron_plugin(),
'neutron_url': url,
}
missing = [k for k, v in neutron_ctxt.iteritems() if v in ['', None]]
if missing:
log('Missing required relation settings for Quantum: ' +
' '.join(missing))
return {}
neutron_ctxt['neutron_security_groups'] = _neutron_security_groups()
ks_url = 'http://%s:%s/v2.0' % (neutron_ctxt['keystone_host'],
neutron_ctxt['auth_port'])
neutron_ctxt['neutron_admin_auth_url'] = ks_url
if self.network_manager == 'quantum':
return _legacy_quantum(neutron_ctxt)
return neutron_ctxt
def volume_context(self):
# provide basic validation that the volume manager is supported on the
# given openstack release (nova-volume is only supported for E and F)
# it is up to release templates to set the correct volume driver.
if not self.volume_service:
return {}
os_rel = os_release('nova-common')
# ensure volume service is supported on specific openstack release.
if self.volume_service == 'cinder':
if os_rel == 'essex':
e = ('Attempting to configure cinder volume manager on '
'an unsupported OpenStack release (essex)')
log(e, level=ERROR)
raise context.OSContextError(e)
return 'cinder'
elif self.volume_service == 'nova-volume':
if os_release('nova-common') not in ['essex', 'folsom']:
e = ('Attempting to configure nova-volume manager on '
'an unsupported OpenStack release (%s).' % os_rel)
log(e, level=ERROR)
raise context.OSContextError(e)
return 'nova-volume'
else:
e = ('Invalid volume service received via cloud-compute: %s' %
self.volume_service)
log(e, level=ERROR)
raise context.OSContextError(e)
def network_manager_context(self):
ctxt = {}
if self.network_manager == 'flatdhcpmanager':
ctxt = self.flat_dhcp_context()
elif self.network_manager in ['neutron', 'quantum']:
ctxt = self.neutron_context()
_save_flag_file(path='/etc/nova/nm.conf', data=self.network_manager)
log('Generated config context for %s network manager.' %
self.network_manager)
return ctxt
def __call__(self):
rids = relation_ids('cloud-compute')
if not rids:
return {}
ctxt = {}
net_manager = self.network_manager_context()
if net_manager:
ctxt['network_manager'] = self.network_manager
ctxt['network_manager_config'] = net_manager
vol_service = self.volume_context()
if vol_service:
ctxt['volume_service'] = vol_service
return ctxt
class NeutronComputeContext(context.NeutronContext):
interfaces = []
@property
def plugin(self):
return _neutron_plugin()
@property
def network_manager(self):
return _network_manager()
@property
def neutron_security_groups(self):
return _neutron_security_groups()
def _ensure_bridge(self):
if not service_running('openvswitch-switch'):
service_start('openvswitch-switch')
add_bridge(OVS_BRIDGE)
def ovs_ctxt(self):
# In addition to generating config context, ensure the OVS service
# is running and the OVS bridge exists. Also need to ensure
# local_ip points to actual IP, not hostname.
ovs_ctxt = super(NeutronComputeContext, self).ovs_ctxt()
if not ovs_ctxt:
return {}
self._ensure_bridge()
ovs_ctxt['local_ip'] = get_host_ip(unit_get('private-address'))
return ovs_ctxt

210
hooks/nova_compute_hooks.py Executable file
View File

@ -0,0 +1,210 @@
#!/usr/bin/python
import sys
from charmhelpers.core.hookenv import (
Hooks,
config,
log,
relation_ids,
relation_get,
relation_set,
service_name,
unit_get,
UnregisteredHookError,
)
from charmhelpers.core.host import (
restart_on_change,
)
from charmhelpers.fetch import (
apt_install,
apt_update,
filter_installed_packages,
)
from charmhelpers.contrib.openstack.utils import (
configure_installation_source,
openstack_upgrade_available,
)
from charmhelpers.contrib.storage.linux.ceph import ensure_ceph_keyring
from charmhelpers.contrib.openstack.neutron import neutron_plugin_attribute
from charmhelpers.payload.execd import execd_preinstall
from nova_compute_utils import (
create_libvirt_secret,
determine_packages,
import_authorized_keys,
import_keystone_ca_cert,
initialize_ssh_keys,
migration_enabled,
network_manager,
neutron_plugin,
do_openstack_upgrade,
public_ssh_key,
restart_map,
register_configs,
NOVA_CONF,
QUANTUM_CONF, NEUTRON_CONF,
CEPH_CONF, CEPH_SECRET
)
from nova_compute_context import CEPH_SECRET_UUID
hooks = Hooks()
CONFIGS = register_configs()
@hooks.hook()
def install():
execd_preinstall()
configure_installation_source(config('openstack-origin'))
apt_update()
apt_install(determine_packages(), fatal=True)
@hooks.hook('config-changed')
@restart_on_change(restart_map())
def config_changed():
if openstack_upgrade_available('nova-common'):
do_openstack_upgrade(CONFIGS)
if migration_enabled() and config('migration-auth-type') == 'ssh':
# Check-in with nova-c-c and register new ssh key, if it has just been
# generated.
initialize_ssh_keys()
[compute_joined(rid) for rid in relation_ids('cloud-compute')]
CONFIGS.write_all()
@hooks.hook('amqp-relation-joined')
@restart_on_change(restart_map())
def amqp_joined():
relation_set(username=config('rabbit-user'), vhost=config('rabbit-vhost'))
@hooks.hook('amqp-relation-changed')
@restart_on_change(restart_map())
def amqp_changed():
if 'amqp' not in CONFIGS.complete_contexts():
log('amqp relation incomplete. Peer not ready?')
return
CONFIGS.write(NOVA_CONF)
if network_manager() == 'quantum':
CONFIGS.write(QUANTUM_CONF)
if network_manager() == 'neutron':
CONFIGS.write(NEUTRON_CONF)
@hooks.hook('shared-db-relation-joined')
def db_joined(rid=None):
relation_set(relation_id=rid,
nova_database=config('database'),
nova_username=config('database-user'),
nova_hostname=unit_get('private-address'))
if network_manager() in ['quantum', 'neutron']:
# XXX: Renaming relations from quantum_* to neutron_* here.
relation_set(relation_id=rid,
neutron_database=config('neutron-database'),
neutron_username=config('neutron-database-user'),
neutron_hostname=unit_get('private-address'))
@hooks.hook('shared-db-relation-changed')
@restart_on_change(restart_map())
def db_changed():
if 'shared-db' not in CONFIGS.complete_contexts():
log('shared-db relation incomplete. Peer not ready?')
return
CONFIGS.write(NOVA_CONF)
nm = network_manager()
if nm in ['quantum', 'neutron']:
plugin = neutron_plugin()
CONFIGS.write(neutron_plugin_attribute(plugin, 'config', nm))
@hooks.hook('image-service-relation-changed')
@restart_on_change(restart_map())
def image_service_changed():
if 'image-service' not in CONFIGS.complete_contexts():
log('image-service relation incomplete. Peer not ready?')
return
CONFIGS.write(NOVA_CONF)
@hooks.hook('cloud-compute-relation-joined')
def compute_joined(rid=None):
if not migration_enabled():
return
auth_type = config('migration-auth-type')
settings = {
'migration_auth_type': auth_type
}
if auth_type == 'ssh':
settings['ssh_public_key'] = public_ssh_key()
relation_set(relation_id=rid, **settings)
@hooks.hook('cloud-compute-relation-changed')
@restart_on_change(restart_map())
def compute_changed():
# rewriting all configs to pick up possible net or vol manager
# config advertised from controller.
CONFIGS.write_all()
import_authorized_keys()
import_keystone_ca_cert()
if network_manager() in ['quantum', 'neutron']:
# in case we already have a database relation, need to request
# access to the additional neutron database.
[db_joined(rid) for rid in relation_ids('shared-db')]
@hooks.hook('ceph-relation-joined')
@restart_on_change(restart_map())
def ceph_joined():
apt_install(filter_installed_packages(['ceph-common']), fatal=True)
@hooks.hook('ceph-relation-changed')
@restart_on_change(restart_map())
def ceph_changed():
if 'ceph' not in CONFIGS.complete_contexts():
log('ceph relation incomplete. Peer not ready?')
return
svc = service_name()
if not ensure_ceph_keyring(service=svc):
log('Could not create ceph keyring: peer not ready?')
return
CONFIGS.write(CEPH_CONF)
CONFIGS.write(CEPH_SECRET)
CONFIGS.write(NOVA_CONF)
# With some refactoring, this can move into NovaComputeCephContext
# and allow easily extended to support other compute flavors.
if config('virt-type') in ['kvm', 'qemu', 'lxc']:
create_libvirt_secret(secret_file=CEPH_SECRET,
secret_uuid=CEPH_SECRET_UUID,
key=relation_get('key'))
@hooks.hook('amqp-relation-broken',
'ceph-relation-broken',
'image-service-relation-broken',
'shared-db-relation-broken')
@restart_on_change(restart_map())
def relation_broken():
CONFIGS.write_all()
def main():
try:
hooks.execute(sys.argv)
except UnregisteredHookError as e:
log('Unknown hook {} - skipping.'.format(e))
if __name__ == '__main__':
main()

376
hooks/nova_compute_utils.py Normal file
View File

@ -0,0 +1,376 @@
import os
import pwd
from base64 import b64decode
from copy import deepcopy
from subprocess import check_call, check_output
from charmhelpers.fetch import apt_update, apt_install
from charmhelpers.core.hookenv import (
config,
log,
related_units,
relation_ids,
relation_get,
DEBUG,
)
from charmhelpers.contrib.openstack.neutron import neutron_plugin_attribute
from charmhelpers.contrib.openstack import templating, context
from charmhelpers.contrib.openstack.utils import (
configure_installation_source,
get_os_codename_install_source,
os_release
)
from nova_compute_context import (
CloudComputeContext,
NovaComputeLibvirtContext,
NovaComputeCephContext,
NeutronComputeContext,
)
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
TEMPLATES = 'templates/'
BASE_PACKAGES = [
'nova-compute',
'genisoimage', # was missing as a package dependency until raring.
]
QEMU_CONF = '/etc/libvirt/qemu.conf'
LIBVIRTD_CONF = '/etc/libvirt/libvirtd.conf'
LIBVIRT_BIN = '/etc/default/libvirt-bin'
NOVA_CONF = '/etc/nova/nova.conf'
BASE_RESOURCE_MAP = {
QEMU_CONF: {
'services': ['libvirt-bin'],
'contexts': [],
},
LIBVIRTD_CONF: {
'services': ['libvirt-bin'],
'contexts': [NovaComputeLibvirtContext()],
},
LIBVIRT_BIN: {
'services': ['libvirt-bin'],
'contexts': [NovaComputeLibvirtContext()],
},
NOVA_CONF: {
'services': ['nova-compute'],
'contexts': [context.AMQPContext(),
context.SharedDBContext(relation_prefix='nova'),
context.ImageServiceContext(),
context.OSConfigFlagContext(),
CloudComputeContext(),
NovaComputeLibvirtContext(),
NovaComputeCephContext()],
},
}
CEPH_CONF = '/etc/ceph/ceph.conf'
CEPH_SECRET = '/etc/ceph/secret.xml'
CEPH_RESOURCES = {
CEPH_CONF: {
'contexts': [NovaComputeCephContext()],
'services': [],
},
CEPH_SECRET: {
'contexts': [NovaComputeCephContext()],
'services': [],
}
}
QUANTUM_CONF = '/etc/quantum/quantum.conf'
QUANTUM_RESOURCES = {
QUANTUM_CONF: {
'services': [],
'contexts': [context.AMQPContext(), NeutronComputeContext()],
}
}
NEUTRON_CONF = '/etc/neutron/neutron.conf'
NEUTRON_RESOURCES = {
NEUTRON_CONF: {
'services': [],
'contexts': [context.AMQPContext(), NeutronComputeContext()],
}
}
# Maps virt-type config to a compute package(s).
VIRT_TYPES = {
'kvm': ['nova-compute-kvm'],
'qemu': ['nova-compute-qemu'],
'xen': ['nova-compute-xen'],
'uml': ['nova-compute-uml'],
'lxc': ['nova-compute-lxc'],
}
def resource_map():
'''
Dynamically generate a map of resources that will be managed for a single
hook execution.
'''
# TODO: Cache this on first call?
resource_map = deepcopy(BASE_RESOURCE_MAP)
net_manager = network_manager()
# Network manager gets set late by the cloud-compute interface.
# FlatDHCPManager only requires some extra packages.
if (net_manager in ['flatmanager', 'flatdhcpmanager'] and
config('multi-host').lower() == 'yes'):
resource_map[NOVA_CONF]['services'].extend(
['nova-api', 'nova-network']
)
# Neutron/quantum requires additional contexts, as well as new resources
# depending on the plugin used.
if net_manager in ['neutron', 'quantum']:
if net_manager == 'quantum':
nm_rsc = QUANTUM_RESOURCES
if net_manager == 'neutron':
nm_rsc = NEUTRON_RESOURCES
resource_map.update(nm_rsc)
resource_map[NOVA_CONF]['contexts'].append(NeutronComputeContext())
plugin = neutron_plugin()
if plugin:
conf = neutron_plugin_attribute(plugin, 'config', net_manager)
svcs = neutron_plugin_attribute(plugin, 'services', net_manager)
ctxts = (neutron_plugin_attribute(plugin, 'contexts', net_manager)
or [])
resource_map[conf] = {}
resource_map[conf]['services'] = svcs
resource_map[conf]['contexts'] = ctxts
resource_map[conf]['contexts'].append(NeutronComputeContext())
# associate the plugin agent with main network manager config(s)
[resource_map[nmc]['services'].extend(svcs) for nmc in nm_rsc]
if relation_ids('ceph'):
resource_map.update(CEPH_RESOURCES)
return resource_map
def restart_map():
'''
Constructs a restart map based on charm config settings and relation
state.
'''
return {k: v['services'] for k, v in resource_map().iteritems()}
def register_configs():
'''
Returns an OSTemplateRenderer object with all required configs registered.
'''
release = os_release('nova-common')
configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
openstack_release=release)
for cfg, d in resource_map().iteritems():
configs.register(cfg, d['contexts'])
return configs
def determine_packages():
packages = [] + BASE_PACKAGES
net_manager = network_manager()
if (net_manager in ['flatmanager', 'flatdhcpmanager'] and
config('multi-host').lower() == 'yes'):
packages.extend(['nova-api', 'nova-network'])
elif net_manager == 'quantum':
plugin = neutron_plugin()
packages.extend(
neutron_plugin_attribute(plugin, 'packages', net_manager))
if relation_ids('ceph'):
packages.append('ceph-common')
virt_type = config('virt-type')
try:
packages.extend(VIRT_TYPES[virt_type])
except KeyError:
log('Unsupported virt-type configured: %s' % virt_type)
raise
return packages
def migration_enabled():
# XXX: confirm juju-core bool behavior is the same.
return config('enable-live-migration')
def quantum_enabled():
manager = config('network-manager')
if not manager:
return False
return manager.lower() == 'quantum'
def _network_config():
'''
Obtain all relevant network configuration settings from nova-c-c via
cloud-compute interface.
'''
settings = ['network_manager', 'neutron_plugin', 'quantum_plugin']
net_config = {}
for rid in relation_ids('cloud-compute'):
for unit in related_units(rid):
for setting in settings:
value = relation_get(setting, rid=rid, unit=unit)
if value:
net_config[setting] = value
return net_config
def neutron_plugin():
return (_network_config().get('neutron_plugin') or
_network_config().get('quantum_plugin'))
def network_manager():
'''
Obtain the network manager advertised by nova-c-c, renaming to Quantum
if required
'''
manager = _network_config().get('network_manager')
if manager:
manager = manager.lower()
if manager not in ['quantum', 'neutron']:
return manager
if os_release('nova-common') in ['folsom', 'grizzly']:
return 'quantum'
else:
return 'neutron'
return manager
def public_ssh_key(user='root'):
home = pwd.getpwnam(user).pw_dir
try:
with open(os.path.join(home, '.ssh', 'id_rsa.pub')) as key:
return key.read().strip()
except:
return None
def initialize_ssh_keys(user='root'):
home_dir = pwd.getpwnam(user).pw_dir
ssh_dir = os.path.join(home_dir, '.ssh')
if not os.path.isdir(ssh_dir):
os.mkdir(ssh_dir)
priv_key = os.path.join(ssh_dir, 'id_rsa')
if not os.path.isfile(priv_key):
log('Generating new ssh key for user %s.' % user)
cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048',
'-f', priv_key]
check_output(cmd)
pub_key = '%s.pub' % priv_key
if not os.path.isfile(pub_key):
log('Generating missing ssh public key @ %s.' % pub_key)
cmd = ['ssh-keygen', '-y', '-f', priv_key]
p = check_output(cmd).strip()
with open(pub_key, 'wb') as out:
out.write(p)
check_output(['chown', '-R', user, ssh_dir])
def import_authorized_keys(user='root'):
"""Import SSH authorized_keys + known_hosts from a cloud-compute relation
and store in user's $HOME/.ssh.
"""
# XXX: Should this be managed via templates + contexts?
hosts = relation_get('known_hosts')
auth_keys = relation_get('authorized_keys')
# XXX: Need to fix charm-helpers to return None for empty settings,
# in all cases.
if not hosts or not auth_keys:
return
dest = os.path.join(pwd.getpwnam(user).pw_dir, '.ssh')
log('Saving new known_hosts and authorized_keys file to: %s.' % dest)
with open(os.path.join(dest, 'authorized_keys'), 'wb') as _keys:
_keys.write(b64decode(auth_keys))
with open(os.path.join(dest, 'known_hosts'), 'wb') as _hosts:
_hosts.write(b64decode(hosts))
def configure_live_migration(configs=None):
"""
Ensure libvirt live migration is properly configured or disabled,
depending on current config setting.
"""
# dont think we need this
return
configs = configs or register_configs()
configs.write(LIBVIRTD_CONF)
configs.write(LIBVIRT_BIN)
configs.write(NOVA_CONF)
if not migration_enabled():
return
if config('migration-auth-type') == 'ssh':
initialize_ssh_keys()
def do_openstack_upgrade(configs):
new_src = config('openstack-origin')
new_os_rel = get_os_codename_install_source(new_src)
log('Performing OpenStack upgrade to %s.' % (new_os_rel))
configure_installation_source(new_src)
apt_update()
dpkg_opts = [
'--option', 'Dpkg::Options::=--force-confnew',
'--option', 'Dpkg::Options::=--force-confdef',
]
apt_install(packages=determine_packages(), options=dpkg_opts, fatal=True)
# set CONFIGS to load templates from new release and regenerate config
configs.set_release(openstack_release=new_os_rel)
configs.write_all()
def import_keystone_ca_cert():
"""If provided, improt the Keystone CA cert that gets forwarded
to compute nodes via the cloud-compute interface
"""
ca_cert = relation_get('ca_cert')
if not ca_cert:
return
log('Writing Keystone CA certificate to %s' % CA_CERT_PATH)
with open(CA_CERT_PATH, 'wb') as out:
out.write(b64decode(ca_cert))
check_call(['update-ca-certificates'])
def create_libvirt_secret(secret_file, secret_uuid, key):
if secret_uuid in check_output(['virsh', 'secret-list']):
log('Libvirt secret already exists for uuid %s.' % secret_uuid,
level=DEBUG)
return
log('Defining new libvirt secret for uuid %s.' % secret_uuid)
cmd = ['virsh', 'secret-define', '--file', secret_file]
check_call(cmd)
cmd = ['virsh', 'secret-set-value', '--secret', secret_uuid,
'--base64', key]
check_call(cmd)

View File

@ -0,0 +1 @@
nova_compute_hooks.py

View File

@ -0,0 +1 @@
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -1 +1 @@
nova-compute-relations
nova_compute_hooks.py

View File

@ -5,6 +5,8 @@ description: |
OpenStack Compute, codenamed Nova, is a cloud computing fabric controller. In
addition to its "native" API (the OpenStack API), it also supports the Amazon
EC2 API.
categories:
- openstack
provides:
cloud-compute:
interface: nova-compute

View File

@ -1 +1 @@
92
129

33
templates/essex/nova.conf Normal file
View File

@ -0,0 +1,33 @@
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
--dhcpbridge_flagfile=/etc/nova/nova.conf
--dhcpbridge=/usr/bin/nova-dhcpbridge
--logdir=/var/log/nova
--state_path=/var/lib/nova
--lock_path=/var/lock/nova
--force_dhcp_release
--iscsi_helper=tgtadm
--libvirt_use_virtio_for_bridges
--connection_type=libvirt
--root_helper=sudo nova-rootwrap
--verbose
--ec2_private_dns_show_ip
{% if database_host -%}
--sql_connection=mysql://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}
{% endif -%}
{% if rabbitmq_host -%}
--rabbit_host={{ rabbitmq_host }}
--rabbit_userid={{ rabbitmq_user }}
--rabbit_password={{ rabbitmq_password }}
--rabbit_virtual_host={{ rabbitmq_virtual_host }}
{% endif -%}
{% if glance_api_servers -%}
--glance_api_servers={{ glance_api_servers }}
{% endif -%}
{% if rbd_pool -%}
--rbd_pool={{ rbd_pool }}
--rbd_user={{ rbd_user }}
--rbd_secret_uuid={{ rbd_secret_uuid }}
{% endif -%}

View File

@ -0,0 +1,79 @@
# folsom
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[DEFAULT]
dhcpbridge_flagfile=/etc/nova/nova.conf
dhcpbridge=/usr/bin/nova-dhcpbridge
logdir=/var/log/nova
state_path=/var/lib/nova
lock_path=/var/lock/nova
force_dhcp_release=True
iscsi_helper=tgtadm
libvirt_use_virtio_for_bridges=True
connection_type=libvirt
root_helper=sudo nova-rootwrap /etc/nova/rootwrap.conf
verbose=True
ec2_private_dns_show_ip=True
api_paste_config=/etc/nova/api-paste.ini
volumes_path=/var/lib/nova/volumes
enabled_apis=ec2,osapi_compute,metadata
auth_strategy=keystone
compute_driver=libvirt.LibvirtDriver
{% if database_host -%}
sql_connection = mysql://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}
{% endif -%}
{% if rabbitmq_host -%}
rabbit_host = {{ rabbitmq_host }}
rabbit_userid = {{ rabbitmq_user }}
rabbit_password = {{ rabbitmq_password }}
rabbit_virtual_host = {{ rabbitmq_virtual_host }}
{% endif -%}
{% if glance_api_servers -%}
glance_api_servers = {{ glance_api_servers }}
{% endif -%}
{% if rbd_pool -%}
rbd_pool = {{ rbd_pool }}
rbd_user = {{ rbd_user }}
rbd_secret_uuid = {{ rbd_secret_uuid }}
{% endif -%}
{% if neutron_plugin and neutron_plugin == 'ovs' -%}
libvirt_vif_driver = nova.virt.libvirt.vif.LibvirtHybridOVSBridgeDriver
libvirt_user_virtio_for_bridges = True
{% if neutron_security_groups -%}
security_group_api = quantum
firewall_driver = nova.virt.firewall.NoopFirewallDriver
{% endif -%}
{% endif -%}
{% if network_manager_config -%}
{% for key, value in network_manager_config.iteritems() -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif -%}
{% if network_manager == 'quantum' -%}
network_api_class = nova.network.quantumv2.api.API
force_config_drive = True
{% else -%}
network_manager = nova.network.manager.FlatDHCPManager
{% endif -%}
{% if volume_service -%}
volume_api_class = nova.volume.cinder.API
{% endif -%}
{% if user_config_flags -%}
{% for key, value in user_config_flags.iteritems() -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif -%}
{% if live_migration_uri -%}
live_migration_uri = {{ live_migration_uri }}
{% endif -%}

View File

@ -0,0 +1,27 @@
# grizzly
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[OVS]
tunnel_id_ranges = 1:1000
tenant_network_type = gre
enable_tunneling = True
local_ip = {{ local_ip }}
[DATABASE]
{% if database_host -%}
sql_connection = mysql://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}?charset=utf8
reconnect_interval = 2
{% else -%}
connection = sqlite:////var/lib/quantum/quantum.sqlite
{% endif -%}
[AGENT]
polling_interval = 2
[SECURITYGROUP]
{% if neutron_security_groups -%}
firewall_driver = quantum.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver
{% endif -%}

View File

@ -0,0 +1,37 @@
# grizzly
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[DEFAULT]
state_path = /var/lib/quantum
lock_path = $state_path/lock
bind_host = 0.0.0.0
bind_port = 9696
{% if core_plugin -%}
core_plugin = {{ core_plugin }}
{% endif -%}
api_paste_config = /etc/quantum/api-paste.ini
auth_strategy = keystone
notification_driver = quantum.openstack.common.notifier.rpc_notifier
default_notification_level = INFO
notification_topics = notifications
{% if rabbitmq_host -%}
rabbit_host = {{ rabbitmq_host }}
rabbit_userid = {{ rabbitmq_user }}
rabbit_password = {{ rabbitmq_password }}
rabbit_virtual_host = {{ rabbitmq_virtual_host }}
{% endif -%}
[QUOTAS]
[DEFAULT_SERVICETYPE]
[AGENT]
root_helper = sudo quantum-rootwrap /etc/quantum/rootwrap.conf
[keystone_authtoken]
signing_dir = /var/lib/quantum/keystone-signing

View File

@ -0,0 +1,81 @@
# grizzly
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[DEFAULT]
dhcpbridge_flagfile=/etc/nova/nova.conf
dhcpbridge=/usr/bin/nova-dhcpbridge
logdir=/var/log/nova
state_path=/var/lib/nova
lock_path=/var/lock/nova
force_dhcp_release=True
iscsi_helper=tgtadm
libvirt_use_virtio_for_bridges=True
connection_type=libvirt
root_helper=sudo nova-rootwrap /etc/nova/rootwrap.conf
verbose=True
ec2_private_dns_show_ip=True
api_paste_config=/etc/nova/api-paste.ini
volumes_path=/var/lib/nova/volumes
enabled_apis=ec2,osapi_compute,metadata
auth_strategy=keystone
compute_driver=libvirt.LibvirtDriver
{% if database_host -%}
sql_connection = mysql://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}
{% endif -%}
{% if rabbitmq_host -%}
rabbit_host = {{ rabbitmq_host }}
rabbit_userid = {{ rabbitmq_user }}
rabbit_password = {{ rabbitmq_password }}
rabbit_virtual_host = {{ rabbitmq_virtual_host }}
{% endif -%}
{% if glance_api_servers -%}
glance_api_servers = {{ glance_api_servers }}
{% endif -%}
{% if rbd_pool -%}
rbd_pool = {{ rbd_pool }}
rbd_user = {{ rbd_user }}
rbd_secret_uuid = {{ rbd_secret_uuid }}
{% endif -%}
{% if neutron_plugin and neutron_plugin == 'ovs' -%}
libvirt_vif_driver = nova.virt.libvirt.vif.LibvirtGenericVIFDriver
{% if neutron_security_groups -%}
security_group_api = quantum
firewall_driver = nova.virt.firewall.NoopFirewallDriver
{% endif -%}
{% endif -%}
{% if neutron_plugin and neutron_plugin == 'nvp' -%}
libvirt_vif_driver = nova.virt.libvirt.vif.LibvirtOpenVswitchVirtualPortDriver
{% endif -%}
{% if network_manager_config -%}
{% for key, value in network_manager_config.iteritems() -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif -%}
{% if network_manager == 'quantum' -%}
network_api_class = nova.network.quantumv2.api.API
{% else -%}
network_manager = nova.network.manager.FlatDHCPManager
{% endif -%}
{% if volume_service -%}
volume_api_class = nova.volume.cinder.API
{% endif -%}
{% if user_config_flags -%}
{% for key, value in user_config_flags.iteritems() -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif -%}
{% if live_migration_uri -%}
live_migration_uri = {{ live_migration_uri }}
{% endif -%}

View File

@ -0,0 +1,39 @@
# grizzly
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[DEFAULT]
state_path = /var/lib/neutron
lock_path = $state_path/lock
bind_host = 0.0.0.0
bind_port = 9696
{% if core_plugin -%}
core_plugin = {{ core_plugin }}
{% endif -%}
api_paste_config = /etc/neutron/api-paste.ini
auth_strategy = keystone
notification_driver = neutron.openstack.common.notifier.rpc_notifier
default_notification_level = INFO
notification_topics = notifications
{% if rabbitmq_host -%}
rabbit_host = {{ rabbitmq_host }}
rabbit_userid = {{ rabbitmq_user }}
rabbit_password = {{ rabbitmq_password }}
rabbit_virtual_host = {{ rabbitmq_virtual_host }}
{% endif -%}
[QUOTAS]
[DEFAULT_SERVICETYPE]
[AGENT]
root_helper = sudo neutron-rootwrap /etc/neutron/rootwrap.conf
[keystone_authtoken]
signing_dir = /var/lib/neutron/keystone-signing

View File

@ -0,0 +1,81 @@
# havana
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[DEFAULT]
dhcpbridge_flagfile=/etc/nova/nova.conf
dhcpbridge=/usr/bin/nova-dhcpbridge
logdir=/var/log/nova
state_path=/var/lib/nova
lock_path=/var/lock/nova
force_dhcp_release=True
iscsi_helper=tgtadm
libvirt_use_virtio_for_bridges=True
connection_type=libvirt
root_helper=sudo nova-rootwrap /etc/nova/rootwrap.conf
verbose=True
ec2_private_dns_show_ip=True
api_paste_config=/etc/nova/api-paste.ini
volumes_path=/var/lib/nova/volumes
enabled_apis=ec2,osapi_compute,metadata
auth_strategy=keystone
compute_driver=libvirt.LibvirtDriver
{% if database_host -%}
sql_connection = mysql://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}
{% endif -%}
{% if rabbitmq_host -%}
rabbit_host = {{ rabbitmq_host }}
rabbit_userid = {{ rabbitmq_user }}
rabbit_password = {{ rabbitmq_password }}
rabbit_virtual_host = {{ rabbitmq_virtual_host }}
{% endif -%}
{% if glance_api_servers -%}
glance_api_servers = {{ glance_api_servers }}
{% endif -%}
{% if rbd_pool -%}
rbd_pool = {{ rbd_pool }}
rbd_user = {{ rbd_user }}
rbd_secret_uuid = {{ rbd_secret_uuid }}
{% endif -%}
{% if neutron_plugin and neutron_plugin == 'ovs' -%}
libvirt_vif_driver = nova.virt.libvirt.vif.LibvirtGenericVIFDriver
{% if neutron_security_groups -%}
security_group_api = neutron
firewall_driver = nova.virt.firewall.NoopFirewallDriver
{% endif -%}
{% endif -%}
{% if neutron_plugin and neutron_plugin == 'nvp' -%}
libvirt_vif_driver = nova.virt.libvirt.vif.LibvirtOpenVswitchVirtualPortDriver
{% endif -%}
{% if network_manager_config -%}
{% for key, value in network_manager_config.iteritems() -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif -%}
{% if network_manager == 'neutron' -%}
network_api_class = nova.network.neutronv2.api.API
{% else -%}
network_manager = nova.network.manager.FlatDHCPManager
{% endif -%}
{% if volume_service -%}
volume_api_class = nova.volume.cinder.API
{% endif -%}
{% if user_config_flags -%}
{% for key, value in user_config_flags.iteritems() -%}
{{ key }} = {{ value }}
{% endfor -%}
{% endif -%}
{% if live_migration_uri -%}
live_migration_uri = {{ live_migration_uri }}
{% endif -%}

View File

@ -0,0 +1,27 @@
# havana
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
[OVS]
tunnel_id_ranges = 1:1000
tenant_network_type = gre
enable_tunneling = True
local_ip = {{ local_ip }}
[DATABASE]
{% if database_host -%}
sql_connection = mysql://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}?charset=utf8
reconnect_interval = 2
{% else -%}
connection = sqlite:////var/lib/neutron/neutron.sqlite
{% endif -%}
[AGENT]
polling_interval = 2
[SECURITYGROUP]
{% if neutron_security_groups -%}
firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver
{% endif -%}

16
templates/libvirt-bin Normal file
View File

@ -0,0 +1,16 @@
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
# Defaults for libvirt-bin initscript (/etc/init.d/libvirt-bin)
# This is a POSIX shell fragment
# Start libvirtd to handle qemu/kvm:
start_libvirtd="yes"
# options passed to libvirtd, add "-l" to listen on tcp
libvirtd_opts="{{ libvirtd_opts }}"
# pass in location of kerberos keytab
#export KRB5_KTNAME=/etc/libvirt/libvirt.keytab

400
templates/libvirtd.conf Normal file
View File

@ -0,0 +1,400 @@
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
# Master libvirt daemon configuration file
#
# For further information consult http://libvirt.org/format.html
#
# NOTE: the tests/daemon-conf regression test script requires
# that each "PARAMETER = VALUE" line in this file have the parameter
# name just after a leading "#".
#################################################################
#
# Network connectivity controls
#
# Flag listening for secure TLS connections on the public TCP/IP port.
# NB, must pass the --listen flag to the libvirtd process for this to
# have any effect.
#
# It is necessary to setup a CA and issue server certificates before
# using this capability.
#
# This is enabled by default, uncomment this to disable it
listen_tls = {{ listen_tls }}
# Listen for unencrypted TCP connections on the public TCP/IP port.
# NB, must pass the --listen flag to the libvirtd process for this to
# have any effect.
#
# Using the TCP socket requires SASL authentication by default. Only
# SASL mechanisms which support data encryption are allowed. This is
# DIGEST_MD5 and GSSAPI (Kerberos5)
#
# This is disabled by default, uncomment this to enable it.
#listen_tcp = 1
# Override the port for accepting secure TLS connections
# This can be a port number, or service name
#
#tls_port = "16514"
# Override the port for accepting insecure TCP connections
# This can be a port number, or service name
#
#tcp_port = "16509"
# Override the default configuration which binds to all network
# interfaces. This can be a numeric IPv4/6 address, or hostname
#
#listen_addr = "192.168.0.1"
# Flag toggling mDNS advertizement of the libvirt service.
#
# Alternatively can disable for all services on a host by
# stopping the Avahi daemon
#
# This is disabled by default, uncomment this to enable it
#mdns_adv = 1
# Override the default mDNS advertizement name. This must be
# unique on the immediate broadcast network.
#
# The default is "Virtualization Host HOSTNAME", where HOSTNAME
# is subsituted for the short hostname of the machine (without domain)
#
#mdns_name = "Virtualization Host Joe Demo"
#################################################################
#
# UNIX socket access controls
#
# Set the UNIX domain socket group ownership. This can be used to
# allow a 'trusted' set of users access to management capabilities
# without becoming root.
#
# This is restricted to 'root' by default.
unix_sock_group = "libvirtd"
# Set the UNIX socket permissions for the R/O socket. This is used
# for monitoring VM status only
#
# Default allows any user. If setting group ownership may want to
# restrict this to:
#unix_sock_ro_perms = "0777"
# Set the UNIX socket permissions for the R/W socket. This is used
# for full management of VMs
#
# Default allows only root. If PolicyKit is enabled on the socket,
# the default will change to allow everyone (eg, 0777)
#
# If not using PolicyKit and setting group ownership for access
# control then you may want to relax this to:
unix_sock_rw_perms = "0770"
# Set the name of the directory in which sockets will be found/created.
#unix_sock_dir = "/var/run/libvirt"
#################################################################
#
# Authentication.
#
# - none: do not perform auth checks. If you can connect to the
# socket you are allowed. This is suitable if there are
# restrictions on connecting to the socket (eg, UNIX
# socket permissions), or if there is a lower layer in
# the network providing auth (eg, TLS/x509 certificates)
#
# - sasl: use SASL infrastructure. The actual auth scheme is then
# controlled from /etc/sasl2/libvirt.conf. For the TCP
# socket only GSSAPI & DIGEST-MD5 mechanisms will be used.
# For non-TCP or TLS sockets, any scheme is allowed.
#
# - polkit: use PolicyKit to authenticate. This is only suitable
# for use on the UNIX sockets. The default policy will
# require a user to supply their own password to gain
# full read/write access (aka sudo like), while anyone
# is allowed read/only access.
#
# Set an authentication scheme for UNIX read-only sockets
# By default socket permissions allow anyone to connect
#
# To restrict monitoring of domains you may wish to enable
# an authentication mechanism here
auth_unix_ro = "none"
# Set an authentication scheme for UNIX read-write sockets
# By default socket permissions only allow root. If PolicyKit
# support was compiled into libvirt, the default will be to
# use 'polkit' auth.
#
# If the unix_sock_rw_perms are changed you may wish to enable
# an authentication mechanism here
auth_unix_rw = "none"
# Change the authentication scheme for TCP sockets.
#
# If you don't enable SASL, then all TCP traffic is cleartext.
# Don't do this outside of a dev/test scenario. For real world
# use, always enable SASL and use the GSSAPI or DIGEST-MD5
# mechanism in /etc/sasl2/libvirt.conf
#auth_tcp = "sasl"
# Change the authentication scheme for TLS sockets.
#
# TLS sockets already have encryption provided by the TLS
# layer, and limited authentication is done by certificates
#
# It is possible to make use of any SASL authentication
# mechanism as well, by using 'sasl' for this option
#auth_tls = "none"
#################################################################
#
# TLS x509 certificate configuration
#
# Override the default server key file path
#
#key_file = "/etc/pki/libvirt/private/serverkey.pem"
# Override the default server certificate file path
#
#cert_file = "/etc/pki/libvirt/servercert.pem"
# Override the default CA certificate path
#
#ca_file = "/etc/pki/CA/cacert.pem"
# Specify a certificate revocation list.
#
# Defaults to not using a CRL, uncomment to enable it
#crl_file = "/etc/pki/CA/crl.pem"
#################################################################
#
# Authorization controls
#
# Flag to disable verification of our own server certificates
#
# When libvirtd starts it performs some sanity checks against
# its own certificates.
#
# Default is to always run sanity checks. Uncommenting this
# will disable sanity checks which is not a good idea
#tls_no_sanity_certificate = 1
# Flag to disable verification of client certificates
#
# Client certificate verification is the primary authentication mechanism.
# Any client which does not present a certificate signed by the CA
# will be rejected.
#
# Default is to always verify. Uncommenting this will disable
# verification - make sure an IP whitelist is set
#tls_no_verify_certificate = 1
# A whitelist of allowed x509 Distinguished Names
# This list may contain wildcards such as
#
# "C=GB,ST=London,L=London,O=Red Hat,CN=*"
#
# See the POSIX fnmatch function for the format of the wildcards.
#
# NB If this is an empty list, no client can connect, so comment out
# entirely rather than using empty list to disable these checks
#
# By default, no DN's are checked
#tls_allowed_dn_list = ["DN1", "DN2"]
# A whitelist of allowed SASL usernames. The format for usernames
# depends on the SASL authentication mechanism. Kerberos usernames
# look like username@REALM
#
# This list may contain wildcards such as
#
# "*@EXAMPLE.COM"
#
# See the POSIX fnmatch function for the format of the wildcards.
#
# NB If this is an empty list, no client can connect, so comment out
# entirely rather than using empty list to disable these checks
#
# By default, no Username's are checked
#sasl_allowed_username_list = ["joe@EXAMPLE.COM", "fred@EXAMPLE.COM" ]
#################################################################
#
# Processing controls
#
# The maximum number of concurrent client connections to allow
# over all sockets combined.
#max_clients = 20
# The minimum limit sets the number of workers to start up
# initially. If the number of active clients exceeds this,
# then more threads are spawned, up to max_workers limit.
# Typically you'd want max_workers to equal maximum number
# of clients allowed
#min_workers = 5
#max_workers = 20
# The number of priority workers. If all workers from above
# pool will stuck, some calls marked as high priority
# (notably domainDestroy) can be executed in this pool.
#prio_workers = 5
# Total global limit on concurrent RPC calls. Should be
# at least as large as max_workers. Beyond this, RPC requests
# will be read into memory and queued. This directly impact
# memory usage, currently each request requires 256 KB of
# memory. So by default up to 5 MB of memory is used
#
# XXX this isn't actually enforced yet, only the per-client
# limit is used so far
#max_requests = 20
# Limit on concurrent requests from a single client
# connection. To avoid one client monopolizing the server
# this should be a small fraction of the global max_requests
# and max_workers parameter
#max_client_requests = 5
#################################################################
#
# Logging controls
#
# Logging level: 4 errors, 3 warnings, 2 information, 1 debug
# basically 1 will log everything possible
#log_level = 3
# Logging filters:
# A filter allows to select a different logging level for a given category
# of logs
# The format for a filter is one of:
# x:name
# x:+name
# where name is a string which is matched against source file name,
# e.g., "remote", "qemu", or "util/json", the optional "+" prefix
# tells libvirt to log stack trace for each message matching name,
# and x is the minimal level where matching messages should be logged:
# 1: DEBUG
# 2: INFO
# 3: WARNING
# 4: ERROR
#
# Multiple filter can be defined in a single @filters, they just need to be
# separated by spaces.
#
# e.g. to only get warning or errors from the remote layer and only errors
# from the event layer:
#log_filters="3:remote 4:event"
# Logging outputs:
# An output is one of the places to save logging information
# The format for an output can be:
# x:stderr
# output goes to stderr
# x:syslog:name
# use syslog for the output and use the given name as the ident
# x:file:file_path
# output to a file, with the given filepath
# In all case the x prefix is the minimal level, acting as a filter
# 1: DEBUG
# 2: INFO
# 3: WARNING
# 4: ERROR
#
# Multiple output can be defined, they just need to be separated by spaces.
# e.g. to log all warnings and errors to syslog under the libvirtd ident:
#log_outputs="3:syslog:libvirtd"
#
# Log debug buffer size: default 64
# The daemon keeps an internal debug log buffer which will be dumped in case
# of crash or upon receiving a SIGUSR2 signal. This setting allows to override
# the default buffer size in kilobytes.
# If value is 0 or less the debug log buffer is deactivated
#log_buffer_size = 64
##################################################################
#
# Auditing
#
# This setting allows usage of the auditing subsystem to be altered:
#
# audit_level == 0 -> disable all auditing
# audit_level == 1 -> enable auditing, only if enabled on host (default)
# audit_level == 2 -> enable auditing, and exit if disabled on host
#
#audit_level = 2
#
# If set to 1, then audit messages will also be sent
# via libvirt logging infrastructure. Defaults to 0
#
#audit_logging = 1
###################################################################
# UUID of the host:
# Provide the UUID of the host here in case the command
# 'dmidecode -s system-uuid' does not provide a valid uuid. In case
# 'dmidecode' does not provide a valid UUID and none is provided here, a
# temporary UUID will be generated.
# Keep the format of the example UUID below. UUID must not have all digits
# be the same.
# NB This default all-zeros UUID will not work. Replace
# it with the output of the 'uuidgen' command and then
# uncomment this entry
#host_uuid = "00000000-0000-0000-0000-000000000000"
###################################################################
# Keepalive protocol:
# This allows libvirtd to detect broken client connections or even
# dead client. A keepalive message is sent to a client after
# keepalive_interval seconds of inactivity to check if the client is
# still responding; keepalive_count is a maximum number of keepalive
# messages that are allowed to be sent to the client without getting
# any response before the connection is considered broken. In other
# words, the connection is automatically closed approximately after
# keepalive_interval * (keepalive_count + 1) seconds since the last
# message received from the client. If keepalive_interval is set to
# -1, libvirtd will never send keepalive requests; however clients
# can still send them and the deamon will send responses. When
# keepalive_count is set to 0, connections will be automatically
# closed after keepalive_interval seconds of inactivity without
# sending any keepalive messages.
#
#keepalive_interval = 5
#keepalive_count = 5
#
# If set to 1, libvirtd will refuse to talk to clients that do not
# support keepalive protocol. Defaults to 0.
#
#keepalive_required = 1

12
templates/qemu.conf Normal file
View File

@ -0,0 +1,12 @@
###############################################################################
# [ WARNING ]
# Configuration file maintained by Juju. Local changes may be overwritten.
###############################################################################
# File installed by Juju nova-compute charm
cgroup_device_acl = [
"/dev/null", "/dev/full", "/dev/zero",
"/dev/random", "/dev/urandom",
"/dev/ptmx", "/dev/kvm", "/dev/kqemu",
"/dev/rtc", "/dev/hpet", "/dev/net/tun",
]

8
templates/secret.xml Normal file
View File

@ -0,0 +1,8 @@
{% if ceph_secret_uuid -%}
<secret ephemeral='no' private='no'>
<uuid>{{ ceph_secret_uuid }}</uuid>
<usage type='ceph'>
<name>client.{{ service_name }} secret</name>
</usage>
</secret>
{% endif -%}

3
unit_tests/__init__.py Normal file
View File

@ -0,0 +1,3 @@
import sys
sys.path.append('hooks/')

View File

@ -0,0 +1,164 @@
from mock import patch
from test_utils import CharmTestCase
from charmhelpers.contrib.openstack.context import OSContextError
import nova_compute_context as context
TO_PATCH = [
'apt_install',
'filter_installed_packages',
'relation_ids',
'relation_get',
'related_units',
'config',
'log',
'os_release',
'_save_flag_file',
]
QUANTUM_CONTEXT = {
'network_manager': 'quantum',
'quantum_auth_strategy': 'keystone',
'keystone_host': 'keystone_host',
'auth_port': '5000',
'quantum_url': 'http://quantum_url',
'service_tenant_name': 'admin',
'service_username': 'admin',
'service_password': 'openstack',
'quantum_security_groups': 'yes',
'quantum_plugin': 'ovs',
'auth_host': 'keystone_host',
}
# Context for an OVS plugin contains at least the following. Other bits
# (driver names) are dependent on OS release.
BASE_QUANTUM_OVS_PLUGIN_CONTEXT = {
'core_plugin': 'quantum.plugins.openvswitch.ovs_quantum_plugin.'
'OVSQuantumPluginV2',
'enable_tunneling': True,
'libvirt_use_virtio_for_bridges': True,
'local_ip': '10.0.0.1',
'nova_firewall_driver': 'nova.virt.firewall.NoopFirewallDriver',
'ovs_firewall_driver': 'quantum.agent.linux.iptables_firewall.'
'OVSHybridIptablesFirewallDriver',
'tenant_network_type': 'gre',
'tunnel_id_ranges': '1:1000',
'quantum_plugin': 'ovs',
'quantum_security_groups': 'yes',
}
def fake_log(msg, level=None):
level = level or 'INFO'
print '[juju test log (%s)] %s' % (level, msg)
class NovaComputeContextTests(CharmTestCase):
def setUp(self):
super(NovaComputeContextTests, self).setUp(context, TO_PATCH)
self.relation_get.side_effect = self.test_relation.get
self.config.side_effect = self.test_config.get
self.log.side_effect = fake_log
def test_cloud_compute_context_no_relation(self):
self.relation_ids.return_value = []
cloud_compute = context.CloudComputeContext()
self.assertEquals({}, cloud_compute())
@patch.object(context, '_network_manager')
def test_cloud_compute_volume_context_cinder(self, netman):
netman.return_value = None
self.relation_ids.return_value = 'cloud-compute:0'
self.related_units.return_value = 'nova-cloud-controller/0'
cloud_compute = context.CloudComputeContext()
self.test_relation.set({'volume_service': 'cinder'})
self.assertEquals({'volume_service': 'cinder'}, cloud_compute())
@patch.object(context, '_network_manager')
def test_cloud_compute_volume_context_nova_vol(self, netman):
netman.return_value = None
self.relation_ids.return_value = 'cloud-compute:0'
self.related_units.return_value = 'nova-cloud-controller/0'
cloud_compute = context.CloudComputeContext()
self.os_release.return_value = 'essex'
self.test_relation.set({'volume_service': 'nova-volume'})
self.assertEquals({'volume_service': 'nova-volume'}, cloud_compute())
@patch.object(context, '_network_manager')
def test_cloud_compute_volume_context_nova_vol_unsupported(self, netman):
self.skipTest('TODO')
netman.return_value = None
self.relation_ids.return_value = 'cloud-compute:0'
cloud_compute = context.CloudComputeContext()
# n-vol doesn't exist in grizzly
self.os_release.return_value = 'grizzly'
self.test_relation.set({'volume_service': 'nova-volume'})
self.assertRaises(OSContextError, cloud_compute)
@patch.object(context, '_network_manager')
def test_cloud_compute_flatdhcp_context(self, netman):
netman.return_value = 'flatdhcpmanager'
self.relation_ids.return_value = 'cloud-compute:0'
self.related_units.return_value = 'nova-cloud-controller/0'
self.test_relation.set({
'network_manager': 'FlatDHCPManager',
'ec2_host': 'novaapihost'})
cloud_compute = context.CloudComputeContext()
ex_ctxt = {
'network_manager': 'flatdhcpmanager',
'network_manager_config': {
'ec2_dmz_host': 'novaapihost',
'flat_interface': 'eth1'
}
}
self.assertEquals(ex_ctxt, cloud_compute())
@patch.object(context, '_neutron_plugin')
@patch.object(context, '_neutron_url')
@patch.object(context, '_network_manager')
def test_cloud_compute_quantum_context(self, netman, url, plugin):
self.relation_ids.return_value = 'cloud-compute:0'
self.related_units.return_value = 'nova-cloud-controller/0'
netman.return_value = 'quantum'
plugin.return_value = 'ovs'
url.return_value = 'http://nova-c-c:9696'
self.test_relation.set(QUANTUM_CONTEXT)
cloud_compute = context.CloudComputeContext()
ex_ctxt = {
'network_manager': 'quantum',
'network_manager_config': {
'auth_port': '5000',
'keystone_host': 'keystone_host',
'quantum_admin_auth_url': 'http://keystone_host:5000/v2.0',
'quantum_admin_password': 'openstack',
'quantum_admin_tenant_name': 'admin',
'quantum_admin_username': 'admin',
'quantum_auth_strategy': 'keystone',
'quantum_plugin': 'ovs',
'quantum_security_groups': True,
'quantum_url': 'http://nova-c-c:9696'
}
}
self.assertEquals(ex_ctxt, cloud_compute())
self._save_flag_file.assert_called_with(
path='/etc/nova/nm.conf', data='quantum')
@patch.object(context.NeutronComputeContext, 'network_manager')
@patch.object(context.NeutronComputeContext, 'plugin')
def test_quantum_plugin_context_no_setting(self, plugin, nm):
plugin.return_value = None
qplugin = context.NeutronComputeContext()
with patch.object(qplugin, '_ensure_packages'):
self.assertEquals({}, qplugin())
def test_libvirt_bin_context_no_migration(self):
self.test_config.set('enable-live-migration', False)
libvirt = context.NovaComputeLibvirtContext()
self.assertEquals({'libvirtd_opts': '-d', 'listen_tls': 0}, libvirt())
def test_libvirt_bin_context_migration_tcp_listen(self):
self.test_config.set('enable-live-migration', True)
libvirt = context.NovaComputeLibvirtContext()
self.assertEquals(
{'libvirtd_opts': '-d -l', 'listen_tls': 0}, libvirt())

View File

@ -0,0 +1,278 @@
from mock import call, patch, MagicMock
from test_utils import CharmTestCase
import nova_compute_utils as utils
_reg = utils.register_configs
_map = utils.restart_map
utils.register_configs = MagicMock()
utils.restart_map = MagicMock()
import nova_compute_hooks as hooks
utils.register_configs = _reg
utils.restart_map = _map
TO_PATCH = [
# charmhelpers.core.hookenv
'Hooks',
'config',
'log',
'relation_get',
'relation_ids',
'relation_set',
'service_name',
'unit_get',
# charmhelpers.core.host
'apt_install',
'apt_update',
'filter_installed_packages',
'restart_on_change',
#charmhelpers.contrib.openstack.utils
'configure_installation_source',
'openstack_upgrade_available',
# nova_compute_utils
#'PACKAGES',
'create_libvirt_secret',
'restart_map',
'determine_packages',
'import_authorized_keys',
'import_keystone_ca_cert',
'initialize_ssh_keys',
'migration_enabled',
'do_openstack_upgrade',
'network_manager',
'neutron_plugin_attribute',
'neutron_plugin',
'public_ssh_key',
'register_configs',
# misc_utils
'ensure_ceph_keyring',
'execd_preinstall'
]
def fake_filter(packages):
return packages
class NovaComputeRelationsTests(CharmTestCase):
def setUp(self):
super(NovaComputeRelationsTests, self).setUp(hooks,
TO_PATCH)
self.config.side_effect = self.test_config.get
self.filter_installed_packages.side_effect = fake_filter
def test_install_hook(self):
repo = 'cloud:precise-grizzly'
self.test_config.set('openstack-origin', repo)
self.determine_packages.return_value = ['foo', 'bar']
hooks.install()
self.configure_installation_source.assert_called_with(repo)
self.assertTrue(self.apt_update.called)
self.apt_install.assert_called_with(['foo', 'bar'], fatal=True)
self.execd_preinstall.assert_called()
def test_config_changed_with_upgrade(self):
self.openstack_upgrade_available.return_value = True
hooks.config_changed()
self.assertTrue(self.do_openstack_upgrade.called)
@patch.object(hooks, 'compute_joined')
def test_config_changed_with_migration(self, compute_joined):
self.migration_enabled.return_value = True
self.test_config.set('migration-auth-type', 'ssh')
self.relation_ids.return_value = [
'cloud-compute:0',
'cloud-compute:1'
]
hooks.config_changed()
ex = [
call('cloud-compute:0'),
call('cloud-compute:1'),
]
self.assertEquals(ex, compute_joined.call_args_list)
@patch.object(hooks, 'compute_joined')
def test_config_changed_no_upgrade_no_migration(self, compute_joined):
self.openstack_upgrade_available.return_value = False
self.migration_enabled.return_value = False
hooks.config_changed()
self.assertFalse(self.do_openstack_upgrade.called)
self.assertFalse(compute_joined.called)
def test_amqp_joined(self):
hooks.amqp_joined()
self.relation_set.assert_called_with(username='nova', vhost='nova')
@patch.object(hooks, 'CONFIGS')
def test_amqp_changed_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
hooks.amqp_changed()
self.log.assert_called_with(
'amqp relation incomplete. Peer not ready?'
)
def _amqp_test(self, configs, quantum=False):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['amqp']
configs.write = MagicMock()
if quantum:
self.network_manager.return_value = 'quantum'
hooks.amqp_changed()
@patch.object(hooks, 'CONFIGS')
def test_amqp_changed_with_data_no_quantum(self, configs):
self._amqp_test(configs, quantum=False)
self.assertEquals([call('/etc/nova/nova.conf')],
configs.write.call_args_list)
@patch.object(hooks, 'CONFIGS')
def test_amqp_changed_with_data_and_quantum(self, configs):
self._amqp_test(configs, quantum=True)
self.assertEquals([call('/etc/nova/nova.conf'),
call('/etc/quantum/quantum.conf')],
configs.write.call_args_list)
def test_db_joined(self):
self.unit_get.return_value = 'nova.foohost.com'
hooks.db_joined()
self.relation_set.assert_called_with(relation_id=None,
nova_database='nova',
nova_username='nova',
nova_hostname='nova.foohost.com')
self.unit_get.assert_called_with('private-address')
def test_db_joined_quantum(self):
self.unit_get.return_value = 'nova.foohost.com'
self.network_manager.return_value = 'quantum'
hooks.db_joined(rid='shared-db:0')
calls = [call(nova_database='nova',
nova_username='nova',
nova_hostname='nova.foohost.com',
relation_id='shared-db:0'),
call(neutron_database='neutron',
neutron_username='neutron',
neutron_hostname='nova.foohost.com',
relation_id='shared-db:0')]
[self.assertIn(c, self.relation_set.call_args_list)
for c in calls]
self.unit_get.assert_called_with('private-address')
@patch.object(hooks, 'CONFIGS')
def test_db_changed_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
hooks.db_changed()
self.log.assert_called_with(
'shared-db relation incomplete. Peer not ready?'
)
def _shared_db_test(self, configs, quantum=False):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['shared-db']
configs.write = MagicMock()
if quantum:
self.network_manager.return_value = 'quantum'
hooks.db_changed()
@patch.object(hooks, 'CONFIGS')
def test_db_changed_with_data_no_quantum(self, configs):
self._shared_db_test(configs, quantum=False)
self.assertEquals([call('/etc/nova/nova.conf')],
configs.write.call_args_list)
@patch.object(hooks, 'CONFIGS')
def test_db_changed_with_data_and_quantum(self, configs):
self.neutron_plugin_attribute.return_value = '/etc/quantum/plugin.conf'
self._shared_db_test(configs, quantum=True)
ex = [call('/etc/nova/nova.conf'), call('/etc/quantum/plugin.conf')]
self.assertEquals(ex, configs.write.call_args_list)
@patch.object(hooks, 'CONFIGS')
def test_image_service_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
hooks.image_service_changed()
self.log.assert_called_with(
'image-service relation incomplete. Peer not ready?'
)
@patch.object(hooks, 'CONFIGS')
def test_image_service_with_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.write = MagicMock()
configs.complete_contexts.return_value = ['image-service']
hooks.image_service_changed()
configs.write.assert_called_with('/etc/nova/nova.conf')
def test_compute_joined_no_migration(self):
self.migration_enabled.return_value = False
hooks.compute_joined()
self.assertFalse(self.relation_set.called)
def test_compute_joined_with_ssh_migration(self):
self.migration_enabled.return_value = True
self.test_config.set('migration-auth-type', 'ssh')
self.public_ssh_key.return_value = 'foo'
hooks.compute_joined()
self.relation_set.assert_called_with(
relation_id=None,
ssh_public_key='foo',
migration_auth_type='ssh'
)
hooks.compute_joined(rid='cloud-compute:2')
self.relation_set.assert_called_with(
relation_id='cloud-compute:2',
ssh_public_key='foo',
migration_auth_type='ssh'
)
def test_compute_changed(self):
hooks.compute_changed()
expected_funcs = [
self.import_authorized_keys,
self.import_keystone_ca_cert,
]
for func in expected_funcs:
self.assertTrue(func.called)
def test_ceph_joined(self):
hooks.ceph_joined()
self.apt_install.assert_called_with(['ceph-common'], fatal=True)
@patch.object(hooks, 'CONFIGS')
def test_ceph_changed_missing_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = []
hooks.ceph_changed()
self.log.assert_called_with(
'ceph relation incomplete. Peer not ready?'
)
@patch.object(hooks, 'CONFIGS')
def test_ceph_changed_no_keyring(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['ceph']
self.ensure_ceph_keyring.return_value = False
hooks.ceph_changed()
self.log.assert_called_with(
'Could not create ceph keyring: peer not ready?'
)
@patch.object(hooks, 'CONFIGS')
def test_ceph_changed_with_key_and_relation_data(self, configs):
configs.complete_contexts = MagicMock()
configs.complete_contexts.return_value = ['ceph']
configs.write = MagicMock()
self.ensure_ceph_keyring.return_value = True
hooks.ceph_changed()
ex = [
call('/etc/ceph/ceph.conf'),
call('/etc/ceph/secret.xml'),
call('/etc/nova/nova.conf'),
]
self.assertEquals(ex, configs.write.call_args_list)

View File

@ -0,0 +1,244 @@
from mock import patch, MagicMock, call
from test_utils import CharmTestCase, patch_open
import nova_compute_utils as utils
TO_PATCH = [
'config',
'os_release',
'log',
'neutron_plugin_attribute',
'related_units',
'relation_ids',
'relation_get',
]
OVS_PKGS = [
'quantum-plugin-openvswitch-agent',
'openvswitch-datapath-dkms',
]
class NovaComputeUtilsTests(CharmTestCase):
def setUp(self):
super(NovaComputeUtilsTests, self).setUp(utils, TO_PATCH)
self.config.side_effect = self.test_config.get
@patch.object(utils, 'network_manager')
def test_determine_packages_nova_network(self, net_man):
net_man.return_value = 'flatdhcpmanager'
self.relation_ids.return_value = []
result = utils.determine_packages()
ex = utils.BASE_PACKAGES + [
'nova-api',
'nova-network',
'nova-compute-kvm'
]
self.assertEquals(ex, result)
@patch.object(utils, 'neutron_plugin')
@patch.object(utils, 'network_manager')
def test_determine_packages_quantum(self, net_man, n_plugin):
self.neutron_plugin_attribute.return_value = OVS_PKGS
net_man.return_value = 'quantum'
n_plugin.return_value = 'ovs'
self.relation_ids.return_value = []
result = utils.determine_packages()
ex = utils.BASE_PACKAGES + OVS_PKGS + ['nova-compute-kvm']
self.assertEquals(ex, result)
@patch.object(utils, 'neutron_plugin')
@patch.object(utils, 'network_manager')
def test_determine_packages_quantum_ceph(self, net_man, n_plugin):
self.neutron_plugin_attribute.return_value = OVS_PKGS
net_man.return_value = 'quantum'
n_plugin.return_value = 'ovs'
self.relation_ids.return_value = ['ceph:0']
result = utils.determine_packages()
ex = (utils.BASE_PACKAGES + OVS_PKGS +
['ceph-common', 'nova-compute-kvm'])
self.assertEquals(ex, result)
@patch.object(utils, 'network_manager')
def test_resource_map_nova_network_no_multihost(self, net_man):
self.skipTest('skipped until contexts are properly mocked')
self.test_config.set('multi-host', 'no')
net_man.return_value = 'FlatDHCPManager'
result = utils.resource_map()
ex = {
'/etc/default/libvirt-bin': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/libvirt/qemu.conf': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/nova/nova-compute.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/nova/nova.conf': {
'contexts': [],
'services': ['nova-compute']
},
}
self.assertEquals(ex, result)
@patch.object(utils, 'network_manager')
def test_resource_map_nova_network(self, net_man):
self.skipTest('skipped until contexts are properly mocked')
net_man.return_value = 'FlatDHCPManager'
result = utils.resource_map()
ex = {
'/etc/default/libvirt-bin': {
'contexts': [], 'services': ['libvirt-bin']
},
'/etc/libvirt/qemu.conf': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/nova/nova-compute.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/nova/nova.conf': {
'contexts': [],
'services': ['nova-compute', 'nova-api', 'nova-network']
}
}
self.assertEquals(ex, result)
@patch.object(utils, 'neutron_plugin')
@patch.object(utils, 'network_manager')
def test_resource_map_quantum_ovs(self, net_man, _plugin):
self.skipTest('skipped until contexts are properly mocked.')
net_man.return_value = 'Quantum'
_plugin.return_value = 'ovs'
result = utils.resource_map()
ex = {
'/etc/default/libvirt-bin': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/libvirt/qemu.conf': {
'contexts': [],
'services': ['libvirt-bin']
},
'/etc/nova/nova-compute.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/nova/nova.conf': {
'contexts': [],
'services': ['nova-compute']
},
'/etc/quantum/plugins/openvswitch/ovs_quantum_plugin.ini': {
'contexts': [],
'services': ['quantum-plugin-openvswitch-agent']
},
'/etc/quantum/quantum.conf': {
'contexts': [],
'services': ['quantum-plugin-openvswitch-agent']}
}
self.assertEquals(ex, result)
def fake_user(self, username='foo'):
user = MagicMock()
user.pw_dir = '/home/' + username
return user
@patch('__builtin__.open')
@patch('pwd.getpwnam')
def test_public_ssh_key_not_found(self, getpwnam, _open):
_open.side_effect = Exception
getpwnam.return_value = self.fake_user('foo')
self.assertEquals(None, utils.public_ssh_key())
@patch('pwd.getpwnam')
def test_public_ssh_key(self, getpwnam):
getpwnam.return_value = self.fake_user('foo')
with patch_open() as (_open, _file):
_file.read.return_value = 'mypubkey'
result = utils.public_ssh_key('foo')
self.assertEquals(result, 'mypubkey')
def test_import_authorized_keys_missing_data(self):
self.relation_get.return_value = None
with patch_open() as (_open, _file):
utils.import_authorized_keys(user='foo')
self.assertFalse(_open.called)
@patch('pwd.getpwnam')
def test_import_authorized_keys(self, getpwnam):
getpwnam.return_value = self.fake_user('foo')
self.relation_get.side_effect = [
'Zm9vX2tleQo=', # relation_get('known_hosts')
'Zm9vX2hvc3QK', # relation_get('authorized_keys')
]
ex_open = [
call('/home/foo/.ssh/authorized_keys', 'wb'),
call('/home/foo/.ssh/known_hosts', 'wb')
]
ex_write = [
call('foo_host\n'),
call('foo_key\n'),
]
with patch_open() as (_open, _file):
utils.import_authorized_keys(user='foo')
self.assertEquals(ex_open, _open.call_args_list)
self.assertEquals(ex_write, _file.write.call_args_list)
@patch('subprocess.check_call')
def test_import_keystone_cert_missing_data(self, check_call):
self.relation_get.return_value = None
with patch_open() as (_open, _file):
utils.import_keystone_ca_cert()
self.assertFalse(_open.called)
self.assertFalse(check_call.called)
@patch.object(utils, 'check_call')
def test_import_keystone_cert(self, check_call):
self.relation_get.return_value = 'Zm9vX2NlcnQK'
with patch_open() as (_open, _file):
utils.import_keystone_ca_cert()
_open.assert_called_with(utils.CA_CERT_PATH, 'wb')
_file.write.assert_called_with('foo_cert\n')
check_call.assert_called_with(['update-ca-certificates'])
@patch('charmhelpers.contrib.openstack.templating.OSConfigRenderer')
@patch.object(utils, 'quantum_enabled')
@patch.object(utils, 'resource_map')
def test_register_configs(self, resource_map, quantum, renderer):
quantum.return_value = False
self.os_release.return_value = 'havana'
fake_renderer = MagicMock()
fake_renderer.register = MagicMock()
renderer.return_value = fake_renderer
ctxt1 = MagicMock()
ctxt2 = MagicMock()
rsc_map = {
'/etc/nova/nova.conf': {
'services': ['nova-compute'],
'contexts': [ctxt1],
},
'/etc/nova/nova-compute.conf': {
'services': ['nova-compute'],
'contexts': [ctxt2],
},
}
resource_map.return_value = rsc_map
utils.register_configs()
renderer.assert_called_with(
openstack_release='havana', templates_dir='templates/')
ex_reg = [
call('/etc/nova/nova-compute.conf', [ctxt2]),
call('/etc/nova/nova.conf', [ctxt1])
]
self.assertEquals(fake_renderer.register.call_args_list, ex_reg)

118
unit_tests/test_utils.py Normal file
View File

@ -0,0 +1,118 @@
import logging
import unittest
import os
import yaml
from contextlib import contextmanager
from mock import patch, MagicMock
def load_config():
'''
Walk backwords from __file__ looking for config.yaml, load and return the
'options' section'
'''
config = None
f = __file__
while config is None:
d = os.path.dirname(f)
if os.path.isfile(os.path.join(d, 'config.yaml')):
config = os.path.join(d, 'config.yaml')
break
f = d
if not config:
logging.error('Could not find config.yaml in any parent directory '
'of %s. ' % file)
raise Exception
return yaml.safe_load(open(config).read())['options']
def get_default_config():
'''
Load default charm config from config.yaml return as a dict.
If no default is set in config.yaml, its value is None.
'''
default_config = {}
config = load_config()
for k, v in config.iteritems():
if 'default' in v:
default_config[k] = v['default']
else:
default_config[k] = None
return default_config
class CharmTestCase(unittest.TestCase):
def setUp(self, obj, patches):
super(CharmTestCase, self).setUp()
self.patches = patches
self.obj = obj
self.test_config = TestConfig()
self.test_relation = TestRelation()
self.patch_all()
def patch(self, method):
_m = patch.object(self.obj, method)
mock = _m.start()
self.addCleanup(_m.stop)
return mock
def patch_all(self):
for method in self.patches:
setattr(self, method, self.patch(method))
class TestConfig(object):
def __init__(self):
self.config = get_default_config()
def get(self, attr=None):
if not attr:
return self.get_all()
try:
return self.config[attr]
except KeyError:
return None
def get_all(self):
return self.config
def set(self, attr, value):
if attr not in self.config:
raise KeyError
self.config[attr] = value
class TestRelation(object):
def __init__(self, relation_data={}):
self.relation_data = relation_data
def set(self, relation_data):
self.relation_data = relation_data
def get(self, attr=None, unit=None, rid=None):
if attr is None:
return self.relation_data
elif attr in self.relation_data:
return self.relation_data[attr]
return None
@contextmanager
def patch_open():
'''Patch open() to allow mocking both open() itself and the file that is
yielded.
Yields the mock for "open" and "file", respectively.'''
mock_open = MagicMock(spec=open)
mock_file = MagicMock(spec=file)
@contextmanager
def stub_open(*args, **kwargs):
mock_open(*args, **kwargs)
yield mock_file
with patch('__builtin__.open', stub_open):
yield mock_open, mock_file