Refactoring to split up utils, use common lib

This commit is contained in:
James Page
2013-03-12 11:17:32 +00:00
parent cc580d3c82
commit 41db63fe69
23 changed files with 720 additions and 654 deletions

View File

@@ -50,13 +50,19 @@ options:
ssl_cert:
type: string
description: |
SSL certificate to install and use for API ports. Setting this value
and ssl_key will enable reverse proxying, point Swifts's entry in the
Keystone catalog to use https, and override any certficiate and key
issued by Keystone (if it is configured to do so).
Base64 encoded SSL certificate to install and use for API ports.
.
juju set swift-proxy ssl_cert="$(cat cert | base64)" \
ssl_key="$(cat key | base64)"
.
Setting this value (and ssl_key) will enable reverse proxying, point
Swifts's entry in the Keystone catalog to use https, and override
any certficiate and key issued by Keystone (if it is configured to
do so).
ssl_key:
type: string
description: SSL key to use with certificate specified as ssl_cert.
description: |
Base64 encoded SSL key to use with certificate specified as ssl_cert.
# Locally generated CA Cert info (only use without keystone)
# These options are deprecated and will be removed sometime
use-https:

197
hooks/lib/apache_utils.py Normal file
View File

@@ -0,0 +1,197 @@
#
# Copyright 2012 Canonical Ltd.
#
# Authors:
# James Page <james.page@ubuntu.com>
#
from lib.utils import (
relation_ids,
relation_list,
relation_get,
render_template,
juju_log,
config_get,
install,
get_host_ip,
restart
)
from lib.cluster_utils import https
import os
import subprocess
from base64 import b64decode, b64encode
APACHE_SITE_DIR = "/etc/apache2/sites-available"
SITE_TEMPLATE = "apache2_site.tmpl"
RELOAD_CHECK = "To activate the new configuration"
def enable_https(port_maps, namespace):
'''
For a given number of port mappings, configures apache2
HTTPs local reverse proxying using certficates and keys provided in
either configuration data (preferred) or relation data. Assumes ports
are not in use (calling charm should ensure that).
port_maps: dict: external to internal port mappings
namespace: str: name of charm
'''
juju_log('INFO', "Enabling HTTPS for port mappings: {}".format(port_maps))
http_restart = False
# allow overriding of keystone provided certs with those set manually
# in config.
cert = config_get('ssl_cert')
key = config_get('ssl_key')
ca_cert = None
if not (cert and key):
juju_log('INFO',
"Inspecting identity-service relations for SSL certificate.")
cert = key = ca_cert = 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)
if not ca_cert:
ca_cert = relation_get('ca_cert', rid=r_id, unit=unit)
if (not (cert and key and ca_cert) and
config_get('use-https')):
juju_log('INFO',
"Using self-signed SSL certificate.")
(cert, key) = generate_cert()
else:
juju_log('INFO',
"Using SSL certificate provided in service config.")
if cert:
cert = b64decode(cert)
if key:
key = b64decode(key)
if ca_cert:
ca_cert = b64decode(ca_cert)
# TODO: Implement check tosee if certs have changed
if not cert and not key:
juju_log('ERROR',
"Expected but could not find SSL certificate data, not "
"configuring HTTPS!")
return False
install('apache2')
if RELOAD_CHECK in subprocess.check_output(['a2enmod', 'ssl',
'proxy', 'proxy_http']):
http_restart = True
ssl_dir = os.path.join('/etc/apache2/ssl', namespace)
if not os.path.exists(ssl_dir):
os.makedirs(ssl_dir)
with open(os.path.join(ssl_dir, 'cert'), 'w') as fcert:
fcert.write(cert)
with open(os.path.join(ssl_dir, 'key'), 'w') as fkey:
fkey.write(key)
os.chmod(os.path.join(ssl_dir, 'key'), 0600)
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'])
sites_dir = '/etc/apache2/sites-available'
for ext_port, int_port in port_maps.items():
juju_log('INFO',
'Creating apache2 reverse proxy vhost'
' for {}:{}'.format(ext_port,
int_port))
site = "{}_{}".format(namespace, ext_port)
site_path = os.path.join(sites_dir, site)
with open(site_path, 'w') as fsite:
context = {
"ext": ext_port,
"int": int_port,
"namespace": namespace,
"private_address": get_host_ip()
}
fsite.write(render_template(SITE_TEMPLATE,
context))
if RELOAD_CHECK in subprocess.check_output(['a2ensite', site]):
http_restart = True
if http_restart:
restart('apache2')
return True
def disable_https(port_maps, namespace):
'''
Ensure HTTPS reverse proxying is disables for given port mappings
port_maps: dict: of ext -> int port mappings
namespace: str: name of chamr
'''
juju_log('INFO', 'Ensuring HTTPS disabled for {}'.format(port_maps))
if (not os.path.exists('/etc/apache2') or
not os.path.exists(os.path.join('/etc/apache2/ssl', namespace))):
return
http_restart = False
for ext_port in port_maps.keys():
if os.path.exists(os.path.join(APACHE_SITE_DIR,
"{}_{}".format(namespace,
ext_port))):
juju_log('INFO',
"Disabling HTTPS reverse proxy"
" for {} {}.".format(namespace,
ext_port))
if (RELOAD_CHECK in
subprocess.check_output(['a2dissite',
'{}_{}'.format(namespace,
ext_port)])):
http_restart = True
if http_restart:
restart(['apache2'])
def setup_https(port_maps, namespace):
'''
Ensures HTTPS is either enabled or disabled for given port
mapping.
port_maps: dict: of ext -> int port mappings
namespace: str: name of charm
'''
if not https:
disable_https(port_maps, namespace)
else:
enable_https(port_maps, namespace)
def generate_cert():
'''
Generates a self signed certificate and key using the
provided charm configuration data.
returns: tuple of (cert, key)
'''
CERT = '/etc/swift/ssl.cert'
KEY = '/etc/swift/ssl.key'
if (not os.path.exists(CERT) and
not os.path.exists(KEY)):
subj = '/C=%s/ST=%s/L=%s/CN=%s' %\
(config_get('country'), config_get('state'),
config_get('locale'), config_get('common-name'))
cmd = ['openssl', 'req', '-new', '-x509', '-nodes',
'-out', CERT, '-keyout', KEY,
'-subj', subj]
subprocess.check_call(cmd)
# Slurp as base64 encoded - makes handling easier up the stack
with open(CERT, 'r') as cfile:
ssl_cert = b64encode(cfile.read())
with open(KEY, 'r') as kfile:
ssl_key = b64encode(kfile.read())
return (ssl_cert, ssl_key)

