files: """ Manages HAProxy operations. """
  import errno
  import logging
  import os
  import pkgutil
  import subprocess
  import time
  
  from tornado import gen
  
  from appscale.common.appscale_info import get_private_ip
  
  logger = logging.getLogger('appscale-admin')
  
  # The directory that contains HAProxy config files.
  CONFIG_DIR = os.path.join('/', 'etc', 'haproxy')
  
  
  class InvalidConfig(Exception):
    """ Indicates that a given HAProxy configuration cannot be enforced. """
    pass
  
  
  class HAProxyAppVersion(object):
    """ Represents a version's HAProxy configuration. """
  
    # The template for a server config line.
    SERVER_TEMPLATE = ('server gae_{version}-{server} {server} '
                       'maxconn {max_connections} check')
  
    # The template for a version block.
    VERSION_TEMPLATE = pkgutil.get_data('appscale.admin.routing',
                                        'templates/version.cfg')
  
    def __init__(self, version_key, port, max_connections):
      """ Creates a new HAProxyAppVersion instance.
  
      Args:
        version_key: A string specifying a version
      """
      self.version_key = version_key
      self.port = port
      self.max_connections = max_connections
      self.servers = []
  
      self._private_ip = get_private_ip()
  
    def __repr__(self):
      """ Returns a print-friendly representation of the version config. """
      return 'HAProxyAppVersion<{}:{}, maxconn:{}, servers:{}>'.format(
        self.version_key, self.port, self.max_connections, self.servers)
  
    @property
    def block(self):
      """ Represents the version as a configuration block.
  
      Returns:
        A string containing the configuration block or None.
      """
      if not self.servers:
        return None
  
      server_lines = [
        self.SERVER_TEMPLATE.format(version=self.version_key, server=server,
                                    max_connections=self.max_connections)
        for server in self.servers]
      server_lines.sort()
      bind_location = ':'.join([self._private_ip, str(self.port)])
      return self.VERSION_TEMPLATE.format(
        version=self.version_key, bind_location=bind_location,
        servers='\n  '.join(server_lines))
  
  
  class HAProxy(object):
    """ Manages HAProxy operations. """
  
    # The location of the combined HAProxy config file for AppServer instances.
    APP_CONFIG = os.path.join(CONFIG_DIR, 'app-haproxy.cfg')
  
    # The location of the pidfile for instance-related HAProxy processes.
    APP_PID = os.path.join('/', 'var', 'run', 'app-haproxy.pid')
  
    # The location of the unix socket used for reporting stats.
    APP_STATS_SOCKET = os.path.join(CONFIG_DIR, 'stats')
  
    # The template for the configuration file.
    BASE_TEMPLATE = pkgutil.get_data('appscale.admin.routing',
                                     'templates/base.cfg')
  
    # The seconds a request can wait in the queue before it fails with a 503.
    DEFAULT_CONNECT_TIMEOUT = 120
  
    # The seconds a client is allowed to remain inactive.
    DEFAULT_CLIENT_TIMEOUT = 50
  
    # The seconds a downstream server can hold onto a request.
    DEFAULT_SERVER_TIMEOUT = 600
  
    # The minimum number of seconds to wait between each reload operation.
    RELOAD_COOLDOWN = .1
  
    def __init__(self):
      """ Creates a new HAProxy operator. """
      self.connect_timeout_ms = self.DEFAULT_CONNECT_TIMEOUT * 1000
      self.client_timeout_ms = self.DEFAULT_CLIENT_TIMEOUT * 1000
      self.server_timeout_ms = self.DEFAULT_SERVER_TIMEOUT * 1000
      self.versions = {}
      self.reload_future = None
      self.last_reload = time.time()
  
    @property
    def config(self):
      """ Represents the current state as an HAProxy configuration file.
  
      Returns:
        A string containing a complete HAProxy configuration.
      """
      unique_ports = set()
      for version_key, version in self.versions.items():
        if version.port in unique_ports:
          raise InvalidConfig('Port {} is used by more than one '
                              'version'.format(version.port))
  
        unique_ports.add(version.port)
  
      version_blocks = [self.versions[key].block
                        for key in sorted(self.versions.keys())
                        if self.versions[key].block]
      return self.BASE_TEMPLATE.format(
        stats_socket=self.APP_STATS_SOCKET,
        connect_timeout=self.connect_timeout_ms,
        client_timeout=self.client_timeout_ms,
        server_timeout=self.server_timeout_ms,
        versions='\n'.join(version_blocks))
  
    @gen.coroutine
    def reload(self):
      """ Groups closely-timed reload operations. """
      if self.reload_future is None or self.reload_future.done():
        self.reload_future = self._reload()
  
      yield self.reload_future
  
    @gen.coroutine
    def _reload(self):
      """ Updates the routing entries if they've changed. """
      time_since_reload = time.time() - self.last_reload
      wait_time = max(self.RELOAD_COOLDOWN - time_since_reload, 0)
      yield gen.sleep(wait_time)
      self.last_reload = time.time()
  
      try:
        new_content = self.config
      except InvalidConfig as error:
        logger.error(str(error))
        return
  
      try:
        with open(self.APP_CONFIG, 'r') as app_config_file:
          existing_content = app_config_file.read()
      except IOError as error:
        if error.errno != errno.ENOENT:
          raise
  
        existing_content = ''
  
      if new_content == existing_content:
        return
  
      with open(self.APP_CONFIG, 'w') as app_config_file:
        app_config_file.write(new_content)
  
      subprocess.check_call(['haproxy', '-f', self.APP_CONFIG, '-D',
                             '-p', self.APP_PID])
      logger.info('Updated HAProxy config')
  
