Compare commits

...

35 Commits

Author SHA1 Message Date
Min RK
d7d8459edb 1.0.0b2 2019-04-09 10:45:31 +02:00
Min RK
39a7116d16 npm run fmt
with latest prettier
2019-04-09 10:45:31 +02:00
Min RK
d27c970cc4 rev js dependencies 2019-04-09 10:45:31 +02:00
Min RK
cf56dbb97b latest changes in changelog 2019-04-09 10:36:33 +02:00
Min RK
a4ccfe4e11 Merge pull request #2511 from ryogesh/do-not-show-db-password
Redact DB password before logging connection string
2019-04-09 10:19:48 +02:00
Min RK
f1871bbe24 Merge pull request #2510 from minrk/fix-delete-named
ensure spawner for named servers is fully deleted
2019-04-09 10:17:07 +02:00
bdmon
1cc9153a91 Redact DB password before logging connection string 2019-04-09 09:26:54 +02:00
Tim Head
4258254c39 Merge pull request #2509 from minrk/sec-doc
Add security-reporting to docs
2019-04-05 17:33:44 +02:00
Min RK
f3aee9bd16 ensure spawner for named servers is fully deleted
if spawner wasn't running, the wrapper could have been left in the user.spawners dict
2019-04-05 16:50:55 +02:00
Min RK
5cb8ccf8b2 Merge pull request #2494 from minrk/retry-better
include retry link after failed spawn
2019-04-05 15:29:40 +02:00
Tim Head
1d63e417ca Merge pull request #2508 from minrk/discourse-link
add discourse link to communication doc
2019-04-05 12:06:02 +02:00
Min RK
ee0020e8fa add security-reporting to docs 2019-04-05 11:51:02 +02:00
Min RK
2d83575a24 add discourse link to communication docs 2019-04-05 11:46:44 +02:00
Min RK
33c168530e Merge pull request #2496 from minrk/all-users-admin
ensure default server exists in the db at user creation
2019-04-05 10:29:20 +02:00
Min RK
5d4d34b24d Merge pull request #2498 from minrk/oauthlib-3
allow oauthlib 3
2019-04-05 10:25:59 +02:00
Min RK
49cc794937 include exception in template vars
for custom templates
2019-04-05 10:25:40 +02:00
Min RK
7f9e77ce5b Allow Spawners to customize spawn-failed message
by raising an exception with a `jupyterhub_message` attribute.
This will be a string displayed as escaped HTML (HTML is not allowed).
2019-04-05 10:22:47 +02:00
Min RK
6fa3b429db include retry link after failed spawn 2019-04-01 17:05:58 +02:00
Min RK
e89836c035 Merge pull request #2495 from minrk/service-oauth-state-typo
typo raising error on missing oauth state
2019-04-01 17:05:44 +02:00
Min RK
784b5cb6f0 ensure default server exists in the db at user creation
avoids issues in e.g. all_spawners being empty
2019-04-01 17:05:23 +02:00
Min RK
daaa763c3b allow oauthlib 3
requires updating our is_absolute_uri check
2019-04-01 17:04:59 +02:00
Min RK
2b18c64081 Merge pull request #2497 from minrk/mysql-connector-python
[travis] trade mysql-connector for mysql-connector-python
2019-04-01 17:04:30 +02:00
Min RK
785addc245 mysql-connector-python in test_db 2019-04-01 16:47:46 +02:00
Min RK
b4758db017 specify native auth plugin for mysql testing
sha2 plugin isn't available on travis
2019-04-01 16:31:36 +02:00
Min RK
10fbfee157 travis: install mysql-connector-python
instead of mysql-connector, which is deprecated
2019-04-01 15:06:43 +02:00
Min RK
c58a251dbd typo raising error on missing oauth state
need to specify a status code
2019-04-01 14:53:35 +02:00
Min RK
27be5e4847 Changelog for 0.9.6
replace 0.9.5 which has only a partial fix

