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)
130 if re_broken_package.match(key):
131 # Someone had a space too much (or not enough), we have something looking like
132 # "package1 - package2" now.
133 print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, key)
136 for package in t["packages"]:
137 if type(package) != str:
138 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
142 # In case someone has an empty packages list
143 print "ERROR: No packages defined in transition %s" % (test)
147 elif type(t[key]) != str:
148 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
151 # And now the other way round - are all our keys defined?
152 for key in checkkeys:
154 print "ERROR: Missing key %s in transition %s" % (key, test)
162 ################################################################################
164 #####################################
165 #### This may run within sudo !! ####
166 #####################################
168 for retry in range(10):
169 lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
171 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
174 if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
175 print "Unable to get lock for %s (try %d of 10)" % \
181 daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
183 ################################################################################
185 #####################################
186 #### This may run within sudo !! ####
187 #####################################
188 def write_transitions(from_trans):
189 """Update the active transitions file safely.
190 This function takes a parsed input file (which avoids invalid
191 files or files that may be be modified while the function is
192 active), and ensure the transitions file is updated atomically
195 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
196 trans_temp = trans_file + ".tmp"
198 trans_lock = lock_file(trans_file)
199 temp_lock = lock_file(trans_temp)
201 destfile = file(trans_temp, 'w')
202 syck.dump(from_trans, destfile)
205 os.rename(trans_temp, trans_file)
209 ################################################################################
211 class ParseException(Exception):
214 ##########################################
215 #### This usually runs within sudo !! ####
216 ##########################################
217 def write_transitions_from_file(from_file):
218 """We have a file we think is valid; if we're using sudo, we invoke it
219 here, otherwise we just parse the file and call write_transitions"""
221 # Lets check if from_file is in the directory we expect it to be in
222 if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
223 print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
227 os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
228 "/usr/local/bin/dak", "transitions", "--import", from_file)
230 trans = load_transitions(from_file)
232 raise ParseException, "Unparsable transitions file %s" % (file)
233 write_transitions(trans)
235 ################################################################################
237 def temp_transitions_file(transitions):
238 # NB: file is unlinked by caller, but fd is never actually closed.
239 # We need the chmod, as the file is (most possibly) copied from a
240 # sudo-ed script and would be unreadable if it has default mkstemp mode
242 (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
245 syck.dump(transitions, f)
248 ################################################################################
250 def edit_transitions():
251 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
252 edit_file = temp_transitions_file(load_transitions(trans_file))
254 editor = os.environ.get("EDITOR", "vi")
257 result = os.system("%s %s" % (editor, edit_file))
260 daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
262 # Now try to load the new file
263 test = load_transitions(edit_file)
267 print "Edit was unparsable."
268 prompt = "[E]dit again, Drop changes?"
271 print "Edit looks okay.\n"
272 print "The following transitions are defined:"
273 print "------------------------------------------------------------------------"
274 transition_info(test)
276 prompt = "[S]ave, Edit again, Drop changes?"
280 while prompt.find(answer) == -1:
281 answer = daklib.utils.our_raw_input(prompt)
284 answer = answer[:1].upper()
290 print "OK, discarding changes"
296 print "You pressed something you shouldn't have :("
299 # We seem to be done and also have a working file. Copy over.
300 write_transitions_from_file(edit_file)
303 print "Transitions file updated."
305 ################################################################################
307 def check_transitions(transitions):
310 # Now look through all defined transitions
311 for trans in transitions:
312 t = transitions[trans]
316 # Will be None if nothing is in testing.
317 current = daklib.database.get_suite_version(source, "testing")
319 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
322 # No package in testing
323 print "Transition source %s not in testing, transition still ongoing." % (source)
325 compare = apt_pkg.VersionCompare(current, expected)
327 # This is still valid, the current version in database is older than
328 # the new version we wait for
329 print "This transition is still ongoing, we currently have version %s" % (current)
331 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
332 print "%s wanted version: %s, has %s" % (source, expected, current)
333 to_remove.append(trans)
335 print "-------------------------------------------------------------------------"
338 prompt = "Removing: "
339 for remove in to_remove:
343 prompt += " Commit Changes? (y/N)"
346 if Options["no-action"]:
349 answer = daklib.utils.our_raw_input(prompt).lower()
355 print "Not committing changes"
359 for remove in to_remove:
360 del transitions[remove]
362 edit_file = temp_transitions_file(transitions)
363 write_transitions_from_file(edit_file)
367 print "WTF are you typing?"
370 ################################################################################
372 def print_info(trans, source, expected, rm, reason, packages):
373 print """Looking at transition: %s
378 Blocked Packages (total: %d): %s
379 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
382 ################################################################################
384 def transition_info(transitions):
385 for trans in transitions:
386 t = transitions[trans]
390 # Will be None if nothing is in testing.
391 current = daklib.database.get_suite_version(source, "testing")
393 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
396 # No package in testing
397 print "Transition source %s not in testing, transition still ongoing." % (source)
399 compare = apt_pkg.VersionCompare(current, expected)
400 print "Apt compare says: %s" % (compare)
402 # This is still valid, the current version in database is older than
403 # the new version we wait for
404 print "This transition is still ongoing, we currently have version %s" % (current)
406 print "This transition is over, the target package reached testing, should be removed"
407 print "%s wanted version: %s, has %s" % (source, expected, current)
408 print "-------------------------------------------------------------------------"
410 ################################################################################
415 #####################################
416 #### This can run within sudo !! ####
417 #####################################
420 # Check if there is a file defined (and existant)
421 transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
423 daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
425 if not os.path.exists(transpath):
426 daklib.utils.warn("ReleaseTransitions file, %s, not found." %
427 (Cnf["Dinstall::Reject::ReleaseTransitions"]))
429 # Also check if our temp directory is defined and existant
430 temppath = Cnf.get("Transitions::TempPath", "")
432 daklib.utils.warn("Transitions::TempPath not defined")
434 if not os.path.exists(temppath):
435 daklib.utils.warn("Temporary path %s not found." %
436 (Cnf["Transitions::TempPath"]))
439 if Options["import"]:
441 write_transitions_from_file(Options["import"])
442 except ParseException, m:
446 ##############################################
447 #### Up to here it can run within sudo !! ####
448 ##############################################
450 # Parse the yaml file
451 transitions = load_transitions(transpath)
452 if transitions == None:
453 # Something very broken with the transitions, exit
454 daklib.utils.warn("Could not parse existing transitions file. Aborting.")
458 # Let's edit the transitions file
460 elif Options["check"]:
461 # Check and remove outdated transitions
462 check_transitions(transitions)
464 # Output information about the currently defined transitions.
465 print "Currently defined transitions:"
466 transition_info(transitions)
470 ################################################################################
472 if __name__ == '__main__':