Forráskód Böngészése

Merge pull request #6451 from blink1073/jhub-integration

JupyterHub integration
Steven Silvester 5 éve
szülő
commit
3d710e0a26

+ 3 - 0
dev_mode/package.json

@@ -34,6 +34,7 @@
     "@jupyterlab/fileeditor-extension": "^1.0.0-alpha.9",
     "@jupyterlab/help-extension": "^1.0.0-alpha.9",
     "@jupyterlab/htmlviewer-extension": "^1.0.0-alpha.10",
+    "@jupyterlab/hub-extension": "^1.0.0-alpha.8",
     "@jupyterlab/imageviewer": "^1.0.0-alpha.9",
     "@jupyterlab/imageviewer-extension": "^1.0.0-alpha.9",
     "@jupyterlab/inspector-extension": "^1.0.0-alpha.9",
@@ -130,6 +131,7 @@
       "@jupyterlab/fileeditor-extension": "",
       "@jupyterlab/help-extension": "",
       "@jupyterlab/htmlviewer-extension": "",
+      "@jupyterlab/hub-extension": "",
       "@jupyterlab/imageviewer-extension": "",
       "@jupyterlab/inspector-extension": "",
       "@jupyterlab/launcher-extension": "",
@@ -225,6 +227,7 @@
       "@jupyterlab/fileeditor-extension": "../packages/fileeditor-extension",
       "@jupyterlab/help-extension": "../packages/help-extension",
       "@jupyterlab/htmlviewer-extension": "../packages/htmlviewer-extension",
+      "@jupyterlab/hub-extension": "../packages/hub-extension",
       "@jupyterlab/imageviewer-extension": "../packages/imageviewer-extension",
       "@jupyterlab/inspector-extension": "../packages/inspector-extension",
       "@jupyterlab/javascript-extension": "../packages/javascript-extension",

+ 8 - 0
jupyterlab/extension.py

@@ -204,6 +204,14 @@ def load_jupyter_server_extension(nbapp):
     # Must add before the root server handlers to avoid shadowing.
     web_app.add_handlers('.*$', handlers)
 
+    # If running under JupyterHub, add more metadata.
+    if hasattr(nbapp, 'hub_prefix'):
+        page_config['hubPrefix'] = nbapp.hub_prefix
+        page_config['hubHost'] = nbapp.hub_host
+        page_config['hubUser'] = nbapp.user
+        api_token = os.getenv('JUPYTERHUB_API_TOKEN', '')
+        page_config['token'] = api_token
+
     # Add the root handlers if we have not errored.
     if not errored:
         add_handlers(web_app, config)

+ 18 - 16
jupyterlab/labhubapp.py

@@ -1,4 +1,5 @@
 import os
+import warnings
 
 from traitlets import default
 
@@ -11,29 +12,30 @@ except ImportError:
     raise ImportError('You must have jupyterhub installed for this to work.')
 else:
     class SingleUserLabApp(SingleUserNotebookApp, LabApp):
-
+        """
+        A sublcass of JupyterHub's SingleUserNotebookApp which includes LabApp
+        as a mixin. This makes the LabApp configurables available to the spawned
+        jupyter server.
+
+        If you don't need to change any of the configurables from their default
+        values, then this class is not necessary, and you can deploy JupyterLab
+        by ensuring that its server extension is enabled and setting the
+        `Spawner.default_url` to '/lab'.
+
+        If you do need to configure JupyterLab, then use this application by
+        setting `Spawner.cmd = ['jupyter-labhub']`.
+        """
         @default("default_url")
         def _default_url(self):
             """when using jupyter-labhub, jupyterlab is default ui"""
             return "/lab"
 
         def init_webapp(self, *args, **kwargs):
+            warnings.warn(
+                "SingleUserLabApp is deprecated, use SingleUserNotebookApp and set " + \
+                "c.Spawner.default_url = '/lab' in jupyterhub_config.py", DeprecationWarning
+            )
             super().init_webapp(*args, **kwargs)
-            settings = self.web_app.settings
-            if 'page_config_data' not in settings:
-                settings['page_config_data'] = {}
-            settings['page_config_data']['hub_prefix'] = self.hub_prefix
-            settings['page_config_data']['hub_host'] = self.hub_host
-            settings['page_config_data']['hub_user'] = self.user
-            api_token = os.getenv('JUPYTERHUB_API_TOKEN')
-            if not api_token:
-                api_token = ''
-            if not self.token:
-                try:
-                    self.token = api_token
-                except AttributeError:
-                    self.log.error("Can't set self.token")
-            settings['page_config_data']['token'] = api_token
 
 
 def main(argv=None):

