Checkin initial python redux work.
This commit is contained in:
parent
866b47a50f
commit
5eebc6c787
0
hooks/__init__.py
Normal file
0
hooks/__init__.py
Normal file
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
58
hooks/charmhelpers/contrib/hahelpers/apache.py
Normal file
58
hooks/charmhelpers/contrib/hahelpers/apache.py
Normal file
@ -0,0 +1,58 @@
|
||||
#
|
||||
# Copyright 2012 Canonical Ltd.
|
||||
#
|
||||
# This file is sourced from lp:openstack-charm-helpers
|
||||
#
|
||||
# Authors:
|
||||
# James Page <james.page@ubuntu.com>
|
||||
# Adam Gandelman <adamg@ubuntu.com>
|
||||
#
|
||||
|
||||
import subprocess
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
config as config_get,
|
||||
relation_get,
|
||||
relation_ids,
|
||||
related_units as relation_list,
|
||||
log,
|
||||
INFO,
|
||||
)
|
||||
|
||||
|
||||
def get_cert():
|
||||
cert = config_get('ssl_cert')
|
||||
key = config_get('ssl_key')
|
||||
if not (cert and key):
|
||||
log("Inspecting identity-service relations for SSL certificate.",
|
||||
level=INFO)
|
||||
cert = key = None
|
||||
for r_id in relation_ids('identity-service'):
|
||||
for unit in relation_list(r_id):
|
||||
if not cert:
|
||||
cert = relation_get('ssl_cert',
|
||||
rid=r_id, unit=unit)
|
||||
if not key:
|
||||
key = relation_get('ssl_key',
|
||||
rid=r_id, unit=unit)
|
||||
return (cert, key)
|
||||
|
||||
|
||||
def get_ca_cert():
|
||||
ca_cert = None
|
||||
log("Inspecting identity-service relations for CA SSL certificate.",
|
||||
level=INFO)
|
||||
for r_id in relation_ids('identity-service'):
|
||||
for unit in relation_list(r_id):
|
||||
if not ca_cert:
|
||||
ca_cert = relation_get('ca_cert',
|
||||
rid=r_id, unit=unit)
|
||||
return ca_cert
|
||||
|
||||
|
||||
def install_ca_cert(ca_cert):
|
||||
if ca_cert:
|
||||
with open('/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt',
|
||||
'w') as crt:
|
||||
crt.write(ca_cert)
|
||||
subprocess.check_call(['update-ca-certificates', '--fresh'])
|
278
hooks/charmhelpers/contrib/hahelpers/ceph.py
Normal file
278
hooks/charmhelpers/contrib/hahelpers/ceph.py
Normal file
@ -0,0 +1,278 @@
|
||||
#
|
||||
# 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
|
||||
|
||||
from subprocess import (
|
||||
check_call,
|
||||
check_output,
|
||||
CalledProcessError
|
||||
)
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
relation_get,
|
||||
relation_ids,
|
||||
related_units,
|
||||
log,
|
||||
INFO,
|
||||
)
|
||||
|
||||
from charmhelpers.core.host import (
|
||||
apt_install,
|
||||
mount,
|
||||
mounts,
|
||||
service_start,
|
||||
service_stop,
|
||||
umount,
|
||||
)
|
||||
|
||||
KEYRING = '/etc/ceph/ceph.client.%s.keyring'
|
||||
KEYFILE = '/etc/ceph/ceph.client.%s.key'
|
||||
|
||||
CEPH_CONF = """[global]
|
||||
auth supported = %(auth)s
|
||||
keyring = %(keyring)s
|
||||
mon host = %(mon_hosts)s
|
||||
"""
|
||||
|
||||
|
||||
def running(service):
|
||||
# this local util can be dropped as soon the following branch lands
|
||||
# in lp:charm-helpers
|
||||
# https://code.launchpad.net/~gandelman-a/charm-helpers/service_running/
|
||||
try:
|
||||
output = check_output(['service', service, 'status'])
|
||||
except CalledProcessError:
|
||||
return False
|
||||
else:
|
||||
if ("start/running" in output or "is running" in output):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def install():
|
||||
ceph_dir = "/etc/ceph"
|
||||
if not os.path.isdir(ceph_dir):
|
||||
os.mkdir(ceph_dir)
|
||||
apt_install('ceph-common', fatal=True)
|
||||
|
||||
|
||||
def rbd_exists(service, pool, rbd_img):
|
||||
(rc, out) = commands.getstatusoutput('rbd list --id %s --pool %s' %
|
||||
(service, pool))
|
||||
return rbd_img in out
|
||||
|
||||
|
||||
def create_rbd_image(service, pool, image, sizemb):
|
||||
cmd = [
|
||||
'rbd',
|
||||
'create',
|
||||
image,
|
||||
'--size',
|
||||
str(sizemb),
|
||||
'--id',
|
||||
service,
|
||||
'--pool',
|
||||
pool
|
||||
]
|
||||
check_call(cmd)
|
||||
|
||||
|
||||
def pool_exists(service, name):
|
||||
(rc, out) = commands.getstatusoutput("rados --id %s lspools" % service)
|
||||
return name in out
|
||||
|
||||
|
||||
def create_pool(service, name):
|
||||
cmd = [
|
||||
'rados',
|
||||
'--id',
|
||||
service,
|
||||
'mkpool',
|
||||
name
|
||||
]
|
||||
check_call(cmd)
|
||||
|
||||
|
||||
def keyfile_path(service):
|
||||
return KEYFILE % service
|
||||
|
||||
|
||||
def keyring_path(service):
|
||||
return KEYRING % service
|
||||
|
||||
|
||||
def create_keyring(service, key):
|
||||
keyring = keyring_path(service)
|
||||
if os.path.exists(keyring):
|
||||
log('ceph: Keyring exists at %s.' % keyring, level=INFO)
|
||||
cmd = [
|
||||
'ceph-authtool',
|
||||
keyring,
|
||||
'--create-keyring',
|
||||
'--name=client.%s' % service,
|
||||
'--add-key=%s' % key
|
||||
]
|
||||
check_call(cmd)
|
||||
log('ceph: Created new ring at %s.' % keyring, level=INFO)
|
||||
|
||||
|
||||
def create_key_file(service, key):
|
||||
# create a file containing the key
|
||||
keyfile = keyfile_path(service)
|
||||
if os.path.exists(keyfile):
|
||||
log('ceph: Keyfile exists at %s.' % keyfile, level=INFO)
|
||||
fd = open(keyfile, 'w')
|
||||
fd.write(key)
|
||||
fd.close()
|
||||
log('ceph: Created new keyfile at %s.' % keyfile, level=INFO)
|
||||
|
||||
|
||||
def get_ceph_nodes():
|
||||
hosts = []
|
||||
for r_id in relation_ids('ceph'):
|
||||
for unit in related_units(r_id):
|
||||
hosts.append(relation_get('private-address', unit=unit, rid=r_id))
|
||||
return hosts
|
||||
|
||||
|
||||
def configure(service, key, auth):
|
||||
create_keyring(service, key)
|
||||
create_key_file(service, key)
|
||||
hosts = get_ceph_nodes()
|
||||
mon_hosts = ",".join(map(str, hosts))
|
||||
keyring = keyring_path(service)
|
||||
with open('/etc/ceph/ceph.conf', 'w') as ceph_conf:
|
||||
ceph_conf.write(CEPH_CONF % locals())
|
||||
modprobe_kernel_module('rbd')
|
||||
|
||||
|
||||
def image_mapped(image_name):
|
||||
(rc, out) = commands.getstatusoutput('rbd showmapped')
|
||||
return image_name in out
|
||||
|
||||
|
||||
def map_block_storage(service, pool, image):
|
||||
cmd = [
|
||||
'rbd',
|
||||
'map',
|
||||
'%s/%s' % (pool, image),
|
||||
'--user',
|
||||
service,
|
||||
'--secret',
|
||||
keyfile_path(service),
|
||||
]
|
||||
check_call(cmd)
|
||||
|
||||
|
||||
def filesystem_mounted(fs):
|
||||
return fs in [f for m, f in mounts()]
|
||||
|
||||
|
||||
def make_filesystem(blk_device, fstype='ext4'):
|
||||
log('ceph: Formatting block device %s as filesystem %s.' %
|
||||
(blk_device, fstype), level=INFO)
|
||||
cmd = ['mkfs', '-t', fstype, blk_device]
|
||||
check_call(cmd)
|
||||
|
||||
|
||||
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)
|
180
hooks/charmhelpers/contrib/hahelpers/cluster.py
Normal file
180
hooks/charmhelpers/contrib/hahelpers/cluster.py
Normal file
@ -0,0 +1,180 @@
|
||||
#
|
||||
# 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,
|
||||
)
|
||||
|
||||
|
||||
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):
|
||||
if None not in [
|
||||
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),
|
||||
]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def determine_api_port(public_port):
|
||||
'''
|
||||
Determine correct API server listening port based on
|
||||
existence of HTTPS reverse proxy and/or haproxy.
|
||||
|
||||
public_port: int: standard public port for given service
|
||||
|
||||
returns: int: the correct listening port for the API service
|
||||
'''
|
||||
i = 0
|
||||
if len(peer_units()) > 0 or is_clustered():
|
||||
i += 1
|
||||
if https():
|
||||
i += 1
|
||||
return public_port - (i * 10)
|
||||
|
||||
|
||||
def determine_haproxy_port(public_port):
|
||||
'''
|
||||
Description: Determine correct proxy listening port based on public IP +
|
||||
existence of HTTPS reverse proxy.
|
||||
|
||||
public_port: int: standard public port for given service
|
||||
|
||||
returns: int: the correct listening port for the HAProxy service
|
||||
'''
|
||||
i = 0
|
||||
if https():
|
||||
i += 1
|
||||
return public_port - (i * 10)
|
||||
|
||||
|
||||
def get_hacluster_config():
|
||||
'''
|
||||
Obtains all relevant configuration from charm configuration required
|
||||
for initiating a relation to hacluster:
|
||||
|
||||
ha-bindiface, ha-mcastport, vip, vip_iface, vip_cidr
|
||||
|
||||
returns: dict: A dict containing settings keyed by setting name.
|
||||
raises: HAIncompleteConfig if settings are missing.
|
||||
'''
|
||||
settings = ['ha-bindiface', 'ha-mcastport', 'vip', 'vip_iface', 'vip_cidr']
|
||||
conf = {}
|
||||
for setting in settings:
|
||||
conf[setting] = config_get(setting)
|
||||
missing = []
|
||||
[missing.append(s) for s, v in conf.iteritems() if v is None]
|
||||
if missing:
|
||||
log('Insufficient config data to configure hacluster.', level=ERROR)
|
||||
raise HAIncompleteConfig
|
||||
return conf
|
||||
|
||||
|
||||
def canonical_url(configs, vip_setting='vip'):
|
||||
'''
|
||||
Returns the correct HTTP URL to this host given the state of HTTPS
|
||||
configuration and hacluster.
|
||||
|
||||
:configs : OSTemplateRenderer: A config tempating object to inspect for
|
||||
a complete https context.
|
||||
:vip_setting: str: Setting in charm config that specifies
|
||||
VIP address.
|
||||
'''
|
||||
scheme = 'http'
|
||||
if 'https' in configs.complete_contexts():
|
||||
scheme = 'https'
|
||||
if is_clustered():
|
||||
addr = config_get(vip_setting)
|
||||
else:
|
||||
addr = get_unit_hostname()
|
||||
return '%s://%s' % (scheme, addr)
|
0
hooks/charmhelpers/contrib/openstack/__init__.py
Normal file
0
hooks/charmhelpers/contrib/openstack/__init__.py
Normal file
271
hooks/charmhelpers/contrib/openstack/context.py
Normal file
271
hooks/charmhelpers/contrib/openstack/context.py
Normal file
@ -0,0 +1,271 @@
|
||||
import os
|
||||
|
||||
from base64 import b64decode
|
||||
|
||||
from subprocess import (
|
||||
check_call
|
||||
)
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
config,
|
||||
local_unit,
|
||||
log,
|
||||
relation_get,
|
||||
relation_ids,
|
||||
related_units,
|
||||
unit_get,
|
||||
)
|
||||
|
||||
from charmhelpers.contrib.hahelpers.cluster import (
|
||||
determine_api_port,
|
||||
determine_haproxy_port,
|
||||
https,
|
||||
is_clustered,
|
||||
peer_units,
|
||||
)
|
||||
|
||||
from charmhelpers.contrib.hahelpers.apache import (
|
||||
get_cert,
|
||||
get_ca_cert,
|
||||
)
|
||||
|
||||
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
|
||||
|
||||
|
||||
class OSContextError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def context_complete(ctxt):
|
||||
_missing = []
|
||||
for k, v in ctxt.iteritems():
|
||||
if v is None or v == '':
|
||||
_missing.append(k)
|
||||
if _missing:
|
||||
log('Missing required data: %s' % ' '.join(_missing), level='INFO')
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
class OSContextGenerator(object):
|
||||
interfaces = []
|
||||
|
||||
def __call__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SharedDBContext(OSContextGenerator):
|
||||
interfaces = ['shared-db']
|
||||
|
||||
def __call__(self):
|
||||
log('Generating template context for shared-db')
|
||||
conf = config()
|
||||
try:
|
||||
database = conf['database']
|
||||
username = conf['database-user']
|
||||
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('shared-db'):
|
||||
for unit in related_units(rid):
|
||||
ctxt = {
|
||||
'database_host': relation_get('db_host', rid=rid,
|
||||
unit=unit),
|
||||
'database': database,
|
||||
'database_user': username,
|
||||
'database_password': relation_get('password', rid=rid,
|
||||
unit=unit)
|
||||
}
|
||||
if not context_complete(ctxt):
|
||||
return {}
|
||||
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):
|
||||
ctxt = {
|
||||
'service_port': relation_get('service_port', rid=rid,
|
||||
unit=unit),
|
||||
'service_host': relation_get('service_host', rid=rid,
|
||||
unit=unit),
|
||||
'auth_host': relation_get('auth_host', rid=rid, unit=unit),
|
||||
'auth_port': relation_get('auth_port', rid=rid, unit=unit),
|
||||
'admin_tenant_name': relation_get('service_tenant',
|
||||
rid=rid, unit=unit),
|
||||
'admin_user': relation_get('service_username', rid=rid,
|
||||
unit=unit),
|
||||
'admin_password': relation_get('service_password', rid=rid,
|
||||
unit=unit),
|
||||
# XXX: Hard-coded http.
|
||||
'service_protocol': 'http',
|
||||
'auth_protocol': 'http',
|
||||
}
|
||||
if not context_complete(ctxt):
|
||||
return {}
|
||||
return ctxt
|
||||
|
||||
|
||||
class AMQPContext(OSContextGenerator):
|
||||
interfaces = ['amqp']
|
||||
|
||||
def __call__(self):
|
||||
log('Generating template context for amqp')
|
||||
conf = config()
|
||||
try:
|
||||
username = conf['rabbit-user']
|
||||
vhost = conf['rabbit-vhost']
|
||||
except KeyError as e:
|
||||
log('Could not generate shared_db context. '
|
||||
'Missing required charm config options: %s.' % e)
|
||||
raise OSContextError
|
||||
|
||||
ctxt = {}
|
||||
for rid in relation_ids('amqp'):
|
||||
for unit in related_units(rid):
|
||||
if relation_get('clustered', rid=rid, unit=unit):
|
||||
rabbitmq_host = relation_get('vip', rid=rid, unit=unit)
|
||||
else:
|
||||
rabbitmq_host = relation_get('private-address',
|
||||
rid=rid, unit=unit)
|
||||
ctxt = {
|
||||
'rabbitmq_host': rabbitmq_host,
|
||||
'rabbitmq_user': username,
|
||||
'rabbitmq_password': relation_get('password', rid=rid,
|
||||
unit=unit),
|
||||
'rabbitmq_virtual_host': vhost,
|
||||
}
|
||||
if not context_complete(ctxt):
|
||||
return {}
|
||||
return ctxt
|
||||
|
||||
|
||||
class CephContext(OSContextGenerator):
|
||||
interfaces = ['ceph']
|
||||
|
||||
def __call__(self):
|
||||
'''This generates context for /etc/ceph/ceph.conf templates'''
|
||||
log('Generating tmeplate context for ceph')
|
||||
mon_hosts = []
|
||||
auth = None
|
||||
for rid in relation_ids('ceph'):
|
||||
for unit in related_units(rid):
|
||||
mon_hosts.append(relation_get('private-address', rid=rid,
|
||||
unit=unit))
|
||||
auth = relation_get('auth', rid=rid, unit=unit)
|
||||
|
||||
ctxt = {
|
||||
'mon_hosts': ' '.join(mon_hosts),
|
||||
'auth': auth,
|
||||
}
|
||||
if not context_complete(ctxt):
|
||||
return {}
|
||||
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 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))
|
||||
|
||||
def __call__(self):
|
||||
if isinstance(self.external_ports, basestring):
|
||||
self.external_ports = [self.external_ports]
|
||||
if (not self.external_ports or not https()):
|
||||
return {}
|
||||
|
||||
self.configure_cert()
|
||||
self.enable_modules()
|
||||
|
||||
ctxt = {
|
||||
'namespace': self.service_namespace,
|
||||
'private_address': unit_get('private-address'),
|
||||
'endpoints': []
|
||||
}
|
||||
for ext_port in self.external_ports:
|
||||
if peer_units() or is_clustered():
|
||||
int_port = determine_haproxy_port(ext_port)
|
||||
else:
|
||||
int_port = determine_api_port(ext_port)
|
||||
portmap = (int(ext_port), int(int_port))
|
||||
ctxt['endpoints'].append(portmap)
|
||||
return ctxt
|
@ -0,0 +1,2 @@
|
||||
# dummy __init__.py to fool syncer into thinking this is a syncable python
|
||||
# module
|
11
hooks/charmhelpers/contrib/openstack/templates/ceph.conf
Normal file
11
hooks/charmhelpers/contrib/openstack/templates/ceph.conf
Normal file
@ -0,0 +1,11 @@
|
||||
###############################################################################
|
||||
# [ WARNING ]
|
||||
# cinder configuration file maintained by Juju
|
||||
# local changes may be overwritten.
|
||||
###############################################################################
|
||||
{% if auth %}
|
||||
[global]
|
||||
auth_supported = {{ auth }}
|
||||
keyring = /etc/ceph/$cluster.$name.keyring
|
||||
mon host = {{ mon_hosts }}
|
||||
{% endif %}
|
37
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg
Normal file
37
hooks/charmhelpers/contrib/openstack/templates/haproxy.cfg
Normal file
@ -0,0 +1,37 @@
|
||||
global
|
||||
log 127.0.0.1 local0
|
||||
log 127.0.0.1 local1 notice
|
||||
maxconn 20000
|
||||
user haproxy
|
||||
group haproxy
|
||||
spread-checks 0
|
||||
|
||||
defaults
|
||||
log global
|
||||
mode http
|
||||
option httplog
|
||||
option dontlognull
|
||||
retries 3
|
||||
timeout queue 1000
|
||||
timeout connect 1000
|
||||
timeout client 30000
|
||||
timeout server 30000
|
||||
|
||||
listen stats :8888
|
||||
mode http
|
||||
stats enable
|
||||
stats hide-version
|
||||
stats realm Haproxy\ Statistics
|
||||
stats uri /
|
||||
stats auth admin:password
|
||||
|
||||
{% if units %}
|
||||
{% for service, ports in service_ports.iteritems() -%}
|
||||
listen {{ service }} 0.0.0.0:{{ ports[0] }}
|
||||
balance roundrobin
|
||||
option tcplog
|
||||
{% for unit, address in units.iteritems() -%}
|
||||
server {{ unit }} {{ address }}:{{ ports[1] }} check
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
@ -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 %}
|
261
hooks/charmhelpers/contrib/openstack/templating.py
Normal file
261
hooks/charmhelpers/contrib/openstack/templating.py
Normal file
@ -0,0 +1,261 @@
|
||||
import os
|
||||
|
||||
from charmhelpers.core.host 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
|
||||
except ImportError:
|
||||
# python-jinja2 may not be installed yet, or we're running unittests.
|
||||
FileSystemLoader = ChoiceLoader = Environment = 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)
|
||||
log('Rendering from template: %s' % _tmpl, level=INFO)
|
||||
template = self._get_template(_tmpl)
|
||||
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
|
||||
with open(config_file, 'wb') as out:
|
||||
out.write(self.render(config_file))
|
||||
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
|
271
hooks/charmhelpers/contrib/openstack/utils.py
Normal file
271
hooks/charmhelpers/contrib/openstack/utils.py
Normal file
@ -0,0 +1,271 @@
|
||||
#!/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 sys
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
config,
|
||||
log as juju_log,
|
||||
)
|
||||
|
||||
from charmhelpers.core.host import (
|
||||
lsb_release,
|
||||
apt_install
|
||||
)
|
||||
|
||||
CLOUD_ARCHIVE_URL = "http://ubuntu-cloud.archive.canonical.com/ubuntu"
|
||||
CLOUD_ARCHIVE_KEY_ID = '5EDB1B62EC4926EA'
|
||||
|
||||
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
|
||||
('oneiric', 'diablo'),
|
||||
('precise', 'essex'),
|
||||
('quantal', 'folsom'),
|
||||
('raring', 'grizzly'),
|
||||
('saucy', 'havana'),
|
||||
])
|
||||
|
||||
|
||||
OPENSTACK_CODENAMES = OrderedDict([
|
||||
('2011.2', 'diablo'),
|
||||
('2012.1', 'essex'),
|
||||
('2012.2', 'folsom'),
|
||||
('2013.1', 'grizzly'),
|
||||
('2013.2', 'havana'),
|
||||
('2014.1', 'icehouse'),
|
||||
])
|
||||
|
||||
# The ugly duckling
|
||||
SWIFT_CODENAMES = {
|
||||
'1.4.3': 'diablo',
|
||||
'1.4.8': 'essex',
|
||||
'1.7.4': 'folsom',
|
||||
'1.7.6': 'grizzly',
|
||||
'1.7.7': 'grizzly',
|
||||
'1.8.0': 'grizzly',
|
||||
'1.9.0': 'havana',
|
||||
'1.9.1': 'havana',
|
||||
}
|
||||
|
||||
|
||||
def error_out(msg):
|
||||
juju_log("FATAL ERROR: %s" % msg, level='ERROR')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_os_codename_install_source(src):
|
||||
'''Derive OpenStack release codename from a given installation source.'''
|
||||
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
||||
rel = ''
|
||||
if src == 'distro':
|
||||
try:
|
||||
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
|
||||
except KeyError:
|
||||
e = 'Could not derive openstack release for '\
|
||||
'this Ubuntu release: %s' % ubuntu_rel
|
||||
error_out(e)
|
||||
return rel
|
||||
|
||||
if src.startswith('cloud:'):
|
||||
ca_rel = src.split(':')[1]
|
||||
ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0]
|
||||
return ca_rel
|
||||
|
||||
# Best guess match based on deb string provided
|
||||
if src.startswith('deb') or src.startswith('ppa'):
|
||||
for k, v in OPENSTACK_CODENAMES.iteritems():
|
||||
if v in src:
|
||||
return v
|
||||
|
||||
|
||||
def get_os_version_install_source(src):
|
||||
codename = get_os_codename_install_source(src)
|
||||
return get_os_version_codename(codename)
|
||||
|
||||
|
||||
def get_os_codename_version(vers):
|
||||
'''Determine OpenStack codename from version number.'''
|
||||
try:
|
||||
return OPENSTACK_CODENAMES[vers]
|
||||
except KeyError:
|
||||
e = 'Could not determine OpenStack codename for version %s' % vers
|
||||
error_out(e)
|
||||
|
||||
|
||||
def get_os_version_codename(codename):
|
||||
'''Determine OpenStack version number from codename.'''
|
||||
for k, v in OPENSTACK_CODENAMES.iteritems():
|
||||
if v == codename:
|
||||
return k
|
||||
e = 'Could not derive OpenStack version for '\
|
||||
'codename: %s' % codename
|
||||
error_out(e)
|
||||
|
||||
|
||||
def get_os_codename_package(package, fatal=True):
|
||||
'''Derive OpenStack release codename from an installed package.'''
|
||||
apt.init()
|
||||
cache = apt.Cache()
|
||||
|
||||
try:
|
||||
pkg = cache[package]
|
||||
except:
|
||||
if not fatal:
|
||||
return None
|
||||
# the package is unknown to the current apt cache.
|
||||
e = 'Could not determine version of package with no installation '\
|
||||
'candidate: %s' % package
|
||||
error_out(e)
|
||||
|
||||
if not pkg.current_ver:
|
||||
if not fatal:
|
||||
return None
|
||||
# package is known, but no version is currently installed.
|
||||
e = 'Could not determine version of uninstalled package: %s' % package
|
||||
error_out(e)
|
||||
|
||||
vers = apt.UpstreamVersion(pkg.current_ver.ver_str)
|
||||
|
||||
try:
|
||||
if 'swift' in pkg.name:
|
||||
vers = vers[:5]
|
||||
return SWIFT_CODENAMES[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)
|
||||
|
||||
|
||||
def import_key(keyid):
|
||||
cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \
|
||||
"--recv-keys %s" % keyid
|
||||
try:
|
||||
subprocess.check_call(cmd.split(' '))
|
||||
except subprocess.CalledProcessError:
|
||||
error_out("Error importing repo key %s" % keyid)
|
||||
|
||||
|
||||
def configure_installation_source(rel):
|
||||
'''Configure apt installation source.'''
|
||||
if rel == 'distro':
|
||||
return
|
||||
elif rel[:4] == "ppa:":
|
||||
src = rel
|
||||
subprocess.check_call(["add-apt-repository", "-y", src])
|
||||
elif rel[:3] == "deb":
|
||||
l = len(rel.split('|'))
|
||||
if l == 2:
|
||||
src, key = rel.split('|')
|
||||
juju_log("Importing PPA key from keyserver for %s" % src)
|
||||
import_key(key)
|
||||
elif l == 1:
|
||||
src = rel
|
||||
with open('/etc/apt/sources.list.d/juju_deb.list', 'w') as f:
|
||||
f.write(src)
|
||||
elif rel[:6] == 'cloud:':
|
||||
ubuntu_rel = lsb_release()['DISTRIB_CODENAME']
|
||||
rel = rel.split(':')[1]
|
||||
u_rel = rel.split('-')[0]
|
||||
ca_rel = rel.split('-')[1]
|
||||
|
||||
if u_rel != ubuntu_rel:
|
||||
e = 'Cannot install from Cloud Archive pocket %s on this Ubuntu '\
|
||||
'version (%s)' % (ca_rel, ubuntu_rel)
|
||||
error_out(e)
|
||||
|
||||
if 'staging' in ca_rel:
|
||||
# staging is just a regular PPA.
|
||||
os_rel = ca_rel.split('/')[0]
|
||||
ppa = 'ppa:ubuntu-cloud-archive/%s-staging' % os_rel
|
||||
cmd = 'add-apt-repository -y %s' % ppa
|
||||
subprocess.check_call(cmd.split(' '))
|
||||
return
|
||||
|
||||
# map charm config options to actual archive pockets.
|
||||
pockets = {
|
||||
'folsom': 'precise-updates/folsom',
|
||||
'folsom/updates': 'precise-updates/folsom',
|
||||
'folsom/proposed': 'precise-proposed/folsom',
|
||||
'grizzly': 'precise-updates/grizzly',
|
||||
'grizzly/updates': 'precise-updates/grizzly',
|
||||
'grizzly/proposed': 'precise-proposed/grizzly',
|
||||
'havana': 'precise-updates/havana',
|
||||
'havana/updates': 'precise-updates/havana',
|
||||
'havana/proposed': 'precise-proposed/havana',
|
||||
}
|
||||
|
||||
try:
|
||||
pocket = pockets[ca_rel]
|
||||
except KeyError:
|
||||
e = 'Invalid Cloud Archive release specified: %s' % rel
|
||||
error_out(e)
|
||||
|
||||
src = "deb %s %s main" % (CLOUD_ARCHIVE_URL, pocket)
|
||||
apt_install('ubuntu-cloud-keyring', fatal=True)
|
||||
|
||||
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as f:
|
||||
f.write(src)
|
||||
else:
|
||||
error_out("Invalid openstack-release specified: %s" % rel)
|
||||
|
||||
|
||||
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
|
||||
"""
|
||||
Write an rc file in the charm-delivered directory containing
|
||||
exported environment variables provided by env_vars. Any charm scripts run
|
||||
outside the juju hook environment can source this scriptrc to obtain
|
||||
updated config information necessary to perform health checks or
|
||||
service changes.
|
||||
"""
|
||||
unit_name = os.getenv('JUJU_UNIT_NAME').replace('/', '-')
|
||||
juju_rc_path = "/var/lib/juju/units/%s/charm/%s" % (unit_name, script_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
|
0
hooks/charmhelpers/contrib/storage/__init__.py
Normal file
0
hooks/charmhelpers/contrib/storage/__init__.py
Normal file
59
hooks/charmhelpers/contrib/storage/linux/loopback.py
Normal file
59
hooks/charmhelpers/contrib/storage/linux/loopback.py
Normal file
@ -0,0 +1,59 @@
|
||||
|
||||
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)
|
||||
'''
|
||||
cmd = ['losetup', '--find', file_path]
|
||||
return check_output(cmd).strip()
|
||||
|
||||
|
||||
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])
|
25
hooks/charmhelpers/contrib/storage/linux/utils.py
Normal file
25
hooks/charmhelpers/contrib/storage/linux/utils.py
Normal file
@ -0,0 +1,25 @@
|
||||
from os import stat
|
||||
from stat import S_ISBLK
|
||||
|
||||
from subprocess import (
|
||||
check_call
|
||||
)
|
||||
|
||||
|
||||
def is_block_device(path):
|
||||
'''
|
||||
Confirm device at path is a valid block device node.
|
||||
|
||||
:returns: boolean: True if path is a block device, False if not.
|
||||
'''
|
||||
return S_ISBLK(stat(path).st_mode)
|
||||
|
||||
|
||||
def zap_disk(block_device):
|
||||
'''
|
||||
Clear a block device of partition table. Relies on sgdisk, which is
|
||||
installed as pat of the 'gdisk' package in Ubuntu.
|
||||
|
||||
:param block_device: str: Full path of block device to clean.
|
||||
'''
|
||||
check_call(['sgdisk', '--zap-all', block_device])
|
0
hooks/charmhelpers/core/__init__.py
Normal file
0
hooks/charmhelpers/core/__init__.py
Normal file
340
hooks/charmhelpers/core/hookenv.py
Normal file
340
hooks/charmhelpers/core/hookenv.py
Normal file
@ -0,0 +1,340 @@
|
||||
"Interactions with the Juju environment"
|
||||
# Copyright 2013 Canonical Ltd.
|
||||
#
|
||||
# Authors:
|
||||
# Charm Helpers Developers <juju@lists.ubuntu.com>
|
||||
|
||||
import os
|
||||
import json
|
||||
import yaml
|
||||
import subprocess
|
||||
import UserDict
|
||||
|
||||
CRITICAL = "CRITICAL"
|
||||
ERROR = "ERROR"
|
||||
WARNING = "WARNING"
|
||||
INFO = "INFO"
|
||||
DEBUG = "DEBUG"
|
||||
MARKER = object()
|
||||
|
||||
cache = {}
|
||||
|
||||
|
||||
def cached(func):
|
||||
''' Cache return values for multiple executions of func + args
|
||||
|
||||
For example:
|
||||
|
||||
@cached
|
||||
def unit_get(attribute):
|
||||
pass
|
||||
|
||||
unit_get('test')
|
||||
|
||||
will cache the result of unit_get + 'test' for future calls.
|
||||
'''
|
||||
def wrapper(*args, **kwargs):
|
||||
global cache
|
||||
key = str((func, args, kwargs))
|
||||
try:
|
||||
return cache[key]
|
||||
except KeyError:
|
||||
res = func(*args, **kwargs)
|
||||
cache[key] = res
|
||||
return res
|
||||
return wrapper
|
||||
|
||||
|
||||
def flush(key):
|
||||
''' Flushes any entries from function cache where the
|
||||
key is found in the function+args '''
|
||||
flush_list = []
|
||||
for item in cache:
|
||||
if key in item:
|
||||
flush_list.append(item)
|
||||
for item in flush_list:
|
||||
del cache[item]
|
||||
|
||||
|
||||
def log(message, level=None):
|
||||
"Write a message to the juju log"
|
||||
command = ['juju-log']
|
||||
if level:
|
||||
command += ['-l', level]
|
||||
command += [message]
|
||||
subprocess.call(command)
|
||||
|
||||
|
||||
class Serializable(UserDict.IterableUserDict):
|
||||
"Wrapper, an object that can be serialized to yaml or json"
|
||||
|
||||
def __init__(self, obj):
|
||||
# wrap the object
|
||||
UserDict.IterableUserDict.__init__(self)
|
||||
self.data = obj
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# See if this object has attribute.
|
||||
if attr in ("json", "yaml", "data"):
|
||||
return self.__dict__[attr]
|
||||
# Check for attribute in wrapped object.
|
||||
got = getattr(self.data, attr, MARKER)
|
||||
if got is not MARKER:
|
||||
return got
|
||||
# Proxy to the wrapped object via dict interface.
|
||||
try:
|
||||
return self.data[attr]
|
||||
except KeyError:
|
||||
raise AttributeError(attr)
|
||||
|
||||
def __getstate__(self):
|
||||
# Pickle as a standard dictionary.
|
||||
return self.data
|
||||
|
||||
def __setstate__(self, state):
|
||||
# Unpickle into our wrapper.
|
||||
self.data = state
|
||||
|
||||
def json(self):
|
||||
"Serialize the object to json"
|
||||
return json.dumps(self.data)
|
||||
|
||||
def yaml(self):
|
||||
"Serialize the object to yaml"
|
||||
return yaml.dump(self.data)
|
||||
|
||||
|
||||
def execution_environment():
|
||||
"""A convenient bundling of the current execution context"""
|
||||
context = {}
|
||||
context['conf'] = config()
|
||||
if relation_id():
|
||||
context['reltype'] = relation_type()
|
||||
context['relid'] = relation_id()
|
||||
context['rel'] = relation_get()
|
||||
context['unit'] = local_unit()
|
||||
context['rels'] = relations()
|
||||
context['env'] = os.environ
|
||||
return context
|
||||
|
||||
|
||||
def in_relation_hook():
|
||||
"Determine whether we're running in a relation hook"
|
||||
return 'JUJU_RELATION' in os.environ
|
||||
|
||||
|
||||
def relation_type():
|
||||
"The scope for the current relation hook"
|
||||
return os.environ.get('JUJU_RELATION', None)
|
||||
|
||||
|
||||
def relation_id():
|
||||
"The relation ID for the current relation hook"
|
||||
return os.environ.get('JUJU_RELATION_ID', None)
|
||||
|
||||
|
||||
def local_unit():
|
||||
"Local unit ID"
|
||||
return os.environ['JUJU_UNIT_NAME']
|
||||
|
||||
|
||||
def remote_unit():
|
||||
"The remote unit for the current relation hook"
|
||||
return os.environ['JUJU_REMOTE_UNIT']
|
||||
|
||||
|
||||
def service_name():
|
||||
"The name service group this unit belongs to"
|
||||
return local_unit().split('/')[0]
|
||||
|
||||
|
||||
@cached
|
||||
def config(scope=None):
|
||||
"Juju charm configuration"
|
||||
config_cmd_line = ['config-get']
|
||||
if scope is not None:
|
||||
config_cmd_line.append(scope)
|
||||
config_cmd_line.append('--format=json')
|
||||
try:
|
||||
return json.loads(subprocess.check_output(config_cmd_line))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
@cached
|
||||
def relation_get(attribute=None, unit=None, rid=None):
|
||||
_args = ['relation-get', '--format=json']
|
||||
if rid:
|
||||
_args.append('-r')
|
||||
_args.append(rid)
|
||||
_args.append(attribute or '-')
|
||||
if unit:
|
||||
_args.append(unit)
|
||||
try:
|
||||
return json.loads(subprocess.check_output(_args))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def relation_set(relation_id=None, relation_settings={}, **kwargs):
|
||||
relation_cmd_line = ['relation-set']
|
||||
if relation_id is not None:
|
||||
relation_cmd_line.extend(('-r', relation_id))
|
||||
for k, v in (relation_settings.items() + kwargs.items()):
|
||||
if v is None:
|
||||
relation_cmd_line.append('{}='.format(k))
|
||||
else:
|
||||
relation_cmd_line.append('{}={}'.format(k, v))
|
||||
subprocess.check_call(relation_cmd_line)
|
||||
# Flush cache of any relation-gets for local unit
|
||||
flush(local_unit())
|
||||
|
||||
|
||||
@cached
|
||||
def relation_ids(reltype=None):
|
||||
"A list of relation_ids"
|
||||
reltype = reltype or relation_type()
|
||||
relid_cmd_line = ['relation-ids', '--format=json']
|
||||
if reltype is not None:
|
||||
relid_cmd_line.append(reltype)
|
||||
return json.loads(subprocess.check_output(relid_cmd_line)) or []
|
||||
return []
|
||||
|
||||
|
||||
@cached
|
||||
def related_units(relid=None):
|
||||
"A list of related units"
|
||||
relid = relid or relation_id()
|
||||
units_cmd_line = ['relation-list', '--format=json']
|
||||
if relid is not None:
|
||||
units_cmd_line.extend(('-r', relid))
|
||||
return json.loads(subprocess.check_output(units_cmd_line)) or []
|
||||
|
||||
|
||||
@cached
|
||||
def relation_for_unit(unit=None, rid=None):
|
||||
"Get the json represenation of a unit's relation"
|
||||
unit = unit or remote_unit()
|
||||
relation = relation_get(unit=unit, rid=rid)
|
||||
for key in relation:
|
||||
if key.endswith('-list'):
|
||||
relation[key] = relation[key].split()
|
||||
relation['__unit__'] = unit
|
||||
return relation
|
||||
|
||||
|
||||
@cached
|
||||
def relations_for_id(relid=None):
|
||||
"Get relations of a specific relation ID"
|
||||
relation_data = []
|
||||
relid = relid or relation_ids()
|
||||
for unit in related_units(relid):
|
||||
unit_data = relation_for_unit(unit, relid)
|
||||
unit_data['__relid__'] = relid
|
||||
relation_data.append(unit_data)
|
||||
return relation_data
|
||||
|
||||
|
||||
@cached
|
||||
def relations_of_type(reltype=None):
|
||||
"Get relations of a specific type"
|
||||
relation_data = []
|
||||
reltype = reltype or relation_type()
|
||||
for relid in relation_ids(reltype):
|
||||
for relation in relations_for_id(relid):
|
||||
relation['__relid__'] = relid
|
||||
relation_data.append(relation)
|
||||
return relation_data
|
||||
|
||||
|
||||
@cached
|
||||
def relation_types():
|
||||
"Get a list of relation types supported by this charm"
|
||||
charmdir = os.environ.get('CHARM_DIR', '')
|
||||
mdf = open(os.path.join(charmdir, 'metadata.yaml'))
|
||||
md = yaml.safe_load(mdf)
|
||||
rel_types = []
|
||||
for key in ('provides', 'requires', 'peers'):
|
||||
section = md.get(key)
|
||||
if section:
|
||||
rel_types.extend(section.keys())
|
||||
mdf.close()
|
||||
return rel_types
|
||||
|
||||
|
||||
@cached
|
||||
def relations():
|
||||
rels = {}
|
||||
for reltype in relation_types():
|
||||
relids = {}
|
||||
for relid in relation_ids(reltype):
|
||||
units = {local_unit(): relation_get(unit=local_unit(), rid=relid)}
|
||||
for unit in related_units(relid):
|
||||
reldata = relation_get(unit=unit, rid=relid)
|
||||
units[unit] = reldata
|
||||
relids[relid] = units
|
||||
rels[reltype] = relids
|
||||
return rels
|
||||
|
||||
|
||||
def open_port(port, protocol="TCP"):
|
||||
"Open a service network port"
|
||||
_args = ['open-port']
|
||||
_args.append('{}/{}'.format(port, protocol))
|
||||
subprocess.check_call(_args)
|
||||
|
||||
|
||||
def close_port(port, protocol="TCP"):
|
||||
"Close a service network port"
|
||||
_args = ['close-port']
|
||||
_args.append('{}/{}'.format(port, protocol))
|
||||
subprocess.check_call(_args)
|
||||
|
||||
|
||||
@cached
|
||||
def unit_get(attribute):
|
||||
_args = ['unit-get', '--format=json', attribute]
|
||||
try:
|
||||
return json.loads(subprocess.check_output(_args))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def unit_private_ip():
|
||||
return unit_get('private-address')
|
||||
|
||||
|
||||
class UnregisteredHookError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Hooks(object):
|
||||
def __init__(self):
|
||||
super(Hooks, self).__init__()
|
||||
self._hooks = {}
|
||||
|
||||
def register(self, name, function):
|
||||
self._hooks[name] = function
|
||||
|
||||
def execute(self, args):
|
||||
hook_name = os.path.basename(args[0])
|
||||
if hook_name in self._hooks:
|
||||
self._hooks[hook_name]()
|
||||
else:
|
||||
raise UnregisteredHookError(hook_name)
|
||||
|
||||
def hook(self, *hook_names):
|
||||
def wrapper(decorated):
|
||||
for hook_name in hook_names:
|
||||
self.register(hook_name, decorated)
|
||||
else:
|
||||
self.register(decorated.__name__, decorated)
|
||||
if '_' in decorated.__name__:
|
||||
self.register(
|
||||
decorated.__name__.replace('_', '-'), decorated)
|
||||
return decorated
|
||||
return wrapper
|
||||
|
||||
|
||||
def charm_dir():
|
||||
return os.environ.get('CHARM_DIR')
|
272
hooks/charmhelpers/core/host.py
Normal file
272
hooks/charmhelpers/core/host.py
Normal file
@ -0,0 +1,272 @@
|
||||
"""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 apt_pkg
|
||||
import os
|
||||
import pwd
|
||||
import grp
|
||||
import subprocess
|
||||
import hashlib
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from hookenv import log, execution_environment
|
||||
|
||||
|
||||
def service_start(service_name):
|
||||
service('start', service_name)
|
||||
|
||||
|
||||
def service_stop(service_name):
|
||||
service('stop', service_name)
|
||||
|
||||
|
||||
def service_restart(service_name):
|
||||
service('restart', service_name)
|
||||
|
||||
|
||||
def service_reload(service_name, restart_on_failure=False):
|
||||
if not service('reload', service_name) and restart_on_failure:
|
||||
service('restart', service_name)
|
||||
|
||||
|
||||
def service(action, service_name):
|
||||
cmd = ['service', service_name, action]
|
||||
return subprocess.call(cmd) == 0
|
||||
|
||||
|
||||
def service_running(service):
|
||||
try:
|
||||
output = subprocess.check_output(['service', service, 'status'])
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
else:
|
||||
if ("start/running" in output or "is running" in output):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def adduser(username, password=None, shell='/bin/bash', system_user=False):
|
||||
"""Add a user"""
|
||||
try:
|
||||
user_info = pwd.getpwnam(username)
|
||||
log('user {0} already exists!'.format(username))
|
||||
except KeyError:
|
||||
log('creating user {0}'.format(username))
|
||||
cmd = ['useradd']
|
||||
if system_user or password is None:
|
||||
cmd.append('--system')
|
||||
else:
|
||||
cmd.extend([
|
||||
'--create-home',
|
||||
'--shell', shell,
|
||||
'--password', password,
|
||||
])
|
||||
cmd.append(username)
|
||||
subprocess.check_call(cmd)
|
||||
user_info = pwd.getpwnam(username)
|
||||
return user_info
|
||||
|
||||
|
||||
def add_user_to_group(username, group):
|
||||
"""Add a user to a group"""
|
||||
cmd = [
|
||||
'gpasswd', '-a',
|
||||
username,
|
||||
group
|
||||
]
|
||||
log("Adding user {} to group {}".format(username, group))
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def rsync(from_path, to_path, flags='-r', options=None):
|
||||
"""Replicate the contents of a path"""
|
||||
context = execution_environment()
|
||||
options = options or ['--delete', '--executability']
|
||||
cmd = ['/usr/bin/rsync', flags]
|
||||
cmd.extend(options)
|
||||
cmd.append(from_path.format(**context))
|
||||
cmd.append(to_path.format(**context))
|
||||
log(" ".join(cmd))
|
||||
return subprocess.check_output(cmd).strip()
|
||||
|
||||
|
||||
def symlink(source, destination):
|
||||
"""Create a symbolic link"""
|
||||
context = execution_environment()
|
||||
log("Symlinking {} as {}".format(source, destination))
|
||||
cmd = [
|
||||
'ln',
|
||||
'-sf',
|
||||
source.format(**context),
|
||||
destination.format(**context)
|
||||
]
|
||||
subprocess.check_call(cmd)
|
||||
|
||||
|
||||
def mkdir(path, owner='root', group='root', perms=0555, force=False):
|
||||
"""Create a directory"""
|
||||
context = execution_environment()
|
||||
log("Making dir {} {}:{} {:o}".format(path, owner, group,
|
||||
perms))
|
||||
uid = pwd.getpwnam(owner.format(**context)).pw_uid
|
||||
gid = grp.getgrnam(group.format(**context)).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 filter_installed_packages(packages):
|
||||
"""Returns a list of packages that require installation"""
|
||||
apt_pkg.init()
|
||||
cache = apt_pkg.Cache()
|
||||
_pkgs = []
|
||||
for package in packages:
|
||||
try:
|
||||
p = cache[package]
|
||||
p.current_ver or _pkgs.append(package)
|
||||
except KeyError:
|
||||
log('Package {} has no installation candidate.'.format(package),
|
||||
level='WARNING')
|
||||
_pkgs.append(package)
|
||||
return _pkgs
|
||||
|
||||
|
||||
def apt_install(packages, options=None, fatal=False):
|
||||
"""Install one or more packages"""
|
||||
options = options or []
|
||||
cmd = ['apt-get', '-y']
|
||||
cmd.extend(options)
|
||||
cmd.append('install')
|
||||
if isinstance(packages, basestring):
|
||||
cmd.append(packages)
|
||||
else:
|
||||
cmd.extend(packages)
|
||||
log("Installing {} with options: {}".format(packages,
|
||||
options))
|
||||
if fatal:
|
||||
subprocess.check_call(cmd)
|
||||
else:
|
||||
subprocess.call(cmd)
|
||||
|
||||
|
||||
def apt_update(fatal=False):
|
||||
"""Update local apt cache"""
|
||||
cmd = ['apt-get', 'update']
|
||||
if fatal:
|
||||
subprocess.check_call(cmd)
|
||||
else:
|
||||
subprocess.call(cmd)
|
||||
|
||||
|
||||
def mount(device, mountpoint, options=None, persist=False):
|
||||
'''Mount a filesystem'''
|
||||
cmd_args = ['mount']
|
||||
if options is not None:
|
||||
cmd_args.extend(['-o', options])
|
||||
cmd_args.extend([device, mountpoint])
|
||||
try:
|
||||
subprocess.check_output(cmd_args)
|
||||
except subprocess.CalledProcessError, e:
|
||||
log('Error mounting {} at {}\n{}'.format(device, mountpoint, e.output))
|
||||
return False
|
||||
if persist:
|
||||
# TODO: update fstab
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def umount(mountpoint, persist=False):
|
||||
'''Unmount a filesystem'''
|
||||
cmd_args = ['umount', mountpoint]
|
||||
try:
|
||||
subprocess.check_output(cmd_args)
|
||||
except subprocess.CalledProcessError, e:
|
||||
log('Error unmounting {}\n{}'.format(mountpoint, e.output))
|
||||
return False
|
||||
if persist:
|
||||
# TODO: update fstab
|
||||
pass
|
||||
return True
|
||||
|
||||
|
||||
def mounts():
|
||||
'''List of all mounted volumes as [[mountpoint,device],[...]]'''
|
||||
with open('/proc/mounts') as f:
|
||||
# [['/mount/point','/dev/path'],[...]]
|
||||
system_mounts = [m[1::-1] for m in [l.strip().split()
|
||||
for l in f.readlines()]]
|
||||
return system_mounts
|
||||
|
||||
|
||||
def file_hash(path):
|
||||
''' Generate a md5 hash of the contents of 'path' or None if not found '''
|
||||
if os.path.exists(path):
|
||||
h = hashlib.md5()
|
||||
with open(path, 'r') as source:
|
||||
h.update(source.read()) # IGNORE:E1101 - it does have update
|
||||
return h.hexdigest()
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def restart_on_change(restart_map):
|
||||
''' Restart services based on configuration files changing
|
||||
|
||||
This function is used a decorator, for example
|
||||
|
||||
@restart_on_change({
|
||||
'/etc/ceph/ceph.conf': [ 'cinder-api', 'cinder-volume' ]
|
||||
})
|
||||
def ceph_client_changed():
|
||||
...
|
||||
|
||||
In this example, the cinder-api and cinder-volume services
|
||||
would be restarted if /etc/ceph/ceph.conf is changed by the
|
||||
ceph_client_changed function.
|
||||
'''
|
||||
def wrap(f):
|
||||
def wrapped_f(*args):
|
||||
checksums = {}
|
||||
for path in restart_map:
|
||||
checksums[path] = file_hash(path)
|
||||
f(*args)
|
||||
restarts = []
|
||||
for path in restart_map:
|
||||
if checksums[path] != file_hash(path):
|
||||
restarts += restart_map[path]
|
||||
for service_name in list(OrderedDict.fromkeys(restarts)):
|
||||
service('restart', service_name)
|
||||
return wrapped_f
|
||||
return wrap
|
||||
|
||||
|
||||
def lsb_release():
|
||||
'''Return /etc/lsb-release in a dict'''
|
||||
d = {}
|
||||
with open('/etc/lsb-release', 'r') as lsb:
|
||||
for l in lsb:
|
||||
k, v = l.split('=')
|
||||
d[k.strip()] = v.strip()
|
||||
return d
|
@ -1 +1 @@
|
||||
swift-storage-node-relations
|
||||
swift_storage_relations.py
|
@ -1 +1 @@
|
||||
swift-storage-node-relations
|
||||
swift_storage_relations.py
|
@ -1 +0,0 @@
|
||||
swift-storage-node-relations
|
@ -1 +0,0 @@
|
||||
swift-storage-node-relations
|
@ -1,153 +0,0 @@
|
||||
#!/bin/bash
|
||||
set -ue
|
||||
|
||||
CHARM="swift-storage"
|
||||
HOOKS_DIR="$CHARM_DIR/hooks"
|
||||
|
||||
CONF_DIR="/etc/swift"
|
||||
|
||||
if [[ -e "$HOOKS_DIR/lib/openstack-common" ]] ; then
|
||||
. $HOOKS_DIR/lib/openstack-common
|
||||
else
|
||||
juju-log "ERROR: Couldn't load $HOOKS_DIR/lib/openstack-common." && exit 1
|
||||
fi
|
||||
|
||||
DEFAULT_ETH=$(ip route | grep default | awk '{ print $5 }')
|
||||
IP=$(ifconfig $DEFAULT_ETH | grep 'inet addr' | awk '{ print $2 }' | cut -d: -f2)
|
||||
|
||||
# TODO: Need to use different addresses for internal swift traffic
|
||||
# as this the only security measure in place is network isolation
|
||||
STORAGE_LOCAL_NET_IP=$IP
|
||||
|
||||
PACKAGES="swift swift-account swift-container swift-object xfsprogs gdisk"
|
||||
|
||||
OPENSTACK_ORIGIN="$(config-get openstack-origin)"
|
||||
|
||||
src=$(get_os_codename_install_source "$OPENSTACK_ORIGIN")
|
||||
# Install python-swiftclient for Folsom and beyond.
|
||||
if dpkg --compare-versions $(get_os_version_codename "$src") gt \
|
||||
$(get_os_version_codename "essex") ; then
|
||||
PACKAGES="$PACKAGES python-swiftclient"
|
||||
fi
|
||||
|
||||
function set_swift_hash {
|
||||
# TODO: Do this with augeas and put in a utility function for use elsewhere
|
||||
cat >/etc/swift/swift.conf <<EOF
|
||||
[swift-hash]
|
||||
# random unique string that can never change (DO NOT LOSE)
|
||||
swift_hash_path_suffix = $1
|
||||
EOF
|
||||
}
|
||||
|
||||
function configure_rsyncd {
|
||||
cat >/etc/rsyncd.conf <<EOF
|
||||
uid = swift
|
||||
gid = swift
|
||||
log file = /var/log/rsyncd.log
|
||||
pid file = /var/run/rsyncd.pid
|
||||
address = $STORAGE_LOCAL_NET_IP
|
||||
|
||||
[account]
|
||||
max connections = 2
|
||||
path = /srv/node/
|
||||
read only = false
|
||||
lock file = /var/lock/account.lock
|
||||
|
||||
[container]
|
||||
max connections = 2
|
||||
path = /srv/node/
|
||||
read only = false
|
||||
lock file = /var/lock/container.lock
|
||||
|
||||
[object]
|
||||
max connections = 2
|
||||
path = /srv/node/
|
||||
read only = false
|
||||
lock file = /var/lock/object.lock
|
||||
EOF
|
||||
perl -pi -e 's/RSYNC_ENABLE=false/RSYNC_ENABLE=true/' /etc/default/rsync
|
||||
service rsync start
|
||||
}
|
||||
|
||||
function create_server_conf {
|
||||
# $1 should be: account, container or object
|
||||
case $1 in
|
||||
"account") subconf="replicator auditor reaper" ;;
|
||||
"object"|"container") subconf="replicator updater auditor sync" ;;
|
||||
esac
|
||||
local port="$2"
|
||||
cat >/etc/swift/$1-server.conf <<EOF
|
||||
[DEFAULT]
|
||||
bind_ip = $STORAGE_LOCAL_NET_IP
|
||||
bind_port = $port
|
||||
workers = 2
|
||||
|
||||
[pipeline:main]
|
||||
pipeline = recon $1-server
|
||||
|
||||
[filter:recon]
|
||||
use = egg:swift#recon
|
||||
recon_cache_path = /var/cache/swift
|
||||
|
||||
[app:$1-server]
|
||||
use = egg:swift#$1
|
||||
|
||||
EOF
|
||||
for i in $subconf ; do
|
||||
echo -e "[$1-$i]\n" >>/etc/swift/$1-server.conf
|
||||
done
|
||||
}
|
||||
|
||||
function determine_block_devs {
|
||||
juju-log "determine_block_devs: $@"
|
||||
local devices=$(config-get block-device)
|
||||
if [ "$devices" = "guess" ]; then
|
||||
# This should be more smart
|
||||
devices=$(awk '($4 ~ /^(sd[a-z]|vd[a-z]|cciss\/c[0-9]d[0-9])$/) && ($4 != "sda") && ($4 != "vda") && ($4 != "cciss/c0d0") {print $4}' </proc/partitions)
|
||||
fi
|
||||
|
||||
for dev in $devices ; do
|
||||
echo $(get_block_device "$dev")
|
||||
done
|
||||
}
|
||||
|
||||
function setup_storage {
|
||||
mkdir -p /srv/node
|
||||
local devs=$(determine_block_devs)
|
||||
local overwrite="$(config-get overwrite)"
|
||||
local nodename=""
|
||||
juju-log "Preparing storage: $devs"
|
||||
|
||||
for dev in $devs ; do
|
||||
[[ "$overwrite" == "true" ]] || [[ "$overwrite" == "True" ]] &&
|
||||
clean_storage $dev
|
||||
mkfs.xfs -f -i size=1024 $dev
|
||||
nodename=$(basename $dev)
|
||||
if ! echo "$nodename" | grep "loop" ; then
|
||||
fstab_entry="$dev /srv/node/$nodename xfs noatime,nodiratime,nobarrier,logbufs=8 0 0"
|
||||
grep -q "^$fstab_entry" /etc/fstab || echo "$fstab_entry" >>/etc/fstab
|
||||
fi
|
||||
mkdir -p /srv/node/$nodename
|
||||
mount $dev /srv/node/$nodename
|
||||
done
|
||||
chown -R swift:swift /srv/node
|
||||
}
|
||||
|
||||
function do_openstack_upgrade {
|
||||
# update openstack components to those provided by a new installation source
|
||||
# it is assumed the calling hook has confirmed that the upgrade is sane.
|
||||
local rel="$1"
|
||||
shift
|
||||
local packages=$@
|
||||
|
||||
# Backup the config directory.
|
||||
local stamp=$(date +"%Y%m%d%M%S")
|
||||
tar -pcf /var/lib/juju/$CHARM-backup-$stamp.tar $CONF_DIR
|
||||
|
||||
configure_install_source "$rel"
|
||||
apt-get update
|
||||
DEBIAN_FRONTEND=noninteractive apt-get \
|
||||
--option Dpkg::Options::=--force-confnew -y \
|
||||
install --no-install-recommends $packages
|
||||
swift-init all restart || true
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
#!/bin/bash
|
||||
# test
|
||||
set -eu
|
||||
|
||||
HOOKS_DIR="$CHARM_DIR/hooks"
|
||||
ARG0=${0##*/}
|
||||
|
||||
if [[ -e $HOOKS_DIR/swift-storage-node-common ]] ; then
|
||||
. $HOOKS_DIR/swift-storage-node-common
|
||||
else
|
||||
echo "ERROR: Could not load swift-storage-node-common from $HOOKS_DIR"
|
||||
fi
|
||||
|
||||
function config_changed {
|
||||
# perform openstack upgrade if openstack-origin has been bumped.
|
||||
local install_src=$(config-get openstack-origin)
|
||||
local cur=$(get_os_codename_package "python-swift")
|
||||
local available=$(get_os_codename_install_source "$install_src")
|
||||
if [[ "$available" != "unknown" ]] ; then
|
||||
if dpkg --compare-versions $(get_os_version_codename "$cur") lt \
|
||||
$(get_os_version_codename "$available") ; then
|
||||
juju-log "$CHARM: Upgrading OpenStack release: $cur -> $available."
|
||||
do_openstack_upgrade "$install_src" $PACKAGES
|
||||
fi
|
||||
fi
|
||||
declare -a env_vars=()
|
||||
for i in account container object ; do
|
||||
port=$(config-get ${i}-server-port)
|
||||
local url="http://$STORAGE_LOCAL_NET_IP:$port/recon/diskusage"
|
||||
# append to env_vars
|
||||
env_vars+=("OPENSTACK_PORT_${i^^}=$port")
|
||||
env_vars+=("OPENSTACK_SWIFT_SERVICE_${i^^}=${i}-server")
|
||||
|
||||
# Ensure we have at least one device mounted as reported by swift-recon
|
||||
env_vars+=('OPENSTACK_URL_'${i^^}'="'$url'|\"mounted\":+true"')
|
||||
|
||||
create_server_conf $i "$port"
|
||||
done
|
||||
|
||||
# Save our scriptrc env variables for health checks
|
||||
save_script_rc ${env_vars[@]}
|
||||
}
|
||||
|
||||
function install_hook {
|
||||
apt-get -y --force-yes install python-software-properties || exit 1
|
||||
|
||||
configure_install_source "$OPENSTACK_ORIGIN"
|
||||
apt-get update
|
||||
|
||||
DEBIAN_FRONTEND=noninteractive apt-get -y \
|
||||
install --no-install-recommends $PACKAGES || exit 1
|
||||
|
||||
[[ ! -d /etc/swift ]] && mkdir /etc/swift
|
||||
[[ ! -d /var/cache/swift ]] && mkdir /var/cache/swift
|
||||
chown swift:swift /etc/swift
|
||||
chown -R swift:swift /var/cache/swift
|
||||
configure_rsyncd
|
||||
swift-init all stop || true
|
||||
setup_storage
|
||||
config_changed
|
||||
}
|
||||
|
||||
function storage_joined {
|
||||
local devs=$(determine_block_devs)
|
||||
local reldevices=""
|
||||
for dev in $devs ; do
|
||||
local srvnodename=$(basename $dev)
|
||||
if [[ -n "$reldevices" ]] ; then
|
||||
reldevices="$reldevices:$srvnodename"
|
||||
else
|
||||
reldevices="$srvnodename"
|
||||
fi
|
||||
done
|
||||
relation-set zone="$(config-get zone)" \
|
||||
device="$reldevices" \
|
||||
object_port="$(config-get object-server-port)" \
|
||||
container_port="$(config-get container-server-port)" \
|
||||
account_port="$(config-get account-server-port)"
|
||||
}
|
||||
|
||||
function storage_changed {
|
||||
local rings_url=`relation-get rings_url`
|
||||
local swift_hash=`relation-get swift_hash`
|
||||
[[ -z $rings_url ]] || [[ -z $swift_hash ]] && exit 0
|
||||
|
||||
set_swift_hash $swift_hash
|
||||
for i in account object container ; do
|
||||
echo "Fetching $rings_url/$i.ring.gz"
|
||||
wget "$rings_url/$i.ring.gz" -O /etc/swift/$i.ring.gz
|
||||
done
|
||||
set_swift_hash $swift_hash
|
||||
chown swift -R /etc/swift
|
||||
swift-init all start || true
|
||||
}
|
||||
|
||||
case $ARG0 in
|
||||
"install") install_hook ;;
|
||||
"start"|"stop") exit 0 ;;
|
||||
"config-changed") config_changed ;;
|
||||
"swift-storage-relation-joined") storage_joined ;;
|
||||
"swift-storage-relation-changed") storage_changed ;;
|
||||
"config-changed") config_changed ;;
|
||||
esac
|
@ -1 +1 @@
|
||||
swift-storage-node-relations
|
||||
swift_storage_relations.py
|
@ -1 +1 @@
|
||||
swift-storage-node-relations
|
||||
swift_storage_relations.py
|
83
hooks/swift_storage_relations.py
Executable file
83
hooks/swift_storage_relations.py
Executable file
@ -0,0 +1,83 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
from swift_storage_utils import (
|
||||
PACKAGES,
|
||||
determine_block_devices,
|
||||
do_openstack_upgrade,
|
||||
ensure_swift_directories,
|
||||
fetch_swift_rings,
|
||||
register_configs,
|
||||
swift_init, # move to openstack utils
|
||||
setup_storage,
|
||||
)
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
Hooks,
|
||||
config,
|
||||
log,
|
||||
relation_get,
|
||||
relation_set,
|
||||
)
|
||||
|
||||
from charmhelpers.core.host import (
|
||||
apt_install,
|
||||
apt_update,
|
||||
)
|
||||
|
||||
|
||||
from charmhelpers.contrib.openstack.utils import (
|
||||
configure_installation_source,
|
||||
openstack_upgrade_available,
|
||||
)
|
||||
|
||||
hooks = Hooks()
|
||||
CONFIGS = register_configs()
|
||||
|
||||
|
||||
@hooks.hook()
|
||||
def install():
|
||||
conf = config()
|
||||
src = conf['openstack-origin']
|
||||
configure_installation_source(src)
|
||||
apt_update()
|
||||
apt_install(PACKAGES)
|
||||
CONFIGS.write('/etc/rsyncd.conf')
|
||||
swift_init('all', 'stop')
|
||||
setup_storage()
|
||||
ensure_swift_directories()
|
||||
|
||||
|
||||
@hooks.hook()
|
||||
def config_changed():
|
||||
if openstack_upgrade_available('swift'):
|
||||
do_openstack_upgrade(configs=CONFIGS)
|
||||
CONFIGS.write_all()
|
||||
# TODO: save landscape scriptrc
|
||||
|
||||
|
||||
@hooks.hook()
|
||||
def swift_storage_relation_joined():
|
||||
devs = [os.path.basename(dev) for dev in determine_block_devices()]
|
||||
rel_settings = {
|
||||
'zone': config('zone'),
|
||||
'object_port': config('object-server-port'),
|
||||
'container_port': config('container-server-port'),
|
||||
'account_port': config('account-server-port'),
|
||||
'device': ':'.join(devs),
|
||||
}
|
||||
relation_set(**rel_settings)
|
||||
|
||||
|
||||
@hooks.hook()
|
||||
def swift_storage_relation_changed():
|
||||
rings_url = relation_get('rings_url')
|
||||
swift_hash = relation_get('swift_hash')
|
||||
if None in [rings_url, swift_hash]:
|
||||
log('swift_storage_relation_changed: Peer not ready?')
|
||||
sys.exit(0)
|
||||
CONFIGS.write('/etc/swift/swift.conf')
|
||||
fetch_swift_rings(rings_url)
|
||||
swift_init('all', 'start')
|
102
hooks/swift_storage_utils.py
Normal file
102
hooks/swift_storage_utils.py
Normal file
@ -0,0 +1,102 @@
|
||||
import os
|
||||
|
||||
from subprocess import check_call, call
|
||||
|
||||
from charmhelpers.core.host import (
|
||||
mkdir,
|
||||
mount,
|
||||
umount as ensure_block_device,
|
||||
umount as clean_storage,
|
||||
)
|
||||
|
||||
from charmhelpers.core.hookenv import (
|
||||
config,
|
||||
log,
|
||||
ERROR,
|
||||
)
|
||||
|
||||
PACKAGES = [
|
||||
'swift', 'swift-account', 'swift-container',
|
||||
'swift-object' 'xfsprogs' 'gdisk'
|
||||
]
|
||||
|
||||
|
||||
def ensure_swift_directories():
|
||||
'''
|
||||
Ensure all directories required for a swift storage node exist with
|
||||
correct permissions.
|
||||
'''
|
||||
dirs = [
|
||||
'/etc/swift',
|
||||
'/var/cache/swift',
|
||||
'/srv/node',
|
||||
]
|
||||
[mkdir(d, owner='swift', group='swift') for d in dirs
|
||||
if not os.path.isdir(d)]
|
||||
|
||||
|
||||
def register_configs():
|
||||
return None
|
||||
|
||||
|
||||
def swift_init(target, action, fatal=False):
|
||||
'''
|
||||
Call swift-init on a specific target with given action, potentially
|
||||
raising exception.
|
||||
'''
|
||||
cmd = ['swift-init', target, action]
|
||||
if fatal:
|
||||
return check_call(cmd)
|
||||
return call(cmd)
|
||||
|
||||
|
||||
def do_openstack_upgrade(configs):
|
||||
pass
|
||||
|
||||
|
||||
def find_block_devices():
|
||||
pass
|
||||
|
||||
|
||||
def determine_block_devices():
|
||||
block_device = config('block-device')
|
||||
|
||||
if not block_device or block_device in ['None', 'none']:
|
||||
log('No storage devices specified in config as block-device',
|
||||
level=ERROR)
|
||||
return None
|
||||
|
||||
if block_device == 'guess':
|
||||
bdevs = find_block_devices()
|
||||
else:
|
||||
bdevs = block_device.split(' ')
|
||||
|
||||
return [ensure_block_device(bd) for bd in bdevs]
|
||||
|
||||
|
||||
def mkfs_xfs(bdev):
|
||||
cmd = ['mkfs.xfs', '-f', '-i', 'size=1024', bdev]
|
||||
check_call(cmd)
|
||||
|
||||
|
||||
def setup_storage():
|
||||
for dev in determine_block_devices():
|
||||
if config('overwrite') in ['True', 'true']:
|
||||
clean_storage(dev)
|
||||
# if not cleaned and in use, mkfs should fail.
|
||||
mkfs_xfs(dev)
|
||||
_dev = os.path.basename(dev)
|
||||
_mp = os.path.join('/srv', 'node', _dev)
|
||||
mkdir(_mp, owner='swift', group='swift')
|
||||
mount(dev, '/srv/node/%s' % _dev, persist=True)
|
||||
# TODO: chown again post-mount?
|
||||
|
||||
|
||||
def fetch_swift_rings(rings_url):
|
||||
log('swift-storage-node: Fetching all swift rings from proxy @ %s.' %
|
||||
rings_url)
|
||||
for server in ['account', 'object', 'container']:
|
||||
url = '%s/%s.ring.gz' % (rings_url, server)
|
||||
log('Fetching %s.' % url)
|
||||
cmd = ['wget', url, '-O', '/etc/swift/%s.ring.gz' % server]
|
||||
check_call(cmd)
|
Loading…
Reference in New Issue
Block a user