1 import base64
2 import datetime
3 from functools import wraps
4 import json
5 import os
6 import flask
7 import sqlalchemy
8 import json
9 import requests
10 from wtforms import ValidationError
11
12 from werkzeug import secure_filename
13
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.users_logic import UsersLogic
24 from coprs.logic.packages_logic import PackagesLogic
25 from coprs.logic.modules_logic import ModulesLogic
26
27 from coprs.views.misc import login_required, api_login_required
28
29 from coprs.views.api_ns import api_ns
30
31 from coprs.logic import builds_logic
32 from coprs.logic import coprs_logic
33 from coprs.logic.coprs_logic import CoprsLogic
34 from coprs.logic.actions_logic import ActionsLogic
35
36 from coprs.exceptions import (ActionInProgressException,
37 InsufficientRightsException,
38 DuplicateException,
39 LegacyApiError,
40 UnknownSourceTypeException)
53 return wrapper
54
58 """
59 Render the home page of the api.
60 This page provides information on how to call/use the API.
61 """
62
63 return flask.render_template("api.html")
64
65
66 @api_ns.route("/new/", methods=["GET", "POST"])
87
90 infos = []
91
92
93 proxyuser_keys = ["username"]
94 allowed = form.__dict__.keys() + proxyuser_keys
95 for post_key in flask.request.form.keys():
96 if post_key not in allowed:
97 infos.append("Unknown key '{key}' received.".format(key=post_key))
98 return infos
99
100
101 @api_ns.route("/coprs/<username>/new/", methods=["POST"])
104 """
105 Receive information from the user on how to create its new copr,
106 check their validity and create the corresponding copr.
107
108 :arg name: the name of the copr to add
109 :arg chroots: a comma separated list of chroots to use
110 :kwarg repos: a comma separated list of repository that this copr
111 can use.
112 :kwarg initial_pkgs: a comma separated list of initial packages to
113 build in this new copr
114
115 """
116
117 form = forms.CoprFormFactory.create_form_cls()(csrf_enabled=False)
118 infos = []
119
120
121 infos.extend(validate_post_keys(form))
122
123 if form.validate_on_submit():
124 group = ComplexLogic.get_group_by_name_safe(username[1:]) if username[0] == "@" else None
125
126 auto_prune = True
127 if "auto_prune" in flask.request.form:
128 auto_prune = form.auto_prune.data
129
130 try:
131 copr = CoprsLogic.add(
132 name=form.name.data.strip(),
133 repos=" ".join(form.repos.data.split()),
134 user=flask.g.user,
135 selected_chroots=form.selected_chroots,
136 description=form.description.data,
137 instructions=form.instructions.data,
138 check_for_duplicates=True,
139 disable_createrepo=form.disable_createrepo.data,
140 unlisted_on_hp=form.unlisted_on_hp.data,
141 build_enable_net=form.build_enable_net.data,
142 group=group,
143 persistent=form.persistent.data,
144 auto_prune=auto_prune,
145 )
146 infos.append("New project was successfully created.")
147
148 if form.initial_pkgs.data:
149 pkgs = form.initial_pkgs.data.split()
150 for pkg in pkgs:
151 builds_logic.BuildsLogic.add(
152 user=flask.g.user,
153 pkgs=pkg,
154 copr=copr)
155
156 infos.append("Initial packages were successfully "
157 "submitted for building.")
158
159 output = {"output": "ok", "message": "\n".join(infos)}
160 db.session.commit()
161 except (exceptions.DuplicateException,
162 exceptions.NonAdminCannotCreatePersistentProject,
163 exceptions.NonAdminCannotDisableAutoPrunning) as err:
164 db.session.rollback()
165 raise LegacyApiError(str(err))
166
167 else:
168 errormsg = "Validation error\n"
169 if form.errors:
170 for field, emsgs in form.errors.items():
171 errormsg += "- {0}: {1}\n".format(field, "\n".join(emsgs))
172
173 errormsg = errormsg.replace('"', "'")
174 raise LegacyApiError(errormsg)
175
176 return flask.jsonify(output)
177
178
179 @api_ns.route("/coprs/<username>/<coprname>/delete/", methods=["POST"])
204
205
206 @api_ns.route("/coprs/<username>/<coprname>/fork/", methods=["POST"])
207 @api_login_required
208 @api_req_with_copr
209 -def api_copr_fork(copr):
210 """ Fork the project and builds in it
211 """
212 form = forms.CoprForkFormFactory\
213 .create_form_cls(copr=copr, user=flask.g.user, groups=flask.g.user.user_groups)(csrf_enabled=False)
214
215 if form.validate_on_submit() and copr:
216 try:
217 dstgroup = ([g for g in flask.g.user.user_groups if g.at_name == form.owner.data] or [None])[0]
218 if flask.g.user.name != form.owner.data and not dstgroup:
219 return LegacyApiError("There is no such group: {}".format(form.owner.data))
220
221 fcopr, created = ComplexLogic.fork_copr(copr, flask.g.user, dstname=form.name.data, dstgroup=dstgroup)
222 if created:
223 msg = ("Forking project {} for you into {}.\nPlease be aware that it may take a few minutes "
224 "to duplicate a backend data.".format(copr.full_name, fcopr.full_name))
225 elif not created and form.confirm.data == True:
226 msg = ("Updating packages in {} from {}.\nPlease be aware that it may take a few minutes "
227 "to duplicate a backend data.".format(copr.full_name, fcopr.full_name))
228 else:
229 raise LegacyApiError("You are about to fork into existing project: {}\n"
230 "Please use --confirm if you really want to do this".format(fcopr.full_name))
231
232 output = {"output": "ok", "message": msg}
233 db.session.commit()
234
235 except (exceptions.ActionInProgressException,
236 exceptions.InsufficientRightsException) as err:
237 db.session.rollback()
238 raise LegacyApiError(str(err))
239 else:
240 raise LegacyApiError("Invalid request: {0}".format(form.errors))
241
242 return flask.jsonify(output)
243
244
245 @api_ns.route("/coprs/")
246 @api_ns.route("/coprs/<username>/")
247 -def api_coprs_by_owner(username=None):
248 """ Return the list of coprs owned by the given user.
249 username is taken either from GET params or from the URL itself
250 (in this order).
251
252 :arg username: the username of the person one would like to the
253 coprs of.
254
255 """
256 username = flask.request.args.get("username", None) or username
257 if username is None:
258 raise LegacyApiError("Invalid request: missing `username` ")
259
260 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
261
262 if username.startswith("@"):
263 group_name = username[1:]
264 query = CoprsLogic.get_multiple()
265 query = CoprsLogic.filter_by_group_name(query, group_name)
266 else:
267 query = CoprsLogic.get_multiple_owned_by_username(username)
268
269 query = CoprsLogic.join_builds(query)
270 query = CoprsLogic.set_query_order(query)
271
272 repos = query.all()
273 output = {"output": "ok", "repos": []}
274 for repo in repos:
275 yum_repos = {}
276 for build in repo.builds:
277 if build.results:
278 for chroot in repo.active_chroots:
279 release = release_tmpl.format(chroot=chroot)
280 yum_repos[release] = fix_protocol_for_backend(
281 os.path.join(build.results, release + '/'))
282 break
283
284 output["repos"].append({"name": repo.name,
285 "additional_repos": repo.repos,
286 "yum_repos": yum_repos,
287 "description": repo.description,
288 "instructions": repo.instructions,
289 "persistent": repo.persistent,
290 "unlisted_on_hp": repo.unlisted_on_hp,
291 "auto_prune": repo.auto_prune,
292 })
293
294 return flask.jsonify(output)
295
300 """ Return detail of one project.
301
302 :arg username: the username of the person one would like to the
303 coprs of.
304 :arg coprname: the name of project.
305
306 """
307 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
308 output = {"output": "ok", "detail": {}}
309 yum_repos = {}
310
311 build = models.Build.query.filter(
312 models.Build.copr_id == copr.id, models.Build.results != None).first()
313
314 if build:
315 for chroot in copr.active_chroots:
316 release = release_tmpl.format(chroot=chroot)
317 yum_repos[release] = fix_protocol_for_backend(
318 os.path.join(build.results, release + '/'))
319
320 output["detail"] = {
321 "name": copr.name,
322 "additional_repos": copr.repos,
323 "yum_repos": yum_repos,
324 "description": copr.description,
325 "instructions": copr.instructions,
326 "last_modified": builds_logic.BuildsLogic.last_modified(copr),
327 "auto_createrepo": copr.auto_createrepo,
328 "persistent": copr.persistent,
329 "unlisted_on_hp": copr.unlisted_on_hp,
330 "auto_prune": copr.auto_prune,
331 }
332 return flask.jsonify(output)
333
334
335 @api_ns.route("/coprs/<username>/<coprname>/new_build/", methods=["POST"])
336 @api_login_required
337 @api_req_with_copr
338 -def copr_new_build(copr):
350 return process_creating_new_build(copr, form, create_new_build)
351
352
353 @api_ns.route("/coprs/<username>/<coprname>/new_build_upload/", methods=["POST"])
367 return process_creating_new_build(copr, form, create_new_build)
368
369
370 @api_ns.route("/coprs/<username>/<coprname>/new_build_pypi/", methods=["POST"])
390 return process_creating_new_build(copr, form, create_new_build)
391
392
393 @api_ns.route("/coprs/<username>/<coprname>/new_build_tito/", methods=["POST"])
410 return process_creating_new_build(copr, form, create_new_build)
411
412
413 @api_ns.route("/coprs/<username>/<coprname>/new_build_mock/", methods=["POST"])
430 return process_creating_new_build(copr, form, create_new_build)
431
432
433 @api_ns.route("/coprs/<username>/<coprname>/new_build_rubygems/", methods=["POST"])
447 return process_creating_new_build(copr, form, create_new_build)
448
449
450 @api_ns.route("/coprs/<username>/<coprname>/new_build_distgit/", methods=["POST"])
465 return process_creating_new_build(copr, form, create_new_build)
466
469 infos = []
470
471
472 infos.extend(validate_post_keys(form))
473
474 if not form.validate_on_submit():
475 raise LegacyApiError("Invalid request: bad request parameters: {0}".format(form.errors))
476
477 if not flask.g.user.can_build_in(copr):
478 raise LegacyApiError("Invalid request: user {} is not allowed to build in the copr: {}"
479 .format(flask.g.user.username, copr.full_name))
480
481
482 try:
483
484
485 build = create_new_build()
486 db.session.commit()
487 ids = [build.id] if type(build) != list else [b.id for b in build]
488 infos.append("Build was added to {0}:".format(copr.name))
489 for build_id in ids:
490 infos.append(" " + flask.url_for("coprs_ns.copr_build_redirect",
491 build_id=build_id,
492 _external=True))
493
494 except (ActionInProgressException, InsufficientRightsException) as e:
495 raise LegacyApiError("Invalid request: {}".format(e))
496
497 output = {"output": "ok",
498 "ids": ids,
499 "message": "\n".join(infos)}
500
501 return flask.jsonify(output)
502
503
504 @api_ns.route("/coprs/build_status/<int:build_id>/", methods=["GET"])
511
512
513 @api_ns.route("/coprs/build_detail/<int:build_id>/", methods=["GET"])
514 @api_ns.route("/coprs/build/<int:build_id>/", methods=["GET"])
516 build = ComplexLogic.get_build_safe(build_id)
517
518 chroots = {}
519 results_by_chroot = {}
520 for chroot in build.build_chroots:
521 chroots[chroot.name] = chroot.state
522 results_by_chroot[chroot.name] = chroot.result_dir_url
523
524 built_packages = None
525 if build.built_packages:
526 built_packages = build.built_packages.split("\n")
527
528 output = {
529 "output": "ok",
530 "status": build.state,
531 "project": build.copr.name,
532 "owner": build.copr.owner_name,
533 "results": build.results,
534 "built_pkgs": built_packages,
535 "src_version": build.pkg_version,
536 "chroots": chroots,
537 "submitted_on": build.submitted_on,
538 "started_on": build.min_started_on,
539 "ended_on": build.max_ended_on,
540 "src_pkg": build.pkgs,
541 "submitted_by": build.user.name,
542 "results_by_chroot": results_by_chroot
543 }
544 return flask.jsonify(output)
545
546
547 @api_ns.route("/coprs/cancel_build/<int:build_id>/", methods=["POST"])
560
561
562 @api_ns.route("/coprs/delete_build/<int:build_id>/", methods=["POST"])
575
576
577 @api_ns.route('/coprs/<username>/<coprname>/modify/', methods=["POST"])
578 @api_login_required
579 @api_req_with_copr
580 -def copr_modify(copr):
623
624
625 @api_ns.route('/coprs/<username>/<coprname>/modify/<chrootname>/', methods=["POST"])
642
643
644 @api_ns.route('/coprs/<username>/<coprname>/chroot/edit/<chrootname>/', methods=["POST"])
645 @api_login_required
646 @api_req_with_copr
647 -def copr_edit_chroot(copr, chrootname):
648 form = forms.ModifyChrootForm(csrf_enabled=False)
649 chroot = ComplexLogic.get_copr_chroot_safe(copr, chrootname)
650
651 if not form.validate_on_submit():
652 raise LegacyApiError("Invalid request: {0}".format(form.errors))
653 else:
654 buildroot_pkgs = repos = comps_xml = comps_name = None
655 if "buildroot_pkgs" in flask.request.form:
656 buildroot_pkgs = form.buildroot_pkgs.data
657 if "repos" in flask.request.form:
658 repos = form.repos.data
659 if form.upload_comps.has_file():
660 comps_xml = form.upload_comps.data.stream.read()
661 comps_name = form.upload_comps.data.filename
662 if form.delete_comps.data:
663 coprs_logic.CoprChrootsLogic.remove_comps(flask.g.user, chroot)
664 coprs_logic.CoprChrootsLogic.update_chroot(
665 flask.g.user, chroot, buildroot_pkgs, repos, comps=comps_xml, comps_name=comps_name)
666 db.session.commit()
667
668 output = {
669 "output": "ok",
670 "message": "Edit chroot operation was successful.",
671 "chroot": chroot.to_dict(),
672 }
673 return flask.jsonify(output)
674
675
676 @api_ns.route('/coprs/<username>/<coprname>/detail/<chrootname>/', methods=["GET"])
683
684 @api_ns.route('/coprs/<username>/<coprname>/chroot/get/<chrootname>/', methods=["GET"])
690
694 """ Return the list of coprs found in search by the given text.
695 project is taken either from GET params or from the URL itself
696 (in this order).
697
698 :arg project: the text one would like find for coprs.
699
700 """
701 project = flask.request.args.get("project", None) or project
702 if not project:
703 raise LegacyApiError("No project found.")
704
705 try:
706 query = CoprsLogic.get_multiple_fulltext(project)
707
708 repos = query.all()
709 output = {"output": "ok", "repos": []}
710 for repo in repos:
711 output["repos"].append({"username": repo.user.name,
712 "coprname": repo.name,
713 "description": repo.description})
714 except ValueError as e:
715 raise LegacyApiError("Server error: {}".format(e))
716
717 return flask.jsonify(output)
718
722 """ Return list of coprs which are part of playground """
723 query = CoprsLogic.get_playground()
724 repos = query.all()
725 output = {"output": "ok", "repos": []}
726 for repo in repos:
727 output["repos"].append({"username": repo.owner_name,
728 "coprname": repo.name,
729 "chroots": [chroot.name for chroot in repo.active_chroots]})
730
731 jsonout = flask.jsonify(output)
732 jsonout.status_code = 200
733 return jsonout
734
735
736 @api_ns.route("/coprs/<username>/<coprname>/monitor/", methods=["GET"])
737 @api_req_with_copr
738 -def monitor(copr):
742
743
744
745 @api_ns.route("/coprs/<username>/<coprname>/package/add/<source_type_text>/", methods=["POST"])
746 @api_login_required
747 @api_req_with_copr
748 -def copr_add_package(copr, source_type_text):
750
751
752 @api_ns.route("/coprs/<username>/<coprname>/package/<package_name>/edit/<source_type_text>/", methods=["POST"])
753 @api_login_required
754 @api_req_with_copr
755 -def copr_edit_package(copr, package_name, source_type_text):
761
793
796 params = {}
797 if flask.request.args.get('with_latest_build'):
798 params['with_latest_build'] = True
799 if flask.request.args.get('with_latest_succeeded_build'):
800 params['with_latest_succeeded_build'] = True
801 if flask.request.args.get('with_all_builds'):
802 params['with_all_builds'] = True
803 return params
804
807 """
808 A lagging generator to stream JSON so we don't have to hold everything in memory
809 This is a little tricky, as we need to omit the last comma to make valid JSON,
810 thus we use a lagging generator, similar to http://stackoverflow.com/questions/1630320/
811 """
812 packages = query.__iter__()
813 try:
814 prev_package = next(packages)
815 except StopIteration:
816
817 yield '{"packages": []}'
818 raise StopIteration
819
820 yield '{"packages": ['
821
822 for package in packages:
823 yield json.dumps(prev_package.to_dict(**params)) + ', '
824 prev_package = package
825
826 yield json.dumps(prev_package.to_dict(**params)) + ']}'
827
828
829 @api_ns.route("/coprs/<username>/<coprname>/package/list/", methods=["GET"])
835
836
837
838 @api_ns.route("/coprs/<username>/<coprname>/package/get/<package_name>/", methods=["GET"])
848
849
850 @api_ns.route("/coprs/<username>/<coprname>/package/delete/<package_name>/", methods=["POST"])
870
871
872 @api_ns.route("/coprs/<username>/<coprname>/package/reset/<package_name>/", methods=["POST"])
873 @api_login_required
874 @api_req_with_copr
875 -def copr_reset_package(copr, package_name):
892
893
894 @api_ns.route("/coprs/<username>/<coprname>/package/build/<package_name>/", methods=["POST"])
895 @api_login_required
896 @api_req_with_copr
897 -def copr_build_package(copr, package_name):
898 form = forms.BuildFormRebuildFactory.create_form_cls(copr.active_chroots)(csrf_enabled=False)
899
900 try:
901 package = PackagesLogic.get(copr.id, package_name)[0]
902 except IndexError:
903 raise LegacyApiError("No package with name {name} in copr {copr}".format(name=package_name, copr=copr.name))
904
905 if form.validate_on_submit():
906 try:
907 build = PackagesLogic.build_package(flask.g.user, copr, package, form.selected_chroots, **form.data)
908 db.session.commit()
909 except (InsufficientRightsException, ActionInProgressException, NoPackageSourceException) as e:
910 raise LegacyApiError(str(e))
911 else:
912 raise LegacyApiError(form.errors)
913
914 return flask.jsonify({
915 "output": "ok",
916 "ids": [build.id],
917 "message": "Build was added to {0}.".format(copr.name)
918 })
919
920
921 @api_ns.route("/module/build/", methods=["POST"])
924 form = forms.ModuleBuildForm(csrf_enabled=False)
925 if not form.validate_on_submit():
926 raise LegacyApiError(form.errors)
927
928 try:
929 common = {"owner": flask.g.user.name,
930 "copr_owner": form.copr_owner.data,
931 "copr_project": form.copr_project.data}
932 if form.scmurl.data:
933 kwargs = {"json": dict({"scmurl": form.scmurl.data, "branch": form.branch.data}, **common)}
934 else:
935 kwargs = {"data": common, "files": {"yaml": (form.modulemd.data.filename, form.modulemd.data)}}
936
937 response = requests.post(flask.current_app.config["MBS_URL"], verify=False, **kwargs)
938 if response.status_code == 500:
939 raise LegacyApiError("Error from MBS: {} - {}".format(response.status_code, response.reason))
940
941 resp = json.loads(response.content)
942 if response.status_code != 201:
943 raise LegacyApiError("Error from MBS: {}".format(resp["message"]))
944
945 return flask.jsonify({
946 "output": "ok",
947 "message": "Created module {}-{}-{}".format(resp["name"], resp["stream"], resp["version"]),
948 })
949
950 except requests.ConnectionError:
951 raise LegacyApiError("Can't connect to MBS instance")
952
953
954 @api_ns.route("/coprs/<username>/<coprname>/module/make/", methods=["POST"])
958 form = forms.ModuleFormUploadFactory(csrf_enabled=False)
959 if not form.validate_on_submit():
960
961 raise LegacyApiError(form.errors)
962
963 modulemd = form.modulemd.data.read()
964 module = ModulesLogic.from_modulemd(modulemd)
965 try:
966 ModulesLogic.validate(modulemd)
967 msg = "Nothing happened"
968 if form.create.data:
969 module = ModulesLogic.add(flask.g.user, copr, module)
970 db.session.flush()
971 msg = "Module was created"
972
973 if form.build.data:
974 if not module.id:
975 module = ModulesLogic.get_by_nsv(copr, module.name, module.stream, module.version).one()
976 ActionsLogic.send_build_module(flask.g.user, copr, module)
977 msg = "Module build was submitted"
978 db.session.commit()
979
980 return flask.jsonify({
981 "output": "ok",
982 "message": msg,
983 "modulemd": modulemd,
984 })
985
986 except sqlalchemy.exc.IntegrityError:
987 raise LegacyApiError({"nsv": ["Module {} already exists".format(module.nsv)]})
988
989 except sqlalchemy.orm.exc.NoResultFound:
990 raise LegacyApiError({"nsv": ["Module {} doesn't exist. You need to create it first".format(module.nsv)]})
991
992 except ValidationError as ex:
993 raise LegacyApiError({"nsv": [ex.message]})
994
995
996 @api_ns.route("/coprs/<username>/<coprname>/build-config/<chroot>/", methods=["GET"])
997 @api_ns.route("/g/<group_name>/<coprname>/build-config/<chroot>/", methods=["GET"])
1000 """
1001 Generate build configuration.
1002 """
1003 output = {
1004 "output": "ok",
1005 "build_config": generate_build_config(copr, chroot),
1006 }
1007
1008 if not output['build_config']:
1009 raise LegacyApiError('Chroot not found.')
1010
1011 return flask.jsonify(output)
1012
1013
1014 @api_ns.route("/module/repo/", methods=["POST"])
1030