1#! /usr/bin/env python3 

2 

3""" 

4Checks Debian packages from Incoming 

5@contact: Debian FTP Master <ftpmaster@debian.org> 

6@copyright: 2000, 2001, 2002, 2003, 2004, 2005, 2006 James Troup <james@nocrew.org> 

7@copyright: 2009 Joerg Jaspert <joerg@debian.org> 

8@copyright: 2009 Mark Hymers <mhy@debian.org> 

9@copyright: 2009 Frank Lichtenheld <djpig@debian.org> 

10@license: GNU General Public License version 2 or later 

11""" 

12 

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

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

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

16# (at your option) any later version. 

17 

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

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

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

21# GNU General Public License for more details. 

22 

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

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

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

26 

27# based on process-unchecked and process-accepted 

28 

29## pu|pa: locking (daily.lock) 

30## pu|pa: parse arguments -> list of changes files 

31## pa: initialize urgency log 

32## pu|pa: sort changes list 

33 

34## foreach changes: 

35### pa: load dak file 

36## pu: copy CHG to tempdir 

37## pu: check CHG signature 

38## pu: parse changes file 

39## pu: checks: 

40## pu: check distribution (mappings, rejects) 

41## pu: copy FILES to tempdir 

42## pu: check whether CHG already exists in CopyChanges 

43## pu: check whether FILES already exist in one of the policy queues 

44## for deb in FILES: 

45## pu: extract control information 

46## pu: various checks on control information 

47## pu|pa: search for source (in CHG, projectb, policy queues) 

48## pu|pa: check whether "Version" fulfills target suite requirements/suite propagation 

49## pu|pa: check whether deb already exists in the pool 

50## for src in FILES: 

51## pu: various checks on filenames and CHG consistency 

52## pu: if isdsc: check signature 

53## for file in FILES: 

54## pu: various checks 

55## pu: NEW? 

56## //pu: check whether file already exists in the pool 

57## pu: store what "Component" the package is currently in 

58## pu: check whether we found everything we were looking for in CHG 

59## pu: check the DSC: 

60## pu: check whether we need and have ONE DSC 

61## pu: parse the DSC 

62## pu: various checks //maybe drop some of the in favor of lintian 

63## pu|pa: check whether "Version" fulfills target suite requirements/suite propagation 

64## pu: check whether DSC_FILES is consistent with "Format" 

65## for src in DSC_FILES: 

66## pu|pa: check whether file already exists in the pool (with special handling for .orig.tar.gz) 

67## pu: create new tempdir 

68## pu: create symlink mirror of source 

69## pu: unpack source 

70## pu: extract changelog information for BTS 

71## //pu: create missing .orig symlink 

72## pu: check with lintian 

73## for file in FILES: 

74## pu: check checksums and sizes 

75## for file in DSC_FILES: 

76## pu: check checksums and sizes 

77## pu: CHG: check urgency 

78## for deb in FILES: 

79## pu: extract contents list and check for dubious timestamps 

80## pu: check that the uploader is actually allowed to upload the package 

81### pa: install: 

82### if stable_install: 

83### pa: remove from p-u 

84### pa: add to stable 

85### pa: move CHG to morgue 

86### pa: append data to ChangeLog 

87### pa: send mail 

88### pa: remove .dak file 

89### else: 

90### pa: add dsc to db: 

91### for file in DSC_FILES: 

92### pa: add file to file 

93### pa: add file to dsc_files 

94### pa: create source entry 

95### pa: update source associations 

96### pa: update src_uploaders 

97### for deb in FILES: 

98### pa: add deb to db: 

99### pa: add file to file 

100### pa: find source entry 

101### pa: create binaries entry 

102### pa: update binary associations 

103### pa: .orig component move 

104### pa: move files to pool 

105### pa: save CHG 

106### pa: move CHG to done/ 

107### pa: change entry in queue_build 

108## pu: use dispatch table to choose target queue: 

109## if NEW: 

110## pu: write .dak file 

111## pu: move to NEW 

112## pu: send mail 

113## elsif AUTOBYHAND: 

