sha1 supprt checkpoint
[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 warning
11 try:
12 import hashlib
13 except ImportError:
14 # Python-2.4.z ... gah! (or even 2.3!)
15 import sha
16 class hashlib:
17 @staticmethod
18 def new(algo):
19 if algo == 'sha1':
20 warnings.warn('sha1 is deprecated',DeprecationWarning)
21 return sha.new()
22 raise ValueError, "Bad checksum type"
23
24
25 import xmlrpclib
26 from glob import glob
27 from time import sleep
28 from certmaster import certmaster as certmaster
29
30 func_import_failure = None
31 try:
32 from func.overlord.client import Client
33 from func.CommonErrors import Func_Client_Exception
34 import func.jobthing as jobthing
35 except ImportError, e:
36 func_import_failure = str(e)
37
38 def syncable(cert_list):
39 """
40 Calls out to known hosts to find out who is configured for
41 peering. Returns a list of hostnames who support peering.
42 """
43 try:
44 fc = Client('*', async=True, nforks=len(cert_list))
45 except Func_Client_Exception:
46 # we are either:
47 # - signing the first minion
48 # - cleaning the only minion
49 # so there's nothing to hit. This shouldn't happen
50 # when we get called from the 'post-fetch' trigger
51 # (future work)
52 return None
53
54 # Only wait for a few seconds. Assume anything that doesn't get
55 # back by then is a lost cause. Don't want this trigger to spin
56 # too long.
57 ticks = 0
58 return_code = jobthing.JOB_ID_RUNNING
59 results = None
60 job_id = fc.certmastermod.peering_enabled()
61 while return_code != jobthing.JOB_ID_FINISHED and ticks < 3:
62 sleep(1)
63 (return_code, results) = fc.job_status(job_id)
64 ticks += 1
65
66 hosts = []
67 for host, result in results.iteritems():
68 if result == True:
69 hosts.append(host)
70 return hosts
71
72 def remote_peers(hosts):
73 """
74 Calls out to hosts to collect peer information
75 """
76 fc = Client(';'.join(hosts))
77 return fc.certmastermod.known_peers()
78
79 def local_certs():
80 """
81 Returns (hostname, hashval) hash of local certs
82 """
83 globby = '*.%s' % cm.cfg.cert_extension
84 globby = os.path.join(cm.cfg.certroot, globby)
85 files = glob(globby)
86 results = []
87 for f in files:
88 hostname = os.path.basename(f).replace('.' + cm.cfg.cert_extension, '')
89 dirname = os.path.dirname(f)
90 digest = checksum(f,cm.cfg.hashfunc)
91 results.append([hostname, digest, dirname])
92 return results
93
94 def checksum(f,hashfunc):
95 thissum = hashlib.new(hashfunc)
96 if os.path.exists(f):
97 fo = open(f, 'r')
98 data = fo.read()
99 fo.close()
100 thissum.update(data)
101
102 return thissum.hexdigest()
103
104 def remove_stale_certs(local, remote):
105 """
106 For each cert on each remote host, make sure it exists locally.
107 If not then it has been cleaned locally and needs unlinked
108 remotely.
109 """
110 local = [foo[0] for foo in local] # don't care about checksums
111 for host, peers in remote.iteritems():
112 fc = Client(host)
113 die = []
114 for peer in peers:
115 if peer[0] not in local:
116 die.append(peer[0])
117 if die != []:
118 fc.certmastermod.remove_peer_certs(die)
119
120 def copy_updated_certs(local, remote):
121 """
122 For each local cert, make sure it exists on the remote with the
123 correct hash. If not, copy it over!
124 """
125 for host, peers in remote.iteritems():
126 fc = Client(host)
127 for cert in local:
128 if cert not in peers:
129 cert_name = '%s.%s' % (cert[0], cm.cfg.cert_extension)
130 full_path = os.path.join(cert[2], cert_name)
131 fd = open(full_path)
132 certblob = fd.read()
133 fd.close()
134 fc.certmastermod.copy_peer_cert(cert[0], xmlrpclib.Binary(certblob))
135
136 def main():
137 forced = False
138 try:
139 if sys.argv[1] in ['-f', '--force']:
140 forced = True
141 except IndexError:
142 pass
143
144 if not cm.cfg.sync_certs and not forced:
145 sys.exit(0)
146
147 # Don't complain about func not being available until you actually want it
148 if func_import_failure != None:
149 print >> sys.stderr, "errors importing func: %s" % func_import_failure
150 sys.exit(1)
151
152 certs = glob(os.path.join(cm.cfg.certroot,
153 '*.%s' % cm.cfg.cert_extension))
154 hosts = syncable(certs)
155 if not hosts:
156 return 0
157 remote = remote_peers(hosts)
158 local = local_certs()
159 remove_stale_certs(local, remote)
160 copy_updated_certs(local, remote)
161
162 if __name__ == "__main__":
163 cm = certmaster.CertMaster()
164 main()