issue is now confirmed to affect all browsers
2019-04-01 12:30:16 +02:00
Min RK
be97a0c95b Further login redirect validation 2019-04-01 12:29:29 +02:00
Min RK
689a312756 Merge pull request #2490 from mathematicalmichael/patch-1
Fix 1.0 date in changelog
2019-03-29 17:08:49 +01:00
Michael Pilosov
1484869ee3 Update changelog.md
fix date
2019-03-29 08:16:55 -06:00
Min RK
a090632a48 Merge pull request #2488 from minrk/post_push
Docker hook fixes
2019-03-28 16:02:50 +01:00
Min RK
451a16c57e changelog for 0.9.5 2019-03-28 13:34:22 +01:00
Min RK
6e14e86a1a protect against some browsers' buggy handling of backslash as slash 2019-03-28 13:33:23 +01:00
Min RK
a142f543ba [docker] tag stable releases with :latest 2019-03-28 13:06:18 +01:00
Min RK
0bb3996c30 [docker] fix unbound variable in post_push hook for stable releases 2019-03-28 13:05:05 +01:00
26 changed files with 227 additions and 39 deletions

View File

@@ -27,7 +27,7 @@ before_install:
unset MYSQL_UNIX_PORT unset MYSQL_UNIX_PORT
DB=mysql bash ci/docker-db.sh DB=mysql bash ci/docker-db.sh
DB=mysql bash ci/init-db.sh DB=mysql bash ci/init-db.sh
pip install 'mysql-connector<2.2' pip install 'mysql-connector-python'
elif [[ $JUPYTERHUB_TEST_DB_URL == postgresql* ]]; then elif [[ $JUPYTERHUB_TEST_DB_URL == postgresql* ]]; then
DB=postgres bash ci/init-db.sh DB=postgres bash ci/init-db.sh
pip install psycopg2-binary pip install psycopg2-binary

View File

