Merge branch '16778-setup-fed-user' refs #16778
authorPeter Amstutz <peter.amstutz@curii.com>
Fri, 4 Sep 2020 15:25:42 +0000 (11:25 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Fri, 4 Sep 2020 15:25:42 +0000 (11:25 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

22 files changed:
apps/workbench/app/views/users/_virtual_machines.html.erb
apps/workbench/app/views/virtual_machines/webshell.html.erb
apps/workbench/config/initializers/assets.rb
apps/workbench/lib/assets/javascripts/webshell/shell_in_a_box.js [moved from apps/workbench/public/webshell/shell_in_a_box.js with 99% similarity]
apps/workbench/lib/assets/stylesheets/webshell/styles.css [moved from apps/workbench/public/webshell/styles.css with 93% similarity]
doc/user/getting_started/vm-login-with-webshell.html.textile.liquid
doc/user/getting_started/workbench.html.textile.liquid
doc/user/index.html.textile.liquid
lib/cloud/cloudtest/tester.go
lib/dispatchcloud/dispatcher_test.go
lib/dispatchcloud/worker/pool.go
lib/dispatchcloud/worker/verify.go
lib/dispatchcloud/worker/worker.go
tools/arvbox/bin/arvbox
tools/arvbox/lib/arvbox/docker/Dockerfile.base
tools/arvbox/lib/arvbox/docker/cluster-config.sh
tools/arvbox/lib/arvbox/docker/common.sh
tools/arvbox/lib/arvbox/docker/service/nginx/run
tools/arvbox/lib/arvbox/docker/service/webshell/log/main/.gitstub [new file with mode: 0644]
tools/arvbox/lib/arvbox/docker/service/webshell/log/run [new symlink]
tools/arvbox/lib/arvbox/docker/service/webshell/run [new file with mode: 0755]
tools/arvbox/lib/arvbox/docker/service/webshell/run-service [new file with mode: 0755]

index e2ce5b39bc1c7e47373c6c8285d8abd8349428d7..57b4d6aa380daed239f919df72cc09daf35cf1f6 100644 (file)
@@ -69,7 +69,7 @@ SPDX-License-Identifier: AGPL-3.0 %>
           <th> Login name </th>
           <th> Command line </th>
           <% if Rails.configuration.Services.WebShell.ExternalURL != URI("") %>
-            <th> Web shell <span class="label label-info">beta</span></th>
+            <th> Web shell</th>
           <% end %>
         </tr>
       </thead>
index 4c63115a1669cb389cf9c97d77fa6fef75a056b3..735583faec8efd2608843be11272bb817ec0ed99 100644 (file)
@@ -30,17 +30,43 @@ SPDX-License-Identifier: AGPL-3.0 %>
 
       function login(username, token) {
         var sh = new ShellInABox("<%= j @webshell_url %>");
-        setTimeout(function() {
-          sh.keysPressed("<%= j params[:login] %>\n");
-          setTimeout(function() {
-            sh.keysPressed("<%= j Thread.current[:arvados_api_token] %>\n");
-            sh.vt100('(sent authentication token)\n');
-          }, 2000);
-        }, 2000);
+
+        var findText = function(txt) {
+          var a = document.querySelectorAll("span.ansi0");
+          for (var i = 0; i < a.length; i++) {
+            if (a[i].textContent.indexOf(txt) > -1) {
+              return true;
+            }
+          }
+          return false;
+        }
+
+        var trySendToken = function() {
+          // change this text when PAM is reconfigured to present a
+          // password prompt that we can wait for.
+          if (findText("assword:")) {
+             sh.keysPressed("<%= j Thread.current[:arvados_api_token] %>\n");
+             sh.vt100('(sent authentication token)\n');
+          } else {
+            setTimeout(trySendToken, 200);
+          }
+        };
+
+        var trySendLogin = function() {
+          if (findText("login:")) {
+            sh.keysPressed("<%= j params[:login] %>\n");
+            // Make this wait shorter when PAM is reconfigured to
+            // present a password prompt that we can wait for.
+            setTimeout(trySendToken, 200);
+          } else {
+            setTimeout(trySendLogin, 200);
+          }
+        };
+
+        trySendLogin();
       }
     // -->
 </script>
-    <link rel="icon" href="<%= asset_path 'favicon.ico' %>" type="image/x-icon">
     <script type="text/javascript" src="<%= asset_path 'webshell/shell_in_a_box.js' %>"></script>
   </head>
   <!-- Load ShellInABox from a timer as Konqueror sometimes fails to
index f02c87b73143fc0e01427ca2ff56e198c5cd2611..2cb9ae908c88ed7e333d16e34864b8cab1567ef0 100644 (file)
@@ -12,4 +12,4 @@ Rails.application.config.assets.version = '1.0'
 
 # Precompile additional assets.
 # application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
-# Rails.application.config.assets.precompile += %w( search.js )
+Rails.application.config.assets.precompile += %w( webshell/styles.css webshell/shell_in_a_box.js )
similarity index 99%
rename from apps/workbench/public/webshell/shell_in_a_box.js
rename to apps/workbench/lib/assets/javascripts/webshell/shell_in_a_box.js
index 0c7e800ef8e71aa308fb8d2af6f51b0498d0e8ac..1002f7a9f8846bc39f53a3c559de7bea2dab6e5d 100644 (file)
@@ -1,3 +1,7 @@
+// Copyright (C) 2008-2010 Markus Gutschke <markus@shellinabox.com> All rights reserved.
+//
+// SPDX-License-Identifier: GPL-2.0
+
 // This file contains code from shell_in_a_box.js and vt100.js
 
 
@@ -363,7 +367,7 @@ ShellInABox.prototype.extendContextMenu = function(entries, actions) {
       }
     }
   }
