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 os 

45import sys 

46import apt_pkg 

47 

48from daklib.config import Config 

49from daklib.dbconn import * 

50from daklib import utils 

51from daklib.rm import remove 

52 

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

54 

55Options = None 

56 

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

58 

59 

60def usage(exit_code=0): 

61 print("""Usage: dak rm [OPTIONS] PACKAGE[...] 

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

63 

64 -A, --no-arch-all-rdeps Do not report breaking arch:all packages 

65 or Build-Depends-Indep 

66 -a, --architecture=ARCH only act on this architecture 

67 -b, --binary PACKAGE are binary packages to remove 

68 -B, --binary-only remove binaries only 

69 --binary-version=VER only remove packages with binary vesion VER 

70 -c, --component=COMPONENT act on this component 

71 -C, --carbon-copy=EMAIL send a CC of removal message to EMAIL 

72 -d, --done=BUG# send removal message as closure to bug# 

73 -D, --do-close also close all bugs associated to that package 

74 -h, --help show this help and exit 

75 -m, --reason=MSG reason for removal 

76 -n, --no-action don't do anything 

77 -o, --outdated remove only outdated sources or binaries that were 

78 built from previous source versions 

79 -p, --partial don't affect override files 

80 -R, --rdep-check check reverse dependencies 

81 -s, --suite=SUITE act on this suite 

82 -S, --source-only remove source only 

83 --source-version=VER only remove packages with source version VER 

84 

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

86 --architecture=amd64,i386""") 

87 

88 sys.exit(exit_code) 

89 

90################################################################################ 

91 

92# "Hudson: What that's great, that's just fucking great man, now what 

93# the fuck are we supposed to do? We're in some real pretty shit now 

94# man...That's it man, game over man, game over, man! Game over! What 

95# the fuck are we gonna do now? What are we gonna do?" 

96 

97 

98def game_over(): 

99 answer = utils.input_or_exit("Continue (y/N)? ").lower() 

100 if answer != "y": 100 ↛ 101line 100 didn't jump to line 101, because the condition on line 100 was never true

101 print("Aborted.") 

102 sys.exit(1) 

103 

104################################################################################ 

105 

106 

107def reverse_depends_check(removals, suite, arches=None, session=None, include_arch_all=True): 

108 print("Checking reverse dependencies...") 

109 if utils.check_reverse_depends(removals, suite, arches, session, include_arch_all=include_arch_all): 109 ↛ 110line 109 didn't jump to line 110, because the condition on line 109 was never true

110 print("Dependency problem found.") 

111 if not Options["No-Action"]: 

112 game_over() 

113 else: 

114 print("No dependency problem found.") 

115 print() 

116 

117################################################################################ 

118 

119 

120def main(): 

121 global Options 

122 

123 cnf = Config() 

124 

125 Arguments = [('h', "help", "Rm::Options::Help"), 

126 ('A', 'no-arch-all-rdeps', 'Rm::Options::NoArchAllRdeps'), 

127 ('a', "architecture", "Rm::Options::Architecture", "HasArg"), 

128 ('b', "binary", "Rm::Options::Binary"), 

129 ('B', "binary-only", "Rm::Options::Binary-Only"), 

130 ('\0', "binary-version", "Rm::Options::Binary-Version", "HasArg"), 

131 ('c', "component", "Rm::Options::Component", "HasArg"), 

132 ('C', "carbon-copy", "Rm::Options::Carbon-Copy", "HasArg"), # Bugs to Cc 

133 ('d', "done", "Rm::Options::Done", "HasArg"), # Bugs fixed 

134 ('D', "do-close", "Rm::Options::Do-Close"), 

135 ('R', "rdep-check", "Rm::Options::Rdep-Check"), 

136 ('m', "reason", "Rm::Options::Reason", "HasArg"), # Hysterical raisins; -m is old-dinstall option for rejection reason 

137 ('n', "no-action", "Rm::Options::No-Action"), 

138 ('o', "outdated", "Rm::Options::Outdated"), 

139 ('p', "partial", "Rm::Options::Partial"), 

140 ('s', "suite", "Rm::Options::Suite", "HasArg"), 

141 ('S', "source-only", "Rm::Options::Source-Only"), 

142 ('\0', "source-version", "Rm::Options::Source-Version", "HasArg"), 

143 ] 

144 

145 for i in ['NoArchAllRdeps', 

146 "architecture", "binary", "binary-only", "carbon-copy", "component", 

147 "done", "help", "no-action", "outdated", "partial", "rdep-check", "reason", 

148 "source-only", "Do-Close"]: 

149 key = "Rm::Options::%s" % (i) 

