X-Git-Url: https://git.decadent.org.uk/gitweb/?a=blobdiff_plain;f=dak%2Ftransitions.py;h=b7e50651e8d4daf7b8d284f89bf4a9e5c0fe10cf;hb=f8996e240d9d0278bce098e23be63db0bcc6fbee;hp=15aef62e2c48a0b68d57db19ab3267f395f1d4a6;hpb=1673e4fd9f62822a1df8eca7dd6b9cfd46408942;p=dak.git diff --git a/dak/transitions.py b/dak/transitions.py index 15aef62e..b7e50651 100755 --- a/dak/transitions.py +++ b/dak/transitions.py @@ -23,31 +23,37 @@ ################################################################################ -import os, pg, sys, time, errno, fcntl, tempfile +import os, pg, sys, time, errno, fcntl, tempfile, pwd, re import apt_pkg -import daklib.database -import daklib.utils -import syck +from daklib import database +from daklib import utils +from daklib.dak_exceptions import TransitionsError +import yaml # Globals Cnf = None Options = None projectB = None +re_broken_package = re.compile(r"[a-zA-Z]\w+\s+\-.*") + ################################################################################ +##################################### +#### This may run within sudo !! #### +##################################### def init(): global Cnf, Options, projectB apt_pkg.init() - Cnf = daklib.utils.get_conf() + Cnf = utils.get_conf() Arguments = [('h',"help","Edit-Transitions::Options::Help"), ('e',"edit","Edit-Transitions::Options::Edit"), ('i',"import","Edit-Transitions::Options::Import", "HasArg"), ('c',"check","Edit-Transitions::Options::Check"), - ('S',"use-sudo","Edit-Transitions::Options::Sudo"), + ('s',"sudo","Edit-Transitions::Options::Sudo"), ('n',"no-action","Edit-Transitions::Options::No-Action")] for i in ["help", "no-action", "edit", "import", "check", "sudo"]: @@ -58,18 +64,24 @@ def init(): Options = Cnf.SubTree("Edit-Transitions::Options") - projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"])) - daklib.database.init(Cnf, projectB) - if Options["help"]: usage() + whoami = os.getuid() + whoamifull = pwd.getpwuid(whoami) + username = whoamifull[0] + if username != "dak": + print "Non-dak user: %s" % username + Options["sudo"] = "y" + + projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"])) + database.init(Cnf, projectB) + ################################################################################ def usage (exit_code=0): - print """Usage: edit_transitions [OPTION]... + print """Usage: transitions [OPTION]... Update and check the release managers transition file. -transitions. Options: @@ -78,30 +90,103 @@ Options: -i, --import check and import transitions from file -c, --check check the transitions file, remove outdated entries -S, --sudo use sudo to update transitions file - -n, --no-action don't do anything""" + -n, --no-action don't do anything (only affects check)""" sys.exit(exit_code) ################################################################################ +##################################### +#### This may run within sudo !! #### +##################################### def load_transitions(trans_file): # Parse the yaml file sourcefile = file(trans_file, 'r') sourcecontent = sourcefile.read() + failure = False try: - trans = syck.load(sourcecontent) - except syck.error, msg: + trans = yaml.load(sourcecontent) + except yaml.YAMLError, exc: # Someone fucked it up - print "ERROR: %s" % (msg) + print "ERROR: %s" % (exc) + return None + + # lets do further validation here + checkkeys = ["source", "reason", "packages", "new", "rm"] + + # If we get an empty definition - we just have nothing to check, no transitions defined + if type(trans) != dict: + # This can be anything. We could have no transitions defined. Or someone totally fucked up the + # file, adding stuff in a way we dont know or want. Then we set it empty - and simply have no + # transitions anymore. User will see it in the information display after he quit the editor and + # could fix it + trans = "" + return trans + + try: + for test in trans: + t = trans[test] + + # First check if we know all the keys for the transition and if they have + # the right type (and for the packages also if the list has the right types + # included, ie. not a list in list, but only str in the list) + for key in t: + if key not in checkkeys: + print "ERROR: Unknown key %s in transition %s" % (key, test) + failure = True + + if key == "packages": + if type(t[key]) != list: + print "ERROR: Unknown type %s for packages in transition %s." % (type(t[key]), test) + failure = True + try: + for package in t["packages"]: + if type(package) != str: + print "ERROR: Packages list contains invalid type %s (as %s) in transition %s" % (type(package), package, test) + failure = True + if re_broken_package.match(package): + # Someone had a space too much (or not enough), we have something looking like + # "package1 - package2" now. + print "ERROR: Invalid indentation of package list in transition %s, around package(s): %s" % (test, package) + failure = True + except TypeError: + # In case someone has an empty packages list + print "ERROR: No packages defined in transition %s" % (test) + failure = True + continue + + elif type(t[key]) != str: + if key == "new" and type(t[key]) == int: + # Ok, debian native version + continue + else: + print "ERROR: Unknown type %s for key %s in transition %s" % (type(t[key]), key, test) + failure = True + + # And now the other way round - are all our keys defined? + for key in checkkeys: + if key not in t: + print "ERROR: Missing key %s in transition %s" % (key, test) + failure = True + except TypeError: + # In case someone defined very broken things + print "ERROR: Unable to parse the file" + failure = True + + + if failure: return None - # could do further validation here + return trans ################################################################################ -def lock_file(file): +##################################### +#### This may run within sudo !! #### +##################################### +def lock_file(f): for retry in range(10): - lock_fd = os.open(file, os.O_RDWR | os.O_CREAT) + lock_fd = os.open(f, os.O_RDWR | os.O_CREAT) try: fcntl.lockf(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) return lock_fd @@ -113,10 +198,13 @@ def lock_file(file): else: raise - daklib.utils.fubar("Couldn't obtain lock for %s." % (lockfile)) + utils.fubar("Couldn't obtain lock for %s." % (f)) ################################################################################ +##################################### +#### This may run within sudo !! #### +##################################### def write_transitions(from_trans): """Update the active transitions file safely. This function takes a parsed input file (which avoids invalid @@ -126,12 +214,12 @@ def write_transitions(from_trans): trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"] trans_temp = trans_file + ".tmp" - + trans_lock = lock_file(trans_file) temp_lock = lock_file(trans_temp) destfile = file(trans_temp, 'w') - syck.dump(from_trans, destfile) + yaml.dump(from_trans, destfile, default_flow_style=False) destfile.close() os.rename(trans_temp, trans_file) @@ -140,30 +228,38 @@ def write_transitions(from_trans): ################################################################################ -class ParseException(Exception): - pass - +########################################## +#### This usually runs within sudo !! #### +########################################## def write_transitions_from_file(from_file): """We have a file we think is valid; if we're using sudo, we invoke it here, otherwise we just parse the file and call write_transitions""" + # Lets check if from_file is in the directory we expect it to be in + if not os.path.abspath(from_file).startswith(Cnf["Transitions::TempPath"]): + print "Will not accept transitions file outside of %s" % (Cnf["Transitions::TempPath"]) + sys.exit(3) + if Options["sudo"]: - os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H", + os.spawnl(os.P_WAIT, "/usr/bin/sudo", "/usr/bin/sudo", "-u", "dak", "-H", "/usr/local/bin/dak", "transitions", "--import", from_file) else: trans = load_transitions(from_file) if trans is None: - raise ParseException, "Unparsable transitions file %s" % (file) + raise TransitionsError, "Unparsable transitions file %s" % (file) write_transitions(trans) ################################################################################ def temp_transitions_file(transitions): # NB: file is unlinked by caller, but fd is never actually closed. + # We need the chmod, as the file is (most possibly) copied from a + # sudo-ed script and would be unreadable if it has default mkstemp mode - (fd, path) = tempfile.mkstemp("","transitions") + (fd, path) = tempfile.mkstemp("", "transitions", Cnf["Transitions::TempPath"]) + os.chmod(path, 0644) f = open(path, "w") - syck.dump(transitions, f) + yaml.dump(transitions, f, default_flow_style=False) return path ################################################################################ @@ -178,40 +274,50 @@ def edit_transitions(): result = os.system("%s %s" % (editor, edit_file)) if result != 0: os.unlink(edit_file) - daklib.utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file)) - + utils.fubar("%s invocation failed for %s, not removing tempfile." % (editor, edit_file)) + # Now try to load the new file test = load_transitions(edit_file) if test == None: # Edit is broken - answer = "XXX" - prompt = "Broken edit: [E]dit again, Drop changes?" - - while prompt.find(answer) == -1: - answer = daklib.utils.our_raw_input(prompt) - if answer == "": - answer = "E" - answer = answer[:1].upper() - - if answer == 'E': - continue - elif answer == 'D': - os.unlink(edit_file) - print "OK, discarding changes" - sys.exit(0) + print "Edit was unparsable." + prompt = "[E]dit again, Drop changes?" + default = "E" else: - # No problems in loading the new file, jump out of the while loop + print "Edit looks okay.\n" + print "The following transitions are defined:" + print "------------------------------------------------------------------------" + transition_info(test) + + prompt = "[S]ave, Edit again, Drop changes?" + default = "S" + + answer = "XXX" + while prompt.find(answer) == -1: + answer = utils.our_raw_input(prompt) + if answer == "": + answer = default + answer = answer[:1].upper() + + if answer == 'E': + continue + elif answer == 'D': + os.unlink(edit_file) + print "OK, discarding changes" + sys.exit(0) + elif answer == 'S': + # Ready to save break + else: + print "You pressed something you shouldn't have :(" + sys.exit(1) # We seem to be done and also have a working file. Copy over. write_transitions_from_file(edit_file) os.unlink(edit_file) - # Before we finish print out transition info again - print "\n\n------------------------------------------------------------------------" - print "Edit done, file saved, currently defined transitions:\n" - transition_info(load_transitions(trans_file)) + print "Transitions file updated." ################################################################################ @@ -225,7 +331,7 @@ def check_transitions(transitions): expected = t["new"] # Will be None if nothing is in testing. - current = daklib.database.get_suite_version(source, "testing") + current = database.get_suite_version(source, "testing") print_info(trans, source, expected, t["rm"], t["reason"], t["packages"]) @@ -257,7 +363,7 @@ def check_transitions(transitions): if Options["no-action"]: answer="n" else: - answer = daklib.utils.our_raw_input(prompt).lower() + answer = utils.our_raw_input(prompt).lower() if answer == "": answer = "n" @@ -269,7 +375,7 @@ def check_transitions(transitions): print "Committing" for remove in to_remove: del transitions[remove] - + edit_file = temp_transitions_file(transitions) write_transitions_from_file(edit_file) @@ -281,15 +387,14 @@ def check_transitions(transitions): ################################################################################ def print_info(trans, source, expected, rm, reason, packages): - print """ -Looking at transition: %s - Source: %s - New Version: %s - Responsible: %s - Description: %s - Blocked Packages (total: %d): %s + print """Looking at transition: %s +Source: %s +New Version: %s +Responsible: %s +Description: %s +Blocked Packages (total: %d): %s """ % (trans, source, expected, rm, reason, len(packages), ", ".join(packages)) - return + return ################################################################################ @@ -300,7 +405,7 @@ def transition_info(transitions): expected = t["new"] # Will be None if nothing is in testing. - current = daklib.database.get_suite_version(source, "testing") + current = database.get_suite_version(source, "testing") print_info(trans, source, expected, t["rm"], t["reason"], t["packages"]) @@ -324,31 +429,46 @@ def transition_info(transitions): def main(): global Cnf + ##################################### + #### This can run within sudo !! #### + ##################################### init() - + # Check if there is a file defined (and existant) transpath = Cnf.get("Dinstall::Reject::ReleaseTransitions", "") if transpath == "": - daklib.utils.warn("Dinstall::Reject::ReleaseTransitions not defined") + utils.warn("Dinstall::Reject::ReleaseTransitions not defined") sys.exit(1) if not os.path.exists(transpath): - daklib.utils.warn("ReleaseTransitions file, %s, not found." % + utils.warn("ReleaseTransitions file, %s, not found." % (Cnf["Dinstall::Reject::ReleaseTransitions"])) sys.exit(1) - + # Also check if our temp directory is defined and existant + temppath = Cnf.get("Transitions::TempPath", "") + if temppath == "": + utils.warn("Transitions::TempPath not defined") + sys.exit(1) + if not os.path.exists(temppath): + utils.warn("Temporary path %s not found." % + (Cnf["Transitions::TempPath"])) + sys.exit(1) + if Options["import"]: try: write_transitions_from_file(Options["import"]) - except ParseException, m: + except TransitionsError, m: print m sys.exit(2) sys.exit(0) + ############################################## + #### Up to here it can run within sudo !! #### + ############################################## # Parse the yaml file transitions = load_transitions(transpath) if transitions == None: # Something very broken with the transitions, exit - daklib.utils.warn("Could not parse existing transitions file. Aborting.") + utils.warn("Could not parse existing transitions file. Aborting.") sys.exit(2) if Options["edit"]: @@ -363,7 +483,7 @@ def main(): transition_info(transitions) sys.exit(0) - + ################################################################################ if __name__ == '__main__':