@@ -9,7 +9,7 @@ command line for details.
## 1.0 ## 1.0
### [1.0.0] 2018-03-XX ### [1.0.0] 2019-04-XX
JupyterHub 1.0 is a major milestone for JupyterHub. JupyterHub 1.0 is a major milestone for JupyterHub.
Huge thanks to the many people who have contributed to this release, Huge thanks to the many people who have contributed to this release,
@@ -94,6 +94,8 @@ whether it was through discussion, testing, documentation, or development.
- `Spawner.options_from_form` may now be async - `Spawner.options_from_form` may now be async
- Added `JupyterHub.shutdown_on_logout` option to trigger shutdown of a user's - Added `JupyterHub.shutdown_on_logout` option to trigger shutdown of a user's
servers when they log out. servers when they log out.
- When `Spawner.start` raises an Exception,
a message can be passed on to the user if the exception has a `.jupyterhub_message` attribute.
#### Changes #### Changes
@@ -131,6 +133,7 @@ whether it was through discussion, testing, documentation, or development.
- Fewer redirects following a visit to the default `/` url - Fewer redirects following a visit to the default `/` url
- Error when progress is requested before progress is ready - Error when progress is requested before progress is ready
- Error when API requests are made to a not-running server without authentication - Error when API requests are made to a not-running server without authentication
- Avoid logging database password on connect if password is specified in `JupyterHub.db_url`.
#### Development changes #### Development changes
@@ -148,6 +151,14 @@ In general, see `CONTRIBUTING.md` for contribution info or ask if you have quest
## 0.9 ## 0.9
### [0.9.6] 2019-04-01
JupyterHub 0.9.6 is a security release.
- Fixes an Open Redirect vulnerability (CVE-2019-10255).
JupyterHub 0.9.5 included a partial fix for this issue.
### [0.9.4] 2018-09-24 ### [0.9.4] 2018-09-24
JupyterHub 0.9.4 is a small bugfix release. JupyterHub 0.9.4 is a small bugfix release.
@@ -566,7 +577,8 @@ First preview release
[Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.0.0...HEAD [Unreleased]: https://github.com/jupyterhub/jupyterhub/compare/1.0.0...HEAD
[1.0.0]: https://github.com/jupyterhub/jupyterhub/compare/0.9.4...HEAD [1.0.0]: https://github.com/jupyterhub/jupyterhub/compare/0.9.5...HEAD
[0.9.6]: https://github.com/jupyterhub/jupyterhub/compare/0.9.4...0.9.6
[0.9.4]: https://github.com/jupyterhub/jupyterhub/compare/0.9.3...0.9.4 [0.9.4]: https://github.com/jupyterhub/jupyterhub/compare/0.9.3...0.9.4
[0.9.3]: https://github.com/jupyterhub/jupyterhub/compare/0.9.2...0.9.3 [0.9.3]: https://github.com/jupyterhub/jupyterhub/compare/0.9.2...0.9.3
[0.9.2]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...0.9.2 [0.9.2]: https://github.com/jupyterhub/jupyterhub/compare/0.9.1...0.9.2

View File

@@ -4,8 +4,13 @@
Community communication channels Community communication channels
================================ ================================
We use `Gitter <https://gitter.im>`_ for online, real-time text chat. The We use `Discourse <https://discourse.jupyter.org>` for online discussion.
primary channel for JupyterHub is `jupyterhub/jupyterhub <https://gitter.im/jupyterhub/jupyterhub>`_. Everyone in the Jupyter community is welcome to bring ideas and questions there.
In addition, we use `Gitter <https://gitter.im>`_ for online, real-time text chat,
a place for more ephemeral discussions.
The primary Gitter channel for JupyterHub is `jupyterhub/jupyterhub <https://gitter.im/jupyterhub/jupyterhub>`_.
Gitter isn't archived or searchable, so we recommend going to discourse first
to make sure that discussions are most useful and accessible to the community.
Remember that our community is distributed across the world in various Remember that our community is distributed across the world in various
timezones, so be patient if you do not get an answer immediately! timezones, so be patient if you do not get an answer immediately!

View File

@@ -0,0 +1,10 @@
Reporting security issues in Jupyter or JupyterHub
==================================================
If you find a security vulnerability in Jupyter or JupyterHub,
whether it is a failure of the security model described in :doc:`../reference/websecurity`
or a failure in implementation,
please report it to security@ipython.org.
If you prefer to encrypt your security reports,
you can use :download:`this PGP public key </ipython_security.asc>`.

View File

@@ -116,6 +116,7 @@ helps keep our community welcoming to as many people as possible.
contributing/docs contributing/docs
contributing/tests contributing/tests
contributing/roadmap contributing/roadmap
contributing/security
Upgrading JupyterHub Upgrading JupyterHub
-------------------- --------------------

View File

@@ -0,0 +1,52 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2.0.22 (GNU/Linux)
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
5vmLc+Ibz+eT+rM=
=XVDe
-----END PGP PUBLIC KEY BLOCK-----

View File

@@ -12,8 +12,11 @@ function get_hub_version() {
split=( ${hub_xyz//./ } ) split=( ${hub_xyz//./ } )
hub_xy="${split[0]}.${split[1]}" hub_xy="${split[0]}.${split[1]}"
# add .dev on hub_xy so it's 1.0.dev # add .dev on hub_xy so it's 1.0.dev
if [[ ! -z "${split[3]}" ]]; then if [[ ! -z "${split[3]:-}" ]]; then
hub_xy="${hub_xy}.${split[3]}" hub_xy="${hub_xy}.${split[3]}"
latest=0
else
latest=1
fi fi
} }
@@ -31,3 +34,11 @@ docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:$hub_xy
docker push $DOCKER_REPO:$hub_xy docker push $DOCKER_REPO:$hub_xy
docker tag $ONBUILD:$DOCKER_TAG $ONBUILD:$hub_xy docker tag $ONBUILD:$DOCKER_TAG $ONBUILD:$hub_xy
docker push $ONBUILD:$hub_xyz docker push $ONBUILD:$hub_xyz
# if building a stable release, tag latest as well
if [[ "$latest" == "1" ]]; then
docker tag $DOCKER_REPO:$DOCKER_TAG $DOCKER_REPO:latest
docker push $DOCKER_REPO:latest
docker tag $ONBUILD:$DOCKER_TAG $ONBUILD:latest
docker push $ONBUILD:latest
fi

View File

@@ -6,7 +6,7 @@ version_info = (
1, 1,
0, 0,
0, 0,
"b1", # release (b1, rc1, or "" for final or dev) "b2", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing # "dev", # dev or nothing
) )

View File

@@ -427,6 +427,7 @@ class UserServerAPIHandler(APIHandler):
return return
self.log.info("Deleting spawner %s", spawner._log_name) self.log.info("Deleting spawner %s", spawner._log_name)
self.db.delete(spawner.orm_spawner) self.db.delete(spawner.orm_spawner)
user.spawners.pop(server_name, None)
self.db.commit() self.db.commit()
if server_name: if server_name:

View File

@@ -1410,7 +1410,18 @@ class JupyterHub(Application):
def init_db(self): def init_db(self):
"""Create the database connection""" """Create the database connection"""
self.log.debug("Connecting to db: %s", self.db_url) urlinfo = urlparse(self.db_url)
if urlinfo.password:
# avoid logging the database password
urlinfo = urlinfo._replace(
netloc='{}:[redacted]@{}:{}'.format(
urlinfo.username, urlinfo.hostname, urlinfo.port
)
)
db_log_url = urlinfo.geturl()
else:
db_log_url = self.db_url
self.log.debug("Connecting to db: %s", db_log_url)
if self.upgrade_db: if self.upgrade_db:
dbutil.upgrade_if_needed(self.db_url, log=self.log) dbutil.upgrade_if_needed(self.db_url, log=self.log)
@@ -1420,7 +1431,7 @@ class JupyterHub(Application):
) )
self.db = self.session_factory() self.db = self.session_factory()
except OperationalError as e: except OperationalError as e:
self.log.error("Failed to connect to db: %s", self.db_url) self.log.error("Failed to connect to db: %s", db_log_url)
self.log.debug("Database error was:", exc_info=True) self.log.debug("Database error was:", exc_info=True)
if self.db_url.startswith('sqlite:///'): if self.db_url.startswith('sqlite:///'):
self._check_db_path(self.db_url.split(':///', 1)[1]) self._check_db_path(self.db_url.split(':///', 1)[1])

