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 client(self, session_mock, url_prefix): client = RestClient( bg_host="host", bg_port=80, api_version=1, url_prefix=url_prefix ) client.session = session_mock return client
def ssl_client(self, session_mock, url_prefix): client = RestClient(bg_host='host', bg_port=80, api_version=1, url_prefix=url_prefix, ssl_enabled=True) client.session = session_mock return client
def _load_from_url(url): # type: (str) -> Union[str, dict] """Load a definition from a URL""" from brewtils.rest.client import RestClient # Use a RestClient's session since TLS will (hopefully) be configured response = RestClient(bg_host="").session.get(url) if "application/json" in response.headers.get("content-type", "").lower(): return response.json() return response.text
def client(self, session_mock, url_prefix): client = RestClient( bg_host="host", bg_port=80, api_version=1, url_prefix=url_prefix, username="******", password="******", ssl_enabled=False, ) client.session = session_mock return client
def test_session_no_ca_verify(self, monkeypatch): urllib_mock = Mock() monkeypatch.setattr(brewtils.rest.client, "urllib3", urllib_mock) client = RestClient(bg_host="host", bg_port=80, api_version=1, ca_verify=False) assert client.session.verify is False assert urllib_mock.disable_warnings.called is True
def __init__(self): self._client = RestClient( brew_view.config.web.public_fqdn, brew_view.config.web.port, ssl_enabled=brew_view.config.web.ssl.enabled, url_prefix=brew_view.config.web.url_prefix, )
def test_args_from_config(self, monkeypatch): brewtils.plugin.CONFIG.bg_host = "localhost" brewtils.plugin.CONFIG.bg_port = 3000 client = RestClient() assert client.bg_host == "localhost" assert client.bg_port == 3000
def test_proxy_config_ssl(self, monkeypatch): proxy = "test:1234" brewtils.plugin.CONFIG.bg_host = "localhost" brewtils.plugin.CONFIG.bg_port = 3000 brewtils.plugin.CONFIG.ssl_enabled = True brewtils.plugin.CONFIG.proxy = proxy client = RestClient() assert client.session.proxies["https"] == proxy
def test_old_positional_args(self, client, url_prefix): with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") test_client = RestClient("host", 80, api_version=1, url_prefix=url_prefix, ssl_enabled=False) assert test_client.version_url == client.version_url assert len(w) == 2
def test_client_cert_without_username_password(self, monkeypatch): get_tokens_mock = Mock() monkeypatch.setattr(brewtils.rest.client.RestClient, "get_tokens", get_tokens_mock) client = RestClient( bg_host="host", bg_port=443, api_version=1, ssl_enabled=True, client_cert="/path/to/cert", ) session_get_response = Mock() session_get_response.status_code = 401 session_get_mock = Mock(return_value=session_get_response) monkeypatch.setattr(client.session, "get", session_get_mock) client.get_garden("somegarden") assert get_tokens_mock.called is True
class EasyClient(object): """Client for communicating with beer-garden This class provides nice wrappers around the functionality provided by a :py:class:`brewtils.rest.client.RestClient` :param bg_host: beer-garden REST API hostname. :param bg_port: beer-garden REST API port. :param ssl_enabled: Flag indicating whether to use HTTPS when communicating with beer-garden. :param api_version: The beer-garden REST API version. Will default to the latest version. :param ca_cert: beer-garden REST API server CA certificate. :param client_cert: The client certificate to use when making requests. :param parser: The parser to use. If None will default to an instance of SchemaParser. :param logger: The logger to use. If None one will be created. :param url_prefix: beer-garden REST API URL Prefix. :param ca_verify: Flag indicating whether to verify server certificate when making a request. :param username: Username for Beergarden authentication :param password: Password for Beergarden authentication :param access_token: Access token for Beergarden authentication :param refresh_token: Refresh token for Beergarden authentication :param client_timeout: Max time to will wait for server response """ 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 can_connect(self, **kwargs): """Determine if the Beergarden server is responding. Kwargs: Arguments passed to the underlying Requests method Returns: A bool indicating if the connection attempt was successful. Will return False only if a ConnectionError is raised during the attempt. Any other exception will be re-raised. Raises: requests.exceptions.RequestException: The connection attempt resulted in an exception that indicates something other than a basic connection error. For example, an error with certificate verification. """ try: self.client.get_config(**kwargs) except requests.exceptions.ConnectionError as ex: if type(ex) == requests.exceptions.ConnectionError: return False raise return True def get_version(self, **kwargs): response = self.client.get_version(**kwargs) if response.ok: return response else: self._handle_response_failure(response, default_exc=FetchError) def find_unique_system(self, **kwargs): """Find a unique system using keyword arguments as search parameters :param kwargs: Search parameters :return: One system instance """ if 'id' in kwargs: return self._find_system_by_id(kwargs.pop('id'), **kwargs) else: systems = self.find_systems(**kwargs) if not systems: return None if len(systems) > 1: raise FetchError( "More than one system found that specifies the given constraints" ) return systems[0] def find_systems(self, **kwargs): """Find systems using keyword arguments as search parameters :param kwargs: Search parameters :return: A list of system instances satisfying the given search parameters """ response = self.client.get_systems(**kwargs) if response.ok: return self.parser.parse_system(response.json(), many=True) else: self._handle_response_failure(response, default_exc=FetchError) def _find_system_by_id(self, system_id, **kwargs): """Finds a system by id, convert JSON to a system object and return it""" response = self.client.get_system(system_id, **kwargs) if response.ok: return self.parser.parse_system(response.json()) else: self._handle_response_failure(response, default_exc=FetchError, raise_404=False) def create_system(self, system): """Create a new system by POSTing :param system: The system to create :return: The system creation response """ json_system = self.parser.serialize_system(system) response = self.client.post_systems(json_system) if response.ok: return self.parser.parse_system(response.json()) else: self._handle_response_failure(response, default_exc=SaveError) def update_system(self, system_id, new_commands=None, **kwargs): """Update a system by PATCHing :param system_id: The ID of the system to update :param new_commands: The new commands :Keyword Arguments: * *metadata* (``dict``) The updated metadata for the system * *description* (``str``) The updated description for the system * *display_name* (``str``) The updated display_name for the system * *icon_name* (``str``) The updated icon_name for the system :return: The response """ operations = [] metadata = kwargs.pop("metadata", {}) if new_commands: operations.append( PatchOperation( 'replace', '/commands', self.parser.serialize_command(new_commands, to_string=False, many=True))) if metadata: operations.append(PatchOperation('update', '/metadata', metadata)) for attr, value in kwargs.items(): if value is not None: operations.append( PatchOperation('replace', '/%s' % attr, value)) response = self.client.patch_system( system_id, self.parser.serialize_patch(operations, many=True)) if response.ok: return self.parser.parse_system(response.json()) else: self._handle_response_failure(response, default_exc=SaveError) def remove_system(self, **kwargs): """Remove a specific system by DELETEing, using keyword arguments as search parameters :param kwargs: Search parameters :return: The response """ system = self.find_unique_system(**kwargs) if system is None: raise FetchError( "Could not find system matching the given search parameters") return self._remove_system_by_id(system.id) def _remove_system_by_id(self, system_id): if system_id is None: raise DeleteError("Cannot delete a system without an id") response = self.client.delete_system(system_id) if response.ok: return True else: self._handle_response_failure(response, default_exc=DeleteError) def initialize_instance(self, instance_id): """Start an instance by PATCHing :param instance_id: The ID of the instance to start :return: The start response """ response = self.client.patch_instance( instance_id, self.parser.serialize_patch(PatchOperation('initialize'))) if response.ok: return self.parser.parse_instance(response.json()) else: self._handle_response_failure(response, default_exc=SaveError) def get_instance_status(self, instance_id): """Get an instance's status Args: instance_id: The Id Returns: The status """ response = self.client.get_instance(instance_id) if response.ok: return self.parser.parse_instance(response.json()) else: self._handle_response_failure(response, default_exc=FetchError) def update_instance_status(self, instance_id, new_status): """Update an instance by PATCHing :param instance_id: The ID of the instance to start :param new_status: The updated status :return: The start response """ payload = PatchOperation('replace', '/status', new_status) response = self.client.patch_instance( instance_id, self.parser.serialize_patch(payload)) if response.ok: return self.parser.parse_instance(response.json()) else: self._handle_response_failure(response, default_exc=SaveError) def instance_heartbeat(self, instance_id): """Send heartbeat for health and status :param instance_id: The ID of the instance :return: The response """ payload = PatchOperation('heartbeat') response = self.client.patch_instance( instance_id, self.parser.serialize_patch(payload)) if response.ok: return True else: self._handle_response_failure(response, default_exc=SaveError) def remove_instance(self, instance_id): """Remove an instance :param instance_id: The ID of the instance :return: The response """ if instance_id is None: raise DeleteError("Cannot delete an instance without an id") response = self.client.delete_instance(instance_id) if response.ok: return True else: self._handle_response_failure(response, default_exc=DeleteError) def find_unique_request(self, **kwargs): """Find a unique request using keyword arguments as search parameters .. note:: If 'id' is present in kwargs then all other parameters will be ignored. :param kwargs: Search parameters :return: One request instance """ if 'id' in kwargs: return self._find_request_by_id(kwargs.pop('id')) else: requests = self.find_requests(**kwargs) if not requests: return None if len(requests) > 1: raise FetchError("More than one request found that specifies " "the given constraints") return requests[0] def find_requests(self, **kwargs): """Find requests using keyword arguments as search parameters :param kwargs: Search parameters :return: A list of request instances satisfying the given search parameters """ response = self.client.get_requests(**kwargs) if response.ok: return self.parser.parse_request(response.json(), many=True) else: self._handle_response_failure(response, default_exc=FetchError) def _find_request_by_id(self, request_id): """Finds a request by id, convert JSON to a request object and return it""" response = self.client.get_request(request_id) if response.ok: return self.parser.parse_request(response.json()) else: self._handle_response_failure(response, default_exc=FetchError, raise_404=False) def create_request(self, request, **kwargs): """Create a new request by POSTing Args: request: New request definition kwargs: Extra request parameters Keyword Args: blocking: Wait for request to complete timeout: Maximum seconds to wait Returns: Response to the request """ json_request = self.parser.serialize_request(request) response = self.client.post_requests(json_request, **kwargs) if response.ok: return self.parser.parse_request(response.json()) else: self._handle_response_failure(response, default_exc=SaveError) def update_request(self, request_id, status=None, output=None, error_class=None): """Set various fields on a request by PATCHing :param request_id: The ID of the request to update :param status: The new status :param output: The new output :param error_class: The new error class :return: The 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)) response = self.client.patch_request( request_id, self.parser.serialize_patch(operations, many=True)) if response.ok: return self.parser.parse_request(response.json()) else: self._handle_response_failure(response, default_exc=SaveError) def get_logging_config(self, system_name): """Get the logging configuration for a particular system. :param system_name: Name of system :return: LoggingConfig object """ response = self.client.get_logging_config(system_name=system_name) if response.ok: return self.parser.parse_logging_config(response.json()) else: self._handle_response_failure(response, default_exc=RestConnectionError) def publish_event(self, *args, **kwargs): """Publish a new event by POSTing :param args: The Event to create :param _publishers: Optional list of specific publishers. If None all publishers will be used. :param kwargs: If no Event is given in the *args, on will be constructed from the kwargs :return: The response """ publishers = kwargs.pop('_publishers', None) json_event = self.parser.serialize_event(args[0] if args else Event( **kwargs)) response = self.client.post_event(json_event, publishers=publishers) if response.ok: return True else: self._handle_response_failure(response) def get_queues(self): """Retrieve all queue information :return: The response """ response = self.client.get_queues() if response.ok: return self.parser.parse_queue(response.json(), many=True) else: self._handle_response_failure(response) def clear_queue(self, queue_name): """Cancel and clear all messages from a queue :return: The response """ response = self.client.delete_queue(queue_name) if response.ok: return True else: self._handle_response_failure(response) def clear_all_queues(self): """Cancel and clear all messages from all queues :return: The response """ response = self.client.delete_queues() if response.ok: return True else: self._handle_response_failure(response) def find_jobs(self, **kwargs): """Find jobs using keyword arguments as search parameters Args: **kwargs: Search parameters Returns: List of jobs. """ response = self.client.get_jobs(**kwargs) if response.ok: return self.parser.parse_job(response.json(), many=True) else: self._handle_response_failure(response, default_exc=FetchError) def create_job(self, job): """Create a new job by POSTing Args: job: The job to create Returns: The job creation response. """ json_job = self.parser.serialize_job(job) response = self.client.post_jobs(json_job) if response.ok: return self.parser.parse_job(response.json()) else: self._handle_response_failure(response, default_exc=SaveError) def remove_job(self, job_id): """Remove a job by ID. Args: job_id: The ID of the job to remove. Returns: True if successful, raises an error otherwise. """ response = self.client.delete_job(job_id) if response.ok: return True else: self._handle_response_failure(response, default_exc=DeleteError) def pause_job(self, job_id): """Pause a Job by ID. Args: job_id: The ID of the job to pause. Returns: A copy of the job. """ self._patch_job(job_id, [PatchOperation('update', '/status', 'PAUSED')]) def _patch_job(self, job_id, operations): response = self.client.patch_job( job_id, self.parser.serialize_patch(operations, many=True)) if response.ok: return self.parser.parse_job(response.json()) else: self._handle_response_failure(response, default_exc=SaveError) def resume_job(self, job_id): """Resume a job by ID. Args: job_id: The ID of the job to resume. Returns: A copy of the job. """ self._patch_job(job_id, [PatchOperation('update', '/status', 'RUNNING')]) def who_am_i(self): """Find the user represented by the current set of credentials :return: The current user """ return self.get_user(self.client.username or 'anonymous') def get_user(self, user_identifier): """Find a specific user using username or ID :param user_identifier: ID or username of User :return: A User """ response = self.client.get_user(user_identifier) if response.ok: return self.parser.parse_principal(response.json()) else: self._handle_response_failure(response, default_exc=FetchError) @staticmethod def _handle_response_failure(response, default_exc=RestError, raise_404=True): if response.status_code == 404: if raise_404: raise NotFoundError(response.json()) else: return None elif response.status_code == 408: raise WaitExceededError(response.json()) elif response.status_code == 409: raise ConflictError(response.json()) elif 400 <= response.status_code < 500: raise ValidationError(response.json()) elif response.status_code == 503: raise RestConnectionError(response.json()) else: raise default_exc(response.json())
def test_session_no_ca_cert(self): client = RestClient(bg_host='host', bg_port=80, api_version=1) assert client.session.verify is True
def test_session_ca_cert(self): client = RestClient(bg_host='host', bg_port=80, api_version=1, ca_cert='/path/to/ca/cert') assert client.session.verify == '/path/to/ca/cert'
class EasyClient(object): """Client for simplified communication with Beergarden This class is intended to be a middle ground between the RestClient and SystemClient. It provides a 'cleaner' interface to some common Beergarden operations than is exposed by the lower-level RestClient. On the other hand, the SystemClient is much better for generating Beergarden Requests. Keyword Args: bg_host (str): Beergarden hostname bg_port (int): Beergarden port ssl_enabled (Optional[bool]): Whether to use SSL (HTTP vs HTTPS) api_version (Optional[int]): The REST API version ca_cert (Optional[str]): Path to CA certificate file client_cert (Optional[str]): Path to client certificate file parser (Optional[SchemaParser]): Parser to use logger (Optional[Logger]): Logger to use url_prefix (Optional[str]): Beergarden REST API prefix ca_verify (Optional[bool]): Whether to verify the server cert hostname username (Optional[str]): Username for authentication password (Optional[str]): Password for authentication access_token (Optional[str]): Access token for authentication refresh_token (Optional[str]): Refresh token for authentication client_timeout (Optional[float]): Max time to wait for a server response """ 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 can_connect(self, **kwargs): """Determine if the Beergarden server is responding. Kwargs: Arguments passed to the underlying Requests method Returns: A bool indicating if the connection attempt was successful. Will return False only if a ConnectionError is raised during the attempt. Any other exception will be re-raised. Raises: requests.exceptions.RequestException: The connection attempt resulted in an exception that indicates something other than a basic connection error. For example, an error with certificate verification. """ try: self.client.get_config(**kwargs) except requests.exceptions.ConnectionError as ex: if type(ex) == requests.exceptions.ConnectionError: return False raise return True @wrap_response(default_exc=FetchError) def get_version(self, **kwargs): """Get Bartender, Brew-view, and API version information Args: **kwargs: Extra parameters Returns: dict: Response object with version information in the body """ return self.client.get_version(**kwargs) @wrap_response(parse_method="parse_logging_config", default_exc=RestConnectionError) def get_logging_config(self, system_name): """Get logging configuration for a System Args: system_name (str): The name of the System Returns: LoggingConfig: The configuration object """ return self.client.get_logging_config(system_name=system_name) def find_unique_system(self, **kwargs): """Find a unique system .. note:: If 'id' is a given keyword argument then all other parameters will be ignored. Args: **kwargs: Search parameters Returns: System, None: The System if found, None otherwise Raises: FetchError: More than one matching System was found """ if "id" in kwargs: return self._find_system_by_id(kwargs.pop("id"), **kwargs) else: systems = self.find_systems(**kwargs) if not systems: return None if len(systems) > 1: raise FetchError("More than one matching System found") return systems[0] @wrap_response(parse_method="parse_system", parse_many=True, default_exc=FetchError) def find_systems(self, **kwargs): """Find Systems using keyword arguments as search parameters Args: **kwargs: Search parameters Returns: List[System]: List of Systems matching the search parameters """ return self.client.get_systems(**kwargs) @wrap_response(parse_method="parse_system", parse_many=False, default_exc=SaveError) def create_system(self, system): """Create a new System Args: system (System): The System to create Returns: System: The newly-created system """ return self.client.post_systems(self.parser.serialize_system(system)) @wrap_response(parse_method="parse_system", parse_many=False, default_exc=SaveError) def update_system(self, system_id, new_commands=None, **kwargs): """Update a System Args: system_id (str): The System ID new_commands (Optional[List[Command]]): New System commands Keyword Args: metadata (dict): New System metadata description (str): New System description display_name (str): New System display name icon_name (str) The: New System icon name Returns: System: The updated system """ operations = [] metadata = kwargs.pop("metadata", {}) if new_commands: commands = self.parser.serialize_command(new_commands, to_string=False, many=True) operations.append(PatchOperation("replace", "/commands", commands)) if metadata: operations.append(PatchOperation("update", "/metadata", metadata)) for key, value in kwargs.items(): if value is not None: operations.append(PatchOperation("replace", "/%s" % key, value)) return self.client.patch_system( system_id, self.parser.serialize_patch(operations, many=True)) def remove_system(self, **kwargs): """Remove a unique System Args: **kwargs: Search parameters Returns: bool: True if removal was successful Raises: FetchError: Couldn't find a System matching given parameters """ system = self.find_unique_system(**kwargs) if system is None: raise FetchError("No matching System found") return self._remove_system_by_id(system.id) @wrap_response(parse_method="parse_instance", parse_many=False, default_exc=SaveError) def initialize_instance(self, instance_id): """Start an Instance Args: instance_id (str): The Instance ID Returns: Instance: The updated Instance """ return self.client.patch_instance( instance_id, self.parser.serialize_patch(PatchOperation("initialize"))) @wrap_response(parse_method="parse_instance", parse_many=False, default_exc=FetchError) def get_instance(self, instance_id): """Get an Instance Args: instance_id: The Id Returns: The Instance """ return self.client.get_instance(instance_id) def get_instance_status(self, instance_id): """Get an Instance WARNING: This method currently returns the Instance, not the Instance's status. This behavior will be corrected in 3.0. To prepare for this change please use get_instance() instead of this method. Args: instance_id: The Id Returns: The status """ warnings.warn( "This method currently returns the Instance, not the Instance's status. " "This behavior will be corrected in 3.0. To prepare please use " "get_instance() instead of this method.", FutureWarning, ) return self.get_instance(instance_id) @wrap_response(parse_method="parse_instance", parse_many=False, default_exc=SaveError) def update_instance_status(self, instance_id, new_status): """Update an Instance status Args: instance_id (str): The Instance ID new_status (str): The new status Returns: Instance: The updated Instance """ return self.client.patch_instance( instance_id, self.parser.serialize_patch( PatchOperation("replace", "/status", new_status)), ) @wrap_response(return_boolean=True, default_exc=SaveError) def instance_heartbeat(self, instance_id): """Send an Instance heartbeat Args: instance_id (str): The Instance ID Returns: bool: True if the heartbeat was successful """ return self.client.patch_instance( instance_id, self.parser.serialize_patch(PatchOperation("heartbeat"))) @wrap_response(return_boolean=True, default_exc=DeleteError) def remove_instance(self, instance_id): """Remove an Instance Args: instance_id (str): The Instance ID Returns: bool: True if the remove was successful """ if instance_id is None: raise DeleteError("Cannot delete an instance without an id") return self.client.delete_instance(instance_id) def find_unique_request(self, **kwargs): """Find a unique request .. note:: If 'id' is a given keyword argument then all other parameters will be ignored. Args: **kwargs: Search parameters Returns: Request, None: The Request if found, None otherwise Raises: FetchError: More than one matching Request was found """ if "id" in kwargs: return self._find_request_by_id(kwargs.pop("id")) else: all_requests = self.find_requests(**kwargs) if not all_requests: return None if len(all_requests) > 1: raise FetchError("More than one matching Request found") return all_requests[0] @wrap_response(parse_method="parse_request", parse_many=True, default_exc=FetchError) def find_requests(self, **kwargs): """Find Requests using keyword arguments as search parameters Args: **kwargs: Search parameters Returns: List[Request]: List of Systems matching the search parameters """ return self.client.get_requests(**kwargs) @wrap_response(parse_method="parse_request", parse_many=False, default_exc=SaveError) def create_request(self, request, **kwargs): """Create a new Request Args: request: New request definition kwargs: Extra request parameters Keyword Args: blocking (bool): Wait for request to complete before returning timeout (int): Maximum seconds to wait for completion Returns: Request: The newly-created Request """ return self.client.post_requests( self.parser.serialize_request(request), **kwargs) @wrap_response(parse_method="parse_request", parse_many=False, default_exc=SaveError) 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, self.parser.serialize_patch(operations, many=True)) @wrap_response(return_boolean=True) 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(self.parser.serialize_event(event), publishers=publishers) @wrap_response(parse_method="parse_queue", parse_many=True) def get_queues(self): """Retrieve all queue information :return: The response """ return self.client.get_queues() @wrap_response(return_boolean=True) def clear_queue(self, queue_name): """Cancel and remove all Requests from a message queue Args: queue_name (str): The name of the queue to clear Returns: bool: True if the clear was successful """ return self.client.delete_queue(queue_name) @wrap_response(return_boolean=True) def clear_all_queues(self): """Cancel and remove all Requests in all queues Returns: bool: True if the clear was successful """ return self.client.delete_queues() @wrap_response(parse_method="parse_job", parse_many=True, default_exc=FetchError) def find_jobs(self, **kwargs): """Find Jobs using keyword arguments as search parameters Args: **kwargs: Search parameters Returns: List[Job]: List of Jobs matching the search parameters """ return self.client.get_jobs(**kwargs) @wrap_response(parse_method="parse_job", parse_many=False, default_exc=SaveError) def create_job(self, job): """Create a new Job Args: job (Job): New Job definition Returns: Job: The newly-created Job """ return self.client.post_jobs(self.parser.serialize_job(job)) @wrap_response(return_boolean=True, default_exc=DeleteError) def remove_job(self, job_id): """Remove a unique Job Args: job_id (str): The Job ID Returns: bool: True if removal was successful Raises: DeleteError: Couldn't remove Job """ return self.client.delete_job(job_id) def pause_job(self, job_id): """Pause a Job Args: job_id (str): The Job ID Returns: Job: The updated Job """ self._patch_job(job_id, [PatchOperation("update", "/status", "PAUSED")]) def resume_job(self, job_id): """Resume a Job Args: job_id (str): The Job ID Returns: Job: The updated Job """ self._patch_job(job_id, [PatchOperation("update", "/status", "RUNNING")]) @wrap_response(parse_method="parse_principal", parse_many=False, default_exc=FetchError) def get_user(self, user_identifier): """Find a user Args: user_identifier (str): User ID or username Returns: Principal: The User """ return self.client.get_user(user_identifier) def who_am_i(self): """Find user using the current set of credentials Returns: Principal: The User """ return self.get_user(self.client.username or "anonymous") @wrap_response( parse_method="parse_system", parse_many=False, default_exc=FetchError, raise_404=False, ) def _find_system_by_id(self, system_id, **kwargs): return self.client.get_system(system_id, **kwargs) @wrap_response(return_boolean=True, default_exc=DeleteError) def _remove_system_by_id(self, system_id): if system_id is None: raise DeleteError("Cannot delete a system without an id") return self.client.delete_system(system_id) @wrap_response( parse_method="parse_request", parse_many=False, default_exc=FetchError, raise_404=False, ) def _find_request_by_id(self, request_id): return self.client.get_request(request_id) @wrap_response(parse_method="parse_job", parse_many=False, default_exc=SaveError) def _patch_job(self, job_id, operations): return self.client.patch_job( job_id, self.parser.serialize_patch(operations, many=True))
def test_session_client_cert(self): client = RestClient( bg_host="host", bg_port=80, api_version=1, client_cert="/path/to/cert" ) assert client.session.cert == "/path/to/cert"
def test_bad_args(self, kwargs): with pytest.raises(YapconfItemError): RestClient(**kwargs)
def __init__(self, *args, **kwargs): # This points DeprecationWarnings at the right line kwargs.setdefault("stacklevel", 4) self.client = RestClient(*args, **kwargs)
def test_old_positional_args(self, client, url_prefix): test_client = RestClient('host', 80, api_version=1, url_prefix=url_prefix) assert test_client.version_url == client.version_url
def test_bad_args(self, kwargs): with pytest.raises(ValueError): RestClient(**kwargs)
class EasyClient(object): """Client for simplified communication with Beergarden This class is intended to be a middle ground between the RestClient and SystemClient. It provides a 'cleaner' interface to some common Beergarden operations than is exposed by the lower-level RestClient. On the other hand, the SystemClient is much better for generating Beergarden Requests. Args: bg_host (str): Beer-garden hostname bg_port (int): Beer-garden port bg_url_prefix (str): URL path that will be used as a prefix when communicating with Beer-garden. Useful if Beer-garden is running on a URL other than '/'. ssl_enabled (bool): Whether to use SSL for Beer-garden communication ca_cert (str): Path to certificate file containing the certificate of the authority that issued the Beer-garden server certificate ca_verify (bool): Whether to verify Beer-garden server certificate client_cert (str): Path to client certificate to use when communicating with Beer-garden api_version (int): Beer-garden API version to use client_timeout (int): Max time to wait for Beer-garden server response username (str): Username for Beer-garden authentication password (str): Password for Beer-garden authentication access_token (str): Access token for Beer-garden authentication refresh_token (str): Refresh token for Beer-garden authentication """ _default_file_params = { "chunk_size": 255 * 1024, } def __init__(self, *args, **kwargs): # This points DeprecationWarnings at the right line kwargs.setdefault("stacklevel", 4) self.client = RestClient(*args, **kwargs) def can_connect(self, **kwargs): # type: (**Any) -> bool """Determine if the Beergarden server is responding. Args: **kwargs: Keyword arguments passed to the underlying Requests method Returns: A bool indicating if the connection attempt was successful. Will return False only if a ConnectionError is raised during the attempt. Any other exception will be re-raised. Raises: requests.exceptions.RequestException: The connection attempt resulted in an exception that indicates something other than a basic connection error. For example, an error with certificate verification. """ return self.client.can_connect(**kwargs) @wrap_response(default_exc=FetchError) def get_version(self, **kwargs): """Get Bartender, Brew-view, and API version information Args: **kwargs: Extra parameters Returns: dict: Response object with version information in the body """ return self.client.get_version(**kwargs) @wrap_response(default_exc=FetchError) def get_config(self): """Get configuration Returns: dict: Configuration dictionary """ return self.client.get_config() @wrap_response(default_exc=FetchError) def get_logging_config(self, system_name=None, local=False): """Get a logging configuration Note that the system_name is not relevant and is only provided for backward-compatibility. Args: system_name (str): UNUSED Returns: dict: The configuration object """ return self.client.get_logging_config(local=local) @wrap_response(parse_method="parse_garden", parse_many=False, default_exc=FetchError) def get_garden(self, garden_name): """Get a Garden Args: garden_name: Name of garden to retrieve Returns: The Garden """ return self.client.get_garden(garden_name) @wrap_response(parse_method="parse_garden", parse_many=True, default_exc=FetchError) def get_gardens(self): """Get all Gardens. Returns: A list of all the Gardens """ return self.client.get_gardens() @wrap_response(parse_method="parse_garden", parse_many=False, default_exc=SaveError) def create_garden(self, garden): """Create a new Garden Args: garden (Garden): The Garden to create Returns: Garden: The newly-created Garden """ return self.client.post_gardens(SchemaParser.serialize_garden(garden)) @wrap_response(return_boolean=True, raise_404=True) def remove_garden(self, garden_name): """Remove a unique Garden Args: garden_name (String): Name of Garden to remove Returns: bool: True if removal was successful Raises: NotFoundError: Couldn't find a Garden matching given name """ return self.client.delete_garden(garden_name) @wrap_response(parse_method="parse_garden", default_exc=FetchError) def update_garden(self, garden): garden_as_dict = SchemaParser.serialize_garden(garden, to_string=False) patches = json.dumps([{ "operation": "config", "path": "", "value": garden_as_dict, }]) return self.client.patch_garden(garden.name, patches) @wrap_response(parse_method="parse_system", parse_many=False, default_exc=FetchError) def get_system(self, system_id): """Get a Garden Args: system_id: The Id Returns: The System """ return self.client.get_system(system_id) def find_unique_system(self, **kwargs): """Find a unique system .. note:: If 'id' is a given keyword argument then all other parameters will be ignored. Args: **kwargs: Search parameters Returns: System, None: The System if found, None otherwise Raises: FetchError: More than one matching System was found """ if "id" in kwargs: try: return self.get_system(kwargs.pop("id"), **kwargs) except NotFoundError: return None else: systems = self.find_systems(**kwargs) if not systems: return None if len(systems) > 1: raise FetchError("More than one matching System found") return systems[0] @wrap_response(parse_method="parse_system", parse_many=True, default_exc=FetchError) def find_systems(self, **kwargs): """Find Systems using keyword arguments as search parameters Args: **kwargs: Search parameters Returns: List[System]: List of Systems matching the search parameters """ return self.client.get_systems(**kwargs) @wrap_response(parse_method="parse_system", parse_many=False, default_exc=SaveError) def create_system(self, system): """Create a new System Args: system (System): The System to create Returns: System: The newly-created system """ return self.client.post_systems(SchemaParser.serialize_system(system)) @wrap_response(parse_method="parse_system", parse_many=False, default_exc=SaveError) def update_system(self, system_id, new_commands=None, **kwargs): """Update a System Args: system_id (str): The System ID new_commands (Optional[List[Command]]): New System commands Keyword Args: add_instance (Instance): An Instance to append metadata (dict): New System metadata description (str): New System description display_name (str): New System display name icon_name (str): New System icon name template (str): New System template Returns: System: The updated system """ operations = [] if new_commands is not None: commands = SchemaParser.serialize_command(new_commands, to_string=False, many=True) operations.append(PatchOperation("replace", "/commands", commands)) add_instance = kwargs.pop("add_instance", None) if add_instance: instance = SchemaParser.serialize_instance(add_instance, to_string=False) operations.append(PatchOperation("add", "/instance", instance)) metadata = kwargs.pop("metadata", {}) if metadata: operations.append(PatchOperation("update", "/metadata", metadata)) # The remaining kwargs are all strings # Sending an empty string (instead of None) ensures they're actually cleared for key, value in kwargs.items(): operations.append( PatchOperation("replace", "/%s" % key, value or "")) return self.client.patch_system( system_id, SchemaParser.serialize_patch(operations, many=True)) def remove_system(self, **kwargs): """Remove a unique System Args: **kwargs: Search parameters Returns: bool: True if removal was successful Raises: FetchError: Couldn't find a System matching given parameters """ system = self.find_unique_system(**kwargs) if system is None: raise FetchError("No matching System found") return self._remove_system_by_id(system.id) @wrap_response(parse_method="parse_instance", parse_many=False, default_exc=SaveError) def initialize_instance(self, instance_id, runner_id=None): """Start an Instance Args: instance_id (str): The Instance ID runner_id (str): The PluginRunner ID, if any Returns: Instance: The updated Instance """ return self.client.patch_instance( instance_id, SchemaParser.serialize_patch( PatchOperation(operation="initialize", value={"runner_id": runner_id})), ) @wrap_response(parse_method="parse_instance", parse_many=False, default_exc=FetchError) def get_instance(self, instance_id): """Get an Instance Args: instance_id: The Id Returns: The Instance """ return self.client.get_instance(instance_id) @wrap_response(parse_method="parse_instance", parse_many=False, default_exc=SaveError) 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)) def get_instance_status(self, instance_id): """ .. deprecated: 3.0 Will be removed in 4.0. Use ``get_instance()`` instead Get an Instance's status Args: instance_id: The Id Returns: The Instance's status """ _deprecate( "This method is deprecated and scheduled to be removed in 4.0. " "Please use get_instance() instead.") return self.get_instance(instance_id).status def update_instance_status(self, instance_id, new_status): """ .. deprecated: 3.0 Will be removed in 4.0. Use ``update_instance()`` instead Get an Instance's status Args: instance_id (str): The Instance ID new_status (str): The new status Returns: Instance: The updated Instance """ _deprecate( "This method is deprecated and scheduled to be removed in 4.0. " "Please use update_instance() instead.") return self.update_instance(instance_id, new_status=new_status) @wrap_response(return_boolean=True, default_exc=SaveError) def instance_heartbeat(self, instance_id): """Send an Instance heartbeat Args: instance_id (str): The Instance ID Returns: bool: True if the heartbeat was successful """ return self.client.patch_instance( instance_id, SchemaParser.serialize_patch(PatchOperation("heartbeat"))) @wrap_response(return_boolean=True, default_exc=DeleteError) def remove_instance(self, instance_id): """Remove an Instance Args: instance_id (str): The Instance ID Returns: bool: True if the remove was successful """ if instance_id is None: raise DeleteError("Cannot delete an instance without an id") return self.client.delete_instance(instance_id) @wrap_response(parse_method="parse_request", parse_many=False, default_exc=FetchError) def get_request(self, request_id): """Get a Request Args: request_id: The Id Returns: The Request """ return self.client.get_request(request_id) def find_unique_request(self, **kwargs): """Find a unique request .. note:: If 'id' is a given keyword argument then all other parameters will be ignored. Args: **kwargs: Search parameters Returns: Request, None: The Request if found, None otherwise Raises: FetchError: More than one matching Request was found """ if "id" in kwargs: try: return self.get_request(kwargs.pop("id")) except NotFoundError: return None else: all_requests = self.find_requests(**kwargs) if not all_requests: return None if len(all_requests) > 1: raise FetchError("More than one matching Request found") return all_requests[0] @wrap_response(parse_method="parse_request", parse_many=True, default_exc=FetchError) def find_requests(self, **kwargs): """Find Requests using keyword arguments as search parameters Args: **kwargs: Search parameters Returns: List[Request]: List of Systems matching the search parameters """ return self.client.get_requests(**kwargs) @wrap_response(parse_method="parse_request", parse_many=False, default_exc=SaveError) def create_request(self, request, **kwargs): """Create a new Request Args: request: New request definition **kwargs: Extra request parameters Keyword Args: blocking (bool): Wait for request to complete before returning timeout (int): Maximum seconds to wait for completion Returns: Request: The newly-created Request """ return self.client.post_requests( SchemaParser.serialize_request(request), **kwargs) @wrap_response(parse_method="parse_request", parse_many=False, default_exc=SaveError) 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)) @wrap_response(return_boolean=True) 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) @wrap_response(parse_method="parse_queue", parse_many=True, default_exc=FetchError) def get_queues(self): """Retrieve all queue information Returns: List[Queue]: List of all Queues """ return self.client.get_queues() @wrap_response(return_boolean=True, default_exc=DeleteError) def clear_queue(self, queue_name): """Cancel and remove all Requests from a message queue Args: queue_name (str): The name of the queue to clear Returns: bool: True if the clear was successful """ return self.client.delete_queue(queue_name) @wrap_response(return_boolean=True, default_exc=DeleteError) def clear_all_queues(self): """Cancel and remove all Requests in all queues Returns: bool: True if the clear was successful """ return self.client.delete_queues() @wrap_response(parse_method="parse_job", parse_many=True, default_exc=FetchError) def find_jobs(self, **kwargs): """Find Jobs using keyword arguments as search parameters Args: **kwargs: Search parameters Returns: List[Job]: List of Jobs matching the search parameters """ return self.client.get_jobs(**kwargs) @wrap_response(parse_method="parse_job", parse_many=True, default_exc=FetchError) def export_jobs(self, job_id_list=None): # type: (Optional[List[str]]) -> List[Job] """Export jobs from an optional job ID list. If `job_id_list` is None or empty, definitions for all jobs are returned. Args: job_id_list: A list of job IDS, optional Returns: A list of job definitions """ # we should check that the argument is a list (if it's not None) because the # call to `len` will otherwise produce an unhelpful error message if job_id_list is not None and not isinstance(job_id_list, list): raise TypeError( "Argument must be a list of job IDs, an empty list or None") payload = (SchemaParser.serialize_job_ids(job_id_list, many=True) if job_id_list is not None and len(job_id_list) > 0 else "{}") return self.client.post_export_jobs( payload) # noqa # wrapper changes type @wrap_response(parse_method="parse_job_ids") def import_jobs(self, job_list): # type: (List[Job]) -> List[str] """Import job definitions from a list of Jobs. Args: job_list: A list of jobs to import Returns: A list of the job IDs created """ return self.client.post_import_jobs( # noqa # wrapper changes type SchemaParser.serialize_job_for_import(job_list, many=True)) @wrap_response(parse_method="parse_job", parse_many=False, default_exc=SaveError) def create_job(self, job): """Create a new Job Args: job (Job): New Job definition Returns: Job: The newly-created Job """ return self.client.post_jobs(SchemaParser.serialize_job(job)) @wrap_response(return_boolean=True, default_exc=DeleteError) def remove_job(self, job_id): """Remove a unique Job Args: job_id (str): The Job ID Returns: bool: True if removal was successful Raises: DeleteError: Couldn't remove Job """ return self.client.delete_job(job_id) def pause_job(self, job_id): """Pause a Job Args: job_id (str): The Job ID Returns: Job: The updated Job """ return self._patch_job(job_id, [PatchOperation("update", "/status", "PAUSED")]) def resume_job(self, job_id): """Resume a Job Args: job_id (str): The Job ID Returns: Job: The updated Job """ return self._patch_job( job_id, [PatchOperation("update", "/status", "RUNNING")]) def execute_job(self, job_id, reset_interval=False): """Execute a Job Args: job_id (str): The Job ID reset_interval (bool): Restarts the job's interval time to now if the job's trigger is an interval Returns: Request: The returned request """ return self.client.post_execute_job(job_id, reset_interval) @wrap_response(parse_method="parse_resolvable") def upload_bytes(self, data): # type: (bytes) -> Any """Upload a file Args: data: The bytes to upload Returns: The bytes Resolvable """ return self.client.post_file(data) def download_bytes(self, file_id): # type: (str) -> bytes """Download bytes Args: file_id: Id of bytes to download Returns: The bytes data """ return self.client.get_file(file_id).content @wrap_response(parse_method="parse_resolvable") def upload_file(self, path): # type: (Union[str, Path]) -> Any """Upload a file Args: path: Path to file Returns: The file Resolvable """ # As of now this just converts the data param to bytes and uses the bytes API # Ideally this would fail-over to using the chunks API if necessary with open(path, "rb") as f: bytes_data = f.read() return self.client.post_file(bytes_data) def download_file(self, file_id, path): # type: (str, Union[str, Path]) -> Union[str, Path] """Download a file Args: file_id: The File id. path: Location for downloaded file Returns: Path to downloaded file """ data = self.download_bytes(file_id) with open(path, "wb") as f: f.write(data) return path @wrap_response(parse_method="parse_resolvable") def upload_chunked_file(self, file_to_upload, desired_filename=None, file_params=None): """Upload a given file to the Beer Garden server. Args: file_to_upload: Can either be an open file descriptor or a path. desired_filename: The desired filename, if none is provided it will use the basename of the file_to_upload file_params: The metadata surrounding the file. Valid Keys: See brewtils File model Returns: A BG file ID. """ default_file_params = {} # Establish the file descriptor if isinstance(file_to_upload, six.string_types): try: fd = open(file_to_upload, "rb") except Exception: raise ValidationError( "Could not open the requested file name.") require_close = True else: fd = file_to_upload require_close = False try: default_file_params["file_name"] = desired_filename or fd.name except AttributeError: default_file_params["file_name"] = "no_file_name_provided" # Determine the file size cur_cursor = fd.tell() default_file_params["file_size"] = fd.seek(0, 2) - cur_cursor fd.seek(cur_cursor) if file_params is not None: file_params["file_size"] = default_file_params["file_size"] # Set the parameters to be sent file_params = file_params or dict(default_file_params, ** self._default_file_params) try: response = self.client.post_chunked_file( fd, file_params, current_position=cur_cursor) fd.seek(cur_cursor) finally: if require_close: fd.close() if not response.ok: handle_response_failure(response, default_exc=SaveError) # The file post is best effort; make sure to verify before we let the # user do anything with it file_id = response.json()["details"]["file_id"] valid, meta = self._check_chunked_file_validity(file_id) if not valid: # Clean up if you can self.client.delete_chunked_file(file_id) raise ValidationError("Error occurred while uploading file %s" % default_file_params["file_name"]) return response def download_chunked_file(self, file_id): """Download a chunked file from the Beer Garden server. Args: file_id: The beer garden-assigned file id. Returns: A file object """ (valid, meta) = self._check_chunked_file_validity(file_id) file_obj = BytesIO() if valid: for x in range(meta["number_of_chunks"]): resp = self.client.get_chunked_file(file_id, params={"chunk": x}) if resp.ok: data = resp.json()["data"] file_obj.write(b64decode(data)) else: raise ValueError("Could not fetch chunk %d" % x) else: raise ValidationError("Requested file %s is incomplete." % file_id) file_obj.seek(0) return file_obj def delete_chunked_file(self, file_id): """Delete a given file on the Beer Garden server. Args: file_id: The beer garden-assigned file id. Returns: The API response """ return self.client.delete_chunked_file(file_id) def forward(self, operation, **kwargs): """Forwards an Operation Args: operation: The Operation to be forwarded **kwargs: Keyword arguments to pass to Requests session call Returns: The API response """ return self.client.post_forward( SchemaParser.serialize_operation(operation), **kwargs) @wrap_response(parse_method="parse_principal", parse_many=False, default_exc=FetchError) def get_user(self, user_identifier): """Find a user Args: user_identifier (str): User ID or username Returns: Principal: The User """ return self.client.get_user(user_identifier) def who_am_i(self): """Find user using the current set of credentials Returns: Principal: The User """ return self.get_user(self.client.username or "anonymous") @wrap_response(return_boolean=True) def rescan(self): """Rescan local plugin directory Returns: bool: True if rescan was successful """ return self.client.patch_admin(payload=SchemaParser.serialize_patch( PatchOperation(operation="rescan"))) @wrap_response(return_boolean=True, default_exc=DeleteError) def _remove_system_by_id(self, system_id): if system_id is None: raise DeleteError("Cannot delete a system without an id") return self.client.delete_system(system_id) @wrap_response(parse_method="parse_job", parse_many=False, default_exc=SaveError) def _patch_job(self, job_id, operations): return self.client.patch_job( job_id, SchemaParser.serialize_patch(operations, many=True)) def _check_chunked_file_validity(self, file_id): """Verify a chunked file Args: file_id: The BG-assigned file id. Returns: A tuple containing the result and supporting metadata, if available """ response = self.client.get_chunked_file(file_id, params={"verify": True}) if not response.ok: return False, None metadata_json = response.json() if "valid" in metadata_json and metadata_json["valid"]: return True, metadata_json else: return False, metadata_json