Example #1
0
def get_checked_field(document, name, value_type):
    """Validates and retrieves a typed field from a document.

    This function attempts to look up doc[name], and raises
    appropriate HTTP errors if the field is missing or not an
    instance of the given type.

    :param document: dict-like object
    :param name: field name
    :param value_type: expected value type, or '*' to accept any type
    :raises: HTTPBadRequest if the field is missing or not an
        instance of value_type
    :returns: value obtained from doc[name]
    """

    try:
        value = document[name]
    except KeyError:
        description = _(u'Missing "{name}" field.').format(name=name)
        raise exceptions.HTTPBadRequestBody(description)

    if value_type == '*' or isinstance(value, value_type):
        return value

    description = _(u'The value of the "{name}" field must be a {vtype}.')
    description = description.format(name=name, vtype=value_type.__name__)
    raise exceptions.HTTPBadRequestBody(description)
Example #2
0
    def on_patch(self, request, response, shard):
        """Allows one to update a shard's weight, location, and/or options.

        This method expects the user to submit a JSON object
        containing atleast one of: 'hosts', 'weight', 'options'. If
        none are found, the request is flagged as bad. There is also
        strict format checking through the use of
        jsonschema. Appropriate errors are returned in each case for
        badly formatted input.

        :returns: HTTP | 200,400
        """
        LOG.debug(u'PATCH shard - name: %s', shard)
        data = utils.load(request)

        EXPECT = ('weight', 'location', 'options')
        if not any([(field in data) for field in EXPECT]):
            LOG.debug(u'PATCH shard, bad params')
            raise wsgi_errors.HTTPBadRequestBody(
                'One of `location`, `weight`, or `options` needs '
                'to be specified')

        for field in EXPECT:
            utils.validate(self._validators[field], data)

        try:
            fields = dict((k, v) for k, v in data.items()
                          if k in EXPECT and v is not None)

            self._ctrl.update(shard, **fields)
        except exceptions.ShardDoesNotExist as ex:
            LOG.exception(ex)
            raise falcon.HTTPNotFound()
Example #3
0
    def on_patch(self, request, response, partition):
        """Allows one to update a partition's weight and/or hosts.

        This method expects the user to submit a JSON object
        containing both or either of 'hosts' and 'weight'. If neither
        is found, the request is flagged as bad. There is also strict
        format checking through the use of jsonschema. Appropriate
        errors are returned in each case for badly formatted input.

        :returns: HTTP | 200,400

        """
        LOG.debug('PATCH partition - name: {0}'.format(partition))
        data = utils.load(request)

        if 'weight' not in data and 'hosts' not in data:
            LOG.debug('PATCH partition, bad params')
            raise wsgi_errors.HTTPBadRequestBody(
                'One of `hosts` or `weight` needs to be specified')

        utils.validate(self._weight_validator, data)
        utils.validate(self._hosts_validator, data)
        try:
            fields = dict((k, v) for k, v in six.iteritems(data)
                          if k in ('hosts', 'weight') and v is not None)

            self._ctrl.update(partition, **fields)
        except exceptions.PartitionNotFound as ex:
            LOG.exception(ex)
            raise falcon.HTTPNotFound()
Example #4
0
    def on_post(self, req, resp, project_id, queue_name):
        LOG.debug(
            _(u'Claims collection POST - queue: %(queue)s, '
              u'project: %(project)s'), {
                  'queue': queue_name,
                  'project': project_id
              })

        # Check for an explicit limit on the # of messages to claim
        limit = req.get_param_as_int('limit')
        claim_options = {} if limit is None else {'limit': limit}

        # Place JSON size restriction before parsing
        if req.content_length > self._metadata_max_length:
            description = _(u'Claim metadata size is too large.')
            raise wsgi_exceptions.HTTPBadRequestBody(description)

        # Read claim metadata (e.g., TTL) and raise appropriate
        # HTTP errors as needed.
        metadata, = wsgi_utils.filter_stream(req.stream, req.content_length,
                                             CLAIM_POST_SPEC)

        # Claim some messages
        try:
            self._validate.claim_creation(metadata, **claim_options)
            cid, msgs = self.claim_controller.create(queue_name,
                                                     metadata=metadata,
                                                     project=project_id,
                                                     **claim_options)

            # Buffer claimed messages
            # TODO(kgriffs): optimize, along with serialization (below)
            resp_msgs = list(msgs)

        except validation.ValidationFailed as ex:
            raise wsgi_exceptions.HTTPBadRequestAPI(six.text_type(ex))

        except Exception as ex:
            LOG.exception(ex)
            description = _(u'Claim could not be created.')
            raise wsgi_exceptions.HTTPServiceUnavailable(description)

        # Serialize claimed messages, if any. This logic assumes
        # the storage driver returned well-formed messages.
        if len(resp_msgs) != 0:
            for msg in resp_msgs:
                msg['href'] = _msg_uri_from_claim(
                    req.path.rpartition('/')[0], msg['id'], cid)

                del msg['id']

            resp.location = req.path + '/' + cid
            resp.body = utils.to_json(resp_msgs)
            resp.status = falcon.HTTP_201
        else:
            resp.status = falcon.HTTP_204
