Permalink
Browse files

move BYOR functionality to a separate class

  • Loading branch information...
1 parent 92835db commit da9d883b01a2a1095aa308a2e92f7710df768f49 @StrausMG StrausMG committed Jun 25, 2017
Showing with 97 additions and 60 deletions.
  1. +1 −0 everware/__init__.py
  2. +82 −0 everware/byor_spawner.py
  3. +14 −60 everware/spawner.py
View
@@ -1,5 +1,6 @@
__version__ = "0.11.0"
from .spawner import *
+from .byor_spawner import *
from .authenticator import *
from .user_spawn_handler import *
from .user_wait_handler import *
View
@@ -0,0 +1,82 @@
+import docker
+from docker.errors import DockerException
+from traitlets import Int
+from tornado import gen
+
+from .spawner import CustomDockerSpawner
+
+
+class ByorDockerSpawner(CustomDockerSpawner):
+ def __init__(self, **kwargs):
+ self._byor_client = None
+ CustomDockerSpawner.__init__(self, **kwargs)
+
+ @property
+ def client(self):
+ if self._byor_client is not None:
+ return self._byor_client
+ return super(ByorDockerSpawner, self).client
+
+ @property
+ def byor_is_used(self):
+ return self.user_options.get('byor_is_needed', False)
+
+ def _reset_byor(self):
+ self.container_ip = str(self.__class__.container_ip)
+ self._byor_client = None
+
+ byor_timeout = Int(20, min=1, config=True,
+ help='Timeout for connection to BYOR Docker daemon')
+
+ def options_from_form(self, formdata):
+ options = {}
+ options['byor_is_needed'] = formdata.pop('byor_is_needed', [''])[0].strip() == 'on'
+ for field in ('byor_docker_ip', 'byor_docker_port'):
+ options[field] = formdata.pop(field, [''])[0].strip()
+ options.update(
+ super(ByorDockerSpawner, self).options_from_form(formdata)
+ )
+ return options
+
+ @gen.coroutine
+ def _configure_byor(self):
+ """Configure BYOR settings or reset them if BYOR is not needed."""
+ if not self.byor_is_used:
+ self._reset_byor()
+ return
+ byor_ip = self.user_options['byor_docker_ip']
+ byor_port = self.user_options['byor_docker_port']
+ try:
+ # version='auto' causes a connection to the daemon.
+ # That's why the method must be a coroutine.
+ self._byor_client = docker.Client('{}:{}'.format(byor_ip, byor_port),
+ version='auto',
+ timeout=self.byor_timeout)
+ except DockerException as e:
+ self._is_failed = True
+ message = str(e)
+ if 'ConnectTimeoutError' in message:
+ log_message = 'Connection to the Docker daemon took too long (> {} secs)'.format(
+ self.byor_timeout
+ )
+ notification_message = 'BYOR timeout limit {} exceeded'.format(self.byor_timeout)
+ else:
+ log_message = "Failed to establish connection with the Docker daemon"
+ notification_message = log_message
+ self._add_to_log(log_message, level=2)
+ yield self.notify_about_fail(notification_message)
+ self._is_building = False
+ raise
+
+ self.container_ip = byor_ip
+
+ @gen.coroutine
+ def _prepare_for_start(self):
+ super(ByorDockerSpawner, self)._prepare_for_start()
+ yield self._configure_byor()
+
+ @gen.coroutine
+ def start(self, image=None):
+ yield self._prepare_for_start()
+ ip_port = yield self._start(image)
+ return ip_port
View
@@ -6,8 +6,7 @@
from concurrent.futures import ThreadPoolExecutor
-import docker
-from docker.errors import APIError, DockerException
+from docker.errors import APIError
from smtplib import SMTPException
from jupyterhub.utils import wait_for_http_server
@@ -43,41 +42,9 @@ def __init__(self, **kwargs):
self._image_handler = ImageHandler()
self._cur_waiter = None
self._is_empty = False
- self._byor_client = None
ContainerHandler.__init__(self, **kwargs)
EmailNotificator.__init__(self)
- @property
- def client(self):
- if self._byor_client is not None:
- return self._byor_client
- return super(CustomDockerSpawner, self).client
-
- @property
- def byor_is_used(self):
- return self.user_options.get('byor_is_needed', False)
-
- def _reset_byor(self):
- self.container_ip = str(self.__class__.container_ip)
- self._byor_client = None
-
- byor_timeout = Int(20, min=1, config=True,
- help='Timeout for connection to BYOR Docker daemon')
-
- @gen.coroutine
- def _set_client(self):
- """Prepare a client for the user."""
- if not self.byor_is_used:
- self._reset_byor()
- return
- byor_ip = self.user_options['byor_docker_ip']
- byor_port = self.user_options['byor_docker_port']
- # version='auto' causes a connection to the daemon.
- # That's why the method must be a coroutine.
- self._byor_client = docker.Client('{}:{}'.format(byor_ip, byor_port),
- version='auto',
- timeout=self.byor_timeout)
- self.container_ip = byor_ip
# We override the executor here to increase the number of threads
@property
@@ -88,9 +55,9 @@ def executor(self):
cls._executor = ThreadPoolExecutor(20)
return cls._executor
+
def _docker(self, method, *args, **kwargs):
"""wrapper for calling docker methods
-
to be passed to ThreadPoolExecutor
"""
# methods that return a generator object return instantly
@@ -121,7 +88,6 @@ def lister(mm):
def clear_state(self):
state = super(CustomDockerSpawner, self).clear_state()
self.container_id = ''
- self._reset_byor()
def get_state(self):
state = DockerSpawner.get_state(self)
@@ -172,11 +138,9 @@ def _options_form_default(self):
def options_from_form(self, formdata):
options = {}
options['repo_url'] = formdata.get('repository_url', [''])[0].strip()
- options['byor_is_needed'] = formdata.get('byor_is_needed', [''])[0].strip() == 'on'
- for field in ('byor_docker_ip', 'byor_docker_port'):
- options[field] = formdata.pop(field, [''])[0].strip()
options.update(formdata)
- options['need_remove'] = formdata.get('need_remove', ['on'])[0].strip() == 'on'
+ need_remove = formdata.get('need_remove', ['on'])[0].strip()
+ options['need_remove'] = need_remove == 'on'
if not options['repo_url']:
raise Exception('You have to provide the URL to a git repository.')
return options
@@ -367,17 +331,17 @@ def remove_old_container(self):
except APIError as e:
self.log.info("Can't erase container %s due to %s" % (self.container_name, e))
- @gen.coroutine
- def start(self, image=None):
- """start the single-user server in a docker container"""
+ def _prepare_for_start(self):
self._user_log = []
self._is_up = False
self._is_failed = False
self._is_building = True
self._is_empty = False
+ @gen.coroutine
+ def _start(self, image):
+ """start the single-user server in a docker container"""
try:
- yield self._set_client()
f = self.build_image()
image_name = yield gen.with_timeout(
timedelta(seconds=self.start_timeout),
@@ -395,21 +359,6 @@ def start(self, image=None):
yield ContainerHandler.start(self,
image=image_name
)
- except DockerException as e:
- self._is_failed = True
- message = str(e)
- if 'ConnectTimeoutError' in message:
- log_message = 'Connection to the Docker daemon took too long (> {} secs)'.format(
- self.byor_timeout
- )
- notification_message = 'BYOR timeout limit {} exceeded'.format(self.byor_timeout)
- else:
- log_message = "Failed to establish connection with the Docker daemon"
- notification_message = log_message
- self._add_to_log(log_message, level=2)
- yield self.notify_about_fail(notification_message)
- self._is_building = False
- raise
except gen.TimeoutError:
self._is_failed = True
if self._cur_waiter:
@@ -444,9 +393,14 @@ def start(self, image=None):
return self.user.server.ip, self.user.server.port # jupyterhub 0.7 prefers returning ip, port
@gen.coroutine
+ def start(self, image=None):
+ self._prepare_for_start()
+ ip_port = yield self._start(image)
+ return ip_port
+
+ @gen.coroutine
def stop(self, now=False):
"""Stop the container
-
Consider using pause/unpause when docker-py adds support
"""
self._is_empty = True

0 comments on commit da9d883

Please sign in to comment.