Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2 

3""" 

4 (c) 2019 - Copyright Red Hat Inc 

5 

6 Authors: 

7 Pierre-Yves Chibon <pingou@pingoured.fr> 

8 Karsten Hopp <karsten@redhat.com> 

9 

10""" 

11 

12from __future__ import print_function, unicode_literals 

13 

14import collections 

15import logging 

16 

17import flask 

18 

19import pagure.utils 

20from pagure.api import APIERROR, api_login_required, api_method 

21from pagure.api.utils import _check_token, _get_repo 

22from pagure.exceptions import PagureException 

23 

24import pagure_distgit.forms 

25from pagure_distgit import model 

26 

27import requests 

28 

29from sqlalchemy.exc import SQLAlchemyError 

30 

31 

32_log = logging.getLogger(__name__) 

33 

34DISTGIT_NS = flask.Blueprint( 

35 "distgit_ns", __name__, url_prefix="/_dg", template_folder="templates" 

36) 

37 

38 

39@DISTGIT_NS.route("/anitya/<namespace>/<repo>", methods=["GET"]) 

40def anitya_get_endpoint(namespace, repo): 

41 """Returns the current status of the monitoring in anitya of this 

42 package.""" 

43 repo = flask.g.repo 

44 output = {"monitoring": "no-monitoring"} 

45 if repo.anitya: 

46 output = {"monitoring": repo.anitya.anitya_status} 

47 return flask.jsonify(output) 

48 

49 

50@DISTGIT_NS.route("/anitya/<namespace>/<repo>", methods=["POST"]) 

51@api_login_required(acls=["modify_project"]) 

52@api_method 

53def anitya_patch_endpoint(namespace, repo): 

54 """Updates the current status of the monitoring in anitya of this 

55 package.""" 

56 

57 repo = _get_repo(repo, namespace=namespace) 

58 _check_token(repo, project_token=False) 

59 

60 is_site_admin = pagure.utils.is_admin() 

61 admins = [u.username for u in repo.get_project_users("admin")] 

62 # Only allow the main admin, the admins of the project, and Pagure site 

63 # admins to modify projects' monitoring, even if the user has the right 

64 # ACLs on their token 

65 if ( 

66 flask.g.fas_user.username not in admins 

67 and flask.g.fas_user.username != repo.user.username 

68 and not is_site_admin 

69 ): 

70 raise pagure.exceptions.APIError( 

71 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED 

72 ) 

73 

74 form = pagure_distgit.forms.AnityaForm(csrf_enabled=False) 

75 if form.validate_on_submit(): 

76 try: 

77 if repo.anitya: 

78 repo.anitya.anitya_status = form.anitya_status.data 

79 flask.g.session.add(repo) 

80 else: 

81 mapping = model.PagureAnitya( 

82 project_id=repo.id, anitya_status=form.anitya_status.data 

83 ) 

84 flask.g.session.add(mapping) 

85 flask.g.session.commit() 

86 except SQLAlchemyError as err: # pragma: no cover 

87 flask.g.session.rollback() 

88 _log.exception(err) 

89 raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) 

90 else: 

91 raise pagure.exceptions.APIError( 

92 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors 

93 ) 

94 

95 return anitya_get_endpoint(namespace, repo.name) 

96 

97 

98@DISTGIT_NS.route("/actived/<namespace>/<repo>", methods=["GET"]) 

99@api_method 

100def get_actived_status(namespace, repo): 

101 """Retrieves the active status of the specified package.""" 

102 _log.info("Received a request to unorphan: %s/%s", namespace, repo) 

103 

104 repo = _get_repo(repo, namespace=namespace) 

105 

106 if repo.user.user != "orphan": 

107 raise pagure.exceptions.APIError( 

108 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED 

109 ) 

110 

111 # Check if the project is retired in PDC 

112 active = _is_active_in_pdc(repo.name, repo.namespace) 

113 

114 output = {"active": active} 

