1#! /usr/bin/env python3 

2 

3""" Manipulate suite tags """ 

4# Copyright (C) 2000, 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org> 

5 

6# This program is free software; you can redistribute it and/or modify 

7# it under the terms of the GNU General Public License as published by 

8# the Free Software Foundation; either version 2 of the License, or 

9# (at your option) any later version. 

10 

11# This program is distributed in the hope that it will be useful, 

12# but WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

14# GNU General Public License for more details. 

15 

16# You should have received a copy of the GNU General Public License 

17# along with this program; if not, write to the Free Software 

18# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 

19 

20####################################################################################### 

21 

22# 8to6Guy: "Wow, Bob, You look rough!" 

23# BTAF: "Mbblpmn..." 

24# BTAF <.oO>: "You moron! This is what you get for staying up all night drinking vodka and salad dressing!" 

25# BTAF <.oO>: "This coffee I.V. drip is barely even keeping me awake! I need something with more kick! But what?" 

26# BTAF: "OMIGOD! I OVERDOSED ON HEROIN" 

27# CoWorker#n: "Give him air!!" 

28# CoWorker#n+1: "We need a syringe full of adrenaline!" 

29# CoWorker#n+2: "Stab him in the heart!" 

30# BTAF: "*YES!*" 

31# CoWorker#n+3: "Bob's been overdosing quite a bit lately..." 

32# CoWorker#n+4: "Third time this week." 

33 

34# -- http://www.angryflower.com/8to6.gif 

35 

36####################################################################################### 

37 

38# Adds or removes packages from a suite. Takes the list of files 

39# either from stdin or as a command line argument. Special action 

40# "set", will reset the suite (!) and add all packages from scratch. 

41 

42####################################################################################### 

43 

44import sys 

45import apt_pkg 

46import functools 

47import os 

48 

49from daklib.archive import ArchiveTransaction 

50from daklib.config import Config 

51from daklib.dbconn import * 

52from daklib import daklog 

53from daklib import utils 

54from daklib.queue import get_suite_version_by_package, get_suite_version_by_source 

55 

56####################################################################################### 

57 

58Logger = None 

59 

60################################################################################ 

61 

62 

63def usage(exit_code=0): 

64 print("""Usage: dak control-suite [OPTIONS] [FILE] 

65Display or alter the contents of a suite using FILE(s), or stdin. 

66 

67 -a, --add=SUITE add to SUITE 

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

69 -l, --list=SUITE list the contents of SUITE 

70 -r, --remove=SUITE remove from SUITE 

71 -s, --set=SUITE set SUITE 

72 -b, --britney generate changelog entry for britney runs""") 

73 

74 sys.exit(exit_code) 

75 

76####################################################################################### 

77 

78 

79def get_pkg(package, version, architecture, session): 

80 if architecture == 'source': 

81 q = session.query(DBSource).filter_by(source=package, version=version) \ 

82 .join(DBSource.poolfile) 

83 else: 

84 q = session.query(DBBinary).filter_by(package=package, version=version) \ 

85 .join(DBBinary.architecture).filter(Architecture.arch_string.in_([architecture, 'all'])) \ 

86 .join(DBBinary.poolfile) 

87 

88 pkg = q.first() 

89 if pkg is None: 89 ↛ 90line 89 didn't jump to line 90, because the condition on line 89 was never true

90 utils.warn("Could not find {0}_{1}_{2}.".format(package, version, architecture)) 

91 return pkg 

92 

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

94 

95 

96def britney_changelog(packages, suite, session): 

97 

98 old = {} 

99 current = {} 

100 Cnf = utils.get_conf() 

101 

102 try: 

103 q = session.execute("SELECT changelog FROM suite WHERE id = :suiteid", 

104 {'suiteid': suite.suite_id}) 

105 brit_file = q.fetchone()[0] 

106 except: 

107 brit_file = None 

108 

109 if brit_file: 109 ↛ 112line 109 didn't jump to line 112, because the condition on line 109 was never false

110 brit_file = os.path.join(Cnf['Dir::Root'], brit_file) 

111 else: 

112 return 

113 

114 q = session.execute("""SELECT s.source, s.version, sa.id 

115 FROM source s, src_associations sa 

116 WHERE sa.suite = :suiteid 

117 AND sa.source = s.id""", {'suiteid': suite.suite_id}) 

118 

119 for p in q.fetchall(): 

120 current[p[0]] = p[1] 

121 for p in packages.keys(): 121 ↛ 122line 121 didn't jump to line 122, because the loop on line 121 never started