150 if key not in cnf: 150 ↛ 145line 150 didn't jump to line 145, because the condition on line 150 was never false

151 cnf[key] = "" 

152 if "Rm::Options::Suite" not in cnf: 152 ↛ 155line 152 didn't jump to line 155, because the condition on line 152 was never false

153 cnf["Rm::Options::Suite"] = "unstable" 

154 

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

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

157 

158 if Options["Help"]: 

159 usage() 

160 

161 session = DBConn().session() 

162 

163 # Sanity check options 

164 if not arguments: 164 ↛ 165line 164 didn't jump to line 165, because the condition on line 164 was never true

165 utils.fubar("need at least one package name as an argument.") 

166 if Options["Architecture"] and Options["Source-Only"]: 166 ↛ 167line 166 didn't jump to line 167, because the condition on line 166 was never true

167 utils.fubar("can't use -a/--architecture and -S/--source-only options simultaneously.") 

168 actions = [Options["Binary"], Options["Binary-Only"], Options["Source-Only"]] 

169 nr_actions = len([act for act in actions if act]) 

170 if nr_actions > 1: 170 ↛ 171line 170 didn't jump to line 171, because the condition on line 170 was never true

171 utils.fubar("Only one of -b/--binary, -B/--binary-only and -S/--source-only can be used.") 

172 if Options["Architecture"] and not Options["Partial"]: 172 ↛ 173line 172 didn't jump to line 173, because the condition on line 172 was never true

173 utils.warn("-a/--architecture implies -p/--partial.") 

174 Options["Partial"] = "true" 

175 if Options["Outdated"] and not Options["Partial"]: 175 ↛ 176line 175 didn't jump to line 176, because the condition on line 175 was never true

176 utils.warn("-o/--outdated implies -p/--partial.") 

177 Options["Partial"] = "true" 

178 if Options["Do-Close"] and not Options["Done"]: 178 ↛ 179line 178 didn't jump to line 179, because the condition on line 178 was never true

179 utils.fubar("-D/--do-close needs -d/--done (bugnr).") 

180 if (Options["Do-Close"] 180 ↛ 182line 180 didn't jump to line 182, because the condition on line 180 was never true

181 and (Options["Binary"] or Options["Binary-Only"] or Options["Source-Only"])): 

182 utils.fubar("-D/--do-close cannot be used with -b/--binary, -B/--binary-only or -S/--source-only.") 

183 

184 # Force the admin to tell someone if we're not doing a 'dak 

185 # cruft-report' inspired removal (or closing a bug, which counts 

186 # as telling someone). 

187 if not Options["No-Action"] and not Options["Carbon-Copy"] \ 187 ↛ 189line 187 didn't jump to line 189, because the condition on line 187 was never true

188 and not Options["Done"] and Options["Reason"].find("[auto-cruft]") == -1: 

189 utils.fubar("Need a -C/--carbon-copy if not closing a bug and not doing a cruft removal.") 

190 

191 parameters = { 

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

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

194 } 

195 

196 if Options["Binary"]: 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true

197 con_packages = "AND b.package IN :packages" 

198 parameters['packages'] = tuple(arguments) 

199 else: 

200 con_packages = "AND s.source IN :sources" 

201 parameters['sources'] = tuple(arguments) 

202 

203 (con_suites, con_architectures, con_components, check_source) = \ 

204 utils.parse_args(Options) 

205 

206 # Additional suite checks 

207 suite_ids_list = [] 

208 whitelists = [] 

209 suites = utils.split_args(Options["Suite"]) 

210 suites_list = utils.join_with_commas_and(suites) 

211 if not Options["No-Action"]: 

212 for suite in suites: 

213 s = get_suite(suite, session=session) 

214 if s is not None: 214 ↛ 217line 214 didn't jump to line 217, because the condition on line 214 was never false

215 suite_ids_list.append(s.suite_id) 

216 whitelists.append(s.mail_whitelist) 

217 if suite in ("oldstable", "stable"): 217 ↛ 218line 217 didn't jump to line 218, because the condition on line 217 was never true

218 print("**WARNING** About to remove from the (old)stable suite!") 

219 print("This should only be done just prior to a (point) release and not at") 

220 print("any other time.") 

221 game_over() 

222 elif suite == "testing": 222 ↛ 223line 222 didn't jump to line 223, because the condition on line 222 was never true

223 print("**WARNING About to remove from the testing suite!") 

224 print("There's no need to do this normally as removals from unstable will") 

225 print("propogate to testing automagically.") 

226 game_over() 

227 

228 # Additional architecture checks 

229 if Options["Architecture"] and check_source: 229 ↛ 230line 229 didn't jump to line 230, because the condition on line 229 was never true

