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, re
28 import daklib.database
37 re_broken_package = re.compile(r"[a-zA-Z]\w+\s+\-.*")
39 ################################################################################
41 #####################################
42 #### This may run within sudo !! ####
43 #####################################
45 global Cnf, Options, projectB
49 Cnf = daklib.utils.get_conf()
51 Arguments = [('h',"help","Edit-Transitions::Options::Help"),
52 ('e',"edit","Edit-Transitions::Options::Edit"),
53 ('i',"import","Edit-Transitions::Options::Import", "HasArg"),
54 ('c',"check","Edit-Transitions::Options::Check"),
55 ('s',"sudo","Edit-Transitions::Options::Sudo"),
56 ('n',"no-action","Edit-Transitions::Options::No-Action")]
58 for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
59 if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
60 Cnf["Edit-Transitions::Options::%s" % (i)] = ""
62 apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
64 Options = Cnf.SubTree("Edit-Transitions::Options")
70 whoamifull = pwd.getpwuid(whoami)
71 username = whoamifull[0]
73 print "Non-dak user: %s" % username
76 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
77 daklib.database.init(Cnf, projectB)
79 ################################################################################
81 def usage (exit_code=0):
82 print """Usage: transitions [OPTION]...
83 Update and check the release managers transition file.
87 -h, --help show this help and exit.
88 -e, --edit edit the transitions file
89 -i, --import <file> check and import transitions from file
90 -c, --check check the transitions file, remove outdated entries
91 -S, --sudo use sudo to update transitions file
92 -n, --no-action don't do anything (only affects check)"""
96 ################################################################################
98 #####################################
99 #### This may run within sudo !! ####
100 #####################################
101 def load_transitions(trans_file):
102 # Parse the yaml file
103 sourcefile = file(trans_file, 'r')
104 sourcecontent = sourcefile.read()
107 trans = syck.load(sourcecontent)
108 except syck.error, msg:
109 # Someone fucked it up
110 print "ERROR: %s" % (msg)
113 # lets do further validation here
114 checkkeys = ["source", "reason", "packages", "new", "rm"]
118 # First check if we know all the keys for the transition and if they have
119 # the right type (and for the packages also if the list has the right types
120 # included, ie. not a list in list, but only str in the list)
122 if key not in checkkeys:
123 print "ERROR: Unknown key %s in transition %s" % (key, test)
126 if key == "packages":
127 if type(t[key]) != list:
128 print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
131 for package in t["packages"]:
132 if type(package) != str:
133 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
135 if re_broken_package.match(package):
136 # Someone had a space too much (or not enough), we have something looking like
137 # "package1 - package2" now.
138 print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, package)
141 # In case someone has an empty packages list
142 print "ERROR: No packages defined in transition %s" % (test)
146 elif type(t[key]) != str:
147 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
150 # And now the other way round - are all our keys defined?
151 for key in checkkeys:
153 print "ERROR: Missing key %s in transition %s" % (key, test)
161 ################################################################################
163 #####################################
164 #### This may run within sudo !! ####
165 #####################################
167 for retry in range(10):
168 lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
170 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
173 if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
174 print "Unable to get lock for %s (try %d of 10)" % \
180 daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
182 ################################################################################
184 #####################################
185 #### This may run within sudo !! ####
186 #####################################
187 def write_transitions(from_trans):
188 """Update the active transitions file safely.
189 This function takes a parsed input file (which avoids invalid
190 files or files that may be be modified while the function is
191 active), and ensure the transitions file is updated atomically
194 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
195 trans_temp = trans_file + ".tmp"
197 trans_lock = lock_file(trans_file)
198 temp_lock = lock_file(trans_temp)
200 destfile = file(trans_temp, 'w')
201 syck.dump(from_trans, destfile)
204 os.rename(trans_temp, trans_file)
208 ################################################################################
210 class ParseException(Exception):
213 ##########################################
214 #### This usually runs within sudo !! ####
215 ##########################################
216 def write_transitions_from_file(from_file):
217 """We have a file we think is valid; if we're using sudo, we invoke it
218 here, otherwise we just parse the file and call write_transitions"""
220 # Lets check if from_file is in the directory we expect it to be in
221 if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
222 print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
226 os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
227 "/usr/local/bin/dak", "transitions", "--import", from_file)
229 trans = load_transitions(from_file)
231 raise ParseException, "Unparsable transitions file %s" % (file)
232 write_transitions(trans)
234 ################################################################################
236 def temp_transitions_file(transitions):
237 # NB: file is unlinked by caller, but fd is never actually closed.
238 # We need the chmod, as the file is (most possibly) copied from a
239 # sudo-ed script and would be unreadable if it has default mkstemp mode
241 (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
244 syck.dump(transitions, f)
247 ################################################################################
249 def edit_transitions():
250 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
251 edit_file = temp_transitions_file(load_transitions(trans_file))
253 editor = os.environ.get("EDITOR", "vi")
256 result = os.system("%s %s" % (editor, edit_file))
259 daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
261 # Now try to load the new file
262 test = load_transitions(edit_file)
266 print "Edit was unparsable."
267 prompt = "[E]dit again, Drop changes?"
270 print "Edit looks okay.\n"
271 print "The following transitions are defined:"
272 print "------------------------------------------------------------------------"
273 transition_info(test)
275 prompt = "[S]ave, Edit again, Drop changes?"
279 while prompt.find(answer) == -1:
280 answer = daklib.utils.our_raw_input(prompt)
283 answer = answer[:1].upper()
289 print "OK, discarding changes"
295 print "You pressed something you shouldn't have :("
298 # We seem to be done and also have a working file. Copy over.
299 write_transitions_from_file(edit_file)
302 print "Transitions file updated."
304 ################################################################################
306 def check_transitions(transitions):
309 # Now look through all defined transitions
310 for trans in transitions:
311 t = transitions[trans]
315 # Will be None if nothing is in testing.
316 current = daklib.database.get_suite_version(source, "testing")
318 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
321 # No package in testing
322 print "Transition source %s not in testing, transition still ongoing." % (source)
324 compare = apt_pkg.VersionCompare(current, expected)
326 # This is still valid, the current version in database is older than
327 # the new version we wait for
328 print "This transition is still ongoing, we currently have version %s" % (current)
330 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
331 print "%s wanted version: %s, has %s" % (source, expected, current)
332 to_remove.append(trans)
334 print "-------------------------------------------------------------------------"
337 prompt = "Removing: "
338 for remove in to_remove:
342 prompt += " Commit Changes? (y/N)"
345 if Options["no-action"]:
348 answer = daklib.utils.our_raw_input(prompt).lower()
354 print "Not committing changes"
358 for remove in to_remove:
359 del transitions[remove]
361 edit_file = temp_transitions_file(transitions)
362 write_transitions_from_file(edit_file)
366 print "WTF are you typing?"
369 ################################################################################
371 def print_info(trans, source, expected, rm, reason, packages):
372 print """Looking at transition: %s
377 Blocked Packages (total: %d): %s
378 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
381 ################################################################################
383 def transition_info(transitions):
384 for trans in transitions:
385 t = transitions[trans]
389 # Will be None if nothing is in testing.
390 current = daklib.database.get_suite_version(source, "testing")
392 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
395 # No package in testing
396 print "Transition source %s not in testing, transition still ongoing." % (source)
398 compare = apt_pkg.VersionCompare(current, expected)
399 print "Apt compare says: %s" % (compare)
401 # This is still valid, the current version in database is older than
402 # the new version we wait for
403 print "This transition is still ongoing, we currently have version %s" % (current)
405 print "This transition is over, the target package reached testing, should be removed"
406 print "%s wanted version: %s, has %s" % (source, expected, current)
407 print "-------------------------------------------------------------------------"
409 ################################################################################
414 #####################################
415 #### This can run within sudo !! ####
416 #####################################
419 # Check if there is a file defined (and existant)
420 transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
422 daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
424 if not os.path.exists(transpath):
425 daklib.utils.warn("ReleaseTransitions file, %s, not found." %
426 (Cnf["Dinstall::Reject::ReleaseTransitions"]))
428 # Also check if our temp directory is defined and existant
429 temppath = Cnf.get("Transitions::TempPath", "")
431 daklib.utils.warn("Transitions::TempPath not defined")
433 if not os.path.exists(temppath):
434 daklib.utils.warn("Temporary path %s not found." %
435 (Cnf["Transitions::TempPath"]))
438 if Options["import"]:
440 write_transitions_from_file(Options["import"])
441 except ParseException, m:
445 ##############################################
446 #### Up to here it can run within sudo !! ####
447 ##############################################
449 # Parse the yaml file
450 transitions = load_transitions(transpath)
451 if transitions == None:
452 # Something very broken with the transitions, exit
453 daklib.utils.warn("Could not parse existing transitions file. Aborting.")
457 # Let's edit the transitions file
459 elif Options["check"]:
460 # Check and remove outdated transitions
461 check_transitions(transitions)
463 # Output information about the currently defined transitions.
464 print "Currently defined transitions:"
465 transition_info(transitions)
469 ################################################################################
471 if __name__ == '__main__':