files.new_contents: """ A server that handles application deployments. """
  
  import argparse
  import base64
  import errno
  import hashlib
  import json
  import logging
  import os
  import re
  import sys
  import time
  
  from appscale.appcontroller_client import AppControllerException
  from appscale.common import appscale_info
  from appscale.common.constants import (
    HTTPCodes,
    LOG_FORMAT,
    VERSION_PATH_SEPARATOR,
    ZK_PERSISTENT_RECONNECTS
  )
  from appscale.common.monit_interface import MonitOperator
  from appscale.common.appscale_utils import get_md5
  from appscale.common.ua_client import UAClient
  from appscale.common.ua_client import UAException
  from concurrent.futures import ThreadPoolExecutor
  from datetime import datetime
  from kazoo.client import KazooClient
  from kazoo.exceptions import NodeExistsError
  from kazoo.exceptions import NoNodeError
  from kazoo.exceptions import NotEmptyError
  from tabulate import tabulate
  from tornado import gen
  from tornado.options import options
  from tornado import web
  from tornado.escape import json_decode
  from tornado.escape import json_encode
  from tornado.ioloop import IOLoop
  from . import utils
  from . import constants
  from .appengine_api import UpdateCronHandler
  from .appengine_api import UpdateIndexesHandler
  from .appengine_api import UpdateQueuesHandler
  from .base_handler import BaseHandler
  from .constants import (
    AccessTokenErrors,
    CustomHTTPError,
    OperationTimeout,
    REDEPLOY_WAIT,
    ServingStatus,
    SUPPORTED_INBOUND_SERVICES,
    VALID_RUNTIMES,
    VersionNotChanged
  )
  from .operation import (
    DeleteServiceOperation,
    CreateVersionOperation,
    DeleteVersionOperation,
    UpdateVersionOperation
  )
  from .operations_cache import OperationsCache
  from .push_worker_manager import GlobalPushWorkerManager
  from .service_manager import ServiceManager
  from .summary import get_combined_services
  
  logger = logging.getLogger('appscale-admin')
  
  # The state of each operation.
  operations = OperationsCache()
  
  
  @gen.coroutine
  def wait_for_port_to_open(http_port, operation_id, deadline):
    """ Waits until port is open.
  
    Args:
      http_port: An integer specifying the version's port number.
      operation_id: A string specifying an operation ID.
      deadline: A float containing a unix timestamp.
    Raises:
      OperationTimeout if the deadline is exceeded.
    """
    logger.debug('Waiting for {} to open'.format(http_port))
    try:
      operation = operations[operation_id]
    except KeyError:
      raise OperationTimeout('Operation no longer in cache')
  
    while True:
      if time.time() > deadline:
        message = 'Deploy operation took too long.'
        operation.set_error(message)
        raise OperationTimeout(message)
  
      if utils.port_is_open(options.login_ip, http_port):
        break
  
      yield gen.sleep(1)
  
    for load_balancer in appscale_info.get_load_balancer_ips():
      while True:
        if time.time() > deadline:
          # The version is reachable from the login IP, but it's not reachable
          # from every registered load balancer. It makes more sense to mark the
          # operation as a success than a failure because the lagging load
          # balancers should eventually reflect the registered instances.
          break
  
        if utils.port_is_open(load_balancer, http_port):
          break
  
        yield gen.sleep(1)
  
  
  @gen.coroutine
  def wait_for_deploy(operation_id, acc):
    """ Tracks the progress of a deployment.
  
    Args:
      operation_id: A string specifying the operation ID.
      acc: An AppControllerClient instance.
    Raises:
      OperationTimeout if the deadline is exceeded.
    """
    try:
      operation = operations[operation_id]
    except KeyError:
      raise OperationTimeout('Operation no longer in cache')
  
    start_time = time.time()
    deadline = start_time + constants.MAX_OPERATION_TIME
  
    http_port = operation.version['appscaleExtensions']['httpPort']
    yield wait_for_port_to_open(http_port, operation_id, deadline)
  
    url = 'http://{}:{}'.format(options.login_ip, http_port)
    operation.finish(url)
  
    logger.info('Finished operation {}'.format(operation_id))
  
  
  @gen.coroutine
  def wait_for_delete(operation_id, ports_to_close):
    """ Tracks the progress of removing version(s).
  
    Args:
      operation_id: A string specifying the operation ID.
      ports_to_close: A list of integers specifying the ports to wait for.
    Raises:
     
  
  [... the rest is omitted ...]
files.new_path: AdminServer/appscale/admin/__init__.py
files.old_contents: """ A server that handles application deployments. """
  
  import argparse
  import base64
  import errno
  import hashlib
  import json
  import logging
  import os
  import re
  import sys
  import time
  
  from appscale.appcontroller_client import AppControllerException
  from appscale.common import appscale_info
  from appscale.common.constants import (
    HTTPCodes,
    LOG_FORMAT,
    VERSION_PATH_SEPARATOR,
    ZK_PERSISTENT_RECONNECTS
  )
  from appscale.common.monit_interface import MonitOperator
  from appscale.common.appscale_utils import get_md5
  from appscale.common.ua_client import UAClient
  from appscale.common.ua_client import UAException
  from concurrent.futures import ThreadPoolExecutor
  from datetime import datetime
  from kazoo.client import KazooClient
  from kazoo.exceptions import NodeExistsError
  from kazoo.exceptions import NoNodeError
  from kazoo.exceptions import NotEmptyError
  from tabulate import tabulate
  from tornado import gen
  from tornado.options import options
  from tornado import web
  from tornado.escape import json_decode
  from tornado.escape import json_encode
  from tornado.ioloop import IOLoop
  from . import utils
  from . import constants
  from .appengine_api import UpdateCronHandler
  from .appengine_api import UpdateQueuesHandler
  from .base_handler import BaseHandler
  from .constants import (
    AccessTokenErrors,
    CustomHTTPError,
    OperationTimeout,
    REDEPLOY_WAIT,
    ServingStatus,
    SUPPORTED_INBOUND_SERVICES,
    VALID_RUNTIMES,
    VersionNotChanged
  )
  from .operation import (
    DeleteServiceOperation,
    CreateVersionOperation,
    DeleteVersionOperation,
    UpdateVersionOperation
  )
  from .operations_cache import OperationsCache
  from .push_worker_manager import GlobalPushWorkerManager
  from .service_manager import ServiceManager
  from .summary import get_combined_services
  
  logger = logging.getLogger('appscale-admin')
  
  # The state of each operation.
  operations = OperationsCache()
  
  
  @gen.coroutine
  def wait_for_port_to_open(http_port, operation_id, deadline):
    """ Waits until port is open.
  
    Args:
      http_port: An integer specifying the version's port number.
      operation_id: A string specifying an operation ID.
      deadline: A float containing a unix timestamp.
    Raises:
      OperationTimeout if the deadline is exceeded.
    """
    logger.debug('Waiting for {} to open'.format(http_port))
    try:
      operation = operations[operation_id]
    except KeyError:
      raise OperationTimeout('Operation no longer in cache')
  
    while True:
      if time.time() > deadline:
        message = 'Deploy operation took too long.'
        operation.set_error(message)
        raise OperationTimeout(message)
  
      if utils.port_is_open(options.login_ip, http_port):
        break
  
      yield gen.sleep(1)
  
    for load_balancer in appscale_info.get_load_balancer_ips():
      while True:
        if time.time() > deadline:
          # The version is reachable from the login IP, but it's not reachable
          # from every registered load balancer. It makes more sense to mark the
          # operation as a success than a failure because the lagging load
          # balancers should eventually reflect the registered instances.
          break
  
        if utils.port_is_open(load_balancer, http_port):
          break
  
        yield gen.sleep(1)
  
  
  @gen.coroutine
  def wait_for_deploy(operation_id, acc):
    """ Tracks the progress of a deployment.
  
    Args:
      operation_id: A string specifying the operation ID.
      acc: An AppControllerClient instance.
    Raises:
      OperationTimeout if the deadline is exceeded.
    """
    try:
      operation = operations[operation_id]
    except KeyError:
      raise OperationTimeout('Operation no longer in cache')
  
    start_time = time.time()
    deadline = start_time + constants.MAX_OPERATION_TIME
  
    http_port = operation.version['appscaleExtensions']['httpPort']
    yield wait_for_port_to_open(http_port, operation_id, deadline)
  
    url = 'http://{}:{}'.format(options.login_ip, http_port)
    operation.finish(url)
  
    logger.info('Finished operation {}'.format(operation_id))
  
  
  @gen.coroutine
  def wait_for_delete(operation_id, ports_to_close):
    """ Tracks the progress of removing version(s).
  
    Args:
      operation_id: A string specifying the operation ID.
      ports_to_close: A list of integers specifying the ports to wait for.
    Raises:
      OperationTimeout if the deadline is exceeded.
    """
   
  
  [... the rest is omitted ...]
files.old_path: AdminServer/appscale/admin/__init__.py
files.new_contents: """ Implements the App Engine services API.
  
  This API is not documented, but it is used by the Google Cloud SDK.
  """
  
  import json
  import logging
  import yaml
  from kazoo.exceptions import NoNodeError
  from yaml.parser import ParserError
  
  from appscale.appcontroller_client import AppControllerException
  from appscale.common.constants import HTTPCodes
  from .base_handler import BaseHandler
  from .constants import CustomHTTPError
  from .constants import InvalidConfiguration
  from .utils import cron_from_dict
  from .utils import queues_from_dict
  
  logger = logging.getLogger('appscale-admin')
  
  
  class IndexProperty(object):
    """ Represents a datastore index property. """
  
    __slots__ = ['name', 'direction']
  
    def __init__(self, name, direction):
      """ Creates a new IndexProperty object.
  
      Args:
        name: A string specifying the property name.
        direction: A string specifying the index direction (asc or desc).
      """
      if not name:
        raise InvalidConfiguration('Index property missing "name"')
  
      if direction not in ('asc', 'desc'):
        raise InvalidConfiguration(
          'Invalid "direction" value: {}'.format(direction))
  
      self.name = name
      self.direction = direction
  
    @property
    def id(self):
      if self.direction == 'asc':
        return self.name
      else:
        return ','.join([self.name, 'desc'])
  
    def to_dict(self):
      """ Generates a JSON-safe dictionary representation of the property.
  
      Returns:
        A dictionary containing the property details.
      """
      return {'name': self.name, 'direction': self.direction}
  
    @classmethod
    def from_dict(cls, prop):
      """ Constructs an IndexProperty from a JSON-derived dictionary.
  
      Args:
        prop: A dictionary containing the name and direction fields.
      Returns:
        An IndexProperty object.
      """
      return cls(prop['name'], prop['direction'])
  
  
  class DatastoreIndex(object):
    """ Represents a datastore index. """
  
    __slots__ = ['kind', 'ancestor', 'properties']
  
    # Separates fields of an encoded index.
    ENCODING_DELIMITER = '|'
  
    def __init__(self, kind, ancestor, properties):
      """ Creates a new DatastoreIndex object.
  
      Args:
        kind: A string specifying the datastore kind.
        ancestor: A boolean indicating whether or not the index is for
          satisfying ancestor queries.
        properties: A list of IndexProperty objects.
      """
      self.kind = kind
      self.ancestor = ancestor
      self.properties = properties
  
    @property
    def id(self):
      encoded_ancestor = '1' if self.ancestor else '0'
      encoded_properties = self.ENCODING_DELIMITER.join(
        [prop.id for prop in self.properties])
      return self.ENCODING_DELIMITER.join(
        [self.kind, encoded_ancestor, encoded_properties])
  
    @classmethod
    def from_yaml(cls, entry):
      """ Constructs a DatastoreIndex from a parsed index.yaml entry.
  
      Args:
        entry: A dictionary generated from a index.yaml file.
      Returns:
        A DatastoreIndex object.
      Raises:
        InvalidConfiguration exception if entry is invalid.
      """
      kind = entry.get('kind')
      if not kind:
        raise InvalidConfiguration('Index entry is missing "kind" field')
  
      ancestor = entry.get('ancestor', False)
      if not isinstance(ancestor, bool):
        if ancestor.lower() not in ('yes', 'no', 'true', 'false'):
          raise InvalidConfiguration(
            'Invalid "ancestor" value: {}'.format(ancestor))
  
        ancestor = ancestor.lower() in ('yes', 'true')
  
      configured_props = entry.get('properties', [])
      if not configured_props:
        raise InvalidConfiguration('Index missing properties')
  
      properties = [IndexProperty(prop.get('name'), prop.get('direction', 'asc'))
                    for prop in configured_props]
      return cls(kind, ancestor, properties)
  
    def to_dict(self):
      """ Generates a JSON-safe dictionary representation of the index.
  
      Returns:
        A dictionary containing the index details.
      """
      return {
        'kind': self.kind,
        'ancestor': self.ancestor,
        'properties': [prop.to_dict() for prop in self.properties]
      }
  
    @classmethod
    def from_dict(cls, entry):
      """ Constructs a DatastoreIndex from a JSON-derived dictionary.
  
      Args:
        entry: A dictionary containing the kind, ancestor, and properties fields.
      Returns
  
  [... the rest is omitted ...]
