def __init__(self, registry, clients, plugin_manager, request_validator): self.logger = logging.getLogger(__name__) self.registry = registry self.clients = clients self.plugin_manager = plugin_manager self.request_validator = request_validator self.parser = SchemaParser()
def test_patch_add_role(self, http_client, base_url, mongo_principal, mongo_role): mongo_role.save() mongo_principal.save() new_role = Role(name="new_role", description="Some desc", roles=[], permissions=["bg-all"]) new_role.save() body = PatchOperation(operation="add", path="/roles", value="new_role") url = base_url + "/api/v1/users/" + str(mongo_principal.id) request = HTTPRequest( url, method="PATCH", headers={"content-type": "application/json"}, body=SchemaParser.serialize_patch(body), ) response = yield http_client.fetch(request, raise_error=False) assert response.code == 200 updated = SchemaParser.parse_principal(response.body.decode("utf-8"), from_string=True) assert len(updated.roles) == 2
def test_parsed_start(self, model, assertion, data): assertion( SchemaParser.parse(SchemaParser.serialize(data, to_string=False), model, from_string=False), data, )
def test_patch_role_description( self, http_client, base_url, mongo_role, operation, value, expected_value, succeed, ): mongo_role.save() body = PatchOperation(operation=operation, path="/description", value=value) request = HTTPRequest( base_url + "/api/v1/roles/" + str(mongo_role.id), method="PATCH", headers={"content-type": "application/json"}, body=SchemaParser.serialize_patch(body), ) response = yield http_client.fetch(request, raise_error=False) if succeed: assert response.code == 200 updated = SchemaParser.parse_role(response.body.decode("utf-8"), from_string=True) assert updated.description == expected_value else: assert response.code >= 400
def test_success(self, plugin, request_args): # Need to reset the real parser plugin.parser = SchemaParser() assert_request_equal( plugin._pre_process(json.dumps(request_args)), SchemaParser.parse_request(request_args), )
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_patch_serialized_start(self, patch_dict_no_envelop): """Patches are always parsed into a list, so they need a tweak to test""" serialized = SchemaParser.serialize( SchemaParser.parse_patch(patch_dict_no_envelop, from_string=False), to_string=False, ) assert len(serialized) == 1 assert serialized[0] == patch_dict_no_envelop
def test_patch_model_start(self, bg_patch): """Patches are always parsed into a list, so they need a tweak to test""" parsed = SchemaParser.parse( SchemaParser.serialize(bg_patch, to_string=False), brewtils.models.PatchOperation, from_string=False, ) assert len(parsed) == 1 assert_patch_equal(parsed[0], bg_patch)
def test_serialize_excludes(bg_system, system_dict, keys, excludes): for key in keys: system_dict.pop(key) parser = SchemaParser() actual = parser.serialize_system(bg_system, to_string=False, include_commands=False, exclude=excludes) assert actual == system_dict
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()
def test_patch_commands( self, http_client, base_url, mongo_system, system_id, bg_command, field, value, dev, succeed, ): if dev: mongo_system.version += ".dev" mongo_system.deep_save() # Make changes to the new command if field: if field == "parameters": value = [value] setattr(bg_command, field, value) # Also delete the id, otherwise mongo gets really confused delattr(bg_command, "id") body = PatchOperation( operation="replace", path="/commands", value=SchemaParser.serialize_command( [bg_command], to_string=False, many=True ), ) request = HTTPRequest( base_url + "/api/v1/systems/" + system_id, method="PATCH", headers={"content-type": "application/json"}, body=SchemaParser.serialize_patch(body), ) response = yield http_client.fetch(request, raise_error=False) if succeed: assert response.code == 200 updated = SchemaParser.parse_system( response.body.decode("utf-8"), from_string=True ) assert_command_equal(bg_command, updated.commands[0]) else: assert response.code == 400
def put(request: Request, headers: dict = None, **kwargs) -> None: """Put a Request on a queue If a routing_key is specified in the kwargs, it will be used. If not, system and instance info on the Request will be used, along with the ``is_admin`` kwarg. If the Request has an ID it will be added to the headers as 'request_id'. Args: request: The Request to publish headers: Headers to use when publishing **kwargs: is_admin: Will be passed to get_routing_key Other arguments will be passed to the client publish method Returns: None """ kwargs["headers"] = headers or {} if request.id: kwargs["headers"]["request_id"] = request.id if "routing_key" not in kwargs: kwargs["routing_key"] = get_routing_key( request.namespace, request.system, request.system_version, request.instance_name, is_admin=kwargs.get("is_admin", False), ) clients["pika"].publish(SchemaParser.serialize_request(request), **kwargs)
def publish(cls, event): if event.name in WEBSOCKET_EVENT_TYPE_BLOCKLIST: return if len(cls.listeners) > 0: message = SchemaParser.serialize(event, to_string=True) for listener in cls.listeners: if _auth_enabled(): user = listener.get_current_user() if user is None: listener.request_authorization("Valid access token required") continue if not _user_can_receive_messages_for_event(user, event): logger.debug( "Skipping websocket publish of event %s to user %s due to " "lack of access", event.name, user.username, ) continue listener.write_message(message)
def publish_event(self, *args, **kwargs): """Publish a new event Args: *args: If a positional argument is given it's assumed to be an Event and will be used **kwargs: Will be used to construct a new Event to publish if no Event is given in the positional arguments Keyword Args: _publishers (Optional[List[str]]): List of publisher names. If given the Event will only be published to the specified publishers. Otherwise all publishers known to Beergarden will be used. Returns: bool: True if the publish was successful """ publishers = kwargs.pop("_publishers", None) event = args[0] if args else Event(**kwargs) return self.client.post_event(SchemaParser.serialize_event(event), publishers=publishers)
def post_bg_request(base_url, bg_request): return HTTPRequest( base_url + "/api/v1/requests/", method="POST", headers=RestClient.JSON_HEADERS, body=SchemaParser.serialize_request(bg_request, to_string=True), )
async def post(self): """ --- summary: Forward a request from a parent or child BG instance description: | When a Beer Garden needs to forward a request, this API will support routing to all CRUD actions exposed by the entry points. parameters: - name: forward in: body required: true description: The Forward Object schema: $ref: '#/definitions/Forward' responses: 200: description: Forward Request Accepted 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Forward """ operation = SchemaParser.parse_operation(self.request.decoded_body, from_string=True) response = await self.client(operation) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
def test_patch_many(self, patch_many_dict, bg_patch, bg_patch2, kwargs): """Parametrize for the 'many' kwarg because the parser should ignore it""" patches = SchemaParser.parse_patch(patch_many_dict, **kwargs) sorted_patches = sorted(patches, key=lambda x: x.operation) for index, patch in enumerate([bg_patch, bg_patch2]): assert_patch_equal(patch, sorted_patches[index])
def __init__(self, bg_host=None, bg_port=None, ssl_enabled=False, api_version=None, ca_cert=None, client_cert=None, parser=None, logger=None, url_prefix=None, ca_verify=True, **kwargs): bg_host = bg_host or kwargs.get('host') bg_port = bg_port or kwargs.get('port') self.logger = logger or logging.getLogger(__name__) self.parser = parser or SchemaParser() self.client = RestClient(bg_host=bg_host, bg_port=bg_port, ssl_enabled=ssl_enabled, api_version=api_version, ca_cert=ca_cert, client_cert=client_cert, url_prefix=url_prefix, ca_verify=ca_verify, **kwargs)
def update_request(self, request_id, status=None, output=None, error_class=None): """Update a Request Args: request_id (str): The Request ID status (Optional[str]): New Request status output (Optional[str]): New Request output error_class (Optional[str]): New Request error class Returns: Response: The updated response """ operations = [] if status: operations.append(PatchOperation("replace", "/status", status)) if output: operations.append(PatchOperation("replace", "/output", output)) if error_class: operations.append( PatchOperation("replace", "/error_class", error_class)) return self.client.patch_request( request_id, SchemaParser.serialize_patch(operations, many=True))
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)
def to_brewtils( obj: Union[MongoModel, List[MongoModel], QuerySet] ) -> Union[ModelItem, List[ModelItem], None]: """Convert an item from its Mongo model to its Brewtils one Args: obj: The Mongo model item or QuerySet Returns: The Brewtils model item """ if obj is None: return obj if isinstance(obj, (list, QuerySet)): if len(obj) == 0: return [] model_class = obj[0].brewtils_model many = True else: model_class = obj.brewtils_model many = False if getattr(obj, "pre_serialize", None): obj.pre_serialize() serialized = MongoParser.serialize(obj, to_string=True) parsed = SchemaParser.parse(serialized, model_class, from_string=True, many=many) return parsed
def on_message(self, headers: Dict[str, Any], message: Any) -> None: """Set a property when a MESSAGE frame is received by the STOMP connection. If the needed key is in the headers dict and has the correct value, signal that we've been successful. Args: headers: a dictionary containing all headers sent by the server as key/value pairs. message: the frame's payload - the message body. """ try: if "model_class" in headers: if headers["model_class"] == "Operation": parsed = SchemaParser.parse_operation(message, from_string=True) if parsed.model and parsed.model.name: if parsed.model.name.startswith("REQUEST"): self.create_event_captured = True else: print("ERROR: no 'REQUEST' in parsed model") else: print("ERROR: no parsed model found") elif headers["model_class"] == "error_message": print(f"ERROR: Message returned: {message!r}") else: print( f"ERROR: 'model_class' not in headers, message={message}") except Exception: print(f"ERROR: unable to parse, message={message}")
def websocket_publish(item): """Will serialize an event and publish it to all event websocket endpoints""" try: beer_garden.api.http.io_loop.add_callback( EventSocket.publish, SchemaParser.serialize(item, to_string=True)) except Exception as ex: logger.exception(f"Error publishing event to websocket: {ex}")
def clear_queue(self, queue_name: str) -> None: """Remove all messages in a queue. Args: queue_name: The queue name """ self.logger.info("Clearing Queue: %s", queue_name) queue_dictionary = self._client.get_queue(self._virtual_host, queue_name) number_of_messages = queue_dictionary.get("messages_ready", 0) while number_of_messages > 0: self.logger.debug("Getting the Next Message") messages = self._client.get_messages(self._virtual_host, queue_name, count=1, requeue=False) if messages and len(messages) > 0: message = messages[0] try: request = SchemaParser.parse_request(message["payload"], from_string=True) beer_garden.requests.cancel_request(request.id) except Exception as ex: self.logger.exception(f"Error canceling message: {ex}") else: self.logger.debug( "Race condition: The while loop thought there were more messages " "to ingest but no more messages could be received.") break number_of_messages -= 1
def test_non_strict_failure(self, system_dict): system_dict["name"] = 1234 value = SchemaParser.parse_system(system_dict, from_string=False, strict=False) assert value.get("name") is None assert value["version"] == system_dict["version"]
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 post(self): """ --- summary: Create a new File parameters: - name: body in: body required: true description: The data responses: 201: description: A new File is created schema: $ref: '#/definitions/FileStatus' 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Files """ db_file = RawFile() db_file.file.put(io.BytesIO(self.request.body)) db_file.save() resolvable = Resolvable(id=str(db_file.id), type="bytes", storage="gridfs") response = SchemaParser.serialize(resolvable, to_string=True) self.set_header("Content-Type", "application/json; charset=UTF-8") self.write(response)
class EventPublisherAPI(BaseHandler): parser = SchemaParser() @authenticated(permissions=[Permissions.CREATE]) def post(self): """ --- summary: Publish a new event parameters: - name: bg-namespace in: header required: false description: Namespace to use type: string - name: event in: body description: The the Event object schema: $ref: '#/definitions/Event' responses: 204: description: An Event has been published 400: $ref: '#/definitions/400Error' 50x: $ref: '#/definitions/50xError' tags: - Event """ publish(SchemaParser.parse_event(self.request.decoded_body, from_string=True)) self.set_status(204)
def update_instance(self, instance_id, **kwargs): """Update an Instance status Args: instance_id (str): The Instance ID Keyword Args: new_status (str): The new status metadata (dict): Will be added to existing instance metadata Returns: Instance: The updated Instance """ operations = [] new_status = kwargs.pop("new_status", None) metadata = kwargs.pop("metadata", {}) if new_status: operations.append(PatchOperation("replace", "/status", new_status)) if metadata: operations.append(PatchOperation("update", "/metadata", metadata)) return self.client.patch_instance( instance_id, SchemaParser.serialize_patch(operations, many=True))
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)