Merge branch 'master' into 14260-runtime-token
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Thu, 18 Oct 2018 15:58:57 +0000 (11:58 -0400)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Thu, 18 Oct 2018 15:58:57 +0000 (11:58 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

72 files changed:
apps/workbench/app/views/layouts/body.html.erb
apps/workbench/config/application.default.yml
build/go-python-package-scripts/postinst
build/go-python-package-scripts/prerm
build/package-test-dockerfiles/.gitignore [new file with mode: 0644]
build/package-test-dockerfiles/Makefile [new file with mode: 0644]
build/package-test-dockerfiles/README [new file with mode: 0644]
build/package-test-dockerfiles/centos7/Dockerfile
build/package-test-dockerfiles/debian8/Dockerfile
build/package-test-dockerfiles/debian9/D39DC0E3.asc [deleted file]
build/package-test-dockerfiles/debian9/Dockerfile
build/package-test-dockerfiles/ubuntu1404/Dockerfile
build/package-test-dockerfiles/ubuntu1604/Dockerfile
build/package-test-dockerfiles/ubuntu1804/Dockerfile
build/run-build-packages-one-target.sh
build/run-build-packages.sh
build/run-tests.sh
doc/_includes/_create_superuser_token.liquid [new file with mode: 0644]
doc/_includes/_install_compute_docker.liquid
doc/admin/health-checks.html.textile.liquid
doc/admin/metrics.html.textile.liquid
doc/install/crunch2-slurm/install-dispatch.html.textile.liquid
doc/install/install-keep-balance.html.textile.liquid
doc/install/install-keepproxy.html.textile.liquid
doc/install/install-keepstore.html.textile.liquid
doc/install/install-shell-server.html.textile.liquid
sdk/go/arvados/config.go
sdk/go/arvadostest/fixtures.go
sdk/go/auth/auth.go
sdk/go/auth/handlers.go [new file with mode: 0644]
sdk/go/auth/handlers_test.go [new file with mode: 0644]
sdk/go/health/aggregator.go
sdk/go/health/aggregator_test.go
sdk/go/httpserver/metrics.go
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/collections_controller.rb
services/api/app/models/arvados_model.rb
services/api/app/models/collection.rb
services/api/app/models/link.rb
services/api/config/application.default.yml
services/api/db/migrate/20180913175443_add_version_info_to_collections.rb [new file with mode: 0644]
services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb [new file with mode: 0644]
services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb [new file with mode: 0644]
services/api/db/migrate/20181001175023_add_preserve_version_to_collections.rb [new file with mode: 0644]
services/api/db/migrate/20181004131141_add_current_version_uuid_to_collection_search_index.rb [new file with mode: 0644]
services/api/db/structure.sql
services/api/test/fixtures/collections.yml
services/api/test/functional/arvados/v1/collections_controller_test.rb
services/api/test/functional/arvados/v1/schema_controller_test.rb
services/api/test/unit/collection_test.rb
services/api/test/unit/link_test.rb
services/arv-git-httpd/auth_handler.go
services/dockercleaner/arvados-docker-cleaner.service
services/keep-balance/balance.go
services/keep-balance/balance_run_test.go
services/keep-balance/block_state.go
services/keep-balance/integration_test.go
services/keep-balance/main.go
services/keep-balance/metrics.go [new file with mode: 0644]
services/keep-balance/server.go [new file with mode: 0644]
services/keep-balance/time_me.go [deleted file]
services/keep-balance/usage.go
services/keep-web/handler.go
services/keep-web/handler_test.go
services/keep-web/server.go
services/keep-web/server_test.go
services/keepstore/config.go
services/keepstore/handlers.go
services/keepstore/mounts_test.go
services/nodemanager/MANIFEST.in
services/nodemanager/arvados-node-manager.service [new file with mode: 0644]
services/nodemanager/setup.py

index 124a78577f3e5cac875569c8912217d65b8fc1ce..b017b4a29ae2bbd35877301f5a6f021555eb6f11 100644 (file)
@@ -87,7 +87,9 @@ SPDX-License-Identifier: AGPL-3.0 %>
                     <i class="fa fa-lg fa-terminal fa-fw"></i> Virtual machines
                   <% end %>
                 </li>
+                <% if Rails.configuration.repositories %>
                 <li role="menuitem"><a href="/repositories" role="menuitem"><i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories </a></li>
+                <% end -%>
                 <li role="menuitem"><a href="/current_token" role="menuitem"><i class="fa fa-lg fa-ticket fa-fw"></i> Current token</a></li>
                 <li role="menuitem">
                   <%= link_to ssh_keys_user_path(current_user), role: 'menu-item' do %>
@@ -121,9 +123,11 @@ SPDX-License-Identifier: AGPL-3.0 %>
                   <li role="presentation" class="dropdown-header">
                     Admin Settings
                   </li>
+                  <% if Rails.configuration.repositories %>
                   <li role="menuitem"><a href="/repositories">
                       <i class="fa fa-lg fa-code-fork fa-fw"></i> Repositories
                   </a></li>
+                  <% end -%>
                   <li role="menuitem"><a href="/virtual_machines">
                       <i class="fa fa-lg fa-terminal fa-fw"></i> Virtual machines
                   </a></li>
index e4ec4131286dac66d9a12947ad6d0ddd6bbad358..4e0a35a5550360252cae77e49e22ac1d7dec370f 100644 (file)
@@ -320,3 +320,9 @@ common:
   # Link to use for Arvados Workflow Composer app, or false if not available.
   #
   composer_url: false
+
+  #
+  # Should workbench allow management of local git repositories? Set to false if
+  # the jobs api is disabled and there are no local git repositories.
+  #
+  repositories: true
index 6d303f2e3e8cdd00b0d22cd8290d1655643472f2..ab2568ab9b8f940a07aa83e86ad7b45ba6501ab1 100755 (executable)
@@ -5,7 +5,9 @@
 
 set -e
 
-if [ "%{name}" != "%\{name\}" ]; then
+# Detect rpm-based systems: the exit code of the following command is zero
+# on rpm-based systems
+if /usr/bin/rpm -q -f /usr/bin/rpm >/dev/null 2>&1; then
     # Red Hat ("%{...}" is interpolated at package build time)
     pkg="%{name}"
     pkgtype=rpm
index d840ee1bd16cbf272a7807a2b0b36310cbb466ba..c0f45d60c698cb77f9c2472d7d18b5d3715b33c9 100755 (executable)
@@ -5,7 +5,9 @@
 
 set -e
 
-if [ "%{name}" != "%\{name\}" ]; then
+# Detect rpm-based systems: the exit code of the following command is zero
+# on rpm-based systems
+if /usr/bin/rpm -q -f /usr/bin/rpm >/dev/null 2>&1; then
     # Red Hat ("%{...}" is interpolated at package build time)
     pkg="%{name}"
     pkgtype=rpm
diff --git a/build/package-test-dockerfiles/.gitignore b/build/package-test-dockerfiles/.gitignore
new file mode 100644 (file)
index 0000000..ceee9fa
--- /dev/null
@@ -0,0 +1,2 @@
+*/generated
+common-generated/
diff --git a/build/package-test-dockerfiles/Makefile b/build/package-test-dockerfiles/Makefile
new file mode 100644 (file)
index 0000000..f2fd49b
--- /dev/null
@@ -0,0 +1,39 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+all: centos7/generated debian8/generated debian9/generated ubuntu1404/generated ubuntu1604/generated ubuntu1804/generated
+
+centos7/generated: common-generated-all
+       test -d centos7/generated || mkdir centos7/generated
+       cp -rlt centos7/generated common-generated/*
+
+debian8/generated: common-generated-all
+       test -d debian8/generated || mkdir debian8/generated
+       cp -rlt debian8/generated common-generated/*
+
+debian9/generated: common-generated-all
+       test -d debian9/generated || mkdir debian9/generated
+       cp -rlt debian9/generated common-generated/*
+
+ubuntu1404/generated: common-generated-all
+       test -d ubuntu1404/generated || mkdir ubuntu1404/generated
+       cp -rlt ubuntu1404/generated common-generated/*
+
+ubuntu1604/generated: common-generated-all
+       test -d ubuntu1604/generated || mkdir ubuntu1604/generated
+       cp -rlt ubuntu1604/generated common-generated/*
+
+ubuntu1804/generated: common-generated-all
+       test -d ubuntu1804/generated || mkdir ubuntu1804/generated
+       cp -rlt ubuntu1804/generated common-generated/*
+
+RVMKEY=rvm.asc
+
+common-generated-all: common-generated/$(RVMKEY)
+
+common-generated/$(RVMKEY): common-generated
+       wget -cqO common-generated/$(RVMKEY) https://rvm.io/mpapis.asc
+
+common-generated:
+       mkdir common-generated
diff --git a/build/package-test-dockerfiles/README b/build/package-test-dockerfiles/README
new file mode 100644 (file)
index 0000000..f938d42
--- /dev/null
@@ -0,0 +1,7 @@
+==================
+DOCKER IMAGE BUILD
+==================
+
+1. `make`
+2. `cd DISTRO`
+3. `docker build -t arvados/build:DISTRO .`
index fa959a1eb8cc2e7aa39d89338ae203c5fe599933..36be0ba32b0ac0cb11ee30a416e6d8c380a96989 100644 (file)
@@ -3,13 +3,15 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM centos:7
-MAINTAINER Ward Vandewege <ward@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
 
+# Install dependencies.
 RUN yum -q -y install scl-utils centos-release-scl which tar
 
 # Install RVM
+ADD generated/rvm.asc /tmp/
 RUN touch /var/lib/rpm/* && \
-    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+    gpg --import /tmp/rvm.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.3 && \
index c40ed820790a8672e63b9f01e19ba4ea86c3313f..fdefadea5080e4cacbd2ecbba04b7c5db1fd9b90 100644 (file)
@@ -3,14 +3,17 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM debian:8
-MAINTAINER Ward Vandewege <ward@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
 
 ENV DEBIAN_FRONTEND noninteractive
 
-# Install RVM
+# Install dependencies
 RUN apt-get update && \
-    apt-get -y install --no-install-recommends curl ca-certificates && \
-    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+    apt-get -y install --no-install-recommends curl ca-certificates
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.3
diff --git a/build/package-test-dockerfiles/debian9/D39DC0E3.asc b/build/package-test-dockerfiles/debian9/D39DC0E3.asc
deleted file mode 100644 (file)
index 231fbdd..0000000
+++ /dev/null
@@ -1,692 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-Version: GnuPG v1
-
-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
-/4cVEpiwwKMkxIWrseXTtsKJ8+hWhElzLL8cNTFCz+4ax7PKE8eCGTUXcyMyzAe0
-SysHI2bCWFfQe8RGEy+1cnLH5+qO2MwLDKKN4+at2ADZFrYn4FEZ8wvlb9/is1iN
-7h7+s2GDPpvEJrrnpeHeNTX1BEeykvbjW+Bsb89g6kJfks7PvhD+BQwVMaNrmjR8
-KYi+GaopHdQ30yLWD4ck97B+UYZnqSKk7E8NOpwU22b31VlZgI/JXWeXgQ5cY58J
-0ShGmbfypHXerthzlm6UHzjg6g+yujqDrxaOPTdfxYAzJmX5ztk3D9xMEhCAD8yp
-efTdMGQb0B2q5Yjsl3sHqNNqxTeg1iorUNz6UtEhX3RmGrTDdY0rgn0prjgiMTEM
-WWxAmgeYw/gOkpWfEZNrX64La65OkhTg+aBZEe6broHrc1x6dXpoRkxGJrlRfcsx
-Ocgy2M0/C0gk1L1yYWXRL4yENkQ/hNuPZmdDtrvWxcSTnMPfclwq6kklflC099uj
-CZf5NuOG80mZi03917LQBtgWp4INmG+B6aLY52KuINmWXm25wYPY5MfEbyO2Kzng
-JSpxdR90GbP4F5lri2Ho9eSByisb0dLq0ugBEAABAQAAAAAAAAAAAAAAAP/Y/+AA
-EEpGSUYAAQEBAGAAYAAA/+EAQEV4aWYAAElJKgAIAAAAAQBphwQAAQAAABoAAAAA
-AAAAAgACoAkAAQAAAJ8AAAADoAkAAQAAAJ8AAAAAAAAA/9sAQwAIBgYHBgUIBwcH
-CQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04
-MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy
-MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAnwCfAwEiAAIRAQMRAf/E
-AB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQE
-AAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBka
-JSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SF
-hoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY
-2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgME
-BQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKB
-CBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNU
-VVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ip
-qrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/a
-AAwDAQACEQMRAD8A9YFKTzRikxikSLTutMpw6UAKBRxQc9qOaYCe1I2BznisHX/F
-dnoSFW/fXH/PNT0+p7V5J4k8calrExRZXgg/55xEgd+vrWsKTlqcdXGQjLljqz2T
-UvEekaTj7ZfRIx/hzk/kKoQ+PvDMzhBqiBiM/MrAfnjFfP085kIBYsR+NN+6gYnB
-7YrT2MTH63U3PoR/HXhqPJ/tSM4/uqx/kKt6d4p0TU5PLtNSheT+6x2n8jivnaJg
-Y8k80hlYEccU/YR7krGVL7H1CCGGVII9jTq+crDxXqWkSIba8lXB4Utkfka9C8O/
-Fe3vJEt9Xg8h+nnpypPuO1ZyotbanRTxal8SseldTQRmo4Z47iJZInV42GQynINS
-8isTrTuJ04oNGaSgYn1opN3PSlwKAHGlptLSAMYpRSHmkBzQA+qerX8emafLdSZw
-o4x69qtj61w/xGvzHawWcbcsDI/8h/WtqMOeaRy4ys6VFyW55V4nvp7jUZHMpO87
-ic9c1iIk0gOwE+tdr4Z8HXPia482bKWqHBf19q9V0/wHo9lEgFsrMv8AEwrerVjB
-2OHC4WpOmrfefP8AHpV4SCsTE4yRtNWf7DvPL3NA+BzX0eugWIx+4TI74qZ9EtGT
-aYUwO2Kw+sdkdf1CT3kfNqac8Iw8Lg9uKryQYypVlr6Kn8L2MnWFfxFYOqeB7O4B
-8uMBs5qvrK6oyeW1L3TPBpok5yeRUEbmE9Rkd69UvvhqzZMbY9hXI6v4Lv8AT4jI
-IyyDrgU41oN6Ml4erFe8tDX+H/jT+xrs2d27NaTsOevln1r28HIyDwa+UlLRTLkY
-IPIr6D8AazJq/htDK+6aBvLY47dv0pVVdcyNcNPll7N9djqqPwopCawO8UYopM4p
-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
-aXOo2lPgP440izPGh5s38Oc=
-=8iHq
------END PGP PUBLIC KEY BLOCK-----
index 330d7d88a53adfe6cfd71f1910f5c9b3f4d39589..0bb7019fce2af0841bbd53cdeb2e52919399068f 100644 (file)
@@ -3,15 +3,17 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM debian:stretch
-MAINTAINER Nico Cesar <nico@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
 
 ENV DEBIAN_FRONTEND noninteractive
 
-# Install RVM
-COPY D39DC0E3.asc /tmp
+# Install dependencies
 RUN apt-get update && \
-    apt-get -y install --no-install-recommends curl ca-certificates gpg procps && \
-    gpg --import /tmp/D39DC0E3.asc && \
+    apt-get -y install --no-install-recommends curl ca-certificates gpg procps
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.3
index 6d929e84948e3eb3d91f974b5b292b18b22fea62..5f5b1d88191b0ddf3019594094a505b0fac13ba5 100644 (file)
@@ -3,14 +3,17 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM ubuntu:trusty
-MAINTAINER Ward Vandewege <ward@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
 
 ENV DEBIAN_FRONTEND noninteractive
 
-# Install dependencies and RVM
+# Install dependencies
 RUN apt-get update && \
-    apt-get -y install --no-install-recommends curl ca-certificates python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip binutils build-essential ca-certificates  && \
-    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+    apt-get -y install --no-install-recommends curl ca-certificates python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip binutils build-essential ca-certificates
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.3
index 54b1f401cf00cd2a3b6248676de51aee99915b29..1f65c7a474c3226976f1516850b1059094a493c0 100644 (file)
@@ -3,14 +3,17 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM ubuntu:xenial
-MAINTAINER Ward Vandewege <ward@curoverse.com>
+MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
 
 ENV DEBIAN_FRONTEND noninteractive
 
-# Install RVM
+# Install dependencies
 RUN apt-get update && \
-    apt-get -y install --no-install-recommends curl ca-certificates && \
-    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+    apt-get -y install --no-install-recommends curl ca-certificates
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.3
index 506abac11bfbef5e3f05afe160da15522db341a0..9d326c72946bb645900637ebc852b87c2d253743 100644 (file)
@@ -7,10 +7,13 @@ MAINTAINER Ward Vandewege <wvandewege@veritasgenetics.com>
 
 ENV DEBIAN_FRONTEND noninteractive
 
-# Install RVM
+# Install dependencies
 RUN apt-get update && \
-    apt-get -y install --no-install-recommends curl ca-certificates gnupg2 && \
-    gpg --keyserver ha.pool.sks-keyservers.net --recv-keys D39DC0E3 && \
+    apt-get -y install --no-install-recommends curl ca-certificates gnupg2
+
+# Install RVM
+ADD generated/rvm.asc /tmp/
+RUN gpg --import /tmp/rvm.asc && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
     /usr/local/rvm/bin/rvm alias create default ruby-2.3
index 900a5e25efc6ab024640be8950889a4ec2ac2152..46379e7b9ad0c5b29d1cd85797ad4cdbeb7e2e99 100755 (executable)
@@ -154,6 +154,7 @@ JENKINS_DIR=$(dirname "$(readlink -e "$0")")
 
 if [[ -n "$test_packages" ]]; then
     pushd "$JENKINS_DIR/package-test-dockerfiles"
+    make "$TARGET/generated"
 else
     pushd "$JENKINS_DIR/package-build-dockerfiles"
     make "$TARGET/generated"
index 88466bd376b64eb5516e1b782052c982949235c6..fe01e4b2ef2528222dfff7d097729b9b5c773b30 100755 (executable)
@@ -412,16 +412,17 @@ fi
 cd $WORKSPACE/packages/$TARGET
 rm -rf "$WORKSPACE/services/nodemanager/build"
 nodemanager_version=${ARVADOS_BUILDING_VERSION:-$(awk '($1 == "Version:"){print $2}' $WORKSPACE/services/nodemanager/arvados_node_manager.egg-info/PKG-INFO)}
-test_package_presence arvados-node-manager "$nodemanager_version" python
+iteration="${ARVADOS_BUILDING_ITERATION:-1}"
+test_package_presence arvados-node-manager "$nodemanager_version" python "$iteration"
 if [[ "$?" == "0" ]]; then
-  fpm_build $WORKSPACE/services/nodemanager arvados-node-manager 'Curoverse, Inc.' 'python' "$nodemanager_version" "--url=https://arvados.org" "--description=The Arvados node manager" --depends "${PYTHON2_PKG_PREFIX}-setuptools"
+  fpm_build $WORKSPACE/services/nodemanager arvados-node-manager 'Curoverse, Inc.' 'python' "$nodemanager_version" "--url=https://arvados.org" "--description=The Arvados node manager" --depends "${PYTHON2_PKG_PREFIX}-setuptools" --iteration "$iteration"
 fi
 
 # The Docker image cleaner
 cd $WORKSPACE/packages/$TARGET
 rm -rf "$WORKSPACE/services/dockercleaner/build"
 dockercleaner_version=${ARVADOS_BUILDING_VERSION:-$(awk '($1 == "Version:"){print $2}' $WORKSPACE/services/dockercleaner/arvados_docker_cleaner.egg-info/PKG-INFO)}
-iteration="${ARVADOS_BUILDING_ITERATION:-3}"
+iteration="${ARVADOS_BUILDING_ITERATION:-4}"
 test_package_presence arvados-docker-cleaner "$dockercleaner_version" python "$iteration"
 if [[ "$?" == "0" ]]; then
   fpm_build $WORKSPACE/services/dockercleaner arvados-docker-cleaner 'Curoverse, Inc.' 'python3' "$dockercleaner_version" "--url=https://arvados.org" "--description=The Arvados Docker image cleaner" --depends "${PYTHON3_PKG_PREFIX}-websocket-client = 0.37.0" --iteration "$iteration"
@@ -468,8 +469,9 @@ declare -a PIP_DOWNLOAD_SWITCHES=(--no-deps)
 pip install --no-use-wheel >/dev/null 2>&1
 case "$?" in
     0) PIP_DOWNLOAD_SWITCHES+=(--no-use-wheel) ;;
+    1) ;;
     2) ;;
-    *) echo "WARNING: `pip wheel` test returned unknown exit code $?" ;;
+    *) echo "WARNING: 'pip install --no-use-wheel' test returned unknown exit code $?" ;;
 esac
 
 while read -r line || [[ -n "$line" ]]; do
index 4ddbf89c1d7ccb286fcfe887fb941734bffbb519..26a907fc2f99f36b7cc7d0aa7fa8d2bf097af5f0 100755 (executable)
@@ -101,6 +101,7 @@ sdk/python:py3
 sdk/ruby
 sdk/go/arvados
 sdk/go/arvadosclient
+sdk/go/auth
 sdk/go/dispatch
 sdk/go/keepclient
 sdk/go/health
@@ -925,6 +926,7 @@ gostuff=(
     lib/dispatchcloud
     sdk/go/arvados
     sdk/go/arvadosclient
+    sdk/go/auth
     sdk/go/blockdigest
     sdk/go/dispatch
     sdk/go/health
diff --git a/doc/_includes/_create_superuser_token.liquid b/doc/_includes/_create_superuser_token.liquid
new file mode 100644 (file)
index 0000000..07d8a4a
--- /dev/null
@@ -0,0 +1,14 @@
+{% comment %}
+Copyright (C) The Arvados Authors. All rights reserved.
+
+SPDX-License-Identifier: CC-BY-SA-3.0
+{% endcomment %}
+
+On the <strong>API server</strong>, use the following commands:
+
+<notextile>
+<pre><code>~$ <span class="userinput">cd /var/www/arvados-api/current</span>
+$ <span class="userinput">sudo -u <b>webserver-user</b> RAILS_ENV=production bundle exec script/create_superuser_token.rb</span>
+zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
+</code></pre>
+</notextile>
index eb808e41835bdb3a887758f5e0044cfe03d449c8..06db793314931b8200651ba24db3df37fd1730f8 100644 (file)
@@ -56,7 +56,7 @@ Here we create a default project for the standard Arvados Docker images, and giv
 
 <notextile>
 <pre><code>~$ <span class="userinput">uuid_prefix=`arv --format=uuid user current | cut -d- -f1`</span>
-~$ <span class="userinput">project_uuid=`arv --format=uuid group create --group "{\"owner_uuid\":\"$uuid_prefix-tpzed-000000000000000\", \"name\":\"Arvados Standard Docker Images\"}"`</span>
+~$ <span class="userinput">project_uuid=`arv --format=uuid group create --group "{\"owner_uuid\":\"$uuid_prefix-tpzed-000000000000000\", \"group_class\":\"project\", \"name\":\"Arvados Standard Docker Images\"}"`</span>
 ~$ <span class="userinput">echo "Arvados project uuid is '$project_uuid'"</span>
 ~$ <span class="userinput">read -rd $'\000' newlink &lt;&lt;EOF; arv link create --link "$newlink"</span>
 <span class="userinput">{
index 630c6a178f1cbd39db459c7344ca081bc460604c..eb71fda5e628b13d0fb77153e673861edc5d6c20 100644 (file)
@@ -39,32 +39,35 @@ The healthcheck aggregator uses the @NodeProfile@ section of the cluster-wide @a
 Cluster:
   # The cluster uuid prefix
   zzzzz:
+    ManagementToken: xyzzy
     NodeProfile:
       # For each node, the profile name corresponds to a
       # locally-resolvable hostname, and describes which Arvados
       # services are available on that machine.
       api:
         arvados-controller:
-          Listen: 8000
+          Listen: :8000
         arvados-api-server:
-          Listen: 8001
+          Listen: :8001
       manage:
        arvados-node-manager:
-         Listen: 8002
+         Listen: :8002
       workbench:
        arvados-workbench:
-         Listen: 8003
+         Listen: :8003
        arvados-ws:
-         Listen: 8004
+         Listen: :8004
       keep:
        keep-web:
-         Listen: 8005
+         Listen: :8005
        keepproxy:
-         Listen: 8006
+         Listen: :8006
+       keep-balance:
+         Listen: :9005
       keep0:
         keepstore:
-         Listen: 25701
+         Listen: :25107
       keep1:
         keepstore:
-         Listen: 25701
+         Listen: :25107
 </pre>
index 45b9ece8c9926e0147313c05504f33cad994a8ef..893eac1c8325c2033c7d36b398541f1cccedfb0f 100644 (file)
@@ -146,6 +146,33 @@ h3. Example response
 }
 </pre>
 
+h2. Keep-balance
+
+Keep-balance exports metrics at @/metrics@ -- e.g., @http://keep.zzzzz.arvadosapi.com:9005/metrics@.
+
+table(table table-bordered table-condensed).
+|_. Name|_. Type|_. Description|
+|arvados_keep_total_{replicas,blocks,bytes}|gauge|stored data (stored in backend volumes, whether referenced or not)|
+|arvados_keep_garbage_{replicas,blocks,bytes}|gauge|garbage data (unreferenced, and old enough to trash)|
+|arvados_keep_transient_{replicas,blocks,bytes}|gauge|transient data (unreferenced, but too new to trash)|
+|arvados_keep_overreplicated_{replicas,blocks,bytes}|gauge|overreplicated data (more replicas exist than are needed)|
+|arvados_keep_underreplicated_{replicas,blocks,bytes}|gauge|underreplicated data (fewer replicas exist than are needed)|
+|arvados_keep_lost_{replicas,blocks,bytes}|gauge|lost data (referenced by collections, but not found on any backend volume)|
+|arvados_keep_dedup_block_ratio|gauge|deduplication ratio (block references in collections &divide; distinct blocks referenced)|
+|arvados_keep_dedup_byte_ratio|gauge|deduplication ratio (block references in collections &divide; distinct blocks referenced, weighted by block size)|
+|arvados_keepbalance_get_state_seconds|summary|time to get all collections and keepstore volume indexes for one iteration|
+|arvados_keepbalance_changeset_compute_seconds|summary|time to compute changesets for one iteration|
+|arvados_keepbalance_send_pull_list_seconds|summary|time to send pull lists to all keepstore servers for one iteration|
+|arvados_keepbalance_send_trash_list_seconds|summary|time to send trash lists to all keepstore servers for one iteration|
+|arvados_keepbalance_sweep_seconds|summary|time to complete one iteration|
+
+Each @arvados_keep_@ storage state statistic above is presented as a set of three metrics:
+
+table(table table-bordered table-condensed).
+|*_blocks|distinct block hashes|
+|*_bytes|bytes stored on backend volumes|
+|*_replicas|objects/files stored on backend volumes|
+
 h2. Node manager
 
 The node manager status end point provides a snapshot of internal status at the time of the most recent wishlist update.
index 4b3f4ec0b01fe016def2d2dbaf7e92e95b04787f..cd338296b3f4170ebe04879796ffb8171c9bcae4 100644 (file)
@@ -33,14 +33,9 @@ On Debian-based systems:
 
 h2. Create a dispatcher token
 
-Create an Arvados superuser token for use by the dispatcher. If you have multiple dispatch processes, you should give each one a different token.  *On the API server*, run:
+Create an Arvados superuser token for use by the dispatcher. If you have multiple dispatch processes, you should give each one a different token.
 
-<notextile>
-<pre><code>apiserver:~$ <span class="userinput">cd /var/www/arvados-api/current</span>
-apiserver:/var/www/arvados-api/current$ <span class="userinput">sudo -u <b>webserver-user</b> RAILS_ENV=production bundle exec script/create_superuser_token.rb</span>
-zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
-</code></pre>
-</notextile>
+{% include 'create_superuser_token' %}
 
 h2. Configure the dispatcher
 
index 3a8dce078dd092bfe687639f912415b2553bf14c..68bf07a4ae50020a40e73efffddb876c216d1607 100644 (file)
@@ -55,7 +55,7 @@ Options:
 
 h3. Create a keep-balance token
 
-Create an Arvados superuser token for use by keep-balance. *On the API server*, run:
+Create an Arvados superuser token for use by keep-balance.
 
 {% include 'create_superuser_token' %}
 
@@ -75,11 +75,14 @@ h3. Create a keep-balance configuration file
 On the host running keep-balance, create @/etc/arvados/keep-balance/keep-balance.yml@ using the token you generated above.  Follow this YAML format:
 
 <notextile>
-<pre><code>Client:
+<pre><code>Listen: :9005
+Client:
   APIHost: <span class="userinput">uuid_prefix.your.domain</span>:443
   AuthToken: zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz
 KeepServiceTypes:
   - disk
+Listen: :9005
+ManagementToken: <span class="userinput">xyzzy</span>
 RunPeriod: 10m
 CollectionBatchSize: 100000
 CollectionBuffers: 1000
index 9f580c0f8b2af0f0244c1ae1570c4346d33cd6ac..0a47eba1bdf02b18ff3a2f9325ed8f1c583af5ac 100644 (file)
@@ -105,7 +105,7 @@ h3. Tell the API server about the Keepproxy server
 
 The API server needs to be informed about the presence of your Keepproxy server.
 
-First, if you don't already have an admin token, create a superuser token:
+First, if you don't already have an admin token, create a superuser token.
 
 {% include 'create_superuser_token' %}
 
index 943c9bae36b1c7e2358a83f9636bb3eb3ddf3cd3..5044cc0c21596c6ee577eb6011e1f901cc82c0a5 100644 (file)
@@ -88,9 +88,9 @@ Listen: :25107
 # Format of request/response and error logs: "json" or "text".
 LogFormat: json
 
-# The secret key that must be provided by monitoring services
-# wishing to access the health check endpoint (/_health).
-ManagementToken: ""
+# The secret key that must be provided by monitoring services when
+# using the health check and metrics endpoints (/_health, /metrics).
+ManagementToken: xyzzy
 
 # Maximum RAM to use for data buffers, given in multiples of block
 # size (64 MiB). When this limit is reached, HTTP requests requiring
@@ -200,7 +200,7 @@ h3. Tell the API server about the Keepstore servers
 
 The API server needs to be informed about the presence of your Keepstore servers.
 
-First, if you don't already have an admin token, create a superuser token:
+First, if you don't already have an admin token, create a superuser token.
 
 {% include 'create_superuser_token' %}
 
index b8ffcc54b3d41643777ede7b9fb027f436751c3c..1cbe74997213ad24379085a6fd74a1d31e374654 100644 (file)
@@ -88,7 +88,7 @@ Create an Arvados virtual_machine object representing this shell server. This wi
 
 <notextile>
 <pre>
-<code>apiserver:~$ <span class="userinput">arv --format=uuid virtual_machine create --virtual-machine '{"hostname":"<b>your.shell.server.hostname</b>"}'</span>
+<code>apiserver:~$ <span class="userinput">arv --format=uuid virtual_machine create --virtual-machine '{"hostname":"<b>your.shell.server.hostname.without.domain</b>"}'</span>
 zzzzz-2x53u-zzzzzzzzzzzzzzz</code>
 </pre>
 </notextile>
index c723be7d10d4013b04e7a8b75ff7176bfb8f58f3..e2e9907d5d115ebb61a53ae873249511b3732a50 100644 (file)
@@ -161,6 +161,7 @@ func (cc *Cluster) GetNodeProfile(node string) (*NodeProfile, error) {
 type NodeProfile struct {
        Controller  SystemServiceInstance `json:"arvados-controller"`
        Health      SystemServiceInstance `json:"arvados-health"`
+       Keepbalance SystemServiceInstance `json:"keep-balance"`
        Keepproxy   SystemServiceInstance `json:"keepproxy"`
        Keepstore   SystemServiceInstance `json:"keepstore"`
        Keepweb     SystemServiceInstance `json:"keep-web"`
@@ -178,6 +179,7 @@ const (
        ServiceNameNodemanager ServiceName = "arvados-node-manager"
        ServiceNameWorkbench   ServiceName = "arvados-workbench"
        ServiceNameWebsocket   ServiceName = "arvados-ws"
+       ServiceNameKeepbalance ServiceName = "keep-balance"
        ServiceNameKeepweb     ServiceName = "keep-web"
        ServiceNameKeepproxy   ServiceName = "keepproxy"
        ServiceNameKeepstore   ServiceName = "keepstore"
@@ -192,6 +194,7 @@ func (np *NodeProfile) ServicePorts() map[ServiceName]string {
                ServiceNameNodemanager: np.Nodemanager.Listen,
                ServiceNameWorkbench:   np.Workbench.Listen,
                ServiceNameWebsocket:   np.Websocket.Listen,
+               ServiceNameKeepbalance: np.Keepbalance.Listen,
                ServiceNameKeepweb:     np.Keepweb.Listen,
                ServiceNameKeepproxy:   np.Keepproxy.Listen,
                ServiceNameKeepstore:   np.Keepstore.Listen,
index eb79b5b7d208a4d16e7cb8c660efc54c1d2142a7..114faf17b74e245aeaacf72aeaaf5bb6f8e5046a 100644 (file)
@@ -23,6 +23,7 @@ const (
        NonexistentCollection   = "zzzzz-4zz18-totallynotexist"
        HelloWorldCollection    = "zzzzz-4zz18-4en62shvi99lxd4"
        FooBarDirCollection     = "zzzzz-4zz18-foonbarfilesdir"
+       WazVersion1Collection   = "zzzzz-4zz18-25k12570yk1ver1"
        UserAgreementPDH        = "b519d9cb706a29fc7ea24dbea2f05851+93"
        FooPdh                  = "1f4b0bc7583c2a7f9102c395f4ffc5e3+45"
        HelloWorldPdh           = "55713e6a34081eb03609e7ad5fcad129+62"
index ad1d398c763d7eaacefefcde8993e39044582f2a..3c266e0d3afda2254df6b3c7ccad7157a121bc6c 100644 (file)
@@ -19,7 +19,11 @@ func NewCredentials() *Credentials {
        return &Credentials{Tokens: []string{}}
 }
 
-func NewCredentialsFromHTTPRequest(r *http.Request) *Credentials {
+func CredentialsFromRequest(r *http.Request) *Credentials {
+       if c, ok := r.Context().Value(contextKeyCredentials).(*Credentials); ok {
+               // preloaded by middleware
+               return c
+       }
        c := NewCredentials()
        c.LoadTokensFromHTTPRequest(r)
        return c
diff --git a/sdk/go/auth/handlers.go b/sdk/go/auth/handlers.go
new file mode 100644 (file)
index 0000000..ad1fa51
--- /dev/null
@@ -0,0 +1,50 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package auth
+
+import (
+       "context"
+       "net/http"
+)
+
+type contextKey string
+
+var contextKeyCredentials contextKey = "credentials"
+
+// LoadToken wraps the next handler, adding credentials to the request
+// context so subsequent handlers can access them efficiently via
+// CredentialsFromRequest.
+func LoadToken(next http.Handler) http.Handler {
+       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               if _, ok := r.Context().Value(contextKeyCredentials).(*Credentials); !ok {
+                       r = r.WithContext(context.WithValue(r.Context(), contextKeyCredentials, CredentialsFromRequest(r)))
+               }
+               next.ServeHTTP(w, r)
+       })
+}
+
+// RequireLiteralToken wraps the next handler, rejecting any request
+// that doesn't supply the given token. If the given token is empty,
+// RequireLiteralToken returns next (i.e., no auth checks are
+// performed).
+func RequireLiteralToken(token string, next http.Handler) http.Handler {
+       if token == "" {
+               return next
+       }
+       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               c := CredentialsFromRequest(r)
+               if len(c.Tokens) == 0 {
+                       http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
+                       return
+               }
+               for _, t := range c.Tokens {
+                       if t == token {
+                               next.ServeHTTP(w, r)
+                               return
+                       }
+               }
+               http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
+       })
+}
diff --git a/sdk/go/auth/handlers_test.go b/sdk/go/auth/handlers_test.go
new file mode 100644 (file)
index 0000000..362aeb7
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package auth
+
+import (
+       "net/http"
+       "net/http/httptest"
+       "testing"
+
+       check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+func Test(t *testing.T) {
+       check.TestingT(t)
+}
+
+var _ = check.Suite(&HandlersSuite{})
+
+type HandlersSuite struct {
+       served         int
+       gotCredentials *Credentials
+}
+
+func (s *HandlersSuite) SetUpTest(c *check.C) {
+       s.served = 0
+       s.gotCredentials = nil
+}
+
+func (s *HandlersSuite) TestLoadToken(c *check.C) {
+       handler := LoadToken(s)
+       handler.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/foo/bar?api_token=xyzzy", nil))
+       c.Assert(s.gotCredentials, check.NotNil)
+       c.Assert(s.gotCredentials.Tokens, check.HasLen, 1)
+       c.Check(s.gotCredentials.Tokens[0], check.Equals, "xyzzy")
+}
+
+func (s *HandlersSuite) TestRequireLiteralTokenEmpty(c *check.C) {
+       handler := RequireLiteralToken("", s)
+
+       w := httptest.NewRecorder()
+       handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar?api_token=abcdef", nil))
+       c.Check(s.served, check.Equals, 1)
+       c.Check(w.Code, check.Equals, http.StatusOK)
+
+       w = httptest.NewRecorder()
+       handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar", nil))
+       c.Check(s.served, check.Equals, 2)
+       c.Check(w.Code, check.Equals, http.StatusOK)
+}
+
+func (s *HandlersSuite) TestRequireLiteralToken(c *check.C) {
+       handler := RequireLiteralToken("xyzzy", s)
+
+       w := httptest.NewRecorder()
+       handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar?api_token=abcdef", nil))
+       c.Check(s.served, check.Equals, 0)
+       c.Check(w.Code, check.Equals, http.StatusForbidden)
+
+       w = httptest.NewRecorder()
+       handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar", nil))
+       c.Check(s.served, check.Equals, 0)
+       c.Check(w.Code, check.Equals, http.StatusUnauthorized)
+
+       w = httptest.NewRecorder()
+       handler.ServeHTTP(w, httptest.NewRequest("GET", "/foo/bar?api_token=xyzzy", nil))
+       c.Check(s.served, check.Equals, 1)
+       c.Check(w.Code, check.Equals, http.StatusOK)
+       c.Assert(s.gotCredentials, check.NotNil)
+       c.Assert(s.gotCredentials.Tokens, check.HasLen, 1)
+       c.Check(s.gotCredentials.Tokens[0], check.Equals, "xyzzy")
+}
+
+func (s *HandlersSuite) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       s.served++
+       s.gotCredentials = CredentialsFromRequest(r)
+}
index a6cb8798aa328a468c1db98c3c3e5bf38773f15c..564331327a8d53ad250b044112f25e1b07730444 100644 (file)
@@ -217,7 +217,7 @@ func (agg *Aggregator) ping(url string, cluster *arvados.Cluster) (result CheckR
 }
 
 func (agg *Aggregator) checkAuth(req *http.Request, cluster *arvados.Cluster) bool {
-       creds := auth.NewCredentialsFromHTTPRequest(req)
+       creds := auth.CredentialsFromRequest(req)
        for _, token := range creds.Tokens {
                if token != "" && token == cluster.ManagementToken {
                        return true
index a96ed136cbd1539d986a1332a4914c61af335d6a..cb47c9e6705ea096087199813050e3c3095f4974 100644 (file)
@@ -108,6 +108,7 @@ func (s *AggregatorSuite) TestHealthy(c *check.C) {
        defer srv.Close()
        s.handler.Config.Clusters["zzzzz"].NodeProfiles["localhost"] = arvados.NodeProfile{
                Controller:  arvados.SystemServiceInstance{Listen: listen},
+               Keepbalance: arvados.SystemServiceInstance{Listen: listen},
                Keepproxy:   arvados.SystemServiceInstance{Listen: listen},
                Keepstore:   arvados.SystemServiceInstance{Listen: listen},
                Keepweb:     arvados.SystemServiceInstance{Listen: listen},
@@ -132,6 +133,7 @@ func (s *AggregatorSuite) TestHealthyAndUnhealthy(c *check.C) {
        defer srvU.Close()
        s.handler.Config.Clusters["zzzzz"].NodeProfiles["localhost"] = arvados.NodeProfile{
                Controller:  arvados.SystemServiceInstance{Listen: listenH},
+               Keepbalance: arvados.SystemServiceInstance{Listen: listenH},
                Keepproxy:   arvados.SystemServiceInstance{Listen: listenH},
                Keepstore:   arvados.SystemServiceInstance{Listen: listenH},
                Keepweb:     arvados.SystemServiceInstance{Listen: listenH},
index b52068e9571d8518a5eb5afee1267e9470821617..a0455f11b11b19ac2d4c88d87554d9d7c5794d2a 100644 (file)
@@ -10,6 +10,7 @@ import (
        "strings"
        "time"
 
+       "git.curoverse.com/arvados.git/sdk/go/auth"
        "git.curoverse.com/arvados.git/sdk/go/stats"
        "github.com/Sirupsen/logrus"
        "github.com/gogo/protobuf/jsonpb"
@@ -23,7 +24,7 @@ type Handler interface {
        // Returns an http.Handler that serves the Handler's metrics
        // data at /metrics and /metrics.json, and passes other
        // requests through to next.
-       ServeAPI(next http.Handler) http.Handler
+       ServeAPI(token string, next http.Handler) http.Handler
 }
 
 type metrics struct {
@@ -73,19 +74,24 @@ func (m *metrics) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 // metrics API endpoints (currently "GET /metrics(.json)?") and passes
 // other requests through to next.
 //
+// If the given token is not empty, that token must be supplied by a
+// client in order to access the metrics endpoints.
+//
 // Typical example:
 //
 //     m := Instrument(...)
-//     srv := http.Server{Handler: m.ServeAPI(m)}
-func (m *metrics) ServeAPI(next http.Handler) http.Handler {
+//     srv := http.Server{Handler: m.ServeAPI("secrettoken", m)}
+func (m *metrics) ServeAPI(token string, next http.Handler) http.Handler {
+       jsonMetrics := auth.RequireLiteralToken(token, http.HandlerFunc(m.exportJSON))
+       plainMetrics := auth.RequireLiteralToken(token, m.exportProm)
        return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
                switch {
                case req.Method != "GET" && req.Method != "HEAD":
                        next.ServeHTTP(w, req)
                case req.URL.Path == "/metrics.json":
-                       m.exportJSON(w, req)
+                       jsonMetrics.ServeHTTP(w, req)
                case req.URL.Path == "/metrics":
-                       m.exportProm.ServeHTTP(w, req)
+                       plainMetrics.ServeHTTP(w, req)
                default:
                        next.ServeHTTP(w, req)
                }
index a0555d13d762a495d9e30a57347ed3336f0e0984..24a2214d974d91d61f5a884226056860d73e2697 100644 (file)
@@ -189,7 +189,10 @@ class ApplicationController < ActionController::Base
   end
 
   def find_objects_for_index
-    @objects ||= model_class.readable_by(*@read_users, {:include_trash => (params[:include_trash] || 'untrash' == action_name)})
+    @objects ||= model_class.readable_by(*@read_users, {
+      :include_trash => (params[:include_trash] || 'untrash' == action_name),
+      :include_old_versions => params[:include_old_versions]
+    })
     apply_where_limit_order_params
   end
 
index 6e77c12a1d6f37a88b45c4875ee43e7c912b94a9..5d7a7ae266b82fa918a47312c6584d0897a6231c 100644 (file)
@@ -15,6 +15,9 @@ class Arvados::V1::CollectionsController < ApplicationController
         include_trash: {
           type: 'boolean', required: false, description: "Include collections whose is_trashed attribute is true."
         },
+        include_old_versions: {
+          type: 'boolean', required: false, description: "Include past collection versions."
+        },
       })
   end
 
@@ -23,17 +26,26 @@ class Arvados::V1::CollectionsController < ApplicationController
       resource_attrs[:portable_data_hash] = loc.to_s
       resource_attrs.delete :uuid
     end
+    resource_attrs.delete :version
+    resource_attrs.delete :current_version_uuid
     super
   end
 
   def find_objects_for_index
+    opts = {}
     if params[:include_trash] || ['destroy', 'trash', 'untrash'].include?(action_name)
-      @objects = Collection.readable_by(*@read_users, {include_trash: true})
+      opts.update({include_trash: true})
+    end
+    if params[:include_old_versions] || @include_old_versions
+      opts.update({include_old_versions: true})
     end
+    @objects = Collection.readable_by(*@read_users, opts) if !opts.empty?
     super
   end
 
   def find_object_by_uuid
+    @include_old_versions = true
+
     if loc = Keep::Locator.parse(params[:id])
       loc.strip_hints!
 
index c67a3961d94a404468fcebd08107869b8e59ac46..801da17dbee5455e33991a64d542d4ff9eaad1da 100644 (file)
@@ -240,7 +240,7 @@ class ArvadosModel < ActiveRecord::Base
     end.compact.uniq
   end
 
-  # Return a query with read permissions restricted to the union of of the
+  # Return a query with read permissions restricted to the union of the
   # permissions of the members of users_list, i.e. if something is readable by
   # any user in users_list, it will be readable in the query returned by this
   # function.
@@ -258,6 +258,7 @@ class ArvadosModel < ActiveRecord::Base
     # Collect the UUIDs of the authorized users.
     sql_table = kwargs.fetch(:table_name, table_name)
     include_trash = kwargs.fetch(:include_trash, false)
+    include_old_versions = kwargs.fetch(:include_old_versions, false)
 
     sql_conds = nil
     user_uuids = users_list.map { |u| u.uuid }
@@ -268,6 +269,11 @@ class ArvadosModel < ActiveRecord::Base
       exclude_trashed_records = "AND #{sql_table}.is_trashed = false"
     end
 
+    exclude_old_versions = ""
+    if !include_old_versions && sql_table == "collections"
+      exclude_old_versions = "AND #{sql_table}.uuid = #{sql_table}.current_version_uuid"
+    end
+
     if users_list.select { |u| u.is_admin }.any?
       # Admin skips most permission checks, but still want to filter on trashed items.
       if !include_trash
@@ -275,7 +281,7 @@ class ArvadosModel < ActiveRecord::Base
           # Only include records where the owner is not trashed
           sql_conds = "NOT EXISTS(SELECT 1 FROM #{PERMISSION_VIEW} "+
                       "WHERE trashed = 1 AND "+
-                      "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records}"
+                      "(#{sql_table}.owner_uuid = target_uuid)) #{exclude_trashed_records} #{exclude_old_versions}"
         end
       end
     else
@@ -312,7 +318,7 @@ class ArvadosModel < ActiveRecord::Base
                        "(#{sql_table}.head_uuid IN (:user_uuids) OR #{sql_table}.tail_uuid IN (:user_uuids)))"
       end
 
-      sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records}"
+      sql_conds = "(#{direct_check} #{owner_check} #{links_cond}) #{exclude_trashed_records} #{exclude_old_versions}"
 
     end
 
@@ -363,7 +369,13 @@ class ArvadosModel < ActiveRecord::Base
         end
 
         self[:name] = new_name
-        self[:uuid] = nil if uuid_was.nil? && !uuid.nil?
+        if uuid_was.nil? && !uuid.nil?
+          self[:uuid] = nil
+          if self.is_a? Collection
+            # Reset so that is assigned to the new UUID
+            self[:current_version_uuid] = nil
+          end
+        end
         conn.exec_query 'SAVEPOINT save_with_unique_name'
         retry
       ensure
@@ -537,12 +549,12 @@ class ArvadosModel < ActiveRecord::Base
 
   def update_modified_by_fields
     current_time = db_current_time
-    self.created_at = created_at_was || current_time
+    self.created_at ||= created_at_was || current_time
     self.updated_at = current_time
     self.owner_uuid ||= current_default_owner if self.respond_to? :owner_uuid=
-    self.modified_at = current_time
     if !anonymous_updater
       self.modified_by_user_uuid = current_user ? current_user.uuid : nil
+      self.modified_at = current_time
     end
     self.modified_by_client_uuid = current_api_client ? current_api_client.uuid : nil
     true
index 525a80b9eef45871e6313861cb2accb0ae8a6680..718ffc0d0a51416440ff75ec98c442cfe64423b9 100644 (file)
@@ -27,7 +27,10 @@ class Collection < ArvadosModel
   validate :ensure_pdh_matches_manifest_text
   validate :ensure_storage_classes_desired_is_not_empty
   validate :ensure_storage_classes_contain_non_empty_strings
+  validate :versioning_metadata_updates, on: :update
+  validate :past_versions_cannot_be_updated, on: :update
   before_save :set_file_names
+  around_update :manage_versioning
 
   api_accessible :user, extend: :common do |t|
     t.add :name
@@ -45,6 +48,9 @@ class Collection < ArvadosModel
     t.add :delete_at
     t.add :trash_at
     t.add :is_trashed
+    t.add :version
+    t.add :current_version_uuid
+    t.add :preserve_version
   end
 
   after_initialize do
@@ -211,6 +217,101 @@ class Collection < ArvadosModel
     self.manifest_text ||= ''
   end
 
+  def skip_uuid_existence_check
+    # Avoid checking the existence of current_version_uuid, as it's
+    # assigned on creation of a new 'current version' collection, so
+    # the collection's UUID only lives on memory when the validation check
+    # is performed.
+    ['current_version_uuid']
+  end
+
+  def manage_versioning
+    should_preserve_version = should_preserve_version? # Time sensitive, cache value
+    return(yield) unless (should_preserve_version || syncable_updates.any?)
+
+    # Put aside the changes because with_lock forces a record reload
+    changes = self.changes
+    snapshot = nil
+    with_lock do
+      # Copy the original state to save it as old version
+      if should_preserve_version
+        snapshot = self.dup
+        snapshot.uuid = nil # Reset UUID so it's created as a new record
+        snapshot.created_at = self.created_at
+      end
+
+      # Restore requested changes on the current version
+      changes.keys.each do |attr|
+        if attr == 'preserve_version' && changes[attr].last == false
+          next # Ignore false assignment, once true it'll be true until next version
+        end
+        self.attributes = {attr => changes[attr].last}
+        if attr == 'uuid'
+          # Also update the current version reference
+          self.attributes = {'current_version_uuid' => changes[attr].last}
+        end
+      end
+
+      if should_preserve_version
+        self.version += 1
+        self.preserve_version = false
+      end
+
+      yield
+
+      sync_past_versions if syncable_updates.any?
+      if snapshot
+        snapshot.attributes = self.syncable_updates
+        snapshot.save
+      end
+    end
+  end
+
+  def syncable_updates
+    updates = {}
+    (syncable_attrs & self.changes.keys).each do |attr|
+      if attr == 'uuid'
+        # Point old versions to current version's new UUID
+        updates['current_version_uuid'] = self.changes[attr].last
+      else
+        updates[attr] = self.changes[attr].last
+      end
+    end
+    return updates
+  end
+
+  def sync_past_versions
+    updates = self.syncable_updates
+    Collection.where('current_version_uuid = ? AND uuid != ?', self.uuid_was, self.uuid_was).each do |c|
+      c.attributes = updates
+      # Use a different validation context to skip the 'old_versions_cannot_be_updated'
+      # validator, as on this case it is legal to update some fields.
+      leave_modified_by_user_alone do
+        c.save(context: :update_old_versions)
+      end
+    end
+  end
+
+  def versionable_updates?(attrs)
+    (['manifest_text', 'description', 'properties', 'name'] & attrs).any?
+  end
+
+  def syncable_attrs
+    ['uuid', 'owner_uuid', 'delete_at', 'trash_at', 'is_trashed', 'replication_desired', 'storage_classes_desired']
+  end
+
+  def should_preserve_version?
+    return false unless (Rails.configuration.collection_versioning && versionable_updates?(self.changes.keys))
+
+    idle_threshold = Rails.configuration.preserve_version_if_idle
+    if !self.preserve_version_was &&
+      (idle_threshold < 0 ||
+        (idle_threshold > 0 && self.modified_at_was > db_current_time-idle_threshold.seconds))
+      return false
+    end
+    return true
+  end
+
   def check_encoding
     if manifest_text.encoding.name == 'UTF-8' and manifest_text.valid_encoding?
       true
@@ -443,7 +544,7 @@ class Collection < ArvadosModel
   end
 
   def self.full_text_searchable_columns
-    super - ["manifest_text", "storage_classes_desired", "storage_classes_confirmed"]
+    super - ["manifest_text", "storage_classes_desired", "storage_classes_confirmed", "current_version_uuid"]
   end
 
   def self.where *args
@@ -516,4 +617,32 @@ class Collection < ArvadosModel
       end
     end
   end
+
+  def past_versions_cannot_be_updated
+    # We check for the '_was' values just in case the update operation
+    # includes a change on current_version_uuid or uuid.
+    if current_version_uuid_was != uuid_was
+      errors.add(:base, "past versions cannot be updated")
+      false
+    end
+  end
+
+  def versioning_metadata_updates
+    valid = true
+    if (current_version_uuid_was == uuid_was) && current_version_uuid_changed?
+      errors.add(:current_version_uuid, "cannot be updated")
+      valid = false
+    end
+    if version_changed?
+      errors.add(:version, "cannot be updated")
+      valid = false
+    end
+    valid
+  end
+
+  def assign_uuid
+    super
+    self.current_version_uuid ||= self.uuid
+    true
+  end
 end
index dc961667b0cd474301c817a3c87c9717ba4424f7..bf21cf4b672263b784d24b4f2cfcb00d65c0b195 100644 (file)
@@ -48,8 +48,12 @@ class Link < ArvadosModel
     # Administrators can grant permissions
     return true if current_user.is_admin
 
-    # All users can grant permissions on objects they own or can manage
     head_obj = ArvadosModel.find_by_uuid(head_uuid)
+
+    # No permission links can be pointed to past collection versions
+    return false if head_obj.is_a?(Collection) && head_obj.current_version_uuid != head_uuid
+
+    # All users can grant permissions on objects they own or can manage
     return true if current_user.can?(manage: head_obj)
 
     # Default = deny.
index 4aa21733c1e44cd94441645b22b4d9c312e61fbf..dcf270e3fb5d1a59a25e9858fc65e2eb2b901c42 100644 (file)
@@ -518,6 +518,17 @@ common:
   # keep_web_service_url: https://download.uuid_prefix.arvadosapi.com/
   keep_web_service_url: false
 
+  # If true, enable collection versioning.
+  # When a collection's preserve_version field is true or the current version
+  # is older than the amount of seconds defined on preserve_version_if_idle,
+  # a snapshot of the collection's previous state is created and linked to
+  # the current collection.
+  collection_versioning: false
+  #   0 = auto-create a new version on every update.
+  #  -1 = never auto-create new versions.
+  # > 0 = auto-create a new version when older than the specified number of seconds.
+  preserve_version_if_idle: -1
+
 development:
   force_ssl: false
   cache_classes: false
diff --git a/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb b/services/api/db/migrate/20180913175443_add_version_info_to_collections.rb
new file mode 100644 (file)
index 0000000..a624dd9
--- /dev/null
@@ -0,0 +1,14 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class AddVersionInfoToCollections < ActiveRecord::Migration
+  def change
+    # Do changes in bulk to save time on huge tables
+    change_table :collections, :bulk => true do |t|
+      t.string :current_version_uuid
+      t.integer :version, null: false, default: 1
+      t.index [:current_version_uuid, :version], unique: true
+    end
+  end
+end
diff --git a/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb b/services/api/db/migrate/20180915155335_set_current_version_uuid_on_collections.rb
new file mode 100644 (file)
index 0000000..12a08e0
--- /dev/null
@@ -0,0 +1,13 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class SetCurrentVersionUuidOnCollections < ActiveRecord::Migration
+  def up
+    # Set the current version uuid as itself
+    Collection.where(current_version_uuid: nil).update_all("current_version_uuid=uuid")
+  end
+
+  def down
+  end
+end
diff --git a/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb b/services/api/db/migrate/20180919001158_recreate_collection_unique_name_index.rb
new file mode 100644 (file)
index 0000000..6403956
--- /dev/null
@@ -0,0 +1,27 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class RecreateCollectionUniqueNameIndex < ActiveRecord::Migration
+  def up
+    Collection.transaction do
+      remove_index(:collections,
+                   name: 'index_collections_on_owner_uuid_and_name')
+      add_index(:collections, [:owner_uuid, :name],
+                unique: true,
+                where: 'is_trashed = false AND current_version_uuid = uuid',
+                name: 'index_collections_on_owner_uuid_and_name')
+    end
+  end
+
+  def down
+    Collection.transaction do
+      remove_index(:collections,
+                   name: 'index_collections_on_owner_uuid_and_name')
+      add_index(:collections, [:owner_uuid, :name],
+                unique: true,
+                where: 'is_trashed = false',
+                name: 'index_collections_on_owner_uuid_and_name')
+    end
+  end
+end
diff --git a/services/api/db/migrate/20181001175023_add_preserve_version_to_collections.rb b/services/api/db/migrate/20181001175023_add_preserve_version_to_collections.rb
new file mode 100644 (file)
index 0000000..fbdc397
--- /dev/null
@@ -0,0 +1,9 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class AddPreserveVersionToCollections < ActiveRecord::Migration
+  def change
+    add_column :collections, :preserve_version, :boolean, default: false
+  end
+end
diff --git a/services/api/db/migrate/20181004131141_add_current_version_uuid_to_collection_search_index.rb b/services/api/db/migrate/20181004131141_add_current_version_uuid_to_collection_search_index.rb
new file mode 100644 (file)
index 0000000..63e9919
--- /dev/null
@@ -0,0 +1,17 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class AddCurrentVersionUuidToCollectionSearchIndex < ActiveRecord::Migration
+  disable_ddl_transaction!
+
+  def up
+    remove_index :collections, :name => 'collections_search_index'
+    add_index :collections, ["owner_uuid", "modified_by_client_uuid", "modified_by_user_uuid", "portable_data_hash", "uuid", "name", "current_version_uuid"], name: 'collections_search_index', algorithm: :concurrently
+  end
+
+  def down
+    remove_index :collections, :name => 'collections_search_index'
+    add_index :collections, ["owner_uuid", "modified_by_client_uuid", "modified_by_user_uuid", "portable_data_hash", "uuid", "name"], name: 'collections_search_index', algorithm: :concurrently
+  end
+end
index 636306f976e3b98a3368f0f1ed9f4dca4dd50287..b9db1add053cf53a1295f187a1b8d9244895ec99 100644 (file)
@@ -172,7 +172,10 @@ CREATE TABLE public.collections (
     is_trashed boolean DEFAULT false NOT NULL,
     storage_classes_desired jsonb DEFAULT '["default"]'::jsonb,
     storage_classes_confirmed jsonb DEFAULT '[]'::jsonb,
-    storage_classes_confirmed_at timestamp without time zone
+    storage_classes_confirmed_at timestamp without time zone,
+    current_version_uuid character varying,
+    version integer DEFAULT 1 NOT NULL,
+    preserve_version boolean DEFAULT false
 );
 
 
@@ -1635,7 +1638,7 @@ CREATE INDEX collections_full_text_search_idx ON public.collections USING gin (t
 -- Name: collections_search_index; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE INDEX collections_search_index ON public.collections USING btree (owner_uuid, modified_by_client_uuid, modified_by_user_uuid, portable_data_hash, uuid, name);
+CREATE INDEX collections_search_index ON public.collections USING btree (owner_uuid, modified_by_client_uuid, modified_by_user_uuid, portable_data_hash, uuid, name, current_version_uuid);
 
 
 --
@@ -1785,6 +1788,13 @@ CREATE UNIQUE INDEX index_authorized_keys_on_uuid ON public.authorized_keys USIN
 CREATE INDEX index_collections_on_created_at ON public.collections USING btree (created_at);
 
 
+--
+-- Name: index_collections_on_current_version_uuid_and_version; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE UNIQUE INDEX index_collections_on_current_version_uuid_and_version ON public.collections USING btree (current_version_uuid, version);
+
+
 --
 -- Name: index_collections_on_delete_at; Type: INDEX; Schema: public; Owner: -
 --
@@ -1824,7 +1834,7 @@ CREATE INDEX index_collections_on_owner_uuid ON public.collections USING btree (
 -- Name: index_collections_on_owner_uuid_and_name; Type: INDEX; Schema: public; Owner: -
 --
 
-CREATE UNIQUE INDEX index_collections_on_owner_uuid_and_name ON public.collections USING btree (owner_uuid, name) WHERE (is_trashed = false);
+CREATE UNIQUE INDEX index_collections_on_owner_uuid_and_name ON public.collections USING btree (owner_uuid, name) WHERE ((is_trashed = false) AND ((current_version_uuid)::text = (uuid)::text));
 
 
 --
@@ -3173,9 +3183,18 @@ INSERT INTO schema_migrations (version) VALUES ('20180824155207');
 
 INSERT INTO schema_migrations (version) VALUES ('20180904110712');
 
+INSERT INTO schema_migrations (version) VALUES ('20180913175443');
+
+INSERT INTO schema_migrations (version) VALUES ('20180915155335');
+
 INSERT INTO schema_migrations (version) VALUES ('20180917205609');
 
 INSERT INTO schema_migrations (version) VALUES ('20181005192222');
 
 INSERT INTO schema_migrations (version) VALUES ('20181011184200');
 
+INSERT INTO schema_migrations (version) VALUES ('20180919001158');
+
+INSERT INTO schema_migrations (version) VALUES ('20181001175023');
+
+INSERT INTO schema_migrations (version) VALUES ('20181004131141');
index 7ff67f82ee9f0fa1f5d1bdf0f3b45b02b3b7fae8..29b486c452d4b01a353f364e2672dda2c2f5cee1 100644 (file)
@@ -4,6 +4,7 @@
 
 user_agreement:
   uuid: zzzzz-4zz18-t68oksiu9m80s4y
+  current_version_uuid: zzzzz-4zz18-t68oksiu9m80s4y
   portable_data_hash: b519d9cb706a29fc7ea24dbea2f05851+93
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2013-12-26T19:22:54Z
@@ -16,6 +17,7 @@ user_agreement:
 
 collection_owned_by_active:
   uuid: zzzzz-4zz18-bv31uwvy3neko21
+  current_version_uuid: zzzzz-4zz18-bv31uwvy3neko21
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -25,9 +27,25 @@ collection_owned_by_active:
   updated_at: 2014-02-03T17:22:54Z
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: owned_by_active
+  version: 2
+
+collection_owned_by_active_past_version_1:
+  uuid: zzzzz-4zz18-znfnqtbbv4spast
+  current_version_uuid: zzzzz-4zz18-bv31uwvy3neko21
+  portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2014-02-03T17:22:54Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2014-02-03T15:22:54Z
+  updated_at: 2014-02-03T15:22:54Z
+  manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+  name: owned_by_active_version_1
+  version: 1
 
 foo_file:
   uuid: zzzzz-4zz18-znfnqtbbv4spc3w
+  current_version_uuid: zzzzz-4zz18-znfnqtbbv4spc3w
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2015-02-03T17:22:54Z
@@ -40,6 +58,7 @@ foo_file:
 
 bar_file:
   uuid: zzzzz-4zz18-ehbhgtheo8909or
+  current_version_uuid: zzzzz-4zz18-ehbhgtheo8909or
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2015-02-03T17:22:54Z
@@ -52,6 +71,7 @@ bar_file:
 
 baz_file:
   uuid: zzzzz-4zz18-y9vne9npefyxh8g
+  current_version_uuid: zzzzz-4zz18-y9vne9npefyxh8g
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2014-02-03T17:22:54Z
@@ -64,6 +84,7 @@ baz_file:
 
 w_a_z_file:
   uuid: zzzzz-4zz18-25k12570yk134b3
+  current_version_uuid: zzzzz-4zz18-25k12570yk134b3
   portable_data_hash: 8706aadd12a0ebc07d74cae88762ba9e+56
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-09T10:53:38Z
@@ -73,9 +94,25 @@ w_a_z_file:
   updated_at: 2015-02-09T10:53:38Z
   manifest_text: ". 4c6c2c0ac8aa0696edd7316a3be5ca3c+5 0:5:w\\040\\141\\040z\n"
   name: "\"w a z\" file"
+  version: 2
+
+w_a_z_file_version_1:
+  uuid: zzzzz-4zz18-25k12570yk1ver1
+  current_version_uuid: zzzzz-4zz18-25k12570yk134b3
+  portable_data_hash: ba4ba4c7b99a58806b1ed70ea1263afe+45
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  created_at: 2015-02-09T10:53:38Z
+  modified_by_client_uuid: zzzzz-ozdt8-brczlopd8u8d0jr
+  modified_by_user_uuid: zzzzz-tpzed-d9tiejq69daie8f
+  modified_at: 2015-02-09T10:53:38Z
+  updated_at: 2015-02-09T10:53:38Z
+  manifest_text: ". 4d20280d5e516a0109768d49ab0f3318+3 0:3:waz\n"
+  name: "waz file"
+  version: 1
 
 multilevel_collection_1:
   uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
+  current_version_uuid: zzzzz-4zz18-pyw8yp9g3pr7irn
   portable_data_hash: 1fd08fc162a5c6413070a8bd0bffc818+150
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -88,6 +125,7 @@ multilevel_collection_1:
 
 multilevel_collection_2:
   uuid: zzzzz-4zz18-45xf9hw1sxkhl6q
+  current_version_uuid: zzzzz-4zz18-45xf9hw1sxkhl6q
   # All of this collection's files are deep in subdirectories.
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -101,6 +139,7 @@ multilevel_collection_2:
 
 docker_image:
   uuid: zzzzz-4zz18-1v45jub259sjjgb
+  current_version_uuid: zzzzz-4zz18-1v45jub259sjjgb
   # This Collection has links with Docker image metadata.
   portable_data_hash: fa3c1a9cb6783f85f2ecda037e07b8c3+167
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -115,6 +154,7 @@ docker_image:
 # tagged docker image with sha256:{hash}.tar filename
 docker_image_1_12:
   uuid: zzzzz-4zz18-1g4g0vhpjn9wq7i
+  current_version_uuid: zzzzz-4zz18-1g4g0vhpjn9wq7i
   portable_data_hash: d740a57097711e08eb9b2a93518f20ab+174
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2016-10-19 08:50:45.653552268 Z
@@ -127,6 +167,7 @@ docker_image_1_12:
 
 unlinked_docker_image:
   uuid: zzzzz-4zz18-d0d8z5wofvfgwad
+  current_version_uuid: zzzzz-4zz18-d0d8z5wofvfgwad
   # This Collection contains a file that looks like a Docker image,
   # but has no Docker metadata links pointing to it.
   portable_data_hash: 9ae44d5792468c58bcf85ce7353c7027+124
@@ -141,6 +182,7 @@ unlinked_docker_image:
 
 empty:
   uuid: zzzzz-4zz18-gs9ooj1h9sd5mde
+  current_version_uuid: zzzzz-4zz18-gs9ooj1h9sd5mde
   # Empty collection owned by anonymous_group is added with rake db:seed.
   portable_data_hash: d41d8cd98f00b204e9800998ecf8427e+0
   owner_uuid: zzzzz-tpzed-000000000000000
@@ -154,6 +196,7 @@ empty:
 
 foo_collection_in_aproject:
   uuid: zzzzz-4zz18-fy296fx3hot09f7
+  current_version_uuid: zzzzz-4zz18-fy296fx3hot09f7
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
@@ -164,6 +207,7 @@ foo_collection_in_aproject:
 
 user_agreement_in_anonymously_accessible_project:
   uuid: zzzzz-4zz18-uukreo9rbgwsujr
+  current_version_uuid: zzzzz-4zz18-uukreo9rbgwsujr
   portable_data_hash: b519d9cb706a29fc7ea24dbea2f05851+93
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   created_at: 2014-06-13 20:42:26 -0800
@@ -174,6 +218,7 @@ user_agreement_in_anonymously_accessible_project:
 
 public_text_file:
   uuid: zzzzz-4zz18-4en62shvi99lxd4
+  current_version_uuid: zzzzz-4zz18-4en62shvi99lxd4
   portable_data_hash: 55713e6a34081eb03609e7ad5fcad129+62
   owner_uuid: zzzzz-j7d0g-zhxawtyetzwc5f0
   created_at: 2015-02-12 16:58:03 -0500
@@ -184,6 +229,7 @@ public_text_file:
 
 baz_collection_name_in_asubproject:
   uuid: zzzzz-4zz18-lsitwcf548ui4oe
+  current_version_uuid: zzzzz-4zz18-lsitwcf548ui4oe
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   created_at: 2014-04-21 15:37:48 -0400
@@ -194,6 +240,7 @@ baz_collection_name_in_asubproject:
 
 empty_collection_name_in_active_user_home_project:
   uuid: zzzzz-4zz18-5qa38qghh1j3nvv
+  current_version_uuid: zzzzz-4zz18-5qa38qghh1j3nvv
   portable_data_hash: d41d8cd98f00b204e9800998ecf8427e+0
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-08-06 22:11:51.242392533 Z
@@ -203,6 +250,7 @@ empty_collection_name_in_active_user_home_project:
 
 baz_file_in_asubproject:
   uuid: zzzzz-4zz18-0mri2x4u7ftngez
+  current_version_uuid: zzzzz-4zz18-0mri2x4u7ftngez
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
   created_at: 2014-02-03T17:22:54Z
@@ -215,6 +263,7 @@ baz_file_in_asubproject:
 
 collection_to_move_around_in_aproject:
   uuid: zzzzz-4zz18-0mri2x4u7ft1234
+  current_version_uuid: zzzzz-4zz18-0mri2x4u7ft1234
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-02-03T17:22:54Z
@@ -229,6 +278,7 @@ collection_to_move_around_in_aproject:
 # because it is not in default scope
 expired_collection:
   uuid: zzzzz-4zz18-mto52zx1s7sn3ih
+  current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3ih
   portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -244,6 +294,7 @@ expired_collection:
 
 trashed_on_next_sweep:
   uuid: zzzzz-4zz18-4guozfh77ewd2f0
+  current_version_uuid: zzzzz-4zz18-4guozfh77ewd2f0
   portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2016-12-07T22:01:00.123456Z
@@ -261,6 +312,7 @@ trashed_on_next_sweep:
 # because it is not in default scope
 deleted_on_next_sweep:
   uuid: zzzzz-4zz18-3u1p5umicfpqszp
+  current_version_uuid: zzzzz-4zz18-3u1p5umicfpqszp
   portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2016-12-07T22:01:00.234567Z
@@ -276,6 +328,7 @@ deleted_on_next_sweep:
 
 collection_expires_in_future:
   uuid: zzzzz-4zz18-padkqo7yb8d9i3j
+  current_version_uuid: zzzzz-4zz18-padkqo7yb8d9i3j
   portable_data_hash: 0b21a217243bfce5617fb9224b95bcb9+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -290,6 +343,7 @@ collection_expires_in_future:
 
 unique_expired_collection:
   uuid: zzzzz-4zz18-mto52zx1s7sn3jk
+  current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3jk
   portable_data_hash: 4ad199f90029935844dc3f098f4fca2a+49
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -305,6 +359,7 @@ unique_expired_collection:
 
 unique_expired_collection2:
   uuid: zzzzz-4zz18-mto52zx1s7sn3jr
+  current_version_uuid: zzzzz-4zz18-mto52zx1s7sn3jr
   portable_data_hash: 4ad199f90029935844dc3f098f4fca2b+49
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2014-02-03T17:22:54Z
@@ -326,6 +381,7 @@ unique_expired_collection2:
 #
 real_log_collection:
   uuid: zzzzz-4zz18-op4e2lbej01tcvu
+  current_version_uuid: zzzzz-4zz18-op4e2lbej01tcvu
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-09-01 12:00:00
   modified_at: 2014-09-01 12:00:00
@@ -335,6 +391,7 @@ real_log_collection:
 
 collection_in_home_project_with_same_name_as_in_aproject:
   uuid: zzzzz-4zz18-12342x4u7ftabcd
+  current_version_uuid: zzzzz-4zz18-12342x4u7ftabcd
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -347,6 +404,7 @@ collection_in_home_project_with_same_name_as_in_aproject:
 
 collection_in_aproject_with_same_name_as_in_home_project:
   uuid: zzzzz-4zz18-56782x4u7ftefgh
+  current_version_uuid: zzzzz-4zz18-56782x4u7ftefgh
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-02-03T17:22:54Z
@@ -359,6 +417,7 @@ collection_in_aproject_with_same_name_as_in_home_project:
 
 collection_owned_by_foo:
   uuid: zzzzz-4zz18-50surkhkbhsp31b
+  current_version_uuid: zzzzz-4zz18-50surkhkbhsp31b
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   manifest_text: ". 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz\n"
   owner_uuid: zzzzz-tpzed-81hsbo6mk8nl05c
@@ -369,6 +428,7 @@ collection_owned_by_foo:
 collection_to_remove_from_subproject:
   # The Workbench tests remove this from subproject.
   uuid: zzzzz-4zz18-subprojgonecoll
+  current_version_uuid: zzzzz-4zz18-subprojgonecoll
   portable_data_hash: 2386ca6e3fffd4be5e197a72c6c80fb2+51
   manifest_text: ". 8258b505536a9ab47baa2f4281cb932a+9 0:9:missingno\n"
   owner_uuid: zzzzz-j7d0g-axqo7eu9pwvna1x
@@ -378,6 +438,7 @@ collection_to_remove_from_subproject:
 
 collection_with_files_in_subdir:
   uuid: zzzzz-4zz18-filesinsubdir00
+  current_version_uuid: zzzzz-4zz18-filesinsubdir00
   name: collection_files_in_subdir
   portable_data_hash: 85877ca2d7e05498dd3d109baf2df106+95
   owner_uuid: zzzzz-tpzed-user1withloadab
@@ -390,6 +451,7 @@ collection_with_files_in_subdir:
 
 graph_test_collection1:
   uuid: zzzzz-4zz18-bv31uwvy3neko22
+  current_version_uuid: zzzzz-4zz18-bv31uwvy3neko22
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
@@ -399,6 +461,7 @@ graph_test_collection1:
 
 graph_test_collection2:
   uuid: zzzzz-4zz18-uukreo9rbgwsujx
+  current_version_uuid: zzzzz-4zz18-uukreo9rbgwsujx
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   portable_data_hash: 65b17c95fdbc9800fc48acda4e9dcd0b+93
   manifest_text: ". 6a4ff0499484c6c79c95cd8c566bd25f+249025 0:249025:FOO_General_Public_License,_version_3.pdf\n"
@@ -408,6 +471,7 @@ graph_test_collection2:
 
 graph_test_collection3:
   uuid: zzzzz-4zz18-uukreo9rbgwsujj
+  current_version_uuid: zzzzz-4zz18-uukreo9rbgwsujj
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   manifest_text: ". 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz\n"
@@ -417,6 +481,7 @@ graph_test_collection3:
 
 collection_1_owned_by_fuse:
   uuid: zzzzz-4zz18-ovx05bfzormx3bg
+  current_version_uuid: zzzzz-4zz18-ovx05bfzormx3bg
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-0fusedrivertest
   created_at: 2014-02-03T17:22:54Z
@@ -429,6 +494,7 @@ collection_1_owned_by_fuse:
 
 collection_2_owned_by_fuse:
   uuid: zzzzz-4zz18-8ubpy4w74twtwzr
+  current_version_uuid: zzzzz-4zz18-8ubpy4w74twtwzr
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-tpzed-0fusedrivertest
   created_at: 2014-02-03T17:22:54Z
@@ -441,6 +507,7 @@ collection_2_owned_by_fuse:
 
 collection_in_fuse_project:
   uuid: zzzzz-4zz18-vx4mtkjqfrb534f
+  current_version_uuid: zzzzz-4zz18-vx4mtkjqfrb534f
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   owner_uuid: zzzzz-j7d0g-0000ownedbyfuse
   created_at: 2014-02-03T17:22:54Z
@@ -453,6 +520,7 @@ collection_in_fuse_project:
 
 collection_with_no_name_in_aproject:
   uuid: zzzzz-4zz18-00000nonamecoll
+  current_version_uuid: zzzzz-4zz18-00000nonamecoll
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
@@ -462,6 +530,7 @@ collection_with_no_name_in_aproject:
 
 collection_to_search_for_in_aproject:
   uuid: zzzzz-4zz18-abcd6fx123409f7
+  current_version_uuid: zzzzz-4zz18-abcd6fx123409f7
   portable_data_hash: 1f4b0bc7583c2a7f9102c395f4ffc5e3+45
   owner_uuid: zzzzz-j7d0g-v955i6s2oi1cbso
   created_at: 2014-04-21 15:37:48 -0400
@@ -472,6 +541,7 @@ collection_to_search_for_in_aproject:
 
 upload_sandbox:
   uuid: zzzzz-4zz18-js48y3ykkfdfjd3
+  current_version_uuid: zzzzz-4zz18-js48y3ykkfdfjd3
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-12-09 15:03:16
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -483,6 +553,7 @@ upload_sandbox:
 
 collection_with_unique_words_to_test_full_text_search:
   uuid: zzzzz-4zz18-mnt690klmb51aud
+  current_version_uuid: zzzzz-4zz18-mnt690klmb51aud
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -495,6 +566,8 @@ collection_with_unique_words_to_test_full_text_search:
   description: The quick_brown_fox jumps over the lazy_dog
 
 replication_undesired_unconfirmed:
+  uuid: zzzzz-4zz18-wjxq7uzx2m9jj4a
+  current_version_uuid: zzzzz-4zz18-wjxq7uzx2m9jj4a
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:19:28.596506247 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -504,11 +577,12 @@ replication_undesired_unconfirmed:
   replication_confirmed_at: ~
   replication_confirmed: ~
   updated_at: 2015-02-07 00:19:28.596236608 Z
-  uuid: zzzzz-4zz18-wjxq7uzx2m9jj4a
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: replication want=null have=null
 
 replication_desired_2_unconfirmed:
+  uuid: zzzzz-4zz18-3t236wrz4769h7x
+  current_version_uuid: zzzzz-4zz18-3t236wrz4769h7x
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:21:35.050333515 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -518,11 +592,12 @@ replication_desired_2_unconfirmed:
   replication_confirmed_at: ~
   replication_confirmed: ~
   updated_at: 2015-02-07 00:21:35.050126576 Z
-  uuid: zzzzz-4zz18-3t236wrz4769h7x
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: replication want=2 have=null
 
 replication_desired_2_confirmed_2:
+  uuid: zzzzz-4zz18-434zv1tnnf2rygp
+  current_version_uuid: zzzzz-4zz18-434zv1tnnf2rygp
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:19:28.596506247 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -532,11 +607,12 @@ replication_desired_2_confirmed_2:
   replication_confirmed_at: 2015-02-07 00:24:52.983381227 Z
   replication_confirmed: 2
   updated_at: 2015-02-07 00:24:52.983381227 Z
-  uuid: zzzzz-4zz18-434zv1tnnf2rygp
   manifest_text: ". acbd18db4cc2f85cedef654fccc4a4d8+3 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 3:3:bar\n"
   name: replication want=2 have=2
 
 storage_classes_desired_default_unconfirmed:
+  uuid: zzzzz-4zz18-3t236wrz4769tga
+  current_version_uuid: zzzzz-4zz18-3t236wrz4769tga
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:21:35.050333515 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -546,11 +622,12 @@ storage_classes_desired_default_unconfirmed:
   storage_classes_confirmed_at: ~
   storage_classes_confirmed: ~
   updated_at: 2015-02-07 00:21:35.050126576 Z
-  uuid: zzzzz-4zz18-3t236wrz4769tga
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: storage classes want=[default] have=[]
 
 storage_classes_desired_default_confirmed_default:
+  uuid: zzzzz-4zz18-3t236wr12769tga
+  current_version_uuid: zzzzz-4zz18-3t236wr12769tga
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:21:35.050333515 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -560,11 +637,12 @@ storage_classes_desired_default_confirmed_default:
   storage_classes_confirmed_at: 2015-02-07 00:21:35.050126576 Z
   storage_classes_confirmed: ["default"]
   updated_at: 2015-02-07 00:21:35.050126576 Z
-  uuid: zzzzz-4zz18-3t236wr12769tga
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: storage classes want=[default] have=[default]
 
 storage_classes_desired_archive_confirmed_default:
+  uuid: zzzzz-4zz18-3t236wr12769qqa
+  current_version_uuid: zzzzz-4zz18-3t236wr12769qqa
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-07 00:21:35.050333515 Z
   modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
@@ -574,12 +652,12 @@ storage_classes_desired_archive_confirmed_default:
   storage_classes_confirmed_at: ~
   storage_classes_confirmed: ["default"]
   updated_at: 2015-02-07 00:21:35.050126576 Z
-  uuid: zzzzz-4zz18-3t236wr12769qqa
   manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
   name: storage classes want=[archive] have=[default]
 
 collection_with_empty_properties:
   uuid: zzzzz-4zz18-emptyproperties
+  current_version_uuid: zzzzz-4zz18-emptyproperties
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -593,6 +671,7 @@ collection_with_empty_properties:
 
 collection_with_one_property:
   uuid: zzzzz-4zz18-withoneproperty
+  current_version_uuid: zzzzz-4zz18-withoneproperty
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -608,6 +687,7 @@ collection_with_one_property:
 # The following four collections are used to test combining collections with repeated filenames
 collection_with_repeated_filenames_and_contents_in_two_dirs_1:
   uuid: zzzzz-4zz18-duplicatenames1
+  current_version_uuid: zzzzz-4zz18-duplicatenames1
   portable_data_hash: f3a67fad3a19c31c658982fb8158fa58+144
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -620,6 +700,7 @@ collection_with_repeated_filenames_and_contents_in_two_dirs_1:
 
 collection_with_repeated_filenames_and_contents_in_two_dirs_2:
   uuid: zzzzz-4zz18-duplicatenames2
+  current_version_uuid: zzzzz-4zz18-duplicatenames2
   portable_data_hash: f3a67fad3a19c31c658982fb8158fa58+144
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -632,6 +713,7 @@ collection_with_repeated_filenames_and_contents_in_two_dirs_2:
 
 foo_and_bar_files_in_dir:
   uuid: zzzzz-4zz18-foonbarfilesdir
+  current_version_uuid: zzzzz-4zz18-foonbarfilesdir
   portable_data_hash: 6bbac24198d09a93975f60098caf0bdf+62
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -644,6 +726,7 @@ foo_and_bar_files_in_dir:
 
 multi_level_to_combine:
   uuid: zzzzz-4zz18-pyw8yp9g3ujh45f
+  current_version_uuid: zzzzz-4zz18-pyw8yp9g3ujh45f
   portable_data_hash: 7a6ef4c162a5c6413070a8bd0bffc818+150
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -657,6 +740,7 @@ multi_level_to_combine:
 # collection with several file types to test view icon enabled state in collection show page
 collection_with_several_supported_file_types:
   uuid: zzzzz-4zz18-supportedtypes1
+  current_version_uuid: zzzzz-4zz18-supportedtypes1
   portable_data_hash: 020d82cf7dedb70fd2b7788b5d0634da+269
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -669,6 +753,7 @@ collection_with_several_supported_file_types:
 
 collection_with_several_unsupported_file_types:
   uuid: zzzzz-4zz18-supportedtypes2
+  current_version_uuid: zzzzz-4zz18-supportedtypes2
   portable_data_hash: 71ac42f87464ee5f9fd396d560d400c3+59
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -681,6 +766,7 @@ collection_with_several_unsupported_file_types:
 
 collection_not_readable_by_active:
   uuid: zzzzz-4zz18-cd42uwvy3neko21
+  current_version_uuid: zzzzz-4zz18-cd42uwvy3neko21
   portable_data_hash: bb89eb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-000000000000000
   created_at: 2014-02-03T17:22:54Z
@@ -693,6 +779,7 @@ collection_not_readable_by_active:
 
 collection_to_remove_and_rename_files:
   uuid: zzzzz-4zz18-a21ux3541sxa8sf
+  current_version_uuid: zzzzz-4zz18-a21ux3541sxa8sf
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -705,6 +792,7 @@ collection_to_remove_and_rename_files:
 
 collection_with_tags_owned_by_active:
   uuid: zzzzz-4zz18-taggedcolletion
+  current_version_uuid: zzzzz-4zz18-taggedcolletion
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -720,6 +808,7 @@ collection_with_tags_owned_by_active:
 
 trashed_collection_to_test_name_conflict_on_untrash:
   uuid: zzzzz-4zz18-trashedcolnamec
+  current_version_uuid: zzzzz-4zz18-trashedcolnamec
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -735,6 +824,7 @@ trashed_collection_to_test_name_conflict_on_untrash:
 
 same_name_as_trashed_coll_to_test_name_conflict_on_untrash:
   uuid: zzzzz-4zz18-namesameastrash
+  current_version_uuid: zzzzz-4zz18-namesameastrash
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2014-02-03T17:22:54Z
@@ -747,6 +837,7 @@ same_name_as_trashed_coll_to_test_name_conflict_on_untrash:
 
 collection_in_trashed_subproject:
   uuid: zzzzz-4zz18-trashedproj2col
+  current_version_uuid: zzzzz-4zz18-trashedproj2col
   portable_data_hash: 80cf6dd2cf079dd13f272ec4245cb4a8+48
   owner_uuid: zzzzz-j7d0g-trashedproject2
   created_at: 2014-02-03T17:22:54Z
@@ -759,6 +850,7 @@ collection_in_trashed_subproject:
 
 collection_with_prop1_value1:
   uuid: zzzzz-4zz18-withprop1value1
+  current_version_uuid: zzzzz-4zz18-withprop1value1
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -773,6 +865,7 @@ collection_with_prop1_value1:
 
 collection_with_prop1_value2:
   uuid: zzzzz-4zz18-withprop1value2
+  current_version_uuid: zzzzz-4zz18-withprop1value2
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -787,6 +880,7 @@ collection_with_prop1_value2:
 
 collection_with_prop1_value3:
   uuid: zzzzz-4zz18-withprop1value3
+  current_version_uuid: zzzzz-4zz18-withprop1value3
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -801,6 +895,7 @@ collection_with_prop1_value3:
 
 collection_with_prop1_other1:
   uuid: zzzzz-4zz18-withprop1other1
+  current_version_uuid: zzzzz-4zz18-withprop1other1
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -815,6 +910,7 @@ collection_with_prop1_other1:
 
 collection_with_prop2_1:
   uuid: zzzzz-4zz18-withprop2value1
+  current_version_uuid: zzzzz-4zz18-withprop2value1
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -829,6 +925,7 @@ collection_with_prop2_1:
 
 collection_with_prop2_5:
   uuid: zzzzz-4zz18-withprop2value5
+  current_version_uuid: zzzzz-4zz18-withprop2value5
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -843,6 +940,7 @@ collection_with_prop2_5:
 
 collection_with_uri_prop:
   uuid: zzzzz-4zz18-withuripropval1
+  current_version_uuid: zzzzz-4zz18-withuripropval1
   portable_data_hash: fa7aeb5140e2848d39b416daeef4ffc5+45
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
   created_at: 2015-02-13T17:22:54Z
@@ -866,6 +964,7 @@ collection_<%=i%>_of_10:
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   manifest_text: ". 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz\n"
   uuid: zzzzz-4zz18-10gneyn6brkx<%= i.to_s.rjust(3, '0') %>
+  current_version_uuid: zzzzz-4zz18-10gneyn6brkx<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-0010collections
   created_at: <%= i.minute.ago.to_s(:db) %>
   modified_at: <%= i.minute.ago.to_s(:db) %>
@@ -878,6 +977,7 @@ collection_<%=i%>_of_201:
   portable_data_hash: ea10d51bcf88862dbcc36eb292017dfd+45
   manifest_text: ". 73feffa4b7f6bb68e44cf984c85f6e88+3 0:3:baz\n"
   uuid: zzzzz-4zz18-201gneyn6brd<%= i.to_s.rjust(3, '0') %>
+  current_version_uuid: zzzzz-4zz18-201gneyn6brd<%= i.to_s.rjust(3, '0') %>
   owner_uuid: zzzzz-j7d0g-0201collections
   created_at: <%= i.minute.ago.to_s(:db) %>
   modified_at: <%= i.minute.ago.to_s(:db) %>
index 98c4bd11e44db845e99a881d1239c6d9f0ddb87e..f5bed638497d0f9c62d6d130cf314576cb4c2685 100644 (file)
@@ -51,6 +51,19 @@ class Arvados::V1::CollectionsControllerTest < ActionController::TestCase
     assert(assigns(:objects).andand.any?, "no Collections returned in index")
     refute(json_response["items"].any? { |c| c.has_key?("manifest_text") },
            "basic Collections index included manifest_text")
+    refute(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid },
+           "basic Collections index included past version")
+  end
+
+  test "get index with include_old_versions" do
+    authorize_with :active
+    get :index, {
+      include_old_versions: true
+    }
+    assert_response :success
+    assert(assigns(:objects).andand.any?, "no Collections returned in index")
+    assert(json_response["items"].any? { |c| c["uuid"] == collections(:collection_owned_by_active_past_version_1).uuid },
+           "past version not included on index")
   end
 
   test "collections.get returns signed locators, and no unsigned_manifest_text" do
@@ -1180,4 +1193,50 @@ EOS
     end
     assert_includes(item_uuids, collections(:collection_in_trashed_subproject).uuid)
   end
+
+  test 'can get collection with past versions' do
+    authorize_with :active
+    get :index, {
+      filters: [['current_version_uuid','=',collections(:collection_owned_by_active).uuid]],
+      include_old_versions: true
+    }
+    assert_response :success
+    assert_equal 2, assigns(:objects).length
+    assert_equal 2, json_response['items_available']
+    assert_equal 2, json_response['items'].count
+    json_response['items'].each do |c|
+      assert_equal collections(:collection_owned_by_active).uuid,
+                   c['current_version_uuid'],
+                   'response includes a version from a different collection'
+    end
+  end
+
+  test 'can get old version collection by uuid' do
+    authorize_with :active
+    get :show, {
+      id: collections(:collection_owned_by_active_past_version_1).uuid,
+    }
+    assert_response :success
+    assert_equal collections(:collection_owned_by_active_past_version_1).name,
+                  json_response['name']
+  end
+
+  test 'version and current_version_uuid are ignored at creation time' do
+    permit_unsigned_manifests
+    authorize_with :active
+    manifest_text = ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n"
+    post :create, {
+      collection: {
+        name: 'Test collection',
+        version: 42,
+        current_version_uuid: collections(:collection_owned_by_active).uuid,
+        manifest_text: manifest_text,
+        # portable_data_hash: "d30fe8ae534397864cb96c544f4cf102+47"
+      }
+    }
+    assert_response :success
+    resp = JSON.parse(@response.body)
+    assert_equal 1, resp['version']
+    assert_equal resp['uuid'], resp['current_version_uuid']
+  end
 end
index 40868c87b8857ba34cd5aff2bfbf065506d50b00..53c1ed72e7910c8cd19f36f0721aef5e53ea87ed 100644 (file)
@@ -101,7 +101,7 @@ class Arvados::V1::SchemaControllerTest < ActionController::TestCase
     specimens_index_params = discovery_doc['resources']['specimens']['methods']['index']['parameters']  # no changes from super
     coll_index_params = discovery_doc['resources']['collections']['methods']['index']['parameters']
 
-    assert_equal coll_index_params.keys.sort, (specimens_index_params.keys + ['include_trash']).sort
+    assert_equal (specimens_index_params.keys + ['include_trash', 'include_old_versions']).sort, coll_index_params.keys.sort
 
     include_trash_param = coll_index_params['include_trash']
     assert_equal 'boolean', include_trash_param['type']
index 8b8c48fe1c865f23e73bdf2c00befbb27031724a..9797ed63dc0d098898d38a4e0741ecd9fc7e0e4c 100644 (file)
@@ -11,7 +11,7 @@ class CollectionTest < ActiveSupport::TestCase
   def create_collection name, enc=nil
     txt = ". d41d8cd98f00b204e9800998ecf8427e+0 0:0:#{name}.txt\n"
     txt.force_encoding(enc) if enc
-    return Collection.create(manifest_text: txt)
+    return Collection.create(manifest_text: txt, name: name)
   end
 
   test 'accept ASCII manifest_text' do
@@ -106,6 +106,312 @@ class CollectionTest < ActiveSupport::TestCase
     end
   end
 
+  test "auto-create version after idle setting" do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 600 # 10 minutes
+    act_as_user users(:active) do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      assert_equal 1, c.version
+      assert_equal false, c.preserve_version
+      # Make a versionable update, it shouldn't create a new version yet
+      c.update_attributes!({'name' => 'bar'})
+      c.reload
+      assert_equal 'bar', c.name
+      assert_equal 1, c.version
+      # Update modified_at to trigger a version auto-creation
+      fifteen_min_ago = Time.now - 15.minutes
+      c.update_column('modified_at', fifteen_min_ago) # Update without validations/callbacks
+      c.reload
+      assert_equal fifteen_min_ago.to_i, c.modified_at.to_i
+      c.update_attributes!({'name' => 'baz'})
+      c.reload
+      assert_equal 'baz', c.name
+      assert_equal 2, c.version
+      # Make another update, no new version should be created
+      c.update_attributes!({'name' => 'foobar'})
+      c.reload
+      assert_equal 'foobar', c.name
+      assert_equal 2, c.version
+    end
+  end
+
+  test "preserve_version=false assignment is ignored while being true and not producing a new version" do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 3600
+    act_as_user users(:active) do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      assert_equal 1, c.version
+      assert_equal false, c.preserve_version
+      # This update shouldn't produce a new version, as the idle time is not up
+      c.update_attributes!({
+        'name' => 'bar',
+        'preserve_version' => true
+      })
+      c.reload
+      assert_equal 1, c.version
+      assert_equal 'bar', c.name
+      assert_equal true, c.preserve_version
+      # Make sure preserve_version is not disabled after being enabled, unless
+      # a new version is created.
+      c.update_attributes!({
+        'preserve_version' => false,
+        'replication_desired' => 2
+      })
+      c.reload
+      assert_equal 1, c.version
+      assert_equal 2, c.replication_desired
+      assert_equal true, c.preserve_version
+      c.update_attributes!({'name' => 'foobar'})
+      c.reload
+      assert_equal 2, c.version
+      assert_equal false, c.preserve_version
+      assert_equal 'foobar', c.name
+    end
+  end
+
+  [
+    ['version', 10],
+    ['current_version_uuid', 'zzzzz-4zz18-bv31uwvy3neko21'],
+  ].each do |name, new_value|
+    test "'#{name}' updates on current version collections are not allowed" do
+      act_as_user users(:active) do
+        # Set up initial collection
+        c = create_collection 'foo', Encoding::US_ASCII
+        assert c.valid?
+        assert_equal 1, c.version
+
+        assert_raises(ActiveRecord::RecordInvalid) do
+          c.update_attributes!({
+            name => new_value
+          })
+        end
+      end
+    end
+  end
+
+  test "uuid updates on current version make older versions update their pointers" do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
+    act_as_system_user do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      assert_equal 1, c.version
+      # Make changes so that a new version is created
+      c.update_attributes!({'name' => 'bar'})
+      c.reload
+      assert_equal 2, c.version
+      assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
+      new_uuid = 'zzzzz-4zz18-somefakeuuidnow'
+      assert_empty Collection.where(uuid: new_uuid)
+      # Update UUID on current version, check that both collections point to it
+      c.update_attributes!({'uuid' => new_uuid})
+      c.reload
+      assert_equal new_uuid, c.uuid
+      assert_equal 2, Collection.where(current_version_uuid: new_uuid).count
+    end
+  end
+
+  test "older versions' modified_at indicate when they're created" do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
+    act_as_user users(:active) do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      # Make changes so that a new version is created
+      c.update_attributes!({'name' => 'bar'})
+      c.reload
+      assert_equal 2, c.version
+      # Get the old version
+      c_old = Collection.where(current_version_uuid: c.uuid, version: 1).first
+      assert_not_nil c_old
+
+      version_creation_datetime = c_old.modified_at.to_f
+      assert_equal c.created_at.to_f, c_old.created_at.to_f
+      # Current version is updated just a few milliseconds before the version is
+      # saved on the database.
+      assert_operator c.modified_at.to_f, :<, version_creation_datetime
+
+      # Make update on current version so old version get the attribute synced;
+      # its modified_at should not change.
+      new_replication = 3
+      c.update_attributes!({'replication_desired' => new_replication})
+      c.reload
+      assert_equal new_replication, c.replication_desired
+      c_old.reload
+      assert_equal new_replication, c_old.replication_desired
+      assert_equal version_creation_datetime, c_old.modified_at.to_f
+      assert_operator c.modified_at.to_f, :>, c_old.modified_at.to_f
+    end
+  end
+
+  test "past versions should not be directly updatable" do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
+    act_as_system_user do
+      # Set up initial collection
+      c = create_collection 'foo', Encoding::US_ASCII
+      assert c.valid?
+      # Make changes so that a new version is created
+      c.update_attributes!({'name' => 'bar'})
+      c.reload
+      assert_equal 2, c.version
+      # Get the old version
+      c_old = Collection.where(current_version_uuid: c.uuid, version: 1).first
+      assert_not_nil c_old
+      # With collection versioning still being enabled, try to update
+      c_old.name = 'this was foo'
+      assert c_old.invalid?
+      c_old.reload
+      # Try to fool the validator attempting to make c_old to look like a
+      # current version, it should also fail.
+      c_old.current_version_uuid = c_old.uuid
+      assert c_old.invalid?
+      c_old.reload
+      # Now disable collection versioning, it should behave the same way
+      Rails.configuration.collection_versioning = false
+      c_old.name = 'this was foo'
+      assert c_old.invalid?
+    end
+  end
+
+  [
+    ['owner_uuid', 'zzzzz-tpzed-d9tiejq69daie8f', 'zzzzz-tpzed-xurymjxw79nv3jz'],
+    ['replication_desired', 2, 3],
+    ['storage_classes_desired', ['hot'], ['archive']],
+    ['is_trashed', true, false],
+  ].each do |attr, first_val, second_val|
+    test "sync #{attr} with older versions" do
+      Rails.configuration.collection_versioning = true
+      Rails.configuration.preserve_version_if_idle = 0
+      act_as_system_user do
+        # Set up initial collection
+        c = create_collection 'foo', Encoding::US_ASCII
+        assert c.valid?
+        assert_equal 1, c.version
+        assert_not_equal first_val, c.attributes[attr]
+        # Make changes so that a new version is created and a synced field is
+        # updated on both
+        c.update_attributes!({'name' => 'bar', attr => first_val})
+        c.reload
+        assert_equal 2, c.version
+        assert_equal first_val, c.attributes[attr]
+        assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
+        assert_equal first_val, Collection.where(current_version_uuid: c.uuid, version: 1).first.attributes[attr]
+        # Only make an update on the same synced field & check that the previously
+        # created version also gets it.
+        c.update_attributes!({attr => second_val})
+        c.reload
+        assert_equal 2, c.version
+        assert_equal second_val, c.attributes[attr]
+        assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
+        assert_equal second_val, Collection.where(current_version_uuid: c.uuid, version: 1).first.attributes[attr]
+      end
+    end
+  end
+
+  [
+    [false, 'name', 'bar', false],
+    [false, 'description', 'The quick brown fox jumps over the lazy dog', false],
+    [false, 'properties', {'new_version' => true}, false],
+    [false, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", false],
+    [true, 'name', 'bar', true],
+    [true, 'description', 'The quick brown fox jumps over the lazy dog', true],
+    [true, 'properties', {'new_version' => true}, true],
+    [true, 'manifest_text', ". d41d8cd98f00b204e9800998ecf8427e 0:0:foo.txt\n", true],
+    # Non-versionable attribute updates shouldn't create new versions
+    [true, 'replication_desired', 5, false],
+    [false, 'replication_desired', 5, false],
+  ].each do |versioning, attr, val, new_version_expected|
+    test "update #{attr} with versioning #{versioning ? '' : 'not '}enabled should #{new_version_expected ? '' : 'not '}create a new version" do
+      Rails.configuration.collection_versioning = versioning
+      Rails.configuration.preserve_version_if_idle = 0
+      act_as_user users(:active) do
+        # Create initial collection
+        c = create_collection 'foo', Encoding::US_ASCII
+        assert c.valid?
+        assert_equal 'foo', c.name
+
+        # Check current version attributes
+        assert_equal 1, c.version
+        assert_equal c.uuid, c.current_version_uuid
+
+        # Update attribute and check if version number should be incremented
+        old_value = c.attributes[attr]
+        c.update_attributes!({attr => val})
+        assert_equal new_version_expected, c.version == 2
+        assert_equal val, c.attributes[attr]
+
+        if versioning && new_version_expected
+          # Search for the snapshot & previous value
+          assert_equal 2, Collection.where(current_version_uuid: c.uuid).count
+          s = Collection.where(current_version_uuid: c.uuid, version: 1).first
+          assert_not_nil s
+          assert_equal old_value, s.attributes[attr]
+        else
+          # If versioning is disabled or no versionable attribute was updated,
+          # only the current version should exist
+          assert_equal 1, Collection.where(current_version_uuid: c.uuid).count
+          assert_equal c, Collection.where(current_version_uuid: c.uuid).first
+        end
+      end
+    end
+  end
+
+  test 'current_version_uuid is ignored during update' do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
+    act_as_user users(:active) do
+      # Create 1st collection
+      col1 = create_collection 'foo', Encoding::US_ASCII
+      assert col1.valid?
+      assert_equal 1, col1.version
+
+      # Create 2nd collection, update it so it becomes version:2
+      # (to avoid unique index violation)
+      col2 = create_collection 'bar', Encoding::US_ASCII
+      assert col2.valid?
+      assert_equal 1, col2.version
+      col2.update_attributes({name: 'baz'})
+      assert_equal 2, col2.version
+
+      # Try to make col2 a past version of col1. It shouldn't be possible
+      col2.update_attributes({current_version_uuid: col1.uuid})
+      assert col2.invalid?
+      col2.reload
+      assert_not_equal col1.uuid, col2.current_version_uuid
+    end
+  end
+
+  test 'with versioning enabled, simultaneous updates increment version correctly' do
+    Rails.configuration.collection_versioning = true
+    Rails.configuration.preserve_version_if_idle = 0
+    act_as_user users(:active) do
+      # Create initial collection
+      col = create_collection 'foo', Encoding::US_ASCII
+      assert col.valid?
+      assert_equal 1, col.version
+
+      # Simulate simultaneous updates
+      c1 = Collection.where(uuid: col.uuid).first
+      assert_equal 1, c1.version
+      c1.name = 'bar'
+      c2 = Collection.where(uuid: col.uuid).first
+      c2.description = 'foo collection'
+      c1.save!
+      assert_equal 1, c2.version
+      # with_lock forces a reload, so this shouldn't produce an unique violation error
+      c2.save!
+      assert_equal 3, c2.version
+      assert_equal 'foo collection', c2.description
+    end
+  end
+
   test 'create and update collection and verify file_names' do
     act_as_system_user do
       c = create_collection 'foo', Encoding::US_ASCII
index cba5d20cb2cba366d94e7a4a52ec3d7504220efa..00f3cc291352493b11258aa0f9750fc883a263ff 100644 (file)
@@ -80,4 +80,9 @@ class LinkTest < ActiveSupport::TestCase
   test "link granting project permissions to unreadable user is invalid" do
     refute new_active_link_valid?(tail_uuid: users(:admin).uuid)
   end
+
+  test "permission link can't exist on past collection versions" do
+    refute new_active_link_valid?(tail_uuid: groups(:public).uuid,
+                                  head_uuid: collections(:w_a_z_file_version_1).uuid)
+  end
 end
index b4dc58b24fc1cb1436cbb1db9dbc6b73deec373c..3b3032afda5d9707616ce474c431e10d2e629e37 100644 (file)
@@ -91,7 +91,7 @@ func (h *authHandler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
                httpserver.Log(r.RemoteAddr, passwordToLog, w.WroteStatus(), statusText, repoName, r.Method, r.URL.Path)
        }()
 
-       creds := auth.NewCredentialsFromHTTPRequest(r)
+       creds := auth.CredentialsFromRequest(r)
        if len(creds.Tokens) == 0 {
                statusCode, statusText = http.StatusUnauthorized, "no credentials provided"
                w.Header().Add("WWW-Authenticate", "Basic realm=\"git\"")
index b48b46bb9a4c48b52770996fba12821c60e5cb8e..0221707cf410f7a0e417a42ccfda8afa27d5bf70 100644 (file)
@@ -6,7 +6,7 @@
 Description=Arvados Docker Image Cleaner
 Documentation=https://doc.arvados.org/
 After=network.target
-AssertPathExists=/etc/arvados/docker-cleaner/docker-cleaner.json
+#AssertPathExists=/etc/arvados/docker-cleaner/docker-cleaner.json
 
 # systemd==229 (ubuntu:xenial) obeys StartLimitInterval in the [Unit] section
 StartLimitInterval=0
index d86234a936cc96702f3a79d12c10d04548c0faa2..5f5f9afdc4741d6c8b74fb582f52a706fd5ea9be 100644 (file)
@@ -10,7 +10,6 @@ import (
        "fmt"
        "log"
        "math"
-       "os"
        "runtime"
        "sort"
        "strings"
@@ -19,20 +18,9 @@ import (
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/keepclient"
+       "github.com/Sirupsen/logrus"
 )
 
-// CheckConfig returns an error if anything is wrong with the given
-// config and runOptions.
-func CheckConfig(config Config, runOptions RunOptions) error {
-       if len(config.KeepServiceList.Items) > 0 && config.KeepServiceTypes != nil {
-               return fmt.Errorf("cannot specify both KeepServiceList and KeepServiceTypes in config")
-       }
-       if !runOptions.Once && config.RunPeriod == arvados.Duration(0) {
-               return fmt.Errorf("you must either use the -once flag, or specify RunPeriod in config")
-       }
-       return nil
-}
-
 // Balancer compares the contents of keepstore servers with the
 // collections stored in Arvados, and issues pull/trash requests
 // needed to get (closer to) the optimal data layout.
@@ -43,11 +31,13 @@ func CheckConfig(config Config, runOptions RunOptions) error {
 // BlobSignatureTTL; and all N existing replicas of a given data block
 // are in the N best positions in rendezvous probe order.
 type Balancer struct {
+       Logger  *logrus.Logger
+       Dumper  *logrus.Logger
+       Metrics *metrics
+
        *BlockStateMap
        KeepServices       map[string]*KeepService
        DefaultReplication int
-       Logger             *log.Logger
-       Dumper             *log.Logger
        MinMtime           int64
 
        classes       []string
@@ -72,13 +62,7 @@ type Balancer struct {
 func (bal *Balancer) Run(config Config, runOptions RunOptions) (nextRunOptions RunOptions, err error) {
        nextRunOptions = runOptions
 
-       bal.Dumper = runOptions.Dumper
-       bal.Logger = runOptions.Logger
-       if bal.Logger == nil {
-               bal.Logger = log.New(os.Stderr, "", log.LstdFlags)
-       }
-
-       defer timeMe(bal.Logger, "Run")()
+       defer bal.time("sweep", "wall clock time to run one full sweep")()
 
        if len(config.KeepServiceList.Items) > 0 {
                err = bal.SetKeepServices(config.KeepServiceList)
@@ -269,7 +253,7 @@ func (bal *Balancer) ClearTrashLists(c *arvados.Client) error {
 //
 // It encodes the resulting information in BlockStateMap.
 func (bal *Balancer) GetCurrentState(c *arvados.Client, pageSize, bufs int) error {
-       defer timeMe(bal.Logger, "GetCurrentState")()
+       defer bal.time("get_state", "wall clock time to get current state")()
        bal.BlockStateMap = NewBlockStateMap()
 
        dd, err := c.DiscoveryDocument()
@@ -413,7 +397,7 @@ func (bal *Balancer) addCollection(coll arvados.Collection) error {
 func (bal *Balancer) ComputeChangeSets() {
        // This just calls balanceBlock() once for each block, using a
        // pool of worker goroutines.
-       defer timeMe(bal.Logger, "ComputeChangeSets")()
+       defer bal.time("changeset_compute", "wall clock time to compute changesets")()
        bal.setupLookupTables()
 
        type balanceTask struct {
@@ -796,6 +780,26 @@ type balancerStats struct {
        trashes       int
        replHistogram []int
        classStats    map[string]replicationStats
+
+       // collectionBytes / collectionBlockBytes = deduplication ratio
+       collectionBytes      int64 // sum(bytes in referenced blocks) across all collections
+       collectionBlockBytes int64 // sum(block size) across all blocks referenced by collections
+       collectionBlockRefs  int64 // sum(number of blocks referenced) across all collections
+       collectionBlocks     int64 // number of blocks referenced by any collection
+}
+
+func (s *balancerStats) dedupByteRatio() float64 {
+       if s.collectionBlockBytes == 0 {
+               return 0
+       }
+       return float64(s.collectionBytes) / float64(s.collectionBlockBytes)
+}
+
+func (s *balancerStats) dedupBlockRatio() float64 {
+       if s.collectionBlocks == 0 {
+               return 0
+       }
+       return float64(s.collectionBlockRefs) / float64(s.collectionBlocks)
 }
 
 type replicationStats struct {
@@ -819,6 +823,13 @@ func (bal *Balancer) collectStatistics(results <-chan balanceResult) {
                surplus := result.have - result.want
                bytes := result.blkid.Size()
 
+               if rc := int64(result.blk.RefCount); rc > 0 {
+                       s.collectionBytes += rc * bytes
+                       s.collectionBlockBytes += bytes
+                       s.collectionBlockRefs += rc
+                       s.collectionBlocks++
+               }
+
                for class, state := range result.classState {
                        cs := s.classStats[class]
                        if state.unachievable {
@@ -893,6 +904,7 @@ func (bal *Balancer) collectStatistics(results <-chan balanceResult) {
                s.trashes += len(srv.ChangeSet.Trashes)
        }
        bal.stats = s
+       bal.Metrics.UpdateStats(s)
 }
 
 // PrintStatistics writes statistics about the computed changes to
@@ -986,6 +998,7 @@ func (bal *Balancer) CheckSanityLate() error {
 // existing blocks that are either underreplicated or poorly
 // distributed according to rendezvous hashing.
 func (bal *Balancer) CommitPulls(c *arvados.Client) error {
+       defer bal.time("send_pull_lists", "wall clock time to send pull lists")()
        return bal.commitAsync(c, "send pull list",
                func(srv *KeepService) error {
                        return srv.CommitPulls(c)
@@ -996,6 +1009,7 @@ func (bal *Balancer) CommitPulls(c *arvados.Client) error {
 // keepstore servers. This has the effect of deleting blocks that are
 // overreplicated or unreferenced.
 func (bal *Balancer) CommitTrash(c *arvados.Client) error {
+       defer bal.time("send_trash_lists", "wall clock time to send trash lists")()
        return bal.commitAsync(c, "send trash list",
                func(srv *KeepService) error {
                        return srv.CommitTrash(c)
@@ -1009,7 +1023,6 @@ func (bal *Balancer) commitAsync(c *arvados.Client, label string, f func(srv *Ke
                        var err error
                        defer func() { errs <- err }()
                        label := fmt.Sprintf("%s: %v", srv, label)
-                       defer timeMe(bal.Logger, label)()
                        err = f(srv)
                        if err != nil {
                                err = fmt.Errorf("%s: %v", label, err)
@@ -1033,6 +1046,17 @@ func (bal *Balancer) logf(f string, args ...interface{}) {
        }
 }
 
+func (bal *Balancer) time(name, help string) func() {
+       observer := bal.Metrics.DurationObserver(name+"_seconds", help)
+       t0 := time.Now()
+       bal.Logger.Printf("%s: start", name)
+       return func() {
+               dur := time.Since(t0)
+               observer.Observe(dur.Seconds())
+               bal.Logger.Printf("%s: took %vs", name, dur.Seconds())
+       }
+}
+
 // Rendezvous hash sort function. Less efficient than sorting on
 // precomputed rendezvous hashes, but also rarely used.
 func rendezvousLess(i, j string, blkid arvados.SizedDigest) bool {
index 28776abc47c600ce8540949d8b6fdd7ed63708ff..923ea47c429a6051377575376860b0bf51fa5915 100644 (file)
@@ -9,7 +9,6 @@ import (
        "fmt"
        "io"
        "io/ioutil"
-       "log"
        "net/http"
        "net/http/httptest"
        "strings"
@@ -17,6 +16,7 @@ import (
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "github.com/Sirupsen/logrus"
 
        check "gopkg.in/check.v1"
 )
@@ -184,7 +184,8 @@ func (s *stubServer) serveFooBarFileCollections() *reqTracker {
                if strings.Contains(r.Form.Get("filters"), `modified_at`) {
                        io.WriteString(w, `{"items_available":0,"items":[]}`)
                } else {
-                       io.WriteString(w, `{"items_available":2,"items":[
+                       io.WriteString(w, `{"items_available":3,"items":[
+                               {"uuid":"zzzzz-4zz18-aaaaaaaaaaaaaaa","portable_data_hash":"fa7aeb5140e2848d39b416daeef4ffc5+45","manifest_text":". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n","modified_at":"2014-02-03T17:22:54Z"},
                                {"uuid":"zzzzz-4zz18-ehbhgtheo8909or","portable_data_hash":"fa7aeb5140e2848d39b416daeef4ffc5+45","manifest_text":". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n","modified_at":"2014-02-03T17:22:54Z"},
                                {"uuid":"zzzzz-4zz18-znfnqtbbv4spc3w","portable_data_hash":"1f4b0bc7583c2a7f9102c395f4ffc5e3+45","manifest_text":". acbd18db4cc2f85cedef654fccc4a4d8+3 0:3:foo\n","modified_at":"2014-02-03T17:22:54Z"}]}`)
                }
@@ -282,7 +283,7 @@ type runSuite struct {
 }
 
 // make a log.Logger that writes to the current test's c.Log().
-func (s *runSuite) logger(c *check.C) *log.Logger {
+func (s *runSuite) logger(c *check.C) *logrus.Logger {
        r, w := io.Pipe()
        go func() {
                buf := make([]byte, 10000)
@@ -299,7 +300,9 @@ func (s *runSuite) logger(c *check.C) *log.Logger {
                        }
                }
        }()
-       return log.New(w, "", log.LstdFlags)
+       logger := logrus.New()
+       logger.Out = w
+       return logger
 }
 
 func (s *runSuite) SetUpTest(c *check.C) {
@@ -308,7 +311,9 @@ func (s *runSuite) SetUpTest(c *check.C) {
                        AuthToken: "xyzzy",
                        APIHost:   "zzzzz.arvadosapi.com",
                        Client:    s.stub.Start()},
-               KeepServiceTypes: []string{"disk"}}
+               KeepServiceTypes: []string{"disk"},
+               RunPeriod:        arvados.Duration(time.Second),
+       }
        s.stub.serveDiscoveryDoc()
        s.stub.logf = c.Logf
 }
@@ -330,7 +335,9 @@ func (s *runSuite) TestRefuseZeroCollections(c *check.C) {
        s.stub.serveKeepstoreIndexFoo4Bar1()
        trashReqs := s.stub.serveKeepstoreTrash()
        pullReqs := s.stub.serveKeepstorePull()
-       _, err := (&Balancer{}).Run(s.config, opts)
+       srv, err := NewServer(s.config, opts)
+       c.Assert(err, check.IsNil)
+       _, err = srv.Run()
        c.Check(err, check.ErrorMatches, "received zero collections")
        c.Check(trashReqs.Count(), check.Equals, 4)
        c.Check(pullReqs.Count(), check.Equals, 0)
@@ -349,7 +356,9 @@ func (s *runSuite) TestServiceTypes(c *check.C) {
        s.stub.serveKeepstoreMounts()
        indexReqs := s.stub.serveKeepstoreIndexFoo4Bar1()
        trashReqs := s.stub.serveKeepstoreTrash()
-       _, err := (&Balancer{}).Run(s.config, opts)
+       srv, err := NewServer(s.config, opts)
+       c.Assert(err, check.IsNil)
+       _, err = srv.Run()
        c.Check(err, check.IsNil)
        c.Check(indexReqs.Count(), check.Equals, 0)
        c.Check(trashReqs.Count(), check.Equals, 0)
@@ -367,7 +376,9 @@ func (s *runSuite) TestRefuseNonAdmin(c *check.C) {
        s.stub.serveKeepstoreMounts()
        trashReqs := s.stub.serveKeepstoreTrash()
        pullReqs := s.stub.serveKeepstorePull()
-       _, err := (&Balancer{}).Run(s.config, opts)
+       srv, err := NewServer(s.config, opts)
+       c.Assert(err, check.IsNil)
+       _, err = srv.Run()
        c.Check(err, check.ErrorMatches, "current user .* is not .* admin user")
        c.Check(trashReqs.Count(), check.Equals, 0)
        c.Check(pullReqs.Count(), check.Equals, 0)
@@ -386,7 +397,9 @@ func (s *runSuite) TestDetectSkippedCollections(c *check.C) {
        s.stub.serveKeepstoreIndexFoo4Bar1()
        trashReqs := s.stub.serveKeepstoreTrash()
        pullReqs := s.stub.serveKeepstorePull()
-       _, err := (&Balancer{}).Run(s.config, opts)
+       srv, err := NewServer(s.config, opts)
+       c.Assert(err, check.IsNil)
+       _, err = srv.Run()
        c.Check(err, check.ErrorMatches, `Retrieved 2 collections with modtime <= .* but server now reports there are 3 collections.*`)
        c.Check(trashReqs.Count(), check.Equals, 4)
        c.Check(pullReqs.Count(), check.Equals, 0)
@@ -405,8 +418,9 @@ func (s *runSuite) TestDryRun(c *check.C) {
        s.stub.serveKeepstoreIndexFoo4Bar1()
        trashReqs := s.stub.serveKeepstoreTrash()
        pullReqs := s.stub.serveKeepstorePull()
-       var bal Balancer
-       _, err := bal.Run(s.config, opts)
+       srv, err := NewServer(s.config, opts)
+       c.Assert(err, check.IsNil)
+       bal, err := srv.Run()
        c.Check(err, check.IsNil)
        for _, req := range collReqs.reqs {
                c.Check(req.Form.Get("include_trash"), check.Equals, "true")
@@ -419,6 +433,8 @@ func (s *runSuite) TestDryRun(c *check.C) {
 }
 
 func (s *runSuite) TestCommit(c *check.C) {
+       s.config.Listen = ":"
+       s.config.ManagementToken = "xyzzy"
        opts := RunOptions{
                CommitPulls: true,
                CommitTrash: true,
@@ -432,8 +448,9 @@ func (s *runSuite) TestCommit(c *check.C) {
        s.stub.serveKeepstoreIndexFoo4Bar1()
        trashReqs := s.stub.serveKeepstoreTrash()
        pullReqs := s.stub.serveKeepstorePull()
-       var bal Balancer
-       _, err := bal.Run(s.config, opts)
+       srv, err := NewServer(s.config, opts)
+       c.Assert(err, check.IsNil)
+       bal, err := srv.Run()
        c.Check(err, check.IsNil)
        c.Check(trashReqs.Count(), check.Equals, 8)
        c.Check(pullReqs.Count(), check.Equals, 4)
@@ -442,9 +459,18 @@ func (s *runSuite) TestCommit(c *check.C) {
        // "bar" block is underreplicated by 1, and its only copy is
        // in a poor rendezvous position
        c.Check(bal.stats.pulls, check.Equals, 2)
+
+       metrics := s.getMetrics(c, srv)
+       c.Check(metrics, check.Matches, `(?ms).*\narvados_keep_total_bytes 15\n.*`)
+       c.Check(metrics, check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_sum [0-9\.]+\n.*`)
+       c.Check(metrics, check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_count 1\n.*`)
+       c.Check(metrics, check.Matches, `(?ms).*\narvados_keep_dedup_byte_ratio 1\.5\n.*`)
+       c.Check(metrics, check.Matches, `(?ms).*\narvados_keep_dedup_block_ratio 1\.5\n.*`)
 }
 
 func (s *runSuite) TestRunForever(c *check.C) {
+       s.config.Listen = ":"
+       s.config.ManagementToken = "xyzzy"
        opts := RunOptions{
                CommitPulls: true,
                CommitTrash: true,
@@ -461,7 +487,14 @@ func (s *runSuite) TestRunForever(c *check.C) {
 
        stop := make(chan interface{})
        s.config.RunPeriod = arvados.Duration(time.Millisecond)
-       go RunForever(s.config, opts, stop)
+       srv, err := NewServer(s.config, opts)
+       c.Assert(err, check.IsNil)
+
+       done := make(chan bool)
+       go func() {
+               srv.RunForever(stop)
+               close(done)
+       }()
 
        // Each run should send 4 pull lists + 4 trash lists. The
        // first run should also send 4 empty trash lists at
@@ -471,6 +504,21 @@ func (s *runSuite) TestRunForever(c *check.C) {
                time.Sleep(time.Millisecond)
        }
        stop <- true
+       <-done
        c.Check(pullReqs.Count() >= 16, check.Equals, true)
        c.Check(trashReqs.Count(), check.Equals, pullReqs.Count()+4)
+       c.Check(s.getMetrics(c, srv), check.Matches, `(?ms).*\narvados_keepbalance_changeset_compute_seconds_count `+fmt.Sprintf("%d", pullReqs.Count()/4)+`\n.*`)
+}
+
+func (s *runSuite) getMetrics(c *check.C, srv *Server) string {
+       resp, err := http.Get("http://" + srv.listening + "/metrics")
+       c.Assert(err, check.IsNil)
+       c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
+
+       resp, err = http.Get("http://" + srv.listening + "/metrics?api_token=xyzzy")
+       c.Assert(err, check.IsNil)
+       c.Check(resp.StatusCode, check.Equals, http.StatusOK)
+       buf, err := ioutil.ReadAll(resp.Body)
+       c.Check(err, check.IsNil)
+       return string(buf)
 }
index 22e89c019ab9fa5a5fb833bf84bbc63df7a4e93b..46e69059c9c796c5b23318f8f9b78b4f3c83651e 100644 (file)
@@ -23,6 +23,7 @@ type Replica struct {
 // replicas actually stored (according to the keepstore indexes we
 // know about).
 type BlockState struct {
+       RefCount int
        Replicas []Replica
        Desired  map[string]int
        // TODO: Support combinations of classes ("private + durable")
@@ -42,6 +43,7 @@ func (bs *BlockState) addReplica(r Replica) {
 }
 
 func (bs *BlockState) increaseDesired(classes []string, n int) {
+       bs.RefCount++
        if len(classes) == 0 {
                classes = defaultClasses
        }
index 9fc47623e73f40157ad14d0056ae1b21f85e1e1e..5280b40c9123012210010585308dd883e3d7962a 100644 (file)
@@ -6,7 +6,6 @@ package main
 
 import (
        "bytes"
-       "log"
        "os"
        "strings"
        "testing"
@@ -16,6 +15,7 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
        "git.curoverse.com/arvados.git/sdk/go/arvadostest"
        "git.curoverse.com/arvados.git/sdk/go/keepclient"
+       "github.com/Sirupsen/logrus"
 
        check "gopkg.in/check.v1"
 )
@@ -67,6 +67,7 @@ func (s *integrationSuite) SetUpTest(c *check.C) {
                        Insecure:  true,
                },
                KeepServiceTypes: []string{"disk"},
+               RunPeriod:        arvados.Duration(time.Second),
        }
 }
 
@@ -74,12 +75,19 @@ func (s *integrationSuite) TestBalanceAPIFixtures(c *check.C) {
        var logBuf *bytes.Buffer
        for iter := 0; iter < 20; iter++ {
                logBuf := &bytes.Buffer{}
+               logger := logrus.New()
+               logger.Out = logBuf
                opts := RunOptions{
                        CommitPulls: true,
                        CommitTrash: true,
-                       Logger:      log.New(logBuf, "", log.LstdFlags),
+                       Logger:      logger,
                }
-               nextOpts, err := (&Balancer{}).Run(s.config, opts)
+
+               bal := &Balancer{
+                       Logger:  logger,
+                       Metrics: newMetrics(),
+               }
+               nextOpts, err := bal.Run(s.config, opts)
                c.Check(err, check.IsNil)
                c.Check(nextOpts.SafeRendezvousState, check.Not(check.Equals), "")
                c.Check(nextOpts.CommitPulls, check.Equals, true)
index 90235cbf3188d91bc274412ddd5522dc639fa812..e3e90d3581517c0ae8831f76be2881aaa0f1a44c 100644 (file)
@@ -11,67 +11,13 @@ import (
        "log"
        "net/http"
        "os"
-       "os/signal"
-       "syscall"
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/config"
+       "github.com/Sirupsen/logrus"
 )
 
-var version = "dev"
-
-const defaultConfigPath = "/etc/arvados/keep-balance/keep-balance.yml"
-
-// Config specifies site configuration, like API credentials and the
-// choice of which servers are to be balanced.
-//
-// Config is loaded from a JSON config file (see usage()).
-type Config struct {
-       // Arvados API endpoint and credentials.
-       Client arvados.Client
-
-       // List of service types (e.g., "disk") to balance.
-       KeepServiceTypes []string
-
-       KeepServiceList arvados.KeepServiceList
-
-       // How often to check
-       RunPeriod arvados.Duration
-
-       // Number of collections to request in each API call
-       CollectionBatchSize int
-
-       // Max collections to buffer in memory (bigger values consume
-       // more memory, but can reduce store-and-forward latency when
-       // fetching pages)
-       CollectionBuffers int
-
-       // Timeout for outgoing http request/response cycle.
-       RequestTimeout arvados.Duration
-}
-
-// RunOptions controls runtime behavior. The flags/options that belong
-// here are the ones that are useful for interactive use. For example,
-// "CommitTrash" is a runtime option rather than a config item because
-// it invokes a troubleshooting feature rather than expressing how
-// balancing is meant to be done at a given site.
-//
-// RunOptions fields are controlled by command line flags.
-type RunOptions struct {
-       Once        bool
-       CommitPulls bool
-       CommitTrash bool
-       Logger      *log.Logger
-       Dumper      *log.Logger
-
-       // SafeRendezvousState from the most recent balance operation,
-       // or "" if unknown. If this changes from one run to the next,
-       // we need to watch out for races. See
-       // (*Balancer)ClearTrashLists.
-       SafeRendezvousState string
-}
-
 var debugf = func(string, ...interface{}) {}
 
 func main() {
@@ -130,15 +76,17 @@ func main() {
                }
        }
        if *dumpFlag {
-               runOptions.Dumper = log.New(os.Stdout, "", log.LstdFlags)
+               runOptions.Dumper = logrus.New()
+               runOptions.Dumper.Out = os.Stdout
+               runOptions.Dumper.Formatter = &logrus.TextFormatter{}
        }
-       err := CheckConfig(cfg, runOptions)
+       srv, err := NewServer(cfg, runOptions)
        if err != nil {
                // (don't run)
        } else if runOptions.Once {
-               _, err = (&Balancer{}).Run(cfg, runOptions)
+               _, err = srv.Run()
        } else {
-               err = RunForever(cfg, runOptions, nil)
+               err = srv.RunForever(nil)
        }
        if err != nil {
                log.Fatal(err)
@@ -150,53 +98,3 @@ func mustReadConfig(dst interface{}, path string) {
                log.Fatal(err)
        }
 }
-
-// RunForever runs forever, or (for testing purposes) until the given
-// stop channel is ready to receive.
-func RunForever(config Config, runOptions RunOptions, stop <-chan interface{}) error {
-       if runOptions.Logger == nil {
-               runOptions.Logger = log.New(os.Stderr, "", log.LstdFlags)
-       }
-       logger := runOptions.Logger
-
-       ticker := time.NewTicker(time.Duration(config.RunPeriod))
-
-       // The unbuffered channel here means we only hear SIGUSR1 if
-       // it arrives while we're waiting in select{}.
-       sigUSR1 := make(chan os.Signal)
-       signal.Notify(sigUSR1, syscall.SIGUSR1)
-
-       logger.Printf("starting up: will scan every %v and on SIGUSR1", config.RunPeriod)
-
-       for {
-               if !runOptions.CommitPulls && !runOptions.CommitTrash {
-                       logger.Print("WARNING: Will scan periodically, but no changes will be committed.")
-                       logger.Print("=======  Consider using -commit-pulls and -commit-trash flags.")
-               }
-
-               bal := &Balancer{}
-               var err error
-               runOptions, err = bal.Run(config, runOptions)
-               if err != nil {
-                       logger.Print("run failed: ", err)
-               } else {
-                       logger.Print("run succeeded")
-               }
-
-               select {
-               case <-stop:
-                       signal.Stop(sigUSR1)
-                       return nil
-               case <-ticker.C:
-                       logger.Print("timer went off")
-               case <-sigUSR1:
-                       logger.Print("received SIGUSR1, resetting timer")
-                       // Reset the timer so we don't start the N+1st
-                       // run too soon after the Nth run is triggered
-                       // by SIGUSR1.
-                       ticker.Stop()
-                       ticker = time.NewTicker(time.Duration(config.RunPeriod))
-               }
-               logger.Print("starting next run")
-       }
-}
diff --git a/services/keep-balance/metrics.go b/services/keep-balance/metrics.go
new file mode 100644 (file)
index 0000000..5f3c987
--- /dev/null
@@ -0,0 +1,118 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "fmt"
+       "net/http"
+       "sync"
+
+       "github.com/prometheus/client_golang/prometheus"
+       "github.com/prometheus/client_golang/prometheus/promhttp"
+)
+
+type observer interface{ Observe(float64) }
+type setter interface{ Set(float64) }
+
+type metrics struct {
+       reg         *prometheus.Registry
+       statsGauges map[string]setter
+       observers   map[string]observer
+       setupOnce   sync.Once
+       mtx         sync.Mutex
+}
+
+func newMetrics() *metrics {
+       return &metrics{
+               reg:         prometheus.NewRegistry(),
+               statsGauges: map[string]setter{},
+               observers:   map[string]observer{},
+       }
+}
+
+func (m *metrics) DurationObserver(name, help string) observer {
+       m.mtx.Lock()
+       defer m.mtx.Unlock()
+       if obs, ok := m.observers[name]; ok {
+               return obs
+       }
+       summary := prometheus.NewSummary(prometheus.SummaryOpts{
+               Namespace: "arvados",
+               Name:      name,
+               Subsystem: "keepbalance",
+               Help:      help,
+       })
+       m.reg.MustRegister(summary)
+       m.observers[name] = summary
+       return summary
+}
+
+// UpdateStats updates prometheus metrics using the given
+// balancerStats. It creates and registers the needed gauges on its
+// first invocation.
+func (m *metrics) UpdateStats(s balancerStats) {
+       type gauge struct {
+               Value interface{}
+               Help  string
+       }
+       s2g := map[string]gauge{
+               "total":             {s.current, "current backend storage usage"},
+               "garbage":           {s.garbage, "garbage (unreferenced, old)"},
+               "transient":         {s.unref, "transient (unreferenced, new)"},
+               "overreplicated":    {s.overrep, "overreplicated"},
+               "underreplicated":   {s.underrep, "underreplicated"},
+               "lost":              {s.lost, "lost"},
+               "dedup_byte_ratio":  {s.dedupByteRatio(), "deduplication ratio, bytes referenced / bytes stored"},
+               "dedup_block_ratio": {s.dedupBlockRatio(), "deduplication ratio, blocks referenced / blocks stored"},
+       }
+       m.setupOnce.Do(func() {
+               // Register gauge(s) for each balancerStats field.
+               addGauge := func(name, help string) {
+                       g := prometheus.NewGauge(prometheus.GaugeOpts{
+                               Namespace: "arvados",
+                               Name:      name,
+                               Subsystem: "keep",
+                               Help:      help,
+                       })
+                       m.reg.MustRegister(g)
+                       m.statsGauges[name] = g
+               }
+               for name, gauge := range s2g {
+                       switch gauge.Value.(type) {
+                       case blocksNBytes:
+                               for _, sub := range []string{"blocks", "bytes", "replicas"} {
+                                       addGauge(name+"_"+sub, sub+" of "+gauge.Help)
+                               }
+                       case int, int64, float64:
+                               addGauge(name, gauge.Help)
+                       default:
+                               panic(fmt.Sprintf("bad gauge type %T", gauge.Value))
+                       }
+               }
+       })
+       // Set gauges to values from s.
+       for name, gauge := range s2g {
+               switch val := gauge.Value.(type) {
+               case blocksNBytes:
+                       m.statsGauges[name+"_blocks"].Set(float64(val.blocks))
+                       m.statsGauges[name+"_bytes"].Set(float64(val.bytes))
+                       m.statsGauges[name+"_replicas"].Set(float64(val.replicas))
+               case int:
+                       m.statsGauges[name].Set(float64(val))
+               case int64:
+                       m.statsGauges[name].Set(float64(val))
+               case float64:
+                       m.statsGauges[name].Set(float64(val))
+               default:
+                       panic(fmt.Sprintf("bad gauge type %T", gauge.Value))
+               }
+       }
+}
+
+func (m *metrics) Handler(log promhttp.Logger) http.Handler {
+       return promhttp.HandlerFor(m.reg, promhttp.HandlerOpts{
+               ErrorLog: log,
+       })
+}
diff --git a/services/keep-balance/server.go b/services/keep-balance/server.go
new file mode 100644 (file)
index 0000000..ad13be7
--- /dev/null
@@ -0,0 +1,197 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package main
+
+import (
+       "fmt"
+       "net/http"
+       "os"
+       "os/signal"
+       "syscall"
+       "time"
+
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/httpserver"
+       "github.com/Sirupsen/logrus"
+)
+
+var version = "dev"
+
+const (
+       defaultConfigPath = "/etc/arvados/keep-balance/keep-balance.yml"
+       rfc3339NanoFixed  = "2006-01-02T15:04:05.000000000Z07:00"
+)
+
+// Config specifies site configuration, like API credentials and the
+// choice of which servers are to be balanced.
+//
+// Config is loaded from a JSON config file (see usage()).
+type Config struct {
+       // Arvados API endpoint and credentials.
+       Client arvados.Client
+
+       // List of service types (e.g., "disk") to balance.
+       KeepServiceTypes []string
+
+       KeepServiceList arvados.KeepServiceList
+
+       // address, address:port, or :port for management interface
+       Listen string
+
+       // token for management APIs
+       ManagementToken string
+
+       // How often to check
+       RunPeriod arvados.Duration
+
+       // Number of collections to request in each API call
+       CollectionBatchSize int
+
+       // Max collections to buffer in memory (bigger values consume
+       // more memory, but can reduce store-and-forward latency when
+       // fetching pages)
+       CollectionBuffers int
+
+       // Timeout for outgoing http request/response cycle.
+       RequestTimeout arvados.Duration
+}
+
+// RunOptions controls runtime behavior. The flags/options that belong
+// here are the ones that are useful for interactive use. For example,
+// "CommitTrash" is a runtime option rather than a config item because
+// it invokes a troubleshooting feature rather than expressing how
+// balancing is meant to be done at a given site.
+//
+// RunOptions fields are controlled by command line flags.
+type RunOptions struct {
+       Once        bool
+       CommitPulls bool
+       CommitTrash bool
+       Logger      *logrus.Logger
+       Dumper      *logrus.Logger
+
+       // SafeRendezvousState from the most recent balance operation,
+       // or "" if unknown. If this changes from one run to the next,
+       // we need to watch out for races. See
+       // (*Balancer)ClearTrashLists.
+       SafeRendezvousState string
+}
+
+type Server struct {
+       config     Config
+       runOptions RunOptions
+       metrics    *metrics
+       listening  string // for tests
+
+       Logger *logrus.Logger
+       Dumper *logrus.Logger
+}
+
+// NewServer returns a new Server that runs Balancers using the given
+// config and runOptions.
+func NewServer(config Config, runOptions RunOptions) (*Server, error) {
+       if len(config.KeepServiceList.Items) > 0 && config.KeepServiceTypes != nil {
+               return nil, fmt.Errorf("cannot specify both KeepServiceList and KeepServiceTypes in config")
+       }
+       if !runOptions.Once && config.RunPeriod == arvados.Duration(0) {
+               return nil, fmt.Errorf("you must either use the -once flag, or specify RunPeriod in config")
+       }
+
+       if runOptions.Logger == nil {
+               log := logrus.New()
+               log.Formatter = &logrus.JSONFormatter{
+                       TimestampFormat: rfc3339NanoFixed,
+               }
+               log.Out = os.Stderr
+               runOptions.Logger = log
+       }
+
+       srv := &Server{
+               config:     config,
+               runOptions: runOptions,
+               metrics:    newMetrics(),
+               Logger:     runOptions.Logger,
+               Dumper:     runOptions.Dumper,
+       }
+       return srv, srv.start()
+}
+
+func (srv *Server) start() error {
+       if srv.config.Listen == "" {
+               return nil
+       }
+       server := &httpserver.Server{
+               Server: http.Server{
+                       Handler: httpserver.LogRequests(srv.Logger,
+                               auth.RequireLiteralToken(srv.config.ManagementToken,
+                                       srv.metrics.Handler(srv.Logger))),
+               },
+               Addr: srv.config.Listen,
+       }
+       err := server.Start()
+       if err != nil {
+               return err
+       }
+       srv.Logger.Printf("listening at %s", server.Addr)
+       srv.listening = server.Addr
+       return nil
+}
+
+func (srv *Server) Run() (*Balancer, error) {
+       bal := &Balancer{
+               Logger:  srv.Logger,
+               Dumper:  srv.Dumper,
+               Metrics: srv.metrics,
+       }
+       var err error
+       srv.runOptions, err = bal.Run(srv.config, srv.runOptions)
+       return bal, err
+}
+
+// RunForever runs forever, or (for testing purposes) until the given
+// stop channel is ready to receive.
+func (srv *Server) RunForever(stop <-chan interface{}) error {
+       logger := srv.runOptions.Logger
+
+       ticker := time.NewTicker(time.Duration(srv.config.RunPeriod))
+
+       // The unbuffered channel here means we only hear SIGUSR1 if
+       // it arrives while we're waiting in select{}.
+       sigUSR1 := make(chan os.Signal)
+       signal.Notify(sigUSR1, syscall.SIGUSR1)
+
+       logger.Printf("starting up: will scan every %v and on SIGUSR1", srv.config.RunPeriod)
+
+       for {
+               if !srv.runOptions.CommitPulls && !srv.runOptions.CommitTrash {
+                       logger.Print("WARNING: Will scan periodically, but no changes will be committed.")
+                       logger.Print("=======  Consider using -commit-pulls and -commit-trash flags.")
+               }
+
+               _, err := srv.Run()
+               if err != nil {
+                       logger.Print("run failed: ", err)
+               } else {
+                       logger.Print("run succeeded")
+               }
+
+               select {
+               case <-stop:
+                       signal.Stop(sigUSR1)
+                       return nil
+               case <-ticker.C:
+                       logger.Print("timer went off")
+               case <-sigUSR1:
+                       logger.Print("received SIGUSR1, resetting timer")
+                       // Reset the timer so we don't start the N+1st
+                       // run too soon after the Nth run is triggered
+                       // by SIGUSR1.
+                       ticker.Stop()
+                       ticker = time.NewTicker(time.Duration(srv.config.RunPeriod))
+               }
+               logger.Print("starting next run")
+       }
+}
diff --git a/services/keep-balance/time_me.go b/services/keep-balance/time_me.go
deleted file mode 100644 (file)
index 06d727d..0000000
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-package main
-
-import (
-       "log"
-       "time"
-)
-
-func timeMe(logger *log.Logger, label string) func() {
-       t0 := time.Now()
-       logger.Printf("%s: start", label)
-       return func() {
-               logger.Printf("%s: took %v", label, time.Since(t0))
-       }
-}
index 4c7d5067182fe89783e104c56063fdaf86545c1b..b39e83905d617f58b605c22c7e1a43cc8aa4c8cb 100644 (file)
@@ -17,6 +17,8 @@ Client:
     Insecure: false
 KeepServiceTypes:
     - disk
+Listen: ":9005"
+ManagementToken: xyzzy
 RunPeriod: 600s
 CollectionBatchSize: 100000
 CollectionBuffers: 1000
index 912398fa64db5d8b18605178f14a77884e234f1d..95948e32505f40112cff4da72c88692d7ea6edff 100644 (file)
@@ -320,7 +320,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 
        if useSiteFS {
                if tokens == nil {
-                       tokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
+                       tokens = auth.CredentialsFromRequest(r).Tokens
                }
                h.serveSiteFS(w, r, tokens, credentialsOK, attachment)
                return
@@ -342,7 +342,7 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
 
        if tokens == nil {
                if credentialsOK {
-                       reqTokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
+                       reqTokens = auth.CredentialsFromRequest(r).Tokens
                }
                tokens = append(reqTokens, h.Config.AnonymousTokens...)
        }
index 4ea697bbed6883172bfc459b7df78f4d939f2c90..39fb87fbaa5f6f0de5aee86258114b10b6df8e6e 100644 (file)
@@ -350,6 +350,28 @@ func (s *IntegrationSuite) TestVhostRedirectQueryTokenSiteFS(c *check.C) {
        c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
 }
 
+func (s *IntegrationSuite) TestPastCollectionVersionFileAccess(c *check.C) {
+       s.testServer.Config.AttachmentOnlyHost = "download.example.com"
+       resp := s.testVhostRedirectTokenToCookie(c, "GET",
+               "download.example.com/c="+arvadostest.WazVersion1Collection+"/waz",
+               "?api_token="+arvadostest.ActiveToken,
+               "",
+               "",
+               http.StatusOK,
+               "waz",
+       )
+       c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
+       resp = s.testVhostRedirectTokenToCookie(c, "GET",
+               "download.example.com/by_id/"+arvadostest.WazVersion1Collection+"/waz",
+               "?api_token="+arvadostest.ActiveToken,
+               "",
+               "",
+               http.StatusOK,
+               "waz",
+       )
+       c.Check(resp.Header().Get("Content-Disposition"), check.Matches, "attachment(;.*)?")
+}
+
 func (s *IntegrationSuite) TestVhostRedirectQueryTokenTrustAllContent(c *check.C) {
        s.testServer.Config.TrustAllContent = true
        s.testVhostRedirectTokenToCookie(c, "GET",
@@ -656,6 +678,18 @@ func (s *IntegrationSuite) TestDirectoryListing(c *check.C) {
                        header: authHeader,
                        expect: nil,
                },
+               {
+                       uri:     "download.example.com/c=" + arvadostest.WazVersion1Collection,
+                       header:  authHeader,
+                       expect:  []string{"waz"},
+                       cutDirs: 1,
+               },
+               {
+                       uri:     "download.example.com/by_id/" + arvadostest.WazVersion1Collection,
+                       header:  authHeader,
+                       expect:  []string{"waz"},
+                       cutDirs: 2,
+               },
        } {
                c.Logf("HTML: %q => %q", trial.uri, trial.expect)
                resp := httptest.NewRecorder()
index 68ff8a7b013c2d685299eae2dc7c7da1d84f5606..f70dd1a71f6ae92ecdc3f2979e2296f33238e28f 100644 (file)
@@ -21,7 +21,7 @@ func (srv *server) Start() error {
        reg := prometheus.NewRegistry()
        h.Config.Cache.registry = reg
        mh := httpserver.Instrument(reg, nil, httpserver.AddRequestIDs(httpserver.LogRequests(nil, h)))
-       h.MetricsAPI = mh.ServeAPI(http.NotFoundHandler())
+       h.MetricsAPI = mh.ServeAPI(h.Config.ManagementToken, http.NotFoundHandler())
        srv.Handler = mh
        srv.Addr = srv.Config.Listen
        return srv.Server.Start()
index 7e738cb9f3467a63c5da91cbac253429f0dc5cad..8b689efbdc1f1d731bc2a9dfb106c12e3c214cef 100644 (file)
@@ -323,6 +323,18 @@ func (s *IntegrationSuite) TestMetrics(c *check.C) {
        req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
        resp, err = http.DefaultClient.Do(req)
        c.Assert(err, check.IsNil)
+       c.Check(resp.StatusCode, check.Equals, http.StatusUnauthorized)
+
+       req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
+       req.Header.Set("Authorization", "Bearer badtoken")
+       resp, err = http.DefaultClient.Do(req)
+       c.Assert(err, check.IsNil)
+       c.Check(resp.StatusCode, check.Equals, http.StatusForbidden)
+
+       req, _ = http.NewRequest("GET", origin+"/metrics.json", nil)
+       req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken)
+       resp, err = http.DefaultClient.Do(req)
+       c.Assert(err, check.IsNil)
        c.Check(resp.StatusCode, check.Equals, http.StatusOK)
        type summary struct {
                SampleCount string  `json:"sample_count"`
@@ -403,6 +415,7 @@ func (s *IntegrationSuite) SetUpSuite(c *check.C) {
        kc.PutB([]byte("Hello world\n"))
        kc.PutB([]byte("foo"))
        kc.PutB([]byte("foobar"))
+       kc.PutB([]byte("waz"))
 }
 
 func (s *IntegrationSuite) TearDownSuite(c *check.C) {
@@ -418,6 +431,7 @@ func (s *IntegrationSuite) SetUpTest(c *check.C) {
                Insecure: true,
        }
        cfg.Listen = "127.0.0.1:0"
+       cfg.ManagementToken = arvadostest.ManagementToken
        s.testServer = &server{Config: cfg}
        err := s.testServer.Start()
        c.Assert(err, check.Equals, nil)
index 1f8c7e31a2997ac2884ae2936ea174a0d859e017..2e3fe0a5b130fe5259550b450dea2bf0237cd295 100644 (file)
@@ -46,8 +46,7 @@ type Config struct {
        systemAuthToken string
        debugLogf       func(string, ...interface{})
 
-       ManagementToken string `doc: The secret key that must be provided by monitoring services
-wishing to access the health check endpoint (/_health).`
+       ManagementToken string
 }
 
 var (
index d84ede6ef6b599fbac4aea3c94f43ba8009ff035..e079b96784a16b985ed6ce47f99655e39a571ce9 100644 (file)
@@ -87,9 +87,9 @@ func MakeRESTRouter(cluster *arvados.Cluster) http.Handler {
 
        rtr.limiter = httpserver.NewRequestLimiter(theConfig.MaxRequests, rtr)
 
-       stack := httpserver.Instrument(nil, nil,
+       instrumented := httpserver.Instrument(nil, nil,
                httpserver.AddRequestIDs(httpserver.LogRequests(nil, rtr.limiter)))
-       return stack.ServeAPI(stack)
+       return instrumented.ServeAPI(theConfig.ManagementToken, instrumented)
 }
 
 // BadRequestHandler is a HandleFunc to address bad requests.
index 9fa0090aa739be1d640b0d2ba3a693a659087284..31b1a684fe6a077ebbbfebf7bb846f6f508a00b5 100644 (file)
@@ -27,6 +27,7 @@ func (s *MountsSuite) SetUpTest(c *check.C) {
        KeepVM = s.vm
        theConfig = DefaultConfig()
        theConfig.systemAuthToken = arvadostest.DataManagerToken
+       theConfig.ManagementToken = arvadostest.ManagementToken
        theConfig.Start()
        s.rtr = MakeRESTRouter(testCluster)
 }
@@ -104,6 +105,10 @@ func (s *MountsSuite) TestMetrics(c *check.C) {
        s.call("PUT", "/"+TestHash, "", TestBlock)
        s.call("PUT", "/"+TestHash2, "", TestBlock2)
        resp := s.call("GET", "/metrics.json", "", nil)
+       c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
+       resp = s.call("GET", "/metrics.json", "foobar", nil)
+       c.Check(resp.Code, check.Equals, http.StatusForbidden)
+       resp = s.call("GET", "/metrics.json", arvadostest.ManagementToken, nil)
        c.Check(resp.Code, check.Equals, http.StatusOK)
        var j []struct {
                Name   string
@@ -144,7 +149,7 @@ func (s *MountsSuite) call(method, path, tok string, body []byte) *httptest.Resp
        resp := httptest.NewRecorder()
        req, _ := http.NewRequest(method, path, bytes.NewReader(body))
        if tok != "" {
-               req.Header.Set("Authorization", "OAuth2 "+tok)
+               req.Header.Set("Authorization", "Bearer "+tok)
        }
        s.rtr.ServeHTTP(resp, req)
        return resp
index 7322a0ab52f613469c4345a89eed533a444a5107..84104202a3b165356a0caf5056c743fe45667e32 100644 (file)
@@ -4,4 +4,5 @@
 
 include agpl-3.0.txt
 include README.rst
-include arvados_version.py
\ No newline at end of file
+include arvados_version.py
+include arvados-node-manager.service
diff --git a/services/nodemanager/arvados-node-manager.service b/services/nodemanager/arvados-node-manager.service
new file mode 100644 (file)
index 0000000..38c525b
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+[Unit]
+Description=Arvados Node Manager Daemon
+Documentation=https://doc.arvados.org/
+After=network.target
+AssertPathExists=/etc/arvados-node-manager/config.ini
+
+# systemd==229 (ubuntu:xenial) obeys StartLimitInterval in the [Unit] section
+StartLimitInterval=0
+
+# systemd>=230 (debian:9) obeys StartLimitIntervalSec in the [Unit] section
+StartLimitIntervalSec=0
+
+[Service]
+EnvironmentFile=-/etc/default/arvados-node-manager
+LimitDATA=3145728K
+LimitRSS=3145728K
+LimitMEMLOCK=3145728K
+LimitNOFILE=10240
+Type=simple
+ExecStart=/usr/bin/env sh -c '/usr/bin/arvados-node-manager --foreground --config /etc/arvados-node-manager/config.ini 2>&1 | cat'
+Restart=always
+RestartSec=1
+
+# systemd<=219 (centos:7, debian:8, ubuntu:trusty) obeys StartLimitInterval in the [Service] section
+StartLimitInterval=0
+
+[Install]
+WantedBy=multi-user.target
index 1e41f3dad2fd32cfa3f42c461f2b21362796cb8e..4f00d54e7e28d10eb366e47da1e0b2f1957d017f 100644 (file)
@@ -32,7 +32,7 @@ setup(name='arvados-node-manager',
       packages=find_packages(),
       scripts=['bin/arvados-node-manager'],
       data_files=[
-          ('share/doc/arvados-node-manager', ['agpl-3.0.txt', 'README.rst']),
+          ('share/doc/arvados-node-manager', ['agpl-3.0.txt', 'README.rst', 'arvados-node-manager.service']),
       ],
       install_requires=[
           'apache-libcloud>=2.3.1.dev1',