114## pu: run autobyhand script 

115## pu: if stuff left, do byhand or accept 

116## elsif targetqueue in (oldstable, stable, embargo, unembargo): 

117## pu: write .dak file 

118## pu: check overrides 

119## pu: move to queue 

120## pu: send mail 

121## else: 

122## pu: write .dak file 

123## pu: move to ACCEPTED 

124## pu: send mails 

125## pu: create files for BTS 

126## pu: create entry in queue_build 

127## pu: check overrides 

128 

129# Integrity checks 

130## GPG 

131## Parsing changes (check for duplicates) 

132## Parse dsc 

133## file list checks 

134 

135# New check layout (TODO: Implement) 

136## Permission checks 

137### suite mappings 

138### ACLs 

139### version checks (suite) 

140### override checks 

141 

142## Source checks 

143### copy orig 

144### unpack 

145### BTS changelog 

146### src contents 

147### lintian 

148### urgency log 

149 

150## Binary checks 

151### timestamps 

152### control checks 

153### src relation check 

154### contents 

155 

156## Database insertion (? copy from stuff) 

157### BYHAND / NEW / Policy queues 

158### Pool 

159 

160## Queue builds 

161 

162import datetime 

163import errno 

164import fcntl 

165import functools 

166import os 

167import sys 

168import traceback 

169import apt_pkg 

170import time 

171from collections.abc import Iterable 

172from typing import NoReturn 

173 

174from daklib import daklog 

175from daklib.dbconn import * 

176from daklib.urgencylog import UrgencyLog 

177from daklib.summarystats import SummaryStats 

178from daklib.config import Config 

179import daklib.utils as utils 

180from daklib.regexes import * 

181 

182import daklib.announce 

183import daklib.archive 

184import daklib.checks 

185import daklib.upload 

186 

187############################################################################### 

188 

189Options = None 

190Logger = None 

191 

192############################################################################### 

193 

194 

195def usage(exit_code=0) -> NoReturn: 

196 print("""Usage: dak process-upload [OPTION]... [CHANGES]... 

197 -a, --automatic automatic run 

198 -d, --directory <DIR> process uploads in <DIR> 

199 -h, --help show this help and exit. 

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

201 -p, --no-lock don't check lockfile !! for cron.daily only !! 

202 -s, --no-mail don't send any mail 

203 -V, --version display the version number and exit""") 

204 sys.exit(exit_code) 

205 

206############################################################################### 

207 

208 

209def try_or_reject(function): 

210 """Try to call function or reject the upload if that fails 

211 """ 

212 @functools.wraps(function) 

213 def wrapper(directory: str, upload: daklib.archive.ArchiveUpload, *args, **kwargs): 

214 reason = 'No exception caught. This should not happen.' 

215 

216 try: 

217 return function(directory, upload, *args, **kwargs) 

218 except (daklib.archive.ArchiveException, daklib.checks.Reject) as e: 

219 reason = str(e) 

220 except Exception as e: 

221 reason = "There was an uncaught exception when processing your upload:\n{0}\nAny original reject reason follows below.".format(traceback.format_exc()) 

222 

223 try: 

224 upload.rollback() 

225 return real_reject(directory, upload, reason=reason) 

226 except Exception as e: 

227 reason = "In addition there was an exception when rejecting the package:\n{0}\nPrevious reasons:\n{1}".format(traceback.format_exc(), reason) 

228 upload.rollback() 

229 return real_reject(directory, upload, reason=reason, notify=False) 

230 

231 raise Exception('Rejecting upload failed after multiple tries. Giving up. Last reason:\n{0}'.format(reason)) 

232 

233 return wrapper 

234 

235 

236def get_processed_upload(upload: daklib.archive.ArchiveUpload) -> daklib.announce.ProcessedUpload: 

237 changes = upload.changes 

238 control = upload.changes.changes 

239 

240 pu = daklib.announce.ProcessedUpload() 

241 

242 pu.maintainer = control.get('Maintainer') 

243 pu.changed_by = control.get('Changed-By') 

