blob: 5af9d9212b92c9f355fa2075d6a3bfb04e2fb38b [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""A helper file with a helper class for opening ports."""
import logging
from google.appengine.client.services import vme_errors
# These ports are used by our code or critical system daemons.
RESERVED_HOST_PORTS = [22, # SSH
5000, # Docker registry
8080, # HTTP server
10000, # For unlocking?
10001, # Nanny stubby proxy endpoint
]
# We allow users to forward traffic to our HTTP server internally.
RESERVED_DOCKER_PORTS = [22, # SSH
5000, # Docker registry
10001, # Nanny stubby proxy endpoint
]
DEFAULT_CONTAINER_PORT = 8080
VM_PORT_FOR_CONTAINER = 8080
class InconsistentPortConfigurationError(vme_errors.PermanentAppError):
"""The port is already in use."""
pass
class IllegalPortConfigurationError(vme_errors.PermanentAppError):
"""Raised if the port configuration is illegal."""
pass
def CreatePortManager(forwarded_ports, container_port):
"""Construct a PortManager object with port forwarding configured.
Args:
forwarded_ports: A string containing desired mappings from VM host ports
to docker container ports.
container_port: An integer port number for the container port.
Returns:
The PortManager instance.
"""
port_manager_obj = PortManager(container_port)
ports_list = forwarded_ports if forwarded_ports else []
logging.debug('setting forwarded ports %s', ports_list)
port_manager_obj.Add(ports_list, 'forwarded')
return port_manager_obj
class PortManager(object):
"""A helper class for VmManager to deal with port mappings."""
def __init__(self, container_port=DEFAULT_CONTAINER_PORT):
self.used_host_ports = {}
self._port_mappings = {}
self.container_port = container_port
def Add(self, ports, kind):
"""Load port configurations and adds them to an internal dict.
Args:
ports: A list of strings or a CSV representing port forwarding.
kind: what kind of port configuration this is, only used for error
reporting.
Raises:
InconsistentPortConfigurationError: If a port is configured to do
two different conflicting things.
IllegalPortConfigurationError: If the port is out of range or
is not a number.
Returns:
A dictionary with forwarding rules as external_port => local_port.
"""
if isinstance(ports, basestring):
# split a csv
ports = [port.strip() for port in ports.split(',')]
port_translations = {}
for port in ports:
try:
if ':' in port:
host_port, docker_port = (int(p.strip()) for p in port.split(':'))
port_translations[host_port] = docker_port
else:
host_port = int(port)
docker_port = host_port
port_translations[host_port] = host_port
if (host_port in self.used_host_ports and
self.used_host_ports[host_port] != docker_port):
raise InconsistentPortConfigurationError(
'Configuration conflict, port %d configured to forward '
'differently.' % host_port)
self.used_host_ports[host_port] = docker_port
if (host_port < 1 or host_port > 65535 or
docker_port < 1 or docker_port > 65535):
raise IllegalPortConfigurationError(
'Failed to load %s port configuration: invalid port %s'
% (kind, port))
if docker_port < 1024:
raise IllegalPortConfigurationError(
'Cannot listen on port %d as it is priviliged, use a forwarding '
'port.' % docker_port)
if docker_port in RESERVED_DOCKER_PORTS:
raise IllegalPortConfigurationError(
'Cannot use port %d as it is reserved on the VM.'
% docker_port)
if host_port in RESERVED_HOST_PORTS:
raise IllegalPortConfigurationError(
'Cannot use port %d as it is reserved on the VM.'
% host_port)
except ValueError as e:
logging.exception('Bad port description')
raise IllegalPortConfigurationError(
'Failed to load %s port configuration: "%s" error: "%s"'
% (kind, port, e))
# At this point we know they are not destructive.
self._port_mappings.update(port_translations)
return port_translations
def GetAllMappedPorts(self):
"""Returns all mapped ports.
Returns:
A dict of port mappings {host: docker}
"""
if not self._port_mappings:
return {}
else:
return self._port_mappings
# TODO: look into moving this into a DockerManager.
def _BuildDockerPublishArgumentString(self):
"""Generates a string of ports to expose to the Docker container.
Returns:
A string with --publish=host:docker pairs.
"""
port_map = self.GetAllMappedPorts()
# Map container port to port 8080 on the VM (default to 8080 if not set)
port_map[VM_PORT_FOR_CONTAINER] = int(self.container_port)
result = ''
for k, v in port_map.iteritems():
result += '--publish=%d:%s ' % (k, v)
return result
def GetReplicaPoolParameters(self):
"""Returns the contribution to the replica template."""
publish_ports = self._BuildDockerPublishArgumentString()
maps = {
'template': {
'vmParams': {
'metadata': {
'items': [
{'key': 'gae_publish_ports', 'value': publish_ports}
]
}
}
}
}
return maps