Example #1
0
File: api_v1.py Project: lcarva/iib
def get_builds():
    """
    Retrieve the paginated build requests.

    :rtype: flask.Response
    """
    state = flask.request.args.get('state')
    verbose = str_to_bool(flask.request.args.get('verbose'))
    max_per_page = flask.current_app.config['IIB_MAX_PER_PAGE']

    query = Request.query.options(*Request.get_query_options(verbose=verbose))
    if state:
        RequestStateMapping.validate_state(state)
        state_int = RequestStateMapping.__members__[state].value
        query = query.join(Request.state)
        query = query.filter(RequestState.state == state_int)

    pagination_query = query.order_by(Request.id.desc()).paginate(max_per_page=max_per_page)
    requests = pagination_query.items

    query_params = {}
    if state:
        query_params['state'] = state
    if verbose:
        query_params['verbose'] = verbose

    response = {
        'items': [request.to_json(verbose=verbose) for request in requests],
        'meta': pagination_metadata(pagination_query, **query_params),
    }
    return flask.jsonify(response)
Example #2
0
def get_build_logs(request_id):
    """
    Retrieve the logs for the build request.

    :param int request_id: the request ID that was passed in through the URL.
    :rtype: flask.Response
    :raise NotFound: if the request is not found or there are no logs for the request
    :raise Gone: if the logs for the build request have been removed due to expiration
    """
    request_log_dir = flask.current_app.config['IIB_REQUEST_LOGS_DIR']
    if not request_log_dir:
        raise NotFound()

    request = Request.query.get_or_404(request_id)
    log_file_path = os.path.join(request_log_dir, f'{request_id}.log')
    if not os.path.exists(log_file_path):
        expired = request.logs_expiration < datetime.utcnow()
        if expired:
            raise Gone(
                f'The logs for the build request {request_id} no longer exist')
        finalized = request.state.state_name in RequestStateMapping.get_final_states(
        )
        if finalized:
            raise NotFound()
        # The request may not have been initiated yet. Return empty logs until it's processed.
        return flask.Response('', mimetype='text/plain')

    with open(log_file_path) as f:
        return flask.Response(f.read(), mimetype='text/plain')
Example #3
0
def get_builds():
    """
    Retrieve the paginated build requests.

    :rtype: flask.Response
    """
    batch_id = flask.request.args.get('batch')
    state = flask.request.args.get('state')
    verbose = str_to_bool(flask.request.args.get('verbose'))
    max_per_page = flask.current_app.config['IIB_MAX_PER_PAGE']

    # Create an alias class to load the polymorphic classes
    poly_request = with_polymorphic(Request, '*')
    query = poly_request.query.options(*get_request_query_options(
        verbose=verbose))
    if state:
        RequestStateMapping.validate_state(state)
        state_int = RequestStateMapping.__members__[state].value
        query = query.join(Request.state)
        query = query.filter(RequestState.state == state_int)

    if batch_id is not None:
        batch_id = Batch.validate_batch(batch_id)
        query = query.filter_by(batch_id=batch_id)

    pagination_query = query.order_by(
        Request.id.desc()).paginate(max_per_page=max_per_page)
    requests = pagination_query.items

    query_params = {}
    if state:
        query_params['state'] = state
    if verbose:
        query_params['verbose'] = verbose
    if batch_id:
        query_params['batch'] = batch_id

    response = {
        'items': [request.to_json(verbose=verbose) for request in requests],
        'meta': pagination_metadata(pagination_query, **query_params),
    }
    return flask.jsonify(response)
Example #4
0
def _get_batch_state_change_envelope(batch, new_batch=False):
    """
    Generate a batch state change ``Envelope`` object.

    No message will be generated if IIB is not configured to send batch state change messages or
    no batch state change message is needed .

    :param iib.web.models.Batch batch: the batch that changed states
    :param bool new_batch: if ``True``, a new batch message will be generated; if ``False``,
        IIB will generate a batch state change message if the batch is no longer ``in_progress``
    :return: the ``Envelope`` for the batch state change or ``None``
    :rtype: Envelope or None
    """
    batch_address = current_app.config.get(
        'IIB_MESSAGING_BATCH_STATE_DESTINATION')
    if not batch_address:
        current_app.logger.debug(
            'No batch state change message will be generated since the configuration '
            '"IIB_MESSAGING_BATCH_STATE_DESTINATION" is not set')
        return

    if new_batch:
        # Avoid querying the database for the batch state since we know it's a new batch
        batch_state = 'in_progress'
    else:
        batch_state = batch.state

    if new_batch or batch_state in RequestStateMapping.get_final_states():
        current_app.logger.debug(
            'Preparing to send a state change message for batch %d', batch.id)
        batch_username = getattr(batch.user, 'username', None)
        content = {
            'batch':
            batch.id,
            'annotations':
            batch.annotations,
            'requests': [{
                'id': request.id,
                'organization': getattr(request, 'organization', None),
                'request_type': request.type_name,
            } for request in batch.requests],
            'state':
            batch_state,
            'user':
            batch_username,
        }
        properties = {
            'batch': batch.id,
            'state': batch_state,
            'user': batch_username,
        }
        return json_to_envelope(batch_address, content, properties)
