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