From cd7c424c4870bdf1b93c43b9283b25fb803361eb Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Fri, 13 Mar 2009 15:39:37 -0400 Subject: [PATCH] Minion-to-minion support, certmaster half. --- certmaster.spec | 2 + certmaster/certmaster.py | 7 ++ certmaster/commonconfig.py | 4 +- scripts/certmaster-sync | 139 +++++++++++++++++++++++++++++++++++++ setup.py | 2 +- 5 files changed, 152 insertions(+), 2 deletions(-) create mode 100644 scripts/certmaster-sync diff --git a/certmaster.spec b/certmaster.spec index e03ec78..f7fa30c 100644 --- a/certmaster.spec +++ b/certmaster.spec @@ -74,6 +74,7 @@ rm -fr $RPM_BUILD_ROOT %{_bindir}/certmaster %{_bindir}/certmaster-request %{_bindir}/certmaster-ca +%{_bindir}/certmaster-sync /etc/init.d/certmaster %dir %{_sysconfdir}/%{name} %dir %{_sysconfdir}/%{name}/minion-acl.d/ @@ -85,6 +86,7 @@ rm -fr $RPM_BUILD_ROOT %{python_sitelib}/certmaster/*.py* %dir /var/log/certmaster %dir /var/lib/certmaster +%dir /var/lib/certmaster/peers %dir /var/lib/certmaster/triggers/sign/ %dir /var/lib/certmaster/triggers/sign/pre %dir /var/lib/certmaster/triggers/sign/post diff --git a/certmaster/certmaster.py b/certmaster/certmaster.py index 506a029..58cb50b 100644 --- a/certmaster/certmaster.py +++ b/certmaster/certmaster.py @@ -276,6 +276,13 @@ class CertMaster(object): return signed_certs + def get_peer_certs(self): + """ + Returns a list of all certs under peerroot + """ + myglob = os.path.join(self.cfg.peerroot, '*.%s' % self.cfg.cert_extension) + return glob.glob(myglob) + # return a list of the cert hash string we use to identify systems def get_cert_hashes(self, hostglobs=None): certglob = "%s/*.cert" % (self.cfg.certroot) diff --git a/certmaster/commonconfig.py b/certmaster/commonconfig.py index 4be491e..5d0361e 100644 --- a/certmaster/commonconfig.py +++ b/certmaster/commonconfig.py @@ -26,10 +26,12 @@ class CMConfig(BaseConfig): csrroot = Option('/var/lib/certmaster/certmaster/csrs') cert_extension = Option('cert') autosign = BoolOption(False) + sync_certs = BoolOption(False) + peering = BoolOption(True) + peerroot = Option('/var/lib/certmaster/peers') class MinionConfig(BaseConfig): log_level = Option('INFO') certmaster = Option('certmaster') certmaster_port = IntOption(51235) cert_dir = Option('/etc/pki/certmaster') - diff --git a/scripts/certmaster-sync b/scripts/certmaster-sync new file mode 100644 index 0000000..bd27af5 --- /dev/null +++ b/scripts/certmaster-sync @@ -0,0 +1,139 @@ +#!/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() diff --git a/setup.py b/setup.py index c647170..8cf70eb 100644 --- a/setup.py +++ b/setup.py @@ -31,7 +31,7 @@ if __name__ == "__main__": license = "GPL", scripts = [ "scripts/certmaster", "scripts/certmaster-ca", - "scripts/certmaster-request", + "scripts/certmaster-request", "scripts/certmaster-sync", ], # package_data = { '' : ['*.*'] }, package_dir = {"%s" % NAME: "%s" % NAME -- 2.39.2