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
39 re_broken_package = re.compile(r"[a-zA-Z]\w+\s+\-.*")
41 ################################################################################
43 #####################################
44 #### This may run within sudo !! ####
45 #####################################
47 global Cnf, Options, projectB
51 Cnf = utils.get_conf()
53 Arguments = [('h',"help","Edit-Transitions::Options::Help"),
54 ('e',"edit","Edit-Transitions::Options::Edit"),
55 ('i',"import","Edit-Transitions::Options::Import", "HasArg"),
56 ('c',"check","Edit-Transitions::Options::Check"),
57 ('s',"sudo","Edit-Transitions::Options::Sudo"),
58 ('n',"no-action","Edit-Transitions::Options::No-Action")]
60 for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
61 if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
62 Cnf["Edit-Transitions::Options::%s" % (i)] = ""
64 apt_pkg.ParseCommandLine(Cnf, Arguments, sys.argv)
66 Options = Cnf.SubTree("Edit-Transitions::Options")
72 whoamifull = pwd.getpwuid(whoami)
73 username = whoamifull[0]
75 print "Non-dak user: %s" % username
78 projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]))
79 database.init(Cnf, projectB)
81 ################################################################################
83 def usage (exit_code=0):
84 print """Usage: transitions [OPTION]...
85 Update and check the release managers transition file.
89 -h, --help show this help and exit.
90 -e, --edit edit the transitions file
91 -i, --import <file> check and import transitions from file
92 -c, --check check the transitions file, remove outdated entries
93 -S, --sudo use sudo to update transitions file
94 -n, --no-action don't do anything (only affects check)"""
98 ################################################################################
100 #####################################
101 #### This may run within sudo !! ####
102 #####################################
103 def load_transitions(trans_file):
104 # Parse the yaml file
105 sourcefile = file(trans_file, 'r')
106 sourcecontent = sourcefile.read()
109 trans = syck.load(sourcecontent)
110 except syck.error, msg:
111 # Someone fucked it up
112 print "ERROR: %s" % (msg)
115 # lets do further validation here
116 checkkeys = ["source", "reason", "packages", "new", "rm"]
118 # If we get an empty definition - we just have nothing to check, no transitions defined
119 if type(trans) != dict:
120 # This can be anything. We could have no transitions defined. Or someone totally fucked up the
121 # file, adding stuff in a way we dont know or want. Then we set it empty - and simply have no
122 # transitions anymore. User will see it in the information display after he quit the editor and
131 # First check if we know all the keys for the transition and if they have
132 # the right type (and for the packages also if the list has the right types
133 # included, ie. not a list in list, but only str in the list)
135 if key not in checkkeys:
136 print "ERROR: Unknown key %s in transition %s" % (key, test)
139 if key == "packages":
140 if type(t[key]) != list:
141 print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test)
144 for package in t["packages"]:
145 if type(package) != str:
146 print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test)
148 if re_broken_package.match(package):
149 # Someone had a space too much (or not enough), we have something looking like
150 # "package1 - package2" now.
151 print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, package)
154 # In case someone has an empty packages list
155 print "ERROR: No packages defined in transition %s" % (test)
159 elif type(t[key]) != str:
160 if key == "new" and type(t[key]) == int:
161 # Ok, debian native version
164 print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test)
167 # And now the other way round - are all our keys defined?
168 for key in checkkeys:
170 print "ERROR: Missing key %s in transition %s" % (key, test)
173 # In case someone defined very broken things
174 print "ERROR: Unable to parse the file"
183 ################################################################################
185 #####################################
186 #### This may run within sudo !! ####
187 #####################################
189 for retry in range(10):
190 lock_fd = os.open(f, os.O_RDWR | os.O_CREAT)
192 fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
195 if errno.errorcode[e.errno] == 'EACCES' or errno.errorcode[e.errno] == 'EEXIST':
196 print "Unable to get lock for %s (try %d of 10)" % \
202 utils.fubar("Couldn't obtain lock for %s." % (f))
204 ################################################################################
206 #####################################
207 #### This may run within sudo !! ####
208 #####################################
209 def write_transitions(from_trans):
210 """Update the active transitions file safely.
211 This function takes a parsed input file (which avoids invalid
212 files or files that may be be modified while the function is
213 active), and ensure the transitions file is updated atomically
216 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
217 trans_temp = trans_file + ".tmp"
219 trans_lock = lock_file(trans_file)
220 temp_lock = lock_file(trans_temp)
222 destfile = file(trans_temp, 'w')
223 yaml.dump(from_trans, destfile, default_flow_style=False)
226 os.rename(trans_temp, trans_file)
230 ################################################################################
232 ##########################################
233 #### This usually runs within sudo !! ####
234 ##########################################
235 def write_transitions_from_file(from_file):
236 """We have a file we think is valid; if we're using sudo, we invoke it
237 here, otherwise we just parse the file and call write_transitions"""
239 # Lets check if from_file is in the directory we expect it to be in
240 if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]):
241 print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"])
245 os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H",
246 "/usr/local/bin/dak", "transitions", "--import", from_file)
248 trans = load_transitions(from_file)
250 raise TransitionsError, "Unparsable transitions file %s" % (file)
251 write_transitions(trans)
253 ################################################################################
255 def temp_transitions_file(transitions):
256 # NB: file is unlinked by caller, but fd is never actually closed.
257 # We need the chmod, as the file is (most possibly) copied from a
258 # sudo-ed script and would be unreadable if it has default mkstemp mode
260 (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"])
263 yaml.dump(transitions, f, default_flow_style=False)
266 ################################################################################
268 def edit_transitions():
269 trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
270 edit_file = temp_transitions_file(load_transitions(trans_file))
272 editor = os.environ.get("EDITOR", "vi")
275 result = os.system("%s %s" % (editor, edit_file))
278 utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file))
280 # Now try to load the new file
281 test = load_transitions(edit_file)
285 print "Edit was unparsable."
286 prompt = "[E]dit again, Drop changes?"
289 print "Edit looks okay.\n"
290 print "The following transitions are defined:"
291 print "------------------------------------------------------------------------"
292 transition_info(test)
294 prompt = "[S]ave, Edit again, Drop changes?"
298 while prompt.find(answer) == -1:
299 answer = utils.our_raw_input(prompt)
302 answer = answer[:1].upper()
308 print "OK, discarding changes"
314 print "You pressed something you shouldn't have :("
317 # We seem to be done and also have a working file. Copy over.
318 write_transitions_from_file(edit_file)
321 print "Transitions file updated."
323 ################################################################################
325 def check_transitions(transitions):
328 # Now look through all defined transitions
329 for trans in transitions:
330 t = transitions[trans]
334 # Will be None if nothing is in testing.
335 current = database.get_suite_version(source, "testing")
337 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
340 # No package in testing
341 print "Transition source %s not in testing, transition still ongoing." % (source)
343 compare = apt_pkg.VersionCompare(current, expected)
345 # This is still valid, the current version in database is older than
346 # the new version we wait for
347 print "This transition is still ongoing, we currently have version %s" % (current)
349 print "REMOVE: This transition is over, the target package reached testing. REMOVE"
350 print "%s wanted version: %s, has %s" % (source, expected, current)
351 to_remove.append(trans)
353 print "-------------------------------------------------------------------------"
356 prompt = "Removing: "
357 for remove in to_remove:
361 prompt += " Commit Changes? (y/N)"
364 if Options["no-action"]:
367 answer = utils.our_raw_input(prompt).lower()
373 print "Not committing changes"
377 for remove in to_remove:
378 del transitions[remove]
380 edit_file = temp_transitions_file(transitions)
381 write_transitions_from_file(edit_file)
385 print "WTF are you typing?"
388 ################################################################################
390 def print_info(trans, source, expected, rm, reason, packages):
391 print """Looking at transition: %s
396 Blocked Packages (total: %d): %s
397 """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages))
400 ################################################################################
402 def transition_info(transitions):
403 for trans in transitions:
404 t = transitions[trans]
408 # Will be None if nothing is in testing.
409 current = database.get_suite_version(source, "testing")
411 print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
414 # No package in testing
415 print "Transition source %s not in testing, transition still ongoing." % (source)
417 compare = apt_pkg.VersionCompare(current, expected)
418 print "Apt compare says: %s" % (compare)
420 # This is still valid, the current version in database is older than
421 # the new version we wait for
422 print "This transition is still ongoing, we currently have version %s" % (current)
424 print "This transition is over, the target package reached testing, should be removed"
425 print "%s wanted version: %s, has %s" % (source, expected, current)
426 print "-------------------------------------------------------------------------"
428 ################################################################################
433 #####################################
434 #### This can run within sudo !! ####
435 #####################################
438 # Check if there is a file defined (and existant)
439 transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "")
441 utils.warn("Dinstall::Reject::ReleaseTransitions not defined")
443 if not os.path.exists(transpath):
444 utils.warn("ReleaseTransitions file, %s, not found." %
445 (Cnf["Dinstall::Reject::ReleaseTransitions"]))
447 # Also check if our temp directory is defined and existant
448 temppath = Cnf.get("Transitions::TempPath", "")
450 utils.warn("Transitions::TempPath not defined")
452 if not os.path.exists(temppath):
453 utils.warn("Temporary path %s not found." %
454 (Cnf["Transitions::TempPath"]))
457 if Options["import"]:
459 write_transitions_from_file(Options["import"])
460 except TransitionsError, m:
464 ##############################################
465 #### Up to here it can run within sudo !! ####
466 ##############################################
468 # Parse the yaml file
469 transitions = load_transitions(transpath)
470 if transitions == None:
471 # Something very broken with the transitions, exit
472 utils.warn("Could not parse existing transitions file. Aborting.")
476 # Let's edit the transitions file
478 elif Options["check"]:
479 # Check and remove outdated transitions
480 check_transitions(transitions)
482 # Output information about the currently defined transitions.
483 print "Currently defined transitions:"
484 transition_info(transitions)
488 ################################################################################
490 if __name__ == '__main__':