class EventDependency(FreshmakerBase): __tablename__ = "event_dependencies" id = db.Column(db.Integer, primary_key=True) event_id = db.Column(db.Integer, db.ForeignKey('events.id'), nullable=False) event_dependency_id = db.Column(db.Integer, db.ForeignKey('events.id'), nullable=False)
class ArtifactBuildCompose(FreshmakerBase): __tablename__ = 'artifact_build_composes' build_id = db.Column(db.Integer, db.ForeignKey('artifact_builds.id'), primary_key=True) compose_id = db.Column(db.Integer, db.ForeignKey('composes.id'), primary_key=True) build = db.relationship('ArtifactBuild', back_populates='composes') compose = db.relationship('Compose', back_populates='builds')
class ArtifactBuild(FreshmakerBase): __tablename__ = "artifact_builds" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String, nullable=False) original_nvr = db.Column(db.String, nullable=True) rebuilt_nvr = db.Column(db.String, nullable=True) type = db.Column(db.Integer) state = db.Column(db.Integer, nullable=False) state_reason = db.Column(db.String, nullable=True) time_submitted = db.Column(db.DateTime, nullable=False) time_completed = db.Column(db.DateTime) # Link to the Artifact on which this one depends and which triggered # the rebuild of this Artifact. dep_on_id = db.Column(db.Integer, db.ForeignKey('artifact_builds.id')) dep_on = relationship('ArtifactBuild', remote_side=[id]) # Event associated with this Build event_id = db.Column(db.Integer, db.ForeignKey('events.id')) event = relationship("Event", back_populates="builds") # Id of corresponding real build in external build system. Currently, it # could be ID of a build in MBS or Koji, maybe others in the future. # build_id may be NULL, which means this build has not been built in # external build system. build_id = db.Column(db.Integer) # Build args in json format. build_args = db.Column(db.String, nullable=True) # The reason why this artifact is rebuilt. Set according to # `freshmaker.types.RebuildReason`. rebuild_reason = db.Column(db.Integer, nullable=True) # pullspec overrides _bundle_pullspec_overrides = db.Column("bundle_pullspec_overrides", db.Text, nullable=True) composes = db.relationship('ArtifactBuildCompose', back_populates='build') @classmethod def create(cls, session, event, name, type, build_id=None, dep_on=None, state=None, original_nvr=None, rebuilt_nvr=None, rebuild_reason=0): now = datetime.utcnow() build = cls(name=name, original_nvr=original_nvr, rebuilt_nvr=rebuilt_nvr, type=type, event=event, state=state or ArtifactBuildState.BUILD.value, build_id=build_id, time_submitted=now, dep_on=dep_on, rebuild_reason=rebuild_reason) session.add(build) return build @validates('state') def validate_state(self, key, field): if field in [s.value for s in list(ArtifactBuildState)]: return field if field in [s.name.lower() for s in list(ArtifactBuildState)]: return ArtifactBuildState[field.upper()].value if isinstance(field, ArtifactBuildState): return field.value raise ValueError("%s: %s, not in %r" % (key, field, list(ArtifactBuildState))) @validates('type') def validate_type(self, key, field): if field in [t.value for t in list(ArtifactType)]: return field if field in [t.name.lower() for t in list(ArtifactType)]: return ArtifactType[field.upper()].value if isinstance(field, ArtifactType): return field.value raise ValueError("%s: %s, not in %r" % (key, field, list(ArtifactType))) @classmethod def get_lowest_build_id(cls, session): """ Returns the lowest build_id. If there is no build so far, returns 0. """ build = ( session.query(ArtifactBuild).filter(cls.build_id != None) # noqa .order_by(ArtifactBuild.build_id.asc()).first()) if not build: return 0 return build.build_id @property def bundle_pullspec_overrides(self): """Return the Python representation of the JSON bundle_pullspec_overrides.""" return (json.loads(self._bundle_pullspec_overrides) if self._bundle_pullspec_overrides else None) @bundle_pullspec_overrides.setter def bundle_pullspec_overrides(self, bundle_pullspec_overrides): """ Set the bundle_pullspec_overrides column to the input bundle_pullspec_overrides as a JSON string. If ``None`` is provided, it will be simply set to ``None`` and not be converted to JSON. :param dict bundle_pullspec_overrides: the dictionary of the bundle_pullspec_overrides or ``None`` """ self._bundle_pullspec_overrides = (json.dumps( bundle_pullspec_overrides, sort_keys=True) if bundle_pullspec_overrides is not None else None) def depending_artifact_builds(self): """ Returns list of artifact builds depending on this one. """ return ArtifactBuild.query.filter_by(dep_on_id=self.id).all() def transition(self, state, state_reason): """ Sets the state and state_reason of this ArtifactBuild. :param state: ArtifactBuildState value :param state_reason: Reason why this state has been set. :return: True/False, whether state was changed """ # Convert state from its possible representation to number. state = self.validate_state("state", state) # Log the state and state_reason if state == ArtifactBuildState.FAILED.value: log_fnc = log.error else: log_fnc = log.info log_fnc("Artifact build %r moved to state %s, %r" % (self, ArtifactBuildState(state).name, state_reason)) if self.state == state: return False self.state = state if ArtifactBuildState(state).counter: ArtifactBuildState(state).counter.inc() self.state_reason = state_reason if self.state in [ ArtifactBuildState.DONE.value, ArtifactBuildState.FAILED.value, ArtifactBuildState.CANCELED.value ]: self.time_completed = datetime.utcnow() # For FAILED/CANCELED states, move also all the artifacts depending # on this one to FAILED/CANCELED state, because there is no way we # can rebuild them. if self.state in [ ArtifactBuildState.FAILED.value, ArtifactBuildState.CANCELED.value ]: for build in self.depending_artifact_builds(): build.transition( self.state, "Cannot build artifact, because its " "dependency cannot be built.") messaging.publish('build.state.changed', self.json()) return True def __repr__(self): return "<ArtifactBuild %s, type %s, state %s, event %s>" % ( self.name, ArtifactType(self.type).name, ArtifactBuildState(self.state).name, self.event.message_id) def json(self): build_args = {} if self.build_args: build_args = json.loads(self.build_args) build_url = get_url_for('build', id=self.id) db.session.add(self) return { "id": self.id, "name": self.name, "original_nvr": self.original_nvr, "rebuilt_nvr": self.rebuilt_nvr, "type": self.type, "type_name": ArtifactType(self.type).name, "state": self.state, "state_name": ArtifactBuildState(self.state).name, "state_reason": self.state_reason, "dep_on": self.dep_on.name if self.dep_on else None, "dep_on_id": self.dep_on.id if self.dep_on else None, "time_submitted": _utc_datetime_to_iso(self.time_submitted), "time_completed": _utc_datetime_to_iso(self.time_completed), "event_id": self.event_id, "build_id": self.build_id, "url": build_url, "build_args": build_args, "odcs_composes": [rel.compose.odcs_compose_id for rel in self.composes], "rebuild_reason": RebuildReason(self.rebuild_reason or 0).name.lower() } def get_root_dep_on(self): dep_on = self.dep_on while dep_on: dep = dep_on.dep_on if dep: dep_on = dep else: break return dep_on def add_composes(self, session, composes): """Add an ODCS compose to this build""" for compose in composes: session.add( ArtifactBuildCompose(build_id=self.id, compose_id=compose.id)) @property def composes_ready(self): """Check if composes this build has have been done in ODCS""" return all((rel.compose.finished for rel in self.composes))