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