def patch(self, ticket_id): # TODO: restart expire timer if not ticket_id: raise HTTPBadRequest("Ticket id is required") try: patch = self.request.json except ValueError as e: raise HTTPBadRequest("Invalid patch: %s" % e) try: timeout = patch["timeout"] except KeyError: raise HTTPBadRequest("Missing timeout key") try: timeout = int(timeout) except ValueError as e: raise HTTPBadRequest("Invalid timeout value: %s" % e) try: ticket = tickets.get(ticket_id) except KeyError: raise HTTPNotFound("No such ticket: %s" % ticket_id) self.log.info("[%s] EXTEND timeout=%s ticket=%s", web.client_address(self.request), timeout, ticket_id) ticket.extend(timeout) return web.response()
def _flush(self, ticket_id, msg): ticket = tickets.authorize(ticket_id, "write", 0, 0) self.log.info("[%s] FLUSH ticket=%s", web.client_address(self.request), ticket_id) op = directio.Flush(ticket.url.path, clock=self.clock) ticket.run(op) return web.response()
def options(self, ticket_id): if not ticket_id: raise HTTPBadRequest("Ticket id is required") self.log.info("[%s] OPTIONS ticket=%s", web.client_address(self.request), ticket_id) if ticket_id == "*": # Reporting the meta-capabilities for all images. allow = ["OPTIONS", "GET", "PUT", "PATCH"] features = ["zero", "flush"] else: # Reporting real image capabilities per ticket. # This check will fail if the ticket has expired. ticket = tickets.authorize(ticket_id, "read", 0, 0) # Accessing ticket options considered as client activity. ticket.touch() allow = ["OPTIONS"] features = [] if ticket.may("read"): allow.append("GET") if ticket.may("write"): allow.extend(("PUT", "PATCH")) features = ["zero", "flush"] return web.response(payload={ "features": features, "unix_socket": self.config.images.socket, }, allow=",".join(allow))
def put(self, ticket_id): if not ticket_id: raise HTTPBadRequest("Ticket id is required") size = self.request.content_length if size is None: raise HTTPBadRequest("Content-Length header is required") if size < 0: raise HTTPBadRequest("Invalid Content-Length header: %r" % size) content_range = web.content_range(self.request) offset = content_range.start or 0 # For backward compatibility, we flush by default. flush = validate.enum(self.request.params, "flush", ("y", "n"), default="y") flush = (flush == "y") ticket = tickets.authorize(ticket_id, "write", offset, size) # TODO: cancel copy if ticket expired or revoked self.log.info("[%s] WRITE size=%d offset=%d flush=%s ticket=%s", web.client_address(self.request), size, offset, flush, ticket_id) op = directio.Receive(ticket.url.path, self.request.body_file_raw, size, offset=offset, flush=flush, buffersize=self.config.daemon.buffer_size, clock=self.clock) try: ticket.run(op) except errors.PartialContent as e: raise HTTPBadRequest(str(e)) return web.response()
def patch(self, res_id): """ Proxy PATCH request to daemon. """ if not self.request.content_length: raise exc.HTTPBadRequest("Content-Length is required") logging.info("[%s] PATCH ticket=%s", web.client_address(self.request), self.ticket.id) # Notes: # - PATCH response is not cachable, no need for cache-control. # - We cannot have read_timeout since PATCH can take unpredictable # time, depending on size of the modified byte range, and the storage # capabillties. res = self.make_imaged_request( "PATCH", self.get_imaged_url(self.ticket), self.request.headers, web.CappedStream(self.request.body_file, self.request.content_length), False, connection_timeout=self.config.imaged_connection_timeout_sec) # TODO: We expect empty response from the daemon. If we start to return # non-empty response, this must be changed to stream the daemon # response to the caller. return web.response(res.status_code)
def get(self, ticket_id): if not ticket_id: raise HTTPBadRequest("Ticket id is required") try: ticket = tickets.get(ticket_id) except KeyError: raise HTTPNotFound("No such ticket %r" % ticket_id) ticket_info = ticket.info() self.log.debug("[%s] GET ticket=%s", web.client_address(self.request), ticket_info) return web.response(payload=ticket_info)
def options(self, res_id): """ Proxy OPTIONS request to daemon. """ logging.info("[%s] OPTIONS", web.client_address(self.request)) allow = {"GET", "PUT", "PATCH", "OPTIONS"} features = {"zero", "flush"} # Reporting the meta-capabilities for all images if res_id == "*": return web.response(payload={"features": list(features)}, allow=','.join(allow)) ticket = auth.authorize_request(res_id, self.request) try: res = self.make_imaged_request( "OPTIONS", self.get_imaged_url(ticket), self.request.headers, None, False, connection_timeout=self.config.imaged_connection_timeout_sec, read_timeout=self.config.imaged_read_timeout_sec) except exc.HTTPMethodNotAllowed: # An old daemon - we estimate its methods. We also assume that # the ticket is readable and writable, since only the daemon # knows about that. logging.info("The daemon does not support OPTIONS, " "returning an estimation") return web.response(payload={"features": []}, allow="OPTIONS,GET,PUT") if res.status_code != httplib.OK: raise exc.HTTPInternalServerError( "Got unexpected response from host: %d %s" % (res.status_code, res.content)) try: daemon_allow = set(res.headers.get("Allow").split(",")) except KeyError: raise exc.HTTPInternalServerError( "Got invalid response from host: missing Allow header") try: daemon_features = set(res.json()["features"]) except (ValueError, KeyError): raise exc.HTTPInternalServerError( "Got invalid response from host: " "invalid JSON or missing 'features'") allow = allow.intersection(daemon_allow) features = features.intersection(daemon_features) return web.response(payload={"features": list(features)}, allow=','.join(allow))
def delete(self, ticket_id): """ Delete a ticket if exists. Note that DELETE is idempotent; the client can issue multiple DELETE requests in case of network failures. See https://tools.ietf.org/html/rfc7231#section-4.2.2. """ # TODO: cancel requests using deleted tickets self.log.info("[%s] REMOVE ticket=%s", web.client_address(self.request), ticket_id) if ticket_id: try: tickets.remove(ticket_id) except KeyError: log.debug("Ticket %s does not exists", ticket_id) else: tickets.clear() return web.response(status=204)
def put(self, ticket_id): # TODO # - reject invalid or expired ticket # - start expire timer if not ticket_id: raise HTTPBadRequest("Ticket id is required") try: ticket_dict = self.request.json except ValueError as e: raise HTTPBadRequest("Ticket is not in a json format: %s" % e) self.log.info("[%s] ADD ticket=%s", web.client_address(self.request), ticket_dict) try: tickets.add(ticket_dict) except errors.InvalidTicket as e: raise HTTPBadRequest("Invalid ticket: %s" % e) return web.response()
def get(self, ticket_id): # TODO: cancel copy if ticket expired or revoked if not ticket_id: raise HTTPBadRequest("Ticket id is required") # TODO: support partial range (e.g. bytes=0-*) offset = 0 size = None if self.request.range: offset = self.request.range.start if self.request.range.end is not None: size = self.request.range.end - offset ticket = tickets.authorize(ticket_id, "read", offset, size) if size is None: size = ticket.size - offset self.log.info("[%s] READ size=%d offset=%d ticket=%s", web.client_address(self.request), size, offset, ticket_id) op = directio.Send(ticket.url.path, None, size, offset=offset, buffersize=self.config.daemon.buffer_size, clock=self.clock) content_disposition = "attachment" if ticket.filename: filename = ticket.filename.encode("utf-8") content_disposition += "; filename=%s" % filename resp = webob.Response( status=206 if self.request.range else 200, app_iter=ticket.bind(op), content_type="application/octet-stream", content_length=str(size), content_disposition=content_disposition, ) if self.request.range: content_range = self.request.range.content_range(ticket.size) resp.headers["content-range"] = str(content_range) return resp
def _zero(self, ticket_id, msg): size = validate.integer(msg, "size", minval=0) offset = validate.integer(msg, "offset", minval=0, default=0) flush = validate.boolean(msg, "flush", default=False) ticket = tickets.authorize(ticket_id, "write", offset, size) self.log.info("[%s] ZERO size=%d offset=%d flush=%s ticket=%s", web.client_address(self.request), size, offset, flush, ticket_id) op = directio.Zero(ticket.url.path, size, offset=offset, flush=flush, buffersize=self.config.daemon.buffer_size, clock=self.clock, sparse=ticket.sparse) try: ticket.run(op) except errors.PartialContent as e: raise HTTPBadRequest(str(e)) return web.response()
def put(self, res_id): """ Handles sending data to host for PUT or PATCH. :param request: http request object :type request: webob.Request :return: http response object :rtype: webob.Response """ imaged_url = self.get_imaged_url(self.ticket) if "flush" in self.request.params: imaged_url += "?flush=" + self.request.params["flush"] headers = self.get_default_headers(res_id) if 'Content-Length' not in self.request.headers: raise exc.HTTPBadRequest("Content-Length header is required") headers['Content-Length'] = self.request.headers['Content-Length'] if 'Content-Range' in self.request.headers: headers['Content-Range'] = self.request.headers['Content-Range'] max_transfer_bytes = int(headers['Content-Length']) body = web.CappedStream(self.request.body_file, max_transfer_bytes) stream = False content_range = web.content_range(self.request) offset = content_range.start or 0 flush = self.request.params.get("flush", "y") == "y" logging.info("[%s] WRITE size=%d offset=%d flush=%s ticket=%s", web.client_address(self.request), max_transfer_bytes, offset, flush, self.ticket.id) imaged_response = self.make_imaged_request( self.request.method, imaged_url, headers, body, stream, connection_timeout=self.config.imaged_connection_timeout_sec, read_timeout=self.config.imaged_read_timeout_sec) response = web.response(imaged_response.status_code) response.headers['Cache-Control'] = 'no-cache, no-store' return response
def get(self, res_id): imaged_url = self.get_imaged_url(self.ticket) headers = self.get_default_headers(res_id) # Note that webob request.headers is case-insensitive. if 'Range' in self.request.headers: headers['Range'] = self.request.headers['Range'] body = "" stream = True # Don't let Requests read entire body into memory logging.info("[%s] READ ticket=%s", web.client_address(self.request), self.ticket.id) imaged_response = self.make_imaged_request( self.request.method, imaged_url, headers, body, stream, connection_timeout=self.config.imaged_connection_timeout_sec, read_timeout=self.config.imaged_read_timeout_sec) response = web.response(imaged_response.status_code) response.headers['Cache-Control'] = 'no-cache, no-store' response.headers['Content-Range'] = \ imaged_response.headers.get('Content-Range', '') disposition = imaged_response.headers.get('Content-Disposition') if disposition is not None: response.headers['Content-Disposition'] = disposition max_transfer_bytes = int(imaged_response.headers.get('Content-Length')) response.body_file = web.CappedStream(RequestStreamAdapter( imaged_response.iter_content(4096, False)), max_transfer_bytes) response.headers['Content-Length'] = str(max_transfer_bytes) logging.debug("Resource %s: transferring %d bytes from host", res_id, max_transfer_bytes) return response