115 return flask.jsonify(output) 

116 

117 

118def _is_active_in_pdc(name, namespace): 

119 """Queries PDC and return whether the project is active on the master 

120 branch in PDC or not. 

121 """ 

122 pdc_url = flask.current_app.config.get("PDC_URL") 

123 if not pdc_url: 

124 raise pagure.exceptions.APIError( 

125 500, 

126 error_code=APIERROR.ENOCODE, 

127 error="This pagure instance has no PDC_URL configured, please " 

128 "inform your pagure administrators", 

129 ) 

130 else: 

131 pdc_url = "%s/component-branches/" % pdc_url.rstrip("/") 

132 

133 _log.debug("Based PDC url: %s", pdc_url) 

134 

135 to_pdc_namespace = { 

136 "rpms": "rpm", 

137 "modules": "module", 

138 "container": "container", 

139 "flatpaks": "flatpak" 

140 } 

141 to_pdc_namespace = ( 

142 flask.current_app.config.get("PDC_NAMESPACES") or to_pdc_namespace 

143 ) 

144 

145 try: 

146 pdc_namespace = to_pdc_namespace[namespace] 

147 except Exception: 

148 raise pagure.exceptions.APIError( 

149 500, 

150 error_code=APIERROR.ENOCODE, 

151 error="Namespace: %s could not be converted to a PDC namespace" 

152 % namespace, 

153 ) 

154 

155 branch = "master" 

156 if namespace in ["rpms", "container"]: 

157 branch = "rawhide" 

158 

159 url = "%s?global_component=%s&name=%s&type=%s" % ( 

160 pdc_url, 

161 name, 

162 branch, 

163 pdc_namespace, 

164 ) 

165 

166 _log.info("Querying PDC at: %s", url) 

167 try: 

168 req = requests.get(url, timeout=(30, 30)) 

169 except requests.RequestException as err: 

170 raise pagure.exceptions.APIError( 

171 500, 

172 error_code=APIERROR.ENOCODE, 

173 error="An error occured while querying pdc: %s" % err, 

174 ) 

175 

176 try: 

177 data = req.json() 

178 except Exception: 

179 raise pagure.exceptions.APIError( 

180 500, 

181 error_code=APIERROR.ENOCODE, 

182 error="The output of %s could not be converted to JSON" % req.url, 

183 ) 

184 

185 _log.info( 

186 "%s/%s is active: %s", namespace, name, data["results"][0]["active"] 

187 ) 

188 return data["results"][0]["active"] is True 

189 

190 

191@DISTGIT_NS.route("/orphan/<namespace>/<repo>", methods=["GET"]) 

192@api_method 

193def orphan_get_endpoint(namespace, repo): 

194 """ 

195 Orphan state 

196 ------------ 

197 Returns the current orphan state of package with reason. 

198 

199 :: 

200 

201 GET /_dg/orphan/<namespace>/<repo> 

202 

203 Sample response 

204 ^^^^^^^^^^^^^^^ 

205 

206 :: 

207 

208 { 

209 "orphan": true, 

210 "reason": "Lack of Time", 

211 "reason_info": "Personal issues" 

212 } 

213 """ 

214 repo = _get_repo(repo, namespace=namespace) 

215 

216 output = {"orphan": False, "reason": "", "reason_info": ""} 

217 

218 if repo.user.user == "orphan": 

219 output["orphan"] = True 

220 if repo.orphan_reason: 

221 output["reason"] = repo.orphan_reason.reason 

222 output["reason_info"] = repo.orphan_reason.reason_info 

223 return flask.jsonify(output) 

224 

225 

226@DISTGIT_NS.route("/orphan/<namespace>/<repo>", methods=["POST"]) 

227@api_login_required(acls=["modify_project"]) 

228@api_method 

229def orphan_endpoint(namespace, repo): 