122 if p[2] == "source": 

123 old[p[0]] = p[1] 

124 

125 new = {} 

126 for p in current.keys(): 

127 if p in old: 127 ↛ 128line 127 didn't jump to line 128, because the condition on line 127 was never true

128 if apt_pkg.version_compare(current[p], old[p]) > 0: 

129 new[p] = [current[p], old[p]] 

130 else: 

131 new[p] = [current[p], None] 

132 

133 params = {} 

134 query = "SELECT source, changelog FROM changelogs WHERE" 

135 for n, p in enumerate(new.keys()): 

136 query += f" source = :source_{n} AND (:version1_{n} IS NULL OR version > :version1_{n}) AND version <= :version2_{n}" 

137 query += " AND architecture LIKE '%source%' AND distribution in \ 

138 ('unstable', 'experimental', 'testing-proposed-updates') OR" 

139 params[f'source_{n}'] = p 

140 params[f'version1_{n}'] = new[p][1] 

141 params[f'version2_{n}'] = new[p][0] 

142 query += " False ORDER BY source, version DESC" 

143 q = session.execute(query, params) 

144 

145 pu = None 

146 with open(brit_file, 'w') as brit: 

147 

148 for u in q: 

149 if pu and pu != u[0]: 149 ↛ 150line 149 didn't jump to line 150, because the condition on line 149 was never true

150 brit.write("\n") 

151 brit.write("%s\n" % u[1]) 

152 pu = u[0] 

153 if q.rowcount: 153 ↛ 156line 153 didn't jump to line 156, because the condition on line 153 was never false

154 brit.write("\n\n\n") 

155 

156 for p in list(set(old.keys()).difference(current.keys())): 156 ↛ 157line 156 didn't jump to line 157, because the loop on line 156 never started

157 brit.write("REMOVED: %s %s\n" % (p, old[p])) 

158 

159 brit.flush() 

160 

161 

162####################################################################################### 

163 

164 

165class VersionCheck: 

166 def __init__(self, target_suite: str, force: bool, session): 

167 self.target_suite = target_suite 

168 self.force = force 

169 self.session = session 

170 