View File

@@ -9,6 +9,7 @@ from contextlib import contextmanager
from datetime import datetime from datetime import datetime
from subprocess import check_call from subprocess import check_call
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from urllib.parse import urlparse
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -118,7 +119,18 @@ def upgrade_if_needed(db_url, backup=True, log=None):
else: else:
# nothing to do # nothing to do
return return
log.info("Upgrading %s", db_url) urlinfo = urlparse(db_url)
if urlinfo.password:
# avoid logging the database password
urlinfo = urlinfo._replace(
netloc='{}:[redacted]@{}:{}'.format(
urlinfo.username, urlinfo.hostname, urlinfo.port
)
)
db_log_url = urlinfo.geturl()
else:
db_log_url = db_url
log.info("Upgrading %s", db_log_url)
# we need to upgrade, backup the database # we need to upgrade, backup the database
if backup and db_url.startswith('sqlite:///'): if backup and db_url.startswith('sqlite:///'):
db_file = db_url.split(':///', 1)[1] db_file = db_url.split(':///', 1)[1]

View File

@@ -549,6 +549,8 @@ class BaseHandler(RequestHandler):
- else: /hub/home - else: /hub/home
""" """
next_url = self.get_argument('next', default='') next_url = self.get_argument('next', default='')
# protect against some browsers' buggy handling of backslash as slash
next_url = next_url.replace('\\', '%5C')
if (next_url + '/').startswith( if (next_url + '/').startswith(
( (
'%s://%s/' % (self.request.protocol, self.request.host), '%s://%s/' % (self.request.protocol, self.request.host),
@@ -562,15 +564,23 @@ class BaseHandler(RequestHandler):
) )
): ):
# treat absolute URLs for our host as absolute paths: # treat absolute URLs for our host as absolute paths:
# below, redirects that aren't strictly paths
parsed = urlparse(next_url) parsed = urlparse(next_url)
next_url = parsed.path next_url = parsed.path
if parsed.query: if parsed.query:
next_url = next_url + '?' + parsed.query next_url = next_url + '?' + parsed.query
if parsed.fragment: if parsed.fragment:
next_url = next_url + '#' + parsed.fragment next_url = next_url + '#' + parsed.fragment
if next_url and (urlparse(next_url).netloc or not next_url.startswith('/')):
# if it still has host info, it didn't match our above check for *this* host
if next_url and (
'://' in next_url
or next_url.startswith('//')
or not next_url.startswith('/')
):
self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url) self.log.warning("Disallowing redirect outside JupyterHub: %r", next_url)
next_url = '' next_url = ''
if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')): if next_url and next_url.startswith(url_path_join(self.base_url, 'user/')):
# add /hub/ prefix, to ensure we redirect to the right user's server. # add /hub/ prefix, to ensure we redirect to the right user's server.
# The next request will be handled by SpawnHandler, # The next request will be handled by SpawnHandler,