-  
+
 };
 
 ShellInABox.prototype.about = function() {
@@ -738,7 +742,7 @@ VT100.prototype.initializeUserCSSStyles = function() {
         var label                        = userCSSList[i][0];
         var newGroup                     = userCSSList[i][1];
         var enabled                      = userCSSList[i][2];
-      
+
         // Add user style sheet to document
         var style                        = document.createElement('link');
         var id                           = document.createAttribute('id');
@@ -756,7 +760,7 @@ VT100.prototype.initializeUserCSSStyles = function() {
         document.getElementsByTagName('head')[0].appendChild(style);
         style.disabled                   = !enabled;
       }
-    
+
       // Add entry to menu
       if (newGroup || i == userCSSList.length) {
         if (beginOfGroup != 0 && (i - beginOfGroup > 1 || !wasSingleSel)) {
@@ -963,7 +967,7 @@ VT100.prototype.addKeyBinding = function(elem, ch, key, CH, KEY) {
   this.addListener(elem, 'mousedown',
     function(vt100, elem, key) { return function(e) {
       if ((e.which || e.button) == 1) {
-        if (vt100.lastSelectedKey) {       
+        if (vt100.lastSelectedKey) {
           vt100.lastSelectedKey.className= '';
         }
         // Highlight the key while the mouse button is held down.
@@ -1364,7 +1368,7 @@ VT100.prototype.initializeElements = function(container) {
         vt100.indicateSize     = true;
       };
     }(this), 100);
-    this.addListener(window, 'resize', 
+    this.addListener(window, 'resize',
                      function(vt100) {
                        return function() {
                          vt100.hideContextMenu();
@@ -1372,7 +1376,7 @@ VT100.prototype.initializeElements = function(container) {
                          vt100.showCurrentSize();
                         }
                       }(this));
-    
+
     // Hide extra scrollbars attached to window
     document.body.style.margin = '0px';
     try { document.body.style.overflow ='hidden'; } catch (e) { }
@@ -1447,7 +1451,7 @@ VT100.prototype.initializeElements = function(container) {
       // Add a listener for the drop event
       this.addListener(this.scrollable, 'drop', dropEvent(this));
   }
-  
+
   // Initialize the blank terminal window.
   this.currentScreen           = 0;
   this.cursorX                 = 0;
@@ -1514,7 +1518,7 @@ VT100.prototype.repairElements = function(console) {
         for (var span = line.firstChild; span; span = span.nextSibling) {
           var newSpan             = document.createElement(span.tagName);
           newSpan.style.cssText   = span.style.cssText;
-          newSpan.className      = span.className;
+          newSpan.className       = span.className;
           this.setTextContent(newSpan, this.getTextContent(span));
           newLine.appendChild(newSpan);
         }
@@ -1925,7 +1929,7 @@ VT100.prototype.insertBlankLine = function(y, color, style) {
     line                 = document.createElement('div');
     var span             = document.createElement('span');
     span.style.cssText   = style;
-    span.className      = color;
+    span.className       = color;
     this.setTextContent(span, this.spaces(this.terminalWidth));
     line.appendChild(span);
   }
@@ -2062,7 +2066,7 @@ VT100.prototype.putString = function(x, y, text, color, style) {
       this.insertBlankLine(yIdx);
     }
     line                            = console.childNodes[yIdx];
-    
+
     // If necessary, promote blank '\n' line to a <div> tag
     if (line.tagName != 'DIV') {
       var div                       = document.createElement('div');
@@ -2106,7 +2110,7 @@ VT100.prototype.putString = function(x, y, text, color, style) {
           s                        += ' ';
         } while (xPos + s.length < x);
       }
-    
+
       // If styles do not match, create a new <span>
       var del                       = text.length - s.length + x - xPos;
       if (oldColor != color ||
@@ -2165,7 +2169,7 @@ VT100.prototype.putString = function(x, y, text, color, style) {
       }
       this.setTextContent(span, s);
 
-      
+
       // Delete all subsequent <span>'s that have just been overwritten
       sibling                       = span.nextSibling;
       while (del > 0 && sibling) {
@@ -2180,7 +2184,7 @@ VT100.prototype.putString = function(x, y, text, color, style) {
           break;
         }
       }
-      
+
       // Merge <span> with next sibling, if styles are identical
       if (sibling && span.className == sibling.className &&
           span.style.cssText == sibling.style.cssText) {
@@ -2261,7 +2265,7 @@ VT100.prototype.putString = function(x, y, text, color, style) {
                           this.getTextContent(span));
       line.removeChild(sibling);
     }
-    
+
     // Prune white space from the end of the current line
     span                            = line.lastChild;
     while (span &&
@@ -2342,7 +2346,7 @@ VT100.prototype.enableAlternateScreen = function(state) {
     this.resizer();
     return;
   }
-  
+
   // We save the full state of the normal screen, when we switch away from it.
   // But for the alternate screen, no saving is necessary. We always reset
   // it when we switch to it.
@@ -2588,7 +2592,7 @@ VT100.prototype.scrollRegion = function(x, y, w, h, incX, incY,
           while (console.childNodes.length < this.terminalHeight) {
             this.insertBlankLine(this.terminalHeight);
           }
-          
+
           // Add new lines at bottom in order to force scrolling
           for (var i = 0; i < y; i++) {
             this.insertBlankLine(console.childNodes.length, color, style);
@@ -2947,7 +2951,7 @@ VT100.prototype.showContextMenu = function(x, y) {
   this.menu.style.height      =  this.container.offsetHeight + 'px';
   popup.style.left            = '0px';
   popup.style.top             = '0px';
-  
+
   var margin                  = 2;
   if (x + popup.clientWidth >= this.container.offsetWidth - margin) {
     x              = this.container.offsetWidth-popup.clientWidth - margin - 1;
@@ -3035,7 +3039,7 @@ VT100.prototype.handleKey = function(event) {
   ch                                  = this.applyModifiers(ch, event);
 
   // By this point, "ch" is either defined and contains the character code, or
-  // it is undefined and "key" defines the code of a function key 
+  // it is undefined and "key" defines the code of a function key
   if (ch != undefined) {
     this.scrollable.scrollTop         = this.numScrollbackLines *
                                         this.cursorHeight + 1;
@@ -3260,7 +3264,7 @@ VT100.prototype.fixEvent = function(event) {
     case  61: /* = -> + */ u = 61; s =  43; break;
     case  91: /* [ -> { */ u = 91; s = 123; break;
     case  92: /* \ -> | */ u = 92; s = 124; break;
-    case  93: /* ] -> } */ u = 93; s = 125; break; 
+    case  93: /* ] -> } */ u = 93; s = 125; break;
     case  96: /* ` -> ~ */ u = 96; s = 126; break;
 
     case 109: /* - -> _ */ u = 45; s =  95; break;
@@ -3276,7 +3280,7 @@ VT100.prototype.fixEvent = function(event) {
     case 192: /* ` -> ~ */ u = 96; s = 126; break;
     case 219: /* [ -> { */ u = 91; s = 123; break;
     case 220: /* \ -> | */ u = 92; s = 124; break;
-    case 221: /* ] -> } */ u = 93; s = 125; break; 
+    case 221: /* ] -> } */ u = 93; s = 125; break;
     case 222: /* ' -> " */ u = 39; s =  34; break;
     default:                                break;
     }
@@ -3989,7 +3993,7 @@ VT100.prototype.sendControlToPrinter = function(ch) {
           break;
         }
         // Fall through
-      case 3 /* ESgetpars */: 
+      case 3 /* ESgetpars */:
         if (ch == 0x3B /*;*/) {
           this.npar++;
           break;
@@ -4351,7 +4355,7 @@ VT100.prototype.doControl = function(ch) {
       }
       // Fall through
     case 5 /* ESdeviceattr */:
-    case 3 /* ESgetpars */: 
+    case 3 /* ESgetpars */:
 /*;*/ if (ch == 0x3B) {
         this.npar++;
         break;
@@ -4626,7 +4630,7 @@ VT100.prototype.vt100 = function(s) {
        this.utfEnabled && ch >= 128 ||
        !(this.dispCtrl ? this.ctrlAlways : this.ctrlAction)[ch & 0x1F]) &&
       (ch != 0x7F || this.dispCtrl);
-    
+
     if (isNormalCharacter && this.isEsc == 0 /* ESnormal */) {
       if (ch < 256) {
         ch                = this.translate[this.toggleMeta ? (ch | 0x80) : ch];
@@ -4831,5 +4835,3 @@ VT100.prototype.ctrlAlways = [
   false, false, false, false, false, false, false, false,
   false, false, false, true,  false, false, false, false
 ];
-
-
similarity index 93%
rename from apps/workbench/public/webshell/styles.css
rename to apps/workbench/lib/assets/stylesheets/webshell/styles.css
index 3097cb45bf645893f8210d47cff5a5968151fb65..1fc8a67046550ece2da9e5ee079b92136cf6cc02 100644 (file)
@@ -1,9 +1,13 @@
-#vt100 a { 
+/* Copyright (C) 2008-2010 Markus Gutschke <markus@shellinabox.com> All rights reserved.
+   SPDX-License-Identifier: GPL-2.0
+*/
+
+#vt100 a {
   text-decoration:      none;
   color:                inherit;
 }
 
-#vt100 a:hover { 
+#vt100 a:hover {
   text-decoration:      underline;
 }
 
@@ -12,7 +16,7 @@
   z-index:              2;
 }
 
-#vt100 #reconnect input { 
+#vt100 #reconnect input {
   padding:              1ex;
   font-weight:          bold;
   font-size:            x-large;
@@ -29,7 +33,7 @@
   z-index:              2;
 }
 
-#vt100 pre { 
+#vt100 pre {
   margin:               0px;
 }
 
   padding:              1px;
 }
 
-#vt100 #console, #vt100 #alt_console, #vt100 #cursor, #vt100 #lineheight, #vt100 .hidden pre { 
+#vt100 #console, #vt100 #alt_console, #vt100 #cursor, #vt100 #lineheight, #vt100 .hidden pre {
   font-family:          "DejaVu Sans Mono", "Everson Mono", FreeMono, "Andale Mono", monospace;
 }
 
-#vt100 #lineheight { 
+#vt100 #lineheight {
   position:             absolute;
   visibility:           hidden;
 }
@@ -75,7 +79,7 @@
   margin:               -1px;
 }
 
-#vt100 #padding { 
+#vt100 #padding {
   visibility:           hidden;
   width:                1px;
   height:               0px;
@@ -90,7 +94,7 @@
   height:               0px;
 }
 
-#vt100 #menu { 
+#vt100 #menu {
   overflow:             visible;
   position:             absolute;
   z-index:              3;
   position:             absolute;
 }
 
-#vt100 #menu .popup ul { 
+#vt100 #menu .popup ul {
   list-style-type:      none;
   padding:              0px;
   margin:               0px;
   min-width:            10em;
 }
 
-#vt100 #menu .popup li { 
+#vt100 #menu .popup li {
   padding:              3px 0.5ex 3px 0.5ex;
 }
 
   color:                #AAAAAA;
 }
 
-#vt100 #menu .popup hr { 
+#vt100 #menu .popup hr {
   margin:               0.5ex 0px 0.5ex 0px;
 }
 
-#vt100 #menu img { 
+#vt100 #menu img {
   margin-right:         0.5ex;
   width:                1ex;
   height:               1ex;
 #vt100 #scrollable.inverted { color:            #ffffff;
                               background-color: #000000; }
 
-#vt100 #kbd_button { 
+#vt100 #kbd_button {
   float:                left;
   position:             fixed;
   z-index:              0;
   visibility:           hidden;
 }
 
-#vt100 #keyboard .shifted { 
+#vt100 #keyboard .shifted {
   display:              none;
 }
 
     display:            none;
   }
 
-  #vt100 #reconnect, #vt100 #cursor, #vt100 #menu, #vt100 #kbd_button, #vt100 #keyboard { 
+  #vt100 #reconnect, #vt100 #cursor, #vt100 #menu, #vt100 #kbd_button, #vt100 #keyboard {
     visibility:         hidden;
   }
 
-  #vt100 #scrollable { 
+  #vt100 #scrollable {
     overflow:           hidden;
   }
 
-  #vt100 #console, #vt100 #alt_console { 
+  #vt100 #console, #vt100 #alt_console {
     overflow:           hidden;
     width:              1000000ex;
   }
index 2aa494ae9fab3d123dccf585de1678b090587c83..0aeabab11bea1db943c031c9409d1ab6b693b50f 100644 (file)
@@ -19,7 +19,7 @@ Webshell gives you access to an arvados virtual machine from your browser with n
 Some Arvados clusters may not have webshell set up.  If you do not see a "Log in" button or "web shell" column, you will have to follow the "Unix":ssh-access-unix.html or "Windows":ssh-access-windows.html @ssh@ instructions.
 {% include 'notebox_end' %}
 
-In the Arvados Workbench, click on the dropdown menu icon <span class="fa fa-lg fa-user"></span> <span class="caret"></span> in the upper right corner of the top navigation menu to access the user settings menu, and click on the menu item *Virtual machines* to see the list of virtual machines you can access.  If you do not have access to any virtual machines, please click on <span class="btn btn-sm btn-primary">Send request for shell access</span> or send an email to "support@curoverse.com":mailto:support@curoverse.com.
+In the Arvados Workbench, click on the dropdown menu icon <span class="fa fa-lg fa-user"></span> <span class="caret"></span> in the upper right corner of the top navigation menu to access the user settings menu, and click on the menu item *Virtual machines* to see the list of virtual machines you can access.  If you do not have access to any virtual machines,  please click on <span class="btn btn-sm btn-primary">Send request for shell access</span> (if present) or contact your system administrator.  For the Arvados Playground, this is "info@curii.com":mailto:info@curii.com .
 
 Each row in the Virtual Machines panel lists the hostname of the VM, along with a <code>Log in as *you*</code> button under the column "Web shell". Clicking on this button will open up a webshell terminal for you in a new browser tab and log you in.
 
index e8f76b626099af5f2d6412d9f0041b465ba70bfc..644cf7d2086967b057309d37ae733c782114f724 100644 (file)
@@ -15,11 +15,17 @@ This guide covers the classic Arvados Workbench web application, sometimes refer
 This guide will be updated to cover "Workbench 2" in the future.
 {% include 'notebox_end' %}
 
-If you are using the "playground" Arvados instance for this guide, you can Access Arvados Workbench using this link:
+You can access the Arvados Workbench used in this guide using this link:
 
-<a href="{{site.arvados_workbench_host}}/" target="_blank">{{site.arvados_workbench_host}}/</a>
+<a href="{{site.arvados_workbench_host}}/" target="_blank">{{site.arvados_workbench_host}}</a>
 
-(If you are using a different Arvados instance than the default for this guide, replace *{{ site.arvados_workbench_host }}* with your private instance in all of the examples in this guide.)
+If you are using a different Arvados instance replace @{{ site.arvados_workbench_host }}@ with your private instance in all of the examples in this guide.
+
+h2. Playground
+
+Curii operates a public demonstration instance of Arvados called the Arvados Playground, which can be found at <a href="https://playground.arvados.org" target="_blank">https://playground.arvados.org</a> .  Some examples in this guide involve getting data from the Playground instance.
+
+h2. Logging in
 
 You will be asked to log in.  Arvados uses only your name and email address for identification, and will never access any personal information.  If you are accessing Arvados for the first time, the Workbench may indicate your account status is *New / inactive*.  If this is the case, contact the administrator of the Arvados instance to request activation of your account.
 
index 9749d1f284bbdb27d8b828ff0e7e66c6cf798e07..4b0a443d3ce03d6c12c85ba9e78438a8429c9a96 100644 (file)
@@ -16,7 +16,7 @@ Arvados is an open source platform for managing, processing, and sharing genomic
 * Accessing, organizing, and sharing data, workflows and results using the "Arvados Workbench":{{site.baseurl}}/user/getting_started/workbench.html web application.
 * Running an analysis using multiple clusters (HPC, cloud, or hybrid) with "Federated Multi-Cluster Workflows":{{site.baseurl}}/user/cwl/federated-workflows.html .
 
-The examples in this guide use the public Arvados instance located at <a href="{{site.arvados_workbench_host}}/" target="_blank">{{site.arvados_workbench_host}}</a>.  If you are using a different Arvados instance replace @{{ site.arvados_workbench_host }}@ with your private instance in all of the examples in this guide.
+The examples in this guide use the Arvados instance located at <a href="{{site.arvados_workbench_host}}/" target="_blank">{{site.arvados_workbench_host}}</a>.  If you are using a different Arvados instance replace @{{ site.arvados_workbench_host }}@ with your private instance in all of the examples in this guide.
 
 h2. Typographic conventions
 
index 60938341798a100064e066ee8f78c686e2e953f5..5288b5c76cd2b3f0d3cd87c78f76f6706889e8f7 100644 (file)
@@ -127,7 +127,7 @@ func (t *tester) Run() bool {
        defer t.destroyTestInstance()
 
        bootDeadline := time.Now().Add(t.TimeoutBooting)
-       initCommand := worker.TagVerifier{nil, t.secret}.InitCommand()
+       initCommand := worker.TagVerifier{Instance: nil, Secret: t.secret, ReportVerified: nil}.InitCommand()
 
        t.Logger.WithFields(logrus.Fields{
                "InstanceType":         t.InstanceType.Name,
@@ -160,7 +160,7 @@ func (t *tester) Run() bool {
                // Create() succeeded. Make sure the new instance
                // appears right away in the Instances() list.
                lgrC.WithField("Instance", inst.ID()).Info("created instance")
-               t.testInstance = &worker.TagVerifier{inst, t.secret}
+               t.testInstance = &worker.TagVerifier{Instance: inst, Secret: t.secret, ReportVerified: nil}
                t.showLoginInfo()
                err = t.refreshTestInstance()
                if err == errTestInstanceNotFound {
@@ -236,7 +236,7 @@ func (t *tester) refreshTestInstance() error {
                        "Instance": i.ID(),
                        "Address":  i.Address(),
                }).Info("found our instance in returned list")
-               t.testInstance = &worker.TagVerifier{i, t.secret}
+               t.testInstance = &worker.TagVerifier{Instance: i, Secret: t.secret, ReportVerified: nil}
                if !t.showedLoginInfo {
                        t.showLoginInfo()
                }
index 42decff31d0cada0bb83ce66ba509aa5f5d13448..6e1850410b28bf3394ec4e29c4416a9551ec6d91 100644 (file)
@@ -215,6 +215,12 @@ func (s *DispatcherSuite) TestDispatchToStubDriver(c *check.C) {
        c.Check(resp.Body.String(), check.Matches, `(?ms).*boot_outcomes{outcome="success"} [^0].*`)
        c.Check(resp.Body.String(), check.Matches, `(?ms).*instances_disappeared{state="shutdown"} [^0].*`)
        c.Check(resp.Body.String(), check.Matches, `(?ms).*instances_disappeared{state="unknown"} 0\n.*`)
+       c.Check(resp.Body.String(), check.Matches, `(?ms).*time_to_ssh_seconds{quantile="0.95"} [0-9.]*`)
+       c.Check(resp.Body.String(), check.Matches, `(?ms).*time_to_ssh_seconds_count [0-9]*`)
+       c.Check(resp.Body.String(), check.Matches, `(?ms).*time_to_ssh_seconds_sum [0-9.]*`)
+       c.Check(resp.Body.String(), check.Matches, `(?ms).*time_to_ready_for_container_seconds{quantile="0.95"} [0-9.]*`)
+       c.Check(resp.Body.String(), check.Matches, `(?ms).*time_to_ready_for_container_seconds_count [0-9]*`)
+       c.Check(resp.Body.String(), check.Matches, `(?ms).*time_to_ready_for_container_seconds_sum [0-9.]*`)
 }
 
 func (s *DispatcherSuite) TestAPIPermissions(c *check.C) {
index 435b6e43ae4a3e150f680883aa7e124daf6e4230..086887cb44176f05c9446341a68d7176dd5ed7aa 100644 (file)
@@ -170,13 +170,15 @@ type Pool struct {
        runnerMD5    [md5.Size]byte
        runnerCmd    string
 
-       mContainersRunning prometheus.Gauge
-       mInstances         *prometheus.GaugeVec
-       mInstancesPrice    *prometheus.GaugeVec
-       mVCPUs             *prometheus.GaugeVec
-       mMemory            *prometheus.GaugeVec
-       mBootOutcomes      *prometheus.CounterVec
-       mDisappearances    *prometheus.CounterVec
+       mContainersRunning       prometheus.Gauge
+       mInstances               *prometheus.GaugeVec
+       mInstancesPrice          *prometheus.GaugeVec
+       mVCPUs                   *prometheus.GaugeVec
+       mMemory                  *prometheus.GaugeVec
+       mBootOutcomes            *prometheus.CounterVec
+       mDisappearances          *prometheus.CounterVec
+       mTimeToSSH               prometheus.Summary
+       mTimeToReadyForContainer prometheus.Summary
 }
 
 type createCall struct {
@@ -323,7 +325,7 @@ func (wp *Pool) Create(it arvados.InstanceType) bool {
                        wp.tagKeyPrefix + tagKeyIdleBehavior:   string(IdleBehaviorRun),
                        wp.tagKeyPrefix + tagKeyInstanceSecret: secret,
                }
-               initCmd := TagVerifier{nil, secret}.InitCommand()
+               initCmd := TagVerifier{nil, secret, nil}.InitCommand()
                inst, err := wp.instanceSet.Create(it, wp.imageID, tags, initCmd, wp.installPublicKey)
                wp.mtx.Lock()
                defer wp.mtx.Unlock()
@@ -367,6 +369,23 @@ func (wp *Pool) SetIdleBehavior(id cloud.InstanceID, idleBehavior IdleBehavior)
        return nil
 }
 
+// Successful connection to the SSH daemon, update the mTimeToSSH metric
+func (wp *Pool) reportSSHConnected(inst cloud.Instance) {
+       wp.mtx.Lock()
+       defer wp.mtx.Unlock()
+       wkr := wp.workers[inst.ID()]
+       if wkr.state != StateBooting || !wkr.firstSSHConnection.IsZero() {
+               // the node is not in booting state (can happen if a-d-c is restarted) OR
+               // this is not the first SSH connection
+               return
+       }
+
+       wkr.firstSSHConnection = time.Now()
+       if wp.mTimeToSSH != nil {
+               wp.mTimeToSSH.Observe(wkr.firstSSHConnection.Sub(wkr.appeared).Seconds())
+       }
+}
+
 // Add or update worker attached to the given instance.
 //
 // The second return value is true if a new worker is created.
@@ -377,7 +396,7 @@ func (wp *Pool) SetIdleBehavior(id cloud.InstanceID, idleBehavior IdleBehavior)
 // Caller must have lock.
 func (wp *Pool) updateWorker(inst cloud.Instance, it arvados.InstanceType) (*worker, bool) {
        secret := inst.Tags()[wp.tagKeyPrefix+tagKeyInstanceSecret]
-       inst = TagVerifier{inst, secret}
+       inst = TagVerifier{Instance: inst, Secret: secret, ReportVerified: wp.reportSSHConnected}
        id := inst.ID()
        if wkr := wp.workers[id]; wkr != nil {
                wkr.executor.SetTarget(inst)
@@ -626,6 +645,22 @@ func (wp *Pool) registerMetrics(reg *prometheus.Registry) {
                wp.mDisappearances.WithLabelValues(v).Add(0)
        }
        reg.MustRegister(wp.mDisappearances)
+       wp.mTimeToSSH = prometheus.NewSummary(prometheus.SummaryOpts{
+               Namespace:  "arvados",
+               Subsystem:  "dispatchcloud",
+               Name:       "instances_time_to_ssh_seconds",
+               Help:       "Number of seconds between instance creation and the first successful SSH connection.",
+               Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
+       })
+       reg.MustRegister(wp.mTimeToSSH)
+       wp.mTimeToReadyForContainer = prometheus.NewSummary(prometheus.SummaryOpts{
+               Namespace:  "arvados",
+               Subsystem:  "dispatchcloud",
+               Name:       "instances_time_to_ready_for_container_seconds",
+               Help:       "Number of seconds between the first successful SSH connection and ready to run a container.",
+               Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.95: 0.005, 0.99: 0.001},
+       })
+       reg.MustRegister(wp.mTimeToReadyForContainer)
 }
 
 func (wp *Pool) runMetrics() {
index 597950fca699a9834795dfbf25f6957cd9fdc92b..559bb28973d27fc81662a262a00cefe5e020627c 100644 (file)
@@ -23,7 +23,8 @@ var (
 
 type TagVerifier struct {
        cloud.Instance
-       Secret string
+       Secret         string
+       ReportVerified func(cloud.Instance)
 }
 
 func (tv TagVerifier) InitCommand() cloud.InitCommand {
@@ -31,6 +32,9 @@ func (tv TagVerifier) InitCommand() cloud.InitCommand {
 }
 
 func (tv TagVerifier) VerifyHostKey(pubKey ssh.PublicKey, client *ssh.Client) error {
+       if tv.ReportVerified != nil {
+               tv.ReportVerified(tv.Instance)
+       }
        if err := tv.Instance.VerifyHostKey(pubKey, client); err != cloud.ErrNotImplemented || tv.Secret == "" {
                // If the wrapped instance indicates it has a way to
                // verify the key, return that decision.
index 5d2360f3ccc64671b7193b281a7807d7b70de23b..9199d4bafe764d806312638328cf13fd3b422e4d 100644 (file)
@@ -103,11 +103,13 @@ type worker struct {
        updated             time.Time
        busy                time.Time
        destroyed           time.Time
+       firstSSHConnection  time.Time
        lastUUID            string
        running             map[string]*remoteRunner // remember to update state idle<->running when this changes
        starting            map[string]*remoteRunner // remember to update state idle<->running when this changes
        probing             chan struct{}
        bootOutcomeReported bool
+       timeToReadyReported bool
 }
 
 func (wkr *worker) onUnkillable(uuid string) {
@@ -140,6 +142,17 @@ func (wkr *worker) reportBootOutcome(outcome BootOutcome) {
        wkr.bootOutcomeReported = true
 }
 
+// caller must have lock.
+func (wkr *worker) reportTimeBetweenFirstSSHAndReadyForContainer() {
+       if wkr.timeToReadyReported {
+               return
+       }
+       if wkr.wp.mTimeToSSH != nil {
+               wkr.wp.mTimeToReadyForContainer.Observe(time.Since(wkr.firstSSHConnection).Seconds())
+       }
+       wkr.timeToReadyReported = true
+}
+
 // caller must have lock.
 func (wkr *worker) setIdleBehavior(idleBehavior IdleBehavior) {
        wkr.logger.WithField("IdleBehavior", idleBehavior).Info("set idle behavior")
@@ -313,6 +326,9 @@ func (wkr *worker) probeAndUpdate() {
 
        // Update state if this was the first successful boot-probe.
        if booted && (wkr.state == StateUnknown || wkr.state == StateBooting) {
+               if wkr.state == StateBooting {
+                       wkr.reportTimeBetweenFirstSSHAndReadyForContainer()
+               }
                // Note: this will change again below if
                // len(wkr.starting)+len(wkr.running) > 0.
                wkr.state = StateIdle
index 2d930c5e6d9bb941814e3e3751bb9edb980cf61b..279d46c08b4ee98a652d78b88f949f1587453d8a 100755 (executable)
@@ -206,6 +206,7 @@ run() {
               --publish=25101:25101
               --publish=8001:8001
               --publish=8002:8002
+              --publish=4202:4202
              --publish=45000-45020:45000-45020"
     else
         PUBLIC=""
index c5c3774a963f74063e1bbb0c413bee5a10d57d9e..bde5ffe89826c3af5d1e0a8f6bd1e20b3062b842 100644 (file)
@@ -21,7 +21,7 @@ RUN apt-get update && \
     libgnutls28-dev python3-dev vim cadaver cython gnupg dirmngr \
     libsecret-1-dev r-base r-cran-testthat libxml2-dev pandoc \
     python3-setuptools python3-pip openjdk-8-jdk bsdmainutils net-tools \
-    ruby2.3 ruby-dev bundler && \
+    ruby2.3 ruby-dev bundler shellinabox && \
     apt-get clean
 
 ENV RUBYVERSION_MINOR 2.3
index db17780925e8ce328c1fb8272dd61645a9d726aa..bebf983b6bf07dc1af6748b4583fd8d17b43e86c 100755 (executable)
@@ -110,6 +110,9 @@ Clusters:
         ExternalURL: "https://$localip:${services[controller-ssl]}"
         InternalURLs:
           "http://localhost:${services[controller]}": {}
+      WebShell:
+        InternalURLs: {}
+        ExternalURL: "https://$localip:${services[webshell-ssl]}"
     PostgreSQL:
       ConnectionPool: 32 # max concurrent connections per arvados server daemon
       Connection:
index 05491c5361ae45ee89879366091eef9bcc2a44b1..e81e8108e249fc7215c4ca19c4e25a4bb81efbd9 100644 (file)
@@ -46,6 +46,8 @@ services=(
   [doc]=8001
   [websockets]=8005
   [websockets-ssl]=8002
+  [webshell]=4201
+  [webshell-ssl]=4202
 )
 
 if test "$(id arvbox -u 2>/dev/null)" = 0 ; then
index d6fecb4436069e431f80682af5baf66f0d04bf82..cfb7788defc080986bb91fa0e5f1e9b871b27c5f 100755 (executable)
@@ -186,6 +186,51 @@ http {
     }
   }
 
+
+upstream arvados-webshell {
+  server                localhost:${services[webshell]};
+}
+server {
+  listen                ${services[webshell-ssl]} ssl;
+  server_name           arvados-webshell;
+
+  proxy_connect_timeout 90s;
+  proxy_read_timeout    300s;
+
+  ssl                   on;
+  ssl_certificate "${server_cert}";
+  ssl_certificate_key "${server_cert_key}";
+
+  location / {
+    if (\$request_method = 'OPTIONS') {
+       add_header 'Access-Control-Allow-Origin' '*';
+       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+       add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+       add_header 'Access-Control-Max-Age' 1728000;
+       add_header 'Content-Type' 'text/plain charset=UTF-8';
+       add_header 'Content-Length' 0;
+       return 204;
+    }
+    if (\$request_method = 'POST') {
+       add_header 'Access-Control-Allow-Origin' '*';
+       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+       add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+    }
+    if (\$request_method = 'GET') {
+       add_header 'Access-Control-Allow-Origin' '*';
+       add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
+       add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
+    }
+
+    proxy_ssl_session_reuse off;
+    proxy_read_timeout  90;
+    proxy_set_header    X-Forwarded-Proto https;
+    proxy_set_header    Host \$http_host;
+    proxy_set_header    X-Real-IP \$remote_addr;
+    proxy_set_header    X-Forwarded-For \$proxy_add_x_forwarded_for;
+    proxy_pass          http://arvados-webshell;
+  }
+}
 }
 
 EOF
diff --git a/tools/arvbox/lib/arvbox/docker/service/webshell/log/main/.gitstub b/tools/arvbox/lib/arvbox/docker/service/webshell/log/main/.gitstub
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tools/arvbox/lib/arvbox/docker/service/webshell/log/run b/tools/arvbox/lib/arvbox/docker/service/webshell/log/run
new file mode 120000 (symlink)
index 0000000..d6aef4a
--- /dev/null
@@ -0,0 +1 @@
+/usr/local/lib/arvbox/logger
\ No newline at end of file
diff --git a/tools/arvbox/lib/arvbox/docker/service/webshell/run b/tools/arvbox/lib/arvbox/docker/service/webshell/run
new file mode 100755 (executable)
index 0000000..c2bf42f
--- /dev/null
@@ -0,0 +1,43 @@
+#!/bin/bash
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+exec 2>&1
+set -ex -o pipefail
+
+. /usr/local/lib/arvbox/common.sh
+
+/usr/local/lib/arvbox/runsu.sh $0-service
+
+cat > /etc/pam.d/shellinabox <<EOF
+# This example is a stock debian "login" file with pam_arvados
+# replacing pam_unix. It can be installed as /etc/pam.d/shellinabox .
+
+auth       optional   pam_faildelay.so  delay=3000000
+auth [success=ok new_authtok_reqd=ok ignore=ignore user_unknown=bad default=die] pam_securetty.so
+auth       requisite  pam_nologin.so
+session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so close
+session       required   pam_env.so readenv=1
+session       required   pam_env.so readenv=1 envfile=/etc/default/locale
+
+auth [success=1 default=ignore] /usr/local/lib/pam_arvados.so $localip:${services[controller-ssl]} $localip
+auth    requisite            pam_deny.so
+auth    required            pam_permit.so
+
+auth       optional   pam_group.so
+session    required   pam_limits.so
+session    optional   pam_lastlog.so
+session    optional   pam_motd.so  motd=/run/motd.dynamic
+session    optional   pam_motd.so
+session    optional   pam_mail.so standard
+
+@include common-account
+@include common-session
+@include common-password
+
+session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux.so open
+EOF
+
+exec shellinaboxd --verbose --port ${services[webshell]} --user arvbox --group arvbox \
+                  --disable-ssl --no-beep --service=/$localip:AUTH:HOME:SHELL
\ No newline at end of file
diff --git a/tools/arvbox/lib/arvbox/docker/service/webshell/run-service b/tools/arvbox/lib/arvbox/docker/service/webshell/run-service
new file mode 100755 (executable)
index 0000000..92b0c3d
--- /dev/null
@@ -0,0 +1,13 @@
+#!/bin/bash
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+exec 2>&1
+set -ex -o pipefail
+
+. /usr/local/lib/arvbox/common.sh
+. /usr/local/lib/arvbox/go-setup.sh
+
+flock /var/lib/gopath/gopath.lock go build -buildmode=c-shared -o ${GOPATH}/bin/pam_arvados.so git.arvados.org/arvados.git/lib/pam
+install $GOPATH/bin/pam_arvados.so /usr/local/lib
\ No newline at end of file