Merge branch '17464-download-activity' refs #17464
authorPeter Amstutz <peter.amstutz@curii.com>
Wed, 30 Jun 2021 19:49:01 +0000 (15:49 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Wed, 30 Jun 2021 19:49:01 +0000 (15:49 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

56 files changed:
apps/workbench/Gemfile
apps/workbench/Gemfile.lock
apps/workbench/config/boot.rb
apps/workbench/test/integration/logins_test.rb
doc/_config.yml
doc/api/tokens_sso.html.textile.liquid [deleted file]
doc/install/salt-multi-host.html.textile.liquid
doc/install/salt.html.textile.liquid
doc/user/topics/storage-classes.html.textile.liquid
lib/cloud/ec2/ec2.go
lib/cloud/ec2/ec2_test.go
lib/config/config.default.yml
lib/config/deprecated.go
lib/config/export.go
lib/config/generated_config.go
lib/controller/handler_test.go
lib/controller/localdb/login.go
lib/controller/localdb/login_oidc_test.go
lib/controller/rpc/conn_test.go
sdk/cwl/arvados_cwl/arvtool.py
sdk/cwl/arvados_cwl/fsaccess.py
sdk/cwl/tests/17801-runtime-outdir.cwl [new file with mode: 0644]
sdk/cwl/tests/arvados-tests.yml
sdk/cwl/tests/test_submit.py
sdk/go/arvados/config.go
sdk/python/arvados/commands/put.py
sdk/python/tests/run_test_server.py
services/api/Gemfile
services/api/Gemfile.lock
services/api/app/assets/stylesheets/application.css
services/api/app/controllers/application_controller.rb
services/api/app/controllers/user_sessions_controller.rb
services/api/app/views/layouts/application.html.erb
services/api/app/views/static/intro.html.erb
services/api/app/views/static/login_failure.html.erb
services/api/app/views/user_sessions/failure.html.erb
services/api/config/arvados_config.rb
services/api/config/boot.rb
services/api/config/environment.rb
services/api/config/initializers/omniauth_init.rb [deleted file]
services/api/lib/josh_id.rb [deleted file]
services/api/test/functional/user_sessions_controller_test.rb
services/api/test/integration/login_workflow_test.rb
services/api/test/integration/user_sessions_test.rb
services/api/test/test_helper.rb
services/fuse/arvados_fuse/__init__.py
services/fuse/arvados_fuse/command.py
services/fuse/arvados_fuse/fusedir.py
services/fuse/tests/test_mount.py
services/fuse/tests/test_tmp_collection.py
services/keepproxy/keepproxy.go
services/keepproxy/keepproxy_test.go
tools/salt-install/local.params.example.multiple_hosts
tools/salt-install/local.params.example.single_host_multiple_hostnames
tools/salt-install/local.params.example.single_host_single_hostname
tools/salt-install/provision.sh

index 239c24d950efa7ebab1e4b905e3ba132f47df491..46c1e8e60c9c2d3db64c5b75f865392616762d17 100644 (file)
@@ -18,9 +18,6 @@ gem 'responders', '~> 2.0'
 # See: https://github.com/rails/sprockets-rails/issues/443
 gem 'sprockets', '~> 3.0'
 
-# Fast app boot times
-gem 'bootsnap', require: false
-
 # Note: keeping this out of the "group :assets" section "may" allow us
 # to use Coffescript for UJS responses. It also prevents a
 # warning/problem when running tests: "WARN: tilt autoloading
index 709e5eb3f6d5e1928bcb8105d1e112608138004a..178e5cdfe1844a60443cf86ec95d2ce128b24908 100644 (file)
@@ -87,8 +87,6 @@ GEM
       multi_json (>= 1.0.0)
     autoprefixer-rails (9.5.1.1)
       execjs
-    bootsnap (1.4.7)
-      msgpack (~> 1.0)
     bootstrap-sass (3.4.1)
       autoprefixer-rails (>= 5.2.1)
       sassc (>= 2.0.0)
@@ -186,7 +184,6 @@ GEM
       metaclass (~> 0.0.1)
     morrisjs-rails (0.5.1.2)
       railties (> 3.1, < 6)
-    msgpack (1.3.3)
     multi_json (1.15.0)
     multipart-post (2.1.1)
     net-scp (2.0.0)
@@ -333,7 +330,6 @@ DEPENDENCIES
   andand
   angularjs-rails (~> 1.3.8)
   arvados (~> 2.1.5)
-  bootsnap
   bootstrap-sass (~> 3.4.1)
   bootstrap-tab-history-rails
   bootstrap-x-editable-rails
@@ -383,4 +379,4 @@ DEPENDENCIES
   uglifier (~> 2.0)
 
 BUNDLED WITH
-   1.17.3
+   2.2.19
index 6add5911f6238f87ff72b91fef710fc05d9b67ba..8153266683f6161a8666f74843ce6810d093ffc0 100644 (file)
@@ -8,7 +8,6 @@ require 'rubygems'
 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
 
 require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
-require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
 
 # Use ARVADOS_API_TOKEN environment variable (if set) in console
 require 'rails'
index f079fbb8f1ce39eb4d88e33f232c3f8796dce89c..3c6ab7c7436e1ddb06621a01136ba85673ec78f2 100644 (file)
@@ -17,10 +17,7 @@ class LoginsTest < ActionDispatch::IntegrationTest
 
   test "trying to use expired token redirects to login page" do
     visit page_with_token('expired_trustedclient')
-    buttons = all("a.btn", text: /Log in/)
+    buttons = all("button.btn", text: /Log in/)
     assert_equal(1, buttons.size, "Failed to find one login button")
-    login_link = buttons.first[:href]
-    assert_match(%r{//[^/]+/login}, login_link)
-    assert_no_match(/\bapi_token=/, login_link)
   end
 end
index 55987c062fad7666e4541477b60584788fc7027f..06d15952131f118d1d7b79c2e80a97032ee5d3d5 100644 (file)
@@ -113,7 +113,6 @@ navbar:
       - api/requests.html.textile.liquid
       - api/methods.html.textile.liquid
       - api/resources.html.textile.liquid
-      - api/tokens_sso.html.textile.liquid
     - Permission and authentication:
       - api/methods/api_client_authorizations.html.textile.liquid
       - api/methods/api_clients.html.textile.liquid
diff --git a/doc/api/tokens_sso.html.textile.liquid b/doc/api/tokens_sso.html.textile.liquid
deleted file mode 100644 (file)
index 5d52465..0000000
+++ /dev/null
@@ -1,78 +0,0 @@
----
-layout: default
-navsection: api
-title: API Authorization with SSO (deprecated)
-...
-{% comment %}
-Copyright (C) The Arvados Authors. All rights reserved.
-
-SPDX-License-Identifier: CC-BY-SA-3.0
-{% endcomment %}
-
-{% include 'notebox_begin_warning' %}
-This page describes the deprecated login flow via the SSO server. SSO server authentication will be removed in a future Arvados release and should not be used for new installations. See "Set up web based login":{{site.baseurl}}/install/setup-login.html for more information.
-{% include 'notebox_end' %}
-
-All requests to the API server must have an API token.  API tokens can be issued by going though the login flow, or created via the API.  At this time, only browser based applications can perform login from email/password.  Command line applications and services must use an API token provided via the @ARVADOS_API_TOKEN@ environment variable or configuration file.
-
-h2. Browser login
-
-Browser based applications can perform log in via the following highlevel flow:
-
-# The web application presents a "login" link to @/login@ on the API server with a @return_to@ parameter provided in the query portion of the URL.  For example @https://{{ site.arvados_api_host }}/login?return_to=XXX@ , where  @return_to=XXX@ is the URL of the login page for the web application.
-# The "login" link takes the browser to the login page (this may involve several redirects)
-# The user logs in.  API server authenticates the user and issues a new API token.
-# The browser is redirected to the login page URL provided in @return_to=XXX@ with the addition of @?api_token=xxxxapitokenxxxx@.
-# The web application gets the login request with the included authorization token.
-
-!{{site.baseurl}}/images/Session_Establishment_with_SSO.svg!
-
-The "browser authentication process is documented in detail on the Arvados wiki.":https://dev.arvados.org/projects/arvados/wiki/Workbench_authentication_process
-
-h2. User activation
-
-"Creation and activation of new users is described here.":{{site.baseurl}}/admin/user-management.html
-
-h2. Creating tokens via the API
-
-The browser login method above issues a new token.  Using that token, it is possible to make API calls to create additional tokens.  To do so, use the @create@ method of the "API client authorizations":{{site.baseurl}}/api/methods/api_client_authorizations.html resource.
-
-h2. Trusted API clients
-
-The "api_clients":{{site.baseurl}}/api/methods/api_clients.html resource determines if web applications that have gone through the browser login flow may create or list API tokens.
-
-After the user has authenticated, but before an authorization token is issued and browser redirect sent (sending the browser back to the @return_to@ login page bearing @api_token@), the server strips the path and query portion from @return_to@ to get @url_prefix@.  The @url_prefix@ is used to find or create an ApiClient object.  The newly issued API client authorization (API token) is associated with this ApiClient object.
-
-API clients may be marked as "trusted" by making an API call to create or update an "api_clients":{{site.baseurl}}/api/methods/api_clients.html resource and set the @is_trusted@ flag to @true@. An authorization token associated with a "trusted" client is permitted to list authorization tokens on "API client authorizations":{{site.baseurl}}/api/methods/api_client_authorizations.html .
-
-A authorization token which is not associated with a trusted client may only use the @current@ method to query its own api_client_authorization object.  The "untrusted" token is forbidden performing any other operations on API client authorizations, such as listing other authorizations or creating new authorizations.
-
-Authorization tokens which are not issued via the browser login flow (created directly via the API) inherit the api client of the token used to create them.  They will always be "trusted" because untrusted API clients cannot create tokens.
-
-h2(#scopes). Scopes
-
-Scopes can restrict a token so it may only access certain resources.  This is in addition to normal permission checks for the user associated with the token.
-
-Each entry in scopes consists of a @request_method@ and @request_path@.  The @request_method@ is a HTTP method (one of @GET@, @POST@, @PATCH@ or @DELETE@) and @request_path@ is the request URI.  A given request is permitted if it matches a scopes exactly, or the scope ends with @/@ and the request string is a prefix of the scope.
-
-As a special case, a scope of @["all"]@ allows all resources.  This is the default if no scope is given.
-
-Using scopes is also described on the "Securing API access with scoped tokens":{{site.baseurl}}/admin/scoped-tokens.html page of the admin documentation.
-
-h3. Scope examples
-
-A scope of @GET /arvados/v1/collections@ permits listing collections.
-
-* Requests with different methods, such as creating a new collection using @POST /arvados/v1/collections@, will be rejected.
-* Requests to access other resources, such as @GET /arvados/v1/groups@, will be rejected.
-* Be aware that requests for specific records, such as @GET /arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km@ will also be rejected.  This is because the scope @GET /arvados/v1/collections@ does not end in @/@
-
-A scope of @GET /arvados/v1/collections/@ (with @/@ suffix) will permit access to individual collections.
-
-* The request @GET /arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km@ will succeed
-* Be aware that requests for listing @GET /arvados/v1/collections@ (no @/@ suffix) will be rejected, because it is not a match with the rule @GET /arvados/v1/collections/@
-* A listing request @GET /arvados/v1/collections/@ will have the trailing @/@ suffix trimmed before the scope check, as a result it will not match the rule @GET /arvados/v1/collections/@.
-
-To allow both listing objects and requesting individual objects, include both in the scope: @["GET /arvados/v1/collections", "GET /arvados/v1/collections/"]@
-
-A narrow scope such as @GET /arvados/v1/collections/962eh-4zz18-xi32mpz2621o8km@ will disallow listing objects as well as disallow requesting any object other than those listed in the scope.
index ed57807c727df9cca9833ec3ae4fb8f3017cfaae..89dfc1717d5cf7acdd01f53a9344a3665f0864eb 100644 (file)
@@ -9,6 +9,7 @@ Copyright (C) The Arvados Authors. All rights reserved.
 SPDX-License-Identifier: CC-BY-SA-3.0
 {% endcomment %}
 
+# "Introduction":#introduction
 # "Hosts preparation":#hosts_preparation
 ## "Hosts setup using terraform (experimental)":#hosts_setup_using_terraform
 ## "Create a compute image":#create_a_compute_image
@@ -21,6 +22,18 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 # "Initial user and login":#initial_user
 # "Test the installed cluster running a simple workflow":#test_install
 
+
+
+h2(#introduction). Introduction
+
+Arvados components can be installed in a distributed infrastructure, whether it is an "on-prem" with physical or virtual hosts, or a cloud environment.
+
+As infrastructures vary a great deal from site to site, these instructions should be considered more as 'guidelines' than fixed steps to follow.
+
+We provide an "installer script":salt.html that can help you deploy the different Arvados components. At the time of writing, the provided examples are suitable to install Arvados on AWS.
+
+
+
 h2(#hosts_preparation). Hosts preparation
 
 In order to run Arvados on a multi-host installation, there are a few requirements that your infrastructure has to fulfill.
@@ -50,10 +63,15 @@ We suggest distributing the Arvados components in the following way, creating at
 
 Note that these hosts can be virtual machines in your infrastructure and they don't need to be physical machines.
 
-h3(#hosts_setup_using_terraform). Hosts setup using terraform (experimental)
+Again, if your infrastructure differs from the setup proposed above (ie, using RDS or an existing DB server), remember that you will need to edit the configuration files for the scripts so they work with your infrastructure.
+
+
+h3(#hosts_setup_using_terraform). Hosts setup using terraform (AWS, experimental)
+
+We added a few "terraform":https://terraform.io/ scripts (https://github.com/arvados/arvados/tree/master/tools/terraform) to let you create these instances easier in an AWS account. Check "the Arvados terraform documentation":/doc/install/terraform.html for more details.
+
+
 
-We added a few "terraform":https://terraform.io/ scripts (https://github.com/arvados/arvados/tree/master/tools/terraform) to let you create these instances easier.
-Check "the Arvados terraform documentation":/doc/install/terraform.html for more details.
 
 h2(#multi_host). Multi host install using the provision.sh script
 
@@ -63,7 +81,7 @@ h2(#multi_host). Multi host install using the provision.sh script
 {% assign branchname = 'master' %}
 {% endif %}
 
-This is a package-based installation method. Start with the @provision.sh@ script which is available by cloning the @{{ branchname }}@ branch from "https://git.arvados.org/arvados.git":https://git.arvados.org/arvados.git .  The @provision.sh@ script and its supporting files can be found in the "arvados/tools/salt-install":https://git.arvados.org/arvados.git/tree/refs/heads/{{ branchname }}:/tools/salt-install directory in the Arvados git repository.
+This is a package-based installation method. Start with the @provision.sh@ script which is available by cloning the @{{ branchname }}@ branch from "https://git.arvados.org/arvados.git":https://git.arvados.org/arvados.git . The @provision.sh@ script and its supporting files can be found in the "arvados/tools/salt-install":https://git.arvados.org/arvados.git/tree/refs/heads/{{ branchname }}:/tools/salt-install directory in the Arvados git repository.
 
 This procedure will install all the main Arvados components to get you up and running in a multi-host environment.
 
@@ -73,7 +91,7 @@ After setting up a few variables in a config file (next step), you'll be ready t
 
 h3(#create_a_compute_image). Create a compute image
 
-In a multi-host installation, containers are dispatched in docker daemons running in the <i>compute instances</i>, which need some special setup. We provide a "compute image builder script":https://github.com/arvados/arvados/tree/master/tools/compute-images that you can use to build a template image following "these instructions":https://doc.arvados.org/main/install/crunch2-cloud/install-compute-node.html . Once you have that image created, you can use the image reference in the Arvados configuration in the next steps.
+In a multi-host installation, containers are dispatched in docker daemons running in the <i>compute instances</i>, which need some special setup. We provide a "compute image builder script":https://github.com/arvados/arvados/tree/master/tools/compute-images that you can use to build a template image following "these instructions":https://doc.arvados.org/main/install/crunch2-cloud/install-compute-node.html . Once you have that image created, you can use the image ID in the Arvados configuration in the next steps.
 
 h2(#choose_configuration). Choose the desired configuration
 
@@ -92,7 +110,7 @@ cp -r config_examples/multi_host/aws local_config_dir
 
 Edit the variables in the <i>local.params</i> file. Pay attention to the <b>*_INT_IP, *_TOKEN</b> and <b>*KEY</b> variables. Those variables will be used to do a search and replace on the <i>pillars/*</i> in place of any matching __VARIABLE__.
 
-The <i>multi_host</i> include LetsEncrypt salt code to automatically request and install the certificates for the public-facing hosts (API, Workbench) so it will need the hostnames to be reachable from the Internet. If this cluster will not be the case, please set the variable <i>USE_LETSENCRYPT=no</i>.
+The <i>multi_host</i> include LetsEncrypt salt code to automatically request and install the certificates for the public-facing hosts (API/controller, Workbench, Keepproxy/Keepweb) using AWS' Route53. If you will provide custom certificates, please set the variable <i>USE_LETSENCRYPT=no</i>.
 
 h3(#further_customization). Further customization of the installation (modifying the salt pillars and states)
 
index a9ee08fb886d0747ff5ffda161f323996d664165..d110f73000c1ad9eff4d56cf14c7ec5fcca18817 100644 (file)
@@ -1,7 +1,7 @@
 ---
 layout: default
 navsection: installguide
-title: Salt prerequisites
+title: Planning and prerequisites
 ...
 {% comment %}
 Copyright (C) The Arvados Authors. All rights reserved.
@@ -10,35 +10,107 @@ SPDX-License-Identifier: CC-BY-SA-3.0
 {% endcomment %}
 
 # "Introduction":#introduction
-# "Install Saltstack":#saltstack
-# "Choose an Arvados installation configuration":#installconfiguration
+# "Provisioning Arvados with Saltstack":#provisioning_arvados
+# "The provisioning tool files and directories":#provisioning_tool_files and directories
+# "Choose an Arvados installation configuration":#choose_configuration
+## "Further customization of the installation (modifying the salt pillars and states)":#further_customization
+# "Dump the configuration files created with the provision script":#dump_provision_config
+# "Add the Arvados formula to your Saltstack infrastructure":#add_formula_to_saltstack
 
 h2(#introduction). Introduction
 
-To ease the installation of the various Arvados components, we have developed a "Saltstack":https://www.saltstack.com/ 's "arvados-formula":https://github.com/arvados/arvados-formula.git which can help you get an Arvados cluster up and running.
+To ease the installation of the various Arvados components, we have developed a "Saltstack":https://www.saltstack.com/ 's "arvados-formula":https://git.arvados.org/arvados-formula.git which can help you get an Arvados cluster up and running.
 
-Saltstack is a Python-based, open-source software for event-driven IT automation, remote task execution, and configuration management. It can be used in a master/minion setup or master-less.
+Saltstack is a Python-based, open-source software for event-driven IT automation, remote task execution, and configuration management. It can be used in a _master/minion_ setup (where a master node orchestrates and coordinates the configuration of nodes in an infrastructure) or <i>master-less</i>, where Saltstack is run locally in a node, with no communication with a master node.
 
-This is a package-based installation method. The Salt scripts to install and configure Arvados using this formula are available at the "tools/salt-install":https://github.com/arvados/arvados/tree/master/tools/salt-install directory in the Arvados git repository.
+Similar to other configuration management tools like Puppet, Ansible or Chef, Saltstack uses files named <i>states</i> to describe the tasks that will be performed on a node to take it to a desired state, and <i>pillars</i> to configure variables passed to the states, adding flexibility to the tool.
 
-h2(#saltstack). Install Saltstack
+You don't need to be running a Saltstack infrastructure to install Arvados: we wrote a provisioning script that will take care of setting up Saltstack in the node/s where you want to install Arvados and run a <i>master-less installer</i>. Once Arvados is installed, you can either uninstall Saltstack and its files or you can keep them, to modify/maintain your Arvados installation in the future.
 
-If you already have a Saltstack environment or you plan to use the @provision.sh@ script we provide, you can skip this section.
+This is a package-based installation method.
 
-The simplest way to get Salt up and running on a node is to use the bootstrap script they provide:
 
+
+h2(#provisioning_arvados). Provisioning Arvados with Saltstack
+
+The "tools/salt-install":https://git.arvados.org/arvados.git/tree/{{ branchname }}:/tools/salt-install directory in the Arvados git repository contains a script that you can run in the node/s where you want to install Arvados' components (the @provision.sh@ script) and a few configuration examples for different setups, that you can use to customize your installation.
+
+The @provision.sh@ script will help you deploy Arvados by preparing your environment to be able to run the installer, then running it. The actual installer is located at "arvados-formula":https://git.arvados.org/arvados-formula.git/tree/refs/heads/{{ branchname }} and will be cloned during the running of the @provision.sh@ script. The installer is built using "Saltstack":https://saltproject.io/ and @provision.sh@ performs the install using master-less mode.
+
+After setting up a few variables in a config file and copying a directory from the examples (see below), you'll be ready to run it and get Arvados deployed.
+
+
+
+h2(#provisioning_tool_files and directories). The provisioning tool files and directories
+
+The "tools/salt-install":https://git.arvados.org/arvados.git/tree/{{ branchname }}:/tools/salt-install directory contains the following elements:
+
+* The @provision.sh@ script itself. You don't need to modify it.
+* A few @local.params.*@ example files. You will need to copy one of these files to a file named @local.params@, which is the main configuration file for the @provision.sh@ script.
+* A few @config_examples/*@ directories, with pillars and states templates. You need to copy one of these to a @local_config_dir@ directory, which will be used by the @provision.sh@ script to setup your nodes.
+* A @tests@ directory, with a simple workflow and arvados CLI commands you can run to tests your cluster is capable of running a CWL workflow, upload files and create a user.
+
+Once you decide on an Arvados architecture you want to apply, you need to copy one of the example configuration files and directory, and edit them to suit your needs.
+
+Ie., for a multi-hosts / multi-hostnames in AWS, you need to do this:
 <notextile>
-<pre><code>curl -L https://bootstrap.saltstack.com -o /tmp/bootstrap_salt.sh
-sudo sh /tmp/bootstrap_salt.sh -XUdfP -x python3
+<pre><code>cp local.params.example.multiple_hosts local.params
+cp -r config_examples/multi_host/aws local_config_dir
 </code></pre>
 </notextile>
 
-For more information check "Saltstack's documentation":https://docs.saltstack.com/en/latest/topics/installation/index.html
+These local files will be preserved if you upgrade the repository.
+
+
+
+h2(#choose_configuration). Choose an Arvados installation configuration
+
+The configuration examples provided with this installer are suitable to install Arvados with the following distribution of hosts/roles:
+
+* All roles on a single host, which can be done in two fashions:
+** Using a single hostname, assigning <i>a different port (other than 443) for each user-facing service</i>: This choice is easier to setup, but the user will need to know the port/s for the different services she wants to connect to. See "Single host install using the provision.sh script":salt-single-host.html for more details.
+** Using multiple hostnames on the same IP: this setup involves a few extra steps but each service will have a meaningful hostname so it will make easier to access them later. See "Single host install using the provision.sh script":salt-single-host.html for more details.
+* Roles distributed over multiple AWS instances, using multiple hostnames. This example can be adapted to use on-prem too. See "Multiple hosts installation":salt-multi-host.html for more details.
+
+Once you decide which of these choices you prefer, copy one of the example configuration files and directory, and edit them to suit your needs.
+
+Ie, if you decide to install Arvados on a single host using multiple hostnames:
+<notextile>
+<pre><code>cp local.params.example.single_host_multiple_hostnames local.params
+cp -r config_examples/single_host/multiple_hostnames local_config_dir
+</code></pre>
+</notextile>
+
+Edit the variables in the <i>local.params</i> file.
+
+
+
+h3(#further_customization). Further customization of the installation (modifying the salt pillars and states)
+
+If you want or need further customization, you can edit the Saltstack pillars and states files. Pay particular attention to the <i>pillars/arvados.sls</i> one. Any extra <i>state</i> file you add under <i>local_config_dir/states</i> will be added to the salt run and applied to the host.
+
+
+
+h2(#dump_provision_config). Dump the configuration files created with the provision script
+
+As mentioned above, the @provision.sh@ script helps you create a set of configuration files to be used by the Saltstack @arvados-formula@ and other helper formulas.
+
+Is it possible you want to inspect these files before deploying them or use them within your existing Saltstack environment. In order to get a rendered version of these files, the @provision.sh@ script has a option, @--dump-config@, which takes a directory as mandatory parameter. When this option it used, the script will create the specified directory and write the pillars, states and tests files so you can inspect them.
+
+Ie.
+<notextile>
+<pre><code>./provision.sh --dump-config ./config_dump --role workbench
+</code></pre>
+</notextile>
+
+will dump the configuration files used to install a workbench node under the @config_dump@ directory.
+
+These files are also suitable to be used in your existing Saltstack environment (see below).
+
+
 
-h2(#installconfiguration). Choose an Arvados installation configuration
+h2.(#add_formula_to_saltstack). Add the Arvados formula to your Saltstack infrastructure
 
-The salt formula can be used in a few different ways. Choose one of these three options to install Arvados:
+If you already have a Saltstack environment you can add the arvados-formula to your Saltstack master and apply the corresponding states and pillars to the nodes on your infrastructure that will be used to run Arvados.
 
-* "Arvados on a single host":salt-single-host.html
-* "Arvados across multiple hosts":salt-multi-host.html
-* "Use Vagrant to install Arvados in a virtual machine":salt-vagrant.html
+The @--dump-config@ option described above writes a @pillars/top.sls@ and @salt/top.sls@ files that you can use as a guide to configure your infrastructure.
index 96c808306272add476bcf428d481160d0562baec..99556af10aecfce48a9ac07c37e07959c44e169a 100644 (file)
@@ -16,10 +16,18 @@ Names of storage classes are internal to the cluster and decided by the administ
 
 h3. arv-put
 
-You may specify the desired storage class for a collection uploaded using @arv-put@:
+You may specify one or more desired storage classes for a collection uploaded using @arv-put@:
 
 <pre>
-$ arv-put --storage-classes=hot myfile.txt
+$ arv-put --storage-classes=hot,archival myfile.txt
+</pre>
+
+h3. arv-mount
+
+You can ask @arv-mount@ to use specific storage classes when creating new collections:
+
+<pre>
+$ arv-mount --storage-classes=transient --mount-tmp=scratch keep
 </pre>
 
 h3. arvados-cwl-runner
@@ -46,8 +54,6 @@ h3. Storage class notes
 
 Collection blocks will be in the "default" storage class if not otherwise specified.
 
-Currently, a collection may only have one desired storage class.
-
 Any user with write access to a collection may set any storage class on that collection.
 
 Names of storage classes are internal to the cluster and decided by the administrator.  Aside from "default", Arvados currently does not define any standard storage class names.
index 071c95006c9b305b1f47737bbb6eab588961785c..269a7d8def59a1e38603633691d657aef29d8e81 100644 (file)
@@ -20,6 +20,7 @@ import (
        "git.arvados.org/arvados.git/lib/cloud"
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "github.com/aws/aws-sdk-go/aws"
+       "github.com/aws/aws-sdk-go/aws/awserr"
        "github.com/aws/aws-sdk-go/aws/credentials"
        "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
        "github.com/aws/aws-sdk-go/aws/ec2metadata"
@@ -349,6 +350,31 @@ func (err rateLimitError) EarliestRetry() time.Time {
        return err.earliestRetry
 }
 
+var isCodeCapacity = map[string]bool{
+       "InsufficientInstanceCapacity": true,
+       "VcpuLimitExceeded":            true,
+       "MaxSpotInstanceCountExceeded": true,
+}
+
+// isErrorCapacity returns whether the error is to be throttled based on its code.
+// Returns false if error is nil.
+func isErrorCapacity(err error) bool {
+       if aerr, ok := err.(awserr.Error); ok && aerr != nil {
+               if _, ok := isCodeCapacity[aerr.Code()]; ok {
+                       return true
+               }
+       }
+       return false
+}
+
+type ec2QuotaError struct {
+       error
+}
+
+func (er *ec2QuotaError) IsQuotaError() bool {
+       return true
+}
+
 func wrapError(err error, throttleValue *atomic.Value) error {
        if request.IsErrorThrottle(err) {
                // Back off exponentially until an upstream call
@@ -362,6 +388,8 @@ func wrapError(err error, throttleValue *atomic.Value) error {
                }
                throttleValue.Store(d)
                return rateLimitError{error: err, earliestRetry: time.Now().Add(d)}
+       } else if isErrorCapacity(err) {
+               return &ec2QuotaError{err}
        } else if err != nil {
                throttleValue.Store(time.Duration(0))
                return err
index e7319a0cb66d7fe2e0132c9092f88ab5d5714a28..3cd238ded5a0035adaec66ad4d5c32b9c3fd816a 100644 (file)
@@ -25,6 +25,7 @@ package ec2
 import (
        "encoding/json"
        "flag"
+       "sync/atomic"
        "testing"
 
        "git.arvados.org/arvados.git/lib/cloud"
@@ -32,6 +33,7 @@ import (
        "git.arvados.org/arvados.git/sdk/go/arvados"
        "git.arvados.org/arvados.git/sdk/go/config"
        "github.com/aws/aws-sdk-go/aws"
+       "github.com/aws/aws-sdk-go/aws/awserr"
        "github.com/aws/aws-sdk-go/service/ec2"
        "github.com/sirupsen/logrus"
        check "gopkg.in/check.v1"
@@ -246,4 +248,14 @@ func (*EC2InstanceSetSuite) TestDestroyInstances(c *check.C) {
        }
 }
 
-var TestRateLimitErrorInterface cloud.RateLimitError = rateLimitError{}
+func (*EC2InstanceSetSuite) TestWrapError(c *check.C) {
+       retryError := awserr.New("Throttling", "", nil)
+       wrapped := wrapError(retryError, &atomic.Value{})
+       _, ok := wrapped.(cloud.RateLimitError)
+       c.Check(ok, check.Equals, true)
+
+       quotaError := awserr.New("InsufficientInstanceCapacity", "", nil)
+       wrapped = wrapError(quotaError, nil)
+       _, ok = wrapped.(cloud.QuotaError)
+       c.Check(ok, check.Equals, true)
+}
index ff2f649faca88cdb4e77c76589871d38c1741f22..e28d5cbb7f0cd09b3ad559f6cab0c9a9967c10d5 100644 (file)
@@ -52,9 +52,6 @@ Clusters:
       DispatchCloud:
         InternalURLs: {SAMPLE: {}}
         ExternalURL: "-"
-      SSO:
-        InternalURLs: {SAMPLE: {}}
-        ExternalURL: ""
       Keepproxy:
         InternalURLs: {SAMPLE: {}}
         ExternalURL: ""
@@ -583,7 +580,7 @@ Clusters:
       WebDAVLogEvents: true
 
     Login:
-      # One of the following mechanisms (SSO, Google, PAM, LDAP, or
+      # One of the following mechanisms (Google, PAM, LDAP, or
       # LoginCluster) should be enabled; see
       # https://doc.arvados.org/install/setup-login.html
 
@@ -764,16 +761,6 @@ Clusters:
         # originally supplied by the user will be used.
         UsernameAttribute: uid
 
-      SSO:
-        # Authenticate with a separate SSO server. (Deprecated)
-        Enable: false
-
-        # ProviderAppID and ProviderAppSecret are generated during SSO
-        # setup; see
-        # https://doc.arvados.org/v2.0/install/install-sso.html#update-config
-        ProviderAppID: ""
-        ProviderAppSecret: ""
-
       Test:
         # Authenticate users listed here in the config file. This
         # feature is intended to be used in test environments, and
index 5e68bbfcefa7950163791d66a6a533bc65c19097..efc9f0837ea531872d92f551c8030e4f9241def4 100644 (file)
@@ -103,18 +103,6 @@ func (ldr *Loader) applyDeprecatedConfig(cfg *arvados.Config) error {
                        *dst = *n
                }
 
-               // Provider* moved to SSO.Provider*
-               if dst, n := &cluster.Login.SSO.ProviderAppID, dcluster.Login.ProviderAppID; n != nil && *n != *dst {
-                       *dst = *n
-                       if *n != "" {
-                               // In old config, non-empty ID meant enable
-                               cluster.Login.SSO.Enable = true
-                       }
-               }
-               if dst, n := &cluster.Login.SSO.ProviderAppSecret, dcluster.Login.ProviderAppSecret; n != nil && *n != *dst {
-                       *dst = *n
-               }
-
                cfg.Clusters[id] = cluster
        }
        return nil
index 62262c7ed3dfc4e0ffd9816c3dba3630f7077f81..8753b52f27e17dee80958fd32ab25e46cda2f9fb 100644 (file)
@@ -176,10 +176,6 @@ var whitelist = map[string]bool{
        "Login.PAM.Enable":                                    true,
        "Login.PAM.Service":                                   false,
        "Login.RemoteTokenRefresh":                            true,
-       "Login.SSO":                                           true,
-       "Login.SSO.Enable":                                    true,
-       "Login.SSO.ProviderAppID":                             false,
-       "Login.SSO.ProviderAppSecret":                         false,
        "Login.Test":                                          true,
        "Login.Test.Enable":                                   true,
        "Login.Test.Users":                                    false,
index 34cc07115a56599f8e9e9d1466dfde8ffc28e668..b15bf7eebc29facda6f1a0e2670c5482f83055cf 100644 (file)
@@ -58,9 +58,6 @@ Clusters:
       DispatchCloud:
         InternalURLs: {SAMPLE: {}}
         ExternalURL: "-"
-      SSO:
-        InternalURLs: {SAMPLE: {}}
-        ExternalURL: ""
       Keepproxy:
         InternalURLs: {SAMPLE: {}}
         ExternalURL: ""
@@ -589,7 +586,7 @@ Clusters:
       WebDAVLogEvents: true
 
     Login:
-      # One of the following mechanisms (SSO, Google, PAM, LDAP, or
+      # One of the following mechanisms (Google, PAM, LDAP, or
       # LoginCluster) should be enabled; see
       # https://doc.arvados.org/install/setup-login.html
 
@@ -770,16 +767,6 @@ Clusters:
         # originally supplied by the user will be used.
         UsernameAttribute: uid
 
-      SSO:
-        # Authenticate with a separate SSO server. (Deprecated)
-        Enable: false
-
-        # ProviderAppID and ProviderAppSecret are generated during SSO
-        # setup; see
-        # https://doc.arvados.org/v2.0/install/install-sso.html#update-config
-        ProviderAppID: ""
-        ProviderAppSecret: ""
-
       Test:
         # Authenticate users listed here in the config file. This
         # feature is intended to be used in test environments, and
index 2911a4f031cdac7aef14a80ecff42f349ddd0011..9b71c349a4b5624cf32cdf3eb6bba83d06d737bc 100644 (file)
@@ -164,34 +164,6 @@ func (s *HandlerSuite) TestProxyNotFound(c *check.C) {
        c.Check(jresp["errors"], check.FitsTypeOf, []interface{}{})
 }
 
-func (s *HandlerSuite) TestProxyRedirect(c *check.C) {
-       s.cluster.Login.SSO.Enable = true
-       s.cluster.Login.SSO.ProviderAppID = "test"
-       s.cluster.Login.SSO.ProviderAppSecret = "test"
-       req := httptest.NewRequest("GET", "https://0.0.0.0:1/login?return_to=foo", nil)
-       resp := httptest.NewRecorder()
-       s.handler.ServeHTTP(resp, req)
-       if !c.Check(resp.Code, check.Equals, http.StatusFound) {
-               c.Log(resp.Body.String())
-       }
-       // Old "proxy entire request" code path returns an absolute
-       // URL. New lib/controller/federation code path returns a
-       // relative URL.
-       c.Check(resp.Header().Get("Location"), check.Matches, `(https://0.0.0.0:1)?/auth/joshid\?return_to=%2Cfoo&?`)
-}
-
-func (s *HandlerSuite) TestLogoutSSO(c *check.C) {
-       s.cluster.Login.SSO.Enable = true
-       s.cluster.Login.SSO.ProviderAppID = "test"
-       req := httptest.NewRequest("GET", "https://0.0.0.0:1/logout?return_to=https://example.com/foo", nil)
-       resp := httptest.NewRecorder()
-       s.handler.ServeHTTP(resp, req)
-       if !c.Check(resp.Code, check.Equals, http.StatusFound) {
-               c.Log(resp.Body.String())
-       }
-       c.Check(resp.Header().Get("Location"), check.Equals, "http://localhost:3002/users/sign_out?"+url.Values{"redirect_uri": {"https://example.com/foo"}}.Encode())
-}
-
 func (s *HandlerSuite) TestLogoutGoogle(c *check.C) {
        s.cluster.Login.Google.Enable = true
        s.cluster.Login.Google.ClientID = "test"
index 0d6f2ef027e8500c60fdf644e8f808fecd2f226a..3c7b01baad1361735ebe37b4ef6df7157d1eb750 100644 (file)
@@ -30,15 +30,14 @@ type loginController interface {
 func chooseLoginController(cluster *arvados.Cluster, parent *Conn) loginController {
        wantGoogle := cluster.Login.Google.Enable
        wantOpenIDConnect := cluster.Login.OpenIDConnect.Enable
-       wantSSO := cluster.Login.SSO.Enable
        wantPAM := cluster.Login.PAM.Enable
        wantLDAP := cluster.Login.LDAP.Enable
        wantTest := cluster.Login.Test.Enable
        wantLoginCluster := cluster.Login.LoginCluster != "" && cluster.Login.LoginCluster != cluster.ClusterID
        switch {
-       case 1 != countTrue(wantGoogle, wantOpenIDConnect, wantSSO, wantPAM, wantLDAP, wantTest, wantLoginCluster):
+       case 1 != countTrue(wantGoogle, wantOpenIDConnect, wantPAM, wantLDAP, wantTest, wantLoginCluster):
                return errorLoginController{
-                       error: errors.New("configuration problem: exactly one of Login.Google, Login.OpenIDConnect, Login.SSO, Login.PAM, Login.LDAP, Login.Test, or Login.LoginCluster must be set"),
+                       error: errors.New("configuration problem: exactly one of Login.Google, Login.OpenIDConnect, Login.PAM, Login.LDAP, Login.Test, or Login.LoginCluster must be set"),
                }
        case wantGoogle:
                return &oidcLoginController{
@@ -66,8 +65,6 @@ func chooseLoginController(cluster *arvados.Cluster, parent *Conn) loginControll
                        AcceptAccessToken:      cluster.Login.OpenIDConnect.AcceptAccessToken,
                        AcceptAccessTokenScope: cluster.Login.OpenIDConnect.AcceptAccessTokenScope,
                }
-       case wantSSO:
-               return &ssoLoginController{Parent: parent}
        case wantPAM:
                return &pamLoginController{Cluster: cluster, Parent: parent}
        case wantLDAP:
@@ -93,20 +90,6 @@ func countTrue(vals ...bool) int {
        return n
 }
 
-// Login and Logout are passed through to the parent's railsProxy;
-// UserAuthenticate is rejected.
-type ssoLoginController struct{ Parent *Conn }
-
-func (ctrl *ssoLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
-       return ctrl.Parent.railsProxy.Login(ctx, opts)
-}
-func (ctrl *ssoLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
-       return ctrl.Parent.railsProxy.Logout(ctx, opts)
-}
-func (ctrl *ssoLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
-       return arvados.APIClientAuthorization{}, httpserver.ErrorWithStatus(errors.New("username/password authentication is not available"), http.StatusBadRequest)
-}
-
 type errorLoginController struct{ error }
 
 func (ctrl errorLoginController) Login(context.Context, arvados.LoginOptions) (arvados.LoginResponse, error) {
index c9d6133c480319b9129397ea076068d67bb4a3f5..4be7d58f699c455ee67e31a206bcebfc889ed0ad 100644 (file)
@@ -63,7 +63,7 @@ func (s *OIDCLoginSuite) SetUpTest(c *check.C) {
        c.Assert(err, check.IsNil)
        s.cluster, err = cfg.GetCluster("")
        c.Assert(err, check.IsNil)
-       s.cluster.Login.SSO.Enable = false
+       s.cluster.Login.Test.Enable = false
        s.cluster.Login.Google.Enable = true
        s.cluster.Login.Google.ClientID = "test%client$id"
        s.cluster.Login.Google.ClientSecret = "test#client/secret"
index cf4dbc47673e7713e8f9d77c2ebbb449077e4447..eee8db9ac830588754956afc62806b757d72b89a 100644 (file)
@@ -50,9 +50,8 @@ func (s *RPCSuite) TestLogin(c *check.C) {
        opts := arvados.LoginOptions{
                ReturnTo: "https://foo.example.com/bar",
        }
-       resp, err := s.conn.Login(s.ctx, opts)
-       c.Check(err, check.IsNil)
-       c.Check(resp.RedirectLocation, check.Equals, "/auth/joshid?return_to="+url.QueryEscape(","+opts.ReturnTo))
+       _, err := s.conn.Login(s.ctx, opts)
+       c.Check(err.(*arvados.TransactionError).StatusCode, check.Equals, 404)
 }
 
 func (s *RPCSuite) TestLogout(c *check.C) {
@@ -62,7 +61,7 @@ func (s *RPCSuite) TestLogout(c *check.C) {
        }
        resp, err := s.conn.Logout(s.ctx, opts)
        c.Check(err, check.IsNil)
-       c.Check(resp.RedirectLocation, check.Equals, "http://localhost:3002/users/sign_out?redirect_uri="+url.QueryEscape(opts.ReturnTo))
+       c.Check(resp.RedirectLocation, check.Equals, opts.ReturnTo)
 }
 
 func (s *RPCSuite) TestCollectionCreate(c *check.C) {
index a9361a85f9fe66a5260a64b6719fa74c28cdeee2..89176923519ee4c737d9376032463dee08a80102 100644 (file)
@@ -15,7 +15,16 @@ def validate_cluster_target(arvrunner, runtimeContext):
         runtimeContext.submit_runner_cluster not in arvrunner.api._rootDesc["remoteHosts"] and
         runtimeContext.submit_runner_cluster != arvrunner.api._rootDesc["uuidPrefix"]):
         raise WorkflowException("Unknown or invalid cluster id '%s' known remote clusters are %s" % (runtimeContext.submit_runner_cluster,
-                                                                                                  ", ".join(list(arvrunner.api._rootDesc["remoteHosts"].keys()))))
+                                                                                                     ", ".join(list(arvrunner.api._rootDesc["remoteHosts"].keys()))))
+    if runtimeContext.project_uuid:
+        cluster_target = runtimeContext.submit_runner_cluster or arvrunner.api._rootDesc["uuidPrefix"]
+        if not runtimeContext.project_uuid.startswith(cluster_target):
+            raise WorkflowException("Project uuid '%s' must be for target cluster '%s'" % (runtimeContext.project_uuid, cluster_target))
+        try:
+            arvrunner.api.groups().get(uuid=runtimeContext.project_uuid).execute()
+        except Exception as e:
+            raise WorkflowException("Invalid project uuid '%s': %s" % (runtimeContext.project_uuid, e))
+
 def set_cluster_target(tool, arvrunner, builder, runtimeContext):
     cluster_target_req = None
     for field in ("hints", "requirements"):
index 1e339d5bb7d4ab20f90438ed5670f3d67d51e548..4da8f855692aed44f212739d5e515af3fef2ceb0 100644 (file)
@@ -129,7 +129,7 @@ class CollectionFsAccess(cwltool.stdfsaccess.StdFsAccess):
 
     def glob(self, pattern):
         collection, rest = self.get_collection(pattern)
-        if collection is not None and not rest:
+        if collection is not None and rest in (None, "", "."):
             return [pattern]
         patternsegments = rest.split("/")
         return sorted(self._match(collection, patternsegments, "keep:" + collection.manifest_locator()))
diff --git a/sdk/cwl/tests/17801-runtime-outdir.cwl b/sdk/cwl/tests/17801-runtime-outdir.cwl
new file mode 100644 (file)
index 0000000..c01ef05
--- /dev/null
@@ -0,0 +1,15 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: Apache-2.0
+
+cwlVersion: v1.2
+class: CommandLineTool
+inputs: []
+outputs:
+  stuff:
+    type: Directory
+    outputBinding:
+      glob: $(runtime.outdir)
+requirements:
+  ShellCommandRequirement: {}
+arguments: [{shellQuote: false, valueFrom: "mkdir -p foo && touch baz.txt && touch foo/bar.txt"}]
index 41feb7796baf4301af1bb6316952c3c3020f1b45..b22c9aaa27684084ef088f95f119badf1d04e598 100644 (file)
   }
   tool: 10380-trailing-slash-dir.cwl
   doc: "Test issue 10380 - bug with trailing slash when capturing an output directory"
+
+- job: null
+  output: {
+    "stuff": {
+        "basename": "78f3957c41d044352303a3fa326dff1e+102",
+        "class": "Directory",
+        "listing": [
+            {
+                "basename": "baz.txt",
+                "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709",
+                "class": "File",
+                "location": "78f3957c41d044352303a3fa326dff1e+102/baz.txt",
+                "size": 0
+            },
+            {
+                "basename": "foo",
+                "class": "Directory",
+                "listing": [
+                    {
+                        "basename": "bar.txt",
+                        "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709",
+                        "class": "File",
+                        "location": "78f3957c41d044352303a3fa326dff1e+102/foo/bar.txt",
+                        "size": 0
+                    }
+                ],
+                "location": "78f3957c41d044352303a3fa326dff1e+102/foo"
+            }
+        ],
+        "location": "78f3957c41d044352303a3fa326dff1e+102"
+    }
+  }
+  tool: 17801-runtime-outdir.cwl
+  doc: "Test issue 17801 - bug using $(runtime.outdir) to capture the output directory"
index 05515e0980cb15e228844001cfe385814883f7de..c448f218bc5d7d0029c9b1ce359e44887053a8a6 100644 (file)
@@ -1263,6 +1263,21 @@ class TestSubmit(unittest.TestCase):
             stubs.capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
         self.assertEqual(exited, 1)
 
+    @stubs
+    def test_submit_validate_project_uuid(self, stubs):
+        exited = arvados_cwl.main(
+            ["--submit", "--no-wait", "--api=containers", "--debug", "--project-uuid=zzzzb-j7d0g-zzzzzzzzzzzzzzz",
+             "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
+            stubs.capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
+        self.assertEqual(exited, 1)
+
+        stubs.api.groups().get().execute.side_effect = Exception("Bad project")
+        exited = arvados_cwl.main(
+            ["--submit", "--no-wait", "--api=containers", "--debug", "--project-uuid=zzzzz-j7d0g-zzzzzzzzzzzzzzx",
+             "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
+            stubs.capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
+        self.assertEqual(exited, 1)
+
     @mock.patch("arvados.collection.CollectionReader")
     @stubs
     def test_submit_uuid_inputs(self, stubs, collectionReader):
index 13fe989caee142b78a7e58423072a9bdfd56fa35..6e59828a3cbf5656fef1e6c7fc790ca9d3b6268f 100644 (file)
@@ -190,11 +190,6 @@ type Cluster struct {
                        Service            string
                        DefaultEmailDomain string
                }
-               SSO struct {
-                       Enable            bool
-                       ProviderAppID     string
-                       ProviderAppSecret string
-               }
                Test struct {
                        Enable bool
                        Users  map[string]TestUser
@@ -341,7 +336,6 @@ type Services struct {
        Keepproxy      Service
        Keepstore      Service
        RailsAPI       Service
-       SSO            Service
        WebDAVDownload Service
        WebDAV         Service
        WebShell       Service
index 4f0ac1fffaf9c4ee6875c0092ba1090d6e3eb9a6..ad04807712dd00d7e89e478be1a7eb2c01d0b057 100644 (file)
@@ -1301,7 +1301,7 @@ def main(arguments=None, stdout=sys.stdout, stderr=sys.stderr,
     output = None
     try:
         writer.start(save_collection=not(args.stream or args.raw))
-    except arvados.errors.ApiError as error:
+    except (arvados.errors.ApiError, arvados.errors.KeepWriteError) as error:
         logger.error("\n".join([
             "arv-put: %s" % str(error)]))
         sys.exit(1)
index 917d6100ae211853d379bcc04bbd4a591f3e625f..c022e6c874d23f099406b626374e7cfe5d0951c0 100644 (file)
@@ -755,9 +755,6 @@ def setup_config():
                 "http://%s:%s"%(localhost, keep_web_dl_port): {},
             },
         },
-        "SSO": {
-            "ExternalURL": "http://localhost:3002",
-        },
     }
 
     config = {
@@ -769,10 +766,14 @@ def setup_config():
                     "RequestTimeout": "30s",
                 },
                 "Login": {
-                    "SSO": {
+                    "Test": {
                         "Enable": True,
-                        "ProviderAppID": "arvados-server",
-                        "ProviderAppSecret": "608dbf356a327e2d0d4932b60161e212c2d8d8f5e25690d7b622f850a990cd33",
+                        "Users": {
+                            "alice": {
+                                "Email": "alice@example.com",
+                                "Password": "xyzzy"
+                            }
+                        }
                     },
                 },
                 "SystemLogs": {
index ae1658123615794fc699c840c0ade42552ad5052..39ce5def17e0feddaafc04a91c7a1bfeadd997a6 100644 (file)
@@ -25,9 +25,6 @@ group :test, :development do
   gem 'listen'
 end
 
-# Fast app boot times
-gem 'bootsnap', require: false
-
 gem 'pg', '~> 1.0'
 
 gem 'multi_json'
@@ -47,10 +44,6 @@ gem 'passenger'
 # Locking to 5.10.3 to workaround issue in 5.11.1 (https://github.com/seattlerb/minitest/issues/730)
 gem 'minitest', '5.10.3'
 
-# Restricted because omniauth >= 1.5.0 requires Ruby >= 2.1.9:
-gem 'omniauth', '~> 1.4.0'
-gem 'omniauth-oauth2', '~> 1.1'
-
 gem 'andand'
 
 gem 'optimist'
index 992ff39c099a1c462e9fa3de7bd08a48990439f0..ddecd4a18a785281252b95155d7bcd4cde1509cc 100644 (file)
@@ -80,8 +80,6 @@ GEM
       addressable (>= 2.3.1)
       extlib (>= 0.9.15)
       multi_json (>= 1.0.0)
-    bootsnap (1.4.7)
-      msgpack (~> 1.0)
     builder (3.2.4)
     byebug (11.0.1)
     capistrano (2.15.9)
@@ -112,7 +110,6 @@ GEM
       multi_json (~> 1.11)
       os (>= 0.9, < 2.0)
       signet (~> 0.7)
-    hashie (3.6.0)
     highline (2.0.1)
     httpclient (2.8.3)
     i18n (0.9.5)
@@ -149,9 +146,7 @@ GEM
     minitest (5.10.3)
     mocha (1.8.0)
       metaclass (~> 0.0.1)
-    msgpack (1.3.3)
     multi_json (1.15.0)
-    multi_xml (0.6.0)
     multipart-post (2.1.1)
     net-scp (2.0.0)
       net-ssh (>= 2.6.5, < 6.0.0)
@@ -164,19 +159,7 @@ GEM
     nokogiri (1.11.7)
       mini_portile2 (~> 2.5.0)
       racc (~> 1.4)
-    oauth2 (1.4.1)
-      faraday (>= 0.8, < 0.16.0)
-      jwt (>= 1.0, < 3.0)
-      multi_json (~> 1.3)
-      multi_xml (~> 0.5)
-      rack (>= 1.2, < 3)
     oj (3.9.2)
-    omniauth (1.4.3)
-      hashie (>= 1.2, < 4)
-      rack (>= 1.6.2, < 3)
-    omniauth-oauth2 (1.5.0)
-      oauth2 (~> 1.1)
-      omniauth (~> 1.2)
     optimist (3.0.0)
     os (1.1.1)
     passenger (6.0.2)
@@ -289,7 +272,6 @@ DEPENDENCIES
   acts_as_api
   andand
   arvados (~> 2.1.5)
-  bootsnap
   byebug
   factory_bot_rails
   httpclient
@@ -301,8 +283,6 @@ DEPENDENCIES
   mocha
   multi_json
   oj
-  omniauth (~> 1.4.0)
-  omniauth-oauth2 (~> 1.1)
   optimist
   passenger
   pg (~> 1.0)
@@ -325,4 +305,4 @@ DEPENDENCIES
   uglifier (~> 2.0)
 
 BUNDLED WITH
-   1.17.3
+   2.2.19
index 721ff801c91c5aba35337fbd7a8efab7729d9f14..18895f6efc275cc019a5788ed9bbb6c3d88cf18c 100644 (file)
@@ -62,7 +62,7 @@ div#header span.beta > span {
     border-bottom: 1px solid #fff;
     font-size: 0.8em;
 }
-img.curoverse-logo {
+img.arvados-logo {
     height: 66px;
 }
 #intropage {
index e1ae76ed29f7e6d58862f7d1bffd464c63644a93..fc33dde4477b45d059db2cbd7a63f919eb67e167 100644 (file)
@@ -397,7 +397,7 @@ class ApplicationController < ActionController::Base
     if not current_user
       respond_to do |format|
         format.json { send_error("Not logged in", status: 401) }
-        format.html { redirect_to '/auth/joshid' }
+        format.html { redirect_to '/login' }
       end
       false
     end
index 8e9a26b7a88f3207c4ab1cb76f2aba990d6cce9c..ae34fa76006aabe6c7866cee09d8249f58254567 100644 (file)
@@ -11,7 +11,7 @@ class UserSessionsController < ApplicationController
 
   respond_to :html
 
-  # omniauth callback method
+  # create a new session
   def create
     if !Rails.configuration.Login.LoginCluster.empty? and Rails.configuration.Login.LoginCluster != Rails.configuration.ClusterID
       raise "Local login disabled when LoginCluster is set"
@@ -27,9 +27,7 @@ class UserSessionsController < ApplicationController
       authinfo = SafeJSON.load(params[:auth_info])
       max_expires_at = authinfo["expires_at"]
     else
-      # omniauth middleware verified the user and is passing auth_info
-      # in request.env.
-      authinfo = request.env['omniauth.auth']['info'].with_indifferent_access
+      return send_error "Legacy code path no longer supported", status: 404
     end
 
     if !authinfo['user_uuid'].blank?
@@ -92,19 +90,17 @@ class UserSessionsController < ApplicationController
     flash[:notice] = params[:message]
   end
 
-  # logout - Clear our rack session BUT essentially redirect to the provider
-  # to clean up the Devise session from there too !
+  # logout - this gets intercepted by controller, so this is probably
+  # mostly dead code at this point.
   def logout
     session[:user_id] = nil
 
     flash[:notice] = 'You have logged off'
     return_to = params[:return_to] || root_url
-    redirect_to "#{Rails.configuration.Services.SSO.ExternalURL}users/sign_out?redirect_uri=#{CGI.escape return_to}"
+    redirect_to return_to
   end
 
-  # login - Just bounce to /auth/joshid. The only purpose of this function is
-  # to save the return_to parameter (if it exists; see the application
-  # controller). /auth/joshid bypasses the application controller.
+  # login.  Redirect to LoginCluster.
   def login
     if params[:remote] !~ /^[0-9a-z]{5}$/ && !params[:remote].nil?
       return send_error 'Invalid remote cluster id', status: 400
@@ -136,13 +132,7 @@ class UserSessionsController < ApplicationController
       p << "return_to=#{CGI.escape(params[:return_to])}" if params[:return_to]
       redirect_to "#{login_cluster}/login?#{p.join('&')}"
     else
-      if params[:return_to]
-        # Encode remote param inside callback's return_to, so that we'll get it on
-        # create() after login.
-        remote_param = params[:remote].nil? ? '' : params[:remote]
-        p << "return_to=#{CGI.escape(remote_param + ',' + params[:return_to])}"
-      end
-      redirect_to "/auth/joshid?#{p.join('&')}"
+      return send_error "Legacy code path no longer supported", status: 404
     end
   end
 
index a99b6f165dd74864c160df8597c06eecc15f5477..b4f60de3469cab52848badac471a9e8f25b6cb50 100644 (file)
@@ -23,8 +23,6 @@ SPDX-License-Identifier: AGPL-3.0 %>
     <% end %>
     &nbsp;&bull;&nbsp;
     <a class="logout" href="/logout">Log out</a>
-    <% else %>
-      <!--<a class="logout" href="/auth/joshid">Log in</a>-->
     <% end %>
 
     <% if current_user and session[:real_uid] and session[:switch_back_to] and User.find(session[:real_uid].to_i).verify_userswitch_cookie(session[:switch_back_to]) %>
@@ -41,7 +39,6 @@ SPDX-License-Identifier: AGPL-3.0 %>
 
 <% if current_user or session['invite_code'] %>
 <div id="footer">
-  <div style="float:right">Questions &rarr; <a href="mailto:arvados@curoverse.com">arvados@curoverse.com</a></div>
   <div style="clear:both"></div>
 </div>
 <% end %>
index bdefaa5c1f19bfae8357950038f328e6aa22da2c..8bab0b2ac64d8b217ffa22d0584a8e7b30e48018 100644 (file)
@@ -8,30 +8,23 @@ $(function(){
 });
 <% end %>
 <div id="intropage">
-  <img class="curoverse-logo" src="<%= asset_path('logo.png') %>" style="display:block; margin:2em auto"/>
+  <img class="arvados-logo" src="<%= asset_path('logo.png') %>" style="display:block; margin:2em auto"/>
   <div style="width:30em; margin:2em auto 0 auto">
     <h1>Welcome</h1>
-    <h4>Curoverse ARVADOS</h4>
+    <h4>ARVADOS</h4>
 
     <% if !current_user and session['invite_code'] %>
 
-    <p>Curoverse Arvados lets you manage and process human genomes and exomes.  You can start using the private beta
-    now with your Google account.</p>
+    <p>Arvados lets you manage and process biomedical data.</p>
     <p style="float:right;margin-top:1em">
-      <button class="login" href="/auth/joshid">Log in and get started</button>
+      <button class="login" href="/login">Log in and get started</button>
     </p>
 
     <% else %>
 
-    <p>Curoverse ARVADOS is transforming how researchers and
-    clinical geneticists use whole genome sequences. </p>
-    <p>If you&rsquo;re interested in learning more, we&rsquo;d love to hear
-    from you &mdash;
-    contact <a href="mailto:arvados@curoverse.com">arvados@curoverse.com</a>.</p>
-
     <% if !current_user %>
     <p style="float:right;margin-top:1em">
-      <a href="/auth/joshid">Log in here.</a>
+      <a href="/login">Log in here.</a>
     </p>
     <% end %>
 
index b3c6e70d907f4e460096ecbb06aaa6ccc7ddcd87..6b81a33e875b135a3fabdb12b458ad23386a78c3 100644 (file)
@@ -10,7 +10,7 @@ $(function(){
 
 
 <div id="intropage">
-  <img class="curoverse-logo" src="<%= asset_path('logo.png') rescue '/logo.png' %>" style="display:block; margin:2em auto"/>
+  <img class="arvados-logo" src="<%= asset_path('logo.png') rescue '/logo.png' %>" style="display:block; margin:2em auto"/>
   <div style="width:30em; margin:2em auto 0 auto">
 
     <h1>Error</h1>
index 81c5be27c642e948698b6073f201dac2e1a3522d..e8c5b0846583c9e8dfdae899cb4cd5123a2e63c8 100644 (file)
@@ -7,4 +7,4 @@ SPDX-License-Identifier: AGPL-3.0 %>
 <%= notice %>
 
 <br/>
-<a href="/auth/joshid">Retry Login</a>
+<a href="/login">Retry Login</a>
index 2c259919aeb69e6852076485c30e71ca0dd6f293..a6f1730e86190fc878c4c1ec3fd236af30e7cea3 100644 (file)
@@ -28,22 +28,6 @@ rescue LoadError
   # configured by application.yml (i.e., here!) instead.
 end
 
-if (File.exist?(File.expand_path '../omniauth.rb', __FILE__) and
-    not defined? WARNED_OMNIAUTH_CONFIG)
-  Rails.logger.warn <<-EOS
-DEPRECATED CONFIGURATION:
- Please move your SSO provider config into config/application.yml
- and delete config/initializers/omniauth.rb.
-EOS
-  # Real values will be copied from globals by omniauth_init.rb. For
-  # now, assign some strings so the generic *.yml config loader
-  # doesn't overwrite them or complain that they're missing.
-  Rails.configuration.Login["SSO"]["ProviderAppID"] = 'xxx'
-  Rails.configuration.Login["SSO"]["ProviderAppSecret"] = 'xxx'
-  Rails.configuration.Services["SSO"]["ExternalURL"] = '//xxx'
-  WARNED_OMNIAUTH_CONFIG = true
-end
-
 # Load the defaults, used by config:migrate and fallback loading
 # legacy application.yml
 defaultYAML, stderr, status = Open3.capture3("arvados-server", "config-dump", "-config=-", "-skip-legacy", stdin_data: "Clusters: {xxxxx: {}}")
@@ -114,14 +98,11 @@ arvcfg.declare_config "Users.EmailSubjectPrefix", String, :email_subject_prefix
 arvcfg.declare_config "Users.UserNotifierEmailFrom", String, :user_notifier_email_from
 arvcfg.declare_config "Users.NewUserNotificationRecipients", Hash, :new_user_notification_recipients, ->(cfg, k, v) { arrayToHash cfg, "Users.NewUserNotificationRecipients", v }
 arvcfg.declare_config "Users.NewInactiveUserNotificationRecipients", Hash, :new_inactive_user_notification_recipients, method(:arrayToHash)
-arvcfg.declare_config "Login.SSO.ProviderAppSecret", String, :sso_app_secret
-arvcfg.declare_config "Login.SSO.ProviderAppID", String, :sso_app_id
 arvcfg.declare_config "Login.LoginCluster", String
 arvcfg.declare_config "Login.TrustedClients", Hash
 arvcfg.declare_config "Login.RemoteTokenRefresh", ActiveSupport::Duration
 arvcfg.declare_config "Login.TokenLifetime", ActiveSupport::Duration
 arvcfg.declare_config "TLS.Insecure", Boolean, :sso_insecure
-arvcfg.declare_config "Services.SSO.ExternalURL", String, :sso_provider_url
 arvcfg.declare_config "AuditLogs.MaxAge", ActiveSupport::Duration, :max_audit_log_age
 arvcfg.declare_config "AuditLogs.MaxDeleteBatch", Integer, :max_audit_log_delete_batch
 arvcfg.declare_config "AuditLogs.UnloggedAttributes", Hash, :unlogged_attributes, ->(cfg, k, v) { arrayToHash cfg, "AuditLogs.UnloggedAttributes", v }
index 9605b584e9b4c94f42753fd58ac95fb35a04b048..8087911837bf64f32323b6fa568f49a32575c927 100644 (file)
@@ -6,4 +6,3 @@
 ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
 
 require 'bundler/setup' # Set up gems listed in the Gemfile.
-require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
\ No newline at end of file
index b82ba27f9ae1bd390bdb71c0fd4f79c9c1fb4b70..cd706940a389752fd6263bb32fc82a057fc3c583 100644 (file)
@@ -4,7 +4,6 @@
 
 # Load the rails application
 require_relative 'application'
-require 'josh_id'
 
 # Initialize the rails application
 Rails.application.initialize!
diff --git a/services/api/config/initializers/omniauth_init.rb b/services/api/config/initializers/omniauth_init.rb
deleted file mode 100644 (file)
index a1b2356..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-# This file is called omniauth_init.rb instead of omniauth.rb because
-# older versions had site configuration in omniauth.rb.
-#
-# It must come after omniauth.rb in (lexical) load order.
-
-if defined? CUSTOM_PROVIDER_URL
-  Rails.logger.warn "Copying omniauth from globals in legacy config file."
-  Rails.configuration.Login["SSO"]["ProviderAppID"] = APP_ID
-  Rails.configuration.Login["SSO"]["ProviderAppSecret"] = APP_SECRET
-  Rails.configuration.Services["SSO"]["ExternalURL"] = CUSTOM_PROVIDER_URL.sub(/\/$/, "") + "/"
-else
-  Rails.application.config.middleware.use OmniAuth::Builder do
-    provider(:josh_id,
-             Rails.configuration.Login["SSO"]["ProviderAppID"],
-             Rails.configuration.Login["SSO"]["ProviderAppSecret"],
-             Rails.configuration.Services["SSO"]["ExternalURL"])
-  end
-  OmniAuth.config.on_failure = StaticController.action(:login_failure)
-end
diff --git a/services/api/lib/josh_id.rb b/services/api/lib/josh_id.rb
deleted file mode 100644 (file)
index f18c0ed..0000000
+++ /dev/null
@@ -1,58 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-require 'omniauth-oauth2'
-module OmniAuth
-  module Strategies
-    class JoshId < OmniAuth::Strategies::OAuth2
-
-      args [:client_id, :client_secret, :custom_provider_url]
-
-      option :custom_provider_url, ''
-
-      uid { raw_info['id'] }
-
-      option :client_options, {}
-
-      info do
-        {
-          :first_name => raw_info['info']['first_name'],
-          :last_name => raw_info['info']['last_name'],
-          :email => raw_info['info']['email'],
-          :identity_url => raw_info['info']['identity_url'],
-          :username => raw_info['info']['username'],
-        }
-      end
-
-      extra do
-        {
-          'raw_info' => raw_info
-        }
-      end
-
-      def authorize_params
-        options.authorize_params[:auth_provider] = request.params['auth_provider']
-        super
-      end
-
-      def client
-        options.client_options[:site] = options[:custom_provider_url]
-        options.client_options[:authorize_url] = "#{options[:custom_provider_url]}/auth/josh_id/authorize"
-        options.client_options[:access_token_url] = "#{options[:custom_provider_url]}/auth/josh_id/access_token"
-        if Rails.configuration.TLS.Insecure
-          options.client_options[:ssl] = {verify_mode: OpenSSL::SSL::VERIFY_NONE}
-        end
-        ::OAuth2::Client.new(options.client_id, options.client_secret, deep_symbolize(options.client_options))
-      end
-
-      def callback_url
-        full_host + script_name + callback_path + "?return_to=" + CGI.escape(request.params['return_to'] || '')
-      end
-
-      def raw_info
-        @raw_info ||= access_token.get("/auth/josh_id/user.json?oauth_token=#{access_token.token}").parsed
-      end
-    end
-  end
-end
index 1f919689325d2fef9f9578b150863a77ab55965b..66aff787bd78ecba8f5d897aa29c9d0a99265575 100644 (file)
@@ -9,9 +9,8 @@ class UserSessionsControllerTest < ActionController::TestCase
   test "redirect to joshid" do
     api_client_page = 'http://client.example.com/home'
     get :login, params: {return_to: api_client_page}
-    assert_response :redirect
-    assert_equal("http://test.host/auth/joshid?return_to=%2Chttp%3A%2F%2Fclient.example.com%2Fhome", @response.redirect_url)
-    assert_nil assigns(:api_client)
+    # Not supported any more
+    assert_response 404
   end
 
   test "send token when user is already logged in" do
@@ -107,9 +106,8 @@ class UserSessionsControllerTest < ActionController::TestCase
     Rails.configuration.Login.LoginCluster = 'zzzzz'
     api_client_page = 'http://client.example.com/home'
     get :login, params: {return_to: api_client_page}
-    assert_response :redirect
-    assert_equal("http://test.host/auth/joshid?return_to=%2Chttp%3A%2F%2Fclient.example.com%2Fhome", @response.redirect_url)
-    assert_nil assigns(:api_client)
+    # Doesn't redirect, just fail.
+    assert_response 404
   end
 
   test "controller cannot create session without SystemRootToken" do
index f0741fcfde9f2fe27297720f5bbc4bb88af9b418..ba3b2ac6e3198bbc0ad1eee986b71a2eb80db444 100644 (file)
@@ -30,7 +30,7 @@ class LoginWorkflowTest < ActionDispatch::IntegrationTest
       params: {specimen: {}},
       headers: {'HTTP_ACCEPT' => 'text/html'})
     assert_response 302
-    assert_match(%r{/auth/joshid$}, @response.headers['Location'],
+    assert_match(%r{http://www.example.com/login$}, @response.headers['Location'],
                  "HTML login prompt did not include expected redirect")
   end
 end
index 6e951499adfc173d7653376500a49e1f5a49a8e3..76659f3207fff6b7470e6d85ca95dcbbc936e10b 100644 (file)
@@ -15,21 +15,17 @@ class UserSessionsApiTest < ActionDispatch::IntegrationTest
 
   def mock_auth_with(email: nil, username: nil, identity_url: nil, remote: nil, expected_response: :redirect)
     mock = {
-      'provider' => 'josh_id',
-      'uid' => 'https://edward.example.com',
-      'info' => {
         'identity_url' => 'https://edward.example.com',
         'name' => 'Edward Example',
         'first_name' => 'Edward',
         'last_name' => 'Example',
-      },
     }
-    mock['info']['email'] = email unless email.nil?
-    mock['info']['username'] = username unless username.nil?
-    mock['info']['identity_url'] = identity_url unless identity_url.nil?
-    post('/auth/josh_id/callback',
-      params: {return_to: client_url(remote: remote)},
-      headers: {'omniauth.auth' => mock})
+    mock['email'] = email unless email.nil?
+    mock['username'] = username unless username.nil?
+    mock['identity_url'] = identity_url unless identity_url.nil?
+    post('/auth/controller/callback',
+      params: {return_to: client_url(remote: remote), :auth_info => SafeJSON.dump(mock)},
+      headers: {'Authorization' => 'Bearer ' + Rails.configuration.SystemRootToken})
 
     errors = {
       :redirect => 'Did not redirect to client with token',
index ee7dac4cd90099bb1d9b9fe17a9a781baaee5f1c..843d4f1b23fccfb8777883c3021ab54187b3cf57 100644 (file)
@@ -25,7 +25,6 @@ unless ENV["NO_COVERAGE_TEST"]
     SimpleCov.start do
       add_filter '/test/'
       add_filter 'initializers/secret_token'
-      add_filter 'initializers/omniauth'
     end
   rescue Exception => e
     $stderr.puts "SimpleCov unavailable (#{e}). Proceeding without."
index 3a0316cf9e1e48cb319792c1636c56c8fc1eabc2..1696f856a595616a86ed9e9a0bc1b8b05ff56af2 100644 (file)
@@ -61,24 +61,16 @@ from builtins import next
 from builtins import str
 from builtins import object
 import os
-import sys
 import llfuse
 import errno
 import stat
 import threading
 import arvados
-import pprint
 import arvados.events
-import re
-import apiclient
-import json
 import logging
 import time
-import _strptime
-import calendar
 import threading
 import itertools
-import ciso8601
 import collections
 import functools
 import arvados.keep
index 7bef8a269fd5a2aec7dcd93f272e5a0a5bd99d19..67a2aaa4da881891be106535d38e9bc4969220ab 100644 (file)
@@ -95,6 +95,7 @@ class ArgumentParser(argparse.ArgumentParser):
 
         self.add_argument('--read-only', action='store_false', help="Mount will be read only (default)", dest="enable_write", default=False)
         self.add_argument('--read-write', action='store_true', help="Mount will be read-write", dest="enable_write", default=False)
+        self.add_argument('--storage-classes', type=str, metavar='CLASSES', help="Specify comma separated list of storage classes to be used when saving data of new collections", default=None)
 
         self.add_argument('--crunchstat-interval', type=float, help="Write stats to stderr every N seconds (default disabled)", default=0)
 
@@ -246,6 +247,11 @@ class Mount(object):
         dir_args = [llfuse.ROOT_INODE, self.operations.inodes, self.api, self.args.retries]
         mount_readme = False
 
+        storage_classes = None
+        if self.args.storage_classes is not None:
+            storage_classes = self.args.storage_classes.replace(' ', '').split(',')
+            self.logger.info("Storage classes requested for new collections: {}".format(', '.join(storage_classes)))
+
         if self.args.collection is not None:
             # Set up the request handler with the collection at the root
             # First check that the collection is readable
@@ -295,7 +301,10 @@ class Mount(object):
             mount_readme = True
 
         if dir_class is not None:
-            ent = dir_class(*dir_args)
+            if dir_class in [TagsDirectory, CollectionDirectory]:
+                ent = dir_class(*dir_args)
+            else:
+                ent = dir_class(*dir_args, storage_classes=storage_classes)
             self.operations.inodes.add_entry(ent)
             self.listen_for_events = ent.want_event_subscribe()
             return
@@ -305,17 +314,17 @@ class Mount(object):
         dir_args[0] = e.inode
 
         for name in self.args.mount_by_id:
-            self._add_mount(e, name, MagicDirectory(*dir_args, pdh_only=False))
+            self._add_mount(e, name, MagicDirectory(*dir_args, pdh_only=False, storage_classes=storage_classes))
         for name in self.args.mount_by_pdh:
             self._add_mount(e, name, MagicDirectory(*dir_args, pdh_only=True))
         for name in self.args.mount_by_tag:
             self._add_mount(e, name, TagsDirectory(*dir_args))
         for name in self.args.mount_home:
-            self._add_mount(e, name, ProjectDirectory(*dir_args, project_object=usr, poll=True))
+            self._add_mount(e, name, ProjectDirectory(*dir_args, project_object=usr, poll=True, storage_classes=storage_classes))
         for name in self.args.mount_shared:
-            self._add_mount(e, name, SharedDirectory(*dir_args, exclude=usr, poll=True))
+            self._add_mount(e, name, SharedDirectory(*dir_args, exclude=usr, poll=True, storage_classes=storage_classes))
         for name in self.args.mount_tmp:
-            self._add_mount(e, name, TmpCollectionDirectory(*dir_args))
+            self._add_mount(e, name, TmpCollectionDirectory(*dir_args, storage_classes=storage_classes))
 
         if mount_readme:
             text = self._readme_text(
index a2e3ac139eca44a4b9c1513977181d744fb364be..78cbd0d8cfd06f1c638549151c56e74a32025237 100644 (file)
@@ -487,6 +487,8 @@ class CollectionDirectory(CollectionDirectoryBase):
                             new_collection_record["portable_data_hash"] = new_collection_record["uuid"]
                         if 'manifest_text' not in new_collection_record:
                             new_collection_record['manifest_text'] = coll_reader.manifest_text()
+                        if 'storage_classes_desired' not in new_collection_record:
+                            new_collection_record['storage_classes_desired'] = coll_reader.storage_classes_desired()
 
                         if self.collection_record is None or self.collection_record["portable_data_hash"] != new_collection_record.get("portable_data_hash"):
                             self.new_collection(new_collection_record, coll_reader)
@@ -571,11 +573,12 @@ class TmpCollectionDirectory(CollectionDirectoryBase):
         def save_new(self):
             pass
 
-    def __init__(self, parent_inode, inodes, api_client, num_retries):
+    def __init__(self, parent_inode, inodes, api_client, num_retries, storage_classes=None):
         collection = self.UnsaveableCollection(
             api_client=api_client,
             keep_client=api_client.keep,
-            num_retries=num_retries)
+            num_retries=num_retries,
+            storage_classes_desired=storage_classes)
         super(TmpCollectionDirectory, self).__init__(
             parent_inode, inodes, api_client.config, collection)
         self.collection_record_file = None
@@ -595,6 +598,7 @@ class TmpCollectionDirectory(CollectionDirectoryBase):
                 "uuid": None,
                 "manifest_text": self.collection.manifest_text(),
                 "portable_data_hash": self.collection.portable_data_hash(),
+                "storage_classes_desired": self.collection.storage_classes_desired(),
             }
 
     def __contains__(self, k):
@@ -653,11 +657,12 @@ and the directory will appear if it exists.
 
 """.lstrip()
 
-    def __init__(self, parent_inode, inodes, api, num_retries, pdh_only=False):
+    def __init__(self, parent_inode, inodes, api, num_retries, pdh_only=False, storage_classes=None):
         super(MagicDirectory, self).__init__(parent_inode, inodes, api.config)
         self.api = api
         self.num_retries = num_retries
         self.pdh_only = pdh_only
+        self.storage_classes = storage_classes
 
     def __setattr__(self, name, value):
         super(MagicDirectory, self).__setattr__(name, value)
@@ -687,7 +692,8 @@ and the directory will appear if it exists.
                 if project[u'items_available'] == 0:
                     return False
                 e = self.inodes.add_entry(ProjectDirectory(
-                    self.inode, self.inodes, self.api, self.num_retries, project[u'items'][0]))
+                    self.inode, self.inodes, self.api, self.num_retries,
+                    project[u'items'][0], storage_classes=self.storage_classes))
             else:
                 e = self.inodes.add_entry(CollectionDirectory(
                         self.inode, self.inodes, self.api, self.num_retries, k))
@@ -811,7 +817,7 @@ class ProjectDirectory(Directory):
     """A special directory that contains the contents of a project."""
 
     def __init__(self, parent_inode, inodes, api, num_retries, project_object,
-                 poll=True, poll_time=3):
+                 poll=True, poll_time=3, storage_classes=None):
         super(ProjectDirectory, self).__init__(parent_inode, inodes, api.config)
         self.api = api
         self.num_retries = num_retries
@@ -823,6 +829,7 @@ class ProjectDirectory(Directory):
         self._updating_lock = threading.Lock()
         self._current_user = None
         self._full_listing = False
+        self.storage_classes = storage_classes
 
     def want_event_subscribe(self):
         return True
@@ -831,7 +838,7 @@ class ProjectDirectory(Directory):
         if collection_uuid_pattern.match(i['uuid']):
             return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i)
         elif group_uuid_pattern.match(i['uuid']):
-            return ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i, self._poll, self._poll_time)
+            return ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i, self._poll, self._poll_time, self.storage_classes)
         elif link_uuid_pattern.match(i['uuid']):
             if i['head_kind'] == 'arvados#collection' or portable_data_hash_pattern.match(i['head_uuid']):
                 return CollectionDirectory(self.inode, self.inodes, self.api, self.num_retries, i['head_uuid'])
@@ -982,9 +989,16 @@ class ProjectDirectory(Directory):
     def mkdir(self, name):
         try:
             with llfuse.lock_released:
-                self.api.collections().create(body={"owner_uuid": self.project_uuid,
-                                                    "name": name,
-                                                    "manifest_text": ""}).execute(num_retries=self.num_retries)
+                c = {
+                    "owner_uuid": self.project_uuid,
+                    "name": name,
+                    "manifest_text": "" }
+                if self.storage_classes is not None:
+                    c["storage_classes_desired"] = self.storage_classes
+                try:
+                    self.api.collections().create(body=c).execute(num_retries=self.num_retries)
+                except Exception as e:
+                    raise
             self.invalidate()
         except apiclient_errors.Error as error:
             _logger.error(error)
@@ -1079,7 +1093,7 @@ class SharedDirectory(Directory):
     """A special directory that represents users or groups who have shared projects with me."""
 
     def __init__(self, parent_inode, inodes, api, num_retries, exclude,
-                 poll=False, poll_time=60):
+                 poll=False, poll_time=60, storage_classes=None):
         super(SharedDirectory, self).__init__(parent_inode, inodes, api.config)
         self.api = api
         self.num_retries = num_retries
@@ -1087,6 +1101,7 @@ class SharedDirectory(Directory):
         self._poll = True
         self._poll_time = poll_time
         self._updating_lock = threading.Lock()
+        self.storage_classes = storage_classes
 
     @use_counter
     def update(self):
@@ -1156,8 +1171,6 @@ class SharedDirectory(Directory):
                         obr = objects[r]
                         if obr.get("name"):
                             contents[obr["name"]] = obr
-                        #elif obr.get("username"):
-                        #    contents[obr["username"]] = obr
                         elif "first_name" in obr:
                             contents[u"{} {}".format(obr["first_name"], obr["last_name"])] = obr
 
@@ -1172,7 +1185,7 @@ class SharedDirectory(Directory):
             self.merge(viewitems(contents),
                        lambda i: i[0],
                        lambda a, i: a.uuid() == i[1]['uuid'],
-                       lambda i: ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i[1], poll=self._poll, poll_time=self._poll_time))
+                       lambda i: ProjectDirectory(self.inode, self.inodes, self.api, self.num_retries, i[1], poll=self._poll, poll_time=self._poll_time, storage_classes=self.storage_classes))
         except Exception:
             _logger.exception("arv-mount shared dir error")
         finally:
index 54316bb9a987cd5a2a771da901cf9d6db9da8c51..82e5c441eb18edacadd0c91cbb79122183b9d7b4 100644 (file)
@@ -22,6 +22,7 @@ from . import run_test_server
 
 from .integration_test import IntegrationTest
 from .mount_test_base import MountTestBase
+from .test_tmp_collection import storage_classes_desired
 
 logger = logging.getLogger('arvados.arv-mount')
 
@@ -1262,3 +1263,31 @@ class SlashSubstitutionTest(IntegrationTest):
     def _test_slash_substitution_conflict(self, tmpdir, fusename):
         with open(os.path.join(tmpdir, fusename, 'waz'), 'w') as f:
             f.write('foo')
+
+class StorageClassesTest(IntegrationTest):
+    mnt_args = [
+        '--read-write',
+        '--mount-home', 'homedir',
+    ]
+
+    def setUp(self):
+        super(StorageClassesTest, self).setUp()
+        self.api = arvados.safeapi.ThreadSafeApiCache(arvados.config.settings())
+
+    @IntegrationTest.mount(argv=mnt_args)
+    def test_collection_default_storage_classes(self):
+        coll_path = os.path.join(self.mnt, 'homedir', 'a_collection')
+        self.api.collections().create(body={'name':'a_collection'}).execute()
+        self.pool_test(coll_path)
+    @staticmethod
+    def _test_collection_default_storage_classes(self, coll):
+        self.assertEqual(storage_classes_desired(coll), ['default'])
+
+    @IntegrationTest.mount(argv=mnt_args+['--storage-classes', 'foo'])
+    def test_collection_custom_storage_classes(self):
+        coll_path = os.path.join(self.mnt, 'homedir', 'new_coll')
+        os.mkdir(coll_path)
+        self.pool_test(coll_path)
+    @staticmethod
+    def _test_collection_custom_storage_classes(self, coll):
+        self.assertEqual(storage_classes_desired(coll), ['foo'])
index 50075c96aed6563af73b04f3ed1e0724eaf01c7e..c59024267a4b628dee6948c8259ac7bcda8257e4 100644 (file)
@@ -58,6 +58,9 @@ def current_manifest(tmpdir):
     with open(os.path.join(tmpdir, '.arvados#collection')) as tmp:
         return json.load(tmp)['manifest_text']
 
+def storage_classes_desired(tmpdir):
+    with open(os.path.join(tmpdir, '.arvados#collection')) as tmp:
+        return json.load(tmp)['storage_classes_desired']
 
 class TmpCollectionTest(IntegrationTest):
     mnt_args = [
@@ -65,6 +68,13 @@ class TmpCollectionTest(IntegrationTest):
         '--mount-tmp', 'zzz',
     ]
 
+    @IntegrationTest.mount(argv=mnt_args+['--storage-classes', 'foo, bar'])
+    def test_storage_classes(self):
+        self.pool_test(os.path.join(self.mnt, 'zzz'))
+    @staticmethod
+    def _test_storage_classes(self, zzz):
+        self.assertEqual(storage_classes_desired(zzz), ['foo', 'bar'])
+
     @IntegrationTest.mount(argv=mnt_args+['--mount-tmp', 'yyy'])
     def test_two_tmp(self):
         self.pool_test(os.path.join(self.mnt, 'zzz'),
index 21bbfe40cc9e41b980d11172275c1c1f222e9a0c..c679e5b91cff8f9b86daa0b8ccda60aa20b36f96 100644 (file)
@@ -568,9 +568,9 @@ func (h *proxyHandler) Put(resp http.ResponseWriter, req *http.Request) {
        kc.Arvados = &arvclient
 
        // Check if the client specified the number of replicas
-       if req.Header.Get("X-Keep-Desired-Replicas") != "" {
+       if desiredReplicas := req.Header.Get(keepclient.XKeepDesiredReplicas); desiredReplicas != "" {
                var r int
-               _, err := fmt.Sscanf(req.Header.Get(keepclient.XKeepDesiredReplicas), "%d", &r)
+               _, err := fmt.Sscanf(desiredReplicas, "%d", &r)
                if err == nil {
                        kc.Want_replicas = r
                }
@@ -607,23 +607,28 @@ func (h *proxyHandler) Put(resp http.ResponseWriter, req *http.Request) {
        switch err.(type) {
        case nil:
                status = http.StatusOK
+               if len(kc.StorageClasses) > 0 {
+                       // A successful PUT request with storage classes means that all
+                       // storage classes were fulfilled, so the client will get a
+                       // confirmation via the X-Storage-Classes-Confirmed header.
+                       hdr := ""
+                       isFirst := true
+                       for _, sc := range kc.StorageClasses {
+                               if isFirst {
+                                       hdr = fmt.Sprintf("%s=%d", sc, wroteReplicas)
+                                       isFirst = false
+                               } else {
+                                       hdr += fmt.Sprintf(", %s=%d", sc, wroteReplicas)
+                               }
+                       }
+                       resp.Header().Set(keepclient.XKeepStorageClassesConfirmed, hdr)
+               }
                _, err = io.WriteString(resp, locatorOut)
-
        case keepclient.OversizeBlockError:
                // Too much data
                status = http.StatusRequestEntityTooLarge
-
        case keepclient.InsufficientReplicasError:
-               if wroteReplicas > 0 {
-                       // At least one write is considered success.  The
-                       // client can decide if getting less than the number of
-                       // replications it asked for is a fatal error.
-                       status = http.StatusOK
-                       _, err = io.WriteString(resp, locatorOut)
-               } else {
-                       status = http.StatusServiceUnavailable
-               }
-
+               status = http.StatusServiceUnavailable
        default:
                status = http.StatusBadGateway
        }
index 9dc07ddaa0e1be60ce3f4b522dbc3d90aab4ed02..2d4266d8d591ca1f304f0ca1a18993d20348e8be 100644 (file)
@@ -236,6 +236,28 @@ func (s *ServerRequiredSuite) TestStorageClassesHeader(c *C) {
        c.Check(hdr.Get("X-Keep-Storage-Classes"), Equals, "secure")
 }
 
+func (s *ServerRequiredSuite) TestStorageClassesConfirmedHeader(c *C) {
+       runProxy(c, false, false)
+       defer closeListener()
+
+       content := []byte("foo")
+       hash := fmt.Sprintf("%x", md5.Sum(content))
+       client := &http.Client{}
+
+       req, err := http.NewRequest("PUT",
+               fmt.Sprintf("http://%s/%s", listener.Addr().String(), hash),
+               bytes.NewReader(content))
+       c.Assert(err, IsNil)
+       req.Header.Set("X-Keep-Storage-Classes", "default")
+       req.Header.Set("Authorization", "OAuth2 "+arvadostest.ActiveToken)
+       req.Header.Set("Content-Type", "application/octet-stream")
+
+       resp, err := client.Do(req)
+       c.Assert(err, IsNil)
+       c.Assert(resp.StatusCode, Equals, http.StatusOK)
+       c.Assert(resp.Header.Get("X-Keep-Storage-Classes-Confirmed"), Equals, "default=2")
+}
+
 func (s *ServerRequiredSuite) TestDesiredReplicas(c *C) {
        kc, _ := runProxy(c, false, false, nil)
        defer closeListener()
index f5e40ff153f92889f6293398e7bc2350c3356561..17b7b888846fca194a04f60af829dd5ee271a4e5 100644 (file)
@@ -82,6 +82,7 @@ LE_AWS_SECRET_ACCESS_KEY="thisistherandomstringthatisyoursecretkey"
 # Extra states to apply. If you use your own subdir, change this value accordingly
 # EXTRA_STATES_DIR="${CONFIG_DIR}/states"
 
+# These are ARVADOS-related settings.
 # Which release of Arvados repo you want to use
 RELEASE="production"
 # Which version of Arvados you want to install. Defaults to latest stable
@@ -90,13 +91,13 @@ RELEASE="production"
 # This is an arvados-formula setting.
 # If branch is set, the script will switch to it before running salt
 # Usually not needed, only used for testing
-# BRANCH="master"
+# BRANCH="main"
 
 ##########################################################
 # Usually there's no need to modify things below this line
 
 # Formulas versions
-# ARVADOS_TAG="v1.1.4"
+# ARVADOS_TAG="2.2.0"
 # POSTGRES_TAG="v0.41.6"
 # NGINX_TAG="temp-fix-missing-statements-in-pillar"
 # DOCKER_TAG="v1.0.0"
index 6dd47722c19d67ef3b0c49a0be7976cb3fcae63d..ae54e7437a83db83b7373eaa6ef87d70aa31e8b5 100644 (file)
@@ -54,6 +54,7 @@ USE_LETSENCRYPT="no"
 # Extra states to apply. If you use your own subdir, change this value accordingly
 # EXTRA_STATES_DIR="${CONFIG_DIR}/states"
 
+# These are ARVADOS-related settings.
 # Which release of Arvados repo you want to use
 RELEASE="production"
 # Which version of Arvados you want to install. Defaults to latest stable
@@ -62,13 +63,13 @@ RELEASE="production"
 # This is an arvados-formula setting.
 # If branch is set, the script will switch to it before running salt
 # Usually not needed, only used for testing
-# BRANCH="master"
+# BRANCH="main"
 
 ##########################################################
 # Usually there's no need to modify things below this line
 
 # Formulas versions
-# ARVADOS_TAG="v1.1.4"
+# ARVADOS_TAG="2.2.0"
 # POSTGRES_TAG="v0.41.6"
 # NGINX_TAG="temp-fix-missing-statements-in-pillar"
 # DOCKER_TAG="v1.0.0"
index fda42a9c745ad5f7feb3e219ea85bd29b681c503..a35bd45bffc258d7c3a8dd4b59eb564bfc13c4b8 100644 (file)
@@ -63,6 +63,7 @@ USE_LETSENCRYPT="no"
 # Extra states to apply. If you use your own subdir, change this value accordingly
 # EXTRA_STATES_DIR="${CONFIG_DIR}/states"
 
+# These are ARVADOS-related settings.
 # Which release of Arvados repo you want to use
 RELEASE="production"
 # Which version of Arvados you want to install. Defaults to latest stable
@@ -71,13 +72,13 @@ RELEASE="production"
 # This is an arvados-formula setting.
 # If branch is set, the script will switch to it before running salt
 # Usually not needed, only used for testing
-# BRANCH="master"
+# BRANCH="main"
 
 ##########################################################
 # Usually there's no need to modify things below this line
 
 # Formulas versions
-# ARVADOS_TAG="v1.1.4"
+# ARVADOS_TAG="2.2.0"
 # POSTGRES_TAG="v0.41.6"
 # NGINX_TAG="temp-fix-missing-statements-in-pillar"
 # DOCKER_TAG="v1.0.0"
index 7faf5c2fb554eec71400107ca80f6087970e0550..7ac120e5fd89179f75fcf13608679edfaa2b45e5 100755 (executable)
@@ -1,4 +1,4 @@
-#!/bin/bash -x
+#!/usr/bin/env bash
 
 # Copyright (C) The Arvados Authors. All rights reserved.
 #
@@ -21,7 +21,6 @@ usage() {
   echo >&2
   echo >&2 "${0} options:"
   echo >&2 "  -d, --debug                                 Run salt installation in debug mode"
-  echo >&2 "  -p <N>, --ssl-port <N>                      SSL port to use for the web applications"
   echo >&2 "  -c <local.params>, --config <local.params>  Path to the local.params config file"
   echo >&2 "  -t, --test                                  Test installation running a CWL workflow"
   echo >&2 "  -r, --roles                                 List of Arvados roles to apply to the host, comma separated"
@@ -39,17 +38,35 @@ usage() {
   echo >&2 "                                                workbench2"
   echo >&2 "                                              Defaults to applying them all"
   echo >&2 "  -h, --help                                  Display this help and exit"
+  echo >&2 "  --dump-config <dest_dir>                    Dumps the pillars and states to a directory"
+  echo >&2 "                                              This parameter does not perform any installation at all. It's"
+  echo >&2 "                                              intended to give you a parsed sot of configuration files so"
+  echo >&2 "                                              you can inspect them or use them in you Saltstack infrastructure."
+  echo >&2 "                                              It"
+  echo >&2 "                                                - parses the pillar and states templates,"
+  echo >&2 "                                                - downloads the helper formulas with their desired versions,"
+  echo >&2 "                                                - prepares the 'top.sls' files both for pillars and states"
+  echo >&2 "                                                  for the selected role/s"
+  echo >&2 "                                                - writes the resulting files into <dest_dir>"
   echo >&2 "  -v, --vagrant                               Run in vagrant and use the /vagrant shared dir"
   echo >&2
 }
 
 arguments() {
   # NOTE: This requires GNU getopt (part of the util-linux package on Debian-based distros).
+  if ! which getopt > /dev/null; then
+    echo >&2 "GNU getopt is required to run this script. Please install it and re-reun it"
+    exit 1
+  fi
+
   TEMP=$(getopt -o c:dhp:r:tv \
-    --long config:,debug,help,ssl-port:,roles:,test,vagrant \
+    --long config:,debug,dump-config:,help,roles:,test,vagrant \
     -n "${0}" -- "${@}")
 
-  if [ ${?} != 0 ] ; then echo "GNU getopt missing? Use -h for help"; exit 1 ; fi
+  if [ ${?} != 0 ];
+    then echo "Please check the parameters you entered and re-run again"
+    exit 1
+  fi
   # Note the quotes around `$TEMP': they are essential!
   eval set -- "$TEMP"
 
@@ -62,9 +79,23 @@ arguments() {
       -d | --debug)
         LOG_LEVEL="debug"
         shift
+        set -x
         ;;
-      -p | --ssl-port)
-        CONTROLLER_EXT_SSL_PORT=${2}
+      --dump-config)
+        if [[ ${2} = /* ]]; then
+          DUMP_SALT_CONFIG_DIR=${2}
+        else
+          DUMP_SALT_CONFIG_DIR=${PWD}/${2}
+        fi
+        ## states
+        S_DIR="${DUMP_SALT_CONFIG_DIR}/salt"
+        ## formulas
+        F_DIR="${DUMP_SALT_CONFIG_DIR}/formulas"
+        ## pillars
+        P_DIR="${DUMP_SALT_CONFIG_DIR}/pillars"
+        ## tests
+        T_DIR="${DUMP_SALT_CONFIG_DIR}/tests"
+        DUMP_CONFIG="yes"
         shift 2
         ;;
       -r | --roles)
@@ -102,6 +133,7 @@ arguments() {
 
 CONFIG_FILE="${SCRIPT_DIR}/local.params"
 CONFIG_DIR="local_config_dir"
+DUMP_CONFIG="no"
 LOG_LEVEL="info"
 CONTROLLER_EXT_SSL_PORT=443
 TESTS_DIR="tests"
@@ -127,15 +159,20 @@ WEBSOCKET_EXT_SSL_PORT=8002
 WORKBENCH1_EXT_SSL_PORT=443
 WORKBENCH2_EXT_SSL_PORT=3001
 
+## These are ARVADOS-related parameters
 # For a stable release, change RELEASE "production" and VERSION to the
 # package version (including the iteration, e.g. X.Y.Z-1) of the
 # release.
+# The "local.params.example.*" files already set "RELEASE=production"
+# to deploy  production-ready packages
 RELEASE="development"
 VERSION="latest"
 
-# The arvados-formula version.  For a stable release, this should be a
+# These are arvados-formula-related parameters
+# An arvados-formula tag. For a stable release, this should be a
 # branch name (e.g. X.Y-dev) or tag for the release.
-ARVADOS_TAG="main"
+# ARVADOS_TAG="2.2.0"
+# BRANCH="main"
 
 # Other formula versions we depend on
 POSTGRES_TAG="v0.41.6"
@@ -145,26 +182,31 @@ LOCALE_TAG="v0.3.4"
 LETSENCRYPT_TAG="v2.1.0"
 
 # Salt's dir
+DUMP_SALT_CONFIG_DIR=""
 ## states
 S_DIR="/srv/salt"
 ## formulas
 F_DIR="/srv/formulas"
-##pillars
+## pillars
 P_DIR="/srv/pillars"
+## tests
+T_DIR="/tmp/cluster_tests"
 
 arguments ${@}
 
 if [ -s ${CONFIG_FILE} ]; then
   source ${CONFIG_FILE}
 else
-  echo >&2 "Please create a '${CONFIG_FILE}' file with initial values, as described in"
+  echo >&2 "You don't seem to have a config file with initial values."
+  echo >&2 "Please create a '${CONFIG_FILE}' file as described in"
   echo >&2 "  * https://doc.arvados.org/install/salt-single-host.html#single_host, or"
   echo >&2 "  * https://doc.arvados.org/install/salt-multi-host.html#multi_host_multi_hostnames"
   exit 1
 fi
 
 if [ ! -d ${CONFIG_DIR} ]; then
-  echo >&2 "Please create a '${CONFIG_DIR}' with initial values, as described in"
+  echo >&2 "You don't seem to have a config directory with pillars and states."
+  echo >&2 "Please create a '${CONFIG_DIR}' directory (as configured in your '${CONFIG_FILE}'). Please see"
   echo >&2 "  * https://doc.arvados.org/install/salt-single-host.html#single_host, or"
   echo >&2 "  * https://doc.arvados.org/install/salt-multi-host.html#multi_host_multi_hostnames"
   exit 1
@@ -176,7 +218,7 @@ if grep -q 'fixme_or_this_wont_work' ${CONFIG_FILE} ; then
   exit 1
 fi
 
-if ! grep -E '^[[:alnum:]]{5}$' <<<${CLUSTER} ; then
+if ! grep -qE '^[[:alnum:]]{5}$' <<<${CLUSTER} ; then
   echo >&2 "ERROR: <CLUSTER> must be exactly 5 alphanumeric characters long"
   echo >&2 "Fix the cluster name in the 'local.params' file and re-run the provision script"
   exit 1
@@ -187,20 +229,23 @@ if [ "x${HOSTNAME_EXT}" = "x" ] ; then
   HOSTNAME_EXT="${CLUSTER}.${DOMAIN}"
 fi
 
-apt-get update
-apt-get install -y curl git jq
-
-if which salt-call; then
-  echo "Salt already installed"
+if [ "${DUMP_CONFIG}" = "yes" ]; then
+  echo "The provision installer will just dump a config under ${DUMP_SALT_CONFIG_DIR} and exit"
 else
-  curl -L https://bootstrap.saltstack.com -o /tmp/bootstrap_salt.sh
-  sh /tmp/bootstrap_salt.sh -XdfP -x python3
-  /bin/systemctl stop salt-minion.service
-  /bin/systemctl disable salt-minion.service
-fi
+  apt-get update
+  apt-get install -y curl git jq
+
+  if which salt-call; then
+    echo "Salt already installed"
+  else
+    curl -L https://bootstrap.saltstack.com -o /tmp/bootstrap_salt.sh
+    sh /tmp/bootstrap_salt.sh -XdfP -x python3
+    /bin/systemctl stop salt-minion.service
+    /bin/systemctl disable salt-minion.service
+  fi
 
-# Set salt to masterless mode
-cat > /etc/salt/minion << EOFSM
+  # Set salt to masterless mode
+  cat > /etc/salt/minion << EOFSM
 file_client: local
 file_roots:
   base:
@@ -211,24 +256,36 @@ pillar_roots:
   base:
     - ${P_DIR}
 EOFSM
+fi
 
-mkdir -p ${S_DIR} ${F_DIR} ${P_DIR}
+mkdir -p ${S_DIR} ${F_DIR} ${P_DIR} ${T_DIR}
 
 # Get the formula and dependencies
 cd ${F_DIR} || exit 1
-git clone --branch "${ARVADOS_TAG}"     https://git.arvados.org/arvados-formula.git
-git clone --branch "${DOCKER_TAG}"      https://github.com/saltstack-formulas/docker-formula.git
-git clone --branch "${LOCALE_TAG}"      https://github.com/saltstack-formulas/locale-formula.git
-# git clone --branch "${NGINX_TAG}"       https://github.com/saltstack-formulas/nginx-formula.git
-git clone --branch "${NGINX_TAG}"       https://github.com/netmanagers/nginx-formula.git
-git clone --branch "${POSTGRES_TAG}"    https://github.com/saltstack-formulas/postgres-formula.git
-git clone --branch "${LETSENCRYPT_TAG}" https://github.com/saltstack-formulas/letsencrypt-formula.git
+echo "Cloning formulas"
+rm -rf ${F_DIR}/* || exit 1
+git clone --quiet https://github.com/saltstack-formulas/docker-formula.git ${F_DIR}/docker
+( cd docker && git checkout --quiet tags/"${DOCKER_TAG}" -b "${DOCKER_TAG}" )
+
+git clone --quiet https://github.com/saltstack-formulas/locale-formula.git ${F_DIR}/locale
+( cd locale && git checkout --quiet tags/"${LOCALE_TAG}" -b "${LOCALE_TAG}" )
+
+git clone --quiet https://github.com/netmanagers/nginx-formula.git ${F_DIR}/nginx
+( cd nginx && git checkout --quiet tags/"${NGINX_TAG}" -b "${NGINX_TAG}" )
+
+git clone --quiet https://github.com/saltstack-formulas/postgres-formula.git ${F_DIR}/postgres
+( cd postgres && git checkout --quiet tags/"${POSTGRES_TAG}" -b "${POSTGRES_TAG}" )
+
+git clone --quiet https://github.com/saltstack-formulas/letsencrypt-formula.git ${F_DIR}/letsencrypt
+( cd letsencrypt && git checkout --quiet tags/"${LETSENCRYPT_TAG}" -b "${LETSENCRYPT_TAG}" )
+
+git clone --quiet https://git.arvados.org/arvados-formula.git ${F_DIR}/arvados
 
 # If we want to try a specific branch of the formula
 if [ "x${BRANCH}" != "x" ]; then
-  cd ${F_DIR}/arvados-formula || exit 1
-  git checkout -t origin/"${BRANCH}" -b "${BRANCH}"
-  cd -
+  ( cd ${F_DIR}/arvados && git checkout --quiet -t origin/"${BRANCH}" -b "${BRANCH}" )
+elif [ "x${ARVADOS_TAG}" != "x" ]; then
+( cd ${F_DIR}/arvados && git checkout --quiet tags/"${ARVADOS_TAG}" -b "${ARVADOS_TAG}" )
 fi
 
 if [ "x${VAGRANT}" = "xyes" ]; then
@@ -243,6 +300,8 @@ fi
 
 SOURCE_STATES_DIR="${EXTRA_STATES_DIR}"
 
+echo "Writing pillars and states"
+
 # Replace variables (cluster,  domain, etc) in the pillars, states and tests
 # to ease deployment for newcomers
 if [ ! -d "${SOURCE_PILLARS_DIR}" ]; then
@@ -294,7 +353,7 @@ if [ "x${TEST}" = "xyes" ] && [ ! -d "${SOURCE_TESTS_DIR}" ]; then
   echo "You requested to run tests, but ${SOURCE_TESTS_DIR} does not exist or is not a directory. Exiting."
   exit 1
 fi
-mkdir -p /tmp/cluster_tests
+mkdir -p ${T_DIR}
 # Replace cluster and domain name in the test files
 for f in $(ls "${SOURCE_TESTS_DIR}"/*); do
   sed "s#__CLUSTER__#${CLUSTER}#g;
@@ -306,9 +365,9 @@ for f in $(ls "${SOURCE_TESTS_DIR}"/*); do
        s#__INITIAL_USER__#${INITIAL_USER}#g;
        s#__DATABASE_PASSWORD__#${DATABASE_PASSWORD}#g;
        s#__SYSTEM_ROOT_TOKEN__#${SYSTEM_ROOT_TOKEN}#g" \
-  "${f}" > "/tmp/cluster_tests"/$(basename "${f}")
+  "${f}" > ${T_DIR}/$(basename "${f}")
 done
-chmod 755 /tmp/cluster_tests/run-test.sh
+chmod 755 ${T_DIR}/run-test.sh
 
 # Replace helper state files that differ from the formula's examples
 if [ -d "${SOURCE_STATES_DIR}" ]; then
@@ -500,6 +559,11 @@ else
   done
 fi
 
+if [ "${DUMP_CONFIG}" = "yes" ]; then
+  # We won't run the rest of the script because we're just dumping the config
+  exit 0
+fi
+
 # FIXME! #16992 Temporary fix for psql call in arvados-api-server
 if [ -e /root/.psqlrc ]; then
   if ! ( grep 'pset pager off' /root/.psqlrc ); then
@@ -542,6 +606,6 @@ fi
 
 # Test that the installation finished correctly
 if [ "x${TEST}" = "xyes" ]; then
-  cd /tmp/cluster_tests
+  cd ${T_DIR}
   ./run-test.sh
 fi