7475: Merge branch 'master' into 7475-nodemgr-unsatisfiable-job-comms
authorLucas Di Pentima <lucas@curoverse.com>
Sat, 29 Jul 2017 14:11:43 +0000 (11:11 -0300)
committerLucas Di Pentima <lucas@curoverse.com>
Sat, 29 Jul 2017 14:11:43 +0000 (11:11 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@curoverse.com>

95 files changed:
AUTHORS
apps/workbench/app/controllers/healthcheck_controller.rb [new file with mode: 0644]
apps/workbench/app/helpers/collections_helper.rb
apps/workbench/config/application.default.yml
apps/workbench/config/routes.rb
apps/workbench/test/controllers/healthcheck_controller_test.rb [new file with mode: 0644]
apps/workbench/test/helpers/collections_helper_test.rb
apps/workbench/test/integration/container_requests_test.rb
build/build.list
build/package-build-dockerfiles/Makefile
build/package-build-dockerfiles/centos7/Dockerfile
build/package-build-dockerfiles/debian8/Dockerfile
build/package-build-dockerfiles/debian9/D39DC0E3.asc [new file with mode: 0644]
build/package-build-dockerfiles/debian9/Dockerfile [new file with mode: 0644]
build/package-build-dockerfiles/ubuntu1204/Dockerfile
build/package-build-dockerfiles/ubuntu1404/Dockerfile
build/package-build-dockerfiles/ubuntu1604/Dockerfile
build/package-test-dockerfiles/centos7/Dockerfile
build/package-test-dockerfiles/debian8/Dockerfile
build/package-test-dockerfiles/debian9/D39DC0E3.asc [new file with mode: 0644]
build/package-test-dockerfiles/debian9/Dockerfile [new file with mode: 0644]
build/package-test-dockerfiles/ubuntu1204/Dockerfile
build/package-test-dockerfiles/ubuntu1404/Dockerfile
build/package-test-dockerfiles/ubuntu1604/Dockerfile
build/package-testing/test-packages-debian9.sh [new file with mode: 0755]
build/run-build-packages-sso.sh
build/run-build-packages.sh
build/run-tests.sh
sdk/cli/bin/crunch-job
sdk/cli/test/test_crunch-job.rb
sdk/cwl/arvados_cwl/__init__.py
sdk/cwl/arvados_cwl/arvtool.py
sdk/cwl/arvados_cwl/fsaccess.py
sdk/cwl/arvados_cwl/runner.py
sdk/cwl/setup.py
sdk/cwl/tests/test_submit.py
sdk/cwl/tests/wf/expect_packed.cwl
sdk/go/health/handler.go [new file with mode: 0644]
sdk/go/health/handler_test.go [new file with mode: 0644]
sdk/python/arvados/commands/put.py
sdk/python/tests/test_arv_get.py
sdk/python/tests/test_arv_ls.py
sdk/python/tests/test_arv_put.py
services/api/app/controllers/arvados/v1/healthcheck_controller.rb [new file with mode: 0644]
services/api/app/models/container_request.rb
services/api/app/models/user.rb
services/api/config/application.default.yml
services/api/config/environment.rb
services/api/config/routes.rb
services/api/test/fixtures/container_requests.yml
services/api/test/fixtures/containers.yml
services/api/test/functional/arvados/v1/healthcheck_controller_test.rb [new file with mode: 0644]
services/api/test/integration/errors_test.rb
services/api/test/unit/container_request_test.rb
services/arv-git-httpd/gitolite_test.go
services/arv-git-httpd/integration_test.go
services/arv-git-httpd/main.go
services/arv-git-httpd/server.go
services/arv-git-httpd/server_test.go
services/fuse/README.rst
services/fuse/arvados_fuse/__init__.py
services/fuse/arvados_fuse/command.py
services/fuse/arvados_fuse/fresh.py
services/fuse/arvados_fuse/fusedir.py
services/fuse/setup.py
services/fuse/tests/integration_test.py
services/fuse/tests/mount_test_base.py
services/fuse/tests/test_mount.py
services/keep-web/cache.go
services/keep-web/cache_test.go
services/keep-web/handler.go
services/keep-web/handler_test.go
services/keep-web/main.go
services/keepproxy/keepproxy.go
services/keepproxy/keepproxy_test.go
services/keepproxy/usage.go
services/keepstore/config.go
services/keepstore/handler_test.go
services/keepstore/handlers.go
services/nodemanager/arvnodeman/config.py
services/nodemanager/arvnodeman/status.py
services/nodemanager/tests/test_status.py
services/ws/event_source.go
services/ws/permission.go
services/ws/router.go
services/ws/session_v0.go
services/ws/session_v0_test.go
tools/crunchstat-summary/MANIFEST.in
tools/crunchstat-summary/crunchstat_summary/chartjs.js [deleted file]
tools/crunchstat-summary/crunchstat_summary/chartjs.py [deleted file]
tools/crunchstat-summary/crunchstat_summary/dygraphs.js [new file with mode: 0644]
tools/crunchstat-summary/crunchstat_summary/dygraphs.py [new file with mode: 0644]
tools/crunchstat-summary/crunchstat_summary/summarizer.py
tools/crunchstat-summary/crunchstat_summary/webchart.py [new file with mode: 0644]
vendor/vendor.json

diff --git a/AUTHORS b/AUTHORS
index ad72aea0aa98486b498465b51e4f1cfd9211658a..ea9fa4c7092e8c2069a2105d8eafb25e6107d3ab 100644 (file)
--- a/AUTHORS
+++ b/AUTHORS
@@ -16,3 +16,4 @@ Guillermo Carrasco <guille.ch.88@gmail.com>
 Joshua Randall <joshua.randall@sanger.ac.uk>
 President and Fellows of Harvard College <*@harvard.edu>
 Thomas Mooney <tmooney@genome.wustl.edu>
+Chen Chen <aflyhorse@gmail.com>
diff --git a/apps/workbench/app/controllers/healthcheck_controller.rb b/apps/workbench/app/controllers/healthcheck_controller.rb
new file mode 100644 (file)
index 0000000..60043d9
--- /dev/null
@@ -0,0 +1,35 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class HealthcheckController < ApplicationController
+  skip_around_filter :thread_clear
+  skip_around_filter :set_thread_api_token
+  skip_around_filter :require_thread_api_token
+  skip_before_filter :ensure_arvados_api_exists
+  skip_before_filter :accept_uuid_as_id_param
+  skip_before_filter :check_user_agreements
+  skip_before_filter :check_user_profile
+  skip_before_filter :load_filters_and_paging_params
+  skip_before_filter :find_object_by_uuid
+
+  before_filter :check_auth_header
+
+  def check_auth_header
+    mgmt_token = Rails.configuration.ManagementToken
+    auth_header = request.headers['Authorization']
+
+    if !mgmt_token
+      render :json => {:errors => "disabled"}, :status => 404
+    elsif !auth_header
+      render :json => {:errors => "authorization required"}, :status => 401
+    elsif auth_header != 'Bearer '+mgmt_token
+      render :json => {:errors => "authorization error"}, :status => 403
+    end
+  end
+
+  def ping
+    resp = {"health" => "OK"}
+    render json: resp
+  end
+end
index e249eaa8ac4ee8c18fae631868087db49fc2d705..f5f54851e78dc7161ea263cf8d03b6923cb1bf33 100644 (file)
@@ -64,7 +64,11 @@ module CollectionsHelper
   def preview_allowed_for file_name
     file_type = MIME::Types.type_for(file_name).first
     if file_type.nil?
-      false
+      if file_name.downcase.end_with?('.cwl') # unknown mime type, but we support preview
+        true
+      else
+        false
+      end
     elsif (file_type.raw_media_type == "text") || (file_type.raw_media_type == "image")
       true
     elsif (file_type.raw_media_type == "application") &&
index 6998b17c9ebbac847bdee224162417d26b7fe033..9f6c1ed338042764c3f38d506753caab7682350e 100644 (file)
@@ -209,7 +209,7 @@ common:
   # would be enabled in a collection's show page.
   # It is sufficient to list only applications here.
   # No need to list text and image types.
-  application_mimetypes_with_view_icon: [fasta, go, javascript, json, pdf, python, r, rtf, sam, sh, xml, xsl]
+  application_mimetypes_with_view_icon: [cwl, fasta, go, javascript, json, pdf, python, r, rtf, sam, sh, vnd.realvnc.bed, xml, xsl]
 
   # the maximum number of bytes to load in the log viewer
   log_viewer_max_bytes: 1000000
@@ -295,3 +295,7 @@ common:
   # to suppress these properties
   show_recent_collections_on_dashboard: true
   show_user_notifications: true
+
+  # Token to be included in all healthcheck requests. Disabled by default.
+  # Workbench expects request header of the format "Authorization: Bearer xxx"
+  ManagementToken: false
index f2387f181e6550b6234a835d8fb190a6526072af..a3644e51699871351aecba16ad7dda354695443b 100644 (file)
@@ -126,6 +126,8 @@ ArvadosWorkbench::Application.routes.draw do
 
   root :to => 'projects#index'
 
+  match '/_health/ping', to: 'healthcheck#ping', via: [:get]
+
   # Send unroutable requests to an arbitrary controller
   # (ends up at ApplicationController#render_not_found)
   match '*a', to: 'links#render_not_found', via: [:get, :post]
diff --git a/apps/workbench/test/controllers/healthcheck_controller_test.rb b/apps/workbench/test/controllers/healthcheck_controller_test.rb
new file mode 100644 (file)
index 0000000..9a63a29
--- /dev/null
@@ -0,0 +1,30 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+require 'test_helper'
+
+class HealthcheckControllerTest < ActionController::TestCase
+  [
+    [false, nil, 404, 'disabled'],
+    [true, nil, 401, 'authorization required'],
+    [true, 'badformatwithnoBearer', 403, 'authorization error'],
+    [true, 'Bearer wrongtoken', 403, 'authorization error'],
+    [true, 'Bearer configuredmanagementtoken', 200, '{"health":"OK"}'],
+  ].each do |enabled, header, error_code, error_msg|
+    test "ping when #{if enabled then 'enabled' else 'disabled' end} with header '#{header}'" do
+      Rails.configuration.ManagementToken = 'configuredmanagementtoken' if enabled
+
+      @request.headers['Authorization'] = header
+      get :ping
+      assert_response error_code
+
+      resp = JSON.parse(@response.body)
+      if error_code == 200
+        assert_equal(JSON.load('{"health":"OK"}'), resp)
+      else
+        assert_equal(resp['errors'], error_msg)
+      end
+    end
+  end
+end
index ae41d849c068c6528140e3bddcee841bdca9442c..e02b2abdc775b89ab6d431021116f2b7b13d9f4e 100644 (file)
@@ -29,6 +29,9 @@ class CollectionsHelperTest < ActionView::TestCase
     ["filename.xml", true],
     ["filename.xsl", true],
     ["filename.yml", true],
+    ["filename.yaml", true],
+    ["filename.bed", true],
+    ["filename.cwl", true],
 
     ["filename.bam", false],
     ["filename.tar", false],
index 5bb4337c39b0ae92565719567fe5ee7d37f42868..151654b231d2a8e9e959e5177305a97b50abe22c 100644 (file)
@@ -129,6 +129,11 @@ class ContainerRequestsTest < ActionDispatch::IntegrationTest
   test "index page" do
     visit page_with_token("active", "/container_requests")
 
+    within(".arv-recent-container-requests") do
+      page.execute_script "window.scrollBy(0,999000)"
+      wait_for_ajax
+    end
+
     running_owner_active = api_fixture("container_requests", "requester_for_running")
     anon_accessible_cr = api_fixture("container_requests", "running_anonymous_accessible")
 
index 894eefafe8907c193b7570cb6e3bc6519d2f2022..b71ac890b209908608d2eb59d35fb37bd9f85e43 100644 (file)
@@ -3,35 +3,35 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 #distribution(s)|name|version|iteration|type|architecture|extra fpm arguments
-debian8,ubuntu1204,centos7|python-gflags|2.0|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|google-api-python-client|1.6.2|2|python|all
-debian8,ubuntu1204,ubuntu1404,centos7|oauth2client|1.5.2|2|python|all
-debian8,ubuntu1204,ubuntu1404,centos7|pyasn1|0.1.7|2|python|all
-debian8,ubuntu1204,ubuntu1404,centos7|pyasn1-modules|0.0.5|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|rsa|3.4.2|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|uritemplate|3.0.0|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|httplib2|0.9.2|3|python|all
-debian8,ubuntu1204,centos7|ws4py|0.3.5|2|python|all
-debian8,ubuntu1204,centos7|pykka|1.2.1|2|python|all
-debian8,ubuntu1204,ubuntu1404|six|1.10.0|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|ciso8601|1.0.3|3|python|amd64
-debian8,ubuntu1204,centos7|pycrypto|2.6.1|3|python|amd64
-debian8,ubuntu1204,ubuntu1404,ubuntu1604|backports.ssl_match_hostname|3.5.0.1|2|python|all
-debian8,ubuntu1204,ubuntu1404,centos7|llfuse|0.41.1|3|python|amd64
-debian8,ubuntu1204,ubuntu1404,centos7|pycurl|7.19.5.3|3|python|amd64
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|pyyaml|3.12|2|python|amd64
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|rdflib|4.2.2|2|python|all
-debian8,ubuntu1204,ubuntu1404,centos7|shellescape|3.4.1|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|mistune|0.7.3|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|typing|3.5.3.0|2|python|all
-debian8,ubuntu1204,ubuntu1404,centos7|avro|1.8.1|2|python|all
-debian8,ubuntu1204,ubuntu1404,centos7|ruamel.ordereddict|0.4.9|2|python|amd64
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|cachecontrol|0.11.7|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|pathlib2|2.1.0|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|docker-py|1.7.2|2|python3|all
-debian8,ubuntu1204,centos7|six|1.10.0|2|python3|all
-debian8,ubuntu1204,ubuntu1404,centos7|requests|2.12.4|2|python3|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|websocket-client|0.37.0|2|python3|all
+debian8,debian9,ubuntu1204,centos7|python-gflags|2.0|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|google-api-python-client|1.6.2|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,centos7|oauth2client|1.5.2|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,centos7|pyasn1|0.1.7|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,centos7|pyasn1-modules|0.0.5|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|rsa|3.4.2|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|uritemplate|3.0.0|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|httplib2|0.9.2|3|python|all
+debian8,debian9,ubuntu1204,centos7|ws4py|0.3.5|2|python|all
+debian8,debian9,ubuntu1204,centos7|pykka|1.2.1|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404|six|1.10.0|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|ciso8601|1.0.3|3|python|amd64
+debian8,debian9,ubuntu1204,centos7|pycrypto|2.6.1|3|python|amd64
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604|backports.ssl_match_hostname|3.5.0.1|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|llfuse|1.2|3|python|amd64
+debian8,debian9,ubuntu1204,ubuntu1404,centos7|pycurl|7.19.5.3|3|python|amd64
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|pyyaml|3.12|2|python|amd64
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|rdflib|4.2.2|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,centos7|shellescape|3.4.1|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|mistune|0.7.3|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|typing|3.5.3.0|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|avro|1.8.1|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,centos7|ruamel.ordereddict|0.4.9|2|python|amd64
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|cachecontrol|0.11.7|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|pathlib2|2.1.0|2|python|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|docker-py|1.7.2|2|python3|all
+debian8,debian9,ubuntu1204,centos7|six|1.10.0|2|python3|all
+debian8,debian9,ubuntu1204,ubuntu1404,centos7|requests|2.12.4|2|python3|all
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|websocket-client|0.37.0|2|python3|all
 ubuntu1204|requests|2.12.4|2|python|all
 ubuntu1204,centos7|contextlib2|0.5.4|2|python|all
 ubuntu1204,centos7|isodate|0.5.4|2|python|all
@@ -39,7 +39,7 @@ centos7|daemon|2.1.1|2|python|all
 centos7|pbr|0.11.1|2|python|all
 centos7|pyparsing|2.1.10|2|python|all
 centos7|keepalive|0.5|2|python|all
-debian8,ubuntu1204,ubuntu1404,ubuntu1604,centos7|lockfile|0.12.2|2|python|all|--epoch 1
+debian8,debian9,ubuntu1204,ubuntu1404,ubuntu1604,centos7|lockfile|0.12.2|2|python|all|--epoch 1
 all|ruamel.yaml|0.13.7|2|python|amd64|--python-setup-py-arguments --single-version-externally-managed
 all|cwltest|1.0.20160907111242|3|python|all|--depends 'python-futures >= 3.0.5'
 all|rdflib-jsonld|0.4.0|2|python|all
index d1441259864cfeb93ae9553c17a65cb7ba618d96..e35056b5c079b04c275ec22b09df6a6b85f2b0b2 100644 (file)
@@ -2,7 +2,7 @@
 #
 # SPDX-License-Identifier: AGPL-3.0
 
-all: centos7/generated debian8/generated ubuntu1204/generated ubuntu1404/generated ubuntu1604/generated
+all: centos7/generated debian8/generated debian9/generated ubuntu1204/generated ubuntu1404/generated ubuntu1604/generated
 
 centos7/generated: common-generated-all
        test -d centos7/generated || mkdir centos7/generated
@@ -12,6 +12,10 @@ 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/*
+
 ubuntu1204/generated: common-generated-all
        test -d ubuntu1204/generated || mkdir ubuntu1204/generated
        cp -rlt ubuntu1204/generated common-generated/*
index 0c03de6bbbf63baa5db7e11e2ea89e3ad13180e3..0f084b300b0c20a9114c06a50d2096e34d4fced3 100644 (file)
@@ -3,15 +3,11 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM centos:7
-MAINTAINER Brett Smith <brett@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install build dependencies provided in base distribution
+# Install dependencies.
 RUN yum -q -y install make automake gcc gcc-c++ libyaml-devel patch readline-devel zlib-devel libffi-devel openssl-devel bzip2 libtool bison sqlite-devel rpm-build git perl-ExtUtils-MakeMaker libattr-devel nss-devel libcurl-devel which tar unzip scl-utils centos-release-scl postgresql-devel python-devel python-setuptools fuse-devel xz-libs git
 
-# Install golang binary
-ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
-RUN ln -s /usr/local/go/bin/go /usr/local/bin/
-
 # Install RVM
 RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
@@ -20,10 +16,17 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     /usr/local/rvm/bin/rvm-exec default gem install bundler && \
     /usr/local/rvm/bin/rvm-exec default gem install cure-fpm --version 1.6.0b
 
+# Install golang binary
+ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
+RUN ln -s /usr/local/go/bin/go /usr/local/bin/
+
 # Need to "touch" RPM database to workaround bug in interaction between
 # overlayfs and yum (https://bugzilla.redhat.com/show_bug.cgi?id=1213602)
 RUN touch /var/lib/rpm/* && yum -q -y install python33
 RUN scl enable python33 "easy_install-3.3 pip" && easy_install-2.7 pip
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["scl", "enable", "python33", "/usr/local/rvm/bin/rvm-exec default bash /jenkins/run-build-packages.sh --target centos7"]
index 99ae17f5c8d63444e4542fa1bd7b7217e9847b16..f5aced70d6d6d96131be2489e2cfc8da75aa321c 100644 (file)
@@ -5,7 +5,9 @@
 FROM debian:jessie
 MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install dependencies and set up system.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
 RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git procps libattr1-dev libfuse-dev libgnutls28-dev libpq-dev python-pip unzip
 
 # Install RVM
@@ -20,5 +22,8 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
 ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "debian8"]
diff --git a/build/package-build-dockerfiles/debian9/D39DC0E3.asc b/build/package-build-dockerfiles/debian9/D39DC0E3.asc
new file mode 100644 (file)
index 0000000..231fbdd
--- /dev/null
@@ -0,0 +1,692 @@
+-----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-----
diff --git a/build/package-build-dockerfiles/debian9/Dockerfile b/build/package-build-dockerfiles/debian9/Dockerfile
new file mode 100644 (file)
index 0000000..0003384
--- /dev/null
@@ -0,0 +1,32 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+## dont use debian:9 here since the word 'stretch' is used for rvm precompiled binaries
+FROM debian:stretch
+MAINTAINER Nico Cesar <nico@curoverse.com>
+
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
+RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git procps libattr1-dev libfuse-dev libgnutls28-dev libpq-dev python-pip unzip
+
+# Install RVM
+COPY D39DC0E3.asc /tmp
+RUN gpg --import /tmp/D39DC0E3.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 && \
+    /usr/local/rvm/bin/rvm-exec default gem install bundler && \
+    /usr/local/rvm/bin/rvm-exec default gem install cure-fpm --version 1.6.0b
+
+# Install golang binary
+COPY generated/go1.8.3.linux-amd64.tar.gz /usr/local/
+RUN cd /usr/local && ls && tar xzvf go1.8.3.linux-amd64.tar.gz
+RUN ln -s /usr/local/go/bin/go /usr/local/bin/
+
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
+ENV WORKSPACE /arvados
+CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "debian9"]
index 3006b6d7daa3937b6dc64b4209eee7affe3cbebe..bf0908969103569ccf4638b4239f49a980e55310 100644 (file)
@@ -5,7 +5,9 @@
 FROM ubuntu:precise
 MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install dependencies and set up system.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
 RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip build-essential unzip
 
 # Install RVM
@@ -20,5 +22,8 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
 ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1204"]
index 3e2c86935052c160453731737faeb05493648cad..ecfcefc593b894eb55f8c9b8287211a9d5dcc7f0 100644 (file)
@@ -3,9 +3,11 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM ubuntu:trusty
-MAINTAINER Brett Smith <brett@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install dependencies and set up system.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
 RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip
 
 # Install RVM
@@ -20,5 +22,8 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
 ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1404"]
index 85c36b5243e2ed9c2a1bdcae0c04e7399a27b218..b7c02d79de421b4a587eab86ad18713cc24071bc 100644 (file)
@@ -5,7 +5,9 @@
 FROM ubuntu:xenial
 MAINTAINER Ward Vandewege <ward@curoverse.com>
 
-# Install dependencies and set up system.
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install dependencies.
 RUN /usr/bin/apt-get update && /usr/bin/apt-get install -q -y python2.7-dev python3 python-setuptools python3-setuptools libcurl4-gnutls-dev libgnutls-dev curl git libattr1-dev libfuse-dev libpq-dev python-pip unzip tzdata
 
 # Install RVM
@@ -20,5 +22,8 @@ RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
 ADD generated/go1.8.3.linux-amd64.tar.gz /usr/local/
 RUN ln -s /usr/local/go/bin/go /usr/local/bin/
 
+# Old versions of setuptools cannot build a schema-salad package.
+RUN pip install --upgrade setuptools
+
 ENV WORKSPACE /arvados
 CMD ["/usr/local/rvm/bin/rvm-exec", "default", "bash", "/jenkins/run-build-packages.sh", "--target", "ubuntu1604"]
index 4dc9aa141d683506879d384cda5014fb4800e7e4..fd2f9e3d8cdbafced2990875f56ce72a2719672f 100644 (file)
@@ -3,7 +3,7 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM centos:7
-MAINTAINER Brett Smith <brett@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
 
 RUN yum -q -y install scl-utils centos-release-scl which tar
 
index 3a0b0e918ba6f5ed2b573fd0e0d461dc2ce0beb4..dcf581a1e036876cb85676e305b78eb399381448 100644 (file)
@@ -3,11 +3,13 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM debian:8
-MAINTAINER Peter Amstutz <peter.amstutz@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
+
+ENV DEBIAN_FRONTEND noninteractive
 
 # Install RVM
 RUN apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends curl ca-certificates && \
+    apt-get -y install --no-install-recommends curl ca-certificates && \
     gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
diff --git a/build/package-test-dockerfiles/debian9/D39DC0E3.asc b/build/package-test-dockerfiles/debian9/D39DC0E3.asc
new file mode 100644 (file)
index 0000000..231fbdd
--- /dev/null
@@ -0,0 +1,692 @@
+-----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
+RQIWiikpDFopKUelAB0ryzxFu1vxXNDGSwVxGMdgOD+ua9L1C7Fnp9xcn/llGWH1
+xxXE+DLLdI91McyO3X1rswz5IyqM8jMr1alOguup3Oj2EWnadFbxIFCqK0Vf3qGP
+g1IUJrz27u579OKhFRRMHWnF6r7HHSj5h1oTG0Pds1A4ycU4saruzFj1pNlJCPtH
+XFUrm2hnQo6Ag9sVM6uxpqoQcv0HSsy2lY8Y+IHhRdLuRe2qfuZDyAOhrY+EN7i5
+v7Jj96MSAfQ4/rXceJLOPUNJnikGRtJFeYeCQ+keO7dTxHKWj9sEcfrivRo3nSZ4
+OJ5aWJX3ntxNBGaM8ZNJmsDvCnA4ppOBSr05oAdTSCehp3egdaQwxx0oXmlPNC0w
+MLxZNjQZEx991UjPXnP9Ki0O2ECxLtClVHA6CtTV7VJbINMuUVw2fTFRWbots04O
+VY8EelaSqpUeVHFDDOWN9pLayNiLnGasBgvU15hqHj+5hvJYoIY/KQkBt3LYrNl+
+J8qEHy2Udw1cXOj2eRs9kDrTXkQ8V5Rb/E5JpY02qV43EHmuj03xTFqJAjbLdcU/
+aIapNnYYQ0AR461zF/rn2Ncvwvqax5/H1raMqyc5HJz0o50ynSaV2d40aYzVW4jG
+w4rjofHkFzIRGvy4zuLDApZPH1hFtWSVMdCcjj8qTkibNGnqEhWF0znjpXmN0wtd
+bimG4NFKGBHsc1341O21NfMgkDoRwVrz3WAU1p1zzu/Wu/AT1cTw88pe7Gouh7Wr
+CRFdfusAacR6CobLIsLcHIPlr/Kpj3rJ7nbB3imwwB9aKO2KXoKQx1Hc0gIYceuK
+WkMOcUo6UtJmmBkTXEpa8t5OYyxMftwOK4qHWdQ/tk+Gm067gtS7bbwblV1CltoO
+Opxjg5xn611mr3xi1G3tkUEyuAfp3P5Vb+yC4lt5N20wymQDHXKMuP8Ax7P4Vzc2
+8TtlT1U32PLdU2WDymS0Z33HYqpkmuZvdbv4wG/sdVB7OCT+QHvXvN5o0Nyd20Bx
+0OKxbvw/MxwGVvrGP51Cunqa2jJaOx4zBdedKgn06FHk5Uxg5/nXZ+GUe01GHbbz
+ziVWIiUqGGMc/MQMc+ueldInhFFkEsuxSDkAKM1P4Q0Ce38VX15PK8sUOUi3LgLu
+wcD6KF5/2qe7B+6tDO8ZTzHSXdtMvoQAMtI0RA+u1yf0ry65mtkYLKkszdBtcpn+
+dfQ/jSx+3+HLy1jQGWSFlQ4744rynTvC8l7plvcNHHM7rnDDBA7c+uOvvVNKLJi3
+NJHBrqOnLN5UunzhunMpbH4VejXTr1SYUYEdU3kGu0HhJGfJ0rcwPJ4B/OpY/BCm
+UObVYMHs2c1LmuhSptblLQZzpukvcxu6wQHdLHJhsL/EQQBjA579KNOvNN1rVxfK
+fPV5QsaEFAAoyzH17cV1FxoUdvpUttGgYTLtdW6FSQD+ma5u/sbKzuo47OOK23KV
+CxqFGMjPArSnV5Nepz1sOqrUbXVz0fS/EEWqapLY28X7qCIM0o6E5HArbzXIeCrQ
+W816/UuEwfb/AD/Kuv8AerpNtXZGJjGNS0ULj1opKU1ocw6koIyKO1IoXPpTQRmj
+gUnfNAjPvLZZLn7UOHhUqQR1zwCP1FS2uMBiakupl+zSRZAbg47nmsee++zoornl
+ZM9GF5JXN8yM3AbH4Zqvc3HlIWecAe6isT+1gI8lv1rm9T1aXULlbSBiWY4PPQVL
+qHRGjrrsdNb6gdVuzBby5C/eYDFdLZbEUAMCe5FcLL5uhacJLBVabbhgf4q5608Y
+ana+ZLcxbWJ6K3H60ou24VIKXw6Hq+pS5Bwa4hL6bTdTMFzs8t+YziuW1Xx7cy24
+SMNv9FNOsdRfV4A1zuAjGFz2z1olK+qHTgo6M9LimV0DbF6fwmkYxsT8pP1ri9K1
+p7aT7PKxO3hTnqK15dXXH3qXPcbpW2J9VlKwMEAAx0FcZf2TXV7ayIf3pymPbI5/
+nWzNqRugyL0xirVlYPd2jBGMZJwXA5x6Z7UlqZr3JHSeH4Fg01SCDuOM+oHH+Nap
+6VBawJa2sVug+WNQoqftXalZWPKqS5pNhmgmkx6UgAzTIJc+tHem5BpQBSGBBzxQ
+aUUNTEUry3ilQyMgLoCVboRXNX6GTdt7jNdc3PBHFcs58u4eGTqrFT/Q1z1kduFl
+0MDUDLHZkR5J6VDo1stvNvndPNbkjPSt94IyxRhlT61gatocl2ri1uGtps/LItcy
+0Z36yVjqmCSxBcjkd/WuU1rRDJGTCAWHUKetYFnpWsRytb6pfTBlYYmDfKQT+nFa
+z6C6Ws8qa0A8TEAhxjoCM+nWtHOzsxKCW7MgeHnRi0oXJPc1q2tvFZQshdeTyB6V
+WvdDEflmbWkZmVmJMg5x6Vyk1hqt5dJDZTSRx/xSliM0lLmdinFdDpLgGWTdG4BU
+8VYSSeS0VjkE0zR/D+x0SadpnH3nY9a3rm3iR44kXEaDpUS3Hdoz7SNojljyeTXY
+aJpeokQyyzQrZkB1Vcl2zzg8YH61yrZmu44IgN0jBF+pOBXp8aLFEka/dQBR9BW9
+GF9WcGIquLshcYNO7Yph60ZJrqOAdnPamUo601zhc5xQMzBr1uf4Jf8Av2aUa/bZ
++5N/3wa1/sdt6/8AjtIbK37MP++anUNDK/4SK1z9yb/vg0f8JFbY+5L/AN8GtQ2l
+t/eH/fNH2W19R/3zR7waGWdetM5xJ/3waw9avIZblLmDcMjEgKkfQ1132W1J+8P+
++ajubC0ntpYm2kMpHSpkm0aUp8srnGi7VwOOfWpoGLHGec1iBzDcPbt1UkCtiwAe
+ZW3Ae2a5Op6qlbULqSNP9am0f3sZx9axbiXSjIPPt9Pdh/G2M/ka66501LpfmyAa
+y5PBtnMN0hbJ9MCrXMaxqxSOamm0wcW8dlFzyY1GT+VPtdkjZjTIHG4962JPClpa
+NlGY5PGcVcisIbVBjkHnPtUyuVKomjOgH2bLSdevFZ9xeDc0h4z0qa/u1ZiiYxzX
+O311j5FOT0qLGLlc6HwxPbf219ru5VSOAEru7uen5DP6V3X/AAkOl97yL86860W/
+utNgjEOlJqKPukmTftlAGOUzwT7e1auo+KNAis7W/wDskkljckqk0aA7HHVHXghv
+5jmuum9LI83ERfNdnZf2/pZHF5Efxo/tzTyeLhPzrmtP1XwjehjHqlmhHJE2Yv8A
+0IDP4Vv2lhpF/Hvs5rO4QdWikVgPyrT3jn0JxrNiy5NzGPbdzVS91m1KMqXMYAwS
+xNW20Cz7RQ/pXP6vokC2o2qil5OCCPQ/4VMnKxUUrncKOelLt4zg4p5ULGXLAAcm
+mWWoQ3UX7p1kGSMocim5paFxpSaG7Qe1IAOeBx19q8+1P4iz6H41udLvIEnsEx80
+I+dBgc8nB69OK868UeJv7X8R3M9rNcixmA8uOU4KnA3AgEjrmqV3sQ0k3c9e1nx5
+oWjlkNz9pmGcpb4bH1boKQeINSlhtoRbRQXlwqTSggt9ljb7qnP3pGAJ7BQDxxz4
+lpaR3Otabay48ua7iR8/3S4B/SvV9O1Rr/xNqCSqVddQus577QI1/JVrOu+SJvg6
+Ptajvsk39xW1m1InNxGPmzzVaC+dIhIjZI6jFdJdQhlYYzmuU1K0ls5TNBnbnkdj
+XIjs3RtWvi2AAo/GOoParra5HIg2uB3zmuDmFrdncy7JPbiq7LcRf6ufIHTNXzMu
+MTurjXYoSWkmDccVzt/4qeZWSMfMRgAdhXPS+dJ/rZSaZujg6fM3rSuNouTXRVOS
+d3qaqW8bTzbjzzzSQwS3cnfHf2roLKyWJRxj8Km9hWHed/ZiWMynDLcA4/2cc/pX
+OpcJqGkeOIo8G1jmS8g9FbzduR9VYitDxBcqk6gt8sEZZh6f5xXOaIxtvh94iumG
+Ptlxb2iH1IJkb9APzrTDNuUjTMqSp4ei+ru/l0MMTFcZ65qzpN9e6XqY1CzupYZw
+do2twR6Edx7GswOS3TpVpD0Ga7zxD0bSvidqtoxGoJHeRMDycIwOOMEDBH4VatPH
+EmsXCW17bwRIWLI6ZGMBuDk+9eY7y0p54UfmasRzHJANRNFQ0Z7hZfEfTF0aO8up
+M3EgwIFOSG7gj/PUV5/a+NbnSTqsWlBYILucyIp5MQPZfTtXFJIUj445JpQ/7z68
+1MaKW5rLEN7aE97eyzXstzM7SySYLs3U1UuPnlQLx8vFK53TAdiMUw5Gz/ZO2trW
+MG23dkkNzLDLHKh2zRsHQ+jA5Feoalr2n6bqlp4hiObK+ZbiTaPuGQEP+T7vyryq
+YEHIPvXQeHbGLxHZPoctw0TyygW7kEhXYEqD7Ejn65rnxEOZL1O7L6nJKfnFnssd
+zDdwpNbyrJFINyuhyCKr3NsJoypGa8P0fxPq/hK8ltAweKNystvIcruBwcEdD9P1
+r07QviFo+sFIJPNtrlv+WboWBPsR/XFc86UkOnWi9yvqGj/vGZFx9DWHPbXUJIJY
+fUV6RcQJIm7gg9DWFd2KnPzY/Csrs6YtHEMs7NyGwfbFWrbT3lYFhWldNY2AMl1N
+sX12k/yFYt3470mzBSzhluHHfGxfzPP6U0pS2QpVIx3Z1FrZrEg6ADtWN4g8X2ek
+K0Fsy3F50CKeFPuf6VxGqeMtV1MNGsgtoTxsi4J+p61labD5t/EX5VTvOe+Oa1jR
+sryMfbe0moQ6nRaxdztZJC7mS6uCA+Op9f14q74q2aNpel+GEJ8yyQ3N7/18SAEr
+/wABXaPzqbwv5MU974tv4/NtNJ2+RD/z0nY/uwfQA5Yn2FcleTz311JcXEheeeQy
+SuerMTkmtqMOWIsyxKr1vd+GOi9EJGmRn1qyPlGT2qNBgClkYCPHrXQecKudnPU8
+0/fsHqe9IKhmbFTLYqL1P//ZiQIiBBIBCgAMBQJZQK6VBYMHhh+AAAoJEDbDVqWm
+6mZ2sKkP/iFl4nK/VGMkw9vnCiwJSx4ooQRv+aG1w70R41tfbsSijRQuC497au0I
+ihdxKRv8dzAUHyOp4ahmPee20kturZEz8p4zI61c707WoFfOypM/cm2yV0eyEr0/
+sHaPDkrgtaGYO32pKXCXg6coXUgQz7lPhpU8FABmWIETopBdv4Waf/5yyXblkqgd
+730g1NsVvdFN1+OrYQ/8UUFnf3iv3gE36HkrM7UWSXJL/yYmvF2MFqRQliFGT1O9
+ZPwyc/SOgZBVnBIH9SMKE7ty31y4a+M869opf8Xr192jK6RpbFkvV6mVCiU6te4p
+7bjXnYb3Z34WJNkppSdrH2oiw8W2dypRKvu4X6D6pRhBtG5FQKSCRn436PcY6Qz5
+2e3LFq2R62IrB92fHidZbeQKX0TFRtqyOG/wl4C7CEdJ31wwoS9Jj2kH66g+e4FI
+vE5LJM+Y+imyFerGfh3FIJ0nJYVf9NOtEl2uy5ZoTa3bVKJG4LU/qOLFh1oA7YN0
+zwvaS83WlDKv8jyGBXVQdgr6dhlYCINr8ZimGNbGbkOSSsMYxdg3Hl1dl5+Ok8ih
+O0/F7EbCNX8XSEHIQ+Ie/dees+KajHioghskAFHr5YE/S24YMfAkQtHhZWxPNr0q
+gql0OtaI2e1GG5FMqssWpBksCZykkl6eprfC0uWOymz0t3z400O8iQI3BBMBCAAh
+BQJYseahAhsDBQsJCAcCBhUICQoLAgQWAgMBAh4BAheAAAoJEDgEu4LTncDjYawP
+/0CafFAJq4OAlFl1bhb4BTiw3eAbRnifGpBUldTpxxp65UJyXtqA3wX3ggbhXMWn
+b5zMOh06HzVUFJ7Snse3cxAHgi9+qqueBI53Zx1btPlJNX/AIv6TlESRMZJXE3y2
+pSBdmwW/tiXr5fC5RJqpgUevKfq3eW33aUtybE48FqmLqDJjyAvAbF/jKHY0Zahz
+7iCVkWJwufoGxkcIMMLYh1R633DT2yDSti4xS8xKUYJsiiw2B2t//Ux6zoNh3gzB
+EeMoRDRFPuC3ibfdkwC9zMkbDMNSMSlSB8F0sn52k93uQcnR1jWoWc5nSz9x5m1I
+3xo0Zl0I5YNIkDINz0SL426fN9zBSPHPnXsoOXOnDSF5xGt1Wzg3Do/kErFzlKPE
+xqo5j+W5RbRznuZR+uQw0nvCNGS3kfASbAaXR5lkb0VJiIliLM5HzXKi6W662y/X
+X5iQDkWx7UjjmTx5x7r12RYA6mwLJWlpNQoK4kWDlllUn1QpC+wqjTKePPMM/1vy
+qlh14tRVurxWTZPAh6/2kzrb0a0noq4YBeCNjOU2Ri+1RXZJkthjn2eNsXEvLCp1
+04H7W0uHwDAofiOkX99T823H6dIUEllIyiX5DRyRimZYqC6ux5Qq8AKVJmAKY3+z
+61W+psmFqVXckSMu8fd03DXvVgx60WWgGkGj8hEcoBe3uQINBFRQBC8BEADRTWbS
+pwvuyjGkPQxCsWs2df7qxEbj+NvzETDY7yeyWN49zll0zz1pCYZ11BtzCSiZWTLe
+9Ngk+PJsP4t87gZMwhNgG4YvIEJ9BIcpkPDCMvMyMW6Y8J0rPPjGrscLAPnIQg8V
+TZmsTGeEBUXsQNHFigeBH/OL0Rwf4ydhUt4SfgldrcBCfS2EkJH4ULhUw5enhkfp
+5tt3k5l0C9Wa6+qWYMEdhnN6rBjXGfwksgZAh2VfW7BNbidtuaCn8ZqrSf4p0kC3
+YdClW0wcDSGfT+PpOE04waycxUMB7gH8wvI8phcMSHj7rRJPui6rXytS14AI0BKi
+nfXh0Tw9QwU6V8CxWwq0/SFwcD10Q8Y7gDLfevzptKISpN05ACNSqCf1WmBZn63F
+Wxk68r3JYrxFEYQvVWHMAOQB1doODgrv+84xTxhcn781SAxA/F1BOtMgf/wgiUXU
+Clxn7k4Cf27kLiZb471SM8GTjoi5IVg7WyvN3p9wI6KWy7IrrvuwZKxXvr65Q3iD
+liVP1j0236PaBGJtgznNhaHPyWkaDwBZW9MXQlejc+ixaeTceE9BqKvn+0Z1Dhx7
+WCzfBPgDkqSVLQP5O9kg9qjxkN2TkuQlWVVNJL+0reeQm7CSEyA5SjtZjOmnC9bc
+HDsnLlzPw8Qt9g9OIh8XUhr6inF3k6UrB55nqQARAQABiQREBBgBAgAPBQJUUAQv
+AhsCBQkCPCsAAikJEDgEu4LTncDjwV0gBBkBAgAGBQJUUAQvAAoJEOIGwp+/BP8X
+No8QALBbvo3Dv6Sr8osRYpaGz87Yn7Z5OTUNtO3lQOa1eq/1Fdp4AVJ9+WBqaLdc
+5bXr1xrOoaUu457zrUYB2Bo18VRHRv6hW6qhzDoY2zbUGCyQbrD2SPi94SJogwro
+qXcundbjxrl24mfowskY9RbC2wOx0RhxxapB+mMe2DNxSVeFSszsO6QayzOvXxrt
+FlhVqgn+9BK63mbnbBdRDo46clADCTt5LSl1CETzR0oswI4MQxVtoZJGyC3gVG6u
+kMuUJLfivbS6y9PDJaF0mIkZwf2iKgxfpinNNdvdipJlEstgBV98XK2Q3cD2Qpp/
+btrG0PssXpNuXKm8htKgPYoY64f49VSCzbPJF1IJSOqmo+NGlngZVPpAo3nSCNkD
+Y1osnvtKW7a5uddlVFGhpusWR8hP+YsvV8qIIuC+69cT8RBv59nSECVM5E6bst2Q
+2aLf3l2HOqzIQxq1lwZN1cuI/33mKDIWlms2GX/YzlOsAh6FBzPC4cBNq4BJOuX9
+NciBqDG2vHt+9jf95TypfC1KGCd+pPexy7WqUnsDynu/d3uo7Dh90hhlSUUCdwYS
+n8aOtMTU4t9WkM9JnV+I2g4hkElwCsH4zvJBGxRLpyNOk3FwmwQ/zTM+jJ2mwugm
+wru+rxdryBY1wJ4e7JxZpiS/f9BSj5xwJ9TlfkVT40CcaL7Ye30QAJs8s3ga3axf
+kM+nbUt8TureSPIOF39j6wB9zIq961qpfiKaYoOyy3zX1I/A2SSCJzzxjSwaZooI
+svvu2RiFZUC/1y9qKPpJ6GxKLVZ4H8xvXMGRkNSfRqtIigUkKOF5hmjwfx4jMfyH
+encW4lyrUUEfPymGF+meya5Qvm847HoVU3O24jGGHMZJpt55n30RyodyIl2xrG5h
+A/82Hhsi3i+/mQyVSesQa0GKOqAUp2CHJDgo437LXXuBQFrykFhrSMHqa6sM/YaH
+pMWtIaqLPuEmAmcPf2FpmY2cYIldlo8ImiKBF+yRch2d7dj4A/p5wBvRq09NYlCV
+m0RrmPNtfz0j8YCGx0dEPAmvvWw3P+H/cEETzOUtS1I5SxCJRJPXQOPiTx0Cg8ZK
+6sUTzT1qBAnPZ/0f3F0gIQKnWe75VHDxAvumVgt0KiWbLbY62KtpVYw2gpQxxAF8
+t4E42v5rmXmsv6dPS/qlTJrSiRZzU5l2m/shqK3fAJGrajuNhMlO2D9+utrtuz/j
+GPvb6mJyiQjoX+wEqSo55Fk9nk0UpWUMTn8Av9vcWTRxK54S6yktfzrZM4sOudIm
+wZuRff8GOW2oiAJntzaQ/OqnUFUWNTN94lCmYE4NdKlX/02/FAilRJdQ+XY7upNs
+8Cy0g9PT4+y3M3FyyATOaugHszu1OAq6iQREBBgBAgAPAhsCBQJYsb3rBQkIM/K8
+AinBXSAEGQECAAYFAlRQBC8ACgkQ4gbCn78E/xc2jxAAsFu+jcO/pKvyixFilobP
+ztiftnk5NQ207eVA5rV6r/UV2ngBUn35YGpot1zltevXGs6hpS7jnvOtRgHYGjXx
+VEdG/qFbqqHMOhjbNtQYLJBusPZI+L3hImiDCuipdy6d1uPGuXbiZ+jCyRj1FsLb
+A7HRGHHFqkH6Yx7YM3FJV4VKzOw7pBrLM69fGu0WWFWqCf70ErreZudsF1EOjjpy
+UAMJO3ktKXUIRPNHSizAjgxDFW2hkkbILeBUbq6Qy5Qkt+K9tLrL08MloXSYiRnB
+/aIqDF+mKc01292KkmUSy2AFX3xcrZDdwPZCmn9u2sbQ+yxek25cqbyG0qA9ihjr
+h/j1VILNs8kXUglI6qaj40aWeBlU+kCjedII2QNjWiye+0pbtrm512VUUaGm6xZH
+yE/5iy9Xyogi4L7r1xPxEG/n2dIQJUzkTpuy3ZDZot/eXYc6rMhDGrWXBk3Vy4j/
+feYoMhaWazYZf9jOU6wCHoUHM8LhwE2rgEk65f01yIGoMba8e372N/3lPKl8LUoY
+J36k97HLtapSewPKe793e6jsOH3SGGVJRQJ3BhKfxo60xNTi31aQz0mdX4jaDiGQ
+SXAKwfjO8kEbFEunI06TcXCbBD/NMz6MnabC6CbCu76vF2vIFjXAnh7snFmmJL9/
+0FKPnHAn1OV+RVPjQJxovtgJEDgEu4LTncDj8nYP/18SgUQ7yUTgc2DjwVOlrUFg
+L5TPWtqjiRPaiUC5GwW/aFRKGyD0BcUaFFxJP2qhR5zMdmPrbZq9jTzPqPbDfq7P
+in75pRLg0LIPjDWuygqhXIRECxSBsl5OA+zPsyoZEos0sZZI88OFAQR3jXwWANA6
+hlLYoXohND92bk8s7BcQr9XJqghk93X0TX2HMJxExLKfp1jFF6gFEWpeqx91DIhQ
+yKLStxOeFCPsf8dUccg7QrgHZ60wE8W9jbDQW7kwhOlQ27ClvKKW1H/ZQTg3eFFc
+4IuI86tAAb+AJNlr7CGyf5sQfzqMdPxy9ywvg3Kk9VEK7Y1mHGVcsvqAlfyOgOMJ
+c0fZ2U/1tDEKjk18KNxGsiIRrrwdYT9irwE0hmgWc3o4lxV6zFNJ1pxcmKkRhCMt
+hNa31KCKBiZDhZMQY5fSmUmNAFEsV92Z1o29BF3rK5FmH4FLueO+39aAoZiZO3W2
+jtrSelzq6ol5XlAOB7Ol7o6p+9M1gVx4OgYaR5k/9jDgeI47MMW81rBSxG0FQeRu
+0zlB2/o12Wf5RAz/laXISIUekapwlU5FYMjoP+ziAFZMN9QAbo0MLCuvj8FqX/B7
+NpeBB+VNeou2GdK5+DPbn2sHWp1rtayNVipqMhWlEyrmsFHvkDFDcGYRSKbuSCd7
+O+PKZpCkfwcZAXF25edquQENBFY3Xw0BCADsNcfIEJ23NJ9GmIyotqT6MUggt2CL
+TAON/ats4TeI4et851NdVI1GBMVChcmNhN36wY0OS2J+lSfozyDqksgRojMFs3Bj
+vn4xEdvS7UU6UdX3TTSiy2RamLdjJjd9QA9JznOXxY/q0kIW6A9gG58D/tKt43Ad
+QdglQ5XEMG5JY11rS9bNIrDk+QsNv8bUcN6ElFTWzOXMdFyqm0Im6ZWcxhlSdgbJ
+G2HE86L/DemLdNwOXetL4V2csWz+ZTY6+S6jbk2T2gSqUSel52voVU60k/HxMckY
+UBOaUsAxPY0R1/XV3nRfhZQVfGSfMpzvrggi5Whz1uZ8fVmyTrK5H3dfABEBAAGJ
+Ah8EGAECAAkFAlY3Xw0CGwwACgkQOAS7gtOdwOMgJg/9F2B+yY0bHwja0LHJ6+Wh
+QYyRdSmis5JlAL49EPEGFCptA5mECgUF48F84ZU4tlVs9sbzmKH8U9BVPU4EECHg
+f1HftrX+EoXf8n5BejdvKygFyzevoA2IEqH3u8WkDrOLXoNXDI2CcT4Uu25cQGJB
+6lvVr8ohUZ2Q3krZzugCZtDhSclG56/rk+2MhApT3yeOcs2ED1htvdnU7OIeAxwP
+8Rcu0f8kWUoGvJ3KlbmA6DoMrvlVRheJsKDLXcUV6XGKd2o5CDuKuNbPx5MIg4i5
+FwcAR8gbNGsNVOX29g57YWZnXj7rKF0Ab2Vmx9Ir/y0qoRoSfLgH7no6io2Wx30i
+8mfq2qd/6RwHi4/KimJJs9Xle9BQWb0kGU2FCnzmc8gAXoiGdzUTdvRxtHa2l8HO
+PlI78z0cklwWr1cdnUXU87zJisqGFKvbyTNBEJmItXswDEuQcbzIsIMyDM5REdes
+H1OpSJBc/A095yy/JuT7618R4T6BaWDD0oEvBtCds36Vd2jwtTRBb5cXvcfMc1Rf
+o5lNafpO//HcKgJ/Oa4zAmKaXURzi+rBrxO+CGVpXQex+4+cohxxZ6gTbgNxYmGA
+7v556HDxJBUyytvlzGiKqg97GKsHiISy4o5BLQPYFMpCNgB3rcb5RR0v5tGeeuhr
+aXOo2lPgP440izPGh5s38Oc=
+=8iHq
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/build/package-test-dockerfiles/debian9/Dockerfile b/build/package-test-dockerfiles/debian9/Dockerfile
new file mode 100644 (file)
index 0000000..330d7d8
--- /dev/null
@@ -0,0 +1,22 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+FROM debian:stretch
+MAINTAINER Nico Cesar <nico@curoverse.com>
+
+ENV DEBIAN_FRONTEND noninteractive
+
+# Install RVM
+COPY D39DC0E3.asc /tmp
+RUN apt-get update && \
+    apt-get -y install --no-install-recommends curl ca-certificates gpg procps && \
+    gpg --import /tmp/D39DC0E3.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
+
+# udev daemon can't start in a container, so don't try.
+RUN mkdir -p /etc/udev/disabled
+
+RUN echo "deb file:///arvados/packages/debian9/ /" >>/etc/apt/sources.list
index b1c14998fcb4da589195ba76efe3f5dedb597bed..75c0cea1ded4c54dbaa2cfc63a07ccf2cb48e621 100644 (file)
@@ -3,11 +3,13 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM ubuntu:precise
-MAINTAINER Peter Amstutz <peter.amstutz@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
+
+ENV DEBIAN_FRONTEND noninteractive
 
 # Install RVM
 RUN apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends curl ca-certificates g++ && \
+    apt-get -y install --no-install-recommends curl ca-certificates g++ && \
     gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
index 42d446feba28282243c99192bc0cf74d5721e391..8416847433e046e3f0c2f36f5c3734f06cd4d60a 100644 (file)
@@ -3,11 +3,13 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 FROM ubuntu:trusty
-MAINTAINER Peter Amstutz <peter.amstutz@curoverse.com>
+MAINTAINER Ward Vandewege <ward@curoverse.com>
+
+ENV DEBIAN_FRONTEND noninteractive
 
 # Install RVM
 RUN apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends curl ca-certificates && \
+    apt-get -y install --no-install-recommends curl ca-certificates && \
     gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
index 0580e3afbfd075dd5eb619225464b738d0c2dc3f..25d49dc5019112cc7d9cc6b251904780471faa79 100644 (file)
@@ -5,9 +5,11 @@
 FROM ubuntu:xenial
 MAINTAINER Ward Vandewege <ward@curoverse.com>
 
+ENV DEBIAN_FRONTEND noninteractive
+
 # Install RVM
 RUN apt-get update && \
-    DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends curl ca-certificates && \
+    apt-get -y install --no-install-recommends curl ca-certificates && \
     gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
     curl -L https://get.rvm.io | bash -s stable && \
     /usr/local/rvm/bin/rvm install 2.3 && \
diff --git a/build/package-testing/test-packages-debian9.sh b/build/package-testing/test-packages-debian9.sh
new file mode 100755 (executable)
index 0000000..b4ea35c
--- /dev/null
@@ -0,0 +1,40 @@
+#!/bin/bash
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+set -eu
+
+# Multiple .deb based distros symlink to this script, so extract the target
+# from the invocation path.
+target=$(echo $0 | sed 's/.*test-packages-\([^.]*\)\.sh.*/\1/')
+
+export ARV_PACKAGES_DIR="/arvados/packages/$target"
+
+dpkg-query --show > "$ARV_PACKAGES_DIR/$1.before"
+
+apt-get -qq update
+apt-get --assume-yes --allow-unauthenticated install "$1"
+
+dpkg-query --show > "$ARV_PACKAGES_DIR/$1.after"
+
+set +e
+diff "$ARV_PACKAGES_DIR/$1.before" "$ARV_PACKAGES_DIR/$1.after" > "$ARV_PACKAGES_DIR/$1.diff"
+set -e
+
+mkdir -p /tmp/opts
+cd /tmp/opts
+
+export ARV_PACKAGES_DIR="/arvados/packages/$target"
+
+dpkg-deb -x $(ls -t "$ARV_PACKAGES_DIR/$1"_*.deb | head -n1) .
+
+while read so && [ -n "$so" ]; do
+    echo
+    echo "== Packages dependencies for $so =="
+    ldd "$so" | awk '($3 ~ /^\//){print $3}' | sort -u | xargs dpkg -S | cut -d: -f1 | sort -u
+done <<EOF
+$(find -name '*.so')
+EOF
+
+exec /jenkins/package-testing/common-test-packages.sh "$1"
index a6f432b9e71eb8fe902252e1fd2728bb953f50ca..bd3975841bf40cbb674dce29b43960ac1a08a524 100755 (executable)
@@ -74,6 +74,9 @@ case "$TARGET" in
     debian8)
         FORMAT=deb
         ;;
