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
129 # First check if we know all the keys for the transition and if they have
130 # the right type (and for the packages also if the list has the right types
131 # included, ie. not a list in list, but only str in the list)
133 if key not in checkkeys:
134 print "ERROR: Unknown key %s in transition %s" % (key, test)
137 if key == "packages":
138 if type(t[key]) != list:
139 print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
142 for package in t["packages"]:
143 if type(package) != str:
144 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
146 if re_broken_package.match(package):
147 # Someone had a space too much (or not enough), we have something looking like
148 # "package1 - package2" now.
149 print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, package)
152 # In case someone has an empty packages list
153 print "ERROR: No packages defined in transition %s" % (test)
157 elif type(t[key]) != str:
158 if key == "new" and type(t[key]) == int:
159 # Ok, debian native version
162 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
165 # And now the other way round - are all our keys defined?
166 for key in checkkeys:
168 print "ERROR: Missing key %s in transition %s" % (key, test)
171 # In case someone defined very broken things
172 print "ERROR: Unable to parse the file"
181 ################################################################################
183 #####################################
184 #### This may run within sudo !! ####
185 #####################################
187 for retry in range(10):
188 lock_fd = os.open(f, os.O_RDWR | os.O_CREAT)
190 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
193 if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
194 print "Unable to get lock for %s (try %d of 10)" % \
200 daklib.utils.fubar("Couldn't obtain lock for %s." % (f))
202 ################################################################################
204 #####################################
205 #### This may run within sudo !! ####
206 #####################################
207 def write_transitions(from_trans):
208 """Update the active transitions file safely.
209 This function takes a parsed input file (which avoids invalid
210 files or files that may be be modified while the function is
211 active), and ensure the transitions file is updated atomically
214 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
215 trans_temp = trans_file + ".tmp"
217 trans_lock = lock_file(trans_file)
218 temp_lock = lock_file(trans_temp)
220 destfile = file(trans_temp, 'w')
221 syck.dump(from_trans, destfile)
224 os.rename(trans_temp, trans_file)
228 ################################################################################
230 class ParseException(Exception):
233 ##########################################
234 #### This usually runs within sudo !! ####
235 ##########################################
236 def write_transitions_from_file(from_file):
237 """We have a file we think is valid; if we're using sudo, we invoke it
238 here, otherwise we just parse the file and call write_transitions"""
240 # Lets check if from_file is in the directory we expect it to be in
241 if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
242 print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
246 os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
247 "/usr/local/bin/dak", "transitions", "--import", from_file)
249 trans = load_transitions(from_file)
251 raise ParseException, "Unparsable transitions file %s" % (file)
252 write_transitions(trans)
254 ################################################################################
256 def temp_transitions_file(transitions):
257 # NB: file is unlinked by caller, but fd is never actually closed.
258 # We need the chmod, as the file is (most possibly) copied from a
259 # sudo-ed script and would be unreadable if it has default mkstemp mode
261 (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
264 syck.dump(transitions, f)
267 ################################################################################
269 def edit_transitions():
270 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
271 edit_file = temp_transitions_file(load_transitions(trans_file))
273 editor = os.environ.get("EDITOR", "vi")
276 result = os.system("%s %s" % (editor, edit_file))
279 daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
281 # Now try to load the new file
282 test = load_transitions(edit_file)
286 print "Edit was unparsable."
287 prompt = "[E]dit again, Drop changes?"
290 print "Edit looks okay.\n"
291 print "The following transitions are defined:"
292 print "------------------------------------------------------------------------"
293 transition_info(test)
295 prompt = "[S]ave, Edit again, Drop changes?"
299 while prompt.find(answer) == -1:
300 answer = daklib.utils.our_raw_input(prompt)
303 answer = answer[:1].upper()
309 print "OK, discarding changes"
315 print "You pressed something you shouldn't have :("
318 # We seem to be done and also have a working file. Copy over.
319 write_transitions_from_file(edit_file)
322 print "Transitions file updated."
324 ################################################################################
326 def check_transitions(transitions):
329 # Now look through all defined transitions
330 for trans in transitions:
331 t = transitions[trans]
335 # Will be None if nothing is in testing.
336 current = daklib.database.get_suite_version(source, "testing")
338 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
341 # No package in testing
342 print "Transition source %s not in testing, transition still ongoing." % (source)
344 compare = apt_pkg.VersionCompare(current, expected)
346 # This is still valid, the current version in database is older than
347 # the new version we wait for
348 print "This transition is still ongoing, we currently have version %s" % (current)
350 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
351 print "%s wanted version: %s, has %s" % (source, expected, current)
352 to_remove.append(trans)
354 print "-------------------------------------------------------------------------"
357 prompt = "Removing: "
358 for remove in to_remove:
362 prompt += " Commit Changes? (y/N)"
365 if Options["no-action"]:
368 answer = daklib.utils.our_raw_input(prompt).lower()
374 print "Not committing changes"
378 for remove in to_remove:
379 del transitions[remove]
381 edit_file = temp_transitions_file(transitions)
382 write_transitions_from_file(edit_file)
386 print "WTF are you typing?"
389 ################################################################################
391 def print_info(trans, source, expected, rm, reason, packages):
392 print """Looking at transition: %s
397 Blocked Packages (total: %d): %s
398 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
401 ################################################################################
403 def transition_info(transitions):
404 for trans in transitions:
405 t = transitions[trans]
409 # Will be None if nothing is in testing.
410 current = daklib.database.get_suite_version(source, "testing")
412 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
415 # No package in testing
416 print "Transition source %s not in testing, transition still ongoing." % (source)
418 compare = apt_pkg.VersionCompare(current, expected)
419 print "Apt compare says: %s" % (compare)
421 # This is still valid, the current version in database is older than
422 # the new version we wait for
423 print "This transition is still ongoing, we currently have version %s" % (current)
425 print "This transition is over, the target package reached testing, should be removed"
426 print "%s wanted version: %s, has %s" % (source, expected, current)
427 print "-------------------------------------------------------------------------"
429 ################################################################################
434 #####################################
435 #### This can run within sudo !! ####
436 #####################################
439 # Check if there is a file defined (and existant)
440 transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
442 daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
444 if not os.path.exists(transpath):
445 daklib.utils.warn("ReleaseTransitions file, %s, not found." %
446 (Cnf["Dinstall::Reject::ReleaseTransitions"]))
448 # Also check if our temp directory is defined and existant
449 temppath = Cnf.get("Transitions::TempPath", "")
451 daklib.utils.warn("Transitions::TempPath not defined")
453 if not os.path.exists(temppath):
454 daklib.utils.warn("Temporary path %s not found." %
455 (Cnf["Transitions::TempPath"]))
458 if Options["import"]:
460 write_transitions_from_file(Options["import"])
461 except ParseException, m:
465 ##############################################
466 #### Up to here it can run within sudo !! ####
467 ##############################################
469 # Parse the yaml file
470 transitions = load_transitions(transpath)
471 if transitions == None:
472 # Something very broken with the transitions, exit
473 daklib.utils.warn("Could not parse existing transitions file. Aborting.")
477 # Let's edit the transitions file
479 elif Options["check"]:
480 # Check and remove outdated transitions
481 check_transitions(transitions)
483 # Output information about the currently defined transitions.
484 print "Currently defined transitions:"
485 transition_info(transitions)
489 ################################################################################
491 if __name__ == '__main__':