3 # General purpose archive tool for ftpmaster
4 # Copyright (C) 2000, 2001 James Troup <james@nocrew.org>
5 # $Id: melanie,v 1.7 2001-03-02 02:26:17 troup Exp $
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 2 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program; if not, write to the Free Software
19 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21 # X-Listening-To: Astronomy, Metallica - Garage Inc.
23 ################################################################################
25 import commands, os, pg, pwd, re, string, sys, tempfile
26 import utils, db_access
27 import apt_pkg, apt_inst;
29 ################################################################################
31 re_strip_source_version = re.compile (r'\s+.*$');
33 ################################################################################
38 ################################################################################
41 print "Continue (y/N)? ",
42 answer = string.lower(utils.our_raw_input());
47 ################################################################################
54 Cnf = apt_pkg.newConfiguration();
55 apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
57 Arguments = [('D',"debug","Melanie::Options::Debug", "IntVal"),
58 ('h',"help","Melanie::Options::Help"),
59 ('V',"version","Melanie::Options::Version"),
60 ('a',"architecture","Melanie::Options::Architecture", "HasArg"),
61 ('b',"binary", "Melanie::Options::Binary-Only"),
62 ('c',"component", "Melanie::Options::Component", "HasArg"),
63 ('C',"carbon-copy", "Melanie::Options::Carbon-Copy", "HasArg"), # Bugs to Cc
64 ('d',"done","Melanie::Options::Done", "HasArg"), # Bugs fixed
65 ('m',"reason", "Melanie::Options::Reason", "HasArg"), # Hysterical raisins; -m is old-dinstall option for rejection reason
66 ('n',"no-action","Melanie::Options::No-Action"),
67 ('p',"partial", "Melanie::Options::Partial"),
68 ('s',"suite","Melanie::Options::Suite", "HasArg"),
69 ('S',"source-only", "Melanie::Options::Source-Only"),
72 arguments = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
73 Options = Cnf.SubTree("Melanie::Options")
74 projectB = pg.connect('projectb', 'localhost');
75 db_access.init(Cnf, projectB);
77 # Sanity check options
79 sys.stderr.write("E: need at least one package name as an argument.\n");
81 if Options["Architecture"] and Options["Source-Only"]:
82 sys.stderr.write("E: can't use -a/--architecutre and -S/--source-only options simultaneously.\n");
84 if Options["Binary-Only"] and Options["Source-Only"]:
85 sys.stderr.write("E: can't use -b/--binary-only and -S/--source-only options simultaneously.\n");
87 if Options["Architecture"] and not Options["Partial"]:
88 sys.stderr.write("W: -a/--architecture implies -p/--partial.\n");
89 Options["Partial"] = "true";
91 # Process -C/--carbon-copy
93 # Accept 3 types of arguments (space separated):
94 # 1) a number - assumed to be a bug number, i.e. nnnnn@bugs.debian.org
95 # 2) the keyword 'package' - cc's $arch@packages.debian.org for every argument
96 # 3) contains a '@' - assumed to be an email address, used unmofidied
99 for copy_to in string.split(Options.get("Carbon-Copy")):
100 if utils.str_isnum(copy_to):
101 carbon_copy = carbon_copy + copy_to + "@bugs.debian.org, "
102 elif copy_to == 'package':
103 for package in arguments:
104 carbon_copy = carbon_copy + package + "@packages.debian.org, "
106 carbon_copy = carbon_copy + copy_to + ", "
108 sys.stderr.write("Invalid -C/--carbon-copy argument '%s'; not a bug number, 'package' or email address.\n" % (copy_to));
110 # Make it a real email header
111 if carbon_copy != "":
112 carbon_copy = "Cc: " + carbon_copy[:-2] + '\n'
115 if Options["Binary-Only"]:
119 con_packages = "AND (";
120 for package in arguments:
121 con_packages = con_packages + "%s = '%s' OR " % (field, package)
122 packages[package] = "";
123 con_packages = con_packages[:-3] + ")"
127 con_suites = "AND (";
128 for suite in string.split(Options["Suite"]):
130 if not Options["No-Action"] and suite == "stable":
131 print "**WARNING** About to remove from the stable suite!"
132 print "This should only be done just prior to a (point) release and not at"
133 print "any other time."
135 elif not Options["No-Action"] and suite == "testing":
136 print "**WARNING About to remove from the testing suite!"
137 print "There's no need to do this normally as removals from unstable will"
138 print "propogate to testing automagically."
141 suite_id = db_access.get_suite_id(suite);
143 sys.stderr.write("W: suite '%s' not recognised.\n" % (suite));
145 con_suites = con_suites + "su.id = %s OR " % (suite_id)
147 suites_list = suites_list + suite + ", "
148 suite_ids_list.append(suite_id);
149 con_suites = con_suites[:-3] + ")"
150 suites_list = suites_list[:-2];
152 if Options["Component"]:
153 con_components = "AND (";
154 over_con_components = "AND (";
155 for component in string.split(Options["Component"]):
156 component_id = db_access.get_component_id(component);
157 if component_id == -1:
158 sys.stderr.write("W: component '%s' not recognised.\n" % (component));
160 con_components = con_components + "c.id = %s OR " % (component_id);
161 over_con_components = over_con_components + "component = %s OR " % (component_id);
162 con_components = con_components[:-3] + ")"
163 over_con_components = over_con_components[:-3] + ")";
166 over_con_components = "";
168 if Options["Architecture"]:
169 con_architectures = "AND (";
170 for architecture in string.split(Options["Architecture"]):
171 architecture_id = db_access.get_architecture_id(architecture);
172 if architecture_id == -1:
173 sys.stderr.write("W: architecture '%s' not recognised.\n" % (architecture));
175 con_architectures = con_architectures + "a.id = %s OR " % (architecture_id)
176 con_architectures = con_architectures[:-3] + ")"
178 con_architectures = "";
184 # We have 3 modes of package selection: binary-only, source-only
185 # and source+binary. The first two are trivial and obvious; the
186 # latter is a nasty mess, but very nice from a UI perspective so
187 # we try to support it.
189 if Options["Binary-Only"]:
191 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));
192 for i in q.getresult():
196 source_packages = {};
197 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));
198 for i in q.getresult():
199 source_packages[i[2]] = i[:2];
200 to_remove.append(i[2:]);
201 if not Options["Source-Only"]:
203 binary_packages = {};
204 # First get a list of binary package names we suspect are linked to the source
205 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));
206 for i in q.getresult():
207 binary_packages[i[0]] = "";
208 # Then parse each .dsc that we found earlier to see what binary packages it thinks it produces
209 for i in source_packages.keys():
210 filename = string.join(source_packages[i], '/');
212 dsc = utils.parse_changes(filename, 0);
213 except utils.cant_open_exc:
214 sys.stderr.write("W: couldn't open '%s'.\n" % (filename));
216 for package in string.split(dsc.get("binary"), ','):
217 package = string.strip(package);
218 binary_packages[package] = "";
219 # Then for each binary package: find any version in
220 # unstable, check the Source: field in the deb matches our
221 # source package and if so add it to the list of packages
223 for package in binary_packages.keys():
224 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));
225 for i in q.getresult():
226 filename = string.join(i[:2], '/');
227 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(filename,"r")))
228 source = control.Find("Source", control.Find("Package"));
229 source = re_strip_source_version.sub('', source);
230 if source_packages.has_key(source):
231 to_remove.append(i[2:]);
233 #sys.stderr.write("W: skipping '%s' as it's source ('%s') isn't one of the source packages.\n" % (filename, source));
236 # If we don't have a reason; spawn an editor so the user can add one
237 # Write the rejection email out as the <foo>.reason file
238 if not Options["Reason"] and not Options["No-Action"]:
239 temp_filename = tempfile.mktemp();
240 fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
242 editor = os.environ.get("EDITOR","vi")
243 result = os.system("%s %s" % (editor, temp_filename))
245 sys.stderr.write ("vi invocation failed for `%s'!" % (temp_filename))
247 file = utils.open_file(temp_filename, 'r');
248 for line in file.readlines():
249 Options["Reason"] = Options["Reason"] + line;
250 os.unlink(temp_filename);
252 # Generate the summary of what's to be removed
258 if not d.has_key(package):
260 if not d[package].has_key(version):
261 d[package][version] = [];
262 d[package][version].append(architecture);
267 for package in packages:
268 versions = d[package].keys();
270 for version in versions:
271 summary = summary + "%10s | %10s | " % (package, version);
272 for architecture in d[package][version]:
273 summary = "%s%s, " % (summary, architecture);
274 summary = summary[:-2] + '\n';
276 print "Will remove the following packages from %s:" % (suites_list);
280 print "Will also close bugs: "+Options["Done"];
282 print "Will also "+carbon_copy[:-1]
284 print "------------------- Reason -------------------"
285 print Options["Reason"];
286 print "----------------------------------------------"
289 # If -n/--no-action, drop out here
290 if Options["No-Action"]:
295 whoami = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '');
296 date = commands.getoutput('date -R');
298 # Log first; if it all falls apart I want a record that we at least tried.
299 logfile = utils.open_file(Cnf["Melanie::LogFile"], 'a');
300 logfile.write("=========================================================================\n");
301 logfile.write("[Date: %s] [ftpmaster: %s]\n" % (date, whoami));
302 logfile.write("Removed the following packages from %s:\n\n%s" % (suites_list, summary));
304 logfile.write("Closed bugs: %s\n" % (Options["Done"]));
305 logfile.write("\n------------------- Reason -------------------\n%s\n" % (Options["Reason"]));
306 logfile.write("----------------------------------------------\n");
309 dsc_type_id = db_access.get_override_type_id('dsc');
310 deb_type_id = db_access.get_override_type_id('deb');
312 # Do the actual deletion
315 projectB.query("BEGIN WORK");
320 for suite_id in suite_ids_list:
321 if architecture == "source":
322 projectB.query("DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id));
323 #print "DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id);
325 projectB.query("DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id));
326 #print "DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id);
327 # Delete from the override file
328 if not Options["Partial"]:
329 if architecture == "source":
330 type_id = dsc_type_id;
332 type_id = deb_type_id;
333 projectB.query("DELETE FROM override WHERE package = '%s' AND type = %s AND suite = %s %s" % (package, type_id, suite_id, over_con_components));
334 projectB.query("COMMIT WORK");
337 # Send the bug closing messages
339 for bug in string.split(Options["Done"]):
340 mail_message = """Return-Path: %s
342 To: %s-close@bugs.debian.org
343 Bcc: troup@auric.debian.org
344 Bcc: removed-packages@qa.debian.org
345 %sSubject: Bug#%s: fixed
347 We believe that the bug you reported is now fixed; the following
348 package(s) have been removed from %s:
351 Note that the package(s) have simply been removed from the tag
352 database and may (or may not) still be in the pool; this is not a bug.
353 The package(s) will be physically removed automatically when no suite
354 references them (and in the case of source, when no binary references
355 it). Please also remember that the changes have been done on the
356 master archive (ftp-master.debian.org) and will not propagate to any
357 mirrors (ftp.debian.org included) until the next cron.daily run at the
360 Packages are never removed from testing by hand. Testing tracks
361 unstable and will automatically remove packages which were removed
362 from unstable when removing them from testing causes no dependency
365 Bugs which have been reported against this package are not automatically
366 removed from the Bug Tracking System. Please check all open bugs and
367 close them or re-assign them to another package if the removed package
368 was superseded by another one.
370 Thank you for reporting the bug, which will now be closed. If you
371 have further comments please address them to %s@bugs.debian.org.
373 This message was generated automatically; if you believe that there is
374 a problem with it please contact the archive administrators by mailing
375 ftpmaster@debian.org.
377 Debian distribution maintenance software
379 %s (the ftpmaster behind the curtain)
380 """ % (Cnf["Melanie::MyEmailAddress"], Cnf["Melanie::MyEmailAddress"], bug, carbon_copy, bug, suites_list, summary, bug, whoami);
381 utils.send_mail (mail_message, "")
383 logfile.write("=========================================================================\n");
386 #######################################################################################
388 if __name__ == '__main__':