Package coprs :: Module models
[hide private]
[frames] | no frames]

Source Code for Module coprs.models

   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 six.moves.urllib.parse import urljoin 
  13  from libravatar import libravatar_url 
  14  import zlib 
  15   
  16  from copr_common.enums import ActionTypeEnum, BackendResultEnum, FailTypeEnum, ModuleStatusEnum, StatusEnum 
  17  from coprs import constants 
  18  from coprs import db 
  19  from coprs import helpers 
  20  from coprs import app 
  21   
  22  import itertools 
  23  import operator 
  24  from coprs.helpers import JSONEncodedDict 
  25   
  26  import gi 
  27  gi.require_version('Modulemd', '1.0') 
  28  from gi.repository import Modulemd 
29 30 31 -class CoprSearchRelatedData(object):
34
35 36 -class _UserPublic(db.Model, helpers.Serializer):
37 """ 38 Represents user of the copr frontend 39 """ 40 __tablename__ = "user" 41 42 id = db.Column(db.Integer, primary_key=True) 43 44 # unique username 45 username = db.Column(db.String(100), nullable=False, unique=True) 46 47 # is this user proven? proven users can modify builder memory and 48 # timeout for single builds 49 proven = db.Column(db.Boolean, default=False) 50 51 # is this user admin of the system? 52 admin = db.Column(db.Boolean, default=False) 53 54 # can this user behave as someone else? 55 proxy = db.Column(db.Boolean, default=False) 56 57 # list of groups as retrieved from openid 58 openid_groups = db.Column(JSONEncodedDict)
59
60 61 -class _UserPrivate(db.Model, helpers.Serializer):
62 """ 63 Records all the private information for a user. 64 """ 65 # id (primary key + foreign key) 66 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True, 67 nullable=False) 68 69 # email 70 mail = db.Column(db.String(150), nullable=False) 71 72 # optional timezone 73 timezone = db.Column(db.String(50), nullable=True) 74 75 # stuff for the cli interface 76 api_login = db.Column(db.String(40), nullable=False, default="abc") 77 api_token = db.Column(db.String(40), nullable=False, default="abc") 78 api_token_expiration = db.Column( 79 db.Date, nullable=False, default=datetime.date(2000, 1, 1))
80
81 82 -class User(db.Model, helpers.Serializer):
83 __table__ = outerjoin(_UserPublic.__table__, _UserPrivate.__table__) 84 id = column_property(_UserPublic.__table__.c.id, _UserPrivate.__table__.c.user_id) 85 86 @property
87 - def name(self):
88 """ 89 Return the short username of the user, e.g. bkabrda 90 """ 91 92 return self.username
93
94 - def permissions_for_copr(self, copr):
95 """ 96 Get permissions of this user for the given copr. 97 Caches the permission during one request, 98 so use this if you access them multiple times 99 """ 100 101 if not hasattr(self, "_permissions_for_copr"): 102 self._permissions_for_copr = {} 103 if copr.name not in self._permissions_for_copr: 104 self._permissions_for_copr[copr.name] = ( 105 CoprPermission.query 106 .filter_by(user=self) 107 .filter_by(copr=copr) 108 .first() 109 ) 110 return self._permissions_for_copr[copr.name]
111
112 - def can_build_in(self, copr):
113 """ 114 Determine if this user can build in the given copr. 115 """ 116 can_build = False 117 if copr.user_id == self.id: 118 can_build = True 119 if (self.permissions_for_copr(copr) and 120 self.permissions_for_copr(copr).copr_builder == 121 helpers.PermissionEnum("approved")): 122 123 can_build = True 124 125 # a bit dirty code, here we access flask.session object 126 if copr.group is not None and \ 127 copr.group.fas_name in self.user_teams: 128 return True 129 130 return can_build
131 132 @property
133 - def user_teams(self):
134 if self.openid_groups and 'fas_groups' in self.openid_groups: 135 return self.openid_groups['fas_groups'] 136 else: 137 return []
138 139 @property
140 - def user_groups(self):
141 return Group.query.filter(Group.fas_name.in_(self.user_teams)).all()
142
143 - def can_build_in_group(self, group):
144 """ 145 :type group: Group 146 """ 147 if group.fas_name in self.user_teams: 148 return True 149 else: 150 return False
151
152 - def can_edit(self, copr):
153 """ 154 Determine if this user can edit the given copr. 155 """ 156 157 if copr.user == self or self.admin: 158 return True 159 if (self.permissions_for_copr(copr) and 160 self.permissions_for_copr(copr).copr_admin == 161 helpers.PermissionEnum("approved")): 162 163 return True 164 165 if copr.group is not None and \ 166 copr.group.fas_name in self.user_teams: 167 return True 168 169 return False
170 171 @property
172 - def serializable_attributes(self):
173 # enumerate here to prevent exposing credentials 174 return ["id", "name"]
175 176 @property
177 - def coprs_count(self):
178 """ 179 Get number of coprs for this user. 180 """ 181 182 return (Copr.query.filter_by(user=self). 183 filter_by(deleted=False). 184 filter_by(group_id=None). 185 count())
186 187 @property
188 - def gravatar_url(self):
189 """ 190 Return url to libravatar image. 191 """ 192 193 try: 194 return libravatar_url(email=self.mail, https=True) 195 except IOError: 196 return ""
197
198 199 -class _CoprPublic(db.Model, helpers.Serializer, CoprSearchRelatedData):
200 """ 201 Represents public part of a single copr (personal repo with builds, mock 202 chroots, etc.). 203 """ 204 205 __tablename__ = "copr" 206 __table_args__ = ( 207 db.Index('copr_name_group_id_idx', 'name', 'group_id'), 208 ) 209 210 id = db.Column(db.Integer, primary_key=True) 211 # name of the copr, no fancy chars (checked by forms) 212 name = db.Column(db.String(100), nullable=False) 213 homepage = db.Column(db.Text) 214 contact = db.Column(db.Text) 215 # string containing urls of additional repos (separated by space) 216 # that this copr will pull dependencies from 217 repos = db.Column(db.Text) 218 # time of creation as returned by int(time.time()) 219 created_on = db.Column(db.Integer) 220 # description and instructions given by copr owner 221 description = db.Column(db.Text) 222 instructions = db.Column(db.Text) 223 deleted = db.Column(db.Boolean, default=False) 224 playground = db.Column(db.Boolean, default=False) 225 226 # should copr run `createrepo` each time when build packages are changed 227 auto_createrepo = db.Column(db.Boolean, default=True) 228 229 # relations 230 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) 231 group_id = db.Column(db.Integer, db.ForeignKey("group.id")) 232 forked_from_id = db.Column(db.Integer, db.ForeignKey("copr.id")) 233 234 # enable networking for the builds by default 235 build_enable_net = db.Column(db.Boolean, default=True, 236 server_default="1", nullable=False) 237 238 unlisted_on_hp = db.Column(db.Boolean, default=False, nullable=False) 239 240 # information for search index updating 241 latest_indexed_data_update = db.Column(db.Integer) 242 243 # builds and the project are immune against deletion 244 persistent = db.Column(db.Boolean, default=False, nullable=False, server_default="0") 245 246 # if backend deletion script should be run for the project's builds 247 auto_prune = db.Column(db.Boolean, default=True, nullable=False, server_default="1") 248 249 # use mock's bootstrap container feature 250 use_bootstrap_container = db.Column(db.Boolean, default=False, nullable=False, server_default="0") 251 252 # if chroots for the new branch should be auto-enabled and populated from rawhide ones 253 follow_fedora_branching = db.Column(db.Boolean, default=True, nullable=False, server_default="1") 254 255 # scm integration properties 256 scm_repo_url = db.Column(db.Text) 257 scm_api_type = db.Column(db.Text) 258 259 # temporary project if non-null 260 delete_after = db.Column(db.DateTime, index=True, nullable=True) 261 262 __mapper_args__ = { 263 "order_by": created_on.desc() 264 }
265
266 267 -class _CoprPrivate(db.Model, helpers.Serializer):
268 """ 269 Represents private part of a single copr (personal repo with builds, mock 270 chroots, etc.). 271 """ 272 273 __table_args__ = ( 274 db.Index('copr_private_webhook_secret', 'webhook_secret'), 275 ) 276 277 # copr relation 278 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), index=True, 279 nullable=False, primary_key=True) 280 281 # a secret to be used for webhooks authentication 282 webhook_secret = db.Column(db.String(100)) 283 284 # remote Git sites auth info 285 scm_api_auth_json = db.Column(db.Text)
286
287 288 -class Copr(db.Model, helpers.Serializer):
289 """ 290 Represents private a single copr (personal repo with builds, mock chroots, 291 etc.). 292 """ 293 294 # This model doesn't have a single corresponding database table - so please 295 # define any new Columns in _CoprPublic or _CoprPrivate models! 296 __table__ = outerjoin(_CoprPublic.__table__, _CoprPrivate.__table__) 297 id = column_property( 298 _CoprPublic.__table__.c.id, 299 _CoprPrivate.__table__.c.copr_id 300 ) 301 302 # relations 303 user = db.relationship("User", backref=db.backref("coprs")) 304 group = db.relationship("Group", backref=db.backref("groups")) 305 mock_chroots = association_proxy("copr_chroots", "mock_chroot") 306 forked_from = db.relationship("Copr", 307 remote_side=_CoprPublic.id, 308 foreign_keys=[_CoprPublic.forked_from_id], 309 backref=db.backref("forks")) 310 311 @property
312 - def main_dir(self):
313 """ 314 Return main copr dir for a Copr 315 """ 316 return CoprDir.query.filter(CoprDir.copr_id==self.id).filter(CoprDir.main==True).one()
317 318 @property
319 - def scm_api_auth(self):
320 if not self.scm_api_auth_json: 321 return {} 322 return json.loads(self.scm_api_auth_json)
323 324 @property
325 - def is_a_group_project(self):
326 """ 327 Return True if copr belongs to a group 328 """ 329 return self.group is not None
330 331 @property
332 - def owner(self):
333 """ 334 Return owner (user or group) of this copr 335 """ 336 return self.group if self.is_a_group_project else self.user
337 338 @property
339 - def owner_name(self):
340 """ 341 Return @group.name for a copr owned by a group and user.name otherwise 342 """ 343 return self.group.at_name if self.is_a_group_project else self.user.name
344 345 @property
346 - def repos_list(self):
347 """ 348 Return repos of this copr as a list of strings 349 """ 350 return self.repos.split()
351 352 @property
353 - def active_chroots(self):
354 """ 355 Return list of active mock_chroots of this copr 356 """ 357 return filter(lambda x: x.is_active, self.mock_chroots)
358 359 @property
360 - def active_copr_chroots(self):
361 """ 362 :rtype: list of CoprChroot 363 """ 364 return [c for c in self.copr_chroots if c.is_active]
365 366 @property
367 - def active_chroots_sorted(self):
368 """ 369 Return list of active mock_chroots of this copr 370 """ 371 return sorted(self.active_chroots, key=lambda ch: ch.name)
372 373 @property
374 - def outdated_chroots(self):
375 return sorted([chroot for chroot in self.copr_chroots if chroot.delete_after], 376 key=lambda ch: ch.name)
377 378 @property
379 - def active_chroots_grouped(self):
380 """ 381 Return list of active mock_chroots of this copr 382 """ 383 chroots = [("{} {}".format(c.os_release, c.os_version), c.arch) for c in self.active_chroots_sorted] 384 output = [] 385 for os, chs in itertools.groupby(chroots, operator.itemgetter(0)): 386 output.append((os, [ch[1] for ch in chs])) 387 388 return output
389 390 @property
391 - def build_count(self):
392 """ 393 Return number of builds in this copr 394 """ 395 return len(self.builds)
396 397 @property
398 - def disable_createrepo(self):
399 return not self.auto_createrepo
400 401 @disable_createrepo.setter
402 - def disable_createrepo(self, value):
403 self.auto_createrepo = not bool(value)
404 405 @property
406 - def devel_mode(self):
407 return self.disable_createrepo
408 409 @property
410 - def modified_chroots(self):
411 """ 412 Return list of chroots which has been modified 413 """ 414 modified_chroots = [] 415 for chroot in self.copr_chroots: 416 if ((chroot.buildroot_pkgs or chroot.repos 417 or chroot.with_opts or chroot.without_opts) 418 and chroot.is_active): 419 modified_chroots.append(chroot) 420 return modified_chroots
421
422 - def is_release_arch_modified(self, name_release, arch):
423 if "{}-{}".format(name_release, arch) in \ 424 [chroot.name for chroot in self.modified_chroots]: 425 return True 426 return False
427 428 @property
429 - def full_name(self):
430 return "{}/{}".format(self.owner_name, self.name)
431 432 @property
433 - def repo_name(self):
434 return "{}-{}".format(self.owner_name, self.main_dir.name)
435 436 @property
437 - def repo_url(self):
438 return "/".join([app.config["BACKEND_BASE_URL"], 439 u"results", 440 self.main_dir.full_name])
441 442 @property
443 - def repo_id(self):
444 return "-".join([self.owner_name.replace("@", "group_"), self.name])
445 446 @property
447 - def modules_url(self):
448 return "/".join([self.repo_url, "modules"])
449
450 - def to_dict(self, private=False, show_builds=True, show_chroots=True):
451 result = {} 452 for key in ["id", "name", "description", "instructions"]: 453 result[key] = str(copy.copy(getattr(self, key))) 454 result["owner"] = self.owner_name 455 return result
456 457 @property
458 - def still_forking(self):
459 return bool(Action.query.filter(Action.result == BackendResultEnum("waiting")) 460 .filter(Action.action_type == ActionTypeEnum("fork")) 461 .filter(Action.new_value == self.full_name).all())
462 465 466 @property
467 - def enable_net(self):
468 return self.build_enable_net
469 470 @enable_net.setter
471 - def enable_net(self, value):
472 self.build_enable_net = value
473
474 - def new_webhook_secret(self):
475 self.webhook_secret = str(uuid.uuid4())
476 477 @property
478 - def delete_after_days(self):
479 if self.delete_after is None: 480 return None 481 482 delta = self.delete_after - datetime.datetime.now() 483 return delta.days if delta.days > 0 else 0
484 485 @delete_after_days.setter
486 - def delete_after_days(self, days):
487 if days is None or days == -1: 488 self.delete_after = None 489 return 490 491 delete_after = datetime.datetime.now() + datetime.timedelta(days=days+1) 492 delete_after = delete_after.replace(hour=0, minute=0, second=0, microsecond=0) 493 self.delete_after = delete_after
494 495 @property
496 - def delete_after_msg(self):
497 if self.delete_after_days == 0: 498 return "will be deleted ASAP" 499 return "will be deleted after {} days".format(self.delete_after_days)
500 501 @property
502 - def admin_mails(self):
503 mails = [self.user.mail] 504 for perm in self.copr_permissions: 505 if perm.copr_admin == helpers.PermissionEnum('approved'): 506 mails.append(perm.user.mail) 507 return mails
508
509 -class CoprPermission(db.Model, helpers.Serializer):
510 """ 511 Association class for Copr<->Permission relation 512 """ 513 514 # see helpers.PermissionEnum for possible values of the fields below 515 # can this user build in the copr? 516 copr_builder = db.Column(db.SmallInteger, default=0) 517 # can this user serve as an admin? (-> edit and approve permissions) 518 copr_admin = db.Column(db.SmallInteger, default=0) 519 520 # relations 521 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), primary_key=True) 522 user = db.relationship("User", backref=db.backref("copr_permissions")) 523 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), primary_key=True) 524 copr = db.relationship("Copr", backref=db.backref("copr_permissions")) 525
526 - def set_permission(self, name, value):
527 if name == 'admin': 528 self.copr_admin = value 529 elif name == 'builder': 530 self.copr_builder = value 531 else: 532 raise KeyError("{0} is not a valid copr permission".format(name))
533
534 - def get_permission(self, name):
535 if name == 'admin': 536 return 0 if self.copr_admin is None else self.copr_admin 537 if name == 'builder': 538 return 0 if self.copr_builder is None else self.copr_builder 539 raise KeyError("{0} is not a valid copr permission".format(name))
540
541 542 -class CoprDir(db.Model):
543 """ 544 Represents one of data directories for a copr. 545 """ 546 id = db.Column(db.Integer, primary_key=True) 547 548 name = db.Column(db.Text, index=True) 549 main = db.Column(db.Boolean, default=False, server_default="0", nullable=False) 550 551 ownername = db.Column(db.Text, index=True, nullable=False) 552 553 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), index=True, nullable=False) 554 copr = db.relationship("Copr", backref=db.backref("dirs")) 555 556 __table_args__ = ( 557 db.Index('only_one_main_copr_dir', copr_id, main, 558 unique=True, postgresql_where=(main==True)), 559 560 db.UniqueConstraint('ownername', 'name', 561 name='ownername_copr_dir_uniq'), 562 ) 563
564 - def __init__(self, *args, **kwargs):
565 if kwargs.get('copr') and not kwargs.get('ownername'): 566 kwargs['ownername'] = kwargs.get('copr').owner_name 567 super(CoprDir, self).__init__(*args, **kwargs)
568 569 @property
570 - def full_name(self):
571 return "{}/{}".format(self.copr.owner_name, self.name)
572 573 @property
574 - def repo_name(self):
575 return "{}-{}".format(self.copr.owner_name, self.name)
576 577 @property
578 - def repo_url(self):
579 return "/".join([app.config["BACKEND_BASE_URL"], 580 u"results", self.full_name])
581 582 @property
583 - def repo_id(self):
584 if self.copr.is_a_group_project: 585 return "group_{}-{}".format(self.copr.group.name, self.name) 586 else: 587 return "{}-{}".format(self.copr.user.name, self.name)
588
589 590 -class Package(db.Model, helpers.Serializer, CoprSearchRelatedData):
591 """ 592 Represents a single package in a project_dir. 593 """ 594 595 __table_args__ = ( 596 db.UniqueConstraint('copr_dir_id', 'name', name='packages_copr_dir_pkgname'), 597 db.Index('package_webhook_sourcetype', 'webhook_rebuild', 'source_type'), 598 ) 599
600 - def __init__(self, *args, **kwargs):
601 if kwargs.get('copr') and not kwargs.get('copr_dir'): 602 kwargs['copr_dir'] = kwargs.get('copr').main_dir 603 super(Package, self).__init__(*args, **kwargs)
604 605 id = db.Column(db.Integer, primary_key=True) 606 name = db.Column(db.String(100), nullable=False) 607 # Source of the build: type identifier 608 source_type = db.Column(db.Integer, default=helpers.BuildSourceEnum("unset")) 609 # Source of the build: description in json, example: git link, srpm url, etc. 610 source_json = db.Column(db.Text) 611 # True if the package is built automatically via webhooks 612 webhook_rebuild = db.Column(db.Boolean, default=False) 613 # enable networking during a build process 614 enable_net = db.Column(db.Boolean, default=False, server_default="0", nullable=False) 615 616 # don't keep more builds of this package per copr-dir 617 max_builds = db.Column(db.Integer, index=True) 618 619 @validates('max_builds')
620 - def validate_max_builds(self, field, value):
621 return None if value == 0 else value
622 623 builds = db.relationship("Build", order_by="Build.id") 624 625 # relations 626 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), index=True) 627 copr = db.relationship("Copr", backref=db.backref("packages")) 628 629 copr_dir_id = db.Column(db.Integer, db.ForeignKey("copr_dir.id"), index=True) 630 copr_dir = db.relationship("CoprDir", backref=db.backref("packages")) 631 632 # comma-separated list of wildcards of chroot names that this package should 633 # not be built against, e.g. "fedora-*, epel-*-i386" 634 chroot_blacklist_raw = db.Column(db.Text) 635 636 @property
637 - def dist_git_repo(self):
638 return "{}/{}".format(self.copr_dir.full_name, self.name)
639 640 @property
641 - def source_json_dict(self):
642 if not self.source_json: 643 return {} 644 return json.loads(self.source_json)
645 646 @property
647 - def source_type_text(self):
649 650 @property
651 - def has_source_type_set(self):
652 """ 653 Package's source type (and source_json) is being derived from its first build, which works except 654 for "link" and "upload" cases. Consider these being equivalent to source_type being unset. 655 """ 656 return self.source_type and self.source_type_text != "link" and self.source_type_text != "upload"
657 658 @property
659 - def dist_git_url(self):
660 if "DIST_GIT_URL" in app.config: 661 return "{}/{}.git".format(app.config["DIST_GIT_URL"], self.dist_git_repo) 662 return None
663 664 @property
665 - def dist_git_clone_url(self):
666 if "DIST_GIT_CLONE_URL" in app.config: 667 return "{}/{}.git".format(app.config["DIST_GIT_CLONE_URL"], self.dist_git_repo) 668 else: 669 return self.dist_git_url
670
671 - def last_build(self, successful=False):
672 for build in reversed(self.builds): 673 if not successful or build.state == "succeeded": 674 return build 675 return None
676
677 - def to_dict(self, with_latest_build=False, with_latest_succeeded_build=False, with_all_builds=False):
678 package_dict = super(Package, self).to_dict() 679 package_dict['source_type'] = helpers.BuildSourceEnum(package_dict['source_type']) 680 681 if with_latest_build: 682 build = self.last_build(successful=False) 683 package_dict['latest_build'] = build.to_dict(with_chroot_states=True) if build else None 684 if with_latest_succeeded_build: 685 build = self.last_build(successful=True) 686 package_dict['latest_succeeded_build'] = build.to_dict(with_chroot_states=True) if build else None 687 if with_all_builds: 688 package_dict['builds'] = [build.to_dict(with_chroot_states=True) for build in reversed(self.builds)] 689 690 return package_dict
691 694 695 696 @property
697 - def chroot_blacklist(self):
698 if not self.chroot_blacklist_raw: 699 return [] 700 701 blacklisted = [] 702 for pattern in self.chroot_blacklist_raw.split(','): 703 pattern = pattern.strip() 704 if not pattern: 705 continue 706 blacklisted.append(pattern) 707 708 return blacklisted
709 710 711 @staticmethod
712 - def matched_chroot(chroot, patterns):
713 for pattern in patterns: 714 if fnmatch(chroot.name, pattern): 715 return True 716 return False
717 718 719 @property
720 - def main_pkg(self):
721 if self.copr_dir.main: 722 return self 723 724 main_pkg = Package.query.filter_by( 725 name=self.name, 726 copr_dir_id=self.copr.main_dir.id 727 ).first() 728 return main_pkg
729 730 731 @property
732 - def chroots(self):
733 chroots = list(self.copr.active_chroots) 734 if not self.chroot_blacklist_raw: 735 # no specific blacklist 736 if self.copr_dir.main: 737 return chroots 738 return self.main_pkg.chroots 739 740 filtered = [c for c in chroots if not self.matched_chroot(c, self.chroot_blacklist)] 741 # We never want to filter everything, this is a misconfiguration. 742 return filtered if filtered else chroots
743
744 745 -class Build(db.Model, helpers.Serializer):
746 """ 747 Representation of one build in one copr 748 """ 749 750 SCM_COMMIT = 'commit' 751 SCM_PULL_REQUEST = 'pull-request' 752 753 __table_args__ = (db.Index('build_canceled', "canceled"), 754 db.Index('build_order', "is_background", "id"), 755 db.Index('build_filter', "source_type", "canceled"), 756 db.Index('build_canceled_is_background_source_status_id_idx', 'canceled', "is_background", "source_status", "id"), 757 ) 758
759 - def __init__(self, *args, **kwargs):
760 if kwargs.get('source_type') == helpers.BuildSourceEnum("custom"): 761 source_dict = json.loads(kwargs['source_json']) 762 if 'fedora-latest' in source_dict['chroot']: 763 arch = source_dict['chroot'].rsplit('-', 2)[2] 764 source_dict['chroot'] = \ 765 MockChroot.latest_fedora_branched_chroot(arch=arch).name 766 kwargs['source_json'] = json.dumps(source_dict) 767 768 if kwargs.get('copr') and not kwargs.get('copr_dir'): 769 kwargs['copr_dir'] = kwargs.get('copr').main_dir 770 771 super(Build, self).__init__(*args, **kwargs)
772 773 id = db.Column(db.Integer, primary_key=True) 774 # single url to the source rpm, should not contain " ", "\n", "\t" 775 pkgs = db.Column(db.Text) 776 # built packages 777 built_packages = db.Column(db.Text) 778 # version of the srpm package got by rpm 779 pkg_version = db.Column(db.Text) 780 # was this build canceled by user? 781 canceled = db.Column(db.Boolean, default=False) 782 # list of space separated additional repos 783 repos = db.Column(db.Text) 784 # the three below represent time of important events for this build 785 # as returned by int(time.time()) 786 submitted_on = db.Column(db.Integer, nullable=False) 787 # directory name on backend with the srpm build results 788 result_dir = db.Column(db.Text, default='', server_default='', nullable=False) 789 # memory requirements for backend builder 790 memory_reqs = db.Column(db.Integer, default=constants.DEFAULT_BUILD_MEMORY) 791 # maximum allowed time of build, build will fail if exceeded 792 timeout = db.Column(db.Integer, default=constants.DEFAULT_BUILD_TIMEOUT) 793 # enable networking during a build process 794 enable_net = db.Column(db.Boolean, default=False, 795 server_default="0", nullable=False) 796 # Source of the build: type identifier 797 source_type = db.Column(db.Integer, default=helpers.BuildSourceEnum("unset")) 798 # Source of the build: description in json, example: git link, srpm url, etc. 799 source_json = db.Column(db.Text) 800 # Type of failure: type identifier 801 fail_type = db.Column(db.Integer, default=FailTypeEnum("unset")) 802 # background builds has lesser priority than regular builds. 803 is_background = db.Column(db.Boolean, default=False, server_default="0", nullable=False) 804 805 source_status = db.Column(db.Integer, default=StatusEnum("waiting")) 806 srpm_url = db.Column(db.Text) 807 808 # relations 809 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), index=True) 810 user = db.relationship("User", backref=db.backref("builds")) 811 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), index=True) 812 copr = db.relationship("Copr", backref=db.backref("builds")) 813 package_id = db.Column(db.Integer, db.ForeignKey("package.id"), index=True) 814 package = db.relationship("Package") 815 816 chroots = association_proxy("build_chroots", "mock_chroot") 817 818 batch_id = db.Column(db.Integer, db.ForeignKey("batch.id")) 819 batch = db.relationship("Batch", backref=db.backref("builds")) 820 821 module_id = db.Column(db.Integer, db.ForeignKey("module.id"), index=True) 822 module = db.relationship("Module", backref=db.backref("builds")) 823 824 copr_dir_id = db.Column(db.Integer, db.ForeignKey("copr_dir.id"), index=True) 825 copr_dir = db.relationship("CoprDir", backref=db.backref("builds")) 826 827 # scm integration properties 828 scm_object_id = db.Column(db.Text) 829 scm_object_type = db.Column(db.Text) 830 scm_object_url = db.Column(db.Text) 831 832 # method to call on build state change 833 update_callback = db.Column(db.Text) 834 835 @property
836 - def user_name(self):
837 return self.user.name
838 839 @property
840 - def group_name(self):
841 return self.copr.group.name
842 843 @property
844 - def copr_name(self):
845 return self.copr.name
846 847 @property
848 - def copr_dirname(self):
849 return self.copr_dir.name
850 851 @property
852 - def copr_full_dirname(self):
853 return self.copr_dir.full_name
854 855 @property
856 - def fail_type_text(self):
857 return FailTypeEnum(self.fail_type)
858 859 @property
860 - def repos_list(self):
861 if self.repos is None: 862 return list() 863 else: 864 return self.repos.split()
865 866 @property
867 - def task_id(self):
868 return str(self.id)
869 870 @property
871 - def id_fixed_width(self):
872 return "{:08d}".format(self.id)
873
874 - def get_import_log_urls(self, admin=False):
875 logs = [self.import_log_url_backend] 876 if admin: 877 logs.append(self.import_log_url_distgit) 878 return list(filter(None, logs))
879 880 @property
881 - def import_log_url_distgit(self):
882 if app.config["COPR_DIST_GIT_LOGS_URL"]: 883 return "{}/{}.log".format(app.config["COPR_DIST_GIT_LOGS_URL"], 884 self.task_id.replace('/', '_')) 885 return None
886 887 @property
888 - def import_log_url_backend(self):
889 parts = ["results", self.copr.owner_name, self.copr_dirname, 890 "srpm-builds", self.id_fixed_width, "builder-live.log"] 891 path = os.path.normpath(os.path.join(*parts)) 892 return urljoin(app.config["BACKEND_BASE_URL"], path)
893 894 @property
895 - def source_json_dict(self):
896 if not self.source_json: 897 return {} 898 return json.loads(self.source_json)
899 900 @property
901 - def started_on(self):
902 return self.min_started_on
903 904 @property
905 - def min_started_on(self):
906 mb_list = [chroot.started_on for chroot in 907 self.build_chroots if chroot.started_on] 908 if len(mb_list) > 0: 909 return min(mb_list) 910 else: 911 return None
912 913 @property
914 - def ended_on(self):
915 return self.max_ended_on
916 917 @property
918 - def max_ended_on(self):
919 if not self.build_chroots: 920 return None 921 if any(chroot.ended_on is None for chroot in self.build_chroots): 922 return None 923 return max(chroot.ended_on for chroot in self.build_chroots)
924 925 @property
926 - def chroots_started_on(self):
927 return {chroot.name: chroot.started_on for chroot in self.build_chroots}
928 929 @property
930 - def chroots_ended_on(self):
931 return {chroot.name: chroot.ended_on for chroot in self.build_chroots}
932 933 @property
934 - def source_type_text(self):
936 937 @property
938 - def source_metadata(self):
939 if self.source_json is None: 940 return None 941 942 try: 943 return json.loads(self.source_json) 944 except (TypeError, ValueError): 945 return None
946 947 @property
948 - def chroot_states(self):
949 return map(lambda chroot: chroot.status, self.build_chroots)
950
951 - def get_chroots_by_status(self, statuses=None):
952 """ 953 Get build chroots with states which present in `states` list 954 If states == None, function returns build_chroots 955 """ 956 chroot_states_map = dict(zip(self.build_chroots, self.chroot_states)) 957 if statuses is not None: 958 statuses = set(statuses) 959 else: 960 return self.build_chroots 961 962 return [ 963 chroot for chroot, status in chroot_states_map.items() 964 if status in statuses 965 ]
966 967 @property
968 - def chroots_dict_by_name(self):
969 return {b.name: b for b in self.build_chroots}
970 971 @property
972 - def status(self):
973 """ 974 Return build status. 975 """ 976 if self.canceled: 977 return StatusEnum("canceled") 978 979 use_src_statuses = ["starting", "pending", "running", "failed"] 980 if self.source_status in [StatusEnum(s) for s in use_src_statuses]: 981 return self.source_status 982 983 for state in ["running", "starting", "pending", "failed", "succeeded", "skipped", "forked", "waiting"]: 984 if StatusEnum(state) in self.chroot_states: 985 if state == "waiting": 986 return self.source_status 987 else: 988 return StatusEnum(state) 989 990 return None
991 992 @property
993 - def state(self):
994 """ 995 Return text representation of status of this build. 996 """ 997 if self.status != None: 998 return StatusEnum(self.status) 999 return "unknown"
1000 1001 @property
1002 - def cancelable(self):
1003 """ 1004 Find out if this build is cancelable. 1005 """ 1006 return not self.finished and self.status != StatusEnum("starting")
1007 1008 @property
1009 - def repeatable(self):
1010 """ 1011 Find out if this build is repeatable. 1012 1013 Build is repeatable only if sources has been imported. 1014 """ 1015 return self.source_status == StatusEnum("succeeded")
1016 1017 @property
1018 - def finished(self):
1019 """ 1020 Find out if this build is in finished state. 1021 1022 Build is finished only if all its build_chroots are in finished state or 1023 the build was canceled. 1024 """ 1025 if self.canceled: 1026 return True 1027 if not self.build_chroots: 1028 return StatusEnum(self.source_status) in helpers.FINISHED_STATUSES 1029 return all([chroot.finished for chroot in self.build_chroots])
1030 1031 @property
1032 - def blocked(self):
1033 return bool(self.batch and self.batch.blocked_by and not self.batch.blocked_by.finished)
1034 1035 @property
1036 - def persistent(self):
1037 """ 1038 Find out if this build is persistent. 1039 1040 This property is inherited from the project. 1041 """ 1042 return self.copr.persistent
1043 1044 @property
1045 - def package_name(self):
1046 try: 1047 return self.package.name 1048 except: 1049 return None
1050
1051 - def to_dict(self, options=None, with_chroot_states=False):
1052 result = super(Build, self).to_dict(options) 1053 result["src_pkg"] = result["pkgs"] 1054 del result["pkgs"] 1055 del result["copr_id"] 1056 1057 result['source_type'] = helpers.BuildSourceEnum(result['source_type']) 1058 result["state"] = self.state 1059 1060 if with_chroot_states: 1061 result["chroots"] = {b.name: b.state for b in self.build_chroots} 1062 1063 return result
1064
1065 1066 -class DistGitBranch(db.Model, helpers.Serializer):
1067 """ 1068 1:N mapping: branch -> chroots 1069 """ 1070 1071 # Name of the branch used on dist-git machine. 1072 name = db.Column(db.String(50), primary_key=True)
1073
1074 1075 -class MockChroot(db.Model, helpers.Serializer):
1076 """ 1077 Representation of mock chroot 1078 """ 1079 1080 __table_args__ = ( 1081 db.UniqueConstraint('os_release', 'os_version', 'arch', name='mock_chroot_uniq'), 1082 ) 1083 1084 id = db.Column(db.Integer, primary_key=True) 1085 # fedora/epel/..., mandatory 1086 os_release = db.Column(db.String(50), nullable=False) 1087 # 18/rawhide/..., optional (mock chroot doesn"t need to have this) 1088 os_version = db.Column(db.String(50), nullable=False) 1089 # x86_64/i686/..., mandatory 1090 arch = db.Column(db.String(50), nullable=False) 1091 is_active = db.Column(db.Boolean, default=True) 1092 1093 # Reference branch name 1094 distgit_branch_name = db.Column(db.String(50), 1095 db.ForeignKey("dist_git_branch.name"), 1096 nullable=False) 1097 1098 distgit_branch = db.relationship("DistGitBranch", 1099 backref=db.backref("chroots")) 1100 1101 # After a mock_chroot is EOLed, this is set to true so that copr_prune_results 1102 # will skip all projects using this chroot 1103 final_prunerepo_done = db.Column(db.Boolean, default=False, server_default="0", nullable=False) 1104 1105 @classmethod
1106 - def latest_fedora_branched_chroot(cls, arch='x86_64'):
1107 return (cls.query 1108 .filter(cls.is_active == True) 1109 .filter(cls.os_release == 'fedora') 1110 .filter(cls.os_version != 'rawhide') 1111 .filter(cls.arch == arch) 1112 .order_by(cls.os_version.desc()) 1113 .first())
1114 1115 @property
1116 - def name(self):
1117 """ 1118 Textual representation of name of this chroot 1119 """ 1120 return "{}-{}-{}".format(self.os_release, self.os_version, self.arch)
1121 1122 @property
1123 - def name_release(self):
1124 """ 1125 Textual representation of name of this or release 1126 """ 1127 return "{}-{}".format(self.os_release, self.os_version)
1128 1129 @property
1130 - def os(self):
1131 """ 1132 Textual representation of the operating system name 1133 """ 1134 return "{0} {1}".format(self.os_release, self.os_version)
1135 1136 @property
1137 - def serializable_attributes(self):
1138 attr_list = super(MockChroot, self).serializable_attributes 1139 attr_list.extend(["name", "os"]) 1140 return attr_list
1141
1142 1143 -class CoprChroot(db.Model, helpers.Serializer):
1144 """ 1145 Representation of Copr<->MockChroot relation 1146 """ 1147 1148 buildroot_pkgs = db.Column(db.Text) 1149 repos = db.Column(db.Text, default="", server_default="", nullable=False) 1150 mock_chroot_id = db.Column( 1151 db.Integer, db.ForeignKey("mock_chroot.id"), primary_key=True) 1152 mock_chroot = db.relationship( 1153 "MockChroot", backref=db.backref("copr_chroots")) 1154 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), primary_key=True) 1155 copr = db.relationship("Copr", 1156 backref=db.backref( 1157 "copr_chroots", 1158 single_parent=True, 1159 cascade="all,delete,delete-orphan")) 1160 1161 comps_zlib = db.Column(db.LargeBinary(), nullable=True) 1162 comps_name = db.Column(db.String(127), nullable=True) 1163 1164 with_opts = db.Column(db.Text, default="", server_default="", nullable=False) 1165 without_opts = db.Column(db.Text, default="", server_default="", nullable=False) 1166 1167 # Once mock_chroot gets EOL, copr_chroots are going to be deleted 1168 # if their owner doesn't extend their time span 1169 delete_after = db.Column(db.DateTime, index=True) 1170 delete_notify = db.Column(db.DateTime, index=True) 1171
1172 - def update_comps(self, comps_xml):
1173 if isinstance(comps_xml, str): 1174 data = comps_xml.encode("utf-8") 1175 else: 1176 data = comps_xml 1177 self.comps_zlib = zlib.compress(data)
1178 1179 @property
1180 - def buildroot_pkgs_list(self):
1181 return (self.buildroot_pkgs or "").split()
1182 1183 @property
1184 - def repos_list(self):
1185 return (self.repos or "").split()
1186 1187 @property
1188 - def comps(self):
1189 if self.comps_zlib: 1190 return zlib.decompress(self.comps_zlib).decode("utf-8")
1191 1192 @property
1193 - def comps_len(self):
1194 if self.comps_zlib: 1195 return len(zlib.decompress(self.comps_zlib)) 1196 else: 1197 return 0
1198 1199 @property
1200 - def name(self):
1201 return self.mock_chroot.name
1202 1203 @property
1204 - def is_active(self):
1205 return self.mock_chroot.is_active
1206 1207 @property
1208 - def delete_after_days(self):
1209 if not self.delete_after: 1210 return None 1211 now = datetime.datetime.now() 1212 days = (self.delete_after - now).days 1213 return days if days > 0 else 0
1214
1215 - def to_dict(self):
1216 options = {"__columns_only__": [ 1217 "buildroot_pkgs", "repos", "comps_name", "copr_id", "with_opts", "without_opts" 1218 ]} 1219 d = super(CoprChroot, self).to_dict(options=options) 1220 d["mock_chroot"] = self.mock_chroot.name 1221 return d
1222
1223 1224 -class BuildChroot(db.Model, helpers.Serializer):
1225 """ 1226 Representation of Build<->MockChroot relation 1227 """ 1228 1229 __table_args__ = (db.Index('build_chroot_status_started_on_idx', "status", "started_on"),) 1230 1231 mock_chroot_id = db.Column(db.Integer, db.ForeignKey("mock_chroot.id"), 1232 primary_key=True) 1233 mock_chroot = db.relationship("MockChroot", backref=db.backref("builds")) 1234 build_id = db.Column(db.Integer, db.ForeignKey("build.id"), 1235 primary_key=True) 1236 build = db.relationship("Build", backref=db.backref("build_chroots")) 1237 git_hash = db.Column(db.String(40)) 1238 status = db.Column(db.Integer, default=StatusEnum("waiting")) 1239 1240 started_on = db.Column(db.Integer, index=True) 1241 ended_on = db.Column(db.Integer, index=True) 1242 1243 # directory name on backend with build results 1244 result_dir = db.Column(db.Text, default='', server_default='', nullable=False) 1245 1246 build_requires = db.Column(db.Text) 1247 1248 @property
1249 - def name(self):
1250 """ 1251 Textual representation of name of this chroot 1252 """ 1253 return self.mock_chroot.name
1254 1255 @property
1256 - def state(self):
1257 """ 1258 Return text representation of status of this build chroot 1259 """ 1260 if self.status is not None: 1261 return StatusEnum(self.status) 1262 return "unknown"
1263 1264 @property
1265 - def finished(self):
1266 return self.state in helpers.FINISHED_STATUSES
1267 1268 @property
1269 - def task_id(self):
1270 return "{}-{}".format(self.build_id, self.name)
1271 1272 @property
1273 - def dist_git_url(self):
1274 if app.config["DIST_GIT_URL"]: 1275 if self.state == "forked": 1276 if self.build.copr.forked_from.deleted: 1277 return None 1278 copr_dirname = self.build.copr.forked_from.main_dir.full_name 1279 else: 1280 copr_dirname = self.build.copr_dir.full_name 1281 return "{}/{}/{}.git/commit/?id={}".format(app.config["DIST_GIT_URL"], 1282 copr_dirname, 1283 self.build.package.name, 1284 self.git_hash) 1285 return None
1286 1287 @property
1288 - def result_dir_url(self):
1289 if not self.result_dir: 1290 return None 1291 return urljoin(app.config["BACKEND_BASE_URL"], os.path.join( 1292 "results", self.build.copr_dir.full_name, self.name, self.result_dir, ""))
1293
1294 1295 -class LegalFlag(db.Model, helpers.Serializer):
1296 id = db.Column(db.Integer, primary_key=True) 1297 # message from user who raised the flag (what he thinks is wrong) 1298 raise_message = db.Column(db.Text) 1299 # time of raising the flag as returned by int(time.time()) 1300 raised_on = db.Column(db.Integer) 1301 # time of resolving the flag by admin as returned by int(time.time()) 1302 resolved_on = db.Column(db.Integer) 1303 1304 # relations 1305 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), nullable=True) 1306 # cascade="all" means that we want to keep these even if copr is deleted 1307 copr = db.relationship( 1308 "Copr", backref=db.backref("legal_flags", cascade="all")) 1309 # user who reported the problem 1310 reporter_id = db.Column(db.Integer, db.ForeignKey("user.id")) 1311 reporter = db.relationship("User", 1312 backref=db.backref("legal_flags_raised"), 1313 foreign_keys=[reporter_id], 1314 primaryjoin="LegalFlag.reporter_id==User.id") 1315 # admin who resolved the problem 1316 resolver_id = db.Column( 1317 db.Integer, db.ForeignKey("user.id"), nullable=True) 1318 resolver = db.relationship("User", 1319 backref=db.backref("legal_flags_resolved"), 1320 foreign_keys=[resolver_id], 1321 primaryjoin="LegalFlag.resolver_id==User.id")
1322
1323 1324 -class Action(db.Model, helpers.Serializer):
1325 """ 1326 Representation of a custom action that needs 1327 backends cooperation/admin attention/... 1328 """ 1329 1330 id = db.Column(db.Integer, primary_key=True) 1331 # see ActionTypeEnum 1332 action_type = db.Column(db.Integer, nullable=False) 1333 # copr, ...; downcase name of class of modified object 1334 object_type = db.Column(db.String(20)) 1335 # id of the modified object 1336 object_id = db.Column(db.Integer) 1337 # old and new values of the changed property 1338 old_value = db.Column(db.String(255)) 1339 new_value = db.Column(db.String(255)) 1340 # additional data 1341 data = db.Column(db.Text) 1342 # result of the action, see BackendResultEnum 1343 result = db.Column( 1344 db.Integer, default=BackendResultEnum("waiting")) 1345 # optional message from the backend/whatever 1346 message = db.Column(db.Text) 1347 # time created as returned by int(time.time()) 1348 created_on = db.Column(db.Integer) 1349 # time ended as returned by int(time.time()) 1350 ended_on = db.Column(db.Integer) 1351
1352 - def __str__(self):
1353 return self.__unicode__()
1354
1355 - def __unicode__(self):
1356 if self.action_type == ActionTypeEnum("delete"): 1357 return "Deleting {0} {1}".format(self.object_type, self.old_value) 1358 elif self.action_type == ActionTypeEnum("legal-flag"): 1359 return "Legal flag on copr {0}.".format(self.old_value) 1360 1361 return "Action {0} on {1}, old value: {2}, new value: {3}.".format( 1362 self.action_type, self.object_type, self.old_value, self.new_value)
1363
1364 - def to_dict(self, **kwargs):
1365 d = super(Action, self).to_dict() 1366 if d.get("object_type") == "module": 1367 module = Module.query.filter(Module.id == d["object_id"]).first() 1368 data = json.loads(d["data"]) 1369 data.update({ 1370 "projectname": module.copr.name, 1371 "ownername": module.copr.owner_name, 1372 "modulemd_b64": module.yaml_b64, 1373 }) 1374 d["data"] = json.dumps(data) 1375 return d
1376
1377 1378 -class Krb5Login(db.Model, helpers.Serializer):
1379 """ 1380 Represents additional user information for kerberos authentication. 1381 """ 1382 1383 __tablename__ = "krb5_login" 1384 1385 # FK to User table 1386 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) 1387 1388 # 'string' from 'copr.conf' from KRB5_LOGIN[string] 1389 config_name = db.Column(db.String(30), nullable=False, primary_key=True) 1390 1391 # krb's primary, i.e. 'username' from 'username@EXAMPLE.COM' 1392 primary = db.Column(db.String(80), nullable=False, primary_key=True) 1393 1394 user = db.relationship("User", backref=db.backref("krb5_logins"))
1395
1396 1397 -class CounterStat(db.Model, helpers.Serializer):
1398 """ 1399 Generic store for simple statistics. 1400 """ 1401 1402 name = db.Column(db.String(127), primary_key=True) 1403 counter_type = db.Column(db.String(30)) 1404 1405 counter = db.Column(db.Integer, default=0, server_default="0")
1406
1407 1408 -class Group(db.Model, helpers.Serializer):
1409 1410 """ 1411 Represents FAS groups and their aliases in Copr 1412 """ 1413 1414 id = db.Column(db.Integer, primary_key=True) 1415 name = db.Column(db.String(127)) 1416 1417 # TODO: add unique=True 1418 fas_name = db.Column(db.String(127)) 1419 1420 @property
1421 - def at_name(self):
1422 return u"@{}".format(self.name)
1423
1424 - def __str__(self):
1425 return self.__unicode__()
1426
1427 - def __unicode__(self):
1428 return "{} (fas: {})".format(self.name, self.fas_name)
1429
1430 1431 -class Batch(db.Model):
1432 id = db.Column(db.Integer, primary_key=True) 1433 blocked_by_id = db.Column(db.Integer, db.ForeignKey("batch.id"), nullable=True) 1434 blocked_by = db.relationship("Batch", remote_side=[id]) 1435 1436 @property
1437 - def finished(self):
1438 return all([b.finished for b in self.builds])
1439
1440 1441 -class Module(db.Model, helpers.Serializer):
1442 id = db.Column(db.Integer, primary_key=True) 1443 name = db.Column(db.String(100), nullable=False) 1444 stream = db.Column(db.String(100), nullable=False) 1445 version = db.Column(db.BigInteger, nullable=False) 1446 summary = db.Column(db.String(100), nullable=False) 1447 description = db.Column(db.Text) 1448 created_on = db.Column(db.Integer, nullable=True) 1449 1450 # When someone submits YAML (not generate one on the copr modules page), we might want to use that exact file. 1451 # Yaml produced by deconstructing into pieces and constructed back can look differently, 1452 # which is not desirable (Imo) 1453 # 1454 # Also if there are fields which are not covered by this model, we will be able to add them in the future 1455 # and fill them with data from this blob 1456 yaml_b64 = db.Column(db.Text) 1457 1458 # relations 1459 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id")) 1460 copr = db.relationship("Copr", backref=db.backref("modules")) 1461 1462 __table_args__ = ( 1463 db.UniqueConstraint("copr_id", "name", "stream", "version", name="copr_name_stream_version_uniq"), 1464 ) 1465 1466 @property
1467 - def yaml(self):
1468 return base64.b64decode(self.yaml_b64)
1469 1470 @property
1471 - def modulemd(self):
1472 mmd = Modulemd.ModuleStream() 1473 mmd.import_from_string(self.yaml.decode("utf-8")) 1474 return mmd
1475 1476 @property
1477 - def nsv(self):
1478 return "-".join([self.name, self.stream, str(self.version)])
1479 1480 @property
1481 - def full_name(self):
1482 return "{}/{}".format(self.copr.full_name, self.nsv)
1483 1484 @property
1485 - def action(self):
1486 return Action.query.filter(Action.object_type == "module").filter(Action.object_id == self.id).first()
1487 1488 @property
1489 - def status(self):
1490 """ 1491 Return numeric representation of status of this build 1492 """ 1493 if any(b for b in self.builds if b.status == StatusEnum("failed")): 1494 return ModuleStatusEnum("failed") 1495 return self.action.result if self.action else ModuleStatusEnum("pending")
1496 1497 @property
1498 - def state(self):
1499 """ 1500 Return text representation of status of this build 1501 """ 1502 return ModuleStatusEnum(self.status)
1503 1504 @property
1505 - def rpm_filter(self):
1506 return self.modulemd.get_rpm_filter().get()
1507 1508 @property
1509 - def rpm_api(self):
1510 return self.modulemd.get_rpm_api().get()
1511 1512 @property
1513 - def profiles(self):
1514 return {k: v.get_rpms().get() for k, v in self.modulemd.get_profiles().items()}
1515
1516 1517 -class BuildsStatistics(db.Model):
1518 time = db.Column(db.Integer, primary_key=True) 1519 stat_type = db.Column(db.Text, primary_key=True) 1520 running = db.Column(db.Integer) 1521 pending = db.Column(db.Integer)
1522