melanie 17.0 KB
Newer Older
J
James Troup 已提交
1 2 3
#!/usr/bin/env python

# General purpose archive tool for ftpmaster
4
# Copyright (C) 2000, 2001  James Troup <james@nocrew.org>
5
# $Id: melanie,v 1.11 2001-03-21 05:37:43 troup Exp $
J
James Troup 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

# 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
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

# X-Listening-To: Astronomy, Metallica - Garage Inc.

################################################################################

import commands, os, pg, pwd, re, string, sys, tempfile
import utils, db_access
import apt_pkg, apt_inst;

################################################################################

re_strip_source_version = re.compile (r'\s+.*$');

################################################################################

Cnf = None;
projectB = None;

################################################################################

J
James Troup 已提交
40 41 42
# "That's just fucking great!  Game over, man!  What the fuck are we
#  going to do now?"

J
James Troup 已提交
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
def game_over():
    print "Continue (y/N)? ",
    answer = string.lower(utils.our_raw_input());
    if answer != "y":
        print "Aborted."
        sys.exit(1);

################################################################################

def main ():
    global Cnf, projectB;

    apt_pkg.init();
    
    Cnf = apt_pkg.newConfiguration();
    apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());

    Arguments = [('D',"debug","Melanie::Options::Debug", "IntVal"),
                 ('h',"help","Melanie::Options::Help"),
                 ('V',"version","Melanie::Options::Version"),
                 ('a',"architecture","Melanie::Options::Architecture", "HasArg"),
                 ('b',"binary", "Melanie::Options::Binary-Only"),
                 ('c',"component", "Melanie::Options::Component", "HasArg"),
66
                 ('C',"carbon-copy", "Melanie::Options::Carbon-Copy", "HasArg"), # Bugs to Cc
J
James Troup 已提交
67 68 69 70 71 72 73 74 75
                 ('d',"done","Melanie::Options::Done", "HasArg"), # Bugs fixed
                 ('m',"reason", "Melanie::Options::Reason", "HasArg"), # Hysterical raisins; -m is old-dinstall option for rejection reason
                 ('n',"no-action","Melanie::Options::No-Action"),
                 ('p',"partial", "Melanie::Options::Partial"),
                 ('s',"suite","Melanie::Options::Suite", "HasArg"),
                 ('S',"source-only", "Melanie::Options::Source-Only"),
                 ];

    arguments = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
76
    Options = Cnf.SubTree("Melanie::Options")
77
    projectB = pg.connect(Cnf["DB::Name"], Cnf["DB::Host"], int(Cnf["DB::Port"]));
J
James Troup 已提交
78 79 80 81 82 83
    db_access.init(Cnf, projectB);

    # Sanity check options
    if arguments == []:
        sys.stderr.write("E: need at least one package name as an argument.\n");
        sys.exit(1);
84
    if Options["Architecture"] and Options["Source-Only"]:
J
James Troup 已提交
85 86
        sys.stderr.write("E: can't use -a/--architecutre and -S/--source-only options simultaneously.\n");
        sys.exit(1);
87
    if Options["Binary-Only"] and Options["Source-Only"]:
J
James Troup 已提交
88 89
        sys.stderr.write("E: can't use -b/--binary-only and -S/--source-only options simultaneously.\n");
        sys.exit(1);
90
    if Options["Architecture"] and not Options["Partial"]:
J
James Troup 已提交
91
        sys.stderr.write("W: -a/--architecture implies -p/--partial.\n");
92 93 94 95 96 97 98 99 100 101 102 103
        Options["Partial"] = "true";

    # Process -C/--carbon-copy
    #
    # Accept 3 types of arguments (space separated):
    #  1) a number - assumed to be a bug number, i.e. nnnnn@bugs.debian.org
    #  2) the keyword 'package' - cc's $arch@packages.debian.org for every argument
    #  3) contains a '@' - assumed to be an email address, used unmofidied
    #
    carbon_copy = ""
    for copy_to in string.split(Options.get("Carbon-Copy")):
        if utils.str_isnum(copy_to):
J
James Troup 已提交
104
            carbon_copy = carbon_copy + copy_to + "@" + Cnf["Dinstall::BugServer"] + ", "
105 106
        elif copy_to == 'package':
            for package in arguments:
