Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

Bring Your Own Resources #205

Merged
merged 23 commits into from Jun 30, 2017
Jump to file or symbol
Failed to load files and symbols.
+189 −6
Split
View
@@ -28,3 +28,4 @@ src/*
# py.test's .cache/ directory
.cache/*
.idea/*
+.vscode/*
View
@@ -0,0 +1,9 @@
+c = get_config()
+load_subconfig('etc/base_config.py')
+load_subconfig('etc/github_auth.py')
+
+c.JupyterHub.spawner_class = 'everware.ByorDockerSpawner'
+
+from os.path import join as pjoin
+with open(pjoin(c.JupyterHub.template_paths[0], '_byor_options_form.html')) as form:
+ c.ByorDockerSpawner.options_form = form.read()
@anaderi

anaderi Jun 27, 2017

Owner

move to ByorDockerSpawner.init

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
@@ -58,7 +58,6 @@ def executor(self):
def _docker(self, method, *args, **kwargs):
"""wrapper for calling docker methods
-
to be passed to ThreadPoolExecutor
"""
# methods that return a generator object return instantly
@@ -320,7 +319,6 @@ def generate_image_name(self):
self.commit_sha
)
-
@gen.coroutine
def remove_old_container(self):
try:
@@ -333,14 +331,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))
@anaderi

anaderi Jun 21, 2017

Owner

remove trailing space

- @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(
@@ -393,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,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>