+ 8 - 20
packages/application-extension/src/index.tsx

@@ -2,12 +2,12 @@
 // Distributed under the terms of the Modified BSD License.
 
 import {
-  ConnectionLost,
   IConnectionLost,
   ILabShell,
   ILabStatus,
   ILayoutRestorer,
   IRouter,
+  ConnectionLost,
   JupyterFrontEnd,
   JupyterFrontEndPlugin,
   JupyterLab,
@@ -75,13 +75,14 @@ namespace CommandIDs {
  */
 const main: JupyterFrontEndPlugin<void> = {
   id: '@jupyterlab/application-extension:main',
-  requires: [ICommandPalette, IConnectionLost, IRouter, IWindowResolver],
+  requires: [ICommandPalette, IRouter, IWindowResolver],
+  optional: [IConnectionLost],
   activate: (
     app: JupyterFrontEnd,
     palette: ICommandPalette,
-    connectionLost: IConnectionLost,
     router: IRouter,
-    resolver: IWindowResolver
+    resolver: IWindowResolver,
+    connectionLost: IConnectionLost | undefined
   ) => {
     if (!(app instanceof JupyterLab)) {
       throw new Error(`${main.id} must be activated in JupyterLab.`);
@@ -112,7 +113,8 @@ const main: JupyterFrontEndPlugin<void> = {
     });
 
     // If the connection to the server is lost, handle it with the
-    // connection lost token.
+    // connection lost handler.
+    connectionLost = connectionLost || ConnectionLost;
     app.serviceManager.connectionFailure.connect(connectionLost);
 
     const builder = app.serviceManager.builder;
@@ -746,19 +748,6 @@ const paths: JupyterFrontEndPlugin<JupyterFrontEnd.IPaths> = {
   provides: JupyterFrontEnd.IPaths
 };
 
-/**
- * The default JupyterLab connection lost provider. This may be overridden
- * to provide custom behavior when a connection to the server is lost.
- */
-const connectionlost: JupyterFrontEndPlugin<IConnectionLost> = {
-  id: '@jupyterlab/apputils-extension:connectionlost',
-  activate: (app: JupyterFrontEnd): IConnectionLost => {
-    return ConnectionLost;
-  },
-  autoStart: true,
-  provides: IConnectionLost
-};
-
 /**
  * Export the plugins as default.
  */
@@ -773,8 +762,7 @@ const plugins: JupyterFrontEndPlugin<any>[] = [
   shell,
   status,
   info,
-  paths,
-  connectionlost
+  paths
 ];
 
 export default plugins;

+ 2 - 0
packages/application/src/frontend.ts

@@ -272,6 +272,8 @@ export namespace JupyterFrontEnd {
       readonly themes: string;
       readonly tree: string;
       readonly workspaces: string;
+      readonly hubPrefix?: string;
+      readonly hubHost?: string;
     };
 
     /**

+ 3 - 1
packages/application/src/lab.ts

@@ -244,7 +244,9 @@ export namespace JupyterLab {
       settings: PageConfig.getOption('settingsUrl'),
       themes: PageConfig.getOption('themesUrl'),
       tree: PageConfig.getOption('treeUrl'),
-      workspaces: PageConfig.getOption('workspacesUrl')
+      workspaces: PageConfig.getOption('workspacesUrl'),
+      hubHost: PageConfig.getOption('hubHost') || undefined,
+      hubPrefix: PageConfig.getOption('hubPrefix') || undefined
     },
     directories: {
       appSettings: PageConfig.getOption('appSettingsDir'),

+ 7 - 0
packages/hub-extension/README.md

@@ -0,0 +1,7 @@
+# @jupyterlab/hub-extension
+
+JupyterLab](https://github.com/jupyterlab/jupyterlab) integration for
+[JupyterHub](https://github.com/jupyterhub/jupyterhub).
+
+This adds a "Hub" menu to JupyterLab that allows a user to log out of JupyterHub
+or access their JupyterHub control panel.

+ 48 - 0
packages/hub-extension/package.json

@@ -0,0 +1,48 @@
+{
+  "name": "@jupyterlab/hub-extension",
+  "version": "1.0.0-alpha.8",
+  "description": "JupyterLab integration for JupyterHub",
+  "homepage": "https://github.com/jupyterlab/jupyterlab",
+  "bugs": {
+    "url": "https://github.com/jupyterlab/jupyterlab/issues"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/jupyterlab/jupyterlab.git"
+  },
+  "license": "BSD-3-Clause",
+  "author": "Project Jupyter",
+  "files": [
+    "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
+    "schema/*.json",
+    "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}"
+  ],
+  "main": "lib/index.js",
+  "types": "lib/index.d.ts",
+  "directories": {
+    "lib": "lib/"
+  },
+  "scripts": {
+    "build": "tsc",
+    "clean": "rimraf lib",
+    "prepublishOnly": "npm run build",
+    "watch": "tsc -w --listEmittedFiles"
+  },
+  "dependencies": {
+    "@jupyterlab/application": "^1.0.0-alpha.9",
+    "@jupyterlab/apputils": "^1.0.0-alpha.9",
+    "@jupyterlab/coreutils": "^3.0.0-alpha.9",
+    "@jupyterlab/mainmenu": "^1.0.0-alpha.9",
+    "@jupyterlab/services": "^4.0.0-alpha.9"
+  },
+  "devDependencies": {
+    "rimraf": "~2.6.2",
+    "typescript": "~3.5.1"
+  },
+  "publishConfig": {
+    "access": "public"
+  },
+  "jupyterlab": {
+    "extension": true
+  }
+}

+ 163 - 0
packages/hub-extension/src/index.ts

@@ -0,0 +1,163 @@
+/*-----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import { Dialog, ICommandPalette, showDialog } from '@jupyterlab/apputils';
+
+import {
+  ConnectionLost,
+  IConnectionLost,
+  IRouter,
+  JupyterFrontEnd,
+  JupyterFrontEndPlugin
+} from '@jupyterlab/application';
+
+import { URLExt } from '@jupyterlab/coreutils';
+
+import { IMainMenu } from '@jupyterlab/mainmenu';
+
+import { ServerConnection, ServiceManager } from '@jupyterlab/services';
+
+/**
+ * The command IDs used by the plugin.
+ */
+export namespace CommandIDs {
+  export const controlPanel: string = 'hub:control-panel';
+
+  export const logout: string = 'hub:logout';
+
+  export const restart: string = 'hub:restart';
+}
+
+/**
+ * Activate the jupyterhub extension.
+ */
+function activateHubExtension(
+  app: JupyterFrontEnd,
+  router: IRouter,
+  paths: JupyterFrontEnd.IPaths,
+  palette: ICommandPalette,
+  mainMenu: IMainMenu
+): void {
+  const hubHost = paths.urls.hubHost || '';
+  const hubPrefix = paths.urls.hubPrefix || '';
+  const baseUrl = paths.urls.base;
+
+  // Bail if not running on JupyterHub.
+  if (!hubPrefix) {
+    return;
+  }
+
+  console.log('hub-extension: Found configuration ', {
+    hubHost: hubHost,
+    hubPrefix: hubPrefix
+  });
+
+  const { commands } = app;
+
+  // TODO: use /spawn/:user/:name
+  // but that requires jupyterhub 1.0
+  // and jupyterlab to pass username, servername to PageConfig
+  const restartUrl =
+    hubHost + URLExt.join(hubPrefix, `spawn?next=${hubPrefix}home`);
+
+  commands.addCommand(CommandIDs.restart, {
+    label: 'Restart Server',
+    caption: 'Request that the Hub restart this server',
+    execute: () => {
+      window.open(restartUrl, '_blank');
+    }
+  });
+
+  commands.addCommand(CommandIDs.controlPanel, {
+    label: 'Hub Control Panel',
+    caption: 'Open the Hub control panel in a new browser tab',
+    execute: () => {
+      window.open(hubHost + URLExt.join(hubPrefix, 'home'), '_blank');
+    }
+  });
+
+  commands.addCommand(CommandIDs.logout, {
+    label: 'Log Out',
+    caption: 'Log out of the Hub',
+    execute: () => {
+      window.location.href = hubHost + URLExt.join(baseUrl, 'logout');
+    }
+  });
+
+  // Add commands and menu itmes.
+  mainMenu.fileMenu.addGroup(
+    [{ command: CommandIDs.controlPanel }, { command: CommandIDs.logout }],
+    100
+  );
+  const category = 'Hub';
+  palette.addItem({ category, command: CommandIDs.controlPanel });
+  palette.addItem({ category, command: CommandIDs.logout });
+}
+
+/**
+ * Initialization data for the hub-extension.
+ */
+const hubExtension: JupyterFrontEndPlugin<void> = {
+  activate: activateHubExtension,
+  id: 'jupyter.extensions.hub-extension',
+  requires: [IRouter, JupyterFrontEnd.IPaths, ICommandPalette, IMainMenu],
+  autoStart: true
+};
+
+/**
+ * The default JupyterLab connection lost provider. This may be overridden
+ * to provide custom behavior when a connection to the server is lost.
+ *
+ * If the application is being deployed within a JupyterHub context,
+ * this will provide a dialog that prompts the user to restart the server.
+ * Otherwise, it shows an error dialog.
+ */
+const connectionlost: JupyterFrontEndPlugin<IConnectionLost> = {
+  id: '@jupyterlab/apputils-extension:connectionlost',
+  requires: [JupyterFrontEnd.IPaths],
+  activate: (
+    app: JupyterFrontEnd,
+    paths: JupyterFrontEnd.IPaths
+  ): IConnectionLost => {
+    const hubPrefix = paths.urls.hubPrefix || '';
+    const baseUrl = paths.urls.base;
+
+    // Return the default error message if not running on JupyterHub.
+    if (!hubPrefix) {
+      return ConnectionLost;
+    }
+
+    // If we are running on JupyterHub, return a dialog
+    // that prompts the user to restart their server.
+    let showingError = false;
+    const onConnectionLost: IConnectionLost = async (
+      manager: ServiceManager.IManager,
+      err: ServerConnection.NetworkError
+    ): Promise<void> => {
+      if (showingError) {
+        return;
+      }
+      showingError = true;
+      const result = await showDialog({
+        title: 'Server Not Running',
+        body: `Your server at ${baseUrl} is not running.
+Would you like to restart it?`,
+        buttons: [
+          Dialog.okButton({ label: 'Restart' }),
+          Dialog.cancelButton({ label: 'Dismiss' })
+        ]
+      });
+      showingError = false;
+      if (result.button.accept) {
+        await app.commands.execute(CommandIDs.restart);
+      }
+    };
+    return onConnectionLost;
+  },
+  autoStart: true,
+  provides: IConnectionLost
+};
+
+export default [hubExtension, connectionlost] as JupyterFrontEndPlugin<any>[];

+ 25 - 0
packages/hub-extension/tsconfig.json

@@ -0,0 +1,25 @@
+{
+  "extends": "../../tsconfigbase",
+  "compilerOptions": {
+    "outDir": "lib",
+    "rootDir": "src"
+  },
+  "include": ["src/*"],
+  "references": [
+    {
+      "path": "../application"
+    },
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../coreutils"
+    },
+    {
+      "path": "../mainmenu"
+    },
+    {
+      "path": "../services"
+    }
+  ]
+}

+ 1 - 0
packages/metapackage/package.json

@@ -61,6 +61,7 @@
     "@jupyterlab/help-extension": "^1.0.0-alpha.9",
     "@jupyterlab/htmlviewer": "^1.0.0-alpha.10",
     "@jupyterlab/htmlviewer-extension": "^1.0.0-alpha.10",
+    "@jupyterlab/hub-extension": "^1.0.0-alpha.8",
     "@jupyterlab/imageviewer": "^1.0.0-alpha.9",
     "@jupyterlab/imageviewer-extension": "^1.0.0-alpha.9",
     "@jupyterlab/inspector": "^1.0.0-alpha.9",

+ 3 - 0
packages/metapackage/tsconfig.json

@@ -99,6 +99,9 @@
     {
       "path": "../htmlviewer-extension"
     },
+    {
+      "path": "../hub-extension"
+    },
     {
       "path": "../imageviewer"
     },

+ 8 - 5
packages/services/src/kernel/manager.ts

@@ -116,7 +116,7 @@ export class KernelManager implements Kernel.IManager {
   /**
    * A signal emitted when there is a connection failure.
    */
-  get connectionFailure(): ISignal<this, ServerConnection.NetworkError> {
+  get connectionFailure(): ISignal<this, Error> {
     return this._connectionFailure;
   }
 
@@ -280,7 +280,12 @@ export class KernelManager implements Kernel.IManager {
    */
   protected async requestRunning(): Promise<void> {
     const models = await Kernel.listRunning(this.serverSettings).catch(err => {
-      if (err instanceof ServerConnection.NetworkError) {
+      // Check for a network error, or a 503 error, which is returned
+      // by a JupyterHub when a server is shut down.
+      if (
+        err instanceof ServerConnection.NetworkError ||
+        (err.response && err.response.status === 503)
+      ) {
         this._connectionFailure.emit(err);
         return [] as Kernel.IModel[];
       }
@@ -354,9 +359,7 @@ export class KernelManager implements Kernel.IManager {
   private _runningChanged = new Signal<this, Kernel.IModel[]>(this);
   private _specs: Kernel.ISpecModels | null = null;
   private _specsChanged = new Signal<this, Kernel.ISpecModels>(this);
-  private _connectionFailure = new Signal<this, ServerConnection.NetworkError>(
-    this
-  );
+  private _connectionFailure = new Signal<this, Error>(this);
 }
 
 /**

+ 4 - 12
packages/services/src/manager.ts

@@ -78,7 +78,7 @@ export class ServiceManager implements ServiceManager.IManager {
   /**
    * A signal emitted when there is a connection failure with the kernel.
    */
-  get connectionFailure(): ISignal<this, ServerConnection.NetworkError> {
+  get connectionFailure(): ISignal<this, Error> {
     return this._connectionFailure;
   }
 
@@ -166,19 +166,14 @@ export class ServiceManager implements ServiceManager.IManager {
     return this._readyPromise;
   }
 
-  private _onConnectionFailure(
-    sender: any,
-    err: ServerConnection.NetworkError
-  ): void {
+  private _onConnectionFailure(sender: any, err: Error): void {
     this._connectionFailure.emit(err);
   }
 
   private _isDisposed = false;
   private _readyPromise: Promise<void>;
   private _specsChanged = new Signal<this, Kernel.ISpecModels>(this);
-  private _connectionFailure = new Signal<this, ServerConnection.NetworkError>(
-    this
-  );
+  private _connectionFailure = new Signal<this, Error>(this);
   private _isReady = false;
 }
 
@@ -253,10 +248,7 @@ export namespace ServiceManager {
     /**
      * A signal emitted when there is a connection failure with the server.
      */
-    readonly connectionFailure: ISignal<
-      IManager,
-      ServerConnection.NetworkError
-    >;
+    readonly connectionFailure: ISignal<IManager, Error>;
   }
 
   /**

+ 8 - 5
packages/services/src/session/manager.ts

@@ -85,7 +85,7 @@ export class SessionManager implements Session.IManager {
   /**
    * A signal emitted when there is a connection failure.
    */
-  get connectionFailure(): ISignal<this, ServerConnection.NetworkError> {
+  get connectionFailure(): ISignal<this, Error> {
     return this._connectionFailure;
   }
 
@@ -289,7 +289,12 @@ export class SessionManager implements Session.IManager {
    */
   protected async requestRunning(): Promise<void> {
     const models = await Session.listRunning(this.serverSettings).catch(err => {
-      if (err instanceof ServerConnection.NetworkError) {
+      // Check for a network error, or a 503 error, which is returned
+      // by a JupyterHub when a server is shut down.
+      if (
+        err instanceof ServerConnection.NetworkError ||
+        (err.response && err.response.status === 503)
+      ) {
         this._connectionFailure.emit(err);
         return [] as Session.IModel[];
       }
@@ -380,9 +385,7 @@ export class SessionManager implements Session.IManager {
   private _pollSpecs: Poll;
   private _ready: Promise<void>;
   private _runningChanged = new Signal<this, Session.IModel[]>(this);
-  private _connectionFailure = new Signal<this, ServerConnection.NetworkError>(
-    this
-  );
+  private _connectionFailure = new Signal<this, Error>(this);
   private _sessions = new Set<Session.ISession>();
   private _specs: Kernel.ISpecModels | null = null;
   private _specsChanged = new Signal<this, Kernel.ISpecModels>(this);

+ 8 - 5
packages/services/src/terminal/manager.ts

@@ -69,7 +69,7 @@ export class TerminalManager implements TerminalSession.IManager {
   /**
    * A signal emitted when there is a connection failure.
    */
-  get connectionFailure(): ISignal<this, ServerConnection.NetworkError> {
+  get connectionFailure(): ISignal<this, Error> {
     return this._connectionFailure;
   }
 
@@ -246,7 +246,12 @@ export class TerminalManager implements TerminalSession.IManager {
   protected async requestRunning(): Promise<void> {
     const models = await TerminalSession.listRunning(this.serverSettings).catch(
       err => {
-        if (err instanceof ServerConnection.NetworkError) {
+        // Check for a network error, or a 503 error, which is returned
+        // by a JupyterHub when a server is shut down.
+        if (
+          err instanceof ServerConnection.NetworkError ||
+          (err.response && err.response.status === 503)
+        ) {
           this._connectionFailure.emit(err);
           return [] as TerminalSession.IModel[];
         }
@@ -325,9 +330,7 @@ export class TerminalManager implements TerminalSession.IManager {
   private _sessions = new Set<TerminalSession.ISession>();
   private _ready: Promise<void>;
   private _runningChanged = new Signal<this, TerminalSession.IModel[]>(this);
-  private _connectionFailure = new Signal<this, ServerConnection.NetworkError>(
-    this
-  );
+  private _connectionFailure = new Signal<this, Error>(this);
 }
 
 /**