Source code for warthog.client

# -*- coding: utf-8 -*-
#
# Warthog - Simple client for A10 load balancers
#
# Copyright 2014-2016 Smarter Travel
#
# Available under the MIT license. See LICENSE for details.
#

"""
warthog.client
~~~~~~~~~~~~~~

Simple interface for a load balancer with retry logic and intelligent draining of nodes.
"""

import contextlib
import time

import warthog.core
import warthog.exceptions
import warthog.transport


[docs]class CommandFactory(object): """Factory for getting new :mod:`warthog.core` command instances that each perform some type of request against the load balancer API. It is typically not required for user code to instantiate this class directly unless you have special requirements and need to inject a custom ``transport_factory`` method. This class is thread safe. """
[docs] def __init__(self, transport_factory): """Set the a factory that will create new HTTP Sessions instances to be used for executing commands. :param callable transport_factory: Callable for creating new Session instances for executing commands. """ self._transport_factory = transport_factory
[docs] def get_session_start(self, scheme_host, username, password): """Get a new command instance to start a session. :param basestring scheme_host: Scheme, host, and port combination of the load balancer. :param basestring username: Name of the user to authenticate with. :param basestring password: Password for the user to authenticate with. :return: A new command to start a session. :rtype: warthog.core.SessionStartCommand """ return warthog.core.SessionStartCommand( self._transport_factory(), scheme_host, username, password)
[docs] def get_session_end(self, scheme_host, session_id): """Get a new command instance to close an existing session. :param basestring scheme_host: Scheme, host, and port combination of the load balancer. :param basestring session_id: Previously authenticated session ID. :return: A new command to close a session. :rtype: warthog.core.SessionEndCommand """ return warthog.core.SessionEndCommand( self._transport_factory(), scheme_host, session_id)
[docs] def get_server_status(self, scheme_host, session_id, server): """Get a new command to get the status (enabled / disabled) of a server. :param basestring scheme_host: Scheme, host, and port combination of the load balancer. :param basestring session_id: Previously authenticated session ID. :param basestring server: Host name of the server to get the status of. :return: A new command to get the status of a server. :rtype: warthog.core.NodeStatusCommand """ return warthog.core.NodeStatusCommand( self._transport_factory(), scheme_host, session_id, server)
[docs] def get_enable_server(self, scheme_host, session_id, server): """Get a new command to enable a server at the node level. :param basestring scheme_host: Scheme, host, and port combination of the load balancer. :param basestring session_id: Previously authenticated session ID. :param basestring server: Host name of the server to enable. :return: A new command to enable a server. :rtype: warthog.core.NodeEnableCommand """ return warthog.core.NodeEnableCommand( self._transport_factory(), scheme_host, session_id, server)
[docs] def get_disable_server(self, scheme_host, session_id, server): """Get a new command to disable a server at the node level. :param basestring scheme_host: Scheme, host, and port combination of the load balancer. :param basestring session_id: Previously authenticated session ID. :param basestring server: Host name of the server to disable. :return: A new command to disable a server. :rtype: warthog.core.NodeDisableCommand """ return warthog.core.NodeDisableCommand( self._transport_factory(), scheme_host, session_id, server)
[docs] def get_active_connections(self, scheme_host, session_id, server): """Get a new command to get the number of active connections to a server. :param basestring scheme_host: Scheme, host, and port combination of the load balancer. :param basestring session_id: Previously authenticated session ID. :param basestring server: Host name of the server to get the number of active connections to. :return: A new command to get active connections to a server. :rtype: warthog.core.NodeActiveConnectionsCommand """ return warthog.core.NodeActiveConnectionsCommand( self._transport_factory(), scheme_host, session_id, server)
def _get_default_cmd_factory(verify, ssl_version, retries): """Get a :class:`CommandFactory` instance configured to use the provided TLS version and cert verification policy :param bool verify: ``True`` to perform certificate validation when using HTTPS, ``False`` otherwise, ``None`` to use the default. :param int ssl_version: :mod:`ssl` module constant for specifying which SSL or TLS version to use for connecting to the load balancer over HTTPS, ``None`` to use the default. :param int retries: The maximum number of times to retry operations on transient network errors. :return: Default command factory for building new commands to interact with the A10 load balancer. :rtype: WarthogCommandFactory """ return CommandFactory(warthog.transport.get_transport_factory( verify=verify, ssl_version=ssl_version, retries=retries ))
[docs]class WarthogClient(object): """Client for interacting with an A10 load balancer to get the status of nodes managed by it, enable them, and disable them. This class is thread safe. .. versionchanged:: 0.8.0 Removed .disabled_context() method. """ _logger = warthog.core.get_log() # pylint: disable=too-many-arguments
[docs] def __init__(self, scheme_host, username, password, verify=None, ssl_version=None, network_retries=None, commands=None): """Set the load balancer scheme/host/port combination, username and password to use for connecting and authenticating with the load balancer. Whether or not to verify certificates when using HTTPS may be toggled via the ``verify`` parameter. This can enable you to use a self signed certificate for the load balancer while still using HTTPS. The version of SSL or TLS to use may be specified as a :mod:`ssl` module protocol constant via the ``ssl_version`` parameter. The maximum number of times to retry network operations on transient errors can be specified via the ``network_retries`` parameter. If the command factory is not supplied, a default instance will be used. The command factory is responsible for creating new :class:`requests.Session` instances to be used by each command. It is typically only necessary to override this for unit testing purposes. .. versionchanged:: 0.9.0 Added the optional ``verify`` parameter to make use of self-signed certs easier. .. versionchanged:: 0.10.0 Added the optional ``ssl_version`` parameter to make use of alternate SSL or TLS versions easier. .. versionchanged:: 2.0.0 Added the optional ``network_retries`` parameter to make use of retry logic on transient network errors. A non-zero number of retries is used by default if this is not specified. Previously, no retries were attempted on transient network errors. .. versionchanged:: 2.0.0 Removed the optional ``wait_interval`` parameter. This is now passed directly as an argument to :meth:`enable_node` or :meth:`disable_node` methods. :param basestring scheme_host: Scheme, host, and port combination of the load balancer. :param basestring username: Name of the user to authenticate with. :param basestring password: Password for the user to authenticate with. :param bool|None verify: ``True`` to verify certificates when using HTTPS, ``False`` to skip verification, ``None`` to use the library default. The default is to verify certificates. :param int|None ssl_version: :mod:`ssl` module constant for specifying which version of SSL or TLS to use when connecting to the load balancer over HTTPS, ``None`` to use the library default. The default is to use TLSv1.2. :param int|None network_retries: Maximum number of times to retry network operations on transient network errors. Default is to retry network operations a non-zero number of times. :param CommandFactory commands: Factory instance for creating new commands for starting and ending sessions with the load balancer. """ self._scheme_host = scheme_host self._username = username self._password = password self._commands = commands if commands is not None else \ _get_default_cmd_factory(verify, ssl_version, network_retries)
@contextlib.contextmanager def _session_context(self): """Context manager that makes a request to start an authenticated session, yields the session ID, and then closes the session afterwards. :return: The session ID of the newly established session. """ self._logger.debug('Creating new session context for %s', self._scheme_host) session = None try: start_cmd = self._commands.get_session_start( self._scheme_host, self._username, self._password) session = start_cmd.send() yield session finally: if session is not None: end_cmd = self._commands.get_session_end(self._scheme_host, session) end_cmd.send()
[docs] def get_status(self, server): """Get the current status of the given server, at the node level. The status will be one of the constants :data:`warthog.core.STATUS_ENABLED` :data:`warthog.core.STATUS_DISABLED`, or :data:`warthog.core.STATUS_DOWN`. :param basestring server: Hostname of the server to get the status of. :return: The current status of the server, enabled, disabled, or down. :rtype: basestring :raises warthog.exceptions.WarthogAuthFailureError: If authentication with the load balancer failed when trying to establish a new session for this operation. :raises warthog.exceptions.WarthogNoSuchNodeError: If the load balancer does not recognize the given hostname. :raises warthog.exceptions.WarthogApiError: If there are any other problems getting the status of the given server. """ with self._session_context() as session: cmd = self._commands.get_server_status(self._scheme_host, session, server) return cmd.send()
[docs] def get_connections(self, server): """Get the current number of active connections to a server, at the node level. The number of connections will be 0 or a positive integer. :param basestring server: Hostname of the server to get the number of active connections for. :return: The number of active connections total for the node, across all groups the server is in. :rtype: int :raises warthog.exceptions.WarthogAuthFailureError: If authentication with the load balancer failed when trying to establish a new session for this operation. :raises warthog.exceptions.WarthogNoSuchNodeError: If the load balancer does not recognize the given hostname. :raises warthog.exceptions.WarthogApiError: If there are any other problems getting the active connections for the given server. .. versionadded:: 0.4.0 """ with self._session_context() as session: cmd = self._commands.get_active_connections(self._scheme_host, session, server) return cmd.send()
[docs] def disable_server(self, server, max_retries=5, wait_interval=2.0): """Disable a server at the node level, optionally retrying when there are transient errors and waiting for the number of active connections to the server to reach zero. If ``max_retries`` is zero, no attempt will be made to wait until there are no active connections to the server, the method will try a single time to disable the server and then return immediately. .. versionchanged:: 2.0.0 Added the optional ``wait_interval`` parameter. :param basestring server: Hostname of the server to disable :param int max_retries: Max number of times to sleep and retry while waiting for the number of active connections to a server to reach zero. :param float wait_interval: How long (in seconds) to wait between each check to see if the number of active connections to a server has reached zero. :return: True if the server was disabled, false otherwise. :rtype: bool :raises warthog.exceptions.WarthogAuthFailureError: If authentication with the load balancer failed when trying to establish a new session for this operation. :raises warthog.exceptions.WarthogNoSuchNodeError: If the load balancer does not recognize the given hostname. :raises warthog.exceptions.WarthogApiError: If there are any other problems disabling the given server. """ with self._session_context() as session: disable = self._commands.get_disable_server(self._scheme_host, session, server) disable.send() active = self._commands.get_active_connections(self._scheme_host, session, server) self._wait_for_connections(active.send, max_retries, wait_interval) status = self._commands.get_server_status(self._scheme_host, session, server) return warthog.core.STATUS_DISABLED == status.send()
def _wait_for_connections(self, conn_method, max_retries, interval): """Repeatedly execute a command to get the number of active connections until the number of active connections drops to zero or we run out of retries. """ retries = 0 while retries < max_retries: conns = conn_method() if conns == 0: break self._logger.debug( "Connections still active: %s, sleeping for %s seconds...", conns, interval) time.sleep(interval) retries += 1
[docs] def enable_server(self, server, max_retries=5, wait_interval=2.0): """Enable a server at the node level, optionally retrying when there are transient errors and waiting for the server to enter the expected, enabled state. If ``max_retries`` is zero, no attempt will be made to wait until the server enters the expected, enabled state, the method will try a single time to enable the server then return immediately. .. versionchanged:: 2.0.0 Added the optional ``wait_interval`` parameter. :param basestring server: Hostname of the server to enable :param int max_retries: Max number of times to sleep and retry while waiting for the server to enter the "enabled" state. :param float wait_interval: How long (in seconds) to wait between each check to see if the server has entered the "enabled" state. :return: True if the server was enabled, false otherwise :rtype: bool :raises warthog.exceptions.WarthogAuthFailureError: If authentication with the load balancer failed when trying to establish a new session for this operation. :raises warthog.exceptions.WarthogNoSuchNodeError: If the load balancer does not recognize the given hostname. :raises warthog.exceptions.WarthogApiError: If there are any other problems enabling the given server. """ with self._session_context() as session: enable = self._commands.get_enable_server(self._scheme_host, session, server) enable.send() status = self._commands.get_server_status(self._scheme_host, session, server) self._wait_for_enable(status.send, max_retries, wait_interval) return warthog.core.STATUS_ENABLED == status.send()
def _wait_for_enable(self, status_method, max_retries, interval): """Repeatedly execute a command to get the status of a node until the node becomes enabled or we run out of retries. """ retries = 0 while retries < max_retries: status = status_method() if status == warthog.core.STATUS_ENABLED: break self._logger.debug( "Server is not yet enabled (%s), sleeping for %s seconds...", status, interval) time.sleep(interval) retries += 1