]> git.decadent.org.uk Git - dak.git/blob - dak/transitions.py
2a4fc9bfdc0fa78527273c1e356a9382ddedf0db
[dak.git] / dak / transitions.py
1 #!/usr/bin/env python
2
3 # Display, edit and check the release manager's transition file.
4 # Copyright (C) 2008 Joerg Jaspert <joerg@debian.org>
5
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19
20 ################################################################################
21
22 # <elmo> if klecker.d.o died, I swear to god, I'm going to migrate to gentoo.
23
24 ################################################################################
25
26 import os, pg, sys, time, errno, fcntl, tempfile, pwd
27 import apt_pkg
28 import daklib.database
29 import daklib.utils
30 import syck
31
32 # Globals
33 Cnf = None
34 Options = None
35 projectB = None
36
37 ################################################################################
38
39 def init():
40     global Cnf, Options, projectB
41
42     apt_pkg.init()
43
44     Cnf = daklib.utils.get_conf()
45
46     Arguments = [('h',"help","Edit-Transitions::Options::Help"),
47                  ('e',"edit","Edit-Transitions::Options::Edit"),
48                  ('i',"import","Edit-Transitions::Options::Import", "HasArg"),
49                  ('c',"check","Edit-Transitions::Options::Check"),
50                  ('s',"sudo","Edit-Transitions::Options::Sudo"),
51                  ('n',"no-action","Edit-Transitions::Options::No-Action")]
52
53     for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
54         if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
55             Cnf["Edit-Transitions::Options::%s" % (i)] = ""
56
57     apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
58
59     Options = Cnf.SubTree("Edit-Transitions::Options")
60
61     if Options["help"]:
62         usage()
63
64     whoami = os.getuid()
65     whoamifull = pwd.getpwuid(whoami)
66     username = whoamifull[0]
67     if username != "dak":
68         print "Non-dak user: %s" % username
69         Options["sudo"] = "y"
70
71     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
72     daklib.database.init(Cnf, projectB)
73     
74 ################################################################################
75
76 def usage (exit_code=0):
77     print """Usage: transitions [OPTION]...
78 Update and check the release managers transition file.
79
80 Options:
81
82   -h, --help                show this help and exit.
83   -e, --edit                edit the transitions file
84   -i, --import <file>       check and import transitions from file
85   -c, --check               check the transitions file, remove outdated entries
86   -S, --sudo                use sudo to update transitions file
87   -n, --no-action           don't do anything (only affects check)"""
88
89     sys.exit(exit_code)
90
91 ################################################################################
92
93 def load_transitions(trans_file):
94     # Parse the yaml file
95     sourcefile = file(trans_file, 'r')
96     sourcecontent = sourcefile.read()
97     failure = False
98     try:
99         trans = syck.load(sourcecontent)
100     except syck.error, msg:
101         # Someone fucked it up
102         print "ERROR: %s" % (msg)
103         return None
104
105     # lets do further validation here
106     checkkeys = ["source", "reason", "packages", "new", "rm"]
107     for test in trans:
108         t = trans[test]
109
110         # First check if we know all the keys for the transition and if they have
111         # the right type (and for the packages also if the list has the right types
112         # included, ie. not a list in list, but only str in the list)
113         for key in t:
114             if key not in checkkeys:
115                 print "ERROR: Unknown key %s in transition %s" % (key, test)
116                 failure = True
117
118             if key == "packages":
119                 if type(t[key]) != list:
120                     print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
121                     failure = True
122
123                 try:
124                     for package in t["packages"]:
125                         if type(package) != str:
126                             print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
127                             failure = True
128                 except TypeError:
129                     # In case someone has an empty packages list
130                     print "ERROR: No packages defined in transition %s" % (test)
131                     failure = True
132                     continue
133
134             elif type(t[key]) != str:
135                 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
136                 failure = True
137
138         # And now the other way round - are all our keys defined?
139         for key in checkkeys:
140             if key not in t:
141                 print "ERROR: Missing key %s in transition %s" % (key, test)
142                 failure = True
143
144     if failure:
145         return None
146
147     return trans
148
149 ################################################################################
150
151 def lock_file(file):
152     for retry in range(10):
153         lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
154         try:
155             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
156             return lock_fd
157         except OSError, e:
158             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
159                 print "Unable to get lock for %s (try %d of 10)" % \
160                         (file, retry+1)
161                 time.sleep(60)
162             else:
163                 raise
164
165     daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
166
167 ################################################################################
168
169 def write_transitions(from_trans):
170     """Update the active transitions file safely.
171        This function takes a parsed input file (which avoids invalid
172        files or files that may be be modified while the function is
173        active), and ensure the transitions file is updated atomically
174        to avoid locks."""
175
176     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
177     trans_temp = trans_file + ".tmp"
178   
179     trans_lock = lock_file(trans_file)
180     temp_lock  = lock_file(trans_temp)
181
182     destfile = file(trans_temp, 'w')
183     syck.dump(from_trans, destfile)
184     destfile.close()
185
186     os.rename(trans_temp, trans_file)
187     os.close(temp_lock)
188     os.close(trans_lock)
189
190 ################################################################################
191
192 class ParseException(Exception):
193     pass
194
195 def write_transitions_from_file(from_file):
196     """We have a file we think is valid; if we're using sudo, we invoke it
197        here, otherwise we just parse the file and call write_transitions"""
198
199     if Options["sudo"]:
200         os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H", 
201               "/usr/local/bin/dak", "transitions", "--import", from_file)
202     else:
203         trans = load_transitions(from_file)
204         if trans is None:
205             raise ParseException, "Unparsable transitions file %s" % (file)
206         write_transitions(trans)
207
208 ################################################################################
209
210 def temp_transitions_file(transitions):
211     # NB: file is unlinked by caller, but fd is never actually closed.
212     # We need the chmod, as the file is (most possibly) copied from a
213     # sudo-ed script and would be unreadable if it has default mkstemp mode
214     
215     (fd, path) = tempfile.mkstemp("","transitions")
216     os.chmod(path, 0644)
217     f = open(path, "w")
218     syck.dump(transitions, f)
219     return path
220
221 ################################################################################
222
223 def edit_transitions():
224     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
225     edit_file = temp_transitions_file(load_transitions(trans_file))
226
227     editor = os.environ.get("EDITOR", "vi")
228
229     while True:
230         result = os.system("%s %s" % (editor, edit_file))
231         if result != 0:
232             os.unlink(edit_file)
233             daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
234     
235         # Now try to load the new file
236         test = load_transitions(edit_file)
237
238         if test == None:
239             # Edit is broken
240             print "Edit was unparsable."
241             prompt = "[E]dit again, Drop changes?"
242             default = "E"
243         else:
244             print "Edit looks okay.\n"
245             print "The following transitions are defined:"
246             print "------------------------------------------------------------------------"
247             transition_info(test)
248
249             prompt = "[S]ave, Edit again, Drop changes?"
250             default = "S"
251
252         answer = "XXX"
253         while prompt.find(answer) == -1:
254             answer = daklib.utils.our_raw_input(prompt)
255             if answer == "":
256                 answer = default
257             answer = answer[:1].upper()
258
259         if answer == 'E':
260             continue
261         elif answer == 'D':
262             os.unlink(edit_file)
263             print "OK, discarding changes"
264             sys.exit(0)
265         elif answer == 'S':
266             # Ready to save
267             break
268         else:
269             print "You pressed something you shouldn't have :("
270             sys.exit(1)
271
272     # We seem to be done and also have a working file. Copy over.
273     write_transitions_from_file(edit_file)
274     os.unlink(edit_file)
275
276     print "Transitions file updated."
277
278 ################################################################################
279
280 def check_transitions(transitions):
281     to_dump = 0
282     to_remove = []
283     # Now look through all defined transitions
284     for trans in transitions:
285         t = transitions[trans]
286         source = t["source"]
287         expected = t["new"]
288
289         # Will be None if nothing is in testing.
290         current = daklib.database.get_suite_version(source, "testing")
291
292         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
293
294         if current == None:
295             # No package in testing
296             print "Transition source %s not in testing, transition still ongoing." % (source)
297         else:
298             compare = apt_pkg.VersionCompare(current, expected)
299             if compare < 0:
300                 # This is still valid, the current version in database is older than
301                 # the new version we wait for
302                 print "This transition is still ongoing, we currently have version %s" % (current)
303             else:
304                 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
305                 print "%s wanted version: %s, has %s" % (source, expected, current)
306                 to_remove.append(trans)
307                 to_dump = 1
308         print "-------------------------------------------------------------------------"
309
310     if to_dump:
311         prompt = "Removing: "
312         for remove in to_remove:
313             prompt += remove
314             prompt += ","
315
316         prompt += " Commit Changes? (y/N)"
317         answer = ""
318
319         if Options["no-action"]:
320             answer="n"
321         else:
322             answer = daklib.utils.our_raw_input(prompt).lower()
323
324         if answer == "":
325             answer = "n"
326
327         if answer == 'n':
328             print "Not committing changes"
329             sys.exit(0)
330         elif answer == 'y':
331             print "Committing"
332             for remove in to_remove:
333                 del transitions[remove]
334     
335             edit_file = temp_transitions_file(transitions)
336             write_transitions_from_file(edit_file)
337
338             print "Done"
339         else:
340             print "WTF are you typing?"
341             sys.exit(0)
342
343 ################################################################################
344
345 def print_info(trans, source, expected, rm, reason, packages):
346         print """Looking at transition: %s
347  Source:      %s
348  New Version: %s
349  Responsible: %s
350  Description: %s
351  Blocked Packages (total: %d): %s
352 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
353         return
354
355 ################################################################################
356
357 def transition_info(transitions):
358     for trans in transitions:
359         t = transitions[trans]
360         source = t["source"]
361         expected = t["new"]
362
363         # Will be None if nothing is in testing.
364         current = daklib.database.get_suite_version(source, "testing")
365
366         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
367
368         if current == None:
369             # No package in testing
370             print "Transition source %s not in testing, transition still ongoing." % (source)
371         else:
372             compare = apt_pkg.VersionCompare(current, expected)
373             print "Apt compare says: %s" % (compare)
374             if compare < 0:
375                 # This is still valid, the current version in database is older than
376                 # the new version we wait for
377                 print "This transition is still ongoing, we currently have version %s" % (current)
378             else:
379                 print "This transition is over, the target package reached testing, should be removed"
380                 print "%s wanted version: %s, has %s" % (source, expected, current)
381         print "-------------------------------------------------------------------------"
382
383 ################################################################################
384
385 def main():
386     global Cnf
387
388     init()
389     
390     # Check if there is a file defined (and existant)
391     transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
392     if transpath == "":
393         daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
394         sys.exit(1)
395     if not os.path.exists(transpath):
396         daklib.utils.warn("ReleaseTransitions file, %s, not found." %
397                           (Cnf["Dinstall::Reject::ReleaseTransitions"]))
398         sys.exit(1)
399    
400     if Options["import"]:
401         try:
402             write_transitions_from_file(Options["import"])
403         except ParseException, m:
404             print m
405             sys.exit(2)
406         sys.exit(0)
407
408     # Parse the yaml file
409     transitions = load_transitions(transpath)
410     if transitions == None:
411         # Something very broken with the transitions, exit
412         daklib.utils.warn("Could not parse existing transitions file. Aborting.")
413         sys.exit(2)
414
415     if Options["edit"]:
416         # Let's edit the transitions file
417         edit_transitions()
418     elif Options["check"]:
419         # Check and remove outdated transitions
420         check_transitions(transitions)
421     else:
422         # Output information about the currently defined transitions.
423         print "Currently defined transitions:"
424         transition_info(transitions)
425
426     sys.exit(0)
427     
428 ################################################################################
429
430 if __name__ == '__main__':
431     main()