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