16421: Merge branch 'master' into 16421-document-collection-deletion-lifecycle
authorWard Vandewege <ward@curii.com>
Tue, 8 Sep 2020 15:41:04 +0000 (11:41 -0400)
committerWard Vandewege <ward@curii.com>
Tue, 8 Sep 2020 15:41:19 +0000 (11:41 -0400)
Arvados-DCO-1.1-Signed-off-by: Ward Vandewege <ward@curii.com>

50 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/install/crunch2-cloud/install-dispatch-cloud.html.textile.liquid
doc/install/install-shell-server.html.textile.liquid
doc/install/install-webshell.html.textile.liquid
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
docker/jobs/Dockerfile
lib/boot/seed.go
lib/boot/supervisor.go
lib/cloud/cloudtest/tester.go
lib/config/cmd.go
lib/config/config.default.yml
lib/config/export.go
lib/config/generated_config.go
lib/controller/federation/conn.go
lib/controller/integration_test.go
lib/controller/semaphore.go
lib/crunchrun/copier.go
lib/dispatchcloud/dispatcher_test.go
lib/dispatchcloud/scheduler/run_queue.go
lib/dispatchcloud/scheduler/run_queue_test.go
lib/dispatchcloud/test/stub_driver.go
lib/dispatchcloud/worker/pool.go
lib/dispatchcloud/worker/pool_test.go
lib/dispatchcloud/worker/verify.go
lib/dispatchcloud/worker/worker.go
sdk/go/arvados/config.go
sdk/go/blockdigest/blockdigest.go
sdk/python/setup.py
services/api/app/models/api_client_authorization.rb
services/api/script/get_anonymous_user_token.rb
services/api/test/integration/remote_user_test.rb
services/keep-web/s3.go
services/keep-web/s3_test.go
services/login-sync/bin/arvados-login-sync
services/login-sync/test/test_add_user.rb
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 68417784701ce387e7437bb0f0b8e62a2335e5ff..151e211653c0d77d73af31749bf71836124998e1 100644 (file)
@@ -100,6 +100,9 @@ Using managed disks:
       CloudVMs:
         ImageID: "zzzzz-compute-v1597349873"
         Driver: azure
+        # (azure) managed disks: set MaxConcurrentInstanceCreateOps to 20 to avoid timeouts, cf
+        # https://docs.microsoft.com/en-us/azure/virtual-machines/linux/capture-image
+        MaxConcurrentInstanceCreateOps: 20
         DriverParameters:
           # Credentials.
           SubscriptionID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
