1 import base64
2 import datetime
3 from functools import wraps
4 import os
5 import flask
6 import sqlalchemy
7 import json
8 from requests.exceptions import RequestException, InvalidSchema
9 from wtforms import ValidationError
10
11 from werkzeug import secure_filename
12
13 from copr_common.enums import StatusEnum
14 from coprs import db
15 from coprs import exceptions
16 from coprs import forms
17 from coprs import helpers
18 from coprs import models
19 from coprs.helpers import fix_protocol_for_backend, generate_build_config
20 from coprs.logic.api_logic import MonitorWrapper
21 from coprs.logic.builds_logic import BuildsLogic
22 from coprs.logic.complex_logic import ComplexLogic
23 from coprs.logic.packages_logic import PackagesLogic
24 from coprs.logic.modules_logic import ModuleProvider, ModuleBuildFacade
25
26 from coprs.views.misc import login_required, api_login_required
27
28 from coprs.views.api_ns import api_ns
29
30 from coprs.logic import builds_logic
31 from coprs.logic import coprs_logic
32 from coprs.logic.coprs_logic import CoprsLogic
33
34 from coprs.exceptions import (ActionInProgressException,
35 InsufficientRightsException,
36 DuplicateException,
37 LegacyApiError,
38 NoPackageSourceException,
39 UnknownSourceTypeException)
52 return wrapper
53
57 """
58 Render the home page of the api.
59 This page provides information on how to call/use the API.
60 """
61
62 return flask.render_template("api.html")
63
64
65 @api_ns.route("/new/", methods=["GET", "POST"])
66 @login_required
67 -def api_new_token():
86
89 infos = []
90
91
92 proxyuser_keys = ["username"]
93 allowed = list(form.__dict__.keys()) + proxyuser_keys
94 for post_key in flask.request.form.keys():
95 if post_key not in allowed:
96 infos.append("Unknown key '{key}' received.".format(key=post_key))
97 return infos
98
111
112
113 @api_ns.route("/coprs/<username>/new/", methods=["POST"])
114 @api_login_required
115 -def api_new_copr(username):
116 """
117 Receive information from the user on how to create its new copr,
118 check their validity and create the corresponding copr.
119
120 :arg name: the name of the copr to add
121 :arg chroots: a comma separated list of chroots to use
122 :kwarg repos: a comma separated list of repository that this copr
123 can use.
124 :kwarg initial_pkgs: a comma separated list of initial packages to
125 build in this new copr
126
127 """
128
129 form = forms.CoprFormFactory.create_form_cls()(meta={'csrf': False})
130 infos = []
131
132
133 infos.extend(validate_post_keys(form))
134
135 if form.validate_on_submit():
136 group = ComplexLogic.get_group_by_name_safe(username[1:]) if username[0] == "@" else None
137
138 auto_prune = True
139 if "auto_prune" in flask.request.form:
140 auto_prune = form.auto_prune.data
141
142 use_bootstrap_container = True
143 if "use_bootstrap_container" in flask.request.form:
144 use_bootstrap_container = form.use_bootstrap_container.data
145
146 try:
147 copr = CoprsLogic.add(
148 name=form.name.data.strip(),
149 repos=" ".join(form.repos.data.split()),
150 user=flask.g.user,
151 selected_chroots=form.selected_chroots,
152 description=form.description.data,
153 instructions=form.instructions.data,
154 check_for_duplicates=True,
155 disable_createrepo=form.disable_createrepo.data,
156 unlisted_on_hp=form.unlisted_on_hp.data,
157 build_enable_net=form.build_enable_net.data,
158 group=group,
159 persistent=form.persistent.data,
160 auto_prune=auto_prune,
161 use_bootstrap_container=use_bootstrap_container,
162 )
163 infos.append("New project was successfully created.")
164
165 if form.initial_pkgs.data:
166 pkgs = form.initial_pkgs.data.split()
167 for pkg in pkgs:
168 builds_logic.BuildsLogic.add(
169 user=flask.g.user,
170 pkgs=pkg,
171 srpm_url=pkg,
172 copr=copr)
173
174 infos.append("Initial packages were successfully "
175 "submitted for building.")
176
177 output = {"output": "ok", "message": "\n".join(infos)}
178 db.session.commit()
179 except (exceptions.DuplicateException,
180 exceptions.NonAdminCannotCreatePersistentProject,
181 exceptions.NonAdminCannotDisableAutoPrunning) as err:
182 db.session.rollback()
183 raise LegacyApiError(str(err))
184
185 else:
186 errormsg = "Validation error\n"
187 if form.errors:
188 for field, emsgs in form.errors.items():
189 errormsg += "- {0}: {1}\n".format(field, "\n".join(emsgs))
190
191 errormsg = errormsg.replace('"', "'")
192 raise LegacyApiError(errormsg)
193
194 return flask.jsonify(output)
195
196
197 @api_ns.route("/coprs/<username>/<coprname>/delete/", methods=["POST"])
198 @api_login_required
199 @api_req_with_copr
200 -def api_copr_delete(copr):
222
223
224 @api_ns.route("/coprs/<username>/<coprname>/fork/", methods=["POST"])
225 @api_login_required
226 @api_req_with_copr
227 -def api_copr_fork(copr):
228 """ Fork the project and builds in it
229 """
230 form = forms.CoprForkFormFactory\
231 .create_form_cls(copr=copr, user=flask.g.user, groups=flask.g.user.user_groups)(meta={'csrf': False})
232
233 if form.validate_on_submit() and copr:
234 try:
235 dstgroup = ([g for g in flask.g.user.user_groups if g.at_name == form.owner.data] or [None])[0]
236 if flask.g.user.name != form.owner.data and not dstgroup:
237 return LegacyApiError("There is no such group: {}".format(form.owner.data))
238
239 fcopr, created = ComplexLogic.fork_copr(copr, flask.g.user, dstname=form.name.data, dstgroup=dstgroup)
240 if created:
241 msg = ("Forking project {} for you into {}.\nPlease be aware that it may take a few minutes "
242 "to duplicate backend data.".format(copr.full_name, fcopr.full_name))
243 elif not created and form.confirm.data == True:
244 msg = ("Updating packages in {} from {}.\nPlease be aware that it may take a few minutes "
245 "to duplicate backend data.".format(copr.full_name, fcopr.full_name))
246 else:
247 raise LegacyApiError("You are about to fork into existing project: {}\n"
248 "Please use --confirm if you really want to do this".format(fcopr.full_name))
249
250 output = {"output": "ok", "message": msg}
251 db.session.commit()
252
253 except (exceptions.ActionInProgressException,
254 exceptions.InsufficientRightsException) as err:
255 db.session.rollback()
256 raise LegacyApiError(str(err))
257 else:
258 raise LegacyApiError("Invalid request: {0}".format(form.errors))
259
260 return flask.jsonify(output)
261
262
263 @api_ns.route("/coprs/")
264 @api_ns.route("/coprs/<username>/")
265 -def api_coprs_by_owner(username=None):
266 """ Return the list of coprs owned by the given user.
267 username is taken either from GET params or from the URL itself
268 (in this order).
269
270 :arg username: the username of the person one would like to the
271 coprs of.
272
273 """
274 username = flask.request.args.get("username", None) or username
275 if username is None:
276 raise LegacyApiError("Invalid request: missing `username` ")
277
278 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
279
280 if username.startswith("@"):
281 group_name = username[1:]
282 query = CoprsLogic.get_multiple()
283 query = CoprsLogic.filter_by_group_name(query, group_name)
284 else:
285 query = CoprsLogic.get_multiple_owned_by_username(username)
286
287 query = CoprsLogic.join_builds(query)
288 query = CoprsLogic.set_query_order(query)
289
290 repos = query.all()
291 output = {"output": "ok", "repos": []}
292 for repo in repos:
293 yum_repos = {}
294 for build in repo.builds:
295 for chroot in repo.active_chroots:
296 release = release_tmpl.format(chroot=chroot)
297 yum_repos[release] = fix_protocol_for_backend(
298 os.path.join(build.copr.repo_url, release + '/'))
299 break
300
301 output["repos"].append({"name": repo.name,
302 "additional_repos": repo.repos,
303 "yum_repos": yum_repos,
304 "description": repo.description,
305 "instructions": repo.instructions,
306 "persistent": repo.persistent,
307 "unlisted_on_hp": repo.unlisted_on_hp,
308 "auto_prune": repo.auto_prune,
309 })
310
311 return flask.jsonify(output)
312
317 """ Return detail of one project.
318
319 :arg username: the username of the person one would like to the
320 coprs of.
321 :arg coprname: the name of project.
322
323 """
324 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
325 output = {"output": "ok", "detail": {}}
326 yum_repos = {}
327
328 build = models.Build.query.filter(models.Build.copr_id == copr.id).first()
329
330 if build:
331 for chroot in copr.active_chroots:
332 release = release_tmpl.format(chroot=chroot)
333 yum_repos[release] = fix_protocol_for_backend(
334 os.path.join(build.copr.repo_url, release + '/'))
335
336 output["detail"] = {
337 "name": copr.name,
338 "additional_repos": copr.repos,
339 "yum_repos": yum_repos,
340 "description": copr.description,
341 "instructions": copr.instructions,
342 "last_modified": builds_logic.BuildsLogic.last_modified(copr),
343 "auto_createrepo": copr.auto_createrepo,
344 "persistent": copr.persistent,
345 "unlisted_on_hp": copr.unlisted_on_hp,
346 "auto_prune": copr.auto_prune,
347 "use_bootstrap_container": copr.use_bootstrap_container,
348 }
349 return flask.jsonify(output)
350
351
352 @api_ns.route("/auth_check/", methods=["POST"])
353 @api_login_required
354 -def api_auth_check():
355 output = {"output": "ok"}
356 return flask.jsonify(output)
357
358
359 @api_ns.route("/coprs/<username>/<coprname>/new_webhook_secret/", methods=["POST"])
360 @api_login_required
361 @api_req_with_copr
362 -def new_webhook_secret(copr):
375
376
377 @api_ns.route("/coprs/<username>/<coprname>/new_build/", methods=["POST"])
378 @api_login_required
379 @api_req_with_copr
380 -def copr_new_build(copr):
392 return process_creating_new_build(copr, form, create_new_build)
393
394
395 @api_ns.route("/coprs/<username>/<coprname>/new_build_upload/", methods=["POST"])
396 @api_login_required
397 @api_req_with_copr
398 -def copr_new_build_upload(copr):
409 return process_creating_new_build(copr, form, create_new_build)
410
411
412 @api_ns.route("/coprs/<username>/<coprname>/new_build_pypi/", methods=["POST"])
413 @api_login_required
414 @api_req_with_copr
415 -def copr_new_build_pypi(copr):
433 return process_creating_new_build(copr, form, create_new_build)
434
435
436 @api_ns.route("/coprs/<username>/<coprname>/new_build_tito/", methods=["POST"])
437 @api_login_required
438 @api_req_with_copr
439 -def copr_new_build_tito(copr):
457 return process_creating_new_build(copr, form, create_new_build)
458
459
460 @api_ns.route("/coprs/<username>/<coprname>/new_build_mock/", methods=["POST"])
461 @api_login_required
462 @api_req_with_copr
463 -def copr_new_build_mock(copr):
481 return process_creating_new_build(copr, form, create_new_build)
482
483
484 @api_ns.route("/coprs/<username>/<coprname>/new_build_rubygems/", methods=["POST"])
485 @api_login_required
486 @api_req_with_copr
487 -def copr_new_build_rubygems(copr):
498 return process_creating_new_build(copr, form, create_new_build)
499
500
501 @api_ns.route("/coprs/<username>/<coprname>/new_build_custom/", methods=["POST"])
502 @api_login_required
503 @api_req_with_copr
504 -def copr_new_build_custom(copr):
517 return process_creating_new_build(copr, form, create_new_build)
518
519
520 @api_ns.route("/coprs/<username>/<coprname>/new_build_scm/", methods=["POST"])
521 @api_login_required
522 @api_req_with_copr
523 -def copr_new_build_scm(copr):
524 form = forms.BuildFormScmFactory(copr.active_chroots)(meta={'csrf': False})
525
526 def create_new_build():
527 return BuildsLogic.create_new_from_scm(
528 flask.g.user,
529 copr,
530 scm_type=form.scm_type.data,
531 clone_url=form.clone_url.data,
532 committish=form.committish.data,
533 subdirectory=form.subdirectory.data,
534 spec=form.spec.data,
535 srpm_build_method=form.srpm_build_method.data,
536 chroot_names=form.selected_chroots,
537 background=form.background.data,
538 )
539 return process_creating_new_build(copr, form, create_new_build)
540
541
542 @api_ns.route("/coprs/<username>/<coprname>/new_build_distgit/", methods=["POST"])
543 @api_login_required
544 @api_req_with_copr
545 -def copr_new_build_distgit(copr):
561 return process_creating_new_build(copr, form, create_new_build)
562
598
599
600 @api_ns.route("/coprs/build_status/<int:build_id>/", methods=["GET"])
601 -def build_status(build_id):
606
607
608 @api_ns.route("/coprs/build_detail/<int:build_id>/", methods=["GET"])
609 @api_ns.route("/coprs/build/<int:build_id>/", methods=["GET"])
610 -def build_detail(build_id):
611 build = ComplexLogic.get_build_safe(build_id)
612
613 chroots = {}
614 results_by_chroot = {}
615 for chroot in build.build_chroots:
616 chroots[chroot.name] = chroot.state
617 results_by_chroot[chroot.name] = chroot.result_dir_url
618
619 built_packages = None
620 if build.built_packages:
621 built_packages = build.built_packages.split("\n")
622
623 output = {
624 "output": "ok",
625 "status": build.state,
626 "project": build.copr_name,
627 "project_dirname": build.copr_dirname,
628 "owner": build.copr.owner_name,
629 "results": build.copr.repo_url,
630 "built_pkgs": built_packages,
631 "src_version": build.pkg_version,
632 "chroots": chroots,
633 "submitted_on": build.submitted_on,
634 "started_on": build.min_started_on,
635 "ended_on": build.max_ended_on,
636 "src_pkg": build.pkgs,
637 "submitted_by": build.user.name if build.user else None,
638 "results_by_chroot": results_by_chroot
639 }
640 return flask.jsonify(output)
641
642
643 @api_ns.route("/coprs/cancel_build/<int:build_id>/", methods=["POST"])
644 @api_login_required
645 -def cancel_build(build_id):
656
657
658 @api_ns.route("/coprs/delete_build/<int:build_id>/", methods=["POST"])
659 @api_login_required
660 -def delete_build(build_id):
671
672
673 @api_ns.route('/coprs/<username>/<coprname>/modify/', methods=["POST"])
674 @api_login_required
675 @api_req_with_copr
676 -def copr_modify(copr):
677 form = forms.CoprModifyForm(meta={'csrf': False})
678
679 if not form.validate_on_submit():
680 raise LegacyApiError("Invalid request: {0}".format(form.errors))
681
682
683
684 if form.description.raw_data and len(form.description.raw_data):
685 copr.description = form.description.data
686 if form.instructions.raw_data and len(form.instructions.raw_data):
687 copr.instructions = form.instructions.data
688 if form.repos.raw_data and len(form.repos.raw_data):
689 copr.repos = form.repos.data
690 if form.disable_createrepo.raw_data and len(form.disable_createrepo.raw_data):
691 copr.disable_createrepo = form.disable_createrepo.data
692
693 if "unlisted_on_hp" in flask.request.form:
694 copr.unlisted_on_hp = form.unlisted_on_hp.data
695 if "build_enable_net" in flask.request.form:
696 copr.build_enable_net = form.build_enable_net.data
697 if "auto_prune" in flask.request.form:
698 copr.auto_prune = form.auto_prune.data
699 if "use_bootstrap_container" in flask.request.form:
700 copr.use_bootstrap_container = form.use_bootstrap_container.data
701 if "chroots" in flask.request.form:
702 coprs_logic.CoprChrootsLogic.update_from_names(
703 flask.g.user, copr, form.chroots.data)
704
705 try:
706 CoprsLogic.update(flask.g.user, copr)
707 if copr.group:
708 _ = copr.group.id
709 db.session.commit()
710 except (exceptions.ActionInProgressException,
711 exceptions.InsufficientRightsException,
712 exceptions.NonAdminCannotDisableAutoPrunning) as e:
713 db.session.rollback()
714 raise LegacyApiError("Invalid request: {}".format(e))
715
716 output = {
717 'output': 'ok',
718 'description': copr.description,
719 'instructions': copr.instructions,
720 'repos': copr.repos,
721 'chroots': [c.name for c in copr.mock_chroots],
722 }
723
724 return flask.jsonify(output)
725
726
727 @api_ns.route('/coprs/<username>/<coprname>/modify/<chrootname>/', methods=["POST"])
728 @api_login_required
729 @api_req_with_copr
730 -def copr_modify_chroot(copr, chrootname):
744
745
746 @api_ns.route('/coprs/<username>/<coprname>/chroot/edit/<chrootname>/', methods=["POST"])
747 @api_login_required
748 @api_req_with_copr
749 -def copr_edit_chroot(copr, chrootname):
750 form = forms.ModifyChrootForm(meta={'csrf': False})
751 chroot = ComplexLogic.get_copr_chroot_safe(copr, chrootname)
752
753 if not form.validate_on_submit():
754 raise LegacyApiError("Invalid request: {0}".format(form.errors))
755 else:
756 buildroot_pkgs = repos = comps_xml = comps_name = None
757 if "buildroot_pkgs" in flask.request.form:
758 buildroot_pkgs = form.buildroot_pkgs.data
759 if "repos" in flask.request.form:
760 repos = form.repos.data
761 if form.upload_comps.has_file():
762 comps_xml = form.upload_comps.data.stream.read()
763 comps_name = form.upload_comps.data.filename
764 if form.delete_comps.data:
765 coprs_logic.CoprChrootsLogic.remove_comps(flask.g.user, chroot)
766 coprs_logic.CoprChrootsLogic.update_chroot(
767 flask.g.user, chroot, buildroot_pkgs, repos, comps=comps_xml, comps_name=comps_name)
768 db.session.commit()
769
770 output = {
771 "output": "ok",
772 "message": "Edit chroot operation was successful.",
773 "chroot": chroot.to_dict(),
774 }
775 return flask.jsonify(output)
776
777
778 @api_ns.route('/coprs/<username>/<coprname>/detail/<chrootname>/', methods=["GET"])
779 @api_req_with_copr
780 -def copr_chroot_details(copr, chrootname):
785
786 @api_ns.route('/coprs/<username>/<coprname>/chroot/get/<chrootname>/', methods=["GET"])
787 @api_req_with_copr
788 -def copr_get_chroot(copr, chrootname):
792
796 """ Return the list of coprs found in search by the given text.
797 project is taken either from GET params or from the URL itself
798 (in this order).
799
800 :arg project: the text one would like find for coprs.
801
802 """
803 project = flask.request.args.get("project", None) or project
804 if not project:
805 raise LegacyApiError("No project found.")
806
807 try:
808 query = CoprsLogic.get_multiple_fulltext(project)
809
810 repos = query.all()
811 output = {"output": "ok", "repos": []}
812 for repo in repos:
813 output["repos"].append({"username": repo.user.name,
814 "coprname": repo.name,
815 "description": repo.description})
816 except ValueError as e:
817 raise LegacyApiError("Server error: {}".format(e))
818
819 return flask.jsonify(output)
820
824 """ Return list of coprs which are part of playground """
825 query = CoprsLogic.get_playground()
826 repos = query.all()
827 output = {"output": "ok", "repos": []}
828 for repo in repos:
829 output["repos"].append({"username": repo.owner_name,
830 "coprname": repo.name,
831 "chroots": [chroot.name for chroot in repo.active_chroots]})
832
833 jsonout = flask.jsonify(output)
834 jsonout.status_code = 200
835 return jsonout
836
837
838 @api_ns.route("/coprs/<username>/<coprname>/monitor/", methods=["GET"])
839 @api_req_with_copr
840 -def monitor(copr):
844
845
846
847 @api_ns.route("/coprs/<username>/<coprname>/package/add/<source_type_text>/", methods=["POST"])
848 @api_login_required
849 @api_req_with_copr
850 -def copr_add_package(copr, source_type_text):
852
853
854 @api_ns.route("/coprs/<username>/<coprname>/package/<package_name>/edit/<source_type_text>/", methods=["POST"])
855 @api_login_required
856 @api_req_with_copr
857 -def copr_edit_package(copr, package_name, source_type_text):
864
914
917 params = {}
918 if flask.request.args.get('with_latest_build'):
919 params['with_latest_build'] = True
920 if flask.request.args.get('with_latest_succeeded_build'):
921 params['with_latest_succeeded_build'] = True
922 if flask.request.args.get('with_all_builds'):
923 params['with_all_builds'] = True
924 return params
925
928 """
929 A lagging generator to stream JSON so we don't have to hold everything in memory
930 This is a little tricky, as we need to omit the last comma to make valid JSON,
931 thus we use a lagging generator, similar to http://stackoverflow.com/questions/1630320/
932 """
933 packages = query.__iter__()
934 try:
935 prev_package = next(packages)
936 except StopIteration:
937
938 yield '{"packages": []}'
939 raise StopIteration
940
941 yield '{"packages": ['
942
943 for package in packages:
944 yield json.dumps(prev_package.to_dict(**params)) + ', '
945 prev_package = package
946
947 yield json.dumps(prev_package.to_dict(**params)) + ']}'
948
949
950 @api_ns.route("/coprs/<username>/<coprname>/package/list/", methods=["GET"])
951 @api_req_with_copr
952 -def copr_list_packages(copr):
956
957
958
959 @api_ns.route("/coprs/<username>/<coprname>/package/get/<package_name>/", methods=["GET"])
960 @api_req_with_copr
961 -def copr_get_package(copr, package_name):
970
971
972 @api_ns.route("/coprs/<username>/<coprname>/package/delete/<package_name>/", methods=["POST"])
973 @api_login_required
974 @api_req_with_copr
975 -def copr_delete_package(copr, package_name):
992
993
994 @api_ns.route("/coprs/<username>/<coprname>/package/reset/<package_name>/", methods=["POST"])
995 @api_login_required
996 @api_req_with_copr
997 -def copr_reset_package(copr, package_name):
1014
1015
1016 @api_ns.route("/coprs/<username>/<coprname>/package/build/<package_name>/", methods=["POST"])
1017 @api_login_required
1018 @api_req_with_copr
1019 -def copr_build_package(copr, package_name):
1020 form = forms.BuildFormRebuildFactory.create_form_cls(copr.active_chroots)(meta={'csrf': False})
1021
1022 try:
1023 package = PackagesLogic.get(copr.main_dir.id, package_name)[0]
1024 except IndexError:
1025 raise LegacyApiError("No package with name {name} in copr {copr}".format(name=package_name, copr=copr.name))
1026
1027 if form.validate_on_submit():
1028 try:
1029 build = PackagesLogic.build_package(flask.g.user, copr, package, form.selected_chroots, **form.data)
1030 db.session.commit()
1031 except (InsufficientRightsException, ActionInProgressException, NoPackageSourceException) as e:
1032 raise LegacyApiError(str(e))
1033 else:
1034 raise LegacyApiError(form.errors)
1035
1036 return flask.jsonify({
1037 "output": "ok",
1038 "ids": [build.id],
1039 "message": "Build was added to {0}.".format(copr.name)
1040 })
1041
1042
1043 @api_ns.route("/coprs/<username>/<coprname>/module/build/", methods=["POST"])
1044 @api_login_required
1045 @api_req_with_copr
1046 -def copr_build_module(copr):
1069
1070
1071 @api_ns.route("/coprs/<username>/<coprname>/build-config/<chroot>/", methods=["GET"])
1072 @api_ns.route("/g/<group_name>/<coprname>/build-config/<chroot>/", methods=["GET"])
1073 @api_req_with_copr
1074 -def copr_build_config(copr, chroot):
1075 """
1076 Generate build configuration.
1077 """
1078 output = {
1079 "output": "ok",
1080 "build_config": generate_build_config(copr, chroot),
1081 }
1082
1083 if not output['build_config']:
1084 raise LegacyApiError('Chroot not found.')
1085
1086
1087 for repo in output["build_config"]["repos"]:
1088 repo["url"] = repo["baseurl"]
1089
1090 return flask.jsonify(output)
1091