files: AdminServer/appscale/admin/routing/haproxy.py
files: """ Manages HAProxy operations. """
  import errno
  import logging
  import os
  import pkgutil
  import subprocess
  import time
  
  from tornado import gen
  
  from appscale.common.appscale_info import get_private_ip
  
  logger = logging.getLogger('appscale-admin')
  
  # The directory that contains HAProxy config files.
  CONFIG_DIR = os.path.join('/', 'etc', 'haproxy')
  
  
  class HAProxyAppVersion(object):
    """ Represents a version's HAProxy configuration. """
  
    # The template for a server config line.
    SERVER_TEMPLATE = ('server gae_{version}-{server} {server} '
                       'maxconn {max_connections} check')
  
    # The template for a version block.
    VERSION_TEMPLATE = pkgutil.get_data('appscale.admin.routing',
                                        'templates/version.cfg')
  
    def __init__(self, version_key, port, max_connections):
      """ Creates a new HAProxyAppVersion instance.
  
      Args:
        version_key: A string specifying a version
      """
      self.version_key = version_key
      self.port = port
      self.max_connections = max_connections
      self.servers = []
  
      self._private_ip = get_private_ip()
  
    def __repr__(self):
      """ Returns a print-friendly representation of the version config. """
      return 'HAProxyAppVersion<{}:{}, maxconn:{}, servers:{}>'.format(
        self.version_key, self.port, self.max_connections, self.servers)
  
    @property
    def block(self):
      """ Represents the version as a configuration block.
  
      Returns:
        A string containing the configuration block or None.
      """
      if not self.servers:
        return None
  
      server_lines = [
        self.SERVER_TEMPLATE.format(version=self.version_key, server=server,
                                    max_connections=self.max_connections)
        for server in self.servers]
      server_lines.sort()
      bind_location = ':'.join([self._private_ip, str(self.port)])
      return self.VERSION_TEMPLATE.format(
        version=self.version_key, bind_location=bind_location,
        servers='\n  '.join(server_lines))
  
  
  class HAProxy(object):
    """ Manages HAProxy operations. """
  
    # The location of the combined HAProxy config file for AppServer instances.
    APP_CONFIG = os.path.join(CONFIG_DIR, 'app-haproxy.cfg')
  
    # The location of the pidfile for instance-related HAProxy processes.
    APP_PID = os.path.join('/', 'var', 'run', 'app-haproxy.pid')
  
    # The location of the unix socket used for reporting stats.
    APP_STATS_SOCKET = os.path.join(CONFIG_DIR, 'stats')
  
    # The template for the configuration file.
    BASE_TEMPLATE = pkgutil.get_data('appscale.admin.routing',
                                     'templates/base.cfg')
  
    # The seconds a request can wait in the queue before it fails with a 503.
    DEFAULT_CONNECT_TIMEOUT = 120
  
    # The seconds a client is allowed to remain inactive.
    DEFAULT_CLIENT_TIMEOUT = 50
  
    # The seconds a downstream server can hold onto a request.
    DEFAULT_SERVER_TIMEOUT = 600
  
    # The minimum number of seconds to wait between each reload operation.
    RELOAD_COOLDOWN = .1
  
    def __init__(self):
      """ Creates a new HAProxy operator. """
      self.connect_timeout_ms = self.DEFAULT_CONNECT_TIMEOUT * 1000
      self.client_timeout_ms = self.DEFAULT_CLIENT_TIMEOUT * 1000
      self.server_timeout_ms = self.DEFAULT_SERVER_TIMEOUT * 1000
      self.versions = {}
      self.reload_future = None
      self.last_reload = time.time()
  
    @property
    def config(self):
      """ Represents the current state as an HAProxy configuration file.
  
      Returns:
        A string containing a complete HAProxy configuration.
      """
      version_blocks = [self.versions[key].block
                        for key in sorted(self.versions.keys())
                        if self.versions[key].block]
      return self.BASE_TEMPLATE.format(
        stats_socket=self.APP_STATS_SOCKET,
        connect_timeout=self.connect_timeout_ms,
        client_timeout=self.client_timeout_ms,
        server_timeout=self.server_timeout_ms,
        versions='\n'.join(version_blocks))
  
    @gen.coroutine
    def reload(self):
      """ Groups closely-timed reload operations. """
      if self.reload_future is None or self.reload_future.done():
        self.reload_future = self._reload()
  
      yield self.reload_future
  
    @gen.coroutine
    def _reload(self):
      """ Updates the routing entries if they've changed. """
      time_since_reload = time.time() - self.last_reload
      wait_time = max(self.RELOAD_COOLDOWN - time_since_reload, 0)
      yield gen.sleep(wait_time)
      self.last_reload = time.time()
  
      new_content = self.config
  
      try:
        with open(self.APP_CONFIG, 'r') as app_config_file:
          existing_content = app_config_file.read()
      except IOError as error:
        if error.errno != errno.ENOENT:
          raise
  
        existing_content = ''
  
      if new_content == existing_content:
        return
  
      with open(self.APP_CONFIG, 'w') as app_config_file:
        app_config_file.write(new_content)
  
      subprocess.check_call(['haproxy', '-f', self.APP_CONFIG, '-D',
                             '-p', self.APP_PID])
      logger.info('Updated HAProxy config')
  
