1#! /usr/bin/env python3 

2 

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> 

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################################################################################ 

22 

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. 

38 

39# http://slashdot.org/comments.pl?sid=26697&cid=2883271 

40 

41################################################################################ 

42 

43import functools 

44import sys 

45 

46import apt_pkg 

47 

48from daklib import utils 

49from daklib.config import Config 

50from daklib.dbconn import DBConn, get_maintainer, get_suite 

51from daklib.rm import remove 

52 

53################################################################################ 

54 

55Options = None 

56 

57################################################################################ 

58 

59 

60def usage(exit_code=0): 

61 print( 

62 """Usage: dak rm [OPTIONS] PACKAGE[...] 

63Remove PACKAGE(s) from suite(s). 

64 

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 

85 

86ARCH, BUG#, COMPONENT and SUITE can be comma (or space) separated lists, e.g. 

87 --architecture=amd64,i386""" 

88 ) 

89 

90 sys.exit(exit_code) 

91 

92 

93################################################################################ 

94 

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?" 

99 

100 

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) 

106 

107 

108################################################################################ 

109 

110 

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() 

124 

125 

126################################################################################ 

127 

128 

129def main(): 

130 global Options 

131 

132 cnf = Config() 

133 

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 ] 

159 

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" 

182 

183 arguments = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) 

184 Options = cnf.subtree("Rm::Options") 

185 

186 if Options["Help"]: 

187 usage() 

188 

189 session = DBConn().session() 

190 

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 ) 

218 

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 ) 

231 

232 parameters = { 

233 "binary_version": Options.get("Binary-Version", "") or None, 

234 "source_version": Options.get("Source-Version", "") or None, 

235 } 

236 

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) 

243 

244 (con_suites, con_architectures, con_components, check_source) = utils.parse_args( 

245 Options 

246 ) 

247 

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() 

273 

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.") 

277 

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.") 

281 

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" 

285 

286 to_remove = [] 

287 maintainers = {} 

288 

289 # We have 3 modes of package selection: binary, source-only, binary-only 

290 # and source+binary. 

291 

292 # XXX: TODO: This all needs converting to use placeholders or the object 

293 # API. It's an SQL injection dream at the moment 

294 

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) 

372 

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) 

376 

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 ) 

405 

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() 

410 

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) 

428 

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() 

458 

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 ) 

468 

469 # If -n/--no-action, drop out here 

470 if Options["No-Action"]: 

471 sys.exit(0) 

472 

473 print("Going to remove the packages now.") 

474 game_over() 

475 

476 # Do the actual deletion 

477 print("Deleting...", end=" ") 

478 sys.stdout.flush() 

479 

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.") 

497 

498 

499####################################################################################### 

500 

501 

502if __name__ == "__main__": 

503 main()