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
28 import daklib.database
37 ################################################################################
40 global Cnf, Options, projectB
44 Cnf = daklib.utils.get_conf()
46 Arguments = [('h',"help","Edit-Transitions::Options::Help"),
47 ('e',"edit","Edit-Transitions::Options::Edit"),
48 ('i',"import","Edit-Transitions::Options::Import", "HasArg"),
49 ('c',"check","Edit-Transitions::Options::Check"),
50 ('s',"sudo","Edit-Transitions::Options::Sudo"),
51 ('n',"no-action","Edit-Transitions::Options::No-Action")]
53 for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
54 if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
55 Cnf["Edit-Transitions::Options::%s" % (i)] = ""
57 apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
59 Options = Cnf.SubTree("Edit-Transitions::Options")
65 whoamifull = pwd.getpwuid(whoami)
66 username = whoamifull[0]
68 print "Non-dak user: %s" % username
71 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
72 daklib.database.init(Cnf, projectB)
74 ################################################################################
76 def usage (exit_code=0):
77 print """Usage: transitions [OPTION]...
78 Update and check the release managers transition file.
82 -h, --help show this help and exit.
83 -e, --edit edit the transitions file
84 -i, --import <file> check and import transitions from file
85 -c, --check check the transitions file, remove outdated entries
86 -S, --sudo use sudo to update transitions file
87 -n, --no-action don't do anything (only affects check)"""
91 ################################################################################
93 #####################################
94 #### This may run within sudo !! ####
95 #####################################
96 def load_transitions(trans_file):
98 sourcefile = file(trans_file, 'r')
99 sourcecontent = sourcefile.read()
102 trans = syck.load(sourcecontent)
103 except syck.error, msg:
104 # Someone fucked it up
105 print "ERROR: %s" % (msg)
108 # lets do further validation here
109 checkkeys = ["source", "reason", "packages", "new", "rm"]
113 # First check if we know all the keys for the transition and if they have
114 # the right type (and for the packages also if the list has the right types
115 # included, ie. not a list in list, but only str in the list)
117 if key not in checkkeys:
118 print "ERROR: Unknown key %s in transition %s" % (key, test)
121 if key == "packages":
122 if type(t[key]) != list:
123 print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
127 for package in t["packages"]:
128 if type(package) != str:
129 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
132 # In case someone has an empty packages list
133 print "ERROR: No packages defined in transition %s" % (test)
137 elif type(t[key]) != str:
138 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
141 # And now the other way round - are all our keys defined?
142 for key in checkkeys:
144 print "ERROR: Missing key %s in transition %s" % (key, test)
152 ################################################################################
154 #####################################
155 #### This may run within sudo !! ####
156 #####################################
158 for retry in range(10):
159 lock_fd = os.open(file, os.O_RDWR | os.O_CREAT)
161 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
164 if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
165 print "Unable to get lock for %s (try %d of 10)" % \
171 daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile))
173 ################################################################################
175 #####################################
176 #### This may run within sudo !! ####
177 #####################################
178 def write_transitions(from_trans):
179 """Update the active transitions file safely.
180 This function takes a parsed input file (which avoids invalid
181 files or files that may be be modified while the function is
182 active), and ensure the transitions file is updated atomically
185 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
186 trans_temp = trans_file + ".tmp"
188 trans_lock = lock_file(trans_file)
189 temp_lock = lock_file(trans_temp)
191 destfile = file(trans_temp, 'w')
192 syck.dump(from_trans, destfile)
195 os.rename(trans_temp, trans_file)
199 ################################################################################
201 class ParseException(Exception):
204 ##########################################
205 #### This usually runs within sudo !! ####
206 ##########################################
207 def write_transitions_from_file(from_file):
208 """We have a file we think is valid; if we're using sudo, we invoke it
209 here, otherwise we just parse the file and call write_transitions"""
212 os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
213 "/usr/local/bin/dak", "transitions", "--import", from_file)
215 trans = load_transitions(from_file)
217 raise ParseException, "Unparsable transitions file %s" % (file)
218 write_transitions(trans)
220 ################################################################################
222 def temp_transitions_file(transitions):
223 # NB: file is unlinked by caller, but fd is never actually closed.
224 # We need the chmod, as the file is (most possibly) copied from a
225 # sudo-ed script and would be unreadable if it has default mkstemp mode
227 (fd, path) = tempfile.mkstemp("","transitions")
230 syck.dump(transitions, f)
233 ################################################################################
235 def edit_transitions():
236 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
237 edit_file = temp_transitions_file(load_transitions(trans_file))
239 editor = os.environ.get("EDITOR", "vi")
242 result = os.system("%s %s" % (editor, edit_file))
245 daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
247 # Now try to load the new file
248 test = load_transitions(edit_file)
252 print "Edit was unparsable."
253 prompt = "[E]dit again, Drop changes?"
256 print "Edit looks okay.\n"
257 print "The following transitions are defined:"
258 print "------------------------------------------------------------------------"
259 transition_info(test)
261 prompt = "[S]ave, Edit again, Drop changes?"
265 while prompt.find(answer) == -1:
266 answer = daklib.utils.our_raw_input(prompt)
269 answer = answer[:1].upper()
275 print "OK, discarding changes"
281 print "You pressed something you shouldn't have :("
284 # We seem to be done and also have a working file. Copy over.
285 write_transitions_from_file(edit_file)
288 print "Transitions file updated."
290 ################################################################################
292 def check_transitions(transitions):
295 # Now look through all defined transitions
296 for trans in transitions:
297 t = transitions[trans]
301 # Will be None if nothing is in testing.
302 current = daklib.database.get_suite_version(source, "testing")
304 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
307 # No package in testing
308 print "Transition source %s not in testing, transition still ongoing." % (source)
310 compare = apt_pkg.VersionCompare(current, expected)
312 # This is still valid, the current version in database is older than
313 # the new version we wait for
314 print "This transition is still ongoing, we currently have version %s" % (current)
316 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
317 print "%s wanted version: %s, has %s" % (source, expected, current)
318 to_remove.append(trans)
320 print "-------------------------------------------------------------------------"
323 prompt = "Removing: "
324 for remove in to_remove:
328 prompt += " Commit Changes? (y/N)"
331 if Options["no-action"]:
334 answer = daklib.utils.our_raw_input(prompt).lower()
340 print "Not committing changes"
344 for remove in to_remove:
345 del transitions[remove]
347 edit_file = temp_transitions_file(transitions)
348 write_transitions_from_file(edit_file)
352 print "WTF are you typing?"
355 ################################################################################
357 def print_info(trans, source, expected, rm, reason, packages):
358 print """Looking at transition: %s
363 Blocked Packages (total: %d): %s
364 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
367 ################################################################################
369 def transition_info(transitions):
370 for trans in transitions:
371 t = transitions[trans]
375 # Will be None if nothing is in testing.
376 current = daklib.database.get_suite_version(source, "testing")
378 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
381 # No package in testing
382 print "Transition source %s not in testing, transition still ongoing." % (source)
384 compare = apt_pkg.VersionCompare(current, expected)
385 print "Apt compare says: %s" % (compare)
387 # This is still valid, the current version in database is older than
388 # the new version we wait for
389 print "This transition is still ongoing, we currently have version %s" % (current)
391 print "This transition is over, the target package reached testing, should be removed"
392 print "%s wanted version: %s, has %s" % (source, expected, current)
393 print "-------------------------------------------------------------------------"
395 ################################################################################
400 #####################################
401 #### This can run within sudo !! ####
402 #####################################
405 # Check if there is a file defined (and existant)
406 transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
408 daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
410 if not os.path.exists(transpath):
411 daklib.utils.warn("ReleaseTransitions file, %s, not found." %
412 (Cnf["Dinstall::Reject::ReleaseTransitions"]))
415 if Options["import"]:
417 write_transitions_from_file(Options["import"])
418 except ParseException, m:
422 ##############################################
423 #### Up to here it can run within sudo !! ####
424 ##############################################
426 # Parse the yaml file
427 transitions = load_transitions(transpath)
428 if transitions == None:
429 # Something very broken with the transitions, exit
430 daklib.utils.warn("Could not parse existing transitions file. Aborting.")
434 # Let's edit the transitions file
436 elif Options["check"]:
437 # Check and remove outdated transitions
438 check_transitions(transitions)
440 # Output information about the currently defined transitions.
441 print "Currently defined transitions:"
442 transition_info(transitions)
446 ################################################################################
448 if __name__ == '__main__':