127
hooks/lib/cluster_utils.py Normal file
View File

@@ -0,0 +1,127 @@
#
# Copyright 2012 Canonical Ltd.
#
# Authors:
# James Page <james.page@ubuntu.com>
#
from lib.utils import (
juju_log,
relation_ids,
relation_list,
relation_get,
get_unit_hostname,
config_get
)
import subprocess
import os
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 = os.getenv('JUJU_UNIT_NAME').split('/')[1]
for peer in peers:
remote_unit_no = 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):
juju_log('INFO', 'Deferring action to CRM leader.')
return False
else:
peers = peer_units()
if peers and not oldest_peer(peers):
juju_log('INFO', 'Deferring action to oldest service unit.')
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'):
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 (relation_get('https_keystone', rid=r_id, unit=unit) and
relation_get('ssl_cert', rid=r_id, unit=unit) and
relation_get('ssl_key', rid=r_id, unit=unit) and
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)

View File

@@ -0,0 +1,52 @@
#
# Copyright 2012 Canonical Ltd.
#
# Authors:
# James Page <james.page@ubuntu.com>
#
from lib.utils import (
relation_ids,
relation_list,
relation_get,
unit_get,
reload,
render_template
)
import os
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
HAPROXY_DEFAULT = '/etc/default/haproxy'
def configure_haproxy(service_ports):
'''
Configure HAProxy based on the current peers in the service
cluster using the provided port map:
"swift": [ 8080, 8070 ]
HAproxy will also be reloaded/started if required
service_ports: dict: dict of lists of [ frontend, backend ]
'''
cluster_hosts = {}
cluster_hosts[os.getenv('JUJU_UNIT_NAME').replace('/', '-')] = \
unit_get('private-address')
for r_id in relation_ids('cluster'):
for unit in relation_list(r_id):
cluster_hosts[unit.replace('/', '-')] = \
relation_get(attribute='private-address',
rid=r_id,
unit=unit)
context = {
'units': cluster_hosts,
'service_ports': service_ports
}
with open(HAPROXY_CONF, 'w') as f:
f.write(render_template(os.path.basename(HAPROXY_CONF),
context))
with open(HAPROXY_DEFAULT, 'w') as f:
f.write('ENABLED=1')
reload('haproxy')

View File