230 """ 

231 Orphan 

232 ------ 

233 Orphan the package. 

234 

235 :: 

236 

237 POST /_dg/orphan/<namespace>/<repo> 

238 

239 Input 

240 ^^^^^ 

241 

242 +-----------------------+---------+--------------+------------------------+ 

243 | Key | Type | Optionality | Description | 

244 +=======================+=========+==============+========================+ 

245 | ``orphan_reason`` | string | Optional | | The reason to orphan | 

246 | | | | the package. | 

247 +-----------------------+---------+--------------+------------------------+ 

248 | ``orphan_reason_info``| string | Optional | | Additional info for | 

249 | | | | provided reason. | 

250 +-----------------------+---------+--------------+------------------------+ 

251 

252 Sample response 

253 ^^^^^^^^^^^^^^^ 

254 

255 :: 

256 

257 { 

258 "orphan": true, 

259 "reason": "Other", 

260 "reason_info": "Voices in my head tell me to stop" 

261 } 

262 """ 

263 _log.info("Received a request to orphan: %s/%s", namespace, repo) 

264 

265 repo = _get_repo(repo, namespace=namespace) 

266 _check_token(repo, project_token=False) 

267 

268 user_obj = pagure.lib.query.get_user( 

269 flask.g.session, flask.g.fas_user.username 

270 ) 

271 if not user_obj: 

272 raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOUSER) 

273 

274 if repo.user.user == "orphan": 

275 raise pagure.exceptions.APIError( 

276 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED 

277 ) 

278 

279 try: 

280 orphan_user_obj = pagure.lib.query.get_user(flask.g.session, "orphan") 

281 except PagureException: 

282 _log.exception("Error when retrieving orphan user") 

283 raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) 

284 

285 form = pagure_distgit.forms.OrphanReasonForm(csrf_enabled=False) 

286 if form.validate_on_submit(): 

287 try: 

288 reason = model.PagureOrphanReason( 

289 project_id=repo.id, 

290 reason=form.orphan_reason.data, 

291 reason_info=form.orphan_reason_info.data, 

292 ) 

293 flask.g.session.add(reason) 

294 pagure.lib.query.set_project_owner( 

295 flask.g.session, repo, orphan_user_obj 

296 ) 

297 if user_obj in repo.users: 

298 pagure.lib.query.remove_user_of_project( 

299 flask.g.session, user_obj, repo, user_obj.user 

300 ) 

301 pagure.lib.query.update_watch_status( 

302 flask.g.session, repo, user_obj.username, "-1" 

303 ) 

304 flask.g.session.commit() 

305 except SQLAlchemyError: # pragma: no cover 

306 flask.g.session.rollback() 

307 _log.exception("Error when orphaning project") 

308 raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) 

309 else: 

310 raise pagure.exceptions.APIError( 

311 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors 

312 ) 

313 

314 pagure.lib.notify.log( 

315 repo, 

316 topic="project.orphan", 

317 msg={ 

318 "project": repo.to_json(public=True), 

319 "agent": user_obj.username, 

320 "reason": repo.orphan_reason.reason, 

321 "reason_info": repo.orphan_reason.reason_info, 

322 }, 

323 ) 

324 

325 return orphan_get_endpoint(namespace, repo.name) 

326 

327 

328@DISTGIT_NS.route("/take_orphan/<namespace>/<repo>", methods=["POST"]) 

329@api_login_required(acls=["modify_project"]) 

330@api_method 

331def take_orphan_endpoint(namespace, repo): 

332 """Updates the current point of contact of orphan packages.""" 

333 _log.info("Received a request to unorphan: %s/%s", namespace, repo) 

334 

335 repo = _get_repo(repo, namespace=namespace) 

336 _check_token(repo, project_token=False) 

337 

338 user_obj = pagure.lib.query.get_user( 

339 flask.g.session, flask.g.fas_user.username 

340 ) 

341 if not user_obj: 

342 raise pagure.exceptions.APIError(404, error_code=APIERROR.ENOUSER) 