View File

@@ -281,13 +281,20 @@ class SpawnPendingHandler(BaseHandler):
# Implicit spawn on /user/:name is not allowed if the user's last spawn failed. # Implicit spawn on /user/:name is not allowed if the user's last spawn failed.
# We should point the user to Home if the most recent spawn failed. # We should point the user to Home if the most recent spawn failed.
exc = spawner._spawn_future.exception() exc = spawner._spawn_future.exception()
self.log.error( self.log.error("Previous spawn for %s failed: %s", spawner._log_name, exc)
"Preventing implicit spawn for %s because last spawn failed: %s", spawn_url = url_path_join(self.hub.base_url, "spawn", user.escaped_name)
spawner._log_name, self.set_status(500)
exc, html = self.render_template(
"not_running.html",
user=user,
server_name=server_name,
spawn_url=spawn_url,
failed=True,
failed_message=getattr(exc, 'jupyterhub_message', ''),
exception=exc,
) )
# raise a copy because each time an Exception object is re-raised, its traceback grows self.finish(html)
raise copy.copy(exc).with_traceback(exc.__traceback__) return
# Check for pending events. This should usually be the case # Check for pending events. This should usually be the case
# when we are on this page. # when we are on this page.

View File

@@ -5,9 +5,11 @@ implements https://oauthlib.readthedocs.io/en/latest/oauth2/server.html
from datetime import datetime from datetime import datetime
from urllib.parse import urlparse from urllib.parse import urlparse
from oauthlib import uri_validate
from oauthlib.oauth2 import RequestValidator from oauthlib.oauth2 import RequestValidator
from oauthlib.oauth2 import WebApplicationServer from oauthlib.oauth2 import WebApplicationServer
from oauthlib.oauth2.rfc6749.grant_types import authorization_code from oauthlib.oauth2.rfc6749.grant_types import authorization_code
from oauthlib.oauth2.rfc6749.grant_types import base
from sqlalchemy.orm import scoped_session from sqlalchemy.orm import scoped_session
from tornado import web from tornado import web
from tornado.escape import url_escape from tornado.escape import url_escape
@@ -21,7 +23,16 @@ from ..utils import url_path_join
# patch absolute-uri check # patch absolute-uri check
# because we want to allow relative uri oauth # because we want to allow relative uri oauth
# for internal services # for internal services
authorization_code.is_absolute_uri = lambda uri: True
def is_absolute_uri(uri):
if uri.startswith('/'):
return True
return uri_validate.is_absolute_uri(uri)
authorization_code.is_absolute_uri = is_absolute_uri
base.is_absolute_uri = is_absolute_uri
class JupyterHubRequestValidator(RequestValidator): class JupyterHubRequestValidator(RequestValidator):

View File

@@ -960,7 +960,7 @@ class HubOAuthCallbackHandler(HubOAuthenticated, RequestHandler):
# validate OAuth state # validate OAuth state
arg_state = self.get_argument("state", None) arg_state = self.get_argument("state", None)
if arg_state is None: if arg_state is None:
raise HTTPError("oauth state is missing. Try logging in again.") raise HTTPError(500, "oauth state is missing. Try logging in again.")
cookie_name = self.hub_auth.get_state_cookie_name(arg_state) cookie_name = self.hub_auth.get_state_cookie_name(arg_state)
cookie_state = self.get_secure_cookie(cookie_name) cookie_state = self.get_secure_cookie(cookie_name)
# clear cookie state now that we've consumed it # clear cookie state now that we've consumed it

View File

@@ -323,6 +323,8 @@ class MockHub(JupyterHub):
self.pid_file = NamedTemporaryFile(delete=False).name self.pid_file = NamedTemporaryFile(delete=False).name
self.db_file = NamedTemporaryFile() self.db_file = NamedTemporaryFile()
self.db_url = os.getenv('JUPYTERHUB_TEST_DB_URL') or self.db_file.name self.db_url = os.getenv('JUPYTERHUB_TEST_DB_URL') or self.db_file.name
if 'mysql' in self.db_url:
self.db_kwargs['connect_args'] = {'auth_plugin': 'mysql_native_password'}
yield super().initialize([]) yield super().initialize([])
# add an initial user # add an initial user

