Exemple #1
0
    def _fake_build_container(self, source_url, build_target, build_opts):
        """
        Fake KojiSession.buildContainer method used dry run mode.

        Logs the arguments and emits BrewContainerTaskStateChangeEvent of
        CLOSED state.

        :rtype: number
        :return: Fake task_id.
        """
        log.info("DRY RUN: Calling fake buildContainer with args: %r",
                 (source_url, build_target, build_opts))

        # Get the task_id
        KojiService._FAKE_TASK_ID -= 1
        task_id = KojiService._FAKE_TASK_ID

        # Parse the source_url to get the name of container and generate
        # fake event.
        m = re.match(r".*/(?P<container>[^#]*)", source_url)
        container = m.group('container')
        event = BrewContainerTaskStateChangeEvent("fake_koji_msg_%d" % task_id,
                                                  container,
                                                  build_opts["git_branch"],
                                                  build_target, task_id,
                                                  "BUILDING", "CLOSED")
        event.dry_run = self.dry_run

        # Inject the fake event.
        log.info("DRY RUN: Injecting fake event: %r", event)
        work_queue_put(event)

        return task_id
Exemple #2
0
    def patch(self, id):
        """
        Manage Freshmaker event defined by ID. The request must be
        :mimetype:`application/json`.

        Returns the cancelled Freshmaker event as JSON.

        **Sample request**:

        .. sourcecode:: http

            PATCH /api/1/events HTTP/1.1
            Accept: application/json
            Content-Type: application/json

            {
                "action": "cancel"
            }

        :jsonparam string action: Action to do with an Event. Currently only "cancel"
            is supported.
        :statuscode 200: Cancelled event is returned.
        :statuscode 400: Action is missing or is unsupported.
        """
        data = request.get_json(force=True)
        if 'action' not in data:
            return json_error(
                400, "Bad Request", "Missing action in request."
                " Don't know what to do with the event.")

        if data["action"] != "cancel":
            return json_error(400, "Bad Request", "Unsupported action requested.")

        event = models.Event.query.filter_by(id=id).first()
        if not event:
            return json_error(400, "Not Found", "No such event found.")

        if event.requester != g.user.username and not user_has_role("admin"):
            return json_error(
                403, "Forbidden", "You must be an admin to cancel someone else's event.")

        msg = "Event id %s requested for canceling by user %s" % (event.id, g.user.username)
        log.info(msg)

        event.transition(EventState.CANCELED, msg)
        event.builds_transition(
            ArtifactBuildState.CANCELED.value,
            "Build canceled before running on external build system.",
            filters={'state': ArtifactBuildState.PLANNED.value})
        builds_id = event.builds_transition(
            ArtifactBuildState.CANCELED.value, None,
            filters={'state': ArtifactBuildState.BUILD.value})
        db.session.commit()

        data["action"] = self._freshmaker_manage_prefix + data["action"]
        data["event_id"] = event.id
        data["builds_id"] = builds_id
        messaging.publish("manage.eventcancel", data)
        # Return back the JSON representation of Event to client.
        return jsonify(event.json()), 200
    def rebuild_if_not_exists(self, event, errata_id):
        """
        Initiates rebuild of artifacts based on Errata advisory with
        `errata_id` id.

        :rtype: List of ErrataAdvisoryRPMsSignedEvent instances.
        :return: List of extra events generated to initiate the rebuild.
        """

        db_event = db.session.query(Event).filter_by(
            event_type_id=EVENT_TYPES[ErrataAdvisoryRPMsSignedEvent],
            search_key=str(errata_id)).first()
        if (db_event and db_event.state != EventState.FAILED.value and
                not event.manual):
            log.debug("Ignoring Errata advisory %d - it already exists in "
                      "Freshmaker db.", errata_id)
            return []

        # Get additional info from Errata to fill in the needed data.
        errata = Errata()
        advisories = errata.advisories_from_event(event)
        if not advisories:
            log.error("Unknown Errata advisory %d" % errata_id)
            return []

        log.info("Generating ErrataAdvisoryRPMsSignedEvent for Errata "
                 "advisory %d, because its state changed to %s.", errata_id,
                 event.advisory.state)
        advisory = advisories[0]
        new_event = ErrataAdvisoryRPMsSignedEvent(
            event.msg_id + "." + str(advisory.name), advisory)
        new_event.dry_run = event.dry_run
        new_event.manual = event.manual
        return [new_event]
Exemple #4
0
 def krb_login(self):
     # No need to login on dry run, this makes dry run much faster.
     if not self.dry_run:
         self.session.gssapi_login(principal=conf.krb_auth_principal,
                                   keytab=conf.krb_auth_client_keytab,
                                   ccache=conf.krb_auth_ccache_file)
     else:
         log.info("DRY RUN: Skipping login in dry run mode.")
    def can_handle(self, event):
        if not isinstance(event, ErrataAdvisoryStateChangedEvent):
            return False

        if not {'rpm', 'module'} & set(event.advisory.content_types):
            log.info('Skip non-RPM advisory %s.', event.advisory.errata_id)
            return False

        return True
Exemple #6
0
 def _make_request(self, *args, **kwargs):
     try:
         return super(RetryingODCS, self)._make_request(*args, **kwargs)
     except HTTPError as e:
         if e.response.status_code == 401:
             log.info("CCache file probably expired, removing it.")
             os.unlink(conf.krb_auth_ccache_file)
             return super(RetryingODCS, self)._make_request(*args, **kwargs)
         else:
             raise
    def poll(self):
        try:
            self.check_unfinished_koji_tasks(db.session)
        except _sa_disconnect_exceptions as ex:
            db.session.rollback()
            log.error("Invalid request, session is rolled back: %s", ex.orig)
        except Exception:
            msg = 'Error in poller execution:'
            log.exception(msg)

        log.info('Poller will now sleep for "{}" seconds'
                 .format(conf.polling_interval))
