async def patch(self): """ --- summary: Initiate administrative actions description: | The body of the request needs to contain a set of instructions detailing the operations to perform. Currently the supported operations are `rescan`: ```JSON [ { "operation": "rescan" } ] ``` * Will remove from the registry and database any currently stopped plugins who's directory has been removed. * Will add and start any new plugin directories. And reloading the plugin logging configuration: ```JSON [ { "operation": "reload", "path": "/config/logging/plugin" } ] ``` parameters: - name: patch in: body required: true description: Instructions for operations schema: $ref: '#/definitions/Patch' responses: 204: description: Operation successfully initiated 50x: $ref: '#/definitions/50xError' tags: - Admin """ self.verify_user_permission_for_object(GARDEN_UPDATE, local_garden()) operations = SchemaParser.parse_patch( self.request.decoded_body, many=True, from_string=True ) for op in operations: if op.operation == "rescan": await self.client(Operation(operation_type="RUNNER_RESCAN")) elif op.operation == "reload": if op.path == "/config/logging/plugin": await self.client(Operation(operation_type="PLUGIN_LOG_RELOAD")) else: raise ModelValidationError(f"Unsupported path '{op.path}'") else: raise ModelValidationError(f"Unsupported operation '{op.operation}'") self.set_status(204)
async def _generate_get_response(self, instance_id, start_line, end_line): wait_event = Event() response = await self.client( Operation( operation_type="INSTANCE_LOGS", args=[instance_id], kwargs={ "wait_event": wait_event, "start_line": start_line, "end_line": end_line, }, ), serialize_kwargs={"to_string": False}, ) wait_timeout = float(self.get_argument("timeout", default="15")) if wait_timeout < 0: wait_timeout = None if not await event_wait(wait_event, wait_timeout): raise TimeoutExceededError("Timeout exceeded") response = await self.client( Operation(operation_type="REQUEST_READ", args=[response["id"]]), serialize_kwargs={"to_string": False}, ) if response["status"] == "ERROR": raise RequestProcessingError(response["output"])
def forward_file(operation: Operation) -> None: """Send file data before forwarding an operation with a file parameter.""" # HEADS UP - THIS IS PROBABLY BROKEN # import here bypasses circular dependency import beer_garden.router as router for file_id in _find_chunk_params(operation.model.parameters): file = check_chunks(file_id) args = [file.file_name, file.file_size, file.chunk_size] # Make sure we get all of the other data kwargs = _safe_build_object( dict, file, ignore=[ "file_name", "file_size", "chunk_size", ], upsert=True, ) file_op = Operation( operation_type="FILE_CREATE", args=args, kwargs=kwargs, target_garden_name=operation.target_garden_name, source_garden_name=operation.source_garden_name, ) # This should put push the file operations before the current one router.forward_processor.put(file_op) for chunk_id in file.chunks.values(): chunk = check_chunk(chunk_id) c_args = [chunk.file_id, chunk.offset, chunk.data] c_kwargs = _safe_build_object(dict, chunk, ignore=["file_id", "offset", "data"], upsert=True) chunk_op = Operation( operation_type="FILE_CHUNK", args=c_args, kwargs=c_kwargs, target_garden_name=operation.target_garden_name, source_garden_name=operation.source_garden_name, ) # This should put push the file operations before the current one router.forward_processor.put(chunk_op)
async def delete(self, runner_id): """ --- summary: Delete a runner parameters: - name: runner_id in: path required: true description: The ID of the Runner type: string responses: 200: description: List of runner states schema: $ref: '#/definitions/Runner' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Runners """ response = await self.client( Operation( operation_type="RUNNER_DELETE", kwargs={ "runner_id": runner_id, "remove": True }, )) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def get(self, garden_name): """ --- summary: Retrieve a specific Garden parameters: - name: garden_name in: path required: true description: Read specific Garden Information type: string responses: 200: description: Garden with the given garden_name schema: $ref: '#/definitions/Garden' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Garden """ response = await self.client( Operation(operation_type="GARDEN_READ", args=[garden_name]) ) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def delete(self, instance_id): """ --- summary: Delete a specific Instance parameters: - name: instance_id in: path required: true description: The ID of the Instance type: string responses: 204: description: Instance has been successfully deleted 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Instances """ _ = self.get_or_raise(System, SYSTEM_UPDATE, instances__id=instance_id) await self.client( Operation(operation_type="INSTANCE_DELETE", args=[instance_id])) self.set_status(204)
async def get(self, instance_id): """ --- summary: Retrieve queue information for instance parameters: - name: instance_id in: path required: true description: The instance ID to pull queues for type: string responses: 200: description: List of queue information objects for this instance schema: type: array items: $ref: '#/definitions/Queue' 50x: $ref: '#/definitions/50xError' tags: - Queues """ _ = self.get_or_raise(System, QUEUE_READ, instances__id=instance_id) response = await self.client( Operation(operation_type="QUEUE_READ_INSTANCE", args=[instance_id])) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
def test_listen_create_request(self): """Published the Request over HTTP and verifies of STOMP""" stomp_connection = self.create_stomp_connection() request_model = self.create_request("test_listen_create_request") sample_operation_request = Operation( operation_type="REQUEST_CREATE", model=request_model, model_type="Request", ) listener = MessageListener() stomp_connection.set_listener('', listener) stomp_connection.subscribe(destination='Beer_Garden_Events', id='event_listener', ack='auto', headers={ 'subscription-type': 'MULTICAST', 'durable-subscription-name': 'events' }) self.easy_client.forward(sample_operation_request) time.sleep(10) assert listener.create_event_captured if stomp_connection.is_connected(): stomp_connection.disconnect()
async def get(self): """ --- summary: Retrieve all Jobs. responses: 200: description: Successfully retrieved all systems. schema: type: array items: $ref: '#/definitions/Job' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ filter_params = {} for key in self.request.arguments.keys(): if key in JobSchema.get_attribute_names(): filter_params[key] = self.get_query_argument(key) response = await self.client( Operation(operation_type="JOB_READ_ALL", kwargs={"filter_params": filter_params})) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def get(self): """ --- summary: Get a list of all namespaces known to this garden responses: 200: description: List of Namespaces 50x: $ref: '#/definitions/50xError' tags: - Namespace """ permitted_gardens = self.permissioned_queryset(Garden, GARDEN_READ) permitted_requests = self.permissioned_queryset(Request, REQUEST_READ) permitted_systems = self.permissioned_queryset(System, SYSTEM_READ) response = await self.client( Operation( operation_type="NAMESPACE_READ_ALL", kwargs={ "garden_queryset": permitted_gardens, "system_queryset": permitted_systems, "request_queryset": permitted_requests, }, ) ) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def delete(self, job_id): """ --- summary: Delete a specific Job. description: Will remove a specific job. No further executions will occur. parameters: - name: job_id in: path required: true description: The ID of the Job type: string responses: 204: description: Job has been successfully deleted. 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ await self.client(Operation(operation_type="JOB_DELETE", args=[job_id])) self.set_status(204)
async def get(self, job_id): """ --- summary: Retrieve a specific Job parameters: - name: job_id in: path required: true description: The ID of the Job type: string responses: 200: description: Job with the given ID schema: $ref: '#/definitions/Job' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ response = await self.client( Operation(operation_type="JOB_READ", args=[job_id])) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def delete(self, garden_name): """ --- summary: Delete a specific Garden parameters: - name: garden_name in: path required: true description: Garden to use type: string responses: 204: description: Garden has been successfully deleted 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Garden """ garden = self.get_or_raise(Garden, GARDEN_DELETE, name=garden_name) await self.client( Operation(operation_type="GARDEN_DELETE", args=[garden.name])) self.set_status(204)
async def delete(self): """ --- summary: Delete a file parameters: - name: file_id in: query required: true description: The ID of the file type: string responses: 200: description: The file and all of its contents have been removed. schema: $ref: '#/definitions/FileStatus' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Files """ file_id = self.get_argument("file_id", default=None) if file_id is None: raise ValueError("Cannot delete a file without an id.") response = await self.client( Operation(operation_type="FILE_DELETE", args=[file_id])) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def get(self, instance_id): """ --- summary: Retrieve a specific Instance parameters: - name: instance_id in: path required: true description: The ID of the Instance type: string responses: 200: description: Instance with the given ID schema: $ref: '#/definitions/Instance' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Instances """ _ = self.get_or_raise(System, SYSTEM_READ, instances__id=instance_id) response = await self.client( Operation(operation_type="INSTANCE_READ", args=[instance_id])) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def get(self, request_id): """ --- summary: Retrieve a specific Request parameters: - name: request_id in: path required: true description: The ID of the Request type: string responses: 200: description: Request with the given ID schema: $ref: '#/definitions/Request' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Requests """ _ = self.get_or_raise(Request, REQUEST_READ, id=request_id) response = await self.client( Operation(operation_type="REQUEST_READ", args=[request_id]) ) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def get(self, system_id, command_name): """ --- summary: Retrieve a specific Command parameters: - name: system_id in: path required: true description: The ID of the System type: string - name: command_name in: path required: true description: The name of the Command type: string responses: 200: description: Command with the given name schema: $ref: '#/definitions/Command' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Commands """ response = await self.client( Operation(operation_type="COMMAND_READ", args=[system_id, command_name])) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def get(self): """ --- summary: Get the plugin logging configuration deprecated: true parameters: - name: system_name in: query required: false description: UNUSED type: string responses: 200: description: Logging Configuration for system schema: $ref: '#/definitions/LoggingConfig' 50x: $ref: '#/definitions/50xError' tags: - Deprecated """ response = await self.client( Operation(operation_type="PLUGIN_LOG_READ_LEGACY")) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def get(self): """ --- summary: Get plugin logging configuration description: | Will return a Python logging configuration that can be used to configure plugin logging. parameters: - name: local in: query required: false description: Whether to request the local plugin logging configuration type: boolean default: false responses: 200: description: Logging Configuration for system 50x: $ref: '#/definitions/50xError' tags: - Logging """ local = self.get_query_argument("local", None) if local is None: local = False else: local = bool(local.lower() == "true") response = await self.client( Operation(operation_type="PLUGIN_LOG_READ", kwargs={"local": local})) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
def process(body) -> Tuple[str, dict]: """Processes a message body prior to sending We always want to send Operations. So if the given message is an Event we'll wrap it in an Operation. Args: body: the message body to process Returns: Tuple of the serialized message and headers dict """ many = isinstance(body, list) if body.__class__.__name__ == "Event": body = Operation(operation_type="PUBLISH_EVENT", model=body, model_type="Event") model_class = (body[0] if many else body).__class__.__name__ if not isinstance(body, str): body = SchemaParser.serialize(body, to_string=True, many=many) return body, {"model_class": model_class, "many": many}
async def delete(self, queue_name): """ --- summary: Clear a queue by canceling all requests parameters: - name: queue_name in: path required: true description: The name of the queue to clear type: string responses: 204: description: Queue successfully cleared 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Queues """ self.verify_user_permission_for_object(QUEUE_DELETE, local_garden()) await self.client( Operation(operation_type="QUEUE_DELETE", args=[queue_name])) self.set_status(204)
def process_wait(request: Request, timeout: float) -> Request: """Helper to process a request and wait for completion using a threading.Event Args: request: Request to create timeout: Timeout used for wait Returns: The completed request """ # We need a better solution for this. Because the Request library is imported # everywhere it causes issues when importing the router at the top because all of # the functions are not initialized. So we either leave this as is, or move the # requests import to the end of all of the files. import beer_garden.router as router req_complete = threading.Event() # Send the request through the router to allow for commands to work across Gardens created_request = router.route( Operation( operation_type="REQUEST_CREATE", model=request, model_type="Request", kwargs={"wait_event": req_complete}, )) if not req_complete.wait(timeout): raise TimeoutError( "Request did not complete before the specified timeout") return db.query_unique(Request, id=created_request.id)
async def post(self): """ --- summary: Exports a list of Jobs from a list of IDs. description: | Jobs will be scheduled from a provided list to run on the intervals set in their trigger arguments. parameters: - name: ids in: body description: A list of the Jobs IDs whose job definitions should be \ exported. Omitting this parameter or providing an empty map will export \ all jobs. schema: $ref: '#/definitions/JobExport' responses: 201: description: A list of jobs has been exported. schema: type: array items: $ref: '#/definitions/JobImport' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Jobs """ filter_params_dict = {} permitted_objects_filter = self.permitted_objects_filter(Job, JOB_READ) # self.request_body is designed to return a 400 on a completely absent body # but we want to return all jobs if that's the case if len(self.request.body) > 0: decoded_body_as_dict = self.request_body if len(decoded_body_as_dict) > 0: # i.e. it has keys input_schema = JobExportInputSchema() validated_input_data_dict = input_schema.load( decoded_body_as_dict).data filter_params_dict["id__in"] = validated_input_data_dict["ids"] response_objects = await self.client( Operation( operation_type="JOB_READ_ALL", kwargs={ "q_filter": permitted_objects_filter, "filter_params": filter_params_dict, }, ), serialize_kwargs={"return_raw": True}, ) response = SchemaParser.serialize(response_objects, to_string=True, schema_name="JobExportSchema") self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def patch(self): """ --- summary: Partially update a Garden description: | The body of the request needs to contain a set of instructions detailing the updates to apply. Currently the only operations are: * sync ```JSON [ { "operation": "" } ] ``` parameters: - name: garden_name in: path required: true description: Garden to use type: string - name: patch in: body required: true description: Instructions for how to update the Garden schema: $ref: '#/definitions/Patch' responses: 200: description: Execute Patch action against Gardens schema: $ref: '#/definitions/Garden' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Garden """ patch = SchemaParser.parse_patch(self.request.decoded_body, from_string=True) for op in patch: operation = op.operation.lower() if operation == "sync": response = await self.client( Operation( operation_type="GARDEN_SYNC", ) ) else: raise ModelValidationError(f"Unsupported operation '{op.operation}'") self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
def test_publish_create_request(self): """Publish a Request over STOMP and verify it via HTTP.""" stomp_connection = self.create_stomp_connection() request_model = self.create_request("test_publish_create_request") sample_operation_request = Operation( operation_type="REQUEST_CREATE", model=request_model, model_type="Request", target_garden_name="docker", ) listener = MessageListener() stomp_connection.set_listener("", listener) stomp_connection.subscribe( destination="Beer_Garden_Events", id="event_listener", ack="auto", headers={ "subscription-type": "MULTICAST", "durable-subscription-name": "events", }, ) stomp_connection.send( body=SchemaParser.serialize_operation(sample_operation_request, to_string=True), headers={ "model_class": sample_operation_request.__class__.__name__, }, destination="Beer_Garden_Operations", ) time.sleep(10) requests = self.easy_client.find_requests() found_request = False print(len(requests)) for request in requests: print(SchemaParser.serialize_request(request, to_string=True)) if ("generated-by" in request.metadata and request.metadata["generated-by"] == "test_publish_create_request"): found_request = True break assert found_request assert listener.create_event_captured if stomp_connection.is_connected(): stomp_connection.disconnect()
def test_publish_create_request(self): """Published the Request over STOMP and verifies of HTTP""" stomp_connection = self.create_stomp_connection() request_model = self.create_request("test_publish_create_request") sample_operation_request = Operation( operation_type="REQUEST_CREATE", model=request_model, model_type="Request", ) listener = MessageListener() stomp_connection.set_listener('', listener) stomp_connection.subscribe(destination='Beer_Garden_Events', id='event_listener', ack='auto', headers={ 'subscription-type': 'MULTICAST', 'durable-subscription-name': 'events' }) stomp_connection.send( body=SchemaParser.serialize_operation(sample_operation_request, to_string=True), headers={ "model_class": sample_operation_request.__class__.__name__, }, destination="Beer_Garden_Operations", ) time.sleep(10) requests = self.easy_client.find_requests() found_request = False print(len(requests)) for request in requests: print(SchemaParser.serialize_request(request, to_string=True)) if "generated-by" in request.metadata and request.metadata[ "generated-by"] == "test_publish_create_request": found_request = True break assert found_request assert listener.create_event_captured if stomp_connection.is_connected(): stomp_connection.disconnect()
async def patch(self): """ --- summary: Update runners description: | The body of the request needs to contain a set of instructions detailing the updates to apply. Currently the only operations are: * reload ```JSON [ { "operation": "reload", "path": "echo-3.0.0" } ] ``` parameters: - name: patch in: body required: true description: Instructions for how to update the Runner schema: $ref: '#/definitions/Patch' responses: 200: description: Reloaded Runners schema: $ref: '#/definitions/Runner' 400: $ref: '#/definitions/400Error' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Runners """ patch = SchemaParser.parse_patch(self.request.decoded_body, from_string=True) for op in patch: operation = op.operation.lower() if operation == "reload": response = await self.client( Operation(operation_type="RUNNER_RELOAD", kwargs={"path": op.path})) else: raise ModelValidationError( f"Unsupported operation '{op.operation}'") self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
async def get(self): """ --- summary: Retrieve a File or FileChunk parameters: - name: file_id in: query required: true description: The ID of the file type: string - name: chunk in: query required: false description: The chunk number requested type: integer - name: verify in: query required: false description: Flag that will cause verification information to be returned instead of the file data type: boolean responses: 200: description: The requested File or FileChunk data schema: $ref: '#/definitions/FileStatus' 404: $ref: '#/definitions/404Error' 50x: $ref: '#/definitions/50xError' tags: - Files """ file_id = self.get_argument("file_id", default=None) chunk = self.get_argument("chunk", default=None) verify = self.get_argument("verify", default="").lower() == "true" if file_id is None: raise ValueError("Cannot fetch a file or chunk without a file ID.") response = await self.client( Operation( operation_type="FILE_FETCH", args=[file_id], kwargs={ "chunk": chunk, "verify": verify }, )) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
def process(self, event: Event): # TODO - This shouldn't be set here event.garden = conf.get("garden.name") if not event_blocklisted(event): try: operation = Operation( operation_type="PUBLISH_EVENT", model=event, model_type="Event" ) self._ez_client.forward(operation) except RequestException as ex: self.logger.error(f"Error while publishing event to parent: {ex}") self._connected = False self._reconnect()
async def delete(self): """ --- summary: Cancel and clear all requests in all queues responses: 204: description: All queues successfully cleared 50x: $ref: '#/definitions/50xError' tags: - Queues """ await self.client(Operation(operation_type="QUEUE_DELETE_ALL")) self.set_status(204)