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