Exemple #8
0
 def _errata_authorized_get(self, *args, **kwargs):
     try:
         r = requests.get(
             *args,
             auth=HTTPKerberosAuth(principal=conf.krb_auth_principal),
             **kwargs)
         r.raise_for_status()
     except requests.exceptions.RequestException as e:
         if e.response is not None and e.response.status_code == 401:
             log.info("CCache file probably expired, removing it.")
             os.unlink(conf.krb_auth_ccache_file)
         raise
     return r.json()
Exemple #9
0
    def _get_rpms(self, errata_id, rhel_release_prefix=None):
        """
        Returns dictionary of NVRs of builds added to the advisory.
        "source_rpms" key with SRPMs as a value
        "binary_rpms" key with binary rpms as a value

        If module build is attached to advisory, also all the NVRs of builds
        included in this module build are returned, together with the NVR of
        the module build.

        :param number errata_id: ID of advisory.
        :param string rhel_release_prefix: When set to non-empty string,
            it will be used to limit the set of builds returned by this
            method to only builds based on the RHEL version starting with
            `rhel_release_prefix`. For example to return only RHEL-7 builds,
            this should be set to "RHEL-7".
            Defaults to conf.errata_rhel_release_prefix.
        :rtype: dict
        :return: Dictionary with source and binary rpms.
        """
        if rhel_release_prefix is None:
            rhel_release_prefix = conf.errata_rhel_release_prefix

        builds_per_product = self._errata_http_get("advisory/%s/builds.json" %
                                                   str(errata_id))

        # Store NVRs of all builds in advisory to nvrs set.
        source_rpms = set()
        binary_rpms = set()
        for product_version, builds in builds_per_product.items():
            if rhel_release_prefix:
                rhel_release = Errata.product_region.get(product_version)
                if not rhel_release:
                    rhel_release = self._rhel_release_from_product_version(
                        errata_id, product_version)
                    Errata.product_region.set(product_version, rhel_release)

                if not rhel_release.startswith(rhel_release_prefix):
                    log.info("Skipping builds for %s - not based on RHEL %s",
                             product_version, rhel_release_prefix)
                    continue

            for build in builds:
                for variant_arch in build.values():
                    for arch_rpms in variant_arch.values():
                        for arch, rpms in arch_rpms.items():
                            if arch == "SRPMS":
                                source_rpms.update(rpms)
                            else:
                                binary_rpms.update(rpms)
        return {"source_rpms": source_rpms, "binary_rpms": binary_rpms}
    def build_container(self,
                        scm_url,
                        branch,
                        target,
                        repo_urls=None,
                        isolated=False,
                        release=None,
                        koji_parent_build=None,
                        arch_override=None,
                        compose_ids=None,
                        operator_csv_modifications_url=None):
        """
        Build a container in Koji.

        :param str scm_url: refer to ``KojiService.build_container``.
        :param str branch: refer to ``KojiService.build_container``.
        :param str target: refer to ``KojiService.build_container``.
        :param list[str] repo_urls: refer to ``KojiService.build_container``.
        :param bool isolated: refer to ``KojiService.build_container``.
        :param str release: refer to ``KojiService.build_container``.
        :param str koji_parent_build: refer to ``KojiService.build_container``.
        :param str arch_override: refer to ``KojiService.build_container``.
        :param list[int] compose_ids: refer to ``KojiService.build_container``.
        :param str operator_csv_modifications_url: refer to ``KojiService.build_container``.
        :return: task id returned from Koji buildContainer API.
        :rtype: int
        """
        with koji_service(profile=conf.koji_profile,
                          logger=log,
                          dry_run=self.dry_run) as service:
            log.info(
                'Building container from source: %s, '
                'release=%r, parent=%r, target=%r, arch=%r, compose_ids=%r',
                scm_url, release, koji_parent_build, target, arch_override,
                compose_ids)

            return service.build_container(
                scm_url,
                branch,
                target,
                repo_urls=repo_urls,
                isolated=isolated,
                release=release,
                koji_parent_build=koji_parent_build,
                arch_override=arch_override,
                scratch=conf.koji_container_scratch_build,
                compose_ids=compose_ids,
                operator_csv_modifications_url=operator_csv_modifications_url,
            )
    def mark_as_released(self, errata_id):
        """
        Marks the Errata advisory with `errata_id` ID as "released", so it
        is not included in further container images rebuilds.
        """
        # check db to see whether this advisory exists in db
        db_event = db.session.query(Event).filter_by(
            event_type_id=EVENT_TYPES[ErrataAdvisoryRPMsSignedEvent],
            search_key=str(errata_id)).first()
        if not db_event:
            log.debug("Ignoring Errata advisory %d - it does not exist in "
                      "Freshmaker db.", errata_id)
            return []

        self.set_context(db_event)

        db_event.released = True
        db.session.commit()
        log.info("Errata advisory %d is now marked as released", errata_id)
Exemple #12
0
    def _filter_out_existing_advisories(self, advisories):
        """
        Filter out all advisories which have been already handled by
        Freshmaker.

        :param advisories: List of ErrataAdvisory instances.
        :rtype: List of ErrataAdvisory
        :return: List of ErrataAdvisory instances without already handled
                 advisories.
        """
        ret = []
        for advisory in advisories:
            if (db.session.query(Event).filter_by(
                    search_key=str(advisory.errata_id)).count() != 0):
                log.info(
                    "Skipping advisory %s (%d), already handled by "
                    "Freshmaker", advisory.name, advisory.errata_id)
                continue
            ret.append(advisory)
        return ret
