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"]
116 # If we get an empty definition - we just have nothing to check, no transitions defined
117 if type(trans) != dict:
118 # This can be anything. We could have no transitions defined. Or someone totally fucked up the
119 # file, adding stuff in a way we dont know or want. Then we set it empty - and simply have no
120 # transitions anymore. User will see it in the information display after he quit the editor and
128 # First check if we know all the keys for the transition and if they have
129 # the right type (and for the packages also if the list has the right types
130 # included, ie. not a list in list, but only str in the list)
132 if key not in checkkeys:
133 print "ERROR: Unknown key %s in transition %s" % (key, test)
136 if key == "packages":
137 if type(t[key]) != list:
138 print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
141 for package in t["packages"]:
142 if type(package) != str:
143 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
145 if re_broken_package.match(package):
146 # Someone had a space too much (or not enough), we have something looking like
147 # "package1 - package2" now.
148 print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, package)
151 # In case someone has an empty packages list
152 print "ERROR: No packages defined in transition %s" % (test)
156 elif type(t[key]) != str:
157 if t[key] == "new" and type(t[key]) == int:
158 # Ok, debian native version
160 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
163 # And now the other way round - are all our keys defined?
164 for key in checkkeys:
166 print "ERROR: Missing key %s in transition %s" % (key, test)
174 ################################################################################
176 #####################################
177 #### This may run within sudo !! ####
178 #####################################
180 for retry in range(10):
181 lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
183 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
186 if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
187 print "Unable to get lock for %s (try %d of 10)" % \
193 daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
195 ################################################################################
197 #####################################
198 #### This may run within sudo !! ####
199 #####################################
200 def write_transitions(from_trans):
201 """Update the active transitions file safely.
202 This function takes a parsed input file (which avoids invalid
203 files or files that may be be modified while the function is
204 active), and ensure the transitions file is updated atomically
207 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
208 trans_temp = trans_file + ".tmp"
210 trans_lock = lock_file(trans_file)
211 temp_lock = lock_file(trans_temp)
213 destfile = file(trans_temp, 'w')
214 syck.dump(from_trans, destfile)
217 os.rename(trans_temp, trans_file)
221 ################################################################################
223 class ParseException(Exception):
226 ##########################################
227 #### This usually runs within sudo !! ####
228 ##########################################
229 def write_transitions_from_file(from_file):
230 """We have a file we think is valid; if we're using sudo, we invoke it
231 here, otherwise we just parse the file and call write_transitions"""
233 # Lets check if from_file is in the directory we expect it to be in
234 if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
235 print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
239 os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
240 "/usr/local/bin/dak", "transitions", "--import", from_file)
242 trans = load_transitions(from_file)
244 raise ParseException, "Unparsable transitions file %s" % (file)
245 write_transitions(trans)
247 ################################################################################
249 def temp_transitions_file(transitions):
250 # NB: file is unlinked by caller, but fd is never actually closed.
251 # We need the chmod, as the file is (most possibly) copied from a
252 # sudo-ed script and would be unreadable if it has default mkstemp mode
254 (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
257 syck.dump(transitions, f)
260 ################################################################################
262 def edit_transitions():
263 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
264 edit_file = temp_transitions_file(load_transitions(trans_file))
266 editor = os.environ.get("EDITOR", "vi")
269 result = os.system("%s %s" % (editor, edit_file))
272 daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
274 # Now try to load the new file
275 test = load_transitions(edit_file)
279 print "Edit was unparsable."
280 prompt = "[E]dit again, Drop changes?"
283 print "Edit looks okay.\n"
284 print "The following transitions are defined:"
285 print "------------------------------------------------------------------------"
286 transition_info(test)
288 prompt = "[S]ave, Edit again, Drop changes?"
292 while prompt.find(answer) == -1:
293 answer = daklib.utils.our_raw_input(prompt)
296 answer = answer[:1].upper()
302 print "OK, discarding changes"
308 print "You pressed something you shouldn't have :("
311 # We seem to be done and also have a working file. Copy over.
312 write_transitions_from_file(edit_file)
315 print "Transitions file updated."
317 ################################################################################
319 def check_transitions(transitions):
322 # Now look through all defined transitions
323 for trans in transitions:
324 t = transitions[trans]
328 # Will be None if nothing is in testing.
329 current = daklib.database.get_suite_version(source, "testing")
331 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
334 # No package in testing
335 print "Transition source %s not in testing, transition still ongoing." % (source)
337 compare = apt_pkg.VersionCompare(current, expected)
339 # This is still valid, the current version in database is older than
340 # the new version we wait for
341 print "This transition is still ongoing, we currently have version %s" % (current)
343 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
344 print "%s wanted version: %s, has %s" % (source, expected, current)
345 to_remove.append(trans)
347 print "-------------------------------------------------------------------------"
350 prompt = "Removing: "
351 for remove in to_remove:
355 prompt += " Commit Changes? (y/N)"
358 if Options["no-action"]:
361 answer = daklib.utils.our_raw_input(prompt).lower()
367 print "Not committing changes"
371 for remove in to_remove:
372 del transitions[remove]
374 edit_file = temp_transitions_file(transitions)
375 write_transitions_from_file(edit_file)
379 print "WTF are you typing?"
382 ################################################################################
384 def print_info(trans, source, expected, rm, reason, packages):
385 print """Looking at transition: %s
390 Blocked Packages (total: %d): %s
391 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
394 ################################################################################
396 def transition_info(transitions):
397 for trans in transitions:
398 t = transitions[trans]
402 # Will be None if nothing is in testing.
403 current = daklib.database.get_suite_version(source, "testing")
405 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
408 # No package in testing
409 print "Transition source %s not in testing, transition still ongoing." % (source)
411 compare = apt_pkg.VersionCompare(current, expected)
412 print "Apt compare says: %s" % (compare)
414 # This is still valid, the current version in database is older than
415 # the new version we wait for
416 print "This transition is still ongoing, we currently have version %s" % (current)
418 print "This transition is over, the target package reached testing, should be removed"
419 print "%s wanted version: %s, has %s" % (source, expected, current)
420 print "-------------------------------------------------------------------------"
422 ################################################################################
427 #####################################
428 #### This can run within sudo !! ####
429 #####################################
432 # Check if there is a file defined (and existant)
433 transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
435 daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
437 if not os.path.exists(transpath):
438 daklib.utils.warn("ReleaseTransitions file, %s, not found." %
439 (Cnf["Dinstall::Reject::ReleaseTransitions"]))
441 # Also check if our temp directory is defined and existant
442 temppath = Cnf.get("Transitions::TempPath", "")
444 daklib.utils.warn("Transitions::TempPath not defined")
446 if not os.path.exists(temppath):
447 daklib.utils.warn("Temporary path %s not found." %
448 (Cnf["Transitions::TempPath"]))
451 if Options["import"]:
453 write_transitions_from_file(Options["import"])
454 except ParseException, m:
458 ##############################################
459 #### Up to here it can run within sudo !! ####
460 ##############################################
462 # Parse the yaml file
463 transitions = load_transitions(transpath)
464 if transitions == None:
465 # Something very broken with the transitions, exit
466 daklib.utils.warn("Could not parse existing transitions file. Aborting.")
470 # Let's edit the transitions file
472 elif Options["check"]:
473 # Check and remove outdated transitions
474 check_transitions(transitions)
476 # Output information about the currently defined transitions.
477 print "Currently defined transitions:"
478 transition_info(transitions)
482 ################################################################################
484 if __name__ == '__main__':