#!/usr/bin/python -tt # Syncs the valid CA-signed certificates from certmaster to all known # hosts via func. To be called during the post-sign hook to copy new # certificates and from the post-clean hook in order to purge stale # certificates. Requires 'sync_certs' to be set in certmaster.conf. import os import sys import sha import xmlrpclib from glob import glob from time import sleep from certmaster import certmaster as certmaster from func.overlord.client import Client from func.CommonErrors import Func_Client_Exception import func.jobthing as jobthing def syncable(cert_list): """ Calls out to known hosts to find out who is configured for peering. Returns a list of hostnames who support peering. """ try: fc = Client('*', async=True, nforks=len(cert_list)) except Func_Client_Exception: # we are either: # - signing the first minion # - cleaning the only minion # so there's nothing to hit. This shouldn't happen # when we get called from the 'post-fetch' trigger # (future work) return None # Only wait for a few seconds. Assume anything that doesn't get # back by then is a lost cause. Don't want this trigger to spin # too long. ticks = 0 return_code = jobthing.JOB_ID_RUNNING results = None job_id = fc.certmastermod.peering_enabled() while return_code != jobthing.JOB_ID_FINISHED and ticks < 3: sleep(1) (return_code, results) = fc.job_status(job_id) ticks += 1 hosts = [] for host, result in results.iteritems(): if result == True: hosts.append(host) return hosts def remote_peers(hosts): """ Calls out to hosts to collect peer information """ fc = Client(';'.join(hosts)) return fc.certmastermod.known_peers() def local_certs(): """ Returns (hostname, sha1) hash of local certs """ globby = '*.%s' % cm.cfg.cert_extension globby = os.path.join(cm.cfg.certroot, globby) files = glob(globby) results = [] for f in files: hostname = os.path.basename(f).replace('.' + cm.cfg.cert_extension, '') digest = checksum(f) results.append([hostname, digest]) return results def checksum(f): thissum = sha.new() if os.path.exists(f): fo = open(f, 'r') data = fo.read() fo.close() thissum.update(data) return thissum.hexdigest() def remove_stale_certs(local, remote): """ For each cert on each remote host, make sure it exists locally. If not then it has been cleaned locally and needs unlinked remotely. """ local = [foo[0] for foo in local] # don't care about checksums for host, peers in remote.iteritems(): fc = Client(host) die = [] for peer in peers: if peer[0] not in local: die.append(peer[0]) if die != []: fc.certmastermod.remove_peer_certs(die) def copy_updated_certs(local, remote): """ For each local cert, make sure it exists on the remote with the correct hash. If not, copy it over! """ for host, peers in remote.iteritems(): fc = Client(host) for cert in local: if cert not in peers: cert_name = '%s.%s' % (cert[0], cm.cfg.cert_extension) full_path = os.path.join(cm.cfg.certroot, cert_name) fd = open(full_path) certblob = fd.read() fd.close() fc.certmastermod.copy_peer_cert(cert[0], xmlrpclib.Binary(certblob)) def main(): forced = False try: if sys.argv[1] in ['-f', '--force']: forced = True except IndexError: pass if not cm.cfg.sync_certs and not forced: sys.exit(0) certs = glob(os.path.join(cm.cfg.certroot, '*.%s' % cm.cfg.cert_extension)) hosts = syncable(certs) if not hosts: return 0 remote = remote_peers(hosts) local = local_certs() remove_stale_certs(local, remote) copy_updated_certs(local, remote) if __name__ == "__main__": cm = certmaster.CertMaster() main()