コード例 #1
0
class RequestState(db.Model):
    """Represents a state (historical or present) of a request."""

    id = db.Column(db.Integer, primary_key=True)
    state = db.Column(db.Integer, nullable=False)
    state_reason = db.Column(db.String, nullable=False)
    updated = db.Column(db.DateTime(),
                        nullable=False,
                        default=sqlalchemy.func.now())
    request_id = db.Column(db.Integer,
                           db.ForeignKey("request.id"),
                           index=True,
                           nullable=False)
    request = db.relationship("Request",
                              foreign_keys=[request_id],
                              back_populates="states")

    @property
    def state_name(self):
        """Get the state's display name."""
        if self.state:
            return RequestStateMapping(self.state).name

    def __repr__(self):
        return '<RequestState id={} state="{}" request_id={}>'.format(
            self.id, self.state_name, self.request_id)
コード例 #2
0
ファイル: models.py プロジェクト: pombredanne/cachito
class RequestState(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    state = db.Column(db.Integer, nullable=False)
    state_reason = db.Column(db.String, nullable=False)
    updated = db.Column(db.DateTime(),
                        nullable=False,
                        default=sqlalchemy.func.now())
    request_id = db.Column(db.Integer,
                           db.ForeignKey('request.id'),
                           nullable=False)
    request = db.relationship('Request', back_populates='states')

    @property
    def state_name(self):
        """Get the state's display name."""
        if self.state:
            return RequestStateMapping(self.state).name

    def __repr__(self):
        return '<RequestState id={} state="{}" request_id={}>'.format(
            self.id, self.state_name, self.request_id)
コード例 #3
0
class RequestState(db.Model):
    """Represents a state (historical or present) of a request."""

    id = db.Column(db.Integer, primary_key=True)
    state = db.Column(db.Integer, nullable=False)
    state_reason = db.Column(db.String, nullable=False)
    updated = db.Column(db.DateTime(),
                        nullable=False,
                        default=sqlalchemy.func.now())
    request_id = db.Column(db.Integer,
                           db.ForeignKey("request.id"),
                           index=True,
                           nullable=False)
    request = db.relationship("Request",
                              foreign_keys=[request_id],
                              back_populates="states")

    @property
    def state_name(self):
        """Get the state's display name."""
        if self.state:
            return RequestStateMapping(self.state).name

    def __repr__(self):
        return '<RequestState id={} state="{}" request_id={}>'.format(
            self.id, self.state_name, self.request_id)

    @classmethod
    def get_final_states_query(cls):
        """Return query that filters complete/failed states and includes a 'duration' column."""
        # We enumerate states for each request and get 'updated' field of the following state.
        # It allows to find out time between two states.
        states = db.session.query(
            cls.request_id,
            cls.updated,
            func.lead(cls.updated,
                      1).over(partition_by=cls.request_id,
                              order_by=cls.updated).label("next_updated"),
            func.row_number().over(partition_by=cls.request_id,
                                   order_by=cls.updated).label("num"),
        ).subquery()
        return (
            db.session.query(
                cls.request_id,
                cls.state,
                cls.state_reason,
                cls.updated,
                func.extract(
                    "epoch",
                    cls.updated.cast(TIMESTAMP) -
                    states.c.updated.cast(TIMESTAMP),
                ).label("duration"),
                func.extract(
                    "epoch",
                    states.c.next_updated.cast(TIMESTAMP) -
                    states.c.updated.cast(TIMESTAMP),
                ).label("time_in_queue"),
            ).join(states, states.c.request_id == cls.request_id).filter(
                cls.state.in_([
                    RequestStateMapping.complete.value,
                    RequestStateMapping.failed.value
                ]))
            # We need only 'init' state information here to join it with the final state.
            .filter(states.c.num == 1))
コード例 #4
0
class Request(db.Model):
    """A Cachito user request."""

    id = db.Column(db.Integer, primary_key=True)
    created = db.Column(db.DateTime(),
                        nullable=True,
                        index=True,
                        default=sqlalchemy.func.now())
    repo = db.Column(db.String, nullable=False, index=True)
    ref = db.Column(db.String, nullable=False, index=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    submitted_by_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    request_state_id = db.Column(db.Integer,
                                 db.ForeignKey("request_state.id"),
                                 index=True,
                                 unique=True)
    packages_count = db.Column(db.Integer)
    dependencies_count = db.Column(db.Integer)

    state = db.relationship("RequestState", foreign_keys=[request_state_id])
    pkg_managers = db.relationship("PackageManager",
                                   secondary=request_pkg_manager_table,
                                   backref="requests")
    states = db.relationship(
        "RequestState",
        foreign_keys="RequestState.request_id",
        back_populates="request",
        order_by="RequestState.updated",
    )
    environment_variables = db.relationship(
        "EnvironmentVariable",
        secondary=request_environment_variable_table,
        backref="requests",
        order_by="EnvironmentVariable.name",
    )
    submitted_by = db.relationship("User", foreign_keys=[submitted_by_id])
    user = db.relationship("User",
                           foreign_keys=[user_id],
                           back_populates="requests")
    flags = db.relationship("Flag",
                            secondary=request_flag_table,
                            backref="requests",
                            order_by="Flag.name")
    config_files_base64 = db.relationship(
        "ConfigFileBase64",
        secondary=request_config_file_base64_table,
        backref="requests")

    def __repr__(self):
        return "<Request {0!r}>".format(self.id)

    @property
    def content_manifest(self):
        """
        Get the Image Content Manifest for a request.

        :return: the ContentManifest object for the request
        :rtype: ContentManifest
        """
        packages_data = self._get_packages_data()
        packages = [
            content_manifest.Package.from_json(package)
            for package in packages_data.packages
        ]

        return content_manifest.ContentManifest(self, packages)

    def _is_complete(self):
        if len(self.states) > 0:
            latest_state = self.states[-1]
            return latest_state.state_name == RequestStateMapping.complete.name

        return False

    def _get_packages_data(self):
        packages_data = PackagesData()

        if self._is_complete():
            bundle_dir = RequestBundleDir(
                self.id, root=flask.current_app.config["CACHITO_BUNDLES_DIR"])
            packages_data.load(bundle_dir.packages_data)

        return packages_data

    def to_json(self, verbose=True):
        """
        Generate the JSON representation of the request.

        :param bool verbose: determines if the JSON should have verbose details
        :return: the JSON representation of the request
        :rtype: dict
        """
        pkg_managers = [
            pkg_manager.to_json() for pkg_manager in self.pkg_managers
        ]
        user = None
        # If auth is disabled, there will not be a user associated with this request
        if self.user:
            user = self.user.username

        env_vars_json = OrderedDict()
        for env_var in self.environment_variables:
            env_vars_json[env_var.name] = env_var.value
        rv = {
            "id": self.id,
            "created":
            None if self.created is None else self.created.isoformat(),
            "repo": self.repo,
            "ref": self.ref,
            "pkg_managers": pkg_managers,
            "user": user,
            "environment_variables": env_vars_json,
            "flags": [flag.to_json() for flag in self.flags],
        }
        if self.submitted_by:
            rv["submitted_by"] = self.submitted_by.username
        else:
            rv["submitted_by"] = None

        def _state_to_json(state):
            return {
                "state": RequestStateMapping(state.state).name,
                "state_reason": state.state_reason,
                "updated": state.updated.isoformat(),
            }

        if verbose:
            rv["configuration_files"] = flask.url_for(
                "api_v1.get_request_config_files",
                request_id=self.id,
                _external=True)
            rv["content_manifest"] = flask.url_for(
                "api_v1.get_request_content_manifest",
                request_id=self.id,
                _external=True)
            rv["environment_variables_info"] = flask.url_for(
                "api_v1.get_request_environment_variables",
                request_id=self.id,
                _external=True)
            # Use this list comprehension instead of a RequestState.to_json method to avoid
            # including redundant information about the request itself
            states = [_state_to_json(state) for state in self.states]
            # Reverse the list since the latest states should be first
            states = list(reversed(states))
            latest_state = states[0]
            rv["state_history"] = states

            packages_data = self._get_packages_data()
            rv["packages"] = packages_data.packages
            rv["dependencies"] = packages_data.all_dependencies

            dep: Dict[str, Any]
            for dep in itertools.chain(
                    rv["dependencies"],
                (pkg_dep for pkg in rv["packages"]
                 for pkg_dep in pkg["dependencies"]),
            ):
                dep.setdefault("replaces", None)

            if flask.current_app.config["CACHITO_REQUEST_FILE_LOGS_DIR"]:
                rv["logs"] = {
                    "url":
                    flask.url_for("api_v1.get_request_logs",
                                  request_id=self.id,
                                  _external=True)
                }
        else:
            latest_state = _state_to_json(self.state)
            rv["packages"] = self.packages_count
            rv["dependencies"] = self.dependencies_count

        # Show the latest state information in the first level of the JSON
        rv.update(latest_state)
        return rv

    @classmethod
    def from_json(cls, kwargs):
        """
        Create a Request object from JSON.

        :param dict kwargs: the dictionary representing the request
        :return: the Request object
        :rtype: Request
        """
        # Validate all required parameters are present
        required_params = {"repo", "ref"}
        optional_params = {
            "created",
            "dependency_replacements",
            "flags",
            "packages",
            "pkg_managers",
            "user",
        }

        missing_params = required_params - set(kwargs.keys()) - optional_params
        if missing_params:
            raise ValidationError("Missing required parameter(s): {}".format(
                ", ".join(missing_params)))

        # Don't allow the user to set arbitrary columns or relationships
        invalid_params = set(kwargs.keys()) - required_params - optional_params
        if invalid_params:
            raise ValidationError(
                "The following parameters are invalid: {}".format(
                    ", ".join(invalid_params)))

        if not is_request_ref_valid(kwargs["ref"]):
            raise ValidationError(
                'The "ref" parameter must be a 40 character hex string')

        request_kwargs = deepcopy(kwargs)

        # Validate package managers are correctly provided
        pkg_managers_names = request_kwargs.pop("pkg_managers", None)
        # Default to the default package managers
        if pkg_managers_names is None:
            flask.current_app.logger.debug(
                "Using the default package manager(s) (%s) on the request",
                ", ".join(flask.current_app.
                          config["CACHITO_DEFAULT_PACKAGE_MANAGERS"]),
            )
            pkg_managers_names = flask.current_app.config[
                "CACHITO_DEFAULT_PACKAGE_MANAGERS"]

        pkg_managers = PackageManager.get_pkg_managers(pkg_managers_names)
        request_kwargs["pkg_managers"] = pkg_managers

        _validate_request_package_configs(request_kwargs, pkg_managers_names
                                          or [])
        # Remove this from the request kwargs since it's not used as part of the creation of
        # the request object
        request_kwargs.pop("packages", None)

        flag_names = request_kwargs.pop("flags", None)
        if flag_names:
            flag_names = set(flag_names)
            found_flags = Flag.query.filter(Flag.name.in_(flag_names)).filter(
                Flag.active).all()

            if len(flag_names) != len(found_flags):
                found_flag_names = set(flag.name for flag in found_flags)
                invalid_flags = flag_names - found_flag_names
                raise ValidationError("Invalid/Inactive flag(s): {}".format(
                    ", ".join(invalid_flags)))

            request_kwargs["flags"] = found_flags

        dependency_replacements = request_kwargs.pop("dependency_replacements",
                                                     [])
        validate_dependency_replacements(dependency_replacements)

        submitted_for_username = request_kwargs.pop("user", None)
        # current_user.is_authenticated is only ever False when auth is disabled
        if submitted_for_username and not current_user.is_authenticated:
            raise ValidationError(
                'Cannot set "user" when authentication is disabled')
        if current_user.is_authenticated:
            if submitted_for_username:
                allowed_users = flask.current_app.config[
                    "CACHITO_USER_REPRESENTATIVES"]
                if current_user.username not in allowed_users:
                    flask.current_app.logger.error(
                        "The user %s tried to submit a request on behalf of another user, but is "
                        "not allowed",
                        current_user.username,
                    )
                    raise Forbidden(
                        "You are not authorized to create a request on behalf of another user"
                    )

                submitted_for = User.get_or_create(submitted_for_username)
                request_kwargs["user"] = submitted_for
                request_kwargs["submitted_by"] = current_user
            else:
                request_kwargs["user"] = current_user._get_current_object()
        request = cls(**request_kwargs)
        request.add_state("in_progress", "The request was initiated")
        return request

    def add_state(self, state, state_reason):
        """
        Add a RequestState associated with the current request.

        :param str state: the state name
        :param str state_reason: the reason explaining the state transition
        :raises ValidationError: if the state is invalid
        """
        try:
            new_state: RequestStateMapping = RequestStateMapping[state]
        except KeyError:
            raise ValidationError(
                'The state "{}" is invalid. It must be one of: {}.'.format(
                    state, ", ".join(RequestStateMapping.get_state_names())))

        if self.state:
            from_state_name: str = self.state.state_name
            from_state = RequestStateMapping[from_state_name]
            if not RequestStateMapping.allow_transition(from_state, new_state):
                raise ValidationError(
                    f"State transition is not allowed from {from_state_name} to {state}."
                )

        request_state = RequestState(state=new_state.value,
                                     state_reason=state_reason)
        self.states.append(request_state)
        # Send the changes queued up in SQLAlchemy to the database's transaction buffer.
        # This will generate an ID that can be used below.
        db.session.add(request_state)
        db.session.flush()
        self.state = request_state