Example #5
0
File: api_v1.py Project: zanssa/iib
def get_build_logs(request_id):
    """
    Retrieve the logs for the build request.

    :param int request_id: the request ID that was passed in through the URL.
    :rtype: flask.Response
    :raise NotFound: if the request is not found or there are no logs for the request
    :raise Gone: if the logs for the build request have been removed due to expiration
    :raise ValidationError: if the request has not completed yet
    """
    request_log_dir = flask.current_app.config['IIB_REQUEST_LOGS_DIR']
    s3_bucket_name = flask.current_app.config['IIB_AWS_S3_BUCKET_NAME']
    if not s3_bucket_name and not request_log_dir:
        raise NotFound()

    request = Request.query.get_or_404(request_id)

    finalized = request.state.state_name in RequestStateMapping.get_final_states(
    )
    if not finalized:
        raise ValidationError(
            f'The request {request_id} is not complete yet.'
            ' logs will be available once the request is complete.')

    # If S3 bucket is configured, fetch the log file from the S3 bucket.
    # Else, check if logs are stored on the system itself and return them.
    # Otherwise, raise an IIBError.
    if s3_bucket_name:
        log_file = _get_artifact_file_from_s3_bucket(
            'request_logs',
            f'{request_id}.log',
            request_id,
            request.temporary_data_expiration,
            s3_bucket_name,
        )
        return flask.Response(log_file.read(), mimetype='text/plain')

    local_log_file_path = os.path.join(request_log_dir, f'{request_id}.log')
    if not os.path.exists(local_log_file_path):
        expired = request.temporary_data_expiration < datetime.utcnow()
        if expired:
            raise Gone(
                f'The logs for the build request {request_id} no longer exist')
        flask.current_app.logger.warning(
            ' Please make sure either an S3 bucket is configured or the logs are'
            ' stored locally in a directory by specifying IIB_REQUEST_LOGS_DIR'
        )
        raise IIBError(
            'IIB is done processing the request and could not find logs.')

    with open(local_log_file_path) as f:
        return flask.Response(f.read(), mimetype='text/plain')
Example #6
0
def get_related_bundles(request_id):
    """
    Retrieve the related bundle images from the bundle CSV for a regenerate-bundle request.

    :param int request_id: the request ID that was passed in through the URL.
    :rtype: flask.Response
    :raise NotFound: if the request is not found or there are no related bundles for the request
    :raise Gone: if the related bundles for the build request have been removed due to expiration
    """
    request_related_bundles_dir = flask.current_app.config['IIB_REQUEST_RELATED_BUNDLES_DIR']
    if not request_related_bundles_dir:
        raise NotFound()

    request = Request.query.get_or_404(request_id)
    if request.type != RequestTypeMapping.regenerate_bundle.value:
        raise ValidationError(
            f'The request {request_id} is of type {request.type_name}. '
            'This endpoint is only valid for requests of type regenerate-bundle.'
        )

    finalized = request.state.state_name in RequestStateMapping.get_final_states()
    if not finalized:
        raise ValidationError(
            f'The request {request_id} is not complete yet.'
            ' related_bundles will be available once the request is complete.'
        )

    related_bundles_file_path = os.path.join(
        request_related_bundles_dir, f'{request_id}_related_bundles.json'
    )
    if not os.path.exists(related_bundles_file_path):
        expired = request.temporary_data_expiration < datetime.utcnow()
        if expired:
            raise Gone(f'The related_bundles for the build request {request_id} no longer exist')
        raise IIBError(
            'IIB is done processing the request and cannot find related_bundles. Please make '
            f'sure the iib_organizaiton_customizations for organization {request.organization}'
            ' has related_bundles customization type set'
        )

    with open(related_bundles_file_path) as f:
        return flask.Response(f.read(), mimetype='application/json')
