1#! /usr/bin/env python3
3"""General purpose package removal tool for ftpmaster"""
4# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2006 James Troup <james@nocrew.org>
5# Copyright (C) 2010 Alexander Reichle-Schmehl <tolimar@debian.org>
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.
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.
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
21################################################################################
23# o OpenBSD team wants to get changes incorporated into IPF. Darren no
24# respond.
25# o Ask again -> No respond. Darren coder supreme.
26# o OpenBSD decide to make changes, but only in OpenBSD source
27# tree. Darren hears, gets angry! Decides: "LICENSE NO ALLOW!"
28# o Insert Flame War.
29# o OpenBSD team decide to switch to different packet filter under BSD
30# license. Because Project Goal: Every user should be able to make
31# changes to source tree. IPF license bad!!
32# o Darren try get back: says, NetBSD, FreeBSD allowed! MUAHAHAHAH!!!
33# o Theo say: no care, pf much better than ipf!
34# o Darren changes mind: changes license. But OpenBSD will not change
35# back to ipf. Darren even much more bitter.
36# o Darren so bitterbitter. Decides: I'LL GET BACK BY FORKING OPENBSD AND
37# RELEASING MY OWN VERSION. HEHEHEHEHE.
39# http://slashdot.org/comments.pl?sid=26697&cid=2883271
41################################################################################
43import functools
44import sys
46import apt_pkg
48from daklib import utils
49from daklib.config import Config
50from daklib.dbconn import DBConn, get_maintainer, get_suite
51from daklib.rm import remove
53################################################################################
55Options = None
57################################################################################
60def usage(exit_code=0):
61 print(
62 """Usage: dak rm [OPTIONS] PACKAGE[...]
63Remove PACKAGE(s) from suite(s).
65 -A, --no-arch-all-rdeps Do not report breaking arch:all packages
66 or Build-Depends-Indep
67 -a, --architecture=ARCH only act on this architecture
68 -b, --binary PACKAGE are binary packages to remove
69 -B, --binary-only remove binaries only
70 --binary-version=VER only remove packages with binary vesion VER
71 -c, --component=COMPONENT act on this component
72 -C, --carbon-copy=EMAIL send a CC of removal message to EMAIL
73 -d, --done=BUG# send removal message as closure to bug#
74 -D, --do-close also close all bugs associated to that package
75 -h, --help show this help and exit
76 -m, --reason=MSG reason for removal
77 -n, --no-action don't do anything
78 -o, --outdated remove only outdated sources or binaries that were
79 built from previous source versions
80 -p, --partial don't affect override files
81 -R, --rdep-check check reverse dependencies
82 -s, --suite=SUITE act on this suite
83 -S, --source-only remove source only
84 --source-version=VER only remove packages with source version VER
86ARCH, BUG#, COMPONENT and SUITE can be comma (or space) separated lists, e.g.
87 --architecture=amd64,i386"""
88 )
90 sys.exit(exit_code)
93################################################################################
95# "Hudson: What that's great, that's just fucking great man, now what
96# the fuck are we supposed to do? We're in some real pretty shit now
97# man...That's it man, game over man, game over, man! Game over! What
98# the fuck are we gonna do now? What are we gonna do?"
101def game_over():
102 answer = utils.input_or_exit("Continue (y/N)? ").lower()
103 if answer != "y": 103 ↛ 104line 103 didn't jump to line 104, because the condition on line 103 was never true
104 print("Aborted.")
105 sys.exit(1)
108################################################################################
111def reverse_depends_check(
112 removals, suite, arches=None, session=None, include_arch_all=True
113):
114 print("Checking reverse dependencies...")
115 if utils.check_reverse_depends( 115 ↛ 118line 115 didn't jump to line 118, because the condition on line 115 was never true
116 removals, suite, arches, session, include_arch_all=include_arch_all
117 ):
118 print("Dependency problem found.")
119 if not Options["No-Action"]:
120 game_over()
121 else:
122 print("No dependency problem found.")
123 print()
126################################################################################
129def main():
130 global Options
132 cnf = Config()
134 Arguments = [
135 ("h", "help", "Rm::Options::Help"),
136 ("A", "no-arch-all-rdeps", "Rm::Options::NoArchAllRdeps"),
137 ("a", "architecture", "Rm::Options::Architecture", "HasArg"),
138 ("b", "binary", "Rm::Options::Binary"),
139 ("B", "binary-only", "Rm::Options::Binary-Only"),
140 ("\0", "binary-version", "Rm::Options::Binary-Version", "HasArg"),
141 ("c", "component", "Rm::Options::Component", "HasArg"),
142 ("C", "carbon-copy", "Rm::Options::Carbon-Copy", "HasArg"), # Bugs to Cc
143 ("d", "done", "Rm::Options::Done", "HasArg"), # Bugs fixed
144 ("D", "do-close", "Rm::Options::Do-Close"),
145 ("R", "rdep-check", "Rm::Options::Rdep-Check"),
146 (
147 "m",
148 "reason",
149 "Rm::Options::Reason",
150 "HasArg",
151 ), # Hysterical raisins; -m is old-dinstall option for rejection reason
152 ("n", "no-action", "Rm::Options::No-Action"),
153 ("o", "outdated", "Rm::Options::Outdated"),
154 ("p", "partial", "Rm::Options::Partial"),
155 ("s", "suite", "Rm::Options::Suite", "HasArg"),
156 ("S", "source-only", "Rm::Options::Source-Only"),
157 ("\0", "source-version", "Rm::Options::Source-Version", "HasArg"),
158 ]
160 for i in [
161 "NoArchAllRdeps",
162 "architecture",
163 "binary",
164 "binary-only",
165 "carbon-copy",
166 "component",
167 "done",
168 "help",
169 "no-action",
170 "outdated",
171 "partial",
172 "rdep-check",
173 "reason",
174 "source-only",
175 "Do-Close",
176 ]:
177 key = "Rm::Options::%s" % (i)
178 if key not in cnf: 178 ↛ 160line 178 didn't jump to line 160
179 cnf[key] = ""
180 if "Rm::Options::Suite" not in cnf: 180 ↛ 183line 180 didn't jump to line 183, because the condition on line 180 was never false
181 cnf["Rm::Options::Suite"] = "unstable"
183 arguments = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv)
184 Options = cnf.subtree("Rm::Options")
186 if Options["Help"]:
187 usage()
189 session = DBConn().session()
191 # Sanity check options
192 if not arguments: 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true
193 utils.fubar("need at least one package name as an argument.")
194 if Options["Architecture"] and Options["Source-Only"]: 194 ↛ 195line 194 didn't jump to line 195, because the condition on line 194 was never true
195 utils.fubar(
196 "can't use -a/--architecture and -S/--source-only options simultaneously."
197 )
198 actions = [Options["Binary"], Options["Binary-Only"], Options["Source-Only"]]
199 nr_actions = len([act for act in actions if act])
200 if nr_actions > 1: 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true
201 utils.fubar(
202 "Only one of -b/--binary, -B/--binary-only and -S/--source-only can be used."
203 )
204 if Options["Architecture"] and not Options["Partial"]: 204 ↛ 205line 204 didn't jump to line 205, because the condition on line 204 was never true
205 utils.warn("-a/--architecture implies -p/--partial.")
206 Options["Partial"] = "true"
207 if Options["Outdated"] and not Options["Partial"]: 207 ↛ 208line 207 didn't jump to line 208, because the condition on line 207 was never true
208 utils.warn("-o/--outdated implies -p/--partial.")
209 Options["Partial"] = "true"
210 if Options["Do-Close"] and not Options["Done"]: 210 ↛ 211line 210 didn't jump to line 211, because the condition on line 210 was never true
211 utils.fubar("-D/--do-close needs -d/--done (bugnr).")
212 if Options["Do-Close"] and ( 212 ↛ 215line 212 didn't jump to line 215, because the condition on line 212 was never true
213 Options["Binary"] or Options["Binary-Only"] or Options["Source-Only"]
214 ):
215 utils.fubar(
216 "-D/--do-close cannot be used with -b/--binary, -B/--binary-only or -S/--source-only."
217 )
219 # Force the admin to tell someone if we're not doing a 'dak
220 # cruft-report' inspired removal (or closing a bug, which counts
221 # as telling someone).
222 if ( 222 ↛ 228line 222 didn't jump to line 228
223 not Options["No-Action"]
224 and not Options["Carbon-Copy"]
225 and not Options["Done"]
226 and Options["Reason"].find("[auto-cruft]") == -1
227 ):
228 utils.fubar(
229 "Need a -C/--carbon-copy if not closing a bug and not doing a cruft removal."
230 )
232 parameters = {
233 "binary_version": Options.get("Binary-Version", "") or None,
234 "source_version": Options.get("Source-Version", "") or None,
235 }
237 if Options["Binary"]: 237 ↛ 238line 237 didn't jump to line 238, because the condition on line 237 was never true
238 con_packages = "AND b.package IN :packages"
239 parameters["packages"] = tuple(arguments)
240 else:
241 con_packages = "AND s.source IN :sources"
242 parameters["sources"] = tuple(arguments)
244 (con_suites, con_architectures, con_components, check_source) = utils.parse_args(
245 Options
246 )
248 # Additional suite checks
249 suite_ids_list = []
250 whitelists = []
251 suites = utils.split_args(Options["Suite"])
252 suites_list = utils.join_with_commas_and(suites)
253 if not Options["No-Action"]:
254 for suite in suites:
255 s = get_suite(suite, session=session)
256 if s is not None: 256 ↛ 259line 256 didn't jump to line 259, because the condition on line 256 was never false
257 suite_ids_list.append(s.suite_id)
258 whitelists.append(s.mail_whitelist)
259 if suite in ("oldstable", "stable"): 259 ↛ 260line 259 didn't jump to line 260, because the condition on line 259 was never true
260 print("**WARNING** About to remove from the (old)stable suite!")
261 print(
262 "This should only be done just prior to a (point) release and not at"
263 )
264 print("any other time.")
265 game_over()
266 elif suite == "testing": 266 ↛ 267line 266 didn't jump to line 267, because the condition on line 266 was never true
267 print("**WARNING About to remove from the testing suite!")
268 print(
269 "There's no need to do this normally as removals from unstable will"
270 )
271 print("propogate to testing automagically.")
272 game_over()
274 # Additional architecture checks
275 if Options["Architecture"] and check_source: 275 ↛ 276line 275 didn't jump to line 276, because the condition on line 275 was never true
276 utils.warn("'source' in -a/--argument makes no sense and is ignored.")
278 # Don't do dependency checks on multiple suites
279 if Options["Rdep-Check"] and len(suites) > 1: 279 ↛ 280line 279 didn't jump to line 280, because the condition on line 279 was never true
280 utils.fubar("Reverse dependency check on multiple suites is not implemented.")
282 q_outdated = "TRUE"
283 if Options["Outdated"]: 283 ↛ 284line 283 didn't jump to line 284, because the condition on line 283 was never true
284 q_outdated = "s.version < newest_source.version"
286 to_remove = []
287 maintainers = {}
289 # We have 3 modes of package selection: binary, source-only, binary-only
290 # and source+binary.
292 # XXX: TODO: This all needs converting to use placeholders or the object
293 # API. It's an SQL injection dream at the moment
295 if Options["Binary"]: 295 ↛ 297line 295 didn't jump to line 297, because the condition on line 295 was never true
296 # Removal by binary package name
297 q = session.execute(
298 """
299 SELECT b.package, b.version, a.arch_string, b.id, b.maintainer, s.source,
300 s.version as source_version, newest_source.version as newest_sversion
301 FROM binaries b
302 JOIN source s ON s.id = b.source
303 JOIN bin_associations ba ON ba.bin = b.id
304 JOIN architecture a ON a.id = b.architecture
305 JOIN suite su ON su.id = ba.suite
306 JOIN files f ON f.id = b.file
307 JOIN files_archive_map af ON af.file_id = f.id AND af.archive_id = su.archive_id
308 JOIN component c ON c.id = af.component_id
309 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite
310 WHERE
311 (:binary_version IS NULL OR b.version = :binary_version)
312 AND (:source_version IS NULL OR s.version = :source_version)
313 AND %s %s %s %s %s
314 """
315 % (q_outdated, con_packages, con_suites, con_components, con_architectures),
316 parameters,
317 )
318 to_remove.extend(q)
319 else:
320 # Source-only
321 if not Options["Binary-Only"]: 321 ↛ 342line 321 didn't jump to line 342, because the condition on line 321 was never false
322 q = session.execute(
323 """
324 SELECT s.source, s.version, 'source', s.id, s.maintainer, s.source,
325 s.version as source_version, newest_source.version as newest_sversion
326 FROM source s
327 JOIN src_associations sa ON sa.source = s.id
328 JOIN suite su ON su.id = sa.suite
329 JOIN archive ON archive.id = su.archive_id
330 JOIN files f ON f.id = s.file
331 JOIN files_archive_map af ON af.file_id = f.id AND af.archive_id = su.archive_id
332 JOIN component c ON c.id = af.component_id
333 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite
334 WHERE
335 (:source_version IS NULL OR s.version = :source_version)
336 AND %s %s %s %s
337 """
338 % (q_outdated, con_packages, con_suites, con_components),
339 parameters,
340 )
341 to_remove.extend(q)
342 if not Options["Source-Only"]: 342 ↛ 373line 342 didn't jump to line 373, because the condition on line 342 was never false
343 # Source + Binary
344 q = session.execute(
345 """
346 SELECT b.package, b.version, a.arch_string, b.id, b.maintainer, s.source,
347 s.version as source_version, newest_source.version as newest_sversion
348 FROM binaries b
349 JOIN bin_associations ba ON b.id = ba.bin
350 JOIN architecture a ON b.architecture = a.id
351 JOIN suite su ON ba.suite = su.id
352 JOIN archive ON archive.id = su.archive_id
353 JOIN files_archive_map af ON b.file = af.file_id AND af.archive_id = archive.id
354 JOIN component c ON af.component_id = c.id
355 JOIN source s ON b.source = s.id
356 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite
357 WHERE
358 (:binary_version IS NULL OR b.version = :binary_version)
359 AND (:source_version IS NULL OR s.version = :source_version)
360 AND %s %s %s %s %s
361 """
362 % (
363 q_outdated,
364 con_packages,
365 con_suites,
366 con_components,
367 con_architectures,
368 ),
369 parameters,
370 )
371 to_remove.extend(q)
373 if not to_remove: 373 ↛ 374line 373 didn't jump to line 374, because the condition on line 373 was never true
374 print("Nothing to do.")
375 sys.exit(0)
377 # Process -C/--carbon-copy
378 #
379 # Accept 3 types of arguments (space separated):
380 # 1) a number - assumed to be a bug number, i.e. nnnnn@bugs.debian.org
381 # 2) the keyword 'package' - cc's $package@packages.debian.org for every argument
382 # 3) contains a '@' - assumed to be an email address, used unmodified
383 #
384 carbon_copy = []
385 for copy_to in utils.split_args(Options.get("Carbon-Copy")):
386 if copy_to.isdigit(): 386 ↛ 387line 386 didn't jump to line 387, because the condition on line 386 was never true
387 if "Dinstall::BugServer" in cnf:
388 carbon_copy.append(copy_to + "@" + cnf["Dinstall::BugServer"])
389 else:
390 utils.fubar(
391 "Asked to send mail to #%s in BTS but Dinstall::BugServer is not configured"
392 % copy_to
393 )
394 elif copy_to == "package": 394 ↛ 395line 394 didn't jump to line 395, because the condition on line 394 was never true
395 for package in set([s[5] for s in to_remove]):
396 if "Dinstall::PackagesServer" in cnf:
397 carbon_copy.append(package + "@" + cnf["Dinstall::PackagesServer"])
398 elif "@" in copy_to: 398 ↛ 401line 398 didn't jump to line 401, because the condition on line 398 was never false
399 carbon_copy.append(copy_to)
400 else:
401 utils.fubar(
402 "Invalid -C/--carbon-copy argument '%s'; not a bug number, 'package' or email address."
403 % (copy_to)
404 )
406 # If we don't have a reason; spawn an editor so the user can add one
407 # Write the rejection email out as the <foo>.reason file
408 if not Options["Reason"] and not Options["No-Action"]: 408 ↛ 409line 408 didn't jump to line 409, because the condition on line 408 was never true
409 Options["Reason"] = utils.call_editor()
411 # Generate the summary of what's to be removed
412 d = {}
413 for i in to_remove:
414 package = i[0]
415 version = i[1]
416 architecture = i[2]
417 maintainer = i[4]
418 maintainers[maintainer] = ""
419 # source = i[5]
420 # source_version = i[6]
421 # source_newest = i[7]
422 if package not in d:
423 d[package] = {}
424 if version not in d[package]:
425 d[package][version] = []
426 if architecture not in d[package][version]: 426 ↛ 413line 426 didn't jump to line 413, because the condition on line 426 was never false
427 d[package][version].append(architecture)
429 maintainer_list = []
430 for maintainer_id in maintainers.keys():
431 maintainer_list.append(get_maintainer(maintainer_id).name)
432 summary = ""
433 removals = sorted(d)
434 for package in removals:
435 versions = sorted(d[package], key=functools.cmp_to_key(apt_pkg.version_compare))
436 for version in versions:
437 d[package][version].sort(key=utils.ArchKey)
438 summary += "%10s | %10s | %s\n" % (
439 package,
440 version,
441 ", ".join(d[package][version]),
442 )
443 print("Will remove the following packages from %s:" % (suites_list))
444 print()
445 print(summary)
446 print("Maintainer: %s" % ", ".join(maintainer_list))
447 if Options["Done"]:
448 print("Will also close bugs: " + Options["Done"])
449 if carbon_copy:
450 print("Will also send CCs to: " + ", ".join(carbon_copy))
451 if Options["Do-Close"]: 451 ↛ 452line 451 didn't jump to line 452, because the condition on line 451 was never true
452 print("Will also close associated bug reports.")
453 print()
454 print("------------------- Reason -------------------")
455 print(Options["Reason"])
456 print("----------------------------------------------")
457 print()
459 if Options["Rdep-Check"]:
460 arches = utils.split_args(Options["Architecture"])
461 include_arch_all = Options["NoArchAllRdeps"] == ""
462 if include_arch_all and "all" in arches: 462 ↛ 464line 462 didn't jump to line 464, because the condition on line 462 was never true
463 # when arches is None, rdeps are checked on all arches in the suite
464 arches = None
465 reverse_depends_check(
466 removals, suites[0], arches, session, include_arch_all=include_arch_all
467 )
469 # If -n/--no-action, drop out here
470 if Options["No-Action"]:
471 sys.exit(0)
473 print("Going to remove the packages now.")
474 game_over()
476 # Do the actual deletion
477 print("Deleting...", end=" ")
478 sys.stdout.flush()
480 try:
481 bugs = utils.split_args(Options["Done"])
482 remove(
483 session,
484 Options["Reason"],
485 suites,
486 to_remove,
487 partial=Options["Partial"],
488 components=utils.split_args(Options["Component"]),
489 done_bugs=bugs,
490 carbon_copy=carbon_copy,
491 close_related_bugs=Options["Do-Close"],
492 )
493 except ValueError as ex:
494 utils.fubar(ex.message)
495 else:
496 print("done.")
499#######################################################################################
502if __name__ == "__main__":
503 main()