343 

344 if repo.user.user != "orphan": 

345 raise pagure.exceptions.APIError( 

346 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED 

347 ) 

348 

349 user_grps = set(user_obj.groups) 

350 req_grps = set(["packager"]) 

351 if not user_grps.intersection(req_grps): 

352 raise pagure.exceptions.APIError( 

353 403, 

354 error_code=APIERROR.ENOTHIGHENOUGH, 

355 errors="You must be a packager to adopt a package.", 

356 ) 

357 

358 # Check if the project is retired in PDC 

359 if not _is_active_in_pdc(repo.name, repo.namespace): 

360 raise pagure.exceptions.APIError( 

361 400, 

362 error_code=APIERROR.EINVALIDREQ, 

363 errors="This project has been retired and cannot be unorphaned " 

364 "here, please open a releng ticket for it.", 

365 ) 

366 

367 try: 

368 repo.user = user_obj 

369 if repo.orphan_reason: 

370 reason = repo.orphan_reason 

371 flask.g.session.delete(reason) 

372 flask.g.session.add(repo) 

373 flask.g.session.commit() 

374 except SQLAlchemyError as err: # pragma: no cover 

375 flask.g.session.rollback() 

376 _log.exception(err) 

377 raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) 

378 

379 pagure.lib.notify.log( 

380 repo, 

381 topic="project.adopt", 

382 msg=dict(project=repo.to_json(public=True), agent=user_obj.username), 

383 ) 

384 

385 output = {"point_of_contact": repo.user.user} 

386 

387 return flask.jsonify(output) 

388 

389 

390@DISTGIT_NS.route("/bodhi_updates/<namespace>/<repo>", methods=["GET"]) 

391@api_method 

392def bodhi_updates_endpoint(namespace, repo): 

393 """Retrieves the current updates in bodhi for the specified package.""" 

394 if namespace not in ["rpms"]: 

395 raise pagure.exceptions.APIError( 

396 400, 

397 error_code=APIERROR.EINVALIDREQ, 

398 errors=["Namespace not supported"], 

399 ) 

400 

401 _log.info( 

402 "Received request for the bodhi updates of: %s/%s", namespace, repo 

403 ) 

404 

405 repo = _get_repo(repo, namespace=namespace) 

406 html = pagure.utils.is_true(flask.request.args.get("html", False)) 

407 

408 bodhi_base_url = "https://bodhi.fedoraproject.org/" 

409 

410 # Retrieves the active releases from bodhi 

411 releases_url = "%s/releases/" % bodhi_base_url 

412 releases = {} 

413 page = 1 

414 pages = 1 

415 while True: 

416 url = "%s?page=%s" % (releases_url, page) 

417 _log.info("Calling bodhi at: %s", url) 

418 req = requests.get(url) 

419 if not req.ok: 

420 raise pagure.exceptions.APIError( 

421 400, 

422 error_code=APIERROR.EINVALIDREQ, 

423 errors=["Could not call bodhi"], 

424 ) 

425 

426 data = req.json() 

427 for release in data.get("releases", []): 

428 if any( 

429 word in release["long_name"] 

430 for word in ["Flatpaks", "Containers", "Modular"] 

431 ): 

432 continue 

433 if release["state"] in ["pending", "current"]: 

434 releases[release["name"]] = release["long_name"] 

435 

436 pages = data["pages"] 

437 page = data["page"] 

438 if page == pages: 

439 break 

440 page += 1 

441 

442 # Set Rawhide as the highest-sorted Fedora release 

443 rawhide_number = 0 

444 for release in releases: 

445 # Skip EL / EPEL / ELN releases 

446 if not release["name"].startswith("F"): 

447 continue 

448 # Skip Container, Flatpak, and Module releases 

449 try: 

450 base_release = int(release["name"].lstrip("F").rstrip("CFM")) 

451 except ValueError: 

452 continue 

453 if base_release > rawhide_number: 