files.new_path: AdminServer/appscale/admin/appengine_api.py
files.old_contents: """ Implements the App Engine services API.
  
  This API is not documented, but it is used by the Google Cloud SDK.
  """
  
  import json
  import logging
  import yaml
  from kazoo.exceptions import NoNodeError
  from yaml.parser import ParserError
  
  from appscale.appcontroller_client import AppControllerException
  from appscale.common.constants import HTTPCodes
  from .base_handler import BaseHandler
  from .constants import CustomHTTPError
  from .constants import InvalidConfiguration
  from .utils import cron_from_dict
  from .utils import queues_from_dict
  
  logger = logging.getLogger('appscale-admin')
  
  
  class UpdateQueuesHandler(BaseHandler):
    """ Handles UpdateQueues operations. """
    def initialize(self, zk_client, ua_client):
      """ Defines required resources to handle requests.
  
      Args:
        zk_client: A KazooClient.
        ua_client: A UAClient.
      """
      self.zk_client = zk_client
      self.ua_client = ua_client
  
    def post(self):
      """ Handles UpdateQueues operations. """
      project_id = self.get_argument('app_id', None)
      if project_id is None:
        raise CustomHTTPError(HTTPCodes.BAD_REQUEST,
                              message='app_id parameter is required')
      self.authenticate(project_id, self.ua_client)
  
      try:
        payload = yaml.safe_load(self.request.body)
      except ParserError:
        raise InvalidConfiguration('Payload must be valid YAML')
  
      try:
        queues = queues_from_dict(payload)
      except InvalidConfiguration as error:
        raise CustomHTTPError(HTTPCodes.BAD_REQUEST, message=error.message)
  
      queue_node = '/appscale/projects/{}/queues'.format(project_id)
      try:
        self.zk_client.set(queue_node, json.dumps(queues))
      except NoNodeError:
        try:
          self.zk_client.create(queue_node, json.dumps(queues))
        except NoNodeError:
          raise CustomHTTPError(HTTPCodes.NOT_FOUND,
                                message='{} not found'.format(project_id))
  
      logger.info('Updated queues for {}'.format(project_id))
  
  
  class UpdateCronHandler(BaseHandler):
    """ Handles UpdateCron operations. """
    def initialize(self, acc, zk_client, ua_client):
      """ Defines required resources to handle requests.
  
      Args:
        acc: An AppControllerClient.
        zk_client: A KazooClient.
        ua_client: A UAClient.
      """
      self.acc = acc
      self.zk_client = zk_client
      self.ua_client = ua_client
  
    def post(self):
      """ Handles UpdateCron operations. """
      project_id = self.get_argument('app_id', None)
      if project_id is None:
        raise CustomHTTPError(HTTPCodes.BAD_REQUEST,
                              message='app_id parameter is required')
  
      self.authenticate(project_id, self.ua_client)
  
      try:
        payload = yaml.safe_load(self.request.body)
      except ParserError:
        raise InvalidConfiguration('Payload must be valid YAML')
  
      try:
        cron_jobs = cron_from_dict(payload)
      except InvalidConfiguration as error:
        raise CustomHTTPError(HTTPCodes.BAD_REQUEST, message=error.message)
  
      cron_node = '/appscale/projects/{}/cron'.format(project_id)
      try:
        self.zk_client.set(cron_node, json.dumps(cron_jobs))
      except NoNodeError:
        try:
          self.zk_client.create(cron_node, json.dumps(cron_jobs))
        except NoNodeError:
          raise CustomHTTPError(HTTPCodes.NOT_FOUND,
                                message='{} not found'.format(project_id))
  
      try:
        self.acc.update_cron(project_id)
      except AppControllerException as error:
        message = 'Error while updating cron: {}'.format(error)
        raise CustomHTTPError(HTTPCodes.INTERNAL_ERROR, message=message)
  
      logger.info('Updated cron jobs for {}'.format(project_id))
  
  
  [... the rest is omitted ...]
files.old_path: AdminServer/appscale/admin/appengine_api.py