@@ -12,7 +12,7 @@ ubuntu_openstack_release = {
'oneiric': 'diablo',
'precise': 'essex',
'quantal': 'folsom',
'raring' : 'grizzly'
'raring': 'grizzly'
}
@@ -20,7 +20,8 @@ openstack_codenames = {
'2011.2': 'diablo',
'2012.1': 'essex',
'2012.2': 'folsom',
'2013.1': 'grizzly'
'2013.1': 'grizzly',
'2013.2': 'havana'
}
# The ugly duckling
@@ -32,6 +33,7 @@ swift_codenames = {
'1.7.7': 'grizzly'
}
def juju_log(msg):
subprocess.check_call(['juju-log', msg])
@@ -76,6 +78,7 @@ def get_os_codename_install_source(src):
if v in src:
return v
def get_os_codename_version(vers):
'''Determine OpenStack codename from version number.'''
try:
@@ -115,7 +118,7 @@ def get_os_codename_package(pkg):
return clean
vers = None
for l in output.split('\n'):
for l in str(output).split('\n'):
if l.startswith('ii'):
l = _clean(l)
if l[1] == pkg:
@@ -153,16 +156,17 @@ def get_os_version_package(pkg):
e = "Could not determine OpenStack version for package: %s" % pkg
error_out(e)
def configure_installation_source(rel):
'''Configure apt installation source.'''
def _import_key(id):
def _import_key(keyid):
cmd = "apt-key adv --keyserver keyserver.ubuntu.com " \
"--recv-keys %s" % id
"--recv-keys %s" % keyid
try:
subprocess.check_call(cmd.split(' '))
except:
error_out("Error importing repo key %s" % id)
except subprocess.CalledProcessError:
error_out("Error importing repo key %s" % keyid)
if rel == 'distro':
return
@@ -171,7 +175,7 @@ def configure_installation_source(rel):
subprocess.check_call(["add-apt-repository", "-y", src])
elif rel[:3] == "deb":
l = len(rel.split('|'))
if l == 2:
if l == 2:
src, key = rel.split('|')
juju_log("Importing PPA key from keyserver for %s" % src)
_import_key(key)
@@ -225,26 +229,6 @@ def configure_installation_source(rel):
else:
error_out("Invalid openstack-release specified: %s" % rel)
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
HAPROXY_DEFAULT = '/etc/default/haproxy'
def configure_haproxy(units, service_ports, template_dir=None):
template_dir = template_dir or 'templates'
import jinja2
context = {
'units': units,
'service_ports': service_ports
}
templates = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir)
)
template = templates.get_template(
os.path.basename(HAPROXY_CONF)
)
with open(HAPROXY_CONF, 'w') as f:
f.write(template.render(context))
with open(HAPROXY_DEFAULT, 'w') as f:
f.write('ENABLED=1')
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
"""
@@ -255,7 +239,7 @@ def save_script_rc(script_path="scripts/scriptrc", **env_vars):
service changes.
"""
unit_name = os.getenv('JUJU_UNIT_NAME').replace('/', '-')
juju_rc_path="/var/lib/juju/units/%s/charm/%s" % (unit_name, script_path)
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")

260
hooks/lib/utils.py Normal file
View File

@@ -0,0 +1,260 @@
#
# Copyright 2012 Canonical Ltd.
#
# Authors:
# James Page <james.page@ubuntu.com>
# Paul Collins <paul.collins@canonical.com>
#
import json
import os
import subprocess
import socket
import sys
def do_hooks(hooks):
hook = os.path.basename(sys.argv[0])
try:
hook_func = hooks[hook]
except KeyError:
juju_log('INFO',
"This charm doesn't know how to handle '{}'.".format(hook))
else:
hook_func()
def install(*pkgs):
cmd = [
'apt-get',
'-y',
'install'
]
for pkg in pkgs:
cmd.append(pkg)
subprocess.check_call(cmd)
TEMPLATES_DIR = 'templates'
try:
import jinja2
except ImportError:
install('python-jinja2')
import jinja2
try:
import dns.resolver
except ImportError:
install('python-dnspython')
import dns.resolver
def render_template(template_name, context, template_dir=TEMPLATES_DIR):
templates = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir)
)
template = templates.get_template(template_name)
return template.render(context)
CLOUD_ARCHIVE = \
""" # Ubuntu Cloud Archive
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
"""
CLOUD_ARCHIVE_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'
}
def configure_source():
source = str(config_get('openstack-origin'))
if not source:
return
if source.startswith('ppa:'):
cmd = [
'add-apt-repository',
source
]
subprocess.check_call(cmd)
if source.startswith('cloud:'):
install('ubuntu-cloud-keyring')
pocket = source.split(':')[1]
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
apt.write(CLOUD_ARCHIVE.format(CLOUD_ARCHIVE_POCKETS[pocket]))
if source.startswith('deb'):
l = len(source.split('|'))
if l == 2:
(apt_line, key) = source.split('|')
cmd = [
'apt-key',
'adv', '--keyserver keyserver.ubuntu.com',
'--recv-keys', key
]
subprocess.check_call(cmd)
elif l == 1:
apt_line = source
with open('/etc/apt/sources.list.d/quantum.list', 'w') as apt:
apt.write(apt_line + "\n")
cmd = [
'apt-get',
'update'
]
subprocess.check_call(cmd)
# Protocols
TCP = 'TCP'
UDP = 'UDP'
def expose(port, protocol='TCP'):
cmd = [
'open-port',
'{}/{}'.format(port, protocol)
]
subprocess.check_call(cmd)
def juju_log(severity, message):
cmd = [
'juju-log',
'--log-level', severity,
message
]
subprocess.check_call(cmd)
def relation_ids(relation):
cmd = [
'relation-ids',
relation
]
result = str(subprocess.check_output(cmd)).split()
if result == "":
return None
else:
return result
def relation_list(rid):
cmd = [
'relation-list',
'-r', rid,
]
result = str(subprocess.check_output(cmd)).split()
if result == "":
return None
else:
return result
def relation_get(attribute, unit=None, rid=None):
cmd = [
'relation-get',
]
if rid:
cmd.append('-r')
cmd.append(rid)
cmd.append(attribute)
if unit:
cmd.append(unit)
value = subprocess.check_output(cmd).strip() # IGNORE:E1103
if value == "":
return None
else:
return value
def relation_set(**kwargs):
cmd = [
'relation-set'
]
args = []
for k, v in kwargs.items():
if k == 'rid':
if v:
cmd.append('-r')
cmd.append(v)
else:
args.append('{}={}'.format(k, v))
cmd += args
subprocess.check_call(cmd)
def unit_get(attribute):
cmd = [
'unit-get',
attribute
]
value = subprocess.check_output(cmd).strip() # IGNORE:E1103
if value == "":
return None
else:
return value
def config_get(attribute):
cmd = [
'config-get',
'--format',
'json',
]
out = subprocess.check_output(cmd).strip() # IGNORE:E1103
cfg = json.loads(out)
try:
return cfg[attribute]
except KeyError:
return None
def get_unit_hostname():
return socket.gethostname()
def get_host_ip(hostname=unit_get('private-address')):
try:
# Test to see if already an IPv4 address
socket.inet_aton(hostname)
return hostname
except socket.error:
answers = dns.resolver.query(hostname, 'A')
if answers:
return answers[0].address
return None
def _svc_control(service, action):
subprocess.check_call(['service', service, action])
def restart(*services):
for service in services:
_svc_control(service, 'restart')
def stop(*services):
for service in services:
_svc_control(service, 'stop')
def start(*services):
for service in services:
_svc_control(service, 'start')
def reload(*services):
for service in services:
try:
_svc_control(service, 'reload')
except subprocess.CalledProcessError:
# Reload failed - either service does not support reload
# or it was not running - restart will fixup most things
_svc_control(service, 'restart')

