]> git.decadent.org.uk Git - dak.git/blob - dak/transitions.py
Add more exception handling
[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, re
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 re_broken_package = re.compile(r"[a-zA-Z]\w+\s+\-.*")
38
39 ################################################################################
40
41 #####################################
42 #### This may run within sudo !! ####
43 #####################################
44 def init():
45     global Cnf, Options, projectB
46
47     apt_pkg.init()
48
49     Cnf = daklib.utils.get_conf()
50
51     Arguments = [('h',"help","Edit-Transitions::Options::Help"),
52                  ('e',"edit","Edit-Transitions::Options::Edit"),
53                  ('i',"import","Edit-Transitions::Options::Import", "HasArg"),
54                  ('c',"check","Edit-Transitions::Options::Check"),
55                  ('s',"sudo","Edit-Transitions::Options::Sudo"),
56                  ('n',"no-action","Edit-Transitions::Options::No-Action")]
57
58     for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
59         if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
60             Cnf["Edit-Transitions::Options::%s" % (i)] = ""
61
62     apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
63
64     Options = Cnf.SubTree("Edit-Transitions::Options")
65
66     if Options["help"]:
67         usage()
68
69     whoami = os.getuid()
70     whoamifull = pwd.getpwuid(whoami)
71     username = whoamifull[0]
72     if username != "dak":
73         print "Non-dak user: %s" % username
74         Options["sudo"] = "y"
75
76     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
77     daklib.database.init(Cnf, projectB)
78     
79 ################################################################################
80
81 def usage (exit_code=0):
82     print """Usage: transitions [OPTION]...
83 Update and check the release managers transition file.
84
85 Options:
86
87   -h, --help                show this help and exit.
88   -e, --edit                edit the transitions file
89   -i, --import <file>       check and import transitions from file
90   -c, --check               check the transitions file, remove outdated entries
91   -S, --sudo                use sudo to update transitions file
92   -n, --no-action           don't do anything (only affects check)"""
93
94     sys.exit(exit_code)
95
96 ################################################################################
97
98 #####################################
99 #### This may run within sudo !! ####
100 #####################################
101 def load_transitions(trans_file):
102     # Parse the yaml file
103     sourcefile = file(trans_file, 'r')
104     sourcecontent = sourcefile.read()
105     failure = False
106     try:
107         trans = syck.load(sourcecontent)
108     except syck.error, msg:
109         # Someone fucked it up
110         print "ERROR: %s" % (msg)
111         return None
112
113     # lets do further validation here
114     checkkeys = ["source", "reason", "packages", "new", "rm"]
115
116     # If we get an empty definition - we just have nothing to check, no transitions defined
117     if type(trans) != dict:
118         # This can be anything. We could have no transitions defined. Or someone totally fucked up the
119         # file, adding stuff in a way we dont know or want. Then we set it empty - and simply have no
120         # transitions anymore. User will see it in the information display after he quit the editor and
121         # could fix it
122         trans = ""
123         return trans
124
125     try:
126         for test in trans:
127             t = trans[test]
128         
129             # First check if we know all the keys for the transition and if they have
130             # the right type (and for the packages also if the list has the right types
131             # included, ie. not a list in list, but only str in the list)
132             for key in t:
133                 if key not in checkkeys:
134                     print "ERROR: Unknown key %s in transition %s" % (key, test)
135                     failure = True
136         
137                 if key == "packages":
138                     if type(t[key]) != list:
139                         print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
140                         failure = True
141                     try:
142                         for package in t["packages"]:
143                             if type(package) != str:
144                                 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
145                                 failure = True
146                             if re_broken_package.match(package):
147                                 # Someone had a space too much (or not enough), we have something looking like
148                                 # "package1 - package2" now.
149                                 print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, package)
150                                 failure = True
151                     except TypeError:
152                         # In case someone has an empty packages list
153                         print "ERROR: No packages defined in transition %s" % (test)
154                         failure = True
155                         continue
156         
157                 elif type(t[key]) != str:
158                     if t[key] == "new" and type(t[key]) == int:
159                         # Ok, debian native version
160                     else:
161                         print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
162                         failure = True
163         
164             # And now the other way round - are all our keys defined?
165             for key in checkkeys:
166                 if key not in t:
167                     print "ERROR: Missing key %s in transition %s" % (key, test)
168                     failure = True
169     except TypeError:
170         # In case someone defined very broken things
171         print "ERROR: Unable to parse the file"
172         failure = True
173
174
175     if failure:
176         return None
177
178     return trans
179
180 ################################################################################
181
182 #####################################
183 #### This may run within sudo !! ####
184 #####################################
185 def lock_file(file):
186     for retry in range(10):
187         lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
188         try:
189             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
190             return lock_fd
191         except OSError, e:
192             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
193                 print "Unable to get lock for %s (try %d of 10)" % \
194                         (file, retry+1)
195                 time.sleep(60)
196             else:
197                 raise
198
199     daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
200
201 ################################################################################
202
203 #####################################
204 #### This may run within sudo !! ####
205 #####################################
206 def write_transitions(from_trans):
207     """Update the active transitions file safely.
208        This function takes a parsed input file (which avoids invalid
209        files or files that may be be modified while the function is
210        active), and ensure the transitions file is updated atomically
211        to avoid locks."""
212
213     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
214     trans_temp = trans_file + ".tmp"
215   
216     trans_lock = lock_file(trans_file)
217     temp_lock  = lock_file(trans_temp)
218
219     destfile = file(trans_temp, 'w')
220     syck.dump(from_trans, destfile)
221     destfile.close()
222
223     os.rename(trans_temp, trans_file)
224     os.close(temp_lock)
225     os.close(trans_lock)
226
227 ################################################################################
228
229 class ParseException(Exception):
230     pass
231
232 ##########################################
233 #### This usually runs within sudo !! ####
234 ##########################################
235 def write_transitions_from_file(from_file):
236     """We have a file we think is valid; if we're using sudo, we invoke it
237        here, otherwise we just parse the file and call write_transitions"""
238
239     # Lets check if from_file is in the directory we expect it to be in
240     if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
241         print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
242         sys.exit(3)
243
244     if Options["sudo"]:
245         os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H", 
246               "/usr/local/bin/dak", "transitions", "--import", from_file)
247     else:
248         trans = load_transitions(from_file)
249         if trans is None:
250             raise ParseException, "Unparsable transitions file %s" % (file)
251         write_transitions(trans)
252
253 ################################################################################
254
255 def temp_transitions_file(transitions):
256     # NB: file is unlinked by caller, but fd is never actually closed.
257     # We need the chmod, as the file is (most possibly) copied from a
258     # sudo-ed script and would be unreadable if it has default mkstemp mode
259     
260     (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
261     os.chmod(path, 0644)
262     f = open(path, "w")
263     syck.dump(transitions, f)
264     return path
265
266 ################################################################################
267
268 def edit_transitions():
269     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
270     edit_file = temp_transitions_file(load_transitions(trans_file))
271
272     editor = os.environ.get("EDITOR", "vi")
273
274     while True:
275         result = os.system("%s %s" % (editor, edit_file))
276         if result != 0:
277             os.unlink(edit_file)
278             daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
279     
280         # Now try to load the new file
281         test = load_transitions(edit_file)
282
283         if test == None:
284             # Edit is broken
285             print "Edit was unparsable."
286             prompt = "[E]dit again, Drop changes?"
287             default = "E"
288         else:
289             print "Edit looks okay.\n"
290             print "The following transitions are defined:"
291             print "------------------------------------------------------------------------"
292             transition_info(test)
293
294             prompt = "[S]ave, Edit again, Drop changes?"
295             default = "S"
296
297         answer = "XXX"
298         while prompt.find(answer) == -1:
299             answer = daklib.utils.our_raw_input(prompt)
300             if answer == "":
301                 answer = default
302             answer = answer[:1].upper()
303
304         if answer == 'E':
305             continue
306         elif answer == 'D':
307             os.unlink(edit_file)
308             print "OK, discarding changes"
309             sys.exit(0)
310         elif answer == 'S':
311             # Ready to save
312             break
313         else:
314             print "You pressed something you shouldn't have :("
315             sys.exit(1)
316
317     # We seem to be done and also have a working file. Copy over.
318     write_transitions_from_file(edit_file)
319     os.unlink(edit_file)
320
321     print "Transitions file updated."
322
323 ################################################################################
324
325 def check_transitions(transitions):
326     to_dump = 0
327     to_remove = []
328     # Now look through all defined transitions
329     for trans in transitions:
330         t = transitions[trans]
331         source = t["source"]
332         expected = t["new"]
333
334         # Will be None if nothing is in testing.
335         current = daklib.database.get_suite_version(source, "testing")
336
337         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
338
339         if current == None:
340             # No package in testing
341             print "Transition source %s not in testing, transition still ongoing." % (source)
342         else:
343             compare = apt_pkg.VersionCompare(current, expected)
344             if compare < 0:
345                 # This is still valid, the current version in database is older than
346                 # the new version we wait for
347                 print "This transition is still ongoing, we currently have version %s" % (current)
348             else:
349                 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
350                 print "%s wanted version: %s, has %s" % (source, expected, current)
351                 to_remove.append(trans)
352                 to_dump = 1
353         print "-------------------------------------------------------------------------"
354
355     if to_dump:
356         prompt = "Removing: "
357         for remove in to_remove:
358             prompt += remove
359             prompt += ","
360
361         prompt += " Commit Changes? (y/N)"
362         answer = ""
363
364         if Options["no-action"]:
365             answer="n"
366         else:
367             answer = daklib.utils.our_raw_input(prompt).lower()
368
369         if answer == "":
370             answer = "n"
371
372         if answer == 'n':
373             print "Not committing changes"
374             sys.exit(0)
375         elif answer == 'y':
376             print "Committing"
377             for remove in to_remove:
378                 del transitions[remove]
379     
380             edit_file = temp_transitions_file(transitions)
381             write_transitions_from_file(edit_file)
382
383             print "Done"
384         else:
385             print "WTF are you typing?"
386             sys.exit(0)
387
388 ################################################################################
389
390 def print_info(trans, source, expected, rm, reason, packages):
391         print """Looking at transition: %s
392  Source:      %s
393  New Version: %s
394  Responsible: %s
395  Description: %s
396  Blocked Packages (total: %d): %s
397 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
398         return
399
400 ################################################################################
401
402 def transition_info(transitions):
403     for trans in transitions:
404         t = transitions[trans]
405         source = t["source"]
406         expected = t["new"]
407
408         # Will be None if nothing is in testing.
409         current = daklib.database.get_suite_version(source, "testing")
410
411         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
412
413         if current == None:
414             # No package in testing
415             print "Transition source %s not in testing, transition still ongoing." % (source)
416         else:
417             compare = apt_pkg.VersionCompare(current, expected)
418             print "Apt compare says: %s" % (compare)
419             if compare < 0:
420                 # This is still valid, the current version in database is older than
421                 # the new version we wait for
422                 print "This transition is still ongoing, we currently have version %s" % (current)
423             else:
424                 print "This transition is over, the target package reached testing, should be removed"
425                 print "%s wanted version: %s, has %s" % (source, expected, current)
426         print "-------------------------------------------------------------------------"
427
428 ################################################################################
429
430 def main():
431     global Cnf
432
433     #####################################
434     #### This can run within sudo !! ####
435     #####################################
436     init()
437     
438     # Check if there is a file defined (and existant)
439     transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
440     if transpath == "":
441         daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
442         sys.exit(1)
443     if not os.path.exists(transpath):
444         daklib.utils.warn("ReleaseTransitions file, %s, not found." %
445                           (Cnf["Dinstall::Reject::ReleaseTransitions"]))
446         sys.exit(1)
447     # Also check if our temp directory is defined and existant
448     temppath = Cnf.get("Transitions::TempPath", "")
449     if temppath == "":
450         daklib.utils.warn("Transitions::TempPath not defined")
451         sys.exit(1)
452     if not os.path.exists(temppath):
453         daklib.utils.warn("Temporary path %s not found." %
454                           (Cnf["Transitions::TempPath"]))
455         sys.exit(1)
456    
457     if Options["import"]:
458         try:
459             write_transitions_from_file(Options["import"])
460         except ParseException, m:
461             print m
462             sys.exit(2)
463         sys.exit(0)
464     ##############################################
465     #### Up to here it can run within sudo !! ####
466     ##############################################
467
468     # Parse the yaml file
469     transitions = load_transitions(transpath)
470     if transitions == None:
471         # Something very broken with the transitions, exit
472         daklib.utils.warn("Could not parse existing transitions file. Aborting.")
473         sys.exit(2)
474
475     if Options["edit"]:
476         # Let's edit the transitions file
477         edit_transitions()
478     elif Options["check"]:
479         # Check and remove outdated transitions
480         check_transitions(transitions)
481     else:
482         # Output information about the currently defined transitions.
483         print "Currently defined transitions:"
484         transition_info(transitions)
485
486     sys.exit(0)
487     
488 ################################################################################
489
490 if __name__ == '__main__':
491     main()