244 pu.fingerprint = changes.primary_fingerprint 

245 

246 pu.suites = upload.final_suites or [] 

247 pu.from_policy_suites = [] 

248 

249 with open(upload.changes.path, 'r') as fd: 

250 pu.changes = fd.read() 

251 pu.changes_filename = upload.changes.filename 

252 pu.sourceful = upload.changes.sourceful 

253 pu.source = control.get('Source') 

254 pu.version = control.get('Version') 

255 pu.architecture = control.get('Architecture') 

256 pu.bugs = changes.closed_bugs 

257 

258 pu.program = "process-upload" 

259 

260 pu.warnings = upload.warnings 

261 

262 return pu 

263 

264 

265@try_or_reject 

266def accept(directory: str, upload: daklib.archive.ArchiveUpload) -> None: 

267 cnf = Config() 

268 

269 Logger.log(['ACCEPT', upload.changes.filename]) 

270 print("ACCEPT") 

271 

272 upload.install() 

273 utils.process_buildinfos(upload.directory, upload.changes.buildinfo_files, 

274 upload.transaction.fs, Logger) 

275 

276 accepted_to_real_suite = any(suite.policy_queue is None for suite in upload.final_suites) 

277 sourceful_upload = upload.changes.sourceful 

278 

279 control = upload.changes.changes 

280 if sourceful_upload and not Options['No-Action']: 

281 urgency = control.get('Urgency') 

282 # As per policy 5.6.17, the urgency can be followed by a space and a 

283 # comment. Extract only the urgency from the string. 

284 if ' ' in urgency: 284 ↛ 285line 284 didn't jump to line 285, because the condition on line 284 was never true

285 urgency, comment = urgency.split(' ', 1) 

286 if urgency not in cnf.value_list('Urgency::Valid'): 286 ↛ 287line 286 didn't jump to line 287, because the condition on line 286 was never true

287 urgency = cnf['Urgency::Default'] 

288 UrgencyLog().log(control['Source'], control['Version'], urgency) 

289 

290 pu = get_processed_upload(upload) 

291 daklib.announce.announce_accept(pu) 

292 

293 # Move .changes to done, but only for uploads that were accepted to a 

294 # real suite. process-policy will handle this for uploads to queues. 

295 if accepted_to_real_suite: 

296 src = os.path.join(upload.directory, upload.changes.filename) 

297 

298 now = datetime.datetime.now() 

299 donedir = os.path.join(cnf['Dir::Done'], now.strftime('%Y/%m/%d')) 

300 dst = os.path.join(donedir, upload.changes.filename) 

301 dst = utils.find_next_free(dst) 

302 

303 upload.transaction.fs.copy(src, dst, mode=0o644) 

304 

305 SummaryStats().accept_count += 1 

306 SummaryStats().accept_bytes += upload.changes.bytes 

307 

308 

309@try_or_reject 

310def accept_to_new(directory: str, upload: daklib.archive.ArchiveUpload) -> None: 

311 

312 Logger.log(['ACCEPT-TO-NEW', upload.changes.filename]) 

313 print("ACCEPT-TO-NEW") 

314 

315 upload.install_to_new() 

316 # TODO: tag bugs pending 

317 

318 pu = get_processed_upload(upload) 

319 daklib.announce.announce_new(pu) 

320 

321 SummaryStats().accept_count += 1 

322 SummaryStats().accept_bytes += upload.changes.bytes 

323 

324 

325@try_or_reject 

326def reject(directory: str, upload: daklib.archive.ArchiveUpload, reason=None, notify=True) -> None: 

327 real_reject(directory, upload, reason, notify) 

328 

329 

330def real_reject(directory: str, upload: daklib.archive.ArchiveUpload, reason=None, notify=True) -> None: 

331 # XXX: rejection itself should go to daklib.archive.ArchiveUpload 

332 cnf = Config() 

333 

334 Logger.log(['REJECT', upload.changes.filename]) 

335 print("REJECT") 

336 

337 fs = upload.transaction.fs 

338 rejectdir = cnf['Dir::Reject'] 

