]> git.decadent.org.uk Git - dak.git/blob - melanie
c76d7015d4dc19bcf537a1e1ab74873f202b30d9
[dak.git] / melanie
1 #!/usr/bin/env python
2
3 # General purpose archive tool for ftpmaster
4 # Copyright (C) 2000  James Troup <james@nocrew.org>
5 # $Id: melanie,v 1.3 2001-01-28 09:06:44 troup Exp $
6
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.
11
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.
16
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
20
21 # X-Listening-To: Astronomy, Metallica - Garage Inc.
22
23 ################################################################################
24
25 import commands, os, pg, pwd, re, string, sys, tempfile
26 import utils, db_access
27 import apt_pkg, apt_inst;
28
29 ################################################################################
30
31 re_strip_source_version = re.compile (r'\s+.*$');
32
33 ################################################################################
34
35 Cnf = None;
36 projectB = None;
37
38 ################################################################################
39
40 def game_over():
41     print "Continue (y/N)? ",
42     answer = string.lower(utils.our_raw_input());
43     if answer != "y":
44         print "Aborted."
45         sys.exit(1);
46
47 ################################################################################
48
49 def main ():
50     global Cnf, projectB;
51
52     apt_pkg.init();
53     
54     Cnf = apt_pkg.newConfiguration();
55     apt_pkg.ReadConfigFileISC(Cnf,utils.which_conf_file());
56
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"),
69                  ];
70
71     arguments = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
72     projectB = pg.connect('projectb', 'localhost');
73     db_access.init(Cnf, projectB);
74
75     # Sanity check options
76     if arguments == []:
77         sys.stderr.write("E: need at least one package name as an argument.\n");
78         sys.exit(1);
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");
81         sys.exit(1);
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");
84         sys.exit(1);
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";
88
89     packages = {};
90     if Cnf["Melanie::Options::Binary-Only"]:
91         field = "b.package";
92     else:
93         field = "s.source";
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] + ")"
99
100     suites_list = "";
101     suite_ids_list = [];
102     con_suites = "AND (";
103     for suite in string.split(Cnf["Melanie::Options::Suite"]):
104         
105         if 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."
109             game_over();
110         elif 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."
114             game_over();
115             
116         suite_id = db_access.get_suite_id(suite);
117         if suite_id == -1:
118             sys.stderr.write("W: suite '%s' not recognised.\n" % (suite));
119         else:
120             con_suites = con_suites + "su.id = %s OR " % (suite_id)
121
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];
126
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));
134             else:
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] + ")";
139     else:
140         con_components = "";    
141         over_con_components = "";
142
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));
149             else:
150                 con_architectures = con_architectures + "a.id = %s OR " % (architecture_id)
151         con_architectures = con_architectures[:-3] + ")"
152     else:
153         con_architectures = "";
154
155
156     print "Working...",
157     sys.stdout.flush();
158     to_remove = [];
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.
163
164     if Cnf["Melanie::Options::Binary-Only"]:
165         # 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():
168             to_remove.append(i);
169     else:
170         # Source-only
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"]:
177             # Source + Binary
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], '/');
186                 try:
187                     dsc = utils.parse_changes(filename, 0);
188                 except utils.cant_open_exc:
189                     sys.stderr.write("W: couldn't open '%s'.\n" % (filename));
190                     continue;
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
197             # to be removed.
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:]);
207                     #else:
208                         #sys.stderr.write("W: skipping '%s' as it's source ('%s') isn't one of the source packages.\n" % (filename, source));
209     print "done."
210
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"]:
214         temp_filename = tempfile.mktemp();
215         fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
216         os.close(fd);
217         result = os.system("vi %s" % (temp_filename))
218         if result != 0:
219             sys.stderr.write ("vi invocation failed for `%s'!" % (temp_filename))
220             sys.exit(result)
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);
225
226     # Generate the summary of what's to be removed
227     d = {};
228     for i in to_remove:
229         package = i[0];
230         version = i[1];
231         architecture = i[2];
232         if not d.has_key(package):
233             d[package] = {};
234         if not d[package].has_key(version):
235             d[package][version] = [];
236         d[package][version].append(architecture);
237
238     summary = "";
239     packages = d.keys();
240     packages.sort();
241     for package in packages:
242         versions = d[package].keys();
243         versions.sort();
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';
249
250     print "Will remove the following packages from %s:" % (suites_list);
251     print
252     print summary
253     if Cnf["Melanie::Options::Done"]:
254         print "Will also close bugs: "+Cnf["Melanie::Options::Done"];
255     print
256     print "------------------- Reason -------------------"
257     print Cnf["Melanie::Options::Reason"];
258     print "----------------------------------------------"
259     print
260
261     # If -n/--no-action, drop out here
262     if Cnf["Melanie::Options::No-Action"]:
263         sys.exit(0);
264         
265     game_over();
266
267     whoami = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '');
268     date = commands.getoutput('date -R');
269
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");
279     logfile.flush();
280         
281     dsc_type_id = db_access.get_override_type_id('dsc');
282     deb_type_id = db_access.get_override_type_id('deb');
283     
284     # Do the actual deletion
285     print "Deleting...",
286     sys.stdout.flush();
287     projectB.query("BEGIN WORK");
288     for i in to_remove:
289         package = i[0];
290         architecture = i[2];
291         package_id = i[3];
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);
296             else:
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;
303                 else:
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");
307     print "done."
308
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
313 From: %s
314 To: %s-close@bugs.debian.org
315 Bcc: troup@auric.debian.org
316 Subject: Bug#%s: fixed
317
318 We believe that the bug you reported is now fixed; the following
319 package(s) have been removed from %s:
320
321 %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
329 earliest.
330
331 Bugs which have been reported against this package are not automatically
332 removed from the Bug Tracking System.  Please check all open bugs and
333 close them or re-assign them to another package if the removed package
334 was superseded by another one.
335
336 Thank you for reporting the bug, which will now be closed.  If you
337 have further comments please address them to %s@bugs.debian.org.
338
339 This message was generated automatically; if you believe that there is
340 a problem with it please contact the archive administrators by mailing
341 ftpmaster@debian.org.
342
343 Debian distribution maintenance software
344 pp.
345 %s (the ftpmaster behind the curtain)
346 """ % (Cnf["Melanie::MyEmailAddress"], Cnf["Melanie::MyEmailAddress"], bug, bug, suites_list, summary, bug, whoami);
347             utils.send_mail (mail_message, "")
348             
349     logfile.write("=========================================================================\n");
350     logfile.close();
351
352 #######################################################################################
353
354 if __name__ == '__main__':
355     main()
356