diff --git a/ipynbsrv/core/models.py b/ipynbsrv/core/models.py index 34771b1..ff3883c 100644 --- a/ipynbsrv/core/models.py +++ b/ipynbsrv/core/models.py @@ -607,11 +607,18 @@ def create_snapshot(self, name, description=None): snapshot.save() return snapshot - def get_clones(self): + def get_backend_base_url(self): """ - Get all containers that are clones of the one this method is called on. + Get the base url under which the protected service should listen. """ - return Container.objects.filter(clone_of=self) + base_url = None + if self.has_protected_port(): + for port_mapping in self.port_mappings.all(): + if port_mapping.internal_port == self.image.protected_port: + ip_and_port = port_mapping.server.internal_ip + ':' + str(port_mapping.external_port) + base_url = settings.CONTAINER_ACCESS_BASE_URI + ip_and_port.encode('hex') + '/' + break + return base_url def get_backend_name(self): """ @@ -619,30 +626,17 @@ def get_backend_name(self): """ return 'u%i-%s' % (self.owner.backend_id, self.name) - def get_friendly_name(self): + def get_clones(self): """ - Return the human-friendly name of this container. + Get all containers that are clones of the one this method is called on. """ - return self.owner.django_user.get_username() + '_' + self.name + return Container.objects.filter(clone_of=self) - def get_port_mappings(self, tuples=False): + def get_friendly_name(self): """ - Return the port mappings for this container. - - :param tuples: If `True`, tuples in the form (internal, exposed) are returned. + Return the human-friendly name of this container. """ - mappings = [] - reported_mappings = self.server.get_container_backend().get_container(self.backend_pk) \ - .get(ContainerBackend.CONTAINER_KEY_PORT_MAPPINGS) - if tuples: - for reported_mapping in reported_mappings: - mappings.append(( - reported_mapping.get(ContainerBackend.PORT_MAPPING_KEY_INTERNAL), - reported_mapping.get(ContainerBackend.PORT_MAPPING_KEY_EXTERNAL) - )) - else: - mappings = reported_mappings - return mappings + return self.owner.django_user.get_username() + '_' + self.name def has_clones(self): """ @@ -651,9 +645,16 @@ def has_clones(self): return Container.objects.filter(clone_of=self).exists() has_clones.boolean = True + def has_protected_port(self): + """ + Return `True` if the container is exposing a protected port. + """ + return self.image.protected_port is not None + has_protected_port.boolean = True + def is_clone(self): """ - Return true if this container is a clone of another one. + Return `True` if this container is a clone of another one. """ return self.clone_of is not None is_clone.boolean = True @@ -1114,6 +1115,73 @@ def __unicode__(self): return self.__str__() +class PortMapping(models.Model): + + """ + TODO: write doc. + """ + + server = models.ForeignKey( + 'Server', + related_name='port_mappings', + help_text='The server on which this port mapping is active.' + ) + container = models.ForeignKey( + 'Container', + related_name='port_mappings', + help_text='The container for which this port mapping is.' + ) + external_port = models.PositiveIntegerField( + help_text='The public port for this port mapping (e.g. the one under which it can be accessed).' + ) + internal_port = models.PositiveIntegerField( + help_text='The container internal port.' + ) + + def clean(self): + """ + :inherit. + """ + if not self.server and self.container: + self.server = self.container.server + if not self.external_port: + self.external_port = PortMapping.get_available_server_port(self.server) + + @classmethod + def get_available_server_port(cls, server): + """ + Get a free port safe to use as `external_port` on `server`. + + :param server: The server to get a free port of. + """ + port = settings.CONTAINER_PORT_MAPPINGS_START_PORT + mappings = cls.objects.filter(server=server) + if mappings.exists(): + port = mappings.latest('external_port').external_port + 1 + # TODO: if reached settings.CONTAINER_PORT_MAPPINGS_END_PORT + # start over and look while i+1 for free port. + # ServerSelectionAlgorithm must guarantee enough ports are free! + return port + + def __str__(self): + """ + :inherit. + """ + return smart_unicode("%s:%i->%s:%i" % (self.server, self.external_port, self.container, self.internal_port)) + + def __unicode__(self): + """ + :inherit. + """ + return self.__str__() + + class Meta: + unique_together = ( + ('server', 'external_port'), + ('container', 'external_port', 'internal_port') + ) + + class Server(models.Model): """ diff --git a/ipynbsrv/core/settings.py b/ipynbsrv/core/settings.py index b0d4231..8de118a 100644 --- a/ipynbsrv/core/settings.py +++ b/ipynbsrv/core/settings.py @@ -1,3 +1,9 @@ +""" +Settings related to containers. +""" +CONTAINER_ACCESS_BASE_URI = '/ct/' +CONTAINER_PORT_MAPPINGS_START_PORT = 49152 +CONTAINER_PORT_MAPPINGS_END_PORT = 65534 """ Settings storing the paths (relative to STORAGE_DIR_BASE) under which (user) directories should be created. diff --git a/ipynbsrv/core/signals/containers.py b/ipynbsrv/core/signals/containers.py index f85d255..c12cdd7 100644 --- a/ipynbsrv/core/signals/containers.py +++ b/ipynbsrv/core/signals/containers.py @@ -4,7 +4,7 @@ from ipynbsrv.contract.errors import ContainerBackendError, ContainerNotFoundError from ipynbsrv.core import settings from ipynbsrv.core.helpers import get_storage_backend -from ipynbsrv.core.models import Container, ContainerImage +from ipynbsrv.core.models import Container, ContainerImage, PortMapping from ipynbsrv.core.signals.signals import * from os import path import time @@ -13,25 +13,69 @@ storage_backend = get_storage_backend() +def create_container_port_mappings(container): + """ + Create the port mappings for the given container. + + :param container: The container to create the mappings for. + """ + ports = [] + image = None + if container.is_image_based(): + image = container.image + elif container.is_clone() and container.clone_of.is_image_based(): + image = container.clone_of.image + + if image: + protected_port = image.protected_port + public_ports = image.public_ports + if protected_port: + mapping = PortMapping( + server=container.server, + container=container, + external_port=PortMapping.get_available_server_port(container.server), + internal_port=protected_port + ) + mapping.save() + ports.append({ + ContainerBackend.PORT_MAPPING_KEY_ADDRESS: mapping.server.internal_ip, + ContainerBackend.PORT_MAPPING_KEY_EXTERNAL: mapping.external_port, + ContainerBackend.PORT_MAPPING_KEY_INTERNAL: mapping.internal_port + }) + if public_ports: + for port in public_ports.split(','): + mapping = PortMapping( + server=container.server, + container=container, + external_port=PortMapping.get_available_server_port(container.server), + internal_port=port + ) + mapping.save() + ports.append({ + ContainerBackend.PORT_MAPPING_KEY_ADDRESS: '0.0.0.0', + ContainerBackend.PORT_MAPPING_KEY_EXTERNAL: mapping.external_port, + ContainerBackend.PORT_MAPPING_KEY_INTERNAL: mapping.internal_port + }) + return ports + + @receiver(container_created) def create_on_server(sender, container, **kwargs): """ Create the newly saved container on the server's container backend. """ if container is not None: - ports = [] + ports = create_container_port_mappings(container) + clone_of = None cmd = None image = None if container.is_image_based(): - ports = get_container_port_mappings(container) cmd = container.image.command image = container.image.backend_pk - clone_of = None - if container.is_clone(): + elif container.is_clone() and container.clone_of.is_image_based(): clone_of = container.clone_of.backend_pk - if container.clone_of.is_image_based(): - ports = get_container_port_mappings(container.clone_of) - cmd = container.clone_of.image.command + cmd = container.clone_of.image.command + image = container.clone_of.image.backend_pk result = None try: @@ -55,6 +99,7 @@ def create_on_server(sender, container, **kwargs): } ], cmd=cmd, + base_url=container.get_backend_base_url(), image=image, clone_of=clone_of ) @@ -108,25 +153,6 @@ def delete_on_server(sender, container, **kwargs): raise ex -def get_container_port_mappings(container): - """ - Return the list of port mappings for the container that can be passed to container backends. - """ - ports = [] - if container.image.protected_port is not None: - ports.append({ - ContainerBackend.PORT_MAPPING_KEY_ADDRESS: container.server.internal_ip, - ContainerBackend.PORT_MAPPING_KEY_INTERNAL: container.image.protected_port - }) - if container.image.public_ports is not None: - for port in container.image.public_ports.split(','): - ports.append({ - ContainerBackend.PORT_MAPPING_KEY_ADDRESS: '0.0.0.0', - ContainerBackend.PORT_MAPPING_KEY_INTERNAL: int(port) - }) - return ports - - @receiver(container_restarted) def restart_on_server(sender, container, **kwargs): """ diff --git a/lib/confs/nginx/ipynbsrv.conf b/lib/confs/nginx/ipynbsrv.conf index 58f6bc3..946d899 100644 --- a/lib/confs/nginx/ipynbsrv.conf +++ b/lib/confs/nginx/ipynbsrv.conf @@ -54,17 +54,15 @@ server { } # proxy/workspace location - location ~* /workspace/(\d+)/(.*) { + location ~* /ct/([^\/]+)/(.*) { # authorization # ensure only container's owner can access it - satisfy all; - auth_request /auth; + #satisfy all; + #auth_request /auth; - # use the Django error pages - # forbidden = 404 so the user doesn't know there is a container - # 50x grouped to 500 - error_page 403 404 /error/404; - error_page 500 502 503 504 /error/500; + # get the IP and port from encoded part + set $decoded_backend ''; + set_decode_hex $decoded_backend $1; # needed for websockets connections proxy_http_version 1.1; @@ -78,6 +76,6 @@ server { proxy_set_header X-Forwarded-Protocol $scheme; proxy_set_header X-Real-IP $remote_addr; - proxy_pass http://172.17.42.1:$1; + proxy_pass http://$decoded_backend; } }