230 utils.warn("'source' in -a/--argument makes no sense and is ignored.") 

231 

232 # Don't do dependency checks on multiple suites 

233 if Options["Rdep-Check"] and len(suites) > 1: 233 ↛ 234line 233 didn't jump to line 234, because the condition on line 233 was never true

234 utils.fubar("Reverse dependency check on multiple suites is not implemented.") 

235 

236 q_outdated = "TRUE" 

237 if Options["Outdated"]: 237 ↛ 238line 237 didn't jump to line 238, because the condition on line 237 was never true

238 q_outdated = "s.version < newest_source.version" 

239 

240 to_remove = [] 

241 maintainers = {} 

242 

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

244 # and source+binary. 

245 

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

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

248 

249 if Options["Binary"]: 249 ↛ 251line 249 didn't jump to line 251, because the condition on line 249 was never true

250 # Removal by binary package name 

251 q = session.execute(""" 

252 SELECT b.package, b.version, a.arch_string, b.id, b.maintainer, s.source, 

253 s.version as source_version, newest_source.version as newest_sversion 

254 FROM binaries b 

255 JOIN source s ON s.id = b.source 

256 JOIN bin_associations ba ON ba.bin = b.id 

257 JOIN architecture a ON a.id = b.architecture 

258 JOIN suite su ON su.id = ba.suite 

259 JOIN files f ON f.id = b.file 

260 JOIN files_archive_map af ON af.file_id = f.id AND af.archive_id = su.archive_id 

261 JOIN component c ON c.id = af.component_id 

262 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite 

263 WHERE 

264 (:binary_version IS NULL OR b.version = :binary_version) 

265 AND (:source_version IS NULL OR s.version = :source_version) 

266 AND %s %s %s %s %s 

267 """ % (q_outdated, con_packages, con_suites, con_components, con_architectures), parameters) 

268 to_remove.extend(q) 

269 else: 

270 # Source-only 

271 if not Options["Binary-Only"]: 271 ↛ 288line 271 didn't jump to line 288, because the condition on line 271 was never false

272 q = session.execute(""" 

273 SELECT s.source, s.version, 'source', s.id, s.maintainer, s.source, 

274 s.version as source_version, newest_source.version as newest_sversion 

275 FROM source s 

276 JOIN src_associations sa ON sa.source = s.id 

277 JOIN suite su ON su.id = sa.suite 

278 JOIN archive ON archive.id = su.archive_id 

279 JOIN files f ON f.id = s.file 

280 JOIN files_archive_map af ON af.file_id = f.id AND af.archive_id = su.archive_id 

281 JOIN component c ON c.id = af.component_id 

282 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite 

283 WHERE 

284 (:source_version IS NULL OR s.version = :source_version) 

285 AND %s %s %s %s 

286 """ % (q_outdated, con_packages, con_suites, con_components), parameters) 

287 to_remove.extend(q) 

288 if not Options["Source-Only"]: 288 ↛ 309line 288 didn't jump to line 309, because the condition on line 288 was never false

289 # Source + Binary 

290 q = session.execute(""" 

291 SELECT b.package, b.version, a.arch_string, b.id, b.maintainer, s.source, 

292 s.version as source_version, newest_source.version as newest_sversion 

293 FROM binaries b 

294 JOIN bin_associations ba ON b.id = ba.bin 

295 JOIN architecture a ON b.architecture = a.id 

296 JOIN suite su ON ba.suite = su.id 

297 JOIN archive ON archive.id = su.archive_id 

298 JOIN files_archive_map af ON b.file = af.file_id AND af.archive_id = archive.id 

299 JOIN component c ON af.component_id = c.id 

300 JOIN source s ON b.source = s.id 

301 JOIN newest_source on s.source = newest_source.source AND su.id = newest_source.suite 

302 WHERE 

303 (:binary_version IS NULL OR b.version = :binary_version) 

304 AND (:source_version IS NULL OR s.version = :source_version) 

305 AND %s %s %s %s %s 

306 """ % (q_outdated, con_packages, con_suites, con_components, con_architectures), parameters) 

307 to_remove.extend(q) 

308 

309 if not to_remove: 309 ↛ 310line 309 didn't jump to line 310, because the condition on line 309 was never true

310 print("Nothing to do.") 

311 sys.exit(0) 

312 

313 # Process -C/--carbon-copy 

314 # 

315 # Accept 3 types of arguments (space separated): 

316 # 1) a number - assumed to be a bug number, i.e. nnnnn@bugs.debian.org 

317 # 2) the keyword 'package' - cc's $package@packages.debian.org for every argument 

318 # 3) contains a '@' - assumed to be an email address, used unmodified 

319 # 

320 carbon_copy = [] 