View File

@@ -13,7 +13,10 @@ from jupyterhub import orm
def populate_db(url): def populate_db(url):
"""Populate a jupyterhub database""" """Populate a jupyterhub database"""
db = orm.new_session_factory(url)() connect_args = {}
if 'mysql' in url:
connect_args['auth_plugin'] = 'mysql_native_password'
db = orm.new_session_factory(url, connect_args=connect_args)()
# create some users # create some users
admin = orm.User(name='admin', admin=True) admin = orm.User(name='admin', admin=True)
db.add(admin) db.add(admin)

View File

@@ -28,7 +28,7 @@ def generate_old_db(env_dir, hub_version, db_url):
check_call([sys.executable, '-m', 'virtualenv', env_dir]) check_call([sys.executable, '-m', 'virtualenv', env_dir])
pkgs = ['jupyterhub==' + hub_version] pkgs = ['jupyterhub==' + hub_version]
if 'mysql' in db_url: if 'mysql' in db_url:
pkgs.append('mysql-connector<2.2') pkgs.append('mysql-connector-python')
elif 'postgres' in db_url: elif 'postgres' in db_url:
pkgs.append('psycopg2') pkgs.append('psycopg2')
check_call([env_pip, 'install'] + pkgs) check_call([env_pip, 'install'] + pkgs)

View File

@@ -162,8 +162,10 @@ async def test_delete_named_server(app, named_servers):
) )
r.raise_for_status() r.raise_for_status()
assert r.status_code == 204 assert r.status_code == 204
# low-level record is now removes # low-level record is now removed
assert servername not in user.orm_spawners assert servername not in user.orm_spawners
# and it's still not in the high-level wrapper dict
assert servername not in user.spawners
async def test_named_server_disabled(app): async def test_named_server_disabled(app):

View File

@@ -467,9 +467,12 @@ async def test_login_strip(app):
(False, '/absolute', '/absolute'), (False, '/absolute', '/absolute'),
(False, '/has?query#andhash', '/has?query#andhash'), (False, '/has?query#andhash', '/has?query#andhash'),
# next_url outside is not allowed # next_url outside is not allowed
(False, 'relative/path', ''),
(False, 'https://other.domain', ''), (False, 'https://other.domain', ''),
(False, 'ftp://other.domain', ''), (False, 'ftp://other.domain', ''),
(False, '//other.domain', ''), (False, '//other.domain', ''),
(False, '///other.domain/triple', ''),
(False, '\\\\other.domain/backslashes', ''),
], ],
) )
async def test_login_redirect(app, running, next_url, location): async def test_login_redirect(app, running, next_url, location):
@@ -485,7 +488,7 @@ async def test_login_redirect(app, running, next_url, location):
url = 'login' url = 'login'
if next_url: if next_url:
if '//' not in next_url: if '//' not in next_url and next_url.startswith('/'):
next_url = ujoin(app.base_url, next_url, '') next_url = ujoin(app.base_url, next_url, '')
url = url_concat(url, dict(next=next_url)) url = url_concat(url, dict(next=next_url))

View File

@@ -163,6 +163,10 @@ class User:
self.spawners = _SpawnerDict(self._new_spawner) self.spawners = _SpawnerDict(self._new_spawner)
# ensure default spawner exists in the database
if '' not in self.orm_user.orm_spawners:
self._new_orm_spawner('')
@property @property
def authenticator(self): def authenticator(self):
return self.settings.get('authenticator', None) return self.settings.get('authenticator', None)
@@ -221,6 +225,14 @@ class User:
# otherwise, yield low-level ORM object (server is not active) # otherwise, yield low-level ORM object (server is not active)
yield orm_spawner yield orm_spawner
def _new_orm_spawner(self, server_name):
"""Creat the low-level orm Spawner object"""
orm_spawner = orm.Spawner(user=self.orm_user, name=server_name)
self.db.add(orm_spawner)
self.db.commit()
assert server_name in self.orm_spawners
return orm_spawner
def _new_spawner(self, server_name, spawner_class=None, **kwargs): def _new_spawner(self, server_name, spawner_class=None, **kwargs):
"""Create a new spawner""" """Create a new spawner"""
if spawner_class is None: if spawner_class is None:
@@ -229,10 +241,7 @@ class User:
orm_spawner = self.orm_spawners.get(server_name) orm_spawner = self.orm_spawners.get(server_name)
if orm_spawner is None: if orm_spawner is None:
orm_spawner = orm.Spawner(user=self.orm_user, name=server_name) orm_spawner = self._new_orm_spawner(server_name)
self.db.add(orm_spawner)
self.db.commit()
assert server_name in self.orm_spawners
if server_name == '' and self.state: if server_name == '' and self.state:
# migrate user.state to spawner.state # migrate user.state to spawner.state
orm_spawner.state = self.state orm_spawner.state = self.state