Exemple #13
0
    def get_operator_indices(self):
        """ Get all index images for organization(s)(configurable) from Pyxis """
        request_params = {}
        organizations = conf.pyxis_index_image_organizations
        if organizations:
            rsql = " or ".join([
                f"organization=={organization}"
                for organization in organizations
            ])
            request_params["filter"] = rsql
        indices = self._pagination("operators/indices", request_params)
        log.debug("Found the following index images: %s",
                  ", ".join(i["path"] for i in indices))

        # Operator indices can be available in pyxis prior to the Openshift version
        # is released, so we need to filter out such indices
        indices = list(
            filter(lambda x: self.ocp_is_released(x["ocp_version"]), indices))
        log.info("Using the following GA index images: %s",
                 ", ".join(i["path"] for i in indices))
        return indices
    def rebuild_dependent_containers(self, found_build):
        """ Rebuild containers depend on the success build as necessary. """
        if found_build.state == ArtifactBuildState.DONE.value:
            # check db to see whether there is any planned image build
            # depends on this build
            planned_builds = db.session.query(ArtifactBuild).filter_by(
                type=ArtifactType.IMAGE.value,
                state=ArtifactBuildState.PLANNED.value,
                dep_on=found_build).all()

            log.info(
                "Found following PLANNED builds to rebuild that "
                "depends on %r", found_build)
            for build in planned_builds:
                log.info("  %r", build)

            self.start_to_build_images(planned_builds)

        # Finally, we check if all builds scheduled by event
        # found_build.event (ErrataAdvisoryRPMsSignedEvent) have been
        # switched to FAILED or COMPLETE. If yes, mark the event COMPLETE.
        self._mark_event_complete_when_all_builds_done(found_build.event)
Exemple #15
0
    def builds_signed(self, errata_id):
        """
        Returns True if all builds in the advisory are signed.
        :param str or int errata_id: Errata advisory ID to check.
        :return: True if all builds in advisory are signed.
        :rtype: bool
        """
        builds_per_product = self._get_attached_builds(errata_id)

        # Store NVRs of all builds in advisory to nvrs set.
        nvrs = set()
        for builds in builds_per_product.values():
            for build in builds:
                nvrs.update(set(build.keys()))

        # For each NVR, check that all the rpms are signed.
        for nvr in nvrs:
            log.info("Checking whether the build %s is signed", str(nvr))
            build = self._errata_rest_get("build/%s" % str(nvr))
            if "rpms_signed" not in build or not build["rpms_signed"]:
                return False

        return True
Exemple #16
0
    def process_event(self, msg):
        log.debug(
            'Received a message with an ID of "{0}" and of type "{1}"'.format(
                getattr(msg, 'msg_id', None),
                type(msg).__name__))

        handlers = load_classes(conf.handlers)
        handlers = sorted(handlers,
                          key=lambda handler: getattr(handler, "order", 50))
        for handler_class in handlers:
            handler = handler_class()

            if not handler.can_handle(msg):
                continue

            idx = "%s: %s, %s" % (type(handler).__name__, type(msg).__name__,
                                  msg.msg_id)
            log.debug("Calling %s" % idx)
            try:
                further_work = handler.handle(msg) or []
            except Exception:
                err = 'Could not process message handler. See the traceback.'
                log.exception(err)
            else:
                # Handlers can *optionally* return a list of fake messages that
                # should be re-inserted back into the main work queue. We can
                # use this (for instance) when we submit a new component build
                # but (for some reason) it has already been built, then it can
                # fake its own completion back to the scheduler so that work
                # resumes as if it was submitted for real and koji announced
                # its completion.
                for event in further_work:
                    log.info("  Scheduling faked event %r" % event)
                    self.incoming.put(event)

            log.debug("Done with %s" % idx)
Exemple #17
0
    def handle(self, event):
        log.info("Finding out all advisories including %s", event.nvr)

        # When get a signed RPM, first step is to find out advisories
        # containing that RPM and ensure all builds are signed.
        errata = Errata()
        advisories = errata.advisories_from_event(event)

        # Filter out advisories which are not allowed by configuration.
        advisories = [
            advisory for advisory in advisories if self.allow_build(
                ArtifactType.IMAGE,
                advisory_name=advisory.name,
                advisory_security_impact=advisory.security_impact,
                advisory_highest_cve_severity=advisory.highest_cve_severity,
                advisory_state=advisory.state)
        ]

        # Filter out advisories which are already in Freshmaker DB.
        advisories = self._filter_out_existing_advisories(advisories)

        if not advisories:
            log.info("No advisories found suitable for rebuilding Docker "
                     "images")
            return []

        if not all((errata.builds_signed(advisory.errata_id)
                    for advisory in advisories)):
            log.info(
                'Not all builds in %s are signed. Do not rebuild any '
                'docker image until signed.', advisories)
            return []

        # Now we know that all advisories with this signed RPM have also other
        # RPMs signed. We can then proceed and generate
        # ErrataAdvisoryRPMsSignedEvent.
        new_events = []
        for advisory in advisories:
            new_event = ErrataAdvisoryRPMsSignedEvent(
                event.msg_id + "." + str(advisory.name), advisory)
            db_event = Event.create(db.session,
                                    new_event.msg_id,
                                    new_event.search_key,
                                    new_event.__class__,
                                    released=False)
            db.session.add(db_event)
            new_events.append(new_event)
        db.session.commit()
        return new_events
Exemple #18
0
    def build_container(self,
                        source_url,
                        branch,
                        target,
                        scratch=None,
                        repo_urls=None,
                        isolated=False,
                        release=None,
                        koji_parent_build=None,
                        arch_override=None,
                        compose_ids=None,
                        operator_csv_modifications_url=None):
        """Build container by buildContainer

        :param str source_url: the container repository URL.
        :param str target: specify a specific build target.
        :param str branch: a build option passed to ``buildContainer``.
        :param bool scratch: a build option passed to ``buildContainer``.
        :param list[str] repo_urls: a build option passed to ``buildContainer``.
        :param bool isolated: a build option passed to ``buildContainer``.
        :param str release: a build option passed to ``buildContainer``.
        :param str koji_parent_build: a build option passed to ``buildContainer``.
        :param str arch_override: a build option passed to ``buildContainer``.
        :param list[str] compose_ids: a build option passed to ``buildContainer``.
            For details of these build options, please refer to
            ``PARAMS_SCHEMA`` defined inside ``BuildContainerTask`` in the
            koji-containerbuild plugin.
        :param str operator_csv_modifications_url: a build option passed to ``buildContainer``.
            This is necessary for bundle image rebuilds.
        :return: the container build task ID returned from ``buildContainer``.
        :rtype: int
        """

        build_target = target
        build_opts = {
            'scratch': False if scratch is None else scratch,
            'git_branch': branch,
        }

        if repo_urls:
            build_opts['yum_repourls'] = repo_urls
        if compose_ids:
            build_opts['compose_ids'] = compose_ids
        if isolated:
            build_opts['isolated'] = True
        if koji_parent_build:
            build_opts['koji_parent_build'] = koji_parent_build
        if arch_override:
            build_opts['arch_override'] = arch_override
        if release:
            build_opts['release'] = release
        if operator_csv_modifications_url:
            build_opts[
                'operator_csv_modifications_url'] = operator_csv_modifications_url

        log.debug('Build from target: %s', build_target)
        log.debug('Build options: %s', build_opts)

        if not self.dry_run:
            task_id = self.session.buildContainer(source_url, build_target,
                                                  build_opts)
        else:
            task_id = self._fake_build_container(source_url, build_target,
                                                 build_opts)

        log.info('Task %s is created to build docker image for %s', task_id,
                 source_url)
        log.info('Task info: %s/taskinfo?taskID=%s', self.weburl, task_id)

        return task_id
