]> git.decadent.org.uk Git - dak.git/blob - melanie
melanie: use $EDITOR if available (fallback on vi)
[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.6 2001-02-25 02:41:44 mjb 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 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."
109             game_over();
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."
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"] 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);
216         os.close(fd);
217         editor = os.environ.get("EDITOR","vi")
218         result = os.system("%s %s" % (editor, 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 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
335 problems.
336
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.
341
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.
344
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.
348
349 Debian distribution maintenance software
350 pp.
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, "")
354             
355     logfile.write("=========================================================================\n");
356     logfile.close();
357
358 #######################################################################################
359
360 if __name__ == '__main__':
361     main()
362