3 # Display, edit and check the release manager's transition file.
4 # Copyright (C) 2008 Joerg Jaspert <joerg@debian.org>
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.
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.
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
20 ################################################################################
22 # <elmo> if klecker.d.o died, I swear to god, I'm going to migrate to gentoo.
24 ################################################################################
26 import os, pg, sys, time, errno, fcntl, tempfile, pwd
28 import daklib.database
37 ################################################################################
39 #####################################
40 #### This may run within sudo !! ####
41 #####################################
43 global Cnf, Options, projectB
47 Cnf = daklib.utils.get_conf()
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")]
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)] = ""
60 apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
62 Options = Cnf.SubTree("Edit-Transitions::Options")
68 whoamifull = pwd.getpwuid(whoami)
69 username = whoamifull[0]
71 print "Non-dak user: %s" % username
74 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
75 daklib.database.init(Cnf, projectB)
77 ################################################################################
79 def usage (exit_code=0):
80 print """Usage: transitions [OPTION]...
81 Update and check the release managers transition file.
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)"""
94 ################################################################################
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()
105 trans = syck.load(sourcecontent)
106 except syck.error, msg:
107 # Someone fucked it up
108 print "ERROR: %s" % (msg)
111 # lets do further validation here
112 checkkeys = ["source", "reason", "packages", "new", "rm"]
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)
120 if key not in checkkeys:
121 print "ERROR: Unknown key %s in transition %s" % (key, test)
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)
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)
135 # In case someone has an empty packages list
136 print "ERROR: No packages defined in transition %s" % (test)
140 elif type(t[key]) != str:
141 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
144 # And now the other way round - are all our keys defined?
145 for key in checkkeys:
147 print "ERROR: Missing key %s in transition %s" % (key, test)
155 ################################################################################
157 #####################################
158 #### This may run within sudo !! ####
159 #####################################
161 for retry in range(10):
162 lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
164 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
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)" % \
174 daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
176 ################################################################################
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
188 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
189 trans_temp = trans_file + ".tmp"
191 trans_lock = lock_file(trans_file)
192 temp_lock = lock_file(trans_temp)
194 destfile = file(trans_temp, 'w')
195 syck.dump(from_trans, destfile)
198 os.rename(trans_temp, trans_file)
202 ################################################################################
204 class ParseException(Exception):
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"""
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"])
220 os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
221 "/usr/local/bin/dak", "transitions", "--import", from_file)
223 trans = load_transitions(from_file)
225 raise ParseException, "Unparsable transitions file %s" % (file)
226 write_transitions(trans)
228 ################################################################################
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
235 (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
238 syck.dump(transitions, f)
241 ################################################################################
243 def edit_transitions():
244 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
245 edit_file = temp_transitions_file(load_transitions(trans_file))
247 editor = os.environ.get("EDITOR", "vi")
250 result = os.system("%s %s" % (editor, edit_file))
253 daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
255 # Now try to load the new file
256 test = load_transitions(edit_file)
260 print "Edit was unparsable."
261 prompt = "[E]dit again, Drop changes?"
264 print "Edit looks okay.\n"
265 print "The following transitions are defined:"
266 print "------------------------------------------------------------------------"
267 transition_info(test)
269 prompt = "[S]ave, Edit again, Drop changes?"
273 while prompt.find(answer) == -1:
274 answer = daklib.utils.our_raw_input(prompt)
277 answer = answer[:1].upper()
283 print "OK, discarding changes"
289 print "You pressed something you shouldn't have :("
292 # We seem to be done and also have a working file. Copy over.
293 write_transitions_from_file(edit_file)
296 print "Transitions file updated."
298 ################################################################################
300 def check_transitions(transitions):
303 # Now look through all defined transitions
304 for trans in transitions:
305 t = transitions[trans]
309 # Will be None if nothing is in testing.
310 current = daklib.database.get_suite_version(source, "testing")
312 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
315 # No package in testing
316 print "Transition source %s not in testing, transition still ongoing." % (source)
318 compare = apt_pkg.VersionCompare(current, expected)
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)
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)
328 print "-------------------------------------------------------------------------"
331 prompt = "Removing: "
332 for remove in to_remove:
336 prompt += " Commit Changes? (y/N)"
339 if Options["no-action"]:
342 answer = daklib.utils.our_raw_input(prompt).lower()
348 print "Not committing changes"
352 for remove in to_remove:
353 del transitions[remove]
355 edit_file = temp_transitions_file(transitions)
356 write_transitions_from_file(edit_file)
360 print "WTF are you typing?"
363 ################################################################################
365 def print_info(trans, source, expected, rm, reason, packages):
366 print """Looking at transition: %s
371 Blocked Packages (total: %d): %s
372 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
375 ################################################################################
377 def transition_info(transitions):
378 for trans in transitions:
379 t = transitions[trans]
383 # Will be None if nothing is in testing.
384 current = daklib.database.get_suite_version(source, "testing")
386 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
389 # No package in testing
390 print "Transition source %s not in testing, transition still ongoing." % (source)
392 compare = apt_pkg.VersionCompare(current, expected)
393 print "Apt compare says: %s" % (compare)
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)
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 "-------------------------------------------------------------------------"
403 ################################################################################
408 #####################################
409 #### This can run within sudo !! ####
410 #####################################
413 # Check if there is a file defined (and existant)
414 transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
416 daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
418 if not os.path.exists(transpath):
419 daklib.utils.warn("ReleaseTransitions file, %s, not found." %
420 (Cnf["Dinstall::Reject::ReleaseTransitions"]))
422 # Also check if our temp directory is defined and existant
423 temppath = Cnf.get("Transitions::TempPath", "")
425 daklib.utils.warn("Transitions::TempPath not defined")
427 if not os.path.exists(temppath):
428 daklib.utils.warn("Temporary path %s not found." %
429 (Cnf["Transitions::TempPath"]))
432 if Options["import"]:
434 write_transitions_from_file(Options["import"])
435 except ParseException, m:
439 ##############################################
440 #### Up to here it can run within sudo !! ####
441 ##############################################
443 # Parse the yaml file
444 transitions = load_transitions(transpath)
445 if transitions == None:
446 # Something very broken with the transitions, exit
447 daklib.utils.warn("Could not parse existing transitions file. Aborting.")
451 # Let's edit the transitions file
453 elif Options["check"]:
454 # Check and remove outdated transitions
455 check_transitions(transitions)
457 # Output information about the currently defined transitions.
458 print "Currently defined transitions:"
459 transition_info(transitions)
463 ################################################################################
465 if __name__ == '__main__':