1 import copy
2 import datetime
3 import os
4 import json
5 import base64
6 import uuid
7 from fnmatch import fnmatch
8
9 from sqlalchemy import outerjoin
10 from sqlalchemy.ext.associationproxy import association_proxy
11 from sqlalchemy.orm import column_property, validates
12 from sqlalchemy.event import listens_for
13 from six.moves.urllib.parse import urljoin
14 from libravatar import libravatar_url
15 import zlib
16
17 from flask import url_for
18
19 from copr_common.enums import (ActionTypeEnum, BackendResultEnum, FailTypeEnum,
20 ModuleStatusEnum, StatusEnum, DefaultActionPriorityEnum)
21 from coprs import db
22 from coprs import helpers
23 from coprs import app
24
25 import itertools
26 import operator
27 from coprs.helpers import JSONEncodedDict
28
29 import gi
30 gi.require_version('Modulemd', '1.0')
31 from gi.repository import Modulemd
40
43 """
44 Represents user of the copr frontend
45 """
46 __tablename__ = "user"
47
48 id = db.Column(db.Integer, primary_key=True)
49
50
51 username = db.Column(db.String(100), nullable=False, unique=True)
52
53
54
55 proven = db.Column(db.Boolean, default=False)
56
57
58 admin = db.Column(db.Boolean, default=False)
59
60
61 proxy = db.Column(db.Boolean, default=False)
62
63
64 openid_groups = db.Column(JSONEncodedDict)
65
68 """
69 Records all the private information for a user.
70 """
71
72 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True,
73 nullable=False)
74
75
76 mail = db.Column(db.String(150), nullable=False)
77
78
79 timezone = db.Column(db.String(50), nullable=True)
80
81
82 api_login = db.Column(db.String(40), nullable=False, default="abc")
83 api_token = db.Column(db.String(40), nullable=False, default="abc")
84 api_token_expiration = db.Column(
85 db.Date, nullable=False, default=datetime.date(2000, 1, 1))
86
87
88 -class User(db.Model, helpers.Serializer):
89 __table__ = outerjoin(_UserPublic.__table__, _UserPrivate.__table__)
90 id = column_property(_UserPublic.__table__.c.id, _UserPrivate.__table__.c.user_id)
91
92 @property
94 """
95 Return the short username of the user, e.g. bkabrda
96 """
97
98 return self.username
99
100 @property
102 """
103 Filter-out the permissions for deleted projects from
104 self.copr_permissions_unfiltered.
105 """
106 return [perm for perm in self.copr_permissions_unfiltered
107 if not perm.copr.deleted]
108
110 """
111 Get permissions of this user for the given copr.
112 Caches the permission during one request,
113 so use this if you access them multiple times
114 """
115
116 if not hasattr(self, "_permissions_for_copr"):
117 self._permissions_for_copr = {}
118 if copr.name not in self._permissions_for_copr:
119 self._permissions_for_copr[copr.name] = (
120 CoprPermission.query
121 .filter_by(user=self)
122 .filter_by(copr=copr)
123 .first()
124 )
125 return self._permissions_for_copr[copr.name]
126
143
144 @property
150
151 @property
154
156 """
157 :type group: Group
158 """
159 if group.fas_name in self.user_teams:
160 return True
161 else:
162 return False
163
182
183 @property
185
186 return ["id", "name"]
187
188 @property
190 """
191 Get number of coprs for this user.
192 """
193
194 return (Copr.query.filter_by(user=self).
195 filter_by(deleted=False).
196 filter_by(group_id=None).
197 count())
198
199 @property
201 """
202 Return url to libravatar image.
203 """
204
205 try:
206 return libravatar_url(email=self.mail, https=True)
207 except IOError:
208 return ""
209
221
224 """
225 Representation of User or Group <-> Copr relation
226 """
227 id = db.Column(db.Integer, primary_key=True)
228
229 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"))
230 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True, index=True)
231 group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=True, index=True)
232 position = db.Column(db.Integer, nullable=False)
233
234 copr = db.relationship("Copr")
235 user = db.relationship("User")
236 group = db.relationship("Group")
237
238
239 -class CoprScore(db.Model, helpers.Serializer):
240 """
241 Users can upvote or downvote projects
242 """
243 id = db.Column(db.Integer, primary_key=True)
244 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), nullable=False, index=True)
245 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
246 score = db.Column(db.Integer, nullable=False)
247
248 copr = db.relationship("Copr")
249 user = db.relationship("User")
250
251 __table_args__ = (
252 db.UniqueConstraint("copr_id", "user_id",
253 name="copr_score_copr_id_user_id_uniq"),
254 )
255
256
257 -class _CoprPublic(db.Model, helpers.Serializer, CoprSearchRelatedData):
258 """
259 Represents public part of a single copr (personal repo with builds, mock
260 chroots, etc.).
261 """
262
263 __tablename__ = "copr"
264 __table_args__ = (
265 db.Index('copr_name_group_id_idx', 'name', 'group_id'),
266 db.Index('copr_deleted_name', 'deleted', 'name'),
267 )
268
269 id = db.Column(db.Integer, primary_key=True)
270
271 name = db.Column(db.String(100), nullable=False)
272 homepage = db.Column(db.Text)
273 contact = db.Column(db.Text)
274
275
276 repos = db.Column(db.Text)
277
278 created_on = db.Column(db.Integer)
279
280 description = db.Column(db.Text)
281 instructions = db.Column(db.Text)
282 deleted = db.Column(db.Boolean, default=False)
283 playground = db.Column(db.Boolean, default=False)
284
285
286 auto_createrepo = db.Column(db.Boolean, default=True, nullable=False)
287
288
289 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True)
290 group_id = db.Column(db.Integer, db.ForeignKey("group.id"))
291 forked_from_id = db.Column(db.Integer, db.ForeignKey("copr.id"))
292
293
294 build_enable_net = db.Column(db.Boolean, default=True,
295 server_default="1", nullable=False)
296
297 unlisted_on_hp = db.Column(db.Boolean, default=False, nullable=False)
298
299
300 latest_indexed_data_update = db.Column(db.Integer)
301
302
303 persistent = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
304
305
306 auto_prune = db.Column(db.Boolean, default=True, nullable=False, server_default="1")
307
308 bootstrap = db.Column(db.Text, default="default")
309
310
311 follow_fedora_branching = db.Column(db.Boolean, default=True, nullable=False, server_default="1")
312
313
314 scm_repo_url = db.Column(db.Text)
315 scm_api_type = db.Column(db.Text)
316
317
318 delete_after = db.Column(db.DateTime, index=True, nullable=True)
319
320 multilib = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
321 module_hotfixes = db.Column(db.Boolean, default=False, nullable=False, server_default="0")
322
323 runtime_dependencies = db.Column(db.Text)
324
327 """
328 Represents private part of a single copr (personal repo with builds, mock
329 chroots, etc.).
330 """
331
332 __table_args__ = (
333 db.Index('copr_private_webhook_secret', 'webhook_secret'),
334 )
335
336
337 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), nullable=False,
338 primary_key=True)
339
340
341 webhook_secret = db.Column(db.String(100))
342
343
344 scm_api_auth_json = db.Column(db.Text)
345
346
347 -class Copr(db.Model, helpers.Serializer):
348 """
349 Represents private a single copr (personal repo with builds, mock chroots,
350 etc.).
351 """
352
353
354
355 __table__ = outerjoin(_CoprPublic.__table__, _CoprPrivate.__table__)
356 id = column_property(
357 _CoprPublic.__table__.c.id,
358 _CoprPrivate.__table__.c.copr_id
359 )
360
361
362 user = db.relationship("User", backref=db.backref("coprs"))
363 group = db.relationship("Group", backref=db.backref("groups"))
364 mock_chroots = association_proxy("copr_chroots", "mock_chroot")
365 forked_from = db.relationship("Copr",
366 remote_side=_CoprPublic.id,
367 foreign_keys=[_CoprPublic.forked_from_id],
368 backref=db.backref("all_forks"))
369
370 @property
372 return [fork for fork in self.all_forks if not fork.deleted]
373
374 @property
375 - def main_dir(self):
376 """
377 Return main copr dir for a Copr
378 """
379 return CoprDir.query.filter(CoprDir.copr_id==self.id).filter(CoprDir.main==True).one()
380
381 @property
386
387 @property
389 """
390 Return True if copr belongs to a group
391 """
392 return self.group is not None
393
394 @property
400
401 @property
407
408 @property
410 """
411 Return repos of this copr as a list of strings
412 """
413 return self.repos.split()
414
415 @property
421
422 @property
442
443
444 @property
446 """
447 :rtype: list of CoprChroot
448 """
449 return [c for c in self.copr_chroots if c.is_active]
450
451 @property
453 """
454 Return list of active mock_chroots of this copr
455 """
456 return sorted(self.active_chroots, key=lambda ch: ch.name)
457
458 @property
462
463 @property
465 """
466 Return list of active mock_chroots of this copr
467 """
468 chroots = [("{} {}".format(c.os_release, c.os_version), c.arch) for c in self.active_chroots_sorted]
469 output = []
470 for os, chs in itertools.groupby(chroots, operator.itemgetter(0)):
471 output.append((os, [ch[1] for ch in chs]))
472
473 return output
474
475 @property
477 """
478 Return number of builds in this copr
479 """
480 return len(self.builds)
481
482 @property
485
486 @disable_createrepo.setter
489
490 @property
493
494 @property
506
512
513 @property
516
517 @property
520
521 @property
526
527 @property
529 return "-".join([self.owner_name.replace("@", "group_"), self.name])
530
531 @property
533 return "/".join([self.repo_url, "modules"])
534
535 - def to_dict(self, private=False, show_builds=True, show_chroots=True):
536 result = {}
537 for key in ["id", "name", "description", "instructions"]:
538 result[key] = str(copy.copy(getattr(self, key)))
539 result["owner"] = self.owner_name
540 return result
541
542 @property
547
550
551 @property
554
555 @enable_net.setter
558
561
562 @property
564 if self.delete_after is None:
565 return None
566
567 delta = self.delete_after - datetime.datetime.now()
568 return delta.days if delta.days > 0 else 0
569
570 @delete_after_days.setter
579
580 @property
585
586 @property
593
594 @property
596 """
597 Return a list of runtime dependencies"
598 """
599 dependencies = set()
600 if self.runtime_dependencies:
601 for dep in self.runtime_dependencies.split(" "):
602 if not dep:
603 continue
604 dependencies.add(dep)
605
606 return list(dependencies)
607
608 @property
613
614 @property
617
618 @property
621
622 @property
625
628 """
629 Association class for Copr<->Permission relation
630 """
631
632
633
634 copr_builder = db.Column(db.SmallInteger, default=0)
635
636 copr_admin = db.Column(db.SmallInteger, default=0)
637
638
639 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True)
640 user = db.relationship("User",
641 backref=db.backref("copr_permissions_unfiltered"))
642 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), primary_key=True,
643 index=True)
644 copr = db.relationship("Copr", backref=db.backref("copr_permissions"))
645
647 if name == 'admin':
648 self.copr_admin = value
649 elif name == 'builder':
650 self.copr_builder = value
651 else:
652 raise KeyError("{0} is not a valid copr permission".format(name))
653
660
663 """
664 Represents one of data directories for a copr.
665 """
666 id = db.Column(db.Integer, primary_key=True)
667
668 name = db.Column(db.Text, index=True)
669 main = db.Column(db.Boolean, index=True, default=False, server_default="0", nullable=False)
670
671 ownername = db.Column(db.Text, index=True, nullable=False)
672
673 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), index=True, nullable=False)
674 copr = db.relationship("Copr", backref=db.backref("dirs"))
675
676 __table_args__ = (
677
678
679
680 db.Index('only_one_main_copr_dir', copr_id, main,
681 unique=True, postgresql_where=(main==True)),
682
683 db.UniqueConstraint('ownername', 'name',
684 name='ownername_copr_dir_uniq'),
685 )
686
691
692 @property
695
696 @property
699
700 @property
704
705 @property
711
712
713 -class Package(db.Model, helpers.Serializer, CoprSearchRelatedData):
714 """
715 Represents a single package in a project_dir.
716 """
717
718 __table_args__ = (
719 db.UniqueConstraint('copr_dir_id', 'name', name='packages_copr_dir_pkgname'),
720 db.Index('package_copr_id_name', 'copr_id', 'name'),
721 db.Index('package_webhook_sourcetype', 'webhook_rebuild', 'source_type'),
722 )
723
728
729 id = db.Column(db.Integer, primary_key=True)
730 name = db.Column(db.String(100), nullable=False)
731
732 source_type = db.Column(db.Integer, default=helpers.BuildSourceEnum("unset"))
733
734 source_json = db.Column(db.Text)
735
736 webhook_rebuild = db.Column(db.Boolean, default=False)
737
738 enable_net = db.Column(db.Boolean, default=False, server_default="0", nullable=False)
739
740
741 max_builds = db.Column(db.Integer, index=True)
742
743 @validates('max_builds')
745 return None if value == 0 else value
746
747 builds = db.relationship("Build", order_by="Build.id")
748
749
750 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), index=True)
751 copr = db.relationship("Copr", backref=db.backref("packages"))
752
753 copr_dir_id = db.Column(db.Integer, db.ForeignKey("copr_dir.id"), index=True)
754 copr_dir = db.relationship("CoprDir", backref=db.backref("packages"))
755
756
757
758 chroot_blacklist_raw = db.Column(db.Text)
759
760 @property
763
764 @property
769
770 @property
772 return helpers.BuildSourceEnum(self.source_type)
773
774 @property
776 """
777 Package's source type (and source_json) is being derived from its first build, which works except
778 for "link" and "upload" cases. Consider these being equivalent to source_type being unset.
779 """
780 return self.source_type and self.source_type_text != "link" and self.source_type_text != "upload"
781
782 @property
787
788 @property
794
800
801 - def to_dict(self, with_latest_build=False, with_latest_succeeded_build=False, with_all_builds=False):
802 package_dict = super(Package, self).to_dict()
803 package_dict['source_type'] = helpers.BuildSourceEnum(package_dict['source_type'])
804
805 if with_latest_build:
806 build = self.last_build(successful=False)
807 package_dict['latest_build'] = build.to_dict(with_chroot_states=True) if build else None
808 if with_latest_succeeded_build:
809 build = self.last_build(successful=True)
810 package_dict['latest_succeeded_build'] = build.to_dict(with_chroot_states=True) if build else None
811 if with_all_builds:
812 package_dict['builds'] = [build.to_dict(with_chroot_states=True) for build in reversed(self.builds)]
813
814 return package_dict
815
818
819
820 @property
822 if not self.chroot_blacklist_raw:
823 return []
824
825 blacklisted = []
826 for pattern in self.chroot_blacklist_raw.split(','):
827 pattern = pattern.strip()
828 if not pattern:
829 continue
830 blacklisted.append(pattern)
831
832 return blacklisted
833
834
835 @staticmethod
837 for pattern in patterns:
838 if fnmatch(chroot.name, pattern):
839 return True
840 return False
841
842
843 @property
844 - def main_pkg(self):
845 if self.copr_dir.main:
846 return self
847
848 main_pkg = Package.query.filter_by(
849 name=self.name,
850 copr_dir_id=self.copr.main_dir.id
851 ).first()
852 return main_pkg
853
854
855 @property
867
868
869 -class Build(db.Model, helpers.Serializer):
870 """
871 Representation of one build in one copr
872 """
873
874 SCM_COMMIT = 'commit'
875 SCM_PULL_REQUEST = 'pull-request'
876
877 __table_args__ = (db.Index('build_canceled', "canceled"),
878 db.Index('build_order', "is_background", "id"),
879 db.Index('build_filter', "source_type", "canceled"),
880 db.Index('build_canceled_is_background_source_status_id_idx', 'canceled', "is_background", "source_status", "id"),
881 db.Index('build_copr_id_package_id', "copr_id", "package_id")
882 )
883
897
898 id = db.Column(db.Integer, primary_key=True)
899
900 pkgs = db.Column(db.Text)
901
902 built_packages = db.Column(db.Text)
903
904 pkg_version = db.Column(db.Text)
905
906 canceled = db.Column(db.Boolean, default=False)
907
908 repos = db.Column(db.Text)
909
910
911 submitted_on = db.Column(db.Integer, nullable=False)
912
913 result_dir = db.Column(db.Text, default='', server_default='', nullable=False)
914
915 memory_reqs = db.Column(db.Integer, default=app.config["DEFAULT_BUILD_MEMORY"])
916
917 timeout = db.Column(db.Integer, default=app.config["DEFAULT_BUILD_TIMEOUT"])
918
919 enable_net = db.Column(db.Boolean, default=False,
920 server_default="0", nullable=False)
921
922 source_type = db.Column(db.Integer, default=helpers.BuildSourceEnum("unset"))
923
924 source_json = db.Column(db.Text)
925
926 fail_type = db.Column(db.Integer, default=FailTypeEnum("unset"))
927
928 is_background = db.Column(db.Boolean, default=False, server_default="0", nullable=False)
929
930 source_status = db.Column(db.Integer, default=StatusEnum("waiting"))
931 srpm_url = db.Column(db.Text)
932
933 bootstrap = db.Column(db.Text)
934
935
936 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True)
937 user = db.relationship("User", backref=db.backref("builds"))
938 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), index=True)
939 copr = db.relationship("Copr", backref=db.backref("builds"))
940 package_id = db.Column(db.Integer, db.ForeignKey("package.id"), index=True)
941 package = db.relationship("Package")
942
943 chroots = association_proxy("build_chroots", "mock_chroot")
944
945 batch_id = db.Column(db.Integer, db.ForeignKey("batch.id"))
946 batch = db.relationship("Batch", backref=db.backref("builds"))
947
948 module_id = db.Column(db.Integer, db.ForeignKey("module.id"), index=True)
949 module = db.relationship("Module", backref=db.backref("builds"))
950
951 copr_dir_id = db.Column(db.Integer, db.ForeignKey("copr_dir.id"), index=True)
952 copr_dir = db.relationship("CoprDir", backref=db.backref("builds"))
953
954
955 scm_object_id = db.Column(db.Text)
956 scm_object_type = db.Column(db.Text)
957 scm_object_url = db.Column(db.Text)
958
959
960 update_callback = db.Column(db.Text)
961
962
963 submitted_by = db.Column(db.Text)
964
965
966
967
968 resubmitted_from_id = db.Column(db.Integer)
969
970 _cached_status = None
971 _cached_status_set = None
972
973 @property
976
977 @property
980
981 @property
984
985 @property
988
989 @property
992
993 @property
994 - def fail_type_text(self):
995 return FailTypeEnum(self.fail_type)
996
997 @property
999 if self.repos is None:
1000 return list()
1001 else:
1002 return self.repos.split()
1003
1004 @property
1007
1008 @property
1010 return "{:08d}".format(self.id)
1011
1021
1022 @property
1024 if app.config["COPR_DIST_GIT_LOGS_URL"]:
1025 return "{}/{}.log".format(app.config["COPR_DIST_GIT_LOGS_URL"],
1026 self.task_id.replace('/', '_'))
1027 return None
1028
1029 @property
1031 """
1032 URL for the result-directory on backend (the source/SRPM build).
1033 """
1034 if not self.result_dir:
1035 return None
1036 parts = [
1037 "results", self.copr.owner_name, self.copr_dirname,
1038
1039 "srpm-builds", self.id_fixed_width,
1040 ]
1041 path = os.path.normpath(os.path.join(*parts))
1042 return urljoin(app.config["BACKEND_BASE_URL"], path)
1043
1053
1054 @property
1056 """
1057 Full URL to the builder-live.log(.gz) for the source (SRPM) build.
1058 """
1059 return self._compressed_log_variant(
1060 "builder-live.log", ["running"]
1061 )
1062
1063 @property
1065 """
1066 Full URL to the builder-live.log(.gz) for the source (SRPM) build.
1067 """
1068 return self._compressed_log_variant(
1069 "backend.log", ["starting", "running"]
1070 )
1071
1072 @property
1077
1078 @property
1081
1082 @property
1090
1091 @property
1094
1095 @property
1097 if not self.build_chroots:
1098 return None
1099 if any(chroot.ended_on is None for chroot in self.build_chroots):
1100 return None
1101 return max(chroot.ended_on for chroot in self.build_chroots)
1102
1103 @property
1106
1107 @property
1110
1111 @property
1112 - def source_type_text(self):
1113 return helpers.BuildSourceEnum(self.source_type)
1114
1115 @property
1124
1125 @property
1128
1130 """
1131 Get build chroots with states which present in `states` list
1132 If states == None, function returns build_chroots
1133 """
1134 chroot_states_map = dict(zip(self.build_chroots, self.chroot_states))
1135 if statuses is not None:
1136 statuses = set(statuses)
1137 else:
1138 return self.build_chroots
1139
1140 return [
1141 chroot for chroot, status in chroot_states_map.items()
1142 if status in statuses
1143 ]
1144
1145 @property
1147 return {b.name: b for b in self.build_chroots}
1148
1149 @property
1151 """
1152 Return text representation of status of this build
1153 """
1154 if self.source_status is None:
1155 return "unknown"
1156 return StatusEnum(self.source_status)
1157
1158 @property
1160 """
1161 Return build status.
1162 """
1163 if self.canceled:
1164 return StatusEnum("canceled")
1165
1166 use_src_statuses = ["starting", "pending", "running", "importing", "failed"]
1167 if self.source_status in [StatusEnum(s) for s in use_src_statuses]:
1168 return self.source_status
1169
1170 if not self.chroot_states:
1171
1172
1173
1174
1175
1176
1177
1178 app.logger.error("Build %s has source_status succeeded, but "
1179 "no build_chroots", self.id)
1180 return StatusEnum("waiting")
1181
1182 for state in ["running", "starting", "pending", "failed", "succeeded", "skipped", "forked"]:
1183 if StatusEnum(state) in self.chroot_states:
1184 return StatusEnum(state)
1185
1186 if StatusEnum("waiting") in self.chroot_states:
1187
1188
1189
1190
1191 app.logger.error("Build chroots pending, even though build %s"
1192 " has succeeded source_status", self.id)
1193 return StatusEnum("pending")
1194
1195 return None
1196
1197 @property
1199 """
1200 Return text representation of status of this build.
1201 """
1202 if self.status != None:
1203 return StatusEnum(self.status)
1204 return "unknown"
1205
1206 @property
1208 """
1209 Find out if this build is cancelable.
1210 """
1211 return not self.finished
1212
1213 @property
1215 """
1216 Find out if this build is repeatable.
1217
1218 Build is repeatable only if sources has been imported.
1219 """
1220 return self.source_status == StatusEnum("succeeded")
1221
1222 @property
1224 """
1225 Check if the build has finished, and if that happened prematurely
1226 because:
1227 - it was canceled
1228 - it failed to generate/download sources).
1229 That said, whether it's clear that the build has finished and we don't
1230 have to do additional SQL query to check corresponding BuildChroots.
1231 """
1232 if self.canceled:
1233 return True
1234 if self.source_status in [StatusEnum("failed"), StatusEnum("canceled")]:
1235 return True
1236 return False
1237
1238 @property
1240 """
1241 Find out if this build is in finished state.
1242
1243 Build is finished only if all its build_chroots are in finished state or
1244 the build was canceled.
1245 """
1246 if self.finished_early:
1247 return True
1248 if not self.build_chroots:
1249 return StatusEnum(self.source_status) in helpers.FINISHED_STATUSES
1250 return all([chroot.finished for chroot in self.build_chroots])
1251
1252 @property
1255
1256 @property
1258 """
1259 Find out if this build is persistent.
1260
1261 This property is inherited from the project.
1262 """
1263 return self.copr.persistent
1264
1265 @property
1267 try:
1268 return self.package.name
1269 except:
1270 return None
1271
1272 - def to_dict(self, options=None, with_chroot_states=False):
1285
1286 @property
1288 """
1289 Return tuple (submitter_string, submitter_link), while the
1290 submitter_link may be empty if we are not able to detect it
1291 wisely.
1292 """
1293 if self.user:
1294 user = self.user.name
1295 return (user, url_for('coprs_ns.coprs_by_user', username=user))
1296
1297 if self.submitted_by:
1298 links = ['http://', 'https://']
1299 if any([self.submitted_by.startswith(x) for x in links]):
1300 return (self.submitted_by, self.submitted_by)
1301
1302 return (self.submitted_by, None)
1303
1304 return (None, None)
1305
1306 @property
1308 """
1309 Return a string unique to project + submitter. At this level copr
1310 backend later applies builder user-VM separation policy (VMs are only
1311 re-used for builds which have the same build.sandbox value)
1312 """
1313 submitter, _ = self.submitter
1314 if not submitter:
1315
1316
1317 submitter = uuid.uuid4()
1318
1319 return '{0}--{1}'.format(self.copr.full_name, submitter)
1320
1321 @property
1324
1325 @property
1328
1329 @property
1331 """ Is bootstrap config from project/chroot overwritten by build? """
1332 if not self.bootstrap:
1333 return False
1334 return self.bootstrap != "unchanged"
1335
1337 """
1338 Check if the USER can operate with this build in batches, eg create a
1339 new batch for it, or add other builds to the existing batch. Return the
1340 error message (or None, if everything is OK).
1341 """
1342
1343 if self.batch:
1344 if not modify:
1345
1346
1347 return None
1348
1349 if self.batch.finished:
1350 return "Batch {} is already finished".format(self.batch.id)
1351
1352 if self.batch.can_assign_builds(user):
1353
1354 return None
1355
1356 project_names = [c.full_name for c in self.batch.assigned_projects]
1357 projects = helpers.pluralize("project", project_names)
1358 return (
1359 "The batch {} belongs to {}. You are not allowed to "
1360 "build there, so you neither can edit the batch."
1361 ).format(self.batch.id, projects)
1362
1363
1364 msgbase = "Build {} is not yet in any batch, and ".format(self.id)
1365 if not user.can_build_in(self.copr):
1366 return msgbase + (
1367 "user '{}' doesn't have the build permissions in project '{}' "
1368 "to create a new one"
1369 ).format(user.username, self.copr.full_name)
1370
1371 if self.finished:
1372 return msgbase + (
1373 "new batch can not be created because the build has "
1374 "already finished"
1375 )
1376
1377 return None
1378
1381 """
1382 1:N mapping: branch -> chroots
1383 """
1384
1385
1386 name = db.Column(db.String(50), primary_key=True)
1387
1388
1389 -class MockChroot(db.Model, helpers.Serializer):
1390 """
1391 Representation of mock chroot
1392 """
1393
1394 __table_args__ = (
1395 db.UniqueConstraint('os_release', 'os_version', 'arch', name='mock_chroot_uniq'),
1396 )
1397
1398 id = db.Column(db.Integer, primary_key=True)
1399
1400 os_release = db.Column(db.String(50), nullable=False)
1401
1402 os_version = db.Column(db.String(50), nullable=False)
1403
1404 arch = db.Column(db.String(50), nullable=False)
1405 is_active = db.Column(db.Boolean, default=True)
1406
1407
1408 distgit_branch_name = db.Column(db.String(50),
1409 db.ForeignKey("dist_git_branch.name"),
1410 nullable=False)
1411
1412 distgit_branch = db.relationship("DistGitBranch",
1413 backref=db.backref("chroots"))
1414
1415
1416
1417 final_prunerepo_done = db.Column(db.Boolean, default=False, server_default="0", nullable=False)
1418
1419 comment = db.Column(db.Text, nullable=True)
1420
1421 multilib_pairs = {
1422 'x86_64': 'i386',
1423 }
1424
1425 @classmethod
1435
1436 @property
1438 """
1439 Textual representation of name of this chroot
1440 """
1441 return "{}-{}-{}".format(self.os_release, self.os_version, self.arch)
1442
1443 @property
1445 """
1446 Textual representation of name of this or release
1447 """
1448 return "{}-{}".format(self.os_release, self.os_version)
1449
1450 @property
1452 """
1453 Textual representation of the operating system name
1454 """
1455 return "{0} {1}".format(self.os_release, self.os_version)
1456
1457 @property
1462
1463
1464 -class CoprChroot(db.Model, helpers.Serializer):
1465 """
1466 Representation of Copr<->MockChroot M:N relation.
1467
1468 This table basically determines what chroots are enabled in what projects.
1469 But it also contains configuration for assigned Copr/MockChroot pairs.
1470
1471 We create/delete instances of this class when user enables/disables the
1472 chroots in his project. That said, we don't keep history of changes here
1473 which means that there's only one configuration at any time.
1474 """
1475
1476 id = db.Column('id', db.Integer, primary_key=True)
1477
1478 __table_args__ = (
1479
1480
1481
1482
1483 db.UniqueConstraint("mock_chroot_id", "copr_id",
1484 name="copr_chroot_mock_chroot_id_copr_id_uniq"),
1485 )
1486
1487 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"))
1488
1489 buildroot_pkgs = db.Column(db.Text)
1490 repos = db.Column(db.Text, default="", server_default="", nullable=False)
1491 mock_chroot_id = db.Column(db.Integer, db.ForeignKey("mock_chroot.id"),
1492 nullable=False)
1493 mock_chroot = db.relationship(
1494 "MockChroot", backref=db.backref("copr_chroots"))
1495 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), nullable=False,
1496 index=True)
1497 copr = db.relationship("Copr",
1498 backref=db.backref(
1499 "copr_chroots",
1500 single_parent=True,
1501 cascade="all,delete,delete-orphan"))
1502
1503 comps_zlib = db.Column(db.LargeBinary(), nullable=True)
1504 comps_name = db.Column(db.String(127), nullable=True)
1505
1506 module_toggle = db.Column(db.Text, nullable=True)
1507
1508 with_opts = db.Column(db.Text, default="", server_default="", nullable=False)
1509 without_opts = db.Column(db.Text, default="", server_default="", nullable=False)
1510
1511
1512
1513 delete_after = db.Column(db.DateTime, index=True)
1514
1515
1516 delete_notify = db.Column(db.DateTime, index=True)
1517
1518 bootstrap = db.Column(db.Text)
1519 bootstrap_image = db.Column(db.Text)
1520
1522 if isinstance(comps_xml, str):
1523 data = comps_xml.encode("utf-8")
1524 else:
1525 data = comps_xml
1526 self.comps_zlib = zlib.compress(data)
1527
1528 @property
1531
1532 @property
1534 return (self.repos or "").split()
1535
1536 @property
1540
1541 @property
1547
1548 @property
1551
1552 @property
1555
1556 @property
1558 if not self.delete_after:
1559 return None
1560 now = datetime.datetime.now()
1561 days = (self.delete_after - now).days
1562 return days if days > 0 else 0
1563
1564 @property
1566 if not self.module_toggle:
1567 return []
1568 module_enable = []
1569 for m in self.module_toggle.split(','):
1570 if m[0] != "!":
1571 module_enable.append(m)
1572 return module_enable
1573
1575 options = {"__columns_only__": [
1576 "buildroot_pkgs", "repos", "comps_name", "copr_id", "with_opts", "without_opts"
1577 ]}
1578 d = super(CoprChroot, self).to_dict(options=options)
1579 d["mock_chroot"] = self.mock_chroot.name
1580 return d
1581
1582 @property
1584 """ Get Copr+CoprChroot consolidated bootstrap configuration """
1585 settings = {}
1586 settings['bootstrap'] = self.copr.bootstrap
1587
1588 if self.bootstrap and self.bootstrap != 'unchanged':
1589
1590 settings['bootstrap'] = self.bootstrap
1591 if settings['bootstrap'] == 'custom_image':
1592 settings['bootstrap_image'] = self.bootstrap_image
1593 if settings['bootstrap'] in [None, "default"]:
1594 return {}
1595 return settings
1596
1598 """
1599 Representation of Build<->MockChroot relation
1600 """
1601
1602 __table_args__ = (
1603 db.Index("build_chroot_status_started_on_idx", "status", "started_on"),
1604 db.UniqueConstraint("mock_chroot_id", "build_id",
1605 name="build_chroot_mock_chroot_id_build_id_uniq"),
1606 )
1607
1608 id = db.Column('id', db.Integer, primary_key=True)
1609
1610
1611
1612 copr_chroot_id = db.Column(
1613 db.Integer,
1614 db.ForeignKey("copr_chroot.id", ondelete="SET NULL"),
1615 nullable=True, index=True,
1616 )
1617 copr_chroot = db.relationship("CoprChroot",
1618 backref=db.backref("build_chroots"))
1619
1620
1621
1622 mock_chroot_id = db.Column(db.Integer, db.ForeignKey("mock_chroot.id"),
1623 nullable=False)
1624 mock_chroot = db.relationship("MockChroot", backref=db.backref("builds"))
1625 build_id = db.Column(db.Integer,
1626 db.ForeignKey("build.id", ondelete="CASCADE"),
1627 index=True, nullable=False)
1628 build = db.relationship("Build", backref=db.backref("build_chroots", cascade="all, delete-orphan",
1629 passive_deletes=True))
1630 git_hash = db.Column(db.String(40))
1631 status = db.Column(db.Integer, default=StatusEnum("waiting"))
1632
1633 started_on = db.Column(db.Integer, index=True)
1634 ended_on = db.Column(db.Integer, index=True)
1635
1636
1637 result_dir = db.Column(db.Text, default='', server_default='', nullable=False)
1638
1639 build_requires = db.Column(db.Text)
1640
1641 @property
1643 """
1644 Textual representation of name of this chroot
1645 """
1646 return self.mock_chroot.name
1647
1648 @property
1650 """
1651 Return text representation of status of this build chroot
1652 """
1653 if self.status is not None:
1654 return StatusEnum(self.status)
1655 return "unknown"
1656
1657 @property
1662
1663 @property
1666
1667 @property
1681
1682 @property
1688
1690 if not self.result_dir:
1691 return None
1692 if not self.build.package:
1693
1694 return None
1695 if self.state in states_raw_log:
1696 return os.path.join(self.result_dir_url,
1697 basename)
1698 if self.state in ["failed", "succeeded", "canceled", "importing"]:
1699 return os.path.join(self.result_dir_url,
1700 basename + ".gz")
1701 return None
1702
1703 @property
1705 """ Full URL to the builder-live.log.gz for RPM build. """
1706 return self._compressed_log_variant("builder-live.log", ["running"])
1707
1708 @property
1710 """ Link to backend.log[.gz] related to RPM build. """
1711 return self._compressed_log_variant("backend.log",
1712 ["starting", "running"])
1713
1714 @property
1725
1726
1727 -class LegalFlag(db.Model, helpers.Serializer):
1728 id = db.Column(db.Integer, primary_key=True)
1729
1730 raise_message = db.Column(db.Text)
1731
1732 raised_on = db.Column(db.Integer)
1733
1734 resolved_on = db.Column(db.Integer, index=True)
1735
1736
1737 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), nullable=True)
1738
1739 copr = db.relationship(
1740 "Copr", backref=db.backref("legal_flags", cascade="all"))
1741
1742 reporter_id = db.Column(db.Integer, db.ForeignKey("user.id"))
1743 reporter = db.relationship("User",
1744 backref=db.backref("legal_flags_raised"),
1745 foreign_keys=[reporter_id],
1746 primaryjoin="LegalFlag.reporter_id==User.id")
1747
1748 resolver_id = db.Column(
1749 db.Integer, db.ForeignKey("user.id"), nullable=True)
1750 resolver = db.relationship("User",
1751 backref=db.backref("legal_flags_resolved"),
1752 foreign_keys=[resolver_id],
1753 primaryjoin="LegalFlag.resolver_id==User.id")
1754
1755
1756 -class Action(db.Model, helpers.Serializer):
1757 """
1758 Representation of a custom action that needs
1759 backends cooperation/admin attention/...
1760 """
1761
1762 __table_args__ = (
1763 db.Index('action_result_action_type', 'result', 'action_type'),
1764 )
1765
1766 id = db.Column(db.Integer, primary_key=True)
1767
1768 action_type = db.Column(db.Integer, nullable=False)
1769
1770 object_type = db.Column(db.String(20))
1771
1772 object_id = db.Column(db.Integer)
1773
1774 old_value = db.Column(db.String(255))
1775 new_value = db.Column(db.String(255))
1776
1777
1778 priority = db.Column(db.Integer, nullable=True, default=0)
1779
1780 data = db.Column(db.Text)
1781
1782 result = db.Column(
1783 db.Integer, default=BackendResultEnum("waiting"))
1784
1785 message = db.Column(db.Text)
1786
1787 created_on = db.Column(db.Integer, index=True)
1788
1789 ended_on = db.Column(db.Integer, index=True)
1790
1793
1802
1815
1816 @property
1818 action_type_str = ActionTypeEnum(self.action_type)
1819 return DefaultActionPriorityEnum.vals.get(action_type_str, 0)
1820
1821
1822 -class Krb5Login(db.Model, helpers.Serializer):
1823 """
1824 Represents additional user information for kerberos authentication.
1825 """
1826
1827 __tablename__ = "krb5_login"
1828
1829
1830 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
1831
1832
1833 config_name = db.Column(db.String(30), nullable=False, primary_key=True)
1834
1835
1836 primary = db.Column(db.String(80), nullable=False, primary_key=True)
1837
1838 user = db.relationship("User", backref=db.backref("krb5_logins"))
1839
1842 """
1843 Generic store for simple statistics.
1844 """
1845
1846 name = db.Column(db.String(127), primary_key=True)
1847 counter_type = db.Column(db.String(30))
1848
1849 counter = db.Column(db.Integer, default=0, server_default="0")
1850
1851
1852 -class Group(db.Model, helpers.Serializer):
1853
1854 """
1855 Represents FAS groups and their aliases in Copr
1856 """
1857
1858 id = db.Column(db.Integer, primary_key=True)
1859 name = db.Column(db.String(127))
1860
1861
1862 fas_name = db.Column(db.String(127))
1863
1864 @property
1866 return u"@{}".format(self.name)
1867
1870
1873
1874
1875 -class Batch(db.Model):
1876 id = db.Column(db.Integer, primary_key=True)
1877 blocked_by_id = db.Column(db.Integer, db.ForeignKey("batch.id"), nullable=True)
1878 blocked_by = db.relationship("Batch", remote_side=[id])
1879
1880 @property
1886
1887 @property
1892
1893 @property
1895 """ Get a list (generator) of assigned projects """
1896 seen = set()
1897 for build in self.builds:
1898 copr = build.copr
1899 if copr in seen:
1900 continue
1901 seen.add(copr)
1902 yield copr
1903
1905 """
1906 Check if USER has permissions to assign builds to this batch. Since we
1907 support cross-project batches, user is allowed to add a build to this
1908 batch as long as:
1909 - the batch has no builds yet (user has created a new batch now)
1910 - the batch has at least one build which belongs to project where the
1911 user has build access
1912 """
1913 if not self.builds:
1914 return True
1915 for copr in self.assigned_projects:
1916 if user.can_build_in(copr):
1917 return True
1918 return False
1919
1920 -class Module(db.Model, helpers.Serializer):
1921 id = db.Column(db.Integer, primary_key=True)
1922 name = db.Column(db.String(100), nullable=False)
1923 stream = db.Column(db.String(100), nullable=False)
1924 version = db.Column(db.BigInteger, nullable=False)
1925 summary = db.Column(db.String(100), nullable=False)
1926 description = db.Column(db.Text)
1927 created_on = db.Column(db.Integer, nullable=True)
1928
1929
1930
1931
1932
1933
1934
1935 yaml_b64 = db.Column(db.Text)
1936
1937
1938 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"))
1939 copr = db.relationship("Copr", backref=db.backref("modules"))
1940
1941 __table_args__ = (
1942 db.UniqueConstraint("copr_id", "name", "stream", "version", name="copr_name_stream_version_uniq"),
1943 )
1944
1945 @property
1947 return base64.b64decode(self.yaml_b64)
1948
1949 @property
1951 mmd = Modulemd.ModuleStream()
1952 mmd.import_from_string(self.yaml.decode("utf-8"))
1953 return mmd
1954
1955 @property
1958
1959 @property
1962
1963 @property
1966
1967 @property
1969 """
1970 Return numeric representation of status of this build
1971 """
1972 if self.action:
1973 return { BackendResultEnum("success"): ModuleStatusEnum("succeeded"),
1974 BackendResultEnum("failure"): ModuleStatusEnum("failed"),
1975 BackendResultEnum("waiting"): ModuleStatusEnum("waiting"),
1976 }[self.action.result]
1977 build_statuses = [b.status for b in self.builds]
1978 for state in ["canceled", "running", "starting", "pending", "failed", "succeeded"]:
1979 if ModuleStatusEnum(state) in build_statuses:
1980 return ModuleStatusEnum(state)
1981 return ModuleStatusEnum("unknown")
1982
1983 @property
1985 """
1986 Return text representation of status of this build
1987 """
1988 return ModuleStatusEnum(self.status)
1989
1990 @property
1993
1994 @property
1997
1998 @property
2000 return {k: v.get_rpms().get() for k, v in self.modulemd.get_profiles().items()}
2001
2008
2015
2018 """ Dist-git instances, e.g. Fedora/CentOS/RHEL/ """
2019
2020
2021 id = db.Column(db.Integer, primary_key=True)
2022
2023
2024 name = db.Column(db.String(50), nullable=False, unique=True)
2025
2026
2027 clone_url = db.Column(db.String(100), nullable=False)
2028
2029
2030
2031 clone_package_uri = db.Column(db.String(100), nullable=False)
2032
2033
2034 priority = db.Column(db.Integer, default=100, nullable=False)
2035
2037 """
2038 Get the right git clone url for the package hosted in this dist git
2039 instance.
2040 """
2041 url = '/'.join([self.clone_url, self.clone_package_uri])
2042 try:
2043 if namespace:
2044 return url.format(pkgname=pkgname, namespace=namespace)
2045
2046 return url.format(pkgname=pkgname)
2047 except KeyError as k:
2048 raise KeyError("DistGit '{}' requires {} specified".format(
2049 self.name, k
2050 ))
2051
2054 """ Requests for backend to cancel some background job """
2055
2056
2057 what = db.Column(db.String(100), nullable=False, primary_key=True)
2058
2061 id = db.Column(db.Integer, primary_key=True)
2062
2063 user_id = db.Column(
2064 db.Integer,
2065 db.ForeignKey("user.id"),
2066 nullable=False,
2067 index=True,
2068 )
2069 copr_chroot_id = db.Column(
2070 db.Integer,
2071 db.ForeignKey("copr_chroot.id", ondelete="CASCADE"),
2072 nullable=False,
2073 )
2074
2075 user = db.relationship(
2076 "User",
2077 backref=db.backref("reviewed_outdated_chroots"),
2078 )
2079 copr_chroot = db.relationship(
2080 "CoprChroot",
2081 backref=db.backref("reviewed_outdated_chroots")
2082 )
2083
2084
2085 @listens_for(DistGitInstance.__table__, 'after_create')
2093