9
from lib.openstack_common import(
7
from base64 import b64encode
8
from collections import OrderedDict
9
from copy import deepcopy
11
from charmhelpers.contrib.hahelpers.cluster import(
18
from charmhelpers.contrib.openstack import context, templating
20
from charmhelpers.contrib.openstack.utils import (
21
configure_installation_source,
10
23
get_os_codename_install_source,
11
get_os_codename_package,
13
configure_installation_source
25
save_script_rc as _save_script_rc)
27
import charmhelpers.contrib.unison as unison
29
from charmhelpers.core.hookenv import (
38
from charmhelpers.fetch import (
43
from charmhelpers.core.host import (
48
import keystone_context
16
49
import keystone_ssl as ssl
17
import lib.unison as unison
18
import lib.utils as utils
19
import lib.cluster_utils as cluster
22
keystone_conf = "/etc/keystone/keystone.conf"
23
stored_passwd = "/var/lib/keystone/keystone.passwd"
24
stored_token = "/var/lib/keystone/keystone.token"
51
TEMPLATES = 'templates/'
53
# removed from original: charm-helper-sh
58
'python-keystoneclient',
70
'keystone-admin': config('admin-port'),
71
'keystone-public': config('service-port')
74
KEYSTONE_CONF = "/etc/keystone/keystone.conf"
75
KEYSTONE_CONF_DIR = os.path.dirname(KEYSTONE_CONF)
76
STORED_PASSWD = "/var/lib/keystone/keystone.passwd"
77
STORED_TOKEN = "/var/lib/keystone/keystone.token"
25
78
SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd'
80
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
81
APACHE_CONF = '/etc/apache2/sites-available/openstack_https_frontend'
82
APACHE_24_CONF = '/etc/apache2/sites-available/openstack_https_frontend.conf'
27
84
SSL_DIR = '/var/lib/keystone/juju_ssl/'
28
85
SSL_CA_NAME = 'Ubuntu Cloud'
29
86
CLUSTER_RES = 'res_ks_vip'
30
87
SSH_USER = 'juju_keystone'
33
def execute(cmd, die=False, echo=False):
34
""" Executes a command
36
if die=True, script will exit(1) if command does not return 0
37
if echo=True, output of command will be printed to stdout
39
returns a tuple: (stdout, stderr, return code)
41
p = subprocess.Popen(cmd.split(" "),
42
stdout=subprocess.PIPE,
43
stdin=subprocess.PIPE,
44
stderr=subprocess.PIPE)
53
for l in iter(p.stdout.readline, ''):
56
for l in iter(p.stderr.readline, ''):
64
error_out("ERROR: command %s return non-zero.\n" % cmd)
65
return (stdout, stderr, rc)
69
""" Obtain the units config via 'config-get'
70
Returns a dict representing current config.
71
private-address and IP of the unit is also tacked on for
74
output = execute("config-get --format json")[0]
75
config = json.loads(output)
76
# make sure no config element is blank after config-get
77
for c in config.keys():
79
error_out("ERROR: Config option has no paramter: %s" % c)
80
# tack on our private address and ip
81
config["hostname"] = utils.unit_get('private-address')
89
BASE_RESOURCE_MAP = OrderedDict([
91
'services': BASE_SERVICES,
92
'contexts': [keystone_context.KeystoneContext(),
93
context.SharedDBContext(ssl_dir=KEYSTONE_CONF_DIR),
94
context.SyslogContext(),
95
keystone_context.HAProxyContext()],
98
'contexts': [context.HAProxyContext(),
99
keystone_context.HAProxyContext()],
100
'services': ['haproxy'],
103
'contexts': [keystone_context.ApacheSSLContext()],
104
'services': ['apache2'],
107
'contexts': [keystone_context.ApacheSSLContext()],
108
'services': ['apache2'],
112
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
117
"desc": "Nova Compute Service"
121
"desc": "Nova Volume Service"
125
"desc": "Cinder Volume Service"
129
"desc": "EC2 Compatibility Layer"
133
"desc": "Glance Image Service"
137
"desc": "S3 Compatible object-store"
140
"type": "object-store",
141
"desc": "Swift Object Storage Service"
145
"desc": "Quantum Networking Service"
149
"desc": "Oxygen Cloud Image Service"
153
"desc": "Ceilometer Metering Service"
156
"type": "orchestration",
157
"desc": "Heat Orchestration API"
160
"type": "cloudformation",
161
"desc": "Heat CloudFormation API"
168
Dynamically generate a map of resources that will be managed for a single
171
resource_map = deepcopy(BASE_RESOURCE_MAP)
173
if os.path.exists('/etc/apache2/conf-available'):
174
resource_map.pop(APACHE_CONF)
176
resource_map.pop(APACHE_24_CONF)
180
def register_configs():
181
release = os_release('keystone')
182
configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
183
openstack_release=release)
184
for cfg, rscs in resource_map().iteritems():
185
configs.register(cfg, rscs['contexts'])
190
return OrderedDict([(cfg, v['services'])
191
for cfg, v in resource_map().iteritems()
195
def determine_ports():
196
'''Assemble a list of API ports for services we are managing'''
197
ports = [config('admin-port'), config('service-port')]
198
return list(set(ports))
201
def api_port(service):
202
return API_PORTS[service]
205
def determine_packages():
206
# currently all packages match service names
207
packages = [] + BASE_PACKAGES
208
for k, v in resource_map().iteritems():
209
packages.extend(v['services'])
210
return list(set(packages))
213
def save_script_rc():
214
env_vars = {'OPENSTACK_SERVICE_KEYSTONE': 'keystone',
215
'OPENSTACK_PORT_ADMIN': determine_api_port(
216
api_port('keystone-admin')),
217
'OPENSTACK_PORT_PUBLIC': determine_api_port(
218
api_port('keystone-public'))}
219
_save_script_rc(**env_vars)
222
def do_openstack_upgrade(configs):
223
new_src = config('openstack-origin')
224
new_os_rel = get_os_codename_install_source(new_src)
225
log('Performing OpenStack upgrade to %s.' % (new_os_rel))
227
configure_installation_source(new_src)
231
'--option', 'Dpkg::Options::=--force-confnew',
232
'--option', 'Dpkg::Options::=--force-confdef',
235
apt_install(packages=determine_packages(), options=dpkg_opts, fatal=True)
237
# set CONFIGS to load templates from new release and regenerate config
238
configs.set_release(openstack_release=new_os_rel)
241
if eligible_leader(CLUSTER_RES):
245
def migrate_database():
246
'''Runs keystone-manage to initialize a new database or migrate existing'''
247
log('Migrating the keystone database.', level=INFO)
248
service_stop('keystone')
249
cmd = ['keystone-manage', 'db_sync']
250
subprocess.check_output(cmd)
251
service_start('keystone')
86
257
def get_local_endpoint():
87
258
""" Returns the URL for the local end-point bypassing haproxy/ssl """
88
259
local_endpoint = 'http://localhost:{}/v2.0/'.format(
89
cluster.determine_api_port(utils.config_get('admin-port'))
260
determine_api_port(api_port('keystone-admin'))
91
262
return local_endpoint
94
def set_admin_token(admin_token):
265
def set_admin_token(admin_token='None'):
95
266
"""Set admin token according to deployment config or use a randomly
96
267
generated token if none is specified (default).
98
269
if admin_token != 'None':
99
utils.juju_log('INFO',
100
'Configuring Keystone to use'
101
' a pre-configured admin token.')
270
log('Configuring Keystone to use a pre-configured admin token.')
102
271
token = admin_token
104
utils.juju_log('INFO',
105
'Configuring Keystone to use a random admin token.')
106
if os.path.isfile(stored_token):
273
log('Configuring Keystone to use a random admin token.')
274
if os.path.isfile(STORED_TOKEN):
107
275
msg = 'Loading a previously generated' \
108
' admin token from %s' % stored_token
109
utils.juju_log('INFO', msg)
110
f = open(stored_token, 'r')
276
' admin token from %s' % STORED_TOKEN
278
f = open(STORED_TOKEN, 'r')
111
279
token = f.read().strip()
114
token = execute('pwgen -c 32 1', die=True)[0].strip()
115
out = open(stored_token, 'w')
282
cmd = ['pwgen', '-c', '32', '1']
283
token = str(subprocess.check_output(cmd)).strip()
284
out = open(STORED_TOKEN, 'w')
116
285
out.write('%s\n' % token)
118
update_config_block('DEFAULT', admin_token=token)
121
290
def get_admin_token():
122
291
"""Temporary utility to grab the admin token as configured in
125
with open(keystone_conf, 'r') as f:
294
with open(KEYSTONE_CONF, 'r') as f:
126
295
for l in f.readlines():
127
296
if l.split(' ')[0] == 'admin_token':
129
298
return l.split('=')[1].strip()
131
300
error_out('Could not parse admin_token line from %s' %
133
error_out('Could not find admin_token line in %s' % keystone_conf)
136
# Track all updated config settings.
137
_config_dirty = [False]
140
return True in _config_dirty
142
def update_config_block(section, **kwargs):
143
""" Updates keystone.conf blocks given kwargs.
144
Update a config setting in a specific setting of a config
145
file (/etc/keystone/keystone.conf, by default)
148
conf_file = kwargs['file']
151
conf_file = keystone_conf
152
config = ConfigParser.RawConfigParser()
153
config.read(conf_file)
155
if section != 'DEFAULT' and not config.has_section(section):
156
config.add_section(section)
157
_config_dirty[0] = True
159
for k, v in kwargs.iteritems():
161
cur = config.get(section, k)
163
_config_dirty[0] = True
164
except (ConfigParser.NoSectionError,
165
ConfigParser.NoOptionError):
166
_config_dirty[0] = True
167
config.set(section, k, v)
168
with open(conf_file, 'wb') as out:
302
error_out('Could not find admin_token line in %s' % KEYSTONE_CONF)
172
305
def create_service_entry(service_name, service_type, service_desc, owner=None):
337
446
create_tenant("admin")
338
create_tenant(config["service-tenant"])
447
create_tenant(config("service-tenant"))
341
if config["admin-password"] != "None":
342
passwd = config["admin-password"]
343
elif os.path.isfile(stored_passwd):
344
utils.juju_log('INFO', "Loading stored passwd from %s" % stored_passwd)
345
passwd = open(stored_passwd, 'r').readline().strip('\n')
450
if config("admin-password") != "None":
451
passwd = config("admin-password")
452
elif os.path.isfile(STORED_PASSWD):
453
log("Loading stored passwd from %s" % STORED_PASSWD)
454
passwd = open(STORED_PASSWD, 'r').readline().strip('\n')
347
utils.juju_log('INFO', "Generating new passwd for user: %s" % \
348
config["admin-user"])
349
passwd = execute("pwgen -c 16 1", die=True)[0]
350
open(stored_passwd, 'w+').writelines("%s\n" % passwd)
456
log("Generating new passwd for user: %s" %
457
config("admin-user"))
458
cmd = ['pwgen', '-c', '16', '1']
459
passwd = str(subprocess.check_output(cmd)).strip()
460
open(STORED_PASSWD, 'w+').writelines("%s\n" % passwd)
352
create_user(config['admin-user'], passwd, tenant='admin')
353
update_user_password(config['admin-user'], passwd)
354
create_role(config['admin-role'], config['admin-user'], 'admin')
462
create_user(config('admin-user'), passwd, tenant='admin')
463
update_user_password(config('admin-user'), passwd)
464
create_role(config('admin-role'), config('admin-user'), 'admin')
355
465
# TODO(adam_g): The following roles are likely not needed since redux merge
356
create_role("KeystoneAdmin", config["admin-user"], 'admin')
357
create_role("KeystoneServiceAdmin", config["admin-user"], 'admin')
466
create_role("KeystoneAdmin", config("admin-user"), 'admin')
467
create_role("KeystoneServiceAdmin", config("admin-user"), 'admin')
358
468
create_service_entry("keystone", "identity", "Keystone Identity Service")
360
if cluster.is_clustered():
361
utils.juju_log('INFO', "Creating endpoint for clustered configuration")
362
service_host = auth_host = config["vip"]
471
log("Creating endpoint for clustered configuration")
472
service_host = auth_host = config("vip")
364
utils.juju_log('INFO', "Creating standard endpoint")
365
service_host = auth_host = config["hostname"]
474
log("Creating standard endpoint")
475
service_host = auth_host = unit_private_ip()
367
for region in config['region'].split():
477
for region in config('region').split():
368
478
create_keystone_endpoint(service_host=service_host,
369
service_port=config["service-port"],
479
service_port=config("service-port"),
370
480
auth_host=auth_host,
371
auth_port=config["admin-port"],
481
auth_port=config("admin-port"),
375
485
def create_keystone_endpoint(service_host, service_port,
376
486
auth_host, auth_port, region):
377
public_url = "http://%s:%s/v2.0" % (service_host, service_port)
378
admin_url = "http://%s:%s/v2.0" % (auth_host, auth_port)
379
internal_url = "http://%s:%s/v2.0" % (service_host, service_port)
489
log("Setting https keystone endpoint")
491
public_url = "%s://%s:%s/v2.0" % (proto, service_host, service_port)
492
admin_url = "%s://%s:%s/v2.0" % (proto, auth_host, auth_port)
493
internal_url = "%s://%s:%s/v2.0" % (proto, service_host, service_port)
380
494
create_endpoint_template(region, "keystone", public_url,
381
495
admin_url, internal_url)
428
def configure_pki_tokens(config):
429
'''Configure PKI token signing, if enabled.'''
430
if config['enable-pki'] not in ['True', 'true']:
431
update_config_block('signing', token_format='UUID')
433
utils.juju_log('INFO', 'TODO: PKI Support, setting to UUID for now.')
434
update_config_block('signing', token_format='UUID')
437
def do_openstack_upgrade(install_src, packages):
438
'''Upgrade packages from a given install src.'''
440
config = config_get()
441
old_vers = get_os_codename_package('keystone')
442
new_vers = get_os_codename_install_source(install_src)
444
utils.juju_log('INFO',
445
"Beginning Keystone upgrade: %s -> %s" % \
446
(old_vers, new_vers))
448
# Backup previous config.
449
utils.juju_log('INFO', "Backing up contents of /etc/keystone.")
450
stamp = time.strftime('%Y%m%d%H%M')
451
cmd = 'tar -pcf /var/lib/juju/keystone-backup-%s.tar /etc/keystone' % stamp
452
execute(cmd, die=True, echo=True)
454
configure_installation_source(install_src)
455
execute('apt-get update', die=True, echo=True)
456
os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
457
cmd = 'apt-get --option Dpkg::Options::=--force-confnew -y '\
459
execute(cmd, echo=True, die=True)
461
# we have new, fresh config files that need updating.
462
# set the admin token, which is still stored in config.
463
set_admin_token(config['admin-token'])
465
# set the sql connection string if a shared-db relation is found.
466
ids = utils.relation_ids('shared-db')
470
for unit in utils.relation_list(rid):
471
utils.juju_log('INFO',
472
'Configuring new keystone.conf for '
473
'database access on existing database'
474
' relation to %s' % unit)
475
relation_data = utils.relation_get_dict(relation_id=rid,
478
update_config_block('sql', connection="mysql://%s:%s@%s/%s" %
479
(config["database-user"],
480
relation_data["password"],
481
relation_data["private-address"],
484
utils.stop('keystone')
485
if (cluster.eligible_leader(CLUSTER_RES)):
486
utils.juju_log('INFO',
487
'Running database migrations for %s' % new_vers)
488
execute('keystone-manage db_sync', echo=True, die=True)
490
utils.juju_log('INFO',
491
'Not cluster leader; snoozing whilst'
492
' leader upgrades DB')
494
utils.start('keystone')
496
utils.juju_log('INFO',
497
'Completed Keystone upgrade: '
498
'%s -> %s' % (old_vers, new_vers))
501
542
def synchronize_service_credentials():
503
544
Broadcast service credentials to peers or consume those that have been
504
545
broadcasted by peer, depending on hook context.
506
if (not cluster.eligible_leader(CLUSTER_RES) or
507
not os.path.isfile(SERVICE_PASSWD_PATH)):
547
if (not eligible_leader(CLUSTER_RES) or
548
not os.path.isfile(SERVICE_PASSWD_PATH)):
509
utils.juju_log('INFO', 'Synchronizing service passwords to all peers.')
510
unison.sync_to_peers(peer_interface='cluster',
511
paths=[SERVICE_PASSWD_PATH], user=SSH_USER,
550
log('Synchronizing service passwords to all peers.')
552
unison.sync_to_peers(peer_interface='cluster',
553
paths=[SERVICE_PASSWD_PATH], user=SSH_USER,
555
if config('https-service-endpoints') in ['True', 'true']:
556
unison.sync_to_peers(peer_interface='cluster',
557
paths=[SSL_DIR], user=SSH_USER, verbose=True)
527
572
ca_dir=os.path.join(SSL_DIR,
528
573
'%s_intermediate_ca' % d_name),
529
574
root_ca_dir=os.path.join(SSL_DIR,
530
'%s_root_ca' % d_name))
575
'%s_root_ca' % d_name))
531
576
# SSL_DIR is synchronized via all peers over unison+ssh, need
532
577
# to ensure permissions.
533
execute('chown -R %s.%s %s' % (user, group, SSL_DIR))
534
execute('chmod -R g+rwx %s' % SSL_DIR)
578
subprocess.check_output(['chown', '-R', '%s.%s' % (user, group),
580
subprocess.check_output(['chmod', '-R', 'g+rwx', '%s' % SSL_DIR])
540
if (utils.config_get('https-service-endpoints') in ["yes", "true", "True"]
585
def relation_list(rid):
590
result = str(subprocess.check_output(cmd)).split()
597
def add_service_to_keystone(relation_id=None, remote_unit=None):
598
settings = relation_get(rid=relation_id, unit=remote_unit)
599
# the minimum settings needed per endpoint
600
single = set(['service', 'region', 'public_url', 'admin_url',
602
if single.issubset(settings):
603
# other end of relation advertised only one endpoint
604
if 'None' in [v for k, v in settings.iteritems()]:
605
# Some backend services advertise no endpoint but require a
606
# hook execution to update auth strategy.
608
# Check if clustered and use vip + haproxy ports if so
610
relation_data["auth_host"] = config('vip')
611
relation_data["service_host"] = config('vip')
613
relation_data["auth_host"] = unit_private_ip()
614
relation_data["service_host"] = unit_private_ip()
616
relation_data["auth_protocol"] = "https"
617
relation_data["service_protocol"] = "https"
619
relation_data["auth_protocol"] = "http"
620
relation_data["service_protocol"] = "http"
621
relation_data["auth_port"] = config('admin-port')
622
relation_data["service_port"] = config('service-port')
623
if config('https-service-endpoints') in ['True', 'true']:
624
# Pass CA cert as client will need it to
625
# verify https connections
626
ca = get_ca(user=SSH_USER)
627
ca_bundle = ca.get_ca_bundle()
628
relation_data['https_keystone'] = 'True'
629
relation_data['ca_cert'] = b64encode(ca_bundle)
630
# Allow the remote service to request creation of any additional
631
# roles. Currently used by Horizon
632
for role in get_requested_roles(settings):
633
log("Creating requested role: %s" % role)
635
relation_set(relation_id=relation_id,
639
ensure_valid_service(settings['service'])
640
add_endpoint(region=settings['region'],
641
service=settings['service'],
642
publicurl=settings['public_url'],
643
adminurl=settings['admin_url'],
644
internalurl=settings['internal_url'])
645
service_username = settings['service']
646
https_cn = urlparse.urlparse(settings['internal_url'])
647
https_cn = https_cn.hostname
649
# assemble multiple endpoints from relation data. service name
650
# should be prepended to setting name, ie:
651
# realtion-set ec2_service=$foo ec2_region=$foo ec2_public_url=$foo
652
# relation-set nova_service=$foo nova_region=$foo nova_public_url=$foo
653
# Results in a dict that looks like:
666
for k, v in settings.iteritems():
668
x = '_'.join(k.split('_')[1:])
669
if ep not in endpoints:
675
# weed out any unrelated relation stuff Juju might have added
676
# by ensuring each possible endpiont has appropriate fields
677
# ['service', 'region', 'public_url', 'admin_url', 'internal_url']
678
if single.issubset(endpoints[ep]):
680
ensure_valid_service(ep['service'])
681
add_endpoint(region=ep['region'], service=ep['service'],
682
publicurl=ep['public_url'],
683
adminurl=ep['admin_url'],
684
internalurl=ep['internal_url'])
685
services.append(ep['service'])
687
https_cn = urlparse.urlparse(ep['internal_url'])
688
https_cn = https_cn.hostname
689
service_username = '_'.join(services)
691
if 'None' in [v for k, v in settings.iteritems()]:
694
if not service_username:
697
token = get_admin_token()
698
log("Creating service credentials for '%s'" % service_username)
700
service_password = get_service_password(service_username)
701
create_user(service_username, service_password, config('service-tenant'))
702
grant_role(service_username, config('admin-role'),
703
config('service-tenant'))
705
# Allow the remote service to request creation of any additional roles.
706
# Currently used by Swift and Ceilometer.
707
for role in get_requested_roles(settings):
708
log("Creating requested role: %s" % role)
709
create_role(role, service_username,
710
config('service-tenant'))
712
# As of https://review.openstack.org/#change,4675, all nodes hosting
713
# an endpoint(s) needs a service username and password assigned to
714
# the service tenant and granted admin role.
715
# note: config('service-tenant') is created in utils.ensure_initial_admin()
716
# we return a token, information about our API endpoints, and the generated
717
# service credentials
719
"admin_token": token,
720
"service_host": unit_private_ip(),
721
"service_port": config("service-port"),
722
"auth_host": unit_private_ip(),
723
"auth_port": config("admin-port"),
724
"service_username": service_username,
725
"service_password": service_password,
726
"service_tenant": config('service-tenant'),
727
"https_keystone": "False",
733
# Check if clustered and use vip + haproxy ports if so
735
relation_data["auth_host"] = config('vip')
736
relation_data["service_host"] = config('vip')
738
relation_data["auth_protocol"] = "https"
739
relation_data["service_protocol"] = "https"
741
relation_data["auth_protocol"] = "http"
742
relation_data["service_protocol"] = "http"
743
# generate or get a new cert/key for service if set to manage certs.
744
if config('https-service-endpoints') in ['True', 'true']:
745
ca = get_ca(user=SSH_USER)
746
cert, key = ca.get_cert_and_key(common_name=https_cn)
747
ca_bundle = ca.get_ca_bundle()
748
relation_data['ssl_cert'] = b64encode(cert)
749
relation_data['ssl_key'] = b64encode(key)
750
relation_data['ca_cert'] = b64encode(ca_bundle)
751
relation_data['https_keystone'] = 'True'
752
relation_set(relation_id=relation_id,
756
def ensure_valid_service(service):
757
if service not in valid_services.keys():
758
log("Invalid service requested: '%s'" % service)
759
relation_set(admin_token=-1)
763
def add_endpoint(region, service, publicurl, adminurl, internalurl):
764
desc = valid_services[service]["desc"]
765
service_type = valid_services[service]["type"]
766
create_service_entry(service, service_type, desc)
767
create_endpoint_template(region=region, service=service,
770
internalurl=internalurl)
773
def get_requested_roles(settings):
774
''' Retrieve any valid requested_roles from dict settings '''
775
if ('requested_roles' in settings and
776
settings['requested_roles'] not in ['None', None]):
777
return settings['requested_roles'].split(',')