<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>
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
# 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 )
+// 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
}
}
}
-
+
};
ShellInABox.prototype.about = 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');
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)) {
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.
vt100.indicateSize = true;
};
}(this), 100);
- this.addListener(window, 'resize',
+ this.addListener(window, 'resize',
function(vt100) {
return function() {
vt100.hideContextMenu();
vt100.showCurrentSize();
}
}(this));
-
+
// Hide extra scrollbars attached to window
document.body.style.margin = '0px';
try { document.body.style.overflow ='hidden'; } catch (e) { }
// 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;
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);
}
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);
}
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');
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 ||
}
this.setTextContent(span, s);
-
+
// Delete all subsequent <span>'s that have just been overwritten
sibling = span.nextSibling;
while (del > 0 && sibling) {
break;
}
}
-
+
// Merge <span> with next sibling, if styles are identical
if (sibling && span.className == sibling.className &&
span.style.cssText == sibling.style.cssText) {
this.getTextContent(span));
line.removeChild(sibling);
}
-
+
// Prune white space from the end of the current line
span = line.lastChild;
while (span &&
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.
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);
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;
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;
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;
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;
}
break;
}
// Fall through
- case 3 /* ESgetpars */:
+ case 3 /* ESgetpars */:
if (ch == 0x3B /*;*/) {
this.npar++;
break;
}
// Fall through
case 5 /* ESdeviceattr */:
- case 3 /* ESgetpars */:
+ case 3 /* ESgetpars */:
/*;*/ if (ch == 0x3B) {
this.npar++;
break;
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];
false, false, false, false, false, false, false, false,
false, false, false, true, false, false, false, false
];
-
-
-#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;
}
z-index: 2;
}
-#vt100 #reconnect input {
+#vt100 #reconnect input {
padding: 1ex;
font-weight: bold;
font-size: x-large;
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;
}
margin: -1px;
}
-#vt100 #padding {
+#vt100 #padding {
visibility: hidden;
width: 1px;
height: 0px;
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;
}
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
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
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 <<EOF
+<code>shellserver:# <span class="userinput">umask 0700; tee /etc/cron.d/arvados-login-sync <<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>
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 → Account menu → "SSH keys" item → "Add new SSH key" button.
# As an admin user, you have given the user permission to log in using the Workbench → Admin menu → "Users" item → "Show" button → "Admin" tab → "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
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;
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
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
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
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.
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.
* 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
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.
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
}
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 {
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,
// 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 {
"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()
}
if problems {
return 1
- } else {
- return 0
}
+ return 0
}
func warnAboutProblems(logger logrus.FieldLogger, cfg *arvados.Config) bool {
# 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
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: |
"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,
"Workbench.EnableGettingStartedPopup": true,
"Workbench.EnablePublicProjectsPage": true,
"Workbench.FileViewersConfigURL": true,
+ "Workbench.IdleTimeout": true,
"Workbench.InactivePageHTML": true,
"Workbench.LogViewerMaxBytes": true,
"Workbench.MultiSiteSearch": true,
# 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
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: |
}
}
+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.
}
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) {
}
}
+// 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))
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{
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)
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
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 {
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)
+}
if max > 0 {
ch := make(chan bool, max)
return func() { ch <- true }, func() { <-ch }
- } else {
- return func() {}, func() {}
}
+ return func() {}, func() {}
}
}
if walkMountsBelow {
return cp.walkMountsBelow(dest, src)
- } else {
- return nil
}
+ return nil
}
func (cp *copier) walkMountsBelow(dest, src string) error {
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) {
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] {
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
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 {
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{
}
queue.Update()
pool := stubPool{
+ quota: 1000,
unalloc: map[arvados.InstanceType]int{
test.InstanceType(1): 1,
test.InstanceType(2): 2,
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 {
}
}
-// 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{
}
queue.Update()
pool := stubPool{
- atQuota: quota == 0,
+ quota: quota,
unalloc: map[arvados.InstanceType]int{
test.InstanceType(2): 2,
},
}
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)
+ }
}
}
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,
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,
},
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)
}
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
}
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()
// 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() {
// 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{}
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 {
}
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()
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()
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.
// 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)
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() {
}
}
+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}
type TagVerifier struct {
cloud.Instance
- Secret string
+ Secret string
+ ReportVerified func(cloud.Instance)
}
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.
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) {
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")
// 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
InactivePageHTML string
SSHHelpPageHTML string
SSHHelpHostSuffix string
+ IdleTimeout Duration
}
ForceLegacyAPI14 bool
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
//
// 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 (
'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',
# 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
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
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
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
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,
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']
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
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']
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']
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
// 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{
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) {
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)
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"
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")
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']
--publish=25101:25101
--publish=8001:8001
--publish=8002:8002
+ --publish=4202:4202
--publish=45000-45020:45000-45020"
else
PUBLIC=""
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
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:
[doc]=8001
[websockets]=8005
[websockets-ssl]=8002
+ [webshell]=4201
+ [webshell-ssl]=4202
)
if test "$(id arvbox -u 2>/dev/null)" = 0 ; then
}
}
+
+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
--- /dev/null
+/usr/local/lib/arvbox/logger
\ No newline at end of file
--- /dev/null
+#!/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
--- /dev/null
+#!/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