Ejemplo n.º 1
0
class Plugin(object):
    """A Beer-garden Plugin

    This class represents a Beer-garden Plugin - a continuously-running process
    that can receive and process Requests.

    To work, a Plugin needs a Client instance - an instance of a class defining
    which Requests this plugin can accept and process. The easiest way to define
    a ``Client`` is by annotating a class with the ``@system`` decorator.

    A Plugin needs certain pieces of information in order to function correctly. These
    can be grouped into two high-level categories: identifying information and
    connection information.

    Identifying information is how Beer-garden differentiates this Plugin from all
    other Plugins. If you already have fully-defined System model you can pass that
    directly to the Plugin (``system=my_system``). However, normally it's simpler to
    pass the pieces directly:

        - ``name`` (required)
        - ``version`` (required)
        - ``instance_name`` (required, but defaults to "default")
        - ``namespace``
        - ``description``
        - ``icon_name``
        - ``metadata``
        - ``display_name``

    Connection information tells the Plugin how to communicate with Beer-garden. The
    most important of these is the ``bg_host`` (to tell the plugin where to find the
    Beer-garden you want to connect to):

        - ``bg_host``
        - ``bg_port``
        - ``bg_url_prefix``
        - ``ssl_enabled``
        - ``ca_cert``
        - ``ca_verify``
        - ``client_cert``

    An example plugin might look like this:

    .. code-block:: python

        Plugin(
            name="Test",
            version="1.0.0",
            instance_name="default",
            namespace="test plugins",
            description="A Test",
            bg_host="localhost",
        )

    Plugins use `Yapconf <https://github.com/loganasherjones/yapconf>`_ for
    configuration loading, which means that values can be discovered from sources other
    than direct argument passing. Config can be passed as command line arguments::

        python my_plugin.py --bg-host localhost

    Values can also be specified as environment variables with a "\\BG_" prefix::

        BG_HOST=localhost python my_plugin.py

    Plugins service requests using a
    :py:class:`concurrent.futures.ThreadPoolExecutor`. The maximum number of
    threads available is controlled by the ``max_concurrent`` argument.

    .. warning::
        Normally the processing of each Request occurs in a distinct thread context. If
        you need to access shared state please be careful to use appropriate
        concurrency mechanisms.

    .. warning::
        The default value for ``max_concurrent`` is 5, but setting it to 1 is allowed.
        This means that a Plugin will essentially be single-threaded, but realize this
        means that if the Plugin invokes a Command on itself in the course of processing
        a Request then the Plugin **will** deadlock!

    Args:
        client: Instance of a class annotated with ``@system``.

        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. NOTE: This is required to be a cert / key bundle if SSL/TLS is
            enabled for rabbitmq in your environment.
        client_key (str): Path to client key. Not necessary if client_cert is a bundle.
        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

        system (:py:class:`brewtils.models.System`): A Beer-garden System definition.
            Incompatible with name, version, description, display_name, icon_name,
            max_instances, and metadata parameters.
        name (str): System name
        version (str): System version
        description (str): System description
        display_name (str): System display name
        icon_name (str): System icon name
        max_instances (int): System maximum instances
        metadata (dict): System metadata
        instance_name (str): Instance name
        namespace (str): Namespace name

        logger (:py:class:`logging.Logger`): Logger that will be used by the Plugin.
            Passing a logger will prevent the Plugin from preforming any additional
            logging configuration.

        worker_shutdown_timeout (int): Time to wait during shutdown to finish processing
        max_concurrent (int): Maximum number of requests to process concurrently
        max_attempts (int): Number of times to attempt updating of a Request
            before giving up. Negative numbers are interpreted as no maximum.
        max_timeout (int): Maximum amount of time to wait between Request update
            attempts. Negative numbers are interpreted as no maximum.
        starting_timeout (int): Initial time to wait between Request update attempts.
            Will double on subsequent attempts until reaching max_timeout.

        mq_max_attempts (int): Number of times to attempt reconnection to message queue
            before giving up. Negative numbers are interpreted as no maximum.
        mq_max_timeout (int): Maximum amount of time to wait between message queue
            reconnect attempts. Negative numbers are interpreted as no maximum.
        mq_starting_timeout (int): Initial time to wait between message queue reconnect
            attempts. Will double on subsequent attempts until reaching mq_max_timeout.
        working_directory (str): Path to a preferred working directory. Only used
            when working with bytes parameters.
    """

    def __init__(self, client=None, system=None, logger=None, **kwargs):
        self._client = None
        self._instance = None
        self._admin_processor = None
        self._request_processor = None
        self._shutdown_event = threading.Event()

        # Need to set up logging before loading config
        self._custom_logger = False
        self._logger = self._setup_logging(logger=logger, **kwargs)

        # Now that logging is configured we can load the real config
        self._config = load_config(**kwargs)

        # If global config has already been set that's a warning
        global CONFIG
        if len(CONFIG):
            self._logger.warning(
                "Global CONFIG object is not empty! If multiple plugins are running in "
                "this process please ensure any [System|Easy|Rest]Clients are passed "
                "connection information as kwargs as auto-discovery may be incorrect."
            )
        CONFIG = Box(self._config.to_dict(), default_box=True)

        # Now set up the system
        self._system = self._setup_system(system, kwargs)

        # Make sure this is set after self._system
        if client:
            self.client = client

        # Now that the config is loaded we can create the EasyClient
        self._ez_client = EasyClient(logger=self._logger, **self._config)

        # With the EasyClient we can determine if this is an old garden
        self._legacy = self._legacy_garden()

        if not self._legacy:
            # Namespace setup depends on self._system and self._ez_client
            self._setup_namespace()

            # And with _system and _ez_client we can ask for the real logging config
            self._initialize_logging()

    def run(self):
        if not self._client:
            raise AttributeError(
                "Unable to start a Plugin without a Client. Please set the 'client' "
                "attribute to an instance of a class decorated with @brewtils.system"
            )

        self._startup()
        self._logger.info("Plugin %s has started", self.unique_name)

        try:
            # Need the timeout param so this works correctly in Python 2
            while not self._shutdown_event.wait(timeout=0.1):
                pass
        except KeyboardInterrupt:
            self._logger.debug("Received KeyboardInterrupt - shutting down")
        except Exception as ex:
            self._logger.exception("Exception during wait, shutting down: %s", ex)

        self._shutdown()
        self._logger.info("Plugin %s has terminated", self.unique_name)

    @property
    def client(self):
        return self._client

    @client.setter
    def client(self, new_client):
        if self._client:
            raise AttributeError("Sorry, you can't change a plugin's client once set")

        if new_client is None:
            return

        # Several _system properties can come from the client, so update if needed
        if not self._system.name:
            self._system.name = getattr(new_client, "_bg_name")  # noqa
        if not self._system.version:
            self._system.version = getattr(new_client, "_bg_version")  # noqa
        if not self._system.description and new_client.__doc__:
            self._system.description = new_client.__doc__.split("\n")[0]

        # Now roll up / interpret all metadata to get the Commands
        self._system.commands = _parse_client(new_client)

        try:
            # Put some attributes on the Client class
            client_clazz = type(new_client)
            client_clazz.current_request = property(
                lambda _: request_context.current_request
            )

            # Add for back-compatibility
            client_clazz._bg_name = self._system.name
            client_clazz._bg_version = self._system.version
            client_clazz._bg_commands = self._system.commands
            client_clazz._current_request = client_clazz.current_request
        except TypeError:
            if sys.version_info.major != 2:
                raise

            self._logger.warning(
                "Unable to assign attributes to Client class - current_request will "
                "not be available. If you're using an old-style class declaration "
                "it's recommended to switch to new-style if possible."
            )

        self._client = new_client

    @property
    def system(self):
        return self._system

    @property
    def instance(self):
        return self._instance

    @property
    def unique_name(self):
        return "%s:%s[%s]-%s" % (
            self._system.namespace,
            self._system.name,
            self._config.instance_name,
            self._system.version,
        )

    @staticmethod
    def _set_signal_handlers():
        """Ensure that SIGINT and SIGTERM will gracefully stop the Plugin"""

        def _handler(_signal, _frame):
            raise KeyboardInterrupt

        signal.signal(signal.SIGINT, _handler)
        signal.signal(signal.SIGTERM, _handler)

    @staticmethod
    def _set_exception_hook(logger):
        """Ensure uncaught exceptions are logged instead of being written to stderr"""

        def _hook(exc_type, exc_value, traceback):
            logger.error(
                "An uncaught exception was raised in the plugin process:",
                exc_info=(exc_type, exc_value, traceback),
            )

        sys.excepthook = _hook

    def _startup(self):
        """Plugin startup procedure

        This method actually starts the plugin. When it completes the plugin should be
        considered in a "running" state - listening to the appropriate message queues,
        connected to the Beer-garden server, and ready to process Requests.

        This method should be the first time that a connection to the Beer-garden
        server is *required*.
        """
        self._logger.debug("About to start up plugin %s", self.unique_name)

        if not self._ez_client.can_connect():
            raise RestConnectionError("Cannot connect to the Beer-garden server")

        # If namespace couldn't be determined at init try one more time
        if not self._legacy and not self._config.namespace:
            self._setup_namespace()

        self._system = self._initialize_system()
        self._instance = self._initialize_instance()

        if self._config.working_directory is None:
            app_parts = [self._system.name, self._instance.name]
            if self._system.namespace:
                app_parts.insert(0, self._system.namespace)

            self._config.working_directory = appdirs.user_data_dir(
                appname=os.path.join(*app_parts), version=self._system.version
            )

            workdir = Path(self._config.working_directory)
            if not workdir.exists():
                workdir.mkdir(parents=True)

        self._logger.debug("Initializing and starting processors")
        self._admin_processor, self._request_processor = self._initialize_processors()
        self._admin_processor.startup()
        self._request_processor.startup()

        self._logger.debug("Setting signal handlers")
        self._set_signal_handlers()

    def _shutdown(self):
        """Plugin shutdown procedure

        This method gracefully stops the plugin. When it completes the plugin should be
        considered in a "stopped" state - the message processors shut down and all
        connections closed.
        """
        self._logger.debug("About to shut down plugin %s", self.unique_name)
        self._shutdown_event.set()

        self._logger.debug("Shutting down processors")
        self._request_processor.shutdown()
        self._admin_processor.shutdown()

        try:
            self._ez_client.update_instance(self._instance.id, new_status="STOPPED")
        except Exception:
            self._logger.warning(
                "Unable to notify Beer-garden that this plugin is STOPPED, so this "
                "plugin's status may be incorrect in Beer-garden"
            )

        self._logger.debug("Successfully shutdown plugin {0}".format(self.unique_name))

    def _initialize_logging(self):
        """Configure logging with Beer-garden's configuration for this plugin.

        This method will ask Beer-garden for a logging configuration specific to this
        plugin and will apply that configuration to the logging module.

        Note that this method will do nothing if the logging module's configuration was
        already set or a logger kwarg was given during Plugin construction.

        Returns:
            None
        """
        if self._custom_logger:
            self._logger.debug("Skipping logging init: custom logger detected")
            return

        try:
            log_config = self._ez_client.get_logging_config(
                local=bool(self._config.runner_id)
            )
        except Exception as ex:
            self._logger.warning(
                "Unable to retrieve logging configuration from Beergarden, the default "
                "configuration will be used instead. Caused by: {0}".format(ex)
            )
            return

        try:
            configure_logging(
                log_config,
                namespace=self._system.namespace,
                system_name=self._system.name,
                system_version=self._system.version,
                instance_name=self._config.instance_name,
            )
        except Exception as ex:
            # Reset to default config as logging can be seriously wrong now
            logging.config.dictConfig(default_config(level=self._config.log_level))

            self._logger.exception(
                "Error encountered during logging configuration. This most likely "
                "indicates an issue with the Beergarden server plugin logging "
                "configuration. The default configuration will be used instead. Caused "
                "by: {0}".format(ex)
            )
            return

        # Finally, log uncaught exceptions using the configuration instead of stderr
        self._set_exception_hook(self._logger)

    def _initialize_system(self):
        """Let Beergarden know about System-level info

        This will attempt to find a system with a name and version matching this plugin.
        If one is found this will attempt to update it (with commands, metadata, etc.
        from this plugin).

        If a System is not found this will attempt to create one.

        Returns:
            Definition of a Beergarden System this plugin belongs to.

        Raises:
            PluginValidationError: Unable to find or create a System for this Plugin
        """
        # Make sure that the system is actually valid before trying anything
        self._validate_system()

        # Do any necessary template resolution
        self._system.template = resolve_template(self._system.template)

        existing_system = self._ez_client.find_unique_system(
            name=self._system.name,
            version=self._system.version,
            namespace=self._system.namespace,
        )

        if not existing_system:
            try:
                # If this succeeds can just finish here
                return self._ez_client.create_system(self._system)
            except ConflictError:
                # If multiple instances are starting up at once and this is a new system
                # the create can return a conflict. In that case just try the get again
                existing_system = self._ez_client.find_unique_system(
                    name=self._system.name,
                    version=self._system.version,
                    namespace=self._system.namespace,
                )

        # If we STILL can't find a system something is really wrong
        if not existing_system:
            raise PluginValidationError(
                "Unable to find or create system {0}".format(self._system)
            )

        # We always update with these fields
        update_kwargs = {
            "new_commands": self._system.commands,
            "metadata": self._system.metadata,
            "description": self._system.description,
            "display_name": self._system.display_name,
            "icon_name": self._system.icon_name,
            "template": self._system.template,
        }

        # And if this particular instance doesn't exist we want to add it
        if not existing_system.has_instance(self._config.instance_name):
            update_kwargs["add_instance"] = Instance(name=self._config.instance_name)

        return self._ez_client.update_system(existing_system.id, **update_kwargs)

    def _initialize_instance(self):
        """Let Beer-garden know this instance is ready to process Requests"""
        # Sanity check to make sure an instance with this name was registered
        if not self._system.has_instance(self._config.instance_name):
            raise PluginValidationError(
                "Unable to find registered instance with name '%s'"
                % self._config.instance_name
            )

        return self._ez_client.initialize_instance(
            self._system.get_instance_by_name(self._config.instance_name).id,
            runner_id=self._config.runner_id,
        )

    def _initialize_processors(self):
        """Create RequestProcessors for the admin and request queues"""
        # If the queue connection is TLS we need to update connection params with
        # values specified at plugin creation
        connection_info = self._instance.queue_info["connection"]
        if "ssl" in connection_info:
            if self._config.ca_verify:
                connection_info["ssl"]["ca_verify"] = self._config.ca_verify

            if self._config.ca_cert:
                connection_info["ssl"]["ca_cert"] = self._config.ca_cert

            if self._config.client_cert:
                connection_info["ssl"]["client_cert"] = self._config.client_cert

        # Each RequestProcessor needs a RequestConsumer, so start with those
        common_args = {
            "connection_type": self._instance.queue_type,
            "connection_info": connection_info,
            "panic_event": self._shutdown_event,
            "max_reconnect_attempts": self._config.mq.max_attempts,
            "max_reconnect_timeout": self._config.mq.max_timeout,
            "starting_reconnect_timeout": self._config.mq.starting_timeout,
        }
        admin_consumer = RequestConsumer.create(
            thread_name="Admin Consumer",
            queue_name=self._instance.queue_info["admin"]["name"],
            max_concurrent=1,
            **common_args
        )
        request_consumer = RequestConsumer.create(
            thread_name="Request Consumer",
            queue_name=self._instance.queue_info["request"]["name"],
            max_concurrent=self._config.max_concurrent,
            **common_args
        )

        # Both RequestProcessors need an updater
        updater = HTTPRequestUpdater(
            self._ez_client,
            self._shutdown_event,
            max_attempts=self._config.max_attempts,
            max_timeout=self._config.max_timeout,
            starting_timeout=self._config.starting_timeout,
        )

        # Finally, create the actual RequestProcessors
        admin_processor = AdminProcessor(
            target=self,
            updater=updater,
            consumer=admin_consumer,
            plugin_name=self.unique_name,
            max_workers=1,
        )
        request_processor = RequestProcessor(
            target=self._client,
            updater=updater,
            consumer=request_consumer,
            validation_funcs=[self._correct_system, self._is_running],
            plugin_name=self.unique_name,
            max_workers=self._config.max_concurrent,
            resolver=ResolutionManager(easy_client=self._ez_client),
            system=self._system,
        )

        return admin_processor, request_processor

    def _start(self):
        """Handle start Request"""
        self._instance = self._ez_client.update_instance(
            self._instance.id, new_status="RUNNING"
        )

    def _stop(self):
        """Handle stop Request"""
        # Because the run() method is on a 0.1s sleep there's a race regarding if the
        # admin consumer will start processing the next message on the queue before the
        # main thread can stop it. So stop it here to prevent that.
        self._request_processor.consumer.stop_consuming()
        self._admin_processor.consumer.stop_consuming()

        self._shutdown_event.set()

    def _status(self):
        """Handle status Request"""
        try:
            self._ez_client.instance_heartbeat(self._instance.id)
        except (RequestsConnectionError, RestConnectionError):
            pass

    def _read_log(self, **kwargs):
        """Handle read log Request"""

        log_file = find_log_file()

        if not log_file:
            raise RequestProcessingError(
                "Error attempting to retrieve logs - unable to determine log filename. "
                "Please verify that the plugin is writing to a log file."
            )

        try:
            return read_log_file(log_file=log_file, **kwargs)
        except IOError as e:
            raise RequestProcessingError(
                "Error attempting to retrieve logs - unable to read log file at {0}. "
                "Root cause I/O error {1}: {2}".format(log_file, e.errno, e.strerror)
            )

    def _correct_system(self, request):
        """Validate that a request is intended for this Plugin"""
        request_system = getattr(request, "system") or ""  # noqa
        if request_system.upper() != self._system.name.upper():
            raise DiscardMessageException(
                "Received message for system {0}".format(request.system)
            )

    def _is_running(self, _):
        """Validate that this plugin is still running"""
        if self._shutdown_event.is_set():
            raise RequestProcessingError(
                "Unable to process message - currently shutting down"
            )

    def _legacy_garden(self):
        """Determine if this plugin is connected to a legacy garden"""
        legacy = False

        try:
            # Need to be careful since v2 doesn't have "beer_garden_version"
            raw_version = self._ez_client.get_version()
            if "beer_garden_version" in raw_version:
                bg_version = Version(raw_version["beer_garden_version"])
            else:
                bg_version = Version(raw_version["brew_view_version"])

            if bg_version < Version("3"):
                legacy = True

                _deprecate(
                    "Looks like your plugin is using version 3 brewtils but connecting "
                    "to a version 2 Beer Garden. Please be aware that this "
                    "functionality will stop being officially supported in the next "
                    "brewtils minor release."
                )

                self._logger.warning(
                    "This plugin is using brewtils version {0} but is connected to a "
                    "legacy Beer Garden (version {1}). Please be aware that certain "
                    "features such as namespaces and logging configuration will not "
                    "work correctly until the Beer Garden is upgraded.".format(
                        brewtils.__version__, bg_version
                    )
                )

        except Exception as ex:
            self._logger.warning(
                "An exception was raised while attempting to determine Beer Garden "
                "version, assuming non-legacy."
            )
            self._logger.debug("Underlying exception: %s" % ex, exc_info=True)

        return legacy

    def _setup_logging(self, logger=None, **kwargs):
        """Set up logging configuration and get a logger for the Plugin

        This method will configure Python-wide logging for the process if it has not
        already been configured. Whether or not logging has been configured is
        determined by the root handler count - if there aren't any then it's assumed
        logging has not already been configured.

        The configuration applied (again, if no configuration has already happened) is
        a stream handler with elevated log levels for libraries that are verbose. The
        overall level will be loaded as a configuration option, so it can be set as a
        keyword argument, command line option, or environment variable.

        A logger to be used by the Plugin will be returned. If the ``logger`` keyword
        parameter is given then that logger will be used, otherwise a logger will be
        generated from the standard ``logging`` module.

        Finally, if a the ``logger`` keyword parameter is supplied it's assumed that
        logging is already configured and no further configuration will be applied.

        Args:
            logger: A custom logger
            **kwargs: Will be used to load the bootstrap config

        Returns:
            A logger for the Plugin
        """
        if logger or len(logging.root.handlers) != 0:
            self._custom_logger = True
        else:
            # log_level is the only bootstrap config item
            boot_config = load_config(bootstrap=True, **kwargs)
            logging.config.dictConfig(default_config(level=boot_config.log_level))

            self._custom_logger = False

        return logger or logging.getLogger(__name__)

    def _setup_namespace(self):
        """Determine the namespace the Plugin is operating in

        This function attempts to determine the correct namespace and ensures that
        the value is set in the places it needs to be set.

        First, look in the resolved system (self._system) to see if that has a
        namespace. If it does, either:

        - A complete system definition with a namespace was provided
        - The namespace was resolved from the config

        In the latter case nothing further needs to be done. In the former case we
        need to set the global config namespace value to the system's namespace value
        so that any SystemClients created after the plugin will have the correct value.
        Because we have no way to know which case is correct we assume the former and
        always set the config value.

        If the system does not have a namespace then we attempt to use the EasyClient to
        determine the "default" namespace. If successful we set both the global config
        and the system namespaces to the default value.

        If the attempt to determine the default namespace is not successful we log a
        warning. We don't really want to *require* the connection to Beer-garden until
        Plugin.run() is called. Raising an exception here would do that, so instead we
        just log the warning. Another attempt will be made to determine the namespace
        in Plugin.run() which will raise on failure (but again, SystemClients created
        before the namespace is determined will have potentially incorrect namespaces).
        """
        try:
            ns = self._system.namespace or self._ez_client.get_config()["garden_name"]

            self._system.namespace = ns
            self._config.namespace = ns
            CONFIG.namespace = ns
        except Exception as ex:
            self._logger.warning(
                "Namespace value was not resolved from config sources and an exception "
                "was raised while attempting to determine default namespace value. "
                "Created SystemClients may have unexpected namespace values. "
                "Underlying exception was:\n%s" % ex
            )

    def _setup_system(self, system, plugin_kwargs):
        helper_keywords = {
            "name",
            "version",
            "description",
            "icon_name",
            "display_name",
            "max_instances",
            "metadata",
            "namespace",
            "template",
        }

        if system:
            if helper_keywords.intersection(plugin_kwargs.keys()):
                raise ValidationError(
                    "Sorry, you can't provide a complete system definition as well as "
                    "system creation helper kwargs %s" % helper_keywords
                )

            if not system.instances:
                raise ValidationError(
                    "Explicit system definition requires explicit instance "
                    "definition (use instances=[Instance(name='default')] for "
                    "default behavior)"
                )

            if not system.max_instances:
                system.max_instances = len(system.instances)

        else:
            # Commands are not defined here - they're set in the client property setter
            system = System(
                name=self._config.name,
                version=self._config.version,
                description=self._config.description,
                namespace=self._config.namespace,
                metadata=json.loads(self._config.metadata),
                instances=[Instance(name=self._config.instance_name)],
                max_instances=self._config.max_instances,
                icon_name=self._config.icon_name,
                display_name=self._config.display_name,
                template=self._config.template,
            )

        return system

    def _validate_system(self):
        """Make sure the System definition makes sense"""
        if not self._system.name:
            raise ValidationError("Plugin system must have a name")

        if not self._system.version:
            raise ValidationError("Plugin system must have a version")

        client_name = getattr(self._client, "_bg_name", None)
        if client_name and client_name != self._system.name:
            raise ValidationError(
                "System name '%s' doesn't match name from client decorator: "
                "@system(bg_name=%s)" % (self._system.name, client_name)
            )

        client_version = getattr(self._client, "_bg_version", None)
        if client_version and client_version != self._system.version:
            raise ValidationError(
                "System version '%s' doesn't match version from client decorator: "
                "@system(bg_version=%s)" % (self._system.version, client_version)
            )

    # These are provided for backward-compatibility
    @property
    def bg_host(self):
        """
        .. deprecated:: 3.0
           bg_host is now in ``_config`` (``plugin._config.bg_host``)

        Provided for backward-comptibility
        """
        _deprecate("bg_host is now in _config (plugin._config.bg_host)")
        return self._config.bg_host

    @property
    def bg_port(self):
        """
        .. deprecated:: 3.0
           bg_port is now in _config (``plugin._config.bg_port``)

        Provided for backward-comptibility
        """
        _deprecate("bg_port is now in _config (plugin._config.bg_port)")
        return self._config.bg_port

    @property
    def ssl_enabled(self):
        """
        .. deprecated:: 3.0
           ssl_enabled is now in ``_config`` (``plugin._config.ssl_enabled``)

        Provided for backward-comptibility
        """
        _deprecate("ssl_enabled is now in _config (plugin._config.ssl_enabled)")
        return self._config.ssl_enabled

    @property
    def ca_cert(self):
        """
        .. deprecated:: 3.0
           ca_cert is now in ``_config`` (``plugin._config.ca_cert``)

        Provided for backward-comptibility
        """
        _deprecate("ca_cert is now in _config (plugin._config.ca_cert)")
        return self._config.ca_cert

    @property
    def client_cert(self):
        """
        .. deprecated:: 3.0
           client_cert is now in ``_config`` (``plugin._config.client_cert``)

        Provided for backward-comptibility
        """
        _deprecate("client_cert is now in _config (plugin._config.client_cert)")
        return self._config.client_cert

    @property
    def bg_url_prefix(self):
        """
        .. deprecated:: 3.0
           bg_url_prefix is now in ``_config`` (``plugin._config.bg_url_prefix``)

        Provided for backward-comptibility
        """
        _deprecate("bg_url_prefix is now in _config (plugin._config.bg_url_prefix)")
        return self._config.bg_url_prefix

    @property
    def ca_verify(self):
        """
        .. deprecated:: 3.0
           ca_verify is now in ``_config`` (``plugin._config.ca_verify``)

        Provided for backward-comptibility
        """
        _deprecate("ca_verify is now in _config (plugin._config.ca_verify)")
        return self._config.ca_verify

    @property
    def max_attempts(self):
        """
        .. deprecated:: 3.0
           max_attempts is now in ``_config`` (``plugin._config.max_attempts``)

        Provided for backward-comptibility
        """
        _deprecate("max_attempts is now in _config (plugin._config.max_attempts)")
        return self._config.max_attempts

    @property
    def max_timeout(self):
        """
        .. deprecated:: 3.0
           max_timeout is now in ``_config`` (``plugin._config.max_timeout``)

        Provided for backward-comptibility
        """
        _deprecate("max_timeout has moved into _config (plugin._config.max_timeout)")
        return self._config.max_timeout

    @property
    def starting_timeout(self):
        """
        .. deprecated:: 3.0
           starting_timeout is now in ``_config`` (``plugin._config.starting_timeout``)

        Provided for backward-comptibility
        """
        _deprecate(
            "starting_timeout is now in _config (plugin._config.starting_timeout)"
        )
        return self._config.starting_timeout

    @property
    def max_concurrent(self):
        """
        .. deprecated:: 3.0
           max_concurrent is now in ``_config`` (``plugin._config.max_concurrent``)

        Provided for backward-comptibility
        """
        _deprecate("max_concurrent is now in _config (plugin._config.max_concurrent)")
        return self._config.max_concurrent

    @property
    def instance_name(self):
        """
        .. deprecated:: 3.0
           instance_name is now in ``_config`` (``plugin._config.instance_name``)

        Provided for backward-comptibility
        """
        _deprecate("instance_name is now in _config (plugin._config.instance_name)")
        return self._config.instance_name

    @property
    def connection_parameters(self):
        """
        .. deprecated:: 3.0
           connection_parameters has been removed. Please use ``_config``

        Provided for backward-comptibility
        """
        _deprecate("connection_parameters attribute was removed, please use '_config'")
        return {key: self._config[key] for key in _CONNECTION_SPEC}

    @property
    def metadata(self):
        """
        .. deprecated:: 3.0
           metadata is now part of the ``system`` attribute (``plugin.system.metadata``)

        Provided for backward-comptibility
        """
        _deprecate("metadata is a part of the system attribute (plugin.system.metadata")
        return self._system.metadata

    @property
    def bm_client(self):
        """
        .. deprecated:: 3.0
            bm_client attribute has been renamed to ``_ez_client``.

        Provided for backward-comptibility
        """
        _deprecate("bm_client attribute has been renamed to _ez_client")
        return self._ez_client

    @property
    def shutdown_event(self):
        """
        .. deprecated:: 3.0
            shutdown_event attribute has been renamed to ``_shutdown_event``.

        Provided for backward-comptibility
        """
        _deprecate("shutdown_event attribute has been renamed to _shutdown_event")
        return self._shutdown_event

    @property
    def logger(self):
        """
        .. deprecated:: 3.0
            logger attribute has been renamed to ``_logger``.

        Provided for backward-comptibility
        """
        _deprecate("logger attribute has been renamed to _logger")
        return self._logger