Example #5
0
def validate(validator, document):
    """Verifies a document against a schema.

    :param validator: a validator to use to check validity
    :type validator: jsonschema.Draft4Validator
    :param document: document to check
    :type document: dict
    :raises: wsgi_errors.HTTPBadRequestBody
    """
    try:
        validator.validate(document)
    except jsonschema.ValidationError as ex:
        raise wsgi_errors.HTTPBadRequestBody(
            '{0}: {1}'.format(ex.args, ex.message)
        )
Example #6
0
def load(req):
    """Reads request body, raising an exception if it is not JSON.

    :param req: The request object to read from
    :type req: falcon.Request
    :return: a dictionary decoded from the JSON stream
    :rtype: dict
    :raises: wsgi_errors.HTTPBadRequestBody
    """
    try:
        return json_utils.read_json(req.stream, req.content_length)
    except (json_utils.MalformedJSON, json_utils.OverflowedJSONInteger) as ex:
        LOG.exception(ex)
        raise wsgi_errors.HTTPBadRequestBody(
            'JSON could not be parsed.'
        )
Example #7
0
    def on_put(self, req, resp, project_id, queue_name):
        LOG.debug(
            _(u'Queue metadata PUT - queue: %(queue)s, '
              u'project: %(project)s'), {
                  'queue': queue_name,
                  'project': project_id
              })

        # Place JSON size restriction before parsing
        if req.content_length > self._wsgi_conf.metadata_max_length:
            description = _(u'Queue metadata size is too large.')
            raise wsgi_exceptions.HTTPBadRequestBody(description)

        # Deserialize queue metadata
        metadata, = wsgi_utils.filter_stream(req.stream,
                                             req.content_length,
                                             spec=None)

        try:
            self._validate.queue_content(
                metadata,
                check_size=(self._validate._limits_conf.metadata_size_uplimit <
                            self._wsgi_conf.metadata_max_length))
            self.queue_ctrl.set_metadata(queue_name,
                                         metadata=metadata,
                                         project=project_id)

        except validation.ValidationFailed as ex:
            raise wsgi_exceptions.HTTPBadRequestAPI(six.text_type(ex))

        except storage_exceptions.QueueDoesNotExist:
            raise falcon.HTTPNotFound()

        except Exception as ex:
            LOG.exception(ex)
            description = _(u'Metadata could not be updated.')
            raise wsgi_exceptions.HTTPServiceUnavailable(description)

        resp.status = falcon.HTTP_204
        resp.location = req.path
Example #8
0
    def on_patch(self, req, resp, project_id, queue_name, claim_id):
        LOG.debug(
            _(u'Claim Item PATCH - claim: %(claim_id)s, '
              u'queue: %(queue_name)s, project:%(project_id)s') % {
                  'queue_name': queue_name,
                  'project_id': project_id,
                  'claim_id': claim_id
              })

        # Place JSON size restriction before parsing
        if req.content_length > self._metadata_max_length:
            description = _(u'Claim metadata size is too large.')
            raise wsgi_exceptions.HTTPBadRequestBody(description)

        # Read claim metadata (e.g., TTL) and raise appropriate
        # HTTP errors as needed.
        metadata, = wsgi_utils.filter_stream(req.stream, req.content_length,
                                             CLAIM_PATCH_SPEC)

        try:
            self._validate.claim_updating(metadata)
            self.claim_controller.update(queue_name,
                                         claim_id=claim_id,
                                         metadata=metadata,
                                         project=project_id)

            resp.status = falcon.HTTP_204

        except validation.ValidationFailed as ex:
            raise wsgi_exceptions.HTTPBadRequestAPI(six.text_type(ex))

        except storage_exceptions.DoesNotExist:
            raise falcon.HTTPNotFound()

        except Exception as ex:
            LOG.exception(ex)
            description = _(u'Claim could not be updated.')
            raise wsgi_exceptions.HTTPServiceUnavailable(description)
