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 _cached_status = None 884 _cached_status_set = None 885 886 @property
887 - def user_name(self):
888 return self.user.name
889 890 @property
891 - def group_name(self):
892 return self.copr.group.name
893 894 @property
895 - def copr_name(self):
896 return self.copr.name
897 898 @property
899 - def copr_dirname(self):
900 return self.copr_dir.name
901 902 @property
903 - def copr_full_dirname(self):
904 return self.copr_dir.full_name
905 906 @property
907 - def fail_type_text(self):
908 return FailTypeEnum(self.fail_type)
909 910 @property
911 - def repos_list(self):
912 if self.repos is None: 913 return list() 914 else: 915 return self.repos.split()
916 917 @property
918 - def task_id(self):
919 return str(self.id)
920 921 @property
922 - def id_fixed_width(self):
923 return "{:08d}".format(self.id)
924
925 - def get_import_log_urls(self, admin=False):
926 logs = [self.import_log_url_backend] 927 if admin: 928 logs.append(self.import_log_url_distgit) 929 return list(filter(None, logs))
930 931 @property
932 - def import_log_url_distgit(self):
933 if app.config["COPR_DIST_GIT_LOGS_URL"]: 934 return "{}/{}.log".format(app.config["COPR_DIST_GIT_LOGS_URL"], 935 self.task_id.replace('/', '_')) 936 return None
937 938 @property
939 - def import_log_url_backend(self):
940 parts = ["results", self.copr.owner_name, self.copr_dirname, 941 "srpm-builds", self.id_fixed_width, 942 "builder-live.log" if self.source_status == StatusEnum("running") 943 else "builder-live.log.gz"] 944 path = os.path.normpath(os.path.join(*parts)) 945 return urljoin(app.config["BACKEND_BASE_URL"], path)
946 947 @property
948 - def source_json_dict(self):
949 if not self.source_json: 950 return {} 951 return json.loads(self.source_json)
952 953 @property
954 - def started_on(self):
955 return self.min_started_on
956 957 @property
958 - def min_started_on(self):
959 mb_list = [chroot.started_on for chroot in 960 self.build_chroots if chroot.started_on] 961 if len(mb_list) > 0: 962 return min(mb_list) 963 else: 964 return None
965 966 @property
967 - def ended_on(self):
968 return self.max_ended_on
969 970 @property
971 - def max_ended_on(self):
972 if not self.build_chroots: 973 return None 974 if any(chroot.ended_on is None for chroot in self.build_chroots): 975 return None 976 return max(chroot.ended_on for chroot in self.build_chroots)
977 978 @property
979 - def chroots_started_on(self):
980 return {chroot.name: chroot.started_on for chroot in self.build_chroots}
981 982 @property
983 - def chroots_ended_on(self):
984 return {chroot.name: chroot.ended_on for chroot in self.build_chroots}
985 986 @property
987 - def source_type_text(self):
989 990 @property
991 - def source_metadata(self):
992 if self.source_json is None: 993 return None 994 995 try: 996 return json.loads(self.source_json) 997 except (TypeError, ValueError): 998 return None
999 1000 @property
1001 - def chroot_states(self):
1002 return list(map(lambda chroot: chroot.status, self.build_chroots))
1003
1004 - def get_chroots_by_status(self, statuses=None):
1005 """ 1006 Get build chroots with states which present in `states` list 1007 If states == None, function returns build_chroots 1008 """ 1009 chroot_states_map = dict(zip(self.build_chroots, self.chroot_states)) 1010 if statuses is not None: 1011 statuses = set(statuses) 1012 else: 1013 return self.build_chroots 1014 1015 return [ 1016 chroot for chroot, status in chroot_states_map.items() 1017 if status in statuses 1018 ]
1019 1020 @property
1021 - def chroots_dict_by_name(self):
1022 return {b.name: b for b in self.build_chroots}
1023 1024 @property
1025 - def status(self):
1026 """ 1027 Return build status. 1028 """ 1029 if self.canceled: 1030 return StatusEnum("canceled") 1031 1032 use_src_statuses = ["starting", "pending", "running", "importing", "failed"] 1033 if self.source_status in [StatusEnum(s) for s in use_src_statuses]: 1034 return self.source_status 1035 1036 if not self.chroot_states: 1037 # There were some builds in DB which had source_status equal 1038 # to 'succeeded', while they had no build_chroots created. 1039 # The original source of this inconsistency isn't known 1040 # because we only ever flip source_status to "succeded" directly 1041 # from the "importing" state. 1042 # Anyways, return something meaningful here so we can debug 1043 # properly if such situation happens. 1044 app.logger.error("Build %s has source_status succeeded, but " 1045 "no build_chroots", self.id) 1046 return StatusEnum("waiting") 1047 1048 for state in ["running", "starting", "pending", "failed", "succeeded", "skipped", "forked"]: 1049 if StatusEnum(state) in self.chroot_states: 1050 return StatusEnum(state) 1051 1052 if StatusEnum("waiting") in self.chroot_states: 1053 # We should atomically flip 1054 # a) build.source_status: "importing" -> "succeeded" and 1055 # b) biuld_chroot.status: "waiting" -> "pending" 1056 # so at this point nothing really should be in "waiting" state. 1057 app.logger.error("Build chroots pending, even though build %s" 1058 " has succeeded source_status", self.id) 1059 return StatusEnum("pending") 1060 1061 return None
1062 1063 @property
1064 - def state(self):
1065 """ 1066 Return text representation of status of this build. 1067 """ 1068 if self.status != None: 1069 return StatusEnum(self.status) 1070 return "unknown"
1071 1072 @property
1073 - def cancelable(self):
1074 """ 1075 Find out if this build is cancelable. 1076 """ 1077 return not self.finished and self.status != StatusEnum("starting")
1078 1079 @property
1080 - def repeatable(self):
1081 """ 1082 Find out if this build is repeatable. 1083 1084 Build is repeatable only if sources has been imported. 1085 """ 1086 return self.source_status == StatusEnum("succeeded")
1087 1088 @property
1089 - def finished(self):
1090 """ 1091 Find out if this build is in finished state. 1092 1093 Build is finished only if all its build_chroots are in finished state or 1094 the build was canceled. 1095 """ 1096 if self.canceled: 1097 return True 1098 if not self.build_chroots: 1099 return StatusEnum(self.source_status) in helpers.FINISHED_STATUSES 1100 return all([chroot.finished for chroot in self.build_chroots])
1101 1102 @property
1103 - def blocked(self):
1104 return bool(self.batch and self.batch.blocked_by and not self.batch.blocked_by.finished)
1105 1106 @property
1107 - def persistent(self):
1108 """ 1109 Find out if this build is persistent. 1110 1111 This property is inherited from the project. 1112 """ 1113 return self.copr.persistent
1114 1115 @property
1116 - def package_name(self):
1117 try: 1118 return self.package.name 1119 except: 1120 return None
1121
1122 - def to_dict(self, options=None, with_chroot_states=False):
1123 result = super(Build, self).to_dict(options) 1124 result["src_pkg"] = result["pkgs"] 1125 del result["pkgs"] 1126 del result["copr_id"] 1127 1128 result['source_type'] = helpers.BuildSourceEnum(result['source_type']) 1129 result["state"] = self.state 1130 1131 if with_chroot_states: 1132 result["chroots"] = {b.name: b.state for b in self.build_chroots} 1133 1134 return result
1135 1136 @property
1137 - def submitter(self):
1138 """ 1139 Return tuple (submitter_string, submitter_link), while the 1140 submitter_link may be empty if we are not able to detect it 1141 wisely. 1142 """ 1143 if self.user: 1144 user = self.user.name 1145 return (user, url_for('coprs_ns.coprs_by_user', username=user)) 1146 1147 if self.submitted_by: 1148 links = ['http://', 'https://'] 1149 if any([self.submitted_by.startswith(x) for x in links]): 1150 return (self.submitted_by, self.submitted_by) 1151 1152 return (self.submitted_by, None) 1153 1154 return (None, None)
1155 1156 @property
1157 - def sandbox(self):
1158 """ 1159 Return a string unique to project + submitter. At this level copr 1160 backend later applies builder user-VM separation policy (VMs are only 1161 re-used for builds which have the same build.sandbox value) 1162 """ 1163 submitter, _ = self.submitter 1164 if not submitter: 1165 # If we don't know build submitter, use "random" value and keep the 1166 # build separated from any other. 1167 submitter = uuid.uuid4() 1168 1169 return '{0}--{1}'.format(self.copr.full_name, submitter)
1170 1171 @property
1172 - def resubmitted_from(self):
1173 return Build.query.filter(Build.id == self.resubmitted_from_id).first()
1174 1175 @property
1176 - def source_is_uploaded(self):
1177 return self.source_type == helpers.BuildSourceEnum('upload')
1178
1179 1180 -class DistGitBranch(db.Model, helpers.Serializer):
1181 """ 1182 1:N mapping: branch -> chroots 1183 """ 1184 1185 # Name of the branch used on dist-git machine. 1186 name = db.Column(db.String(50), primary_key=True)
1187
1188 1189 -class MockChroot(db.Model, helpers.Serializer):
1190 """ 1191 Representation of mock chroot 1192 """ 1193 1194 __table_args__ = ( 1195 db.UniqueConstraint('os_release', 'os_version', 'arch', name='mock_chroot_uniq'), 1196 ) 1197 1198 id = db.Column(db.Integer, primary_key=True) 1199 # fedora/epel/..., mandatory 1200 os_release = db.Column(db.String(50), nullable=False) 1201 # 18/rawhide/..., optional (mock chroot doesn"t need to have this) 1202 os_version = db.Column(db.String(50), nullable=False) 1203 # x86_64/i686/..., mandatory 1204 arch = db.Column(db.String(50), nullable=False) 1205 is_active = db.Column(db.Boolean, default=True) 1206 1207 # Reference branch name 1208 distgit_branch_name = db.Column(db.String(50), 1209 db.ForeignKey("dist_git_branch.name"), 1210 nullable=False) 1211 1212 distgit_branch = db.relationship("DistGitBranch", 1213 backref=db.backref("chroots")) 1214 1215 # After a mock_chroot is EOLed, this is set to true so that copr_prune_results 1216 # will skip all projects using this chroot 1217 final_prunerepo_done = db.Column(db.Boolean, default=False, server_default="0", nullable=False) 1218 1219 comment = db.Column(db.Text, nullable=True) 1220 1221 multilib_pairs = { 1222 'x86_64': 'i386', 1223 } 1224 1225 @classmethod
1226 - def latest_fedora_branched_chroot(cls, arch='x86_64'):
1227 return (cls.query 1228 .filter(cls.is_active == True) 1229 .filter(cls.os_release == 'fedora') 1230 .filter(cls.os_version != 'rawhide') 1231 .filter(cls.arch == arch) 1232 .order_by(cls.os_version.desc()) 1233 .first())
1234 1235 @property
1236 - def name(self):
1237 """ 1238 Textual representation of name of this chroot 1239 """ 1240 return "{}-{}-{}".format(self.os_release, self.os_version, self.arch)
1241 1242 @property
1243 - def name_release(self):
1244 """ 1245 Textual representation of name of this or release 1246 """ 1247 return "{}-{}".format(self.os_release, self.os_version)
1248 1249 @property
1250 - def os(self):
1251 """ 1252 Textual representation of the operating system name 1253 """ 1254 return "{0} {1}".format(self.os_release, self.os_version)
1255 1256 @property
1257 - def serializable_attributes(self):
1258 attr_list = super(MockChroot, self).serializable_attributes 1259 attr_list.extend(["name", "os"]) 1260 return attr_list
1261
1262 1263 -class CoprChroot(db.Model, helpers.Serializer):
1264 """ 1265 Representation of Copr<->MockChroot relation 1266 """ 1267 1268 buildroot_pkgs = db.Column(db.Text) 1269 repos = db.Column(db.Text, default="", server_default="", nullable=False) 1270 mock_chroot_id = db.Column( 1271 db.Integer, db.ForeignKey("mock_chroot.id"), primary_key=True) 1272 mock_chroot = db.relationship( 1273 "MockChroot", backref=db.backref("copr_chroots")) 1274 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), primary_key=True) 1275 copr = db.relationship("Copr", 1276 backref=db.backref( 1277 "copr_chroots", 1278 single_parent=True, 1279 cascade="all,delete,delete-orphan")) 1280 1281 comps_zlib = db.Column(db.LargeBinary(), nullable=True) 1282 comps_name = db.Column(db.String(127), nullable=True) 1283 1284 with_opts = db.Column(db.Text, default="", server_default="", nullable=False) 1285 without_opts = db.Column(db.Text, default="", server_default="", nullable=False) 1286 1287 # Once mock_chroot gets EOL, copr_chroots are going to be deleted 1288 # if their owner doesn't extend their time span 1289 delete_after = db.Column(db.DateTime, index=True) 1290 delete_notify = db.Column(db.DateTime, index=True) 1291
1292 - def update_comps(self, comps_xml):
1293 if isinstance(comps_xml, str): 1294 data = comps_xml.encode("utf-8") 1295 else: 1296 data = comps_xml 1297 self.comps_zlib = zlib.compress(data)
1298 1299 @property
1300 - def buildroot_pkgs_list(self):
1301 return (self.buildroot_pkgs or "").split()
1302 1303 @property
1304 - def repos_list(self):
1305 return (self.repos or "").split()
1306 1307 @property
1308 - def comps(self):
1309 if self.comps_zlib: 1310 return zlib.decompress(self.comps_zlib).decode("utf-8")
1311 1312 @property
1313 - def comps_len(self):
1314 if self.comps_zlib: 1315 return len(zlib.decompress(self.comps_zlib)) 1316 else: 1317 return 0
1318 1319 @property
1320 - def name(self):
1321 return self.mock_chroot.name
1322 1323 @property
1324 - def is_active(self):
1325 return self.mock_chroot.is_active
1326 1327 @property
1328 - def delete_after_days(self):
1329 if not self.delete_after: 1330 return None 1331 now = datetime.datetime.now() 1332 days = (self.delete_after - now).days 1333 return days if days > 0 else 0
1334
1335 - def to_dict(self):
1336 options = {"__columns_only__": [ 1337 "buildroot_pkgs", "repos", "comps_name", "copr_id", "with_opts", "without_opts" 1338 ]} 1339 d = super(CoprChroot, self).to_dict(options=options) 1340 d["mock_chroot"] = self.mock_chroot.name 1341 return d
1342
1343 1344 -class BuildChroot(db.Model, helpers.Serializer):
1345 """ 1346 Representation of Build<->MockChroot relation 1347 """ 1348 1349 __table_args__ = (db.Index('build_chroot_status_started_on_idx', "status", "started_on"),) 1350 1351 mock_chroot_id = db.Column(db.Integer, db.ForeignKey("mock_chroot.id"), 1352 primary_key=True) 1353 mock_chroot = db.relationship("MockChroot", backref=db.backref("builds")) 1354 build_id = db.Column(db.Integer, db.ForeignKey("build.id", ondelete="CASCADE"), 1355 primary_key=True) 1356 build = db.relationship("Build", backref=db.backref("build_chroots", cascade="all, delete-orphan", 1357 passive_deletes=True)) 1358 git_hash = db.Column(db.String(40)) 1359 status = db.Column(db.Integer, default=StatusEnum("waiting")) 1360 1361 started_on = db.Column(db.Integer, index=True) 1362 ended_on = db.Column(db.Integer, index=True) 1363 1364 # directory name on backend with build results 1365 result_dir = db.Column(db.Text, default='', server_default='', nullable=False) 1366 1367 build_requires = db.Column(db.Text) 1368 1369 @property
1370 - def name(self):
1371 """ 1372 Textual representation of name of this chroot 1373 """ 1374 return self.mock_chroot.name
1375 1376 @property
1377 - def state(self):
1378 """ 1379 Return text representation of status of this build chroot 1380 """ 1381 if self.status is not None: 1382 return StatusEnum(self.status) 1383 return "unknown"
1384 1385 @property
1386 - def finished(self):
1387 return self.state in helpers.FINISHED_STATUSES
1388 1389 @property
1390 - def task_id(self):
1391 return "{}-{}".format(self.build_id, self.name)
1392 1393 @property
1394 - def dist_git_url(self):
1395 if app.config["DIST_GIT_URL"]: 1396 if self.state == "forked": 1397 if self.build.copr.forked_from.deleted: 1398 return None 1399 copr_dirname = self.build.copr.forked_from.main_dir.full_name 1400 else: 1401 copr_dirname = self.build.copr_dir.full_name 1402 return "{}/{}/{}.git/commit/?id={}".format(app.config["DIST_GIT_URL"], 1403 copr_dirname, 1404 self.build.package.name, 1405 self.git_hash) 1406 return None
1407 1408 @property
1409 - def result_dir_url(self):
1410 if not self.result_dir: 1411 return None 1412 return urljoin(app.config["BACKEND_BASE_URL"], os.path.join( 1413 "results", self.build.copr_dir.full_name, self.name, self.result_dir, ""))
1414 1415 @property
1428
1429 1430 -class LegalFlag(db.Model, helpers.Serializer):
1431 id = db.Column(db.Integer, primary_key=True) 1432 # message from user who raised the flag (what he thinks is wrong) 1433 raise_message = db.Column(db.Text) 1434 # time of raising the flag as returned by int(time.time()) 1435 raised_on = db.Column(db.Integer) 1436 # time of resolving the flag by admin as returned by int(time.time()) 1437 resolved_on = db.Column(db.Integer) 1438 1439 # relations 1440 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id"), nullable=True) 1441 # cascade="all" means that we want to keep these even if copr is deleted 1442 copr = db.relationship( 1443 "Copr", backref=db.backref("legal_flags", cascade="all")) 1444 # user who reported the problem 1445 reporter_id = db.Column(db.Integer, db.ForeignKey("user.id")) 1446 reporter = db.relationship("User", 1447 backref=db.backref("legal_flags_raised"), 1448 foreign_keys=[reporter_id], 1449 primaryjoin="LegalFlag.reporter_id==User.id") 1450 # admin who resolved the problem 1451 resolver_id = db.Column( 1452 db.Integer, db.ForeignKey("user.id"), nullable=True) 1453 resolver = db.relationship("User", 1454 backref=db.backref("legal_flags_resolved"), 1455 foreign_keys=[resolver_id], 1456 primaryjoin="LegalFlag.resolver_id==User.id")
1457
1458 1459 -class Action(db.Model, helpers.Serializer):
1460 """ 1461 Representation of a custom action that needs 1462 backends cooperation/admin attention/... 1463 """ 1464 1465 id = db.Column(db.Integer, primary_key=True) 1466 # see ActionTypeEnum 1467 action_type = db.Column(db.Integer, nullable=False) 1468 # copr, ...; downcase name of class of modified object 1469 object_type = db.Column(db.String(20)) 1470 # id of the modified object 1471 object_id = db.Column(db.Integer) 1472 # old and new values of the changed property 1473 old_value = db.Column(db.String(255)) 1474 new_value = db.Column(db.String(255)) 1475 # additional data 1476 data = db.Column(db.Text) 1477 # result of the action, see BackendResultEnum 1478 result = db.Column( 1479 db.Integer, default=BackendResultEnum("waiting")) 1480 # optional message from the backend/whatever 1481 message = db.Column(db.Text) 1482 # time created as returned by int(time.time()) 1483 created_on = db.Column(db.Integer, index=True) 1484 # time ended as returned by int(time.time()) 1485 ended_on = db.Column(db.Integer, index=True) 1486
1487 - def __str__(self):
1488 return self.__unicode__()
1489
1490 - def __unicode__(self):
1491 if self.action_type == ActionTypeEnum("delete"): 1492 return "Deleting {0} {1}".format(self.object_type, self.old_value) 1493 elif self.action_type == ActionTypeEnum("legal-flag"): 1494 return "Legal flag on copr {0}.".format(self.old_value) 1495 1496 return "Action {0} on {1}, old value: {2}, new value: {3}.".format( 1497 self.action_type, self.object_type, self.old_value, self.new_value)
1498
1499 - def to_dict(self, **kwargs):
1500 d = super(Action, self).to_dict() 1501 if d.get("object_type") == "module": 1502 module = Module.query.filter(Module.id == d["object_id"]).first() 1503 data = json.loads(d["data"]) 1504 data.update({ 1505 "projectname": module.copr.name, 1506 "ownername": module.copr.owner_name, 1507 "modulemd_b64": module.yaml_b64, 1508 }) 1509 d["data"] = json.dumps(data) 1510 return d
1511
1512 1513 -class Krb5Login(db.Model, helpers.Serializer):
1514 """ 1515 Represents additional user information for kerberos authentication. 1516 """ 1517 1518 __tablename__ = "krb5_login" 1519 1520 # FK to User table 1521 user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) 1522 1523 # 'string' from 'copr.conf' from KRB5_LOGIN[string] 1524 config_name = db.Column(db.String(30), nullable=False, primary_key=True) 1525 1526 # krb's primary, i.e. 'username' from 'username@EXAMPLE.COM' 1527 primary = db.Column(db.String(80), nullable=False, primary_key=True) 1528 1529 user = db.relationship("User", backref=db.backref("krb5_logins"))
1530
1531 1532 -class CounterStat(db.Model, helpers.Serializer):
1533 """ 1534 Generic store for simple statistics. 1535 """ 1536 1537 name = db.Column(db.String(127), primary_key=True) 1538 counter_type = db.Column(db.String(30)) 1539 1540 counter = db.Column(db.Integer, default=0, server_default="0")
1541
1542 1543 -class Group(db.Model, helpers.Serializer):
1544 1545 """ 1546 Represents FAS groups and their aliases in Copr 1547 """ 1548 1549 id = db.Column(db.Integer, primary_key=True) 1550 name = db.Column(db.String(127)) 1551 1552 # TODO: add unique=True 1553 fas_name = db.Column(db.String(127)) 1554 1555 @property
1556 - def at_name(self):
1557 return u"@{}".format(self.name)
1558
1559 - def __str__(self):
1560 return self.__unicode__()
1561
1562 - def __unicode__(self):
1563 return "{} (fas: {})".format(self.name, self.fas_name)
1564
1565 1566 -class Batch(db.Model):
1567 id = db.Column(db.Integer, primary_key=True) 1568 blocked_by_id = db.Column(db.Integer, db.ForeignKey("batch.id"), nullable=True) 1569 blocked_by = db.relationship("Batch", remote_side=[id]) 1570 1571 @property
1572 - def finished(self):
1573 return all([b.finished for b in self.builds])
1574
1575 1576 -class Module(db.Model, helpers.Serializer):
1577 id = db.Column(db.Integer, primary_key=True) 1578 name = db.Column(db.String(100), nullable=False) 1579 stream = db.Column(db.String(100), nullable=False) 1580 version = db.Column(db.BigInteger, nullable=False) 1581 summary = db.Column(db.String(100), nullable=False) 1582 description = db.Column(db.Text) 1583 created_on = db.Column(db.Integer, nullable=True) 1584 1585 # When someone submits YAML (not generate one on the copr modules page), we might want to use that exact file. 1586 # Yaml produced by deconstructing into pieces and constructed back can look differently, 1587 # which is not desirable (Imo) 1588 # 1589 # Also if there are fields which are not covered by this model, we will be able to add them in the future 1590 # and fill them with data from this blob 1591 yaml_b64 = db.Column(db.Text) 1592 1593 # relations 1594 copr_id = db.Column(db.Integer, db.ForeignKey("copr.id")) 1595 copr = db.relationship("Copr", backref=db.backref("modules")) 1596 1597 __table_args__ = ( 1598 db.UniqueConstraint("copr_id", "name", "stream", "version", name="copr_name_stream_version_uniq"), 1599 ) 1600 1601 @property
1602 - def yaml(self):
1603 return base64.b64decode(self.yaml_b64)
1604 1605 @property
1606 - def modulemd(self):
1607 mmd = Modulemd.ModuleStream() 1608 mmd.import_from_string(self.yaml.decode("utf-8")) 1609 return mmd
1610 1611 @property
1612 - def nsv(self):
1613 return "-".join([self.name, self.stream, str(self.version)])
1614 1615 @property
1616 - def full_name(self):
1617 return "{}/{}".format(self.copr.full_name, self.nsv)
1618 1619 @property
1620 - def action(self):
1621 return Action.query.filter(Action.object_type == "module").filter(Action.object_id == self.id).first()
1622 1623 @property
1624 - def status(self):
1625 """ 1626 Return numeric representation of status of this build 1627 """ 1628 if self.action: 1629 return { BackendResultEnum("success"): ModuleStatusEnum("succeeded"), 1630 BackendResultEnum("failure"): ModuleStatusEnum("failed"), 1631 BackendResultEnum("waiting"): ModuleStatusEnum("waiting"), 1632 }[self.action.result] 1633 build_statuses = [b.status for b in self.builds] 1634 for state in ["canceled", "running", "starting", "pending", "failed", "succeeded"]: 1635 if ModuleStatusEnum(state) in build_statuses: 1636 return ModuleStatusEnum(state)
1637 1638 @property
1639 - def state(self):
1640 """ 1641 Return text representation of status of this build 1642 """ 1643 return ModuleStatusEnum(self.status)
1644 1645 @property
1646 - def rpm_filter(self):
1647 return self.modulemd.get_rpm_filter().get()
1648 1649 @property
1650 - def rpm_api(self):
1651 return self.modulemd.get_rpm_api().get()
1652 1653 @property
1654 - def profiles(self):
1655 return {k: v.get_rpms().get() for k, v in self.modulemd.get_profiles().items()}
1656
1657 1658 -class BuildsStatistics(db.Model):
1659 time = db.Column(db.Integer, primary_key=True) 1660 stat_type = db.Column(db.Text, primary_key=True) 1661 running = db.Column(db.Integer) 1662 pending = db.Column(db.Integer)
1663
1664 -class ActionsStatistics(db.Model):
1665 time = db.Column(db.Integer, primary_key=True) 1666 stat_type = db.Column(db.Text, primary_key=True) 1667 waiting = db.Column(db.Integer) 1668 success = db.Column(db.Integer) 1669 failed = db.Column(db.Integer)
1670