]> git.decadent.org.uk Git - dak.git/blob - dak/transitions.py
* dak/edit_transitions.py: Add --import option.
[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: edit_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", "edit-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             answer = "XXX"
189             prompt = "Broken edit: [E]dit again, Drop changes?"
190
191             while prompt.find(answer) == -1:
192                 answer = daklib.utils.our_raw_input(prompt)
193                 if answer == "":
194                     answer = "E"
195                 answer = answer[:1].upper()
196
197             if answer == 'E':
198                 continue
199             elif answer == 'D':
200                 os.unlink(edit_file)
201                 print "OK, discarding changes"
202                 sys.exit(0)
203         else:
204             # No problems in loading the new file, jump out of the while loop
205             break
206
207     # We seem to be done and also have a working file. Copy over.
208     write_transitions_from_file(edit_file)
209     os.unlink(edit_file)
210
211     # Before we finish print out transition info again
212     print "\n\n------------------------------------------------------------------------"
213     print "Edit done, file saved, currently defined transitions:\n"
214     transition_info(load_transitions(trans_file))
215
216 ################################################################################
217
218 def check_transitions(transitions):
219     to_dump = 0
220     to_remove = []
221     # Now look through all defined transitions
222     for trans in transitions:
223         t = transitions[trans]
224         source = t["source"]
225         expected = t["new"]
226
227         # Will be None if nothing is in testing.
228         current = daklib.database.get_suite_version(source, "testing")
229
230         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
231
232         if current == None:
233             # No package in testing
234             print "Transition source %s not in testing, transition still ongoing." % (source)
235         else:
236             compare = apt_pkg.VersionCompare(current, expected)
237             if compare < 0:
238                 # This is still valid, the current version in database is older than
239                 # the new version we wait for
240                 print "This transition is still ongoing, we currently have version %s" % (current)
241             else:
242                 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
243                 print "%s wanted version: %s, has %s" % (source, expected, current)
244                 to_remove.append(trans)
245                 to_dump = 1
246         print "-------------------------------------------------------------------------"
247
248     if to_dump:
249         prompt = "Removing: "
250         for remove in to_remove:
251             prompt += remove
252             prompt += ","
253
254         prompt += " Commit Changes? (y/N)"
255         answer = ""
256
257         if Options["no-action"]:
258             answer="n"
259         else:
260             answer = daklib.utils.our_raw_input(prompt).lower()
261
262         if answer == "":
263             answer = "n"
264
265         if answer == 'n':
266             print "Not committing changes"
267             sys.exit(0)
268         elif answer == 'y':
269             print "Committing"
270             for remove in to_remove:
271                 del transitions[remove]
272     
273             edit_file = temp_transitions_file(transitions)
274             write_transitions_from_file(edit_file)
275
276             print "Done"
277         else:
278             print "WTF are you typing?"
279             sys.exit(0)
280
281 ################################################################################
282
283 def print_info(trans, source, expected, rm, reason, packages):
284         print """
285 Looking at transition: %s
286  Source:      %s
287  New Version: %s
288  Responsible: %s
289  Description: %s
290  Blocked Packages (total: %d): %s
291 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
292         return
293
294 ################################################################################
295
296 def transition_info(transitions):
297     for trans in transitions:
298         t = transitions[trans]
299         source = t["source"]
300         expected = t["new"]
301
302         # Will be None if nothing is in testing.
303         current = daklib.database.get_suite_version(source, "testing")
304
305         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
306
307         if current == None:
308             # No package in testing
309             print "Transition source %s not in testing, transition still ongoing." % (source)
310         else:
311             compare = apt_pkg.VersionCompare(current, expected)
312             print "Apt compare says: %s" % (compare)
313             if compare < 0:
314                 # This is still valid, the current version in database is older than
315                 # the new version we wait for
316                 print "This transition is still ongoing, we currently have version %s" % (current)
317             else:
318                 print "This transition is over, the target package reached testing, should be removed"
319                 print "%s wanted version: %s, has %s" % (source, expected, current)
320         print "-------------------------------------------------------------------------"
321
322 ################################################################################
323
324 def main():
325     global Cnf
326
327     init()
328     
329     # Check if there is a file defined (and existant)
330     transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
331     if transpath == "":
332         daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
333         sys.exit(1)
334     if not os.path.exists(transpath):
335         daklib.utils.warn("ReleaseTransitions file, %s, not found." %
336                           (Cnf["Dinstall::Reject::ReleaseTransitions"]))
337         sys.exit(1)
338    
339     if Options["import"]:
340         try:
341             write_transitions_from_file(Options["import"])
342         except ParseException, m:
343             print m
344             sys.exit(2)
345         sys.exit(0)
346
347     # Parse the yaml file
348     transitions = load_transitions(transpath)
349     if transitions == None:
350         # Something very broken with the transitions, exit
351         daklib.utils.warn("Could not parse existing transitions file. Aborting.")
352         sys.exit(2)
353
354     if Options["edit"]:
355         # Let's edit the transitions file
356         edit_transitions()
357     elif Options["check"]:
358         # Check and remove outdated transitions
359         check_transitions(transitions)
360     else:
361         # Output information about the currently defined transitions.
362         print "Currently defined transitions:"
363         transition_info(transitions)
364
365     sys.exit(0)
366     
367 ################################################################################
368
369 if __name__ == '__main__':
370     main()