示例#1
0
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))
示例#2
0
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())
示例#3
0
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