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 -*-
3"""
4 (c) 2019 - Copyright Red Hat Inc
6 Authors:
7 Pierre-Yves Chibon <pingou@pingoured.fr>
8 Karsten Hopp <karsten@redhat.com>
10"""
12from __future__ import print_function, unicode_literals
14import collections
15import logging
17import flask
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
24import pagure_distgit.forms
25from pagure_distgit import model
27import requests
29from sqlalchemy.exc import SQLAlchemyError
32_log = logging.getLogger(__name__)
34DISTGIT_NS = flask.Blueprint(
35 "distgit_ns", __name__, url_prefix="/_dg", template_folder="templates"
36)
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)
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."""
57 repo = _get_repo(repo, namespace=namespace)
58 _check_token(repo, project_token=False)
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 )
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 )
95 return anitya_get_endpoint(namespace, repo.name)
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)
104 repo = _get_repo(repo, namespace=namespace)
106 if repo.user.user != "orphan":
107 raise pagure.exceptions.APIError(
108 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
109 )
111 # Check if the project is retired in PDC
112 active = _is_active_in_pdc(repo.name, repo.namespace)
114 output = {"active": active}
115 return flask.jsonify(output)
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("/")
133 _log.debug("Based PDC url: %s", pdc_url)
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 )
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 )
155 branch = "master"
156 if namespace in ["rpms", "container"]:
157 branch = "rawhide"
159 url = "%s?global_component=%s&name=%s&type=%s" % (
160 pdc_url,
161 name,
162 branch,
163 pdc_namespace,
164 )
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 )
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 )
185 _log.info(
186 "%s/%s is active: %s", namespace, name, data["results"][0]["active"]
187 )
188 return data["results"][0]["active"] is True
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.
199 ::
201 GET /_dg/orphan/<namespace>/<repo>
203 Sample response
204 ^^^^^^^^^^^^^^^
206 ::
208 {
209 "orphan": true,
210 "reason": "Lack of Time",
211 "reason_info": "Personal issues"
212 }
213 """
214 repo = _get_repo(repo, namespace=namespace)
216 output = {"orphan": False, "reason": "", "reason_info": ""}
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)
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.
235 ::
237 POST /_dg/orphan/<namespace>/<repo>
239 Input
240 ^^^^^
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 +-----------------------+---------+--------------+------------------------+
252 Sample response
253 ^^^^^^^^^^^^^^^
255 ::
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)
265 repo = _get_repo(repo, namespace=namespace)
266 _check_token(repo, project_token=False)
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)
274 if repo.user.user == "orphan":
275 raise pagure.exceptions.APIError(
276 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
277 )
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)
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 )
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 )
325 return orphan_get_endpoint(namespace, repo.name)
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)
335 repo = _get_repo(repo, namespace=namespace)
336 _check_token(repo, project_token=False)
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)
344 if repo.user.user != "orphan":
345 raise pagure.exceptions.APIError(
346 401, error_code=APIERROR.EMODIFYPROJECTNOTALLOWED
347 )
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 )
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 )
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)
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 )
385 output = {"point_of_contact": repo.user.user}
387 return flask.jsonify(output)
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 )
401 _log.info(
402 "Received request for the bodhi updates of: %s/%s", namespace, repo
403 )
405 repo = _get_repo(repo, namespace=namespace)
406 html = pagure.utils.is_true(flask.request.args.get("html", False))
408 bodhi_base_url = "https://bodhi.fedoraproject.org/"
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 )
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"]
436 pages = data["pages"]
437 page = data["page"]
438 if page == pages:
439 break
440 page += 1
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)
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)
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 )
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
483 if not updates[update["release"]["name"]].get(update["status"]):
484 updates[update["release"]["name"]][update["status"]] = name
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})
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)
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."""
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
586 else:
587 user_obj = pagure.lib.query.search_user(
588 flask.g.session, username=inputname
589 )
590 if user_obj:
591 valid = True
593 return valid
595 repo = _get_repo(repo, namespace=namespace)
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 )
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 )
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 )
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 )
652 return bzoverride_get_endpoint(repo=repo.name, namespace=namespace)