/
user.py
797 lines (704 loc) · 29.2 KB
/
user.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
import json
import warnings
from collections import defaultdict
from datetime import datetime
from datetime import timedelta
from urllib.parse import quote
from urllib.parse import urlparse
from sqlalchemy import inspect
from tornado import gen
from tornado import web
from tornado.httputil import urlencode
from tornado.log import app_log
from . import orm
from ._version import __version__
from ._version import _check_version
from .crypto import CryptKeeper
from .crypto import decrypt
from .crypto import encrypt
from .crypto import EncryptionUnavailable
from .crypto import InvalidToken
from .metrics import RUNNING_SERVERS
from .metrics import TOTAL_USERS
from .objects import Server
from .spawner import LocalProcessSpawner
from .utils import make_ssl_context
from .utils import maybe_future
from .utils import url_path_join
class UserDict(dict):
"""Like defaultdict, but for users
Getting by a user id OR an orm.User instance returns a User wrapper around the orm user.
"""
def __init__(self, db_factory, settings):
self.db_factory = db_factory
self.settings = settings
super().__init__()
@property
def db(self):
return self.db_factory()
def from_orm(self, orm_user):
return User(orm_user, self.settings)
def add(self, orm_user):
"""Add a user to the UserDict"""
if orm_user.id not in self:
self[orm_user.id] = self.from_orm(orm_user)
TOTAL_USERS.inc()
return self[orm_user.id]
def __contains__(self, key):
if isinstance(key, (User, orm.User)):
key = key.id
return dict.__contains__(self, key)
def __getitem__(self, key):
if isinstance(key, User):
key = key.id
elif isinstance(key, str):
orm_user = self.db.query(orm.User).filter(orm.User.name == key).first()
if orm_user is None:
raise KeyError("No such user: %s" % key)
else:
key = orm_user
if isinstance(key, orm.User):
# users[orm_user] returns User(orm_user)
orm_user = key
if orm_user.id not in self:
user = self[orm_user.id] = User(orm_user, self.settings)
return user
user = dict.__getitem__(self, orm_user.id)
user.db = self.db
return user
elif isinstance(key, int):
id = key
if id not in self:
orm_user = self.db.query(orm.User).filter(orm.User.id == id).first()
if orm_user is None:
raise KeyError("No such user: %s" % id)
user = self.add(orm_user)
else:
user = dict.__getitem__(self, id)
return user
else:
raise KeyError(repr(key))
def __delitem__(self, key):
user = self[key]
for orm_spawner in user.orm_user._orm_spawners:
if orm_spawner in self.db:
self.db.expunge(orm_spawner)
if user.orm_user in self.db:
self.db.expunge(user.orm_user)
dict.__delitem__(self, user.id)
def delete(self, key):
"""Delete a user from the cache and the database"""
user = self[key]
user_id = user.id
self.db.delete(user)
self.db.commit()
# delete from dict after commit
TOTAL_USERS.dec()
del self[user_id]
def count_active_users(self):
"""Count the number of user servers that are active/pending/ready
Returns dict with counts of active/pending/ready servers
"""
counts = defaultdict(lambda: 0)
for user in self.values():
for spawner in user.spawners.values():
pending = spawner.pending
if pending:
counts['pending'] += 1
counts[pending + '_pending'] += 1
if spawner.active:
counts['active'] += 1
if spawner.ready:
counts['ready'] += 1
return counts
class _SpawnerDict(dict):
def __init__(self, spawner_factory):
self.spawner_factory = spawner_factory
def __getitem__(self, key):
if key not in self:
self[key] = self.spawner_factory(key)
return super().__getitem__(key)
class User:
"""High-level wrapper around an orm.User object"""
# declare instance attributes
db = None
orm_user = None
log = app_log
settings = None
_auth_refreshed = None
def __init__(self, orm_user, settings=None, db=None):
self.db = db or inspect(orm_user).session
self.settings = settings or {}
self.orm_user = orm_user
self.allow_named_servers = self.settings.get('allow_named_servers', False)
self.base_url = self.prefix = (
url_path_join(self.settings.get('base_url', '/'), 'user', self.escaped_name)
+ '/'
)
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
def authenticator(self):
return self.settings.get('authenticator', None)
@property
def spawner_class(self):
return self.settings.get('spawner_class', LocalProcessSpawner)
async def save_auth_state(self, auth_state):
"""Encrypt and store auth_state"""
if auth_state is None:
self.encrypted_auth_state = None
else:
self.encrypted_auth_state = await encrypt(auth_state)
self.db.commit()
async def get_auth_state(self):
"""Retrieve and decrypt auth_state for the user"""
encrypted = self.encrypted_auth_state
if encrypted is None:
return None
try:
auth_state = await decrypt(encrypted)
except (ValueError, InvalidToken, EncryptionUnavailable) as e:
self.log.warning(
"Failed to retrieve encrypted auth_state for %s because %s",
self.name,
e,
)
return
# loading auth_state
if auth_state:
# Crypt has multiple keys, store again with new key for rotation.
if len(CryptKeeper.instance().keys) > 1:
await self.save_auth_state(auth_state)
return auth_state
def all_spawners(self, include_default=True):
"""Generator yielding all my spawners
including those that are not running.
Spawners that aren't running will be low-level orm.Spawner objects,
while those that are will be higher-level Spawner wrapper objects.
"""
for name, orm_spawner in sorted(self.orm_user.orm_spawners.items()):
if name == '' and not include_default:
continue
if name and not self.allow_named_servers:
continue
if name in self.spawners:
# yield wrapper if it exists (server may be active)
yield self.spawners[name]
else:
# otherwise, yield low-level ORM object (server is not active)
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):
"""Create a new spawner"""
if spawner_class is None:
spawner_class = self.spawner_class
self.log.debug("Creating %s for %s:%s", spawner_class, self.name, server_name)
orm_spawner = self.orm_spawners.get(server_name)
if orm_spawner is None:
orm_spawner = self._new_orm_spawner(server_name)
if server_name == '' and self.state:
# migrate user.state to spawner.state
orm_spawner.state = self.state
self.state = None
# use fully quoted name for client_id because it will be used in cookie-name
# self.escaped_name may contain @ which is legal in URLs but not cookie keys
client_id = 'jupyterhub-user-%s' % quote(self.name)
if server_name:
client_id = '%s-%s' % (client_id, quote(server_name))
trusted_alt_names = []
trusted_alt_names.extend(self.settings.get('trusted_alt_names', []))
if self.settings.get('subdomain_host'):
trusted_alt_names.append('DNS:' + self.domain)
spawn_kwargs = dict(
user=self,
orm_spawner=orm_spawner,
hub=self.settings.get('hub'),
authenticator=self.authenticator,
config=self.settings.get('config'),
proxy_spec=url_path_join(self.proxy_spec, server_name, '/'),
db=self.db,
oauth_client_id=client_id,
cookie_options=self.settings.get('cookie_options', {}),
trusted_alt_names=trusted_alt_names,
)
if self.settings.get('internal_ssl'):
ssl_kwargs = dict(
internal_ssl=self.settings.get('internal_ssl'),
internal_trust_bundles=self.settings.get('internal_trust_bundles'),
internal_certs_location=self.settings.get('internal_certs_location'),
)
spawn_kwargs.update(ssl_kwargs)
# update with kwargs. Mainly for testing.
spawn_kwargs.update(kwargs)
spawner = spawner_class(**spawn_kwargs)
spawner.load_state(orm_spawner.state or {})
return spawner
# singleton property, self.spawner maps onto spawner with empty server_name
@property
def spawner(self):
return self.spawners['']
@spawner.setter
def spawner(self, spawner):
self.spawners[''] = spawner
# pass get/setattr to ORM user
def __getattr__(self, attr):
if hasattr(self.orm_user, attr):
return getattr(self.orm_user, attr)
else:
raise AttributeError(attr)
def __setattr__(self, attr, value):
if not attr.startswith('_') and self.orm_user and hasattr(self.orm_user, attr):
setattr(self.orm_user, attr, value)
else:
super().__setattr__(attr, value)
def __repr__(self):
return repr(self.orm_user)
@property
def running(self):
"""property for whether the user's default server is running"""
if not self.spawners:
return False
return self.spawner.ready
@property
def active(self):
"""True if any server is active"""
if not self.spawners:
return False
return any(s.active for s in self.spawners.values())
@property
def spawn_pending(self):
warnings.warn(
"User.spawn_pending is deprecated in JupyterHub 0.8. Use Spawner.pending",
DeprecationWarning,
)
return self.spawner.pending == 'spawn'
@property
def stop_pending(self):
warnings.warn(
"User.stop_pending is deprecated in JupyterHub 0.8. Use Spawner.pending",
DeprecationWarning,
)
return self.spawner.pending == 'stop'
@property
def server(self):
return self.spawner.server
@property
def escaped_name(self):
"""My name, escaped for use in URLs, cookies, etc."""
return quote(self.name, safe='@~')
@property
def json_escaped_name(self):
"""The user name, escaped for use in javascript inserts, etc."""
return json.dumps(self.name)[1:-1]
@property
def proxy_spec(self):
"""The proxy routespec for my default server"""
if self.settings.get('subdomain_host'):
return url_path_join(self.domain, self.base_url, '/')
else:
return url_path_join(self.base_url, '/')
@property
def domain(self):
"""Get the domain for my server."""
# use underscore as escape char for domains
return (
quote(self.name).replace('%', '_').lower() + '.' + self.settings['domain']
)
@property
def host(self):
"""Get the *host* for my server (proto://domain[:port])"""
# FIXME: escaped_name probably isn't escaped enough in general for a domain fragment
parsed = urlparse(self.settings['subdomain_host'])
h = '%s://%s' % (parsed.scheme, self.domain)
if parsed.port:
h += ':%i' % parsed.port
return h
@property
def url(self):
"""My URL
Full name.domain/path if using subdomains, otherwise just my /base/url
"""
if self.settings.get('subdomain_host'):
url = '{host}{path}'.format(host=self.host, path=self.base_url)
else:
url = self.base_url
if self.settings.get('default_server_name'):
return url_path_join(url, self.settings.get('default_server_name'))
else:
return url
def server_url(self, server_name=''):
"""Get the url for a server with a given name"""
if not server_name:
return self.url
else:
return url_path_join(self.url, server_name)
def progress_url(self, server_name=''):
"""API URL for progress endpoint for a server with a given name"""
url_parts = [self.settings['hub'].base_url, 'api/users', self.escaped_name]
if server_name:
url_parts.extend(['servers', server_name, 'progress'])
else:
url_parts.extend(['server/progress'])
return url_path_join(*url_parts)
async def refresh_auth(self, handler):
"""Refresh authentication if needed
Checks authentication expiry and refresh it if needed.
See Spawner.
If the auth is expired and cannot be refreshed
without forcing a new login, a few things can happen:
1. if this is a normal user spawn,
the user should be redirected to login
and back to spawn after login.
2. if this is a spawn via API or other user,
spawn will fail until the user logs in again.
Args:
handler (RequestHandler):
The handler for the request triggering the spawn.
May be None
"""
authenticator = self.authenticator
if authenticator is None or not authenticator.refresh_pre_spawn:
# nothing to do
return
# refresh auth
auth_user = await handler.refresh_auth(self, force=True)
if auth_user:
# auth refreshed, all done
return
# if we got to here, auth is expired and couldn't be refreshed
self.log.error(
"Auth expired for %s; cannot spawn until they login again", self.name
)
# auth expired, cannot spawn without a fresh login
# it's the current user *and* spawn via GET, trigger login redirect
if handler.request.method == 'GET' and handler.current_user is self:
self.log.info("Redirecting %s to login to refresh auth", self.name)
url = self.get_login_url()
next_url = self.request.uri
sep = '&' if '?' in url else '?'
url += sep + urlencode(dict(next=next_url))
self.redirect(url)
raise web.Finish()
else:
# spawn via POST or on behalf of another user.
# nothing we can do here but fail
raise web.HTTPError(
400, "{}'s authentication has expired".format(self.name)
)
async def spawn(self, server_name='', options=None, handler=None):
"""Start the user's spawner
depending from the value of JupyterHub.allow_named_servers
if False:
JupyterHub expects only one single-server per user
url of the server will be /user/:name
if True:
JupyterHub expects more than one single-server per user
url of the server will be /user/:name/:server_name
"""
db = self.db
if handler:
await self.refresh_auth(handler)
base_url = url_path_join(self.base_url, server_name) + '/'
orm_server = orm.Server(base_url=base_url)
db.add(orm_server)
note = "Server at %s" % base_url
api_token = self.new_api_token(note=note)
db.commit()
spawner = self.spawners[server_name]
spawner.server = server = Server(orm_server=orm_server)
assert spawner.orm_spawner.server is orm_server
# pass requesting handler to the spawner
# e.g. for processing GET params
spawner.handler = handler
# Passing user_options to the spawner
if options is None:
# options unspecified, load from db which should have the previous value
options = spawner.orm_spawner.user_options or {}
else:
# options specified, save for use as future defaults
spawner.orm_spawner.user_options = options
db.commit()
spawner.user_options = options
# we are starting a new server, make sure it doesn't restore state
spawner.clear_state()
# create API and OAuth tokens
spawner.api_token = api_token
spawner.admin_access = self.settings.get('admin_access', False)
client_id = spawner.oauth_client_id
oauth_provider = self.settings.get('oauth_provider')
if oauth_provider:
oauth_client = oauth_provider.fetch_by_client_id(client_id)
# create a new OAuth client + secret on every launch
# containers that resume will be updated below
oauth_provider.add_client(
client_id,
api_token,
url_path_join(self.url, server_name, 'oauth_callback'),
description="Server at %s"
% (url_path_join(self.base_url, server_name) + '/'),
)
db.commit()
# trigger pre-spawn hook on authenticator
authenticator = self.authenticator
try:
if authenticator:
# pre_spawn_start can thow errors that can lead to a redirect loop
# if left uncaught (see https://github.com/jupyterhub/jupyterhub/issues/2683)
await maybe_future(authenticator.pre_spawn_start(self, spawner))
spawner._start_pending = True
# update spawner start time, and activity for both spawner and user
self.last_activity = (
spawner.orm_spawner.started
) = spawner.orm_spawner.last_activity = datetime.utcnow()
db.commit()
# wait for spawner.start to return
# run optional preparation work to bootstrap the notebook
await maybe_future(spawner.run_pre_spawn_hook())
if self.settings.get('internal_ssl'):
self.log.debug("Creating internal SSL certs for %s", spawner._log_name)
hub_paths = await maybe_future(spawner.create_certs())
spawner.cert_paths = await maybe_future(spawner.move_certs(hub_paths))
self.log.debug("Calling Spawner.start for %s", spawner._log_name)
f = maybe_future(spawner.start())
# commit any changes in spawner.start (always commit db changes before yield)
db.commit()
url = await gen.with_timeout(timedelta(seconds=spawner.start_timeout), f)
if url:
# get ip, port info from return value of start()
if isinstance(url, str):
# >= 0.9 can return a full URL string
pass
else:
# >= 0.7 returns (ip, port)
proto = 'https' if self.settings['internal_ssl'] else 'http'
url = '%s://%s:%i' % ((proto,) + url)
urlinfo = urlparse(url)
server.proto = urlinfo.scheme
server.ip = urlinfo.hostname
port = urlinfo.port
if not port:
if urlinfo.scheme == 'https':
port = 443
else:
port = 80
server.port = port
db.commit()
else:
# prior to 0.7, spawners had to store this info in user.server themselves.
# Handle < 0.7 behavior with a warning, assuming info was stored in db by the Spawner.
self.log.warning(
"DEPRECATION: Spawner.start should return a url or (ip, port) tuple in JupyterHub >= 0.9"
)
if spawner.api_token and spawner.api_token != api_token:
# Spawner re-used an API token, discard the unused api_token
orm_token = orm.APIToken.find(self.db, api_token)
if orm_token is not None:
self.db.delete(orm_token)
self.db.commit()
# check if the re-used API token is valid
found = orm.APIToken.find(self.db, spawner.api_token)
if found:
if found.user is not self.orm_user:
self.log.error(
"%s's server is using %s's token! Revoking this token.",
self.name,
(found.user or found.service).name,
)
self.db.delete(found)
self.db.commit()
raise ValueError("Invalid token for %s!" % self.name)
else:
# Spawner.api_token has changed, but isn't in the db.
# What happened? Maybe something unclean in a resumed container.
self.log.warning(
"%s's server specified its own API token that's not in the database",
self.name,
)
# use generated=False because we don't trust this token
# to have been generated properly
self.new_api_token(
spawner.api_token,
generated=False,
note="retrieved from spawner %s" % server_name,
)
# update OAuth client secret with updated API token
if oauth_provider:
oauth_provider.add_client(
client_id,
spawner.api_token,
url_path_join(self.url, server_name, 'oauth_callback'),
)
db.commit()
except Exception as e:
if isinstance(e, gen.TimeoutError):
self.log.warning(
"{user}'s server failed to start in {s} seconds, giving up".format(
user=self.name, s=spawner.start_timeout
)
)
e.reason = 'timeout'
self.settings['statsd'].incr('spawner.failure.timeout')
else:
self.log.error(
"Unhandled error starting {user}'s server: {error}".format(
user=self.name, error=e
)
)
self.settings['statsd'].incr('spawner.failure.error')
e.reason = 'error'
try:
await self.stop(spawner.name)
except Exception:
self.log.error(
"Failed to cleanup {user}'s server that failed to start".format(
user=self.name
),
exc_info=True,
)
# raise original exception
spawner._start_pending = False
raise e
finally:
# clear reference to handler after start finishes
spawner.handler = None
spawner.start_polling()
# store state
if self.state is None:
self.state = {}
spawner.orm_spawner.state = spawner.get_state()
db.commit()
spawner._waiting_for_response = True
await self._wait_up(spawner)
async def _wait_up(self, spawner):
"""Wait for a server to finish starting.
Shuts the server down if it doesn't respond within
spawner.http_timeout.
"""
server = spawner.server
key = self.settings.get('internal_ssl_key')
cert = self.settings.get('internal_ssl_cert')
ca = self.settings.get('internal_ssl_ca')
ssl_context = make_ssl_context(key, cert, cafile=ca)
try:
resp = await server.wait_up(
http=True, timeout=spawner.http_timeout, ssl_context=ssl_context
)
except Exception as e:
if isinstance(e, TimeoutError):
self.log.warning(
"{user}'s server never showed up at {url} "
"after {http_timeout} seconds. Giving up".format(
user=self.name,
url=server.url,
http_timeout=spawner.http_timeout,
)
)
e.reason = 'timeout'
self.settings['statsd'].incr('spawner.failure.http_timeout')
else:
e.reason = 'error'
self.log.error(
"Unhandled error waiting for {user}'s server to show up at {url}: {error}".format(
user=self.name, url=server.url, error=e
)
)
self.settings['statsd'].incr('spawner.failure.http_error')
try:
await self.stop(spawner.name)
except Exception:
self.log.error(
"Failed to cleanup {user}'s server that failed to start".format(
user=self.name
),
exc_info=True,
)
# raise original TimeoutError
raise e
else:
server_version = resp.headers.get('X-JupyterHub-Version')
_check_version(__version__, server_version, self.log)
# record the Spawner version for better error messages
# if it doesn't work
spawner._jupyterhub_version = server_version
finally:
spawner._waiting_for_response = False
spawner._start_pending = False
return spawner
async def stop(self, server_name=''):
"""Stop the user's spawner
and cleanup after it.
"""
spawner = self.spawners[server_name]
spawner._spawn_pending = False
spawner._start_pending = False
spawner._check_pending = False
spawner.stop_polling()
spawner._stop_pending = True
self.log.debug("Stopping %s", spawner._log_name)
try:
api_token = spawner.api_token
status = await spawner.poll()
if status is None:
await spawner.stop()
spawner.clear_state()
spawner.orm_spawner.state = spawner.get_state()
self.last_activity = spawner.orm_spawner.last_activity = datetime.utcnow()
# remove server entry from db
spawner.server = None
if not spawner.will_resume:
# find and remove the API token and oauth client if the spawner isn't
# going to re-use it next time
orm_token = orm.APIToken.find(self.db, api_token)
if orm_token:
self.db.delete(orm_token)
# remove oauth client as well
# handle upgrades from 0.8, where client id will be `user-USERNAME`,
# not just `jupyterhub-user-USERNAME`
client_ids = (
spawner.oauth_client_id,
spawner.oauth_client_id.split('-', 1)[1],
)
for oauth_client in self.db.query(orm.OAuthClient).filter(
orm.OAuthClient.identifier.in_(client_ids)
):
self.log.debug("Deleting oauth client %s", oauth_client.identifier)
self.db.delete(oauth_client)
self.db.commit()
self.log.debug("Finished stopping %s", spawner._log_name)
RUNNING_SERVERS.dec()
finally:
spawner.server = None
spawner.orm_spawner.started = None
self.db.commit()
# trigger post-stop hook
await maybe_future(spawner.run_post_stop_hook())
# trigger post-spawner hook on authenticator
auth = spawner.authenticator
try:
if auth:
await maybe_future(auth.post_spawn_stop(self, spawner))
except Exception:
self.log.exception(
"Error in Authenticator.post_spawn_stop for %s", self
)
spawner._stop_pending = False
if not (
spawner._spawn_future
and (
not spawner._spawn_future.done()
or spawner._spawn_future.exception()
)
):
# pop Spawner *unless* it's stopping due to an error
# because some pages serve latest-spawn error messages
self.spawners.pop(server_name)