Minion-to-minion support, certmaster half.
[certmaster.git] / scripts / certmaster-sync
1 #!/usr/bin/python -tt
2
3 # Syncs the valid CA-signed certificates from certmaster to all known
4 # hosts via func. To be called during the post-sign hook to copy new
5 # certificates and from the post-clean hook in order to purge stale
6 # certificates. Requires 'sync_certs' to be set in certmaster.conf.
7
8 import os
9 import sys
10 import sha
11 import xmlrpclib
12 from glob import glob
13 from time import sleep
14 from certmaster import certmaster as certmaster
15 from func.overlord.client import Client
16 from func.CommonErrors import Func_Client_Exception
17 import func.jobthing as jobthing
18
19 def syncable(cert_list):
20 """
21 Calls out to known hosts to find out who is configured for
22 peering. Returns a list of hostnames who support peering.
23 """
24 try:
25 fc = Client('*', async=True, nforks=len(cert_list))
26 except Func_Client_Exception:
27 # we are either:
28 # - signing the first minion
29 # - cleaning the only minion
30 # so there's nothing to hit. This shouldn't happen
31 # when we get called from the 'post-fetch' trigger
32 # (future work)
33 return None
34
35 # Only wait for a few seconds. Assume anything that doesn't get
36 # back by then is a lost cause. Don't want this trigger to spin
37 # too long.
38 ticks = 0
39 return_code = jobthing.JOB_ID_RUNNING
40 results = None
41 job_id = fc.certmastermod.peering_enabled()
42 while return_code != jobthing.JOB_ID_FINISHED and ticks < 3:
43 sleep(1)
44 (return_code, results) = fc.job_status(job_id)
45 ticks += 1
46
47 hosts = []
48 for host, result in results.iteritems():
49 if result == True:
50 hosts.append(host)
51 return hosts
52
53 def remote_peers(hosts):
54 """
55 Calls out to hosts to collect peer information
56 """
57 fc = Client(';'.join(hosts))
58 return fc.certmastermod.known_peers()
59
60 def local_certs():
61 """
62 Returns (hostname, sha1) hash of local certs
63 """
64 globby = '*.%s' % cm.cfg.cert_extension
65 globby = os.path.join(cm.cfg.certroot, globby)
66 files = glob(globby)
67 results = []
68 for f in files:
69 hostname = os.path.basename(f).replace('.' + cm.cfg.cert_extension, '')
70 digest = checksum(f)
71 results.append([hostname, digest])
72 return results
73
74 def checksum(f):
75 thissum = sha.new()
76 if os.path.exists(f):
77 fo = open(f, 'r')
78 data = fo.read()
79 fo.close()
80 thissum.update(data)
81
82 return thissum.hexdigest()
83
84 def remove_stale_certs(local, remote):
85 """
86 For each cert on each remote host, make sure it exists locally.
87 If not then it has been cleaned locally and needs unlinked
88 remotely.
89 """
90 local = [foo[0] for foo in local] # don't care about checksums
91 for host, peers in remote.iteritems():
92 fc = Client(host)
93 die = []
94 for peer in peers:
95 if peer[0] not in local:
96 die.append(peer[0])
97 if die != []:
98 fc.certmastermod.remove_peer_certs(die)
99
100 def copy_updated_certs(local, remote):
101 """
102 For each local cert, make sure it exists on the remote with the
103 correct hash. If not, copy it over!
104 """
105 for host, peers in remote.iteritems():
106 fc = Client(host)
107 for cert in local:
108 if cert not in peers:
109 cert_name = '%s.%s' % (cert[0], cm.cfg.cert_extension)
110 full_path = os.path.join(cm.cfg.certroot, cert_name)
111 fd = open(full_path)
112 certblob = fd.read()
113 fd.close()
114 fc.certmastermod.copy_peer_cert(cert[0], xmlrpclib.Binary(certblob))
115
116 def main():
117 forced = False
118 try:
119 if sys.argv[1] in ['-f', '--force']:
120 forced = True
121 except IndexError:
122 pass
123
124 if not cm.cfg.sync_certs and not forced:
125 sys.exit(0)
126
127 certs = glob(os.path.join(cm.cfg.certroot,
128 '*.%s' % cm.cfg.cert_extension))
129 hosts = syncable(certs)
130 if not hosts:
131 return 0
132 remote = remote_peers(hosts)
133 local = local_certs()
134 remove_stale_certs(local, remote)
135 copy_updated_certs(local, remote)
136
137 if __name__ == "__main__":
138 cm = certmaster.CertMaster()
139 main()