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 from daklib import database
29 from daklib import utils
30 from daklib.dak_exceptions import TransitionsError
38 re_broken_package = re.compile(r"[a-zA-Z]\w+\s+\-.*")
40 ################################################################################
42 #####################################
43 #### This may run within sudo !! ####
44 #####################################
46 global Cnf, Options, projectB
50 Cnf = utils.get_conf()
52 Arguments = [('h',"help","Edit-Transitions::Options::Help"),
53 ('e',"edit","Edit-Transitions::Options::Edit"),
54 ('i',"import","Edit-Transitions::Options::Import", "HasArg"),
55 ('c',"check","Edit-Transitions::Options::Check"),
56 ('s',"sudo","Edit-Transitions::Options::Sudo"),
57 ('n',"no-action","Edit-Transitions::Options::No-Action")]
59 for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
60 if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
61 Cnf["Edit-Transitions::Options::%s" % (i)] = ""
63 apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
65 Options = Cnf.SubTree("Edit-Transitions::Options")
71 whoamifull = pwd.getpwuid(whoami)
72 username = whoamifull[0]
74 print "Non-dak user: %s" % username
77 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
78 database.init(Cnf, projectB)
80 ################################################################################
82 def usage (exit_code=0):
83 print """Usage: transitions [OPTION]...
84 Update and check the release managers transition file.
88 -h, --help show this help and exit.
89 -e, --edit edit the transitions file
90 -i, --import <file> check and import transitions from file
91 -c, --check check the transitions file, remove outdated entries
92 -S, --sudo use sudo to update transitions file
93 -n, --no-action don't do anything (only affects check)"""
97 ################################################################################
99 #####################################
100 #### This may run within sudo !! ####
101 #####################################
102 def load_transitions(trans_file):
103 # Parse the yaml file
104 sourcefile = file(trans_file, 'r')
105 sourcecontent = sourcefile.read()
108 trans = yaml.load(sourcecontent)
109 except yaml.YAMLError, exc:
110 # Someone fucked it up
111 print "ERROR: %s" % (exc)
114 # lets do further validation here
115 checkkeys = ["source", "reason", "packages", "new", "rm"]
117 # If we get an empty definition - we just have nothing to check, no transitions defined
118 if type(trans) != dict:
119 # This can be anything. We could have no transitions defined. Or someone totally fucked up the
120 # file, adding stuff in a way we dont know or want. Then we set it empty - and simply have no
121 # transitions anymore. User will see it in the information display after he quit the editor and
130 # First check if we know all the keys for the transition and if they have
131 # the right type (and for the packages also if the list has the right types
132 # included, ie. not a list in list, but only str in the list)
134 if key not in checkkeys:
135 print "ERROR: Unknown key %s in transition %s" % (key, test)
138 if key == "packages":
139 if type(t[key]) != list:
140 print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
143 for package in t["packages"]:
144 if type(package) != str:
145 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
147 if re_broken_package.match(package):
148 # Someone had a space too much (or not enough), we have something looking like
149 # "package1 - package2" now.
150 print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, package)
153 # In case someone has an empty packages list
154 print "ERROR: No packages defined in transition %s" % (test)
158 elif type(t[key]) != str:
159 if key == "new" and type(t[key]) == int:
160 # Ok, debian native version
163 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
166 # And now the other way round - are all our keys defined?
167 for key in checkkeys:
169 print "ERROR: Missing key %s in transition %s" % (key, test)
172 # In case someone defined very broken things
173 print "ERROR: Unable to parse the file"
182 ################################################################################
184 #####################################
185 #### This may run within sudo !! ####
186 #####################################
188 for retry in range(10):
189 lock_fd = os.open(f, os.O_RDWR | os.O_CREAT)
191 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
194 if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
195 print "Unable to get lock for %s (try %d of 10)" % \
201 utils.fubar("Couldn't obtain lock for %s." % (f))
203 ################################################################################
205 #####################################
206 #### This may run within sudo !! ####
207 #####################################
208 def write_transitions(from_trans):
209 """Update the active transitions file safely.
210 This function takes a parsed input file (which avoids invalid
211 files or files that may be be modified while the function is
212 active), and ensure the transitions file is updated atomically
215 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
216 trans_temp = trans_file + ".tmp"
218 trans_lock = lock_file(trans_file)
219 temp_lock = lock_file(trans_temp)
221 destfile = file(trans_temp, 'w')
222 yaml.dump(from_trans, destfile, default_flow_style=False)
225 os.rename(trans_temp, trans_file)
229 ################################################################################
231 ##########################################
232 #### This usually runs within sudo !! ####
233 ##########################################
234 def write_transitions_from_file(from_file):
235 """We have a file we think is valid; if we're using sudo, we invoke it
236 here, otherwise we just parse the file and call write_transitions"""
238 # Lets check if from_file is in the directory we expect it to be in
239 if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
240 print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
244 os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
245 "/usr/local/bin/dak", "transitions", "--import", from_file)
247 trans = load_transitions(from_file)
249 raise TransitionsError, "Unparsable transitions file %s" % (file)
250 write_transitions(trans)
252 ################################################################################
254 def temp_transitions_file(transitions):
255 # NB: file is unlinked by caller, but fd is never actually closed.
256 # We need the chmod, as the file is (most possibly) copied from a
257 # sudo-ed script and would be unreadable if it has default mkstemp mode
259 (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
262 yaml.dump(transitions, f, default_flow_style=False)
265 ################################################################################
267 def edit_transitions():
268 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
269 edit_file = temp_transitions_file(load_transitions(trans_file))
271 editor = os.environ.get("EDITOR", "vi")
274 result = os.system("%s %s" % (editor, edit_file))
277 utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
279 # Now try to load the new file
280 test = load_transitions(edit_file)
284 print "Edit was unparsable."
285 prompt = "[E]dit again, Drop changes?"
288 print "Edit looks okay.\n"
289 print "The following transitions are defined:"
290 print "------------------------------------------------------------------------"
291 transition_info(test)
293 prompt = "[S]ave, Edit again, Drop changes?"
297 while prompt.find(answer) == -1:
298 answer = utils.our_raw_input(prompt)
301 answer = answer[:1].upper()
307 print "OK, discarding changes"
313 print "You pressed something you shouldn't have :("
316 # We seem to be done and also have a working file. Copy over.
317 write_transitions_from_file(edit_file)
320 print "Transitions file updated."
322 ################################################################################
324 def check_transitions(transitions):
327 # Now look through all defined transitions
328 for trans in transitions:
329 t = transitions[trans]
333 # Will be None if nothing is in testing.
334 current = database.get_suite_version(source, "testing")
336 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
339 # No package in testing
340 print "Transition source %s not in testing, transition still ongoing." % (source)
342 compare = apt_pkg.VersionCompare(current, expected)
344 # This is still valid, the current version in database is older than
345 # the new version we wait for
346 print "This transition is still ongoing, we currently have version %s" % (current)
348 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
349 print "%s wanted version: %s, has %s" % (source, expected, current)
350 to_remove.append(trans)
352 print "-------------------------------------------------------------------------"
355 prompt = "Removing: "
356 for remove in to_remove:
360 prompt += " Commit Changes? (y/N)"
363 if Options["no-action"]:
366 answer = utils.our_raw_input(prompt).lower()
372 print "Not committing changes"
376 for remove in to_remove:
377 del transitions[remove]
379 edit_file = temp_transitions_file(transitions)
380 write_transitions_from_file(edit_file)
384 print "WTF are you typing?"
387 ################################################################################
389 def print_info(trans, source, expected, rm, reason, packages):
390 print """Looking at transition: %s
395 Blocked Packages (total: %d): %s
396 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
399 ################################################################################
401 def transition_info(transitions):
402 for trans in transitions:
403 t = transitions[trans]
407 # Will be None if nothing is in testing.
408 current = database.get_suite_version(source, "testing")
410 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
413 # No package in testing
414 print "Transition source %s not in testing, transition still ongoing." % (source)
416 compare = apt_pkg.VersionCompare(current, expected)
417 print "Apt compare says: %s" % (compare)
419 # This is still valid, the current version in database is older than
420 # the new version we wait for
421 print "This transition is still ongoing, we currently have version %s" % (current)
423 print "This transition is over, the target package reached testing, should be removed"
424 print "%s wanted version: %s, has %s" % (source, expected, current)
425 print "-------------------------------------------------------------------------"
427 ################################################################################
432 #####################################
433 #### This can run within sudo !! ####
434 #####################################
437 # Check if there is a file defined (and existant)
438 transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
440 utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
442 if not os.path.exists(transpath):
443 utils.warn("ReleaseTransitions file, %s, not found." %
444 (Cnf["Dinstall::Reject::ReleaseTransitions"]))
446 # Also check if our temp directory is defined and existant
447 temppath = Cnf.get("Transitions::TempPath", "")
449 utils.warn("Transitions::TempPath not defined")
451 if not os.path.exists(temppath):
452 utils.warn("Temporary path %s not found." %
453 (Cnf["Transitions::TempPath"]))
456 if Options["import"]:
458 write_transitions_from_file(Options["import"])
459 except TransitionsError, m:
463 ##############################################
464 #### Up to here it can run within sudo !! ####
465 ##############################################
467 # Parse the yaml file
468 transitions = load_transitions(transpath)
469 if transitions == None:
470 # Something very broken with the transitions, exit
471 utils.warn("Could not parse existing transitions file. Aborting.")
475 # Let's edit the transitions file
477 elif Options["check"]:
478 # Check and remove outdated transitions
479 check_transitions(transitions)
481 # Output information about the currently defined transitions.
482 print "Currently defined transitions:"
483 transition_info(transitions)
487 ################################################################################
489 if __name__ == '__main__':