171 self.must_be_newer_than = [vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeNewerThan", session)] 

172 self.must_be_older_than = [vc.reference.suite_name for vc in get_version_checks(target_suite, "MustBeOlderThan", session)] 

173 

174 # Must be newer than an existing version in target_suite 

175 if target_suite not in self.must_be_newer_than: 175 ↛ exitline 175 didn't return from function '__init__', because the condition on line 175 was never false

176 self.must_be_newer_than.append(target_suite) 

177 

178 def __call__(self, package: str, architecture: str, new_version: str): 

179 if architecture == "source": 

180 suite_version_list = get_suite_version_by_source(package, self.session) 

181 else: 

182 suite_version_list = get_suite_version_by_package(package, architecture, self.session) 

183 

184 violations = False 

185 

186 for suite, version in suite_version_list: 

187 cmp = apt_pkg.version_compare(new_version, version) 

188 # for control-suite we allow equal version (for uploads, we don't) 

189 if suite in self.must_be_newer_than and cmp < 0: 

190 utils.warn("%s (%s): version check violated: %s targeted at %s is *not* newer than %s in %s" % (package, architecture, new_version, self.target_suite, version, suite)) 

191 violations = True 

192 if suite in self.must_be_older_than and cmp > 0: 192 ↛ 193line 192 didn't jump to line 193, because the condition on line 192 was never true

193 utils.warn("%s (%s): version check violated: %s targeted at %s is *not* older than %s in %s" % (package, architecture, new_version, self.target_suite, version, suite)) 

194 violations = True 

195 

196 if violations: 

197 if self.force: 

198 utils.warn("Continuing anyway (forced)...") 

199 else: 

200 utils.fubar("Aborting. Version checks violated and not forced.") 

201 

202####################################################################################### 

203 

204 

205def cmp_package_version(a, b): 

206 """ 

207 comparison function for tuples of the form (package-name, version, arch, ...) 

208 """ 

209 res = 0 

210 if a[2] == 'source' and b[2] != 'source': 

211 res = -1 

212 elif a[2] != 'source' and b[2] == 'source': 

213 res = 1 

214 if res == 0: 

215 res = (a[0] > b[0]) - (a[0] < b[0]) 

216 if res == 0: 

217 res = apt_pkg.version_compare(a[1], b[1]) 

218 return res 

219 

220####################################################################################### 

221 

222 

223def copy_to_suites(transaction, pkg, suites): 

224 component = pkg.poolfile.component 

225 if pkg.arch_string == "source": 

226 for s in suites: 

227 transaction.copy_source(pkg, s, component) 

228 else: 

229 for s in suites: 

230 transaction.copy_binary(pkg, s, component) 

231 

232 

233def check_propups(pkg, psuites_current, propups): 

234 key = (pkg.name, pkg.arch_string) 

235 for suite_id in psuites_current: 

236 if key in psuites_current[suite_id]: 

237 old_version = psuites_current[suite_id][key] 

238 if apt_pkg.version_compare(pkg.version, old_version) > 0: 

239 propups[suite_id].add(pkg) 

240 if pkg.arch_string != "source": 

241 source = pkg.source 

242 propups[suite_id].add(source) 

243 

244 

245def get_propup_suites(suite, session): 

246 propup_suites = [] 

247 for rule in Config().value_list("SuiteMappings"): 

248 fields = rule.split() 

249 if fields[0] == "propup-version" and fields[1] == suite.suite_name: 

250 propup_suites.append(session.query(Suite).filter_by(suite_name=fields[2]).one()) 

251 return propup_suites 

252 

253 

254def set_suite(file, suite, transaction, britney=False, force=False): 

255 session = transaction.session 

256 suite_id = suite.suite_id 

257 lines = file.readlines() 

258 suites = [suite] + [q.suite for q in suite.copy_queues] 

259 propup_suites = get_propup_suites(suite, session) 

260 

261 # Our session is already in a transaction 

262 

263 def get_binary_q(suite_id): 

264 return session.execute("""SELECT b.package, b.version, a.arch_string, ba.id 

265 FROM binaries b, bin_associations ba, architecture a 

266 WHERE ba.suite = :suiteid 

267 AND ba.bin = b.id AND b.architecture = a.id 

268 ORDER BY b.version ASC""", {'suiteid': suite_id}) 

269 

270 def get_source_q(suite_id): 

271 return session.execute("""SELECT s.source, s.version, 'source', sa.id 

272 FROM source s, src_associations sa 

273 WHERE sa.suite = :suiteid 

274 AND sa.source = s.id 

275 ORDER BY s.version ASC""", {'suiteid': suite_id}) 

276 

277 # Build up a dictionary of what is currently in the suite 

278 current = {} 

279 

280 q = get_binary_q(suite_id) 

281 for i in q: 

282 key = i[:3] 

283 current[key] = i[3] 

284 

285 q = get_source_q(suite_id) 

286 for i in q: 

287 key = i[:3] 

288 current[key] = i[3] 

289 

290 # Build a dictionary of what's currently in the propup suites 

291 psuites_current = {} 

292 propups_needed = {} 

293 for p_s in propup_suites: 

294 propups_needed[p_s.suite_id] = set() 

295 psuites_current[p_s.suite_id] = {} 

296 q = get_binary_q(p_s.suite_id) 

297 for i in q: 

298 key = (i[0], i[2]) 

299 # the query is sorted, so we only keep the newest version 

300 psuites_current[p_s.suite_id][key] = i[1] 

301 

302 q = get_source_q(p_s.suite_id) 

303 for i in q: 

304 key = (i[0], i[2]) 

305 # the query is sorted, so we only keep the newest version 

306 psuites_current[p_s.suite_id][key] = i[1] 

307 

308 # Build up a dictionary of what should be in the suite 

309 desired = set() 

310 for line in lines: 

311 split_line = line.strip().split() 

312 if len(split_line) != 3: 312 ↛ 313line 312 didn't jump to line 313, because the condition on line 312 was never true

313 utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1])) 

314 continue 

315 desired.add(tuple(split_line)) 

316 

317 version_check = VersionCheck(suite.suite_name, force, session) 

318 

319 # Check to see which packages need added and add them 

320 for key in sorted(desired, key=functools.cmp_to_key(cmp_package_version)): 

321 if key not in current: 

322 (package, version, architecture) = key 

323 version_check(package, architecture, version) 

324 pkg = get_pkg(package, version, architecture, session) 

325 if pkg is None: 325 ↛ 326line 325 didn't jump to line 326, because the condition on line 325 was never true

326 continue 

327 

328 copy_to_suites(transaction, pkg, suites) 

329 Logger.log(["added", suite.suite_name, " ".join(key)]) 

330 

331 check_propups(pkg, psuites_current, propups_needed) 

332 