J
James Troup 已提交
107
                carbon_copy = carbon_copy + package + "@" + Cnf["Dinstall::PackagesServer"] + ", "
108 109 110 111 112 113 114 115
        elif '@' in copy_to:
            carbon_copy = carbon_copy + copy_to + ", "
        else:
            sys.stderr.write("Invalid -C/--carbon-copy argument '%s'; not a bug number, 'package' or email address.\n" % (copy_to));
            sys.exit(1);
    # Make it a real email header
    if carbon_copy != "":
        carbon_copy = "Cc: " + carbon_copy[:-2] + '\n'
J
James Troup 已提交
116 117

    packages = {};
118
    if Options["Binary-Only"]:
J
James Troup 已提交
119 120 121 122 123 124 125 126 127 128 129 130
        field = "b.package";
    else:
        field = "s.source";
    con_packages = "AND (";
    for package in arguments:
        con_packages = con_packages + "%s = '%s' OR " % (field, package)
        packages[package] = "";
    con_packages = con_packages[:-3] + ")"

    suites_list = "";
    suite_ids_list = [];
    con_suites = "AND (";
131
    for suite in string.split(Options["Suite"]):
J
James Troup 已提交
132
        
133
        if not Options["No-Action"] and suite == "stable":
J
James Troup 已提交
134 135 136 137
            print "**WARNING** About to remove from the stable suite!"
            print "This should only be done just prior to a (point) release and not at"
            print "any other time."
            game_over();
138
        elif not Options["No-Action"] and suite == "testing":
J
James Troup 已提交
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154
            print "**WARNING About to remove from the testing suite!"
            print "There's no need to do this normally as removals from unstable will"
            print "propogate to testing automagically."
            game_over();
            
        suite_id = db_access.get_suite_id(suite);
        if suite_id == -1:
            sys.stderr.write("W: suite '%s' not recognised.\n" % (suite));
        else:
            con_suites = con_suites + "su.id = %s OR " % (suite_id)

        suites_list = suites_list + suite + ", "
        suite_ids_list.append(suite_id);
    con_suites = con_suites[:-3] + ")"
    suites_list = suites_list[:-2];

155
    if Options["Component"]:
J
James Troup 已提交
156 157
        con_components = "AND (";
        over_con_components = "AND (";
158
        for component in string.split(Options["Component"]):
J
James Troup 已提交
159 160 161 162 163 164 165 166 167 168 169 170
            component_id = db_access.get_component_id(component);
            if component_id == -1:
                sys.stderr.write("W: component '%s' not recognised.\n" % (component));
            else:
                con_components = con_components + "c.id = %s OR " % (component_id);
                over_con_components = over_con_components + "component = %s OR " % (component_id);
        con_components = con_components[:-3] + ")"
        over_con_components = over_con_components[:-3] + ")";
    else:
        con_components = "";    
        over_con_components = "";

171
    if Options["Architecture"]:
J
James Troup 已提交
172
        con_architectures = "AND (";
173
        for architecture in string.split(Options["Architecture"]):
J
James Troup 已提交
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191
            architecture_id = db_access.get_architecture_id(architecture);
            if architecture_id == -1:
                sys.stderr.write("W: architecture '%s' not recognised.\n" % (architecture));
            else:
                con_architectures = con_architectures + "a.id = %s OR " % (architecture_id)
        con_architectures = con_architectures[:-3] + ")"
    else:
        con_architectures = "";


    print "Working...",
    sys.stdout.flush();
    to_remove = [];
    # We have 3 modes of package selection: binary-only, source-only
    # and source+binary.  The first two are trivial and obvious; the
    # latter is a nasty mess, but very nice from a UI perspective so
    # we try to support it.

192
    if Options["Binary-Only"]:
J
James Troup 已提交
193 194 195 196 197 198 199 200 201 202 203
        # Binary-only
        q = projectB.query("SELECT b.package, b.version, a.arch_string, b.id FROM binaries b, bin_associations ba, architecture a, suite su, files f, location l, component c WHERE ba.bin = b.id AND ba.suite = su.id AND b.architecture = a.id AND b.file = f.id AND f.location = l.id AND l.component = c.id %s %s %s %s" % (con_packages, con_suites, con_components, con_architectures));
        for i in q.getresult():
            to_remove.append(i);
    else:
        # Source-only
        source_packages = {};
        q = projectB.query("SELECT l.path, f.filename, s.source, s.version, 'source', s.id FROM source s, src_associations sa, suite su, files f, location l, component c WHERE sa.source = s.id AND sa.suite = su.id AND s.file = f.id AND f.location = l.id AND l.component = c.id %s %s %s" % (con_packages, con_suites, con_components));
        for i in q.getresult():
            source_packages[i[2]] = i[:2];
            to_remove.append(i[2:]);
