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

# General purpose archive tool for ftpmaster
# Copyright (C) 2000  James Troup <james@nocrew.org>
5
# $Id: melanie,v 1.6 2001-02-25 02:41:44 mjb 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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104

# 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;

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

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"),
                 ('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);
    projectB = pg.connect('projectb', 'localhost');
    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);
    if Cnf["Melanie::Options::Architecture"] and Cnf["Melanie::Options::Source-Only"]:
        sys.stderr.write("E: can't use -a/--architecutre and -S/--source-only options simultaneously.\n");
        sys.exit(1);
    if Cnf["Melanie::Options::Binary-Only"] and Cnf["Melanie::Options::Source-Only"]:
        sys.stderr.write("E: can't use -b/--binary-only and -S/--source-only options simultaneously.\n");
        sys.exit(1);
    if Cnf["Melanie::Options::Architecture"] and not Cnf["Melanie::Options::Partial"]:
        sys.stderr.write("W: -a/--architecture implies -p/--partial.\n");
        Cnf["Melanie::Options::Partial"] = "true";

    packages = {};
    if Cnf["Melanie::Options::Binary-Only"]:
        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 (";
    for suite in string.split(Cnf["Melanie::Options::Suite"]):
        
J
sync  
James Troup 已提交
105
        if not Cnf["Melanie::Options::No-Action"] and suite == "stable":
J
James Troup 已提交
106 107 108 109
            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();
J
sync  
James Troup 已提交
110
        elif not Cnf["Melanie::Options::No-Action"] and suite == "testing":
J
James Troup 已提交
111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186
            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];

    if Cnf["Melanie::Options::Component"]:
        con_components = "AND (";
        over_con_components = "AND (";
        for component in string.split(Cnf["Melanie::Options::Component"]):
            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 = "";

    if Cnf["Melanie::Options::Architecture"]:
        con_architectures = "AND (";
        for architecture in string.split(Cnf["Melanie::Options::Architecture"]):
            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.

    if Cnf["Melanie::Options::Binary-Only"]:
        # 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:]);
        if not Cnf["Melanie::Options::Source-Only"]:
            # 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 已提交
187
                    dsc = utils.parse_changes(filename, 0);
J
James Troup 已提交
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212
                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
J
sync  
James Troup 已提交
213
    if not Cnf["Melanie::Options::Reason"] and not Cnf["Melanie::Options::No-Action"]:
J
James Troup 已提交
214 215 216
        temp_filename = tempfile.mktemp();
        fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
        os.close(fd);
217 218
        editor = os.environ.get("EDITOR","vi")
        result = os.system("%s %s" % (editor, temp_filename))
J
James Troup 已提交
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 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 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331
        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():
            Cnf["Melanie::Options::Reason"] = Cnf["Melanie::Options::Reason"] + line;
        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
    if Cnf["Melanie::Options::Done"]:
        print "Will also close bugs: "+Cnf["Melanie::Options::Done"];
    print
    print "------------------- Reason -------------------"
    print Cnf["Melanie::Options::Reason"];
    print "----------------------------------------------"
    print

    # If -n/--no-action, drop out here
    if Cnf["Melanie::Options::No-Action"]:
        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));
    if Cnf["Melanie::Options::Done"]:
        logfile.write("Closed bugs: %s\n" % (Cnf["Melanie::Options::Done"]));
    logfile.write("\n------------------- Reason -------------------\n%s\n" % (Cnf["Melanie::Options::Reason"]));
    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
            if not Cnf["Melanie::Options::Partial"]:
                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
    if Cnf["Melanie::Options::Done"]:
        for bug in string.split(Cnf["Melanie::Options::Done"]):
            mail_message = """Return-Path: %s
From: %s
To: %s-close@bugs.debian.org
Bcc: troup@auric.debian.org
Subject: Bug#%s: fixed

We believe that the bug you reported is now fixed; the following
package(s) have been removed from %s:

%s
Note that the package(s) have simply been removed from the tag
database and may (or may not) still be in the pool; this is not a bug.
The package(s) will be physically removed automatically when no suite
references them (and in the case of source, when no binary references
it).  Please also remember that the changes have been done on the
master archive (ftp-master.debian.org) and will not propagate to any
mirrors (ftp.debian.org included) until the next cron.daily run at the
earliest.

332 333 334 335 336
Packages are never removed from testing by hand.  Testing tracks
unstable and will automatically remove packages which were removed
from unstable when removing them from testing causes no dependency
problems.

J
sync  
James Troup 已提交
337 338 339 340 341
Bugs which have been reported against this package are not automatically
removed from the Bug Tracking System.  Please check all open bugs and
close them or re-assign them to another package if the removed package
was superseded by another one.

J
James Troup 已提交
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362
Thank you for reporting the bug, which will now be closed.  If you
have further comments please address them to %s@bugs.debian.org.

This message was generated automatically; if you believe that there is
a problem with it please contact the archive administrators by mailing
ftpmaster@debian.org.

Debian distribution maintenance software
pp.
%s (the ftpmaster behind the curtain)
""" % (Cnf["Melanie::MyEmailAddress"], Cnf["Melanie::MyEmailAddress"], bug, bug, suites_list, summary, bug, whoami);
            utils.send_mail (mail_message, "")
            
    logfile.write("=========================================================================\n");
    logfile.close();

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

if __name__ == '__main__':
    main()