<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 %>
<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>
# 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
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
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
--- /dev/null
+*/generated
+common-generated/
--- /dev/null
+# 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
--- /dev/null
+==================
+DOCKER IMAGE BUILD
+==================
+
+1. `make`
+2. `cd DISTRO`
+3. `docker build -t arvados/build:DISTRO .`
# 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 && \
# 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
+++ /dev/null
------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-----
# 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
# 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
# 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
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
if [[ -n "$test_packages" ]]; then
pushd "$JENKINS_DIR/package-test-dockerfiles"
+ make "$TARGET/generated"
else
pushd "$JENKINS_DIR/package-build-dockerfiles"
make "$TARGET/generated"
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"
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
sdk/ruby
sdk/go/arvados
sdk/go/arvadosclient
+sdk/go/auth
sdk/go/dispatch
sdk/go/keepclient
sdk/go/health
lib/dispatchcloud
sdk/go/arvados
sdk/go/arvadosclient
+ sdk/go/auth
sdk/go/blockdigest
sdk/go/dispatch
sdk/go/health
--- /dev/null
+{% 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>
<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 <<EOF; arv link create --link "$newlink"</span>
<span class="userinput">{
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>
}
</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 ÷ distinct blocks referenced)|
+|arvados_keep_dedup_byte_ratio|gauge|deduplication ratio (block references in collections ÷ 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.
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
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' %}
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
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' %}
# 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
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' %}
<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>
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"`
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"
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,
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"
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
--- /dev/null
+// 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)
+ })
+}
--- /dev/null
+// 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)
+}
}
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
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},
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},
"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"
// 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 {
// 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)
}
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
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
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!
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.
# 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 }
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
# 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
"(#{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
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
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
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
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
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
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
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
# 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.
# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
--- /dev/null
+# 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
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
);
-- 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);
--
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: -
--
-- 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));
--
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');
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
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
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
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
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
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
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
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
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
# 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
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
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
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
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
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
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
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
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
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
# 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
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
# 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
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
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
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
#
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
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
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
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
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
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
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"
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"
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"
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
# 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
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
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
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
# 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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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) %>
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) %>
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
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
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']
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
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
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
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\"")
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
"fmt"
"log"
"math"
- "os"
"runtime"
"sort"
"strings"
"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.
// 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
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)
//
// 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()
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 {
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 {
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 {
s.trashes += len(srv.ChangeSet.Trashes)
}
bal.stats = s
+ bal.Metrics.UpdateStats(s)
}
// PrintStatistics writes statistics about the computed changes to
// 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)
// 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)
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)
}
}
+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 {
"fmt"
"io"
"io/ioutil"
- "log"
"net/http"
"net/http/httptest"
"strings"
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
+ "github.com/Sirupsen/logrus"
check "gopkg.in/check.v1"
)
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"}]}`)
}
}
// 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)
}
}
}()
- return log.New(w, "", log.LstdFlags)
+ logger := logrus.New()
+ logger.Out = w
+ return logger
}
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
}
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)
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)
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)
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)
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")
}
func (s *runSuite) TestCommit(c *check.C) {
+ s.config.Listen = ":"
+ s.config.ManagementToken = "xyzzy"
opts := RunOptions{
CommitPulls: true,
CommitTrash: true,
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)
// "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,
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
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)
}
// 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")
}
func (bs *BlockState) increaseDesired(classes []string, n int) {
+ bs.RefCount++
if len(classes) == 0 {
classes = defaultClasses
}
import (
"bytes"
- "log"
"os"
"strings"
"testing"
"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"
)
Insecure: true,
},
KeepServiceTypes: []string{"disk"},
+ RunPeriod: arvados.Duration(time.Second),
}
}
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)
"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() {
}
}
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)
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")
- }
-}
--- /dev/null
+// 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,
+ })
+}
--- /dev/null
+// 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")
+ }
+}
+++ /dev/null
-// 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))
- }
-}
Insecure: false
KeepServiceTypes:
- disk
+Listen: ":9005"
+ManagementToken: xyzzy
RunPeriod: 600s
CollectionBatchSize: 100000
CollectionBuffers: 1000
if useSiteFS {
if tokens == nil {
- tokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
+ tokens = auth.CredentialsFromRequest(r).Tokens
}
h.serveSiteFS(w, r, tokens, credentialsOK, attachment)
return
if tokens == nil {
if credentialsOK {
- reqTokens = auth.NewCredentialsFromHTTPRequest(r).Tokens
+ reqTokens = auth.CredentialsFromRequest(r).Tokens
}
tokens = append(reqTokens, h.Config.AnonymousTokens...)
}
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",
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()
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()
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"`
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) {
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)
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 (
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.
KeepVM = s.vm
theConfig = DefaultConfig()
theConfig.systemAuthToken = arvadostest.DataManagerToken
+ theConfig.ManagementToken = arvadostest.ManagementToken
theConfig.Start()
s.rtr = MakeRESTRouter(testCluster)
}
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
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
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
--- /dev/null
+# 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
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',