View File

@@ -14,16 +14,15 @@
"lessc": "lessc" "lessc": "lessc"
}, },
"devDependencies": { "devDependencies": {
"clean-css": "^3.4.13", "less": "^3.9.0",
"less": "^2.7.1",
"less-plugin-clean-css": "^1.5.1", "less-plugin-clean-css": "^1.5.1",
"prettier": "^1.14.2" "prettier": "^1.16.4"
}, },
"dependencies": { "dependencies": {
"bootstrap": "^3.4.0", "bootstrap": "^3.4.1",
"font-awesome": "^4.7.0", "font-awesome": "^4.7.0",
"jquery": "^3.2.1", "jquery": "^3.3.1",
"moment": "^2.19.3", "moment": "^2.24.0",
"requirejs": "^2.3.4" "requirejs": "^2.3.6"
} }
} }

View File

@@ -3,7 +3,7 @@ async_generator>=1.8
certipy>=0.1.2 certipy>=0.1.2
entrypoints entrypoints
jinja2 jinja2
oauthlib>=2.0,<3 oauthlib>=3.0
pamela pamela
prometheus_client>=0.0.21 prometheus_client>=0.0.21
python-dateutil python-dateutil

View File

@@ -126,7 +126,10 @@ require(["jquery", "bootstrap", "moment", "jhapi", "utils"], function(
var row = getRow(el); var row = getRow(el);
var user = row.data("user"); var user = row.data("user");
var serverName = row.data("server-name"); var serverName = row.data("server-name");
el.attr("href", utils.url_path_join(prefix, "hub/spawn", user, serverName)); el.attr(
"href",
utils.url_path_join(prefix, "hub/spawn", user, serverName)
);
}); });
// cannot start all servers in this case // cannot start all servers in this case
// since it would mean opening a bunch of tabs // since it would mean opening a bunch of tabs

View File

@@ -5,12 +5,36 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="text-center"> <div class="text-center">
{% block heading %}
<h1>
{% if failed %}
Spawn failed
{% else %}
Server not running
{% endif %}
</h1>
{% endblock %}
{% block message %} {% block message %}
<p>Server {{ server_name }} is not running. Would you like to start it?</p> <p>
{% if failed %}
The latest attempt to start your server {{ server_name }} has failed.
{% if failed_message %}
{{ failed_message }}
{% endif %}
Would you like to retry starting it?
{% else %}
Your server {{ server_name }} is not running. Would you like to start it?
{% endif %}
</p>
{% endblock %} {% endblock %}
{% block start_button %} {% block start_button %}
<a id="start" role="button" class="btn btn-lg btn-primary" href="{{ spawn_url }}"> <a id="start" role="button" class="btn btn-lg btn-primary" href="{{ spawn_url }}">
Launch Server {{ server_name }} {% if failed %}
Relaunch
{% else %}
Launch
{% endif %}
Server {{ server_name }}
</a> </a>
{% endblock %} {% endblock %}
</div> </div>

View File

@@ -14,7 +14,7 @@ function get_hub_version() {
split=( ${hub_xyz//./ } ) split=( ${hub_xyz//./ } )
hub_xy="${split[0]}.${split[1]}" hub_xy="${split[0]}.${split[1]}"
# add .dev on hub_xy so it's 1.0.dev # add .dev on hub_xy so it's 1.0.dev
if [[ ! -z "${split[3]}" ]]; then if [[ ! -z "${split[3]:-}" ]]; then
hub_xy="${hub_xy}.${split[3]}" hub_xy="${hub_xy}.${split[3]}"
fi fi
} }