204
        if not Options["Source-Only"]:
J
James Troup 已提交
205 206 207 208 209 210 211 212 213 214
            # Source + Binary
            binary_packages = {};
            # First get a list of binary package names we suspect are linked to the source
            q = projectB.query("SELECT DISTINCT package FROM binaries WHERE EXISTS (SELECT s.source, s.version, l.path, f.filename FROM source s, src_associations sa, suite su, files f, location l, component c WHERE binaries.source = s.id AND sa.source = s.id AND sa.suite = su.id AND s.file = f.id AND f.location = l.id AND l.component = c.id %s %s %s)" % (con_packages, con_suites, con_components));
            for i in q.getresult():
                binary_packages[i[0]] = "";
            # Then parse each .dsc that we found earlier to see what binary packages it thinks it produces
            for i in source_packages.keys():
                filename = string.join(source_packages[i], '/');
                try:
J
James Troup 已提交
215
                    dsc = utils.parse_changes(filename, 0);
J
James Troup 已提交
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
                except utils.cant_open_exc:
                    sys.stderr.write("W: couldn't open '%s'.\n" % (filename));
                    continue;
                for package in string.split(dsc.get("binary"), ','):
                    package = string.strip(package);
                    binary_packages[package] = "";
            # Then for each binary package: find any version in
            # unstable, check the Source: field in the deb matches our
            # source package and if so add it to the list of packages
            # to be removed.
            for package in binary_packages.keys():
                q = projectB.query("SELECT l.path, f.filename, b.package, b.version, a.arch_string, b.id FROM binaries b, bin_associations ba, architecture a, suite su, files f, location l, component c WHERE ba.bin = b.id AND ba.suite = su.id AND b.architecture = a.id AND b.file = f.id AND f.location = l.id AND l.component = c.id %s %s %s AND b.package = '%s'" % (con_suites, con_components, con_architectures, package));
                for i in q.getresult():
                    filename = string.join(i[:2], '/');
                    control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(filename,"r")))
                    source = control.Find("Source", control.Find("Package"));
                    source = re_strip_source_version.sub('', source);
                    if source_packages.has_key(source):
                        to_remove.append(i[2:]);
                    #else:
                        #sys.stderr.write("W: skipping '%s' as it's source ('%s') isn't one of the source packages.\n" % (filename, source));
    print "done."

    # If we don't have a reason; spawn an editor so the user can add one
    # Write the rejection email out as the <foo>.reason file
241
    if not Options["Reason"] and not Options["No-Action"]:
J
James Troup 已提交
242 243 244
        temp_filename = tempfile.mktemp();
        fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
        os.close(fd);
245 246
        editor = os.environ.get("EDITOR","vi")
        result = os.system("%s %s" % (editor, temp_filename))
J
James Troup 已提交
247 248 249 250 251
        if result != 0:
            sys.stderr.write ("vi invocation failed for `%s'!" % (temp_filename))
            sys.exit(result)
        file = utils.open_file(temp_filename, 'r');
        for line in file.readlines():
252
            Options["Reason"] = Options["Reason"] + line;
J
James Troup 已提交
253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
        os.unlink(temp_filename);

    # Generate the summary of what's to be removed
    d = {};
    for i in to_remove:
        package = i[0];
        version = i[1];
        architecture = i[2];
        if not d.has_key(package):
            d[package] = {};
        if not d[package].has_key(version):
            d[package][version] = [];
        d[package][version].append(architecture);

    summary = "";
    packages = d.keys();
    packages.sort();
    for package in packages:
        versions = d[package].keys();
        versions.sort();
        for version in versions:
            summary = summary + "%10s | %10s | " % (package, version);
            for architecture in d[package][version]:
                summary = "%s%s, " % (summary, architecture);
            summary = summary[:-2] + '\n';

    print "Will remove the following packages from %s:" % (suites_list);
    print
    print summary