Example #9
0
def filter_stream(stream, len, spec=None, doctype=JSONObject):
    """Reads, deserializes, and validates a document from a stream.

    :param stream: file-like object from which to read an object or
        array of objects.
    :param len: number of bytes to read from stream
    :param spec: (Default None) Iterable describing expected fields,
        yielding tuples with the form of:

            (field_name, value_type).

        Note that value_type may either be a Python type, or the
        special string '*' to accept any type. If spec is None, the
        incoming documents will not be validated.
    :param doctype: type of document to expect; must be either
        JSONObject or JSONArray.
    :raises: HTTPBadRequest, HTTPServiceUnavailable
    :returns: A sanitized, filtered version of the document list read
        from the stream. If the document contains a list of objects,
        each object will be filtered and returned in a new list. If,
        on the other hand, the document is expected to contain a
        single object, that object will be filtered and returned as
        a single-element iterable.
    """

    if len is None:
        description = _(u'Request body can not be empty')
        raise exceptions.HTTPBadRequestBody(description)

    try:
        # TODO(kgriffs): read_json should stream the resulting list
        # of messages, returning a generator rather than buffering
        # everything in memory (bp/streaming-serialization).
        document = utils.read_json(stream, len)

    except utils.MalformedJSON as ex:
        LOG.exception(ex)
        description = _(u'Request body could not be parsed.')
        raise exceptions.HTTPBadRequestBody(description)

    except utils.OverflowedJSONInteger as ex:
        LOG.exception(ex)
        description = _(u'JSON contains integer that is too large.')
        raise exceptions.HTTPBadRequestBody(description)

    except Exception as ex:
        # Error while reading from the network/server
        LOG.exception(ex)
        description = _(u'Request body could not be read.')
        raise exceptions.HTTPServiceUnavailable(description)

    if doctype is JSONObject:
        if not isinstance(document, JSONObject):
            raise exceptions.HTTPDocumentTypeNotSupported()

        return (document, ) if spec is None else (filter(document, spec), )

    if doctype is JSONArray:
        if not isinstance(document, JSONArray):
            raise exceptions.HTTPDocumentTypeNotSupported()

        if spec is None:
            return document

        return [filter(obj, spec) for obj in document]

    raise TypeError('doctype must be either a JSONObject or JSONArray')
Example #10
0
    def on_post(self, req, resp, project_id, queue_name):
        LOG.debug(_(u'Messages collection POST - queue:  %(queue)s, '
                    u'project: %(project)s'),
                  {'queue': queue_name, 'project': project_id})

        client_uuid = wsgi_utils.get_client_uuid(req)

        # Place JSON size restriction before parsing
        if req.content_length > self._wsgi_conf.content_max_length:
            description = _(u'Message collection size is too large.')
            raise wsgi_exceptions.HTTPBadRequestBody(description)

        # Pull out just the fields we care about
        messages = wsgi_utils.filter_stream(
            req.stream,
            req.content_length,
            MESSAGE_POST_SPEC,
            doctype=wsgi_utils.JSONArray)

        # Enqueue the messages
        partial = False

        try:
            # No need to check each message's size if it
            # can not exceed the request size limit
            self._validate.message_posting(
                messages, check_size=(
                    self._validate._limits_conf.message_size_uplimit <
                    self._wsgi_conf.content_max_length))

            message_ids = self.message_controller.post(
                queue_name,
                messages=messages,
                project=project_id,
                client_uuid=client_uuid)

        except validation.ValidationFailed as ex:
            raise wsgi_exceptions.HTTPBadRequestAPI(six.text_type(ex))

        except storage_exceptions.DoesNotExist:
            raise falcon.HTTPNotFound()

        except storage_exceptions.MessageConflict as ex:
            LOG.exception(ex)
            partial = True
            message_ids = ex.succeeded_ids

            if not message_ids:
                # TODO(kgriffs): Include error code that is different
                # from the code used in the generic case, below.
                description = _(u'No messages could be enqueued.')
                raise wsgi_exceptions.HTTPServiceUnavailable(description)

        except Exception as ex:
            LOG.exception(ex)
            description = _(u'Messages could not be enqueued.')
            raise wsgi_exceptions.HTTPServiceUnavailable(description)

        # Prepare the response
        ids_value = ','.join(message_ids)
        resp.location = req.path + '?ids=' + ids_value

        hrefs = [req.path + '/' + id for id in message_ids]
        body = {'resources': hrefs, 'partial': partial}
        resp.body = utils.to_json(body)
        resp.status = falcon.HTTP_201