using peerstorage to cluster passwords
This commit is contained in:
parent
b5d52c5f36
commit
93cb1bd376
|
@ -6,5 +6,6 @@ include:
|
|||
- contrib.charmsupport
|
||||
- contrib.openstack
|
||||
- contrib.storage
|
||||
- contrib.peerstorage
|
||||
- contrib.ssl
|
||||
- contrib.hahelpers.cluster
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import json
|
||||
import os
|
||||
import time
|
||||
|
||||
from base64 import b64decode
|
||||
|
||||
|
@ -113,7 +114,8 @@ class OSContextGenerator(object):
|
|||
class SharedDBContext(OSContextGenerator):
|
||||
interfaces = ['shared-db']
|
||||
|
||||
def __init__(self, database=None, user=None, relation_prefix=None):
|
||||
def __init__(self,
|
||||
database=None, user=None, relation_prefix=None, ssl_dir=None):
|
||||
'''
|
||||
Allows inspecting relation for settings prefixed with relation_prefix.
|
||||
This is useful for parsing access for multiple databases returned via
|
||||
|
@ -122,6 +124,7 @@ class SharedDBContext(OSContextGenerator):
|
|||
self.relation_prefix = relation_prefix
|
||||
self.database = database
|
||||
self.user = user
|
||||
self.ssl_dir = ssl_dir
|
||||
|
||||
def __call__(self):
|
||||
self.database = self.database or config('database')
|
||||
|
@ -139,19 +142,44 @@ class SharedDBContext(OSContextGenerator):
|
|||
|
||||
for rid in relation_ids('shared-db'):
|
||||
for unit in related_units(rid):
|
||||
passwd = relation_get(password_setting, rid=rid, unit=unit)
|
||||
rdata = relation_get(rid=rid, unit=unit)
|
||||
ctxt = {
|
||||
'database_host': relation_get('db_host', rid=rid,
|
||||
unit=unit),
|
||||
'database_host': rdata.get('db_host'),
|
||||
'database': self.database,
|
||||
'database_user': self.user,
|
||||
'database_password': passwd,
|
||||
'database_password': rdata.get(password_setting)
|
||||
}
|
||||
if context_complete(ctxt):
|
||||
db_ssl(rdata, ctxt, self.ssl_dir)
|
||||
return ctxt
|
||||
return {}
|
||||
|
||||
|
||||
def db_ssl(rdata, ctxt, ssl_dir):
|
||||
if 'ssl_ca' in rdata and ssl_dir:
|
||||
ca_path = os.path.join(ssl_dir, 'db-client.ca')
|
||||
with open(ca_path, 'w') as fh:
|
||||
fh.write(b64decode(rdata['ssl_ca']))
|
||||
ctxt['database_ssl_ca'] = ca_path
|
||||
elif 'ssl_ca' in rdata:
|
||||
log("Charm not setup for ssl support but ssl ca found")
|
||||
return ctxt
|
||||
if 'ssl_cert' in rdata:
|
||||
cert_path = os.path.join(
|
||||
ssl_dir, 'db-client.cert')
|
||||
if not os.path.exists(cert_path):
|
||||
log("Waiting 1m for ssl client cert validity")
|
||||
time.sleep(60)
|
||||
with open(cert_path, 'w') as fh:
|
||||
fh.write(b64decode(rdata['ssl_cert']))
|
||||
ctxt['database_ssl_cert'] = cert_path
|
||||
key_path = os.path.join(ssl_dir, 'db-client.key')
|
||||
with open(key_path, 'w') as fh:
|
||||
fh.write(b64decode(rdata['ssl_key']))
|
||||
ctxt['database_ssl_key'] = key_path
|
||||
return ctxt
|
||||
|
||||
|
||||
class IdentityServiceContext(OSContextGenerator):
|
||||
interfaces = ['identity-service']
|
||||
|
||||
|
@ -161,22 +189,19 @@ class IdentityServiceContext(OSContextGenerator):
|
|||
|
||||
for rid in relation_ids('identity-service'):
|
||||
for unit in related_units(rid):
|
||||
rdata = relation_get(rid=rid, unit=unit)
|
||||
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',
|
||||
'service_port': rdata.get('service_port'),
|
||||
'service_host': rdata.get('service_host'),
|
||||
'auth_host': rdata.get('auth_host'),
|
||||
'auth_port': rdata.get('auth_port'),
|
||||
'admin_tenant_name': rdata.get('service_tenant'),
|
||||
'admin_user': rdata.get('service_username'),
|
||||
'admin_password': rdata.get('service_password'),
|
||||
'service_protocol':
|
||||
rdata.get('service_protocol') or 'http',
|
||||
'auth_protocol':
|
||||
rdata.get('auth_protocol') or 'http',
|
||||
}
|
||||
if context_complete(ctxt):
|
||||
return ctxt
|
||||
|
@ -186,6 +211,9 @@ class IdentityServiceContext(OSContextGenerator):
|
|||
class AMQPContext(OSContextGenerator):
|
||||
interfaces = ['amqp']
|
||||
|
||||
def __init__(self, ssl_dir=None):
|
||||
self.ssl_dir = ssl_dir
|
||||
|
||||
def __call__(self):
|
||||
log('Generating template context for amqp')
|
||||
conf = config()
|
||||
|
@ -196,10 +224,8 @@ class AMQPContext(OSContextGenerator):
|
|||
log('Could not generate shared_db context. '
|
||||
'Missing required charm config options: %s.' % e)
|
||||
raise OSContextError
|
||||
|
||||
ctxt = {}
|
||||
for rid in relation_ids('amqp'):
|
||||
ha_vip_only = False
|
||||
for unit in related_units(rid):
|
||||
if relation_get('clustered', rid=rid, unit=unit):
|
||||
ctxt['clustered'] = True
|
||||
|
@ -212,20 +238,35 @@ class AMQPContext(OSContextGenerator):
|
|||
'rabbitmq_user': username,
|
||||
'rabbitmq_password': relation_get('password', rid=rid,
|
||||
unit=unit),
|
||||
'rabbitmq_virtual_host': vhost
|
||||
'rabbitmq_virtual_host': vhost,
|
||||
})
|
||||
if relation_get('ha_queues', rid=rid, unit=unit):
|
||||
ctxt['rabbitmq_ha_queues'] = relation_get('ha_queues', rid=rid, unit=unit)
|
||||
else:
|
||||
ctxt['rabbitmq_ha_queues'] = False
|
||||
|
||||
ha_vip_only = (relation_get('ha-vip-only', rid=rid, unit=unit) == 'True')
|
||||
ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
|
||||
if ssl_port:
|
||||
ctxt['rabbit_ssl_port'] = ssl_port
|
||||
ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
|
||||
if ssl_ca:
|
||||
ctxt['rabbit_ssl_ca'] = ssl_ca
|
||||
|
||||
if context_complete(ctxt):
|
||||
if 'rabbit_ssl_ca' in ctxt:
|
||||
if not self.ssl_dir:
|
||||
log(("Charm not setup for ssl support "
|
||||
"but ssl ca found"))
|
||||
break
|
||||
ca_path = os.path.join(
|
||||
self.ssl_dir, 'rabbit-client-ca.pem')
|
||||
with open(ca_path, 'w') as fh:
|
||||
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
|
||||
ctxt['rabbit_ssl_ca'] = ca_path
|
||||
# Sufficient information found = break out!
|
||||
break
|
||||
# Used for active/active rabbitmq >= grizzly
|
||||
if ('clustered' not in ctxt or ha_vip_only) and len(related_units(rid)) > 1:
|
||||
if ('clustered' not in ctxt or relation_get('ha-vip-only') == 'True') and \
|
||||
len(related_units(rid)) > 1:
|
||||
if relation_get('ha_queues'):
|
||||
ctxt['rabbitmq_ha_queues'] = relation_get('ha_queues')
|
||||
else:
|
||||
ctxt['rabbitmq_ha_queues'] = False
|
||||
rabbitmq_hosts = []
|
||||
for unit in related_units(rid):
|
||||
rabbitmq_hosts.append(relation_get('private-address',
|
||||
|
@ -391,6 +432,8 @@ class ApacheSSLContext(OSContextGenerator):
|
|||
'private_address': unit_get('private-address'),
|
||||
'endpoints': []
|
||||
}
|
||||
if is_clustered():
|
||||
ctxt['private_address'] = config('vip')
|
||||
for api_port in self.external_ports:
|
||||
ext_port = determine_apache_port(api_port)
|
||||
int_port = determine_api_port(api_port)
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
from charmhelpers.core.hookenv import (
|
||||
relation_ids,
|
||||
relation_get,
|
||||
local_unit,
|
||||
relation_set,
|
||||
)
|
||||
|
||||
"""
|
||||
This helper provides functions to support use of a peer relation
|
||||
for basic key/value storage, with the added benefit that all storage
|
||||
can be replicated across peer units, so this is really useful for
|
||||
services that issue usernames/passwords to remote services.
|
||||
|
||||
def shared_db_changed()
|
||||
# Only the lead unit should create passwords
|
||||
if not is_leader():
|
||||
return
|
||||
username = relation_get('username')
|
||||
key = '{}.password'.format(username)
|
||||
# Attempt to retrieve any existing password for this user
|
||||
password = peer_retrieve(key)
|
||||
if password is None:
|
||||
# New user, create password and store
|
||||
password = pwgen(length=64)
|
||||
peer_store(key, password)
|
||||
create_access(username, password)
|
||||
relation_set(password=password)
|
||||
|
||||
|
||||
def cluster_changed()
|
||||
# Echo any relation data other that *-address
|
||||
# back onto the peer relation so all units have
|
||||
# all *.password keys stored on their local relation
|
||||
# for later retrieval.
|
||||
peer_echo()
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def peer_retrieve(key, relation_name='cluster'):
|
||||
""" Retrieve a named key from peer relation relation_name """
|
||||
cluster_rels = relation_ids(relation_name)
|
||||
if len(cluster_rels) > 0:
|
||||
cluster_rid = cluster_rels[0]
|
||||
return relation_get(attribute=key, rid=cluster_rid,
|
||||
unit=local_unit())
|
||||
else:
|
||||
raise ValueError('Unable to detect'
|
||||
'peer relation {}'.format(relation_name))
|
||||
|
||||
|
||||
def peer_store(key, value, relation_name='cluster'):
|
||||
""" Store the key/value pair on the named peer relation relation_name """
|
||||
cluster_rels = relation_ids(relation_name)
|
||||
if len(cluster_rels) > 0:
|
||||
cluster_rid = cluster_rels[0]
|
||||
relation_set(relation_id=cluster_rid,
|
||||
relation_settings={key: value})
|
||||
else:
|
||||
raise ValueError('Unable to detect '
|
||||
'peer relation {}'.format(relation_name))
|
||||
|
||||
|
||||
def peer_echo(includes=None):
|
||||
"""Echo filtered attributes back onto the same relation for storage
|
||||
|
||||
Note that this helper must only be called within a peer relation
|
||||
changed hook
|
||||
"""
|
||||
rdata = relation_get()
|
||||
echo_data = {}
|
||||
if includes is None:
|
||||
echo_data = rdata.copy()
|
||||
for ex in ['private-address', 'public-address']:
|
||||
echo_data.pop(ex)
|
||||
else:
|
||||
for attribute, value in rdata.iteritems():
|
||||
for include in includes:
|
||||
if include in attribute:
|
||||
echo_data[attribute] = value
|
||||
if len(echo_data) > 0:
|
||||
relation_set(relation_settings=echo_data)
|
|
@ -44,6 +44,12 @@ from charmhelpers.core.host import (
|
|||
from charmhelpers.contrib.charmsupport.nrpe import NRPE
|
||||
from charmhelpers.contrib.ssl.service import ServiceCA
|
||||
|
||||
from charmhelpers.contrib.peerstorage import (
|
||||
peer_store,
|
||||
peer_retrieve,
|
||||
peer_echo
|
||||
)
|
||||
|
||||
hooks = Hooks()
|
||||
|
||||
SERVICE_NAME = os.getenv('JUJU_UNIT_NAME').split('/')[0]
|
||||
|
@ -65,11 +71,11 @@ def install():
|
|||
|
||||
def configure_amqp(username, vhost):
|
||||
# get and update service password
|
||||
password = rabbit.get_clustered_attribute('%s.passwd' % username)
|
||||
password = peer_retrieve('%s.passwd' % username)
|
||||
if not password:
|
||||
# update password in cluster
|
||||
password = pwgen(length=64)
|
||||
rabbit.set_clustered_attribute('%s.passwd' % username, password)
|
||||
peer_store('%s.passwd' % username, password)
|
||||
|
||||
# update vhost
|
||||
rabbit.create_vhost(vhost)
|
||||
|
@ -151,26 +157,24 @@ def cluster_joined():
|
|||
level=ERROR)
|
||||
return
|
||||
cookie = open(rabbit.COOKIE_PATH, 'r').read().strip()
|
||||
rabbit.set_clustered_attribute('cookie', cookie)
|
||||
peer_store('cookie', cookie)
|
||||
|
||||
|
||||
@hooks.hook('cluster-relation-changed')
|
||||
def cluster_changed():
|
||||
# sync passwords
|
||||
rdata = relation_get()
|
||||
echo_data = {}
|
||||
include_data = []
|
||||
for attribute, value in rdata.iteritems():
|
||||
if '.passwd' in attribute or attribute == 'cookie':
|
||||
echo_data[attribute] = value
|
||||
if len(echo_data) > 0:
|
||||
relation_set(relation_settings=echo_data)
|
||||
include_data.append(value)
|
||||
peer_echo(include_data)
|
||||
|
||||
if 'cookie' not in echo_data:
|
||||
log('cluster_joined: cookie not yet set.')
|
||||
return
|
||||
|
||||
# sync cookie
|
||||
cookie = echo_data['cookie']
|
||||
cookie = peer_retrieve('cookie')
|
||||
if open(rabbit.COOKIE_PATH, 'r').read().strip() == cookie:
|
||||
log('Cookie already synchronized with peer.')
|
||||
else:
|
||||
|
@ -365,11 +369,11 @@ def update_nrpe_checks():
|
|||
current_unit = local_unit().replace('/', '-')
|
||||
user = 'nagios-%s' % current_unit
|
||||
vhost = 'nagios-%s' % current_unit
|
||||
password = rabbit.get_clustered_attribute('%s.passwd' % user)
|
||||
password = peer_retrieve('%s.passwd' % user)
|
||||
if not password:
|
||||
log('Setting password for nagios unit: %s' % user)
|
||||
password = pwgen(length=64)
|
||||
rabbit.set_clustered_attribute('%s.passwd' % user, password)
|
||||
peer_store('%s.passwd' % user, password)
|
||||
|
||||
rabbit.create_vhost(vhost)
|
||||
rabbit.create_user(user, password)
|
||||
|
@ -401,12 +405,12 @@ def upgrade_charm():
|
|||
|
||||
# propagate to cluster if needed
|
||||
username = os.path.basename(s)
|
||||
password = rabbit.get_clustered_attribute(username)
|
||||
password = peer_retrieve(username)
|
||||
if password is None:
|
||||
with open(s, 'r') as h:
|
||||
stored_password = h.read()
|
||||
if stored_password:
|
||||
rabbit.set_clustered_attribute(username, stored_password)
|
||||
peer_store(username, stored_password)
|
||||
|
||||
log('upgrade_charm: Migrating stored passwd'
|
||||
' from %s to %s.' % (s, d))
|
||||
|
|
Loading…
Reference in New Issue