529
559
def ensure_initial_admin(config):
530
""" Ensures the minimum admin stuff exists in whatever database we're
560
# Allow retry on fail since leader may not be ready yet.
561
# NOTE(hopem): ks client may not be installed at module import time so we
562
# use this wrapped approach instead.
563
from keystoneclient.apiclient.exceptions import InternalServerError
565
@retry_on_exception(3, base_delay=3, exc_type=InternalServerError)
566
def _ensure_initial_admin(config):
567
"""Ensures the minimum admin stuff exists in whatever database we're
532
570
This and the helper functions it calls are meant to be idempotent and
533
571
run during install as well as during db-changed. This will maintain
534
572
the admin tenant, user, role, service entry and endpoint across every
535
573
datastore we might use.
536
575
TODO: Possibly migrate data from one backend to another after it
539
create_tenant("admin")
540
create_tenant(config("service-tenant"))
541
# User is managed by ldap backend when using ldap identity
542
if not (config('identity-backend') == 'ldap' and config('ldap-readonly')):
543
passwd = get_admin_passwd()
545
create_user(config('admin-user'), passwd, tenant='admin')
546
update_user_password(config('admin-user'), passwd)
547
create_role(config('admin-role'), config('admin-user'), 'admin')
548
create_service_entry("keystone", "identity", "Keystone Identity Service")
550
for region in config('region').split():
551
create_keystone_endpoint(public_ip=resolve_address(PUBLIC),
552
service_port=config("service-port"),
553
internal_ip=resolve_address(INTERNAL),
554
admin_ip=resolve_address(ADMIN),
555
auth_port=config("admin-port"),
578
create_tenant("admin")
579
create_tenant(config("service-tenant"))
580
# User is managed by ldap backend when using ldap identity
581
if not (config('identity-backend') ==
582
'ldap' and config('ldap-readonly')):
583
passwd = get_admin_passwd()
585
create_user(config('admin-user'), passwd, tenant='admin')
586
update_user_password(config('admin-user'), passwd)
587
create_role(config('admin-role'), config('admin-user'),
589
create_service_entry("keystone", "identity",
590
"Keystone Identity Service")
592
for region in config('region').split():
593
create_keystone_endpoint(public_ip=resolve_address(PUBLIC),
594
service_port=config("service-port"),
595
internal_ip=resolve_address(INTERNAL),
596
admin_ip=resolve_address(ADMIN),
597
auth_port=config("admin-port"),
600
return _ensure_initial_admin(config)
559
603
def endpoint_url(ip, port):
624
def synchronize_ca():
626
Broadcast service credentials to peers or consume those that have been
627
broadcasted by peer, depending on hook context.
629
if not eligible_leader(CLUSTER_RES):
631
log('Synchronizing CA to all peers.')
633
if config('https-service-endpoints') in ['True', 'true']:
634
unison.sync_to_peers(peer_interface='cluster',
635
paths=[SSL_DIR], user=SSH_USER, verbose=True)
668
def ensure_permissions(path, user=None, group=None, perms=None):
669
"""Set chownand chmod for path
671
Note that -1 for uid or gid result in no change.
674
uid = pwd.getpwnam(user).pw_uid
679
gid = grp.getgrnam(group).gr_gid
683
os.chown(path, uid, gid)
686
os.chmod(path, perms)
689
def check_peer_actions():
690
"""Honour service action requests from sync master.
692
Check for service action request flags, perform the action then delete the
695
restart = relation_get(attribute='restart-services-trigger')
696
if restart and os.path.isdir(SYNC_FLAGS_DIR):
697
for flagfile in glob.glob(os.path.join(SYNC_FLAGS_DIR, '*')):
698
flag = os.path.basename(flagfile)
699
key = re.compile("^(.+)?\.(.+)?\.(.+)")
700
res = re.search(key, flag)
702
source = res.group(1)
703
service = res.group(2)
704
action = res.group(3)
706
key = re.compile("^(.+)?\.(.+)?")
707
res = re.search(key, flag)
708
source = res.group(1)
709
action = res.group(2)
711
# Don't execute actions requested by this unit.
712
if local_unit().replace('.', '-') != source:
713
if action == 'restart':
714
log("Running action='%s' on service '%s'" %
715
(action, service), level=DEBUG)
716
service_restart(service)
717
elif action == 'start':
718
log("Running action='%s' on service '%s'" %
719
(action, service), level=DEBUG)
720
service_start(service)
721
elif action == 'stop':
722
log("Running action='%s' on service '%s'" %
723
(action, service), level=DEBUG)
724
service_stop(service)
725
elif action == 'update-ca-certificates':
726
log("Running %s" % (action), level=DEBUG)
727
subprocess.check_call(['update-ca-certificates'])
729
log("Unknown action flag=%s" % (flag), level=WARNING)
737
def create_peer_service_actions(action, services):
738
"""Mark remote services for action.
740
Default action is restart. These action will be picked up by peer units
741
e.g. we may need to restart services on peer units after certs have been
744
for service in services:
745
flagfile = os.path.join(SYNC_FLAGS_DIR, '%s.%s.%s' %
746
(local_unit().replace('/', '-'),
747
service.strip(), action))
748
log("Creating action %s" % (flagfile), level=DEBUG)
749
write_file(flagfile, content='', owner=SSH_USER, group='keystone',
753
def create_peer_actions(actions):
754
for action in actions:
755
action = "%s.%s" % (local_unit().replace('/', '-'), action)
756
flagfile = os.path.join(SYNC_FLAGS_DIR, action)
757
log("Creating action %s" % (flagfile), level=DEBUG)
758
write_file(flagfile, content='', owner=SSH_USER, group='keystone',
762
@retry_on_exception(3, base_delay=2, exc_type=subprocess.CalledProcessError)
763
def unison_sync(paths_to_sync):
764
"""Do unison sync and retry a few times if it fails since peers may not be
767
log('Synchronizing CA (%s) to all peers.' % (', '.join(paths_to_sync)),
769
keystone_gid = grp.getgrnam('keystone').gr_gid
770
unison.sync_to_peers(peer_interface='cluster', paths=paths_to_sync,
771
user=SSH_USER, verbose=True, gid=keystone_gid,
775
def get_ssl_sync_request_units():
776
"""Get list of units that have requested to be synced.
778
NOTE: this must be called from cluster relation context.
781
for unit in related_units():
782
settings = relation_get(unit=unit) or {}
783
rkeys = settings.keys()
784
key = re.compile("^ssl-sync-required-(.+)")
786
res = re.search(key, rkey)
788
units.append(res.group(1))
793
def is_ssl_cert_master():
794
"""Return True if this unit is ssl cert master."""
796
for rid in relation_ids('cluster'):
797
master = relation_get(attribute='ssl-cert-master', rid=rid,
800
return master == local_unit()
803
def ensure_ssl_cert_master(use_oldest_peer=False):
804
"""Ensure that an ssl cert master has been elected.
806
Normally the cluster leader will take control but we allow for this to be
807
ignored since this could be called before the cluster is ready.
809
# Don't do anything if we are not in ssl/https mode
810
if not (is_str_true(config('use-https')) or
811
is_str_true(config('https-service-endpoints'))):
812
log("SSL/HTTPS is NOT enabled", level=DEBUG)
816
log("Not syncing certs since there are no peer units.", level=INFO)
820
elect = oldest_peer(peer_units())
822
elect = is_elected_leader(CLUSTER_RES)
826
for rid in relation_ids('cluster'):
827
for unit in related_units(rid):
828
m = relation_get(rid=rid, unit=unit,
829
attribute='ssl-cert-master')
833
# We expect all peers to echo this setting
834
if not masters or 'unknown' in masters:
835
log("Notifying peers this unit is ssl-cert-master", level=INFO)
836
for rid in relation_ids('cluster'):
837
settings = {'ssl-cert-master': local_unit()}
838
relation_set(relation_id=rid, relation_settings=settings)
840
# Return now and wait for cluster-relation-changed (peer_echo) for
843
elif len(set(masters)) != 1 and local_unit() not in masters:
844
log("Did not get concensus from peers on who is master (%s) - "
845
"waiting for current master to release before self-electing" %
846
(masters), level=INFO)
849
if not is_ssl_cert_master():
850
log("Not ssl cert master - skipping sync", level=INFO)
856
def synchronize_ca(fatal=False):
857
"""Broadcast service credentials to peers.
859
By default a failure to sync is fatal and will result in a raised
862
This function uses a relation setting 'ssl-cert-master' to get some
863
leader stickiness while synchronisation is being carried out. This ensures
864
that the last host to create and broadcast cetificates has the option to
865
complete actions before electing the new leader as sync master.
867
paths_to_sync = [SYNC_FLAGS_DIR]
869
if is_str_true(config('https-service-endpoints')):
870
log("Syncing all endpoint certs since https-service-endpoints=True",
872
paths_to_sync.append(SSL_DIR)
873
paths_to_sync.append(APACHE_SSL_DIR)
874
paths_to_sync.append(CA_CERT_PATH)
875
elif is_str_true(config('use-https')):
876
log("Syncing keystone-endpoint certs since use-https=True",
878
paths_to_sync.append(APACHE_SSL_DIR)
879
paths_to_sync.append(CA_CERT_PATH)
881
if not paths_to_sync:
882
log("Nothing to sync - skipping", level=DEBUG)
885
if not os.path.isdir(SYNC_FLAGS_DIR):
886
mkdir(SYNC_FLAGS_DIR, SSH_USER, 'keystone', 0o775)
888
# We need to restart peer apache services to ensure they have picked up
890
create_peer_service_actions('restart', ['apache2'])
891
create_peer_actions(['update-ca-certificates'])
893
# Format here needs to match that used when peers request sync
894
synced_units = [unit.replace('/', '-') for unit in peer_units()]
898
hash1 = hashlib.sha256()
899
for path in paths_to_sync:
900
update_hash_from_path(hash1, path)
903
unison_sync(paths_to_sync)
908
log("Sync failed but fatal=False", level=INFO)
911
hash2 = hashlib.sha256()
912
for path in paths_to_sync:
913
update_hash_from_path(hash2, path)
915
# Detect whether someone else has synced to this unit while we did our
917
if hash1.hexdigest() != hash2.hexdigest():
920
log("SSL dir contents changed during sync - retrying unison "
921
"sync %s more times" % (retries), level=WARNING)
923
log("SSL dir contents changed during sync - retries failed",
929
hash = hash1.hexdigest()
930
log("Sending restart-services-trigger=%s to all peers" % (hash),
933
log("Sync complete", level=DEBUG)
934
return {'restart-services-trigger': hash,
935
'ssl-synced-units': json.dumps(synced_units)}
938
def update_hash_from_path(hash, path, recurse_depth=10):
939
"""Recurse through path and update the provided hash for every file found.
941
if not recurse_depth:
942
log("Max recursion depth (%s) reached for update_hash_from_path() at "
943
"path='%s' - not going any deeper" % (recurse_depth, path),
947
for p in glob.glob("%s/*" % path):
949
update_hash_from_path(hash, p, recurse_depth=recurse_depth - 1)
951
with open(p, 'r') as fd:
952
hash.update(fd.read())
955
def synchronize_ca_if_changed(force=False, fatal=False):
956
"""Decorator to perform ssl cert sync if decorated function modifies them
959
If force is True a sync is done regardless.
961
def inner_synchronize_ca_if_changed1(f):
962
def inner_synchronize_ca_if_changed2(*args, **kwargs):
963
# Only sync master can do sync. Ensure (a) we are not nested and
964
# (b) a master is elected and we are it.
965
acquired = SSL_SYNC_SEMAPHORE.acquire(blocking=0)
968
log("Nested sync - ignoring", level=DEBUG)
969
return f(*args, **kwargs)
971
if not ensure_ssl_cert_master():
972
log("Not leader - ignoring sync", level=DEBUG)
973
return f(*args, **kwargs)
977
ssl_dirs = [SSL_DIR, APACHE_SSL_DIR, CA_CERT_PATH]
979
hash1 = hashlib.sha256()
980
for path in ssl_dirs:
981
update_hash_from_path(hash1, path)
983
ret = f(*args, **kwargs)
985
hash2 = hashlib.sha256()
986
for path in ssl_dirs:
987
update_hash_from_path(hash2, path)
989
if hash1.hexdigest() != hash2.hexdigest():
990
log("SSL certs have changed - syncing peers",
992
peer_settings = synchronize_ca(fatal=fatal)
994
log("SSL certs have not changed - skipping sync",
997
ret = f(*args, **kwargs)
998
log("Doing forced ssl cert sync", level=DEBUG)
999
peer_settings = synchronize_ca(fatal=fatal)
1001
# If we are the sync master but not leader, ensure we have
1002
# relinquished master status.
1003
if not is_elected_leader(CLUSTER_RES):
1004
log("Re-electing ssl cert master.", level=INFO)
1005
peer_settings['ssl-cert-master'] = 'unknown'
1008
for rid in relation_ids('cluster'):
1009
relation_set(relation_id=rid,
1010
relation_settings=peer_settings)
1014
SSL_SYNC_SEMAPHORE.release()
1016
return inner_synchronize_ca_if_changed2
1018
return inner_synchronize_ca_if_changed1
640
1021
def get_ca(user='keystone', group='keystone'):