Principle for Neutron API
This commit is contained in:
commit
af14885052
62
config.yaml
Normal file
62
config.yaml
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
options:
|
||||||
|
openstack-origin:
|
||||||
|
default: distro
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Repository from which to install. May be one of the following:
|
||||||
|
distro (default), ppa:somecustom/ppa, a deb url sources entry,
|
||||||
|
or a supported Cloud Archive release pocket.
|
||||||
|
|
||||||
|
Supported Cloud Archive sources include: cloud:precise-folsom,
|
||||||
|
cloud:precise-folsom/updates, cloud:precise-folsom/staging,
|
||||||
|
cloud:precise-folsom/proposed.
|
||||||
|
|
||||||
|
Note that updating this setting to a source that is known to
|
||||||
|
provide a later version of OpenStack will trigger a software
|
||||||
|
upgrade.
|
||||||
|
rabbit-user:
|
||||||
|
default: nova
|
||||||
|
type: string
|
||||||
|
description: Username used to access rabbitmq queue
|
||||||
|
rabbit-vhost:
|
||||||
|
default: openstack
|
||||||
|
type: string
|
||||||
|
description: Rabbitmq vhost
|
||||||
|
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)
|
||||||
|
use-syslog:
|
||||||
|
type: boolean
|
||||||
|
default: False
|
||||||
|
description: |
|
||||||
|
If set to True, supporting services will log to syslog.
|
||||||
|
region:
|
||||||
|
default: RegionOne
|
||||||
|
type: string
|
||||||
|
description: OpenStack Region
|
||||||
|
neutron-security-groups:
|
||||||
|
type: string
|
||||||
|
default: "no"
|
||||||
|
description: |
|
||||||
|
Use quantum for security group management.
|
||||||
|
.
|
||||||
|
Only supported for >= grizzly.
|
||||||
|
neutron-external-network:
|
||||||
|
type: string
|
||||||
|
default: ext_net
|
||||||
|
description: Name of the external network for floating IP addresses provided by Neutron.
|
||||||
|
neutron-plugin:
|
||||||
|
default: ovs
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Neutron plugin to use for network management; supports
|
||||||
|
.
|
||||||
|
ovs - OpenvSwitch Plugin
|
||||||
|
nvp - Nicira Network Virtualization Platform
|
||||||
|
.
|
||||||
|
|
1
hooks/amqp-relation-broken
Symbolic link
1
hooks/amqp-relation-broken
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/amqp-relation-changed
Symbolic link
1
hooks/amqp-relation-changed
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/amqp-relation-departed
Symbolic link
1
hooks/amqp-relation-departed
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/amqp-relation-joined
Symbolic link
1
hooks/amqp-relation-joined
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
0
hooks/charmhelpers/__init__.py
Normal file
0
hooks/charmhelpers/__init__.py
Normal file
0
hooks/charmhelpers/contrib/__init__.py
Normal file
0
hooks/charmhelpers/contrib/__init__.py
Normal file
0
hooks/charmhelpers/contrib/hahelpers/__init__.py
Normal file
0
hooks/charmhelpers/contrib/hahelpers/__init__.py
Normal file
59
hooks/charmhelpers/contrib/hahelpers/apache.py
Normal file
59
hooks/charmhelpers/contrib/hahelpers/apache.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
#
|
||||||
|
# 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 = config_get('ssl_ca')
|
||||||
|
if ca_cert is 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 ca_cert is None:
|
||||||
|
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'])
|
297
hooks/charmhelpers/contrib/hahelpers/ceph.py
Normal file
297
hooks/charmhelpers/contrib/hahelpers/ceph.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
|
log to syslog = %(use_syslog)s
|
||||||
|
err to syslog = %(use_syslog)s
|
||||||
|
clog to syslog = %(use_syslog)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)
|
183
hooks/charmhelpers/contrib/hahelpers/cluster.py
Normal file
183
hooks/charmhelpers/contrib/hahelpers/cluster.py
Normal 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_apache_port(public_port):
|
||||||
|
'''
|
||||||
|
Description: Determine correct apache listening port based on public IP +
|
||||||
|
state of the cluster.
|
||||||
|
|
||||||
|
public_port: int: standard public port for given service
|
||||||
|
|
||||||
|
returns: int: the correct listening port for the HAProxy service
|
||||||
|
'''
|
||||||
|
i = 0
|
||||||
|
if len(peer_units()) > 0 or is_clustered():
|
||||||
|
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)
|
0
hooks/charmhelpers/contrib/openstack/__init__.py
Normal file
0
hooks/charmhelpers/contrib/openstack/__init__.py
Normal file
17
hooks/charmhelpers/contrib/openstack/alternatives.py
Normal file
17
hooks/charmhelpers/contrib/openstack/alternatives.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
''' Helper for managing alternatives for file conflict resolution '''
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def install_alternative(name, target, source, priority=50):
|
||||||
|
''' Install alternative configuration '''
|
||||||
|
if (os.path.exists(target) and not os.path.islink(target)):
|
||||||
|
# Move existing file/directory away before installing
|
||||||
|
shutil.move(target, '{}.bak'.format(target))
|
||||||
|
cmd = [
|
||||||
|
'update-alternatives', '--force', '--install',
|
||||||
|
target, name, source, str(priority)
|
||||||
|
]
|
||||||
|
subprocess.check_call(cmd)
|
700
hooks/charmhelpers/contrib/openstack/context.py
Normal file
700
hooks/charmhelpers/contrib/openstack/context.py
Normal file
@ -0,0 +1,700 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.contrib.hahelpers.cluster import (
|
||||||
|
determine_apache_port,
|
||||||
|
determine_api_port,
|
||||||
|
https,
|
||||||
|
is_clustered
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def config_flags_parser(config_flags):
|
||||||
|
if config_flags.find('==') >= 0:
|
||||||
|
log("config_flags is not in expected format (key=value)",
|
||||||
|
level=ERROR)
|
||||||
|
raise OSContextError
|
||||||
|
# strip the following from each value.
|
||||||
|
post_strippers = ' ,'
|
||||||
|
# we strip any leading/trailing '=' or ' ' from the string then
|
||||||
|
# split on '='.
|
||||||
|
split = config_flags.strip(' =').split('=')
|
||||||
|
limit = len(split)
|
||||||
|
flags = {}
|
||||||
|
for i in xrange(0, limit - 1):
|
||||||
|
current = split[i]
|
||||||
|
next = split[i + 1]
|
||||||
|
vindex = next.rfind(',')
|
||||||
|
if (i == limit - 2) or (vindex < 0):
|
||||||
|
value = next
|
||||||
|
else:
|
||||||
|
value = next[:vindex]
|
||||||
|
|
||||||
|
if i == 0:
|
||||||
|
key = current
|
||||||
|
else:
|
||||||
|
# if this not the first entry, expect an embedded key.
|
||||||
|
index = current.rfind(',')
|
||||||
|
if index < 0:
|
||||||
|
log("invalid config value(s) at index %s" % (i),
|
||||||
|
level=ERROR)
|
||||||
|
raise OSContextError
|
||||||
|
key = current[index + 1:]
|
||||||
|
|
||||||
|
# Add to collection.
|
||||||
|
flags[key.strip(post_strippers)] = value.rstrip(post_strippers)
|
||||||
|
return flags
|
||||||
|
|
||||||
|
|
||||||
|
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, ssl_dir=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
|
||||||
|
self.ssl_dir = ssl_dir
|
||||||
|
|
||||||
|
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):
|
||||||
|
rdata = relation_get(rid=rid, unit=unit)
|
||||||
|
ctxt = {
|
||||||
|
'database_host': rdata.get('db_host'),
|
||||||
|
'database': self.database,
|
||||||
|
'database_user': self.user,
|
||||||
|
'database_password': rdata.get(password_setting),
|
||||||
|
'database_type': 'mysql'
|
||||||
|
}
|
||||||
|
if context_complete(ctxt):
|
||||||
|
db_ssl(rdata, ctxt, self.ssl_dir)
|
||||||
|
return ctxt
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class PostgresqlDBContext(OSContextGenerator):
|
||||||
|
interfaces = ['pgsql-db']
|
||||||
|
|
||||||
|
def __init__(self, database=None):
|
||||||
|
self.database = database
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self.database = self.database or config('database')
|
||||||
|
if self.database is None:
|
||||||
|
log('Could not generate postgresql_db context. '
|
||||||
|
'Missing required charm config options. '
|
||||||
|
'(database name)')
|
||||||
|
raise OSContextError
|
||||||
|
ctxt = {}
|
||||||
|
|
||||||
|
for rid in relation_ids(self.interfaces[0]):
|
||||||
|
for unit in related_units(rid):
|
||||||
|
ctxt = {
|
||||||
|
'database_host': relation_get('host', rid=rid, unit=unit),
|
||||||
|
'database': self.database,
|
||||||
|
'database_user': relation_get('user', rid=rid, unit=unit),
|
||||||
|
'database_password': relation_get('password', rid=rid, unit=unit),
|
||||||
|
'database_type': 'postgresql',
|
||||||
|
}
|
||||||
|
if context_complete(ctxt):
|
||||||
|
return ctxt
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def db_ssl(rdata, ctxt, ssl_dir):
|
||||||
|
if 'ssl_ca' in rdata and ssl_dir:
|
||||||
|
ca_path = os.path.join(ssl_dir, 'db-client.ca')
|
||||||
|
with open(ca_path, 'w') as fh:
|
||||||
|
fh.write(b64decode(rdata['ssl_ca']))
|
||||||
|
ctxt['database_ssl_ca'] = ca_path
|
||||||
|
elif 'ssl_ca' in rdata:
|
||||||
|
log("Charm not setup for ssl support but ssl ca found")
|
||||||
|
return ctxt
|
||||||
|
if 'ssl_cert' in rdata:
|
||||||
|
cert_path = os.path.join(
|
||||||
|
ssl_dir, 'db-client.cert')
|
||||||
|
if not os.path.exists(cert_path):
|
||||||
|
log("Waiting 1m for ssl client cert validity")
|
||||||
|
time.sleep(60)
|
||||||
|
with open(cert_path, 'w') as fh:
|
||||||
|
fh.write(b64decode(rdata['ssl_cert']))
|
||||||
|
ctxt['database_ssl_cert'] = cert_path
|
||||||
|
key_path = os.path.join(ssl_dir, 'db-client.key')
|
||||||
|
with open(key_path, 'w') as fh:
|
||||||
|
fh.write(b64decode(rdata['ssl_key']))
|
||||||
|
ctxt['database_ssl_key'] = key_path
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
rdata = relation_get(rid=rid, unit=unit)
|
||||||
|
ctxt = {
|
||||||
|
'service_port': rdata.get('service_port'),
|
||||||
|
'service_host': rdata.get('service_host'),
|
||||||
|
'auth_host': rdata.get('auth_host'),
|
||||||
|
'auth_port': rdata.get('auth_port'),
|
||||||
|
'admin_tenant_name': rdata.get('service_tenant'),
|
||||||
|
'admin_user': rdata.get('service_username'),
|
||||||
|
'admin_password': rdata.get('service_password'),
|
||||||
|
'service_protocol':
|
||||||
|
rdata.get('service_protocol') or 'http',
|
||||||
|
'auth_protocol':
|
||||||
|
rdata.get('auth_protocol') or 'http',
|
||||||
|
}
|
||||||
|
if context_complete(ctxt):
|
||||||
|
# NOTE(jamespage) this is required for >= icehouse
|
||||||
|
# so a missing value just indicates keystone needs
|
||||||
|
# upgrading
|
||||||
|
ctxt['admin_tenant_id'] = rdata.get('service_tenant_id')
|
||||||
|
return ctxt
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class AMQPContext(OSContextGenerator):
|
||||||
|
interfaces = ['amqp']
|
||||||
|
|
||||||
|
def __init__(self, ssl_dir=None):
|
||||||
|
self.ssl_dir = ssl_dir
|
||||||
|
|
||||||
|
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'):
|
||||||
|
ha_vip_only = False
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
|
||||||
|
if ssl_port:
|
||||||
|
ctxt['rabbit_ssl_port'] = ssl_port
|
||||||
|
ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
|
||||||
|
if ssl_ca:
|
||||||
|
ctxt['rabbit_ssl_ca'] = ssl_ca
|
||||||
|
|
||||||
|
if relation_get('ha_queues', rid=rid, unit=unit) is not None:
|
||||||
|
ctxt['rabbitmq_ha_queues'] = True
|
||||||
|
|
||||||
|
ha_vip_only = relation_get('ha-vip-only',
|
||||||
|
rid=rid, unit=unit) is not None
|
||||||
|
|
||||||
|
if context_complete(ctxt):
|
||||||
|
if 'rabbit_ssl_ca' in ctxt:
|
||||||
|
if not self.ssl_dir:
|
||||||
|
log(("Charm not setup for ssl support "
|
||||||
|
"but ssl ca found"))
|
||||||
|
break
|
||||||
|
ca_path = os.path.join(
|
||||||
|
self.ssl_dir, 'rabbit-client-ca.pem')
|
||||||
|
with open(ca_path, 'w') as fh:
|
||||||
|
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
|
||||||
|
ctxt['rabbit_ssl_ca'] = ca_path
|
||||||
|
# Sufficient information found = break out!
|
||||||
|
break
|
||||||
|
# Used for active/active rabbitmq >= grizzly
|
||||||
|
if ('clustered' not in ctxt or ha_vip_only) \
|
||||||
|
and len(related_units(rid)) > 1:
|
||||||
|
rabbitmq_hosts = []
|
||||||
|
for unit in related_units(rid):
|
||||||
|
rabbitmq_hosts.append(relation_get('private-address',
|
||||||
|
rid=rid, unit=unit))
|
||||||
|
ctxt['rabbitmq_hosts'] = ','.join(rabbitmq_hosts)
|
||||||
|
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
|
||||||
|
use_syslog = str(config('use-syslog')).lower()
|
||||||
|
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,
|
||||||
|
'use_syslog': use_syslog
|
||||||
|
}
|
||||||
|
|
||||||
|
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': []
|
||||||
|
}
|
||||||
|
if is_clustered():
|
||||||
|
ctxt['private_address'] = config('vip')
|
||||||
|
for api_port in self.external_ports:
|
||||||
|
ext_port = determine_apache_port(api_port)
|
||||||
|
int_port = determine_api_port(api_port)
|
||||||
|
portmap = (int(ext_port), int(int_port))
|
||||||
|
ctxt['endpoints'].append(portmap)
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
class NeutronContext(OSContextGenerator):
|
||||||
|
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/neutron/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)
|
||||||
|
config = neutron_plugin_attribute(self.plugin, 'config',
|
||||||
|
self.network_manager)
|
||||||
|
ovs_ctxt = {
|
||||||
|
'core_plugin': driver,
|
||||||
|
'neutron_plugin': 'ovs',
|
||||||
|
'neutron_security_groups': self.neutron_security_groups,
|
||||||
|
'local_ip': unit_private_ip(),
|
||||||
|
'config': config
|
||||||
|
}
|
||||||
|
|
||||||
|
return ovs_ctxt
|
||||||
|
|
||||||
|
def nvp_ctxt(self):
|
||||||
|
driver = neutron_plugin_attribute(self.plugin, 'driver',
|
||||||
|
self.network_manager)
|
||||||
|
config = neutron_plugin_attribute(self.plugin, 'config',
|
||||||
|
self.network_manager)
|
||||||
|
nvp_ctxt = {
|
||||||
|
'core_plugin': driver,
|
||||||
|
'neutron_plugin': 'nvp',
|
||||||
|
'neutron_security_groups': self.neutron_security_groups,
|
||||||
|
'local_ip': unit_private_ip(),
|
||||||
|
'config': config
|
||||||
|
}
|
||||||
|
|
||||||
|
return nvp_ctxt
|
||||||
|
|
||||||
|
def neutron_ctxt(self):
|
||||||
|
if https():
|
||||||
|
proto = 'https'
|
||||||
|
else:
|
||||||
|
proto = 'http'
|
||||||
|
if is_clustered():
|
||||||
|
host = config('vip')
|
||||||
|
else:
|
||||||
|
host = unit_get('private-address')
|
||||||
|
url = '%s://%s:%s' % (proto, host, '9696')
|
||||||
|
ctxt = {
|
||||||
|
'network_manager': self.network_manager,
|
||||||
|
'neutron_url': url,
|
||||||
|
}
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
self._ensure_packages()
|
||||||
|
|
||||||
|
if self.network_manager not in ['quantum', 'neutron']:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if not self.plugin:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
ctxt = self.neutron_ctxt()
|
||||||
|
|
||||||
|
if self.plugin == 'ovs':
|
||||||
|
ctxt.update(self.ovs_ctxt())
|
||||||
|
elif self.plugin == 'nvp':
|
||||||
|
ctxt.update(self.nvp_ctxt())
|
||||||
|
|
||||||
|
alchemy_flags = config('neutron-alchemy-flags')
|
||||||
|
if alchemy_flags:
|
||||||
|
flags = config_flags_parser(alchemy_flags)
|
||||||
|
ctxt['neutron_alchemy_flags'] = flags
|
||||||
|
|
||||||
|
self._save_flag_file()
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
||||||
|
class OSConfigFlagContext(OSContextGenerator):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Responsible for adding user-defined config-flags in charm config to a
|
||||||
|
template context.
|
||||||
|
|
||||||
|
NOTE: the value of config-flags may be a comma-separated list of
|
||||||
|
key=value pairs and some Openstack config files support
|
||||||
|
comma-separated lists as values.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
config_flags = config('config-flags')
|
||||||
|
if not config_flags:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
flags = config_flags_parser(config_flags)
|
||||||
|
return {'user_config_flags': flags}
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class SyslogContext(OSContextGenerator):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
ctxt = {
|
||||||
|
'use_syslog': config('use-syslog')
|
||||||
|
}
|
||||||
|
return ctxt
|
171
hooks/charmhelpers/contrib/openstack/neutron.py
Normal file
171
hooks/charmhelpers/contrib/openstack/neutron.py
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
QUANTUM_CONF_DIR = '/etc/quantum'
|
||||||
|
|
||||||
|
|
||||||
|
def kernel_version():
|
||||||
|
""" Retrieve the current major kernel version as a tuple e.g. (3, 13) """
|
||||||
|
kver = check_output(['uname', '-r']).strip()
|
||||||
|
kver = kver.split('.')
|
||||||
|
return (int(kver[0]), int(kver[1]))
|
||||||
|
|
||||||
|
|
||||||
|
def determine_dkms_package():
|
||||||
|
""" Determine which DKMS package should be used based on kernel version """
|
||||||
|
# NOTE: 3.13 kernels have support for GRE and VXLAN native
|
||||||
|
if kernel_version() >= (3, 13):
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
return ['openvswitch-datapath-dkms']
|
||||||
|
|
||||||
|
|
||||||
|
# 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',
|
||||||
|
ssl_dir=QUANTUM_CONF_DIR)],
|
||||||
|
'services': ['quantum-plugin-openvswitch-agent'],
|
||||||
|
'packages': [[headers_package()] + determine_dkms_package(),
|
||||||
|
['quantum-plugin-openvswitch-agent']],
|
||||||
|
'server_packages': ['quantum-server',
|
||||||
|
'quantum-plugin-openvswitch'],
|
||||||
|
'server_services': ['quantum-server']
|
||||||
|
},
|
||||||
|
'nvp': {
|
||||||
|
'config': '/etc/quantum/plugins/nicira/nvp.ini',
|
||||||
|
'driver': 'quantum.plugins.nicira.nicira_nvp_plugin.'
|
||||||
|
'QuantumPlugin.NvpPluginV2',
|
||||||
|
'contexts': [
|
||||||
|
context.SharedDBContext(user=config('neutron-database-user'),
|
||||||
|
database=config('neutron-database'),
|
||||||
|
relation_prefix='neutron',
|
||||||
|
ssl_dir=QUANTUM_CONF_DIR)],
|
||||||
|
'services': [],
|
||||||
|
'packages': [],
|
||||||
|
'server_packages': ['quantum-server',
|
||||||
|
'quantum-plugin-nicira'],
|
||||||
|
'server_services': ['quantum-server']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NEUTRON_CONF_DIR = '/etc/neutron'
|
||||||
|
|
||||||
|
|
||||||
|
def neutron_plugins():
|
||||||
|
from charmhelpers.contrib.openstack import context
|
||||||
|
release = os_release('nova-common')
|
||||||
|
plugins = {
|
||||||
|
'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',
|
||||||
|
ssl_dir=NEUTRON_CONF_DIR)],
|
||||||
|
'services': ['neutron-plugin-openvswitch-agent'],
|
||||||
|
'packages': [[headers_package()] + determine_dkms_package(),
|
||||||
|
['neutron-plugin-openvswitch-agent']],
|
||||||
|
'server_packages': ['neutron-server',
|
||||||
|
'neutron-plugin-openvswitch'],
|
||||||
|
'server_services': ['neutron-server']
|
||||||
|
},
|
||||||
|
'nvp': {
|
||||||
|
'config': '/etc/neutron/plugins/nicira/nvp.ini',
|
||||||
|
'driver': 'neutron.plugins.nicira.nicira_nvp_plugin.'
|
||||||
|
'NeutronPlugin.NvpPluginV2',
|
||||||
|
'contexts': [
|
||||||
|
context.SharedDBContext(user=config('neutron-database-user'),
|
||||||
|
database=config('neutron-database'),
|
||||||
|
relation_prefix='neutron',
|
||||||
|
ssl_dir=NEUTRON_CONF_DIR)],
|
||||||
|
'services': [],
|
||||||
|
'packages': [],
|
||||||
|
'server_packages': ['neutron-server',
|
||||||
|
'neutron-plugin-nicira'],
|
||||||
|
'server_services': ['neutron-server']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# NOTE: patch in ml2 plugin for icehouse onwards
|
||||||
|
if release >= 'icehouse':
|
||||||
|
plugins['ovs']['config'] = '/etc/neutron/plugins/ml2/ml2_conf.ini'
|
||||||
|
plugins['ovs']['driver'] = 'neutron.plugins.ml2.plugin.Ml2Plugin'
|
||||||
|
plugins['ovs']['server_packages'] = ['neutron-server',
|
||||||
|
'neutron-plugin-ml2']
|
||||||
|
return plugins
|
||||||
|
|
||||||
|
|
||||||
|
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 = 'neutron'
|
||||||
|
|
||||||
|
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'
|
@ -0,0 +1,2 @@
|
|||||||
|
# dummy __init__.py to fool syncer into thinking this is a syncable python
|
||||||
|
# module
|
15
hooks/charmhelpers/contrib/openstack/templates/ceph.conf
Normal file
15
hooks/charmhelpers/contrib/openstack/templates/ceph.conf
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# cinder configuration file maintained by Juju
|
||||||
|
# local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
[global]
|
||||||
|
{% if auth -%}
|
||||||
|
auth_supported = {{ auth }}
|
||||||
|
keyring = /etc/ceph/$cluster.$name.keyring
|
||||||
|
mon host = {{ mon_hosts }}
|
||||||
|
{% endif -%}
|
||||||
|
log to syslog = {{ use_syslog }}
|
||||||
|
err to syslog = {{ use_syslog }}
|
||||||
|
clog to syslog = {{ use_syslog }}
|
||||||
|
|
36
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg
Normal file
36
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
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 tcp
|
||||||
|
option tcplog
|
||||||
|
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
|
||||||
|
{% for unit, address in units.iteritems() -%}
|
||||||
|
server {{ unit }} {{ address }}:{{ ports[1] }} check
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor -%}
|
||||||
|
{% endif -%}
|
@ -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 -%}
|
@ -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 -%}
|
280
hooks/charmhelpers/contrib/openstack/templating.py
Normal file
280
hooks/charmhelpers/contrib/openstack/templating.py
Normal 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
|
448
hooks/charmhelpers/contrib/openstack/utils.py
Normal file
448
hooks/charmhelpers/contrib/openstack/utils.py
Normal file
@ -0,0 +1,448 @@
|
|||||||
|
#!/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,
|
||||||
|
ERROR,
|
||||||
|
INFO
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.contrib.storage.linux.lvm import (
|
||||||
|
deactivate_lvm_volume_group,
|
||||||
|
is_lvm_physical_volume,
|
||||||
|
remove_lvm_physical_volume,
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.core.host import lsb_release, mounts, umount
|
||||||
|
from charmhelpers.fetch import apt_install
|
||||||
|
from charmhelpers.contrib.storage.linux.utils import is_block_device, zap_disk
|
||||||
|
from charmhelpers.contrib.storage.linux.loopback import ensure_loopback_device
|
||||||
|
|
||||||
|
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
|
||||||
|
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
|
||||||
|
|
||||||
|
DISTRO_PROPOSED = ('deb http://archive.ubuntu.com/ubuntu/ %s-proposed '
|
||||||
|
'restricted main multiverse universe')
|
||||||
|
|
||||||
|
|
||||||
|
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
|
||||||
|
('oneiric', 'diablo'),
|
||||||
|
('precise', 'essex'),
|
||||||
|
('quantal', 'folsom'),
|
||||||
|
('raring', 'grizzly'),
|
||||||
|
('saucy', 'havana'),
|
||||||
|
('trusty', 'icehouse')
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
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'),
|
||||||
|
('1.13.1', 'icehouse'),
|
||||||
|
('1.13.0', 'icehouse'),
|
||||||
|
('1.12.0', 'icehouse'),
|
||||||
|
('1.11.0', 'icehouse'),
|
||||||
|
])
|
||||||
|
|
||||||
|
DEFAULT_LOOPBACK_SIZE = '5G'
|
||||||
|
|
||||||
|
|
||||||
|
def error_out(msg):
|
||||||
|
juju_log("FATAL ERROR: %s" % msg, level='ERROR')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_codename_install_source(src):
|
||||||
|
'''Derive OpenStack release codename from a given installation source.'''
|
||||||
|
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
||||||
|
rel = ''
|
||||||
|
if src in ['distro', 'distro-proposed']:
|
||||||
|
try:
|
||||||
|
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
|
||||||
|
except KeyError:
|
||||||
|
e = 'Could not derive openstack release for '\
|
||||||
|
'this Ubuntu release: %s' % ubuntu_rel
|
||||||
|
error_out(e)
|
||||||
|
return rel
|
||||||
|
|
||||||
|
if src.startswith('cloud:'):
|
||||||
|
ca_rel = src.split(':')[1]
|
||||||
|
ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
|
||||||
|
return ca_rel
|
||||||
|
|
||||||
|
# Best guess match based on deb string provided
|
||||||
|
if src.startswith('deb') or src.startswith('ppa'):
|
||||||
|
for k, v in OPENSTACK_CODENAMES.iteritems():
|
||||||
|
if v in src:
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_version_install_source(src):
|
||||||
|
codename = get_os_codename_install_source(src)
|
||||||
|
return get_os_version_codename(codename)
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_codename_version(vers):
|
||||||
|
'''Determine OpenStack codename from version number.'''
|
||||||
|
try:
|
||||||
|
return OPENSTACK_CODENAMES[vers]
|
||||||
|
except KeyError:
|
||||||
|
e = 'Could not determine OpenStack codename for version %s' % vers
|
||||||
|
error_out(e)
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_version_codename(codename):
|
||||||
|
'''Determine OpenStack version number from codename.'''
|
||||||
|
for k, v in OPENSTACK_CODENAMES.iteritems():
|
||||||
|
if v == codename:
|
||||||
|
return k
|
||||||
|
e = 'Could not derive OpenStack version for '\
|
||||||
|
'codename: %s' % codename
|
||||||
|
error_out(e)
|
||||||
|
|
||||||
|
|
||||||
|
def get_os_codename_package(package, fatal=True):
|
||||||
|
'''Derive OpenStack release codename from an installed package.'''
|
||||||
|
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 hkp://keyserver.ubuntu.com:80 " \
|
||||||
|
"--recv-keys %s" % keyid
|
||||||
|
try:
|
||||||
|
subprocess.check_call(cmd.split(' '))
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
error_out("Error importing repo key %s" % keyid)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_installation_source(rel):
|
||||||
|
'''Configure apt installation source.'''
|
||||||
|
if rel == 'distro':
|
||||||
|
return
|
||||||
|
elif rel == 'distro-proposed':
|
||||||
|
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
||||||
|
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
|
||||||
|
f.write(DISTRO_PROPOSED % ubuntu_rel)
|
||||||
|
elif rel[:4] == "ppa:":
|
||||||
|
src = rel
|
||||||
|
subprocess.check_call(["add-apt-repository", "-y", src])
|
||||||
|
elif rel[:3] == "deb":
|
||||||
|
l = len(rel.split('|'))
|
||||||
|
if l == 2:
|
||||||
|
src, key = rel.split('|')
|
||||||
|
juju_log("Importing PPA key from keyserver for %s" % src)
|
||||||
|
import_key(key)
|
||||||
|
elif l == 1:
|
||||||
|
src = rel
|
||||||
|
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
|
||||||
|
f.write(src)
|
||||||
|
elif rel[:6] == 'cloud:':
|
||||||
|
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
||||||
|
rel = rel.split(':')[1]
|
||||||
|
u_rel = rel.split('-')[0]
|
||||||
|
ca_rel = rel.split('-')[1]
|
||||||
|
|
||||||
|
if u_rel != ubuntu_rel:
|
||||||
|
e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
|
||||||
|
'version (%s)' % (ca_rel, ubuntu_rel)
|
||||||
|
error_out(e)
|
||||||
|
|
||||||
|
if 'staging' in ca_rel:
|
||||||
|
# staging is just a regular PPA.
|
||||||
|
os_rel = ca_rel.split('/')[0]
|
||||||
|
ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
|
||||||
|
cmd = 'add-apt-repository -y %s' % ppa
|
||||||
|
subprocess.check_call(cmd.split(' '))
|
||||||
|
return
|
||||||
|
|
||||||
|
# map charm config options to actual archive pockets.
|
||||||
|
pockets = {
|
||||||
|
'folsom': 'precise-updates/folsom',
|
||||||
|
'folsom/updates': 'precise-updates/folsom',
|
||||||
|
'folsom/proposed': 'precise-proposed/folsom',
|
||||||
|
'grizzly': 'precise-updates/grizzly',
|
||||||
|
'grizzly/updates': 'precise-updates/grizzly',
|
||||||
|
'grizzly/proposed': 'precise-proposed/grizzly',
|
||||||
|
'havana': 'precise-updates/havana',
|
||||||
|
'havana/updates': 'precise-updates/havana',
|
||||||
|
'havana/proposed': 'precise-proposed/havana',
|
||||||
|
'icehouse': 'precise-updates/icehouse',
|
||||||
|
'icehouse/updates': 'precise-updates/icehouse',
|
||||||
|
'icehouse/proposed': 'precise-proposed/icehouse',
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ensure_block_device(block_device):
|
||||||
|
'''
|
||||||
|
Confirm block_device, create as loopback if necessary.
|
||||||
|
|
||||||
|
:param block_device: str: Full path of block device to ensure.
|
||||||
|
|
||||||
|
:returns: str: Full path of ensured block device.
|
||||||
|
'''
|
||||||
|
_none = ['None', 'none', None]
|
||||||
|
if (block_device in _none):
|
||||||
|
error_out('prepare_storage(): Missing required input: '
|
||||||
|
'block_device=%s.' % block_device, level=ERROR)
|
||||||
|
|
||||||
|
if block_device.startswith('/dev/'):
|
||||||
|
bdev = block_device
|
||||||
|
elif block_device.startswith('/'):
|
||||||
|
_bd = block_device.split('|')
|
||||||
|
if len(_bd) == 2:
|
||||||
|
bdev, size = _bd
|
||||||
|
else:
|
||||||
|
bdev = block_device
|
||||||
|
size = DEFAULT_LOOPBACK_SIZE
|
||||||
|
bdev = ensure_loopback_device(bdev, size)
|
||||||
|
else:
|
||||||
|
bdev = '/dev/%s' % block_device
|
||||||
|
|
||||||
|
if not is_block_device(bdev):
|
||||||
|
error_out('Failed to locate valid block device at %s' % bdev,
|
||||||
|
level=ERROR)
|
||||||
|
|
||||||
|
return bdev
|
||||||
|
|
||||||
|
|
||||||
|
def clean_storage(block_device):
|
||||||
|
'''
|
||||||
|
Ensures a block device is clean. That is:
|
||||||
|
- unmounted
|
||||||
|
- any lvm volume groups are deactivated
|
||||||
|
- any lvm physical device signatures removed
|
||||||
|
- partition table wiped
|
||||||
|
|
||||||
|
:param block_device: str: Full path to block device to clean.
|
||||||
|
'''
|
||||||
|
for mp, d in mounts():
|
||||||
|
if d == block_device:
|
||||||
|
juju_log('clean_storage(): %s is mounted @ %s, unmounting.' %
|
||||||
|
(d, mp), level=INFO)
|
||||||
|
umount(mp, persist=True)
|
||||||
|
|
||||||
|
if is_lvm_physical_volume(block_device):
|
||||||
|
deactivate_lvm_volume_group(block_device)
|
||||||
|
remove_lvm_physical_volume(block_device)
|
||||||
|
else:
|
||||||
|
zap_disk(block_device)
|
||||||
|
|
||||||
|
|
||||||
|
def is_ip(address):
|
||||||
|
"""
|
||||||
|
Returns True if address is a valid IP address.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Test to see if already an IPv4 address
|
||||||
|
socket.inet_aton(address)
|
||||||
|
return True
|
||||||
|
except socket.error:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def ns_query(address):
|
||||||
|
try:
|
||||||
|
import dns.resolver
|
||||||
|
except ImportError:
|
||||||
|
apt_install('python-dnspython')
|
||||||
|
import dns.resolver
|
||||||
|
|
||||||
|
if isinstance(address, dns.name.Name):
|
||||||
|
rtype = 'PTR'
|
||||||
|
elif isinstance(address, basestring):
|
||||||
|
rtype = 'A'
|
||||||
|
|
||||||
|
answers = dns.resolver.query(address, rtype)
|
||||||
|
if answers:
|
||||||
|
return str(answers[0])
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_host_ip(hostname):
|
||||||
|
"""
|
||||||
|
Resolves the IP for a given hostname, or returns
|
||||||
|
the input if it is already an IP.
|
||||||
|
"""
|
||||||
|
if is_ip(hostname):
|
||||||
|
return hostname
|
||||||
|
|
||||||
|
return ns_query(hostname)
|
||||||
|
|
||||||
|
|
||||||
|
def get_hostname(address, fqdn=True):
|
||||||
|
"""
|
||||||
|
Resolves hostname for given IP, or returns the input
|
||||||
|
if it is already a hostname.
|
||||||
|
"""
|
||||||
|
if is_ip(address):
|
||||||
|
try:
|
||||||
|
import dns.reversename
|
||||||
|
except ImportError:
|
||||||
|
apt_install('python-dnspython')
|
||||||
|
import dns.reversename
|
||||||
|
|
||||||
|
rev = dns.reversename.from_address(address)
|
||||||
|
result = ns_query(rev)
|
||||||
|
if not result:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
result = address
|
||||||
|
|
||||||
|
if fqdn:
|
||||||
|
# strip trailing .
|
||||||
|
if result.endswith('.'):
|
||||||
|
return result[:-1]
|
||||||
|
else:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
return result.split('.')[0]
|
0
hooks/charmhelpers/contrib/storage/__init__.py
Normal file
0
hooks/charmhelpers/contrib/storage/__init__.py
Normal file
387
hooks/charmhelpers/contrib/storage/linux/ceph.py
Normal file
387
hooks/charmhelpers/contrib/storage/linux/ceph.py
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
#
|
||||||
|
# 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}
|
||||||
|
log to syslog = {use_syslog}
|
||||||
|
err to syslog = {use_syslog}
|
||||||
|
clog to syslog = {use_syslog}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
'''
|
||||||
|
version = ceph_version()
|
||||||
|
if version and version >= '0.56':
|
||||||
|
return json.loads(check_output(['ceph', '--id', service,
|
||||||
|
'osd', 'ls', '--format=json']))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
osds = get_osds(service)
|
||||||
|
if osds:
|
||||||
|
pgnum = (len(osds) * 100 / replicas)
|
||||||
|
else:
|
||||||
|
# NOTE(james-page): Default to 200 for older ceph versions
|
||||||
|
# which don't support OSD query from cli
|
||||||
|
pgnum = 200
|
||||||
|
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, use_syslog):
|
||||||
|
''' 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)),
|
||||||
|
use_syslog=use_syslog))
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def ceph_version():
|
||||||
|
''' Retrieve the local version of ceph '''
|
||||||
|
if os.path.exists('/usr/bin/ceph'):
|
||||||
|
cmd = ['ceph', '-v']
|
||||||
|
output = check_output(cmd)
|
||||||
|
output = output.split()
|
||||||
|
if len(output) > 3:
|
||||||
|
return output[2]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return None
|
62
hooks/charmhelpers/contrib/storage/linux/loopback.py
Normal file
62
hooks/charmhelpers/contrib/storage/linux/loopback.py
Normal 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)
|
88
hooks/charmhelpers/contrib/storage/linux/lvm.py
Normal file
88
hooks/charmhelpers/contrib/storage/linux/lvm.py
Normal 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])
|
35
hooks/charmhelpers/contrib/storage/linux/utils.py
Normal file
35
hooks/charmhelpers/contrib/storage/linux/utils.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from os import stat
|
||||||
|
from stat import S_ISBLK
|
||||||
|
|
||||||
|
from subprocess import (
|
||||||
|
check_call,
|
||||||
|
check_output,
|
||||||
|
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.
|
||||||
|
'''
|
||||||
|
# sometimes sgdisk exits non-zero; this is OK, dd will clean up
|
||||||
|
call(['sgdisk', '--zap-all', '--mbrtogpt',
|
||||||
|
'--clear', block_device])
|
||||||
|
dev_end = check_output(['blockdev', '--getsz', block_device])
|
||||||
|
gpt_end = int(dev_end.split()[0]) - 100
|
||||||
|
check_call(['dd', 'if=/dev/zero', 'of=%s'%(block_device),
|
||||||
|
'bs=1M', 'count=1'])
|
||||||
|
check_call(['dd', 'if=/dev/zero', 'of=%s'%(block_device),
|
||||||
|
'bs=512', 'count=100', 'seek=%s'%(gpt_end)])
|
0
hooks/charmhelpers/core/__init__.py
Normal file
0
hooks/charmhelpers/core/__init__.py
Normal file
401
hooks/charmhelpers/core/hookenv.py
Normal file
401
hooks/charmhelpers/core/hookenv.py
Normal file
@ -0,0 +1,401 @@
|
|||||||
|
"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 sys
|
||||||
|
import UserDict
|
||||||
|
from subprocess import CalledProcessError
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
def hook_name():
|
||||||
|
"""The name of the currently executing hook"""
|
||||||
|
return os.path.basename(sys.argv[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):
|
||||||
|
"""Get relation information"""
|
||||||
|
_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
|
||||||
|
except CalledProcessError, e:
|
||||||
|
if e.returncode == 2:
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def relation_set(relation_id=None, relation_settings={}, **kwargs):
|
||||||
|
"""Set relation information for the current unit"""
|
||||||
|
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():
|
||||||
|
"""Get a nested dictionary of relation data for all related units"""
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@cached
|
||||||
|
def is_relation_made(relation, keys='private-address'):
|
||||||
|
'''
|
||||||
|
Determine whether a relation is established by checking for
|
||||||
|
presence of key(s). If a list of keys is provided, they
|
||||||
|
must all be present for the relation to be identified as made
|
||||||
|
'''
|
||||||
|
if isinstance(keys, str):
|
||||||
|
keys = [keys]
|
||||||
|
for r_id in relation_ids(relation):
|
||||||
|
for unit in related_units(r_id):
|
||||||
|
context = {}
|
||||||
|
for k in keys:
|
||||||
|
context[k] = relation_get(k, rid=r_id,
|
||||||
|
unit=unit)
|
||||||
|
if None not in context.values():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""Get the unit ID for the remote unit"""
|
||||||
|
_args = ['unit-get', '--format=json', attribute]
|
||||||
|
try:
|
||||||
|
return json.loads(subprocess.check_output(_args))
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def unit_private_ip():
|
||||||
|
"""Get this unit's private IP address"""
|
||||||
|
return unit_get('private-address')
|
||||||
|
|
||||||
|
|
||||||
|
class UnregisteredHookError(Exception):
|
||||||
|
"""Raised when an undefined hook is called"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Hooks(object):
|
||||||
|
"""A convenient handler for hook functions.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
hooks = Hooks()
|
||||||
|
|
||||||
|
# register a hook, taking its name from the function name
|
||||||
|
@hooks.hook()
|
||||||
|
def install():
|
||||||
|
...
|
||||||
|
|
||||||
|
# register a hook, providing a custom hook name
|
||||||
|
@hooks.hook("config-changed")
|
||||||
|
def config_changed():
|
||||||
|
...
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# execute a hook based on the name the program is called by
|
||||||
|
hooks.execute(sys.argv)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Hooks, self).__init__()
|
||||||
|
self._hooks = {}
|
||||||
|
|
||||||
|
def register(self, name, function):
|
||||||
|
"""Register a hook"""
|
||||||
|
self._hooks[name] = function
|
||||||
|
|
||||||
|
def execute(self, args):
|
||||||
|
"""Execute a registered hook based on args[0]"""
|
||||||
|
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):
|
||||||
|
"""Decorator, registering them as hooks"""
|
||||||
|
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 the root directory of the current charm"""
|
||||||
|
return os.environ.get('CHARM_DIR')
|
297
hooks/charmhelpers/core/host.py
Normal file
297
hooks/charmhelpers/core/host.py
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
"""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):
|
||||||
|
"""Start a system service"""
|
||||||
|
return service('start', service_name)
|
||||||
|
|
||||||
|
|
||||||
|
def service_stop(service_name):
|
||||||
|
"""Stop a system service"""
|
||||||
|
return service('stop', service_name)
|
||||||
|
|
||||||
|
|
||||||
|
def service_restart(service_name):
|
||||||
|
"""Restart a system service"""
|
||||||
|
return service('restart', service_name)
|
||||||
|
|
||||||
|
|
||||||
|
def service_reload(service_name, restart_on_failure=False):
|
||||||
|
"""Reload a system service, optionally falling back to restart if reload fails"""
|
||||||
|
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):
|
||||||
|
"""Control a system service"""
|
||||||
|
cmd = ['service', service_name, action]
|
||||||
|
return subprocess.call(cmd) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def service_running(service):
|
||||||
|
"""Determine whether a system service is running"""
|
||||||
|
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 to the system"""
|
||||||
|
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 at a particular mountpoint"""
|
||||||
|
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():
|
||||||
|
"""Get a 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, stopstart=False):
|
||||||
|
"""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]
|
||||||
|
services_list = list(OrderedDict.fromkeys(restarts))
|
||||||
|
if not stopstart:
|
||||||
|
for service_name in services_list:
|
||||||
|
service('restart', service_name)
|
||||||
|
else:
|
||||||
|
for action in ['stop', 'start']:
|
||||||
|
for service_name in services_list:
|
||||||
|
service(action, 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))
|
||||||
|
|
||||||
|
|
||||||
|
def list_nics(nic_type):
|
||||||
|
'''Return a list of nics of given type(s)'''
|
||||||
|
if isinstance(nic_type, basestring):
|
||||||
|
int_types = [nic_type]
|
||||||
|
else:
|
||||||
|
int_types = nic_type
|
||||||
|
interfaces = []
|
||||||
|
for int_type in int_types:
|
||||||
|
cmd = ['ip', 'addr', 'show', 'label', int_type + '*']
|
||||||
|
ip_output = subprocess.check_output(cmd).split('\n')
|
||||||
|
ip_output = (line for line in ip_output if line)
|
||||||
|
for line in ip_output:
|
||||||
|
if line.split()[1].startswith(int_type):
|
||||||
|
interfaces.append(line.split()[1].replace(":", ""))
|
||||||
|
return interfaces
|
||||||
|
|
||||||
|
|
||||||
|
def set_nic_mtu(nic, mtu):
|
||||||
|
'''Set MTU on a network interface'''
|
||||||
|
cmd = ['ip', 'link', 'set', nic, 'mtu', mtu]
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def get_nic_mtu(nic):
|
||||||
|
cmd = ['ip', 'addr', 'show', nic]
|
||||||
|
ip_output = subprocess.check_output(cmd).split('\n')
|
||||||
|
mtu = ""
|
||||||
|
for line in ip_output:
|
||||||
|
words = line.split()
|
||||||
|
if 'mtu' in words:
|
||||||
|
mtu = words[words.index("mtu") + 1]
|
||||||
|
return mtu
|
||||||
|
|
||||||
|
|
||||||
|
def get_nic_hwaddr(nic):
|
||||||
|
cmd = ['ip', '-o', '-0', 'addr', 'show', nic]
|
||||||
|
ip_output = subprocess.check_output(cmd)
|
||||||
|
hwaddr = ""
|
||||||
|
words = ip_output.split()
|
||||||
|
if 'link/ether' in words:
|
||||||
|
hwaddr = words[words.index('link/ether') + 1]
|
||||||
|
return hwaddr
|
308
hooks/charmhelpers/fetch/__init__.py
Normal file
308
hooks/charmhelpers/fetch/__init__.py
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
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
|
||||||
|
import os
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
CLOUD_ARCHIVE_POCKETS = {
|
||||||
|
# Folsom
|
||||||
|
'folsom': 'precise-updates/folsom',
|
||||||
|
'precise-folsom': 'precise-updates/folsom',
|
||||||
|
'precise-folsom/updates': 'precise-updates/folsom',
|
||||||
|
'precise-updates/folsom': 'precise-updates/folsom',
|
||||||
|
'folsom/proposed': 'precise-proposed/folsom',
|
||||||
|
'precise-folsom/proposed': 'precise-proposed/folsom',
|
||||||
|
'precise-proposed/folsom': 'precise-proposed/folsom',
|
||||||
|
# Grizzly
|
||||||
|
'grizzly': 'precise-updates/grizzly',
|
||||||
|
'precise-grizzly': 'precise-updates/grizzly',
|
||||||
|
'precise-grizzly/updates': 'precise-updates/grizzly',
|
||||||
|
'precise-updates/grizzly': 'precise-updates/grizzly',
|
||||||
|
'grizzly/proposed': 'precise-proposed/grizzly',
|
||||||
|
'precise-grizzly/proposed': 'precise-proposed/grizzly',
|
||||||
|
'precise-proposed/grizzly': 'precise-proposed/grizzly',
|
||||||
|
# Havana
|
||||||
|
'havana': 'precise-updates/havana',
|
||||||
|
'precise-havana': 'precise-updates/havana',
|
||||||
|
'precise-havana/updates': 'precise-updates/havana',
|
||||||
|
'precise-updates/havana': 'precise-updates/havana',
|
||||||
|
'havana/proposed': 'precise-proposed/havana',
|
||||||
|
'precise-havana/proposed': 'precise-proposed/havana',
|
||||||
|
'precise-proposed/havana': 'precise-proposed/havana',
|
||||||
|
# Icehouse
|
||||||
|
'icehouse': 'precise-updates/icehouse',
|
||||||
|
'precise-icehouse': 'precise-updates/icehouse',
|
||||||
|
'precise-icehouse/updates': 'precise-updates/icehouse',
|
||||||
|
'precise-updates/icehouse': 'precise-updates/icehouse',
|
||||||
|
'icehouse/proposed': 'precise-proposed/icehouse',
|
||||||
|
'precise-icehouse/proposed': 'precise-proposed/icehouse',
|
||||||
|
'precise-proposed/icehouse': 'precise-proposed/icehouse',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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"""
|
||||||
|
if options is None:
|
||||||
|
options = ['--option=Dpkg::Options::=--force-confold']
|
||||||
|
|
||||||
|
cmd = ['apt-get', '--assume-yes']
|
||||||
|
cmd.extend(options)
|
||||||
|
cmd.append('install')
|
||||||
|
if isinstance(packages, basestring):
|
||||||
|
cmd.append(packages)
|
||||||
|
else:
|
||||||
|
cmd.extend(packages)
|
||||||
|
log("Installing {} with options: {}".format(packages,
|
||||||
|
options))
|
||||||
|
env = os.environ.copy()
|
||||||
|
if 'DEBIAN_FRONTEND' not in env:
|
||||||
|
env['DEBIAN_FRONTEND'] = 'noninteractive'
|
||||||
|
|
||||||
|
if fatal:
|
||||||
|
subprocess.check_call(cmd, env=env)
|
||||||
|
else:
|
||||||
|
subprocess.call(cmd, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def apt_upgrade(options=None, fatal=False, dist=False):
|
||||||
|
"""Upgrade all packages"""
|
||||||
|
if options is None:
|
||||||
|
options = ['--option=Dpkg::Options::=--force-confold']
|
||||||
|
|
||||||
|
cmd = ['apt-get', '--assume-yes']
|
||||||
|
cmd.extend(options)
|
||||||
|
if dist:
|
||||||
|
cmd.append('dist-upgrade')
|
||||||
|
else:
|
||||||
|
cmd.append('upgrade')
|
||||||
|
log("Upgrading with options: {}".format(options))
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
if 'DEBIAN_FRONTEND' not in env:
|
||||||
|
env['DEBIAN_FRONTEND'] = 'noninteractive'
|
||||||
|
|
||||||
|
if fatal:
|
||||||
|
subprocess.check_call(cmd, env=env)
|
||||||
|
else:
|
||||||
|
subprocess.call(cmd, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
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', '--assume-yes', '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 apt_hold(packages, fatal=False):
|
||||||
|
"""Hold one or more packages"""
|
||||||
|
cmd = ['apt-mark', 'hold']
|
||||||
|
if isinstance(packages, basestring):
|
||||||
|
cmd.append(packages)
|
||||||
|
else:
|
||||||
|
cmd.extend(packages)
|
||||||
|
log("Holding {}".format(packages))
|
||||||
|
if fatal:
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
else:
|
||||||
|
subprocess.call(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def add_source(source, key=None):
|
||||||
|
if source is None:
|
||||||
|
log('Source is not present. Skipping')
|
||||||
|
return
|
||||||
|
|
||||||
|
if (source.startswith('ppa:') or
|
||||||
|
source.startswith('http') or
|
||||||
|
source.startswith('deb ') or
|
||||||
|
source.startswith('cloud-archive:')):
|
||||||
|
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]
|
||||||
|
if pocket not in CLOUD_ARCHIVE_POCKETS:
|
||||||
|
raise SourceConfigError(
|
||||||
|
'Unsupported cloud: source option %s' %
|
||||||
|
pocket)
|
||||||
|
actual_pocket = CLOUD_ARCHIVE_POCKETS[pocket]
|
||||||
|
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
|
||||||
|
apt.write(CLOUD_ARCHIVE.format(actual_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', 'adv', '--keyserver',
|
||||||
|
'keyserver.ubuntu.com', '--recv',
|
||||||
|
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 = config(keys_var)
|
||||||
|
if keys is not None:
|
||||||
|
keys = safe_load(keys)
|
||||||
|
if isinstance(sources, basestring) and (
|
||||||
|
keys is None or 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
|
63
hooks/charmhelpers/fetch/archiveurl.py
Normal file
63
hooks/charmhelpers/fetch/archiveurl.py
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import os
|
||||||
|
import urllib2
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
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
|
||||||
|
proto, netloc, path, params, query, fragment = urlparse.urlparse(source)
|
||||||
|
if proto in ('http', 'https'):
|
||||||
|
auth, barehost = urllib2.splituser(netloc)
|
||||||
|
if auth is not None:
|
||||||
|
source = urlparse.urlunparse((proto, barehost, path, params, query, fragment))
|
||||||
|
username, password = urllib2.splitpasswd(auth)
|
||||||
|
passman = urllib2.HTTPPasswordMgrWithDefaultRealm()
|
||||||
|
# Realm is set to None in add_password to force the username and password
|
||||||
|
# to be used whatever the realm
|
||||||
|
passman.add_password(None, source, username, password)
|
||||||
|
authhandler = urllib2.HTTPBasicAuthHandler(passman)
|
||||||
|
opener = urllib2.build_opener(authhandler)
|
||||||
|
urllib2.install_opener(opener)
|
||||||
|
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)
|
49
hooks/charmhelpers/fetch/bzrurl.py
Normal file
49
hooks/charmhelpers/fetch/bzrurl.py
Normal 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
|
1
hooks/charmhelpers/payload/__init__.py
Normal file
1
hooks/charmhelpers/payload/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"Tools for working with files injected into a charm just before deployment."
|
50
hooks/charmhelpers/payload/execd.py
Normal file
50
hooks/charmhelpers/payload/execd.py
Normal 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)
|
1
hooks/config-changed
Symbolic link
1
hooks/config-changed
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/identity-service-relation-broken
Symbolic link
1
hooks/identity-service-relation-broken
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/identity-service-relation-changed
Symbolic link
1
hooks/identity-service-relation-changed
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/identity-service-relation-joined
Symbolic link
1
hooks/identity-service-relation-joined
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/install
Symbolic link
1
hooks/install
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/neutron-api-relation-broken
Symbolic link
1
hooks/neutron-api-relation-broken
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/neutron-api-relation-changed
Symbolic link
1
hooks/neutron-api-relation-changed
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/neutron-api-relation-joined
Symbolic link
1
hooks/neutron-api-relation-joined
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
60
hooks/neutron_api_context.py
Normal file
60
hooks/neutron_api_context.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
config,
|
||||||
|
relation_ids,
|
||||||
|
related_units,
|
||||||
|
relation_get,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.openstack import context
|
||||||
|
|
||||||
|
class NeutronPostgresqlDBContext(context.PostgresqlDBContext):
|
||||||
|
interfaces = ['pgsql-neutron-db']
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(NeutronPostgresqlDBContext,
|
||||||
|
self).__init__(config('neutron-database'))
|
||||||
|
|
||||||
|
class IdentityServiceContext(context.IdentityServiceContext):
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
ctxt = super(IdentityServiceContext, self).__call__()
|
||||||
|
if not ctxt:
|
||||||
|
return
|
||||||
|
|
||||||
|
# the ec2 api needs to know the location of the keystone ec2
|
||||||
|
# tokens endpoint, set in nova.conf
|
||||||
|
ec2_tokens = '%s://%s:%s/v2.0/ec2tokens' % (
|
||||||
|
ctxt['service_protocol'] or 'http',
|
||||||
|
ctxt['service_host'],
|
||||||
|
ctxt['service_port']
|
||||||
|
)
|
||||||
|
ctxt['keystone_ec2_url'] = ec2_tokens
|
||||||
|
ctxt['region'] = config('region')
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
class NeutronCCContext(context.NeutronContext):
|
||||||
|
interfaces = []
|
||||||
|
|
||||||
|
@property
|
||||||
|
def network_manager(self):
|
||||||
|
return 'neutron'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def plugin(self):
|
||||||
|
return config('neutron-plugin')
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def neutron_security_groups(self):
|
||||||
|
sec_groups = config('neutron-security-groups')
|
||||||
|
return sec_groups.lower() == 'yes'
|
||||||
|
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
ctxt = super(NeutronCCContext, self).__call__()
|
||||||
|
ctxt['external_network'] = config('neutron-external-network')
|
||||||
|
for rid in relation_ids('neutron-api'):
|
||||||
|
for unit in related_units(rid):
|
||||||
|
ctxt['nova_url'] = relation_get(attribute='nova_url', rid=rid, unit=unit)
|
||||||
|
return ctxt
|
||||||
|
|
||||||
|
|
244
hooks/neutron_api_hooks.py
Executable file
244
hooks/neutron_api_hooks.py
Executable file
@ -0,0 +1,244 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
Hooks,
|
||||||
|
UnregisteredHookError,
|
||||||
|
config,
|
||||||
|
is_relation_made,
|
||||||
|
log,
|
||||||
|
ERROR,
|
||||||
|
relation_get,
|
||||||
|
relation_ids,
|
||||||
|
relation_set,
|
||||||
|
related_units,
|
||||||
|
open_port,
|
||||||
|
unit_get,
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.core.host import (
|
||||||
|
restart_on_change
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.fetch import (
|
||||||
|
apt_install, apt_update
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.contrib.openstack.utils import (
|
||||||
|
configure_installation_source,
|
||||||
|
openstack_upgrade_available,
|
||||||
|
)
|
||||||
|
from charmhelpers.contrib.openstack.neutron import (
|
||||||
|
network_manager,
|
||||||
|
neutron_plugin_attribute,
|
||||||
|
)
|
||||||
|
|
||||||
|
from neutron_api_utils import (
|
||||||
|
determine_endpoints,
|
||||||
|
determine_packages,
|
||||||
|
determine_ports,
|
||||||
|
register_configs,
|
||||||
|
restart_map,
|
||||||
|
NEUTRON_CONF,
|
||||||
|
api_port,
|
||||||
|
auth_token_config,
|
||||||
|
keystone_ca_cert_b64,
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.contrib.hahelpers.cluster import (
|
||||||
|
canonical_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.payload.execd import execd_preinstall
|
||||||
|
|
||||||
|
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)
|
||||||
|
[open_port(port) for port in determine_ports()]
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.hook('config-changed')
|
||||||
|
@restart_on_change(restart_map(), stopstart=True)
|
||||||
|
def config_changed():
|
||||||
|
global CONFIGS
|
||||||
|
CONFIGS.write_all()
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.hook('amqp-relation-joined')
|
||||||
|
def amqp_joined(relation_id=None):
|
||||||
|
relation_set(relation_id=relation_id,
|
||||||
|
username=config('rabbit-user'), vhost=config('rabbit-vhost'))
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.hook('amqp-relation-changed')
|
||||||
|
@hooks.hook('amqp-relation-departed')
|
||||||
|
@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(NEUTRON_CONF)
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.hook('shared-db-relation-joined')
|
||||||
|
def db_joined():
|
||||||
|
if is_relation_made('pgsql-nova-db') or \
|
||||||
|
is_relation_made('pgsql-neutron-db'):
|
||||||
|
# error, postgresql is used
|
||||||
|
e = ('Attempting to associate a mysql database when there is already '
|
||||||
|
'associated a postgresql one')
|
||||||
|
log(e, level=ERROR)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
|
# XXX: Renaming relations from quantum_* to neutron_* here.
|
||||||
|
relation_set(neutron_database=config('neutron-database'),
|
||||||
|
neutron_username=config('neutron-database-user'),
|
||||||
|
neutron_hostname=unit_get('private-address'))
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.hook('pgsql-neutron-db-relation-joined')
|
||||||
|
def pgsql_neutron_db_joined():
|
||||||
|
if is_relation_made('shared-db'):
|
||||||
|
# raise error
|
||||||
|
e = ('Attempting to associate a postgresql database'
|
||||||
|
' when there is already associated a mysql one')
|
||||||
|
log(e, level=ERROR)
|
||||||
|
raise Exception(e)
|
||||||
|
|
||||||
|
relation_set(database=config('neutron-database'))
|
||||||
|
|
||||||
|
|
||||||
|
@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_all()
|
||||||
|
|
||||||
|
@hooks.hook('pgsql-neutron-db-relation-changed')
|
||||||
|
@restart_on_change(restart_map())
|
||||||
|
def postgresql_neutron_db_changed():
|
||||||
|
if network_manager() in ['neutron', 'quantum']:
|
||||||
|
plugin = config('neutron-plugin')
|
||||||
|
# DB config might have been moved to main neutron.conf in H?
|
||||||
|
CONFIGS.write(neutron_plugin_attribute(plugin, 'config'))
|
||||||
|
|
||||||
|
def _auth_config():
|
||||||
|
'''Grab all KS auth token config from api-paste.ini, or return empty {}'''
|
||||||
|
ks_auth_host = auth_token_config('auth_host')
|
||||||
|
if not ks_auth_host:
|
||||||
|
# if there is no auth_host set, identity-service changed hooks
|
||||||
|
# have not fired, yet.
|
||||||
|
return {}
|
||||||
|
cfg = {
|
||||||
|
'auth_host': ks_auth_host,
|
||||||
|
'auth_port': auth_token_config('auth_port'),
|
||||||
|
'auth_protocol': auth_token_config('auth_protocol'),
|
||||||
|
'service_protocol': auth_token_config('service_protocol'),
|
||||||
|
'service_port': auth_token_config('service_port'),
|
||||||
|
'service_username': auth_token_config('admin_user'),
|
||||||
|
'service_password': auth_token_config('admin_password'),
|
||||||
|
'service_tenant_name': auth_token_config('admin_tenant_name'),
|
||||||
|
'auth_uri': auth_token_config('auth_uri'),
|
||||||
|
# quantum-gateway interface deviates a bit.
|
||||||
|
'keystone_host': ks_auth_host,
|
||||||
|
'service_tenant': auth_token_config('admin_tenant_name'),
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
@hooks.hook('amqp-relation-broken',
|
||||||
|
'identity-service-relation-broken',
|
||||||
|
'shared-db-relation-broken',
|
||||||
|
'pgsql-neutron-db-relation-broken')
|
||||||
|
def relation_broken():
|
||||||
|
CONFIGS.write_all()
|
||||||
|
|
||||||
|
@hooks.hook('upgrade-charm')
|
||||||
|
def upgrade_charm():
|
||||||
|
for r_id in relation_ids('amqp'):
|
||||||
|
amqp_joined(relation_id=r_id)
|
||||||
|
for r_id in relation_ids('identity-service'):
|
||||||
|
identity_joined(rid=r_id)
|
||||||
|
|
||||||
|
@hooks.hook('identity-service-relation-joined')
|
||||||
|
def identity_joined(rid=None):
|
||||||
|
base_url = canonical_url(CONFIGS)
|
||||||
|
relation_set(relation_id=rid, **determine_endpoints(base_url))
|
||||||
|
|
||||||
|
@hooks.hook('identity-service-relation-changed')
|
||||||
|
@restart_on_change(restart_map())
|
||||||
|
def identity_changed():
|
||||||
|
if 'identity-service' not in CONFIGS.complete_contexts():
|
||||||
|
log('identity-service relation incomplete. Peer not ready?')
|
||||||
|
return
|
||||||
|
CONFIGS.write(NEUTRON_CONF)
|
||||||
|
for r_id in relation_ids('neutron-api'):
|
||||||
|
neutron_api_relation_joined(rid=r_id)
|
||||||
|
|
||||||
|
def _get_keystone_info():
|
||||||
|
keystone_info = {}
|
||||||
|
for lrid in relation_ids('identity-service'):
|
||||||
|
for unit in related_units(lrid):
|
||||||
|
rdata = relation_get(rid=lrid, unit=unit)
|
||||||
|
keystone_info['service_protocol'] = rdata.get('service_protocol')
|
||||||
|
keystone_info['service_host'] = rdata.get('service_host')
|
||||||
|
keystone_info['service_port'] = rdata.get('service_port')
|
||||||
|
keystone_info['service_tenant'] = rdata.get('service_tenant')
|
||||||
|
keystone_info['service_username'] = rdata.get('service_username')
|
||||||
|
keystone_info['service_password'] = rdata.get('service_password')
|
||||||
|
keystone_info['auth_url'] = "%s://%s:%s/v2.0" % (keystone_info['service_protocol'],
|
||||||
|
keystone_info['service_host'],
|
||||||
|
keystone_info['service_port'])
|
||||||
|
return keystone_info
|
||||||
|
|
||||||
|
@hooks.hook('neutron-api-relation-joined')
|
||||||
|
def neutron_api_relation_joined(rid=None):
|
||||||
|
manager = network_manager()
|
||||||
|
base_url = canonical_url(CONFIGS)
|
||||||
|
neutron_url = '%s:%s' % (base_url, api_port('neutron-server'))
|
||||||
|
relation_data = {
|
||||||
|
'network_manager': manager,
|
||||||
|
'default_floating_pool': config('neutron-external-network'),
|
||||||
|
'external_network': config('neutron-external-network'),
|
||||||
|
manager + '_plugin': config('neutron-plugin'),
|
||||||
|
manager + '_url': neutron_url,
|
||||||
|
manager + '_security_groups': config('neutron-security-groups')
|
||||||
|
}
|
||||||
|
keystone_info = _get_keystone_info()
|
||||||
|
if is_relation_made('identity-service') and keystone_info:
|
||||||
|
relation_data.update({
|
||||||
|
manager + '_admin_tenant_name': keystone_info['service_tenant'],
|
||||||
|
manager + '_admin_username': keystone_info['service_username'],
|
||||||
|
manager + '_admin_password': keystone_info['service_password'],
|
||||||
|
manager + '_admin_auth_url': keystone_info['auth_url'],
|
||||||
|
})
|
||||||
|
relation_set(relation_id=rid, **relation_data)
|
||||||
|
# Nova-cc may have grabbed the quantum endpoint so kick identity-service relation to
|
||||||
|
# register that its here
|
||||||
|
for r_id in relation_ids('identity-service'):
|
||||||
|
identity_joined(rid=r_id)
|
||||||
|
|
||||||
|
@hooks.hook('neutron-api-relation-changed')
|
||||||
|
@restart_on_change(restart_map())
|
||||||
|
def neutron_api_relation_changed():
|
||||||
|
CONFIGS.write(NEUTRON_CONF)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
hooks.execute(sys.argv)
|
||||||
|
except UnregisteredHookError as e:
|
||||||
|
log('Unknown hook {} - skipping.'.format(e))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
175
hooks/neutron_api_utils.py
Normal file
175
hooks/neutron_api_utils.py
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
from copy import deepcopy
|
||||||
|
import ConfigParser
|
||||||
|
import os
|
||||||
|
from base64 import b64encode
|
||||||
|
from charmhelpers.contrib.openstack import context, templating
|
||||||
|
from charmhelpers.contrib.openstack.neutron import (
|
||||||
|
network_manager, neutron_plugin_attribute)
|
||||||
|
|
||||||
|
from charmhelpers.contrib.openstack.utils import (
|
||||||
|
os_release,
|
||||||
|
)
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import (
|
||||||
|
config,
|
||||||
|
)
|
||||||
|
|
||||||
|
import neutron_api_context
|
||||||
|
|
||||||
|
TEMPLATES = 'templates/'
|
||||||
|
|
||||||
|
CLUSTER_RES = 'res_nova_vip'
|
||||||
|
|
||||||
|
# removed from original: charm-helper-sh
|
||||||
|
BASE_PACKAGES = [
|
||||||
|
'python-keystoneclient',
|
||||||
|
'python-mysqldb',
|
||||||
|
'python-psycopg2',
|
||||||
|
'uuid',
|
||||||
|
]
|
||||||
|
|
||||||
|
BASE_SERVICES = [
|
||||||
|
'neutron-server'
|
||||||
|
]
|
||||||
|
API_PORTS = {
|
||||||
|
'neutron-server': 9696,
|
||||||
|
}
|
||||||
|
|
||||||
|
NEUTRON_CONF_DIR = "/etc/neutron"
|
||||||
|
|
||||||
|
NEUTRON_CONF = '%s/neutron.conf' % NEUTRON_CONF_DIR
|
||||||
|
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
|
||||||
|
APACHE_CONF = '/etc/apache2/sites-available/openstack_https_frontend'
|
||||||
|
APACHE_24_CONF = '/etc/apache2/sites-available/openstack_https_frontend.conf'
|
||||||
|
NEUTRON_DEFAULT = '/etc/default/neutron-server'
|
||||||
|
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
|
||||||
|
|
||||||
|
BASE_RESOURCE_MAP = OrderedDict([
|
||||||
|
(NEUTRON_CONF, {
|
||||||
|
'services': ['neutron-server'],
|
||||||
|
'contexts': [context.AMQPContext(ssl_dir=NEUTRON_CONF_DIR),
|
||||||
|
context.SharedDBContext(
|
||||||
|
user=config('neutron-database-user'),
|
||||||
|
database=config('neutron-database'),
|
||||||
|
relation_prefix='neutron',
|
||||||
|
ssl_dir=NEUTRON_CONF_DIR),
|
||||||
|
neutron_api_context.NeutronPostgresqlDBContext(),
|
||||||
|
neutron_api_context.IdentityServiceContext(),
|
||||||
|
neutron_api_context.NeutronCCContext(),
|
||||||
|
context.SyslogContext()],
|
||||||
|
}),
|
||||||
|
(NEUTRON_DEFAULT, {
|
||||||
|
'services': ['neutron-server'],
|
||||||
|
'contexts': [neutron_api_context.NeutronCCContext()],
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
def api_port(service):
|
||||||
|
return API_PORTS[service]
|
||||||
|
|
||||||
|
def determine_endpoints(url):
|
||||||
|
'''Generates a dictionary containing all relevant endpoints to be
|
||||||
|
passed to keystone as relation settings.'''
|
||||||
|
region = config('region')
|
||||||
|
|
||||||
|
neutron_url = '%s:%s' % (url, api_port('neutron-server'))
|
||||||
|
|
||||||
|
endpoints = ({
|
||||||
|
'quantum_service': 'quantum',
|
||||||
|
'quantum_region': region,
|
||||||
|
'quantum_public_url': neutron_url,
|
||||||
|
'quantum_admin_url': neutron_url,
|
||||||
|
'quantum_internal_url': neutron_url,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
return endpoints
|
||||||
|
|
||||||
|
|
||||||
|
def determine_packages():
|
||||||
|
# currently all packages match service names
|
||||||
|
packages = [] + BASE_PACKAGES
|
||||||
|
for v in resource_map().values():
|
||||||
|
packages.extend(v['services'])
|
||||||
|
pkgs = neutron_plugin_attribute(config('neutron-plugin'), 'server_packages',
|
||||||
|
network_manager())
|
||||||
|
packages.extend(pkgs)
|
||||||
|
return list(set(packages))
|
||||||
|
|
||||||
|
def determine_ports():
|
||||||
|
'''Assemble a list of API ports for services we are managing'''
|
||||||
|
ports = []
|
||||||
|
for services in restart_map().values():
|
||||||
|
for service in services:
|
||||||
|
try:
|
||||||
|
ports.append(API_PORTS[service])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
return list(set(ports))
|
||||||
|
|
||||||
|
|
||||||
|
def resource_map():
|
||||||
|
'''
|
||||||
|
Dynamically generate a map of resources that will be managed for a single
|
||||||
|
hook execution.
|
||||||
|
'''
|
||||||
|
resource_map = deepcopy(BASE_RESOURCE_MAP)
|
||||||
|
|
||||||
|
net_manager = network_manager()
|
||||||
|
|
||||||
|
# add neutron plugin requirements. nova-c-c only needs the neutron-server
|
||||||
|
# associated with configs, not the plugin agent.
|
||||||
|
plugin = config('neutron-plugin')
|
||||||
|
conf = neutron_plugin_attribute(plugin, 'config', net_manager)
|
||||||
|
ctxts = (neutron_plugin_attribute(plugin, 'contexts', net_manager)
|
||||||
|
or [])
|
||||||
|
services = neutron_plugin_attribute(plugin, 'server_services',
|
||||||
|
net_manager)
|
||||||
|
resource_map[conf] = {}
|
||||||
|
resource_map[conf]['services'] = services
|
||||||
|
resource_map[conf]['contexts'] = ctxts
|
||||||
|
resource_map[conf]['contexts'].append(
|
||||||
|
neutron_api_context.NeutronCCContext())
|
||||||
|
|
||||||
|
# update for postgres
|
||||||
|
resource_map[conf]['contexts'].append(
|
||||||
|
neutron_api_context.NeutronPostgresqlDBContext())
|
||||||
|
|
||||||
|
return resource_map
|
||||||
|
|
||||||
|
|
||||||
|
def register_configs(release=None):
|
||||||
|
release = release or os_release('nova-common')
|
||||||
|
configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
|
||||||
|
openstack_release=release)
|
||||||
|
for cfg, rscs in resource_map().iteritems():
|
||||||
|
configs.register(cfg, rscs['contexts'])
|
||||||
|
return configs
|
||||||
|
|
||||||
|
|
||||||
|
def restart_map():
|
||||||
|
return OrderedDict([(cfg, v['services'])
|
||||||
|
for cfg, v in resource_map().iteritems()
|
||||||
|
if v['services']])
|
||||||
|
|
||||||
|
def auth_token_config(setting):
|
||||||
|
"""
|
||||||
|
Returns currently configured value for setting in api-paste.ini's
|
||||||
|
authtoken section, or None.
|
||||||
|
"""
|
||||||
|
config = ConfigParser.RawConfigParser()
|
||||||
|
config.read('/etc/neutron/api-paste.ini')
|
||||||
|
try:
|
||||||
|
value = config.get('filter:authtoken', setting)
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
if value.startswith('%'):
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def keystone_ca_cert_b64():
|
||||||
|
'''Returns the local Keystone-provided CA cert if it exists, or None.'''
|
||||||
|
if not os.path.isfile(CA_CERT_PATH):
|
||||||
|
return None
|
||||||
|
with open(CA_CERT_PATH) as _in:
|
||||||
|
return b64encode(_in.read())
|
1
hooks/pgsql-neutron-db-relation-broken
Symbolic link
1
hooks/pgsql-neutron-db-relation-broken
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/pgsql-neutron-db-relation-changed
Symbolic link
1
hooks/pgsql-neutron-db-relation-changed
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/pgsql-neutron-db-relation-joined
Symbolic link
1
hooks/pgsql-neutron-db-relation-joined
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/shared-db-relation-broken
Symbolic link
1
hooks/shared-db-relation-broken
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/shared-db-relation-changed
Symbolic link
1
hooks/shared-db-relation-changed
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
1
hooks/shared-db-relation-joined
Symbolic link
1
hooks/shared-db-relation-joined
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
neutron_api_hooks.py
|
19
metadata.yaml
Normal file
19
metadata.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
name: neutron-api
|
||||||
|
maintainer: Liam Young <liam@canonical.com>
|
||||||
|
summary: "Openstack neutron api service."
|
||||||
|
description: |
|
||||||
|
Openstack neutron api service.
|
||||||
|
categories:
|
||||||
|
- openstack
|
||||||
|
provides:
|
||||||
|
neutron-api:
|
||||||
|
interface: neutron-api
|
||||||
|
requires:
|
||||||
|
shared-db:
|
||||||
|
interface: mysql-shared
|
||||||
|
pgsql-neutron-db:
|
||||||
|
interface: pgsql
|
||||||
|
amqp:
|
||||||
|
interface: rabbitmq
|
||||||
|
identity-service:
|
||||||
|
interface: keystone
|
162
templates/essex/etc_nova_api-paste.ini
Normal file
162
templates/essex/etc_nova_api-paste.ini
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
# essex
|
||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# Configuration file maintained by Juju. Local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
############
|
||||||
|
# Metadata #
|
||||||
|
############
|
||||||
|
[composite:metadata]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/: metaversions
|
||||||
|
/latest: meta
|
||||||
|
/1.0: meta
|
||||||
|
/2007-01-19: meta
|
||||||
|
/2007-03-01: meta
|
||||||
|
/2007-08-29: meta
|
||||||
|
/2007-10-10: meta
|
||||||
|
/2007-12-15: meta
|
||||||
|
/2008-02-01: meta
|
||||||
|
/2008-09-01: meta
|
||||||
|
/2009-04-04: meta
|
||||||
|
|
||||||
|
[pipeline:metaversions]
|
||||||
|
pipeline = ec2faultwrap logrequest metaverapp
|
||||||
|
|
||||||
|
[pipeline:meta]
|
||||||
|
pipeline = ec2faultwrap logrequest metaapp
|
||||||
|
|
||||||
|
[app:metaverapp]
|
||||||
|
paste.app_factory = nova.api.metadata.handler:Versions.factory
|
||||||
|
|
||||||
|
[app:metaapp]
|
||||||
|
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory
|
||||||
|
|
||||||
|
#######
|
||||||
|
# EC2 #
|
||||||
|
#######
|
||||||
|
|
||||||
|
[composite:ec2]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/services/Cloud: ec2cloud
|
||||||
|
|
||||||
|
[composite:ec2cloud]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = ec2faultwrap logrequest ec2noauth cloudrequest validator ec2executor
|
||||||
|
deprecated = ec2faultwrap logrequest authenticate cloudrequest validator ec2executor
|
||||||
|
keystone = ec2faultwrap logrequest ec2keystoneauth cloudrequest validator ec2executor
|
||||||
|
|
||||||
|
[filter:ec2faultwrap]
|
||||||
|
paste.filter_factory = nova.api.ec2:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:logrequest]
|
||||||
|
paste.filter_factory = nova.api.ec2:RequestLogging.factory
|
||||||
|
|
||||||
|
[filter:ec2lockout]
|
||||||
|
paste.filter_factory = nova.api.ec2:Lockout.factory
|
||||||
|
|
||||||
|
[filter:totoken]
|
||||||
|
paste.filter_factory = nova.api.ec2:EC2Token.factory
|
||||||
|
|
||||||
|
[filter:ec2keystoneauth]
|
||||||
|
paste.filter_factory = nova.api.ec2:EC2KeystoneAuth.factory
|
||||||
|
|
||||||
|
[filter:ec2noauth]
|
||||||
|
paste.filter_factory = nova.api.ec2:NoAuth.factory
|
||||||
|
|
||||||
|
[filter:authenticate]
|
||||||
|
paste.filter_factory = nova.api.ec2:Authenticate.factory
|
||||||
|
|
||||||
|
[filter:cloudrequest]
|
||||||
|
controller = nova.api.ec2.cloud.CloudController
|
||||||
|
paste.filter_factory = nova.api.ec2:Requestify.factory
|
||||||
|
|
||||||
|
[filter:authorizer]
|
||||||
|
paste.filter_factory = nova.api.ec2:Authorizer.factory
|
||||||
|
|
||||||
|
[filter:validator]
|
||||||
|
paste.filter_factory = nova.api.ec2:Validator.factory
|
||||||
|
|
||||||
|
[app:ec2executor]
|
||||||
|
paste.app_factory = nova.api.ec2:Executor.factory
|
||||||
|
|
||||||
|
#############
|
||||||
|
# Openstack #
|
||||||
|
#############
|
||||||
|
|
||||||
|
[composite:osapi_compute]
|
||||||
|
use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||||
|
/: oscomputeversions
|
||||||
|
/v1.1: openstack_compute_api_v2
|
||||||
|
/v2: openstack_compute_api_v2
|
||||||
|
|
||||||
|
[composite:osapi_volume]
|
||||||
|
use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||||
|
/: osvolumeversions
|
||||||
|
/v1: openstack_volume_api_v1
|
||||||
|
|
||||||
|
[composite:openstack_compute_api_v2]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = faultwrap noauth ratelimit osapi_compute_app_v2
|
||||||
|
deprecated = faultwrap auth ratelimit osapi_compute_app_v2
|
||||||
|
keystone = faultwrap authtoken keystonecontext ratelimit osapi_compute_app_v2
|
||||||
|
keystone_nolimit = faultwrap authtoken keystonecontext osapi_compute_app_v2
|
||||||
|
|
||||||
|
[composite:openstack_volume_api_v1]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = faultwrap noauth ratelimit osapi_volume_app_v1
|
||||||
|
deprecated = faultwrap auth ratelimit osapi_volume_app_v1
|
||||||
|
keystone = faultwrap authtoken keystonecontext ratelimit osapi_volume_app_v1
|
||||||
|
keystone_nolimit = faultwrap authtoken keystonecontext osapi_volume_app_v1
|
||||||
|
|
||||||
|
[filter:faultwrap]
|
||||||
|
paste.filter_factory = nova.api.openstack:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:auth]
|
||||||
|
paste.filter_factory = nova.api.openstack.auth:AuthMiddleware.factory
|
||||||
|
|
||||||
|
[filter:noauth]
|
||||||
|
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory
|
||||||
|
|
||||||
|
[filter:ratelimit]
|
||||||
|
paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory
|
||||||
|
|
||||||
|
[app:osapi_compute_app_v2]
|
||||||
|
paste.app_factory = nova.api.openstack.compute:APIRouter.factory
|
||||||
|
|
||||||
|
[pipeline:oscomputeversions]
|
||||||
|
pipeline = faultwrap oscomputeversionapp
|
||||||
|
|
||||||
|
[app:osapi_volume_app_v1]
|
||||||
|
paste.app_factory = nova.api.openstack.volume:APIRouter.factory
|
||||||
|
|
||||||
|
[app:oscomputeversionapp]
|
||||||
|
paste.app_factory = nova.api.openstack.compute.versions:Versions.factory
|
||||||
|
|
||||||
|
[pipeline:osvolumeversions]
|
||||||
|
pipeline = faultwrap osvolumeversionapp
|
||||||
|
|
||||||
|
[app:osvolumeversionapp]
|
||||||
|
paste.app_factory = nova.api.openstack.volume.versions:Versions.factory
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Shared #
|
||||||
|
##########
|
||||||
|
|
||||||
|
[filter:keystonecontext]
|
||||||
|
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystone.middleware.auth_token:filter_factory
|
||||||
|
{% if service_host -%}
|
||||||
|
service_protocol = {{ service_protocol }}
|
||||||
|
service_host = {{ service_host }}
|
||||||
|
service_port = {{ service_port }}
|
||||||
|
auth_host = {{ auth_host }}
|
||||||
|
auth_port = {{ auth_port }}
|
||||||
|
auth_protocol = {{ auth_protocol }}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
{% endif -%}
|
||||||
|
|
141
templates/folsom/etc_nova_api-paste.ini
Normal file
141
templates/folsom/etc_nova_api-paste.ini
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
# folsom
|
||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# Configuration file maintained by Juju. Local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
############
|
||||||
|
# Metadata #
|
||||||
|
############
|
||||||
|
[composite:metadata]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/: meta
|
||||||
|
|
||||||
|
[pipeline:meta]
|
||||||
|
pipeline = ec2faultwrap logrequest metaapp
|
||||||
|
|
||||||
|
[app:metaapp]
|
||||||
|
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory
|
||||||
|
|
||||||
|
#######
|
||||||
|
# EC2 #
|
||||||
|
#######
|
||||||
|
|
||||||
|
[composite:ec2]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/services/Cloud: ec2cloud
|
||||||
|
|
||||||
|
[composite:ec2cloud]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = ec2faultwrap logrequest ec2noauth cloudrequest validator ec2executor
|
||||||
|
keystone = ec2faultwrap logrequest ec2keystoneauth cloudrequest validator ec2executor
|
||||||
|
|
||||||
|
[filter:ec2faultwrap]
|
||||||
|
paste.filter_factory = nova.api.ec2:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:logrequest]
|
||||||
|
paste.filter_factory = nova.api.ec2:RequestLogging.factory
|
||||||
|
|
||||||
|
[filter:ec2lockout]
|
||||||
|
paste.filter_factory = nova.api.ec2:Lockout.factory
|
||||||
|
|
||||||
|
[filter:ec2keystoneauth]
|
||||||
|
paste.filter_factory = nova.api.ec2:EC2KeystoneAuth.factory
|
||||||
|
|
||||||
|
[filter:ec2noauth]
|
||||||
|
paste.filter_factory = nova.api.ec2:NoAuth.factory
|
||||||
|
|
||||||
|
[filter:cloudrequest]
|
||||||
|
controller = nova.api.ec2.cloud.CloudController
|
||||||
|
paste.filter_factory = nova.api.ec2:Requestify.factory
|
||||||
|
|
||||||
|
[filter:authorizer]
|
||||||
|
paste.filter_factory = nova.api.ec2:Authorizer.factory
|
||||||
|
|
||||||
|
[filter:validator]
|
||||||
|
paste.filter_factory = nova.api.ec2:Validator.factory
|
||||||
|
|
||||||
|
[app:ec2executor]
|
||||||
|
paste.app_factory = nova.api.ec2:Executor.factory
|
||||||
|
|
||||||
|
#############
|
||||||
|
# Openstack #
|
||||||
|
#############
|
||||||
|
|
||||||
|
[composite:osapi_compute]
|
||||||
|
use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||||
|
/: oscomputeversions
|
||||||
|
/v1.1: openstack_compute_api_v2
|
||||||
|
/v2: openstack_compute_api_v2
|
||||||
|
|
||||||
|
[composite:osapi_volume]
|
||||||
|
use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||||
|
/: osvolumeversions
|
||||||
|
/v1: openstack_volume_api_v1
|
||||||
|
|
||||||
|
[composite:openstack_compute_api_v2]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2
|
||||||
|
keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
|
||||||
|
keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2
|
||||||
|
|
||||||
|
[composite:openstack_volume_api_v1]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = faultwrap sizelimit noauth ratelimit osapi_volume_app_v1
|
||||||
|
keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_volume_app_v1
|
||||||
|
keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_volume_app_v1
|
||||||
|
|
||||||
|
[filter:faultwrap]
|
||||||
|
paste.filter_factory = nova.api.openstack:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:noauth]
|
||||||
|
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory
|
||||||
|
|
||||||
|
[filter:ratelimit]
|
||||||
|
paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory
|
||||||
|
|
||||||
|
[filter:sizelimit]
|
||||||
|
paste.filter_factory = nova.api.sizelimit:RequestBodySizeLimiter.factory
|
||||||
|
|
||||||
|
[app:osapi_compute_app_v2]
|
||||||
|
paste.app_factory = nova.api.openstack.compute:APIRouter.factory
|
||||||
|
|
||||||
|
[pipeline:oscomputeversions]
|
||||||
|
pipeline = faultwrap oscomputeversionapp
|
||||||
|
|
||||||
|
[app:osapi_volume_app_v1]
|
||||||
|
paste.app_factory = nova.api.openstack.volume:APIRouter.factory
|
||||||
|
|
||||||
|
[app:oscomputeversionapp]
|
||||||
|
paste.app_factory = nova.api.openstack.compute.versions:Versions.factory
|
||||||
|
|
||||||
|
[pipeline:osvolumeversions]
|
||||||
|
pipeline = faultwrap osvolumeversionapp
|
||||||
|
|
||||||
|
[app:osvolumeversionapp]
|
||||||
|
paste.app_factory = nova.api.openstack.volume.versions:Versions.factory
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Shared #
|
||||||
|
##########
|
||||||
|
|
||||||
|
[filter:keystonecontext]
|
||||||
|
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystone.middleware.auth_token:filter_factory
|
||||||
|
{% if service_host -%}
|
||||||
|
service_protocol = {{ service_protocol }}
|
||||||
|
service_host = {{ service_host }}
|
||||||
|
service_port = {{ service_port }}
|
||||||
|
auth_host = {{ auth_host }}
|
||||||
|
auth_port = {{ auth_port }}
|
||||||
|
auth_protocol = {{ auth_protocol }}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
{% endif -%}
|
||||||
|
# signing_dir is configurable, but the default behavior of the authtoken
|
||||||
|
# middleware should be sufficient. It will create a temporary directory
|
||||||
|
# in the home directory for the user the nova process is running as.
|
||||||
|
#signing_dir = /var/lib/nova/keystone-signing
|
||||||
|
|
40
templates/folsom/etc_quantum_api-paste.ini
Normal file
40
templates/folsom/etc_quantum_api-paste.ini
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# folsom
|
||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# Configuration file maintained by Juju. Local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
[composite:quantum]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/: quantumversions
|
||||||
|
/v2.0: quantumapi_v2_0
|
||||||
|
|
||||||
|
[composite:quantumapi_v2_0]
|
||||||
|
use = call:quantum.auth:pipeline_factory
|
||||||
|
noauth = extensions quantumapiapp_v2_0
|
||||||
|
keystone = authtoken keystonecontext extensions quantumapiapp_v2_0
|
||||||
|
|
||||||
|
[filter:keystonecontext]
|
||||||
|
paste.filter_factory = quantum.auth:QuantumKeystoneContext.factory
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystone.middleware.auth_token:filter_factory
|
||||||
|
{% if service_host -%}
|
||||||
|
service_protocol = {{ service_protocol }}
|
||||||
|
service_host = {{ service_host }}
|
||||||
|
service_port = {{ service_port }}
|
||||||
|
auth_host = {{ auth_host }}
|
||||||
|
auth_port = {{ auth_port }}
|
||||||
|
auth_protocol = {{ auth_protocol }}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[filter:extensions]
|
||||||
|
paste.filter_factory = quantum.extensions.extensions:plugin_aware_extension_middleware_factory
|
||||||
|
|
||||||
|
[app:quantumversions]
|
||||||
|
paste.app_factory = quantum.api.versions:Versions.factory
|
||||||
|
|
||||||
|
[app:quantumapiapp_v2_0]
|
||||||
|
paste.app_factory = quantum.api.v2.router:APIRouter.factory
|
23
templates/folsom/ovs_quantum_plugin.ini
Normal file
23
templates/folsom/ovs_quantum_plugin.ini
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# 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 = {{ database_type }}://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}{% if database_ssl_ca %}?ssl_ca={{ database_ssl_ca }}{% if database_ssl_cert %}&ssl_cert={{ database_ssl_cert }}&ssl_key={{ database_ssl_key }}{% endif %}{% endif %}
|
||||||
|
reconnect_interval = 2
|
||||||
|
{% else -%}
|
||||||
|
connection = sqlite:////var/lib/quantum/quantum.sqlite
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[SECURITYGROUP]
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
firewall_driver = quantum.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver
|
||||||
|
{% endif -%}
|
6
templates/folsom/quantum-server
Normal file
6
templates/folsom/quantum-server
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# quantum
|
||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# Configuration file maintained by Juju. Local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
QUANTUM_PLUGIN_CONFIG="{{ config }}"
|
48
templates/folsom/quantum.conf
Normal file
48
templates/folsom/quantum.conf
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# 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
|
||||||
|
{% if neutron_bind_port -%}
|
||||||
|
bind_port = {{ neutron_bind_port }}
|
||||||
|
{% else -%}
|
||||||
|
bind_port = 9696
|
||||||
|
{% endif -%}
|
||||||
|
{% if core_plugin -%}
|
||||||
|
core_plugin = {{ core_plugin }}
|
||||||
|
{% endif -%}
|
||||||
|
use_syslog = {{ use_syslog }}
|
||||||
|
api_paste_config = /etc/quantum/api-paste.ini
|
||||||
|
auth_strategy = keystone
|
||||||
|
control_exchange = quantum
|
||||||
|
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 -%}
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
allow_overlapping_ips = True
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
|
||||||
|
[QUOTAS]
|
||||||
|
quota_driver = quantum.db.quota_db.DbQuotaDriver
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
quota_items = network,subnet,port,security_group,security_group_rule
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[DEFAULT_SERVICETYPE]
|
||||||
|
|
||||||
|
[AGENT]
|
||||||
|
root_helper = sudo quantum-rootwrap /etc/quantum/rootwrap.conf
|
||||||
|
|
||||||
|
[keystone_authtoken]
|
||||||
|
# auth_token middleware currently set in /etc/quantum/api-paste.ini
|
124
templates/grizzly/etc_nova_api-paste.ini
Normal file
124
templates/grizzly/etc_nova_api-paste.ini
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
# grizzly
|
||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# Configuration file maintained by Juju. Local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
############
|
||||||
|
# Metadata #
|
||||||
|
############
|
||||||
|
[composite:metadata]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/: meta
|
||||||
|
|
||||||
|
[pipeline:meta]
|
||||||
|
pipeline = ec2faultwrap logrequest metaapp
|
||||||
|
|
||||||
|
[app:metaapp]
|
||||||
|
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory
|
||||||
|
|
||||||
|
#######
|
||||||
|
# EC2 #
|
||||||
|
#######
|
||||||
|
|
||||||
|
[composite:ec2]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/services/Cloud: ec2cloud
|
||||||
|
|
||||||
|
[composite:ec2cloud]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = ec2faultwrap logrequest ec2noauth cloudrequest validator ec2executor
|
||||||
|
keystone = ec2faultwrap logrequest ec2keystoneauth cloudrequest validator ec2executor
|
||||||
|
|
||||||
|
[filter:ec2faultwrap]
|
||||||
|
paste.filter_factory = nova.api.ec2:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:logrequest]
|
||||||
|
paste.filter_factory = nova.api.ec2:RequestLogging.factory
|
||||||
|
|
||||||
|
[filter:ec2lockout]
|
||||||
|
paste.filter_factory = nova.api.ec2:Lockout.factory
|
||||||
|
|
||||||
|
[filter:ec2keystoneauth]
|
||||||
|
paste.filter_factory = nova.api.ec2:EC2KeystoneAuth.factory
|
||||||
|
|
||||||
|
[filter:ec2noauth]
|
||||||
|
paste.filter_factory = nova.api.ec2:NoAuth.factory
|
||||||
|
|
||||||
|
[filter:cloudrequest]
|
||||||
|
controller = nova.api.ec2.cloud.CloudController
|
||||||
|
paste.filter_factory = nova.api.ec2:Requestify.factory
|
||||||
|
|
||||||
|
[filter:authorizer]
|
||||||
|
paste.filter_factory = nova.api.ec2:Authorizer.factory
|
||||||
|
|
||||||
|
[filter:validator]
|
||||||
|
paste.filter_factory = nova.api.ec2:Validator.factory
|
||||||
|
|
||||||
|
[app:ec2executor]
|
||||||
|
paste.app_factory = nova.api.ec2:Executor.factory
|
||||||
|
|
||||||
|
#############
|
||||||
|
# Openstack #
|
||||||
|
#############
|
||||||
|
|
||||||
|
[composite:osapi_compute]
|
||||||
|
use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||||
|
/: oscomputeversions
|
||||||
|
/v1.1: openstack_compute_api_v2
|
||||||
|
/v2: openstack_compute_api_v2
|
||||||
|
|
||||||
|
[composite:openstack_compute_api_v2]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2
|
||||||
|
keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
|
||||||
|
keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2
|
||||||
|
|
||||||
|
[filter:faultwrap]
|
||||||
|
paste.filter_factory = nova.api.openstack:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:noauth]
|
||||||
|
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory
|
||||||
|
|
||||||
|
[filter:ratelimit]
|
||||||
|
paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory
|
||||||
|
|
||||||
|
[filter:sizelimit]
|
||||||
|
paste.filter_factory = nova.api.sizelimit:RequestBodySizeLimiter.factory
|
||||||
|
|
||||||
|
[app:osapi_compute_app_v2]
|
||||||
|
paste.app_factory = nova.api.openstack.compute:APIRouter.factory
|
||||||
|
|
||||||
|
[pipeline:oscomputeversions]
|
||||||
|
pipeline = faultwrap oscomputeversionapp
|
||||||
|
|
||||||
|
[app:oscomputeversionapp]
|
||||||
|
paste.app_factory = nova.api.openstack.compute.versions:Versions.factory
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Shared #
|
||||||
|
##########
|
||||||
|
|
||||||
|
[filter:keystonecontext]
|
||||||
|
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
|
||||||
|
{% if service_host -%}
|
||||||
|
service_protocol = {{ service_protocol }}
|
||||||
|
service_host = {{ service_host }}
|
||||||
|
service_port = {{ service_port }}
|
||||||
|
auth_host = {{ auth_host }}
|
||||||
|
auth_port = {{ auth_port }}
|
||||||
|
auth_protocol = {{ auth_protocol }}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
{% endif -%}
|
||||||
|
# signing_dir is configurable, but the default behavior of the authtoken
|
||||||
|
# middleware should be sufficient. It will create a temporary directory
|
||||||
|
# in the home directory for the user the nova process is running as.
|
||||||
|
#signing_dir = /var/lib/nova/keystone-signing
|
||||||
|
# Workaround for https://bugs.launchpad.net/nova/+bug/1154809
|
||||||
|
auth_version = v2.0
|
||||||
|
|
||||||
|
|
37
templates/grizzly/etc_quantum_api-paste.ini
Normal file
37
templates/grizzly/etc_quantum_api-paste.ini
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# Configuration file maintained by Juju. Local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
[composite:quantum]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/: quantumversions
|
||||||
|
/v2.0: quantumapi_v2_0
|
||||||
|
|
||||||
|
[composite:quantumapi_v2_0]
|
||||||
|
use = call:quantum.auth:pipeline_factory
|
||||||
|
noauth = extensions quantumapiapp_v2_0
|
||||||
|
keystone = authtoken keystonecontext extensions quantumapiapp_v2_0
|
||||||
|
|
||||||
|
[filter:keystonecontext]
|
||||||
|
paste.filter_factory = quantum.auth:QuantumKeystoneContext.factory
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
|
||||||
|
{% if service_host -%}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
auth_host = {{ auth_host }}
|
||||||
|
auth_port = {{ auth_port }}
|
||||||
|
auth_protocol = http
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[filter:extensions]
|
||||||
|
paste.filter_factory = quantum.api.extensions:plugin_aware_extension_middleware_factory
|
||||||
|
|
||||||
|
[app:quantumversions]
|
||||||
|
paste.app_factory = quantum.api.versions:Versions.factory
|
||||||
|
|
||||||
|
[app:quantumapiapp_v2_0]
|
||||||
|
paste.app_factory = quantum.api.v2.router:APIRouter.factory
|
||||||
|
|
45
templates/grizzly/quantum.conf
Normal file
45
templates/grizzly/quantum.conf
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# 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
|
||||||
|
{% if neutron_bind_port -%}
|
||||||
|
bind_port = {{ neutron_bind_port }}
|
||||||
|
{% else -%}
|
||||||
|
bind_port = 9696
|
||||||
|
{% endif -%}
|
||||||
|
{% if core_plugin -%}
|
||||||
|
core_plugin = {{ core_plugin }}
|
||||||
|
{% endif -%}
|
||||||
|
api_paste_config = /etc/quantum/api-paste.ini
|
||||||
|
auth_strategy = keystone
|
||||||
|
control_exchange = quantum
|
||||||
|
notification_driver = quantum.openstack.common.notifier.rpc_notifier
|
||||||
|
default_notification_level = INFO
|
||||||
|
notification_topics = notifications
|
||||||
|
|
||||||
|
{% include "parts/database" %}
|
||||||
|
|
||||||
|
{% include "parts/rabbitmq" %}
|
||||||
|
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
allow_overlapping_ips = True
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[QUOTAS]
|
||||||
|
quota_driver = quantum.db.quota_db.DbQuotaDriver
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
quota_items = network,subnet,port,security_group,security_group_rule
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[DEFAULT_SERVICETYPE]
|
||||||
|
|
||||||
|
[AGENT]
|
||||||
|
root_helper = sudo quantum-rootwrap /etc/quantum/rootwrap.conf
|
||||||
|
|
||||||
|
[keystone_authtoken]
|
||||||
|
# auth_token middleware currently set in /etc/quantum/api-paste.ini
|
6
templates/havana/neutron-server
Normal file
6
templates/havana/neutron-server
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# havana
|
||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# Configuration file maintained by Juju. Local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
NEUTRON_PLUGIN_CONFIG="{{ config }}"
|
56
templates/havana/neutron.conf
Normal file
56
templates/havana/neutron.conf
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
###############################################################################
|
||||||
|
# [ 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
|
||||||
|
auth_strategy = keystone
|
||||||
|
notification_driver = neutron.openstack.common.notifier.rpc_notifier
|
||||||
|
use_syslog = {{ use_syslog }}
|
||||||
|
{% if neutron_bind_port -%}
|
||||||
|
bind_port = {{ neutron_bind_port }}
|
||||||
|
{% else -%}
|
||||||
|
bind_port = 9696
|
||||||
|
{% endif -%}
|
||||||
|
{% if core_plugin -%}
|
||||||
|
core_plugin = {{ core_plugin }}
|
||||||
|
{% if neutron_plugin in ['ovs', 'ml2'] -%}
|
||||||
|
service_plugins = neutron.services.metering.metering_plugin.MeteringPlugin
|
||||||
|
{% endif -%}
|
||||||
|
{% endif -%}
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
allow_overlapping_ips = True
|
||||||
|
neutron_firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% include "parts/rabbitmq" %}
|
||||||
|
|
||||||
|
[quotas]
|
||||||
|
quota_driver = neutron.db.quota_db.DbQuotaDriver
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
quota_items = network,subnet,port,security_group,security_group_rule
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[agent]
|
||||||
|
root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf
|
||||||
|
|
||||||
|
[keystone_authtoken]
|
||||||
|
signing_dir = $state_path/keystone-signing
|
||||||
|
{% if service_host -%}
|
||||||
|
service_protocol = {{ service_protocol }}
|
||||||
|
service_host = {{ service_host }}
|
||||||
|
service_port = {{ service_port }}
|
||||||
|
auth_host = {{ auth_host }}
|
||||||
|
auth_port = {{ auth_port }}
|
||||||
|
auth_protocol = {{ auth_protocol }}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% include "parts/section-database" %}
|
||||||
|
|
||||||
|
[lbaas]
|
||||||
|
[service_providers]
|
12
templates/havana/ovs_neutron_plugin.ini
Normal file
12
templates/havana/ovs_neutron_plugin.ini
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[OVS]
|
||||||
|
tunnel_id_ranges = 1:1000
|
||||||
|
tenant_network_type = gre
|
||||||
|
enable_tunneling = True
|
||||||
|
local_ip = {{ local_ip }}
|
||||||
|
|
||||||
|
[securitygroup]
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver
|
||||||
|
{% else -%}
|
||||||
|
firewall_driver = neutron.agent.firewall.NoopFirewallDriver
|
||||||
|
{% endif -%}
|
124
templates/icehouse/etc_nova_api-paste.ini
Normal file
124
templates/icehouse/etc_nova_api-paste.ini
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
############
|
||||||
|
# Metadata #
|
||||||
|
############
|
||||||
|
[composite:metadata]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/: meta
|
||||||
|
|
||||||
|
[pipeline:meta]
|
||||||
|
pipeline = ec2faultwrap logrequest metaapp
|
||||||
|
|
||||||
|
[app:metaapp]
|
||||||
|
paste.app_factory = nova.api.metadata.handler:MetadataRequestHandler.factory
|
||||||
|
|
||||||
|
#######
|
||||||
|
# EC2 #
|
||||||
|
#######
|
||||||
|
|
||||||
|
[composite:ec2]
|
||||||
|
use = egg:Paste#urlmap
|
||||||
|
/services/Cloud: ec2cloud
|
||||||
|
|
||||||
|
[composite:ec2cloud]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = ec2faultwrap logrequest ec2noauth cloudrequest validator ec2executor
|
||||||
|
keystone = ec2faultwrap logrequest ec2keystoneauth cloudrequest validator ec2executor
|
||||||
|
|
||||||
|
[filter:ec2faultwrap]
|
||||||
|
paste.filter_factory = nova.api.ec2:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:logrequest]
|
||||||
|
paste.filter_factory = nova.api.ec2:RequestLogging.factory
|
||||||
|
|
||||||
|
[filter:ec2lockout]
|
||||||
|
paste.filter_factory = nova.api.ec2:Lockout.factory
|
||||||
|
|
||||||
|
[filter:ec2keystoneauth]
|
||||||
|
paste.filter_factory = nova.api.ec2:EC2KeystoneAuth.factory
|
||||||
|
|
||||||
|
[filter:ec2noauth]
|
||||||
|
paste.filter_factory = nova.api.ec2:NoAuth.factory
|
||||||
|
|
||||||
|
[filter:cloudrequest]
|
||||||
|
controller = nova.api.ec2.cloud.CloudController
|
||||||
|
paste.filter_factory = nova.api.ec2:Requestify.factory
|
||||||
|
|
||||||
|
[filter:authorizer]
|
||||||
|
paste.filter_factory = nova.api.ec2:Authorizer.factory
|
||||||
|
|
||||||
|
[filter:validator]
|
||||||
|
paste.filter_factory = nova.api.ec2:Validator.factory
|
||||||
|
|
||||||
|
[app:ec2executor]
|
||||||
|
paste.app_factory = nova.api.ec2:Executor.factory
|
||||||
|
|
||||||
|
#############
|
||||||
|
# OpenStack #
|
||||||
|
#############
|
||||||
|
|
||||||
|
[composite:osapi_compute]
|
||||||
|
use = call:nova.api.openstack.urlmap:urlmap_factory
|
||||||
|
/: oscomputeversions
|
||||||
|
/v1.1: openstack_compute_api_v2
|
||||||
|
/v2: openstack_compute_api_v2
|
||||||
|
/v3: openstack_compute_api_v3
|
||||||
|
|
||||||
|
[composite:openstack_compute_api_v2]
|
||||||
|
use = call:nova.api.auth:pipeline_factory
|
||||||
|
noauth = faultwrap sizelimit noauth ratelimit osapi_compute_app_v2
|
||||||
|
keystone = faultwrap sizelimit authtoken keystonecontext ratelimit osapi_compute_app_v2
|
||||||
|
keystone_nolimit = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v2
|
||||||
|
|
||||||
|
[composite:openstack_compute_api_v3]
|
||||||
|
use = call:nova.api.auth:pipeline_factory_v3
|
||||||
|
noauth = faultwrap sizelimit noauth_v3 osapi_compute_app_v3
|
||||||
|
keystone = faultwrap sizelimit authtoken keystonecontext osapi_compute_app_v3
|
||||||
|
|
||||||
|
[filter:faultwrap]
|
||||||
|
paste.filter_factory = nova.api.openstack:FaultWrapper.factory
|
||||||
|
|
||||||
|
[filter:noauth]
|
||||||
|
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddleware.factory
|
||||||
|
|
||||||
|
[filter:noauth_v3]
|
||||||
|
paste.filter_factory = nova.api.openstack.auth:NoAuthMiddlewareV3.factory
|
||||||
|
|
||||||
|
[filter:ratelimit]
|
||||||
|
paste.filter_factory = nova.api.openstack.compute.limits:RateLimitingMiddleware.factory
|
||||||
|
|
||||||
|
[filter:sizelimit]
|
||||||
|
paste.filter_factory = nova.api.sizelimit:RequestBodySizeLimiter.factory
|
||||||
|
|
||||||
|
[app:osapi_compute_app_v2]
|
||||||
|
paste.app_factory = nova.api.openstack.compute:APIRouter.factory
|
||||||
|
|
||||||
|
[app:osapi_compute_app_v3]
|
||||||
|
paste.app_factory = nova.api.openstack.compute:APIRouterV3.factory
|
||||||
|
|
||||||
|
[pipeline:oscomputeversions]
|
||||||
|
pipeline = faultwrap oscomputeversionapp
|
||||||
|
|
||||||
|
[app:oscomputeversionapp]
|
||||||
|
paste.app_factory = nova.api.openstack.compute.versions:Versions.factory
|
||||||
|
|
||||||
|
##########
|
||||||
|
# Shared #
|
||||||
|
##########
|
||||||
|
|
||||||
|
[filter:keystonecontext]
|
||||||
|
paste.filter_factory = nova.api.auth:NovaKeystoneContext.factory
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystoneclient.middleware.auth_token:filter_factory
|
||||||
|
{% if service_host -%}
|
||||||
|
# NOTE(jamespage) - not used - but required for relation to nova-compute
|
||||||
|
service_protocol = {{ service_protocol }}
|
||||||
|
service_host = {{ service_host }}
|
||||||
|
service_port = {{ service_port }}
|
||||||
|
auth_host = {{ auth_host }}
|
||||||
|
auth_port = {{ auth_port }}
|
||||||
|
auth_protocol = {{ auth_protocol }}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
{% endif -%}
|
30
templates/icehouse/ml2_conf.ini
Normal file
30
templates/icehouse/ml2_conf.ini
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# icehouse
|
||||||
|
###############################################################################
|
||||||
|
# [ WARNING ]
|
||||||
|
# Configuration file maintained by Juju. Local changes may be overwritten.
|
||||||
|
###############################################################################
|
||||||
|
[ml2]
|
||||||
|
type_drivers = gre,vxlan
|
||||||
|
tenant_network_types = gre,vxlan
|
||||||
|
mechanism_drivers = openvswitch
|
||||||
|
|
||||||
|
[ml2_type_gre]
|
||||||
|
tunnel_id_ranges = 1:1000
|
||||||
|
|
||||||
|
[ml2_type_vxlan]
|
||||||
|
vni_ranges = 1001:2000
|
||||||
|
|
||||||
|
[ovs]
|
||||||
|
enable_tunneling = True
|
||||||
|
local_ip = {{ local_ip }}
|
||||||
|
|
||||||
|
[agent]
|
||||||
|
tunnel_types = gre
|
||||||
|
|
||||||
|
[securitygroup]
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
enable_security_group = True
|
||||||
|
firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver
|
||||||
|
{% else -%}
|
||||||
|
enable_security_group = False
|
||||||
|
{% endif -%}
|
71
templates/icehouse/neutron.conf
Normal file
71
templates/icehouse/neutron.conf
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
###############################################################################
|
||||||
|
# [ 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
|
||||||
|
auth_strategy = keystone
|
||||||
|
notification_driver = neutron.openstack.common.notifier.rpc_notifier
|
||||||
|
|
||||||
|
{% if neutron_bind_port -%}
|
||||||
|
bind_port = {{ neutron_bind_port }}
|
||||||
|
{% else -%}
|
||||||
|
bind_port = 9696
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% if core_plugin -%}
|
||||||
|
core_plugin = {{ core_plugin }}
|
||||||
|
{% if neutron_plugin in ['ovs', 'ml2'] -%}
|
||||||
|
service_plugins = neutron.services.l3_router.l3_router_plugin.L3RouterPlugin,neutron.services.firewall.fwaas_plugin.FirewallPlugin,neutron.services.loadbalancer.plugin.LoadBalancerPlugin,neutron.services.vpn.plugin.VPNDriverPlugin,neutron.services.metering.metering_plugin.MeteringPlugin
|
||||||
|
{% endif -%}
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
allow_overlapping_ips = True
|
||||||
|
neutron_firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% include "parts/rabbitmq" %}
|
||||||
|
|
||||||
|
notify_nova_on_port_status_changes = True
|
||||||
|
notify_nova_on_port_data_changes = True
|
||||||
|
nova_url = {{ nova_url }}
|
||||||
|
nova_region_name = {{ region }}
|
||||||
|
{% if auth_host -%}
|
||||||
|
nova_admin_username = {{ admin_user }}
|
||||||
|
nova_admin_tenant_id = {{ admin_tenant_id }}
|
||||||
|
nova_admin_password = {{ admin_password }}
|
||||||
|
nova_admin_auth_url = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/v2.0
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[quotas]
|
||||||
|
quota_driver = neutron.db.quota_db.DbQuotaDriver
|
||||||
|
{% if neutron_security_groups -%}
|
||||||
|
quota_items = network,subnet,port,security_group,security_group_rule
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
[agent]
|
||||||
|
root_helper = sudo /usr/bin/neutron-rootwrap /etc/neutron/rootwrap.conf
|
||||||
|
|
||||||
|
[keystone_authtoken]
|
||||||
|
signing_dir = $state_path/keystone-signing
|
||||||
|
{% if service_host -%}
|
||||||
|
service_protocol = {{ service_protocol }}
|
||||||
|
service_host = {{ service_host }}
|
||||||
|
service_port = {{ service_port }}
|
||||||
|
auth_host = {{ auth_host }}
|
||||||
|
auth_port = {{ auth_port }}
|
||||||
|
auth_protocol = {{ auth_protocol }}
|
||||||
|
admin_tenant_name = {{ admin_tenant_name }}
|
||||||
|
admin_user = {{ admin_user }}
|
||||||
|
admin_password = {{ admin_password }}
|
||||||
|
{% endif -%}
|
||||||
|
|
||||||
|
{% include "parts/section-database" %}
|
||||||
|
|
||||||
|
[service_providers]
|
||||||
|
service_provider=LOADBALANCER:Haproxy:neutron.services.loadbalancer.drivers.haproxy.plugin_driver.HaproxyOnHostPluginDriver:default
|
||||||
|
service_provider=VPN:openswan:neutron.services.vpn.service_drivers.ipsec.IPsecVPNDriver:default
|
||||||
|
service_provider=FIREWALL:Iptables:neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewallDriver:default
|
3
templates/parts/database
Normal file
3
templates/parts/database
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{% if database_host -%}
|
||||||
|
sql_connection = {{ database_type }}://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}{% if database_ssl_ca %}?ssl_ca={{ database_ssl_ca }}{% if database_ssl_cert %}&ssl_cert={{ database_ssl_cert }}&ssl_key={{ database_ssl_key }}{% endif %}{% endif %}
|
||||||
|
{% endif -%}
|
21
templates/parts/rabbitmq
Normal file
21
templates/parts/rabbitmq
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% if rabbitmq_host or rabbitmq_hosts -%}
|
||||||
|
rabbit_userid = {{ rabbitmq_user }}
|
||||||
|
rabbit_virtual_host = {{ rabbitmq_virtual_host }}
|
||||||
|
rabbit_password = {{ rabbitmq_password }}
|
||||||
|
{% if rabbitmq_hosts -%}
|
||||||
|
rabbit_hosts = {{ rabbitmq_hosts }}
|
||||||
|
{% if rabbitmq_ha_queues -%}
|
||||||
|
rabbit_ha_queues = True
|
||||||
|
rabbit_durable_queues = False
|
||||||
|
{% endif -%}
|
||||||
|
{% else -%}
|
||||||
|
rabbit_host = {{ rabbitmq_host }}
|
||||||
|
{% endif -%}
|
||||||
|
{% if rabbit_ssl_port -%}
|
||||||
|
rabbit_use_ssl = True
|
||||||
|
rabbit_port = {{ rabbit_ssl_port }}
|
||||||
|
{% if rabbit_ssl_ca -%}
|
||||||
|
kombu_ssl_ca_certs = {{ rabbit_ssl_ca }}
|
||||||
|
{% endif -%}
|
||||||
|
{% endif -%}
|
||||||
|
{% endif -%}
|
4
templates/parts/section-database
Normal file
4
templates/parts/section-database
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{% if database_host -%}
|
||||||
|
[database]
|
||||||
|
connection = {{ database_type }}://{{ database_user }}:{{ database_password }}@{{ database_host }}/{{ database }}{% if database_ssl_ca %}?ssl_ca={{ database_ssl_ca }}{% if database_ssl_cert %}&ssl_cert={{ database_ssl_cert }}&ssl_key={{ database_ssl_key }}{% endif %}{% endif %}
|
||||||
|
{% endif -%}
|
Loading…
Reference in New Issue
Block a user