Example #7
0
def patch_request(request_id):
    """
    Modify the given request.

    :param int request_id: the request ID from the URL
    :return: a Flask JSON response
    :rtype: flask.Response
    :raise Forbidden: If the user trying to patch a request is not an IIB worker
    :raise NotFound: if the request is not found
    :raise ValidationError: if the JSON is invalid
    """
    allowed_users = flask.current_app.config['IIB_WORKER_USERNAMES']
    # current_user.is_authenticated is only ever False when auth is disabled
    if current_user.is_authenticated and current_user.username not in allowed_users:
        raise Forbidden('This API endpoint is restricted to IIB workers')

    payload = flask.request.get_json()
    if not isinstance(payload, dict):
        raise ValidationError('The input data must be a JSON object')

    if not payload:
        raise ValidationError(
            'At least one key must be specified to update the request')

    request = Request.query.get_or_404(request_id)

    invalid_keys = payload.keys() - request.get_mutable_keys()
    if invalid_keys:
        raise ValidationError('The following keys are not allowed: {}'.format(
            ', '.join(invalid_keys)))

    for key, value in payload.items():
        if key == 'arches':
            Architecture.validate_architecture_json(value)
        elif key == 'bundle_mapping':
            exc_msg = f'The "{key}" key must be an object with the values as lists of strings'
            if not isinstance(value, dict):
                raise ValidationError(exc_msg)
            for v in value.values():
                if not isinstance(v, list) or any(not isinstance(s, str)
                                                  for s in v):
                    raise ValidationError(exc_msg)
        elif not value or not isinstance(value, str):
            raise ValidationError(
                f'The value for "{key}" must be a non-empty string')

    if 'state' in payload and 'state_reason' not in payload:
        raise ValidationError(
            'The "state_reason" key is required when "state" is supplied')
    elif 'state_reason' in payload and 'state' not in payload:
        raise ValidationError(
            'The "state" key is required when "state_reason" is supplied')

    state_updated = False
    if 'state' in payload and 'state_reason' in payload:
        RequestStateMapping.validate_state(payload['state'])
        new_state = payload['state']
        new_state_reason = payload['state_reason']
        # This is to protect against a Celery task getting executed twice and setting the
        # state each time
        if request.state.state == new_state and request.state.state_reason == new_state_reason:
            flask.current_app.logger.info(
                'Not adding a new state since it matches the last state')
        else:
            request.add_state(new_state, new_state_reason)
            state_updated = True

    image_keys = (
        'binary_image_resolved',
        'bundle_image',
        'from_bundle_image_resolved',
        'from_index_resolved',
        'index_image',
    )
    for key in image_keys:
        if key not in payload:
            continue
        key_value = payload.get(key, None)
        key_object = Image.get_or_create(key_value)
        # SQLAlchemy will not add the object to the database if it's already present
        setattr(request, key, key_object)

    for arch in payload.get('arches', []):
        request.add_architecture(arch)

    for operator, bundles in payload.get('bundle_mapping', {}).items():
        operator_img = Operator.get_or_create(operator)
        for bundle in bundles:
            bundle_img = Image.get_or_create(bundle)
            bundle_img.operator = operator_img

    db.session.commit()

    if state_updated:
        messaging.send_message_for_state_change(request)

    if current_user.is_authenticated:
        flask.current_app.logger.info('The user %s patched request %d',
                                      current_user.username, request.id)
    else:
        flask.current_app.logger.info('An anonymous user patched request %d',
                                      request.id)

    return flask.jsonify(request.to_json()), 200
