| #!/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 |