Exemple #19
0
    def get_builds(self, errata_id, rhel_release_prefix=None):
        """
        Returns set of NVRs of builds added to the advisory. These are just
        brew build NVRs, not the particular RPM NVRs.

        If module build is attached to advisory, also all the NVRs of builds
        included in this module build are returned, together with the NVR of
        the module build.

        :param number errata_id: ID of advisory.
        :param string rhel_release_prefix: When set to non-empty string,
            it will be used to limit the set of builds returned by this
            method to only builds based on the RHEL version starting with
            `rhel_release_prefix`. For example to return only RHEL-7 builds,
            this should be set to "RHEL-7".
            Defaults to conf.errata_rhel_release_prefix.
        :rtype: set of strings
        :return: Set of NVR builds.
        """
        def get_srpms_nvrs(build_dict):
            """ Gets srpms nvrs from the build dictionary. """
            for key, val in build_dict.items():
                if build_dict.get('SRPMS'):
                    return {
                        nvr.split(".src.rpm")[0]
                        for nvr in build_dict['SRPMS']
                    }
                if isinstance(val, dict):
                    return get_srpms_nvrs(val)
                return

        if rhel_release_prefix is None:
            rhel_release_prefix = conf.errata_rhel_release_prefix

        builds_per_product = self._errata_http_get("advisory/%s/builds.json" %
                                                   str(errata_id))

        # Store NVRs of all builds in advisory to nvrs set.
        nvrs = set()
        for product_version, builds in builds_per_product.items():
            if rhel_release_prefix:
                rhel_release = Errata.product_region.get(product_version)
                if not rhel_release:
                    rhel_release = self._rhel_release_from_product_version(
                        errata_id, product_version)
                    Errata.product_region.set(product_version, rhel_release)

                if not rhel_release.startswith(rhel_release_prefix):
                    log.info("Skipping builds for %s - not based on RHEL %s",
                             product_version, rhel_release_prefix)
                    continue

            for build in builds:
                # Add attached Koji build NVRs.
                nvrs.update(set(build.keys()))

                # Add attached SRPM NVRs. For normal RPM builds, these are the
                # same as Koji build NVRs, but for modules, these are SRPMs
                # included in a module.
                srpm_nvrs = get_srpms_nvrs(build)
                if srpm_nvrs:
                    nvrs.update(srpm_nvrs)

        return nvrs