Example #8
0
File: api_v1.py Project: zanssa/iib
def get_builds():
    """
    Retrieve the paginated build requests.

    :rtype: flask.Response
    """
    batch_id = flask.request.args.get('batch')
    state = flask.request.args.get('state')
    verbose = str_to_bool(flask.request.args.get('verbose'))
    max_per_page = flask.current_app.config['IIB_MAX_PER_PAGE']
    request_type = flask.request.args.get('request_type')
    user = flask.request.args.get('user')
    index_image = flask.request.args.get('index_image')

    query_params = {}

    # Create an alias class to load the polymorphic classes
    poly_request = with_polymorphic(Request, '*')
    query = poly_request.query.options(*get_request_query_options(
        verbose=verbose))
    if state:
        query_params['state'] = state
        RequestStateMapping.validate_state(state)
        state_int = RequestStateMapping.__members__[state].value
        query = query.join(Request.state)
        query = query.filter(RequestState.state == state_int)

    if batch_id is not None:
        query_params['batch'] = batch_id
        batch_id = Batch.validate_batch(batch_id)
        query = query.filter_by(batch_id=batch_id)

    if request_type:
        query_params['request_type'] = request_type
        RequestTypeMapping.validate_type(request_type)
        request_type = request_type.replace('-', '_')
        request_type_int = RequestTypeMapping.__members__[request_type].value
        query = query.filter(Request.type == request_type_int)

    if user:
        # join with the user table and then filter on username
        # request table only has the user_id
        query_params['user'] = user
        query = query.join(Request.user).filter(User.username == user)

    if index_image:
        query_params['index_image'] = index_image
        # Get the image id of the image to be searched
        image_result = Image.query.filter_by(
            pull_specification=index_image).first()
        if image_result:
            # join with the Request* tables to get the response as image_ids are stored there
            query = (query.outerjoin(
                RequestCreateEmptyIndex,
                Request.id == RequestCreateEmptyIndex.id).outerjoin(
                    RequestAdd, Request.id == RequestAdd.id).outerjoin(
                        RequestMergeIndexImage,
                        Request.id == RequestMergeIndexImage.id).outerjoin(
                            RequestRm, Request.id == RequestRm.id))

            query = query.filter(
                or_(
                    RequestCreateEmptyIndex.index_image_id == image_result.id,
                    RequestAdd.index_image_id == image_result.id,
                    RequestMergeIndexImage.index_image_id == image_result.id,
                    RequestRm.index_image_id == image_result.id,
                ))
        # if index_image is not found in image table, then raise an error
        else:
            raise ValidationError(f'{index_image} is not a valid index image')

    pagination_query = query.order_by(
        Request.id.desc()).paginate(max_per_page=max_per_page)
    requests = pagination_query.items

    response = {
        'items': [request.to_json(verbose=verbose) for request in requests],
        'meta': pagination_metadata(pagination_query, **query_params),
    }
    return flask.jsonify(response)
Example #9
0
File: api_v1.py Project: zanssa/iib
def get_related_bundles(request_id):
    """
    Retrieve the related bundle images from the bundle CSV for a regenerate-bundle request.

    :param int request_id: the request ID that was passed in through the URL.
    :rtype: flask.Response
    :raise NotFound: if the request is not found or there are no related bundles for the request
    :raise Gone: if the related bundles for the build request have been removed due to expiration
    :raise ValidationError: if the request is of invalid type or is not completed yet
    """
    request_related_bundles_dir = flask.current_app.config[
        'IIB_REQUEST_RELATED_BUNDLES_DIR']
    s3_bucket_name = flask.current_app.config['IIB_AWS_S3_BUCKET_NAME']
    if not s3_bucket_name and not request_related_bundles_dir:
        raise NotFound()

    request = Request.query.get_or_404(request_id)
    if request.type != RequestTypeMapping.regenerate_bundle.value:
        raise ValidationError(
            f'The request {request_id} is of type {request.type_name}. '
            'This endpoint is only valid for requests of type regenerate-bundle.'
        )

    finalized = request.state.state_name in RequestStateMapping.get_final_states(
    )
    if not finalized:
        raise ValidationError(
            f'The request {request_id} is not complete yet.'
            ' related_bundles will be available once the request is complete.')

    # If S3 bucket is configured, fetch the related bundles file from the S3 bucket.
    # Else, check if related bundles are stored on the system itself and return them.
    # Otherwise, raise an IIBError.
    if s3_bucket_name:
        log_file = _get_artifact_file_from_s3_bucket(
            'related_bundles',
            f'{request_id}_related_bundles.json',
            request_id,
            request.temporary_data_expiration,
            s3_bucket_name,
        )
        return flask.Response(log_file.read(), mimetype='application/json')

    related_bundles_file_path = os.path.join(
        request_related_bundles_dir, f'{request_id}_related_bundles.json')
    if not os.path.exists(related_bundles_file_path):
        expired = request.temporary_data_expiration < datetime.utcnow()
        if expired:
            raise Gone(
                f'The related_bundles for the build request {request_id} no longer exist'
            )
        if request.organization:
            raise IIBError(
                'IIB is done processing the request and cannot find related_bundles. Please make '
                f'sure the iib_organization_customizations for organization {request.organization}'
                ' has related_bundles customization type set')
        flask.current_app.logger.warning(
            ' Please make sure either an S3 bucket is configured or the logs are'
            ' stored locally in a directory by specifying IIB_REQUEST_LOGS_DIR'
        )
        raise IIBError(
            'IIB is done processing the request and could not find related_bundles.'
        )

    with open(related_bundles_file_path) as f:
        return flask.Response(f.read(), mimetype='application/json')