charm-keystone/hooks/keystone_utils.py
2013-04-09 10:30:55 -07:00

545 lines
20 KiB
Python
Executable File

#!/usr/bin/python
import ConfigParser
import sys
import json
import time
import subprocess
import os
from lib.openstack_common import(
get_os_codename_install_source,
get_os_codename_package,
error_out,
configure_installation_source
)
import keystone_ssl as ssl
import lib.unison as unison
import lib.utils as utils
import lib.cluster_utils as cluster
keystone_conf = "/etc/keystone/keystone.conf"
stored_passwd = "/var/lib/keystone/keystone.passwd"
stored_token = "/var/lib/keystone/keystone.token"
SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd'
SSL_DIR = '/var/lib/keystone/juju_ssl/'
SSL_CA_NAME = 'Ubuntu Cloud'
CLUSTER_RES = 'res_ks_vip'
SSH_USER = 'juju_keystone'
def execute(cmd, die=False, echo=False):
""" Executes a command
if die=True, script will exit(1) if command does not return 0
if echo=True, output of command will be printed to stdout
returns a tuple: (stdout, stderr, return code)
"""
p = subprocess.Popen(cmd.split(" "),
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.PIPE)
stdout = ""
stderr = ""
def print_line(l):
if echo:
print l.strip('\n')
sys.stdout.flush()
for l in iter(p.stdout.readline, ''):
print_line(l)
stdout += l
for l in iter(p.stderr.readline, ''):
print_line(l)
stderr += l
p.communicate()
rc = p.returncode
if die and rc != 0:
error_out("ERROR: command %s return non-zero.\n" % cmd)
return (stdout, stderr, rc)
def config_get():
""" Obtain the units config via 'config-get'
Returns a dict representing current config.
private-address and IP of the unit is also tacked on for
convienence
"""
output = execute("config-get --format json")[0]
config = json.loads(output)
# make sure no config element is blank after config-get
for c in config.keys():
if not config[c]:
error_out("ERROR: Config option has no paramter: %s" % c)
# tack on our private address and ip
config["hostname"] = utils.unit_get('private-address')
return config
@utils.cached
def get_local_endpoint():
""" Returns the URL for the local end-point bypassing haproxy/ssl """
local_endpoint = 'http://localhost:{}/v2.0/'.format(
cluster.determine_api_port(utils.config_get('admin-port'))
)
return local_endpoint
def set_admin_token(admin_token):
"""Set admin token according to deployment config or use a randomly
generated token if none is specified (default).
"""
if admin_token != 'None':
utils.juju_log('INFO',
'Configuring Keystone to use'
' a pre-configured admin token.')
token = admin_token
else:
utils.juju_log('INFO',
'Configuring Keystone to use a random admin token.')
if os.path.isfile(stored_token):
msg = 'Loading a previously generated' \
' admin token from %s' % stored_token
utils.juju_log('INFO', msg)
f = open(stored_token, 'r')
token = f.read().strip()
f.close()
else:
token = execute('pwgen -c 32 1', die=True)[0].strip()
out = open(stored_token, 'w')
out.write('%s\n' % token)
out.close()
update_config_block('DEFAULT', admin_token=token)
def get_admin_token():
"""Temporary utility to grab the admin token as configured in
keystone.conf
"""
with open(keystone_conf, 'r') as f:
for l in f.readlines():
if l.split(' ')[0] == 'admin_token':
try:
return l.split('=')[1].strip()
except:
error_out('Could not parse admin_token line from %s' %
keystone_conf)
error_out('Could not find admin_token line in %s' % keystone_conf)
# Track all updated config settings.
_config_dirty = [False]
def config_dirty():
return True in _config_dirty
def update_config_block(section, **kwargs):
""" Updates keystone.conf blocks given kwargs.
Update a config setting in a specific setting of a config
file (/etc/keystone/keystone.conf, by default)
"""
if 'file' in kwargs:
conf_file = kwargs['file']
del kwargs['file']
else:
conf_file = keystone_conf
config = ConfigParser.RawConfigParser()
config.read(conf_file)
if section != 'DEFAULT' and not config.has_section(section):
config.add_section(section)
_config_dirty[0] = True
for k, v in kwargs.iteritems():
try:
cur = config.get(section, k)
if cur != v:
_config_dirty[0] = True
except (ConfigParser.NoSectionError,
ConfigParser.NoOptionError):
_config_dirty[0] = True
config.set(section, k, v)
with open(conf_file, 'wb') as out:
config.write(out)
def create_service_entry(service_name, service_type, service_desc, owner=None):
""" Add a new service entry to keystone if one does not already exist """
import manager
manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
token=get_admin_token())
for service in [s._info for s in manager.api.services.list()]:
if service['name'] == service_name:
utils.juju_log('INFO',
"Service entry for '%s' already exists." % \
service_name)
return
manager.api.services.create(name=service_name,
service_type=service_type,
description=service_desc)
utils.juju_log('INFO', "Created new service entry '%s'" % service_name)
def create_endpoint_template(region, service, publicurl, adminurl,
internalurl):
""" Create a new endpoint template for service if one does not already
exist matching name *and* region """
import manager
manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
token=get_admin_token())
service_id = manager.resolve_service_id(service)
for ep in [e._info for e in manager.api.endpoints.list()]:
if ep['service_id'] == service_id and ep['region'] == region:
utils.juju_log('INFO',
"Endpoint template already exists for '%s' in '%s'"
% (service, region))
up_to_date = True
for k in ['publicurl', 'adminurl', 'internalurl']:
if ep[k] != locals()[k]:
up_to_date = False
if up_to_date:
return
else:
# delete endpoint and recreate if endpoint urls need updating.
utils.juju_log('INFO',
"Updating endpoint template with"
" new endpoint urls.")
manager.api.endpoints.delete(ep['id'])
manager.api.endpoints.create(region=region,
service_id=service_id,
publicurl=publicurl,
adminurl=adminurl,
internalurl=internalurl)
utils.juju_log('INFO', "Created new endpoint template for '%s' in '%s'" %
(region, service))
def create_tenant(name):
""" creates a tenant if it does not already exist """
import manager
manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
token=get_admin_token())
tenants = [t._info for t in manager.api.tenants.list()]
if not tenants or name not in [t['name'] for t in tenants]:
manager.api.tenants.create(tenant_name=name,
description='Created by Juju')
utils.juju_log('INFO', "Created new tenant: %s" % name)
return
utils.juju_log('INFO', "Tenant '%s' already exists." % name)
def create_user(name, password, tenant):
""" creates a user if it doesn't already exist, as a member of tenant """
import manager
manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
token=get_admin_token())
users = [u._info for u in manager.api.users.list()]
if not users or name not in [u['name'] for u in users]:
tenant_id = manager.resolve_tenant_id(tenant)
if not tenant_id:
error_out('Could not resolve tenant_id for tenant %s' % tenant)
manager.api.users.create(name=name,
password=password,
email='juju@localhost',
tenant_id=tenant_id)
utils.juju_log('INFO', "Created new user '%s' tenant: %s" % \
(name, tenant_id))
return
utils.juju_log('INFO', "A user named '%s' already exists" % name)
def create_role(name, user=None, tenant=None):
""" creates a role if it doesn't already exist. grants role to user """
import manager
manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
token=get_admin_token())
roles = [r._info for r in manager.api.roles.list()]
if not roles or name not in [r['name'] for r in roles]:
manager.api.roles.create(name=name)
utils.juju_log('INFO', "Created new role '%s'" % name)
else:
utils.juju_log('INFO', "A role named '%s' already exists" % name)
if not user and not tenant:
return
# NOTE(adam_g): Keystone client requires id's for add_user_role, not names
user_id = manager.resolve_user_id(user)
role_id = manager.resolve_role_id(name)
tenant_id = manager.resolve_tenant_id(tenant)
if None in [user_id, role_id, tenant_id]:
error_out("Could not resolve [%s, %s, %s]" %
(user_id, role_id, tenant_id))
grant_role(user, name, tenant)
def grant_role(user, role, tenant):
"""grant user+tenant a specific role"""
import manager
manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
token=get_admin_token())
utils.juju_log('INFO', "Granting user '%s' role '%s' on tenant '%s'" % \
(user, role, tenant))
user_id = manager.resolve_user_id(user)
role_id = manager.resolve_role_id(role)
tenant_id = manager.resolve_tenant_id(tenant)
cur_roles = manager.api.roles.roles_for_user(user_id, tenant_id)
if not cur_roles or role_id not in [r.id for r in cur_roles]:
manager.api.roles.add_user_role(user=user_id,
role=role_id,
tenant=tenant_id)
utils.juju_log('INFO', "Granted user '%s' role '%s' on tenant '%s'" % \
(user, role, tenant))
else:
utils.juju_log('INFO',
"User '%s' already has role '%s' on tenant '%s'" % \
(user, role, tenant))
def generate_admin_token(config):
""" generate and add an admin token """
import manager
manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
token='ADMIN')
if config["admin-token"] == "None":
import random
token = random.randrange(1000000000000, 9999999999999)
else:
return config["admin-token"]
manager.api.add_token(token, config["admin-user"],
"admin", config["token-expiry"])
utils.juju_log('INFO', "Generated and added new random admin token.")
return token
def ensure_initial_admin(config):
""" Ensures the minimum admin stuff exists in whatever database we're
using.
This and the helper functions it calls are meant to be idempotent and
run during install as well as during db-changed. This will maintain
the admin tenant, user, role, service entry and endpoint across every
datastore we might use.
TODO: Possibly migrate data from one backend to another after it
changes?
"""
create_tenant("admin")
create_tenant(config["service-tenant"])
passwd = ""
if config["admin-password"] != "None":
passwd = config["admin-password"]
elif os.path.isfile(stored_passwd):
utils.juju_log('INFO', "Loading stored passwd from %s" % stored_passwd)
passwd = open(stored_passwd, 'r').readline().strip('\n')
if passwd == "":
utils.juju_log('INFO', "Generating new passwd for user: %s" % \
config["admin-user"])
passwd = execute("pwgen -c 16 1", die=True)[0]
open(stored_passwd, 'w+').writelines("%s\n" % passwd)
create_user(config['admin-user'], passwd, tenant='admin')
update_user_password(config['admin-user'], passwd)
create_role(config['admin-role'], config['admin-user'], 'admin')
# TODO(adam_g): The following roles are likely not needed since redux merge
create_role("KeystoneAdmin", config["admin-user"], 'admin')
create_role("KeystoneServiceAdmin", config["admin-user"], 'admin')
create_service_entry("keystone", "identity", "Keystone Identity Service")
if cluster.is_clustered():
utils.juju_log('INFO', "Creating endpoint for clustered configuration")
service_host = auth_host = config["vip"]
else:
utils.juju_log('INFO', "Creating standard endpoint")
service_host = auth_host = config["hostname"]
for region in config['region'].split():
create_keystone_endpoint(service_host=service_host,
service_port=config["service-port"],
auth_host=auth_host,
auth_port=config["admin-port"],
region=region)
def create_keystone_endpoint(service_host, service_port,
auth_host, auth_port, region):
public_url = "http://%s:%s/v2.0" % (service_host, service_port)
admin_url = "http://%s:%s/v2.0" % (auth_host, auth_port)
internal_url = "http://%s:%s/v2.0" % (service_host, service_port)
create_endpoint_template(region, "keystone", public_url,
admin_url, internal_url)
def update_user_password(username, password):
import manager
manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
token=get_admin_token())
utils.juju_log('INFO', "Updating password for user '%s'" % username)
user_id = manager.resolve_user_id(username)
if user_id is None:
error_out("Could not resolve user id for '%s'" % username)
manager.api.users.update_password(user=user_id, password=password)
utils.juju_log('INFO', "Successfully updated password for user '%s'" % \
username)
def load_stored_passwords(path=SERVICE_PASSWD_PATH):
creds = {}
if not os.path.isfile(path):
return creds
stored_passwd = open(path, 'r')
for l in stored_passwd.readlines():
user, passwd = l.strip().split(':')
creds[user] = passwd
return creds
def save_stored_passwords(path=SERVICE_PASSWD_PATH, **creds):
with open(path, 'wb') as stored_passwd:
[stored_passwd.write('%s:%s\n' % (u, p)) for u, p in creds.iteritems()]
def get_service_password(service_username):
creds = load_stored_passwords()
if service_username in creds:
return creds[service_username]
passwd = subprocess.check_output(['pwgen', '-c', '32', '1']).strip()
creds[service_username] = passwd
save_stored_passwords(**creds)
return passwd
def configure_pki_tokens(config):
'''Configure PKI token signing, if enabled.'''
if config['enable-pki'] not in ['True', 'true']:
update_config_block('signing', token_format='UUID')
else:
utils.juju_log('INFO', 'TODO: PKI Support, setting to UUID for now.')
update_config_block('signing', token_format='UUID')
def do_openstack_upgrade(install_src, packages):
'''Upgrade packages from a given install src.'''
config = config_get()
old_vers = get_os_codename_package('keystone')
new_vers = get_os_codename_install_source(install_src)
utils.juju_log('INFO',
"Beginning Keystone upgrade: %s -> %s" % \
(old_vers, new_vers))
# Backup previous config.
utils.juju_log('INFO', "Backing up contents of /etc/keystone.")
stamp = time.strftime('%Y%m%d%H%M')
cmd = 'tar -pcf /var/lib/juju/keystone-backup-%s.tar /etc/keystone' % stamp
execute(cmd, die=True, echo=True)
configure_installation_source(install_src)
execute('apt-get update', die=True, echo=True)
os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
cmd = 'apt-get --option Dpkg::Options::=--force-confnew -y '\
'install %s' % packages
execute(cmd, echo=True, die=True)
# we have new, fresh config files that need updating.
# set the admin token, which is still stored in config.
set_admin_token(config['admin-token'])
# set the sql connection string if a shared-db relation is found.
ids = utils.relation_ids('shared-db')
if ids:
for rid in ids:
for unit in utils.relation_list(rid):
utils.juju_log('INFO',
'Configuring new keystone.conf for '
'database access on existing database'
' relation to %s' % unit)
relation_data = utils.relation_get_dict(relation_id=rid,
remote_unit=unit)
update_config_block('sql', connection="mysql://%s:%s@%s/%s" %
(config["database-user"],
relation_data["password"],
relation_data["private-address"],
config["database"]))
utils.stop('keystone')
if (cluster.eligible_leader(CLUSTER_RES)):
utils.juju_log('INFO',
'Running database migrations for %s' % new_vers)
execute('keystone-manage db_sync', echo=True, die=True)
else:
utils.juju_log('INFO',
'Not cluster leader; snoozing whilst'
' leader upgrades DB')
time.sleep(10)
utils.start('keystone')
time.sleep(5)
utils.juju_log('INFO',
'Completed Keystone upgrade: '
'%s -> %s' % (old_vers, new_vers))
def synchronize_service_credentials():
'''
Broadcast service credentials to peers or consume those that have been
broadcasted by peer, depending on hook context.
'''
if (not cluster.eligible_leader(CLUSTER_RES) or
not os.path.isfile(SERVICE_PASSWD_PATH)):
return
utils.juju_log('INFO', 'Synchronizing service passwords to all peers.')
unison.sync_to_peers(peer_interface='cluster',
paths=[SERVICE_PASSWD_PATH], user=SSH_USER,
verbose=True)
CA = []
def get_ca(user='keystone', group='keystone'):
"""
Initialize a new CA object if one hasn't already been loaded.
This will create a new CA or load an existing one.
"""
if not CA:
if not os.path.isdir(SSL_DIR):
os.mkdir(SSL_DIR)
d_name = '_'.join(SSL_CA_NAME.lower().split(' '))
ca = ssl.JujuCA(name=SSL_CA_NAME, user=user, group=group,
ca_dir=os.path.join(SSL_DIR,
'%s_intermediate_ca' % d_name),
root_ca_dir=os.path.join(SSL_DIR,
'%s_root_ca' % d_name))
# SSL_DIR is synchronized via all peers over unison+ssh, need
# to ensure permissions.
execute('chown -R %s.%s %s' % (user, group, SSL_DIR))
execute('chmod -R g+rwx %s' % SSL_DIR)
CA.append(ca)
return CA[0]
def https():
if (utils.config_get('https-service-endpoints') in ["yes", "true", "True"]
or cluster.https()):
return True
else:
return False