Exemple #20
0
 def shutdown(self):
     log.info("Scheduling shutdown.")
     from moksha.hub.reactor import reactor
     reactor.callFromThread(self.hub.stop)
     reactor.callFromThread(reactor.stop)
    def handle(self, event):
        if event.dry_run:
            self.force_dry_run()
        self.event = event

        db_event = Event.get_or_create_from_event(db.session, event)

        self.set_context(db_event)

        # Check if event is allowed by internal policies
        if not self.event.is_allowed(self):
            msg = ("This image rebuild is not allowed by internal policy. "
                   f"message_id: {event.msg_id}")
            db_event.transition(EventState.SKIPPED, msg)
            self.log_info(msg)
            return []

        # Get builds NVRs from the advisory attached to the message/event and
        # then get original NVR for every build

        # Mapping of original build nvrs to rebuilt nvrs in advisory
        nvrs_mapping = {}
        for product_info in event.advisory.builds.values():
            for build in product_info['builds']:
                # Search for the first build that triggered the chain of rebuilds
                # for every shipped NVR to get original NVR from it
                original_nvr = self.get_published_original_nvr(build['nvr'])
                if original_nvr is None:
                    continue
                nvrs_mapping[original_nvr] = build['nvr']

        original_nvrs = nvrs_mapping.keys()
        self.log_info(
            "Orignial nvrs of build in the advisory #{0} are: {1}".format(
                event.advisory.errata_id, " ".join(original_nvrs)))

        # Get image manifest_list_digest for all original images, manifest_list_digest is used
        # in pullspecs in bundle's related images
        original_digests_by_nvr = {}
        original_nvrs_by_digest = {}
        for nvr in original_nvrs:
            digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr)
            if digest:
                original_digests_by_nvr[nvr] = digest
                original_nvrs_by_digest[digest] = nvr
            else:
                log.warning(
                    f"Image manifest_list_digest not found for original image {nvr} in Pyxis, "
                    "skip this image"
                )

        if not original_digests_by_nvr:
            msg = f"None of the original images have digests in Pyxis: {','.join(original_nvrs)}"
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        # Get image manifest_list_digest for all rebuilt images, manifest_list_digest is used
        # in pullspecs of bundle's related images
        rebuilt_digests_by_nvr = {}
        rebuilt_nvrs = nvrs_mapping.values()
        for nvr in rebuilt_nvrs:
            digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr)
            if digest:
                rebuilt_digests_by_nvr[nvr] = digest
            else:
                log.warning(
                    f"Image manifest_list_digest not found for rebuilt image {nvr} in Pyxis, "
                    "skip this image"
                )

        if not rebuilt_digests_by_nvr:
            msg = f"None of the rebuilt images have digests in Pyxis: {','.join(rebuilt_nvrs)}"
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        index_images = self._pyxis.get_operator_indices()
        # get latest bundle images per channel per index image filtered
        # by the highest semantic version
        all_bundles = self._pyxis.get_latest_bundles(index_images)

        # A set of unique bundle digests
        bundle_digests = set()

        # get bundle digests for original images
        bundle_digests_by_related_nvr = {}
        for image_nvr, image_digest in original_digests_by_nvr.items():
            bundles = self._pyxis.get_bundles_by_related_image_digest(
                image_digest, all_bundles
            )
            if not bundles:
                log.info(f"No latest bundle image with the related image of {image_nvr}")
                continue

            for bundle in bundles:
                bundle_digest = bundle['bundle_path_digest']
                bundle_digests.add(bundle_digest)
                bundle_digests_by_related_nvr.setdefault(image_nvr, []).append(bundle_digest)

        if not bundle_digests_by_related_nvr:
            msg = "None of the original images have related bundles, skip."
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        # Mapping of bundle digest to bundle data
        # {
        #     digest: {
        #         "images": [image_amd64, image_aarch64],
        #         "nvr": NVR,
        #         "auto_rebuild": True/False,
        #         "osbs_pinning": True/False,
        #         "pullspecs": [...],
        #     }
        # }
        bundles_by_digest = {}
        default_bundle_data = {
            'images': [],
            'nvr': None,
            'auto_rebuild': False,
            'osbs_pinning': False,
            'pullspecs': [],
        }

        # Get images for each bundle digest, a bundle digest can have multiple images
        # with different arches.
        for digest in bundle_digests:
            bundles = self._pyxis.get_images_by_digest(digest)
            # If no bundle image found, just skip this bundle digest
            if not bundles:
                continue

            bundles_by_digest.setdefault(digest, copy.deepcopy(default_bundle_data))
            bundles_by_digest[digest]['nvr'] = bundles[0]['brew']['build']
            bundles_by_digest[digest]['images'] = bundles

        # Unauthenticated koji session to fetch build info of bundles
        koji_api = KojiService(conf.koji_profile)

        # For each bundle, check whether it should be rebuilt by comparing the
        # auto_rebuild_tags of repository and bundle's tags
        for digest, bundle_data in bundles_by_digest.items():
            bundle_nvr = bundle_data['nvr']

            # Images are for different arches, just check against the first image
            image = bundle_data['images'][0]
            if self.image_has_auto_rebuild_tag(image):
                bundle_data['auto_rebuild'] = True

            # Fetch buildinfo
            buildinfo = koji_api.get_build(bundle_nvr)
            related_images = (
                buildinfo.get("extra", {})
                .get("image", {})
                .get("operator_manifests", {})
                .get("related_images", {})
            )
            bundle_data['osbs_pinning'] = related_images.get('created_by_osbs', False)
            # Save the original pullspecs
            bundle_data['pullspecs'] = related_images.get('pullspecs', [])

        # Digests of bundles to be rebuilt
        to_rebuild_digests = set()

        # Now for each bundle, replace the original digest with rebuilt
        # digest (override pullspecs)
        for digest, bundle_data in bundles_by_digest.items():
            # Override pullspecs only when auto_rebuild is enabled and OSBS-pinning
            # mechanism is used.
            if not (bundle_data['auto_rebuild'] and bundle_data['osbs_pinning']):
                continue

            for pullspec in bundle_data['pullspecs']:
                # A pullspec item example:
                # {
                #   'new': 'registry.exampe.io/repo/example-operator@sha256:<sha256-value>'
                #   'original': 'registry.example.io/repo/example-operator:v2.2.0',
                #   'pinned': True
                # }

                # If related image is not pinned by OSBS, skip
                if not pullspec.get('pinned', False):
                    continue

                # A pullspec path is in format of "registry/repository@digest"
                pullspec_elems = pullspec.get('new').split('@')
                old_digest = pullspec_elems[1]

                if old_digest not in original_nvrs_by_digest:
                    # This related image is not one of the original images
                    continue

                # This related image is one of our original images
                old_nvr = original_nvrs_by_digest[old_digest]
                new_nvr = nvrs_mapping[old_nvr]
                new_digest = rebuilt_digests_by_nvr[new_nvr]

                # Replace the old digest with new digest
                pullspec_elems[1] = new_digest
                new_pullspec = '@'.join(pullspec_elems)
                pullspec['new'] = new_pullspec

                # Once a pullspec in this bundle has been overrided, add this bundle
                # to rebuild list
                to_rebuild_digests.add(digest)

        # Skip that event because we can't proceed with processing it.
        # TODO
        # Now when we have bundle images' nvrs we can procceed with rebuilding it
        msg = f"Skipping the rebuild of {len(to_rebuild_digests)} bundle images " \
              "due to being blocked on further implementation for now."
        db_event.transition(EventState.SKIPPED, msg)
        return []
    def _handle_auto_rebuild(self, db_event):
        """
        Handle auto rebuild for an advisory created by Botas

        :param db_event: database event that represent rebuild event
        :rtype: list
        :return: list of advisories that should be rebuilt
        """
        # Mapping of original build nvrs to rebuilt nvrs in advisory
        nvrs_mapping = self._create_original_to_rebuilt_nvrs_map()

        original_nvrs = nvrs_mapping.keys()
        self.log_info(
            "Orignial nvrs of build in the advisory #{0} are: {1}".format(
                self.event.advisory.errata_id, " ".join(original_nvrs)))

        # Get image manifest_list_digest for all original images, manifest_list_digest is used
        # in pullspecs in bundle's related images
        original_digests_by_nvr = {}
        original_nvrs_by_digest = {}
        for nvr in original_nvrs:
            digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr)
            if digest:
                original_digests_by_nvr[nvr] = digest
                original_nvrs_by_digest[digest] = nvr
            else:
                log.warning(
                    f"Image manifest_list_digest not found for original image {nvr} in Pyxis, "
                    "skip this image")

        if not original_digests_by_nvr:
            msg = f"None of the original images have digests in Pyxis: {','.join(original_nvrs)}"
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        # Get image manifest_list_digest for all rebuilt images, manifest_list_digest is used
        # in pullspecs of bundle's related images
        rebuilt_digests_by_nvr = {}
        rebuilt_nvrs = nvrs_mapping.values()
        for nvr in rebuilt_nvrs:
            # Don't require that the manifest list digest be published in this case because
            # there's a delay from after an advisory is shipped and when the published repositories
            # entry is populated
            digest = self._pyxis.get_manifest_list_digest_by_nvr(
                nvr, must_be_published=False)
            if digest:
                rebuilt_digests_by_nvr[nvr] = digest
            else:
                log.warning(
                    f"Image manifest_list_digest not found for rebuilt image {nvr} in Pyxis, "
                    "skip this image")

        if not rebuilt_digests_by_nvr:
            msg = f"None of the rebuilt images have digests in Pyxis: {','.join(rebuilt_nvrs)}"
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        index_images = self._pyxis.get_operator_indices()
        # get latest bundle images per channel per index image filtered
        # by the highest semantic version
        all_bundles = self._pyxis.get_latest_bundles(index_images)
        self.log_debug(
            "There are %d bundles that are latest in a channel in the found index images",
            len(all_bundles),
        )

        # A mapping of digests to bundle metadata. This metadata is used to
        # for the CSV metadata updates.
        bundle_mds_by_digest = {}

        # get bundle digests for original images
        bundle_digests_by_related_nvr = {}
        for image_nvr, image_digest in original_digests_by_nvr.items():
            bundles = self._pyxis.get_bundles_by_related_image_digest(
                image_digest, all_bundles)
            if not bundles:
                log.info(
                    f"No latest bundle image with the related image of {image_nvr}"
                )
                continue

            for bundle in bundles:
                bundle_digest = bundle['bundle_path_digest']
                bundle_mds_by_digest[bundle_digest] = bundle
                bundle_digests_by_related_nvr.setdefault(
                    image_nvr, []).append(bundle_digest)

        if not bundle_digests_by_related_nvr:
            msg = "None of the original images have related bundles, skip."
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []
        self.log_info("Found %d bundles with relevant related images",
                      len(bundle_digests_by_related_nvr))

        # Mapping of bundle digest to bundle data
        # {
        #     digest: {
        #         "images": [image_amd64, image_aarch64],
        #         "nvr": NVR,
        #         "auto_rebuild": True/False,
        #         "osbs_pinning": True/False,
        #         "pullspecs": [...],
        #     }
        # }
        bundles_by_digest = {}
        default_bundle_data = {
            'images': [],
            'nvr': None,
            'auto_rebuild': False,
            'osbs_pinning': False,
            # CSV modifications for the rebuilt bundle image
            'pullspec_replacements': [],
            'update': {},
        }

        # Get images for each bundle digest, a bundle digest can have multiple images
        # with different arches.
        for digest in bundle_mds_by_digest:
            bundles = self._pyxis.get_images_by_digest(digest)
            # If no bundle image found, just skip this bundle digest
            if not bundles:
                self.log_warn(
                    'The bundle digest %r was not found in Pyxis. Skipping.',
                    digest)
                continue

            bundles_by_digest.setdefault(digest,
                                         copy.deepcopy(default_bundle_data))
            bundles_by_digest[digest]['nvr'] = bundles[0]['brew']['build']
            bundles_by_digest[digest]['images'] = bundles

        # Unauthenticated koji session to fetch build info of bundles
        koji_api = KojiService(conf.koji_profile)

        # For each bundle, check whether it should be rebuilt by comparing the
        # auto_rebuild_tags of repository and bundle's tags
        for digest, bundle_data in bundles_by_digest.items():
            bundle_nvr = bundle_data['nvr']

            # Images are for different arches, just check against the first image
            image = bundle_data['images'][0]
            if self.image_has_auto_rebuild_tag(image):
                bundle_data['auto_rebuild'] = True

            # Fetch buildinfo
            buildinfo = koji_api.get_build(bundle_nvr)
            related_images = (buildinfo.get('extra', {}).get('image', {}).get(
                'operator_manifests', {}).get('related_images', {}))
            bundle_data['osbs_pinning'] = related_images.get(
                'created_by_osbs', False)
            # Save the original pullspecs
            bundle_data['pullspec_replacements'] = related_images.get(
                'pullspecs', [])

        # Digests of bundles to be rebuilt
        to_rebuild_digests = set()

        # Now for each bundle, replace the original digest with rebuilt
        # digest (override pullspecs)
        for digest, bundle_data in bundles_by_digest.items():
            # Override pullspecs only when auto_rebuild is enabled and OSBS-pinning
            # mechanism is used.
            if not (bundle_data['auto_rebuild']
                    and bundle_data['osbs_pinning']):
                self.log_info(
                    'The bundle %r does not have auto-rebuild tags (%r) and/or OSBS pinning (%r)',
                    bundle_data['nvr'],
                    bundle_data['auto_rebuild'],
                    bundle_data['osbs_pinning'],
                )
                continue

            csv_name = bundle_mds_by_digest[digest]['csv_name']
            version = bundle_mds_by_digest[digest]['version']
            bundle_data.update(self._get_csv_updates(csv_name, version))

            for pullspec in bundle_data['pullspec_replacements']:
                # A pullspec item example:
                # {
                #   'new': 'registry.exampe.io/repo/example-operator@sha256:<sha256-value>',
                #   'original': 'registry.example.io/repo/example-operator:v2.2.0',
                #   'pinned': True,
                #   # value used for internal purpose during manual rebuilds, it's an old pullspec that was replaced
                #   '_old': 'registry.exampe.io/repo/example-operator@sha256:<previous-sha256-value>,
                # }

                # A pullspec path is in format of "registry/repository@digest"
                pullspec_elems = pullspec.get('new').split('@')
                old_digest = pullspec_elems[1]

                if old_digest not in original_nvrs_by_digest:
                    # This related image is not one of the original images
                    continue

                # This related image is one of our original images
                old_nvr = original_nvrs_by_digest[old_digest]
                new_nvr = nvrs_mapping[old_nvr]
                new_digest = rebuilt_digests_by_nvr[new_nvr]

                # save pullspec that image had before rebuild
                pullspec['_old'] = pullspec.get('new')

                # Replace the old digest with new digest
                pullspec_elems[1] = new_digest
                new_pullspec = '@'.join(pullspec_elems)
                pullspec['new'] = new_pullspec
                # Always set pinned to True when it was replaced by Freshmaker
                # since it indicates that the pullspec was modified from the
                # original pullspec
                pullspec['pinned'] = True

                # Once a pullspec in this bundle has been overrided, add this bundle
                # to rebuild list
                self.log_info(
                    'Changing pullspec %r to %r in the bundle %r',
                    pullspec['_old'],
                    pullspec['new'],
                    bundle_data['nvr'],
                )
                to_rebuild_digests.add(digest)

        if not to_rebuild_digests:
            msg = self._no_bundle_prefix + "No bundle images to rebuild for " \
                                           f"advisory {self.event.advisory.name}"
            self.log_info(msg)
            db_event.transition(EventState.SKIPPED, msg)
            db.session.commit()
            return []

        bundles_to_rebuild = list(
            map(lambda x: bundles_by_digest[x], to_rebuild_digests))
        return bundles_to_rebuild
    def handle(self, event):
        if event.dry_run:
            self.force_dry_run()
        self.event = event

        db_event = Event.get_or_create_from_event(db.session, event)

        self.set_context(db_event)

        # Check if event is allowed by internal policies
        if not self.event.is_allowed(self):
            msg = ("This image rebuild is not allowed by internal policy. "
                   f"message_id: {event.msg_id}")
            db_event.transition(EventState.SKIPPED, msg)
            self.log_info(msg)
            return []

        # Mapping of original build nvrs to rebuilt nvrs in advisory
        nvrs_mapping = self._create_original_to_rebuilt_nvrs_map()

        original_nvrs = nvrs_mapping.keys()
        self.log_info(
            "Orignial nvrs of build in the advisory #{0} are: {1}".format(
                event.advisory.errata_id, " ".join(original_nvrs)))

        # Get image manifest_list_digest for all original images, manifest_list_digest is used
        # in pullspecs in bundle's related images
        original_digests_by_nvr = {}
        original_nvrs_by_digest = {}
        for nvr in original_nvrs:
            digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr)
            if digest:
                original_digests_by_nvr[nvr] = digest
                original_nvrs_by_digest[digest] = nvr
            else:
                log.warning(
                    f"Image manifest_list_digest not found for original image {nvr} in Pyxis, "
                    "skip this image")

        if not original_digests_by_nvr:
            msg = f"None of the original images have digests in Pyxis: {','.join(original_nvrs)}"
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        # Get image manifest_list_digest for all rebuilt images, manifest_list_digest is used
        # in pullspecs of bundle's related images
        rebuilt_digests_by_nvr = {}
        rebuilt_nvrs = nvrs_mapping.values()
        for nvr in rebuilt_nvrs:
            digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr)
            if digest:
                rebuilt_digests_by_nvr[nvr] = digest
            else:
                log.warning(
                    f"Image manifest_list_digest not found for rebuilt image {nvr} in Pyxis, "
                    "skip this image")

        if not rebuilt_digests_by_nvr:
            msg = f"None of the rebuilt images have digests in Pyxis: {','.join(rebuilt_nvrs)}"
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        index_images = self._pyxis.get_operator_indices()
        # get latest bundle images per channel per index image filtered
        # by the highest semantic version
        all_bundles = self._pyxis.get_latest_bundles(index_images)

        # A mapping of digests to bundle metadata. This metadata is used to
        # for the CSV metadata updates.
        bundle_mds_by_digest = {}

        # get bundle digests for original images
        bundle_digests_by_related_nvr = {}
        for image_nvr, image_digest in original_digests_by_nvr.items():
            bundles = self._pyxis.get_bundles_by_related_image_digest(
                image_digest, all_bundles)
            if not bundles:
                log.info(
                    f"No latest bundle image with the related image of {image_nvr}"
                )
                continue

            for bundle in bundles:
                bundle_digest = bundle['bundle_path_digest']
                bundle_mds_by_digest[bundle_digest] = bundle
                bundle_digests_by_related_nvr.setdefault(
                    image_nvr, []).append(bundle_digest)

        if not bundle_digests_by_related_nvr:
            msg = "None of the original images have related bundles, skip."
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        # Mapping of bundle digest to bundle data
        # {
        #     digest: {
        #         "images": [image_amd64, image_aarch64],
        #         "nvr": NVR,
        #         "auto_rebuild": True/False,
        #         "osbs_pinning": True/False,
        #         "pullspecs": [...],
        #     }
        # }
        bundles_by_digest = {}
        default_bundle_data = {
            'images': [],
            'nvr': None,
            'auto_rebuild': False,
            'osbs_pinning': False,
            # CSV modifications for the rebuilt bundle image
            'pullspecs': [],
            'append': {},
            'update': {},
        }

        # Get images for each bundle digest, a bundle digest can have multiple images
        # with different arches.
        for digest in bundle_mds_by_digest:
            bundles = self._pyxis.get_images_by_digest(digest)
            # If no bundle image found, just skip this bundle digest
            if not bundles:
                continue

            bundles_by_digest.setdefault(digest,
                                         copy.deepcopy(default_bundle_data))
            bundles_by_digest[digest]['nvr'] = bundles[0]['brew']['build']
            bundles_by_digest[digest]['images'] = bundles

        # Unauthenticated koji session to fetch build info of bundles
        koji_api = KojiService(conf.koji_profile)

        # For each bundle, check whether it should be rebuilt by comparing the
        # auto_rebuild_tags of repository and bundle's tags
        for digest, bundle_data in bundles_by_digest.items():
            bundle_nvr = bundle_data['nvr']

            # Images are for different arches, just check against the first image
            image = bundle_data['images'][0]
            if self.image_has_auto_rebuild_tag(image):
                bundle_data['auto_rebuild'] = True

            # Fetch buildinfo
            buildinfo = koji_api.get_build(bundle_nvr)
            related_images = (buildinfo.get('extra', {}).get('image', {}).get(
                'operator_manifests', {}).get('related_images', {}))
            bundle_data['osbs_pinning'] = related_images.get(
                'created_by_osbs', False)
            # Save the original pullspecs
            bundle_data['pullspecs'] = related_images.get('pullspecs', [])

        # Digests of bundles to be rebuilt
        to_rebuild_digests = set()

        # Now for each bundle, replace the original digest with rebuilt
        # digest (override pullspecs)
        for digest, bundle_data in bundles_by_digest.items():
            # Override pullspecs only when auto_rebuild is enabled and OSBS-pinning
            # mechanism is used.
            if not (bundle_data['auto_rebuild']
                    and bundle_data['osbs_pinning']):
                continue

            csv_name = bundle_mds_by_digest[digest]['csv_name']
            version = bundle_mds_by_digest[digest]['version']
            bundle_data.update(self._get_csv_updates(csv_name, version))

            for pullspec in bundle_data['pullspecs']:
                # A pullspec item example:
                # {
                #   'new': 'registry.exampe.io/repo/example-operator@sha256:<sha256-value>'
                #   'original': 'registry.example.io/repo/example-operator:v2.2.0',
                #   'pinned': True
                # }

                # A pullspec path is in format of "registry/repository@digest"
                pullspec_elems = pullspec.get('new').split('@')
                old_digest = pullspec_elems[1]

                if old_digest not in original_nvrs_by_digest:
                    # This related image is not one of the original images
                    continue

                # This related image is one of our original images
                old_nvr = original_nvrs_by_digest[old_digest]
                new_nvr = nvrs_mapping[old_nvr]
                new_digest = rebuilt_digests_by_nvr[new_nvr]

                # Replace the old digest with new digest
                pullspec_elems[1] = new_digest
                new_pullspec = '@'.join(pullspec_elems)
                pullspec['new'] = new_pullspec
                # Always set pinned to True when it was replaced by Freshmaker
                # since it indicates that the pullspec was modified from the
                # original pullspec
                pullspec['pinned'] = True

                # Once a pullspec in this bundle has been overrided, add this bundle
                # to rebuild list
                to_rebuild_digests.add(digest)

        if not to_rebuild_digests:
            msg = f"No bundle images to rebuild for advisory {event.advisory.name}"
            self.log_info(msg)
            db_event.transition(EventState.SKIPPED, msg)
            db.session.commit()
            return []

        builds = self._prepare_builds(db_event, bundles_by_digest,
                                      to_rebuild_digests)

        # Reset context to db_event.
        self.set_context(db_event)

        self.start_to_build_images(builds)
        msg = f"Advisory {db_event.search_key}: Rebuilding " \
              f"{len(db_event.builds.all())} bundle images."
        db_event.transition(EventState.BUILDING, msg)

        return []
    def _handle_manual_rebuild(self, db_event):
        """
        Handle manual rebuild submitted by Release Driver for an advisory created by Botas

        :param db_event: database event that represents a rebuild event
        :rtype: list
        :return: list of advisories that should be rebuilt
        """
        old_to_new_pullspec_map = self._get_pullspecs_mapping()

        if not old_to_new_pullspec_map:
            msg = self._no_bundle_prefix + 'None of the bundle images have ' \
                                           'applicable pullspecs to replace'
            log.warning(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        # Unauthenticated koji session to fetch build info of bundles
        koji_api = KojiService(conf.koji_profile)
        rebuild_nvr_to_pullspecs_map = dict()
        # compare replaced pullspecs with pullspecs in 'container_images' and
        # create map for bundles that should be rebuilt with their nvrs
        for container_image_nvr in self.event.container_images:
            artifact_build = db.session.query(ArtifactBuild).filter(
                ArtifactBuild.rebuilt_nvr == container_image_nvr,
                ArtifactBuild.type == ArtifactType.IMAGE.value,
            ).one_or_none()
            pullspecs = []
            # Try to find build in FM database, if it's not there check in Brew
            if artifact_build:
                pullspecs = artifact_build.bundle_pullspec_overrides[
                    "pullspec_replacements"]
            else:
                # Fetch buildinfo from Koji
                buildinfo = koji_api.get_build(container_image_nvr)
                # Get the original pullspecs
                pullspecs = (buildinfo.get('extra', {}).get('image', {}).get(
                    'operator_manifests', {}).get('related_images',
                                                  {}).get('pullspecs', []))

            for pullspec in pullspecs:
                if pullspec.get('new') not in old_to_new_pullspec_map:
                    continue
                # use newer pullspecs in the image
                pullspec['new'] = old_to_new_pullspec_map[pullspec['new']]
                rebuild_nvr_to_pullspecs_map[container_image_nvr] = pullspecs

        if not rebuild_nvr_to_pullspecs_map:
            msg = self._no_bundle_prefix + 'None of the container images have ' \
                                           'applicable pullspecs from the input bundle images'
            log.info(msg)
            db_event.transition(EventState.SKIPPED, msg)
            return []

        # list with metadata about every bundle to do rebuild
        to_rebuild_bundles = []
        # fill 'append' and 'update' fields for bundles to rebuild
        for nvr, pullspecs in rebuild_nvr_to_pullspecs_map.items():
            bundle_digest = self._pyxis.get_manifest_list_digest_by_nvr(nvr)
            if bundle_digest is not None:
                bundles = self._pyxis.get_bundles_by_digest(bundle_digest)
                temp_bundle = bundles[0]
                csv_updates = (self._get_csv_updates(temp_bundle['csv_name'],
                                                     temp_bundle['version']))
                to_rebuild_bundles.append({
                    'nvr': nvr,
                    'update': csv_updates['update'],
                    'pullspec_replacements': pullspecs,
                })
            else:
                log.warning('Can\'t find manifest_list_digest for bundle '
                            f'"{nvr}" in Pyxis')

        if not to_rebuild_bundles:
            msg = 'Can\'t find digests for any of the bundles to rebuild'
            log.warning(msg)
            db_event.transition(EventState.FAILED, msg)
            return []

        return to_rebuild_bundles