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.
+153 −29
Split
View
@@ -28,3 +28,4 @@ src/*
# py.test's .cache/ directory
.cache/*
.idea/*
+.vscode/*
View
@@ -0,0 +1,6 @@
+c = get_config()
+load_subconfig('etc/base_config.py')
+load_subconfig('etc/github_auth.py')
+
+from dockerspawner import DockerSpawner
+c.DockerSpawner.hub_ip_connect = c.JupyterHub.hub_ip
@anaderi

anaderi Jun 21, 2017

Owner

let's remove the last 2 lines. check that it doesn't break anything. if so, you can just load_subconfig('etc/local_config.py')

@anaderi

anaderi Jun 21, 2017

Owner

just specify ByorDockerSpawner as spawner

@@ -0,0 +1,87 @@
+SPAWNER_OPTIONS_FORM = """
+<div style="margin-bottom: 0px;">
@anaderi

anaderi Jun 21, 2017

Owner

make separate html file in share/static/html and read it instead.

+<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 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"
@anaderi

anaderi Jun 21, 2017

Owner

should read:
Enter IP address and port of the Docker daemon running on your server.

+ 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>
+"""
View
@@ -6,7 +6,8 @@
from concurrent.futures import ThreadPoolExecutor
-from docker.errors import APIError
+import docker
+from docker.errors import APIError, DockerException
from smtplib import SMTPException
from jupyterhub.utils import wait_for_http_server
@@ -29,6 +30,7 @@
from .email_notificator import EmailNotificator
from .container_handler import ContainerHandler
from . import __version__
+from ._spawner_options_form import SPAWNER_OPTIONS_FORM
ssl._create_default_https_context = ssl._create_unverified_context
@@ -42,9 +44,41 @@ 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
@anaderi

anaderi Jun 21, 2017

Owner

move to ByorDockerSpawner class

# We override the executor here to increase the number of threads
@property
@@ -55,7 +89,6 @@ def executor(self):
cls._executor = ThreadPoolExecutor(20)
return cls._executor
-
def _docker(self, method, *args, **kwargs):
"""wrapper for calling docker methods
@@ -89,6 +122,7 @@ 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)
@@ -112,36 +146,16 @@ def load_state(self, state):
self.user.spawn_pending = False
def _options_form_default(self):
- return """
- <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>
- """
+ return SPAWNER_OPTIONS_FORM
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)
- need_remove = formdata.get('need_remove', ['on'])[0].strip()
- options['need_remove'] = need_remove == 'on'
+ options['need_remove'] = formdata.get('need_remove', ['on'])[0].strip() == 'on'
if not options['repo_url']:
raise Exception('You have to provide the URL to a git repository.')
return options
@@ -320,7 +334,6 @@ def generate_image_name(self):
self.commit_sha
)
-
@gen.coroutine
def remove_old_container(self):
try:
@@ -331,7 +344,7 @@ def remove_old_container(self):
force=True
)
except APIError as e:
- self.log.info("Can't erase container %s due to %s" % (self.container_name, 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):
@@ -341,7 +354,9 @@ def start(self, image=None):
self._is_failed = False
self._is_building = True
self._is_empty = False
+
try:
+ yield self._set_client()
f = self.build_image()
image_name = yield gen.with_timeout(
timedelta(seconds=self.start_timeout),
@@ -359,6 +374,21 @@ 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: