]> git.decadent.org.uk Git - dak.git/blob - daklib/rm.py
Extract a "remove" method from "dak rm"
[dak.git] / daklib / rm.py
1 """General purpose package removal code for ftpmaster
2
3 @contact: Debian FTP Master <ftpmaster@debian.org>
4 @copyright: 2000, 2001, 2002, 2003, 2004, 2006  James Troup <james@nocrew.org>
5 @copyright: 2010 Alexander Reichle-Schmehl <tolimar@debian.org>
6 @copyright: 2015      Niels Thykier <niels@thykier.net>
7 @license: GNU General Public License version 2 or later
8 """
9 # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006  James Troup <james@nocrew.org>
10 # Copyright (C) 2010 Alexander Reichle-Schmehl <tolimar@debian.org>
11
12 # This program is free software; you can redistribute it and/or modify
13 # it under the terms of the GNU General Public License as published by
14 # the Free Software Foundation; either version 2 of the License, or
15 # (at your option) any later version.
16
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, write to the Free Software
24 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
25
26 ################################################################################
27
28 # TODO: Insert "random dak quote" here
29
30 ################################################################################
31
32 import commands
33 import apt_pkg
34 from re import sub
35
36 from daklib.dbconn import *
37 from daklib import utils
38 from daklib.regexes import re_bin_only_nmu
39 import debianbts as bts
40
41 ################################################################################
42
43
44 def remove(session, reason, suites, removals,
45            whoami=None, partial=False, components=None, done_bugs=None, date=None,
46            carbon_copy=None, close_related_bugs=False):
47     """Batch remove a number of packages
48     Verify that the files listed in the Files field of the .dsc are
49     those expected given the announced Format.
50
51     @type session: SQLA Session
52     @param session: The database session in use
53
54     @type reason: string
55     @param reason: The reason for the removal (e.g. "[auto-cruft] NBS (no longer built by <source>)")
56
57     @type suites: list
58     @param suites: A list of the suite names in which the removal should occur
59
60     @type removals: list
61     @param removals: A list of the removals.  Each element should be a tuple (or list) of at least the following
62         for 4 items from the database (in order): package, version, architecture, (database) id.
63         For source packages, the "architecture" should be set to "source".
64
65     @type partial: bool
66     @param partial: Whether the removal is "partial" (e.g. architecture specific).
67
68     @type components: list
69     @param components: List of components involved in a partial removal.  Can be an empty list to not restrict the
70         removal to any components.
71
72     @type whoami: string
73     @param whoami: The person (or entity) doing the removal.  Defaults to utils.whoami()
74
75     @type date: string
76     @param date: The date of the removal. Defaults to commands.getoutput("date -R")
77
78     @type done_bugs: list
79     @param done_bugs: A list of bugs to be closed when doing this removal.
80
81     @type close_related_bugs: bool
82     @param done_bugs: Whether bugs related to the package being removed should be closed as well.  NB: Not implemented
83       for more than one suite.
84
85     @type carbon_copy: list
86     @param carbon_copy: A list of mail addresses to CC when doing removals.  NB: all items are taken "as-is" unlike
87         "dak rm".
88
89     @rtype: None
90     @return: Nothing
91     """
92     # Generate the summary of what's to be removed
93     d = {}
94     summary = ""
95     sources = []
96     binaries = []
97     whitelists = []
98     versions = []
99     suite_ids_list = []
100     suites_list = utils.join_with_commas_and(suites)
101     cnf = utils.get_conf()
102     con_components = None
103
104     #######################################################################################################
105
106     if not reason:
107         raise ValueError("Empty removal reason not permitted")
108
109     if not removals:
110         raise ValueError("Nothing to remove!?")
111
112     if not suites:
113         raise ValueError("Removals without a suite!?")
114
115     if whoami is None:
116         whoami = utils.whoami()
117
118     if date is None:
119         date = commands.getoutput("date -R")
120
121     if partial:
122
123         component_ids_list = []
124         for componentname in components:
125             component = get_component(componentname, session=session)
126             if component is None:
127                 raise ValueError("component '%s' not recognised." % componentname)
128             else:
129                 component_ids_list.append(component.component_id)
130         con_components = "AND component IN (%s)" % ", ".join([str(i) for i in component_ids_list])
131
132     for i in removals:
133         package = i[0]
134         version = i[1]
135         architecture = i[2]
136         if package not in d:
137             d[package] = {}
138         if version not in d[package]:
139             d[package][version] = []
140         if architecture not in d[package][version]:
141             d[package][version].append(architecture)
142
143     for package in sorted(removals):
144         versions = sorted(d[package], cmp=apt_pkg.version_compare)
145         for version in versions:
146             d[package][version].sort(utils.arch_compare_sw)
147             summary += "%10s | %10s | %s\n" % (package, version, ", ".join(d[package][version]))
148
149     for package in summary.split("\n"):
150         for row in package.split("\n"):
151             element = row.split("|")
152             if len(element) == 3:
153                 if element[2].find("source") > 0:
154                     sources.append("%s_%s" % tuple(elem.strip(" ") for elem in element[:2]))
155                     element[2] = sub("source\s?,?", "", element[2]).strip(" ")
156                 if element[2]:
157                     binaries.append("%s_%s [%s]" % tuple(elem.strip(" ") for elem in element))
158
159     dsc_type_id = get_override_type('dsc', session).overridetype_id
160     deb_type_id = get_override_type('deb', session).overridetype_id
161
162     for suite in suites:
163         s = get_suite(suite, session=session)
164         if s is not None:
165             suite_ids_list.append(s.suite_id)
166             whitelists.append(s.mail_whitelist)
167
168     #######################################################################################################
169     log_filename = cnf["Rm::LogFile"]
170     log822_filename = cnf["Rm::LogFile822"]
171     with utils.open_file(log_filename, "a") as logfile, utils.open_file(log822_filename, "a") as logfile822:
172         logfile.write("=========================================================================\n")
173         logfile.write("[Date: %s] [ftpmaster: %s]\n" % (date, whoami))
174         logfile.write("Removed the following packages from %s:\n\n%s" % (suites_list, summary))
175         if done_bugs:
176             logfile.write("Closed bugs: %s\n" % (", ".join(done_bugs)))
177         logfile.write("\n------------------- Reason -------------------\n%s\n" % reason)
178         logfile.write("----------------------------------------------\n")
179
180         logfile822.write("Date: %s\n" % date)
181         logfile822.write("Ftpmaster: %s\n" % whoami)
182         logfile822.write("Suite: %s\n" % suites_list)
183
184         if sources:
185             logfile822.write("Sources:\n")
186             for source in sources:
187                 logfile822.write(" %s\n" % source)
188
189         if binaries:
190             logfile822.write("Binaries:\n")
191             for binary in binaries:
192                 logfile822.write(" %s\n" % binary)
193
194         logfile822.write("Reason: %s\n" % reason.replace('\n', '\n '))
195         if done_bugs:
196             logfile822.write("Bug: %s\n" % (", ".join(done_bugs)))
197
198         for i in removals:
199             package = i[0]
200             architecture = i[2]
201             package_id = i[3]
202             for suite_id in suite_ids_list:
203                 if architecture == "source":
204                     session.execute("DELETE FROM src_associations WHERE source = :packageid AND suite = :suiteid",
205                                     {'packageid': package_id, 'suiteid': suite_id})
206                 else:
207                     session.execute("DELETE FROM bin_associations WHERE bin = :packageid AND suite = :suiteid",
208                                     {'packageid': package_id, 'suiteid': suite_id})
209                 # Delete from the override file
210                 if partial:
211                     if architecture == "source":
212                         type_id = dsc_type_id
213                     else:
214                         type_id = deb_type_id
215                     # TODO: Fix this properly to remove the remaining non-bind argument
216                     session.execute("DELETE FROM override WHERE package = :package AND type = :typeid AND suite = :suiteid %s" % (con_components), {'package': package, 'typeid': type_id, 'suiteid': suite_id})
217
218         session.commit()
219         # ### REMOVAL COMPLETE - send mail time ### #
220
221         # If we don't have a Bug server configured, we're done
222         if "Dinstall::BugServer" not in cnf:
223             if done_bugs or close_related_bugs:
224                 utils.warn("Cannot send mail to BugServer as Dinstall::BugServer is not configured")
225
226             logfile.write("=========================================================================\n")
227             logfile822.write("\n")
228             return
229
230         # read common subst variables for all bug closure mails
231         Subst_common = {}
232         Subst_common["__RM_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
233         Subst_common["__BUG_SERVER__"] = cnf["Dinstall::BugServer"]
234         Subst_common["__CC__"] = "X-DAK: dak rm"
235         if carbon_copy:
236             Subst_common["__CC__"] += "\nCc: " + ", ".join(carbon_copy)
237         Subst_common["__SUITE_LIST__"] = suites_list
238         Subst_common["__SUBJECT__"] = "Removed package(s) from %s" % (suites_list)
239         Subst_common["__ADMIN_ADDRESS__"] = cnf["Dinstall::MyAdminAddress"]
240         Subst_common["__DISTRO__"] = cnf["Dinstall::MyDistribution"]
241         Subst_common["__WHOAMI__"] = whoami
242
243         # Send the bug closing messages
244         if done_bugs:
245             Subst_close_rm = Subst_common
246             bcc = []
247             if cnf.find("Dinstall::Bcc") != "":
248                 bcc.append(cnf["Dinstall::Bcc"])
249             if cnf.find("Rm::Bcc") != "":
250                 bcc.append(cnf["Rm::Bcc"])
251             if bcc:
252                 Subst_close_rm["__BCC__"] = "Bcc: " + ", ".join(bcc)
253             else:
254                 Subst_close_rm["__BCC__"] = "X-Filler: 42"
255             summarymail = "%s\n------------------- Reason -------------------\n%s\n" % (summary, reason)
256             summarymail += "----------------------------------------------\n"
257             Subst_close_rm["__SUMMARY__"] = summarymail
258
259             for bug in done_bugs:
260                 Subst_close_rm["__BUG_NUMBER__"] = bug
261                 if close_related_bugs:
262                     mail_message = utils.TemplateSubst(Subst_close_rm,cnf["Dir::Templates"]+"/rm.bug-close-with-related")
263                 else:
264                     mail_message = utils.TemplateSubst(Subst_close_rm,cnf["Dir::Templates"]+"/rm.bug-close")
265                 utils.send_mail(mail_message, whitelists=whitelists)
266
267         # close associated bug reports
268         if close_related_bugs:
269             Subst_close_other = Subst_common
270             bcc = []
271             wnpp = utils.parse_wnpp_bug_file()
272             versions = list(set([re_bin_only_nmu.sub('', v) for v in versions]))
273             if len(versions) == 1:
274                 Subst_close_other["__VERSION__"] = versions[0]
275             else:
276                 logfile.write("=========================================================================\n")
277                 logfile822.write("\n")
278                 raise ValueError("Closing bugs with multiple package versions is not supported.  Do it yourself.")
279             if bcc:
280                 Subst_close_other["__BCC__"] = "Bcc: " + ", ".join(bcc)
281             else:
282                 Subst_close_other["__BCC__"] = "X-Filler: 42"
283             # at this point, I just assume, that the first closed bug gives
284             # some useful information on why the package got removed
285             Subst_close_other["__BUG_NUMBER__"] = done_bugs[0]
286             if len(sources) == 1:
287                 source_pkg = source.split("_", 1)[0]
288             else:
289                 logfile.write("=========================================================================\n")
290                 logfile822.write("\n")
291                 raise ValueError("Closing bugs for multiple source packages is not supported.  Please do it yourself.")
292             Subst_close_other["__BUG_NUMBER_ALSO__"] = ""
293             Subst_close_other["__SOURCE__"] = source_pkg
294             merged_bugs = set()
295             other_bugs = bts.get_bugs('src', source_pkg, 'status', 'open', 'status', 'forwarded')
296             if other_bugs:
297                 for bugno in other_bugs:
298                     if bugno not in merged_bugs:
299                         for bug in bts.get_status(bugno):
300                             for merged in bug.mergedwith:
301                                 other_bugs.remove(merged)
302                                 merged_bugs.add(merged)
303                 logfile.write("Also closing bug(s):")
304                 logfile822.write("Also-Bugs:")
305                 for bug in other_bugs:
306                     Subst_close_other["__BUG_NUMBER_ALSO__"] += str(bug) + "-done@" + cnf["Dinstall::BugServer"] + ","
307                     logfile.write(" " + str(bug))
308                     logfile822.write(" " + str(bug))
309                 logfile.write("\n")
310                 logfile822.write("\n")
311             if source_pkg in wnpp:
312                 logfile.write("Also closing WNPP bug(s):")
313                 logfile822.write("Also-WNPP:")
314                 for bug in wnpp[source_pkg]:
315                     # the wnpp-rm file we parse also contains our removal
316                     # bugs, filtering that out
317                     if bug != Subst_close_other["__BUG_NUMBER__"]:
318                         Subst_close_other["__BUG_NUMBER_ALSO__"] += str(bug) + "-done@" + cnf["Dinstall::BugServer"] + ","
319                         logfile.write(" " + str(bug))
320                         logfile822.write(" " + str(bug))
321                 logfile.write("\n")
322                 logfile822.write("\n")
323
324             mail_message = utils.TemplateSubst(Subst_close_other, cnf["Dir::Templates"]+"/rm.bug-close-related")
325             if Subst_close_other["__BUG_NUMBER_ALSO__"]:
326                 utils.send_mail(mail_message)
327
328         logfile.write("=========================================================================\n")
329         logfile822.write("\n")