321 for copy_to in utils.split_args(Options.get("Carbon-Copy")): 

322 if copy_to.isdigit(): 322 ↛ 323line 322 didn't jump to line 323, because the condition on line 322 was never true

323 if "Dinstall::BugServer" in cnf: 

324 carbon_copy.append(copy_to + "@" + cnf["Dinstall::BugServer"]) 

325 else: 

326 utils.fubar("Asked to send mail to #%s in BTS but Dinstall::BugServer is not configured" % copy_to) 

327 elif copy_to == 'package': 327 ↛ 328line 327 didn't jump to line 328, because the condition on line 327 was never true

328 for package in set([s[5] for s in to_remove]): 

329 if "Dinstall::PackagesServer" in cnf: 

330 carbon_copy.append(package + "@" + cnf["Dinstall::PackagesServer"]) 

331 elif '@' in copy_to: 331 ↛ 334line 331 didn't jump to line 334, because the condition on line 331 was never false

332 carbon_copy.append(copy_to) 

333 else: 

334 utils.fubar("Invalid -C/--carbon-copy argument '%s'; not a bug number, 'package' or email address." % (copy_to)) 

335 

336 # If we don't have a reason; spawn an editor so the user can add one 

337 # Write the rejection email out as the <foo>.reason file 

338 if not Options["Reason"] and not Options["No-Action"]: 338 ↛ 339line 338 didn't jump to line 339, because the condition on line 338 was never true

339 Options["Reason"] = utils.call_editor() 

340 

341 # Generate the summary of what's to be removed 

342 d = {} 

343 for i in to_remove: 

344 package = i[0] 

345 version = i[1] 

346 architecture = i[2] 

347 maintainer = i[4] 

348 maintainers[maintainer] = "" 

349 source = i[5] 

350 source_version = i[6] 

351 source_newest = i[7] 

352 if package not in d: 

353 d[package] = {} 

354 if version not in d[package]: 

355 d[package][version] = [] 

356 if architecture not in d[package][version]: 356 ↛ 343line 356 didn't jump to line 343, because the condition on line 356 was never false

357 d[package][version].append(architecture) 

358 

359 maintainer_list = [] 

360 for maintainer_id in maintainers.keys(): 

361 maintainer_list.append(get_maintainer(maintainer_id).name) 

362 summary = "" 

363 removals = sorted(d) 

364 for package in removals: 

365 versions = sorted(d[package], key=functools.cmp_to_key(apt_pkg.version_compare)) 

366 for version in versions: 

367 d[package][version].sort(key=utils.ArchKey) 

368 summary += "%10s | %10s | %s\n" % (package, version, ", ".join(d[package][version])) 

369 print("Will remove the following packages from %s:" % (suites_list)) 

370 print() 

371 print(summary) 

372 print("Maintainer: %s" % ", ".join(maintainer_list)) 

373 if Options["Done"]: 

374 print("Will also close bugs: " + Options["Done"]) 

375 if carbon_copy: 

376 print("Will also send CCs to: " + ", ".join(carbon_copy)) 

377 if Options["Do-Close"]: 377 ↛ 378line 377 didn't jump to line 378, because the condition on line 377 was never true

378 print("Will also close associated bug reports.") 

379 print() 

380 print("------------------- Reason -------------------") 

381 print(Options["Reason"]) 

382 print("----------------------------------------------") 

383 print() 

384 

385 if Options["Rdep-Check"]: 

386 arches = utils.split_args(Options["Architecture"]) 

387 include_arch_all = Options['NoArchAllRdeps'] == '' 

388 if include_arch_all and 'all' in arches: 388 ↛ 390line 388 didn't jump to line 390, because the condition on line 388 was never true

389 # when arches is None, rdeps are checked on all arches in the suite 

390 arches = None 

391 reverse_depends_check(removals, suites[0], arches, session, include_arch_all=include_arch_all) 

392 

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

394 if Options["No-Action"]: 

395 sys.exit(0) 

396 

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

398 game_over() 

399 

400 # Do the actual deletion 

401 print("Deleting...", end=' ') 

402 sys.stdout.flush() 

403 

404 try: 

405 bugs = utils.split_args(Options["Done"]) 

406 remove(session, Options["Reason"], suites, to_remove, 

407 partial=Options["Partial"], components=utils.split_args(Options["Component"]), 

408 done_bugs=bugs, carbon_copy=carbon_copy, close_related_bugs=Options["Do-Close"] 

409 ) 

410 except ValueError as ex: 

411 utils.fubar(ex.message) 

412 else: 

413 print("done.") 

414 

415####################################################################################### 

416 

417 

418if __name__ == '__main__': 

419 main()