]> git.decadent.org.uk Git - dak.git/blobdiff - dak/transitions.py
Merge commit 'ftpmaster/master' into sqlalchemy
[dak.git] / dak / transitions.py
index b7e50651e8d4daf7b8d284f89bf4a9e5c0fe10cf..9c4e7d8bc7e97010b23d0dc1b21f349e723f2c93 100755 (executable)
@@ -1,7 +1,12 @@
 #!/usr/bin/env python
 
-# Display, edit and check the release manager's transition file.
-# Copyright (C) 2008 Joerg Jaspert <joerg@debian.org>
+"""
+Display, edit and check the release manager's transition file.
+
+@contact: Debian FTP Master <ftpmaster@debian.org>
+@copyright: 2008 Joerg Jaspert <joerg@debian.org>
+@license: GNU General Public License version 2 or later
+"""
 
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 
 ################################################################################
 
-import os, pg, sys, time, errno, fcntl, tempfile, pwd, re
+import os
+import sys
+import time
+import errno
+import fcntl
+import tempfile
+import pwd
 import apt_pkg
-from daklib import database
+
+from daklib.dbconn import *
 from daklib import utils
 from daklib.dak_exceptions import TransitionsError
+from daklib.regexes import re_broken_package
 import yaml
 
 # Globals
-Cnf = None
-Options = None
-projectB = None
-
-re_broken_package = re.compile(r"[a-zA-Z]\w+\s+\-.*")
+Cnf = None      #: Configuration, apt_pkg.Configuration
+Options = None  #: Parsed CommandLine arguments
 
 ################################################################################
 
@@ -43,20 +53,27 @@ re_broken_package = re.compile(r"[a-zA-Z]\w+\s+\-.*")
 #### This may run within sudo !! ####
 #####################################
 def init():
-    global Cnf, Options, projectB
+    """
+    Initialize. Sets up database connection, parses commandline arguments.
+
+    @attention: This function may run B{within sudo}
+
+    """
+    global Cnf, Options
 
     apt_pkg.init()
 
     Cnf = utils.get_conf()
 
-    Arguments = [('h',"help","Edit-Transitions::Options::Help"),
+    Arguments = [('a',"automatic","Edit-Transitions::Options::Automatic"),
+                 ('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',"sudo","Edit-Transitions::Options::Sudo"),
                  ('n',"no-action","Edit-Transitions::Options::No-Action")]
 
-    for i in ["help", "no-action", "edit", "import", "check", "sudo"]:
+    for i in ["automatic", "help", "no-action", "edit", "import", "check", "sudo"]:
         if not Cnf.has_key("Edit-Transitions::Options::%s" % (i)):
             Cnf["Edit-Transitions::Options::%s" % (i)] = ""
 
@@ -67,15 +84,13 @@ def init():
     if Options["help"]:
         usage()
 
-    whoami = os.getuid()
-    whoamifull = pwd.getpwuid(whoami)
-    username = whoamifull[0]
+    username = utils.getusername()
     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)
+    # Initialise DB connection
+    DBConn()
 
 ################################################################################
 
@@ -90,6 +105,7 @@ Options:
   -i, --import <file>       check and import transitions from file
   -c, --check               check the transitions file, remove outdated entries
   -S, --sudo                use sudo to update transitions file
+  -a, --automatic           don't prompt (only affects check).
   -n, --no-action           don't do anything (only affects check)"""
 
     sys.exit(exit_code)
@@ -100,6 +116,20 @@ Options:
 #### This may run within sudo !! ####
 #####################################
 def load_transitions(trans_file):
+    """
+    Parse a transition yaml file and check it for validity.
+
+    @attention: This function may run B{within sudo}
+
+    @type trans_file: string
+    @param trans_file: filename to parse
+
+    @rtype: dict or None
+    @return: validated dictionary of transition entries or None
+             if validation fails, empty string if reading C{trans_file}
+             returned something else than a dict
+
+    """
     # Parse the yaml file
     sourcefile = file(trans_file, 'r')
     sourcecontent = sourcefile.read()
@@ -185,6 +215,12 @@ def load_transitions(trans_file):
 #### This may run within sudo !! ####
 #####################################
 def lock_file(f):
+    """
+    Lock a file
+
+    @attention: This function may run B{within sudo}
+
+    """
     for retry in range(10):
         lock_fd = os.open(f, os.O_RDWR | os.O_CREAT)
         try:
@@ -206,11 +242,19 @@ def lock_file(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
-       files or files that may be be modified while the function is
-       active), and ensure the transitions file is updated atomically
-       to avoid locks."""
+    """
+    Update the active transitions file safely.
+    This function takes a parsed input file (which avoids invalid
+    files or files that may be be modified while the function is
+    active) and ensure the transitions file is updated atomically
+    to avoid locks.
+
+    @attention: This function may run B{within sudo}
+
+    @type from_trans: dict
+    @param from_trans: transitions dictionary, as returned by L{load_transitions}
+
+    """
 
     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
     trans_temp = trans_file + ".tmp"
@@ -232,8 +276,16 @@ def write_transitions(from_trans):
 #### 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"""
+    """
+    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
+
+    @attention: This function usually runs B{within sudo}
+
+    @type from_file: filename
+    @param from_file: filename of a transitions file
+
+    """
 
     # 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"]):
@@ -252,9 +304,20 @@ def write_transitions_from_file(from_file):
 ################################################################################
 
 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
