1 """General purpose package removal code for ftpmaster
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
9 # Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006 James Troup <james@nocrew.org>
10 # Copyright (C) 2010 Alexander Reichle-Schmehl <tolimar@debian.org>
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.
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.
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
26 ################################################################################
28 # TODO: Insert "random dak quote" here
30 ################################################################################
36 from daklib.dbconn import *
37 from daklib import utils
38 from daklib.regexes import re_bin_only_nmu
39 import debianbts as bts
41 ################################################################################
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.
51 @type session: SQLA Session
52 @param session: The database session in use
55 @param reason: The reason for the removal (e.g. "[auto-cruft] NBS (no longer built by <source>)")
58 @param suites: A list of the suite names in which the removal should occur
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".
66 @param partial: Whether the removal is "partial" (e.g. architecture specific).
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.
73 @param whoami: The person (or entity) doing the removal. Defaults to utils.whoami()
76 @param date: The date of the removal. Defaults to commands.getoutput("date -R")
79 @param done_bugs: A list of bugs to be closed when doing this removal.
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.
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
92 # Generate the summary of what's to be removed
100 suites_list = utils.join_with_commas_and(suites)
101 cnf = utils.get_conf()
102 con_components = None
104 #######################################################################################################
107 raise ValueError("Empty removal reason not permitted")
110 raise ValueError("Nothing to remove!?")
113 raise ValueError("Removals without a suite!?")
116 whoami = utils.whoami()
119 date = commands.getoutput("date -R")
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)
129 component_ids_list.append(component.component_id)
130 con_components = "AND component IN (%s)" % ", ".join([str(i) for i in component_ids_list])
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)
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]))
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(" ")
157 binaries.append("%s_%s [%s]" % tuple(elem.strip(" ") for elem in element))
159 dsc_type_id = get_override_type('dsc', session).overridetype_id
160 deb_type_id = get_override_type('deb', session).overridetype_id
163 s = get_suite(suite, session=session)
165 suite_ids_list.append(s.suite_id)
166 whitelists.append(s.mail_whitelist)
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))
176 logfile.write("Closed bugs: %s\n" % (", ".join(done_bugs)))
177 logfile.write("\n------------------- Reason -------------------\n%s\n" % reason)
178 logfile.write("----------------------------------------------\n")
180 logfile822.write("Date: %s\n" % date)
181 logfile822.write("Ftpmaster: %s\n" % whoami)
182 logfile822.write("Suite: %s\n" % suites_list)
185 logfile822.write("Sources:\n")
186 for source in sources:
187 logfile822.write(" %s\n" % source)
190 logfile822.write("Binaries:\n")
191 for binary in binaries:
192 logfile822.write(" %s\n" % binary)
194 logfile822.write("Reason: %s\n" % reason.replace('\n', '\n '))
196 logfile822.write("Bug: %s\n" % (", ".join(done_bugs)))
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})
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
211 if architecture == "source":
212 type_id = dsc_type_id
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})
219 # ### REMOVAL COMPLETE - send mail time ### #
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")
226 logfile.write("=========================================================================\n")
227 logfile822.write("\n")
230 # read common subst variables for all bug closure mails
232 Subst_common["__RM_ADDRESS__"] = cnf["Dinstall::MyEmailAddress"]
233 Subst_common["__BUG_SERVER__"] = cnf["Dinstall::BugServer"]
234 Subst_common["__CC__"] = "X-DAK: dak rm"
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
243 # Send the bug closing messages
245 Subst_close_rm = Subst_common
247 if cnf.find("Dinstall::Bcc") != "":
248 bcc.append(cnf["Dinstall::Bcc"])
249 if cnf.find("Rm::Bcc") != "":
250 bcc.append(cnf["Rm::Bcc"])
252 Subst_close_rm["__BCC__"] = "Bcc: " + ", ".join(bcc)
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
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")
264 mail_message = utils.TemplateSubst(Subst_close_rm,cnf["Dir::Templates"]+"/rm.bug-close")
265 utils.send_mail(mail_message, whitelists=whitelists)
267 # close associated bug reports
268 if close_related_bugs:
269 Subst_close_other = Subst_common
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]
276 logfile.write("=========================================================================\n")
277 logfile822.write("\n")
278 raise ValueError("Closing bugs with multiple package versions is not supported. Do it yourself.")
280 Subst_close_other["__BCC__"] = "Bcc: " + ", ".join(bcc)
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]
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
295 other_bugs = bts.get_bugs('src', source_pkg, 'status', 'open', 'status', 'forwarded')
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))
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))
322 logfile822.write("\n")
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)
328 logfile.write("=========================================================================\n")
329 logfile822.write("\n")