339 

340 files = [f.filename for f in upload.changes.files.values()] 

341 files.append(upload.changes.filename) 

342 

343 for fn in files: 

344 src = os.path.join(upload.directory, fn) 

345 dst = utils.find_next_free(os.path.join(rejectdir, fn)) 

346 if not os.path.exists(src): 

347 continue 

348 fs.copy(src, dst) 

349 

350 if upload.reject_reasons is not None: 350 ↛ 355line 350 didn't jump to line 355, because the condition on line 350 was never false

351 if reason is None: 351 ↛ 353line 351 didn't jump to line 353, because the condition on line 351 was never false

352 reason = '' 

353 reason = reason + '\n' + '\n'.join(upload.reject_reasons) 

354 

355 if reason is None: 355 ↛ 356line 355 didn't jump to line 356, because the condition on line 355 was never true

356 reason = '(Unknown reason. Please check logs.)' 

357 

358 dst = utils.find_next_free(os.path.join(rejectdir, '{0}.reason'.format(upload.changes.filename))) 

359 fh = fs.create(dst) 

360 fh.write(reason) 

361 fh.close() 

362 

363 if notify: 363 ↛ 367line 363 didn't jump to line 367, because the condition on line 363 was never false

364 pu = get_processed_upload(upload) 

365 daklib.announce.announce_reject(pu, reason) 

366 

367 SummaryStats().reject_count += 1 

368 

369############################################################################### 

370 

371 

372def action(directory: str, upload: daklib.archive.ArchiveUpload) -> bool: 

373 changes = upload.changes 

374 processed = True 

375 

376 global Logger 

377 

378 cnf = Config() 

379 

380 okay = upload.check() 

381 

382 try: 

383 summary = changes.changes.get('Changes', '') 

384 except UnicodeDecodeError as e: 

385 summary = "Reading changes failed: %s" % (e) 

386 # the upload checks should have detected this, but make sure this 

387 # upload gets rejected in any case 

388 upload.reject_reasons.append(summary) 

389 

390 package_info = [] 

391 if okay: 

392 if changes.source is not None: 

393 package_info.append("source:{0}".format(changes.source.dsc['Source'])) 

394 for binary in changes.binaries: 

395 package_info.append("binary:{0}".format(binary.control['Package'])) 

396 

397 (prompt, answer) = ("", "XXX") 

398 if Options["No-Action"] or Options["Automatic"]: 398 ↛ 401line 398 didn't jump to line 401, because the condition on line 398 was never false

399 answer = 'S' 

400 

401 print(summary) 

402 print() 

403 print("\n".join(package_info)) 

404 print() 

405 if len(upload.warnings) > 0: 

406 print("\n".join(upload.warnings)) 

407 print() 

408 

409 if len(upload.reject_reasons) > 0: 

410 print("Reason:") 

411 print("\n".join(upload.reject_reasons)) 

412 print() 

413 

414 path = os.path.join(directory, changes.filename) 

415 created = os.stat(path).st_mtime 

416 now = time.time() 

417 too_new = (now - created < int(cnf['Dinstall::SkipTime'])) 

418 

419 if too_new: 

420 print("SKIP (too new)") 

421 prompt = "[S]kip, Quit ?" 

422 else: 

423 prompt = "[R]eject, Skip, Quit ?" 

424 if Options["Automatic"]: 424 ↛ 435line 424 didn't jump to line 435, because the condition on line 424 was never false

425 answer = 'R' 

426 elif upload.new: 

427 prompt = "[N]ew, Skip, Quit ?" 

428 if Options['Automatic']: 428 ↛ 435line 428 didn't jump to line 435, because the condition on line 428 was never false

429 answer = 'N' 

430 else: 

431 prompt = "[A]ccept, Skip, Quit ?" 

432 if Options['Automatic']: 432 ↛ 435line 432 didn't jump to line 435, because the condition on line 432 was never false

433 answer = 'A' 

434 

435 while prompt.find(answer) == -1: 435 ↛ 436line 435 didn't jump to line 436, because the condition on line 435 was never true