333 # Check to see which packages need removed and remove them 

334 for key, pkid in current.items(): 

335 if key not in desired: 

336 (package, version, architecture) = key 

337 if architecture == "source": 

338 session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': pkid}) 

339 else: 

340 session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': pkid}) 

341 Logger.log(["removed", suite.suite_name, " ".join(key), pkid]) 

342 

343 for p_s in propup_suites: 

344 for pkg in propups_needed[p_s.suite_id]: 

345 copy_to_suites(transaction, pkg, [p_s]) 

346 info = (pkg.name, pkg.version, pkg.arch_string) 

347 Logger.log(["propup", p_s.suite_name, " ".join(info)]) 

348 

349 session.commit() 

350 

351 if britney: 

352 britney_changelog(current, suite, session) 

353 

354####################################################################################### 

355 

356 

357def process_file(file, suite, action, transaction, britney=False, force=False): 

358 session = transaction.session 

359 

360 if action == "set": 

361 set_suite(file, suite, transaction, britney, force) 

362 return 

363 

364 suite_id = suite.suite_id 

365 suites = [suite] + [q.suite for q in suite.copy_queues] 

366 extra_archives = [suite.archive] 

367 

368 request = [] 

369 

370 # Our session is already in a transaction 

371 for line in file: 

372 split_line = line.strip().split() 

373 if len(split_line) != 3: 373 ↛ 374line 373 didn't jump to line 374, because the condition on line 373 was never true

374 utils.warn("'%s' does not break into 'package version architecture'." % (line[:-1])) 

375 continue 

376 request.append(split_line) 

377 

378 request.sort(key=functools.cmp_to_key(cmp_package_version)) 

379 

380 version_check = VersionCheck(suite.suite_name, force, session) 

381 

382 for package, version, architecture in request: 

383 pkg = get_pkg(package, version, architecture, session) 

384 if pkg is None: 384 ↛ 385line 384 didn't jump to line 385, because the condition on line 384 was never true

385 continue 

386 if architecture == 'source': 

387 pkid = pkg.source_id 

388 else: 

389 pkid = pkg.binary_id 

390 

391 component = pkg.poolfile.component 

392 

393 # Do version checks when adding packages 

394 if action == "add": 

395 version_check(package, architecture, version) 

396 

397 if architecture == "source": 

398 # Find the existing association ID, if any 

399 q = session.execute("""SELECT id FROM src_associations 

400 WHERE suite = :suiteid and source = :pkid""", 

401 {'suiteid': suite_id, 'pkid': pkid}) 

402 ql = q.fetchall() 

403 if len(ql) < 1: 

404 association_id = None 

405 else: 

406 association_id = ql[0][0] 

407 

408 # Take action 

409 if action == "add": 

410 if association_id: 410 ↛ 411line 410 didn't jump to line 411, because the condition on line 410 was never true

411 utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite.suite_name)) 

412 continue 

413 else: 

414 for s in suites: 

415 transaction.copy_source(pkg, s, component) 

416 Logger.log(["added", package, version, architecture, suite.suite_name, pkid]) 

417 

418 elif action == "remove": 418 ↛ 382line 418 didn't jump to line 382, because the condition on line 418 was never false

419 if association_id is None: 419 ↛ 420line 419 didn't jump to line 420, because the condition on line 419 was never true

420 utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite)) 

421 continue 

422 else: 

423 session.execute("""DELETE FROM src_associations WHERE id = :pkid""", {'pkid': association_id}) 

424 Logger.log(["removed", package, version, architecture, suite.suite_name, pkid]) 

425 else: 

426 # Find the existing associations ID, if any 

427 q = session.execute("""SELECT id FROM bin_associations 

428 WHERE suite = :suiteid and bin = :pkid""", 

429 {'suiteid': suite_id, 'pkid': pkid}) 

430 ql = q.fetchall() 

431 if len(ql) < 1: 

432 association_id = None 

433 else: 

434 association_id = ql[0][0] 

435 

436 # Take action 

437 if action == "add": 

438 if association_id: 438 ↛ 439line 438 didn't jump to line 439, because the condition on line 438 was never true

439 utils.warn("'%s_%s_%s' already exists in suite %s." % (package, version, architecture, suite)) 

440 continue 

441 else: 

442 for s in suites: 

443 transaction.copy_binary(pkg, s, component, extra_archives=extra_archives) 

444 Logger.log(["added", package, version, architecture, suite.suite_name, pkid]) 

