]> git.decadent.org.uk Git - dak.git/blob - melanie
sync
[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.1 2001-01-10 05:58:26 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                  ('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"),
70                  ];
71
72     arguments = apt_pkg.ParseCommandLine(Cnf,Arguments,sys.argv);
73     projectB = pg.connect('projectb', 'localhost');
74     db_access.init(Cnf, projectB);
75
76     # Sanity check options
77     if arguments == []:
78         sys.stderr.write("E: need at least one package name as an argument.\n");
79         sys.exit(1);
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");
82         sys.exit(1);
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");
85         sys.exit(1);
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";
89
90     packages = {};
91     if Cnf["Melanie::Options::Binary-Only"]:
92         field = "b.package";
93     else:
94         field = "s.source";
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] + ")"
100
101     suites_list = "";
102     suite_ids_list = [];
103     con_suites = "AND (";
104     for suite in string.split(Cnf["Melanie::Options::Suite"]):
105         
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."
110             game_over();
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."
115             game_over();
116             
117         suite_id = db_access.get_suite_id(suite);
118         if suite_id == -1:
119             sys.stderr.write("W: suite '%s' not recognised.\n" % (suite));
120         else:
121             con_suites = con_suites + "su.id = %s OR " % (suite_id)
122
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];
127
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));
135             else:
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] + ")";
140     else:
141         con_components = "";    
142         over_con_components = "";
143
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));
150             else:
151                 con_architectures = con_architectures + "a.id = %s OR " % (architecture_id)
152         con_architectures = con_architectures[:-3] + ")"
153     else:
154         con_architectures = "";
155
156
157     print "Working...",
158     sys.stdout.flush();
159     to_remove = [];
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.
164
165     if Cnf["Melanie::Options::Binary-Only"]:
166         # 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():
169             to_remove.append(i);
170     else:
171         # Source-only
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"]:
178             # Source + Binary
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], '/');
187                 try:
188                     dsc = utils.parse_changes(filename);
189                 except utils.cant_open_exc:
190                     sys.stderr.write("W: couldn't open '%s'.\n" % (filename));
191                     continue;
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
198             # to be removed.
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:]);
208                     #else:
209                         #sys.stderr.write("W: skipping '%s' as it's source ('%s') isn't one of the source packages.\n" % (filename, source));
210     print "done."
211
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);
217         os.close(fd);
218         result = os.system("vi %s" % (temp_filename))
219         if result != 0:
220             sys.stderr.write ("vi invocation failed for `%s'!" % (temp_filename))
221             sys.exit(result)
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);
226
227     # Generate the summary of what's to be removed
228     d = {};
229     for i in to_remove:
230         package = i[0];
231         version = i[1];
232         architecture = i[2];
233         if not d.has_key(package):
234             d[package] = {};
235         if not d[package].has_key(version):
236             d[package][version] = [];
237         d[package][version].append(architecture);
238
239     summary = "";
240     packages = d.keys();
241     packages.sort();
242     for package in packages:
243         versions = d[package].keys();
244         versions.sort();
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';
250
251     print "Will remove the following packages from %s:" % (suites_list);
252     print
253     print summary
254     if Cnf["Melanie::Options::Done"]:
255         print "Will also close bugs: "+Cnf["Melanie::Options::Done"];
256     print
257     print "------------------- Reason -------------------"
258     print Cnf["Melanie::Options::Reason"];
259     print "----------------------------------------------"
260     print
261
262     # If -n/--no-action, drop out here
263     if Cnf["Melanie::Options::No-Action"]:
264         sys.exit(0);
265         
266     game_over();
267
268     whoami = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '');
269     date = commands.getoutput('date -R');
270
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");
280     logfile.flush();
281         
282     dsc_type_id = db_access.get_override_type_id('dsc');
283     deb_type_id = db_access.get_override_type_id('deb');
284     
285     # Do the actual deletion
286     print "Deleting...",
287     sys.stdout.flush();
288     projectB.query("BEGIN WORK");
289     for i in to_remove:
290         package = i[0];
291         architecture = i[2];
292         package_id = i[3];
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);
297             else:
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;
304                 else:
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");
308     print "done."
309
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
314 From: %s
315 To: %s-close@bugs.debian.org
316 Bcc: troup@auric.debian.org
317 Subject: Bug#%s: fixed
318
319 We believe that the bug you reported is now fixed; the following
320 package(s) have been removed from %s:
321
322 %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
330 earliest.
331
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.
334
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.
338
339 Debian distribution maintenance software
340 pp.
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, "")
344             
345     logfile.write("=========================================================================\n");
346     logfile.close();
347
348 #######################################################################################
349
350 if __name__ == '__main__':
351     main()
352