454 rawhide_number = base_release 

455 rawhide = "F" + str(rawhide_number) 

456 

457 # Retrieves the updates of that package in bodhi 

458 update_url = "%s/updates?packages=%s" % ( 

459 bodhi_base_url.rstrip("/"), 

460 repo.name, 

461 ) 

462 updates = collections.defaultdict(dict) 

463 

464 _log.info("Calling bodhi at: %s", update_url) 

465 req = requests.get(update_url) 

466 if not req.ok: 

467 raise pagure.exceptions.APIError( 

468 400, 

469 error_code=APIERROR.EINVALIDREQ, 

470 errors=["Could not call bodhi"], 

471 ) 

472 

473 data = req.json() 

474 for update in data["updates"]: 

475 if update["release"]["name"] not in releases: 

476 continue 

477 name = update["title"] 

478 for build in update["builds"]: 

479 if repo.name in build["nvr"]: 

480 name = build["nvr"] 

481 break 

482 

483 if not updates[update["release"]["name"]].get(update["status"]): 

484 updates[update["release"]["name"]][update["status"]] = name 

485 

486 if html: 

487 html_output = ( 

488 '<table class="table table-bordered table-striped">\n' 

489 '<tr><th scope="col">Release</th><th scope="col">Stable version' 

490 '</th><th scope="col">Version in testing</th></tr>\n' 

491 ) 

492 for release in sorted(releases, reverse=True): 

493 row = ( 

494 "<tr><td scope='row' class='text-align-left'>{release}</td>" 

495 "<td>{stable}</td><td>{testing}</td></tr>" 

496 ) 

497 stable = "" 

498 if updates.get(release, {}).get("stable"): 

499 stable = ( 

500 "<a href='https://koji.fedoraproject.org/koji/search?" 

501 "terms={0}&type=build&match=glob'>{0}</a>".format( 

502 updates[release].get("stable") 

503 ) 

504 ) 

505 else: 

506 # This makes things a little slower but is needed for builds 

507 # that do not go through bodhi, for example, via releng-managed 

508 # side tags (like for Fedora mass rebuilds), which bypass bodhi. 

509 if release == rawhide: 

510 mdapi_release = "rawhide" 

511 else: 

512 mdapi_release = release.lower() 

513 mdapi_url = "https://mdapi.fedoraproject.org/%s/srcpkg/%s" % ( 

514 mdapi_release, 

515 repo.name, 

516 ) 

517 _log.info("Calling mdapi at: %s", mdapi_url) 

518 req = requests.get(mdapi_url) 

519 if req.ok: 

520 _log.info("mdapi returned ok") 

521 data = req.json() 

522 build = "%s-%s-%s" % ( 

523 data["basename"], 

524 data["version"], 

525 data["release"], 

526 ) 

527 stable = ( 

528 "<a href='https://koji.fedoraproject.org/koji/search?" 

529 "terms={0}&type=build&match=glob'>{0}</a>".format( 

530 build 

531 ) 

532 ) 

533 testing = "" 

534 if updates.get(release, {}).get("testing"): 

535 testing = ( 

536 "<a href='https://koji.fedoraproject.org/koji/search?" 

537 "terms={0}&type=build&match=glob'>{0}</a>".format( 

538 updates[release].get("testing") 

539 ) 

540 ) 

541 row = row.format( 

542 release=releases[release], stable=stable, testing=testing 

543 ) 

544 html_output += row 

545 html_output += "</table>" 

546 return html_output 

547 else: 

548 return flask.jsonify({"releases": releases, "updates": updates}) 

549 

550 

551@DISTGIT_NS.route("/bzoverrides/<namespace>/<repo>", methods=["GET"]) 

552@api_method 

553def bzoverride_get_endpoint(repo, namespace): 

554 """Returns the current default assignee(s) of this package. 

555 Defaults to the repo user if unset. 

556 """ 

