1 import tempfile
2 import shutil
3 import json
4 import os
5 import pprint
6 import time
7 import requests
8
9 from sqlalchemy.sql import text
10 from sqlalchemy.sql.expression import not_
11 from sqlalchemy.orm import joinedload, selectinload
12 from sqlalchemy import func, desc, or_, and_
13 from sqlalchemy.sql import false,true
14 from werkzeug.utils import secure_filename
15 from sqlalchemy import bindparam, Integer, String
16 from sqlalchemy.exc import IntegrityError
17
18 from copr_common.enums import FailTypeEnum, StatusEnum
19 from coprs import app
20 from coprs import cache
21 from coprs import db
22 from coprs import models
23 from coprs import helpers
24 from coprs.exceptions import (
25 ActionInProgressException,
26 BadRequest,
27 ConflictingRequest,
28 DuplicateException,
29 InsufficientRightsException,
30 InsufficientStorage,
31 MalformedArgumentException,
32 UnrepeatableBuildException,
33 )
34
35 from coprs.logic import coprs_logic
36 from coprs.logic import users_logic
37 from coprs.logic.actions_logic import ActionsLogic
38 from coprs.logic.dist_git_logic import DistGitLogic
39 from coprs.models import BuildChroot
40 from coprs.logic.coprs_logic import MockChrootsLogic
41 from coprs.logic.packages_logic import PackagesLogic
42 from coprs.logic.batches_logic import BatchesLogic
43
44 from .helpers import get_graph_parameters
45 log = app.logger
46
47
48 PROCESSING_STATES = [StatusEnum(s) for s in [
49 "running", "pending", "starting", "importing", "waiting",
50 ]]
54 @classmethod
55 - def get(cls, build_id):
57
58 @classmethod
69
70 @classmethod
81
82 @classmethod
83 @cache.memoize(timeout=2*60)
113
114 @classmethod
119
120 @classmethod
128
129 @classmethod
150
151 @classmethod
153 query = text("""
154 SELECT COUNT(*) as result
155 FROM build_chroot JOIN build on build.id = build_chroot.build_id
156 WHERE
157 build.submitted_on < :end
158 AND (
159 build_chroot.started_on > :start
160 OR (build_chroot.started_on is NULL AND build_chroot.status = :status)
161 -- for currently pending builds we need to filter on status=pending because there might be
162 -- failed builds that have started_on=NULL
163 )
164 AND NOT build.canceled
165 """)
166
167 res = db.engine.execute(query, start=start, end=end, status=StatusEnum("pending"))
168 return res.first().result
169
170 @classmethod
172 query = text("""
173 SELECT COUNT(*) as result
174 FROM build_chroot
175 WHERE
176 started_on < :end
177 AND (ended_on > :start OR (ended_on is NULL AND status = :status))
178 -- for currently running builds we need to filter on status=running because there might be failed
179 -- builds that have ended_on=NULL
180 """)
181
182 res = db.engine.execute(query, start=start, end=end, status=StatusEnum("running"))
183 return res.first().result
184
185 @classmethod
202
203 @classmethod
205 data = [["pending"], ["running"], ["avg running"], ["time"]]
206 params = get_graph_parameters(type)
207 cached_data = cls.get_cached_graph_data(params)
208 data[0].extend(cached_data["pending"])
209 data[1].extend(cached_data["running"])
210
211 for i in range(len(data[0]) - 1, params["steps"]):
212 step_start = params["start"] + i * params["step"]
213 step_end = step_start + params["step"]
214 pending = cls.get_pending_jobs_bucket(step_start, step_end)
215 running = cls.get_running_jobs_bucket(step_start, step_end)
216 data[0].append(pending)
217 data[1].append(running)
218 cls.cache_graph_data(type, time=step_start, pending=pending, running=running)
219
220 running_total = 0
221 for i in range(1, params["steps"] + 1):
222 running_total += data[1][i]
223
224 data[2].extend([running_total * 1.0 / params["steps"]] * (len(data[0]) - 1))
225
226 for i in range(params["start"], params["end"], params["step"]):
227 data[3].append(time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(i)))
228
229 return data
230
231 @classmethod
246
247 @classmethod
266
267 @classmethod
279
280 @classmethod
289
290 @classmethod
321
322 @classmethod
331
332 @classmethod
335
336 @classmethod
339
340 @classmethod
345
346 @classmethod
353
354 @classmethod
364
365 @classmethod
368
369 @classmethod
377
378 @classmethod
381
382 @classmethod
385
386 @classmethod
389 skip_import = False
390 git_hashes = {}
391
392 if source_build.source_type == helpers.BuildSourceEnum('upload'):
393 if source_build.repeatable:
394 skip_import = True
395 for chroot in source_build.build_chroots:
396 git_hashes[chroot.name] = chroot.git_hash
397 else:
398 raise UnrepeatableBuildException("Build sources were not fully imported into CoprDistGit.")
399
400 build = cls.create_new(user, copr, source_build.source_type, source_build.source_json, chroot_names,
401 pkgs=source_build.pkgs, git_hashes=git_hashes, skip_import=skip_import,
402 srpm_url=source_build.srpm_url, copr_dirname=source_build.copr_dir.name, **build_options)
403 build.package_id = source_build.package_id
404 build.pkg_version = source_build.pkg_version
405 build.resubmitted_from_id = source_build.id
406
407 return build
408
409 @classmethod
410 - def create_new_from_url(cls, user, copr, url, chroot_names=None,
411 copr_dirname=None, **build_options):
425
426 @classmethod
427 - def create_new_from_scm(cls, user, copr, scm_type, clone_url,
428 committish='', subdirectory='', spec='', srpm_build_method='rpkg',
429 chroot_names=None, copr_dirname=None, **build_options):
430 """
431 :type user: models.User
432 :type copr: models.Copr
433
434 :type chroot_names: List[str]
435
436 :rtype: models.Build
437 """
438 source_type = helpers.BuildSourceEnum("scm")
439 source_json = json.dumps({"type": scm_type,
440 "clone_url": clone_url,
441 "committish": committish,
442 "subdirectory": subdirectory,
443 "spec": spec,
444 "srpm_build_method": srpm_build_method})
445 return cls.create_new(user, copr, source_type, source_json, chroot_names, copr_dirname=copr_dirname, **build_options)
446
447 @classmethod
448 - def create_new_from_pypi(cls, user, copr, pypi_package_name, pypi_package_version, spec_template,
449 python_versions, chroot_names=None, copr_dirname=None, **build_options):
467
468 @classmethod
481
482 @classmethod
483 - def create_new_from_custom(cls, user, copr, script, script_chroot=None, script_builddeps=None,
484 script_resultdir=None, chroot_names=None, copr_dirname=None, **kwargs):
485 """
486 :type user: models.User
487 :type copr: models.Copr
488 :type script: str
489 :type script_chroot: str
490 :type script_builddeps: str
491 :type script_resultdir: str
492 :type chroot_names: List[str]
493 :rtype: models.Build
494 """
495 source_type = helpers.BuildSourceEnum("custom")
496 source_dict = {
497 'script': script,
498 'chroot': script_chroot,
499 'builddeps': script_builddeps,
500 'resultdir': script_resultdir,
501 }
502
503 return cls.create_new(user, copr, source_type, json.dumps(source_dict),
504 chroot_names, copr_dirname=copr_dirname, **kwargs)
505
506 @classmethod
507 - def create_new_from_distgit(cls, user, copr, package_name,
508 distgit_name=None, distgit_namespace=None,
509 committish=None, chroot_names=None,
510 copr_dirname=None, **build_options):
523
524 @classmethod
525 - def create_new_from_upload(cls, user, copr, f_uploader, orig_filename,
526 chroot_names=None, copr_dirname=None, **build_options):
527 """
528 :type user: models.User
529 :type copr: models.Copr
530 :param f_uploader(file_path): function which stores data at the given `file_path`
531 :return:
532 """
533 tmp = None
534 try:
535 tmp = tempfile.mkdtemp(dir=app.config["STORAGE_DIR"])
536 tmp_name = os.path.basename(tmp)
537 filename = secure_filename(orig_filename)
538 file_path = os.path.join(tmp, filename)
539 f_uploader(file_path)
540 except OSError as error:
541 if tmp:
542 shutil.rmtree(tmp)
543 raise InsufficientStorage("Can not create storage directory for uploaded file: {}".format(str(error)))
544
545
546 pkg_url = "{baseurl}/tmp/{tmp_dir}/{filename}".format(
547 baseurl=app.config["PUBLIC_COPR_BASE_URL"],
548 tmp_dir=tmp_name,
549 filename=filename)
550
551
552 source_type = helpers.BuildSourceEnum("upload")
553 source_json = json.dumps({"url": pkg_url, "pkg": filename, "tmp": tmp_name})
554 srpm_url = None if pkg_url.endswith('.spec') else pkg_url
555
556 try:
557 build = cls.create_new(user, copr, source_type, source_json,
558 chroot_names, pkgs=pkg_url, srpm_url=srpm_url,
559 copr_dirname=copr_dirname, **build_options)
560 except Exception:
561 shutil.rmtree(tmp)
562 raise
563
564 return build
565
566 @classmethod
567 - def create_new(cls, user, copr, source_type, source_json, chroot_names=None, pkgs="",
568 git_hashes=None, skip_import=False, background=False, batch=None,
569 srpm_url=None, copr_dirname=None, package=None, **build_options):
570 """
571 :type user: models.User
572 :type copr: models.Copr
573 :type chroot_names: List[str]
574 :type source_type: int value from helpers.BuildSourceEnum
575 :type source_json: str in json format
576 :type pkgs: str
577 :type git_hashes: dict
578 :type skip_import: bool
579 :type background: bool
580 :type batch: models.Batch
581 :rtype: models.Build
582 """
583 if not copr.active_copr_chroots:
584 raise BadRequest("Can't create build - project {} has no active chroots".format(copr.full_name))
585
586 chroots = None
587 if chroot_names:
588 chroots = []
589 for chroot in copr.active_chroots:
590 if chroot.name in chroot_names:
591 chroots.append(chroot)
592
593 build = cls.add(
594 user=user,
595 package=package,
596 pkgs=pkgs,
597 copr=copr,
598 chroots=chroots,
599 source_type=source_type,
600 source_json=source_json,
601 enable_net=build_options.get("enable_net", copr.build_enable_net),
602 background=background,
603 git_hashes=git_hashes,
604 skip_import=skip_import,
605 batch=batch,
606 srpm_url=srpm_url,
607 copr_dirname=copr_dirname,
608 bootstrap=build_options.get("bootstrap"),
609 after_build_id=build_options.get("after_build_id"),
610 with_build_id=build_options.get("with_build_id"),
611 )
612
613 if "timeout" in build_options:
614 build.timeout = build_options["timeout"]
615
616 return build
617
618 @classmethod
619 - def _setup_batch(cls, batch, after_build_id, with_build_id, user):
635
636 @classmethod
637 - def add(cls, user, pkgs, copr, source_type=None, source_json=None,
638 repos=None, chroots=None, timeout=None, enable_net=True,
639 git_hashes=None, skip_import=False, background=False, batch=None,
640 srpm_url=None, copr_dirname=None, bootstrap=None,
641 package=None, after_build_id=None, with_build_id=None):
642
643 if chroots is None:
644 chroots = []
645
646 coprs_logic.CoprsLogic.raise_if_unfinished_blocking_action(
647 copr, "Can't build while there is an operation in progress: {action}")
648 users_logic.UsersLogic.raise_if_cant_build_in_copr(
649 user, copr,
650 "You don't have permissions to build in this copr.")
651
652 batch = cls._setup_batch(batch, after_build_id, with_build_id, user)
653
654 if not repos:
655 repos = copr.repos
656
657
658 if pkgs and (" " in pkgs or "\n" in pkgs or "\t" in pkgs or pkgs.strip() != pkgs):
659 raise MalformedArgumentException("Trying to create a build using src_pkg "
660 "with bad characters. Forgot to split?")
661
662
663 if not source_type or not source_json:
664 source_type = helpers.BuildSourceEnum("link")
665 source_json = json.dumps({"url":pkgs})
666
667 if skip_import and srpm_url:
668 chroot_status = StatusEnum("pending")
669 source_status = StatusEnum("succeeded")
670 else:
671 chroot_status = StatusEnum("waiting")
672 source_status = StatusEnum("pending")
673
674 copr_dir = None
675 if copr_dirname:
676 if not copr_dirname.startswith(copr.name+':') and copr_dirname != copr.name:
677 raise MalformedArgumentException("Copr dirname not starting with copr name.")
678 copr_dir = coprs_logic.CoprDirsLogic.get_or_create(copr, copr_dirname)
679
680 build = models.Build(
681 user=user,
682 package=package,
683 pkgs=pkgs,
684 copr=copr,
685 repos=repos,
686 source_type=source_type,
687 source_json=source_json,
688 source_status=source_status,
689 submitted_on=int(time.time()),
690 enable_net=bool(enable_net),
691 is_background=bool(background),
692 batch=batch,
693 srpm_url=srpm_url,
694 copr_dir=copr_dir,
695 bootstrap=bootstrap,
696 )
697
698 if timeout:
699 build.timeout = timeout or app.config["DEFAULT_BUILD_TIMEOUT"]
700
701 db.session.add(build)
702
703 for chroot in chroots:
704
705 git_hash = None
706 if git_hashes:
707 git_hash = git_hashes.get(chroot.name)
708 buildchroot = BuildChrootsLogic.new(
709 build=build,
710 status=chroot_status,
711 mock_chroot=chroot,
712 git_hash=git_hash,
713 )
714 db.session.add(buildchroot)
715
716 return build
717
718 @classmethod
719 - def rebuild_package(cls, package, source_dict_update={}, copr_dir=None, update_callback=None,
720 scm_object_type=None, scm_object_id=None,
721 scm_object_url=None, submitted_by=None):
722 """
723 Rebuild a concrete package by a webhook. This is different from
724 create_new() because we don't have a concrete 'user' who submits this
725 (only submitted_by string).
726 """
727
728 source_dict = package.source_json_dict
729 source_dict.update(source_dict_update)
730 source_json = json.dumps(source_dict)
731
732 if not copr_dir:
733 copr_dir = package.copr.main_dir
734
735 build = models.Build(
736 user=None,
737 pkgs=None,
738 package=package,
739 copr=package.copr,
740 repos=package.copr.repos,
741 source_status=StatusEnum("pending"),
742 source_type=package.source_type,
743 source_json=source_json,
744 submitted_on=int(time.time()),
745 enable_net=package.copr.build_enable_net,
746 timeout=app.config["DEFAULT_BUILD_TIMEOUT"],
747 copr_dir=copr_dir,
748 update_callback=update_callback,
749 scm_object_type=scm_object_type,
750 scm_object_id=scm_object_id,
751 scm_object_url=scm_object_url,
752 submitted_by=submitted_by,
753 )
754 db.session.add(build)
755
756 status = StatusEnum("waiting")
757 for chroot in package.chroots:
758 buildchroot = BuildChrootsLogic.new(
759 build=build,
760 status=status,
761 mock_chroot=chroot,
762 git_hash=None
763 )
764 db.session.add(buildchroot)
765
766 cls.process_update_callback(build)
767 return build
768
769
770 terminal_states = {StatusEnum("failed"), StatusEnum("succeeded"), StatusEnum("canceled")}
771
772 @classmethod
784
785
786 @classmethod
788 """
789 Deletes the locally stored data for build purposes. This is typically
790 uploaded srpm file, uploaded spec file or webhook POST content.
791 """
792
793 data = json.loads(build.source_json)
794 if 'tmp' in data:
795 tmp = data["tmp"]
796 storage_path = app.config["STORAGE_DIR"]
797 try:
798 shutil.rmtree(os.path.join(storage_path, tmp))
799 except:
800 pass
801
802
803 @classmethod
805 """
806 :param build:
807 :param upd_dict:
808 example:
809 {
810 "builds":[
811 {
812 "id": 1,
813 "copr_id": 2,
814 "started_on": 1390866440
815 },
816 {
817 "id": 2,
818 "copr_id": 1,
819 "status": 0,
820 "chroot": "fedora-18-x86_64",
821 "result_dir": "baz",
822 "ended_on": 1390866440
823 }]
824 }
825 """
826 log.info("Updating build {} by: {}".format(build.id, upd_dict))
827
828 pkg_name = upd_dict.get('pkg_name', None)
829 if not build.package and pkg_name:
830
831 if not PackagesLogic.get(build.copr_dir.id, pkg_name).first():
832
833 try:
834 package = PackagesLogic.add(
835 build.copr.user, build.copr_dir,
836 pkg_name, build.source_type, build.source_json)
837 db.session.add(package)
838 db.session.commit()
839 except (IntegrityError, DuplicateException) as e:
840 app.logger.exception(e)
841 db.session.rollback()
842 return
843 build.package = PackagesLogic.get(build.copr_dir.id, pkg_name).first()
844
845 for attr in ["built_packages", "srpm_url", "pkg_version"]:
846 value = upd_dict.get(attr, None)
847 if value:
848 setattr(build, attr, value)
849
850
851 if str(upd_dict.get("task_id")) == str(build.task_id):
852 build.result_dir = upd_dict.get("result_dir", "")
853
854 new_status = upd_dict.get("status")
855 if new_status == StatusEnum("succeeded"):
856 new_status = StatusEnum("importing")
857 chroot_status=StatusEnum("waiting")
858 if not build.build_chroots:
859
860
861 for chroot in build.package.chroots:
862 buildchroot = BuildChrootsLogic.new(
863 build=build,
864 status=chroot_status,
865 mock_chroot=chroot,
866 git_hash=None,
867 )
868 db.session.add(buildchroot)
869 else:
870 for buildchroot in build.build_chroots:
871 buildchroot.status = chroot_status
872 db.session.add(buildchroot)
873
874 build.source_status = new_status
875 if new_status == StatusEnum("failed") or \
876 new_status == StatusEnum("skipped"):
877 for ch in build.build_chroots:
878 ch.status = new_status
879 ch.ended_on = upd_dict.get("ended_on") or time.time()
880 ch.started_on = upd_dict.get("started_on", ch.ended_on)
881 db.session.add(ch)
882
883 if new_status == StatusEnum("failed"):
884 build.fail_type = FailTypeEnum("srpm_build_error")
885
886 cls.process_update_callback(build)
887 db.session.add(build)
888 return
889
890 if "chroot" in upd_dict:
891
892 for build_chroot in build.build_chroots:
893 if build_chroot.name == upd_dict["chroot"]:
894 build_chroot.result_dir = upd_dict.get("result_dir", "")
895
896 if "status" in upd_dict and build_chroot.status not in BuildsLogic.terminal_states:
897 build_chroot.status = upd_dict["status"]
898
899 if upd_dict.get("status") in BuildsLogic.terminal_states:
900 build_chroot.ended_on = upd_dict.get("ended_on") or time.time()
901
902 if upd_dict.get("status") == StatusEnum("starting"):
903 build_chroot.started_on = upd_dict.get("started_on") or time.time()
904
905 db.session.add(build_chroot)
906
907
908
909 if (build.module
910 and upd_dict.get("status") == StatusEnum("succeeded")
911 and all(b.status == StatusEnum("succeeded") for b in build.module.builds)):
912 ActionsLogic.send_build_module(build.copr, build.module)
913
914 cls.process_update_callback(build)
915 db.session.add(build)
916
917 @classmethod
932
933 @classmethod
935 headers = {
936 'Authorization': 'token {}'.format(build.copr.scm_api_auth.get('api_key'))
937 }
938
939 if build.srpm_url:
940 progress = 50
941 else:
942 progress = 10
943
944 state_table = {
945 'failed': ('failure', 0),
946 'succeeded': ('success', 100),
947 'canceled': ('canceled', 0),
948 'running': ('pending', progress),
949 'pending': ('pending', progress),
950 'skipped': ('error', 0),
951 'starting': ('pending', progress),
952 'importing': ('pending', progress),
953 'forked': ('error', 0),
954 'waiting': ('pending', progress),
955 'unknown': ('error', 0),
956 }
957
958 build_url = os.path.join(
959 app.config['PUBLIC_COPR_BASE_URL'],
960 'coprs', build.copr.full_name.replace('@', 'g/'),
961 'build', str(build.id)
962 )
963
964 data = {
965 'username': 'Copr build',
966 'comment': '#{}'.format(build.id),
967 'url': build_url,
968 'status': state_table[build.state][0],
969 'percent': state_table[build.state][1],
970 'uid': str(build.id),
971 }
972
973 log.debug('Sending data to Pagure API: %s', pprint.pformat(data))
974 response = requests.post(api_url, data=data, headers=headers)
975 log.debug('Pagure API response: %s', response.text)
976
977 @classmethod
994
995
996 @classmethod
1010
1011 @classmethod
1012 - def delete_build(cls, user, build, send_delete_action=True):
1023
1024 @classmethod
1026 """
1027 Delete builds specified by list of IDs
1028
1029 :type user: models.User
1030 :type build_ids: list of Int
1031 """
1032 to_delete = []
1033 no_permission = []
1034 still_running = []
1035
1036 build_ids = set(build_ids)
1037 builds = cls.get_by_ids(build_ids)
1038 for build in builds:
1039 try:
1040 cls.check_build_to_delete(user, build)
1041 to_delete.append(build)
1042 except InsufficientRightsException:
1043 no_permission.append(build.id)
1044 except ActionInProgressException:
1045 still_running.append(build.id)
1046 finally:
1047 build_ids.remove(build.id)
1048
1049 if build_ids or no_permission or still_running:
1050 msg = ""
1051 if no_permission:
1052 msg += "You don't have permissions to delete build(s) {0}.\n"\
1053 .format(", ".join(map(str, no_permission)))
1054 if still_running:
1055 msg += "Build(s) {0} are still running.\n"\
1056 .format(", ".join(map(str, still_running)))
1057 if build_ids:
1058 msg += "Build(s) {0} don't exist.\n"\
1059 .format(", ".join(map(str, build_ids)))
1060
1061 raise BadRequest(msg)
1062
1063 if to_delete:
1064 ActionsLogic.send_delete_multiple_builds(to_delete)
1065
1066 for build in to_delete:
1067 for build_chroot in build.build_chroots:
1068 db.session.delete(build_chroot)
1069
1070 db.session.delete(build)
1071
1072 @classmethod
1085
1086 @classmethod
1105
1106 @classmethod
1114
1115 @classmethod
1118
1119 @classmethod
1122
1123 @classmethod
1125 dirs = (
1126 db.session.query(
1127 models.CoprDir.id,
1128 models.Package.id,
1129 models.Package.max_builds)
1130 .join(models.Build, models.Build.copr_dir_id==models.CoprDir.id)
1131 .join(models.Package)
1132 .filter(models.Package.max_builds > 0)
1133 .group_by(
1134 models.CoprDir.id,
1135 models.Package.max_builds,
1136 models.Package.id)
1137 .having(func.count(models.Build.id) > models.Package.max_builds)
1138 )
1139
1140 for dir_id, package_id, limit in dirs.all():
1141 delete_builds = (
1142 models.Build.query.filter(
1143 models.Build.copr_dir_id==dir_id,
1144 models.Build.package_id==package_id)
1145 .order_by(desc(models.Build.id))
1146 .offset(limit)
1147 .all()
1148 )
1149
1150 for build in delete_builds:
1151 try:
1152 cls.delete_build(build.copr.user, build)
1153 except ActionInProgressException:
1154
1155 log.error("Build(id={}) delete failed, unfinished action.".format(build.id))
1156
1157 @classmethod
1173
1174 @classmethod
1193
1196 @classmethod
1197 - def new(cls, build, mock_chroot, **kwargs):
1214
1215 @classmethod
1224
1225 @classmethod
1236
1237 @classmethod
1240
1241 @classmethod
1244
1245 @classmethod
1248
1249 @classmethod
1252
1253 @classmethod
1256
1257 @classmethod
1268
1269 @classmethod
1277
1280 @classmethod
1282 query = """
1283 SELECT
1284 package.id as package_id,
1285 package.name AS package_name,
1286 build.id AS build_id,
1287 build_chroot.status AS build_chroot_status,
1288 build.pkg_version AS build_pkg_version,
1289 mock_chroot.id AS mock_chroot_id,
1290 mock_chroot.os_release AS mock_chroot_os_release,
1291 mock_chroot.os_version AS mock_chroot_os_version,
1292 mock_chroot.arch AS mock_chroot_arch
1293 FROM package
1294 JOIN (SELECT
1295 MAX(build.id) AS max_build_id_for_chroot,
1296 build.package_id AS package_id,
1297 build_chroot.mock_chroot_id AS mock_chroot_id
1298 FROM build
1299 JOIN build_chroot
1300 ON build.id = build_chroot.build_id
1301 WHERE build.copr_id = {copr_id}
1302 AND build_chroot.status != 2
1303 GROUP BY build.package_id,
1304 build_chroot.mock_chroot_id) AS max_build_ids_for_a_chroot
1305 ON package.id = max_build_ids_for_a_chroot.package_id
1306 JOIN build
1307 ON build.id = max_build_ids_for_a_chroot.max_build_id_for_chroot
1308 JOIN build_chroot
1309 ON build_chroot.mock_chroot_id = max_build_ids_for_a_chroot.mock_chroot_id
1310 AND build_chroot.build_id = max_build_ids_for_a_chroot.max_build_id_for_chroot
1311 JOIN mock_chroot
1312 ON mock_chroot.id = max_build_ids_for_a_chroot.mock_chroot_id
1313 JOIN copr_dir ON build.copr_dir_id=copr_dir.id WHERE copr_dir.main IS TRUE
1314 ORDER BY package.name ASC, package.id ASC, mock_chroot.os_release ASC, mock_chroot.os_version ASC, mock_chroot.arch ASC
1315 """.format(copr_id=copr.id)
1316 rows = db.session.execute(query)
1317 return rows
1318