436 answer = utils.input_or_exit(prompt) 

437 m = re_default_answer.match(prompt) 

438 if answer == "": 

439 answer = m.group(1) 

440 answer = answer[:1].upper() 

441 

442 if answer == 'R': 

443 reject(directory, upload) 

444 elif answer == 'A': 

445 # upload.try_autobyhand must not be run with No-Action. 

446 if Options['No-Action']: 446 ↛ 447line 446 didn't jump to line 447, because the condition on line 446 was never true

447 accept(directory, upload) 

448 elif upload.try_autobyhand(): 448 ↛ 451line 448 didn't jump to line 451, because the condition on line 448 was never false

449 accept(directory, upload) 

450 else: 

451 print("W: redirecting to BYHAND as automatic processing failed.") 

452 accept_to_new(directory, upload) 

453 elif answer == 'N': 

454 accept_to_new(directory, upload) 

455 elif answer == 'Q': 455 ↛ 456line 455 didn't jump to line 456, because the condition on line 455 was never true

456 sys.exit(0) 

457 elif answer == 'S': 457 ↛ 460line 457 didn't jump to line 460, because the condition on line 457 was never false

458 processed = False 

459 

460 if not Options['No-Action']: 460 ↛ 463line 460 didn't jump to line 463, because the condition on line 460 was never false

461 upload.commit() 

462 

463 return processed 

464 

465############################################################################### 

466 

467 

468def unlink_if_exists(path: str) -> None: 

469 try: 

470 os.unlink(path) 

471 except OSError as e: 

472 if e.errno != errno.ENOENT: 472 ↛ 473line 472 didn't jump to line 473, because the condition on line 472 was never true

473 raise 

474 

475 

476def process_it(directory: str, changes: daklib.upload.Changes, keyrings: list[str]) -> None: 

477 global Logger 

478 

479 print("\n{0}\n".format(changes.filename)) 

480 Logger.log(["Processing changes file", changes.filename]) 

481 

482 with daklib.archive.ArchiveUpload(directory, changes, keyrings) as upload: 

483 processed = action(directory, upload) 

484 if processed and not Options['No-Action']: 

485 session = DBConn().session() 

486 history = SignatureHistory.from_signed_file(upload.changes) 

487 if history.query(session) is None: 487 ↛ 490line 487 didn't jump to line 490, because the condition on line 487 was never false

488 session.add(history) 

489 session.commit() 

490 session.close() 

491 

492 unlink_if_exists(os.path.join(directory, changes.filename)) 

493 for fn in changes.files: 

494 unlink_if_exists(os.path.join(directory, fn)) 

495 

496############################################################################### 

497 

498 

499def process_changes(changes_filenames: Iterable[str]): 

500 session = DBConn().session() 

501 keyrings = session.query(Keyring).filter_by(active=True).order_by(Keyring.priority) 

502 keyring_files = [k.keyring_name for k in keyrings] 

503 session.close() 

504 

505 changes = [] 

506 for fn in changes_filenames: 

507 try: 

508 directory, filename = os.path.split(fn) 

509 c = daklib.upload.Changes(directory, filename, keyring_files) 

510 changes.append([directory, c]) 

511 except Exception as e: 

512 try: 

513 Logger.log([filename, "Error while loading changes file {0}: {1}".format(fn, e)]) 

514 except Exception as e: 

515 Logger.log([filename, "Error while loading changes file {0}, with additional error while printing exception: {1}".format(fn, repr(e))]) 

516 

517 changes.sort(key=lambda x: x[1]) 

518 

519 for directory, c in changes: 

520 process_it(directory, c, keyring_files) 

521 

522############################################################################### 

523 

524 

525def main(): 

526 global Options, Logger 

527 

528 cnf = Config() 

529 summarystats = SummaryStats() 

530 

531 Arguments = [('a', "automatic", "Dinstall::Options::Automatic"), 

532 ('h', "help", "Dinstall::Options::Help"), 

533 ('n', "no-action", "Dinstall::Options::No-Action"), 

534 ('p', "no-lock", "Dinstall::Options::No-Lock"), 

535 ('s', "no-mail", "Dinstall::Options::No-Mail"), 

536 ('d', "directory", "Dinstall::Options::Directory", "HasArg")] 

