diff --git a/everware/__init__.py b/everware/__init__.py index 6243a2c..f6145b1 100755 --- a/everware/__init__.py +++ b/everware/__init__.py @@ -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 * diff --git a/everware/byor_spawner.py b/everware/byor_spawner.py new file mode 100644 index 0000000..ce635f2 --- /dev/null +++ b/everware/byor_spawner.py @@ -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 diff --git a/everware/spawner.py b/everware/spawner.py index 714a136..158fafd 100755 --- a/everware/spawner.py +++ b/everware/spawner.py @@ -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