View File

@@ -1,13 +1,14 @@
#!/usr/bin/python
import os
import utils
import sys
import shutil
import uuid
from subprocess import check_call
import lib.openstack_common as openstack
import lib.utils as utils
import lib.cluster_utils as cluster
import swift_utils as swift
extra_pkgs = [
@@ -15,6 +16,7 @@ extra_pkgs = [
"python-jinja2"
]
def install():
src = utils.config_get('openstack-origin')
if src != 'distro':
@@ -40,7 +42,7 @@ def install():
swift.write_proxy_config()
# memcached.conf
ctxt = { 'proxy_ip': utils.get_host_ip() }
ctxt = {'proxy_ip': utils.get_host_ip()}
with open(swift.MEMCACHED_CONF, 'w') as conf:
conf.write(swift.render_config(swift.MEMCACHED_CONF, ctxt))
@@ -61,14 +63,14 @@ def install():
def keystone_joined(relid=None):
if not utils.eligible_leader():
if not cluster.eligible_leader(swift.SWIFT_HA_RES):
return
if utils.is_clustered():
if cluster.is_clustered():
hostname = utils.config_get('vip')
else:
hostname = utils.unit_get('private-address')
port = utils.config_get('bind-port')
if utils.https():
if cluster.https():
proto = 'https'
else:
proto = 'http'
@@ -105,7 +107,7 @@ def balance_rings():
shutil.copyfile(os.path.join(swift.SWIFT_CONF_DIR, f),
os.path.join(swift.WWW_DIR, f))
if utils.eligible_leader():
if cluster.eligible_leader(swift.SWIFT_HA_RES):
msg = 'Broadcasting notification to all storage nodes that new '\
'ring is ready for consumption.'
utils.juju_log('INFO', msg)
@@ -119,6 +121,7 @@ def balance_rings():
swift.proxy_control('restart')
def storage_changed():
zone = swift.get_zone(utils.config_get('zone-assignment'))
node_settings = {
@@ -149,9 +152,11 @@ def storage_changed():
if swift.should_balance([r for r in swift.SWIFT_RINGS.itervalues()]):
balance_rings()
def storage_broken():
swift.write_apache_config()
def config_changed():
relids = utils.relation_ids('identity-service')
if relids:
@@ -167,7 +172,7 @@ def cluster_changed():
def ha_relation_changed():
clustered = utils.relation_get('clustered')
if clustered and utils.is_leader():
if clustered and cluster.is_leader(swift.SWIFT_HA_RES):
utils.juju_log('INFO',
'Cluster configured, notifying other services and'
'updating keystone endpoint configuration')

View File

@@ -2,11 +2,14 @@ import os
import pwd
import subprocess
import lib.openstack_common as openstack
import utils
import lib.utils as utils
import lib.haproxy_utils as haproxy
import lib.apache_utils as apache
import lib.cluster_utils as cluster
import sys
# Various config files that are managed via templating.
SWIFT_HASH_FILE='/var/lib/juju/swift-hash-path.conf'
SWIFT_HASH_FILE = '/var/lib/juju/swift-hash-path.conf'
SWIFT_CONF = '/etc/swift/swift.conf'
SWIFT_PROXY_CONF = '/etc/swift/proxy-server.conf'
SWIFT_CONF_DIR = os.path.dirname(SWIFT_CONF)
@@ -33,9 +36,12 @@ BASE_PACKAGES = [
'python-keystone',
]
SWIFT_HA_RES = 'res_swift_vip'
# Folsom-specific packages
FOLSOM_PACKAGES = BASE_PACKAGES + ['swift-plugin-s3']
def proxy_control(action):
'''utility to work around swift-init's bad RCs.'''
def _cmd(action):
@@ -50,8 +56,9 @@ def proxy_control(action):
elif status == 0:
return subprocess.check_call(_cmd('stop'))
# the proxy will not start unless there are balanced rings, gzip'd in /etc/swift
missing=False
# the proxy will not start unless there are balanced rings
# gzip'd in /etc/swift
missing = False
for k in SWIFT_RINGS.keys():
if not os.path.exists(os.path.join(SWIFT_CONF_DIR, '%s.ring.gz' % k)):
missing = True
@@ -70,8 +77,9 @@ def proxy_control(action):
elif status == 1:
return subprocess.check_call(_cmd('start'))
def swift_user(username='swift'):
user = pwd.getpwnam('swift')
user = pwd.getpwnam(username)
return (user.pw_uid, user.pw_gid)
@@ -153,11 +161,16 @@ def get_keystone_auth():
'keystone_host': utils.relation_get('auth_host',
unit, relid),
'auth_port': utils.relation_get('auth_port', unit, relid),
'service_user': utils.relation_get('service_username', unit, relid),
'service_password': utils.relation_get('service_password', unit, relid),
'service_tenant': utils.relation_get('service_tenant', unit, relid),
'service_port': utils.relation_get('service_port', unit, relid),
'admin_token': utils.relation_get('admin_token', unit, relid),
'service_user': utils.relation_get('service_username',
unit, relid),
'service_password': utils.relation_get('service_password',
unit, relid),
'service_tenant': utils.relation_get('service_tenant',
unit, relid),
'service_port': utils.relation_get('service_port',
unit, relid),
'admin_token': utils.relation_get('admin_token',
unit, relid),
}
if None not in ks_auth.itervalues():
return ks_auth
@@ -179,7 +192,7 @@ def write_proxy_config():
ctxt = {
'proxy_ip': utils.get_host_ip(),
'bind_port': utils.determine_api_port(bind_port),
'bind_port': cluster.determine_api_port(bind_port),
'workers': workers,
'operator_roles': utils.config_get('operator-roles')
}
@@ -201,7 +214,7 @@ def write_proxy_config():
def _load_builder(path):
# lifted straight from /usr/bin/swift-ring-builder
from swift.common.ring import RingBuilder, Ring
from swift.common.ring import RingBuilder
import cPickle as pickle
try:
builder = pickle.load(open(path, 'rb'))
@@ -210,10 +223,8 @@ def _load_builder(path):
builder = RingBuilder(1, 1, 1)
builder.copy_from(builder_dict)
except ImportError: # Happens with really old builder pickles
modules['swift.ring_builder'] = \
modules['swift.common.ring.builder']
builder = RingBuilder(1, 1, 1)
builder.copy_from(pickle.load(open(argv[1], 'rb')))
builder.copy_from(pickle.load(open(path, 'rb')))
for dev in builder.devs:
if dev and 'meta' not in dev:
dev['meta'] = ''
@@ -225,8 +236,6 @@ def _write_ring(ring, ring_path):
pickle.dump(ring.to_dict(), open(ring_path, 'wb'), protocol=2)
def ring_port(ring_path, node):
'''determine correct port from relation settings for a given ring file.'''
for name in ['account', 'object', 'container']:
@@ -240,8 +249,8 @@ def initialize_ring(path, part_power, replicas, min_hours):
ring = RingBuilder(part_power, replicas, min_hours)
_write_ring(ring, path)
def exists_in_ring(ring_path, node):
from swift.common.ring import RingBuilder, Ring
ring = _load_builder(ring_path).to_dict()
node['port'] = ring_port(ring_path, node)
@@ -258,7 +267,6 @@ def exists_in_ring(ring_path, node):
def add_to_ring(ring_path, node):
from swift.common.ring import RingBuilder, Ring
ring = _load_builder(ring_path)
port = ring_port(ring_path, node)
@@ -278,8 +286,9 @@ def add_to_ring(ring_path, node):
}
ring.add_dev(new_dev)
_write_ring(ring, ring_path)
msg = 'Added new device to ring %s: %s' % (ring_path,
[k for k in new_dev.iteritems()])
msg = 'Added new device to ring %s: %s' %\
(ring_path,
[k for k in new_dev.iteritems()])
utils.juju_log('INFO', msg)
@@ -324,7 +333,7 @@ def get_zone(assignment_policy):
potential_zones.append(_get_zone(builder))
return set(potential_zones).pop()
else:
utils.juju_log('Invalid zone assignment policy: %s' %\
utils.juju_log('ERROR', 'Invalid zone assignment policy: %s' %\
assignment_policy)
sys.exit(1)
@@ -343,9 +352,10 @@ def balance_ring(ring_path):
# swift-ring-builder returns 1 on WARNING (ring didn't require balance)
return False
else:
utils.juju_log('balance_ring: %s returned %s' % (cmd, rc))
utils.juju_log('ERROR', 'balance_ring: %s returned %s' % (cmd, rc))
sys.exit(1)
def should_balance(rings):
'''Based on zones vs min. replicas, determine whether or not the rings
should be balanaced during initial configuration.'''
@@ -371,35 +381,38 @@ def write_apache_config():
host = utils.relation_get('private-address', unit, relid)
allowed_hosts.append(utils.get_host_ip(host))
ctxt = { 'www_dir': WWW_DIR, 'allowed_hosts': allowed_hosts }
ctxt = {
'www_dir': WWW_DIR,
'allowed_hosts': allowed_hosts
}
with open(APACHE_CONF, 'w') as conf:
conf.write(render_config(APACHE_CONF, ctxt))
subprocess.check_call(['service', 'apache2', 'reload'])
utils.reload('apache2')
def configure_haproxy():
api_port = utils.config_get('bind-port')
service_ports = {
"swift": [
utils.determine_haproxy_port(api_port),
utils.determine_api_port(api_port)
cluster.determine_haproxy_port(api_port),
cluster.determine_api_port(api_port)
]
}
write_proxy_config()
utils.configure_haproxy(service_ports)
haproxy.configure_haproxy(service_ports)
def configure_https():
if utils.https():
if cluster.https():
api_port = utils.config_get('bind-port')
if (len(utils.peer_units()) > 0 or
utils.is_clustered()):
target_port = utils.determine_haproxy_port(api_port)
if (len(cluster.peer_units()) > 0 or
cluster.is_clustered()):
target_port = cluster.determine_haproxy_port(api_port)
configure_haproxy()
else:
target_port = utils.determine_api_port(api_port)
target_port = cluster.determine_api_port(api_port)
write_proxy_config()
utils.setup_https(namespace="swift",
port_maps={api_port: target_port})
apache.setup_https(namespace="swift",
port_maps={api_port: target_port})
else:
return False

View File

@@ -1,578 +0,0 @@
#
# Copyright 2012 Canonical Ltd.
#
# Authors:
# James Page <james.page@ubuntu.com>
# Paul Collins <paul.collins@canonical.com>
#
import json
import os
import subprocess
import socket
import sys
import base64
import tempfile
def do_hooks(hooks):
hook = os.path.basename(sys.argv[0])
try:
hook_func = hooks[hook]
except KeyError:
juju_log('INFO',
"This charm doesn't know how to handle '{}'.".format(hook))
else:
hook_func()
def install(*pkgs):
cmd = [
'apt-get',
'-y',
'install'
]
for pkg in pkgs:
cmd.append(pkg)
subprocess.check_call(cmd)
TEMPLATES_DIR = 'hooks/templates'
try:
import jinja2
except ImportError:
install('python-jinja2')
import jinja2
try:
import dns.resolver
import dns.ipv4
except ImportError:
install('python-dnspython')
import dns.resolver
import dns.ipv4
def render_template(template_name, context, template_dir=TEMPLATES_DIR):
templates = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_dir)
)
template = templates.get_template(template_name)
return template.render(context)
CLOUD_ARCHIVE = \
""" # Ubuntu Cloud Archive
deb http://ubuntu-cloud.archive.canonical.com/ubuntu {} main
"""
CLOUD_ARCHIVE_POCKETS = {
'folsom': 'precise-updates/folsom',
'folsom/updates': 'precise-updates/folsom',
'folsom/proposed': 'precise-proposed/folsom'
}
def configure_source():
source = str(config_get('openstack-origin'))
if not source:
return
if source.startswith('ppa:'):
cmd = [
'add-apt-repository',
source
]
subprocess.check_call(cmd)
if source.startswith('cloud:'):
install('ubuntu-cloud-keyring')
pocket = source.split(':')[1]
with open('/etc/apt/sources.list.d/cloud-archive.list', 'w') as apt:
apt.write(CLOUD_ARCHIVE.format(CLOUD_ARCHIVE_POCKETS[pocket]))
if source.startswith('deb'):
l = len(source.split('|'))
if l == 2:
(apt_line, key) = source.split('|')
cmd = [
'apt-key',
'adv', '--keyserver keyserver.ubuntu.com',
'--recv-keys', key
]
subprocess.check_call(cmd)
elif l == 1:
apt_line = source
with open('/etc/apt/sources.list.d/quantum.list', 'w') as apt:
apt.write(apt_line + "\n")
cmd = [
'apt-get',
'update'
]
subprocess.check_call(cmd)
# Protocols
TCP = 'TCP'
UDP = 'UDP'
def expose(port, protocol='TCP'):
cmd = [
'open-port',
'{}/{}'.format(port, protocol)
]
subprocess.check_call(cmd)
def juju_log(severity, message):
cmd = [
'juju-log',
'--log-level', severity,
message
]
subprocess.check_call(cmd)
def relation_ids(relation):
cmd = [
'relation-ids',
relation
]
result = str(subprocess.check_output(cmd)).split()
if result == "":
return None
else:
return result
def relation_list(rid):
cmd = [
'relation-list',
'-r', rid,
]
result = str(subprocess.check_output(cmd)).split()
if result == "":
return None
else:
return result
def relation_get(attribute, unit=None, rid=None):
cmd = [
'relation-get',
]
if rid:
cmd.append('-r')
cmd.append(rid)
cmd.append(attribute)
if unit:
cmd.append(unit)
value = subprocess.check_output(cmd).strip() # IGNORE:E1103
if value == "":
return None
else:
return value
def relation_set(**kwargs):
cmd = [
'relation-set'
]
args = []
for k, v in kwargs.items():
if k == 'rid':
if v:
cmd.append('-r')
cmd.append(v)
else:
args.append('{}={}'.format(k, v))
cmd += args
subprocess.check_call(cmd)
def unit_get(attribute):
cmd = [
'unit-get',
attribute
]
value = subprocess.check_output(cmd).strip() # IGNORE:E1103
if value == "":
return None
else:
return value
def config_get(attribute):
cmd = [
'config-get',
'--format',
'json',
]
out = subprocess.check_output(cmd).strip() # IGNORE:E1103
cfg = json.loads(out)
try:
return cfg[attribute]
except KeyError:
return None
def get_unit_hostname():
return socket.gethostname()
def get_host_ip(hostname=unit_get('private-address')):
try:
# Test to see if already an IPv4 address
socket.inet_aton(hostname)
return hostname
except socket.error:
answers = dns.resolver.query(hostname, 'A')
if answers:
return answers[0].address
return None
def restart(*services):
for service in services:
subprocess.check_call(['service', service, 'restart'])
def stop(*services):
for service in services:
subprocess.check_call(['service', service, 'stop'])
def start(*services):
for service in services:
subprocess.check_call(['service', service, 'start'])
def reload(*services):
for service in services:
try:
subprocess.check_call(['service', service, 'reload'])
except subprocess.CalledProcessError:
# Reload failed - either service does not support reload
# or it was not running - restart will fixup most things
subprocess.check_call(['service', service, 'restart'])
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():
cmd = [
"crm", "resource",
"show", "res_swift_vip"
]
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 = os.getenv('JUJU_UNIT_NAME').split('/')[1]
for peer in peers:
remote_unit_no = peer.split('/')[1]
if remote_unit_no < local_unit_no:
return False
return True
def eligible_leader():
if is_clustered():
if not is_leader():
juju_log('INFO', 'Deferring action to CRM leader.')
return False
else:
peers = peer_units()
if peers and not oldest_peer(peers):
juju_log('INFO', 'Deferring action to oldest service unit.')
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'):
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 (relation_get('https_keystone', rid=r_id, unit=unit) and
relation_get('ssl_cert', rid=r_id, unit=unit) and
relation_get('ssl_key', rid=r_id, unit=unit) and
relation_get('ca_cert', rid=r_id, unit=unit)):
return True
return False
APACHE_SITE_DIR = "/etc/apache2/sites-available"
SITE_TEMPLATE = "apache2_site.tmpl"
RELOAD_CHECK = "To activate the new configuration"
def enable_https(port_maps, namespace):
'''
For a given number of port mappings, configures apache2
HTTPs local reverse proxying using certficates and keys provided in
either configuration data (preferred) or relation data. Assumes ports
are not in use (calling charm should ensure that).
port_maps: dict: external to internal port mappings
namespace: str: name of charm
'''
juju_log('INFO', "Enabling HTTPS for port mappings: {}".format(port_maps))
http_restart = False
# allow overriding of keystone provided certs with those set manually
# in config.
cert = config_get('ssl_cert')
key = config_get('ssl_key')
ca_cert = None
if not (cert and key):
juju_log('INFO',
"Inspecting identity-service relations for SSL certificate.")
cert = key = ca_cert = 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)
if not ca_cert:
ca_cert = relation_get('ca_cert', rid=r_id, unit=unit)
if (not (cert and key and ca_cert) and
config_get('use-https')):
juju_log('INFO',
"Using self-signed SSL certificate.")
(cert, key) = generate_cert()
else:
juju_log('INFO',
"Using SSL certificate provided in service config.")
if cert:
cert = base64.b64decode(cert)
if key:
key = base64.b64decode(key)
if ca_cert:
ca_cert = base64.b64decode(ca_cert)
if not cert and not key:
juju_log('ERROR',
"Expected but could not find SSL certificate data, not "
"configuring HTTPS!")
return False
install('apache2')
if RELOAD_CHECK in subprocess.check_output(['a2enmod', 'ssl',
'proxy', 'proxy_http']):
http_restart = True
ssl_dir = os.path.join('/etc/apache2/ssl', namespace)
if not os.path.exists(ssl_dir):
os.makedirs(ssl_dir)
with open(os.path.join(ssl_dir, 'cert'), 'w') as fcert:
fcert.write(cert)
with open(os.path.join(ssl_dir, 'key'), 'w') as fkey:
fkey.write(key)
os.chmod(os.path.join(ssl_dir, 'key'), 0600)
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'])
sites_dir = '/etc/apache2/sites-available'
for ext_port, int_port in port_maps.items():
juju_log('INFO',
'Creating apache2 reverse proxy vhost'
' for {}:{}'.format(ext_port,
int_port))
site = "{}_{}".format(namespace, ext_port)
site_path = os.path.join(sites_dir, site)
with open(site_path, 'w') as fsite:
context = {
"ext": ext_port,
"int": int_port,
"namespace": namespace,
"private_address": get_host_ip()
}
fsite.write(render_template(SITE_TEMPLATE,
context))
if RELOAD_CHECK in subprocess.check_output(['a2ensite', site]):
http_restart = True
if http_restart:
restart('apache2')
return True
def disable_https(port_maps, namespace):
'''
Ensure HTTPS reverse proxying is disables for given port mappings
port_maps: dict: of ext -> int port mappings
namespace: str: name of chamr
'''
juju_log('INFO', 'Ensuring HTTPS disabled for {}'.format(port_maps))
if (not os.path.exists('/etc/apache2') or
not os.path.exists(os.path.join('/etc/apache2/ssl', namespace))):
return
http_restart = False
for ext_port in port_maps.keys():
if os.path.exists(os.path.join(APACHE_SITE_DIR,
"{}_{}".format(namespace,
ext_port))):
juju_log('INFO',
"Disabling HTTPS reverse proxy"
" for {} {}.".format(namespace,
ext_port))
if (RELOAD_CHECK in
subprocess.check_output(['a2dissite',
'{}_{}'.format(namespace,
ext_port)])):
http_restart = True
if http_restart:
restart(['apache2'])
def setup_https(port_maps, namespace):
'''
Ensures HTTPS is either enabled or disabled for given port
mapping.
port_maps: dict: of ext -> int port mappings
namespace: str: name of charm
'''
if not https:
disable_https(port_maps, namespace)
else:
enable_https(port_maps, namespace)
def generate_cert():
'''
Generates a self signed certificate and key using the
provided charm configuration data.
returns: tuple of (cert, key)
'''
CERT = '/etc/swift/ssl.cert'
KEY = '/etc/swift/ssl.key'
if (not os.path.exists(CERT) and
not os.path.exists(KEY)):
subj = '/C=%s/ST=%s/L=%s/CN=%s' %\
(config_get('country'), config_get('state'),
config_get('locale'), config_get('common-name'))
cmd = ['openssl', 'req', '-new', '-x509', '-nodes',
'-out', CERT, '-keyout', KEY,
'-subj', subj]
subprocess.check_call(cmd)
# Slurp as base64 encoded - makes handling easier up the stack
with open(CERT, 'r') as cfile:
ssl_cert = base64.b64encode(cfile.read())
with open(KEY, 'r') as kfile:
ssl_key = base64.b64encode(kfile.read())
return (ssl_cert, ssl_key)
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)
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
HAPROXY_DEFAULT = '/etc/default/haproxy'
def configure_haproxy(service_ports):
'''
Configure HAProxy based on the current peers in the service
cluster using the provided port map:
"swift": [ 8080, 8070 ]
HAproxy will also be reloaded/started if required
service_ports: dict: dict of lists of [ frontend, backend ]
'''
cluster_hosts = {}
cluster_hosts[os.getenv('JUJU_UNIT_NAME').replace('/', '-')] = \
unit_get('private-address')
for r_id in relation_ids('cluster'):
for unit in relation_list(r_id):
cluster_hosts[unit.replace('/', '-')] = \
relation_get(attribute='private-address',
rid=r_id,
unit=unit)
context = {
'units': cluster_hosts,
'service_ports': service_ports
}
with open(HAPROXY_CONF, 'w') as f:
f.write(render_template(os.path.basename(HAPROXY_CONF),
context))
with open(HAPROXY_DEFAULT, 'w') as f:
f.write('ENABLED=1')
reload('haproxy')