537 

538 for i in ["automatic", "help", "no-action", "no-lock", "no-mail", 

539 "version", "directory"]: 

540 key = "Dinstall::Options::%s" % i 

541 if key not in cnf: 

542 cnf[key] = "" 

543 

544 changes_files = apt_pkg.parse_commandline(cnf.Cnf, Arguments, sys.argv) 

545 Options = cnf.subtree("Dinstall::Options") 

546 

547 if Options["Help"]: 

548 usage() 

549 

550 # -n/--dry-run invalidates some other options which would involve things happening 

551 if Options["No-Action"]: 551 ↛ 552line 551 didn't jump to line 552, because the condition on line 551 was never true

552 Options["Automatic"] = "" 

553 

554 # Obtain lock if not in no-action mode and initialize the log 

555 if not Options["No-Action"]: 555 ↛ 569line 555 didn't jump to line 569, because the condition on line 555 was never false

556 lock_fd = os.open(os.path.join(cnf["Dir::Lock"], 'process-upload.lock'), os.O_RDWR | os.O_CREAT) 

557 try: 

558 fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) 

559 except OSError as e: 

560 if e.errno in (errno.EACCES, errno.EAGAIN): 

561 utils.fubar("Couldn't obtain lock; assuming another 'dak process-upload' is already running.") 

562 else: 

563 raise 

564 

565 # Initialise UrgencyLog() - it will deal with the case where we don't 

566 # want to log urgencies 

567 urgencylog = UrgencyLog() 

568 

569 Logger = daklog.Logger("process-upload", Options["No-Action"]) 

570 

571 # If we have a directory flag, use it to find our files 

572 if cnf["Dinstall::Options::Directory"] != "": 572 ↛ 585line 572 didn't jump to line 585, because the condition on line 572 was never false

573 # Note that we clobber the list of files we were given in this case 

574 # so warn if the user has done both 

575 if len(changes_files) > 0: 575 ↛ 576line 575 didn't jump to line 576, because the condition on line 575 was never true

576 utils.warn("Directory provided so ignoring files given on command line") 

577 

578 changes_files = utils.get_changes_files(cnf["Dinstall::Options::Directory"]) 

579 # FIXME: quick hack to NOT have p-u run for ages, if some binnmu fun uploads thousands of changes 

580 # might want to make the number configurable at some point 

581 if len(changes_files) > 200: 581 ↛ 582line 581 didn't jump to line 582, because the condition on line 581 was never true

582 import random 

583 changes_files = random.sample(changes_files, k=200) 

584 Logger.log(["Using changes files from directory", cnf["Dinstall::Options::Directory"], len(changes_files)]) 

585 elif not len(changes_files) > 0: 

586 utils.fubar("No changes files given and no directory specified") 

587 else: 

588 Logger.log(["Using changes files from command-line", len(changes_files)]) 

589 

590 process_changes(changes_files) 

591 

592 if summarystats.accept_count: 

593 sets = "set" 

594 if summarystats.accept_count > 1: 

595 sets = "sets" 

596 print("Installed %d package %s, %s." % (summarystats.accept_count, sets, 

597 utils.size_type(int(summarystats.accept_bytes)))) 

598 Logger.log(["total", summarystats.accept_count, summarystats.accept_bytes]) 

599 

600 if summarystats.reject_count: 

601 sets = "set" 

602 if summarystats.reject_count > 1: 

603 sets = "sets" 

604 print("Rejected %d package %s." % (summarystats.reject_count, sets)) 

605 Logger.log(["rejected", summarystats.reject_count]) 

606 

607 if not Options["No-Action"]: 607 ↛ 610line 607 didn't jump to line 610, because the condition on line 607 was never false

608 urgencylog.close() 

609 

610 Logger.close() 

611 

612############################################################################### 

613 

614 

615if __name__ == '__main__': 

616 main()