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("/status")
102 -def api_status():
112
113
114 @api_ns.route("/coprs/<username>/new/", methods=["POST"])
117 """
118 Receive information from the user on how to create its new copr,
119 check their validity and create the corresponding copr.
120
121 :arg name: the name of the copr to add
122 :arg chroots: a comma separated list of chroots to use
123 :kwarg repos: a comma separated list of repository that this copr
124 can use.
125 :kwarg initial_pkgs: a comma separated list of initial packages to
126 build in this new copr
127
128 """
129
130 form = forms.CoprFormFactory.create_form_cls()(csrf_enabled=False)
131 infos = []
132
133
134 infos.extend(validate_post_keys(form))
135
136 if form.validate_on_submit():
137 group = ComplexLogic.get_group_by_name_safe(username[1:]) if username[0] == "@" else None
138
139 auto_prune = True
140 if "auto_prune" in flask.request.form:
141 auto_prune = form.auto_prune.data
142
143 try:
144 copr = CoprsLogic.add(
145 name=form.name.data.strip(),
146 repos=" ".join(form.repos.data.split()),
147 user=flask.g.user,
148 selected_chroots=form.selected_chroots,
149 description=form.description.data,
150 instructions=form.instructions.data,
151 check_for_duplicates=True,
152 disable_createrepo=form.disable_createrepo.data,
153 unlisted_on_hp=form.unlisted_on_hp.data,
154 build_enable_net=form.build_enable_net.data,
155 group=group,
156 persistent=form.persistent.data,
157 auto_prune=auto_prune,
158 )
159 infos.append("New project was successfully created.")
160
161 if form.initial_pkgs.data:
162 pkgs = form.initial_pkgs.data.split()
163 for pkg in pkgs:
164 builds_logic.BuildsLogic.add(
165 user=flask.g.user,
166 pkgs=pkg,
167 copr=copr)
168
169 infos.append("Initial packages were successfully "
170 "submitted for building.")
171
172 output = {"output": "ok", "message": "\n".join(infos)}
173 db.session.commit()
174 except (exceptions.DuplicateException,
175 exceptions.NonAdminCannotCreatePersistentProject,
176 exceptions.NonAdminCannotDisableAutoPrunning) as err:
177 db.session.rollback()
178 raise LegacyApiError(str(err))
179
180 else:
181 errormsg = "Validation error\n"
182 if form.errors:
183 for field, emsgs in form.errors.items():
184 errormsg += "- {0}: {1}\n".format(field, "\n".join(emsgs))
185
186 errormsg = errormsg.replace('"', "'")
187 raise LegacyApiError(errormsg)
188
189 return flask.jsonify(output)
190
191
192 @api_ns.route("/coprs/<username>/<coprname>/delete/", methods=["POST"])
217
218
219 @api_ns.route("/coprs/<username>/<coprname>/fork/", methods=["POST"])
220 @api_login_required
221 @api_req_with_copr
222 -def api_copr_fork(copr):
223 """ Fork the project and builds in it
224 """
225 form = forms.CoprForkFormFactory\
226 .create_form_cls(copr=copr, user=flask.g.user, groups=flask.g.user.user_groups)(csrf_enabled=False)
227
228 if form.validate_on_submit() and copr:
229 try:
230 dstgroup = ([g for g in flask.g.user.user_groups if g.at_name == form.owner.data] or [None])[0]
231 if flask.g.user.name != form.owner.data and not dstgroup:
232 return LegacyApiError("There is no such group: {}".format(form.owner.data))
233
234 fcopr, created = ComplexLogic.fork_copr(copr, flask.g.user, dstname=form.name.data, dstgroup=dstgroup)
235 if created:
236 msg = ("Forking project {} for you into {}.\nPlease be aware that it may take a few minutes "
237 "to duplicate a backend data.".format(copr.full_name, fcopr.full_name))
238 elif not created and form.confirm.data == True:
239 msg = ("Updating packages in {} from {}.\nPlease be aware that it may take a few minutes "
240 "to duplicate a backend data.".format(copr.full_name, fcopr.full_name))
241 else:
242 raise LegacyApiError("You are about to fork into existing project: {}\n"
243 "Please use --confirm if you really want to do this".format(fcopr.full_name))
244
245 output = {"output": "ok", "message": msg}
246 db.session.commit()
247
248 except (exceptions.ActionInProgressException,
249 exceptions.InsufficientRightsException) as err:
250 db.session.rollback()
251 raise LegacyApiError(str(err))
252 else:
253 raise LegacyApiError("Invalid request: {0}".format(form.errors))
254
255 return flask.jsonify(output)
256
257
258 @api_ns.route("/coprs/")
259 @api_ns.route("/coprs/<username>/")
260 -def api_coprs_by_owner(username=None):
261 """ Return the list of coprs owned by the given user.
262 username is taken either from GET params or from the URL itself
263 (in this order).
264
265 :arg username: the username of the person one would like to the
266 coprs of.
267
268 """
269 username = flask.request.args.get("username", None) or username
270 if username is None:
271 raise LegacyApiError("Invalid request: missing `username` ")
272
273 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
274
275 if username.startswith("@"):
276 group_name = username[1:]
277 query = CoprsLogic.get_multiple()
278 query = CoprsLogic.filter_by_group_name(query, group_name)
279 else:
280 query = CoprsLogic.get_multiple_owned_by_username(username)
281
282 query = CoprsLogic.join_builds(query)
283 query = CoprsLogic.set_query_order(query)
284
285 repos = query.all()
286 output = {"output": "ok", "repos": []}
287 for repo in repos:
288 yum_repos = {}
289 for build in repo.builds:
290 if build.results:
291 for chroot in repo.active_chroots:
292 release = release_tmpl.format(chroot=chroot)
293 yum_repos[release] = fix_protocol_for_backend(
294 os.path.join(build.results, release + '/'))
295 break
296
297 output["repos"].append({"name": repo.name,
298 "additional_repos": repo.repos,
299 "yum_repos": yum_repos,
300 "description": repo.description,
301 "instructions": repo.instructions,
302 "persistent": repo.persistent,
303 "unlisted_on_hp": repo.unlisted_on_hp,
304 "auto_prune": repo.auto_prune,
305 })
306
307 return flask.jsonify(output)
308
313 """ Return detail of one project.
314
315 :arg username: the username of the person one would like to the
316 coprs of.
317 :arg coprname: the name of project.
318
319 """
320 release_tmpl = "{chroot.os_release}-{chroot.os_version}-{chroot.arch}"
321 output = {"output": "ok", "detail": {}}
322 yum_repos = {}
323
324 build = models.Build.query.filter(
325 models.Build.copr_id == copr.id, models.Build.results != None).first()
326
327 if build:
328 for chroot in copr.active_chroots:
329 release = release_tmpl.format(chroot=chroot)
330 yum_repos[release] = fix_protocol_for_backend(
331 os.path.join(build.results, release + '/'))
332
333 output["detail"] = {
334 "name": copr.name,
335 "additional_repos": copr.repos,
336 "yum_repos": yum_repos,
337 "description": copr.description,
338 "instructions": copr.instructions,
339 "last_modified": builds_logic.BuildsLogic.last_modified(copr),
340 "auto_createrepo": copr.auto_createrepo,
341 "persistent": copr.persistent,
342 "unlisted_on_hp": copr.unlisted_on_hp,
343 "auto_prune": copr.auto_prune,
344 }
345 return flask.jsonify(output)
346
347
348 @api_ns.route("/coprs/<username>/<coprname>/new_build/", methods=["POST"])
349 @api_login_required
350 @api_req_with_copr
351 -def copr_new_build(copr):
363 return process_creating_new_build(copr, form, create_new_build)
364
365
366 @api_ns.route("/coprs/<username>/<coprname>/new_build_upload/", methods=["POST"])
380 return process_creating_new_build(copr, form, create_new_build)
381
382
383 @api_ns.route("/coprs/<username>/<coprname>/new_build_pypi/", methods=["POST"])
403 return process_creating_new_build(copr, form, create_new_build)
404
405
406 @api_ns.route("/coprs/<username>/<coprname>/new_build_tito/", methods=["POST"])
423 return process_creating_new_build(copr, form, create_new_build)
424
425
426 @api_ns.route("/coprs/<username>/<coprname>/new_build_mock/", methods=["POST"])
444 return process_creating_new_build(copr, form, create_new_build)
445
446
447 @api_ns.route("/coprs/<username>/<coprname>/new_build_rubygems/", methods=["POST"])
461 return process_creating_new_build(copr, form, create_new_build)
462
463
464 @api_ns.route("/coprs/<username>/<coprname>/new_build_distgit/", methods=["POST"])
479 return process_creating_new_build(copr, form, create_new_build)
480
483 infos = []
484
485
486 infos.extend(validate_post_keys(form))
487
488 if not form.validate_on_submit():
489 raise LegacyApiError("Invalid request: bad request parameters: {0}".format(form.errors))
490
491 if not flask.g.user.can_build_in(copr):
492 raise LegacyApiError("Invalid request: user {} is not allowed to build in the copr: {}"
493 .format(flask.g.user.username, copr.full_name))
494
495
496 try:
497
498
499 build = create_new_build()
500 db.session.commit()
501 ids = [build.id] if type(build) != list else [b.id for b in build]
502 infos.append("Build was added to {0}:".format(copr.name))
503 for build_id in ids:
504 infos.append(" " + flask.url_for("coprs_ns.copr_build_redirect",
505 build_id=build_id,
506 _external=True))
507
508 except (ActionInProgressException, InsufficientRightsException) as e:
509 raise LegacyApiError("Invalid request: {}".format(e))
510
511 output = {"output": "ok",
512 "ids": ids,
513 "message": "\n".join(infos)}
514
515 return flask.jsonify(output)
516
517
518 @api_ns.route("/coprs/build_status/<int:build_id>/", methods=["GET"])
525
526
527 @api_ns.route("/coprs/build_detail/<int:build_id>/", methods=["GET"])
528 @api_ns.route("/coprs/build/<int:build_id>/", methods=["GET"])
530 build = ComplexLogic.get_build_safe(build_id)
531
532 chroots = {}
533 results_by_chroot = {}
534 for chroot in build.build_chroots:
535 chroots[chroot.name] = chroot.state
536 results_by_chroot[chroot.name] = chroot.result_dir_url
537
538 built_packages = None
539 if build.built_packages:
540 built_packages = build.built_packages.split("\n")
541
542 output = {
543 "output": "ok",
544 "status": build.state,
545 "project": build.copr.name,
546 "owner": build.copr.owner_name,
547 "results": build.results,
548 "built_pkgs": built_packages,
549 "src_version": build.pkg_version,
550 "chroots": chroots,
551 "submitted_on": build.submitted_on,
552 "started_on": build.min_started_on,
553 "ended_on": build.max_ended_on,
554 "src_pkg": build.pkgs,
555 "submitted_by": build.user.name if build.user else None,
556 "results_by_chroot": results_by_chroot
557 }
558 return flask.jsonify(output)
559
560
561 @api_ns.route("/coprs/cancel_build/<int:build_id>/", methods=["POST"])
574
575
576 @api_ns.route("/coprs/delete_build/<int:build_id>/", methods=["POST"])
589
590
591 @api_ns.route('/coprs/<username>/<coprname>/modify/', methods=["POST"])
592 @api_login_required
593 @api_req_with_copr
594 -def copr_modify(copr):
595 form = forms.CoprModifyForm(csrf_enabled=False)
596
597 if not form.validate_on_submit():
598 raise LegacyApiError("Invalid request: {0}".format(form.errors))
599
600
601
602 if form.description.raw_data and len(form.description.raw_data):
603 copr.description = form.description.data
604 if form.instructions.raw_data and len(form.instructions.raw_data):
605 copr.instructions = form.instructions.data
606 if form.repos.raw_data and len(form.repos.raw_data):
607 copr.repos = form.repos.data
608 if form.disable_createrepo.raw_data and len(form.disable_createrepo.raw_data):
609 copr.disable_createrepo = form.disable_createrepo.data
610
611 if "unlisted_on_hp" in flask.request.form:
612 copr.unlisted_on_hp = form.unlisted_on_hp.data
613 if "build_enable_net" in flask.request.form:
614 copr.build_enable_net = form.build_enable_net.data
615 if "auto_prune" in flask.request.form:
616 copr.auto_prune = form.auto_prune.data
617 if "chroots" in flask.request.form:
618 coprs_logic.CoprChrootsLogic.update_from_names(
619 flask.g.user, copr, form.chroots.data)
620
621 try:
622 CoprsLogic.update(flask.g.user, copr)
623 if copr.group:
624 _ = copr.group.id
625 db.session.commit()
626 except (exceptions.ActionInProgressException,
627 exceptions.InsufficientRightsException,
628 exceptions.NonAdminCannotDisableAutoPrunning) as e:
629 db.session.rollback()
630 raise LegacyApiError("Invalid request: {}".format(e))
631
632 output = {
633 'output': 'ok',
634 'description': copr.description,
635 'instructions': copr.instructions,
636 'repos': copr.repos,
637 'chroots': [c.name for c in copr.mock_chroots],
638 }
639
640 return flask.jsonify(output)
641
642
643 @api_ns.route('/coprs/<username>/<coprname>/modify/<chrootname>/', methods=["POST"])
660
661
662 @api_ns.route('/coprs/<username>/<coprname>/chroot/edit/<chrootname>/', methods=["POST"])
663 @api_login_required
664 @api_req_with_copr
665 -def copr_edit_chroot(copr, chrootname):
666 form = forms.ModifyChrootForm(csrf_enabled=False)
667 chroot = ComplexLogic.get_copr_chroot_safe(copr, chrootname)
668
669 if not form.validate_on_submit():
670 raise LegacyApiError("Invalid request: {0}".format(form.errors))
671 else:
672 buildroot_pkgs = repos = comps_xml = comps_name = None
673 if "buildroot_pkgs" in flask.request.form:
674 buildroot_pkgs = form.buildroot_pkgs.data
675 if "repos" in flask.request.form:
676 repos = form.repos.data
677 if form.upload_comps.has_file():
678 comps_xml = form.upload_comps.data.stream.read()
679 comps_name = form.upload_comps.data.filename
680 if form.delete_comps.data:
681 coprs_logic.CoprChrootsLogic.remove_comps(flask.g.user, chroot)
682 coprs_logic.CoprChrootsLogic.update_chroot(
683 flask.g.user, chroot, buildroot_pkgs, repos, comps=comps_xml, comps_name=comps_name)
684 db.session.commit()
685
686 output = {
687 "output": "ok",
688 "message": "Edit chroot operation was successful.",
689 "chroot": chroot.to_dict(),
690 }
691 return flask.jsonify(output)
692
693
694 @api_ns.route('/coprs/<username>/<coprname>/detail/<chrootname>/', methods=["GET"])
701
702 @api_ns.route('/coprs/<username>/<coprname>/chroot/get/<chrootname>/', methods=["GET"])
708
712 """ Return the list of coprs found in search by the given text.
713 project is taken either from GET params or from the URL itself
714 (in this order).
715
716 :arg project: the text one would like find for coprs.
717
718 """
719 project = flask.request.args.get("project", None) or project
720 if not project:
721 raise LegacyApiError("No project found.")
722
723 try:
724 query = CoprsLogic.get_multiple_fulltext(project)
725
726 repos = query.all()
727 output = {"output": "ok", "repos": []}
728 for repo in repos:
729 output["repos"].append({"username": repo.user.name,
730 "coprname": repo.name,
731 "description": repo.description})
732 except ValueError as e:
733 raise LegacyApiError("Server error: {}".format(e))
734
735 return flask.jsonify(output)
736
740 """ Return list of coprs which are part of playground """
741 query = CoprsLogic.get_playground()
742 repos = query.all()
743 output = {"output": "ok", "repos": []}
744 for repo in repos:
745 output["repos"].append({"username": repo.owner_name,
746 "coprname": repo.name,
747 "chroots": [chroot.name for chroot in repo.active_chroots]})
748
749 jsonout = flask.jsonify(output)
750 jsonout.status_code = 200
751 return jsonout
752
753
754 @api_ns.route("/coprs/<username>/<coprname>/monitor/", methods=["GET"])
755 @api_req_with_copr
756 -def monitor(copr):
760
761
762
763 @api_ns.route("/coprs/<username>/<coprname>/package/add/<source_type_text>/", methods=["POST"])
764 @api_login_required
765 @api_req_with_copr
766 -def copr_add_package(copr, source_type_text):
768
769
770 @api_ns.route("/coprs/<username>/<coprname>/package/<package_name>/edit/<source_type_text>/", methods=["POST"])
771 @api_login_required
772 @api_req_with_copr
773 -def copr_edit_package(copr, package_name, source_type_text):
779
811
814 params = {}
815 if flask.request.args.get('with_latest_build'):
816 params['with_latest_build'] = True
817 if flask.request.args.get('with_latest_succeeded_build'):
818 params['with_latest_succeeded_build'] = True
819 if flask.request.args.get('with_all_builds'):
820 params['with_all_builds'] = True
821 return params
822
825 """
826 A lagging generator to stream JSON so we don't have to hold everything in memory
827 This is a little tricky, as we need to omit the last comma to make valid JSON,
828 thus we use a lagging generator, similar to http://stackoverflow.com/questions/1630320/
829 """
830 packages = query.__iter__()
831 try:
832 prev_package = next(packages)
833 except StopIteration:
834
835 yield '{"packages": []}'
836 raise StopIteration
837
838 yield '{"packages": ['
839
840 for package in packages:
841 yield json.dumps(prev_package.to_dict(**params)) + ', '
842 prev_package = package
843
844 yield json.dumps(prev_package.to_dict(**params)) + ']}'
845
846
847 @api_ns.route("/coprs/<username>/<coprname>/package/list/", methods=["GET"])
853
854
855
856 @api_ns.route("/coprs/<username>/<coprname>/package/get/<package_name>/", methods=["GET"])
866
867
868 @api_ns.route("/coprs/<username>/<coprname>/package/delete/<package_name>/", methods=["POST"])
888
889
890 @api_ns.route("/coprs/<username>/<coprname>/package/reset/<package_name>/", methods=["POST"])
891 @api_login_required
892 @api_req_with_copr
893 -def copr_reset_package(copr, package_name):
910
911
912 @api_ns.route("/coprs/<username>/<coprname>/package/build/<package_name>/", methods=["POST"])
913 @api_login_required
914 @api_req_with_copr
915 -def copr_build_package(copr, package_name):
916 form = forms.BuildFormRebuildFactory.create_form_cls(copr.active_chroots)(csrf_enabled=False)
917
918 try:
919 package = PackagesLogic.get(copr.id, package_name)[0]
920 except IndexError:
921 raise LegacyApiError("No package with name {name} in copr {copr}".format(name=package_name, copr=copr.name))
922
923 if form.validate_on_submit():
924 try:
925 build = PackagesLogic.build_package(flask.g.user, copr, package, form.selected_chroots, **form.data)
926 db.session.commit()
927 except (InsufficientRightsException, ActionInProgressException, NoPackageSourceException) as e:
928 raise LegacyApiError(str(e))
929 else:
930 raise LegacyApiError(form.errors)
931
932 return flask.jsonify({
933 "output": "ok",
934 "ids": [build.id],
935 "message": "Build was added to {0}.".format(copr.name)
936 })
937
938
939 @api_ns.route("/module/build/", methods=["POST"])
942 form = forms.ModuleBuildForm(csrf_enabled=False)
943 if not form.validate_on_submit():
944 raise LegacyApiError(form.errors)
945
946 try:
947 common = {"owner": flask.g.user.name,
948 "copr_owner": form.copr_owner.data,
949 "copr_project": form.copr_project.data}
950 if form.scmurl.data:
951 kwargs = {"json": dict({"scmurl": form.scmurl.data, "branch": form.branch.data}, **common)}
952 else:
953 kwargs = {"data": common, "files": {"yaml": (form.modulemd.data.filename, form.modulemd.data)}}
954
955 response = requests.post(flask.current_app.config["MBS_URL"], verify=False, **kwargs)
956 if response.status_code == 500:
957 raise LegacyApiError("Error from MBS: {} - {}".format(response.status_code, response.reason))
958
959 resp = json.loads(response.content)
960 if response.status_code != 201:
961 raise LegacyApiError("Error from MBS: {}".format(resp["message"]))
962
963 return flask.jsonify({
964 "output": "ok",
965 "message": "Created module {}-{}-{}".format(resp["name"], resp["stream"], resp["version"]),
966 })
967
968 except requests.ConnectionError:
969 raise LegacyApiError("Can't connect to MBS instance")
970
971
972 @api_ns.route("/coprs/<username>/<coprname>/module/make/", methods=["POST"])
976 form = forms.ModuleFormUploadFactory(csrf_enabled=False)
977 if not form.validate_on_submit():
978
979 raise LegacyApiError(form.errors)
980
981 modulemd = form.modulemd.data.read()
982 module = ModulesLogic.from_modulemd(modulemd)
983 try:
984 ModulesLogic.validate(modulemd)
985 msg = "Nothing happened"
986 if form.create.data:
987 module = ModulesLogic.add(flask.g.user, copr, module)
988 db.session.flush()
989 msg = "Module was created"
990
991 if form.build.data:
992 if not module.id:
993 module = ModulesLogic.get_by_nsv(copr, module.name, module.stream, module.version).one()
994 ActionsLogic.send_build_module(flask.g.user, copr, module)
995 msg = "Module build was submitted"
996 db.session.commit()
997
998 return flask.jsonify({
999 "output": "ok",
1000 "message": msg,
1001 "modulemd": modulemd,
1002 })
1003
1004 except sqlalchemy.exc.IntegrityError:
1005 raise LegacyApiError({"nsv": ["Module {} already exists".format(module.nsv)]})
1006
1007 except sqlalchemy.orm.exc.NoResultFound:
1008 raise LegacyApiError({"nsv": ["Module {} doesn't exist. You need to create it first".format(module.nsv)]})
1009
1010 except ValidationError as ex:
1011 raise LegacyApiError({"nsv": [ex.message]})
1012
1013
1014 @api_ns.route("/coprs/<username>/<coprname>/build-config/<chroot>/", methods=["GET"])
1015 @api_ns.route("/g/<group_name>/<coprname>/build-config/<chroot>/", methods=["GET"])
1018 """
1019 Generate build configuration.
1020 """
1021 output = {
1022 "output": "ok",
1023 "build_config": generate_build_config(copr, chroot),
1024 }
1025
1026 if not output['build_config']:
1027 raise LegacyApiError('Chroot not found.')
1028
1029 return flask.jsonify(output)
1030
1031
1032 @api_ns.route("/module/repo/", methods=["POST"])
1048