index 5ac5e9e6b870a2753287b2b8a59e50c6686d80df..97854e524000894c021e80754a4d871fc1637da6 100644 (file)
@@ -22,9 +22,15 @@ h2(#introduction). Introduction
 
 Arvados support for shell nodes allows you to use Arvados permissions to grant Linux shell accounts to users.
 
-A shell node runs the @arvados-login-sync@ service, and has some additional configuration to make it convenient for users to use Arvados utilites and SDKs.  Users are allowed to log in and run arbitrary programs.  For optimal performance, the Arvados shell server should be on the same LAN as the Arvados cluster.
+A shell node runs the @arvados-login-sync@ service to manage user accounts, and typically has Arvados utilities and SDKs pre-installed.  Users are allowed to log in and run arbitrary programs.  For optimal performance, the Arvados shell server should be on the same LAN as the Arvados cluster.
 
-Because it _contains secrets_ shell nodes should *not* have a copy of the complete @config.yml@.  For example, if users have access to the @docker@ daemon, it is trival to gain *root* access to any file on the system.  Users sharing a shell node should be implicitly trusted, or not given access to Docker.  In more secure environments, the admin should allocate a separate VM for each user.
+Because it _contains secrets_ shell nodes should *not* have a copy of the Arvados @config.yml@.
+
+Shell nodes should be separate virtual machines from the VMs running other Arvados services.  You may choose to grant root access to users so that they can customize the node, for example, installing new programs.  This has security considerations depending on whether a shell node is single-user or multi-user.
+
+A single-user shell node should be set up so that it only stores Arvados access tokens that belong to that user.  In that case, that user can be safely granted root access without compromising other Arvados users.
+
+In the multi-user shell node case, a malicious user with @root@ access could access other user's Arvados tokens.  Users should only be given @root@ access on a multi-user shell node if you would trust them them to be Arvados administrators.  Be aware that with access to the @docker@ daemon, it is trival to gain *root* access to any file on the system, so giving users @docker@ access should be considered equivalent to @root@ access.
 
 h2(#dependencies). Install Dependecies and SDKs
 
@@ -52,51 +58,42 @@ Configure git to use the ARVADOS_API_TOKEN environment variable to authenticate
 
 h2(#vm-record). Create record for VM
 
-This program makes it possible for Arvados users to log in to the shell server -- subject to permissions assigned by the Arvados administrator -- using the SSH keys they upload to Workbench. It sets up login accounts, updates group membership, and adds users' public keys to the appropriate @authorized_keys@ files.
-
-Create an Arvados virtual_machine object representing this shell server. This will assign a UUID.
+As an admin, create an Arvados virtual_machine object representing this shell server. This will return a uuid.
 
 <notextile>
 <pre>
-<code>apiserver:~$ <span class="userinput">arv --format=uuid virtual_machine create --virtual-machine '{"hostname":"<b>your.shell.server.hostname.without.domain</b>"}'</span>
+<code>apiserver:~$ <span class="userinput">arv --format=uuid virtual_machine create --virtual-machine '{"hostname":"<b>shell.ClusterID.example.com</b>"}'</span>
 zzzzz-2x53u-zzzzzzzzzzzzzzz</code>
 </pre>
 </notextile>
 
-h2(#scoped-token). Create scoped token
+h2(#arvados-login-sync). Install arvados-login-sync
+
+The @arvados-login-sync@ service makes it possible for Arvados users to log in to the shell server.  It sets up login accounts, updates group membership, adds each user's SSH public keys to the @~/.ssh/authorized_keys@ file, and adds an Arvados token to @~/.config/arvados/settings.conf@ .
 
-As an Arvados admin user (such as the system root user), create a "scoped token":{{site.baseurl}}/admin/scoped-tokens.html that is permits only reading login information for this VM.  Setting a scope on the token means that even though a user with root access on the shell node can access the token, the token is not usable for admin actions on Arvados.
+Install the @arvados-login-sync@ program from RubyGems.
 
 <notextile>
 <pre>
-<code>apiserver:~$ <span class="userinput">arv api_client_authorization create --api-client-authorization '{"scopes":["GET /arvados/v1/virtual_machines/<b>zzzzz-2x53u-zzzzzzzzzzzzzzz</b>/logins"]}'</span>
-{
- ...
- "api_token":"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
- ...
-}</code>
+<code>shellserver:# <span class="userinput">gem install arvados-login-sync</span></code>
 </pre>
 </notextile>
 
-Note the UUID and the API token output by the above commands: you will need them in a minute.
+h2(#arvados-login-sync). Run arvados-login-sync periodically
 
-h2(#arvados-login-sync). Install arvados-login-sync
+Create a cron job to run the @arvados-login-sync@ program every 2 minutes.  This will synchronize user accounts.
 
-Install the arvados-login-sync program from RubyGems.
+If this is a single-user shell node, then @ARVADOS_API_TOKEN@ should be a token for that user.  See "Create a token for a user":{{site.baseurl}}/admin/user-management-cli.html#create-token .
 
-<notextile>
-<pre>
-<code>shellserver:# <span class="userinput">gem install arvados-login-sync</span></code>
-</pre>
-</notextile>
+If this is a multi-user shell node, then @ARVADOS_API_TOKEN@ should be an administrator token such as the @SystemRootToken@.  See discussion in the "introduction":#introduction about security on multi-user shell nodes.
 
-Configure cron to run the @arvados-login-sync@ program every 2 minutes.
+Set @ARVADOS_VIRTUAL_MACHINE_UUID@ to the UUID from "Create record for VM":#vm-record
 
 <notextile>
 <pre>
-<code>shellserver:# <span class="userinput">umask 077; tee /etc/cron.d/arvados-login-sync &lt;&lt;EOF
+<code>shellserver:# <span class="userinput">umask 0700; tee /etc/cron.d/arvados-login-sync &lt;&lt;EOF
 ARVADOS_API_HOST="<strong>ClusterID.example.com</strong>"
-ARVADOS_API_TOKEN="<strong>the_token_you_created_above</strong>"
+ARVADOS_API_TOKEN="<strong>xxxxxxxxxxxxxxxxx</strong>"
 ARVADOS_VIRTUAL_MACHINE_UUID="<strong>zzzzz-2x53u-zzzzzzzzzzzzzzz</strong>"
 */2 * * * * root arvados-login-sync
 EOF</span></code>
@@ -107,8 +104,9 @@ h2(#confirm-working). Confirm working installation
 
 A user should be able to log in to the shell server when the following conditions are satisfied:
 
-# The user has uploaded an SSH public key: Workbench &rarr; Account menu &rarr; "SSH keys" item &rarr; "Add new SSH key" button.
 # As an admin user, you have given the user permission to log in using the Workbench &rarr; Admin menu &rarr; "Users" item &rarr; "Show" button &rarr; "Admin" tab &rarr; "Setup account" button.
 # The cron job has run.
 
+In order to log in via SSH, the user must also upload an SSH public key.  Alternately, if configured, users can log in using "Webshell":install-webshell.html .
+
 See also "how to add a VM login permission link at the command line":../admin/user-management-cli.html
index ae6a8d2109c686a3d6769e515e75d4376c1e8bee..8275a2a831e1fecb2a6f629d061f3da816d57105 100644 (file)
@@ -65,7 +65,7 @@ server {
 
   location /<span class="userinput">shell.ClusterID</span> {
     if ($request_method = 'OPTIONS') {
-       add_header 'Access-Control-Allow-Origin' '*'; 
+       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;
@@ -146,7 +146,7 @@ SHELLINABOX_ARGS="--disable-ssl --no-beep --service=/<span class="userinput">she
 
 h2(#config-pam). Configure pam
 
-Use a text editor to create a new file @/etc/pam.d/shellinabox@ with the following configuration. Options that need attention are marked in <span class="userinput">red</span>.
+Use a text editor to create a new file @/etc/pam.d/shellinabox@ with the following configuration.  Options that need attention are marked in <span class="userinput">red</span>.
 
 <notextile><pre>
 # This example is a stock debian "login" file with pam_arvados
@@ -159,7 +159,11 @@ session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux
 session       required   pam_env.so readenv=1
 session       required   pam_env.so readenv=1 envfile=/etc/default/locale
 
+# The first argument is the address of the API server.  The second
+# argument is this shell node's hostname.  The hostname must match the
+# "hostname" field of the virtual_machine record.
 auth [success=1 default=ignore] /usr/lib/pam_arvados.so <span class="userinput">ClusterID.example.com</span> <span class="userinput">shell.ClusterID.example.com</span>
+
 auth    requisite            pam_deny.so
 auth    required            pam_permit.so
 
@@ -179,5 +183,4 @@ session [success=ok ignore=ignore module_unknown=ignore default=bad] pam_selinux
 
 h2(#confirm-working). Confirm working installation
 
-A user should be able to log in to the shell server, using webshell via workbench. Please refer to "Accessing an Arvados VM with Webshell":{{site.baseurl}}/user/getting_started/vm-login-with-webshell.html
-
+A user should now be able to log in to the shell server, using webshell via workbench. Please refer to "Accessing an Arvados VM with Webshell":{{site.baseurl}}/user/getting_started/vm-login-with-webshell.html
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 15993c4bc322619e125ddb5411a79a2d0f4348f0..69ea34bc81c412f0ec21d6747db904a163f3000f 100644 (file)
@@ -23,12 +23,10 @@ ARG cwl_runner_version
 RUN echo cwl_runner_version $cwl_runner_version python_sdk_version $python_sdk_version
 
 RUN apt-get update -q
-RUN apt-get install -yq --no-install-recommends nodejs \
-    python-arvados-python-client=$python_sdk_version \
-    python3-arvados-cwl-runner=$cwl_runner_version
+RUN apt-get install -yq --no-install-recommends python3-arvados-cwl-runner=$cwl_runner_version
 
 # use the Python executable from the python-arvados-cwl-runner package
-RUN rm -f /usr/bin/python && ln -s /usr/share/python2.7/dist/python-arvados-python-client/bin/python /usr/bin/python
+RUN rm -f /usr/bin/python && ln -s /usr/share/python3/dist/python3-arvados-cwl-runner/bin/python /usr/bin/python
 RUN rm -f /usr/bin/python3 && ln -s /usr/share/python3/dist/python3-arvados-cwl-runner/bin/python /usr/bin/python3
 
 # Install dependencies and set up system.
index d1cf2a870975b662d8318d9ef6a25f08ce204c93..2afccc45b628cc01b00ddac873abdfc4eae20b61 100644 (file)
@@ -24,5 +24,9 @@ func (seedDatabase) Run(ctx context.Context, fail func(error), super *Supervisor
        if err != nil {
                return err
        }
+       err = super.RunProgram(ctx, "services/api", nil, railsEnv, "bundle", "exec", "./script/get_anonymous_user_token.rb")
+       if err != nil {
+               return err
+       }
        return nil
 }
index 3f4fb7482229bc704e9daee1d71a0775aa8ed3fa..3484a1444e786cc5f026f0d0a68ada822b79ffb1 100644 (file)
@@ -617,6 +617,10 @@ func (super *Supervisor) autofillConfig(cfg *arvados.Config) error {
        if cluster.Collections.BlobSigningKey == "" {
                cluster.Collections.BlobSigningKey = randomHexString(64)
        }
+       if cluster.Users.AnonymousUserToken == "" {
+               cluster.Users.AnonymousUserToken = randomHexString(64)
+       }
+
        if super.ClusterType != "production" && cluster.Containers.DispatchPrivateKey == "" {
                buf, err := ioutil.ReadFile(filepath.Join(super.SourcePath, "lib", "dispatchcloud", "test", "sshkey_dispatch"))
                if err != nil {
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 d64106fbce6eaf9a5d2eefb246e15c4bc5017e92..1ea0883ac84730738981cb499fe874983fda6ca5 100644 (file)
@@ -161,9 +161,8 @@ func (checkCommand) RunCommand(prog string, args []string, stdin io.Reader, stdo
 
        if problems {
                return 1
-       } else {
-               return 0
        }
+       return 0
 }
 
 func warnAboutProblems(logger logrus.FieldLogger, cfg *arvados.Config) bool {
index 80294afaf35f1f701928f0c1ce99c3c09ca35e09..b1865a2217ce99c48a13ae8a17e4cf10d336cbf0 100644 (file)
@@ -945,6 +945,12 @@ Clusters:
         # unlimited).
         MaxCloudOpsPerSecond: 0
 
+        # Maximum concurrent node creation operations (0 = unlimited). This is
+        # recommended by Azure in certain scenarios (see
+        # https://docs.microsoft.com/en-us/azure/virtual-machines/linux/capture-image)
+        # and can be used with other cloud providers too, if desired.
+        MaxConcurrentInstanceCreateOps: 0
+
         # Interval between cloud provider syncs/updates ("list all
         # instances").
         SyncInterval: 1m
@@ -1332,6 +1338,10 @@ Clusters:
       VocabularyURL: ""
       FileViewersConfigURL: ""
 
+      # Idle time after which the user's session will be auto closed.
+      # This feature is disabled when set to zero.
+      IdleTimeout: 0s
+
       # Workbench welcome screen, this is HTML text that will be
       # incorporated directly onto the page.
       WelcomePageHTML: |
index b203dff26a1b80272be29f41dd747c8108beb272..1ccb487ad85ddbc7fcdf95ff4ccb8a8e9f85b748 100644 (file)
@@ -173,7 +173,7 @@ var whitelist = map[string]bool{
        "Login.Test":                                   true,
        "Login.Test.Enable":                            true,
        "Login.Test.Users":                             false,
-       "Login.TokenLifetime":                          false,
+       "Login.TokenLifetime":                          true,
        "Mail":                                         true,
        "Mail.EmailFrom":                               false,
        "Mail.IssueReporterEmailFrom":                  false,
@@ -237,6 +237,7 @@ var whitelist = map[string]bool{
        "Workbench.EnableGettingStartedPopup":          true,
        "Workbench.EnablePublicProjectsPage":           true,
        "Workbench.FileViewersConfigURL":               true,
+       "Workbench.IdleTimeout":                        true,
        "Workbench.InactivePageHTML":                   true,
        "Workbench.LogViewerMaxBytes":                  true,
        "Workbench.MultiSiteSearch":                    true,
index 57204cf36a2dbe49731c2d7cc32ad51f09522f0a..201ae3604537f9f44a9e788320b7262685944f98 100644 (file)
@@ -951,6 +951,12 @@ Clusters:
         # unlimited).
         MaxCloudOpsPerSecond: 0
 
+        # Maximum concurrent node creation operations (0 = unlimited). This is
+        # recommended by Azure in certain scenarios (see
+        # https://docs.microsoft.com/en-us/azure/virtual-machines/linux/capture-image)
+        # and can be used with other cloud providers too, if desired.
+        MaxConcurrentInstanceCreateOps: 0
+
         # Interval between cloud provider syncs/updates ("list all
         # instances").
         SyncInterval: 1m
@@ -1338,6 +1344,10 @@ Clusters:
       VocabularyURL: ""
       FileViewersConfigURL: ""
 
+      # Idle time after which the user's session will be auto closed.
+      # This feature is disabled when set to zero.
+      IdleTimeout: 0s
+
       # Workbench welcome screen, this is HTML text that will be
       # incorporated directly onto the page.
       WelcomePageHTML: |
index 418b6811beeb82d814c16603e50b694502372522..d715734c6506acc5090f88cf2c7b1a8b1053bdb4 100644 (file)
@@ -111,6 +111,14 @@ func (conn *Conn) chooseBackend(id string) backend {
        }
 }
 
+func (conn *Conn) localOrLoginCluster() backend {
+       if conn.cluster.Login.LoginCluster != "" {
+               return conn.chooseBackend(conn.cluster.Login.LoginCluster)
+       } else {
+               return conn.local
+       }
+}
+
 // Call fn with the local backend; then, if fn returned 404, call fn
 // on the available remote backends (possibly concurrently) until one
 // succeeds.
@@ -462,15 +470,37 @@ func (conn *Conn) UserMerge(ctx context.Context, options arvados.UserMergeOption
 }
 
 func (conn *Conn) UserActivate(ctx context.Context, options arvados.UserActivateOptions) (arvados.User, error) {
-       return conn.chooseBackend(options.UUID).UserActivate(ctx, options)
+       return conn.localOrLoginCluster().UserActivate(ctx, options)
 }
 
 func (conn *Conn) UserSetup(ctx context.Context, options arvados.UserSetupOptions) (map[string]interface{}, error) {
-       return conn.chooseBackend(options.UUID).UserSetup(ctx, options)
+       upstream := conn.localOrLoginCluster()
+       if upstream != conn.local {
+               // When LoginCluster is in effect, and we're setting
+               // up a remote user, and we want to give that user
+               // access to a local VM, we can't include the VM in
+               // the setup call, because the remote cluster won't
+               // recognize it.
+
+               // Similarly, if we want to create a git repo,
+               // it should be created on the local cluster,
+               // not the remote one.
+
+               upstreamOptions := options
+               upstreamOptions.VMUUID = ""
+               upstreamOptions.RepoName = ""
+
+               ret, err := upstream.UserSetup(ctx, upstreamOptions)
+               if err != nil {
+                       return ret, err
+               }
+       }
+
+       return conn.local.UserSetup(ctx, options)
 }
 
 func (conn *Conn) UserUnsetup(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
-       return conn.chooseBackend(options.UUID).UserUnsetup(ctx, options)
+       return conn.localOrLoginCluster().UserUnsetup(ctx, options)
 }
 
 func (conn *Conn) UserGet(ctx context.Context, options arvados.GetOptions) (arvados.User, error) {
index a73f5f9f828574b1c234932432a2a4b63c769087..077493ffc836f58260f1abb19448323ea25f45e9 100644 (file)
@@ -139,10 +139,15 @@ func (s *IntegrationSuite) TearDownSuite(c *check.C) {
        }
 }
 
+// Get rpc connection struct initialized to communicate with the
+// specified cluster.
 func (s *IntegrationSuite) conn(clusterID string) *rpc.Conn {
        return rpc.NewConn(clusterID, s.testClusters[clusterID].controllerURL, true, rpc.PassthroughTokenProvider)
 }
 
+// Return Context, Arvados.Client and keepclient structs initialized
+// to connect to the specified cluster (by clusterID) using with the supplied
+// Arvados token.
 func (s *IntegrationSuite) clientsWithToken(clusterID string, token string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
        cl := s.testClusters[clusterID].config.Clusters[clusterID]
        ctx := auth.NewContext(context.Background(), auth.NewCredentials(token))
@@ -159,7 +164,11 @@ func (s *IntegrationSuite) clientsWithToken(clusterID string, token string) (con
        return ctx, ac, kc
 }
 
-func (s *IntegrationSuite) userClients(rootctx context.Context, c *check.C, conn *rpc.Conn, clusterID string, activate bool) (context.Context, *arvados.Client, *keepclient.KeepClient) {
+// Log in as a user called "example", get the user's API token,
+// initialize clients with the API token, set up the user and
+// optionally activate the user.  Return client structs for
+// communicating with the cluster on behalf of the 'example' user.
+func (s *IntegrationSuite) userClients(rootctx context.Context, c *check.C, conn *rpc.Conn, clusterID string, activate bool) (context.Context, *arvados.Client, *keepclient.KeepClient, arvados.User) {
        login, err := conn.UserSessionCreate(rootctx, rpc.UserSessionCreateOptions{
                ReturnTo: ",https://example.com",
                AuthInfo: rpc.UserSessionAuthInfo{
@@ -189,18 +198,26 @@ func (s *IntegrationSuite) userClients(rootctx context.Context, c *check.C, conn
                        c.Fatalf("failed to activate user -- %#v", user)
                }
        }
-       return ctx, ac, kc
+       return ctx, ac, kc, user
 }
 
+// Return Context, arvados.Client and keepclient structs initialized
+// to communicate with the cluster as the system root user.
 func (s *IntegrationSuite) rootClients(clusterID string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
        return s.clientsWithToken(clusterID, s.testClusters[clusterID].config.Clusters[clusterID].SystemRootToken)
 }
 
+// Return Context, arvados.Client and keepclient structs initialized
+// to communicate with the cluster as the anonymous user.
+func (s *IntegrationSuite) anonymousClients(clusterID string) (context.Context, *arvados.Client, *keepclient.KeepClient) {
+       return s.clientsWithToken(clusterID, s.testClusters[clusterID].config.Clusters[clusterID].Users.AnonymousUserToken)
+}
+
 func (s *IntegrationSuite) TestGetCollectionByPDH(c *check.C) {
        conn1 := s.conn("z1111")
        rootctx1, _, _ := s.rootClients("z1111")
        conn3 := s.conn("z3333")
-       userctx1, ac1, kc1 := s.userClients(rootctx1, c, conn1, "z1111", true)
+       userctx1, ac1, kc1, _ := s.userClients(rootctx1, c, conn1, "z1111", true)
 
        // Create the collection to find its PDH (but don't save it
        // anywhere yet)
@@ -234,12 +251,77 @@ func (s *IntegrationSuite) TestGetCollectionByPDH(c *check.C) {
        c.Check(coll.PortableDataHash, check.Equals, pdh)
 }
 
+func (s *IntegrationSuite) TestGetCollectionAsAnonymous(c *check.C) {
+       conn1 := s.conn("z1111")
+       conn3 := s.conn("z3333")
+       rootctx1, rootac1, rootkc1 := s.rootClients("z1111")
+       anonctx3, anonac3, _ := s.anonymousClients("z3333")
+
+       // Make sure anonymous token was set
+       c.Assert(anonac3.AuthToken, check.Not(check.Equals), "")
+
+       // Create the collection to find its PDH (but don't save it
+       // anywhere yet)
+       var coll1 arvados.Collection
+       fs1, err := coll1.FileSystem(rootac1, rootkc1)
+       c.Assert(err, check.IsNil)
+       f, err := fs1.OpenFile("test.txt", os.O_CREATE|os.O_RDWR, 0777)
+       c.Assert(err, check.IsNil)
+       _, err = io.WriteString(f, "IntegrationSuite.TestGetCollectionAsAnonymous")
+       c.Assert(err, check.IsNil)
+       err = f.Close()
+       c.Assert(err, check.IsNil)
+       mtxt, err := fs1.MarshalManifest(".")
+       c.Assert(err, check.IsNil)
+       pdh := arvados.PortableDataHash(mtxt)
+
+       // Save the collection on cluster z1111.
+       coll1, err = conn1.CollectionCreate(rootctx1, arvados.CreateOptions{Attrs: map[string]interface{}{
+               "manifest_text": mtxt,
+       }})
+       c.Assert(err, check.IsNil)
+
+       // Share it with the anonymous users group.
+       var outLink arvados.Link
+       err = rootac1.RequestAndDecode(&outLink, "POST", "/arvados/v1/links", nil,
+               map[string]interface{}{"link": map[string]interface{}{
+                       "link_class": "permission",
+                       "name":       "can_read",
+                       "tail_uuid":  "z1111-j7d0g-anonymouspublic",
+                       "head_uuid":  coll1.UUID,
+               },
+               })
+       c.Check(err, check.IsNil)
+
+       // Current user should be z3 anonymous user
+       outUser, err := anonac3.CurrentUser()
+       c.Check(err, check.IsNil)
+       c.Check(outUser.UUID, check.Equals, "z3333-tpzed-anonymouspublic")
+
+       // Get the token uuid
+       var outAuth arvados.APIClientAuthorization
+       err = anonac3.RequestAndDecode(&outAuth, "GET", "/arvados/v1/api_client_authorizations/current", nil, nil)
+       c.Check(err, check.IsNil)
+
+       // Make a v2 token of the z3 anonymous user, and use it on z1
+       _, anonac1, _ := s.clientsWithToken("z1111", outAuth.TokenV2())
+       outUser2, err := anonac1.CurrentUser()
+       c.Check(err, check.IsNil)
+       // z3 anonymous user will be mapped to the z1 anonymous user
+       c.Check(outUser2.UUID, check.Equals, "z1111-tpzed-anonymouspublic")
+
+       // Retrieve the collection (which is on z1) using anonymous from cluster z3333.
+       coll, err := conn3.CollectionGet(anonctx3, arvados.GetOptions{UUID: coll1.UUID})
+       c.Check(err, check.IsNil)
+       c.Check(coll.PortableDataHash, check.Equals, pdh)
+}
+
 // Get a token from the login cluster (z1111), use it to submit a
 // container request on z2222.
 func (s *IntegrationSuite) TestCreateContainerRequestWithFedToken(c *check.C) {
        conn1 := s.conn("z1111")
        rootctx1, _, _ := s.rootClients("z1111")
-       _, ac1, _ := s.userClients(rootctx1, c, conn1, "z1111", true)
+       _, ac1, _, _ := s.userClients(rootctx1, c, conn1, "z1111", true)
 
        // Use ac2 to get the discovery doc with a blank token, so the
        // SDK doesn't magically pass the z1111 token to z2222 before
@@ -310,7 +392,7 @@ func (s *IntegrationSuite) TestListUsers(c *check.C) {
        rootctx1, _, _ := s.rootClients("z1111")
        conn1 := s.conn("z1111")
        conn3 := s.conn("z3333")
-       userctx1, _, _ := s.userClients(rootctx1, c, conn1, "z1111", true)
+       userctx1, _, _, _ := s.userClients(rootctx1, c, conn1, "z1111", true)
 
        // Make sure LoginCluster is properly configured
        for cls := range s.testClusters {
@@ -374,3 +456,67 @@ func (s *IntegrationSuite) TestListUsers(c *check.C) {
        c.Assert(err, check.IsNil)
        c.Check(user1.IsActive, check.Equals, false)
 }
+
+func (s *IntegrationSuite) TestSetupUserWithVM(c *check.C) {
+       conn1 := s.conn("z1111")
+       conn3 := s.conn("z3333")
+       rootctx1, rootac1, _ := s.rootClients("z1111")
+
+       // Create user on LoginCluster z1111
+       _, _, _, user := s.userClients(rootctx1, c, conn1, "z1111", false)
+
+       // Make a new root token (because rootClients() uses SystemRootToken)
+       var outAuth arvados.APIClientAuthorization
+       err := rootac1.RequestAndDecode(&outAuth, "POST", "/arvados/v1/api_client_authorizations", nil, nil)
+       c.Check(err, check.IsNil)
+
+       // Make a v2 root token to communicate with z3333
+       rootctx3, rootac3, _ := s.clientsWithToken("z3333", outAuth.TokenV2())
+
+       // Create VM on z3333
+       var outVM arvados.VirtualMachine
+       err = rootac3.RequestAndDecode(&outVM, "POST", "/arvados/v1/virtual_machines", nil,
+               map[string]interface{}{"virtual_machine": map[string]interface{}{
+                       "hostname": "example",
+               },
+               })
+       c.Check(outVM.UUID[0:5], check.Equals, "z3333")
+       c.Check(err, check.IsNil)
+
+       // Make sure z3333 user list is up to date
+       _, err = conn3.UserList(rootctx3, arvados.ListOptions{Limit: 1000})
+       c.Check(err, check.IsNil)
+
+       // Try to set up user on z3333 with the VM
+       _, err = conn3.UserSetup(rootctx3, arvados.UserSetupOptions{UUID: user.UUID, VMUUID: outVM.UUID})
+       c.Check(err, check.IsNil)
+
+       var outLinks arvados.LinkList
+       err = rootac3.RequestAndDecode(&outLinks, "GET", "/arvados/v1/links", nil,
+               arvados.ListOptions{
+                       Limit: 1000,
+                       Filters: []arvados.Filter{
+                               {
+                                       Attr:     "tail_uuid",
+                                       Operator: "=",
+                                       Operand:  user.UUID,
+                               },
+                               {
+                                       Attr:     "head_uuid",
+                                       Operator: "=",
+                                       Operand:  outVM.UUID,
+                               },
+                               {
+                                       Attr:     "name",
+                                       Operator: "=",
+                                       Operand:  "can_login",
+                               },
+                               {
+                                       Attr:     "link_class",
+                                       Operator: "=",
+                                       Operand:  "permission",
+                               }}})
+       c.Check(err, check.IsNil)
+
+       c.Check(len(outLinks.Items), check.Equals, 1)
+}
index ff607bbb57ae0c927187699f0284924d751372a0..e1cda33f93ae7b477ba1930f9de62a8a9123f82d 100644 (file)
@@ -8,7 +8,6 @@ func semaphore(max int) (acquire, release func()) {
        if max > 0 {
                ch := make(chan bool, max)
                return func() { ch <- true }, func() { <-ch }
-       } else {
-               return func() {}, func() {}
        }
+       return func() {}, func() {}
 }
index b1497277f2d52971d7a2bbe4c24e90e583500360..1b0f168b88856e8251108f11e928321b5d642c0b 100644 (file)
@@ -195,9 +195,8 @@ func (cp *copier) walkMount(dest, src string, maxSymlinks int, walkMountsBelow b
        }
        if walkMountsBelow {
                return cp.walkMountsBelow(dest, src)
-       } else {
-               return nil
        }
+       return nil
 }
 
 func (cp *copier) walkMountsBelow(dest, src string) error {
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 d77dcee947951953b46da631c0a66ee15894a19f..0e8e1dc2ec38f1394bb4c5899240e1e6cbf7cf1c 100644 (file)
@@ -61,30 +61,25 @@ tryrun:
                        if unalloc[it] > 0 {
                                unalloc[it]--
                        } else if sch.pool.AtQuota() {
-                               logger.Debug("not starting: AtQuota and no unalloc workers")
+                               // Don't let lower-priority containers
+                               // starve this one by using keeping
+                               // idle workers alive on different
+                               // instance types.
+                               logger.Debug("unlocking: AtQuota and no unalloc workers")
+                               sch.queue.Unlock(ctr.UUID)
                                overquota = sorted[i:]
                                break tryrun
+                       } else if logger.Info("creating new instance"); sch.pool.Create(it) {
+                               // Success. (Note pool.Create works
+                               // asynchronously and does its own
+                               // logging, so we don't need to.)
                        } else {
-                               logger.Info("creating new instance")
-                               if !sch.pool.Create(it) {
-                                       // (Note pool.Create works
-                                       // asynchronously and logs its
-                                       // own failures, so we don't
-                                       // need to log this as a
-                                       // failure.)
-
-                                       sch.queue.Unlock(ctr.UUID)
-                                       // Don't let lower-priority
-                                       // containers starve this one
-                                       // by using keeping idle
-                                       // workers alive on different
-                                       // instance types.  TODO:
-                                       // avoid getting starved here
-                                       // if instances of a specific
-                                       // type always fail.
-                                       overquota = sorted[i:]
-                                       break tryrun
-                               }
+                               // Failed despite not being at quota,
+                               // e.g., cloud ops throttled.  TODO:
+                               // avoid getting starved here if
+                               // instances of a specific type always
+                               // fail.
+                               continue
                        }
 
                        if dontstart[it] {
index fbc73ef50c4695a7d3605db8180f1a3d263fa869..530eb5db93d02a9c4c2832f0fc090583c0432f93 100644 (file)
@@ -38,7 +38,7 @@ type stubPool struct {
        idle      map[arvados.InstanceType]int
        unknown   map[arvados.InstanceType]int
        running   map[string]time.Time
-       atQuota   bool
+       quota     int
        canCreate int
        creates   []arvados.InstanceType
        starts    []string
@@ -46,7 +46,11 @@ type stubPool struct {
        sync.Mutex
 }
 
-func (p *stubPool) AtQuota() bool               { return p.atQuota }
+func (p *stubPool) AtQuota() bool {
+       p.Lock()
+       defer p.Unlock()
+       return len(p.unalloc)+len(p.running)+len(p.unknown) >= p.quota
+}
 func (p *stubPool) Subscribe() <-chan struct{}  { return p.notify }
 func (p *stubPool) Unsubscribe(<-chan struct{}) {}
 func (p *stubPool) Running() map[string]time.Time {
@@ -122,11 +126,8 @@ var _ = check.Suite(&SchedulerSuite{})
 
 type SchedulerSuite struct{}
 
-// Assign priority=4 container to idle node. Create a new instance for
-// the priority=3 container. Don't try to start any priority<3
-// containers because priority=3 container didn't start
-// immediately. Don't try to create any other nodes after the failed
-// create.
+// Assign priority=4 container to idle node. Create new instances for
+// the priority=3, 2, 1 containers.
 func (*SchedulerSuite) TestUseIdleWorkers(c *check.C) {
        ctx := ctxlog.Context(context.Background(), ctxlog.TestLogger(c))
        queue := test.Queue{
@@ -172,6 +173,7 @@ func (*SchedulerSuite) TestUseIdleWorkers(c *check.C) {
        }
        queue.Update()
        pool := stubPool{
+               quota: 1000,
                unalloc: map[arvados.InstanceType]int{
                        test.InstanceType(1): 1,
                        test.InstanceType(2): 2,
@@ -184,7 +186,7 @@ func (*SchedulerSuite) TestUseIdleWorkers(c *check.C) {
                canCreate: 0,
        }
        New(ctx, &queue, &pool, time.Millisecond, time.Millisecond).runQueue()
-       c.Check(pool.creates, check.DeepEquals, []arvados.InstanceType{test.InstanceType(1)})
+       c.Check(pool.creates, check.DeepEquals, []arvados.InstanceType{test.InstanceType(1), test.InstanceType(1), test.InstanceType(1)})
        c.Check(pool.starts, check.DeepEquals, []string{test.ContainerUUID(4)})
        c.Check(pool.running, check.HasLen, 1)
        for uuid := range pool.running {
@@ -192,14 +194,14 @@ func (*SchedulerSuite) TestUseIdleWorkers(c *check.C) {
        }
 }
 
-// If Create() fails, shutdown some nodes, and don't call Create()
-// again.  Don't call Create() at all if AtQuota() is true.
+// If pool.AtQuota() is true, shutdown some unalloc nodes, and don't
+// call Create().
 func (*SchedulerSuite) TestShutdownAtQuota(c *check.C) {
        ctx := ctxlog.Context(context.Background(), ctxlog.TestLogger(c))
-       for quota := 0; quota < 2; quota++ {
+       for quota := 1; quota < 3; quota++ {
                c.Logf("quota=%d", quota)
                shouldCreate := []arvados.InstanceType{}
-               for i := 0; i < quota; i++ {
+               for i := 1; i < quota; i++ {
                        shouldCreate = append(shouldCreate, test.InstanceType(3))
                }
                queue := test.Queue{
@@ -227,7 +229,7 @@ func (*SchedulerSuite) TestShutdownAtQuota(c *check.C) {
                }
                queue.Update()
                pool := stubPool{
-                       atQuota: quota == 0,
+                       quota: quota,
                        unalloc: map[arvados.InstanceType]int{
                                test.InstanceType(2): 2,
                        },
@@ -241,8 +243,13 @@ func (*SchedulerSuite) TestShutdownAtQuota(c *check.C) {
                }
                New(ctx, &queue, &pool, time.Millisecond, time.Millisecond).runQueue()
                c.Check(pool.creates, check.DeepEquals, shouldCreate)
-               c.Check(pool.starts, check.DeepEquals, []string{})
-               c.Check(pool.shutdowns, check.Not(check.Equals), 0)
+               if len(shouldCreate) == 0 {
+                       c.Check(pool.starts, check.DeepEquals, []string{})
+                       c.Check(pool.shutdowns, check.Not(check.Equals), 0)
+               } else {
+                       c.Check(pool.starts, check.DeepEquals, []string{test.ContainerUUID(2)})
+                       c.Check(pool.shutdowns, check.Equals, 0)
+               }
        }
 }
 
@@ -251,6 +258,7 @@ func (*SchedulerSuite) TestShutdownAtQuota(c *check.C) {
 func (*SchedulerSuite) TestStartWhileCreating(c *check.C) {
        ctx := ctxlog.Context(context.Background(), ctxlog.TestLogger(c))
        pool := stubPool{
+               quota: 1000,
                unalloc: map[arvados.InstanceType]int{
                        test.InstanceType(1): 2,
                        test.InstanceType(2): 2,
@@ -345,6 +353,7 @@ func (*SchedulerSuite) TestStartWhileCreating(c *check.C) {
 func (*SchedulerSuite) TestKillNonexistentContainer(c *check.C) {
        ctx := ctxlog.Context(context.Background(), ctxlog.TestLogger(c))
        pool := stubPool{
+               quota: 1000,
                unalloc: map[arvados.InstanceType]int{
                        test.InstanceType(2): 0,
                },
index 41eb20763c75248c6cea81a2e9854ad2dfde42a8..132bd4d695f0ef88095951b151be592029c31328 100644 (file)
@@ -274,13 +274,11 @@ func (svm *StubVM) Exec(env map[string]string, command string, stdin io.Reader,
                                svm.Lock()
                                defer svm.Unlock()
                                if svm.running[uuid] != pid {
-                                       if !completed {
-                                               bugf := svm.sis.driver.Bugf
-                                               if bugf == nil {
-                                                       bugf = logger.Warnf
-                                               }
-                                               bugf("[test] StubDriver bug or caller bug: pid %d exiting, running[%s]==%d", pid, uuid, svm.running[uuid])
+                                       bugf := svm.sis.driver.Bugf
+                                       if bugf == nil {
+                                               bugf = logger.Warnf
                                        }
+                                       bugf("[test] StubDriver bug or caller bug: pid %d exiting, running[%s]==%d", pid, uuid, svm.running[uuid])
                                } else {
                                        delete(svm.running, uuid)
                                }
@@ -305,7 +303,7 @@ func (svm *StubVM) Exec(env map[string]string, command string, stdin io.Reader,
                        time.Sleep(time.Duration(math_rand.Float64()*20) * time.Millisecond)
 
                        svm.Lock()
-                       killed := svm.running[uuid] != pid
+                       killed := svm.killing[uuid]
                        svm.Unlock()
                        if killed || wantCrashEarly {
                                return
@@ -345,21 +343,9 @@ func (svm *StubVM) Exec(env map[string]string, command string, stdin io.Reader,
        }
        if strings.HasPrefix(command, "crunch-run --kill ") {
                svm.Lock()
-               pid, running := svm.running[uuid]
-               if running && !svm.killing[uuid] {
+               _, running := svm.running[uuid]
+               if running {
                        svm.killing[uuid] = true
-                       go func() {
-                               time.Sleep(time.Duration(math_rand.Float64()*30) * time.Millisecond)
-                               svm.Lock()
-                               defer svm.Unlock()
-                               if svm.running[uuid] == pid {
-                                       // Kill only if the running entry
-                                       // hasn't since been killed and
-                                       // replaced with a different one.
-                                       delete(svm.running, uuid)
-                               }
-                               delete(svm.killing, uuid)
-                       }()
                        svm.Unlock()
                        time.Sleep(time.Duration(math_rand.Float64()*2) * time.Millisecond)
                        svm.Lock()
index 12bc1cdd71636263cebc0c8f21bd283d791aec04..086887cb44176f05c9446341a68d7176dd5ed7aa 100644 (file)
@@ -96,27 +96,28 @@ func duration(conf arvados.Duration, def time.Duration) time.Duration {
 // cluster configuration.
 func NewPool(logger logrus.FieldLogger, arvClient *arvados.Client, reg *prometheus.Registry, instanceSetID cloud.InstanceSetID, instanceSet cloud.InstanceSet, newExecutor func(cloud.Instance) Executor, installPublicKey ssh.PublicKey, cluster *arvados.Cluster) *Pool {
        wp := &Pool{
-               logger:             logger,
-               arvClient:          arvClient,
-               instanceSetID:      instanceSetID,
-               instanceSet:        &throttledInstanceSet{InstanceSet: instanceSet},
-               newExecutor:        newExecutor,
-               bootProbeCommand:   cluster.Containers.CloudVMs.BootProbeCommand,
-               runnerSource:       cluster.Containers.CloudVMs.DeployRunnerBinary,
-               imageID:            cloud.ImageID(cluster.Containers.CloudVMs.ImageID),
-               instanceTypes:      cluster.InstanceTypes,
-               maxProbesPerSecond: cluster.Containers.CloudVMs.MaxProbesPerSecond,
-               probeInterval:      duration(cluster.Containers.CloudVMs.ProbeInterval, defaultProbeInterval),
-               syncInterval:       duration(cluster.Containers.CloudVMs.SyncInterval, defaultSyncInterval),
-               timeoutIdle:        duration(cluster.Containers.CloudVMs.TimeoutIdle, defaultTimeoutIdle),
-               timeoutBooting:     duration(cluster.Containers.CloudVMs.TimeoutBooting, defaultTimeoutBooting),
-               timeoutProbe:       duration(cluster.Containers.CloudVMs.TimeoutProbe, defaultTimeoutProbe),
-               timeoutShutdown:    duration(cluster.Containers.CloudVMs.TimeoutShutdown, defaultTimeoutShutdown),
-               timeoutTERM:        duration(cluster.Containers.CloudVMs.TimeoutTERM, defaultTimeoutTERM),
-               timeoutSignal:      duration(cluster.Containers.CloudVMs.TimeoutSignal, defaultTimeoutSignal),
-               installPublicKey:   installPublicKey,
-               tagKeyPrefix:       cluster.Containers.CloudVMs.TagKeyPrefix,
-               stop:               make(chan bool),
+               logger:                         logger,
+               arvClient:                      arvClient,
+               instanceSetID:                  instanceSetID,
+               instanceSet:                    &throttledInstanceSet{InstanceSet: instanceSet},
+               newExecutor:                    newExecutor,
+               bootProbeCommand:               cluster.Containers.CloudVMs.BootProbeCommand,
+               runnerSource:                   cluster.Containers.CloudVMs.DeployRunnerBinary,
+               imageID:                        cloud.ImageID(cluster.Containers.CloudVMs.ImageID),
+               instanceTypes:                  cluster.InstanceTypes,
+               maxProbesPerSecond:             cluster.Containers.CloudVMs.MaxProbesPerSecond,
+               maxConcurrentInstanceCreateOps: cluster.Containers.CloudVMs.MaxConcurrentInstanceCreateOps,
+               probeInterval:                  duration(cluster.Containers.CloudVMs.ProbeInterval, defaultProbeInterval),
+               syncInterval:                   duration(cluster.Containers.CloudVMs.SyncInterval, defaultSyncInterval),
+               timeoutIdle:                    duration(cluster.Containers.CloudVMs.TimeoutIdle, defaultTimeoutIdle),
+               timeoutBooting:                 duration(cluster.Containers.CloudVMs.TimeoutBooting, defaultTimeoutBooting),
+               timeoutProbe:                   duration(cluster.Containers.CloudVMs.TimeoutProbe, defaultTimeoutProbe),
+               timeoutShutdown:                duration(cluster.Containers.CloudVMs.TimeoutShutdown, defaultTimeoutShutdown),
+               timeoutTERM:                    duration(cluster.Containers.CloudVMs.TimeoutTERM, defaultTimeoutTERM),
+               timeoutSignal:                  duration(cluster.Containers.CloudVMs.TimeoutSignal, defaultTimeoutSignal),
+               installPublicKey:               installPublicKey,
+               tagKeyPrefix:                   cluster.Containers.CloudVMs.TagKeyPrefix,
+               stop:                           make(chan bool),
        }
        wp.registerMetrics(reg)
        go func() {
@@ -132,26 +133,27 @@ func NewPool(logger logrus.FieldLogger, arvClient *arvados.Client, reg *promethe
 // zero Pool should not be used. Call NewPool to create a new Pool.
 type Pool struct {
        // configuration
-       logger             logrus.FieldLogger
-       arvClient          *arvados.Client
-       instanceSetID      cloud.InstanceSetID
-       instanceSet        *throttledInstanceSet
-       newExecutor        func(cloud.Instance) Executor
-       bootProbeCommand   string
-       runnerSource       string
-       imageID            cloud.ImageID
-       instanceTypes      map[string]arvados.InstanceType
-       syncInterval       time.Duration
-       probeInterval      time.Duration
-       maxProbesPerSecond int
-       timeoutIdle        time.Duration
-       timeoutBooting     time.Duration
-       timeoutProbe       time.Duration
-       timeoutShutdown    time.Duration
-       timeoutTERM        time.Duration
-       timeoutSignal      time.Duration
-       installPublicKey   ssh.PublicKey
-       tagKeyPrefix       string
+       logger                         logrus.FieldLogger
+       arvClient                      *arvados.Client
+       instanceSetID                  cloud.InstanceSetID
+       instanceSet                    *throttledInstanceSet
+       newExecutor                    func(cloud.Instance) Executor
+       bootProbeCommand               string
+       runnerSource                   string
+       imageID                        cloud.ImageID
+       instanceTypes                  map[string]arvados.InstanceType
+       syncInterval                   time.Duration
+       probeInterval                  time.Duration
+       maxProbesPerSecond             int
+       maxConcurrentInstanceCreateOps int
+       timeoutIdle                    time.Duration
+       timeoutBooting                 time.Duration
+       timeoutProbe                   time.Duration
+       timeoutShutdown                time.Duration
+       timeoutTERM                    time.Duration
+       timeoutSignal                  time.Duration
+       installPublicKey               ssh.PublicKey
+       tagKeyPrefix                   string
 
        // private state
        subscribers  map[<-chan struct{}]chan<- struct{}
@@ -168,16 +170,15 @@ type Pool struct {
        runnerMD5    [md5.Size]byte
        runnerCmd    string
 
-       throttleCreate    throttle
-       throttleInstances throttle
-
-       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 {
@@ -298,7 +299,19 @@ func (wp *Pool) Create(it arvados.InstanceType) bool {
        }
        wp.mtx.Lock()
        defer wp.mtx.Unlock()
-       if time.Now().Before(wp.atQuotaUntil) || wp.throttleCreate.Error() != nil {
+       if time.Now().Before(wp.atQuotaUntil) || wp.instanceSet.throttleCreate.Error() != nil {
+               return false
+       }
+       // The maxConcurrentInstanceCreateOps knob throttles the number of node create
+       // requests in flight. It was added to work around a limitation in Azure's
+       // managed disks, which support no more than 20 concurrent node creation
+       // requests from a single disk image (cf.
+       // https://docs.microsoft.com/en-us/azure/virtual-machines/linux/capture-image).
+       // The code assumes that node creation, from Azure's perspective, means the
+       // period until the instance appears in the "get all instances" list.
+       if wp.maxConcurrentInstanceCreateOps > 0 && len(wp.creating) >= wp.maxConcurrentInstanceCreateOps {
+               logger.Info("reached MaxConcurrentInstanceCreateOps")
+               wp.instanceSet.throttleCreate.ErrorUntil(errors.New("reached MaxConcurrentInstanceCreateOps"), time.Now().Add(5*time.Second), wp.notify)
                return false
        }
        now := time.Now()
@@ -312,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()
@@ -356,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.
@@ -366,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)
@@ -615,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 0c173c107d4a248ec38ca635f5fa0ac219af6a4b..a85f7383ab3cdc59fcc1bd0e7ad936703666ca2f 100644 (file)
@@ -199,6 +199,46 @@ func (suite *PoolSuite) TestDrain(c *check.C) {
        }
 }
 
+func (suite *PoolSuite) TestNodeCreateThrottle(c *check.C) {
+       logger := ctxlog.TestLogger(c)
+       driver := test.StubDriver{HoldCloudOps: true}
+       instanceSet, err := driver.InstanceSet(nil, "test-instance-set-id", nil, logger)
+       c.Assert(err, check.IsNil)
+
+       type1 := test.InstanceType(1)
+       pool := &Pool{
+               logger:                         logger,
+               instanceSet:                    &throttledInstanceSet{InstanceSet: instanceSet},
+               maxConcurrentInstanceCreateOps: 1,
+               instanceTypes: arvados.InstanceTypeMap{
+                       type1.Name: type1,
+               },
+       }
+
+       c.Check(pool.Unallocated()[type1], check.Equals, 0)
+       res := pool.Create(type1)
+       c.Check(pool.Unallocated()[type1], check.Equals, 1)
+       c.Check(res, check.Equals, true)
+
+       res = pool.Create(type1)
+       c.Check(pool.Unallocated()[type1], check.Equals, 1)
+       c.Check(res, check.Equals, false)
+
+       pool.instanceSet.throttleCreate.err = nil
+       pool.maxConcurrentInstanceCreateOps = 2
+
+       res = pool.Create(type1)
+       c.Check(pool.Unallocated()[type1], check.Equals, 2)
+       c.Check(res, check.Equals, true)
+
+       pool.instanceSet.throttleCreate.err = nil
+       pool.maxConcurrentInstanceCreateOps = 0
+
+       res = pool.Create(type1)
+       c.Check(pool.Unallocated()[type1], check.Equals, 3)
+       c.Check(res, check.Equals, true)
+}
+
 func (suite *PoolSuite) TestCreateUnallocShutdown(c *check.C) {
        logger := ctxlog.TestLogger(c)
        driver := test.StubDriver{HoldCloudOps: true}
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 6e1549224b79c15e3674b76091c06a229589f16d..363d09dafb5452b350077d6933ed2698689fb513 100644 (file)
@@ -259,6 +259,7 @@ type Cluster struct {
                InactivePageHTML       string
                SSHHelpPageHTML        string
                SSHHelpHostSuffix      string
+               IdleTimeout            Duration
        }
 
        ForceLegacyAPI14 bool
@@ -446,23 +447,24 @@ type ContainersConfig struct {
 type CloudVMsConfig struct {
        Enable bool
 
-       BootProbeCommand     string
-       DeployRunnerBinary   string
-       ImageID              string
-       MaxCloudOpsPerSecond int
-       MaxProbesPerSecond   int
-       PollInterval         Duration
-       ProbeInterval        Duration
-       SSHPort              string
-       SyncInterval         Duration
-       TimeoutBooting       Duration
-       TimeoutIdle          Duration
-       TimeoutProbe         Duration
-       TimeoutShutdown      Duration
-       TimeoutSignal        Duration
-       TimeoutTERM          Duration
-       ResourceTags         map[string]string
-       TagKeyPrefix         string
+       BootProbeCommand               string
+       DeployRunnerBinary             string
+       ImageID                        string
+       MaxCloudOpsPerSecond           int
+       MaxProbesPerSecond             int
+       MaxConcurrentInstanceCreateOps int
+       PollInterval                   Duration
+       ProbeInterval                  Duration
+       SSHPort                        string
+       SyncInterval                   Duration
+       TimeoutBooting                 Duration
+       TimeoutIdle                    Duration
+       TimeoutProbe                   Duration
+       TimeoutShutdown                Duration
+       TimeoutSignal                  Duration
+       TimeoutTERM                    Duration
+       ResourceTags                   map[string]string
+       TagKeyPrefix                   string
 
        Driver           string
        DriverParameters json.RawMessage
index b9ecc45abc6a29d6d92642f41efcd26689457d52..52aa45f858746a7da6f79b57a5a6c6f32c044f45 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: Apache-2.0
 
-// Stores a Block Locator Digest compactly. Can be used as a map key.
+// Package blockdigest stores a Block Locator Digest compactly. Can be used as a map key.
 package blockdigest
 
 import (
index 589533177a4b83b5c481e2ff122b7594d536133a..3fff82c42d67f3045fe02d87fc44914709800e54 100644 (file)
@@ -50,7 +50,7 @@ setup(name='arvados-python-client',
           'future',
           'google-api-python-client >=1.6.2, <1.7',
           'httplib2 >=0.9.2',
-          'pycurl >=7.19.5.1',
+          'pycurl >=7.19.5.1, <7.43.0.4', # 7.43.0.4 removes support for python2
           'ruamel.yaml >=0.15.54, <=0.16.5',
           'setuptools',
           'ws4py >=0.4.2',
index c31f097828b8b0401aab0c799720e76483a741f3..ab6fd8000c1f4a966be7e01450402719f7182d5a 100644 (file)
@@ -226,6 +226,11 @@ class ApiClientAuthorization < ArvadosModel
       # Add or update user and token in local database so we can
       # validate subsequent requests faster.
 
+      if remote_user['uuid'][-22..-1] == '-tpzed-anonymouspublic'
+        # Special case: map the remote anonymous user to local anonymous user
+        remote_user['uuid'] = anonymous_user_uuid
+      end
+
       user = User.find_by_uuid(remote_user['uuid'])
 
       if !user
@@ -257,6 +262,11 @@ class ApiClientAuthorization < ArvadosModel
         user.send(attr+'=', remote_user[attr])
       end
 
+      if remote_user['uuid'][-22..-1] == '-tpzed-000000000000000'
+        user.first_name = "root"
+        user.last_name = "from cluster #{remote_user_prefix}"
+      end
+
       act_as_system_user do
         if user.is_active && !remote_user['is_active']
           user.unsetup
index 4bb91e244635d7c6e10dcdff32b72264141ff9de..8775ae59594402a6231ce3169e78669fc49f2740 100755 (executable)
@@ -29,27 +29,37 @@ include ApplicationHelper
 act_as_system_user
 
 def create_api_client_auth(supplied_token=nil)
+  supplied_token = Rails.configuration.Users["AnonymousUserToken"]
 
-  # If token is supplied, see if it exists
-  if supplied_token
-    api_client_auth = ApiClientAuthorization.
-      where(api_token: supplied_token).
-      first
-    if !api_client_auth
-      # fall through to create a token
-    else
-      raise "Token exists, aborting!"
+  if supplied_token.nil? or supplied_token.empty?
+    puts "Users.AnonymousUserToken is empty.  Destroying tokens that belong to anonymous."
+    # Token is empty.  Destroy any anonymous tokens.
+    ApiClientAuthorization.where(user: anonymous_user).destroy_all
+    return nil
+  end
+
+  attr = {user: anonymous_user,
+          api_client_id: 0,
+          scopes: ['GET /']}
+
+  secret = supplied_token
+
+  if supplied_token[0..2] == 'v2/'
+    _, token_uuid, secret, optional = supplied_token.split('/')
+    if token_uuid[0..4] != Rails.configuration.ClusterID
+      # Belongs to a different cluster.
+      puts supplied_token
+      return nil
     end
+    attr[:uuid] = token_uuid
   end
 
-  api_client_auth = ApiClientAuthorization.
-    new(user: anonymous_user,
-        api_client_id: 0,
-        expires_at: Time.now + 100.years,
-        scopes: ['GET /'],
-        api_token: supplied_token)
-  api_client_auth.save!
-  api_client_auth.reload
+  attr[:api_token] = secret
+
+  api_client_auth = ApiClientAuthorization.where(attr).first
+  if !api_client_auth
+    api_client_auth = ApiClientAuthorization.create!(attr)
+  end
   api_client_auth
 end
 
@@ -67,4 +77,6 @@ if !api_client_auth
 end
 
 # print it to the console
-puts api_client_auth.api_token
+if api_client_auth
+  puts "v2/#{api_client_auth.uuid}/#{api_client_auth.api_token}"
+end
index 04a45420fd4b768c105e89f8bd600739d69c8a6f..8ad09894a16df4bc6281ba95db6c8b6b25be589c 100644 (file)
@@ -79,7 +79,7 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     Arvados::V1::SchemaController.any_instance.stubs(:root_url).returns "https://#{@remote_host[0]}"
     @stub_status = 200
     @stub_content = {
-      uuid: 'zbbbb-tpzed-000000000000000',
+      uuid: 'zbbbb-tpzed-000000000000001',
       email: 'foo@example.com',
       username: 'barney',
       is_admin: true,
@@ -98,7 +98,7 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
     assert_equal false, json_response['is_admin']
     assert_equal false, json_response['is_active']
     assert_equal 'foo@example.com', json_response['email']
@@ -286,12 +286,12 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
     assert_equal false, json_response['is_admin']
     assert_equal false, json_response['is_active']
     assert_equal 'foo@example.com', json_response['email']
     assert_equal 'barney', json_response['username']
-    post '/arvados/v1/users/zbbbb-tpzed-000000000000000/activate',
+    post '/arvados/v1/users/zbbbb-tpzed-000000000000001/activate',
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response 422
@@ -303,7 +303,7 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
     assert_equal false, json_response['is_admin']
     assert_equal true, json_response['is_active']
     assert_equal 'foo@example.com', json_response['email']
@@ -316,7 +316,7 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
       params: {format: 'json'},
       headers: auth(remote: 'zbbbb')
     assert_response :success
-    assert_equal 'zbbbb-tpzed-000000000000000', json_response['uuid']
+    assert_equal 'zbbbb-tpzed-000000000000001', json_response['uuid']
     assert_equal true, json_response['is_admin']
     assert_equal true, json_response['is_active']
     assert_equal 'foo@example.com', json_response['email']
@@ -412,4 +412,22 @@ class RemoteUsersTest < ActionDispatch::IntegrationTest
     end
   end
 
+  test 'authenticate with remote token, remote user is system user' do
+    @stub_content[:uuid] = 'zbbbb-tpzed-000000000000000'
+    get '/arvados/v1/users/current',
+      params: {format: 'json'},
+      headers: auth(remote: 'zbbbb')
+    assert_equal 'from cluster zbbbb', json_response['last_name']
+  end
+
+  test 'authenticate with remote token, remote user is anonymous user' do
+    @stub_content[:uuid] = 'zbbbb-tpzed-anonymouspublic'
+    get '/arvados/v1/users/current',
+      params: {format: 'json'},
+      headers: auth(remote: 'zbbbb')
+    assert_response :success
+    assert_equal 'zzzzz-tpzed-anonymouspublic', json_response['uuid']
+  end
+
+
 end
index 4e8028ae6e8de3c429679c7ff25fc87ee58f3cf2..52cfede46642120dce520868d2d38758fb97cc12 100644 (file)
@@ -358,6 +358,11 @@ func (h *handler) s3list(w http.ResponseWriter, r *http.Request, fs arvados.Cust
                // CommonPrefixes is nil, which confuses some clients.
                // Fix by using this nested struct instead.
                CommonPrefixes []commonPrefix
+               // Similarly, we need omitempty here, because an empty
+               // tag confuses some clients (e.g.,
+               // github.com/aws/aws-sdk-net never terminates its
+               // paging loop).
+               NextMarker string `xml:"NextMarker,omitempty"`
        }
        resp := listResp{
                ListResp: s3.ListResp{
index c6d53238e81645928acd4b76aec520bea6674c4c..66f046b13f14674c35bc783351eb9a0bf5f1b64b 100644 (file)
@@ -154,9 +154,10 @@ func (s *IntegrationSuite) testS3GetObject(c *check.C, bucket *s3.Bucket, prefix
        c.Check(err, check.IsNil)
 
        // HeadObject
-       exists, err = bucket.Exists(prefix + "sailboat.txt")
+       resp, err := bucket.Head(prefix+"sailboat.txt", nil)
        c.Check(err, check.IsNil)
-       c.Check(exists, check.Equals, true)
+       c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+       c.Check(resp.ContentLength, check.Equals, int64(4))
 }
 
 func (s *IntegrationSuite) TestS3CollectionPutObjectSuccess(c *check.C) {
@@ -411,6 +412,26 @@ func (s *IntegrationSuite) TestS3ListNoCommonPrefixes(c *check.C) {
        c.Check(string(buf), check.Not(check.Matches), `(?ms).*CommonPrefixes.*`)
 }
 
+// If there is no delimiter in the request, or the results are not
+// truncated, the NextMarker XML tag should not appear in the response
+// body.
+func (s *IntegrationSuite) TestS3ListNoNextMarker(c *check.C) {
+       stage := s.s3setup(c)
+       defer stage.teardown(c)
+
+       for _, query := range []string{"prefix=e&delimiter=/", ""} {
+               req, err := http.NewRequest("GET", stage.collbucket.URL("/"), nil)
+               c.Assert(err, check.IsNil)
+               req.Header.Set("Authorization", "AWS "+arvadostest.ActiveTokenV2+":none")
+               req.URL.RawQuery = query
+               resp, err := http.DefaultClient.Do(req)
+               c.Assert(err, check.IsNil)
+               buf, err := ioutil.ReadAll(resp.Body)
+               c.Assert(err, check.IsNil)
+               c.Check(string(buf), check.Not(check.Matches), `(?ms).*NextMarker.*`)
+       }
+}
+
 func (s *IntegrationSuite) TestS3CollectionList(c *check.C) {
        stage := s.s3setup(c)
        defer stage.teardown(c)
index d5ec159867f6419e2dcd1dc035c09e8d63a1972d..8162e22a2ff6f815ac7904d28ad6ec0413faa7c3 100755 (executable)
@@ -91,38 +91,65 @@ begin
   end
 
   seen = Hash.new()
-  devnull = open("/dev/null", "w")
+
+  current_user_groups = Hash.new
+  while (ent = Etc.getgrent()) do
+    ent.mem.each do |member|
+      current_user_groups[member] ||= Array.new
+      current_user_groups[member].push ent.name
+    end
+  end
+  Etc.endgrent()
 
   logins.each do |l|
     next if seen[l[:username]]
     seen[l[:username]] = true
 
+    username = l[:username]
+
     unless pwnam[l[:username]]
       STDERR.puts "Creating account #{l[:username]}"
-      groups = l[:groups] || []
-      # Adding users to the FUSE group has long been hardcoded behavior.
-      groups << "fuse"
-      groups.select! { |g| Etc.getgrnam(g) rescue false }
       # Create new user
       unless system("useradd", "-m",
-                "-c", l[:username],
+                "-c", username,
                 "-s", "/bin/bash",
-                "-G", groups.join(","),
-                l[:username],
-                out: devnull)
+                username)
         STDERR.puts "Account creation failed for #{l[:username]}: #{$?}"
         next
       end
       begin
-        pwnam[l[:username]] = Etc.getpwnam(l[:username])
+        pwnam[username] = Etc.getpwnam(username)
       rescue => e
         STDERR.puts "Created account but then getpwnam() failed for #{l[:username]}: #{e}"
         raise
       end
     end
 
-    @homedir = pwnam[l[:username]].dir
-    userdotssh = File.join(@homedir, ".ssh")
+    existing_groups = current_user_groups[username] || []
+    groups = l[:groups] || []
+    # Adding users to the FUSE group has long been hardcoded behavior.
+    groups << "fuse"
+    groups << username
+    groups.select! { |g| Etc.getgrnam(g) rescue false }
+
+    groups.each do |addgroup|
+      if existing_groups.index(addgroup).nil?
+        # User should be in group, but isn't, so add them.
+        STDERR.puts "Add user #{username} to #{addgroup} group"
+        system("adduser", username, addgroup)
+      end
+    end
+
+    existing_groups.each do |removegroup|
+      if groups.index(removegroup).nil?
+        # User is in a group, but shouldn't be, so remove them.
+        STDERR.puts "Remove user #{username} from #{removegroup} group"
+        system("deluser", username, removegroup)
+      end
+    end
+
+    homedir = pwnam[l[:username]].dir
+    userdotssh = File.join(homedir, ".ssh")
     Dir.mkdir(userdotssh) if !File.exist?(userdotssh)
 
     newkeys = "###\n###\n" + keys[l[:username]].join("\n") + "\n###\n###\n"
@@ -150,13 +177,41 @@ begin
       f.write(newkeys)
       f.close()
     end
+
+    userdotconfig = File.join(homedir, ".config")
+    if !File.exist?(userdotconfig)
+      Dir.mkdir(userdotconfig)
+    end
+
+    configarvados = File.join(userdotconfig, "arvados")
+    Dir.mkdir(configarvados) if !File.exist?(configarvados)
+
+    tokenfile = File.join(configarvados, "settings.conf")
+
+    begin
+      if !File.exist?(tokenfile)
+        user_token = arv.api_client_authorization.create(api_client_authorization: {owner_uuid: l[:user_uuid], api_client_id: 0})
+        f = File.new(tokenfile, 'w')
+        f.write("ARVADOS_API_HOST=#{ENV['ARVADOS_API_HOST']}\n")
+        f.write("ARVADOS_API_TOKEN=v2/#{user_token[:uuid]}/#{user_token[:api_token]}\n")
+        f.close()
+      end
+    rescue => e
+      STDERR.puts "Error setting token for #{l[:username]}: #{e}"
+    end
+
     FileUtils.chown_R(l[:username], nil, userdotssh)
+    FileUtils.chown_R(l[:username], nil, userdotconfig)
     File.chmod(0700, userdotssh)
-    File.chmod(0750, @homedir)
+    File.chmod(0700, userdotconfig)
+    File.chmod(0700, configarvados)
+    File.chmod(0750, homedir)
     File.chmod(0600, keysfile)
+    if File.exist?(tokenfile)
+      File.chmod(0600, tokenfile)
+    end
   end
 
-  devnull.close
 rescue Exception => bang
   puts "Error: " + bang.to_s
   puts bang.backtrace.join("\n")
index e90c16d64fae900df698c1db9d0cd6814022604b..db909ac83fc63bb2bbbca5aac7ea5c943a0a1c05 100644 (file)
@@ -16,20 +16,15 @@ class TestAddUser < Minitest::Test
     File.open(@tmpdir+'/succeed', 'w') do |f| end
     invoke_sync binstubs: ['new_user']
     spied = File.read(@tmpdir+'/spy')
-    assert_match %r{useradd -m -c active -s /bin/bash -G (fuse)? active}, spied
-    assert_match %r{useradd -m -c adminroot -s /bin/bash -G #{valid_groups.join(',')} adminroot}, spied
+    assert_match %r{useradd -m -c active -s /bin/bash active}, spied
+    assert_match %r{useradd -m -c adminroot -s /bin/bash adminroot}, spied
   end
 
   def test_useradd_success
     # binstub_new_user/useradd will succeed.
     File.open(@tmpdir+'/succeed', 'w') do |f|
-      f.puts 'useradd -m -c active -s /bin/bash -G fuse active'
-      f.puts 'useradd -m -c active -s /bin/bash -G  active'
-      # Accept either form; see note about groups in test_useradd_error.
-      f.puts 'useradd -m -c adminroot -s /bin/bash -G docker,fuse adminroot'
-      f.puts 'useradd -m -c adminroot -s /bin/bash -G docker,admin,fuse adminroot'
-      f.puts 'useradd -m -c adminroot -s /bin/bash -G docker adminroot'
-      f.puts 'useradd -m -c adminroot -s /bin/bash -G docker,admin adminroot'
+      f.puts 'useradd -m -c active -s /bin/bash -G active'
+      f.puts 'useradd -m -c adminroot -s /bin/bash adminroot'
     end
     $stderr.puts "*** Expect crash after getpwnam() fails:"
     invoke_sync binstubs: ['new_user']
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