282 283 284 285
    if Options["Done"]:
        print "Will also close bugs: "+Options["Done"];
    if carbon_copy:
        print "Will also "+carbon_copy[:-1]
J
James Troup 已提交
286 287
    print
    print "------------------- Reason -------------------"
288
    print Options["Reason"];
J
James Troup 已提交
289 290 291 292
    print "----------------------------------------------"
    print

    # If -n/--no-action, drop out here
293
    if Options["No-Action"]:
J
James Troup 已提交
294 295 296 297 298 299 300 301 302 303 304 305
        sys.exit(0);
        
    game_over();

    whoami = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '');
    date = commands.getoutput('date -R');

    # Log first; if it all falls apart I want a record that we at least tried.
    logfile = utils.open_file(Cnf["Melanie::LogFile"], 'a');
    logfile.write("=========================================================================\n");
    logfile.write("[Date: %s] [ftpmaster: %s]\n" % (date, whoami));
    logfile.write("Removed the following packages from %s:\n\n%s" % (suites_list, summary));
306 307 308
    if Options["Done"]:
        logfile.write("Closed bugs: %s\n" % (Options["Done"]));
    logfile.write("\n------------------- Reason -------------------\n%s\n" % (Options["Reason"]));
J
James Troup 已提交
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
    logfile.write("----------------------------------------------\n");
    logfile.flush();
        
    dsc_type_id = db_access.get_override_type_id('dsc');
    deb_type_id = db_access.get_override_type_id('deb');
    
    # Do the actual deletion
    print "Deleting...",
    sys.stdout.flush();
    projectB.query("BEGIN WORK");
    for i in to_remove:
        package = i[0];
        architecture = i[2];
        package_id = i[3];
        for suite_id in suite_ids_list:
            if architecture == "source":
                projectB.query("DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id));
                #print "DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id);
            else:
                projectB.query("DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id));
                #print "DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id);
            # Delete from the override file
331
            if not Options["Partial"]:
J
James Troup 已提交
332 333 334 335 336 337 338 339 340
                if architecture == "source":
                    type_id = dsc_type_id;
                else:
                    type_id = deb_type_id;
                projectB.query("DELETE FROM override WHERE package = '%s' AND type = %s AND suite = %s %s" % (package, type_id, suite_id, over_con_components));
    projectB.query("COMMIT WORK");
    print "done."

    # Send the bug closing messages
341
    if Options["Done"]:
J
James Troup 已提交
342 343 344 345 346 347 348
        Subst = {};
        Subst["__MELANIE_ADDRESS__"] = Cnf["Melanie::MyEmailAddress"];
        Subst["__BUG_SERVER__"] = Cnf["Dinstall::BugServer"];
        if Cnf.Find("Dinstall::Bcc") != "":
            Subst["__BCC__"] = "Bcc: " + Cnf["Dinstall::Bcc"];
        else:
            Subst["__BCC__"] = "X-Filler: 42";
349
        Subst["__CC__"] = "X-Melanie: $Revision: 1.11 $\n" + carbon_copy[:-1];
J
James Troup 已提交
350 351 352 353 354 355 356 357 358
        Subst["__SUITE_LIST__"] = suites_list;
        Subst["__SUMMARY__"] = summary;
        Subst["__ADMIN_ADDRESS__"] = Cnf["Dinstall::MyAdminAddress"];
        Subst["__DISTRO__"] = Cnf["Dinstall::MyDistribution"];
        Subst["__WHOAMI__"] = whoami;
        whereami = utils.where_am_i();
        Archive = Cnf.SubTree("Archive::%s" % (whereami));
        Subst["__MASTER_ARCHIVE__"] = Archive["OriginServer"];
        Subst["__PRIMARY_MIRROR__"] = Archive["PrimaryMirror"];
359
        for bug in string.split(Options["Done"]):
J
James Troup 已提交
360 361
            Subst["__BUG_NUMBER__"] = bug;
            mail_message = utils.TemplateSubst(Subst,open(Cnf["Dir::TemplatesDir"]+"/melanie.bug-close","r").read());
J
James Troup 已提交
362 363 364 365 366 367 368 369 370 371
            utils.send_mail (mail_message, "")
            
    logfile.write("=========================================================================\n");
    logfile.close();

#######################################################################################

if __name__ == '__main__':
    main()