445 elif action == "remove": 445 ↛ 382line 445 didn't jump to line 382, because the condition on line 445 was never false

446 if association_id is None: 446 ↛ 447line 446 didn't jump to line 447, because the condition on line 446 was never true

447 utils.warn("'%s_%s_%s' doesn't exist in suite %s." % (package, version, architecture, suite)) 

448 continue 

449 else: 

450 session.execute("""DELETE FROM bin_associations WHERE id = :pkid""", {'pkid': association_id}) 

451 Logger.log(["removed", package, version, architecture, suite.suite_name, pkid]) 

452 

453 session.commit() 

454 

455####################################################################################### 

456 

457 

458def get_list(suite, session): 

459 suite_id = suite.suite_id 

460 # List binaries 

461 q = session.execute("""SELECT b.package, b.version, a.arch_string 

462 FROM binaries b, bin_associations ba, architecture a 

463 WHERE ba.suite = :suiteid 

464 AND ba.bin = b.id AND b.architecture = a.id""", {'suiteid': suite_id}) 

465 for i in q.fetchall(): 

466 print(" ".join(i)) 

467 

468 # List source 

469 q = session.execute("""SELECT s.source, s.version 

470 FROM source s, src_associations sa 

471 WHERE sa.suite = :suiteid 

472 AND sa.source = s.id""", {'suiteid': suite_id}) 

473 for i in q.fetchall(): 

474 print(" ".join(i) + " source") 

475 

476####################################################################################### 

477 

478 

479def main(): 

480 global Logger 

481 

482 cnf = Config() 

483 

484 Arguments = [('a', "add", "Control-Suite::Options::Add", "HasArg"), 

485 ('b', "britney", "Control-Suite::Options::Britney"), 

486 ('f', 'force', 'Control-Suite::Options::Force'), 

487 ('h', "help", "Control-Suite::Options::Help"), 

488 ('l', "list", "Control-Suite::Options::List", "HasArg"), 

489 ('r', "remove", "Control-Suite::Options::Remove", "HasArg"), 

490 ('s', "set", "Control-Suite::Options::Set", "HasArg")] 

491 

492 for i in ["add", "britney", "help", "list", "remove", "set", "version"]: 

493 key = "Control-Suite::Options::%s" % i 

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

495 cnf[key] = "" 

496 

497 try: 

498 file_list = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) 

499 except SystemError as e: 

500 print("%s\n" % e) 

501 usage(1) 

502 Options = cnf.subtree("Control-Suite::Options") 

503 

504 if Options["Help"]: 

505 usage() 

506 

507 force = "Force" in Options and Options["Force"] 

508 

509 action = None 

510 

511 for i in ("add", "list", "remove", "set"): 

512 if cnf["Control-Suite::Options::%s" % (i)] != "": 

513 suite_name = cnf["Control-Suite::Options::%s" % (i)] 

514 

515 if action: 515 ↛ 516line 515 didn't jump to line 516, because the condition on line 515 was never true

516 utils.fubar("Can only perform one action at a time.") 

517 

518 action = i 

519 

520 # Need an action... 

521 if action is None: 521 ↛ 522line 521 didn't jump to line 522, because the condition on line 521 was never true

522 utils.fubar("No action specified.") 

523 

524 britney = False 

525 if action == "set" and cnf["Control-Suite::Options::Britney"]: 

526 britney = True 

527 

528 if action == "list": 

529 session = DBConn().session() 

530 suite = get_suite(suite_name, session) 

531 get_list(suite, session) 

532 else: 

533 Logger = daklog.Logger("control-suite") 

534 

535 with ArchiveTransaction() as transaction: 

536 session = transaction.session 

537 suite = get_suite(suite_name, session) 

538 

539 if action == "set" and not suite.allowcsset: 

540 if force: 540 ↛ 543line 540 didn't jump to line 543, because the condition on line 540 was never false

541 utils.warn("Would not normally allow setting suite {0} (allowcsset is FALSE), but --force used".format(suite_name)) 

542 else: 

543 utils.fubar("Will not reset suite {0} due to its database configuration (allowcsset is FALSE)".format(suite_name)) 

544 

545 if file_list: 545 ↛ 546line 545 didn't jump to line 546, because the condition on line 545 was never true

546 for f in file_list: 

547 process_file(open(f), suite, action, transaction, britney, force) 

548 else: 

549 process_file(sys.stdin, suite, action, transaction, britney, force) 

550 

551 Logger.close() 

552 

553####################################################################################### 

554 

555 

556if __name__ == '__main__': 

557 main()