+    debian9)
+        FORMAT=deb
+        ;;
     ubuntu1204)
         FORMAT=deb
         ;;
index a8e730c27f562f14210a28c2ce2567d8a06933aa..39c40934311c9786ba0ed7786818807d77086ab6 100755 (executable)
@@ -107,6 +107,9 @@ case "$TARGET" in
     debian8)
         FORMAT=deb
         ;;
+    debian9)
+        FORMAT=deb
+        ;;
     ubuntu1204)
         FORMAT=deb
         ;;
@@ -217,7 +220,7 @@ if [[ -z "$ONLY_BUILD" ]] || [[ "libarvados-perl" = "$ONLY_BUILD" ]] ; then
         make install INSTALLDIRS=perl >"$STDOUT_IF_DEBUG" && \
         fpm_build install/lib/=/usr/share libarvados-perl \
         "Curoverse, Inc." dir "$(version_from_git)" install/man/=/usr/share/man \
-        "$WORKSPACE/LICENSE-2.0.txt=/usr/share/doc/libarvados-perl/LICENSE-2.0.txt" && \
+        "$WORKSPACE/apache-2.0.txt=/usr/share/doc/libarvados-perl/apache-2.0.txt" && \
         mv --no-clobber libarvados-perl*.$FORMAT "$WORKSPACE/packages/$TARGET/"
   fi
 fi
