Permalink
Browse files

Merge pull request #205 from StrausMG/byor

Bring Your Own Resources
  • Loading branch information...
2 parents 4b874f2 + d579b5f commit 7e9dead90b8ad0fbdbc6a8130a8b112a164542ec @anaderi anaderi committed on GitHub Jun 30, 2017
Showing with 192 additions and 7 deletions.
  1. +1 −0 .gitignore
  2. +5 −0 etc/byor_config.py
  3. +2 −1 everware/__init__.py
  4. +88 −0 everware/byor_spawner.py
  5. +11 −6 everware/spawner.py
  6. +85 −0 share/static/html/_byor_options_form.html
View
@@ -28,3 +28,4 @@ src/*
# py.test's .cache/ directory
.cache/*
.idea/*
+.vscode/*
View
@@ -0,0 +1,5 @@
+c = get_config()
+load_subconfig('etc/base_config.py')
+load_subconfig('etc/github_auth.py')
+
+c.JupyterHub.spawner_class = 'everware.ByorDockerSpawner'
View
@@ -1,5 +1,6 @@
-__version__ = "0.11.0"
+__version__ = "0.12.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,88 @@
+from os.path import join as pjoin
+
+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):
+ CustomDockerSpawner.__init__(self, **kwargs)
+ self._byor_client = None
+ if self.options_form == self._options_form_default():
+ with open(pjoin(self.config['JupyterHub']['template_paths'][0],
+ '_byor_options_form.html')) as form:
+ ByorDockerSpawner.options_form = form.read()
+
+ @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
@@ -60,7 +60,6 @@ def get_global_client():
def _docker(self, method, *args, **kwargs):
"""wrapper for calling docker methods
-
to be passed to ThreadPoolExecutor
"""
# methods that return a generator object return instantly
@@ -322,7 +321,6 @@ def generate_image_name(self):
self.commit_sha
)
-
@gen.coroutine
def remove_old_container(self):
try:
@@ -335,14 +333,16 @@ 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:
f = self.build_image()
image_name = yield gen.with_timeout(
@@ -395,9 +395,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,0 +1,85 @@
+<div style="margin-bottom: 0px;">
+<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label" style="width: 50%;">
+<input
+ id="repository_input"
+ type="text"
+ autocapitalize="off"
+ autocorrect="off"
+ name="repository_url"
+ tabindex="1"
+ autofocus="autofocus"
+ class="mdl-textfield__input"
+style="margin-bottom: 3px;" />
+<label class="mdl-textfield__label" for="repository_input">Git repository</label>
+</div>
+
+<label for="need_remove" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" >
+<input type="checkbox"
+ name="need_remove"
+ class="mdl-checkbox__input"
+ id="need_remove"
+ checked />
+<span class="mdl-checkbox__label">Remove previous container if it exists</span>
+</label>
+
+<label for="byor_is_needed" class="mdl-checkbox mdl-js-checkbox mdl-js-ripple-effect" >
+<input type="checkbox"
+ name="byor_is_needed"
+ class="mdl-checkbox__input"
+ id="byor_is_needed"
+ unchecked />
+<span class="mdl-checkbox__label">I want to run the repository on my own server</span>
+</label>
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
+<script>
+window.onload = function () {
+ byor_is_needed = document.getElementById("byor_is_needed");
+ byor_input = document.getElementById("byor_input");
+ if (byor_is_needed.checked) {
+ byor_input.style.display = "inline";
+ } else {
+ byor_input.style.display = "none";
+ }
+}
+$('#byor_is_needed').on('change', function() {
+ $('#byor_input').toggle(speed='normal');
+});
+</script>
+
+<div id='byor_input' style="display: none;">
+<p style="margin-bottom: 0px;">
+ For a successful run <a href="https://www.docker.com/" target="_black">Docker</a>
+ must be installed on your server.<br />
+ Enter IP address and port of the Docker daemon running on your server.<br />
+ (Click <a href="https://docs.docker.com/engine/reference/commandline/dockerd/#daemon-socket-option"
+ target="_blank">
+ here</a> to learn how to run Docker daemon on a particular port)
+</p>
+<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"
+ style="width: 9%; margin-right: 20px;">
+ <input
+ id="byor_docker_ip"
+ type="text"
+ autocapitalize="off"
+ autocorrect="off"
+ name="byor_docker_ip"
+ tabindex="1"
+ autofocus="autofocus"
+ class="mdl-textfield__input"/>
+ <label class="mdl-textfield__label" for="byor_docker_ip">ip</label>
+</div>
+<div class="mdl-textfield mdl-js-textfield mdl-textfield--floating-label"
+ style="width: 9%;">
+ <input
+ id="byor_docker_port"
+ type="text"
+ autocapitalize="off"
+ autocorrect="off"
+ name="byor_docker_port"
+ tabindex="1"
+ autofocus="autofocus"
+ class="mdl-textfield__input"/>
+ <label class="mdl-textfield__label" for="byor_docker_port">port</label>
+</div>
+</div>
+</div>

0 comments on commit 7e9dead

Please sign in to comment.