1 import base64
2 import datetime
3 from functools import wraps
4 import json
5 import os
6 import flask
7 import sqlalchemy
8
9 from werkzeug import secure_filename
10
11 from coprs import db
12 from coprs import exceptions
13 from coprs import forms
14 from coprs import helpers
15 from coprs import models
16 from coprs.helpers import fix_protocol_for_backend, generate_build_config
17 from coprs.logic.api_logic import MonitorWrapper
18 from coprs.logic.builds_logic import BuildsLogic
19 from coprs.logic.complex_logic import ComplexLogic
20 from coprs.logic.users_logic import UsersLogic
21 from coprs.logic.packages_logic import PackagesLogic
22 from coprs.logic.modules_logic import ModulesLogic
23
24 from coprs.views.misc import login_required, api_login_required
25
26 from coprs.views.api_ns import api_ns
27
28 from coprs.logic import builds_logic
29 from coprs.logic import coprs_logic
30 from coprs.logic.coprs_logic import CoprsLogic
31 from coprs.logic.actions_logic import ActionsLogic
32
33 from coprs.exceptions import (ActionInProgressException,
34 InsufficientRightsException,
35 DuplicateException,
36 LegacyApiError,
37 UnknownSourceTypeException)
50 return wrapper
51
55 """
56 Render the home page of the api.
57 This page provides information on how to call/use the API.
58 """
59
60 return flask.render_template("api.html")
61
62
63 @api_ns.route("/new/", methods=["GET", "POST"])
84
85
86 @api_ns.route("/coprs/<username>/new/", methods=["POST"])
89 """
90 Receive information from the user on how to create its new copr,
91 check their validity and create the corresponding copr.
92
93 :arg name: the name of the copr to add
94 :arg chroots: a comma separated list of chroots to use
95 :kwarg repos: a comma separated list of repository that this copr
96 can use.
97 :kwarg initial_pkgs: a comma separated list of initial packages to
98 build in this new copr
99
100 """
101
102 form = forms.CoprFormFactory.create_form_cls()(csrf_enabled=False)
103 infos = []
104
105
106
107 for post_key in flask.request.form.keys():
108 if post_key not in form.__dict__.keys():
109 infos.append("Unknown key '{key}' received.".format(key=post_key))
110
111 if form.validate_on_submit():
112 group = ComplexLogic.get_group_by_name_safe(username[1:]) if username[0] == "@" else None
113
114 auto_prune = True
115 if "auto_prune" in flask.request.form:
116 auto_prune = form.auto_prune.data
117
118 try:
119 copr = CoprsLogic.add(
120 name=form.name.data.strip(),
121 repos=" ".join(form.repos.data.split()),
122 user=flask.g.user,
123 selected_chroots=form.selected_chroots,
124 description=form.description.data,
125 instructions=form.instructions.data,
126 check_for_duplicates=True,
127 disable_createrepo=form.disable_createrepo.data,
128 unlisted_on_hp=form.unlisted_on_hp.data,
129 build_enable_net=form.build_enable_net.data,
130 group=group,
131 persistent=form.persistent.data,
132 auto_prune=auto_prune,
133 )
134 infos.append("New project was successfully created.")
135
136 if form.initial_pkgs.data:
137 pkgs = form.initial_pkgs.data.split()
138 for pkg in pkgs:
139 builds_logic.BuildsLogic.add(
140 user=flask.g.user,
141 pkgs=pkg,
142 copr=copr)
143
144 infos.append("Initial packages were successfully "
145 "submitted for building.")
146
147 output = {"output": "ok", "message": "\n".join(infos)}
148 db.session.commit()
149 except (exceptions.DuplicateException,
150 exceptions.NonAdminCannotCreatePersistentProject,
151 exceptions.NonAdminCannotDisableAutoPrunning) as err:
152 db.session.rollback()
153 raise LegacyApiError(str(err))
154
155 else:
156 errormsg = "Validation error\n"
157 if form.errors:
158 for field, emsgs in form.errors.items():
159 errormsg += "- {0}: {1}\n".format(field, "\n".join(emsgs))
160
161 errormsg = errormsg.replace('"', "'")
162 raise LegacyApiError(errormsg)
163
164 return flask.jsonify(output)
165
166
167 @api_ns.route("/coprs/<username>/<coprname>/delete/", methods=["POST"])
192
193
194 @api_ns.route("/coprs/<username>/<coprname>/fork/", methods=["POST"])
195 @api_login_required
196 @api_req_with_copr
197 -def api_copr_fork(copr):
198 """ Fork the project and builds in it
199 """
200 form = forms.CoprForkFormFactory\
201 .create_form_cls(copr=copr, user=flask.g.user, groups=flask.g.user.user_groups)(csrf_enabled=False)
202
203 if form.validate_on_submit() and copr:
204 try:
205 dstgroup = ([g for g in flask.g.user.user_groups if g.at_name == form.owner.data] or [None])[0]
206 if flask.g.user.name != form.owner.data and not dstgroup:
207 return LegacyApiError("There is no such group: {}".format(form.owner.data))
208
209 fcopr, created = ComplexLogic.fork_copr(copr, flask.g.user, dstname=form.name.data, dstgroup=dstgroup)
210 if created:
211 msg = ("Forking project {} for you into {}.\nPlease be aware that it may take a few minutes "
212 "to duplicate a backend data.".format(copr.full_name, fcopr.full_name))
213 elif not created and form.confirm.data == True:
214 msg = ("Updating packages in {} from {}.\nPlease be aware that it may take a few minutes "
215 "to duplicate a backend data.".format(copr.full_name, fcopr.full_name))
216 else:
217 raise LegacyApiError("You are about to fork into existing project: {}\n"
218 "Please use --confirm if you really want to do this".format(fcopr.full_name))
219
220 output = {"output": "ok", "message": msg}
221 db.session.commit()
222
223 except (exceptions.ActionInProgressException,
224 exceptions.InsufficientRightsException) as err:
225 db.session.rollback()
226 raise LegacyApiError(str(err))
227 else:
228 raise LegacyApiError("Invalid request: {0}".format(form.errors))
229
230 return flask.jsonify(output)
231
232
233 @api_ns.route("/coprs/")
234 @api_ns.route("/coprs/<username>/")
235 -def api_coprs_by_owner(username=None):
236 """ Return the list of coprs owned by the given user.
237 username is taken either from GET params or from the URL itself
238 (in this order).
239
240 :arg username: the username of the person one would like to the
241 coprs of.
242
243 """
244 username = flask.request.args.get("username", None) or username
245 if username is None:
246 raise LegacyApiError("Invalid request: missing `username` ")
247
248 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
249
250 if username.startswith("@"):
251 group_name = username[1:]
252 query = CoprsLogic.get_multiple()
253 query = CoprsLogic.filter_by_group_name(query, group_name)
254 else:
255 query = CoprsLogic.get_multiple_owned_by_username(username)
256
257 query = CoprsLogic.join_builds(query)
258 query = CoprsLogic.set_query_order(query)
259
260 repos = query.all()
261 output = {"output": "ok", "repos": []}
262 for repo in repos:
263 yum_repos = {}
264 for build in repo.builds:
265 if build.results:
266 for chroot in repo.active_chroots:
267 release = release_tmpl.format(chroot=chroot)
268 yum_repos[release] = fix_protocol_for_backend(
269 os.path.join(build.results, release + '/'))
270 break
271
272 output["repos"].append({"name": repo.name,
273 "additional_repos": repo.repos,
274 "yum_repos": yum_repos,
275 "description": repo.description,
276 "instructions": repo.instructions,
277 "persistent": repo.persistent,
278 "unlisted_on_hp": repo.unlisted_on_hp,
279 "auto_prune": repo.auto_prune,
280 })
281
282 return flask.jsonify(output)
283
288 """ Return detail of one project.
289
290 :arg username: the username of the person one would like to the
291 coprs of.
292 :arg coprname: the name of project.
293
294 """
295 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
296 output = {"output": "ok", "detail": {}}
297 yum_repos = {}
298
299 build = models.Build.query.filter(
300 models.Build.copr_id == copr.id, models.Build.results != None).first()
301
302 if build:
303 for chroot in copr.active_chroots:
304 release = release_tmpl.format(chroot=chroot)
305 yum_repos[release] = fix_protocol_for_backend(
306 os.path.join(build.results, release + '/'))
307
308 output["detail"] = {
309 "name": copr.name,
310 "additional_repos": copr.repos,
311 "yum_repos": yum_repos,
312 "description": copr.description,
313 "instructions": copr.instructions,
314 "last_modified": builds_logic.BuildsLogic.last_modified(copr),
315 "auto_createrepo": copr.auto_createrepo,
316 "persistent": copr.persistent,
317 "unlisted_on_hp": copr.unlisted_on_hp,
318 "auto_prune": copr.auto_prune,
319 }
320 return flask.jsonify(output)
321
322
323 @api_ns.route("/coprs/<username>/<coprname>/new_build/", methods=["POST"])
324 @api_login_required
325 @api_req_with_copr
326 -def copr_new_build(copr):
338 return process_creating_new_build(copr, form, create_new_build)
339
340
341 @api_ns.route("/coprs/<username>/<coprname>/new_build_upload/", methods=["POST"])
355 return process_creating_new_build(copr, form, create_new_build)
356
357
358 @api_ns.route("/coprs/<username>/<coprname>/new_build_pypi/", methods=["POST"])
378 return process_creating_new_build(copr, form, create_new_build)
379
380
381 @api_ns.route("/coprs/<username>/<coprname>/new_build_tito/", methods=["POST"])
398 return process_creating_new_build(copr, form, create_new_build)
399
400
401 @api_ns.route("/coprs/<username>/<coprname>/new_build_mock/", methods=["POST"])
418 return process_creating_new_build(copr, form, create_new_build)
419
420
421 @api_ns.route("/coprs/<username>/<coprname>/new_build_rubygems/", methods=["POST"])
435 return process_creating_new_build(copr, form, create_new_build)
436
437
438 @api_ns.route("/coprs/<username>/<coprname>/new_build_distgit/", methods=["POST"])
453 return process_creating_new_build(copr, form, create_new_build)
454
457 infos = []
458
459
460 for post_key in flask.request.form.keys():
461 if post_key not in form.__dict__.keys():
462 infos.append("Unknown key '{key}' received.".format(key=post_key))
463
464 if not form.validate_on_submit():
465 raise LegacyApiError("Invalid request: bad request parameters: {0}".format(form.errors))
466
467 if not flask.g.user.can_build_in(copr):
468 raise LegacyApiError("Invalid request: user {} is not allowed to build in the copr: {}"
469 .format(flask.g.user.username, copr.full_name))
470
471
472 try:
473
474
475 build = create_new_build()
476 db.session.commit()
477 ids = [build.id] if type(build) != list else [b.id for b in build]
478 infos.append("Build was added to {0}:".format(copr.name))
479 for build_id in ids:
480 infos.append(" " + flask.url_for("coprs_ns.copr_build_redirect",
481 build_id=build_id,
482 _external=True))
483
484 except (ActionInProgressException, InsufficientRightsException) as e:
485 raise LegacyApiError("Invalid request: {}".format(e))
486
487 output = {"output": "ok",
488 "ids": ids,
489 "message": "\n".join(infos)}
490
491 return flask.jsonify(output)
492
493
494 @api_ns.route("/coprs/build_status/<int:build_id>/", methods=["GET"])
501
502
503 @api_ns.route("/coprs/build_detail/<int:build_id>/", methods=["GET"])
504 @api_ns.route("/coprs/build/<int:build_id>/", methods=["GET"])
506 build = ComplexLogic.get_build_safe(build_id)
507
508 chroots = {}
509 results_by_chroot = {}
510 for chroot in build.build_chroots:
511 chroots[chroot.name] = chroot.state
512 results_by_chroot[chroot.name] = chroot.result_dir_url
513
514 built_packages = None
515 if build.built_packages:
516 built_packages = build.built_packages.split("\n")
517
518 output = {
519 "output": "ok",
520 "status": build.state,
521 "project": build.copr.name,
522 "owner": build.copr.owner_name,
523 "results": build.results,
524 "built_pkgs": built_packages,
525 "src_version": build.pkg_version,
526 "chroots": chroots,
527 "submitted_on": build.submitted_on,
528 "started_on": build.min_started_on,
529 "ended_on": build.max_ended_on,
530 "src_pkg": build.pkgs,
531 "submitted_by": build.user.name,
532 "results_by_chroot": results_by_chroot
533 }
534 return flask.jsonify(output)
535
536
537 @api_ns.route("/coprs/cancel_build/<int:build_id>/", methods=["POST"])
550
551
552 @api_ns.route("/coprs/delete_build/<int:build_id>/", methods=["POST"])
565
566
567 @api_ns.route('/coprs/<username>/<coprname>/modify/', methods=["POST"])
568 @api_login_required
569 @api_req_with_copr
570 -def copr_modify(copr):
613
614
615 @api_ns.route('/coprs/<username>/<coprname>/modify/<chrootname>/', methods=["POST"])
632
633
634 @api_ns.route('/coprs/<username>/<coprname>/chroot/edit/<chrootname>/', methods=["POST"])
635 @api_login_required
636 @api_req_with_copr
637 -def copr_edit_chroot(copr, chrootname):
638 form = forms.ModifyChrootForm(csrf_enabled=False)
639 chroot = ComplexLogic.get_copr_chroot_safe(copr, chrootname)
640
641 if not form.validate_on_submit():
642 raise LegacyApiError("Invalid request: {0}".format(form.errors))
643 else:
644 buildroot_pkgs = repos = comps_xml = comps_name = None
645 if "buildroot_pkgs" in flask.request.form:
646 buildroot_pkgs = form.buildroot_pkgs.data
647 if "repos" in flask.request.form:
648 repos = form.repos.data
649 if form.upload_comps.has_file():
650 comps_xml = form.upload_comps.data.stream.read()
651 comps_name = form.upload_comps.data.filename
652 if form.delete_comps.data:
653 coprs_logic.CoprChrootsLogic.remove_comps(flask.g.user, chroot)
654 coprs_logic.CoprChrootsLogic.update_chroot(
655 flask.g.user, chroot, buildroot_pkgs, repos, comps=comps_xml, comps_name=comps_name)
656 db.session.commit()
657
658 output = {
659 "output": "ok",
660 "message": "Edit chroot operation was successful.",
661 "chroot": chroot.to_dict(),
662 }
663 return flask.jsonify(output)
664
665
666 @api_ns.route('/coprs/<username>/<coprname>/detail/<chrootname>/', methods=["GET"])
673
674 @api_ns.route('/coprs/<username>/<coprname>/chroot/get/<chrootname>/', methods=["GET"])
680
684 """ Return the list of coprs found in search by the given text.
685 project is taken either from GET params or from the URL itself
686 (in this order).
687
688 :arg project: the text one would like find for coprs.
689
690 """
691 project = flask.request.args.get("project", None) or project
692 if not project:
693 raise LegacyApiError("No project found.")
694
695 try:
696 query = CoprsLogic.get_multiple_fulltext(project)
697
698 repos = query.all()
699 output = {"output": "ok", "repos": []}
700 for repo in repos:
701 output["repos"].append({"username": repo.user.name,
702 "coprname": repo.name,
703 "description": repo.description})
704 except ValueError as e:
705 raise LegacyApiError("Server error: {}".format(e))
706
707 return flask.jsonify(output)
708
712 """ Return list of coprs which are part of playground """
713 query = CoprsLogic.get_playground()
714 repos = query.all()
715 output = {"output": "ok", "repos": []}
716 for repo in repos:
717 output["repos"].append({"username": repo.owner_name,
718 "coprname": repo.name,
719 "chroots": [chroot.name for chroot in repo.active_chroots]})
720
721 jsonout = flask.jsonify(output)
722 jsonout.status_code = 200
723 return jsonout
724
725
726 @api_ns.route("/coprs/<username>/<coprname>/monitor/", methods=["GET"])
727 @api_req_with_copr
728 -def monitor(copr):
732
733
734
735 @api_ns.route("/coprs/<username>/<coprname>/package/add/<source_type_text>/", methods=["POST"])
736 @api_login_required
737 @api_req_with_copr
738 -def copr_add_package(copr, source_type_text):
740
741
742 @api_ns.route("/coprs/<username>/<coprname>/package/<package_name>/edit/<source_type_text>/", methods=["POST"])
743 @api_login_required
744 @api_req_with_copr
745 -def copr_edit_package(copr, package_name, source_type_text):
751
782
785 params = {}
786 if flask.request.args.get('with_latest_build'):
787 params['with_latest_build'] = True
788 if flask.request.args.get('with_latest_succeeded_build'):
789 params['with_latest_succeeded_build'] = True
790 if flask.request.args.get('with_all_builds'):
791 params['with_all_builds'] = True
792 return params
793
796 """
797 A lagging generator to stream JSON so we don't have to hold everything in memory
798 This is a little tricky, as we need to omit the last comma to make valid JSON,
799 thus we use a lagging generator, similar to http://stackoverflow.com/questions/1630320/
800 """
801 packages = query.__iter__()
802 try:
803 prev_package = next(packages)
804 except StopIteration:
805
806 yield '{"packages": []}'
807 raise StopIteration
808
809 yield '{"packages": ['
810
811 for package in packages:
812 yield json.dumps(prev_package.to_dict(**params)) + ', '
813 prev_package = package
814
815 yield json.dumps(prev_package.to_dict(**params)) + ']}'
816
817
818 @api_ns.route("/coprs/<username>/<coprname>/package/list/", methods=["GET"])
824
825
826
827 @api_ns.route("/coprs/<username>/<coprname>/package/get/<package_name>/", methods=["GET"])
837
838
839 @api_ns.route("/coprs/<username>/<coprname>/package/delete/<package_name>/", methods=["POST"])
859
860
861 @api_ns.route("/coprs/<username>/<coprname>/package/reset/<package_name>/", methods=["POST"])
862 @api_login_required
863 @api_req_with_copr
864 -def copr_reset_package(copr, package_name):
881
882
883 @api_ns.route("/coprs/<username>/<coprname>/package/build/<package_name>/", methods=["POST"])
884 @api_login_required
885 @api_req_with_copr
886 -def copr_build_package(copr, package_name):
887 form = forms.BuildFormRebuildFactory.create_form_cls(copr.active_chroots)(csrf_enabled=False)
888
889 try:
890 package = PackagesLogic.get(copr.id, package_name)[0]
891 except IndexError:
892 raise LegacyApiError("No package with name {name} in copr {copr}".format(name=package_name, copr=copr.name))
893
894 if form.validate_on_submit():
895 try:
896 build = PackagesLogic.build_package(flask.g.user, copr, package, form.selected_chroots, **form.data)
897 db.session.commit()
898 except (InsufficientRightsException, ActionInProgressException, NoPackageSourceException) as e:
899 raise LegacyApiError(str(e))
900 else:
901 raise LegacyApiError(form.errors)
902
903 return flask.jsonify({
904 "output": "ok",
905 "ids": [build.id],
906 "message": "Build was added to {0}.".format(copr.name)
907 })
908
909
910 @api_ns.route("/coprs/<username>/<coprname>/module/build/", methods=["POST"])
935
936
937 @api_ns.route("/coprs/<username>/<coprname>/build-config/<chroot>/", methods=["GET"])
938 @api_ns.route("/g/<group_name>/<coprname>/build-config/<chroot>/", methods=["GET"])
941 """
942 Generate build configuration.
943 """
944 output = {
945 "output": "ok",
946 "build_config": generate_build_config(copr, chroot),
947 }
948
949 if not output['build_config']:
950 raise LegacyApiError('Chroot not found.')
951
952 return flask.jsonify(output)
953
954
955 @api_ns.route("/module/repo/", methods=["POST"])
968