files: AdminServer/appscale/admin/routing/haproxy.py
files: """ Configures routing for AppServer instances. """
  import json
  import logging
  
  from tornado.ioloop import IOLoop
  
  from appscale.admin.routing.haproxy import HAProxy, HAProxyAppVersion
  from appscale.common.async_retrying import retry_children_watch_coroutine
  from appscale.common.constants import (VERSION_PATH_SEPARATOR,
                                         VERSION_REGISTRATION_NODE)
  
  logger = logging.getLogger('appscale-admin')
  
  
  class VersionRoutingManager(object):
    """ Configures routing for an AppServer instance. """
  
    # The default number of concurrent connections allowed.
    DEFAULT_MAX_CONNECTIONS = 7
  
    def __init__(self, version_key, zk_client, haproxy):
      """ Creates a new VersionRoutingManager object.
  
      Args:
        version_key: A string specifying a version key.
        zk_client: A KazooClient.
        haproxy: An HAProxy object.
      """
      # Indicates that the watch is still needed.
      self._active = True
  
      self._version_key = version_key
      self._haproxy = haproxy
      self._instances = []
      self._port = None
      self._max_connections = None
      self._zk_client = zk_client
  
      instances_node = '/'.join([VERSION_REGISTRATION_NODE, self._version_key])
      self._zk_client.ensure_path(instances_node)
      self._zk_client.ChildrenWatch(instances_node, self._update_instances_watch)
  
      project_id, service_id, version_id = self._version_key.split(
        VERSION_PATH_SEPARATOR)
      version_node = '/appscale/projects/{}/services/{}/versions/{}'.format(
        project_id, service_id, version_id)
      self._zk_client.DataWatch(version_node, self._update_version_watch)
  
    def stop(self):
      """ Stops routing all instances for the version. """
      self._active = False
      self._instances = []
      self._port = None
      self._max_connections = None
      self._update_version_block()
  
    def _update_instances(self, instances):
      """ Handles changes to list of registered instances.
  
      Args:
        versions: A list of strings specifying registered instances.
      """
      self._instances = instances
      self._update_version_block()
  
    def _update_instances_watch(self, instances):
      """ Handles changes to list of registered instances.
  
      Args:
        versions: A list of strings specifying registered instances.
      """
      if not self._active:
        return False
  
      IOLoop.instance().add_callback(self._update_instances, instances)
  
    def _update_version(self, encoded_version):
      """ Handles changes to the version details.
  
      Args:
        encoded_version: A JSON-encoded string containing version details.
      """
      if encoded_version is None:
        self._port = None
        self._max_connections = None
        self._update_version_block()
        return
  
      version_details = json.loads(encoded_version)
  
      # If the threadsafe value is not defined, the application can handle
      # concurrent requests.
      threadsafe = version_details.get('threadsafe', True)
      if threadsafe:
        self._max_connections = self.DEFAULT_MAX_CONNECTIONS
      else:
        self._max_connections = 1
  
      self._port = version_details.get('appscaleExtensions', {}).\
        get('haproxyPort')
  
      self._update_version_block()
  
    def _update_version_block(self):
      """ Updates HAProxy's version configuration and triggers a reload. """
  
      # If the port or max_connections is not known, it's not possible to route
      # the version.
      if (self._port is None or self._max_connections is None or
          not self._instances):
        self._haproxy.versions.pop(self._version_key, None)
        self._haproxy.reload()
        return
  
      if self._version_key not in self._haproxy.versions:
        self._haproxy.versions[self._version_key] = HAProxyAppVersion(
          self._version_key, self._port, self._max_connections)
  
      haproxy_app_version = self._haproxy.versions[self._version_key]
      haproxy_app_version.port = self._port
      haproxy_app_version.max_connections = self._max_connections
      haproxy_app_version.servers = self._instances
      self._haproxy.reload()
  
    def _update_version_watch(self, version_details, _):
      """ Handles changes to the version details.
  
      Args:
        version_details: A JSON-encoded string containing version details.
      """
      if not self._active:
        return False
  
      IOLoop.instance().add_callback(self._update_version, version_details)
  
  
  class RoutingManager(object):
    """ Configures routing for AppServer instances. """
    def __init__(self, zk_client, controller_state):
      """ Creates a new RoutingManager object.
  
      Args:
        zk_client: A KazooClient.
        controller_state: A ControllerState object.
      """
      self._controller_state = controller_state
      self._haproxy = HAProxy()
      self._versions = {}
      self._zk_client = zk_client
  
    def start(self):
      """ Starts updating routing configuration. """
      self._controller_state.add_callback(self._handle_controller_update)
  
      self._zk_client.ensure_path(VERSION_REGISTRATION_NODE)
      self._zk_client.ChildrenWatch(VERSION_REGISTRATION_NODE,
                                    self._update_versions_watch)
  
    def _update_versions(self, new_version_list):
      """ Handles changes to list of registered versions.
  
      This is intended to be run in the main IO loop.
  
      Args:
        new_version_list: A list of strings specifying registered versions.
      """
      to_stop = [version for version in self._versions
                 if version not in new_version_list]
      for version_key in to_stop:
        self._versions[version_key].stop()
        del self._versions[version_key]
  
      for version_key in new_version_list:
        if version_key not in self._versions:
          self._versions[version_key] = VersionRoutingManager(
            version_key, self._zk_client, self._haproxy)
  
    def _update_versions_watch(self, versions):
      """ Handles changes to list of registered versions.
  
      Args:
        versions: A list of strings specifying registered versions.
      """
      persistent_update_versions = retry_children_watch_coroutine(
        VERSION_REGISTRATION_NODE, self._update_versions)
      IOLoop.instance().add_callback(persistent_update_versions, versions)
  
    def _handle_controller_update(self, state):
      """ Handles changes to the controller state.
  
      Args:
        state: A dictionary containing the updated controller state.
      """
      connect_timeout_ms = state.get('@options', {}).\
        get('lb_connect_timeout', HAProxy.DEFAULT_CONNECT_TIMEOUT * 1000)
      try:
        connect_timeout_ms = int(connect_timeout_ms)
      except ValueError:
        logger.warning(
          'Invalid lb_connect_timeout value: {}'.format(connect_timeout_ms))
        connect_timeout_ms = HAProxy.DEFAULT_CONNECT_TIMEOUT * 1000
  
      if connect_timeout_ms != self._haproxy.connect_timeout_ms:
        self._haproxy.connect_timeout_ms = connect_timeout_ms
        self._haproxy.reload()
  
