3 # General purpose archive tool for ftpmaster
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: melanie,v 1.1 2001-01-10 05:58:26 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 ('o',"orphan", "Melanie::Options::Orphan"),
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 projectB = pg.connect('projectb', 'localhost');
74 db_access.init(Cnf, projectB);
76 # Sanity check options
78 sys.stderr.write("E: need at least one package name as an argument.\n");
80 if Cnf["Melanie::Options::Architecture"] and Cnf["Melanie::Options::Source-Only"]:
81 sys.stderr.write("E: can't use -a/--architecutre and -S/--source-only options simultaneously.\n");
83 if Cnf["Melanie::Options::Binary-Only"] and Cnf["Melanie::Options::Source-Only"]:
84 sys.stderr.write("E: can't use -b/--binary-only and -S/--source-only options simultaneously.\n");
86 if Cnf["Melanie::Options::Architecture"] and not Cnf["Melanie::Options::Partial"]:
87 sys.stderr.write("W: -a/--architecture implies -p/--partial.\n");
88 Cnf["Melanie::Options::Partial"] = "true";
91 if Cnf["Melanie::Options::Binary-Only"]:
95 con_packages = "AND (";
96 for package in arguments:
97 con_packages = con_packages + "%s = '%s' OR " % (field, package)
98 packages[package] = "";
99 con_packages = con_packages[:-3] + ")"
103 con_suites = "AND (";
104 for suite in string.split(Cnf["Melanie::Options::Suite"]):
106 if suite == "stable":
107 print "**WARNING** About to remove from the stable suite!"
108 print "This should only be done just prior to a (point) release and not at"
109 print "any other time."
111 elif suite == "testing":
112 print "**WARNING About to remove from the testing suite!"
113 print "There's no need to do this normally as removals from unstable will"
114 print "propogate to testing automagically."
117 suite_id = db_access.get_suite_id(suite);
119 sys.stderr.write("W: suite '%s' not recognised.\n" % (suite));
121 con_suites = con_suites + "su.id = %s OR " % (suite_id)
123 suites_list = suites_list + suite + ", "
124 suite_ids_list.append(suite_id);
125 con_suites = con_suites[:-3] + ")"
126 suites_list = suites_list[:-2];
128 if Cnf["Melanie::Options::Component"]:
129 con_components = "AND (";
130 over_con_components = "AND (";
131 for component in string.split(Cnf["Melanie::Options::Component"]):
132 component_id = db_access.get_component_id(component);
133 if component_id == -1:
134 sys.stderr.write("W: component '%s' not recognised.\n" % (component));
136 con_components = con_components + "c.id = %s OR " % (component_id);
137 over_con_components = over_con_components + "component = %s OR " % (component_id);
138 con_components = con_components[:-3] + ")"
139 over_con_components = over_con_components[:-3] + ")";
142 over_con_components = "";
144 if Cnf["Melanie::Options::Architecture"]:
145 con_architectures = "AND (";
146 for architecture in string.split(Cnf["Melanie::Options::Architecture"]):
147 architecture_id = db_access.get_architecture_id(architecture);
148 if architecture_id == -1:
149 sys.stderr.write("W: architecture '%s' not recognised.\n" % (architecture));
151 con_architectures = con_architectures + "a.id = %s OR " % (architecture_id)
152 con_architectures = con_architectures[:-3] + ")"
154 con_architectures = "";
160 # We have 3 modes of package selection: binary-only, source-only
161 # and source+binary. The first two are trivial and obvious; the
162 # latter is a nasty mess, but very nice from a UI perspective so
163 # we try to support it.
165 if Cnf["Melanie::Options::Binary-Only"]:
167 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));
168 for i in q.getresult():
172 source_packages = {};
173 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));
174 for i in q.getresult():
175 source_packages[i[2]] = i[:2];
176 to_remove.append(i[2:]);
177 if not Cnf["Melanie::Options::Source-Only"]:
179 binary_packages = {};
180 # First get a list of binary package names we suspect are linked to the source
181 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));
182 for i in q.getresult():
183 binary_packages[i[0]] = "";
184 # Then parse each .dsc that we found earlier to see what binary packages it thinks it produces
185 for i in source_packages.keys():
186 filename = string.join(source_packages[i], '/');
188 dsc = utils.parse_changes(filename);
189 except utils.cant_open_exc:
190 sys.stderr.write("W: couldn't open '%s'.\n" % (filename));
192 for package in string.split(dsc.get("binary"), ','):
193 package = string.strip(package);
194 binary_packages[package] = "";
195 # Then for each binary package: find any version in
196 # unstable, check the Source: field in the deb matches our
197 # source package and if so add it to the list of packages
199 for package in binary_packages.keys():
200 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));
201 for i in q.getresult():
202 filename = string.join(i[:2], '/');
203 control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(filename,"r")))
204 source = control.Find("Source", control.Find("Package"));
205 source = re_strip_source_version.sub('', source);
206 if source_packages.has_key(source):
207 to_remove.append(i[2:]);
209 #sys.stderr.write("W: skipping '%s' as it's source ('%s') isn't one of the source packages.\n" % (filename, source));
212 # If we don't have a reason; spawn an editor so the user can add one
213 # Write the rejection email out as the <foo>.reason file
214 if not Cnf["Melanie::Options::Reason"]:
215 temp_filename = tempfile.mktemp();
216 fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
218 result = os.system("vi %s" % (temp_filename))
220 sys.stderr.write ("vi invocation failed for `%s'!" % (temp_filename))
222 file = utils.open_file(temp_filename, 'r');
223 for line in file.readlines():
224 Cnf["Melanie::Options::Reason"] = Cnf["Melanie::Options::Reason"] + line;
225 os.unlink(temp_filename);
227 # Generate the summary of what's to be removed
233 if not d.has_key(package):
235 if not d[package].has_key(version):
236 d[package][version] = [];
237 d[package][version].append(architecture);
242 for package in packages:
243 versions = d[package].keys();
245 for version in versions:
246 summary = summary + "%10s | %10s | " % (package, version);
247 for architecture in d[package][version]:
248 summary = "%s%s, " % (summary, architecture);
249 summary = summary[:-2] + '\n';
251 print "Will remove the following packages from %s:" % (suites_list);
254 if Cnf["Melanie::Options::Done"]:
255 print "Will also close bugs: "+Cnf["Melanie::Options::Done"];
257 print "------------------- Reason -------------------"
258 print Cnf["Melanie::Options::Reason"];
259 print "----------------------------------------------"
262 # If -n/--no-action, drop out here
263 if Cnf["Melanie::Options::No-Action"]:
268 whoami = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '');
269 date = commands.getoutput('date -R');
271 # Log first; if it all falls apart I want a record that we at least tried.
272 logfile = utils.open_file(Cnf["Melanie::LogFile"], 'a');
273 logfile.write("=========================================================================\n");
274 logfile.write("[Date: %s] [ftpmaster: %s]\n" % (date, whoami));
275 logfile.write("Removed the following packages from %s:\n\n%s" % (suites_list, summary));
276 if Cnf["Melanie::Options::Done"]:
277 logfile.write("Closed bugs: %s\n" % (Cnf["Melanie::Options::Done"]));
278 logfile.write("\n------------------- Reason -------------------\n%s\n" % (Cnf["Melanie::Options::Reason"]));
279 logfile.write("----------------------------------------------\n");
282 dsc_type_id = db_access.get_override_type_id('dsc');
283 deb_type_id = db_access.get_override_type_id('deb');
285 # Do the actual deletion
288 projectB.query("BEGIN WORK");
293 for suite_id in suite_ids_list:
294 if architecture == "source":
295 projectB.query("DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id));
296 #print "DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id);
298 projectB.query("DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id));
299 #print "DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id);
300 # Delete from the override file
301 if not Cnf["Melanie::Options::Partial"]:
302 if architecture == "source":
303 type_id = dsc_type_id;
305 type_id = deb_type_id;
306 projectB.query("DELETE FROM override WHERE package = '%s' AND type = %s AND suite = %s %s" % (package, type_id, suite_id, over_con_components));
307 projectB.query("COMMIT WORK");
310 # Send the bug closing messages
311 if Cnf["Melanie::Options::Done"]:
312 for bug in string.split(Cnf["Melanie::Options::Done"]):
313 mail_message = """Return-Path: %s
315 To: %s-close@bugs.debian.org
316 Bcc: troup@auric.debian.org
317 Subject: Bug#%s: fixed
319 We believe that the bug you reported is now fixed; the following
320 package(s) have been removed from %s:
323 Note that the package(s) have simply been removed from the tag
324 database and may (or may not) still be in the pool; this is not a bug.
325 The package(s) will be physically removed automatically when no suite
326 references them (and in the case of source, when no binary references
327 it). Please also remember that the changes have been done on the
328 master archive (ftp-master.debian.org) and will not propagate to any
329 mirrors (ftp.debian.org included) until the next cron.daily run at the
332 Thank you for reporting the bug, which will now be closed. If you
333 have further comments please address them to %s@bugs.debian.org.
335 This message was generated automatically; if you believe that there is
336 a problem with it please contact the archive administrators by mailing
337 ftpmaster@debian.org.
339 Debian distribution maintenance software
341 %s (the ftpmaster behind the curtain)
342 """ % (Cnf["Melanie::MyEmailAddress"], Cnf["Melanie::MyEmailAddress"], bug, bug, suites_list, summary, bug, whoami);
343 utils.send_mail (mail_message, "")
345 logfile.write("=========================================================================\n");
348 #######################################################################################
350 if __name__ == '__main__':