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.utils 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
20 from coprs.logic.api_logic import MonitorWrapper
21 from coprs.logic.builds_logic import BuildsLogic
22 from coprs.logic.complex_logic import ComplexLogic, BuildConfigLogic
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
143
144 use_bootstrap_container = None
145 if "use_bootstrap_container" in flask.request.form:
146 use_bootstrap_container = form.use_bootstrap_container.data
147 bootstrap = None
148 if use_bootstrap_container is not None:
149 bootstrap = "on" if use_bootstrap_container else "off"
150
151 try:
152 copr = CoprsLogic.add(
153 name=form.name.data.strip(),
154 repos=" ".join(form.repos.data.split()),
155 user=flask.g.user,
156 selected_chroots=form.selected_chroots,
157 description=form.description.data,
158 instructions=form.instructions.data,
159 check_for_duplicates=True,
160 disable_createrepo=form.disable_createrepo.data,
161 unlisted_on_hp=form.unlisted_on_hp.data,
162 build_enable_net=form.build_enable_net.data,
163 group=group,
164 persistent=form.persistent.data,
165 auto_prune=auto_prune,
166 bootstrap=bootstrap,
167 )
168 infos.append("New project was successfully created.")
169
170 if form.initial_pkgs.data:
171 pkgs = form.initial_pkgs.data.split()
172 for pkg in pkgs:
173 builds_logic.BuildsLogic.add(
174 user=flask.g.user,
175 pkgs=pkg,
176 srpm_url=pkg,
177 copr=copr)
178
179 infos.append("Initial packages were successfully "
180 "submitted for building.")
181
182 output = {"output": "ok", "message": "\n".join(infos)}
183 db.session.commit()
184 except (exceptions.DuplicateException,
185 exceptions.NonAdminCannotCreatePersistentProject,
186 exceptions.NonAdminCannotDisableAutoPrunning) as err:
187 db.session.rollback()
188 raise LegacyApiError(str(err))
189
190 else:
191 errormsg = "Validation error\n"
192 if form.errors:
193 for field, emsgs in form.errors.items():
194 errormsg += "- {0}: {1}\n".format(field, "\n".join(emsgs))
195
196 errormsg = errormsg.replace('"', "'")
197 raise LegacyApiError(errormsg)
198
199 return flask.jsonify(output)
200
201
202 @api_ns.route("/coprs/<username>/<coprname>/delete/", methods=["POST"])
203 @api_login_required
204 @api_req_with_copr
205 -def api_copr_delete(copr):
227
228
229 @api_ns.route("/coprs/<username>/<coprname>/fork/", methods=["POST"])
230 @api_login_required
231 @api_req_with_copr
232 -def api_copr_fork(copr):
233 """ Fork the project and builds in it
234 """
235 form = forms.CoprForkFormFactory\
236 .create_form_cls(copr=copr, user=flask.g.user, groups=flask.g.user.user_groups)(meta={'csrf': False})
237
238 if form.validate_on_submit() and copr:
239 try:
240 dstgroup = ([g for g in flask.g.user.user_groups if g.at_name == form.owner.data] or [None])[0]
241 if flask.g.user.name != form.owner.data and not dstgroup:
242 return LegacyApiError("There is no such group: {}".format(form.owner.data))
243
244 fcopr, created = ComplexLogic.fork_copr(copr, flask.g.user, dstname=form.name.data, dstgroup=dstgroup)
245 if created:
246 msg = ("Forking project {} for you into {}.\nPlease be aware that it may take a few minutes "
247 "to duplicate backend data.".format(copr.full_name, fcopr.full_name))
248 elif not created and form.confirm.data == True:
249 msg = ("Updating packages in {} from {}.\nPlease be aware that it may take a few minutes "
250 "to duplicate backend data.".format(copr.full_name, fcopr.full_name))
251 else:
252 raise LegacyApiError("You are about to fork into existing project: {}\n"
253 "Please use --confirm if you really want to do this".format(fcopr.full_name))
254
255 output = {"output": "ok", "message": msg}
256 db.session.commit()
257
258 except (exceptions.ActionInProgressException,
259 exceptions.InsufficientRightsException) as err:
260 db.session.rollback()
261 raise LegacyApiError(str(err))
262 else:
263 raise LegacyApiError("Invalid request: {0}".format(form.errors))
264
265 return flask.jsonify(output)
266
267
268 @api_ns.route("/coprs/")
269 @api_ns.route("/coprs/<username>/")
270 -def api_coprs_by_owner(username=None):
271 """ Return the list of coprs owned by the given user.
272 username is taken either from GET params or from the URL itself
273 (in this order).
274
275 :arg username: the username of the person one would like to the
276 coprs of.
277
278 """
279 username = flask.request.args.get("username", None) or username
280 if username is None:
281 raise LegacyApiError("Invalid request: missing `username` ")
282
283 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
284
285 if username.startswith("@"):
286 group_name = username[1:]
287 query = CoprsLogic.get_multiple()
288 query = CoprsLogic.filter_by_group_name(query, group_name)
289 else:
290 query = CoprsLogic.get_multiple_owned_by_username(username)
291
292 query = CoprsLogic.join_builds(query)
293 query = CoprsLogic.set_query_order(query)
294
295 repos = query.all()
296 output = {"output": "ok", "repos": []}
297 for repo in repos:
298 yum_repos = {}
299 for build in repo.builds:
300 for chroot in repo.active_chroots:
301 release = release_tmpl.format(chroot=chroot)
302 yum_repos[release] = fix_protocol_for_backend(
303 os.path.join(build.copr.repo_url, release + '/'))
304 break
305
306 output["repos"].append({"name": repo.name,
307 "additional_repos": repo.repos,
308 "yum_repos": yum_repos,
309 "description": repo.description,
310 "instructions": repo.instructions,
311 "persistent": repo.persistent,
312 "unlisted_on_hp": repo.unlisted_on_hp,
313 "auto_prune": repo.auto_prune,
314 })
315
316 return flask.jsonify(output)
317
322 """ Return detail of one project.
323
324 :arg username: the username of the person one would like to the
325 coprs of.
326 :arg coprname: the name of project.
327
328 """
329 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
330 output = {"output": "ok", "detail": {}}
331 yum_repos = {}
332
333 build = models.Build.query.filter(models.Build.copr_id == copr.id).first()
334
335 if build:
336 for chroot in copr.active_chroots:
337 release = release_tmpl.format(chroot=chroot)
338 yum_repos[release] = fix_protocol_for_backend(
339 os.path.join(build.copr.repo_url, release + '/'))
340
341 output["detail"] = {
342 "name": copr.name,
343 "additional_repos": copr.repos,
344 "yum_repos": yum_repos,
345 "description": copr.description,
346 "instructions": copr.instructions,
347 "last_modified": builds_logic.BuildsLogic.last_modified(copr),
348 "auto_createrepo": copr.auto_createrepo,
349 "persistent": copr.persistent,
350 "unlisted_on_hp": copr.unlisted_on_hp,
351 "auto_prune": copr.auto_prune,
352 "use_bootstrap_container": copr.bootstrap == "on",
353 }
354 return flask.jsonify(output)
355
356
357 @api_ns.route("/auth_check/", methods=["POST"])
358 @api_login_required
359 -def api_auth_check():
360 output = {"output": "ok"}
361 return flask.jsonify(output)
362
363
364 @api_ns.route("/coprs/<username>/<coprname>/new_webhook_secret/", methods=["POST"])
365 @api_login_required
366 @api_req_with_copr
367 -def new_webhook_secret(copr):
380
381
382 @api_ns.route("/coprs/<username>/<coprname>/new_build/", methods=["POST"])
383 @api_login_required
384 @api_req_with_copr
385 -def copr_new_build(copr):
397 return process_creating_new_build(copr, form, create_new_build)
398
399
400 @api_ns.route("/coprs/<username>/<coprname>/new_build_upload/", methods=["POST"])
401 @api_login_required
402 @api_req_with_copr
403 -def copr_new_build_upload(copr):
414 return process_creating_new_build(copr, form, create_new_build)
415
416
417 @api_ns.route("/coprs/<username>/<coprname>/new_build_pypi/", methods=["POST"])
418 @api_login_required
419 @api_req_with_copr
420 -def copr_new_build_pypi(copr):
438 return process_creating_new_build(copr, form, create_new_build)
439
440
441 @api_ns.route("/coprs/<username>/<coprname>/new_build_tito/", methods=["POST"])
442 @api_login_required
443 @api_req_with_copr
444 -def copr_new_build_tito(copr):
462 return process_creating_new_build(copr, form, create_new_build)
463
464
465 @api_ns.route("/coprs/<username>/<coprname>/new_build_mock/", methods=["POST"])
466 @api_login_required
467 @api_req_with_copr
468 -def copr_new_build_mock(copr):
486 return process_creating_new_build(copr, form, create_new_build)
487
488
489 @api_ns.route("/coprs/<username>/<coprname>/new_build_rubygems/", methods=["POST"])
490 @api_login_required
491 @api_req_with_copr
492 -def copr_new_build_rubygems(copr):
503 return process_creating_new_build(copr, form, create_new_build)
504
505
506 @api_ns.route("/coprs/<username>/<coprname>/new_build_custom/", methods=["POST"])
507 @api_login_required
508 @api_req_with_copr
509 -def copr_new_build_custom(copr):
522 return process_creating_new_build(copr, form, create_new_build)
523
524
525 @api_ns.route("/coprs/<username>/<coprname>/new_build_scm/", methods=["POST"])
526 @api_login_required
527 @api_req_with_copr
528 -def copr_new_build_scm(copr):
529 form = forms.BuildFormScmFactory(copr.active_chroots)(meta={'csrf': False})
530
531
532
533
534
535 def create_new_build():
536 return BuildsLogic.create_new_from_scm(
537 flask.g.user,
538 copr,
539 scm_type=form.scm_type.data,
540 clone_url=form.clone_url.data,
541 committish=form.committish.data,
542 subdirectory=form.subdirectory.data,
543 spec=form.spec.data,
544 srpm_build_method=form.srpm_build_method.data,
545 chroot_names=form.selected_chroots,
546 background=form.background.data,
547 )
548 return process_creating_new_build(copr, form, create_new_build)
549
550
551 @api_ns.route("/coprs/<username>/<coprname>/new_build_distgit/", methods=["POST"])
552 @api_login_required
553 @api_req_with_copr
554 -def copr_new_build_distgit(copr):
570 return process_creating_new_build(copr, form, create_new_build)
571
607
608
609 @api_ns.route("/coprs/build_status/<int:build_id>/", methods=["GET"])
610 -def build_status(build_id):
615
616
617 @api_ns.route("/coprs/build_detail/<int:build_id>/", methods=["GET"])
618 @api_ns.route("/coprs/build/<int:build_id>/", methods=["GET"])
619 -def build_detail(build_id):
620 build = ComplexLogic.get_build_safe(build_id)
621
622 chroots = {}
623 results_by_chroot = {}
624 for chroot in build.build_chroots:
625 chroots[chroot.name] = chroot.state
626 results_by_chroot[chroot.name] = chroot.result_dir_url
627
628 built_packages = None
629 if build.built_packages:
630 built_packages = build.built_packages.split("\n")
631
632 output = {
633 "output": "ok",
634 "status": build.state,
635 "project": build.copr_name,
636 "project_dirname": build.copr_dirname,
637 "owner": build.copr.owner_name,
638 "results": build.copr.repo_url,
639 "built_pkgs": built_packages,
640 "src_version": build.pkg_version,
641 "chroots": chroots,
642 "submitted_on": build.submitted_on,
643 "started_on": build.min_started_on,
644 "ended_on": build.max_ended_on,
645 "src_pkg": build.pkgs,
646 "submitted_by": build.user.name if build.user else None,
647 "results_by_chroot": results_by_chroot
648 }
649 return flask.jsonify(output)
650
651
652 @api_ns.route("/coprs/cancel_build/<int:build_id>/", methods=["POST"])
653 @api_login_required
654 -def cancel_build(build_id):
665
666
667 @api_ns.route("/coprs/delete_build/<int:build_id>/", methods=["POST"])
668 @api_login_required
669 -def delete_build(build_id):
680
681
682 @api_ns.route('/coprs/<username>/<coprname>/modify/', methods=["POST"])
683 @api_login_required
684 @api_req_with_copr
685 -def copr_modify(copr):
686 form = forms.CoprModifyForm(meta={'csrf': False})
687
688 if not form.validate_on_submit():
689 raise LegacyApiError("Invalid request: {0}".format(form.errors))
690
691
692
693 if form.description.raw_data and len(form.description.raw_data):
694 copr.description = form.description.data
695 if form.instructions.raw_data and len(form.instructions.raw_data):
696 copr.instructions = form.instructions.data
697 if form.repos.raw_data and len(form.repos.raw_data):
698 copr.repos = form.repos.data
699 if form.disable_createrepo.raw_data and len(form.disable_createrepo.raw_data):
700 copr.disable_createrepo = form.disable_createrepo.data
701
702 if "unlisted_on_hp" in flask.request.form:
703 copr.unlisted_on_hp = form.unlisted_on_hp.data
704 if "build_enable_net" in flask.request.form:
705 copr.build_enable_net = form.build_enable_net.data
706 if "auto_prune" in flask.request.form:
707 copr.auto_prune = form.auto_prune.data
708 if "use_bootstrap_container" in flask.request.form:
709 copr.bootstrap = "on" if form.use_bootstrap_container.data else "off"
710 if "chroots" in flask.request.form:
711 coprs_logic.CoprChrootsLogic.update_from_names(
712 flask.g.user, copr, form.chroots.data)
713
714 try:
715 CoprsLogic.update(flask.g.user, copr)
716 if copr.group:
717 _ = copr.group.id
718 db.session.commit()
719 except (exceptions.ActionInProgressException,
720 exceptions.InsufficientRightsException,
721 exceptions.NonAdminCannotDisableAutoPrunning) as e:
722 db.session.rollback()
723 raise LegacyApiError("Invalid request: {}".format(e))
724
725 output = {
726 'output': 'ok',
727 'description': copr.description,
728 'instructions': copr.instructions,
729 'repos': copr.repos,
730 'chroots': [c.name for c in copr.mock_chroots],
731 }
732
733 return flask.jsonify(output)
734
735
736 @api_ns.route('/coprs/<username>/<coprname>/modify/<chrootname>/', methods=["POST"])
737 @api_login_required
738 @api_req_with_copr
739 -def copr_modify_chroot(copr, chrootname):
753
754
755 @api_ns.route('/coprs/<username>/<coprname>/chroot/edit/<chrootname>/', methods=["POST"])
756 @api_login_required
757 @api_req_with_copr
758 -def copr_edit_chroot(copr, chrootname):
759 form = forms.ModifyChrootForm(meta={'csrf': False})
760 chroot = ComplexLogic.get_copr_chroot_safe(copr, chrootname)
761
762 if not form.validate_on_submit():
763 raise LegacyApiError("Invalid request: {0}".format(form.errors))
764 else:
765 buildroot_pkgs = repos = comps_xml = comps_name = None
766 if "buildroot_pkgs" in flask.request.form:
767 buildroot_pkgs = form.buildroot_pkgs.data
768 if "repos" in flask.request.form:
769 repos = form.repos.data
770 if form.upload_comps.has_file():
771 comps_xml = form.upload_comps.data.stream.read()
772 comps_name = form.upload_comps.data.filename
773 if form.delete_comps.data:
774 coprs_logic.CoprChrootsLogic.remove_comps(flask.g.user, chroot)
775 coprs_logic.CoprChrootsLogic.update_chroot(
776 flask.g.user, chroot, buildroot_pkgs, repos, comps=comps_xml, comps_name=comps_name)
777 db.session.commit()
778
779 output = {
780 "output": "ok",
781 "message": "Edit chroot operation was successful.",
782 "chroot": chroot.to_dict(),
783 }
784 return flask.jsonify(output)
785
786
787 @api_ns.route('/coprs/<username>/<coprname>/detail/<chrootname>/', methods=["GET"])
788 @api_req_with_copr
789 -def copr_chroot_details(copr, chrootname):
794
795 @api_ns.route('/coprs/<username>/<coprname>/chroot/get/<chrootname>/', methods=["GET"])
796 @api_req_with_copr
797 -def copr_get_chroot(copr, chrootname):
801
805 """ Return the list of coprs found in search by the given text.
806 project is taken either from GET params or from the URL itself
807 (in this order).
808
809 :arg project: the text one would like find for coprs.
810
811 """
812 project = flask.request.args.get("project", None) or project
813 if not project:
814 raise LegacyApiError("No project found.")
815
816 try:
817 query = CoprsLogic.get_multiple_fulltext(project)
818
819 repos = query.all()
820 output = {"output": "ok", "repos": []}
821 for repo in repos:
822 output["repos"].append({"username": repo.user.name,
823 "coprname": repo.name,
824 "description": repo.description})
825 except ValueError as e:
826 raise LegacyApiError("Server error: {}".format(e))
827
828 return flask.jsonify(output)
829
833 """ Return list of coprs which are part of playground """
834 query = CoprsLogic.get_playground()
835 repos = query.all()
836 output = {"output": "ok", "repos": []}
837 for repo in repos:
838 output["repos"].append({"username": repo.owner_name,
839 "coprname": repo.name,
840 "chroots": [chroot.name for chroot in repo.active_chroots]})
841
842 jsonout = flask.jsonify(output)
843 jsonout.status_code = 200
844 return jsonout
845
846
847 @api_ns.route("/coprs/<username>/<coprname>/monitor/", methods=["GET"])
848 @api_req_with_copr
849 -def monitor(copr):
853
854
855
856 @api_ns.route("/coprs/<username>/<coprname>/package/add/<source_type_text>/", methods=["POST"])
857 @api_login_required
858 @api_req_with_copr
859 -def copr_add_package(copr, source_type_text):
861
862
863 @api_ns.route("/coprs/<username>/<coprname>/package/<package_name>/edit/<source_type_text>/", methods=["POST"])
864 @api_login_required
865 @api_req_with_copr
866 -def copr_edit_package(copr, package_name, source_type_text):
873
923
926 params = {}
927 if flask.request.args.get('with_latest_build'):
928 params['with_latest_build'] = True
929 if flask.request.args.get('with_latest_succeeded_build'):
930 params['with_latest_succeeded_build'] = True
931 if flask.request.args.get('with_all_builds'):
932 params['with_all_builds'] = True
933 return params
934
937 """
938 A lagging generator to stream JSON so we don't have to hold everything in memory
939 This is a little tricky, as we need to omit the last comma to make valid JSON,
940 thus we use a lagging generator, similar to http://stackoverflow.com/questions/1630320/
941 """
942 packages = query.__iter__()
943 try:
944 prev_package = next(packages)
945 except StopIteration:
946
947 yield '{"packages": []}'
948 raise StopIteration
949
950 yield '{"packages": ['
951
952 for package in packages:
953 yield json.dumps(prev_package.to_dict(**params)) + ', '
954 prev_package = package
955
956 yield json.dumps(prev_package.to_dict(**params)) + ']}'
957
958
959 @api_ns.route("/coprs/<username>/<coprname>/package/list/", methods=["GET"])
960 @api_req_with_copr
961 -def copr_list_packages(copr):
965
966
967
968 @api_ns.route("/coprs/<username>/<coprname>/package/get/<package_name>/", methods=["GET"])
969 @api_req_with_copr
970 -def copr_get_package(copr, package_name):
979
980
981 @api_ns.route("/coprs/<username>/<coprname>/package/delete/<package_name>/", methods=["POST"])
982 @api_login_required
983 @api_req_with_copr
984 -def copr_delete_package(copr, package_name):
1001
1002
1003 @api_ns.route("/coprs/<username>/<coprname>/package/reset/<package_name>/", methods=["POST"])
1004 @api_login_required
1005 @api_req_with_copr
1006 -def copr_reset_package(copr, package_name):
1023
1024
1025 @api_ns.route("/coprs/<username>/<coprname>/package/build/<package_name>/", methods=["POST"])
1026 @api_login_required
1027 @api_req_with_copr
1028 -def copr_build_package(copr, package_name):
1029 form = forms.BuildFormRebuildFactory.create_form_cls(copr.active_chroots)(meta={'csrf': False})
1030
1031 try:
1032 package = PackagesLogic.get(copr.main_dir.id, package_name)[0]
1033 except IndexError:
1034 raise LegacyApiError("No package with name {name} in copr {copr}".format(name=package_name, copr=copr.name))
1035
1036 if form.validate_on_submit():
1037 try:
1038 build = PackagesLogic.build_package(flask.g.user, copr, package, form.selected_chroots, **form.data)
1039 db.session.commit()
1040 except (InsufficientRightsException, ActionInProgressException, NoPackageSourceException) as e:
1041 raise LegacyApiError(str(e))
1042 else:
1043 raise LegacyApiError(form.errors)
1044
1045 return flask.jsonify({
1046 "output": "ok",
1047 "ids": [build.id],
1048 "message": "Build was added to {0}.".format(copr.name)
1049 })
1050
1051
1052 @api_ns.route("/coprs/<username>/<coprname>/module/build/", methods=["POST"])
1053 @api_login_required
1054 @api_req_with_copr
1055 -def copr_build_module(copr):
1078
1079
1080 @api_ns.route("/coprs/<username>/<coprname>/build-config/<chroot>/", methods=["GET"])
1081 @api_ns.route("/g/<group_name>/<coprname>/build-config/<chroot>/", methods=["GET"])
1082 @api_req_with_copr
1083 -def copr_build_config(copr, chroot):
1084 """
1085 Generate build configuration.
1086 """
1087 output = {
1088 "output": "ok",
1089 "build_config": BuildConfigLogic.generate_build_config(copr, chroot),
1090 }
1091
1092 if not output['build_config']:
1093 raise LegacyApiError('Chroot not found.')
1094
1095
1096 for repo in output["build_config"]["repos"]:
1097 repo["url"] = repo["baseurl"]
1098
1099 return flask.jsonify(output)
1100