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"])
431 return process_creating_new_build(copr, form, create_new_build)
432
433
434 @api_ns.route("/coprs/<username>/<coprname>/new_build_rubygems/", methods=["POST"])
448 return process_creating_new_build(copr, form, create_new_build)
449
450
451 @api_ns.route("/coprs/<username>/<coprname>/new_build_distgit/", methods=["POST"])
466 return process_creating_new_build(copr, form, create_new_build)
467
470 infos = []
471
472
473 infos.extend(validate_post_keys(form))
474
475 if not form.validate_on_submit():
476 raise LegacyApiError("Invalid request: bad request parameters: {0}".format(form.errors))
477
478 if not flask.g.user.can_build_in(copr):
479 raise LegacyApiError("Invalid request: user {} is not allowed to build in the copr: {}"
480 .format(flask.g.user.username, copr.full_name))
481
482
483 try:
484
485
486 build = create_new_build()
487 db.session.commit()
488 ids = [build.id] if type(build) != list else [b.id for b in build]
489 infos.append("Build was added to {0}:".format(copr.name))
490 for build_id in ids:
491 infos.append(" " + flask.url_for("coprs_ns.copr_build_redirect",
492 build_id=build_id,
493 _external=True))
494
495 except (ActionInProgressException, InsufficientRightsException) as e:
496 raise LegacyApiError("Invalid request: {}".format(e))
497
498 output = {"output": "ok",
499 "ids": ids,
500 "message": "\n".join(infos)}
501
502 return flask.jsonify(output)
503
504
505 @api_ns.route("/coprs/build_status/<int:build_id>/", methods=["GET"])
512
513
514 @api_ns.route("/coprs/build_detail/<int:build_id>/", methods=["GET"])
515 @api_ns.route("/coprs/build/<int:build_id>/", methods=["GET"])
517 build = ComplexLogic.get_build_safe(build_id)
518
519 chroots = {}
520 results_by_chroot = {}
521 for chroot in build.build_chroots:
522 chroots[chroot.name] = chroot.state
523 results_by_chroot[chroot.name] = chroot.result_dir_url
524
525 built_packages = None
526 if build.built_packages:
527 built_packages = build.built_packages.split("\n")
528
529 output = {
530 "output": "ok",
531 "status": build.state,
532 "project": build.copr.name,
533 "owner": build.copr.owner_name,
534 "results": build.results,
535 "built_pkgs": built_packages,
536 "src_version": build.pkg_version,
537 "chroots": chroots,
538 "submitted_on": build.submitted_on,
539 "started_on": build.min_started_on,
540 "ended_on": build.max_ended_on,
541 "src_pkg": build.pkgs,
542 "submitted_by": build.user.name,
543 "results_by_chroot": results_by_chroot
544 }
545 return flask.jsonify(output)
546
547
548 @api_ns.route("/coprs/cancel_build/<int:build_id>/", methods=["POST"])
561
562
563 @api_ns.route("/coprs/delete_build/<int:build_id>/", methods=["POST"])
576
577
578 @api_ns.route('/coprs/<username>/<coprname>/modify/', methods=["POST"])
579 @api_login_required
580 @api_req_with_copr
581 -def copr_modify(copr):
624
625
626 @api_ns.route('/coprs/<username>/<coprname>/modify/<chrootname>/', methods=["POST"])
643
644
645 @api_ns.route('/coprs/<username>/<coprname>/chroot/edit/<chrootname>/', methods=["POST"])
646 @api_login_required
647 @api_req_with_copr
648 -def copr_edit_chroot(copr, chrootname):
649 form = forms.ModifyChrootForm(csrf_enabled=False)
650 chroot = ComplexLogic.get_copr_chroot_safe(copr, chrootname)
651
652 if not form.validate_on_submit():
653 raise LegacyApiError("Invalid request: {0}".format(form.errors))
654 else:
655 buildroot_pkgs = repos = comps_xml = comps_name = None
656 if "buildroot_pkgs" in flask.request.form:
657 buildroot_pkgs = form.buildroot_pkgs.data
658 if "repos" in flask.request.form:
659 repos = form.repos.data
660 if form.upload_comps.has_file():
661 comps_xml = form.upload_comps.data.stream.read()
662 comps_name = form.upload_comps.data.filename
663 if form.delete_comps.data:
664 coprs_logic.CoprChrootsLogic.remove_comps(flask.g.user, chroot)
665 coprs_logic.CoprChrootsLogic.update_chroot(
666 flask.g.user, chroot, buildroot_pkgs, repos, comps=comps_xml, comps_name=comps_name)
667 db.session.commit()
668
669 output = {
670 "output": "ok",
671 "message": "Edit chroot operation was successful.",
672 "chroot": chroot.to_dict(),
673 }
674 return flask.jsonify(output)
675
676
677 @api_ns.route('/coprs/<username>/<coprname>/detail/<chrootname>/', methods=["GET"])
684
685 @api_ns.route('/coprs/<username>/<coprname>/chroot/get/<chrootname>/', methods=["GET"])
691
695 """ Return the list of coprs found in search by the given text.
696 project is taken either from GET params or from the URL itself
697 (in this order).
698
699 :arg project: the text one would like find for coprs.
700
701 """
702 project = flask.request.args.get("project", None) or project
703 if not project:
704 raise LegacyApiError("No project found.")
705
706 try:
707 query = CoprsLogic.get_multiple_fulltext(project)
708
709 repos = query.all()
710 output = {"output": "ok", "repos": []}
711 for repo in repos:
712 output["repos"].append({"username": repo.user.name,
713 "coprname": repo.name,
714 "description": repo.description})
715 except ValueError as e:
716 raise LegacyApiError("Server error: {}".format(e))
717
718 return flask.jsonify(output)
719
723 """ Return list of coprs which are part of playground """
724 query = CoprsLogic.get_playground()
725 repos = query.all()
726 output = {"output": "ok", "repos": []}
727 for repo in repos:
728 output["repos"].append({"username": repo.owner_name,
729 "coprname": repo.name,
730 "chroots": [chroot.name for chroot in repo.active_chroots]})
731
732 jsonout = flask.jsonify(output)
733 jsonout.status_code = 200
734 return jsonout
735
736
737 @api_ns.route("/coprs/<username>/<coprname>/monitor/", methods=["GET"])
738 @api_req_with_copr
739 -def monitor(copr):
743
744
745
746 @api_ns.route("/coprs/<username>/<coprname>/package/add/<source_type_text>/", methods=["POST"])
747 @api_login_required
748 @api_req_with_copr
749 -def copr_add_package(copr, source_type_text):
751
752
753 @api_ns.route("/coprs/<username>/<coprname>/package/<package_name>/edit/<source_type_text>/", methods=["POST"])
754 @api_login_required
755 @api_req_with_copr
756 -def copr_edit_package(copr, package_name, source_type_text):
762
794
797 params = {}
798 if flask.request.args.get('with_latest_build'):
799 params['with_latest_build'] = True
800 if flask.request.args.get('with_latest_succeeded_build'):
801 params['with_latest_succeeded_build'] = True
802 if flask.request.args.get('with_all_builds'):
803 params['with_all_builds'] = True
804 return params
805
808 """
809 A lagging generator to stream JSON so we don't have to hold everything in memory
810 This is a little tricky, as we need to omit the last comma to make valid JSON,
811 thus we use a lagging generator, similar to http://stackoverflow.com/questions/1630320/
812 """
813 packages = query.__iter__()
814 try:
815 prev_package = next(packages)
816 except StopIteration:
817
818 yield '{"packages": []}'
819 raise StopIteration
820
821 yield '{"packages": ['
822
823 for package in packages:
824 yield json.dumps(prev_package.to_dict(**params)) + ', '
825 prev_package = package
826
827 yield json.dumps(prev_package.to_dict(**params)) + ']}'
828
829
830 @api_ns.route("/coprs/<username>/<coprname>/package/list/", methods=["GET"])
836
837
838
839 @api_ns.route("/coprs/<username>/<coprname>/package/get/<package_name>/", methods=["GET"])
849
850
851 @api_ns.route("/coprs/<username>/<coprname>/package/delete/<package_name>/", methods=["POST"])
871
872
873 @api_ns.route("/coprs/<username>/<coprname>/package/reset/<package_name>/", methods=["POST"])
874 @api_login_required
875 @api_req_with_copr
876 -def copr_reset_package(copr, package_name):
893
894
895 @api_ns.route("/coprs/<username>/<coprname>/package/build/<package_name>/", methods=["POST"])
896 @api_login_required
897 @api_req_with_copr
898 -def copr_build_package(copr, package_name):
899 form = forms.BuildFormRebuildFactory.create_form_cls(copr.active_chroots)(csrf_enabled=False)
900
901 try:
902 package = PackagesLogic.get(copr.id, package_name)[0]
903 except IndexError:
904 raise LegacyApiError("No package with name {name} in copr {copr}".format(name=package_name, copr=copr.name))
905
906 if form.validate_on_submit():
907 try:
908 build = PackagesLogic.build_package(flask.g.user, copr, package, form.selected_chroots, **form.data)
909 db.session.commit()
910 except (InsufficientRightsException, ActionInProgressException, NoPackageSourceException) as e:
911 raise LegacyApiError(str(e))
912 else:
913 raise LegacyApiError(form.errors)
914
915 return flask.jsonify({
916 "output": "ok",
917 "ids": [build.id],
918 "message": "Build was added to {0}.".format(copr.name)
919 })
920
921
922 @api_ns.route("/module/build/", methods=["POST"])
925 form = forms.ModuleBuildForm(csrf_enabled=False)
926 if not form.validate_on_submit():
927 raise LegacyApiError(form.errors)
928
929 try:
930 common = {"owner": flask.g.user.name,
931 "copr_owner": form.copr_owner.data,
932 "copr_project": form.copr_project.data}
933 if form.scmurl.data:
934 kwargs = {"json": dict({"scmurl": form.scmurl.data, "branch": form.branch.data}, **common)}
935 else:
936 kwargs = {"data": common, "files": {"yaml": (form.modulemd.data.filename, form.modulemd.data)}}
937
938 response = requests.post(flask.current_app.config["MBS_URL"], verify=False, **kwargs)
939 if response.status_code == 500:
940 raise LegacyApiError("Error from MBS: {} - {}".format(response.status_code, response.reason))
941
942 resp = json.loads(response.content)
943 if response.status_code != 201:
944 raise LegacyApiError("Error from MBS: {}".format(resp["message"]))
945
946 return flask.jsonify({
947 "output": "ok",
948 "message": "Created module {}-{}-{}".format(resp["name"], resp["stream"], resp["version"]),
949 })
950
951 except requests.ConnectionError:
952 raise LegacyApiError("Can't connect to MBS instance")
953
954
955 @api_ns.route("/coprs/<username>/<coprname>/module/make/", methods=["POST"])
959 form = forms.ModuleFormUploadFactory(csrf_enabled=False)
960 if not form.validate_on_submit():
961
962 raise LegacyApiError(form.errors)
963
964 modulemd = form.modulemd.data.read()
965 module = ModulesLogic.from_modulemd(modulemd)
966 try:
967 ModulesLogic.validate(modulemd)
968 msg = "Nothing happened"
969 if form.create.data:
970 module = ModulesLogic.add(flask.g.user, copr, module)
971 db.session.flush()
972 msg = "Module was created"
973
974 if form.build.data:
975 if not module.id:
976 module = ModulesLogic.get_by_nsv(copr, module.name, module.stream, module.version).one()
977 ActionsLogic.send_build_module(flask.g.user, copr, module)
978 msg = "Module build was submitted"
979 db.session.commit()
980
981 return flask.jsonify({
982 "output": "ok",
983 "message": msg,
984 "modulemd": modulemd,
985 })
986
987 except sqlalchemy.exc.IntegrityError:
988 raise LegacyApiError({"nsv": ["Module {} already exists".format(module.nsv)]})
989
990 except sqlalchemy.orm.exc.NoResultFound:
991 raise LegacyApiError({"nsv": ["Module {} doesn't exist. You need to create it first".format(module.nsv)]})
992
993 except ValidationError as ex:
994 raise LegacyApiError({"nsv": [ex.message]})
995
996
997 @api_ns.route("/coprs/<username>/<coprname>/build-config/<chroot>/", methods=["GET"])
998 @api_ns.route("/g/<group_name>/<coprname>/build-config/<chroot>/", methods=["GET"])
1001 """
1002 Generate build configuration.
1003 """
1004 output = {
1005 "output": "ok",
1006 "build_config": generate_build_config(copr, chroot),
1007 }
1008
1009 if not output['build_config']:
1010 raise LegacyApiError('Chroot not found.')
1011
1012 return flask.jsonify(output)
1013
1014
1015 @api_ns.route("/module/repo/", methods=["POST"])
1031