+    """
+    Open a temporary file and dump the current transitions into it, so users
+    can edit them.
+
+    @type transitions: dict
+    @param transitions: current defined transitions
+
+    @rtype: string
+    @return: path of newly created tempfile
+
+    @note: 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", Cnf["Transitions::TempPath"])
     os.chmod(path, 0644)
@@ -265,6 +328,7 @@ def temp_transitions_file(transitions):
 ################################################################################
 
 def edit_transitions():
+    """ Edit the defined transitions. """
     trans_file = Cnf["Dinstall::Reject::ReleaseTransitions"]
     edit_file = temp_transitions_file(load_transitions(trans_file))
 
@@ -322,23 +386,36 @@ def edit_transitions():
 ################################################################################
 
 def check_transitions(transitions):
+    """
+    Check if the defined transitions still apply and remove those that no longer do.
+    @note: Asks the user for confirmation first unless -a has been set.
+
+    """
+    global Cnf
+
     to_dump = 0
     to_remove = []
+    info = {}
+
+    session = DBConn().session()
+
     # Now look through all defined transitions
     for trans in transitions:
         t = transitions[trans]
         source = t["source"]
         expected = t["new"]
 
-        # Will be None if nothing is in testing.
-        current = database.get_suite_version(source, "testing")
+        # Will be an empty list if nothing is in testing.
+        sources = get_source_in_suite(source, "testing", session)
 
-        print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
+        info[trans] = get_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
+        print info[trans]
 
-        if current == None:
+        if len(sources) < 1:
             # No package in testing
             print "Transition source %s not in testing, transition still ongoing." % (source)
         else:
+            current = sources[0].version
             compare = apt_pkg.VersionCompare(current, expected)
             if compare < 0:
                 # This is still valid, the current version in database is older than
@@ -362,6 +439,8 @@ def check_transitions(transitions):
 
         if Options["no-action"]:
             answer="n"
+        elif Options["automatic"]:
+            answer="y"
         else:
             answer = utils.our_raw_input(prompt).lower()
 
@@ -373,9 +452,26 @@ def check_transitions(transitions):
             sys.exit(0)
         elif answer == 'y':
             print "Committing"
-            for remove in to_remove:
+            subst = {}
+            subst['__SUBJECT__'] = "Transitions completed: " + ", ".join(sorted(to_remove))
+            subst['__TRANSITION_MESSAGE__'] = "The following transitions were removed:\n"
+            for remove in sorted(to_remove):
+                subst['__TRANSITION_MESSAGE__'] += info[remove] + '\n'
                 del transitions[remove]
 
+            # If we have a mail address configured for transitions,
+            # send a notification
+            subst['__TRANSITION_EMAIL__'] = Cnf.get("Transitions::Notifications", "")
+            if subst['__TRANSITION_EMAIL__'] != "":
+                print "Sending notification to %s" % subst['__TRANSITION_EMAIL__']
+                subst['__DAK_ADDRESS__'] = Cnf["Dinstall::MyEmailAddress"]
+                subst['__BCC__'] = 'X-DAK: dak transitions'
+                if Cnf.has_key("Dinstall::Bcc"):
+                    subst["__BCC__"] += '\nBcc: %s' % Cnf["Dinstall::Bcc"]
+                message = utils.TemplateSubst(subst,
+                                              os.path.join(Cnf["Dir::Templates"], 'transition.removed'))
+                utils.send_mail(message)
+
             edit_file = temp_transitions_file(transitions)
             write_transitions_from_file(edit_file)
 
@@ -386,33 +482,63 @@ def check_transitions(transitions):
 
 ################################################################################
 
-def print_info(trans, source, expected, rm, reason, packages):
-    print """Looking at transition: %s
+def get_info(trans, source, expected, rm, reason, packages):
+    """
+    Print information about a single transition.
+
+    @type trans: string
+    @param trans: Transition name
+
+    @type source: string
+    @param source: Source package
+
+    @type expected: string
+    @param expected: Expected version in testing
+
+    @type rm: string
+    @param rm: Responsible RM
+
+    @type reason: string
+    @param reason: Reason
+
+    @type packages: list
+    @param packages: list of blocked packages
+
+    """
+    return """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
 
 ################################################################################
 
 def transition_info(transitions):
+    """
+    Print information about all defined transitions.
+    Calls L{get_info} for every transition and then tells user if the transition is
+    still ongoing or if the expected version already hit testing.
+
+    @type transitions: dict
+    @param transitions: defined transitions
+    """
     for trans in transitions:
         t = transitions[trans]
         source = t["source"]
         expected = t["new"]
 
-        # Will be None if nothing is in testing.
-        current = database.get_suite_version(source, "testing")
+        # Will be empty list if nothing is in testing.
+        sources = get_suite_version(source, "testing")
 
-        print_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
+        print get_info(trans, source, expected, t["rm"], t["reason"], t["packages"])
 
-        if current == None:
+        if len(sources) < 1:
             # No package in testing
             print "Transition source %s not in testing, transition still ongoing." % (source)
         else:
+            current = sources[0].version
             compare = apt_pkg.VersionCompare(current, expected)
             print "Apt compare says: %s" % (compare)
             if compare < 0:
@@ -427,6 +553,12 @@ def transition_info(transitions):
 ################################################################################
 
 def main():
+    """
+    Prepare the work to be done, do basic checks.
+
+    @attention: This function may run B{within sudo}
+
+    """
     global Cnf
 
     #####################################