3 # General purpose archive tool for ftpmaster
4 # Copyright (C) 2000 James Troup <james@nocrew.org>
5 # $Id: melanie,v 1.6 2001-02-25 02:41:44 mjb 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 editor = os.environ.get("EDITOR","vi")
218 result = os.system("%s %s" % (editor, 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 Packages are never removed from testing by hand. Testing tracks
333 unstable and will automatically remove packages which were removed
334 from unstable when removing them from testing causes no dependency
337 Bugs which have been reported against this package are not automatically
338 removed from the Bug Tracking System. Please check all open bugs and
339 close them or re-assign them to another package if the removed package
340 was superseded by another one.
342 Thank you for reporting the bug, which will now be closed. If you
343 have further comments please address them to %s@bugs.debian.org.
345 This message was generated automatically; if you believe that there is
346 a problem with it please contact the archive administrators by mailing
347 ftpmaster@debian.org.
349 Debian distribution maintenance software
351 %s (the ftpmaster behind the curtain)
352 """ % (Cnf["Melanie::MyEmailAddress"], Cnf["Melanie::MyEmailAddress"], bug, bug, suites_list, summary, bug, whoami);
353 utils.send_mail (mail_message, "")
355 logfile.write("=========================================================================\n");
358 #######################################################################################
360 if __name__ == '__main__':