Ejemplo n.º 2
0
class Plugin(object):
    """A beer-garden Plugin.

    This class represents a beer-garden Plugin - a continuously-running process
    that can receive and process Requests.

    To work, a Plugin needs a Client instance - an instance of a class defining
    which Requests this plugin can accept and process. The easiest way to define
     a ``Client`` is by annotating a class with the ``@system`` decorator.

    When creating a Plugin you can pass certain keyword arguments to let the
    Plugin know how to communicate with the beer-garden instance. These are:

        - ``bg_host``
        - ``bg_port``
        - ``ssl_enabled``
        - ``ca_cert``
        - ``client_cert``
        - ``bg_url_prefix``

    A Plugin also needs some identifying data. You can either pass parameters to
    the Plugin or pass a fully defined System object (but not both). Note that
    some fields are optional::

        Plugin(
            name="Test",
            version="1.0.0",
            instance_name="default",
            description="A Test",
        )

    or::

        the_system = System(
            name="Test",
            version="1.0.0",
            instance_name="default,
            description="A Test",
        )
        Plugin(system=the_system)

    If passing parameters directly note that these fields are required:

    name
        Environment variable ``BG_NAME`` will be used if not specified

    version
        Environment variable ``BG_VERSION`` will be used if not specified

    instance_name
        Environment variable ``BG_INSTANCE_NAME`` will be used if not specified.
        'default' will be used if not specified and loading from environment
        variable was unsuccessful

    And these fields are optional:

    - description  (Will use docstring summary line from Client if unspecified)
    - icon_name
    - metadata
    - display_name

    Plugins service requests using a
    :py:class:`concurrent.futures.ThreadPoolExecutor`. The maximum number of
    threads available is controlled by the max_concurrent argument (the
    'multithreaded' argument has been deprecated).

    .. warning::
        The default value for ``max_concurrent`` is 1. This means that a Plugin
        that invokes a Command on itself in the course of processing a Request
        will deadlock! If you intend to do this, please set ``max_concurrent``
        to a value that makes sense and be aware that Requests are processed in
        separate thread contexts!

    :param client: Instance of a class annotated with @system.
    :param str bg_host: Hostname of a beer-garden.
    :param int bg_port: Port beer-garden is listening on.
    :param bool ssl_enabled: Whether to use SSL for beer-garden communication.
    :param ca_cert: Certificate that issued the server certificate used by the
        beer-garden server.
    :param client_cert: Certificate used by the server making the connection to
        beer-garden.
    :param system: The system definition.
    :param name: The system name.
    :param description: The system description.
    :param version: The system version.
    :param icon_name: The system icon name.
    :param str instance_name: The name of the instance.
    :param logger: A logger that will be used by the Plugin.
    :type logger: :py:class:`logging.Logger`.
    :param parser: The parser to use when communicating with beer-garden.
    :type parser: :py:class:`brewtils.schema_parser.SchemaParser`.
    :param bool multithreaded: DEPRECATED Process requests in a separate thread.
    :param int worker_shutdown_timeout: Time to wait during shutdown to finish
        processing.
    :param dict metadata: Metadata specific to this plugin.
    :param int max_concurrent: Maximum number of requests to process
        concurrently.
    :param str bg_url_prefix: URL Prefix beer-garden is on.
    :param str display_name: The display name to use for the system.
    :param int max_attempts: Number of times to attempt updating the request
        before giving up (default -1 aka never).
    :param int max_timeout: Maximum amount of time to wait before retrying to
        update a request.
    :param int starting_timeout: Initial time to wait before the first retry.
    :param int mq_max_attempts: Number of times to attempt reconnection to message queue
        before giving up (default -1 aka never).
    :param int mq_max_timeout: Maximum amount of time to wait before retrying to
        connect to message queue.
    :param int mq_starting_timeout: Initial time to wait before the first message queue
        connection retry.
    :param int max_instances: Max number of instances allowed for the system.
    :param bool ca_verify: Verify server certificate when making a request.
    :param str username: The username for Beergarden authentication
    :param str password: The password for Beergarden authentication
    :param access_token: Access token for Beergarden authentication
    :param refresh_token: Refresh token for Beergarden authentication
    """
    def __init__(self,
                 client,
                 bg_host=None,
                 bg_port=None,
                 ssl_enabled=None,
                 ca_cert=None,
                 client_cert=None,
                 system=None,
                 name=None,
                 description=None,
                 version=None,
                 icon_name=None,
                 instance_name=None,
                 logger=None,
                 parser=None,
                 multithreaded=None,
                 metadata=None,
                 max_concurrent=None,
                 bg_url_prefix=None,
                 **kwargs):
        # If a logger is specified or the logging module already has additional
        # handlers then we assume that logging has already been configured
        if logger or len(logging.getLogger(__name__).root.handlers) > 0:
            self.logger = logger or logging.getLogger(__name__)
            self._custom_logger = True
        else:
            logging.config.dictConfig(DEFAULT_LOGGING_CONFIG)
            self.logger = logging.getLogger(__name__)
            self._custom_logger = False

        connection_parameters = brewtils.get_connection_info(
            bg_host=bg_host,
            bg_port=bg_port,
            ssl_enabled=ssl_enabled,
            ca_cert=ca_cert,
            client_cert=client_cert,
            url_prefix=bg_url_prefix or kwargs.get("url_prefix", None),
            ca_verify=kwargs.get("ca_verify", None),
            username=kwargs.get("username", None),
            password=kwargs.get("password", None),
            client_timeout=kwargs.get("client_timeout", None),
        )
        self.bg_host = connection_parameters["bg_host"]
        self.bg_port = connection_parameters["bg_port"]
        self.ssl_enabled = connection_parameters["ssl_enabled"]
        self.ca_cert = connection_parameters["ca_cert"]
        self.client_cert = connection_parameters["client_cert"]
        self.bg_url_prefix = connection_parameters["url_prefix"]
        self.ca_verify = connection_parameters["ca_verify"]

        self.max_attempts = kwargs.get("max_attempts", -1)
        self.max_timeout = kwargs.get("max_timeout", 30)
        self.starting_timeout = kwargs.get("starting_timeout", 5)

        self._mq_max_attempts = kwargs.get("mq_max_attempts", -1)
        self._mq_max_timeout = kwargs.get("mq_max_timeout", 30)
        self._mq_starting_timeout = kwargs.get("mq_starting_timeout", 5)
        self._mq_retry_attempt = 0
        self._mq_timeout = self._mq_starting_timeout

        self.max_concurrent = self._setup_max_concurrent(
            multithreaded, max_concurrent)
        self.instance_name = instance_name or os.environ.get(
            "BG_INSTANCE_NAME", "default")
        self.metadata = metadata or {}

        self.instance = None
        self.queue_connection_params = None
        self.admin_consumer = None
        self.request_consumer = None
        self.connection_poll_thread = None
        self.client = client
        self.shutdown_event = threading.Event()
        self.parser = parser or SchemaParser()

        self.system = self._setup_system(
            client,
            self.instance_name,
            system,
            name,
            description,
            version,
            icon_name,
            self.metadata,
            kwargs.get("display_name", None),
            kwargs.get("max_instances", None),
        )

        self.unique_name = "%s[%s]-%s" % (
            self.system.name,
            self.instance_name,
            self.system.version,
        )

        # Tightly manage when we're in an 'error' state, aka Brew-view is down
        self.brew_view_error_condition = threading.Condition()
        self.brew_view_down = False

        self.pool = ThreadPoolExecutor(max_workers=self.max_concurrent)
        self.admin_pool = ThreadPoolExecutor(max_workers=1)

        self.bm_client = EasyClient(logger=self.logger,
                                    parser=self.parser,
                                    **connection_parameters)

    def run(self):
        # Let Beergarden know about our system and instance
        self._initialize()

        self.logger.debug("Creating and starting admin queue consumer")
        self.admin_consumer = self._create_admin_consumer()
        self.admin_consumer.start()

        self.logger.debug("Creating and starting request queue consumer")
        self.request_consumer = self._create_standard_consumer()
        self.request_consumer.start()

        self.logger.debug("Creating and starting connection poll thread")
        self.connection_poll_thread = self._create_connection_poll_thread()
        self.connection_poll_thread.start()

        self.logger.info("Plugin %s has started", self.unique_name)

        try:
            while not self.shutdown_event.wait(0.1):
                self._check_connection_poll_thread()
                self._check_consumers()
        except KeyboardInterrupt:
            self.logger.debug("Received KeyboardInterrupt - shutting down")
        except Exception as ex:
            self.logger.error(
                "Event loop terminated unexpectedly - shutting down")
            self.logger.exception(ex)

        self.logger.debug("About to shut down plugin %s", self.unique_name)
        self._shutdown()

        self.logger.info("Plugin %s has terminated", self.unique_name)

    def process_message(self, target, request, headers):
        """Process a message. Intended to be run on an Executor.

        :param target: The object to invoke received commands on.
            (self or self.client)
        :param request: The parsed Request object
        :param headers: Dictionary of headers from the
            `brewtils.request_consumer.RequestConsumer`
        :return: None
        """
        request.status = "IN_PROGRESS"
        self._update_request(request, headers)

        try:
            # Set request context so this request will be the parent of any
            # generated requests and update status We also need the host/port of
            #  the current plugin. We currently don't support parent/child
            # requests across different servers.
            request_context.current_request = request
            request_context.bg_host = self.bg_host
            request_context.bg_port = self.bg_port

            output = self._invoke_command(target, request)
        except Exception as ex:
            self.logger.log(
                getattr(ex, "_bg_error_log_level", logging.ERROR),
                "Plugin %s raised an exception while processing request %s: %s",
                self.unique_name,
                str(request),
                ex,
                exc_info=not getattr(ex, "_bg_suppress_stacktrace", False),
            )
            request.status = "ERROR"
            request.output = self._format_error_output(request, ex)
            request.error_class = type(ex).__name__
        else:
            request.status = "SUCCESS"
            request.output = self._format_output(output)

        self._update_request(request, headers)

    def process_request_message(self, message, headers):
        """Processes a message from a RequestConsumer

        :param message: A valid string representation of a
            `brewtils.models.Request`
        :param headers: A dictionary of headers from the
            `brewtils.request_consumer.RequestConsumer`
        :return: A `concurrent.futures.Future`
        """

        request = self._pre_process(message)

        # This message has already been processed, all it needs to do is update
        if request.status in Request.COMPLETED_STATUSES:
            return self.pool.submit(self._update_request, request, headers)
        else:
            return self.pool.submit(self.process_message, self.client, request,
                                    headers)

    def process_admin_message(self, message, headers):

        # Admin requests won't have a system, so don't verify it
        request = self._pre_process(message, verify_system=False)

        return self.admin_pool.submit(self.process_message, self, request,
                                      headers)

    def _pre_process(self, message, verify_system=True):

        if self.shutdown_event.is_set():
            raise RequestProcessingError(
                "Unable to process message - currently shutting down")

        try:
            request = self.parser.parse_request(message, from_string=True)
        except Exception as ex:
            self.logger.exception(
                "Unable to parse message body: {0}. Exception: {1}".format(
                    message, ex))
            raise DiscardMessageException("Error parsing message body")

        if (verify_system and request.command_type
                and request.command_type.upper() != "EPHEMERAL"
                and request.system.upper() != self.system.name.upper()):
            raise DiscardMessageException(
                "Received message for a different system {0}".format(
                    request.system.upper()))

        return request

    def _initialize(self):
        self.logger.debug("Initializing plugin %s", self.unique_name)

        # TODO: We should use self.bm_client.upsert_system once it is supported
        existing_system = self.bm_client.find_unique_system(
            name=self.system.name, version=self.system.version)

        if existing_system:
            if not existing_system.has_instance(self.instance_name):
                if len(existing_system.instances
                       ) < existing_system.max_instances:
                    existing_system.instances.append(
                        Instance(name=self.instance_name))
                    self.bm_client.create_system(existing_system)
                else:
                    raise PluginValidationError(
                        'Unable to create plugin with instance name "%s": '
                        'System "%s[%s]" has an instance limit of %d and '
                        "existing instances %s" % (
                            self.instance_name,
                            existing_system.name,
                            existing_system.version,
                            existing_system.max_instances,
                            ", ".join(existing_system.instance_names),
                        ))

            # We always update in case the metadata has changed.
            self.system = self.bm_client.update_system(
                existing_system.id,
                new_commands=self.system.commands,
                metadata=self.system.metadata,
                description=self.system.description,
                display_name=self.system.display_name,
                icon_name=self.system.icon_name,
            )
        else:
            self.system = self.bm_client.create_system(self.system)

        # Sanity check to make sure an instance with this name was registered
        if self.system.has_instance(self.instance_name):
            instance_id = self.system.get_instance(self.instance_name).id
        else:
            raise PluginValidationError(
                'Unable to find registered instance with name "%s"' %
                self.instance_name)

        self.instance = self.bm_client.initialize_instance(instance_id)

        self.queue_connection_params = self.instance.queue_info.get(
            "connection", None)
        if self.queue_connection_params and self.queue_connection_params.get(
                "ssl", None):
            self.queue_connection_params["ssl"].update({
                "ca_cert":
                self.ca_cert,
                "ca_verify":
                self.ca_verify,
                "client_cert":
                self.client_cert,
            })

        self.logger.debug("Plugin %s is initialized", self.unique_name)

    def _shutdown(self):
        self.shutdown_event.set()

        self.logger.debug("About to stop message consuming")
        self.request_consumer.stop_consuming()
        self.admin_consumer.stop_consuming()

        self.logger.debug("About to wake sleeping request processing threads")
        with self.brew_view_error_condition:
            self.brew_view_error_condition.notify_all()

        self.logger.debug("Shutting down request processing pool")
        self.pool.shutdown(wait=True)
        self.logger.debug("Shutting down admin processing pool")
        self.admin_pool.shutdown(wait=True)

        self.logger.debug("Attempting to stop request queue consumer")
        self.request_consumer.stop()
        self.request_consumer.join()

        self.logger.debug("Attempting to stop admin queue consumer")
        self.admin_consumer.stop()
        self.admin_consumer.join()

        try:
            self.bm_client.update_instance_status(self.instance.id, "STOPPED")
        except Exception:
            self.logger.warning(
                "Unable to notify Beer-garden that this plugin is STOPPED, so this "
                "plugin's status may be incorrect in Beer-garden")

        self.logger.debug("Successfully shutdown plugin {0}".format(
            self.unique_name))

    def _create_standard_consumer(self):
        return RequestConsumer(
            thread_name="Request Consumer",
            connection_info=self.queue_connection_params,
            amqp_url=self.instance.queue_info.get("url", None),
            queue_name=self.instance.queue_info["request"]["name"],
            on_message_callback=self.process_request_message,
            panic_event=self.shutdown_event,
            max_concurrent=self.max_concurrent,
        )

    def _create_admin_consumer(self):
        return RequestConsumer(
            thread_name="Admin Consumer",
            connection_info=self.queue_connection_params,
            amqp_url=self.instance.queue_info.get("url", None),
            queue_name=self.instance.queue_info["admin"]["name"],
            on_message_callback=self.process_admin_message,
            panic_event=self.shutdown_event,
            max_concurrent=1,
            logger=logging.getLogger("brewtils.admin_consumer"),
        )

    def _create_connection_poll_thread(self):
        connection_poll_thread = threading.Thread(target=self._connection_poll)
        connection_poll_thread.daemon = True
        return connection_poll_thread

    def _invoke_command(self, target, request):
        """Invoke the function named in request.command.

        :param target: The object to search for the function implementation.
            Will be self or self.client.
        :param request: The request to process
        :raise RequestProcessingError: The specified target does not define a
            callable implementation of request.command
        :return: The output of the function invocation
        """
        if not callable(getattr(target, request.command, None)):
            raise RequestProcessingError(
                "Could not find an implementation of command '%s'" %
                request.command)

        # It's kinda weird that we need to add the object arg only if we're
        # trying to call a function on self. In both cases the function object
        # is bound... think it has something to do with our decorators
        args = [self] if target is self else []
        return getattr(target, request.command)(*args, **request.parameters)

    def _update_request(self, request, headers):
        """Sends a Request update to beer-garden

        Ephemeral requests do not get updated, so we simply skip them.

        If brew-view appears to be down, it will wait for brew-view to come back
         up before updating.

        If this is the final attempt to update, we will attempt a known, good
        request to give some information to the user. If this attempt fails
        then we simply discard the message

        :param request: The request to update
        :param headers: A dictionary of headers from
            `brewtils.request_consumer.RequestConsumer`
        :raise RepublishMessageException: The Request update failed (any reason)
        :return: None
        """

        if request.is_ephemeral:
            sys.stdout.flush()
            return

        with self.brew_view_error_condition:

            self._wait_for_brew_view_if_down(request)

            try:
                if not self._should_be_final_attempt(headers):
                    self._wait_if_not_first_attempt(headers)
                    self.bm_client.update_request(
                        request.id,
                        status=request.status,
                        output=request.output,
                        error_class=request.error_class,
                    )
                else:
                    self.bm_client.update_request(
                        request.id,
                        status="ERROR",
                        output="We tried to update the request, but it failed "
                        "too many times. Please check the plugin logs "
                        "to figure out why the request update failed. "
                        "It is possible for this request to have "
                        "succeeded, but we cannot update beer-garden "
                        "with that information.",
                        error_class=BGGivesUpError.__name__,
                    )
            except Exception as ex:
                self._handle_request_update_failure(request, headers, ex)
            finally:
                sys.stdout.flush()

    def _wait_if_not_first_attempt(self, headers):
        if headers.get("retry_attempt", 0) > 0:
            time_to_sleep = min(
                headers.get("time_to_wait", self.starting_timeout),
                self.max_timeout)
            self.shutdown_event.wait(time_to_sleep)

    def _handle_request_update_failure(self, request, headers, exc):

        # If brew-view is down, we always want to try again
        # Yes, even if it is the 'final_attempt'
        if isinstance(exc, (RequestsConnectionError, RestConnectionError)):
            self.brew_view_down = True
            self.logger.error(
                "Error updating request status: {0} exception: {1}".format(
                    request.id, exc))
            raise RepublishRequestException(request, headers)

        elif isinstance(exc, TooLargeError):
            self.logger.error(
                "Error updating request {0} - the request exceeds the 16MB size "
                "limitation. The status of this request will be marked as ERROR, but "
                "it's possible the request actually completed successfully. If this "
                "happens often please contact the plugin developer.".format(
                    request.id))

            raise RepublishRequestException(
                Request(
                    id=request.id,
                    status="ERROR",
                    output="Request size greater than 16MB",
                    error_class=BGGivesUpError.__name__,
                ),
                headers,
            )

        elif isinstance(exc, RestClientError):
            message = (
                "Error updating request {0} and it is a client error. Probable "
                "cause is that this request is already updated. In which case, "
                "ignore this message. If request {0} did not complete, please "
                "file an issue. Discarding request to avoid an infinte loop. "
                "exception: {1}".format(request.id, exc))
            self.logger.error(message)
            raise DiscardMessageException(message)

        # Time to discard the message because we've given up
        elif self._should_be_final_attempt(headers):
            message = (
                "Could not update request {0} even with a known good status, "
                "output and error_class. We have reached the final attempt and "
                "will now discard the message. Attempted to process this "
                "message {1} times".format(request.id,
                                           headers["retry_attempt"]))
            self.logger.error(message)
            raise DiscardMessageException(message)

        else:
            self._update_retry_attempt_information(headers)
            self.logger.exception(
                "Error updating request (Attempt #{0}: request: {1} exception: "
                "{2}".format(headers.get("retry_attempt", 0), request.id, exc))
            raise RepublishRequestException(request, headers)

    def _update_retry_attempt_information(self, headers):
        headers["retry_attempt"] = headers.get("retry_attempt", 0) + 1
        headers["time_to_wait"] = min(
            headers.get("time_to_wait", self.starting_timeout // 2) * 2,
            self.max_timeout,
        )

    def _should_be_final_attempt(self, headers):
        if self.max_attempts <= 0:
            return False

        return self.max_attempts <= headers.get("retry_attempt", 0)

    def _wait_for_brew_view_if_down(self, request):
        if self.brew_view_down and not self.shutdown_event.is_set():
            self.logger.warning(
                "Currently unable to communicate with Brew-view, about to wait "
                "until connection is reestablished to update request %s",
                request.id,
            )
            self.brew_view_error_condition.wait()

    def _check_connection_poll_thread(self):
        """Ensure the connection poll thread is alive"""
        if not self.connection_poll_thread.isAlive():
            self.logger.warning(
                "Looks like connection poll thread has died - attempting to restart"
            )
            self.connection_poll_thread = self._create_connection_poll_thread()
            self.connection_poll_thread.start()

    def _check_consumers(self):
        """Ensure the RequestConsumers are both alive"""
        if self.admin_consumer.is_connected(
        ) and self.request_consumer.is_connected():
            if self._mq_retry_attempt != 0:
                self.logger.info("Admin and request consumer connections OK")

            self._mq_retry_attempt = 0
            self._mq_timeout = self._mq_starting_timeout
        else:
            if 0 < self._mq_max_attempts < self._mq_retry_attempt:
                self.logger.warning(
                    "Max consumer connection failures, shutting down")
                self.shutdown_event.set()
                return

            if not self.admin_consumer.is_connected():
                self.logger.warning("Looks like admin consumer has died")
            if not self.request_consumer.is_connected():
                self.logger.warning("Looks like request consumer has died")

            self.logger.warning("Waiting %i seconds before restart",
                                self._mq_timeout)
            self.shutdown_event.wait(self._mq_timeout)

            if not self.admin_consumer.is_connected():
                self.admin_consumer = self._create_admin_consumer()
                self.admin_consumer.start()

            if not self.request_consumer.is_connected():
                self.request_consumer = self._create_standard_consumer()
                self.request_consumer.start()

            self._mq_timeout = min(self._mq_timeout * 2, self._mq_max_timeout)
            self._mq_retry_attempt += 1

    def _start(self, request):
        """Handle start message by marking this instance as running.

        :param request: The start message
        :return: Success output message
        """
        self.instance = self.bm_client.update_instance_status(
            self.instance.id, "RUNNING")

        return "Successfully started plugin"

    def _stop(self, request):
        """Handle stop message by marking this instance as stopped.

        :param request: The stop message
        :return: Success output message
        """
        self.shutdown_event.set()

        return "Successfully stopped plugin"

    def _status(self, request):
        """Handle status message by sending a heartbeat.

        :param request: The status message
        :return: None
        """
        with self.brew_view_error_condition:
            if not self.brew_view_down:
                try:
                    self.bm_client.instance_heartbeat(self.instance.id)
                except (RequestsConnectionError, RestConnectionError):
                    self.brew_view_down = True
                    raise

    def _setup_max_concurrent(self, multithreaded, max_concurrent):
        """Determine correct max_concurrent value.
        Will be unnecessary when multithreaded flag is removed."""
        if multithreaded is not None:
            warnings.warn(
                "Keyword argument 'multithreaded' is deprecated and will be "
                "removed in version 3.0, please use 'max_concurrent' instead.",
                DeprecationWarning,
                stacklevel=2,
            )

            # Both multithreaded and max_concurrent kwargs explicitly set
            # check for mutually exclusive settings
            if max_concurrent is not None:
                if multithreaded is True and max_concurrent == 1:
                    self.logger.warning(
                        "Plugin created with multithreaded=True and "
                        "max_concurrent=1, ignoring 'multithreaded' argument")
                elif multithreaded is False and max_concurrent > 1:
                    self.logger.warning(
                        "Plugin created with multithreaded=False and "
                        "max_concurrent>1, ignoring 'multithreaded' argument")

                return max_concurrent
            else:
                return 5 if multithreaded else 1
        else:
            if max_concurrent is None:
                warnings.warn(
                    "Heads up - in 3.0 the default plugin behavior is changing "
                    "from handling requests one at a time to handling multiple "
                    "at once. If this plugin needs to maintain the old "
                    "behavior just set max_concurrent=1 when creating the "
                    "plugin.",
                    PendingDeprecationWarning,
                    stacklevel=2,
                )
                return 1

            return max_concurrent

    def _setup_system(
        self,
        client,
        inst_name,
        system,
        name,
        description,
        version,
        icon_name,
        metadata,
        display_name,
        max_instances,
    ):
        if system:
            if (name or description or version or icon_name or display_name
                    or max_instances):
                raise ValidationError(
                    "Sorry, you can't specify a system as well as system "
                    "creation helper keywords (name, description, version, "
                    "max_instances, display_name, and icon_name)")

            if client._bg_name or client._bg_version:
                raise ValidationError(
                    "Sorry, you can't specify a system as well as system "
                    "info in the @system decorator (bg_name, bg_version)")

            if not system.instances:
                raise ValidationError(
                    "Explicit system definition requires explicit instance "
                    "definition (use instances=[Instance(name='default')] for "
                    "default behavior)")

            if not system.max_instances:
                system.max_instances = len(system.instances)

        else:
            name = name or os.environ.get("BG_NAME", None) or client._bg_name
            version = (version or os.environ.get("BG_VERSION", None)
                       or client._bg_version)

            if client.__doc__ and not description:
                description = self.client.__doc__.split("\n")[0]

            system = System(
                name=name,
                description=description,
                version=version,
                icon_name=icon_name,
                commands=client._commands,
                max_instances=max_instances or 1,
                instances=[Instance(name=inst_name)],
                metadata=metadata,
                display_name=display_name,
            )

        return system

    def _connection_poll(self):
        """Periodically attempt to re-connect to beer-garden"""

        while not self.shutdown_event.wait(5):
            with self.brew_view_error_condition:
                if self.brew_view_down:
                    try:
                        self.bm_client.get_version()
                    except Exception:
                        self.logger.debug(
                            "Attempt to reconnect to Brew-view failed")
                    else:
                        self.logger.info(
                            "Brew-view connection reestablished, about to "
                            "notify any waiting requests")
                        self.brew_view_down = False
                        self.brew_view_error_condition.notify_all()

    @staticmethod
    def _format_error_output(request, exc):
        if request.is_json:
            return parse_exception_as_json(exc)
        else:
            return str(exc)

    @staticmethod
    def _format_output(output):
        """Formats output from Plugins to prevent validation errors"""

        if isinstance(output, six.string_types):
            return output

        try:
            return json.dumps(output)
        except (TypeError, ValueError):
            return str(output)
Ejemplo n.º 3
0
class SystemClient(object):
    """High-level client for generating requests for a beer-garden System.

    SystemClient creation:
        This class is intended to be the main way to create beer-garden requests. Create an
        instance with beer-garden connection information (optionally including a url_prefix) and
        a system name::

            client = SystemClient(host, port, 'example_system', ssl_enabled=True, url_prefix=None)

        Pass additional keyword arguments for more granularity:

            version_constraint:
                Allows specifying a particular system version. Can be a version literal ('1.0.0')
                or the special value 'latest.' Using 'latest' will allow the the SystemClient to
                retry a request if it fails due to a missing system (see Creating Requests).

            default_instance:
                The instance name to use when creating a request if no other instance name is
                specified. Since each request must be addressed to a specific instance this is a
                convenience to prevent needing to specify the 'default' instance for each request.

            always_update:
                Always attempt to reload the system definition before making a request. This is
                useful to ensure Requests are always made against the latest version of the system.
                If not set the System definition will be loaded once (upon making the first
                request) and then only reloaded if a Request fails.

    Loading the System:
        The System definition is lazily loaded, so nothing happens until the first attempt to send
        a Request. At that point the SystemClient will query beer-garden to get a system definition
        that matches the system_name and version_constraint. If no matching system can be found a
        FetchError will be raised. If always_update was set to True this will happen
        before making each request, not just the first.

    Making a Request:
        The standard way to create and send requests is by calling object attributes::

            request = client.example_command(param_1='example_param')

        In the normal case this will block until the request completes. Request completion is
        determined by periodically polling beer-garden to check the Request status. The time
        between polling requests starts at 0.5s and doubles each time the request has still not
        completed, up to max_delay. If a timeout was specified and the Request has not completed
        within that time a ``ConnectionTimeoutError`` will be raised.

        It is also possible to create the SystemClient in non-blocking mode by specifying
        blocking=False. In this case the request creation will immediately return a Future and
        will spawn a separate thread to poll for Request completion. The max_concurrent parameter
        is used to control the maximum threads available for polling.

        .. code-block:: python

            # Create a SystemClient with blocking=False
            client = SystemClient(host, port, 'example_system', ssl_enabled=True, blocking=False)

            # Create and send 5 requests without waiting for request completion
            futures = [client.example_command(param_1=number) for number in range(5)]

            # Now wait on all requests to complete
            concurrent.futures.wait(futures)

        If the request creation process fails (e.g. the command failed validation) and
        version_constraint is 'latest' then the SystemClient will check to see if a different
        version is available, and if so it will attempt to make the request on that version.
        This is so users of the SystemClient that don't necessarily care about the target system
        version don't need to be restarted if the target system is updated.

    Tweaking beer-garden Request Parameters:
        There are several parameters that control how beer-garden routes / processes a request. To
        denote these as intended for beer-garden itself (rather than a parameter to be passed to
        the Plugin) prepend a leading underscore to the argument name.

        Sending to another instance::

            request = client.example_command(_instance_name='instance_2', param_1='example_param')

        Request with a comment::

            request = client.example_command(_comment='I'm a beer-garden comment!',
                                             param_1='example_param')

        Without the leading underscore the arguments would be treated the same as param_1 -
        another parameter to be passed to the plugin.

    :param host: beer-garden REST API hostname.
    :param port: beer-garden REST API port.
    :param system_name: The name of the system to use.
    :param version_constraint: The system version to use. Can be specific or 'latest'.
    :param default_instance: The instance to use if not specified when creating a request.
    :param always_update: Should check for a newer System version before each request.
    :param timeout: Length of time to wait for a request to complete. 'None' means wait forever.
    :param max_delay: Maximum time to wait between checking the status of a created request.
    :param api_version: beer-garden API version.
    :param ssl_enabled: Flag indicating whether to use HTTPS when communicating with beer-garden.
    :param ca_cert: beer-garden REST API server CA certificate.
    :param blocking: Block after request creation until the request completes.
    :param max_concurrent: Maximum number of concurrent requests allowed.
    :param client_cert: The client certificate to use when making requests.
    :param url_prefix: beer-garden REST API URL Prefix.
    :param ca_verify: Flag indicating whether to verify server certificate when making a request.
    :param raise_on_error: Raises an error if the request ends in an error state.
    :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,
                 system_name=None,
                 version_constraint='latest',
                 default_instance='default',
                 always_update=False,
                 timeout=None,
                 max_delay=30,
                 api_version=None,
                 ssl_enabled=False,
                 ca_cert=None,
                 blocking=True,
                 max_concurrent=None,
                 client_cert=None,
                 url_prefix=None,
                 ca_verify=True,
                 raise_on_error=False,
                 **kwargs):
        self._system_name = system_name
        self._version_constraint = version_constraint
        self._default_instance = default_instance
        self._always_update = always_update
        self._timeout = timeout
        self._max_delay = max_delay
        self._blocking = blocking
        self._raise_on_error = raise_on_error
        self._bg_host = bg_host or kwargs.get('host')
        self._bg_port = bg_port or kwargs.get('port')
        self.logger = logging.getLogger(__name__)

        self._loaded = False
        self._system = None
        self._commands = None

        # This is for Python 3.4 compatibility - max_workers MUST be non-None
        # in that version. This logic is what was added in Python 3.5
        if max_concurrent is None:
            max_concurrent = (cpu_count() or 1) * 5
        self._thread_pool = ThreadPoolExecutor(max_workers=max_concurrent)

        self._easy_client = EasyClient(bg_host=self._bg_host,
                                       bg_port=self._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 __getattr__(self, item):
        """Standard way to create and send beer-garden requests"""
        return self.create_bg_request(item)

    def create_bg_request(self, command_name, **kwargs):
        """Create a callable that will execute a beer-garden request when called.

        Normally you interact with the SystemClient by accessing attributes, but there could be
        certain cases where you want to create a request without sending it.

        Example::

            client = SystemClient(host, port, 'system', blocking=False)
            requests = []

            # No arguments
            requests.append(client.create_bg_request('command_1'))

            # arg_1 will be passed as a parameter
            requests.append(client.create_bg_request('command_2', arg_1='Hi!'))

            futures = [request() for request in requests]   # Calling creates and sends the request
            concurrent.futures.wait(futures)                # Wait for all the futures to complete

        :param command_name: The name of the command that will be sent.
        :param kwargs: Additional arguments to pass to send_bg_request.
        :raise AttributeError: The system does not have a command with the given command_name.
        :return: A partial that will create and execute a beer-garden request when called.
        """

        if not self._loaded or self._always_update:
            self.load_bg_system()

        if command_name in self._commands:
            return partial(
                self.send_bg_request,
                _command=command_name,
                _system_name=self._system.name,
                _system_version=self._system.version,
                _system_display=self._system.display_name,
                _output_type=self._commands[command_name].output_type,
                _instance_name=self._default_instance,
                **kwargs)
        else:
            raise AttributeError(
                "System '%s' version '%s' has no command named '%s'" %
                (self._system.name, self._system.version, command_name))

    def send_bg_request(self, **kwargs):
        """Actually create a Request and send it to beer-garden

        .. note::
            This method is intended for advanced use only, mainly cases where you're using the
            SystemClient without a predefined System. It assumes that everything needed to
            construct the request is being passed in kwargs. If this doesn't sound like what you
            want you should check out create_bg_request.

        :param kwargs: All necessary request parameters, including beer-garden internal parameters
        :raise ValidationError: If the Request creation failed validation on the server
        :return: If the SystemClient was created with blocking=True a completed request object,
            otherwise a Future that will return the Request when it completes.
        """
        # Need to pop here, otherwise we'll try to send as a request parameter
        raise_on_error = kwargs.pop('_raise_on_error', self._raise_on_error)
        blocking = kwargs.pop('_blocking', self._blocking)
        timeout = kwargs.pop('_timeout', self._timeout)

        # If the request fails validation and the version constraint allows,
        # check for a new version and retry
        try:
            request = self._construct_bg_request(**kwargs)
            request = self._easy_client.create_request(request,
                                                       blocking=blocking,
                                                       timeout=timeout)
        except ValidationError:
            if self._system and self._version_constraint == 'latest':
                old_version = self._system.version

                self.load_bg_system()

                if old_version != self._system.version:
                    kwargs['_system_version'] = self._system.version
                    return self.send_bg_request(**kwargs)
            raise

        # If not blocking just return the future
        if not blocking:
            return self._thread_pool.submit(self._wait_for_request, request,
                                            raise_on_error, timeout)

        # Brew-view before 2.4 doesn't support the blocking flag, so make sure
        # the request is actually complete before returning
        return self._wait_for_request(request, raise_on_error, timeout)

    def load_bg_system(self):
        """Query beer-garden for a System definition

        This method will make the query to beer-garden for a System matching the name and version
        constraints specified during SystemClient instance creation.

        If this method completes successfully the SystemClient will be ready to create and send
        Requests.

        :raise FetchError: If unable to find a matching System
        :return: None
        """

        if self._version_constraint == 'latest':
            systems = self._easy_client.find_systems(name=self._system_name)
            self._system = sorted(systems,
                                  key=lambda x: x.version,
                                  reverse=True)[0] if systems else None
        else:
            self._system = self._easy_client.find_unique_system(
                name=self._system_name, version=self._version_constraint)

        if self._system is None:
            raise FetchError(
                "Beer-garden has no system named '%s' with a version matching '%s'"
                % (self._system_name, self._version_constraint))

        self._commands = {
            command.name: command
            for command in self._system.commands
        }
        self._loaded = True

    def _wait_for_request(self, request, raise_on_error, timeout):
        """Poll the server until the request is completed or errors"""

        delay_time = 0.5
        total_wait_time = 0
        while request.status not in Request.COMPLETED_STATUSES:

            if timeout and total_wait_time > timeout:
                raise TimeoutExceededError("Timeout waiting for request '%s' "
                                           "to complete" % str(request))

            time.sleep(delay_time)
            total_wait_time += delay_time
            delay_time = min(delay_time * 2, self._max_delay)

            request = self._easy_client.find_unique_request(id=request.id)

        if raise_on_error and request.status == 'ERROR':
            raise RequestFailedError(request)

        return request

    def _get_parent_for_request(self):
        parent = getattr(request_context, 'current_request', None)
        if parent is None:
            return None

        if (request_context.bg_host.upper() != self._bg_host.upper()
                or request_context.bg_port != self._bg_port):
            self.logger.warning(
                "A parent request was found, but the destination beer-garden "
                "appears to be different than the beer-garden to which this plugin "
                "is assigned. Cross-server parent/child requests are not supported "
                "at this time. Removing the parent context so the request doesn't "
                "fail.")
            return None

        return Request(id=str(parent.id))

    def _construct_bg_request(self, **kwargs):
        """Create a request that can be used with EasyClient.create_request"""

        command = kwargs.pop('_command', None)
        system_name = kwargs.pop('_system_name', None)
        system_version = kwargs.pop('_system_version', None)
        system_display = kwargs.pop('_system_display', None)
        instance_name = kwargs.pop('_instance_name', None)
        comment = kwargs.pop('_comment', None)
        output_type = kwargs.pop('_output_type', None)
        metadata = kwargs.pop('_metadata', {})

        parent = self._get_parent_for_request()

        if system_display:
            metadata['system_display_name'] = system_display

        if command is None:
            raise ValidationError('Unable to send a request with no command')
        if system_name is None:
            raise ValidationError(
                'Unable to send a request with no system name')
        if system_version is None:
            raise ValidationError(
                'Unable to send a request with no system version')
        if instance_name is None:
            raise ValidationError(
                'Unable to send a request with no instance name')

        return Request(command=command,
                       system=system_name,
                       system_version=system_version,
                       instance_name=instance_name,
                       comment=comment,
                       output_type=output_type,
                       parent=parent,
                       metadata=metadata,
                       parameters=kwargs)
Ejemplo n.º 4
0
class SystemClient(object):
    """High-level client for generating requests for a Beer-garden System.

    SystemClient creation:
        This class is intended to be the main way to create Beer-garden requests. Create
        an instance with Beer-garden connection information and a system name::

            client = SystemClient(
                system_name='example_system',
                system_namespace='default',
                bg_host="host",
                bg_port=2337,
            )

        Note: Passing an empty string as the system_namespace parameter will evalutate
        to the local garden's default namespace.

        Pass additional keyword arguments for more granularity:

            version_constraint:
                Allows specifying a particular system version. Can be a version literal
                ('1.0.0') or the special value 'latest.' Using 'latest' will allow the
                SystemClient to retry a request if it fails due to a missing system
                (see Creating Requests).

            default_instance:
                The instance name to use when creating a request if no other instance
                name is specified. Since each request must be addressed to a specific
                instance this is a convenience to prevent needing to specify the
                instance for each request.

            always_update:
                If True the SystemClient will always attempt to reload the system
                definition before making a request. This is useful to ensure Requests
                are always made against the latest version of the system.
                If not set the System definition will be loaded when making the first
                request and will only be reloaded if a Request fails.

    Loading the System:
        The System definition is lazily loaded, so nothing happens until the first
        attempt to send a Request. At that point the SystemClient will query Beer-garden
        to get a system definition that matches the system_name and version_constraint.
        If no matching System can be found a FetchError will be raised. If always_update
        was set to True this will happen before making each request, not only the first.

    Making a Request:
        The standard way to create and send requests is by calling object attributes::

            request = client.example_command(param_1='example_param')

        In the normal case this will block until the request completes. Request
        completion is determined by periodically polling Beer-garden to check the
        Request status. The time between polling requests starts at 0.5s and doubles
        each time the request has still not completed, up to max_delay. If a timeout was
        specified and the Request has not completed within that time a
        ``ConnectionTimeoutError`` will be raised.

        It is also possible to create the SystemClient in non-blocking mode by
        specifying blocking=False. In this case the request creation will immediately
        return a Future and will spawn a separate thread to poll for Request completion.
        The max_concurrent parameter is used to control the maximum threads available
        for polling.

        .. code-block:: python

            # Create a SystemClient with blocking=False
            client = SystemClient(
                system_name='example_system',
                system_namespace='default',
                bg_host="localhost",
                bg_port=2337,
                blocking=False,
            )

            # Create and send 5 requests without waiting for request completion
            futures = [client.example_command(param_1=number) for number in range(5)]

            # Now wait on all requests to complete
            concurrent.futures.wait(futures)

        If the request creation process fails (e.g. the command failed validation) and
        version_constraint is 'latest' then the SystemClient will check to see if a
        newer version is available, and if so it will attempt to make the request on
        that version. This is so users of the SystemClient that don't necessarily care
        about the target system version don't need to be restarted every time the target
        system is updated.

        It's also possible to control what happens when a Request results in an ERROR.
        If the ``raise_on_error`` parameter is set to False (the default) then Requests
        that are not successful simply result in a Request with a status of ERROR, and
        it is the plugin developer's responsibility to check for this case. However, if
        ``raise_on_error`` is set to True then this will result in a
        ``RequestFailedError`` being raised. This will happen regardless of the value
        of the ``blocking`` flag.

    Tweaking Beer-garden Request Parameters:
        There are several parameters that control how beer-garden routes / processes a
        request. To denote these as intended for Beer-garden itself (rather than a
        parameter to be passed to the Plugin) prepend a leading underscore to the
        argument name.

        Sending to another instance::

            request = client.example_command(
                _instance_name="instance_2", param_1="example_param"
            )

        Request with a comment::

            request = client.example_command(
                _comment="I'm a beer-garden comment!", param_1="example_param"
            )

        Without the leading underscore the arguments would be treated the same as
        "param_1" - another parameter to be passed to the plugin.

        Request that raises::

            client = SystemClient(
                system_name="foo",
                system_namespace='default',
                bg_host="localhost",
                bg_port=2337,
            )

            try:
                client.command_that_errors(_raise_on_error=True)
            except RequestFailedError:
                print("I could have just ignored this")

    Args:
        system_name (str): Name of the System to make Requests on
        system_namespace (str): Namespace of the System to make Requests on
        version_constraint (str): System version to make Requests on. Can be specific
            ('1.0.0') or 'latest'.
        default_instance (str): Name of the Instance to make Requests on
        always_update (bool): Whether to check if a newer version of the System exists
            before making each Request. Only relevant if ``version_constraint='latest'``
        timeout (int): Seconds to wait for a request to complete. 'None' means wait
            forever.
        max_delay (int): Maximum number of seconds to wait between status checks for a
            created request
        blocking (bool): Flag indicating whether creation will block until the Request
            is complete or return a Future that will complete when the Request does
        max_concurrent (int): Maximum number of concurrent requests allowed.
            Only has an effect when blocking=False.
        raise_on_error (bool): Flag controlling whether created Requests that complete
            with an ERROR state should raise an exception

        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
    """
    def __init__(self, *args, **kwargs):
        self._logger = logging.getLogger(__name__)

        self._loaded = False
        self._system = None
        self._commands = {}

        # Need this for back-compatibility (see #836)
        if len(args) > 2:
            _deprecate(
                "Heads up - passing system_name as a positional argument is deprecated "
                "and will be removed in version 4.0", )
            kwargs.setdefault("system_name", args[2])

        # Now need to determine if the intended target is the current running plugin.
        # Start by ensuring there's a valid Plugin context active
        target_self = bool(brewtils.plugin.CONFIG)

        # If ANY of the target specification arguments don't match the current plugin
        # then the target is different
        config_map = {
            "system_name": "name",
            "version_constraint": "version",
            "default_instance": "instance_name",
            "system_namespace": "namespace",
        }
        for key, value in config_map.items():
            if (kwargs.get(key) is not None
                    and kwargs.get(key) != brewtils.plugin.CONFIG[value]):
                target_self = False
                break

        # Now assign self._system_name, etc based on the value of target_self
        if target_self:
            self._system_name = brewtils.plugin.CONFIG.name
            self._version_constraint = brewtils.plugin.CONFIG.version
            self._default_instance = brewtils.plugin.CONFIG.instance_name
            self._system_namespace = brewtils.plugin.CONFIG.namespace or ""
        else:
            self._system_name = kwargs.get("system_name")
            self._version_constraint = kwargs.get("version_constraint",
                                                  "latest")
            self._default_instance = kwargs.get("default_instance", "default")
            self._system_namespace = kwargs.get(
                "system_namespace", brewtils.plugin.CONFIG.namespace or "")

        self._always_update = kwargs.get("always_update", False)
        self._timeout = kwargs.get("timeout", None)
        self._max_delay = kwargs.get("max_delay", 30)
        self._blocking = kwargs.get("blocking", True)
        self._raise_on_error = kwargs.get("raise_on_error", False)

        # This is for Python 3.4 compatibility - max_workers MUST be non-None
        # in that version. This logic is what was added in Python 3.5
        max_concurrent = kwargs.get("max_concurrent", (cpu_count() or 1) * 5)
        self._thread_pool = ThreadPoolExecutor(max_workers=max_concurrent)

        # This points DeprecationWarnings at the right line
        kwargs.setdefault("stacklevel", 5)

        self._easy_client = EasyClient(*args, **kwargs)
        self._resolver = ResolutionManager(easy_client=self._easy_client)

    def __getattr__(self, item):
        # type: (str) -> partial
        """Standard way to create and send beer-garden requests"""
        return self.create_bg_request(item)

    def __str__(self):
        return "%s[%s]" % (self.bg_system, self.bg_default_instance)

    @property
    def bg_system(self):
        return self._system

    @property
    def bg_default_instance(self):
        return self._default_instance

    def create_bg_request(self, command_name, **kwargs):
        # type: (str, **Any) -> partial
        """Create a callable that will execute a Beer-garden request when called.

        Normally you interact with the SystemClient by accessing attributes, but there
        could be certain cases where you want to create a request without sending it.

        Example::

            client = SystemClient(host, port, 'system', blocking=False)

            # Create two callables - one with a parameter and one without
            uncreated_requests = [
                client.create_bg_request('command_1', arg_1='Hi!'),
                client.create_bg_request('command_2'),
            ]

            # Calling creates and sends the request
            # The result of each is a future because blocking=False on the SystemClient
            futures = [req() for req in uncreated_requests]

            # Wait for all the futures to complete
            concurrent.futures.wait(futures)

        Args:
            command_name (str): Name of the Command to send
            kwargs (dict): Will be passed as parameters when creating the Request

        Returns:
            Partial that will create and execute a Beer-garden request when called

        Raises:
            AttributeError: System does not have a Command with the given command_name
        """

        if not self._loaded or self._always_update:
            self.load_bg_system()

        if command_name in self._commands:
            return partial(
                self.send_bg_request,
                _command=command_name,
                _system_name=self._system.name,
                _system_namespace=self._system.namespace,
                _system_version=self._system.version,
                _system_display=self._system.display_name,
                _output_type=self._commands[command_name].output_type,
                _instance_name=self._default_instance,
                **kwargs)
        else:
            raise AttributeError("System '%s' has no command named '%s'" %
                                 (self._system, command_name))

    def send_bg_request(self, *args, **kwargs):
        """Actually create a Request and send it to Beer-garden

        .. note::
            This method is intended for advanced use only, mainly cases where you're
            using the SystemClient without a predefined System. It assumes that
            everything needed to construct the request is being passed in ``kwargs``. If
            this doesn't sound like what you want you should check out
            ``create_bg_request``.

        Args:
            args (list): Unused. Passing positional parameters indicates a bug
            kwargs (dict): All necessary request parameters, including Beer-garden
                internal parameters

        Returns:
            blocking=True: A completed Request object
            blocking=False: A future that will be completed when the Request does

        Raises:
            ValidationError: Request creation failed validation on the server
        """
        # First, if any positional args were given that's a bug, as it means someone
        # tried to pass a parameter without a key:
        # client.command_name(param)
        if args:
            raise RequestProcessException(
                "Using positional arguments when creating a request is not allowed. "
                "Please use keyword arguments instead.")

        # Need to pop here, otherwise we'll try to send as a request parameter
        raise_on_error = kwargs.pop("_raise_on_error", self._raise_on_error)
        blocking = kwargs.pop("_blocking", self._blocking)
        timeout = kwargs.pop("_timeout", self._timeout)

        # If the request fails validation and the version constraint allows,
        # check for a new version and retry
        try:
            request = self._construct_bg_request(**kwargs)
            request = self._easy_client.create_request(request,
                                                       blocking=blocking,
                                                       timeout=timeout)
        except ValidationError:
            if self._system and self._version_constraint == "latest":
                old_version = self._system.version

                self.load_bg_system()

                if old_version != self._system.version:
                    kwargs["_system_version"] = self._system.version
                    return self.send_bg_request(**kwargs)
            raise

        # If not blocking just return the future
        if not blocking:
            return self._thread_pool.submit(self._wait_for_request, request,
                                            raise_on_error, timeout)

        # Brew-view before 2.4 doesn't support the blocking flag, so make sure
        # the request is actually complete before returning
        return self._wait_for_request(request, raise_on_error, timeout)

    def load_bg_system(self):
        # type: () -> None
        """Query beer-garden for a System definition

        This method will make the query to beer-garden for a System matching the name
        and version constraints specified during SystemClient instance creation.

        If this method completes successfully the SystemClient will be ready to create
        and send Requests.

        Returns:
            None

        Raises:
            FetchError: Unable to find a matching System
        """

        if self._version_constraint == "latest":
            self._system = self._determine_latest(
                self._easy_client.find_systems(
                    name=self._system_name, namespace=self._system_namespace))
        else:
            self._system = self._easy_client.find_unique_system(
                name=self._system_name,
                version=self._version_constraint,
                namespace=self._system_namespace,
            )

        if self._system is None:
            raise FetchError(
                "Beer-garden has no system named '%s' with a version matching '%s' in "
                "namespace '%s'" % (
                    self._system_name,
                    self._version_constraint,
                    self._system_namespace
                    if self._system_namespace else "<garden default>",
                ))

        self._commands = {
            command.name: command
            for command in self._system.commands
        }
        self._loaded = True

    def _wait_for_request(self, request, raise_on_error, timeout):
        # type: (Request, bool, int) -> Request
        """Poll the server until the request is completed or errors"""

        delay_time = 0.5
        total_wait_time = 0
        while request.status not in Request.COMPLETED_STATUSES:

            if timeout and 0 < timeout < total_wait_time:
                raise TimeoutExceededError(
                    "Timeout waiting for request '%s' to complete" %
                    str(request))

            time.sleep(delay_time)
            total_wait_time += delay_time
            delay_time = min(delay_time * 2, self._max_delay)

            request = self._easy_client.find_unique_request(id=request.id)

        if raise_on_error and request.status == "ERROR":
            raise RequestFailedError(request)

        return request

    def _get_parent_for_request(self):
        # type: () -> Optional[Request]
        parent = getattr(brewtils.plugin.request_context, "current_request",
                         None)
        if parent is None:
            return None

        if brewtils.plugin.CONFIG and (
                brewtils.plugin.CONFIG.bg_host.upper() !=
                self._easy_client.client.bg_host.upper()
                or brewtils.plugin.CONFIG.bg_port !=
                self._easy_client.client.bg_port):
            self._logger.warning(
                "A parent request was found, but the destination beer-garden "
                "appears to be different than the beer-garden to which this plugin "
                "is assigned. Cross-server parent/child requests are not supported "
                "at this time. Removing the parent context so the request doesn't fail."
            )
            return None

        return Request(id=str(parent.id))

    def _construct_bg_request(self, **kwargs):
        # type: (**Any) -> Request
        """Create a request that can be used with EasyClient.create_request"""

        command = kwargs.pop("_command", None)
        system_name = kwargs.pop("_system_name", None)
        system_version = kwargs.pop("_system_version", None)
        system_display = kwargs.pop("_system_display", None)
        system_namespace = kwargs.pop("_system_namespace", None)
        instance_name = kwargs.pop("_instance_name", None)
        comment = kwargs.pop("_comment", None)
        output_type = kwargs.pop("_output_type", None)
        metadata = kwargs.pop("_metadata", {})
        parent = kwargs.pop("_parent", self._get_parent_for_request())

        if system_display:
            metadata["system_display_name"] = system_display

        # Don't check namespace - https://github.com/beer-garden/beer-garden/issues/827
        if command is None:
            raise ValidationError("Unable to send a request with no command")
        if system_name is None:
            raise ValidationError(
                "Unable to send a request with no system name")
        if system_version is None:
            raise ValidationError(
                "Unable to send a request with no system version")
        if instance_name is None:
            raise ValidationError(
                "Unable to send a request with no instance name")

        request = Request(
            command=command,
            system=system_name,
            system_version=system_version,
            namespace=system_namespace,
            instance_name=instance_name,
            comment=comment,
            output_type=output_type,
            parent=parent,
            metadata=metadata,
            parameters=kwargs,
        )

        request.parameters = self._resolve_parameters(command, request)

        return request

    def _resolve_parameters(self, command, request):
        # type: (str, Request) -> Dict[str, Any]
        """Attempt to upload any necessary file parameters

        This will inspect the Command model for the given command, looking for file
        parameters. Any file parameters will be "resolved" (aka uploaded) before
        continuing.

        If the command name can not be found in the current list of commands the
        parameter list will just be returned. This most likely indicates a direct
        invocation of send_bg_request since a bad command name should be caught earlier
        in the "normal" workflow.
        """
        if command not in self._commands:
            return request.parameters

        return self._resolver.resolve(request.parameters,
                                      self._commands[command].parameters,
                                      upload=True)

    @staticmethod
    def _determine_latest(systems):
        # type: (Iterable[System]) -> Optional[System]
        return (sorted(systems, key=lambda x: parse(x.version),
                       reverse=True)[0] if systems else None)
Ejemplo n.º 5
0
class EasyClientTest(unittest.TestCase):
    def setUp(self):
        self.parser = Mock(name="parser", spec=SchemaParser)
        self.client = EasyClient(
            host="localhost", port="3000", api_version=1, parser=self.parser
        )
        self.fake_success_response = Mock(
            ok=True, status_code=200, json=Mock(return_value="payload")
        )
        self.fake_client_error_response = Mock(
            ok=False, status_code=400, json=Mock(return_value="payload")
        )
        self.fake_not_found_error_response = Mock(
            ok=False, status_code=404, json=Mock(return_value="payload")
        )
        self.fake_wait_exceeded_response = Mock(
            ok=False, status_code=408, json=Mock(return_value="payload")
        )
        self.fake_conflict_error_response = Mock(
            ok=False, status_code=409, json=Mock(return_value="payload")
        )
        self.fake_too_large_error_response = Mock(
            ok=False, status_code=413, json=Mock(return_value="payload")
        )
        self.fake_server_error_response = Mock(
            ok=False, status_code=500, json=Mock(return_value="payload")
        )
        self.fake_connection_error_response = Mock(
            ok=False, status_code=503, json=Mock(return_value="payload")
        )

    @patch("brewtils.rest.client.RestClient.get_logging_config")
    def test_get_logging_config_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(
            RestConnectionError, self.client.get_logging_config, "system_name"
        )

    # Find systems
    @patch("brewtils.rest.client.RestClient.get_systems")
    def test_find_systems_call_get_systems(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.find_systems()
        mock_get.assert_called()

    @patch("brewtils.rest.client.RestClient.get_systems")
    def test_find_systems_with_params_get_systems(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.find_systems(name="foo")
        mock_get.assert_called_with(name="foo")

    @patch("brewtils.rest.client.RestClient.get_systems")
    def test_find_systems_server_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response
        self.assertRaises(FetchError, self.client.find_systems)

    @patch("brewtils.rest.client.RestClient.get_systems")
    def test_find_systems_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.find_systems)

    @patch("brewtils.rest.client.RestClient.get_systems")
    def test_find_systems_call_parser(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.find_systems()
        self.parser.parse_system.assert_called_with("payload", many=True)

    @patch("brewtils.rest.easy_client.EasyClient._find_system_by_id")
    def test_find_unique_system_by_id(self, find_mock):
        system_mock = Mock()
        find_mock.return_value = system_mock

        self.assertEqual(system_mock, self.client.find_unique_system(id="id"))
        find_mock.assert_called_with("id")

    def test_find_unique_system_none(self):
        self.client.find_systems = Mock(return_value=None)
        self.assertIsNone(self.client.find_unique_system())

    def test_find_unique_system_one(self):
        self.client.find_systems = Mock(return_value=["system1"])
        self.assertEqual("system1", self.client.find_unique_system())

    def test_find_unique_system_multiple(self):
        self.client.find_systems = Mock(return_value=["system1", "system2"])
        self.assertRaises(FetchError, self.client.find_unique_system)

    @patch("brewtils.rest.client.RestClient.get_system")
    def test_find_system_by_id(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_system = Mock(return_value="system")

        self.assertEqual(self.client._find_system_by_id("id", foo="bar"), "system")
        self.parser.parse_system.assert_called_with("payload", many=False)
        mock_get.assert_called_with("id", foo="bar")

    @patch("brewtils.rest.client.RestClient.get_system")
    def test_find_system_by_id_404(self, mock_get):
        mock_get.return_value = self.fake_not_found_error_response

        self.assertIsNone(self.client._find_system_by_id("id", foo="bar"))
        mock_get.assert_called_with("id", foo="bar")

    @patch("brewtils.rest.client.RestClient.get_system")
    def test_find_system_by_id_server_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client._find_system_by_id, "id")
        mock_get.assert_called_with("id")

    @patch("brewtils.rest.client.RestClient.get_system")
    def test_find_system_by_id_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client._find_system_by_id, "id")

    # Create system
    @patch("brewtils.rest.client.RestClient.post_systems")
    def test_create_system(self, mock_post):
        mock_post.return_value = self.fake_success_response
        self.parser.serialize_system = Mock(return_value="json_system")
        self.parser.parse_system = Mock(return_value="system_response")

        self.assertEqual("system_response", self.client.create_system("system"))
        self.parser.serialize_system.assert_called_with("system")
        self.parser.parse_system.assert_called_with("payload", many=False)

    @patch("brewtils.rest.client.RestClient.post_systems")
    def test_create_system_client_error(self, mock_post):
        mock_post.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.create_system, "system")

    @patch("brewtils.rest.client.RestClient.post_systems")
    def test_create_system_server_error(self, mock_post):
        mock_post.return_value = self.fake_server_error_response
        self.assertRaises(SaveError, self.client.create_system, "system")

    @patch("brewtils.rest.client.RestClient.post_systems")
    def test_create_system_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.create_system, "system")

    # Update request
    @patch("brewtils.rest.client.RestClient.patch_system")
    def test_update_system(self, mock_patch):
        mock_patch.return_value = self.fake_success_response
        self.parser.serialize_command = Mock(return_value="new_commands")

        self.client.update_system("id", new_commands="new_commands")
        self.parser.parse_system.assert_called_with("payload", many=False)
        self.assertEqual(1, mock_patch.call_count)
        payload = mock_patch.call_args[0][1]
        self.assertNotEqual(-1, payload.find("new_commands"))

    @patch("brewtils.rest.easy_client.PatchOperation")
    @patch("brewtils.rest.client.RestClient.patch_system")
    def test_update_system_metadata(self, mock_patch, MockPatch):
        MockPatch.return_value = "patch"
        mock_patch.return_value = self.fake_success_response
        metadata = {"foo": "bar"}

        self.client.update_system("id", new_commands=None, metadata=metadata)
        MockPatch.assert_called_with("update", "/metadata", {"foo": "bar"})
        self.parser.serialize_patch.assert_called_with(["patch"], many=True)
        self.parser.parse_system.assert_called_with("payload", many=False)

    @patch("brewtils.rest.easy_client.PatchOperation")
    @patch("brewtils.rest.client.RestClient.patch_system")
    def test_update_system_kwargs(self, mock_patch, MockPatch):
        MockPatch.return_value = "patch"
        mock_patch.return_value = self.fake_success_response

        self.client.update_system("id", new_commands=None, display_name="foo")
        MockPatch.assert_called_with("replace", "/display_name", "foo")
        self.parser.serialize_patch.assert_called_with(["patch"], many=True)
        self.parser.parse_system.assert_called_with("payload", many=False)

    @patch("brewtils.rest.client.RestClient.patch_system")
    def test_update_system_client_error(self, mock_patch):
        mock_patch.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.update_system, "id")
        mock_patch.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_system")
    def test_update_system_invalid_id(self, mock_patch):
        mock_patch.return_value = self.fake_not_found_error_response

        self.assertRaises(NotFoundError, self.client.update_system, "id")
        mock_patch.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_system")
    def test_update_system_conflict(self, mock_patch):
        mock_patch.return_value = self.fake_conflict_error_response

        self.assertRaises(ConflictError, self.client.update_system, "id")
        mock_patch.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_system")
    def test_update_system_server_error(self, mock_patch):
        mock_patch.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.update_system, "id")
        mock_patch.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_system")
    def test_update_system_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.update_system, "system")

    # Remove system
    @patch("brewtils.rest.easy_client.EasyClient._remove_system_by_id")
    @patch("brewtils.rest.easy_client.EasyClient.find_unique_system")
    def test_remove_system(self, find_mock, remove_mock):
        find_mock.return_value = System(id="id")
        remove_mock.return_value = "delete_response"

        self.assertEqual(
            "delete_response", self.client.remove_system(search="search params")
        )
        find_mock.assert_called_once_with(search="search params")
        remove_mock.assert_called_once_with("id")

    @patch("brewtils.rest.easy_client.EasyClient._remove_system_by_id")
    @patch("brewtils.rest.easy_client.EasyClient.find_unique_system")
    def test_remove_system_none_found(self, find_mock, remove_mock):
        find_mock.return_value = None

        self.assertRaises(FetchError, self.client.remove_system, search="search params")
        self.assertFalse(remove_mock.called)
        find_mock.assert_called_once_with(search="search params")

    @patch("brewtils.rest.client.RestClient.delete_system")
    def test_remove_system_by_id(self, mock_delete):
        mock_delete.return_value = self.fake_success_response

        self.assertTrue(self.client._remove_system_by_id("foo"))
        mock_delete.assert_called_with("foo")

    @patch("brewtils.rest.client.RestClient.delete_system")
    def test_remove_system_by_id_client_error(self, mock_remove):
        mock_remove.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client._remove_system_by_id, "foo")

    @patch("brewtils.rest.client.RestClient.delete_system")
    def test_remove_system_by_id_server_error(self, mock_remove):
        mock_remove.return_value = self.fake_server_error_response
        self.assertRaises(DeleteError, self.client._remove_system_by_id, "foo")

    @patch("brewtils.rest.client.RestClient.delete_system")
    def test_remove_system_by_id_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client._remove_system_by_id, "foo")

    def test_remove_system_by_id_none(self):
        self.assertRaises(DeleteError, self.client._remove_system_by_id, None)

    # Initialize instance
    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_initialize_instance(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.client.initialize_instance("id")
        self.assertTrue(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_initialize_instance_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.initialize_instance, "id")
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_initialize_instance_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.initialize_instance, "id")
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_initialize_instance_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.initialize_instance, "id")

    @patch("brewtils.rest.client.RestClient.get_instance")
    def test_get_instance(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.client.get_instance("id")
        self.assertTrue(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id")

    @patch("brewtils.rest.client.RestClient.get_instance")
    def test_get_instance_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.get_instance, "id")
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id")

    @patch("brewtils.rest.client.RestClient.get_instance")
    def test_get_instance_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client.get_instance, "id")
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id")

    @patch("brewtils.rest.client.RestClient.get_instance")
    def test_get_instance_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.get_instance, "id")

    @patch("brewtils.rest.client.RestClient.get_instance")
    def test_get_instance_status(self, request_mock):
        request_mock.return_value = self.fake_success_response

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")

            self.client.get_instance_status("id")
            self.assertTrue(self.parser.parse_instance.called)
            request_mock.assert_called_once_with("id")

            self.assertEqual(1, len(w))
            self.assertEqual(w[0].category, FutureWarning)

    @patch("brewtils.rest.client.RestClient.get_instance")
    def test_get_instance_status_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")

            self.assertRaises(ValidationError, self.client.get_instance_status, "id")
            self.assertFalse(self.parser.parse_instance.called)
            request_mock.assert_called_once_with("id")

            self.assertEqual(1, len(w))
            self.assertEqual(w[0].category, FutureWarning)

    @patch("brewtils.rest.client.RestClient.get_instance")
    def test_get_instance_status_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")

            self.assertRaises(FetchError, self.client.get_instance_status, "id")
            self.assertFalse(self.parser.parse_instance.called)
            request_mock.assert_called_once_with("id")

            self.assertEqual(1, len(w))
            self.assertEqual(w[0].category, FutureWarning)

    @patch("brewtils.rest.client.RestClient.get_instance")
    def test_get_instance_status_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response

        with warnings.catch_warnings(record=True) as w:
            warnings.simplefilter("always")

            self.assertRaises(
                RestConnectionError, self.client.get_instance_status, "id"
            )

            self.assertEqual(1, len(w))
            self.assertEqual(w[0].category, FutureWarning)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_update_instance_status(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.client.update_instance_status("id", "status")
        self.assertTrue(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_update_instance_status_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(
            ValidationError, self.client.update_instance_status, "id", "status"
        )
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_update_instance_status_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.update_instance_status, "id", "status")
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_update_instance_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(
            RestConnectionError, self.client.update_instance_status, "id", "status"
        )

    # Instance heartbeat
    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_instance_heartbeat(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.assertTrue(self.client.instance_heartbeat("id"))
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_instance_heartbeat_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.instance_heartbeat, "id")
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_instance_heartbeat_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.instance_heartbeat, "id")
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_instance")
    def test_instance_heartbeat_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.instance_heartbeat, "id")

    @patch("brewtils.rest.client.RestClient.delete_instance")
    def test_remove_instance(self, mock_delete):
        mock_delete.return_value = self.fake_success_response

        self.assertTrue(self.client.remove_instance("foo"))
        mock_delete.assert_called_with("foo")

    @patch("brewtils.rest.client.RestClient.delete_instance")
    def test_remove_instance_client_error(self, mock_remove):
        mock_remove.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.remove_instance, "foo")

    @patch("brewtils.rest.client.RestClient.delete_instance")
    def test_remove_instance_server_error(self, mock_remove):
        mock_remove.return_value = self.fake_server_error_response
        self.assertRaises(DeleteError, self.client.remove_instance, "foo")

    @patch("brewtils.rest.client.RestClient.delete_instance")
    def test_remove_instance_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.remove_instance, "foo")

    def test_remove_instance_none(self):
        self.assertRaises(DeleteError, self.client.remove_instance, None)

    # Find requests
    @patch("brewtils.rest.easy_client.EasyClient._find_request_by_id")
    def test_find_unique_request_by_id(self, find_mock):
        self.client.find_unique_request(id="id")
        find_mock.assert_called_with("id")

    def test_find_unique_request_none(self):
        self.client.find_requests = Mock(return_value=None)
        self.assertIsNone(self.client.find_unique_request())

    def test_find_unique_request_one(self):
        self.client.find_requests = Mock(return_value=["request1"])
        self.assertEqual("request1", self.client.find_unique_request())

    def test_find_unique_request_multiple(self):
        self.client.find_requests = Mock(return_value=["request1", "request2"])
        self.assertRaises(FetchError, self.client.find_unique_request)

    @patch("brewtils.rest.client.RestClient.get_requests")
    def test_find_requests(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_request = Mock(return_value="request")

        self.assertEqual("request", self.client.find_requests(search="params"))
        self.parser.parse_request.assert_called_with("payload", many=True)
        mock_get.assert_called_with(search="params")

    @patch("brewtils.rest.client.RestClient.get_requests")
    def test_find_requests_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client.find_requests, search="params")
        mock_get.assert_called_with(search="params")

    @patch("brewtils.rest.client.RestClient.get_requests")
    def test_find_requests_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(
            RestConnectionError, self.client.find_requests, search="params"
        )

    @patch("brewtils.rest.client.RestClient.get_request")
    def test_find_request_by_id(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_request = Mock(return_value="request")

        self.assertEqual(self.client._find_request_by_id("id"), "request")
        self.parser.parse_request.assert_called_with("payload", many=False)
        mock_get.assert_called_with("id")

    @patch("brewtils.rest.client.RestClient.get_request")
    def test_find_request_by_id_404(self, mock_get):
        mock_get.return_value = self.fake_not_found_error_response

        self.assertIsNone(self.client._find_request_by_id("id"))
        mock_get.assert_called_with("id")

    @patch("brewtils.rest.client.RestClient.get_request")
    def test_find_request_by_id_server_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client._find_request_by_id, "id")
        mock_get.assert_called_with("id")

    @patch("brewtils.rest.client.RestClient.get_request")
    def test_find_request_by_id_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client._find_request_by_id, "id")

    # Create request
    @patch("brewtils.rest.client.RestClient.post_requests")
    def test_create_request(self, mock_post):
        mock_post.return_value = self.fake_success_response
        self.parser.serialize_request = Mock(return_value="json_request")
        self.parser.parse_request = Mock(return_value="request_response")

        self.assertEqual("request_response", self.client.create_request("request"))
        self.parser.serialize_request.assert_called_with("request")
        self.parser.parse_request.assert_called_with("payload", many=False)

    @patch("brewtils.rest.client.RestClient.post_requests")
    def test_create_request_errors(self, mock_post):
        mock_post.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.create_request, "request")

        mock_post.return_value = self.fake_wait_exceeded_response
        self.assertRaises(WaitExceededError, self.client.create_request, "request")

        mock_post.return_value = self.fake_server_error_response
        self.assertRaises(SaveError, self.client.create_request, "request")

        mock_post.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.create_request, "request")

    # Update request
    @patch("brewtils.rest.client.RestClient.patch_request")
    def test_update_request(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.client.update_request(
            "id", status="new_status", output="new_output", error_class="ValueError"
        )
        self.parser.parse_request.assert_called_with("payload", many=False)
        self.assertEqual(1, request_mock.call_count)
        payload = request_mock.call_args[0][1]
        self.assertNotEqual(-1, payload.find("new_status"))
        self.assertNotEqual(-1, payload.find("new_output"))
        self.assertNotEqual(-1, payload.find("ValueError"))

    @patch("brewtils.rest.client.RestClient.patch_request")
    def test_update_request_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.update_request, "id")
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_request")
    def test_update_request_too_large_error(self, request_mock):
        request_mock.return_value = self.fake_too_large_error_response

        self.assertRaises(TooLargeError, self.client.update_request, "id")
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_request")
    def test_update_request_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.update_request, "id")
        request_mock.assert_called_once_with("id", ANY)

    @patch("brewtils.rest.client.RestClient.patch_request")
    def test_update_request_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.update_request, "id")

    # Publish Event
    @patch("brewtils.rest.client.RestClient.post_event")
    def test_publish_event(self, mock_post):
        mock_post.return_value = self.fake_success_response
        self.assertTrue(self.client.publish_event(Mock()))

    @patch("brewtils.rest.client.RestClient.post_event")
    def test_publish_event_errors(self, mock_post):
        mock_post.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.publish_event, "system")

        mock_post.return_value = self.fake_server_error_response
        self.assertRaises(RestError, self.client.publish_event, "system")

        mock_post.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.publish_event, "system")

    # Queues
    @patch("brewtils.rest.client.RestClient.get_queues")
    def test_get_queues(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.get_queues()
        self.assertTrue(self.parser.parse_queue.called)

    @patch("brewtils.rest.client.RestClient.get_queues")
    def test_get_queues_errors(self, mock_get):
        mock_get.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.get_queues)

        mock_get.return_value = self.fake_server_error_response
        self.assertRaises(RestError, self.client.get_queues)

        mock_get.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.get_queues)

    @patch("brewtils.rest.client.RestClient.delete_queue")
    def test_clear_queue(self, mock_delete):
        mock_delete.return_value = self.fake_success_response
        self.assertTrue(self.client.clear_queue("queue"))

    @patch("brewtils.rest.client.RestClient.delete_queue")
    def test_clear_queue_errors(self, mock_delete):
        mock_delete.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.clear_queue, "queue")

        mock_delete.return_value = self.fake_server_error_response
        self.assertRaises(RestError, self.client.clear_queue, "queue")

        mock_delete.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.clear_queue, "queue")

    @patch("brewtils.rest.client.RestClient.delete_queues")
    def test_clear_all_queues(self, mock_delete):
        mock_delete.return_value = self.fake_success_response
        self.assertTrue(self.client.clear_all_queues())

    @patch("brewtils.rest.client.RestClient.delete_queues")
    def test_clear_all_queues_errors(self, mock_delete):
        mock_delete.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.clear_all_queues)

        mock_delete.return_value = self.fake_server_error_response
        self.assertRaises(RestError, self.client.clear_all_queues)

        mock_delete.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.clear_all_queues)

    # Find Jobs
    @patch("brewtils.rest.client.RestClient.get_jobs")
    def test_find_jobs(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_job = Mock(return_value="job")

        self.assertEqual("job", self.client.find_jobs(search="params"))
        self.parser.parse_job.assert_called_with("payload", many=True)
        mock_get.assert_called_with(search="params")

    @patch("brewtils.rest.client.RestClient.get_jobs")
    def test_find_jobs_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client.find_jobs, search="params")
        mock_get.assert_called_with(search="params")

    # Create Jobs
    @patch("brewtils.rest.client.RestClient.post_jobs")
    def test_create_job(self, mock_post):
        mock_post.return_value = self.fake_success_response
        self.parser.serialize_job = Mock(return_value="json_job")
        self.parser.parse_job = Mock(return_value="job_response")

        self.assertEqual("job_response", self.client.create_job("job"))
        self.parser.serialize_job.assert_called_with("job")
        self.parser.parse_job.assert_called_with("payload", many=False)

    @patch("brewtils.rest.client.RestClient.post_jobs")
    def test_create_job_error(self, mock_post):
        mock_post.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.create_job, "job")

    # Remove Job
    @patch("brewtils.rest.client.RestClient.delete_job")
    def test_delete_job(self, mock_delete):
        mock_delete.return_value = self.fake_success_response
        self.assertEqual(True, self.client.remove_job("job_id"))

    @patch("brewtils.rest.client.RestClient.delete_job")
    def test_delete_job_error(self, mock_delete):
        mock_delete.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.remove_job, "job_id")

    # Pause Job
    @patch("brewtils.rest.easy_client.PatchOperation")
    @patch("brewtils.rest.client.RestClient.patch_job")
    def test_pause_job(self, mock_patch, MockPatch):
        MockPatch.return_value = "patch"
        mock_patch.return_value = self.fake_success_response

        self.client.pause_job("id")
        MockPatch.assert_called_with("update", "/status", "PAUSED")
        self.parser.serialize_patch.assert_called_with(["patch"], many=True)
        self.parser.parse_job.assert_called_with("payload", many=False)

    @patch("brewtils.rest.client.RestClient.patch_job")
    def test_pause_job_error(self, mock_patch):
        mock_patch.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.pause_job, "id")

    @patch("brewtils.rest.easy_client.PatchOperation")
    @patch("brewtils.rest.client.RestClient.patch_job")
    def test_resume_job(self, mock_patch, MockPatch):
        MockPatch.return_value = "patch"
        mock_patch.return_value = self.fake_success_response

        self.client.resume_job("id")
        MockPatch.assert_called_with("update", "/status", "RUNNING")
        self.parser.serialize_patch.assert_called_with(["patch"], many=True)
        self.parser.parse_job.assert_called_with("payload", many=False)

    # Users
    @patch("brewtils.rest.client.RestClient.get_user")
    def test_who_am_i(self, mock_get):
        self.client.who_am_i()
        mock_get.assert_called_with("anonymous")

    @patch("brewtils.rest.client.RestClient.get_user")
    def test_get_user(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.get_user("identifier")
        self.assertTrue(self.parser.parse_principal.called)

    @patch("brewtils.rest.client.RestClient.get_user")
    def test_get_user_errors(self, mock_get):
        mock_get.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.get_user, "identifier")

        mock_get.return_value = self.fake_not_found_error_response
        self.assertRaises(NotFoundError, self.client.get_user, "identifier")
Ejemplo n.º 6
0
class EasyClientTest(unittest.TestCase):
    def setUp(self):
        self.parser = Mock()
        self.client = EasyClient(host='localhost',
                                 port='3000',
                                 api_version=1,
                                 parser=self.parser)
        self.fake_success_response = Mock(ok=True,
                                          status_code=200,
                                          json=Mock(return_value='payload'))
        self.fake_client_error_response = Mock(
            ok=False, status_code=400, json=Mock(return_value='payload'))
        self.fake_not_found_error_response = Mock(
            ok=False, status_code=404, json=Mock(return_value='payload'))
        self.fake_wait_exceeded_response = Mock(
            ok=False, status_code=408, json=Mock(return_value='payload'))
        self.fake_conflict_error_response = Mock(
            ok=False, status_code=409, json=Mock(return_value='payload'))
        self.fake_server_error_response = Mock(
            ok=False, status_code=500, json=Mock(return_value='payload'))
        self.fake_connection_error_response = Mock(
            ok=False, status_code=503, json=Mock(return_value='payload'))

    @patch('brewtils.rest.client.RestClient.get_config', Mock())
    def test_can_connect_success(self):
        self.assertTrue(self.client.can_connect())

    @patch('brewtils.rest.client.RestClient.get_config')
    def test_can_connect_failure(self, get_mock):
        get_mock.side_effect = requests.exceptions.ConnectionError
        self.assertFalse(self.client.can_connect())

    @patch('brewtils.rest.client.RestClient.get_config')
    def test_can_connect_error(self, get_mock):
        get_mock.side_effect = requests.exceptions.SSLError
        self.assertRaises(requests.exceptions.SSLError,
                          self.client.can_connect)

    @patch('brewtils.rest.client.RestClient.get_version')
    def test_get_version(self, mock_get):
        mock_get.return_value = self.fake_success_response

        self.assertEqual(self.fake_success_response, self.client.get_version())
        mock_get.assert_called()

    @patch('brewtils.rest.client.RestClient.get_version')
    def test_get_version_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client.get_version)
        mock_get.assert_called()

    @patch('brewtils.rest.client.RestClient.get_version')
    def test_get_version_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.get_version)

    @patch('brewtils.rest.client.RestClient.get_logging_config')
    def test_get_logging_config(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_logging_config = Mock(return_value='logging_config')

        self.assertEqual('logging_config',
                         self.client.get_logging_config('system_name'))
        self.parser.parse_logging_config.assert_called_with('payload')
        mock_get.assert_called()

    @patch('brewtils.rest.client.RestClient.get_logging_config')
    def test_get_logging_config_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.get_logging_config,
                          'system_name')

    # Find systems
    @patch('brewtils.rest.client.RestClient.get_systems')
    def test_find_systems_call_get_systems(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.find_systems()
        mock_get.assert_called()

    @patch('brewtils.rest.client.RestClient.get_systems')
    def test_find_systems_with_params_get_systems(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.find_systems(name='foo')
        mock_get.assert_called_with(name='foo')

    @patch('brewtils.rest.client.RestClient.get_systems')
    def test_find_systems_server_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response
        self.assertRaises(FetchError, self.client.find_systems)

    @patch('brewtils.rest.client.RestClient.get_systems')
    def test_find_systems_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.find_systems)

    @patch('brewtils.rest.client.RestClient.get_systems')
    def test_find_systems_call_parser(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.find_systems()
        self.parser.parse_system.assert_called_with('payload', many=True)

    @patch('brewtils.rest.easy_client.EasyClient._find_system_by_id')
    def test_find_unique_system_by_id(self, find_mock):
        system_mock = Mock()
        find_mock.return_value = system_mock

        self.assertEqual(system_mock, self.client.find_unique_system(id='id'))
        find_mock.assert_called_with('id')

    def test_find_unique_system_none(self):
        self.client.find_systems = Mock(return_value=None)
        self.assertIsNone(self.client.find_unique_system())

    def test_find_unique_system_one(self):
        self.client.find_systems = Mock(return_value=['system1'])
        self.assertEqual('system1', self.client.find_unique_system())

    def test_find_unique_system_multiple(self):
        self.client.find_systems = Mock(return_value=['system1', 'system2'])
        self.assertRaises(FetchError, self.client.find_unique_system)

    @patch('brewtils.rest.client.RestClient.get_system')
    def test_find_system_by_id(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_system = Mock(return_value='system')

        self.assertEqual(self.client._find_system_by_id('id', foo='bar'),
                         'system')
        self.parser.parse_system.assert_called_with('payload')
        mock_get.assert_called_with('id', foo='bar')

    @patch('brewtils.rest.client.RestClient.get_system')
    def test_find_system_by_id_404(self, mock_get):
        mock_get.return_value = self.fake_not_found_error_response

        self.assertIsNone(self.client._find_system_by_id('id', foo='bar'))
        mock_get.assert_called_with('id', foo='bar')

    @patch('brewtils.rest.client.RestClient.get_system')
    def test_find_system_by_id_server_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client._find_system_by_id, 'id')
        mock_get.assert_called_with('id')

    @patch('brewtils.rest.client.RestClient.get_system')
    def test_find_system_by_id_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client._find_system_by_id,
                          'id')

    # Create system
    @patch('brewtils.rest.client.RestClient.post_systems')
    def test_create_system(self, mock_post):
        mock_post.return_value = self.fake_success_response
        self.parser.serialize_system = Mock(return_value='json_system')
        self.parser.parse_system = Mock(return_value='system_response')

        self.assertEqual('system_response',
                         self.client.create_system('system'))
        self.parser.serialize_system.assert_called_with('system')
        self.parser.parse_system.assert_called_with('payload')

    @patch('brewtils.rest.client.RestClient.post_systems')
    def test_create_system_client_error(self, mock_post):
        mock_post.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.create_system, 'system')

    @patch('brewtils.rest.client.RestClient.post_systems')
    def test_create_system_server_error(self, mock_post):
        mock_post.return_value = self.fake_server_error_response
        self.assertRaises(SaveError, self.client.create_system, 'system')

    @patch('brewtils.rest.client.RestClient.post_systems')
    def test_create_system_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.create_system,
                          'system')

    # Update request
    @patch('brewtils.rest.client.RestClient.patch_system')
    def test_update_system(self, mock_patch):
        mock_patch.return_value = self.fake_success_response
        self.parser.serialize_command = Mock(return_value='new_commands')

        self.client.update_system('id', new_commands='new_commands')
        self.parser.parse_system.assert_called_with('payload')
        self.assertEqual(1, mock_patch.call_count)
        payload = mock_patch.call_args[0][1]
        self.assertNotEqual(-1, payload.find('new_commands'))

    @patch('brewtils.rest.easy_client.PatchOperation')
    @patch('brewtils.rest.client.RestClient.patch_system')
    def test_update_system_metadata(self, mock_patch, MockPatch):
        MockPatch.return_value = "patch"
        mock_patch.return_value = self.fake_success_response
        metadata = {"foo": "bar"}

        self.client.update_system('id', new_commands=None, metadata=metadata)
        MockPatch.assert_called_with('update', '/metadata', {"foo": "bar"})
        self.parser.serialize_patch.assert_called_with(["patch"], many=True)
        self.parser.parse_system.assert_called_with('payload')

    @patch('brewtils.rest.easy_client.PatchOperation')
    @patch('brewtils.rest.client.RestClient.patch_system')
    def test_update_system_kwargs(self, mock_patch, MockPatch):
        MockPatch.return_value = "patch"
        mock_patch.return_value = self.fake_success_response

        self.client.update_system('id', new_commands=None, display_name="foo")
        MockPatch.assert_called_with('replace', '/display_name', "foo")
        self.parser.serialize_patch.assert_called_with(["patch"], many=True)
        self.parser.parse_system.assert_called_with('payload')

    @patch('brewtils.rest.client.RestClient.patch_system')
    def test_update_system_client_error(self, mock_patch):
        mock_patch.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.update_system, 'id')
        mock_patch.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_system')
    def test_update_system_invalid_id(self, mock_patch):
        mock_patch.return_value = self.fake_not_found_error_response

        self.assertRaises(NotFoundError, self.client.update_system, 'id')
        mock_patch.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_system')
    def test_update_system_conflict(self, mock_patch):
        mock_patch.return_value = self.fake_conflict_error_response

        self.assertRaises(ConflictError, self.client.update_system, 'id')
        mock_patch.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_system')
    def test_update_system_server_error(self, mock_patch):
        mock_patch.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.update_system, 'id')
        mock_patch.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_system')
    def test_update_system_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.update_system,
                          'system')

    # Remove system
    @patch('brewtils.rest.easy_client.EasyClient._remove_system_by_id')
    @patch('brewtils.rest.easy_client.EasyClient.find_unique_system')
    def test_remove_system(self, find_mock, remove_mock):
        find_mock.return_value = System(id='id')
        remove_mock.return_value = 'delete_response'

        self.assertEqual('delete_response',
                         self.client.remove_system(search='search params'))
        find_mock.assert_called_once_with(search='search params')
        remove_mock.assert_called_once_with('id')

    @patch('brewtils.rest.easy_client.EasyClient._remove_system_by_id')
    @patch('brewtils.rest.easy_client.EasyClient.find_unique_system')
    def test_remove_system_none_found(self, find_mock, remove_mock):
        find_mock.return_value = None

        self.assertRaises(FetchError,
                          self.client.remove_system,
                          search='search params')
        self.assertFalse(remove_mock.called)
        find_mock.assert_called_once_with(search='search params')

    @patch('brewtils.rest.client.RestClient.delete_system')
    def test_remove_system_by_id(self, mock_delete):
        mock_delete.return_value = self.fake_success_response

        self.assertTrue(self.client._remove_system_by_id('foo'))
        mock_delete.assert_called_with('foo')

    @patch('brewtils.rest.client.RestClient.delete_system')
    def test_remove_system_by_id_client_error(self, mock_remove):
        mock_remove.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client._remove_system_by_id,
                          'foo')

    @patch('brewtils.rest.client.RestClient.delete_system')
    def test_remove_system_by_id_server_error(self, mock_remove):
        mock_remove.return_value = self.fake_server_error_response
        self.assertRaises(DeleteError, self.client._remove_system_by_id, 'foo')

    @patch('brewtils.rest.client.RestClient.delete_system')
    def test_remove_system_by_id_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError,
                          self.client._remove_system_by_id, 'foo')

    def test_remove_system_by_id_none(self):
        self.assertRaises(DeleteError, self.client._remove_system_by_id, None)

    # Initialize instance
    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_initialize_instance(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.client.initialize_instance('id')
        self.assertTrue(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_initialize_instance_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.initialize_instance,
                          'id')
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_initialize_instance_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.initialize_instance, 'id')
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_initialize_instance_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.initialize_instance,
                          'id')

    @patch('brewtils.rest.client.RestClient.get_instance')
    def test_get_instance_status(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.client.get_instance_status('id')
        self.assertTrue(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id')

    @patch('brewtils.rest.client.RestClient.get_instance')
    def test_get_instance_status_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.get_instance_status,
                          'id')
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id')

    @patch('brewtils.rest.client.RestClient.get_instance')
    def test_get_instance_status_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client.get_instance_status, 'id')
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id')

    @patch('brewtils.rest.client.RestClient.get_instance')
    def test_get_instance_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.get_instance_status,
                          'id')

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_update_instance_status(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.client.update_instance_status('id', 'status')
        self.assertTrue(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_update_instance_status_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.update_instance_status,
                          'id', 'status')
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_update_instance_status_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.update_instance_status, 'id',
                          'status')
        self.assertFalse(self.parser.parse_instance.called)
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_update_instance_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError,
                          self.client.update_instance_status, 'id', 'status')

    # Instance heartbeat
    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_instance_heartbeat(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.assertTrue(self.client.instance_heartbeat('id'))
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_instance_heartbeat_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.instance_heartbeat,
                          'id')
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_instance_heartbeat_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.instance_heartbeat, 'id')
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_instance')
    def test_instance_heartbeat_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.instance_heartbeat,
                          'id')

    @patch('brewtils.rest.client.RestClient.delete_instance')
    def test_remove_instance(self, mock_delete):
        mock_delete.return_value = self.fake_success_response

        self.assertTrue(self.client.remove_instance('foo'))
        mock_delete.assert_called_with('foo')

    @patch('brewtils.rest.client.RestClient.delete_instance')
    def test_remove_instance_client_error(self, mock_remove):
        mock_remove.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.remove_instance, 'foo')

    @patch('brewtils.rest.client.RestClient.delete_instance')
    def test_remove_instance_server_error(self, mock_remove):
        mock_remove.return_value = self.fake_server_error_response
        self.assertRaises(DeleteError, self.client.remove_instance, 'foo')

    @patch('brewtils.rest.client.RestClient.delete_instance')
    def test_remove_instance_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.remove_instance,
                          'foo')

    def test_remove_instance_none(self):
        self.assertRaises(DeleteError, self.client.remove_instance, None)

    # Find requests
    @patch('brewtils.rest.easy_client.EasyClient._find_request_by_id')
    def test_find_unique_request_by_id(self, find_mock):
        self.client.find_unique_request(id='id')
        find_mock.assert_called_with('id')

    def test_find_unique_request_none(self):
        self.client.find_requests = Mock(return_value=None)
        self.assertIsNone(self.client.find_unique_request())

    def test_find_unique_request_one(self):
        self.client.find_requests = Mock(return_value=['request1'])
        self.assertEqual('request1', self.client.find_unique_request())

    def test_find_unique_request_multiple(self):
        self.client.find_requests = Mock(return_value=['request1', 'request2'])
        self.assertRaises(FetchError, self.client.find_unique_request)

    @patch('brewtils.rest.client.RestClient.get_requests')
    def test_find_requests(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_request = Mock(return_value='request')

        self.assertEqual('request', self.client.find_requests(search='params'))
        self.parser.parse_request.assert_called_with('payload', many=True)
        mock_get.assert_called_with(search='params')

    @patch('brewtils.rest.client.RestClient.get_requests')
    def test_find_requests_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError,
                          self.client.find_requests,
                          search='params')
        mock_get.assert_called_with(search='params')

    @patch('brewtils.rest.client.RestClient.get_requests')
    def test_find_requests_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError,
                          self.client.find_requests,
                          search='params')

    @patch('brewtils.rest.client.RestClient.get_request')
    def test_find_request_by_id(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_request = Mock(return_value='request')

        self.assertEqual(self.client._find_request_by_id('id'), 'request')
        self.parser.parse_request.assert_called_with('payload')
        mock_get.assert_called_with('id')

    @patch('brewtils.rest.client.RestClient.get_request')
    def test_find_request_by_id_404(self, mock_get):
        mock_get.return_value = self.fake_not_found_error_response

        self.assertIsNone(self.client._find_request_by_id('id'))
        mock_get.assert_called_with('id')

    @patch('brewtils.rest.client.RestClient.get_request')
    def test_find_request_by_id_server_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client._find_request_by_id, 'id')
        mock_get.assert_called_with('id')

    @patch('brewtils.rest.client.RestClient.get_request')
    def test_find_request_by_id_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client._find_request_by_id,
                          'id')

    # Create request
    @patch('brewtils.rest.client.RestClient.post_requests')
    def test_create_request(self, mock_post):
        mock_post.return_value = self.fake_success_response
        self.parser.serialize_request = Mock(return_value='json_request')
        self.parser.parse_request = Mock(return_value='request_response')

        self.assertEqual('request_response',
                         self.client.create_request('request'))
        self.parser.serialize_request.assert_called_with('request')
        self.parser.parse_request.assert_called_with('payload')

    @patch('brewtils.rest.client.RestClient.post_requests')
    def test_create_request_errors(self, mock_post):
        mock_post.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.create_request,
                          'request')

        mock_post.return_value = self.fake_wait_exceeded_response
        self.assertRaises(WaitExceededError, self.client.create_request,
                          'request')

        mock_post.return_value = self.fake_server_error_response
        self.assertRaises(SaveError, self.client.create_request, 'request')

        mock_post.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.create_request,
                          'request')

    # Update request
    @patch('brewtils.rest.client.RestClient.patch_request')
    def test_update_request(self, request_mock):
        request_mock.return_value = self.fake_success_response

        self.client.update_request('id',
                                   status='new_status',
                                   output='new_output',
                                   error_class='ValueError')
        self.parser.parse_request.assert_called_with('payload')
        self.assertEqual(1, request_mock.call_count)
        payload = request_mock.call_args[0][1]
        self.assertNotEqual(-1, payload.find('new_status'))
        self.assertNotEqual(-1, payload.find('new_output'))
        self.assertNotEqual(-1, payload.find('ValueError'))

    @patch('brewtils.rest.client.RestClient.patch_request')
    def test_update_request_client_error(self, request_mock):
        request_mock.return_value = self.fake_client_error_response

        self.assertRaises(ValidationError, self.client.update_request, 'id')
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_request')
    def test_update_request_server_error(self, request_mock):
        request_mock.return_value = self.fake_server_error_response

        self.assertRaises(SaveError, self.client.update_request, 'id')
        request_mock.assert_called_once_with('id', ANY)

    @patch('brewtils.rest.client.RestClient.patch_request')
    def test_update_request_connection_error(self, request_mock):
        request_mock.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.update_request,
                          'id')

    # Publish Event
    @patch('brewtils.rest.client.RestClient.post_event')
    def test_publish_event(self, mock_post):
        mock_post.return_value = self.fake_success_response
        self.assertTrue(self.client.publish_event(Mock()))

    @patch('brewtils.rest.client.RestClient.post_event')
    def test_publish_event_errors(self, mock_post):
        mock_post.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.publish_event, 'system')

        mock_post.return_value = self.fake_server_error_response
        self.assertRaises(RestError, self.client.publish_event, 'system')

        mock_post.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.publish_event,
                          'system')

    # Queues
    @patch('brewtils.rest.client.RestClient.get_queues')
    def test_get_queues(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.get_queues()
        self.assertTrue(self.parser.parse_queue.called)

    @patch('brewtils.rest.client.RestClient.get_queues')
    def test_get_queues_errors(self, mock_get):
        mock_get.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.get_queues)

        mock_get.return_value = self.fake_server_error_response
        self.assertRaises(RestError, self.client.get_queues)

        mock_get.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.get_queues)

    @patch('brewtils.rest.client.RestClient.delete_queue')
    def test_clear_queue(self, mock_delete):
        mock_delete.return_value = self.fake_success_response
        self.assertTrue(self.client.clear_queue('queue'))

    @patch('brewtils.rest.client.RestClient.delete_queue')
    def test_clear_queue_errors(self, mock_delete):
        mock_delete.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.clear_queue, 'queue')

        mock_delete.return_value = self.fake_server_error_response
        self.assertRaises(RestError, self.client.clear_queue, 'queue')

        mock_delete.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.clear_queue,
                          'queue')

    @patch('brewtils.rest.client.RestClient.delete_queues')
    def test_clear_all_queues(self, mock_delete):
        mock_delete.return_value = self.fake_success_response
        self.assertTrue(self.client.clear_all_queues())

    @patch('brewtils.rest.client.RestClient.delete_queues')
    def test_clear_all_queues_errors(self, mock_delete):
        mock_delete.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.clear_all_queues)

        mock_delete.return_value = self.fake_server_error_response
        self.assertRaises(RestError, self.client.clear_all_queues)

        mock_delete.return_value = self.fake_connection_error_response
        self.assertRaises(RestConnectionError, self.client.clear_all_queues)

    # Find Jobs
    @patch('brewtils.rest.client.RestClient.get_jobs')
    def test_find_jobs(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.parser.parse_job = Mock(return_value='job')

        self.assertEqual('job', self.client.find_jobs(search='params'))
        self.parser.parse_job.assert_called_with('payload', many=True)
        mock_get.assert_called_with(search='params')

    @patch('brewtils.rest.client.RestClient.get_jobs')
    def test_find_jobs_error(self, mock_get):
        mock_get.return_value = self.fake_server_error_response

        self.assertRaises(FetchError, self.client.find_jobs, search='params')
        mock_get.assert_called_with(search='params')

    # Create Jobs
    @patch('brewtils.rest.client.RestClient.post_jobs')
    def test_create_job(self, mock_post):
        mock_post.return_value = self.fake_success_response
        self.parser.serialize_job = Mock(return_value='json_job')
        self.parser.parse_job = Mock(return_value='job_response')

        self.assertEqual('job_response', self.client.create_job('job'))
        self.parser.serialize_job.assert_called_with('job')
        self.parser.parse_job.assert_called_with('payload')

    @patch('brewtils.rest.client.RestClient.post_jobs')
    def test_create_job_error(self, mock_post):
        mock_post.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.create_job, 'job')

    # Remove Job
    @patch('brewtils.rest.client.RestClient.delete_job')
    def test_delete_job(self, mock_delete):
        mock_delete.return_value = self.fake_success_response
        self.assertEqual(True, self.client.remove_job('job_id'))

    @patch('brewtils.rest.client.RestClient.delete_job')
    def test_delete_job_error(self, mock_delete):
        mock_delete.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.remove_job, 'job_id')

    # Pause Job
    @patch('brewtils.rest.easy_client.PatchOperation')
    @patch('brewtils.rest.client.RestClient.patch_job')
    def test_pause_job(self, mock_patch, MockPatch):
        MockPatch.return_value = "patch"
        mock_patch.return_value = self.fake_success_response

        self.client.pause_job('id')
        MockPatch.assert_called_with('update', '/status', 'PAUSED')
        self.parser.serialize_patch.assert_called_with(["patch"], many=True)
        self.parser.parse_job.assert_called_with('payload')

    @patch('brewtils.rest.client.RestClient.patch_job')
    def test_pause_job_error(self, mock_patch):
        mock_patch.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.pause_job, 'id')

    @patch('brewtils.rest.easy_client.PatchOperation')
    @patch('brewtils.rest.client.RestClient.patch_job')
    def test_resume_job(self, mock_patch, MockPatch):
        MockPatch.return_value = "patch"
        mock_patch.return_value = self.fake_success_response

        self.client.resume_job('id')
        MockPatch.assert_called_with('update', '/status', 'RUNNING')
        self.parser.serialize_patch.assert_called_with(["patch"], many=True)
        self.parser.parse_job.assert_called_with('payload')

    # Users
    @patch('brewtils.rest.client.RestClient.get_user')
    def test_who_am_i(self, mock_get):
        self.client.who_am_i()
        mock_get.assert_called_with('anonymous')

    @patch('brewtils.rest.client.RestClient.get_user')
    def test_get_user(self, mock_get):
        mock_get.return_value = self.fake_success_response
        self.client.get_user('identifier')
        self.assertTrue(self.parser.parse_principal.called)

    @patch('brewtils.rest.client.RestClient.get_user')
    def test_get_user_errors(self, mock_get):
        mock_get.return_value = self.fake_client_error_response
        self.assertRaises(ValidationError, self.client.get_user, 'identifier')

        mock_get.return_value = self.fake_not_found_error_response
        self.assertRaises(NotFoundError, self.client.get_user, 'identifier')