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