class RequestAPITest(TestHandlerBase): def setUp(self): self.request_mock = Mock() self.ts_epoch = 1451606400000 self.ts_dt = datetime.datetime(2016, 1, 1) self.request_dict = { "children": [], "parent": None, "system": "system_name", "system_version": "0.0.1", "instance_name": "default", "command": "say", "id": "58542eb571afd47ead90d25f", "parameters": {}, "comment": "bye!", "output": "nested output", "output_type": "STRING", "status": "IN_PROGRESS", "command_type": "ACTION", "created_at": self.ts_epoch, "updated_at": self.ts_epoch, "error_class": None, "metadata": {}, "has_parent": True, "requester": None, } self.job_dict = { "name": "job_name", "trigger_type": "date", "trigger": {"run_date": self.ts_epoch, "timezone": "utc"}, "request_template": { "system": "system", "system_version": "1.0.0", "instance_name": "default", "command": "speak", "parameters": {"message": "hey!"}, "comment": "hi!", "metadata": {"request": "stuff"}, }, "misfire_grace_time": 3, "coalesce": True, "next_run_time": self.ts_epoch, "success_count": 0, "error_count": 0, } db_dict = copy.deepcopy(self.job_dict) db_dict["request_template"] = RequestTemplate(**db_dict["request_template"]) db_dict["trigger"]["run_date"] = self.ts_dt db_dict["trigger"] = DateTrigger(**db_dict["trigger"]) db_dict["next_run_time"] = self.ts_dt self.job = Job(**db_dict) db_dict = copy.deepcopy(self.request_dict) db_dict["created_at"] = self.ts_dt db_dict["updated_at"] = self.ts_dt self.request = Request(**db_dict) super(RequestAPITest, self).setUp() def tearDown(self): Request.objects.delete() Job.objects.delete() def test_get(self): self.request.save() response = self.fetch("/api/v1/requests/" + str(self.request.id)) self.assertEqual(200, response.code) data = json.loads(response.body.decode("utf-8")) data.pop("updated_at") self.request_dict.pop("updated_at") self.assertEqual(self.request_dict, data) def test_patch_replace_duplicate(self): self.request.status = "SUCCESS" self.request.output = "output" self.request.save() body = json.dumps( { "operations": [ {"operation": "replace", "path": "/output", "value": "output"}, {"operation": "replace", "path": "/status", "value": "SUCCESS"}, ] } ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.assertEqual(200, response.code) self.request.reload() self.assertEqual("SUCCESS", self.request.status) self.assertEqual("output", self.request.output) def test_patch_replace_status(self): self.request.save() body = json.dumps( { "operations": [ {"operation": "replace", "path": "/status", "value": "SUCCESS"} ] } ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.assertEqual(200, response.code) self.request.reload() self.assertEqual("SUCCESS", self.request.status) def test_patch_replace_output(self): self.request.output = "old_output_but_not_done_with_progress" self.request.save() body = json.dumps( { "operations": [ {"operation": "replace", "path": "/output", "value": "output"} ] } ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.assertEqual(200, response.code) self.request.reload() self.assertEqual("output", self.request.output) def test_patch_replace_error_class(self): self.request.error_class = "Klazz1" body = json.dumps( { "operations": [ {"operation": "replace", "path": "/error_class", "value": "error"} ] } ) self.request.save() response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.request.reload() self.assertEqual(200, response.code) self.assertEqual("error", self.request.error_class) def test_patch_replace_bad_status(self): self.request.save() body = json.dumps( { "operations": [ {"operation": "replace", "path": "/status", "value": "bad"} ] } ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.assertGreaterEqual(response.code, 400) def test_patch_update_output_for_complete_request(self): self.request.status = "SUCCESS" self.request.output = "old_value" self.request.save() body = json.dumps( { "operations": [ { "operation": "replace", "path": "/output", "value": "shouldnt work", } ] } ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.request.reload() self.assertGreaterEqual(response.code, 400) self.assertEqual(self.request.output, "old_value") def test_patch_no_system(self): good_id_does_not_exist = "".join("1" for _ in range(24)) response = self.fetch( "/api/v1/requests/" + good_id_does_not_exist, method="PATCH", body='{"operations": [{"operation": "fake"}]}', headers={"content-type": "application/json"}, ) self.assertEqual(response.code, 404) def test_patch_replace_bad_path(self): self.request.save() body = json.dumps( {"operations": [{"operation": "replace", "path": "/bad", "value": "error"}]} ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.assertGreaterEqual(response.code, 400) def test_patch_bad_operation(self): self.request.save() response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body='{"operations": [{"operation": "fake"}]}', headers={"content-type": "application/json"}, ) self.assertGreaterEqual(response.code, 400) def test_prometheus_endpoint(self): handler = self.app.find_handler(request=Mock(path="/api/v1/requests")) c = handler.handler_class( self.app, Mock(path="/api/v1/requests/111111111111111111111111") ) assert c.prometheus_endpoint == "/api/v1/requests/<ID>" def test_update_job_numbers(self): self.job.save() self.request.metadata["_bg_job_id"] = str(self.job.id) self.request.save() body = json.dumps( { "operations": [ {"operation": "replace", "path": "/status", "value": "SUCCESS"} ] } ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.assertEqual(response.code, 200) self.job.reload() self.assertEqual(self.job.success_count, 1) self.assertEqual(self.job.error_count, 0) def test_update_job_numbers_error(self): self.job.save() self.request.metadata["_bg_job_id"] = str(self.job.id) self.request.save() body = json.dumps( { "operations": [ {"operation": "replace", "path": "/status", "value": "ERROR"} ] } ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.assertEqual(response.code, 200) self.job.reload() self.assertEqual(self.job.success_count, 0) self.assertEqual(self.job.error_count, 1) def test_update_job_invalid_id(self): self.request.metadata["_bg_job_id"] = "".join(["1" for _ in range(24)]) self.request.save() body = json.dumps( { "operations": [ {"operation": "replace", "path": "/status", "value": "ERROR"} ] } ) response = self.fetch( "/api/v1/requests/" + str(self.request.id), method="PATCH", body=body, headers={"content-type": "application/json"}, ) self.assertEqual(response.code, 200)
def post(self): """ --- summary: Create a new Request parameters: - name: request in: body description: The Request definition schema: $ref: '#/definitions/Request' - name: blocking in: query required: false description: Flag indicating whether to wait for request completion type: boolean default: false - name: timeout in: query required: false description: Maximum time (seconds) to wait for request completion type: integer default: None (Wait forever) consumes: - application/json - application/x-www-form-urlencoded responses: 201: description: A new Request has been created schema: $ref: '#/definitions/Request' headers: Instance-Status: type: string description: | Current status of the Instance that will process the created Request 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Requests """ self.request.event.name = Events.REQUEST_CREATED.name if self.request.mime_type == "application/json": request_model = self.parser.parse_request( self.request.decoded_body, from_string=True) elif self.request.mime_type == "application/x-www-form-urlencoded": args = {"parameters": {}} for key, value in self.request.body_arguments.items(): if key.startswith("parameters."): args["parameters"][key.replace("parameters.", "")] = value[0].decode( self.request.charset) else: args[key] = value[0].decode(self.request.charset) request_model = Request(**args) else: raise ModelValidationError( "Unsupported or missing content-type header") if request_model.parent: request_model.parent = Request.objects.get( id=str(request_model.parent.id)) if request_model.parent.status in Request.COMPLETED_STATUSES: raise ConflictError("Parent request has already completed") request_model.has_parent = True else: request_model.has_parent = False if self.current_user: request_model.requester = self.current_user.username # Ok, ready to save request_model.save() request_id = str(request_model.id) # Set up the wait event BEFORE yielding the processRequest call blocking = self.get_argument("blocking", default="").lower() == "true" if blocking: brew_view.request_map[request_id] = Event() with thrift_context() as client: try: yield client.processRequest(request_id) except bg_utils.bg_thrift.InvalidRequest as ex: request_model.delete() raise ModelValidationError(ex.message) except bg_utils.bg_thrift.PublishException as ex: request_model.delete() raise RequestPublishException(ex.message) except Exception: if request_model.id: request_model.delete() raise # Query for request from body id req = Request.objects.get(id=request_id) # Now attempt to add the instance status as a header. # The Request is already created at this point so it's a best-effort thing self.set_header("Instance-Status", "UNKNOWN") try: # Since request has system info we can query for a system object system = System.objects.get(name=req.system, version=req.system_version) # Loop through all instances in the system until we find the instance that # matches the request instance for instance in system.instances: if instance.name == req.instance_name: self.set_header("Instance-Status", instance.status) # The Request is already created at this point so adding the Instance status # header is a best-effort thing except Exception as ex: self.logger.exception( "Unable to get Instance status for Request %s: %s", request_id, ex) self.request.event_extras = {"request": req} # Metrics request_created(request_model) if blocking: # Publish metrics and event here here so they aren't skewed # See https://github.com/beer-garden/beer-garden/issues/190 self.request.publish_metrics = False http_api_latency_total.labels( method=self.request.method.upper(), route=self.prometheus_endpoint, status=self.get_status(), ).observe(request_latency(self.request.created_time)) self.request.publish_event = False brew_view.event_publishers.publish_event( self.request.event, **self.request.event_extras) try: timeout = self.get_argument("timeout", default=None) delta = timedelta(seconds=int(timeout)) if timeout else None event = brew_view.request_map.get(request_id) yield event.wait(delta) request_model.reload() except TimeoutError: raise TimeoutExceededError("Timeout exceeded for request %s" % request_id) finally: brew_view.request_map.pop(request_id, None) self.set_status(201) self.write( self.parser.serialize_request(request_model, to_string=False))