]> git.decadent.org.uk Git - dak.git/blob - dak/transitions.py
Check if our temp transitions file is in the path we expect it to be in
[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     # Lets check if from_file is in the directory we expect it to be in
215     if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
216         print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
217         sys.exit(3)
218
219     if Options["sudo"]:
220         os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H", 
221               "/usr/local/bin/dak", "transitions", "--import", from_file)
222     else:
223         trans = load_transitions(from_file)
224         if trans is None:
225             raise ParseException, "Unparsable transitions file %s" % (file)
226         write_transitions(trans)
227
228 ################################################################################
229
230 def temp_transitions_file(transitions):
231     # NB: file is unlinked by caller, but fd is never actually closed.
232     # We need the chmod, as the file is (most possibly) copied from a
233     # sudo-ed script and would be unreadable if it has default mkstemp mode
234     
235     (fd, path) = tempfile.mkstemp("","transitions",Cnf["Transitions::TempPath"])
236     os.chmod(path, 0644)
237     f = open(path, "w")
238     syck.dump(transitions, f)
239     return path
240
241 ################################################################################
242
243 def edit_transitions():
244     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
245     edit_file = temp_transitions_file(load_transitions(trans_file))
246
247     editor = os.environ.get("EDITOR", "vi")
248
249     while True:
250         result = os.system("%s %s" % (editor, edit_file))
251         if result != 0:
252             os.unlink(edit_file)
253             daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
254     
255         # Now try to load the new file
256         test = load_transitions(edit_file)
257
258         if test == None:
259             # Edit is broken
260             print "Edit was unparsable."
261             prompt = "[E]dit again, Drop changes?"
262             default = "E"
263         else:
264             print "Edit looks okay.\n"
265             print "The following transitions are defined:"
266             print "------------------------------------------------------------------------"
267             transition_info(test)
268
269             prompt = "[S]ave, Edit again, Drop changes?"
270             default = "S"
271
272         answer = "XXX"
273         while prompt.find(answer) == -1:
274             answer = daklib.utils.our_raw_input(prompt)
275             if answer == "":
276                 answer = default
277             answer = answer[:1].upper()
278
279         if answer == 'E':
280             continue
281         elif answer == 'D':
282             os.unlink(edit_file)
283             print "OK, discarding changes"
284             sys.exit(0)
285         elif answer == 'S':
286             # Ready to save
287             break
288         else:
289             print "You pressed something you shouldn't have :("
290             sys.exit(1)
291
292     # We seem to be done and also have a working file. Copy over.
293     write_transitions_from_file(edit_file)
294     os.unlink(edit_file)
295
296     print "Transitions file updated."
297
298 ################################################################################
299
300 def check_transitions(transitions):
301     to_dump = 0
302     to_remove = []
303     # Now look through all defined transitions
304     for trans in transitions:
305         t = transitions[trans]
306         source = t["source"]
307         expected = t["new"]
308
309         # Will be None if nothing is in testing.
310         current = daklib.database.get_suite_version(source, "testing")
311
312         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
313
314         if current == None:
315             # No package in testing
316             print "Transition source %s not in testing, transition still ongoing." % (source)
317         else:
318             compare = apt_pkg.VersionCompare(current, expected)
319             if compare < 0:
320                 # This is still valid, the current version in database is older than
321                 # the new version we wait for
322                 print "This transition is still ongoing, we currently have version %s" % (current)
323             else:
324                 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
325                 print "%s wanted version: %s, has %s" % (source, expected, current)
326                 to_remove.append(trans)
327                 to_dump = 1
328         print "-------------------------------------------------------------------------"
329
330     if to_dump:
331         prompt = "Removing: "
332         for remove in to_remove:
333             prompt += remove
334             prompt += ","
335
336         prompt += " Commit Changes? (y/N)"
337         answer = ""
338
339         if Options["no-action"]:
340             answer="n"
341         else:
342             answer = daklib.utils.our_raw_input(prompt).lower()
343
344         if answer == "":
345             answer = "n"
346
347         if answer == 'n':
348             print "Not committing changes"
349             sys.exit(0)
350         elif answer == 'y':
351             print "Committing"
352             for remove in to_remove:
353                 del transitions[remove]
354     
355             edit_file = temp_transitions_file(transitions)
356             write_transitions_from_file(edit_file)
357
358             print "Done"
359         else:
360             print "WTF are you typing?"
361             sys.exit(0)
362
363 ################################################################################
364
365 def print_info(trans, source, expected, rm, reason, packages):
366         print """Looking at transition: %s
367  Source:      %s
368  New Version: %s
369  Responsible: %s
370  Description: %s
371  Blocked Packages (total: %d): %s
372 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
373         return
374
375 ################################################################################
376
377 def transition_info(transitions):
378     for trans in transitions:
379         t = transitions[trans]
380         source = t["source"]
381         expected = t["new"]
382
383         # Will be None if nothing is in testing.
384         current = daklib.database.get_suite_version(source, "testing")
385
386         print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
387
388         if current == None:
389             # No package in testing
390             print "Transition source %s not in testing, transition still ongoing." % (source)
391         else:
392             compare = apt_pkg.VersionCompare(current, expected)
393             print "Apt compare says: %s" % (compare)
394             if compare < 0:
395                 # This is still valid, the current version in database is older than
396                 # the new version we wait for
397                 print "This transition is still ongoing, we currently have version %s" % (current)
398             else:
399                 print "This transition is over, the target package reached testing, should be removed"
400                 print "%s wanted version: %s, has %s" % (source, expected, current)
401         print "-------------------------------------------------------------------------"
402
403 ################################################################################
404
405 def main():
406     global Cnf
407
408     #####################################
409     #### This can run within sudo !! ####
410     #####################################
411     init()
412     
413     # Check if there is a file defined (and existant)
414     transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
415     if transpath == "":
416         daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
417         sys.exit(1)
418     if not os.path.exists(transpath):
419         daklib.utils.warn("ReleaseTransitions file, %s, not found." %
420                           (Cnf["Dinstall::Reject::ReleaseTransitions"]))
421         sys.exit(1)
422    
423     if Options["import"]:
424         try:
425             write_transitions_from_file(Options["import"])
426         except ParseException, m:
427             print m
428             sys.exit(2)
429         sys.exit(0)
430     ##############################################
431     #### Up to here it can run within sudo !! ####
432     ##############################################
433
434     # Parse the yaml file
435     transitions = load_transitions(transpath)
436     if transitions == None:
437         # Something very broken with the transitions, exit
438         daklib.utils.warn("Could not parse existing transitions file. Aborting.")
439         sys.exit(2)
440
441     if Options["edit"]:
442         # Let's edit the transitions file
443         edit_transitions()
444     elif Options["check"]:
445         # Check and remove outdated transitions
446         check_transitions(transitions)
447     else:
448         # Output information about the currently defined transitions.
449         print "Currently defined transitions:"
450         transition_info(transitions)
451
452     sys.exit(0)
453     
454 ################################################################################
455
456 if __name__ == '__main__':
457     main()