Exemple #1
0
    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
Exemple #2
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