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>
--- /dev/null
+# 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
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") &&
# 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
# 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
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]
--- /dev/null
+# 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
["filename.xml", true],
["filename.xsl", true],
["filename.yml", true],
+ ["filename.yaml", true],
+ ["filename.bed", true],
+ ["filename.cwl", true],
["filename.bam", false],
["filename.tar", false],
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")
# 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
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
#
# 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
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/*
# 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 && \
/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"]
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
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"]
--- /dev/null
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1
+
+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
+/4cVEpiwwKMkxIWrseXTtsKJ8+hWhElzLL8cNTFCz+4ax7PKE8eCGTUXcyMyzAe0
+SysHI2bCWFfQe8RGEy+1cnLH5+qO2MwLDKKN4+at2ADZFrYn4FEZ8wvlb9/is1iN
+7h7+s2GDPpvEJrrnpeHeNTX1BEeykvbjW+Bsb89g6kJfks7PvhD+BQwVMaNrmjR8
+KYi+GaopHdQ30yLWD4ck97B+UYZnqSKk7E8NOpwU22b31VlZgI/JXWeXgQ5cY58J
+0ShGmbfypHXerthzlm6UHzjg6g+yujqDrxaOPTdfxYAzJmX5ztk3D9xMEhCAD8yp
+efTdMGQb0B2q5Yjsl3sHqNNqxTeg1iorUNz6UtEhX3RmGrTDdY0rgn0prjgiMTEM
+WWxAmgeYw/gOkpWfEZNrX64La65OkhTg+aBZEe6broHrc1x6dXpoRkxGJrlRfcsx
+Ocgy2M0/C0gk1L1yYWXRL4yENkQ/hNuPZmdDtrvWxcSTnMPfclwq6kklflC099uj
+CZf5NuOG80mZi03917LQBtgWp4INmG+B6aLY52KuINmWXm25wYPY5MfEbyO2Kzng
+JSpxdR90GbP4F5lri2Ho9eSByisb0dLq0ugBEAABAQAAAAAAAAAAAAAAAP/Y/+AA
+EEpGSUYAAQEBAGAAYAAA/+EAQEV4aWYAAElJKgAIAAAAAQBphwQAAQAAABoAAAAA
+AAAAAgACoAkAAQAAAJ8AAAADoAkAAQAAAJ8AAAAAAAAA/9sAQwAIBgYHBgUIBwcH
+CQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04
+MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAnwCfAwEiAAIRAQMRAf/E
+AB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQE
+AAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBka
+JSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SF
+hoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY
+2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgME
+BQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKB
+CBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNU
+VVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ip
+qrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/a
+AAwDAQACEQMRAD8A9YFKTzRikxikSLTutMpw6UAKBRxQc9qOaYCe1I2BznisHX/F
+dnoSFW/fXH/PNT0+p7V5J4k8calrExRZXgg/55xEgd+vrWsKTlqcdXGQjLljqz2T
+UvEekaTj7ZfRIx/hzk/kKoQ+PvDMzhBqiBiM/MrAfnjFfP085kIBYsR+NN+6gYnB
+7YrT2MTH63U3PoR/HXhqPJ/tSM4/uqx/kKt6d4p0TU5PLtNSheT+6x2n8jivnaJg
+Y8k80hlYEccU/YR7krGVL7H1CCGGVII9jTq+crDxXqWkSIba8lXB4Utkfka9C8O/
+Fe3vJEt9Xg8h+nnpypPuO1ZyotbanRTxal8SseldTQRmo4Z47iJZInV42GQynINS
+8isTrTuJ04oNGaSgYn1opN3PSlwKAHGlptLSAMYpRSHmkBzQA+qerX8emafLdSZw
+o4x69qtj61w/xGvzHawWcbcsDI/8h/WtqMOeaRy4ys6VFyW55V4nvp7jUZHMpO87
+ic9c1iIk0gOwE+tdr4Z8HXPia482bKWqHBf19q9V0/wHo9lEgFsrMv8AEwrerVjB
+2OHC4WpOmrfefP8AHpV4SCsTE4yRtNWf7DvPL3NA+BzX0eugWIx+4TI74qZ9EtGT
+aYUwO2Kw+sdkdf1CT3kfNqac8Iw8Lg9uKryQYypVlr6Kn8L2MnWFfxFYOqeB7O4B
+8uMBs5qvrK6oyeW1L3TPBpok5yeRUEbmE9Rkd69UvvhqzZMbY9hXI6v4Lv8AT4jI
+IyyDrgU41oN6Ml4erFe8tDX+H/jT+xrs2d27NaTsOevln1r28HIyDwa+UlLRTLkY
+IPIr6D8AazJq/htDK+6aBvLY47dv0pVVdcyNcNPll7N9djqqPwopCawO8UYopM4p
+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
+aXOo2lPgP440izPGh5s38Oc=
+=8iHq
+-----END PGP PUBLIC KEY BLOCK-----
--- /dev/null
+# 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"]
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
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"]
# 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
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"]
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
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"]
# 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
# 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 && \
--- /dev/null
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+Version: GnuPG v1
+
+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
+/4cVEpiwwKMkxIWrseXTtsKJ8+hWhElzLL8cNTFCz+4ax7PKE8eCGTUXcyMyzAe0
+SysHI2bCWFfQe8RGEy+1cnLH5+qO2MwLDKKN4+at2ADZFrYn4FEZ8wvlb9/is1iN
+7h7+s2GDPpvEJrrnpeHeNTX1BEeykvbjW+Bsb89g6kJfks7PvhD+BQwVMaNrmjR8
+KYi+GaopHdQ30yLWD4ck97B+UYZnqSKk7E8NOpwU22b31VlZgI/JXWeXgQ5cY58J
+0ShGmbfypHXerthzlm6UHzjg6g+yujqDrxaOPTdfxYAzJmX5ztk3D9xMEhCAD8yp
+efTdMGQb0B2q5Yjsl3sHqNNqxTeg1iorUNz6UtEhX3RmGrTDdY0rgn0prjgiMTEM
+WWxAmgeYw/gOkpWfEZNrX64La65OkhTg+aBZEe6broHrc1x6dXpoRkxGJrlRfcsx
+Ocgy2M0/C0gk1L1yYWXRL4yENkQ/hNuPZmdDtrvWxcSTnMPfclwq6kklflC099uj
+CZf5NuOG80mZi03917LQBtgWp4INmG+B6aLY52KuINmWXm25wYPY5MfEbyO2Kzng
+JSpxdR90GbP4F5lri2Ho9eSByisb0dLq0ugBEAABAQAAAAAAAAAAAAAAAP/Y/+AA
+EEpGSUYAAQEBAGAAYAAA/+EAQEV4aWYAAElJKgAIAAAAAQBphwQAAQAAABoAAAAA
+AAAAAgACoAkAAQAAAJ8AAAADoAkAAQAAAJ8AAAAAAAAA/9sAQwAIBgYHBgUIBwcH
+CQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04
+MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy
+MjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAnwCfAwEiAAIRAQMRAf/E
+AB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQE
+AAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBka
+JSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SF
+hoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY
+2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgME
+BQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKB
+CBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNU
+VVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ip
+qrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/a
+AAwDAQACEQMRAD8A9YFKTzRikxikSLTutMpw6UAKBRxQc9qOaYCe1I2BznisHX/F
+dnoSFW/fXH/PNT0+p7V5J4k8calrExRZXgg/55xEgd+vrWsKTlqcdXGQjLljqz2T
+UvEekaTj7ZfRIx/hzk/kKoQ+PvDMzhBqiBiM/MrAfnjFfP085kIBYsR+NN+6gYnB
+7YrT2MTH63U3PoR/HXhqPJ/tSM4/uqx/kKt6d4p0TU5PLtNSheT+6x2n8jivnaJg
+Y8k80hlYEccU/YR7krGVL7H1CCGGVII9jTq+crDxXqWkSIba8lXB4Utkfka9C8O/
+Fe3vJEt9Xg8h+nnpypPuO1ZyotbanRTxal8SseldTQRmo4Z47iJZInV42GQynINS
+8isTrTuJ04oNGaSgYn1opN3PSlwKAHGlptLSAMYpRSHmkBzQA+qerX8emafLdSZw
+o4x69qtj61w/xGvzHawWcbcsDI/8h/WtqMOeaRy4ys6VFyW55V4nvp7jUZHMpO87
+ic9c1iIk0gOwE+tdr4Z8HXPia482bKWqHBf19q9V0/wHo9lEgFsrMv8AEwrerVjB
+2OHC4WpOmrfefP8AHpV4SCsTE4yRtNWf7DvPL3NA+BzX0eugWIx+4TI74qZ9EtGT
+aYUwO2Kw+sdkdf1CT3kfNqac8Iw8Lg9uKryQYypVlr6Kn8L2MnWFfxFYOqeB7O4B
+8uMBs5qvrK6oyeW1L3TPBpok5yeRUEbmE9Rkd69UvvhqzZMbY9hXI6v4Lv8AT4jI
+IyyDrgU41oN6Ml4erFe8tDX+H/jT+xrs2d27NaTsOevln1r28HIyDwa+UlLRTLkY
+IPIr6D8AazJq/htDK+6aBvLY47dv0pVVdcyNcNPll7N9djqqPwopCawO8UYopM4p
+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-----
--- /dev/null
+# 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
# 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 && \
# 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 && \
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 && \
--- /dev/null
+#!/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"
debian8)
FORMAT=deb
;;
+ debian9)
+ FORMAT=deb
+ ;;
ubuntu1204)
FORMAT=deb
;;
debian8)
FORMAT=deb
;;
+ debian9)
+ FORMAT=deb
+ ;;
ubuntu1204)
FORMAT=deb
;;
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
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
sdk/go/arvadosclient
sdk/go/dispatch
sdk/go/keepclient
+sdk/go/health
sdk/go/httpserver
sdk/go/manifest
sdk/go/blockdigest
sdk/go/arvadosclient
sdk/go/blockdigest
sdk/go/dispatch
+ sdk/go/health
sdk/go/httpserver
sdk/go/manifest
sdk/go/streamer
$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"});
# 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}
}],
{label => "clean work dirs"});
if ($exited != 0) {
- exit(EX_RETRY_UNLOCKED);
+ exit_retry_unlocked();
}
}
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.
$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"],
Log(undef, "Container will run with $dockeruserarg");
}
last;
+ } elsif ($tempfail) {
+ exit_retry_unlocked();
}
}
"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)) {
} 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";
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});
}
}
}
+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
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
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()
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)
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":
def exists(self, fn):
collection, rest = self.get_collection(fn)
- if collection:
+ if collection is not None:
if rest:
return collection.exists(rest)
else:
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
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.
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",
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']),
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)
'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,
'/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'
- 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',
- 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'
--- /dev/null
+// 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(),
+ })
+ }
+ })
+}
--- /dev/null
+// 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), "")
+}
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="""
""")
# 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
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')
# 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')
from __future__ import absolute_import
from future.utils import listitems
import io
+import mock
import os
import re
import shutil
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 = {}
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)
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)
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)
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']
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()
--- /dev/null
+# 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
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
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
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,
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
).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
# 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
# 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
# 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
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]
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
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
--- /dev/null
+# 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
# 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
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
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.*`)
}
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,
}
}
// 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()
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()
import (
"net/http"
+ "git.curoverse.com/arvados.git/sdk/go/health"
"git.curoverse.com/arvados.git/sdk/go/httpserver"
)
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()
package main
import (
+ "net/http"
+ "net/http/httptest"
"os"
"os/exec"
+ "git.curoverse.com/arvados.git/sdk/go/arvadostest"
+
check "gopkg.in/check.v1"
)
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{})
}
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`)
+}
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.
# 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
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()
@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):
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()
def finalize(self):
pass
+
+ def child_event(self, ev):
+ pass
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
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):
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
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."""
],
install_requires=[
'arvados-python-client >= 0.1.20151118035730',
- 'llfuse==0.41.1',
+ 'llfuse>=1.2',
'python-daemon',
'ciso8601',
'setuptools'
import atexit
import functools
import inspect
+import llfuse
import logging
import multiprocessing
import os
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(
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):
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):
var collection *arvados.Collection
if pdh != "" {
- collection = c.lookupCollection(pdh)
+ collection = c.lookupCollection(arv.ApiToken + "\000" + pdh)
}
if collection != nil && permOK {
// 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
}
}
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
}
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)
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)
// 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)
// 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))
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)
}
func (s *UnitSuite) TestCacheForceReloadByUUID(c *check.C) {
- c.Skip("see #11945")
-
arv, err := arvadosclient.MakeArvadosClient()
c.Assert(err, check.Equals, nil)
"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
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) {
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" {
}
}
}
+
+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`)
+}
// 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.
"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
"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"
Timeout arvados.Duration
PIDFile string
Debug bool
+ ManagementToken string
}
func DefaultConfig() *Config {
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"
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")
// 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))
rest.HandleFunc(`/`, h.Options).Methods("OPTIONS")
}
+ rest.Handle("/_health/{check}", &health.Handler{
+ Token: mgmtToken,
+ Prefix: "/_health/",
+ }).Methods("GET")
+
rest.NotFoundHandler = InvalidPathHandler{}
return h
}
// 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
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)
+}
Enable debug logging.
+ManagementToken:
+
+ Authorization token to be included in all health check requests.
+
`, exampleConfigFile)
}
blobSigningKey []byte
systemAuthToken string
debugLogf func(string, ...interface{})
+
+ ManagementToken string
}
var theConfig = DefaultConfig()
"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
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(
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())
+ }
+}
"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"
)
// 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)
'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():
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):
from __future__ import absolute_import, print_function
from future import standard_library
+import json
import requests
import unittest
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
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):
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))
}
func (ps *pgEventSource) DB() *sql.DB {
+ ps.WaitReady()
return ps.db
}
pc.nMisses++
err = pc.RequestAndDecode(&buf, "GET", path, nil, url.Values{
- "select": {`["uuid"]`},
+ "include_trash": {"true"},
+ "select": {`["uuid"]`},
})
var allowed bool
"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"
)
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 {
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),
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())
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}`)
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
}
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
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
}
}
}
- if err := rows.Err(); err != nil {
- sess.log.WithError(err).Error("db.Query failed")
- }
}
type v0subscribe struct {
"io"
"net/url"
"os"
+ "sync"
"time"
"git.curoverse.com/arvados.git/sdk/go/arvados"
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 {
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{}{
}
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",
}), 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{}{
}
}
+// 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{}{
}
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)
// 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{
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 {
}
}
-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 {
}
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
}
# SPDX-License-Identifier: AGPL-3.0
include agpl-3.0.txt
+include crunchstat_summary/dygraphs.js
include crunchstat_summary/chartjs.js
+++ /dev/null
-// 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;
-};
+++ /dev/null
-# 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
--- /dev/null
+// 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;
+};
--- /dev/null
+# 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)
import arvados
import collections
-import crunchstat_summary.chartjs
+import crunchstat_summary.dygraphs
import crunchstat_summary.reader
import datetime
import functools
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
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'])
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()
--- /dev/null
+# 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 ''
"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",