]> git.decadent.org.uk Git - dak.git/blob - dak/transitions.py
121edc2f4dd88b7277a3b7160b0e13151cbda8ad
[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
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 ################################################################################
38
39 def init():
40     global Cnf, Options, projectB
41
42     apt_pkg.init()
43
44     Cnf = daklib.utils.get_conf()
45
46     Arguments = [('h',"help","Edit-Transitions::Options::Help"),
47                  ('e',"edit","Edit-Transitions::Options::Edit"),
48                  ('i',"import","Edit-Transitions::Options::Import", "HasArg"),
49                  ('c',"check","Edit-Transitions::Options::Check"),
50                  ('S',"use-sudo","Edit-Transitions::Options::Sudo"),
51                  ('n',"no-action","Edit-Transitions::Options::No-Action")]
52
53     for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
54         if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
55             Cnf["Edit-Transitions::Options::%s" % (i)] = ""
56
57     apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
58
59     Options = Cnf.SubTree("Edit-Transitions::Options")
60
61     projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
62     daklib.database.init(Cnf, projectB)
63     
64     if Options["help"]:
65         usage()
66
67 ################################################################################
68
69 def usage (exit_code=0):
70     print """Usage: transitions [OPTION]...
71 Update and check the release managers transition file.
72 transitions.
73
74 Options:
75
76   -h, --help                show this help and exit.
77   -e, --edit                edit the transitions file
78   -i, --import <file>       check and import transitions from file
79   -c, --check               check the transitions file, remove outdated entries
80   -S, --sudo                use sudo to update transitions file
81   -n, --no-action           don't do anything"""
82
83     sys.exit(exit_code)
84
85 ################################################################################
86
87 def load_transitions(trans_file):
88     # Parse the yaml file
89     sourcefile = file(trans_file, 'r')
90     sourcecontent = sourcefile.read()
91     try:
92         trans = syck.load(sourcecontent)
93     except syck.error, msg:
94         # Someone fucked it up
95         print "ERROR: %s" % (msg)
96         return None
97     # could do further validation here
98     return trans
99
100 ################################################################################
101
102 def lock_file(file):
103     for retry in range(10):
104         lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
105         try:
106             fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
107             return lock_fd
108         except OSError, e:
109             if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
110                 print "Unable to get lock for %s (try %d of 10)" % \
111                         (file, retry+1)
112                 time.sleep(60)
113             else:
114                 raise
115
116     daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
117
118 ################################################################################
119
120 def write_transitions(from_trans):
121     """Update the active transitions file safely.
122        This function takes a parsed input file (which avoids invalid
123        files or files that may be be modified while the function is
124        active), and ensure the transitions file is updated atomically
125        to avoid locks."""
126
127     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
128     trans_temp = trans_file + ".tmp"
129   
130     trans_lock = lock_file(trans_file)
131     temp_lock  = lock_file(trans_temp)
132
133     destfile = file(trans_temp, 'w')
134     syck.dump(from_trans, destfile)
135     destfile.close()
136
137     os.rename(trans_temp, trans_file)
138     os.close(temp_lock)
139     os.close(trans_lock)
140
141 ################################################################################
142
143 class ParseException(Exception):
144     pass
145
146 def write_transitions_from_file(from_file):
147     """We have a file we think is valid; if we're using sudo, we invoke it
148        here, otherwise we just parse the file and call write_transitions"""
149
150     if Options["sudo"]:
151         os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H", 
152               "/usr/local/bin/dak", "transitions", "--import", from_file)
153     else:
154         trans = load_transitions(from_file)
155         if trans is None:
156             raise ParseException, "Unparsable transitions file %s" % (file)
157         write_transitions(trans)
158
159 ################################################################################
160
161 def temp_transitions_file(transitions):
162     # NB: file is unlinked by caller, but fd is never actually closed.
163
164     (fd, path) = tempfile.mkstemp("","transitions")
165     f = open(path, "w")
166     syck.dump(transitions, f)
167     return path
168
169 ################################################################################
170
171 def edit_transitions():
172     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
173     edit_file = temp_transitions_file(load_transitions(trans_file))
174
175     editor = os.environ.get("EDITOR", "vi")
176
177     while True:
178         result = os.system("%s %s" % (editor, edit_file))
179         if result != 0:
180             os.unlink(edit_file)
181             daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
182     
183         # Now try to load the new file
184         test = load_transitions(edit_file)
185
186         if test == None:
187             # Edit is broken
188             print "Edit was unparsable."
189             prompt = "[E]dit again, Drop changes?"
190             default = "E"
191         else:
192             print "Edit looks okay.\n"
193             print "------------------------------------------------------------------------"
194             transition_info(test)
195
196             prompt = "[S]ave, Edit again, Drop changes?"
197             default = "S"
198
199         answer = "XXX"
200         while prompt.find(answer) == -1:
201             answer = daklib.utils.our_raw_input(prompt)
202             if answer == "":
203                 answer = default
204             answer = answer[:1].upper()
205
206         if answer == 'E':
207             continue
208         elif answer == 'D':
209             os.unlink(edit_file)
210             print "OK, discarding changes"
211             sys.exit(0)
212         elif answer == 'S':
213             # Ready to save
214             break
215         else:
216             print "You pressed something you shouldn't have :("
217             sys.exit(1)
218
219     # We seem to be done and also have a working file. Copy over.
220     write_transitions_from_file(edit_file)
221     os.unlink(edit_file)
222
223     print "Transitions file updated."
224
225 ################################################################################
226
227 def check_transitions(transitions):
228     to_dump = 0
229     to_remove = []
230     # Now look through all defined transitions
231     for trans in transitions:
232         t = transitions[trans]
233         source = t["source"]
234         expected = t["new"]
235
236         # Will be None if nothing is in testing.
237         current = daklib.database.get_suite_version(source, "testing")
238
239         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
240
241         if current == None:
242             # No package in testing
243             print "Transition source %s not in testing, transition still ongoing." % (source)
244         else:
245             compare = apt_pkg.VersionCompare(current, expected)
246             if compare < 0:
247                 # This is still valid, the current version in database is older than
248                 # the new version we wait for
249                 print "This transition is still ongoing, we currently have version %s" % (current)
250             else:
251                 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
252                 print "%s wanted version: %s, has %s" % (source, expected, current)
253                 to_remove.append(trans)
254                 to_dump = 1
255         print "-------------------------------------------------------------------------"
256
257     if to_dump:
258         prompt = "Removing: "
259         for remove in to_remove:
260             prompt += remove
261             prompt += ","
262
263         prompt += " Commit Changes? (y/N)"
264         answer = ""
265
266         if Options["no-action"]:
267             answer="n"
268         else:
269             answer = daklib.utils.our_raw_input(prompt).lower()
270
271         if answer == "":
272             answer = "n"
273
274         if answer == 'n':
275             print "Not committing changes"
276             sys.exit(0)
277         elif answer == 'y':
278             print "Committing"
279             for remove in to_remove:
280                 del transitions[remove]
281     
282             edit_file = temp_transitions_file(transitions)
283             write_transitions_from_file(edit_file)
284
285             print "Done"
286         else:
287             print "WTF are you typing?"
288             sys.exit(0)
289
290 ################################################################################
291
292 def print_info(trans, source, expected, rm, reason, packages):
293         print """Looking at transition: %s
294  Source:      %s
295  New Version: %s
296  Responsible: %s
297  Description: %s
298  Blocked Packages (total: %d): %s
299 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
300         return
301
302 ################################################################################
303
304 def transition_info(transitions):
305     for trans in transitions:
306         t = transitions[trans]
307         source = t["source"]
308         expected = t["new"]
309
310         # Will be None if nothing is in testing.
311         current = daklib.database.get_suite_version(source, "testing")
312
313         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
314
315         if current == None:
316             # No package in testing
317             print "Transition source %s not in testing, transition still ongoing." % (source)
318         else:
319             compare = apt_pkg.VersionCompare(current, expected)
320             print "Apt compare says: %s" % (compare)
321             if compare < 0:
322                 # This is still valid, the current version in database is older than
323                 # the new version we wait for
324                 print "This transition is still ongoing, we currently have version %s" % (current)
325             else:
326                 print "This transition is over, the target package reached testing, should be removed"
327                 print "%s wanted version: %s, has %s" % (source, expected, current)
328         print "-------------------------------------------------------------------------"
329
330 ################################################################################
331
332 def main():
333     global Cnf
334
335     init()
336     
337     # Check if there is a file defined (and existant)
338     transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
339     if transpath == "":
340         daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
341         sys.exit(1)
342     if not os.path.exists(transpath):
343         daklib.utils.warn("ReleaseTransitions file, %s, not found." %
344                           (Cnf["Dinstall::Reject::ReleaseTransitions"]))
345         sys.exit(1)
346    
347     if Options["import"]:
348         try:
349             write_transitions_from_file(Options["import"])
350         except ParseException, m:
351             print m
352             sys.exit(2)
353         sys.exit(0)
354
355     # Parse the yaml file
356     transitions = load_transitions(transpath)
357     if transitions == None:
358         # Something very broken with the transitions, exit
359         daklib.utils.warn("Could not parse existing transitions file. Aborting.")
360         sys.exit(2)
361
362     if Options["edit"]:
363         # Let's edit the transitions file
364         edit_transitions()
365     elif Options["check"]:
366         # Check and remove outdated transitions
367         check_transitions(transitions)
368     else:
369         # Output information about the currently defined transitions.
370         print "Currently defined transitions:"
371         transition_info(transitions)
372
373     sys.exit(0)
374     
375 ################################################################################
376
377 if __name__ == '__main__':
378     main()