3 # General purpose archive tool for ftpmaster
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: melanie,v 1.5 2001-02-04 04:28:16 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 ('d',"done","Melanie::Options::Done", "HasArg"), # Bugs fixed
64 ('m',"reason", "Melanie::Options::Reason", "HasArg"), # Hysterical raisins; -m is old-dinstall option for rejection reason
65 ('n',"no-action","Melanie::Options::No-Action"),
66 ('p',"partial", "Melanie::Options::Partial"),
67 ('s',"suite","Melanie::Options::Suite", "HasArg"),
68 ('S',"source-only", "Melanie::Options::Source-Only"),
71 arguments = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
72 projectB = pg.connect('projectb', 'localhost');
73 db_access.init(Cnf, projectB);
75 # Sanity check options
77 sys.stderr.write("E: need at least one package name as an argument.\n");
79 if Cnf["Melanie::Options::Architecture"] and Cnf["Melanie::Options::Source-Only"]:
80 sys.stderr.write("E: can't use -a/--architecutre and -S/--source-only options simultaneously.\n");
82 if Cnf["Melanie::Options::Binary-Only"] and Cnf["Melanie::Options::Source-Only"]:
83 sys.stderr.write("E: can't use -b/--binary-only and -S/--source-only options simultaneously.\n");
85 if Cnf["Melanie::Options::Architecture"] and not Cnf["Melanie::Options::Partial"]:
86 sys.stderr.write("W: -a/--architecture implies -p/--partial.\n");
87 Cnf["Melanie::Options::Partial"] = "true";
90 if Cnf["Melanie::Options::Binary-Only"]:
94 con_packages = "AND (";
95 for package in arguments:
96 con_packages = con_packages + "%s = '%s' OR " % (field, package)
97 packages[package] = "";
98 con_packages = con_packages[:-3] + ")"
102 con_suites = "AND (";
103 for suite in string.split(Cnf["Melanie::Options::Suite"]):
105 if not Cnf["Melanie::Options::No-Action"] and suite == "stable":
106 print "**WARNING** About to remove from the stable suite!"
107 print "This should only be done just prior to a (point) release and not at"
108 print "any other time."
110 elif not Cnf["Melanie::Options::No-Action"] and suite == "testing":
111 print "**WARNING About to remove from the testing suite!"
112 print "There's no need to do this normally as removals from unstable will"
113 print "propogate to testing automagically."
116 suite_id = db_access.get_suite_id(suite);
118 sys.stderr.write("W: suite '%s' not recognised.\n" % (suite));
120 con_suites = con_suites + "su.id = %s OR " % (suite_id)
122 suites_list = suites_list + suite + ", "
123 suite_ids_list.append(suite_id);
124 con_suites = con_suites[:-3] + ")"
125 suites_list = suites_list[:-2];
127 if Cnf["Melanie::Options::Component"]:
128 con_components = "AND (";
129 over_con_components = "AND (";
130 for component in string.split(Cnf["Melanie::Options::Component"]):
131 component_id = db_access.get_component_id(component);
132 if component_id == -1:
133 sys.stderr.write("W: component '%s' not recognised.\n" % (component));
135 con_components = con_components + "c.id = %s OR " % (component_id);
136 over_con_components = over_con_components + "component = %s OR " % (component_id);
137 con_components = con_components[:-3] + ")"
138 over_con_components = over_con_components[:-3] + ")";
141 over_con_components = "";
143 if Cnf["Melanie::Options::Architecture"]:
144 con_architectures = "AND (";
145 for architecture in string.split(Cnf["Melanie::Options::Architecture"]):
146 architecture_id = db_access.get_architecture_id(architecture);
147 if architecture_id == -1:
148 sys.stderr.write("W: architecture '%s' not recognised.\n" % (architecture));
150 con_architectures = con_architectures + "a.id = %s OR " % (architecture_id)
151 con_architectures = con_architectures[:-3] + ")"
153 con_architectures = "";
159 # We have 3 modes of package selection: binary-only, source-only
160 # and source+binary. The first two are trivial and obvious; the
161 # latter is a nasty mess, but very nice from a UI perspective so
162 # we try to support it.
164 if Cnf["Melanie::Options::Binary-Only"]:
166 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));
167 for i in q.getresult():
171 source_packages = {};
172 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));
173 for i in q.getresult():
174 source_packages[i[2]] = i[:2];
175 to_remove.append(i[2:]);
176 if not Cnf["Melanie::Options::Source-Only"]:
178 binary_packages = {};
179 # First get a list of binary package names we suspect are linked to the source
180 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));
181 for i in q.getresult():
182 binary_packages[i[0]] = "";
183 # Then parse each .dsc that we found earlier to see what binary packages it thinks it produces
184 for i in source_packages.keys():
185 filename = string.join(source_packages[i], '/');
187 dsc = utils.parse_changes(filename, 0);
188 except utils.cant_open_exc:
189 sys.stderr.write("W: couldn't open '%s'.\n" % (filename));
191 for package in string.split(dsc.get("binary"), ','):
192 package = string.strip(package);
193 binary_packages[package] = "";
194 # Then for each binary package: find any version in
195 # unstable, check the Source: field in the deb matches our
196 # source package and if so add it to the list of packages
198 for package in binary_packages.keys():
199 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));
200 for i in q.getresult():
201 filename = string.join(i[:2], '/');
202 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(filename,"r")))
203 source = control.Find("Source", control.Find("Package"));
204 source = re_strip_source_version.sub('', source);
205 if source_packages.has_key(source):
206 to_remove.append(i[2:]);
208 #sys.stderr.write("W: skipping '%s' as it's source ('%s') isn't one of the source packages.\n" % (filename, source));
211 # If we don't have a reason; spawn an editor so the user can add one
212 # Write the rejection email out as the <foo>.reason file
213 if not Cnf["Melanie::Options::Reason"] and not Cnf["Melanie::Options::No-Action"]:
214 temp_filename = tempfile.mktemp();
215 fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
217 result = os.system("vi %s" % (temp_filename))
219 sys.stderr.write ("vi invocation failed for `%s'!" % (temp_filename))
221 file = utils.open_file(temp_filename, 'r');
222 for line in file.readlines():
223 Cnf["Melanie::Options::Reason"] = Cnf["Melanie::Options::Reason"] + line;
224 os.unlink(temp_filename);
226 # Generate the summary of what's to be removed
232 if not d.has_key(package):
234 if not d[package].has_key(version):
235 d[package][version] = [];
236 d[package][version].append(architecture);
241 for package in packages:
242 versions = d[package].keys();
244 for version in versions:
245 summary = summary + "%10s | %10s | " % (package, version);
246 for architecture in d[package][version]:
247 summary = "%s%s, " % (summary, architecture);
248 summary = summary[:-2] + '\n';
250 print "Will remove the following packages from %s:" % (suites_list);
253 if Cnf["Melanie::Options::Done"]:
254 print "Will also close bugs: "+Cnf["Melanie::Options::Done"];
256 print "------------------- Reason -------------------"
257 print Cnf["Melanie::Options::Reason"];
258 print "----------------------------------------------"
261 # If -n/--no-action, drop out here
262 if Cnf["Melanie::Options::No-Action"]:
267 whoami = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '');
268 date = commands.getoutput('date -R');
270 # Log first; if it all falls apart I want a record that we at least tried.
271 logfile = utils.open_file(Cnf["Melanie::LogFile"], 'a');
272 logfile.write("=========================================================================\n");
273 logfile.write("[Date: %s] [ftpmaster: %s]\n" % (date, whoami));
274 logfile.write("Removed the following packages from %s:\n\n%s" % (suites_list, summary));
275 if Cnf["Melanie::Options::Done"]:
276 logfile.write("Closed bugs: %s\n" % (Cnf["Melanie::Options::Done"]));
277 logfile.write("\n------------------- Reason -------------------\n%s\n" % (Cnf["Melanie::Options::Reason"]));
278 logfile.write("----------------------------------------------\n");
281 dsc_type_id = db_access.get_override_type_id('dsc');
282 deb_type_id = db_access.get_override_type_id('deb');
284 # Do the actual deletion
287 projectB.query("BEGIN WORK");
292 for suite_id in suite_ids_list:
293 if architecture == "source":
294 projectB.query("DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id));
295 #print "DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id);
297 projectB.query("DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id));
298 #print "DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id);
299 # Delete from the override file
300 if not Cnf["Melanie::Options::Partial"]:
301 if architecture == "source":
302 type_id = dsc_type_id;
304 type_id = deb_type_id;
305 projectB.query("DELETE FROM override WHERE package = '%s' AND type = %s AND suite = %s %s" % (package, type_id, suite_id, over_con_components));
306 projectB.query("COMMIT WORK");
309 # Send the bug closing messages
310 if Cnf["Melanie::Options::Done"]:
311 for bug in string.split(Cnf["Melanie::Options::Done"]):
312 mail_message = """Return-Path: %s
314 To: %s-close@bugs.debian.org
315 Bcc: troup@auric.debian.org
316 Subject: Bug#%s: fixed
318 We believe that the bug you reported is now fixed; the following
319 package(s) have been removed from %s:
322 Note that the package(s) have simply been removed from the tag
323 database and may (or may not) still be in the pool; this is not a bug.
324 The package(s) will be physically removed automatically when no suite
325 references them (and in the case of source, when no binary references
326 it). Please also remember that the changes have been done on the
327 master archive (ftp-master.debian.org) and will not propagate to any
328 mirrors (ftp.debian.org included) until the next cron.daily run at the
331 Packages are never removed from testing by hand. Testing tracks
332 unstable and will automatically remove packages which were removed
333 from unstable when removing them from testing causes no dependency
336 Bugs which have been reported against this package are not automatically
337 removed from the Bug Tracking System. Please check all open bugs and
338 close them or re-assign them to another package if the removed package
339 was superseded by another one.
341 Thank you for reporting the bug, which will now be closed. If you
342 have further comments please address them to %s@bugs.debian.org.
344 This message was generated automatically; if you believe that there is
345 a problem with it please contact the archive administrators by mailing
346 ftpmaster@debian.org.
348 Debian distribution maintenance software
350 %s (the ftpmaster behind the curtain)
351 """ % (Cnf["Melanie::MyEmailAddress"], Cnf["Melanie::MyEmailAddress"], bug, bug, suites_list, summary, bug, whoami);
352 utils.send_mail (mail_message, "")
354 logfile.write("=========================================================================\n");
357 #######################################################################################
359 if __name__ == '__main__':