]> git.decadent.org.uk Git - dak.git/blob - melanie
cad844a6f7de953933963f483c87d4f9f4a4ccd8
[dak.git] / melanie
1 #!/usr/bin/env python
2
3 # General purpose archive tool for ftpmaster
4 # Copyright (C) 2000, 2001  James Troup <james@nocrew.org>
5 # $Id: melanie,v 1.7 2001-03-02 02:26:17 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                  ('C',"carbon-copy", "Melanie::Options::Carbon-Copy", "HasArg"), # Bugs to Cc
64                  ('d',"done","Melanie::Options::Done", "HasArg"), # Bugs fixed
65                  ('m',"reason", "Melanie::Options::Reason", "HasArg"), # Hysterical raisins; -m is old-dinstall option for rejection reason
66                  ('n',"no-action","Melanie::Options::No-Action"),
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     Options = Cnf.SubTree("Melanie::Options")
74     projectB = pg.connect('projectb', 'localhost');
75     db_access.init(Cnf, projectB);
76
77     # Sanity check options
78     if arguments == []:
79         sys.stderr.write("E: need at least one package name as an argument.\n");
80         sys.exit(1);
81     if Options["Architecture"] and Options["Source-Only"]:
82         sys.stderr.write("E: can't use -a/--architecutre and -S/--source-only options simultaneously.\n");
83         sys.exit(1);
84     if Options["Binary-Only"] and Options["Source-Only"]:
85         sys.stderr.write("E: can't use -b/--binary-only and -S/--source-only options simultaneously.\n");
86         sys.exit(1);
87     if Options["Architecture"] and not Options["Partial"]:
88         sys.stderr.write("W: -a/--architecture implies -p/--partial.\n");
89         Options["Partial"] = "true";
90
91     # Process -C/--carbon-copy
92     #
93     # Accept 3 types of arguments (space separated):
94     #  1) a number - assumed to be a bug number, i.e. nnnnn@bugs.debian.org
95     #  2) the keyword 'package' - cc's $arch@packages.debian.org for every argument
96     #  3) contains a '@' - assumed to be an email address, used unmofidied
97     #
98     carbon_copy = ""
99     for copy_to in string.split(Options.get("Carbon-Copy")):
100         if utils.str_isnum(copy_to):
101             carbon_copy = carbon_copy + copy_to + "@bugs.debian.org, "
102         elif copy_to == 'package':
103             for package in arguments:
104                 carbon_copy = carbon_copy + package + "@packages.debian.org, "
105         elif '@' in copy_to:
106             carbon_copy = carbon_copy + copy_to + ", "
107         else:
108             sys.stderr.write("Invalid -C/--carbon-copy argument '%s'; not a bug number, 'package' or email address.\n" % (copy_to));
109             sys.exit(1);
110     # Make it a real email header
111     if carbon_copy != "":
112         carbon_copy = "Cc: " + carbon_copy[:-2] + '\n'
113
114     packages = {};
115     if Options["Binary-Only"]:
116         field = "b.package";
117     else:
118         field = "s.source";
119     con_packages = "AND (";
120     for package in arguments:
121         con_packages = con_packages + "%s = '%s' OR " % (field, package)
122         packages[package] = "";
123     con_packages = con_packages[:-3] + ")"
124
125     suites_list = "";
126     suite_ids_list = [];
127     con_suites = "AND (";
128     for suite in string.split(Options["Suite"]):
129         
130         if not Options["No-Action"] and suite == "stable":
131             print "**WARNING** About to remove from the stable suite!"
132             print "This should only be done just prior to a (point) release and not at"
133             print "any other time."
134             game_over();
135         elif not Options["No-Action"] and suite == "testing":
136             print "**WARNING About to remove from the testing suite!"
137             print "There's no need to do this normally as removals from unstable will"
138             print "propogate to testing automagically."
139             game_over();
140             
141         suite_id = db_access.get_suite_id(suite);
142         if suite_id == -1:
143             sys.stderr.write("W: suite '%s' not recognised.\n" % (suite));
144         else:
145             con_suites = con_suites + "su.id = %s OR " % (suite_id)
146
147         suites_list = suites_list + suite + ", "
148         suite_ids_list.append(suite_id);
149     con_suites = con_suites[:-3] + ")"
150     suites_list = suites_list[:-2];
151
152     if Options["Component"]:
153         con_components = "AND (";
154         over_con_components = "AND (";
155         for component in string.split(Options["Component"]):
156             component_id = db_access.get_component_id(component);
157             if component_id == -1:
158                 sys.stderr.write("W: component '%s' not recognised.\n" % (component));
159             else:
160                 con_components = con_components + "c.id = %s OR " % (component_id);
161                 over_con_components = over_con_components + "component = %s OR " % (component_id);
162         con_components = con_components[:-3] + ")"
163         over_con_components = over_con_components[:-3] + ")";
164     else:
165         con_components = "";    
166         over_con_components = "";
167
168     if Options["Architecture"]:
169         con_architectures = "AND (";
170         for architecture in string.split(Options["Architecture"]):
171             architecture_id = db_access.get_architecture_id(architecture);
172             if architecture_id == -1:
173                 sys.stderr.write("W: architecture '%s' not recognised.\n" % (architecture));
174             else:
175                 con_architectures = con_architectures + "a.id = %s OR " % (architecture_id)
176         con_architectures = con_architectures[:-3] + ")"
177     else:
178         con_architectures = "";
179
180
181     print "Working...",
182     sys.stdout.flush();
183     to_remove = [];
184     # We have 3 modes of package selection: binary-only, source-only
185     # and source+binary.  The first two are trivial and obvious; the
186     # latter is a nasty mess, but very nice from a UI perspective so
187     # we try to support it.
188
189     if Options["Binary-Only"]:
190         # Binary-only
191         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));
192         for i in q.getresult():
193             to_remove.append(i);
194     else:
195         # Source-only
196         source_packages = {};
197         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));
198         for i in q.getresult():
199             source_packages[i[2]] = i[:2];
200             to_remove.append(i[2:]);
201         if not Options["Source-Only"]:
202             # Source + Binary
203             binary_packages = {};
204             # First get a list of binary package names we suspect are linked to the source
205             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));
206             for i in q.getresult():
207                 binary_packages[i[0]] = "";
208             # Then parse each .dsc that we found earlier to see what binary packages it thinks it produces
209             for i in source_packages.keys():
210                 filename = string.join(source_packages[i], '/');
211                 try:
212                     dsc = utils.parse_changes(filename, 0);
213                 except utils.cant_open_exc:
214                     sys.stderr.write("W: couldn't open '%s'.\n" % (filename));
215                     continue;
216                 for package in string.split(dsc.get("binary"), ','):
217                     package = string.strip(package);
218                     binary_packages[package] = "";
219             # Then for each binary package: find any version in
220             # unstable, check the Source: field in the deb matches our
221             # source package and if so add it to the list of packages
222             # to be removed.
223             for package in binary_packages.keys():
224                 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));
225                 for i in q.getresult():
226                     filename = string.join(i[:2], '/');
227                     control = apt_pkg.ParseSection(apt_inst.debExtractControl(utils.open_file(filename,"r")))
228                     source = control.Find("Source", control.Find("Package"));
229                     source = re_strip_source_version.sub('', source);
230                     if source_packages.has_key(source):
231                         to_remove.append(i[2:]);
232                     #else:
233                         #sys.stderr.write("W: skipping '%s' as it's source ('%s') isn't one of the source packages.\n" % (filename, source));
234     print "done."
235
236     # If we don't have a reason; spawn an editor so the user can add one
237     # Write the rejection email out as the <foo>.reason file
238     if not Options["Reason"] and not Options["No-Action"]:
239         temp_filename = tempfile.mktemp();
240         fd = os.open(temp_filename, os.O_RDWR|os.O_CREAT|os.O_EXCL, 0700);
241         os.close(fd);
242         editor = os.environ.get("EDITOR","vi")
243         result = os.system("%s %s" % (editor, temp_filename))
244         if result != 0:
245             sys.stderr.write ("vi invocation failed for `%s'!" % (temp_filename))
246             sys.exit(result)
247         file = utils.open_file(temp_filename, 'r');
248         for line in file.readlines():
249             Options["Reason"] = Options["Reason"] + line;
250         os.unlink(temp_filename);
251
252     # Generate the summary of what's to be removed
253     d = {};
254     for i in to_remove:
255         package = i[0];
256         version = i[1];
257         architecture = i[2];
258         if not d.has_key(package):
259             d[package] = {};
260         if not d[package].has_key(version):
261             d[package][version] = [];
262         d[package][version].append(architecture);
263
264     summary = "";
265     packages = d.keys();
266     packages.sort();
267     for package in packages:
268         versions = d[package].keys();
269         versions.sort();
270         for version in versions:
271             summary = summary + "%10s | %10s | " % (package, version);
272             for architecture in d[package][version]:
273                 summary = "%s%s, " % (summary, architecture);
274             summary = summary[:-2] + '\n';
275
276     print "Will remove the following packages from %s:" % (suites_list);
277     print
278     print summary
279     if Options["Done"]:
280         print "Will also close bugs: "+Options["Done"];
281     if carbon_copy:
282         print "Will also "+carbon_copy[:-1]
283     print
284     print "------------------- Reason -------------------"
285     print Options["Reason"];
286     print "----------------------------------------------"
287     print
288
289     # If -n/--no-action, drop out here
290     if Options["No-Action"]:
291         sys.exit(0);
292         
293     game_over();
294
295     whoami = string.replace(string.split(pwd.getpwuid(os.getuid())[4],',')[0], '.', '');
296     date = commands.getoutput('date -R');
297
298     # Log first; if it all falls apart I want a record that we at least tried.
299     logfile = utils.open_file(Cnf["Melanie::LogFile"], 'a');
300     logfile.write("=========================================================================\n");
301     logfile.write("[Date: %s] [ftpmaster: %s]\n" % (date, whoami));
302     logfile.write("Removed the following packages from %s:\n\n%s" % (suites_list, summary));
303     if Options["Done"]:
304         logfile.write("Closed bugs: %s\n" % (Options["Done"]));
305     logfile.write("\n------------------- Reason -------------------\n%s\n" % (Options["Reason"]));
306     logfile.write("----------------------------------------------\n");
307     logfile.flush();
308         
309     dsc_type_id = db_access.get_override_type_id('dsc');
310     deb_type_id = db_access.get_override_type_id('deb');
311     
312     # Do the actual deletion
313     print "Deleting...",
314     sys.stdout.flush();
315     projectB.query("BEGIN WORK");
316     for i in to_remove:
317         package = i[0];
318         architecture = i[2];
319         package_id = i[3];
320         for suite_id in suite_ids_list:
321             if architecture == "source":
322                 projectB.query("DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id));
323                 #print "DELETE FROM src_associations WHERE source = %s AND suite = %s" % (package_id, suite_id);
324             else:
325                 projectB.query("DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id));
326                 #print "DELETE FROM bin_associations WHERE bin = %s AND suite = %s" % (package_id, suite_id);
327             # Delete from the override file
328             if not Options["Partial"]:
329                 if architecture == "source":
330                     type_id = dsc_type_id;
331                 else:
332                     type_id = deb_type_id;
333                 projectB.query("DELETE FROM override WHERE package = '%s' AND type = %s AND suite = %s %s" % (package, type_id, suite_id, over_con_components));
334     projectB.query("COMMIT WORK");
335     print "done."
336
337     # Send the bug closing messages
338     if Options["Done"]:
339         for bug in string.split(Options["Done"]):
340             mail_message = """Return-Path: %s
341 From: %s
342 To: %s-close@bugs.debian.org
343 Bcc: troup@auric.debian.org
344 Bcc: removed-packages@qa.debian.org
345 %sSubject: Bug#%s: fixed
346
347 We believe that the bug you reported is now fixed; the following
348 package(s) have been removed from %s:
349
350 %s
351 Note that the package(s) have simply been removed from the tag
352 database and may (or may not) still be in the pool; this is not a bug.
353 The package(s) will be physically removed automatically when no suite
354 references them (and in the case of source, when no binary references
355 it).  Please also remember that the changes have been done on the
356 master archive (ftp-master.debian.org) and will not propagate to any
357 mirrors (ftp.debian.org included) until the next cron.daily run at the
358 earliest.
359
360 Packages are never removed from testing by hand.  Testing tracks
361 unstable and will automatically remove packages which were removed
362 from unstable when removing them from testing causes no dependency
363 problems.
364
365 Bugs which have been reported against this package are not automatically
366 removed from the Bug Tracking System.  Please check all open bugs and
367 close them or re-assign them to another package if the removed package
368 was superseded by another one.
369
370 Thank you for reporting the bug, which will now be closed.  If you
371 have further comments please address them to %s@bugs.debian.org.
372
373 This message was generated automatically; if you believe that there is
374 a problem with it please contact the archive administrators by mailing
375 ftpmaster@debian.org.
376
377 Debian distribution maintenance software
378 pp.
379 %s (the ftpmaster behind the curtain)
380 """ % (Cnf["Melanie::MyEmailAddress"], Cnf["Melanie::MyEmailAddress"], bug, carbon_copy, bug, suites_list, summary, bug, whoami);
381             utils.send_mail (mail_message, "")
382             
383     logfile.write("=========================================================================\n");
384     logfile.close();
385
386 #######################################################################################
387
388 if __name__ == '__main__':
389     main()
390