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