files: AdminServer/appscale/admin/routing/routing_manager.py
files: """ Configures routing for AppServer instances. """
  import json
  import logging
  
  from tornado.ioloop import IOLoop
  
  from appscale.admin.routing.haproxy import HAProxy, HAProxyAppVersion
  from appscale.common.async_retrying import retry_children_watch_coroutine
  from appscale.common.constants import (VERSION_PATH_SEPARATOR,
                                         VERSION_REGISTRATION_NODE)
  
  logger = logging.getLogger('appscale-admin')
  
  
  class VersionRoutingManager(object):
    """ Configures routing for an AppServer instance. """
  
    # The default number of concurrent connections allowed.
    DEFAULT_MAX_CONNECTIONS = 7
  
    def __init__(self, version_key, zk_client, haproxy):
      """ Creates a new VersionRoutingManager object.
  
      Args:
        version_key: A string specifying a version key.
        zk_client: A KazooClient.
        haproxy: An HAProxy object.
      """
      # Indicates that the watch is still needed.
      self.active = True
  
      self._version_key = version_key
      self._haproxy = haproxy
      self._instances = []
      self._port = None
      self._max_connections = None
      self._zk_client = zk_client
  
      instances_node = '/'.join([VERSION_REGISTRATION_NODE, self._version_key])
      self._zk_client.ensure_path(instances_node)
      self._zk_client.ChildrenWatch(instances_node, self._update_instances_watch)
  
      project_id, service_id, version_id = self._version_key.split(
        VERSION_PATH_SEPARATOR)
      version_node = '/appscale/projects/{}/services/{}/versions/{}'.format(
        project_id, service_id, version_id)
      self._zk_client.DataWatch(version_node, self._update_version_watch)
  
    def _update_instances(self, instances):
      """ Handles changes to list of registered instances.
  
      Args:
        versions: A list of strings specifying registered instances.
      """
      self._instances = instances
      self._update_version_block()
  
    def _update_instances_watch(self, instances):
      """ Handles changes to list of registered instances.
  
      Args:
        versions: A list of strings specifying registered instances.
      """
      if not self.active:
        return False
  
      IOLoop.instance().add_callback(self._update_instances, instances)
  
    def _update_version(self, encoded_version):
      """ Handles changes to the version details.
  
      Args:
        encoded_version: A JSON-encoded string containing version details.
      """
      try:
        version_details = json.loads(encoded_version)
      except (TypeError, ValueError):
        self._port = None
        self._max_connections = None
        logger.warning('Invalid version details: {}'.format(encoded_version))
        return
  
      # If the threadsafe value is not defined, the application can handle
      # concurrent requests.
      threadsafe = version_details.get('threadsafe', True)
      if threadsafe:
        self._max_connections = self.DEFAULT_MAX_CONNECTIONS
      else:
        self._max_connections = 1
  
      self._port = version_details.get('appscaleExtensions', {}).\
        get('haproxyPort')
  
      self._update_version_block()
  
    def _update_version_block(self):
      """ Updates HAProxy's version configuration and triggers a reload. """
  
      # If the port or max_connections is not known, it's not possible to route
      # the version.
      if not self._port or not self._max_connections:
        return
  
      if self._version_key not in self._haproxy.versions:
        self._haproxy.versions[self._version_key] = HAProxyAppVersion(
          self._version_key, self._port, self._max_connections)
  
      haproxy_app_version = self._haproxy.versions[self._version_key]
      haproxy_app_version.port = self._port
      haproxy_app_version.max_connections = self._max_connections
      haproxy_app_version.servers = self._instances
      self._haproxy.reload()
  
    def _update_version_watch(self, version_details, _):
      """ Handles changes to the version details.
  
      Args:
        version_details: A JSON-encoded string containing version details.
      """
      if not self.active:
        return False
  
      IOLoop.instance().add_callback(self._update_version, version_details)
  
  
  class RoutingManager(object):
    """ Configures routing for AppServer instances. """
    def __init__(self, zk_client, controller_state):
      """ Creates a new RoutingManager object.
  
      Args:
        zk_client: A KazooClient.
        controller_state: A ControllerState object.
      """
      self._controller_state = controller_state
      self._haproxy = HAProxy()
      self._versions = {}
      self._zk_client = zk_client
  
    def start(self):
      """ Starts updating routing configuration. """
      self._controller_state.add_callback(self._handle_controller_update)
  
      self._zk_client.ensure_path(VERSION_REGISTRATION_NODE)
      self._zk_client.ChildrenWatch(VERSION_REGISTRATION_NODE,
                                    self._update_versions_watch)
  
    def _update_versions(self, new_version_list):
      """ Handles changes to list of registered versions.
  
      This is intended to be run in the main IO loop.
  
      Args:
        new_version_list: A list of strings specifying registered versions.
      """
      to_stop = [version for version in self._versions
                 if version not in new_version_list]
      for version_key in to_stop:
        self._versions[version_key].active = False
        del self._versions[version_key]
  
      for version_key in new_version_list:
        if version_key not in self._versions:
          self._versions[version_key] = VersionRoutingManager(
            version_key, self._zk_client, self._haproxy)
  
    def _update_versions_watch(self, versions):
      """ Handles changes to list of registered versions.
  
      Args:
        versions: A list of strings specifying registered versions.
      """
      persistent_update_versions = retry_children_watch_coroutine(
        VERSION_REGISTRATION_NODE, self._update_versions)
      IOLoop.instance().add_callback(persistent_update_versions, versions)
  
    def _handle_controller_update(self, state):
      """ Handles changes to the controller state.
  
      Args:
        state: A dictionary containing the updated controller state.
      """
      connect_timeout_ms = state.get('@options', {}).\
        get('lb_connect_timeout', HAProxy.DEFAULT_CONNECT_TIMEOUT * 1000)
      try:
        connect_timeout_ms = int(connect_timeout_ms)
      except ValueError:
        logger.warning(
          'Invalid lb_connect_timeout value: {}'.format(connect_timeout_ms))
        connect_timeout_ms = HAProxy.DEFAULT_CONNECT_TIMEOUT * 1000
  
      if connect_timeout_ms != self._haproxy.connect_timeout_ms:
        self._haproxy.connect_timeout_ms = connect_timeout_ms
        self._haproxy.reload()
  
files: AdminServer/appscale/admin/routing/routing_manager.py