557 repo = _get_repo(repo, namespace=namespace) 

558 output = { 

559 "fedora_assignee": repo.user.username, 

560 "epel_assignee": repo.user.username, 

561 } 

562 if repo.bzoverride: 

563 if repo.bzoverride.fedora_assignee: 

564 output["fedora_assignee"] = repo.bzoverride.fedora_assignee 

565 if repo.bzoverride.epel_assignee: 

566 output["epel_assignee"] = repo.bzoverride.epel_assignee 

567 return flask.jsonify(output) 

568 

569 

570@DISTGIT_NS.route("/bzoverrides/<namespace>/<repo>", methods=["POST"]) 

571@api_method 

572@api_login_required(acls=["modify_project"]) 

573def bzoverride_patch_endpoint(repo, namespace): 

574 """Updates the default assignees of this package.""" 

575 

576 def _validate_input(inputname): 

577 """ Validate if the input is either an username or a group name. """ 

578 valid = False 

579 if inputname.startswith("@"): 

580 group = pagure.lib.query.search_groups( 

581 flask.g.session, group_name=inputname[1:] 

582 ) 

583 if group: 

584 valid = True 

585 

586 else: 

587 user_obj = pagure.lib.query.search_user( 

588 flask.g.session, username=inputname 

589 ) 

590 if user_obj: 

591 valid = True 

592 

593 return valid 

594 

595 repo = _get_repo(repo, namespace=namespace) 

596 

597 is_site_admin = pagure.utils.is_admin() 

598 # Only allow the main admin and Pagure site admins to modify projects' 

599 # monitoring, even if the user has the right ACLs on their token 

600 if flask.g.fas_user.username != repo.user.username and not is_site_admin: 

601 raise pagure.exceptions.APIError( 

602 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED 

603 ) 

604 

605 form = pagure_distgit.forms.BZOverrideForm(csrf_enabled=False) 

606 if form.validate_on_submit(): 

607 fedora_assignee = None 

608 if form.fedora_assignee.data: 

609 fedora_assignee = form.fedora_assignee.data.strip() or None 

610 if fedora_assignee and not _validate_input(fedora_assignee): 

611 raise pagure.exceptions.APIError( 

612 400, 

613 error_code=APIERROR.EINVALIDREQ, 

614 errors=["Invalid user or group name as fedora_assignee"], 

615 ) 

616 

617 epel_assignee = None 

618 if form.epel_assignee.data: 

619 epel_assignee = form.epel_assignee.data.strip() or None 

620 if epel_assignee and not _validate_input(epel_assignee): 

621 raise pagure.exceptions.APIError( 

622 400, 

623 error_code=APIERROR.EINVALIDREQ, 

624 errors=["Invalid user or group name as epel_assignee"], 

625 ) 

626 

627 try: 

628 if repo.bzoverride: 

629 if fedora_assignee is None and epel_assignee is None: 

630 flask.g.session.delete(repo.bzoverride) 

631 else: 

632 repo.bzoverride.fedora_assignee = fedora_assignee 

633 repo.bzoverride.epel_assignee = epel_assignee 

634 flask.g.session.add(repo) 

635 elif fedora_assignee is not None or epel_assignee is not None: 

636 mapping = model.PagureBZOverride( 

637 project_id=repo.id, 

638 epel_assignee=epel_assignee, 

639 fedora_assignee=fedora_assignee, 

640 ) 

641 flask.g.session.add(mapping) 

642 flask.g.session.commit() 

643 except SQLAlchemyError as err: # pragma: no cover 

644 flask.g.session.rollback() 

645 _log.exception(err) 

646 raise pagure.exceptions.APIError(400, error_code=APIERROR.EDBERROR) 

647 else: 

648 raise pagure.exceptions.APIError( 

649 400, error_code=APIERROR.EINVALIDREQ, errors=form.errors 

650 ) 

651 

652 return bzoverride_get_endpoint(repo=repo.name, namespace=namespace)