@@ -409,7 +412,7 @@ fi
 saladversion=$(cat "$WORKSPACE/sdk/cwl/setup.py" | grep schema-salad== | sed "s/.*==\(.*\)'.*/\1/")
 test_package_presence python-schema-salad "$saladversion" python
 if [[ "$?" == "0" ]]; then
-  fpm_build schema_salad "" "" python $saladversion --depends "${PYTHON2_PKG_PREFIX}-lockfile >= 1:0.12.2-2"
+  fpm_build schema_salad "" "" python $saladversion --depends "${PYTHON2_PKG_PREFIX}-lockfile >= 1:0.12.2-2" --depends "${PYTHON2_PKG_PREFIX}-avro = 1.8.1-2"
 fi
 
 # And for cwltool we have the same problem as for schema_salad. Ward, 2016-03-17
index 00987a8efab0f7bc81da391beb4c661e69db4813..3952b36604102cf6f3fd78fceb95eac915d8c2d4 100755 (executable)
@@ -95,6 +95,7 @@ sdk/go/arvados
 sdk/go/arvadosclient
 sdk/go/dispatch
 sdk/go/keepclient
+sdk/go/health
 sdk/go/httpserver
 sdk/go/manifest
 sdk/go/blockdigest
@@ -774,6 +775,7 @@ gostuff=(
     sdk/go/arvadosclient
     sdk/go/blockdigest
     sdk/go/dispatch
+    sdk/go/health
     sdk/go/httpserver
     sdk/go/manifest
     sdk/go/streamer
index afca52cb73f1809d5f9f25cca69ba28be8becab1..5a92176e7f02fba11525190ccee511339819e1d2 100755 (executable)
@@ -189,7 +189,7 @@ if (($Job || $local_job)->{docker_image_locator}) {
   $cmd = [$docker_bin, 'ps', '-q'];
 }
 Log(undef, "Sanity check is `@$cmd`");
-my ($exited, $stdout, $stderr) = srun_sync(
+my ($exited, $stdout, $stderr, $tempfail) = srun_sync(
   ["srun", "--nodes=\Q$ENV{SLURM_NNODES}\E", "--ntasks-per-node=1"],
   $cmd,
   {label => "sanity check"});
@@ -397,7 +397,7 @@ if (!defined $no_clear_tmp) {
   # Find FUSE mounts under $CRUNCH_TMP and unmount them.  Then clean
   # up work directories crunch_tmp/work, crunch_tmp/opt,
   # crunch_tmp/src*.
-  my ($exited, $stdout, $stderr) = srun_sync(
+  my ($exited, $stdout, $stderr, $tempfail) = srun_sync(
     ["srun", "--nodelist=$nodelist", "-D", $ENV{'TMPDIR'}],
     ['bash', '-ec', q{
 arv-mount --unmount-timeout 10 --unmount-all ${CRUNCH_TMP}
@@ -405,7 +405,7 @@ rm -rf ${JOB_WORK} ${CRUNCH_INSTALL} ${CRUNCH_TMP}/task ${CRUNCH_TMP}/src* ${CRU
     }],
     {label => "clean work dirs"});
   if ($exited != 0) {
-    exit(EX_RETRY_UNLOCKED);
+    exit_retry_unlocked();
   }
 }
 
@@ -439,20 +439,23 @@ fi
 echo >&2 "image loaded successfully"
 };
 
-  my ($exited, $stdout, $stderr) = srun_sync(
+  my ($exited, $stdout, $stderr, $tempfail) = srun_sync(
     ["srun", "--nodelist=" . join(',', @node)],
     ["/bin/bash", "-o", "pipefail", "-ec", $docker_install_script],
     {label => "load docker image"});
   if ($exited != 0)
   {
-    exit(EX_RETRY_UNLOCKED);
+    exit_retry_unlocked();
   }
 
   # Determine whether this version of Docker supports memory+swap limits.
-  ($exited, $stdout, $stderr) = srun_sync(
+  ($exited, $stdout, $stderr, $tempfail) = srun_sync(
     ["srun", "--nodes=1"],
     [$docker_bin, 'run', '--help'],
     {label => "check --memory-swap feature"});
+  if ($tempfail) {
+    exit_retry_unlocked();
+  }
   $docker_limitmem = ($stdout =~ /--memory-swap/);
 
   # Find a non-root Docker user to use.
@@ -472,7 +475,7 @@ echo >&2 "image loaded successfully"
       $label = "check whether user '$try_user' is UID 0";
       $try_user_arg = "--user=$try_user";
     }
-    my ($exited, $stdout, $stderr) = srun_sync(
+    my ($exited, $stdout, $stderr, $tempfail) = srun_sync(
       ["srun", "--nodes=1"],
       ["/bin/sh", "-ec",
        "$docker_bin run $docker_run_args $try_user_arg $docker_hash id --user"],
@@ -486,6 +489,8 @@ echo >&2 "image loaded successfully"
         Log(undef, "Container will run with $dockeruserarg");
       }
       last;
+    } elsif ($tempfail) {
+      exit_retry_unlocked();
     }
   }
 
@@ -678,11 +683,14 @@ else {
                     "mkdir -p $ENV{CRUNCH_INSTALL} && cd $ENV{CRUNCH_TMP} && perl -");
 
     $ENV{"CRUNCH_GIT_ARCHIVE_HASH"} = md5_hex($git_archive);
-    my ($stdout, $stderr);
-    ($exited, $stdout, $stderr) = srun_sync(
+    my ($stdout, $stderr, $tempfail);
+    ($exited, $stdout, $stderr, $tempfail) = srun_sync(
       \@srunargs, \@execargs,
       {label => "run install script on all workers"},
-      $build_script . $git_archive);
+        $build_script . $git_archive);
+    if ($tempfail) {
+      exit_retry_unlocked();
+    }
 
     my $stderr_anything_from_script = 0;
     for my $line (split(/\n/, $stderr)) {
@@ -1117,7 +1125,7 @@ if (!defined $main::success)
   } elsif ($working_slot_count < 1) {
     save_output_collection();
     save_meta();
-    exit(EX_RETRY_UNLOCKED);
+    exit_retry_unlocked();
   } elsif ($thisround_succeeded == 0 &&
            ($thisround_failed == 0 || $thisround_failed > 4)) {
     my $message = "stop because $thisround_failed tasks failed and none succeeded";
@@ -2044,7 +2052,7 @@ sub srun_sync
   if ($main::please_freeze || $j->{tempfail}) {
     $exited ||= 255;
   }
-  return ($exited, $j->{stdout_captured}, $j->{stderr_captured});
+  return ($exited, $j->{stdout_captured}, $j->{stderr_captured}, $j->{tempfail});
 }
 
 
@@ -2132,6 +2140,11 @@ sub find_docker_image {
   }
 }
 
+sub exit_retry_unlocked {
+  Log(undef, "Transient failure with lock acquired; asking for re-dispatch by exiting ".EX_RETRY_UNLOCKED);
+  exit(EX_RETRY_UNLOCKED);
+}
+
 sub retry_count {
   # Calculate the number of times an operation should be retried,
   # assuming exponential backoff, and that we're willing to retry as
index 6d6e9e1f5e56715de546d4b3eed2d1cc09881ca6..c1465d8d69feb447b5d521dad70e7d0c262e9182 100644 (file)
@@ -96,7 +96,7 @@ class TestCrunchJob < Minitest::Test
       tryjobrecord j, binstubs: ['clean_fail']
     end
     assert_match /Failing mount stub was called/, err
-    assert_match /clean work dirs: exit 44\n$/, err
+    assert_match /clean work dirs: exit 44\n.*Transient failure.* exiting 93\n(.*arv_put.*INFO.*\n)?$/, err
     assert_equal SPECIAL_EXIT[:EX_RETRY_UNLOCKED], $?.exitstatus
   end
 
index 52009dadabbb5828811a551c247a3e4ddc126b27..695597f839c1ceee5ccbb67f3391218d3d1a8e34 100644 (file)
@@ -426,12 +426,6 @@ class ArvCwlRunner(object):
             if self.work_api == "containers":
                 if tool.tool["class"] == "CommandLineTool" and kwargs.get("wait"):
                     kwargs["runnerjob"] = tool.tool["id"]
-                    upload_dependencies(self,
-                                        kwargs["name"],
-                                        tool.doc_loader,
-                                        tool.tool,
-                                        tool.tool["id"],
-                                        False)
                     runnerjob = tool.job(job_order,
                                          self.output_callback,
                                          **kwargs).next()
@@ -756,6 +750,7 @@ def main(args, stdout, stderr, api_client=None, keep_client=None):
     arvargs.use_container = True
     arvargs.relax_path_checks = True
     arvargs.validate = None
+    arvargs.print_supported_versions = False
 
     make_fs_access = partial(CollectionFsAccess,
                            collection_cache=runner.collection_cache)
index 8ab86c173f42ba16cc28699cba9495ec48e8281b..35a068f91be7b28b7eebd0f32538e9222367d8a1 100644 (file)
@@ -15,7 +15,7 @@ class ArvadosCommandTool(CommandLineTool):
         self.arvrunner = arvrunner
         self.work_api = kwargs["work_api"]
 
-    def makeJobRunner(self):
+    def makeJobRunner(self, use_container=True):
         if self.work_api == "containers":
             return ArvadosContainer(self.arvrunner)
         elif self.work_api == "jobs":
index 5582d63cfc6362acd0477f7b5508346772d1d6d3..93e2819084601d784a973c33ca30bbbdb2d6db49 100644 (file)
@@ -95,7 +95,7 @@ class CollectionFsAccess(cwltool.stdfsaccess.StdFsAccess):
 
     def exists(self, fn):
         collection, rest = self.get_collection(fn)
-        if collection:
+        if collection is not None:
             if rest:
                 return collection.exists(rest)
             else:
index 683f548c48cc835cd5ff7664cd0306cb0ea7f17e..087fed3e16e72cb26c95500b4ccb03a83bf71806 100644 (file)
@@ -18,7 +18,7 @@ from cwltool.draft2tool import CommandLineTool
 import cwltool.workflow
 from cwltool.process import get_feature, scandeps, UnsupportedRequirement, normalizeFilesDirs, shortname
 from cwltool.load_tool import fetch_document
-from cwltool.pathmapper import adjustFileObjs, adjustDirObjs
+from cwltool.pathmapper import adjustFileObjs, adjustDirObjs, visit_class
 from cwltool.utils import aslist
 from cwltool.builder import substitute
 from cwltool.pack import pack
@@ -46,6 +46,17 @@ def trim_anonymous_location(obj):
     if obj.get("location", "").startswith("_:"):
         del obj["location"]
 
+def find_defaults(d, op):
+    if isinstance(d, list):
+        for i in d:
+            find_defaults(i, op)
+    elif isinstance(d, dict):
+        if "default" in d:
+            op(d)
+        else:
+            for i in d.itervalues():
+                find_defaults(i, op)
+
 def upload_dependencies(arvrunner, name, document_loader,
                         workflowobj, uri, loadref_run, include_primary=True):
     """Upload the dependencies of the workflowobj document to Keep.
@@ -101,6 +112,23 @@ def upload_dependencies(arvrunner, name, document_loader,
         for s in workflowobj["$schemas"]:
             sc.append({"class": "File", "location": s})
 
+    def capture_default(obj):
+        remove = [False]
+        def add_default(f):
+            if "location" not in f and "path" in f:
+                f["location"] = f["path"]
+                del f["path"]
+            if "location" in f and not arvrunner.fs_access.exists(f["location"]):
+                # Remove from sc
+                sc[:] = [x for x in sc if x["location"] != f["location"]]
+                # Delete "default" from workflowobj
+                remove[0] = True
+        visit_class(obj["default"], ("File", "Directory"), add_default)
+        if remove[0]:
+            del obj["default"]
+
+    find_defaults(workflowobj, capture_default)
+
     mapper = ArvPathMapper(arvrunner, sc, "",
                            "keep:%s",
                            "keep:%s/%s",
index 55987ca1fc1796fb5f84215bdf3d60b5bdf00544..db11705f4ed4b99d7bfd03dec59a2d6b3de20e3d 100644 (file)
@@ -46,18 +46,18 @@ setup(name='arvados-cwl-runner',
       package_data={'arvados_cwl': ['arv-cwl-schema.yml']},
       scripts=[
           'bin/cwl-runner',
-          'bin/arvados-cwl-runner'
+          'bin/arvados-cwl-runner',
       ],
       # Note that arvados/build/run-build-packages.sh looks at this
       # file to determine what version of cwltool and schema-salad to build.
       install_requires=[
-          'cwltool==1.0.20170525215327',
-          'schema-salad==2.5.20170428142041',
+          'cwltool==1.0.20170707200431',
+          'schema-salad==2.6.20170630075932',
           'typing==3.5.3.0',
           'ruamel.yaml==0.13.7',
           'arvados-python-client>=0.1.20170526013812',
           'setuptools',
-          'ciso8601'
+          'ciso8601',
       ],
       data_files=[
           ('share/doc/arvados-cwl-runner', ['LICENSE-2.0.txt', 'README.rst']),
index 15cdc1e7154ea093157c6e98517cf37fb6829478..3d6b91536a2f2732ad2e97d5c172bf41e2ee60e7 100644 (file)
@@ -79,7 +79,12 @@ def stubs(func):
                 if uuid in (v["uuid"], v["portable_data_hash"]):
                     return CollectionExecute(v)
 
-        created_collections = {}
+        created_collections = {
+            "99999999999999999999999999999998+99": {
+                "uuid": "",
+                "portable_data_hash": "99999999999999999999999999999998+99",
+                "manifest_text": ". 99999999999999999999999999999998+99 0:0:file1.txt"
+            }}
         stubs.api.collections().create.side_effect = functools.partial(collection_createstub, created_collections)
         stubs.api.collections().get.side_effect = functools.partial(collection_getstub, created_collections)
 
@@ -142,10 +147,22 @@ def stubs(func):
                     'runtime_constraints': {'docker_image': 'arvados/jobs:'+arvados_cwl.__version__, 'min_ram_mb_per_node': 1024},
                     'script_parameters': {
                         'y': {"value": {'basename': '99999999999999999999999999999998+99', 'location': 'keep:99999999999999999999999999999998+99', 'class': 'Directory'}},
-                        'x': {"value": {'basename': 'blorp.txt', 'class': 'File', 'location': 'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt'}},
+                        'x': {"value": {
+                            'basename': 'blorp.txt',
+                            'class': 'File',
+                            'location': 'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt',
+                            "nameext": ".txt",
+                            "nameroot": "blorp"
+                        }},
                         'z': {"value": {'basename': 'anonymous', 'class': 'Directory',
                               'listing': [
-                                  {'basename': 'renamed.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999998+99/file1.txt'}
+                                  {
+                                      'basename': 'renamed.txt',
+                                      'class': 'File', 'location':
+                                      'keep:99999999999999999999999999999998+99/file1.txt',
+                                      "nameext": ".txt",
+                                      "nameroot": "renamed"
+                                  }
                               ]}},
                         'cwl:tool': '3fffdeaa75e018172e1b583425f4ebff+60/workflow.cwl#main',
                         'arv:enable_reuse': True,
@@ -191,10 +208,24 @@ def stubs(func):
                 '/var/lib/cwl/cwl.input.json': {
                     'kind': 'json',
                     'content': {
-                        'y': {'basename': '99999999999999999999999999999998+99', 'location': 'keep:99999999999999999999999999999998+99', 'class': 'Directory'},
-                        'x': {'basename': u'blorp.txt', 'class': 'File', 'location': u'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt'},
+                        'y': {
+                            'basename': '99999999999999999999999999999998+99',
+                            'location': 'keep:99999999999999999999999999999998+99',
+                            'class': 'Directory'},
+                        'x': {
+                            'basename': u'blorp.txt',
+                            'class': 'File',
+                            'location': u'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt',
+                            "nameext": ".txt",
+                            "nameroot": "blorp"
+                        },
                         'z': {'basename': 'anonymous', 'class': 'Directory', 'listing': [
-                            {'basename': 'renamed.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999998+99/file1.txt'}
+                            {'basename': 'renamed.txt',
+                             'class': 'File',
+                             'location': 'keep:99999999999999999999999999999998+99/file1.txt',
+                             "nameext": ".txt",
+                             "nameroot": "renamed"
+                            }
                         ]}
                     },
                     'kind': 'json'
index 561daf40ff186841dd28645373877aac89ba7d63..32a9255e90d442a475394036ea54dd55a797e6fa 100644 (file)
@@ -24,7 +24,7 @@ $graph:
   - id: '#main/x'
     type: File
     default: {class: File, location: 'keep:169f39d466a5438ac4a90e779bf750c7+53/blorp.txt',
-      basename: blorp.txt}
+      basename: blorp.txt, nameroot: blorp, nameext: .txt}
   - id: '#main/y'
     type: Directory
     default: {class: Directory, location: 'keep:99999999999999999999999999999998+99',
@@ -32,7 +32,8 @@ $graph:
   - id: '#main/z'
     type: Directory
     default: {class: Directory, basename: anonymous, listing: [{basename: renamed.txt,
-          class: File, location: 'keep:99999999999999999999999999999998+99/file1.txt'}]}
+          class: File, location: 'keep:99999999999999999999999999999998+99/file1.txt',
+          nameroot: renamed, nameext: .txt}]}
   outputs: []
   steps:
   - id: '#main/step1'
diff --git a/sdk/go/health/handler.go b/sdk/go/health/handler.go
new file mode 100644 (file)
index 0000000..f19511e
--- /dev/null
@@ -0,0 +1,109 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package health
+
+import (
+       "encoding/json"
+       "errors"
+       "net/http"
+       "strings"
+       "sync"
+)
+
+// Func is a health-check function: it returns nil when healthy, an
+// error when not.
+type Func func() error
+
+// Routes is a map of URI path to health-check function.
+type Routes map[string]Func
+
+// Handler is an http.Handler that responds to authenticated
+// health-check requests with JSON responses like {"health":"OK"} or
+// {"health":"ERROR","error":"error text"}.
+//
+// Fields of a Handler should not be changed after the Handler is
+// first used.
+type Handler struct {
+       setupOnce sync.Once
+       mux       *http.ServeMux
+
+       // Authentication token. If empty, all requests will return 404.
+       Token string
+
+       // Route prefix, typically "/_health/".
+       Prefix string
+
+       // Map of URI paths to health-check Func. The prefix is
+       // omitted: Routes["foo"] is the health check invoked by a
+       // request to "{Prefix}/foo".
+       //
+       // If "ping" is not listed here, it will be added
+       // automatically and will always return a "healthy" response.
+       Routes Routes
+
+       // If non-nil, Log is called after handling each request. The
+       // error argument is nil if the request was succesfully
+       // authenticated and served, even if the health check itself
+       // failed.
+       Log func(*http.Request, error)
+}
+
+// ServeHTTP implements http.Handler.
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+       h.setupOnce.Do(h.setup)
+       h.mux.ServeHTTP(w, r)
+}
+
+func (h *Handler) setup() {
+       h.mux = http.NewServeMux()
+       prefix := h.Prefix
+       if !strings.HasSuffix(prefix, "/") {
+               prefix = prefix + "/"
+       }
+       for name, fn := range h.Routes {
+               h.mux.Handle(prefix+name, h.healthJSON(fn))
+       }
+       if _, ok := h.Routes["ping"]; !ok {
+               h.mux.Handle(prefix+"ping", h.healthJSON(func() error { return nil }))
+       }
+}
+
+var (
+       healthyBody     = []byte(`{"health":"OK"}` + "\n")
+       errNotFound     = errors.New(http.StatusText(http.StatusNotFound))
+       errUnauthorized = errors.New(http.StatusText(http.StatusUnauthorized))
+       errForbidden    = errors.New(http.StatusText(http.StatusForbidden))
+)
+
+func (h *Handler) healthJSON(fn Func) http.Handler {
+       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+               var err error
+               defer func() {
+                       if h.Log != nil {
+                               h.Log(r, err)
+                       }
+               }()
+               if h.Token == "" {
+                       http.Error(w, "disabled", http.StatusNotFound)
+                       err = errNotFound
+               } else if ah := r.Header.Get("Authorization"); ah == "" {
+                       http.Error(w, "authorization required", http.StatusUnauthorized)
+                       err = errUnauthorized
+               } else if ah != "Bearer "+h.Token {
+                       http.Error(w, "authorization error", http.StatusForbidden)
+                       err = errForbidden
+               } else if err = fn(); err == nil {
+                       w.Header().Set("Content-Type", "application/json")
+                       w.Write(healthyBody)
+               } else {
+                       w.Header().Set("Content-Type", "application/json")
+                       enc := json.NewEncoder(w)
+                       err = enc.Encode(map[string]string{
+                               "health": "ERROR",
+                               "error":  err.Error(),
+                       })
+               }
+       })
+}
diff --git a/sdk/go/health/handler_test.go b/sdk/go/health/handler_test.go
new file mode 100644 (file)
index 0000000..c9f6a0b
--- /dev/null
@@ -0,0 +1,137 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package health
+
+import (
+       "encoding/json"
+       "errors"
+       "net/http"
+       "net/http/httptest"
+       "net/url"
+       "testing"
+
+       check "gopkg.in/check.v1"
+)
+
+// Gocheck boilerplate
+var _ = check.Suite(&Suite{})
+
+func Test(t *testing.T) {
+       check.TestingT(t)
+}
+
+type Suite struct{}
+
+const (
+       goodToken = "supersecret"
+       badToken  = "pwn"
+)
+
+func (s *Suite) TestPassFailRefuse(c *check.C) {
+       h := &Handler{
+               Token:  goodToken,
+               Prefix: "/_health/",
+               Routes: Routes{
+                       "success": func() error { return nil },
+                       "miracle": func() error { return errors.New("unimplemented") },
+               },
+       }
+
+       resp := httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/_health/ping", goodToken))
+       s.checkHealthy(c, resp)
+
+       resp = httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/_health/success", goodToken))
+       s.checkHealthy(c, resp)
+
+       resp = httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/_health/miracle", goodToken))
+       s.checkUnhealthy(c, resp)
+
+       resp = httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/_health/miracle", badToken))
+       c.Check(resp.Code, check.Equals, http.StatusForbidden)
+
+       resp = httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/_health/miracle", ""))
+       c.Check(resp.Code, check.Equals, http.StatusUnauthorized)
+
+       resp = httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/_health/theperthcountyconspiracy", ""))
+       c.Check(resp.Code, check.Equals, http.StatusNotFound)
+
+       resp = httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/x/miracle", ""))
+       c.Check(resp.Code, check.Equals, http.StatusNotFound)
+
+       resp = httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/miracle", ""))
+       c.Check(resp.Code, check.Equals, http.StatusNotFound)
+}
+
+func (s *Suite) TestPingOverride(c *check.C) {
+       var ok bool
+       h := &Handler{
+               Token: goodToken,
+               Routes: Routes{
+                       "ping": func() error {
+                               ok = !ok
+                               if ok {
+                                       return nil
+                               } else {
+                                       return errors.New("good error")
+                               }
+                       },
+               },
+       }
+       resp := httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/ping", goodToken))
+       s.checkHealthy(c, resp)
+
+       resp = httptest.NewRecorder()
+       h.ServeHTTP(resp, s.request("/ping", goodToken))
+       s.checkUnhealthy(c, resp)
+}
+
+func (s *Suite) TestZeroValueIsDisabled(c *check.C) {
+       resp := httptest.NewRecorder()
+       (&Handler{}).ServeHTTP(resp, s.request("/ping", goodToken))
+       c.Check(resp.Code, check.Equals, http.StatusNotFound)
+
+       resp = httptest.NewRecorder()
+       (&Handler{}).ServeHTTP(resp, s.request("/ping", ""))
+       c.Check(resp.Code, check.Equals, http.StatusNotFound)
+}
+
+func (s *Suite) request(path, token string) *http.Request {
+       u, _ := url.Parse("http://foo.local" + path)
+       req := &http.Request{
+               Method:     "GET",
+               Host:       u.Host,
+               URL:        u,
+               RequestURI: u.RequestURI(),
+       }
+       if token != "" {
+               req.Header = http.Header{
+                       "Authorization": {"Bearer " + token},
+               }
+       }
+       return req
+}
+
+func (s *Suite) checkHealthy(c *check.C, resp *httptest.ResponseRecorder) {
+       c.Check(resp.Code, check.Equals, http.StatusOK)
+       c.Check(resp.Body.String(), check.Equals, `{"health":"OK"}`+"\n")
+}
+
+func (s *Suite) checkUnhealthy(c *check.C, resp *httptest.ResponseRecorder) {
+       c.Check(resp.Code, check.Equals, http.StatusOK)
+       var result map[string]interface{}
+       err := json.Unmarshal(resp.Body.Bytes(), &result)
+       c.Assert(err, check.IsNil)
+       c.Check(result["health"], check.Equals, "ERROR")
+       c.Check(result["error"].(string), check.Not(check.Equals), "")
+}
index 68f63b1c261bcfcfde9dd6b42cbe6ca8a67e15ab..afd9bdcd89d07b2b4447b059a33b49ccbe483a6c 100644 (file)
@@ -193,6 +193,11 @@ Display machine-readable progress on stderr (bytes and, if known,
 total data size).
 """)
 
+_group.add_argument('--silent', action='store_true',
+                    help="""
+Do not print any debug messages to console. (Any error messages will still be displayed.)
+""")
+
 _group = run_opts.add_mutually_exclusive_group()
 _group.add_argument('--resume', action='store_true', default=True,
                     help="""
@@ -243,7 +248,7 @@ def parse_arguments(arguments):
     """)
 
     # Turn on --progress by default if stderr is a tty.
-    if (not (args.batch_progress or args.no_progress)
+    if (not (args.batch_progress or args.no_progress or args.silent)
         and os.isatty(sys.stderr.fileno())):
         args.progress = True
 
@@ -964,9 +969,12 @@ def desired_project_uuid(api_client, project_uuid, num_retries):
 def main(arguments=None, stdout=sys.stdout, stderr=sys.stderr):
     global api_client
 
-    logger = logging.getLogger('arvados.arv_put')
-    logger.setLevel(logging.INFO)
     args = parse_arguments(arguments)
+    logger = logging.getLogger('arvados.arv_put')
+    if args.silent:
+        logger.setLevel(logging.WARNING)
+    else:
+        logger.setLevel(logging.INFO)
     status = 0
     if api_client is None:
         api_client = arvados.api('v1')
@@ -1135,7 +1143,7 @@ def main(arguments=None, stdout=sys.stdout, stderr=sys.stderr):
     # Print the locator (uuid) of the new collection.
     if output is None:
         status = status or 1
-    else:
+    elif not args.silent:
         stdout.write(output)
         if not output.endswith('\n'):
             stdout.write('\n')
index 2af649c65899e8a5ae7c7eb7eaa0f5a07ff285af..fc6b846c39f2ee4737b91b9638af576121297103 100644 (file)
@@ -5,6 +5,7 @@
 from __future__ import absolute_import
 from future.utils import listitems
 import io
+import mock
 import os
 import re
 import shutil
@@ -16,9 +17,11 @@ import arvados.commands.get as arv_get
 from . import run_test_server
 
 from . import arvados_testutil as tutil
+from .arvados_testutil import ArvadosBaseTestCase
 
 class ArvadosGetTestCase(run_test_server.TestCaseWithServers,
-                         tutil.VersionChecker):
+                         tutil.VersionChecker,
+                         ArvadosBaseTestCase):
     MAIN_SERVER = {}
     KEEP_SERVER = {}
 
@@ -100,7 +103,8 @@ class ArvadosGetTestCase(run_test_server.TestCaseWithServers,
             self.assertEqual(m_from_collection, m_from_file)
 
     def test_get_collection_stripped_manifest(self):
-        col_loc, col_pdh, col_manifest = self.write_test_collection(strip_manifest=True)
+        col_loc, col_pdh, col_manifest = self.write_test_collection(
+            strip_manifest=True)
         # Get the collection manifest by UUID
         r = self.run_get(['--strip-manifest', col_loc, self.tempdir])
         self.assertEqual(0, r)
@@ -138,3 +142,38 @@ class ArvadosGetTestCase(run_test_server.TestCaseWithServers,
         with open(os.path.join(self.tempdir, "foo.txt"), "r") as f:
             self.assertEqual("another foo", f.read())
 
+    def test_no_progress_when_stderr_not_a_tty(self):
+        # Create a collection with a big file (>64MB) to force the progress
+        # to be printed
+        c = collection.Collection()
+        with c.open('bigfile.txt', 'wb') as f:
+            for _ in range(65):
+                f.write("x" * 1024 * 1024)
+        c.save_new()
+        tmpdir = self.make_tmpdir()
+        # Simulate a TTY stderr
+        stderr = mock.MagicMock()
+        stdout = tutil.BytesIO()
+
+        # Confirm that progress is written to stderr when is a tty
+        stderr.isatty.return_value = True
+        r = arv_get.main(['{}/bigfile.txt'.format(c.manifest_locator()),
+                          '{}/bigfile.txt'.format(tmpdir)],
+                         stdout, stderr)
+        self.assertEqual(0, r)
+        self.assertEqual(b'', stdout.getvalue())
+        self.assertTrue(stderr.write.called)
+
+        # Clean up and reset stderr mock
+        os.remove('{}/bigfile.txt'.format(tmpdir))
+        stderr = mock.MagicMock()
+        stdout = tutil.BytesIO()
+
+        # Confirm that progress is not written to stderr when isn't a tty
+        stderr.isatty.return_value = False
+        r = arv_get.main(['{}/bigfile.txt'.format(c.manifest_locator()),
+                          '{}/bigfile.txt'.format(tmpdir)],
+                         stdout, stderr)
+        self.assertEqual(0, r)
+        self.assertEqual(b'', stdout.getvalue())
+        self.assertFalse(stderr.write.called)
index 491fa86393e8aaba00bb6c1ae86f3dfe40e72038..635c6254ad73f9c8d8178634c0ce33c0b038b0d0 100644 (file)
@@ -89,6 +89,9 @@ class ArvLsTestCase(run_test_server.TestCaseWithServers, tutil.VersionChecker):
         self.assertEqual(1, error_mock.call_count)
 
     def test_version_argument(self):
+        if sys.version_info >= (3, 0):
+            import warnings
+            warnings.simplefilter("ignore")
         with redirected_streams(stdout=StringIO, stderr=StringIO) as (out, err):
             with self.assertRaises(SystemExit):
                 self.run_ls(['--version'], None)
index b8065ef3aaea36cc2b65435bc965163663ce8f73..ce6618132d011056c13580c9497abf4072cc7dc3 100644 (file)
@@ -170,7 +170,7 @@ class ArvadosPutResumeCacheTest(ArvadosBaseTestCase):
         self.last_cache.close()
         resume_cache = arv_put.ResumeCache(self.last_cache.filename)
         self.assertNotEqual(None, resume_cache)
-        self.assertRaises(None, resume_cache.check_cache())
+        resume_cache.check_cache()
 
     def test_basic_cache_storage(self):
         thing = ['test', 'list']
@@ -969,6 +969,32 @@ class ArvPutIntegrationTest(run_test_server.TestCaseWithServers,
                          r'^\./%s.*:file2.txt' % os.path.basename(tmpdir))
         self.assertRegex(c['manifest_text'], r'^.*:file3.txt')
 
+    def test_silent_mode_no_errors(self):
+        self.authorize_with('active')
+        tmpdir = self.make_tmpdir()
+        with open(os.path.join(tmpdir, 'test.txt'), 'w') as f:
+            f.write('hello world')
+        pipe = subprocess.Popen(
+            [sys.executable, arv_put.__file__] + ['--silent', tmpdir],
+            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE, env=self.ENVIRON)
+        stdout, stderr = pipe.communicate()
+        # No console output should occur on normal operations
+        self.assertNotRegex(stderr.decode(), r'.+')
+        self.assertNotRegex(stdout.decode(), r'.+')
+
+    def test_silent_mode_does_not_avoid_error_messages(self):
+        self.authorize_with('active')
+        pipe = subprocess.Popen(
+            [sys.executable, arv_put.__file__] + ['--silent',
+                                                  '/path/not/existant'],
+            stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+            stderr=subprocess.PIPE, env=self.ENVIRON)
+        stdout, stderr = pipe.communicate()
+        # Error message should be displayed when errors happen
+        self.assertRegex(stderr.decode(), r'.*ERROR:.*')
+        self.assertNotRegex(stdout.decode(), r'.+')
+
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/services/api/app/controllers/arvados/v1/healthcheck_controller.rb b/services/api/app/controllers/arvados/v1/healthcheck_controller.rb
new file mode 100644 (file)
index 0000000..6d55506
--- /dev/null
@@ -0,0 +1,35 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+class Arvados::V1::HealthcheckController < ApplicationController
+  skip_before_filter :catch_redirect_hint
+  skip_before_filter :find_objects_for_index
+  skip_before_filter :find_object_by_uuid
+  skip_before_filter :load_filters_param
+  skip_before_filter :load_limit_offset_order_params
+  skip_before_filter :load_read_auths
+  skip_before_filter :load_where_param
+  skip_before_filter :render_404_if_no_object
+  skip_before_filter :require_auth_scope
+
+  before_filter :check_auth_header
+
+  def check_auth_header
+    mgmt_token = Rails.configuration.ManagementToken
+    auth_header = request.headers['Authorization']
+
+    if !mgmt_token
+      send_json ({"errors" => "disabled"}), status: 404
+    elsif !auth_header
+      send_json ({"errors" => "authorization required"}), status: 401
+    elsif auth_header != 'Bearer '+mgmt_token
+      send_json ({"errors" => "authorization error"}), status: 403
+    end
+  end
+
+  def ping
+    resp = {"health" => "OK"}
+    send_json resp
+  end
+end
index e8ab0cbf7117e0e25139cf403db566ca86d2e72f..94e4e1f9ddd4289bc8bbe23c0f61d54d06213a18 100644 (file)
@@ -28,6 +28,7 @@ class ContainerRequest < ArvadosModel
   after_save :update_priority
   after_save :finalize_if_needed
   before_create :set_requesting_container_uuid
+  before_destroy :set_priority_zero
 
   api_accessible :user, extend: :common do |t|
     t.add :command
@@ -262,6 +263,10 @@ class ContainerRequest < ArvadosModel
     end
   end
 
+  def set_priority_zero
+    self.update_attributes!(priority: 0) if self.state != Final
+  end
+
   def set_requesting_container_uuid
     return !new_record? if self.requesting_container_uuid   # already set
 
index 26c714eb4a39bda9ec1df0f6f69d488c64adf7c6..f0934104d32ca6454ec6cfcf2f4c227bc3cd4a71 100644 (file)
@@ -10,6 +10,11 @@ class User < ArvadosModel
   include CommonApiTemplate
   include CanBeAnOwner
 
+  # To avoid upgrade bugs, when changing the permission cache value
+  # format, change PERM_CACHE_PREFIX too:
+  PERM_CACHE_PREFIX = "perm_v20170725_"
+  PERM_CACHE_TTL = 172800
+
   serialize :prefs, Hash
   has_many :api_client_authorizations
   validates(:username,
@@ -141,7 +146,7 @@ class User < ArvadosModel
       timestamp = DbCurrentTime::db_current_time.to_i if timestamp.nil?
       connection.execute "NOTIFY invalidate_permissions_cache, '#{timestamp}'"
     else
-      Rails.cache.delete_matched(/^groups_for_user_/)
+      Rails.cache.delete_matched(/^#{PERM_CACHE_PREFIX}/)
     end
   end
 
@@ -183,7 +188,7 @@ class User < ArvadosModel
                   ).rows.each do |group_uuid, max_p_val|
       group_perms[group_uuid] = PERMS_FOR_VAL[max_p_val.to_i]
     end
-    Rails.cache.write "groups_for_user_#{self.uuid}", group_perms
+    Rails.cache.write "#{PERM_CACHE_PREFIX}#{self.uuid}", group_perms, expires_in: PERM_CACHE_TTL
     group_perms
   end
 
@@ -191,12 +196,12 @@ class User < ArvadosModel
   # and perm_hash[:write] are true if this user can read and write
   # objects owned by group_uuid.
   def group_permissions
-    r = Rails.cache.read "groups_for_user_#{self.uuid}"
+    r = Rails.cache.read "#{PERM_CACHE_PREFIX}#{self.uuid}"
     if r.nil?
       if Rails.configuration.async_permissions_update
         while r.nil?
           sleep(0.1)
-          r = Rails.cache.read "groups_for_user_#{self.uuid}"
+          r = Rails.cache.read "#{PERM_CACHE_PREFIX}#{self.uuid}"
         end
       else
         r = calculate_group_permissions
index 8f5c21a40ba6e36591359f43ff6b53c1b7b64b80..2f32556733b1a8a186bc3ad51a540518c485ac57 100644 (file)
@@ -442,6 +442,10 @@ common:
   # Default value for keep_cache_ram of a container's runtime_constraints.
   container_default_keep_cache_ram: 268435456
 
+  # Token to be included in all healthcheck requests. Disabled by default.
+  # Server expects request header of the format "Authorization: Bearer xxx"
+  ManagementToken: false
+
 development:
   force_ssl: false
   cache_classes: false
index d4b606dafcc7f6e7112b4591e16e82a6bbd08a2e..e24eee035cd2c8f0d01c5b708c43f9d870a1fc71 100644 (file)
@@ -8,9 +8,3 @@ require 'josh_id'
 
 # Initialize the rails application
 Server::Application.initialize!
-begin
-  Rails.cache.clear
-rescue Errno::ENOENT => e
-  # Cache directory does not exist? Then cache is clear, proceed.
-  Rails.logger.warn "In Rails.cache.clear, ignoring #{e.inspect}"
-end
index c030d4242fcfd6b8691c328bd927e1d08e9436a4..2e9d618986825d1871c28ca38695c4ef7c046440 100644 (file)
@@ -104,6 +104,8 @@ Server::Application.routes.draw do
 
   match '/static/login_failure', to: 'static#login_failure', as: :login_failure, via: [:get, :post]
 
+  match '/_health/ping', to: 'arvados/v1/healthcheck#ping', via: [:get]
+
   # Send unroutable requests to an arbitrary controller
   # (ends up at ApplicationController#render_not_found)
   match '*a', to: 'static#render_not_found', via: [:get, :post, :put, :patch, :delete, :options]
index da138c480e8d4da5206a646560975f441ccb5913..836f840aea3d6a1f3686efa12f6b9feecd330a11 100644 (file)
@@ -272,6 +272,25 @@ canceled_with_running_container:
     vcpus: 1
     ram: 123
 
+running_to_be_deleted:
+  uuid: zzzzz-xvhdp-cr5runningcntnr
+  owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  name: running to be deleted
+  state: Committed
+  priority: 1
+  created_at: <%= 2.days.ago.to_s(:db) %>
+  updated_at: <%= 1.days.ago.to_s(:db) %>
+  modified_at: <%= 1.days.ago.to_s(:db) %>
+  modified_by_user_uuid: zzzzz-tpzed-xurymjxw79nv3jz
+  container_image: test
+  cwd: test
+  output_path: test
+  command: ["echo", "hello"]
+  container_uuid: zzzzz-dz642-runnincntrtodel
+  runtime_constraints:
+    vcpus: 1
+    ram: 123
+
 completed_with_input_mounts:
   uuid: zzzzz-xvhdp-crwithinputmnts
   owner_uuid: zzzzz-tpzed-xurymjxw79nv3jz
index 929366976e654b4d335ae19042f1b959121b1cb4..a7ad6f0378d90679cb1ac183f69233c34fddc88a 100644 (file)
@@ -216,3 +216,20 @@ running_container_with_logs:
   runtime_constraints:
     ram: 12000000000
     vcpus: 4
+
+running_to_be_deleted:
+  uuid: zzzzz-dz642-runnincntrtodel
+  owner_uuid: zzzzz-tpzed-000000000000000
+  state: Running
+  priority: 1
+  created_at: <%= 1.minute.ago.to_s(:db) %>
+  updated_at: <%= 1.minute.ago.to_s(:db) %>
+  started_at: <%= 1.minute.ago.to_s(:db) %>
+  container_image: test
+  cwd: test
+  output_path: test
+  command: ["echo", "hello"]
+  runtime_constraints:
+    ram: 12000000000
+    vcpus: 4
+  auth_uuid: zzzzz-gj3su-077z32aux8dg2s2
diff --git a/services/api/test/functional/arvados/v1/healthcheck_controller_test.rb b/services/api/test/functional/arvados/v1/healthcheck_controller_test.rb
new file mode 100644 (file)
index 0000000..551eefa
--- /dev/null
@@ -0,0 +1,30 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+require 'test_helper'
+
+class Arvados::V1::HealthcheckControllerTest < ActionController::TestCase
+  [
+    [false, nil, 404, 'disabled'],
+    [true, nil, 401, 'authorization required'],
+    [true, 'badformatwithnoBearer', 403, 'authorization error'],
+    [true, 'Bearer wrongtoken', 403, 'authorization error'],
+    [true, 'Bearer configuredmanagementtoken', 200, '{"health":"OK"}'],
+  ].each do |enabled, header, error_code, error_msg|
+    test "ping when #{if enabled then 'enabled' else 'disabled' end} with header '#{header}'" do
+      Rails.configuration.ManagementToken = 'configuredmanagementtoken' if enabled
+
+      @request.headers['Authorization'] = header
+      get :ping
+      assert_response error_code
+
+      resp = JSON.parse(@response.body)
+      if error_code == 200
+        assert_equal(JSON.load('{"health":"OK"}'), resp)
+      else
+        assert_equal(error_msg, resp['errors'])
+      end
+    end
+  end
+end
index 2ef85bc0af548a18019bd4caf49d2b9c252a93c1..142455880d5d2fbfc7247ab6113bac20e1d95897 100644 (file)
@@ -23,7 +23,7 @@ class ErrorsTest < ActionDispatch::IntegrationTest
       # Generally, new routes should appear under /arvados/v1/. If
       # they appear elsewhere, that might have been caused by default
       # rails generator behavior that we don't want.
-      assert_match(/^\/(|\*a|arvados\/v1\/.*|auth\/.*|login|logout|database\/reset|discovery\/.*|static\/.*|themes\/.*|assets)(\(\.:format\))?$/,
+      assert_match(/^\/(|\*a|arvados\/v1\/.*|auth\/.*|login|logout|database\/reset|discovery\/.*|static\/.*|themes\/.*|assets|_health\/.*)(\(\.:format\))?$/,
                    route.path.spec.to_s,
                    "Unexpected new route: #{route.path.spec}")
     end
index 280e93962c719c13f8ed65aa71c67111c79d523b..e751d6158cdc2eb1b26fd3d80fa277839d7ad590 100644 (file)
@@ -750,4 +750,28 @@ class ContainerRequestTest < ActiveSupport::TestCase
       end
     end
   end
+
+  test "delete container_request and check its container's priority" do
+    act_as_user users(:active) do
+      cr = ContainerRequest.find_by_uuid container_requests(:running_to_be_deleted).uuid
+
+      # initially the cr's container has priority > 0
+      c = Container.find_by_uuid(cr.container_uuid)
+      assert_equal 1, c.priority
+
+      # destroy the cr
+      assert_nothing_raised {cr.destroy}
+
+      # the cr's container now has priority of 0
+      c = Container.find_by_uuid(cr.container_uuid)
+      assert_equal 0, c.priority
+    end
+  end
+
+  test "delete container_request in final state and expect no error due to before_destroy callback" do
+    act_as_user users(:active) do
+      cr = ContainerRequest.find_by_uuid container_requests(:completed).uuid
+      assert_nothing_raised {cr.destroy}
+    end
+  end
 end
index f06b1ac97103ff55c3bbe32f8c99adbb4cc61b41..0656cbf89ad34661d08bb467c96b2be152a7908f 100644 (file)
@@ -107,5 +107,5 @@ func (s *GitoliteSuite) TestPush(c *check.C) {
 
 func (s *GitoliteSuite) TestPushUnwritable(c *check.C) {
        err := s.RunGit(c, spectatorToken, "push", "active/foo.git", "master:gitolite-push-fail")
-       c.Check(err, check.ErrorMatches, `.*HTTP code = 403.*`)
+       c.Check(err, check.ErrorMatches, `.*HTTP (code = )?403.*`)
 }
index 6a2b6401b5e0b293664e4ff0803c9ea285b331ad..10c69eedd3bf2e2c81cb51ea7c92961d108f1204 100644 (file)
@@ -77,9 +77,10 @@ func (s *IntegrationSuite) SetUpTest(c *check.C) {
                                APIHost:  arvadostest.APIHost(),
                                Insecure: true,
                        },
-                       Listen:     ":0",
-                       GitCommand: "/usr/bin/git",
-                       RepoRoot:   s.tmpRepoRoot,
+                       Listen:          ":0",
+                       GitCommand:      "/usr/bin/git",
+                       RepoRoot:        s.tmpRepoRoot,
+                       ManagementToken: arvadostest.ManagementToken,
                }
        }
 
index bee02693ae62745be83a0ac80a3430cfb3cf804e..79a3eb3f7b85de9667c04fe1b24f2a5217db5772 100644 (file)
@@ -18,11 +18,12 @@ import (
 
 // Server configuration
 type Config struct {
-       Client       arvados.Client
-       Listen       string
-       GitCommand   string
-       RepoRoot     string
-       GitoliteHome string
+       Client          arvados.Client
+       Listen          string
+       GitCommand      string
+       RepoRoot        string
+       GitoliteHome    string
+       ManagementToken string
 }
 
 var theConfig = defaultConfig()
@@ -49,6 +50,10 @@ func main() {
 
        cfgPath := flag.String("config", defaultCfgPath, "Configuration file `path`.")
        dumpConfig := flag.Bool("dump-config", false, "write current configuration to stdout and exit (useful for migrating from command line flags to config file)")
+
+       flag.StringVar(&theConfig.ManagementToken, "management-token", theConfig.ManagementToken,
+               "Authorization token to be included in all health check requests.")
+
        flag.Usage = usage
        flag.Parse()
 
index 8a7819a2a6133099d99f6058f01d94e36edb6018..8f0d90f89e29cad0a0d8d590b66e412b02195ad2 100644 (file)
@@ -7,6 +7,7 @@ package main
 import (
        "net/http"
 
+       "git.curoverse.com/arvados.git/sdk/go/health"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
 )
 
@@ -17,6 +18,10 @@ type server struct {
 func (srv *server) Start() error {
        mux := http.NewServeMux()
        mux.Handle("/", &authHandler{handler: newGitHandler()})
+       mux.Handle("/_health/", &health.Handler{
+               Token:  theConfig.ManagementToken,
+               Prefix: "/_health/",
+       })
        srv.Handler = mux
        srv.Addr = theConfig.Listen
        return srv.Server.Start()
index 241ec33cb46b4d5b97d4f2042f3279941f136db7..b4fc532e8f1a2000e070c86131b60ae143a0edf7 100644 (file)
@@ -5,9 +5,13 @@
 package main
 
 import (
+       "net/http"
+       "net/http/httptest"
        "os"
        "os/exec"
 
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+
        check "gopkg.in/check.v1"
 )
 
@@ -36,7 +40,7 @@ func (s *GitSuite) TestReadonly(c *check.C) {
        err := s.RunGit(c, spectatorToken, "fetch", "active/foo.git")
        c.Assert(err, check.Equals, nil)
        err = s.RunGit(c, spectatorToken, "push", "active/foo.git", "master:newbranchfail")
-       c.Assert(err, check.ErrorMatches, `.*HTTP code = 403.*`)
+       c.Assert(err, check.ErrorMatches, `.*HTTP (code = )?403.*`)
        _, err = os.Stat(s.tmpRepoRoot + "/zzzzz-s0uqq-382brsig8rp3666.git/refs/heads/newbranchfail")
        c.Assert(err, check.FitsTypeOf, &os.PathError{})
 }
@@ -104,3 +108,16 @@ func (s *GitSuite) makeArvadosRepo(c *check.C) {
        c.Log(string(msg))
        c.Assert(err, check.Equals, nil)
 }
+
+func (s *GitSuite) TestHealthCheckPing(c *check.C) {
+       req, err := http.NewRequest("GET",
+               "http://"+s.testServer.Addr+"/_health/ping",
+               nil)
+       c.Assert(err, check.Equals, nil)
+       req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken)
+
+       resp := httptest.NewRecorder()
+       s.testServer.Handler.ServeHTTP(resp, req)
+       c.Check(resp.Code, check.Equals, 200)
+       c.Check(resp.Body.String(), check.Matches, `{"health":"OK"}\n`)
+}
index 35e6ff09cad708c90d7cfee2c607eabd668a739e..d91ae05a1158fc3f327162d4507914fb8cebed20 100644 (file)
@@ -38,7 +38,7 @@ Installing on Debian systems
 
 1. Add this Arvados repository to your sources list::
 
-     deb http://apt.arvados.org/ wheezy main
+     deb http://apt.arvados.org/ jessie main
 
 2. Update your package list.
 
index 1bfd5177988af6c81adbf091236c187cf2a5d6bf..30770fc0152a58125495420ebed2d8836768cfd3 100644 (file)
@@ -85,6 +85,8 @@ else:
     # llfuse >= 0.42
     llfuse._notify_queue = Queue.Queue()
 
+LLFUSE_VERSION_0 = llfuse.__version__.startswith('0')
+
 from fusedir import sanitize_filename, Directory, CollectionDirectory, TmpCollectionDirectory, MagicDirectory, TagsDirectory, ProjectDirectory, SharedDirectory, CollectionDirectoryBase
 from fusefile import StringFile, FuseArvadosFile
 
@@ -371,7 +373,9 @@ class Operations(llfuse.Operations):
             self.events.close()
             self.events = None
 
-        if llfuse.lock.acquire():
+        # Different versions of llfuse require and forbid us to
+        # acquire the lock here. See #8345#note-37, #10805#note-9.
+        if LLFUSE_VERSION_0 and llfuse.lock.acquire():
             # llfuse < 0.42
             self.inodes.clear()
             llfuse.lock.release()
@@ -390,30 +394,33 @@ class Operations(llfuse.Operations):
 
     @catch_exceptions
     def on_event(self, ev):
-        if 'event_type' not in ev:
+        if 'event_type' not in ev or ev["event_type"] not in ("create", "update", "delete"):
             return
         with llfuse.lock:
-            new_attrs = (ev.get("properties") or {}).get("new_attributes") or {}
-            pdh = new_attrs.get("portable_data_hash")
-            # new_attributes.modified_at currently lacks
-            # subsecond precision (see #6347) so use event_at
-            # which should always be the same.
-            stamp = ev.get("event_at")
+            properties = ev.get("properties") or {}
+            old_attrs = properties.get("old_attributes") or {}
+            new_attrs = properties.get("new_attributes") or {}
 
             for item in self.inodes.inode_cache.find_by_uuid(ev["object_uuid"]):
                 item.invalidate()
-                if stamp and pdh and ev.get("object_kind") == "arvados#collection":
-                    item.update(to_record_version=(stamp, pdh))
-                else:
-                    item.update()
-
-            oldowner = ((ev.get("properties") or {}).get("old_attributes") or {}).get("owner_uuid")
+                if ev.get("object_kind") == "arvados#collection":
+                    pdh = new_attrs.get("portable_data_hash")
+                    # new_attributes.modified_at currently lacks
+                    # subsecond precision (see #6347) so use event_at
+                    # which should always be the same.
+                    stamp = ev.get("event_at")
+                    if (stamp and pdh and item.writable() and
+                        item.collection is not None and
+                        item.collection.modified() and
+                        new_attrs.get("is_trashed") is not True):
+                        item.update(to_record_version=(stamp, pdh))
+
+            oldowner = old_attrs.get("owner_uuid")
             newowner = ev.get("object_owner_uuid")
             for parent in (
                     self.inodes.inode_cache.find_by_uuid(oldowner) +
                     self.inodes.inode_cache.find_by_uuid(newowner)):
-                parent.invalidate()
-                parent.update()
+                parent.child_event(ev)
 
     @catch_exceptions
     def getattr(self, inode, ctx=None):
index b3717ff07c23cb665505645d869b0670d1566b54..4dad90c86758edb118d7ab4b04958417533b9653 100644 (file)
@@ -205,12 +205,16 @@ class Mount(object):
         self.logger.info("enable write is %s", self.args.enable_write)
 
     def _setup_api(self):
-        self.api = arvados.safeapi.ThreadSafeApiCache(
-            apiconfig=arvados.config.settings(),
-            keep_params={
-                'block_cache': arvados.keep.KeepBlockCache(self.args.file_cache),
-                'num_retries': self.args.retries,
-            })
+        try:
+            self.api = arvados.safeapi.ThreadSafeApiCache(
+                apiconfig=arvados.config.settings(),
+                keep_params={
+                    'block_cache': arvados.keep.KeepBlockCache(self.args.file_cache),
+                    'num_retries': self.args.retries,
+                })
+        except KeyError as e:
+            self.logger.error("Missing environment: %s", e)
+            exit(1)
         # Do a sanity check that we have a working arvados host + token.
         self.api.users().current().execute()
 
index 34295ef319afb125d1fe4971e37519cdb0ec983c..a51dd909b690df3cb39865d021b8f4daea4b471b 100644 (file)
@@ -139,3 +139,6 @@ class FreshBase(object):
 
     def finalize(self):
         pass
+
+    def child_event(self, ev):
+        pass
index 30ae6b40e0ae95c751ccaa1b3c0760b623d793c5..0178fe5544b07ddb30b9fa9c8e08d734a12cde0c 100644 (file)
@@ -771,6 +771,7 @@ class ProjectDirectory(Directory):
         self._poll_time = poll_time
         self._updating_lock = threading.Lock()
         self._current_user = None
+        self._full_listing = False
 
     def want_event_subscribe(self):
         return True
@@ -793,27 +794,35 @@ class ProjectDirectory(Directory):
     def uuid(self):
         return self.project_uuid
 
+    def items(self):
+        self._full_listing = True
+        return super(ProjectDirectory, self).items()
+
+    def namefn(self, i):
+        if 'name' in i:
+            if i['name'] is None or len(i['name']) == 0:
+                return None
+            elif "uuid" in i and (collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid'])):
+                # collection or subproject
+                return i['name']
+            elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
+                # name link
+                return i['name']
+            elif 'kind' in i and i['kind'].startswith('arvados#'):
+                # something else
+                return "{}.{}".format(i['name'], i['kind'][8:])
+        else:
+            return None
+
+
     @use_counter
     def update(self):
         if self.project_object_file == None:
             self.project_object_file = ObjectFile(self.inode, self.project_object)
             self.inodes.add_entry(self.project_object_file)
 
-        def namefn(i):
-            if 'name' in i:
-                if i['name'] is None or len(i['name']) == 0:
-                    return None
-                elif collection_uuid_pattern.match(i['uuid']) or group_uuid_pattern.match(i['uuid']):
-                    # collection or subproject
-                    return i['name']
-                elif link_uuid_pattern.match(i['uuid']) and i['head_kind'] == 'arvados#collection':
-                    # name link
-                    return i['name']
-                elif 'kind' in i and i['kind'].startswith('arvados#'):
-                    # something else
-                    return "{}.{}".format(i['name'], i['kind'][8:])
-            else:
-                return None
+        if not self._full_listing:
+            return
 
         def samefn(a, i):
             if isinstance(a, CollectionDirectory) or isinstance(a, ProjectDirectory):
@@ -835,31 +844,62 @@ class ProjectDirectory(Directory):
                     self.project_object = self.api.users().get(
                         uuid=self.project_uuid).execute(num_retries=self.num_retries)
 
-                contents = arvados.util.list_all(self.api.groups().contents,
-                                                 self.num_retries, uuid=self.project_uuid)
+                contents = arvados.util.list_all(self.api.groups().list,
+                                                 self.num_retries,
+                                                 filters=[["owner_uuid", "=", self.project_uuid],
+                                                          ["group_class", "=", "project"]])
+                contents.extend(arvados.util.list_all(self.api.collections().list,
+                                                      self.num_retries,
+                                                      filters=[["owner_uuid", "=", self.project_uuid]]))
 
             # end with llfuse.lock_released, re-acquire lock
 
             self.merge(contents,
-                       namefn,
+                       self.namefn,
                        samefn,
                        self.createDirectory)
         finally:
             self._updating_lock.release()
 
+    def _add_entry(self, i, name):
+        ent = self.createDirectory(i)
+        self._entries[name] = self.inodes.add_entry(ent)
+        return self._entries[name]
+
     @use_counter
     @check_update
-    def __getitem__(self, item):
-        if item == '.arvados#project':
+    def __getitem__(self, k):
+        if k == '.arvados#project':
             return self.project_object_file
-        else:
-            return super(ProjectDirectory, self).__getitem__(item)
+        elif self._full_listing or super(ProjectDirectory, self).__contains__(k):
+            return super(ProjectDirectory, self).__getitem__(k)
+        with llfuse.lock_released:
+            contents = self.api.groups().list(filters=[["owner_uuid", "=", self.project_uuid],
+                                                       ["group_class", "=", "project"],
+                                                       ["name", "=", k]],
+                                              limit=1).execute(num_retries=self.num_retries)["items"]
+            if not contents:
+                contents = self.api.collections().list(filters=[["owner_uuid", "=", self.project_uuid],
+                                                                ["name", "=", k]],
+                                                       limit=1).execute(num_retries=self.num_retries)["items"]
+        if contents:
+            name = sanitize_filename(self.namefn(contents[0]))
+            if name != k:
+                raise KeyError(k)
+            return self._add_entry(contents[0], name)
+
+        # Didn't find item
+        raise KeyError(k)
 
     def __contains__(self, k):
         if k == '.arvados#project':
             return True
-        else:
-            return super(ProjectDirectory, self).__contains__(k)
+        try:
+            self[k]
+            return True
+        except KeyError:
+            pass
+        return False
 
     @use_counter
     @check_update
@@ -925,6 +965,51 @@ class ProjectDirectory(Directory):
         self._entries[name_new] = ent
         self.inodes.invalidate_entry(src.inode, name_old.encode(self.inodes.encoding))
 
+    @use_counter
+    def child_event(self, ev):
+        properties = ev.get("properties") or {}
+        old_attrs = properties.get("old_attributes") or {}
+        new_attrs = properties.get("new_attributes") or {}
+        old_attrs["uuid"] = ev["object_uuid"]
+        new_attrs["uuid"] = ev["object_uuid"]
+        old_name = sanitize_filename(self.namefn(old_attrs))
+        new_name = sanitize_filename(self.namefn(new_attrs))
+
+        # create events will have a new name, but not an old name
+        # delete events will have an old name, but not a new name
+        # update events will have an old and new name, and they may be same or different
+        # if they are the same, an unrelated field changed and there is nothing to do.
+
+        if old_attrs.get("owner_uuid") != self.project_uuid:
+            # Was moved from somewhere else, so don't try to remove entry.
+            old_name = None
+        if ev.get("object_owner_uuid") != self.project_uuid:
+            # Was moved to somewhere else, so don't try to add entry
+            new_name = None
+
+        if ev.get("object_kind") == "arvados#collection":
+            if old_attrs.get("is_trashed"):
+                # Was previously deleted
+                old_name = None
+            if new_attrs.get("is_trashed"):
+                # Has been deleted
+                new_name = None
+
+        if new_name != old_name:
+            ent = None
+            if old_name in self._entries:
+                ent = self._entries[old_name]
+                del self._entries[old_name]
+                self.inodes.invalidate_entry(self.inode, old_name.encode(self.inodes.encoding))
+
+            if new_name:
+                if ent is not None:
+                    self._entries[new_name] = ent
+                else:
+                    self._add_entry(new_attrs, new_name)
+            elif ent is not None:
+                self.inodes.del_entry(ent)
+
 
 class SharedDirectory(Directory):
     """A special directory that represents users or groups who have shared projects with me."""
index 6abca6027aed0ea63554b3b3996eaa93fe5f3710..d46a3128305581837060727056c27a7eeadd7456 100644 (file)
@@ -41,7 +41,7 @@ setup(name='arvados_fuse',
       ],
       install_requires=[
         'arvados-python-client >= 0.1.20151118035730',
-        'llfuse==0.41.1',
+        'llfuse>=1.2',
         'python-daemon',
         'ciso8601',
         'setuptools'
index 6a0c5de8992b856f4692376acb38d634f2250791..ba9cd88eb7d607c6f50beed6b1cecad17c4fabe9 100644 (file)
@@ -8,6 +8,7 @@ import arvados_fuse.command
 import atexit
 import functools
 import inspect
+import llfuse
 import logging
 import multiprocessing
 import os
@@ -76,6 +77,10 @@ class IntegrationTest(unittest.TestCase):
         def decorator(func):
             @functools.wraps(func)
             def wrapper(self, *args, **kwargs):
+                # Workaround for llfuse deadlock bug. See #10805, #8345,
+                # https://bitbucket.org/nikratio/python-llfuse/issues/108
+                llfuse.close = lambda *args: None
+
                 self.mount = None
                 try:
                     with arvados_fuse.command.Mount(
index 8518d8bbead64b34b0ebfcb8308e18bdc6c6c6d7..96ff889fcfbdb0de8e2d07ed0ea6a15ebd6f0bfe 100644 (file)
@@ -43,6 +43,10 @@ class MountTestBase(unittest.TestCase):
         self.api = api if api else arvados.safeapi.ThreadSafeApiCache(arvados.config.settings())
         self.llfuse_thread = None
 
+        # Workaround for llfuse deadlock bug. See #10805, #8345,
+        # https://bitbucket.org/nikratio/python-llfuse/issues/108
+        llfuse.close = lambda *args: None
+
     # This is a copy of Mount's method.  TODO: Refactor MountTestBase
     # to use a Mount instead of copying its code.
     def _llfuse_main(self):
index 225e4b2d22bc50d8dd8a7a97fae8cf767cc3d638..ec8868af7d799857d0eba14e8478f3030d9969cd 100644 (file)
@@ -220,66 +220,62 @@ class FuseTagsUpdateTest(MountTestBase):
             attempt(self.assertDirContents, 'fuse_test_tag', [bar_uuid])
 
 
+def fuseSharedTestHelper(mounttmp):
+    class Test(unittest.TestCase):
+        def runTest(self):
+            # Double check that we can open and read objects in this folder as a file,
+            # and that its contents are what we expect.
+            baz_path = os.path.join(
+                mounttmp,
+                'FUSE User',
+                'FUSE Test Project',
+                'collection in FUSE project',
+                'baz')
+            with open(baz_path) as f:
+                self.assertEqual("baz", f.read())
+
+            # check mtime on collection
+            st = os.stat(baz_path)
+            try:
+                mtime = st.st_mtime_ns / 1000000000
+            except AttributeError:
+                mtime = st.st_mtime
+            self.assertEqual(mtime, 1391448174)
+
+            # shared_dirs is a list of the directories exposed
+            # by fuse.SharedDirectory (i.e. any object visible
+            # to the current user)
+            shared_dirs = llfuse.listdir(mounttmp)
+            shared_dirs.sort()
+            self.assertIn('FUSE User', shared_dirs)
+
+            # fuse_user_objs is a list of the objects owned by the FUSE
+            # test user (which present as files in the 'FUSE User'
+            # directory)
+            fuse_user_objs = llfuse.listdir(os.path.join(mounttmp, 'FUSE User'))
+            fuse_user_objs.sort()
+            self.assertEqual(['FUSE Test Project',                    # project owned by user
+                              'collection #1 owned by FUSE',          # collection owned by user
+                              'collection #2 owned by FUSE'          # collection owned by user
+                          ], fuse_user_objs)
+
+            # test_proj_files is a list of the files in the FUSE Test Project.
+            test_proj_files = llfuse.listdir(os.path.join(mounttmp, 'FUSE User', 'FUSE Test Project'))
+            test_proj_files.sort()
+            self.assertEqual(['collection in FUSE project'
+                          ], test_proj_files)
+
+
+    Test().runTest()
+
 class FuseSharedTest(MountTestBase):
     def runTest(self):
         self.make_mount(fuse.SharedDirectory,
                         exclude=self.api.users().current().execute()['uuid'])
+        keep = arvados.keep.KeepClient()
+        keep.put("baz")
 
-        # shared_dirs is a list of the directories exposed
-        # by fuse.SharedDirectory (i.e. any object visible
-        # to the current user)
-        shared_dirs = llfuse.listdir(self.mounttmp)
-        shared_dirs.sort()
-        self.assertIn('FUSE User', shared_dirs)
-
-        # fuse_user_objs is a list of the objects owned by the FUSE
-        # test user (which present as files in the 'FUSE User'
-        # directory)
-        fuse_user_objs = llfuse.listdir(os.path.join(self.mounttmp, 'FUSE User'))
-        fuse_user_objs.sort()
-        self.assertEqual(['FUSE Test Project',                    # project owned by user
-                          'collection #1 owned by FUSE',          # collection owned by user
-                          'collection #2 owned by FUSE',          # collection owned by user
-                          'pipeline instance owned by FUSE.pipelineInstance',  # pipeline instance owned by user
-                      ], fuse_user_objs)
-
-        # test_proj_files is a list of the files in the FUSE Test Project.
-        test_proj_files = llfuse.listdir(os.path.join(self.mounttmp, 'FUSE User', 'FUSE Test Project'))
-        test_proj_files.sort()
-        self.assertEqual(['collection in FUSE project',
-                          'pipeline instance in FUSE project.pipelineInstance',
-                          'pipeline template in FUSE project.pipelineTemplate'
-                      ], test_proj_files)
-
-        # Double check that we can open and read objects in this folder as a file,
-        # and that its contents are what we expect.
-        pipeline_template_path = os.path.join(
-                self.mounttmp,
-                'FUSE User',
-                'FUSE Test Project',
-                'pipeline template in FUSE project.pipelineTemplate')
-        with open(pipeline_template_path) as f:
-            j = json.load(f)
-            self.assertEqual("pipeline template in FUSE project", j['name'])
-
-        # check mtime on template
-        st = os.stat(pipeline_template_path)
-        try:
-            mtime = st.st_mtime_ns / 1000000000
-        except AttributeError:
-            mtime = st.st_mtime
-        self.assertEqual(mtime, 1397493304)
-
-        # check mtime on collection
-        st = os.stat(os.path.join(
-                self.mounttmp,
-                'FUSE User',
-                'collection #1 owned by FUSE'))
-        try:
-            mtime = st.st_mtime_ns / 1000000000
-        except AttributeError:
-            mtime = st.st_mtime
-        self.assertEqual(mtime, 1391448174)
+        self.pool.apply(fuseSharedTestHelper, (self.mounttmp,))
 
 
 class FuseHomeTest(MountTestBase):
index d72effc075d65a62d41b722ec177dfa3d465ec5f..c29ae7d43c270b129c0bf459aab25df927dc0407 100644 (file)
@@ -118,7 +118,7 @@ func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceRelo
 
        var collection *arvados.Collection
        if pdh != "" {
-               collection = c.lookupCollection(pdh)
+               collection = c.lookupCollection(arv.ApiToken + "\000" + pdh)
        }
 
        if collection != nil && permOK {
@@ -150,7 +150,7 @@ func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceRelo
                        // PDH changed, but now we know we have
                        // permission -- and maybe we already have the
                        // new PDH in the cache.
-                       if coll := c.lookupCollection(current.PortableDataHash); coll != nil {
+                       if coll := c.lookupCollection(arv.ApiToken + "\000" + current.PortableDataHash); coll != nil {
                                return coll, nil
                        }
                }
@@ -170,14 +170,13 @@ func (c *cache) Get(arv *arvadosclient.ArvadosClient, targetID string, forceRelo
                expire: exp,
                pdh:    collection.PortableDataHash,
        })
-       // Disabled, see #11945
-       // c.collections.Add(collection.PortableDataHash, &cachedCollection{
-       //      expire:     exp,
-       //      collection: collection,
-       // })
-       // if int64(len(collection.ManifestText)) > c.MaxCollectionBytes/int64(c.MaxCollectionEntries) {
-       //      go c.pruneCollections()
-       // }
+       c.collections.Add(arv.ApiToken+"\000"+collection.PortableDataHash, &cachedCollection{
+               expire:     exp,
+               collection: collection,
+       })
+       if int64(len(collection.ManifestText)) > c.MaxCollectionBytes/int64(c.MaxCollectionEntries) {
+               go c.pruneCollections()
+       }
        return collection, nil
 }
 
@@ -238,15 +237,13 @@ func (c *cache) collectionBytes() uint64 {
        return size
 }
 
-func (c *cache) lookupCollection(pdh string) *arvados.Collection {
-       if pdh == "" {
-               return nil
-       } else if ent, cached := c.collections.Get(pdh); !cached {
+func (c *cache) lookupCollection(key string) *arvados.Collection {
+       if ent, cached := c.collections.Get(key); !cached {
                return nil
        } else {
                ent := ent.(*cachedCollection)
                if ent.expire.Before(time.Now()) {
-                       c.collections.Remove(pdh)
+                       c.collections.Remove(key)
                        return nil
                } else {
                        atomic.AddUint64(&c.stats.CollectionHits, 1)
index 05325270e6bd3e566037c0a899ceeac5d7600275..cddeaf489763500b9e7230a75c2b19a4c25f40cf 100644 (file)
@@ -5,14 +5,13 @@
 package main
 
 import (
+       "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
        "git.curoverse.com/arvados.git/sdk/go/arvadostest"
        "gopkg.in/check.v1"
 )
 
 func (s *UnitSuite) TestCache(c *check.C) {
-       c.Skip("see #11945")
-
        arv, err := arvadosclient.MakeArvadosClient()
        c.Assert(err, check.Equals, nil)
 
@@ -22,8 +21,9 @@ func (s *UnitSuite) TestCache(c *check.C) {
        // the first req should cause an API call; the next 4 should
        // hit all caches.
        arv.ApiToken = arvadostest.AdminToken
+       var coll *arvados.Collection
        for i := 0; i < 5; i++ {
-               coll, err := cache.Get(arv, arvadostest.FooCollection, false)
+               coll, err = cache.Get(arv, arvadostest.FooCollection, false)
                c.Check(err, check.Equals, nil)
                c.Assert(coll, check.NotNil)
                c.Check(coll.PortableDataHash, check.Equals, arvadostest.FooPdh)
@@ -37,18 +37,32 @@ func (s *UnitSuite) TestCache(c *check.C) {
 
        // Hit the same collection 2 more times, this time requesting
        // it by PDH and using a different token. The first req should
-       // miss the permission cache. Both reqs should hit the
-       // Collection cache and skip the API lookup.
+       // miss the permission cache and fetch the new manifest; the
+       // second should hit the Collection cache and skip the API
+       // lookup.
        arv.ApiToken = arvadostest.ActiveToken
-       for i := 0; i < 2; i++ {
-               coll, err := cache.Get(arv, arvadostest.FooPdh, false)
-               c.Check(err, check.Equals, nil)
-               c.Assert(coll, check.NotNil)
-               c.Check(coll.PortableDataHash, check.Equals, arvadostest.FooPdh)
-               c.Check(coll.ManifestText[:2], check.Equals, ". ")
-       }
+
+       coll2, err := cache.Get(arv, arvadostest.FooPdh, false)
+       c.Check(err, check.Equals, nil)
+       c.Assert(coll2, check.NotNil)
+       c.Check(coll2.PortableDataHash, check.Equals, arvadostest.FooPdh)
+       c.Check(coll2.ManifestText[:2], check.Equals, ". ")
+       c.Check(coll2.ManifestText, check.Not(check.Equals), coll.ManifestText)
+
+       c.Check(cache.Stats().Requests, check.Equals, uint64(5+1))
+       c.Check(cache.Stats().CollectionHits, check.Equals, uint64(4+0))
+       c.Check(cache.Stats().PermissionHits, check.Equals, uint64(4+0))
+       c.Check(cache.Stats().PDHHits, check.Equals, uint64(4+0))
+       c.Check(cache.Stats().APICalls, check.Equals, uint64(1+1))
+
+       coll2, err = cache.Get(arv, arvadostest.FooPdh, false)
+       c.Check(err, check.Equals, nil)
+       c.Assert(coll2, check.NotNil)
+       c.Check(coll2.PortableDataHash, check.Equals, arvadostest.FooPdh)
+       c.Check(coll2.ManifestText[:2], check.Equals, ". ")
+
        c.Check(cache.Stats().Requests, check.Equals, uint64(5+2))
-       c.Check(cache.Stats().CollectionHits, check.Equals, uint64(4+2))
+       c.Check(cache.Stats().CollectionHits, check.Equals, uint64(4+1))
        c.Check(cache.Stats().PermissionHits, check.Equals, uint64(4+1))
        c.Check(cache.Stats().PDHHits, check.Equals, uint64(4+0))
        c.Check(cache.Stats().APICalls, check.Equals, uint64(1+1))
@@ -67,15 +81,13 @@ func (s *UnitSuite) TestCache(c *check.C) {
                c.Check(err, check.Equals, nil)
        }
        c.Check(cache.Stats().Requests, check.Equals, uint64(5+2+20))
-       c.Check(cache.Stats().CollectionHits, check.Equals, uint64(4+2+18))
+       c.Check(cache.Stats().CollectionHits, check.Equals, uint64(4+1+18))
        c.Check(cache.Stats().PermissionHits, check.Equals, uint64(4+1+18))
        c.Check(cache.Stats().PDHHits, check.Equals, uint64(4+0+18))
        c.Check(cache.Stats().APICalls, check.Equals, uint64(1+1+2))
 }
 
 func (s *UnitSuite) TestCacheForceReloadByPDH(c *check.C) {
-       c.Skip("see #11945")
-
        arv, err := arvadosclient.MakeArvadosClient()
        c.Assert(err, check.Equals, nil)
 
@@ -94,8 +106,6 @@ func (s *UnitSuite) TestCacheForceReloadByPDH(c *check.C) {
 }
 
 func (s *UnitSuite) TestCacheForceReloadByUUID(c *check.C) {
-       c.Skip("see #11945")
-
        arv, err := arvadosclient.MakeArvadosClient()
        c.Assert(err, check.Equals, nil)
 
index 769c2f59ce9a79effcb2e094fd166978941bfa3a..16df210fe83cc12096549f5609fafeecdcc09340 100644 (file)
@@ -21,14 +21,16 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
        "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/health"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
        "git.curoverse.com/arvados.git/sdk/go/keepclient"
 )
 
 type handler struct {
-       Config     *Config
-       clientPool *arvadosclient.ClientPool
-       setupOnce  sync.Once
+       Config        *Config
+       clientPool    *arvadosclient.ClientPool
+       setupOnce     sync.Once
+       healthHandler http.Handler
 }
 
 // parseCollectionIDFromDNSName returns a UUID or PDH if s begins with
@@ -70,7 +72,13 @@ func parseCollectionIDFromURL(s string) string {
 
 func (h *handler) setup() {
        h.clientPool = arvadosclient.MakeClientPool()
+
        keepclient.RefreshServiceDiscoveryOnSIGHUP()
+
+       h.healthHandler = &health.Handler{
+               Token:  h.Config.ManagementToken,
+               Prefix: "/_health/",
+       }
 }
 
 func (h *handler) serveStatus(w http.ResponseWriter, r *http.Request) {
@@ -110,6 +118,11 @@ func (h *handler) ServeHTTP(wOrig http.ResponseWriter, r *http.Request) {
                httpserver.Log(remoteAddr, statusCode, statusText, w.WroteBodyBytes(), r.Method, r.Host, r.URL.Path, r.URL.RawQuery)
        }()
 
+       if strings.HasPrefix(r.URL.Path, "/_health/") && r.Method == "GET" {
+               h.healthHandler.ServeHTTP(w, r)
+               return
+       }
+
        if r.Method == "OPTIONS" {
                method := r.Header.Get("Access-Control-Request-Method")
                if method != "GET" && method != "POST" {
index 1d03b90a3a60e604ef2023a06dada39b000d6f97..04859595e6f337b7975f9305e05189c5aa6f3ce2 100644 (file)
@@ -589,3 +589,24 @@ func (s *IntegrationSuite) TestDirectoryListing(c *check.C) {
                }
        }
 }
+
+func (s *IntegrationSuite) TestHealthCheckPing(c *check.C) {
+       s.testServer.Config.ManagementToken = arvadostest.ManagementToken
+       authHeader := http.Header{
+               "Authorization": {"Bearer " + arvadostest.ManagementToken},
+       }
+
+       resp := httptest.NewRecorder()
+       u := mustParseURL("http://download.example.com/_health/ping")
+       req := &http.Request{
+               Method:     "GET",
+               Host:       u.Host,
+               URL:        u,
+               RequestURI: u.RequestURI(),
+               Header:     authHeader,
+       }
+       s.testServer.Handler.ServeHTTP(resp, req)
+
+       c.Check(resp.Code, check.Equals, http.StatusOK)
+       c.Check(resp.Body.String(), check.Matches, `{"health":"OK"}\n`)
+}
index c4103f409723fbc3c7bbbc3bbd8edcbf7184be42..585dab13e3cbf0ce68ef620c6dad31e4a17c974b 100644 (file)
@@ -34,6 +34,9 @@ type Config struct {
        // Hack to support old command line flag, which is a bool
        // meaning "get actual token from environment".
        deprecatedAllowAnonymous bool
+
+       //Authorization token to be included in all health check requests.
+       ManagementToken string
 }
 
 // DefaultConfig returns the default configuration.
@@ -76,6 +79,9 @@ func main() {
                "Only serve attachments at the given `host:port`"+deprecated)
        flag.BoolVar(&cfg.TrustAllContent, "trust-all-content", false,
                "Serve non-public content from a single origin. Dangerous: read docs before using!"+deprecated)
+       flag.StringVar(&cfg.ManagementToken, "management-token", "",
+               "Authorization token to be included in all health check requests.")
+
        dumpConfig := flag.Bool("dump-config", false,
                "write current configuration to stdout and exit")
        flag.Usage = usage
index 682435515ab5a05ceae51eef2feacffc28e85c58..3d1b4476255d0cadf2274ad12b9aae78f164f72f 100644 (file)
@@ -24,6 +24,7 @@ import (
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
        "git.curoverse.com/arvados.git/sdk/go/config"
+       "git.curoverse.com/arvados.git/sdk/go/health"
        "git.curoverse.com/arvados.git/sdk/go/keepclient"
        "github.com/coreos/go-systemd/daemon"
        "github.com/ghodss/yaml"
@@ -39,6 +40,7 @@ type Config struct {
        Timeout         arvados.Duration
        PIDFile         string
        Debug           bool
+       ManagementToken string
 }
 
 func DefaultConfig() *Config {
@@ -66,6 +68,7 @@ func main() {
        flagset.IntVar(&cfg.DefaultReplicas, "default-replicas", cfg.DefaultReplicas, "Default number of replicas to write if not specified by the client. If 0, use site default."+deprecated)
        flagset.StringVar(&cfg.PIDFile, "pid", cfg.PIDFile, "Path to write pid file."+deprecated)
        timeoutSeconds := flagset.Int("timeout", int(time.Duration(cfg.Timeout)/time.Second), "Timeout (in seconds) on requests to internal Keep services."+deprecated)
+       flagset.StringVar(&cfg.ManagementToken, "management-token", cfg.ManagementToken, "Authorization token to be included in all health check requests.")
 
        var cfgPath string
        const defaultCfgPath = "/etc/arvados/keepproxy/keepproxy.yml"
@@ -160,7 +163,7 @@ func main() {
        signal.Notify(term, syscall.SIGINT)
 
        // Start serving requests.
-       router = MakeRESTRouter(!cfg.DisableGet, !cfg.DisablePut, kc, time.Duration(cfg.Timeout))
+       router = MakeRESTRouter(!cfg.DisableGet, !cfg.DisablePut, kc, time.Duration(cfg.Timeout), cfg.ManagementToken)
        http.Serve(listener, router)
 
        log.Println("shutting down")
@@ -250,7 +253,7 @@ type proxyHandler struct {
 
 // MakeRESTRouter returns an http.Handler that passes GET and PUT
 // requests to the appropriate handlers.
-func MakeRESTRouter(enable_get bool, enable_put bool, kc *keepclient.KeepClient, timeout time.Duration) http.Handler {
+func MakeRESTRouter(enable_get bool, enable_put bool, kc *keepclient.KeepClient, timeout time.Duration, mgmtToken string) http.Handler {
        rest := mux.NewRouter()
 
        transport := *(http.DefaultTransport.(*http.Transport))
@@ -292,6 +295,11 @@ func MakeRESTRouter(enable_get bool, enable_put bool, kc *keepclient.KeepClient,
                rest.HandleFunc(`/`, h.Options).Methods("OPTIONS")
        }
 
+       rest.Handle("/_health/{check}", &health.Handler{
+               Token:  mgmtToken,
+               Prefix: "/_health/",
+       }).Methods("GET")
+
        rest.NotFoundHandler = InvalidPathHandler{}
        return h
 }
index 94be8e23ed1918d93d0d783814103dc4ee956a4c..25619bbb91a3629d8d90563d051b8a31578ed121 100644 (file)
@@ -189,7 +189,7 @@ func (s *ServerRequiredSuite) TestPutWrongContentLength(c *C) {
        // fixes the invalid Content-Length header. In order to test
        // our server behavior, we have to call the handler directly
        // using an httptest.ResponseRecorder.
-       rtr := MakeRESTRouter(true, true, kc, 10*time.Second)
+       rtr := MakeRESTRouter(true, true, kc, 10*time.Second, "")
 
        type testcase struct {
                sendLength   string
@@ -597,3 +597,21 @@ func (s *NoKeepServerSuite) TestAskGetNoKeepServerError(c *C) {
                c.Check(err, ErrorMatches, `.*HTTP 502.*`)
        }
 }
+
+func (s *ServerRequiredSuite) TestPing(c *C) {
+       kc := runProxy(c, nil, false)
+       defer closeListener()
+
+       rtr := MakeRESTRouter(true, true, kc, 10*time.Second, arvadostest.ManagementToken)
+
+       req, err := http.NewRequest("GET",
+               "http://"+listener.Addr().String()+"/_health/ping",
+               nil)
+       c.Assert(err, IsNil)
+       req.Header.Set("Authorization", "Bearer "+arvadostest.ManagementToken)
+
+       resp := httptest.NewRecorder()
+       rtr.ServeHTTP(resp, req)
+       c.Check(resp.Code, Equals, 200)
+       c.Assert(strings.Contains(resp.Body.String(), `{"health":"OK"}`), Equals, true)
+}
index 97509699b8b4cfe4e4d9d37bd9c8573889673329..6d3d21e6f25217744941957a0a2a869fa02fde33 100644 (file)
@@ -82,5 +82,9 @@ Debug:
 
     Enable debug logging.
 
+ManagementToken:
+
+    Authorization token to be included in all health check requests.
+
 `, exampleConfigFile)
 }
index 19a614a1fa6fc42fd3cbe0166a9ed6c6e465043c..0a3ece41ad9d88589513c30a878f172276afd93c 100644 (file)
@@ -40,6 +40,8 @@ type Config struct {
        blobSigningKey  []byte
        systemAuthToken string
        debugLogf       func(string, ...interface{})
+
+       ManagementToken string
 }
 
 var theConfig = DefaultConfig()
index 751a4a77e3c8ba66431ca30d1d58f3cde2208f5d..424910dfa8b2af4a0d22ad9e91bd4eab20076af7 100644 (file)
@@ -27,6 +27,7 @@ import (
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
+       "git.curoverse.com/arvados.git/sdk/go/arvadostest"
 )
 
 // A RequestTester represents the parameters for an HTTP request to
@@ -827,6 +828,18 @@ func IssueRequest(rt *RequestTester) *httptest.ResponseRecorder {
        return response
 }
 
+func IssueHealthCheckRequest(rt *RequestTester) *httptest.ResponseRecorder {
+       response := httptest.NewRecorder()
+       body := bytes.NewReader(rt.requestBody)
+       req, _ := http.NewRequest(rt.method, rt.uri, body)
+       if rt.apiToken != "" {
+               req.Header.Set("Authorization", "Bearer "+rt.apiToken)
+       }
+       loggingRouter := MakeRESTRouter()
+       loggingRouter.ServeHTTP(response, req)
+       return response
+}
+
 // ExpectStatusCode checks whether a response has the specified status code,
 // and reports a test failure if not.
 func ExpectStatusCode(
@@ -1140,3 +1153,21 @@ func TestUntrashHandlerWithNoWritableVolumes(t *testing.T) {
                http.StatusNotFound,
                response)
 }
+
+func TestHealthCheckPing(t *testing.T) {
+       theConfig.ManagementToken = arvadostest.ManagementToken
+       pingReq := &RequestTester{
+               method:   "GET",
+               uri:      "/_health/ping",
+               apiToken: arvadostest.ManagementToken,
+       }
+       response := IssueHealthCheckRequest(pingReq)
+       ExpectStatusCode(t,
+               "",
+               http.StatusOK,
+               response)
+       want := `{"health":"OK"}`
+       if !strings.Contains(response.Body.String(), want) {
+               t.Errorf("expected response to include %s: got %s", want, response.Body.String())
+       }
+}
index f197196c416ae3af23c7b68bca1d0af65376c6d1..0a540f58e66eabe637750ad903bde1ec13dccca6 100644 (file)
@@ -29,6 +29,7 @@ import (
 
        "github.com/gorilla/mux"
 
+       "git.curoverse.com/arvados.git/sdk/go/health"
        "git.curoverse.com/arvados.git/sdk/go/httpserver"
        log "github.com/Sirupsen/logrus"
 )
@@ -78,6 +79,11 @@ func MakeRESTRouter() *router {
        // Untrash moves blocks from trash back into store
        rest.HandleFunc(`/untrash/{hash:[0-9a-f]{32}}`, UntrashHandler).Methods("PUT")
 
+       rest.Handle("/_health/{check}", &health.Handler{
+               Token:  theConfig.ManagementToken,
+               Prefix: "/_health/",
+       }).Methods("GET")
+
        // Any request which does not match any of these routes gets
        // 400 Bad Request.
        rest.NotFoundHandler = http.HandlerFunc(BadRequestHandler)
index 1ba4e375a50c5b84b6fb321e38088537bad31b1a..e47f9fcb1d036b78f94af0af25e8c37dc17b5ad0 100644 (file)
@@ -58,7 +58,8 @@ class NodeManagerConfig(ConfigParser.SafeConfigParser):
                        'watchdog': '600',
                        'node_mem_scaling': '0.95'},
             'Manage': {'address': '127.0.0.1',
-                       'port': '-1'},
+                       'port': '-1',
+                       'ManagementToken': ''},
             'Logging': {'file': '/dev/stderr',
                         'level': 'WARNING'}
         }.iteritems():
index 85719d3069103e8f3d14296b8ff3946241973ac3..cfd611285cffc91933fefb15a2dee37e14995c7e 100644 (file)
@@ -41,12 +41,34 @@ class Handler(http.server.BaseHTTPRequestHandler, object):
             self.send_header('Content-type', 'application/json')
             self.end_headers()
             self.wfile.write(tracker.get_json())
+        elif self.path == '/_health/ping':
+            code, msg = self.check_auth()
+
+            if code != 200:
+              self.send_response(code)
+              self.wfile.write(msg)
+            else:
+              self.send_response(200)
+              self.send_header('Content-type', 'application/json')
+              self.end_headers()
+              self.wfile.write(json.dumps({"health":"OK"}))
         else:
             self.send_response(404)
 
     def log_message(self, fmt, *args, **kwargs):
         _logger.info(fmt, *args, **kwargs)
 
+    def check_auth(self):
+        mgmt_token = self.server._config.get('Manage', 'ManagementToken')
+        auth_header = self.headers.get('Authorization', None)
+
+        if mgmt_token == '':
+          return 404, "disabled"
+        elif auth_header == None:
+          return 401, "authorization required"
+        elif auth_header != 'Bearer '+mgmt_token:
+          return 403, "authorization error"
+        return 200, ""
 
 class Tracker(object):
     def __init__(self):
index a18eff3a5955b0dc0faf7c7bfcbf3b7192633a6c..a236e4f0eecd1d2ba30c3e01adc227e245aba03e 100644 (file)
@@ -6,6 +6,7 @@
 from __future__ import absolute_import, print_function
 from future import standard_library
 
+import json
 import requests
 import unittest
 
@@ -14,10 +15,15 @@ import arvnodeman.config as config
 
 
 class TestServer(object):
+    def __init__(self, management_token=None):
+        self.mgmt_token = management_token
+
     def __enter__(self):
         cfg = config.NodeManagerConfig()
         cfg.set('Manage', 'port', '0')
         cfg.set('Manage', 'address', '127.0.0.1')
+        if self.mgmt_token != None:
+            cfg.set('Manage', 'ManagementToken', self.mgmt_token)
         self.srv = status.Server(cfg)
         self.srv.start()
         addr, port = self.srv.server_address
@@ -33,6 +39,11 @@ class TestServer(object):
     def get_status(self):
         return self.get_status_response().json()
 
+    def get_healthcheck_ping(self, auth_header=None):
+        headers = {}
+        if auth_header != None:
+            headers['Authorization'] = auth_header
+        return requests.get(self.srv_base+'/_health/ping', headers=headers)
 
 class StatusServerUpdates(unittest.TestCase):
     def test_updates(self):
@@ -56,3 +67,32 @@ class StatusServerDisabled(unittest.TestCase):
         self.srv.start()
         self.assertFalse(self.srv.enabled)
         self.assertFalse(getattr(self.srv, '_thread', False))
+
+class HealthcheckPing(unittest.TestCase):
+    def test_ping_disabled(self):
+        with TestServer() as srv:
+            r = srv.get_healthcheck_ping()
+            self.assertEqual(404, r.status_code)
+
+    def test_ping_no_auth(self):
+        with TestServer('configuredmanagementtoken') as srv:
+            r = srv.get_healthcheck_ping()
+            self.assertEqual(401, r.status_code)
+
+    def test_ping_bad_auth_format(self):
+        with TestServer('configuredmanagementtoken') as srv:
+            r = srv.get_healthcheck_ping('noBearer')
+            self.assertEqual(403, r.status_code)
+
+    def test_ping_bad_auth_token(self):
+        with TestServer('configuredmanagementtoken') as srv:
+            r = srv.get_healthcheck_ping('Bearer badtoken')
+            self.assertEqual(403, r.status_code)
+
+    def test_ping_success(self):
+        with TestServer('configuredmanagementtoken') as srv:
+            r = srv.get_healthcheck_ping('Bearer configuredmanagementtoken')
+            self.assertEqual(200, r.status_code)
+            self.assertEqual('application/json', r.headers['content-type'])
+            resp = r.json()
+            self.assertEqual('{"health": "OK"}', json.dumps(resp))
index a4e886872c49829a27a1223d6cdfd691a2e5efff..edeb647e4628e675be696cb68f4b61892b4cc606 100644 (file)
@@ -243,6 +243,7 @@ func (ps *pgEventSource) NewSink() eventSink {
 }
 
 func (ps *pgEventSource) DB() *sql.DB {
+       ps.WaitReady()
        return ps.db
 }
 
index b40c1fa1863219048a2ba70f8b2afb1ffc394391..a39a959312aed582957d8845dddfc70394b649a4 100644 (file)
@@ -74,7 +74,8 @@ func (pc *cachingPermChecker) Check(uuid string) (bool, error) {
 
        pc.nMisses++
        err = pc.RequestAndDecode(&buf, "GET", path, nil, url.Values{
-               "select": {`["uuid"]`},
+               "include_trash": {"true"},
+               "select":        {`["uuid"]`},
        })
 
        var allowed bool
index 3f3a051d8eba54e8574416290ba71f8777f47b30..2b9bd66b1c466cfdee12d534cddc7eb11d4225e4 100644 (file)
@@ -14,6 +14,7 @@ import (
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/ctxlog"
+       "git.curoverse.com/arvados.git/sdk/go/health"
        "github.com/Sirupsen/logrus"
        "golang.org/x/net/websocket"
 )
@@ -60,10 +61,18 @@ func (rtr *router) setup() {
        rtr.mux.Handle("/debug.json", rtr.jsonHandler(rtr.DebugStatus))
        rtr.mux.Handle("/status.json", rtr.jsonHandler(rtr.Status))
 
-       health := http.NewServeMux()
-       rtr.mux.Handle("/_health/", rtr.mgmtAuth(health))
-       health.Handle("/_health/ping", rtr.jsonHandler(rtr.HealthFunc(func() error { return nil })))
-       health.Handle("/_health/db", rtr.jsonHandler(rtr.HealthFunc(rtr.eventSource.DBHealth)))
+       rtr.mux.Handle("/_health/", &health.Handler{
+               Token:  rtr.Config.ManagementToken,
+               Prefix: "/_health/",
+               Routes: health.Routes{
+                       "db": rtr.eventSource.DBHealth,
+               },
+               Log: func(r *http.Request, err error) {
+                       if err != nil {
+                               logger(r.Context()).WithError(err).Error("error")
+                       }
+               },
+       })
 }
 
 func (rtr *router) makeServer(newSession sessionFactory) *websocket.Server {
@@ -111,21 +120,6 @@ func (rtr *router) DebugStatus() interface{} {
        return s
 }
 
-var pingResponseOK = map[string]string{"health": "OK"}
-
-func (rtr *router) HealthFunc(f func() error) func() interface{} {
-       return func() interface{} {
-               err := f()
-               if err == nil {
-                       return pingResponseOK
-               }
-               return map[string]string{
-                       "health": "ERROR",
-                       "error":  err.Error(),
-               }
-       }
-}
-
 func (rtr *router) Status() interface{} {
        return map[string]interface{}{
                "Clients": atomic.LoadInt64(&rtr.status.ReqsActive),
@@ -149,20 +143,6 @@ func (rtr *router) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
        rtr.mux.ServeHTTP(resp, req)
 }
 
-func (rtr *router) mgmtAuth(h http.Handler) http.Handler {
-       return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-               if rtr.Config.ManagementToken == "" {
-                       http.Error(w, "disabled", http.StatusNotFound)
-               } else if ah := r.Header.Get("Authorization"); ah == "" {
-                       http.Error(w, "authorization required", http.StatusUnauthorized)
-               } else if ah != "Bearer "+rtr.Config.ManagementToken {
-                       http.Error(w, "authorization error", http.StatusForbidden)
-               } else {
-                       h.ServeHTTP(w, r)
-               }
-       })
-}
-
 func (rtr *router) jsonHandler(fn func() interface{}) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                logger := logger(r.Context())
index bc7e6ecb9e391e5afef37edabfeffd791c4cd710..58c64231cb53c1204ceed70b0ea030a7050ebb95 100644 (file)
@@ -20,7 +20,16 @@ var (
        errQueueFull   = errors.New("client queue full")
        errFrameTooBig = errors.New("frame too big")
 
-       sendObjectAttributes = []string{"state", "name", "owner_uuid", "portable_data_hash"}
+       // Send clients only these keys from the
+       // log.properties.old_attributes and
+       // log.properties.new_attributes hashes.
+       sendObjectAttributes = []string{
+               "is_trashed",
+               "name",
+               "owner_uuid",
+               "portable_data_hash",
+               "state",
+       }
 
        v0subscribeOK   = []byte(`{"status":200}`)
        v0subscribeFail = []byte(`{"status":400}`)
@@ -90,7 +99,17 @@ func (sess *v0session) EventMessage(e *event) ([]byte, error) {
                return nil, nil
        }
 
-       ok, err := sess.permChecker.Check(detail.ObjectUUID)
+       var permTarget string
+       if detail.EventType == "delete" {
+               // It's pointless to check permission by reading
+               // ObjectUUID if it has just been deleted, but if the
+               // client has permission on the parent project then
+               // it's OK to send the event.
+               permTarget = detail.ObjectOwnerUUID
+       } else {
+               permTarget = detail.ObjectUUID
+       }
+       ok, err := sess.permChecker.Check(permTarget)
        if err != nil || !ok {
                return nil, err
        }
@@ -143,7 +162,7 @@ func (sub *v0subscribe) sendOldEvents(sess *v0session) {
        if sub.LastLogID == 0 {
                return
        }
-       sess.log.WithField("LastLogID", sub.LastLogID).Debug("getOldEvents")
+       sess.log.WithField("LastLogID", sub.LastLogID).Debug("sendOldEvents")
        // Here we do a "select id" query and queue an event for every
        // log since the given ID, then use (*event)Detail() to
        // retrieve the whole row and decide whether to send it. This
@@ -158,17 +177,26 @@ func (sub *v0subscribe) sendOldEvents(sess *v0session) {
                sub.LastLogID,
                time.Now().UTC().Add(-10*time.Minute).Format(time.RFC3339Nano))
        if err != nil {
-               sess.log.WithError(err).Error("db.Query failed")
+               sess.log.WithError(err).Error("sendOldEvents db.Query failed")
                return
        }
-       defer rows.Close()
+
+       var ids []uint64
        for rows.Next() {
                var id uint64
                err := rows.Scan(&id)
                if err != nil {
-                       sess.log.WithError(err).Error("row Scan failed")
+                       sess.log.WithError(err).Error("sendOldEvents row Scan failed")
                        continue
                }
+               ids = append(ids, id)
+       }
+       if err := rows.Err(); err != nil {
+               sess.log.WithError(err).Error("sendOldEvents db.Query failed")
+       }
+       rows.Close()
+
+       for _, id := range ids {
                for len(sess.sendq)*2 > cap(sess.sendq) {
                        // Ugly... but if we fill up the whole client
                        // queue with a backlog of old events, a
@@ -193,9 +221,6 @@ func (sub *v0subscribe) sendOldEvents(sess *v0session) {
                        }
                }
        }
-       if err := rows.Err(); err != nil {
-               sess.log.WithError(err).Error("db.Query failed")
-       }
 }
 
 type v0subscribe struct {
index 1213be5d140555f0a533cfc4aa639ef9959e7db2..9f743e0b5e3d58312d2b3a2636b148bd493b51e0 100644 (file)
@@ -11,6 +11,7 @@ import (
        "io"
        "net/url"
        "os"
+       "sync"
        "time"
 
        "git.curoverse.com/arvados.git/sdk/go/arvados"
@@ -32,14 +33,29 @@ type v0Suite struct {
        serverSuite serverSuite
        token       string
        toDelete    []string
+       wg          sync.WaitGroup
+       ignoreLogID uint64
 }
 
 func (s *v0Suite) SetUpTest(c *check.C) {
        s.serverSuite.SetUpTest(c)
+       go s.serverSuite.srv.Run()
+       s.serverSuite.srv.WaitReady()
+
        s.token = arvadostest.ActiveToken
+       s.ignoreLogID = s.lastLogID(c)
+}
+
+func (s *v0Suite) TearDownTest(c *check.C) {
+       s.wg.Wait()
+       s.serverSuite.srv.Close()
 }
 
 func (s *v0Suite) TearDownSuite(c *check.C) {
+       s.deleteTestObjects(c)
+}
+
+func (s *v0Suite) deleteTestObjects(c *check.C) {
        ac := arvados.NewClientFromEnv()
        ac.AuthToken = arvadostest.AdminToken
        for _, path := range s.toDelete {
@@ -48,11 +64,11 @@ func (s *v0Suite) TearDownSuite(c *check.C) {
                        panic(err)
                }
        }
+       s.toDelete = nil
 }
 
 func (s *v0Suite) TestFilters(c *check.C) {
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
+       conn, r, w := s.testClient()
        defer conn.Close()
 
        c.Check(w.Encode(map[string]interface{}{
@@ -67,15 +83,40 @@ func (s *v0Suite) TestFilters(c *check.C) {
 }
 
 func (s *v0Suite) TestLastLogID(c *check.C) {
-       var lastID uint64
-       c.Assert(testDB().QueryRow(`SELECT MAX(id) FROM logs`).Scan(&lastID), check.IsNil)
+       lastID := s.lastLogID(c)
 
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
-       defer conn.Close()
+       checkLogs := func(r *json.Decoder, uuid string) {
+               for _, etype := range []string{"create", "blip", "update"} {
+                       lg := s.expectLog(c, r)
+                       for lg.ObjectUUID != uuid {
+                               lg = s.expectLog(c, r)
+                       }
+                       c.Check(lg.EventType, check.Equals, etype)
+               }
+       }
 
-       uuidChan := make(chan string, 2)
+       // Connecting connEarly (before sending the early events) lets
+       // us confirm all of the "early" events have already passed
+       // through the server.
+       connEarly, rEarly, wEarly := s.testClient()
+       defer connEarly.Close()
+       c.Check(wEarly.Encode(map[string]interface{}{
+               "method": "subscribe",
+       }), check.IsNil)
+       s.expectStatus(c, rEarly, 200)
+
+       // Send the early events.
+       uuidChan := make(chan string, 1)
        s.emitEvents(uuidChan)
+       uuidEarly := <-uuidChan
+
+       // Wait for the early events to pass through.
+       checkLogs(rEarly, uuidEarly)
+
+       // Connect the client that wants to get old events via
+       // last_log_id.
+       conn, r, w := s.testClient()
+       defer conn.Close()
 
        c.Check(w.Encode(map[string]interface{}{
                "method":      "subscribe",
@@ -83,36 +124,13 @@ func (s *v0Suite) TestLastLogID(c *check.C) {
        }), check.IsNil)
        s.expectStatus(c, r, 200)
 
-       avoidRace := make(chan struct{}, cap(uuidChan))
-       go func() {
-               // When last_log_id is given, although v0session sends
-               // old events in order, and sends new events in order,
-               // it doesn't necessarily finish sending all old
-               // events before sending any new events. To avoid
-               // hitting this bug in the test, we wait for the old
-               // events to arrive before emitting any new events.
-               <-avoidRace
-               s.emitEvents(uuidChan)
-               close(uuidChan)
-       }()
-
-       go func() {
-               for uuid := range uuidChan {
-                       for _, etype := range []string{"create", "blip", "update"} {
-                               lg := s.expectLog(c, r)
-                               for lg.ObjectUUID != uuid {
-                                       lg = s.expectLog(c, r)
-                               }
-                               c.Check(lg.EventType, check.Equals, etype)
-                       }
-                       avoidRace <- struct{}{}
-               }
-       }()
+       checkLogs(r, uuidEarly)
+       s.emitEvents(uuidChan)
+       checkLogs(r, <-uuidChan)
 }
 
 func (s *v0Suite) TestPermission(c *check.C) {
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
+       conn, r, w := s.testClient()
        defer conn.Close()
 
        c.Check(w.Encode(map[string]interface{}{
@@ -137,9 +155,71 @@ func (s *v0Suite) TestPermission(c *check.C) {
        }
 }
 
+// Two users create private objects; admin deletes both objects; each
+// user receives a "delete" event for their own object (not for the
+// other user's object).
+func (s *v0Suite) TestEventTypeDelete(c *check.C) {
+       clients := []struct {
+               token string
+               uuid  string
+               conn  *websocket.Conn
+               r     *json.Decoder
+               w     *json.Encoder
+       }{{token: arvadostest.ActiveToken}, {token: arvadostest.SpectatorToken}}
+       for i := range clients {
+               uuidChan := make(chan string, 1)
+               s.token = clients[i].token
+               s.emitEvents(uuidChan)
+               clients[i].uuid = <-uuidChan
+               clients[i].conn, clients[i].r, clients[i].w = s.testClient()
+
+               c.Check(clients[i].w.Encode(map[string]interface{}{
+                       "method": "subscribe",
+               }), check.IsNil)
+               s.expectStatus(c, clients[i].r, 200)
+       }
+
+       s.ignoreLogID = s.lastLogID(c)
+       s.deleteTestObjects(c)
+
+       for _, client := range clients {
+               lg := s.expectLog(c, client.r)
+               c.Check(lg.ObjectUUID, check.Equals, client.uuid)
+               c.Check(lg.EventType, check.Equals, "delete")
+       }
+}
+
+// Trashing/deleting a collection produces an "update" event with
+// properties["new_attributes"]["is_trashed"] == true.
+func (s *v0Suite) TestTrashedCollection(c *check.C) {
+       ac := arvados.NewClientFromEnv()
+       ac.AuthToken = s.token
+
+       coll := &arvados.Collection{ManifestText: ""}
+       err := ac.RequestAndDecode(coll, "POST", "arvados/v1/collections", s.jsonBody("collection", coll), map[string]interface{}{"ensure_unique_name": true})
+       c.Assert(err, check.IsNil)
+       s.ignoreLogID = s.lastLogID(c)
+
+       conn, r, w := s.testClient()
+       defer conn.Close()
+
+       c.Check(w.Encode(map[string]interface{}{
+               "method": "subscribe",
+       }), check.IsNil)
+       s.expectStatus(c, r, 200)
+
+       err = ac.RequestAndDecode(nil, "DELETE", "arvados/v1/collections/"+coll.UUID, nil, nil)
+       c.Assert(err, check.IsNil)
+
+       lg := s.expectLog(c, r)
+       c.Check(lg.ObjectUUID, check.Equals, coll.UUID)
+       c.Check(lg.EventType, check.Equals, "update")
+       c.Check(lg.Properties["old_attributes"].(map[string]interface{})["is_trashed"], check.Equals, false)
+       c.Check(lg.Properties["new_attributes"].(map[string]interface{})["is_trashed"], check.Equals, true)
+}
+
 func (s *v0Suite) TestSendBadJSON(c *check.C) {
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
+       conn, r, w := s.testClient()
        defer conn.Close()
 
        c.Check(w.Encode(map[string]interface{}{
@@ -158,8 +238,7 @@ func (s *v0Suite) TestSendBadJSON(c *check.C) {
 }
 
 func (s *v0Suite) TestSubscribe(c *check.C) {
-       srv, conn, r, w := s.testClient()
-       defer srv.Close()
+       conn, r, w := s.testClient()
        defer conn.Close()
 
        s.emitEvents(nil)
@@ -190,6 +269,9 @@ func (s *v0Suite) TestSubscribe(c *check.C) {
 // created workflow. If uuidChan is not nil, send the new workflow
 // UUID to uuidChan as soon as it's known.
 func (s *v0Suite) emitEvents(uuidChan chan<- string) {
+       s.wg.Add(1)
+       defer s.wg.Done()
+
        ac := arvados.NewClientFromEnv()
        ac.AuthToken = s.token
        wf := &arvados.Workflow{
@@ -240,7 +322,9 @@ func (s *v0Suite) expectLog(c *check.C, r *json.Decoder) *arvados.Log {
        lg := &arvados.Log{}
        ok := make(chan struct{})
        go func() {
-               c.Check(r.Decode(lg), check.IsNil)
+               for lg.ID <= s.ignoreLogID {
+                       c.Check(r.Decode(lg), check.IsNil)
+               }
                close(ok)
        }()
        select {
@@ -251,9 +335,7 @@ func (s *v0Suite) expectLog(c *check.C, r *json.Decoder) *arvados.Log {
        }
 }
 
-func (s *v0Suite) testClient() (*server, *websocket.Conn, *json.Decoder, *json.Encoder) {
-       go s.serverSuite.srv.Run()
-       s.serverSuite.srv.WaitReady()
+func (s *v0Suite) testClient() (*websocket.Conn, *json.Decoder, *json.Encoder) {
        srv := s.serverSuite.srv
        conn, err := websocket.Dial("ws://"+srv.listener.Addr().String()+"/websocket?api_token="+s.token, "", "http://"+srv.listener.Addr().String())
        if err != nil {
@@ -261,5 +343,11 @@ func (s *v0Suite) testClient() (*server, *websocket.Conn, *json.Decoder, *json.E
        }
        w := json.NewEncoder(conn)
        r := json.NewDecoder(conn)
-       return srv, conn, r, w
+       return conn, r, w
+}
+
+func (s *v0Suite) lastLogID(c *check.C) uint64 {
+       var lastID uint64
+       c.Assert(testDB().QueryRow(`SELECT MAX(id) FROM logs`).Scan(&lastID), check.IsNil)
+       return lastID
 }
index 32a9e7dbfdba2f85b5579fa1a8b4b838f44757ce..f87cccadbfac636f7fa4f9249363c842977f3446 100644 (file)
@@ -3,4 +3,5 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 include agpl-3.0.txt
+include crunchstat_summary/dygraphs.js
 include crunchstat_summary/chartjs.js
diff --git a/tools/crunchstat-summary/crunchstat_summary/chartjs.js b/tools/crunchstat-summary/crunchstat_summary/chartjs.js
deleted file mode 100644 (file)
index 1a84aab..0000000
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-window.onload = function() {
-    var charts = {};
-    sections.forEach(function(section, section_idx) {
-        var h1 = document.createElement('h1');
-        h1.appendChild(document.createTextNode(section.label));
-        document.body.appendChild(h1);
-        section.charts.forEach(function(data, chart_idx) {
-            // Skip chart if every series has zero data points
-            if (0 == data.data.reduce(function(len, series) {
-                return len + series.dataPoints.length;
-            }, 0)) {
-                return;
-            }
-            var id = 'chart-'+section_idx+'-'+chart_idx;
-            var div = document.createElement('div');
-            div.setAttribute('id', id);
-            div.setAttribute('style', 'width: 100%; height: 150px');
-            document.body.appendChild(div);
-            charts[id] = new CanvasJS.Chart(id, data);
-            charts[id].render();
-        });
-    });
-
-    if (typeof window.debug === 'undefined')
-        window.debug = {};
-    window.debug.charts = charts;
-};
diff --git a/tools/crunchstat-summary/crunchstat_summary/chartjs.py b/tools/crunchstat-summary/crunchstat_summary/chartjs.py
deleted file mode 100644 (file)
index bd8af8c..0000000
+++ /dev/null
@@ -1,88 +0,0 @@
-# Copyright (C) The Arvados Authors. All rights reserved.
-#
-# SPDX-License-Identifier: AGPL-3.0
-
-from __future__ import print_function
-
-import cgi
-import json
-import math
-import pkg_resources
-
-from crunchstat_summary import logger
-
-
-class ChartJS(object):
-    JSLIB = 'https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.min.js'
-
-    def __init__(self, label, summarizers):
-        self.label = label
-        self.summarizers = summarizers
-
-    def html(self):
-        return '''<!doctype html><html><head>
-        <title>{} stats</title>
-        <script type="text/javascript" src="{}"></script>
-        <script type="text/javascript">{}</script>
-        </head><body></body></html>
-        '''.format(cgi.escape(self.label), self.JSLIB, self.js())
-
-    def js(self):
-        return 'var sections = {};\n{}'.format(
-            json.dumps(self.sections()),
-            pkg_resources.resource_string('crunchstat_summary', 'chartjs.js'))
-
-    def sections(self):
-        return [
-            {
-                'label': s.long_label(),
-                'charts': self.charts(s.label, s.tasks),
-            }
-            for s in self.summarizers]
-
-    def _axisY(self, tasks, stat):
-        ymax = 1
-        for task in tasks.itervalues():
-            for pt in task.series[stat]:
-                ymax = max(ymax, pt[1])
-        ytick = math.exp((1+math.floor(math.log(ymax, 2)))*math.log(2))/4
-        return {
-            'gridColor': '#cccccc',
-            'gridThickness': 1,
-            'interval': ytick,
-            'minimum': 0,
-            'maximum': ymax,
-            'valueFormatString': "''",
-        }
-
-    def charts(self, label, tasks):
-        return [
-            {
-                'axisY': self._axisY(tasks=tasks, stat=stat),
-                'data': [
-                    {
-                        'type': 'line',
-                        'markerType': 'none',
-                        'dataPoints': self._datapoints(
-                            label=uuid, task=task, series=task.series[stat]),
-                    }
-                    for uuid, task in tasks.iteritems()
-                ],
-                'title': {
-                    'text': '{}: {} {}'.format(label, stat[0], stat[1]),
-                },
-                'zoomEnabled': True,
-            }
-            for stat in (('cpu', 'user+sys__rate'),
-                         ('mem', 'rss'),
-                         ('net:eth0', 'tx+rx__rate'),
-                         ('net:keep0', 'tx+rx__rate'))]
-
-    def _datapoints(self, label, task, series):
-        points = [
-            {'x': pt[0].total_seconds(), 'y': pt[1]}
-            for pt in series]
-        if len(points) > 0:
-            points[-1]['markerType'] = 'cross'
-            points[-1]['markerSize'] = 12
-        return points
diff --git a/tools/crunchstat-summary/crunchstat_summary/dygraphs.js b/tools/crunchstat-summary/crunchstat_summary/dygraphs.js
new file mode 100644 (file)
index 0000000..6a6225d
--- /dev/null
@@ -0,0 +1,63 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+window.onload = function() {
+    var charts = {};
+    var fmt = {
+        iso: function(y) {
+            var s='';
+            if (y > 1000000000) { y=y/1000000000; s='G'; }
+            else if (y > 1000000) { y=y/1000000; s='M'; }
+            else if (y > 1000) { y=y/1000; s='K'; }
+            return y.toFixed(2).replace(/\.0+$/, '')+s;
+        },
+        time: function(s) {
+            var u='s';
+            if (s < 60) return s;
+            var u = 'm'+(s%60)+'s';
+            var m = Math.floor(s/60);
+            if (m < 60) return ''+m+u;
+            u = 'h'+(m%60)+u;
+            var h = Math.floor(m/60);
+            if (h < 24) return ''+h+u;
+            u = 'd'+(h%24)+s;
+            return ''+Math.floor(h/24)+u;
+        },
+    }
+    chartdata.forEach(function(section, section_idx) {
+        var h1 = document.createElement('h1');
+        h1.appendChild(document.createTextNode(section.label));
+        document.body.appendChild(h1);
+        section.charts.forEach(function(chart, chart_idx) {
+            // Skip chart if every series has zero data points
+            if (0 == chart.data.reduce(function(len, series) {
+                return len + series.length;
+            }, 0)) {
+                return;
+            }
+            var id = 'chart-'+section_idx+'-'+chart_idx;
+            var div = document.createElement('div');
+            div.setAttribute('id', id);
+            div.setAttribute('style', 'width: 100%; height: 150px');
+            document.body.appendChild(div);
+            chart.options.valueFormatter = function(y) {
+            }
+            chart.options.axes = {
+                x: {
+                    axisLabelFormatter: fmt.time,
+                    valueFormatter: fmt.time,
+                },
+                y: {
+                    axisLabelFormatter: fmt.iso,
+                    valueFormatter: fmt.iso,
+                },
+            }
+            charts[id] = new Dygraph(div, chart.data, chart.options);
+        });
+    });
+
+    if (typeof window.debug === 'undefined')
+        window.debug = {};
+    window.debug.charts = charts;
+};
diff --git a/tools/crunchstat-summary/crunchstat_summary/dygraphs.py b/tools/crunchstat-summary/crunchstat_summary/dygraphs.py
new file mode 100644 (file)
index 0000000..e72832e
--- /dev/null
@@ -0,0 +1,33 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+import crunchstat_summary.webchart
+
+
+class DygraphsChart(crunchstat_summary.webchart.WebChart):
+    CSS = 'https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.0.0/dygraph.min.css'
+    JSLIB = 'https://cdnjs.cloudflare.com/ajax/libs/dygraph/2.0.0/dygraph.min.js'
+    JSASSET = 'dygraphs.js'
+
+    def headHTML(self):
+        return '<link rel="stylesheet" href="{}">\n'.format(self.CSS)
+
+    def chartdata(self, label, tasks, stat):
+        return {
+            'data': self._collate_data(tasks, stat),
+            'options': {
+                'connectSeparatedPoints': True,
+                'labels': ['elapsed']+[uuid for uuid, _ in tasks.iteritems()],
+                'title': '{}: {} {}'.format(label, stat[0], stat[1]),
+            },
+        }
+
+    def _collate_data(self, tasks, stat):
+        data = []
+        nulls = []
+        for uuid, task in tasks.iteritems():
+            for pt in task.series[stat]:
+                data.append([pt[0].total_seconds()] + nulls + [pt[1]])
+            nulls.append(None)
+        return sorted(data)
index 9b8410e9aafcb9a6b22fbe8aa21a0aa4bb0a27d7..d51c6e34e0d4c8036ac9a9c13d9b4d2a66018089 100644 (file)
@@ -6,7 +6,7 @@ from __future__ import print_function
 
 import arvados
 import collections
-import crunchstat_summary.chartjs
+import crunchstat_summary.dygraphs
 import crunchstat_summary.reader
 import datetime
 import functools
@@ -31,6 +31,9 @@ AVAILABLE_RAM_RATIO = 0.95
 datetime.datetime.strptime('1999-12-31_23:59:59', '%Y-%m-%d_%H:%M:%S')
 
 
+WEBCHART_CLASS = crunchstat_summary.dygraphs.DygraphsChart
+
+
 class Task(object):
     def __init__(self):
         self.starttime = None
@@ -217,7 +220,7 @@ class Summarizer(object):
             self._recommend_gen())) + "\n"
 
     def html_report(self):
-        return crunchstat_summary.chartjs.ChartJS(self.label, [self]).html()
+        return WEBCHART_CLASS(self.label, [self]).html()
 
     def _text_report_gen(self):
         yield "\t".join(['category', 'metric', 'task_max', 'task_max_rate', 'job_total'])
@@ -446,5 +449,4 @@ class PipelineSummarizer(object):
         return txt
 
     def html_report(self):
-        return crunchstat_summary.chartjs.ChartJS(
-            self.label, self.summarizers.itervalues()).html()
+        return WEBCHART_CLASS(self.label, self.summarizers.itervalues()).html()
diff --git a/tools/crunchstat-summary/crunchstat_summary/webchart.py b/tools/crunchstat-summary/crunchstat_summary/webchart.py
new file mode 100644 (file)
index 0000000..790b08d
--- /dev/null
@@ -0,0 +1,61 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+import cgi
+import json
+import pkg_resources
+
+
+class WebChart(object):
+    """Base class for a web chart.
+
+    Subclasses must assign JSLIB and JSASSET, and override the
+    chartdata() method.
+    """
+    JSLIB = None
+    JSASSET = None
+
+    def __init__(self, label, summarizers):
+        self.label = label
+        self.summarizers = summarizers
+
+    def html(self):
+        return '''<!doctype html><html><head>
+        <title>{} stats</title>
+        <script type="text/javascript" src="{}"></script>
+        <script type="text/javascript">{}</script>
+        {}
+        </head><body></body></html>
+        '''.format(cgi.escape(self.label),
+                   self.JSLIB, self.js(), self.headHTML())
+
+    def js(self):
+        return 'var chartdata = {};\n{}'.format(
+            json.dumps(self.sections()),
+            pkg_resources.resource_string('crunchstat_summary', self.JSASSET))
+
+    def sections(self):
+        return [
+            {
+                'label': s.long_label(),
+                'charts': [
+                    self.chartdata(s.label, s.tasks, stat)
+                    for stat in (('cpu', 'user+sys__rate'),
+                                 ('mem', 'rss'),
+                                 ('net:eth0', 'tx+rx__rate'),
+                                 ('net:keep0', 'tx+rx__rate'))],
+            }
+            for s in self.summarizers]
+
+    def chartdata(self, label, tasks, stat):
+        """Return chart data for the given tasks.
+
+        The returned value will be available on the client side as an
+        element of the "chartdata" array.
+        """
+        raise NotImplementedError()
+
+    def headHTML(self):
+        """Return extra HTML text to include in HEAD."""
+        return ''
index 938f6a438172c668aba980d9a585baaaf9ba846f..958c81d4df8f408ade30f5047c2996af35e8c271 100644 (file)
@@ -3,22 +3,22 @@
        "ignore": "test",
        "package": [
                {
-                       "checksumSHA1": "jf7K+UTQNIzRdlG5F4zX/8b++/E=",
+                       "checksumSHA1": "b68aaMZImS90FjnReAxpbp20FGA=",
+                       "origin": "github.com/curoverse/goamz/aws",
                        "path": "github.com/AdRoll/goamz/aws",
-                       "revision": "c5d7d9bd6c743fae44efc6c18450282022445ffc",
-                       "revisionTime": "2017-02-25T09:28:51Z"
+                       "revision": "21e563311c2dc5ac53464a2c31cb91fb833c6cb9"
                },
                {
                        "checksumSHA1": "ey9ddXTW9dncjJz/COKpeYm+sgg=",
+                       "origin": "github.com/curoverse/goamz/s3",
                        "path": "github.com/AdRoll/goamz/s3",
-                       "revision": "c5d7d9bd6c743fae44efc6c18450282022445ffc",
-                       "revisionTime": "2017-02-25T09:28:51Z"
+                       "revision": "21e563311c2dc5ac53464a2c31cb91fb833c6cb9"
                },
                {
                        "checksumSHA1": "pDHYVqUQtRsPYw/X4kUrdK7pxMs=",
+                       "origin": "github.com/curoverse/goamz/s3/s3test",
                        "path": "github.com/AdRoll/goamz/s3/s3test",
-                       "revision": "c5d7d9bd6c743fae44efc6c18450282022445ffc",
-                       "revisionTime": "2017-02-25T09:28:51Z"
+                       "revision": "21e563311c2dc5ac53464a2c31cb91fb833c6cb9"
                },
                {
                        "checksumSHA1": "Rjy2uYZkQ8Kjht6ZFU0qzm2I/kI=",
                        "revision": "1620af6b32398bfc91827ceae54a8cc1f55df04d",
                        "revisionTime": "2016-12-14T20:08:43Z"
                },
-               {
-                       "checksumSHA1": "qjY3SPlNvqT179DPiRaIsRhYZQI=",
-                       "origin": "github.com/docker/docker/vendor/github.com/docker/distribution",
-                       "path": "github.com/docker/distribution",
-                       "revision": "280327cb4d1e1fe4f118d00596ce0b3a6ae6d07e",
-                       "revisionTime": "2017-05-17T20:48:28Z"
-               },
-               {
-                       "checksumSHA1": "0au+tD+jymXNssdb1JgcctY7PN4=",
-                       "origin": "github.com/docker/docker/vendor/github.com/docker/distribution/context",
-                       "path": "github.com/docker/distribution/context",
-                       "revision": "280327cb4d1e1fe4f118d00596ce0b3a6ae6d07e",
-                       "revisionTime": "2017-05-17T20:48:28Z"
-               },
                {
                        "checksumSHA1": "Gj+xR1VgFKKmFXYOJMnAczC3Znk=",
                        "origin": "github.com/docker/docker/vendor/github.com/docker/distribution/digestset",
                        "revision": "280327cb4d1e1fe4f118d00596ce0b3a6ae6d07e",
                        "revisionTime": "2017-05-17T20:48:28Z"
                },
-               {
-                       "checksumSHA1": "oYy5Q1HBImMQvh9t96cmNzWar80=",
-                       "origin": "github.com/docker/docker/vendor/github.com/docker/distribution/manifest",
-                       "path": "github.com/docker/distribution/manifest",
-                       "revision": "280327cb4d1e1fe4f118d00596ce0b3a6ae6d07e",
-                       "revisionTime": "2017-05-17T20:48:28Z"
-               },
-               {
-                       "checksumSHA1": "SK1g7ll2cPbgDyWpK0oVT9beVZY=",
-                       "origin": "github.com/docker/docker/vendor/github.com/docker/distribution/manifest/manifestlist",
-                       "path": "github.com/docker/distribution/manifest/manifestlist",
-                       "revision": "280327cb4d1e1fe4f118d00596ce0b3a6ae6d07e",
-                       "revisionTime": "2017-05-17T20:48:28Z"
-               },
                {
                        "checksumSHA1": "m4wEFD0Mh+ClfprUqgl0GyNmk7Q=",
                        "origin": "github.com/docker/docker/vendor/github.com/docker/distribution/reference",
                        "revision": "280327cb4d1e1fe4f118d00596ce0b3a6ae6d07e",
                        "revisionTime": "2017-05-17T20:48:28Z"
                },
-               {
-                       "checksumSHA1": "cNp7rNReJHvdSfrIetXS9RGsLSo=",
-                       "origin": "github.com/docker/docker/vendor/github.com/docker/distribution/uuid",
-                       "path": "github.com/docker/distribution/uuid",
-                       "revision": "280327cb4d1e1fe4f118d00596ce0b3a6ae6d07e",
-                       "revisionTime": "2017-05-17T20:48:28Z"
-               },
                {
                        "checksumSHA1": "5b7eC73lORtIUFCjz548jXkLlKU=",
                        "path": "github.com/docker/docker/api",