rm -rf "$WORKSPACE/services/dockercleaner/build"
fpm_build $WORKSPACE/services/dockercleaner arvados-docker-cleaner 'Curoverse, Inc.' 'python3' "$(awk '($1 == "Version:"){print $2}' $WORKSPACE/services/dockercleaner/arvados_docker_cleaner.egg-info/PKG-INFO)" "--url=https://arvados.org" "--description=The Arvados Docker image cleaner"
+# The Arvados crunchstat-summary tool
+cd $WORKSPACE/packages/$TARGET
+rm -rf "$WORKSPACE/tools/crunchstat-summary/build"
+fpm_build $WORKSPACE/tools/crunchstat-summary ${PYTHON2_PKG_PREFIX}-crunchstat-summary 'Curoverse, Inc.' 'python' "$(awk '($1 == "Version:"){print $2}' $WORKSPACE/tools/crunchstat-summary/crunchstat_summary.egg-info/PKG-INFO)" "--url=https://arvados.org" "--description=Crunchstat-summary reads Arvados Crunch log files and summarize resource usage"
+
# Forked libcloud
LIBCLOUD_DIR=$(mktemp -d)
(
if ! [[ -e "$venvdest/bin/activate" ]] || ! [[ -e "$venvdest/bin/pip" ]]; then
virtualenv --setuptools "$@" "$venvdest" || fatal "virtualenv $venvdest failed"
fi
- "$venvdest/bin/pip" install 'setuptools>=18' 'pip>=7'
+ if [[ $("$venvdest/bin/python" --version 2>&1) =~ \ 3\.[012]\. ]]; then
+ # pip 8.0.0 dropped support for python 3.2, e.g., debian wheezy
+ "$venvdest/bin/pip" install 'setuptools>=18' 'pip>=7,<8'
+ else
+ "$venvdest/bin/pip" install 'setuptools>=18' 'pip>=7'
+ fi
# ubuntu1404 can't seem to install mock via tests_require, but it can do this.
"$venvdest/bin/pip" install 'mock>=1.0' 'pbr<1.7.0'
}
Log (undef, "docker image hash is $docker_hash");
$docker_stream =~ s/^\.//;
my $docker_install_script = qq{
-if ! $docker_bin images -q --no-trunc --all | grep -qxF \Q$docker_hash\E; then
- arv-get \Q$docker_locator$docker_stream/$docker_hash.tar\E | $docker_bin load
+if $docker_bin images -q --no-trunc --all | grep -qxF \Q$docker_hash\E; then
+ exit 0
+fi
+declare -a exit_codes=("\${PIPESTATUS[@]}")
+if [ 0 != "\${exit_codes[0]}" ]; then
+ exit "\${exit_codes[0]}" # `docker images` failed
+elif [ 1 != "\${exit_codes[1]}" ]; then
+ exit "\${exit_codes[1]}" # `grep` encountered an error
+else
+ # Everything worked fine, but grep didn't find the image on this host.
+ arv-get \Q$docker_locator$docker_stream/$docker_hash.tar\E | $docker_bin load
fi
};
.q{&& SWAP=$(awk '($1 == "SwapTotal:"){print $2}' </proc/meminfo) }
."&& MEMLIMIT=\$(( (\$MEM * 95) / ($ENV{CRUNCH_NODE_SLOTS} * 100) )) "
."&& let SWAPLIMIT=\$MEMLIMIT+\$SWAP "
- ."&& if which crunchrunner >/dev/null ; then VOLUME_CRUNCHRUNNER=\"--volume=\$(which crunchrunner):/usr/local/bin/crunchrunner\" ; fi "
- ."&& if test -f /etc/ssl/certs/ca-certificates.crt ; then VOLUME_CERTS=\"--volume=/etc/ssl/certs/ca-certificates.crt:/etc/arvados/ca-certificates.crt\" ; fi "
- ."&& if test -f /etc/pki/tls/certs/ca-bundle.crt ; then VOLUME_CERTS=\"--volume=/etc/pki/tls/certs/ca-bundle.crt:/etc/arvados/ca-certificates.crt\" ; fi ";
+ # $VOLUME_CRUNCHRUNNER and $VOLUME_CERTS will be passed unquoted as
+ # arguments to `docker run`. They must contain their own quoting.
+ .q{&& VOLUME_CRUNCHRUNNER="" VOLUME_CERTS="" }
+ .q{&& if which crunchrunner >/dev/null ; then VOLUME_CRUNCHRUNNER=--volume=$(which crunchrunner):/usr/local/bin/crunchrunner ; fi }
+ .q{&& if test -f /etc/ssl/certs/ca-certificates.crt ; then VOLUME_CERTS=--volume=/etc/ssl/certs/ca-certificates.crt:/etc/arvados/ca-certificates.crt ; }
+ .q{elif test -f /etc/pki/tls/certs/ca-bundle.crt ; then VOLUME_CERTS=--volume=/etc/pki/tls/certs/ca-bundle.crt:/etc/arvados/ca-certificates.crt ; fi };
$command .= "&& exec arv-mount --read-write --mount-by-pdh=by_pdh --mount-tmp=tmp --crunchstat-interval=10 --allow-other $arv_file_cache \Q$keep_mnt\E --exec ";
$ENV{TASK_KEEPMOUNT} = "$keep_mnt/by_pdh";
# Bind mount the crunchrunner binary and host TLS certificates file into
# the container.
- $command .= "\"\$VOLUME_CRUNCHRUNNER\" \"\$VOLUME_CERTS\" ";
+ $command .= "\$VOLUME_CRUNCHRUNNER \$VOLUME_CERTS ";
while (my ($env_key, $env_val) = each %ENV)
{
self.job_order["cwl:tool"] = workflowmapper.mapper(self.tool.tool["id"])[1]
response = self.arvrunner.api.jobs().create(body={
+ "owner_uuid": self.arvrunner.project_uuid,
"script": "cwl-runner",
"script_version": "master",
"repository": "arvados",
config=$2
shift ; shift
;;
+ -h|--help)
+ echo "$0 [--no-reset-container] [--leave-running] [--config dev|localdemo]"
+ exit
+ ;;
-*)
break
;;
return "%s+%i" % (hashlib.md5(p).hexdigest(), len(p))
keep().put.side_effect = putstub
keepdocker.return_value = True
- api.users().current().execute.return_value = {"uuid": "zzzzz-tpzed-zzzzzzzzzzzzzzz"}
+ user_uuid = "zzzzz-tpzed-zzzzzzzzzzzzzzz"
+ api.users().current().execute.return_value = {"uuid": user_uuid}
api.collections().list().execute.return_value = {"items": []}
api.collections().create().execute.side_effect = ({"uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
"portable_data_hash": "99999999999999999999999999999991+99"},
api.jobs().create.assert_called_with(
body={
+ 'owner_uuid': user_uuid,
'runtime_constraints': {
'docker_image': 'arvados/jobs'
},
- 'script_parameters': {
- 'x': {
- 'path': '99999999999999999999999999999992+99/blorp.txt',
- 'class': 'File'
+ 'script_parameters': {
+ 'x': {
+ 'path': '99999999999999999999999999999992+99/blorp.txt',
+ 'class': 'File'
+ },
+ 'cwl:tool': '99999999999999999999999999999991+99/wf/submit_wf.cwl'
},
- 'cwl:tool': '99999999999999999999999999999991+99/wf/submit_wf.cwl'
+ 'repository': 'arvados',
+ 'script_version': 'master',
+ 'script': 'cwl-runner'
},
- 'repository': 'arvados',
+ find_or_create=True)
+
+ @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
+ @mock.patch("arvados.collection.KeepClient")
+ @mock.patch("arvados.events.subscribe")
+ def test_submit_with_project_uuid(self, events, keep, keepdocker):
+ api = mock.MagicMock()
+ def putstub(p, **kwargs):
+ return "%s+%i" % (hashlib.md5(p).hexdigest(), len(p))
+ keep().put.side_effect = putstub
+ keepdocker.return_value = True
+ api.users().current().execute.return_value = {"uuid": "zzzzz-tpzed-zzzzzzzzzzzzzzz"}
+ api.collections().list().execute.return_value = {"items": []}
+ api.collections().create().execute.side_effect = ({"uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
+ "portable_data_hash": "99999999999999999999999999999991+99"},
+ {"uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
+ "portable_data_hash": "99999999999999999999999999999992+99"})
+ api.jobs().create().execute.return_value = {"uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz", "state": "Queued"}
+ project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
+
+ arvados_cwl.main(["--debug", "--submit", "--project-uuid", project_uuid,
+ "--no-wait", "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
+ sys.stdout, sys.stderr, api_client=api)
+
+ api.jobs().create.assert_called_with(
+ body={
+ 'owner_uuid': project_uuid,
+ 'runtime_constraints': {
+ 'docker_image': 'arvados/jobs'
+ },
+ 'script_parameters': {
+ 'x': {
+ 'path': '99999999999999999999999999999992+99/blorp.txt',
+ 'class': 'File'
+ },
+ 'cwl:tool': '99999999999999999999999999999991+99/wf/submit_wf.cwl'
+ },
+ 'repository': 'arvados',
'script_version': 'master',
'script': 'cwl-runner'
},
_logger = logging.getLogger('arvados.events')
-class EventClient(WebSocketClient):
- def __init__(self, url, filters, on_event, last_log_id):
+class _EventClient(WebSocketClient):
+ def __init__(self, url, filters, on_event, last_log_id, on_closed):
ssl_options = {'ca_certs': arvados.util.ca_certs_path()}
if config.flag_is_true('ARVADOS_API_HOST_INSECURE'):
ssl_options['cert_reqs'] = ssl.CERT_NONE
# IPv4 addresses (common with "localhost"), only one of them
# will be attempted -- and it might not be the right one. See
# ws4py's WebSocketBaseClient.__init__.
- super(EventClient, self).__init__(url, ssl_options=ssl_options)
+ super(_EventClient, self).__init__(url, ssl_options=ssl_options)
+
self.filters = filters
self.on_event = on_event
self.last_log_id = last_log_id
self._closing_lock = threading.RLock()
self._closing = False
self._closed = threading.Event()
+ self.on_closed = on_closed
def opened(self):
- self.subscribe(self.filters, self.last_log_id)
+ for f in self.filters:
+ self.subscribe(f, self.last_log_id)
def closed(self, code, reason=None):
self._closed.set()
+ self.on_closed()
def received_message(self, m):
with self._closing_lock:
:timeout: is the number of seconds to wait for ws4py to
indicate that the connection has closed.
"""
- super(EventClient, self).close(code, reason)
+ super(_EventClient, self).close(code, reason)
with self._closing_lock:
# make sure we don't process any more messages.
self._closing = True
# wait for ws4py to tell us the connection is closed.
self._closed.wait(timeout=timeout)
- def subscribe(self, filters, last_log_id=None):
- m = {"method": "subscribe", "filters": filters}
+ def subscribe(self, f, last_log_id=None):
+ m = {"method": "subscribe", "filters": f}
if last_log_id is not None:
m["last_log_id"] = last_log_id
self.send(json.dumps(m))
- def unsubscribe(self, filters):
- self.send(json.dumps({"method": "unsubscribe", "filters": filters}))
+ def unsubscribe(self, f):
+ self.send(json.dumps({"method": "unsubscribe", "filters": f}))
+
+
+class EventClient(object):
+ def __init__(self, url, filters, on_event_cb, last_log_id):
+ self.url = url
+ if filters:
+ self.filters = [filters]
+ else:
+ self.filters = [[]]
+ self.on_event_cb = on_event_cb
+ self.last_log_id = last_log_id
+ self.is_closed = False
+ self.ec = _EventClient(url, self.filters, self.on_event, last_log_id, self.on_closed)
+
+ def connect(self):
+ self.ec.connect()
+
+ def close_connection(self):
+ self.ec.close_connection()
+
+ def subscribe(self, f, last_log_id=None):
+ self.filters.append(f)
+ self.ec.subscribe(f, last_log_id)
+
+ def unsubscribe(self, f):
+ del self.filters[self.filters.index(f)]
+ self.ec.unsubscribe(f)
+
+ def close(self, code=1000, reason='', timeout=0):
+ self.is_closed = True
+ self.ec.close(code, reason, timeout)
+
+ def on_event(self, m):
+ if m.get('id') != None:
+ self.last_log_id = m.get('id')
+ self.on_event_cb(m)
+
+ def on_closed(self):
+ if self.is_closed == False:
+ _logger.warn("Unexpected close. Reconnecting.")
+ self.ec = _EventClient(self.url, self.filters, self.on_event, self.last_log_id, self.on_closed)
+ while True:
+ try:
+ self.ec.connect()
+ break
+ except Exception as e:
+ _logger.warn("Error '%s' during websocket reconnect. Will retry after 5s.", e, exc_info=e)
+ time.sleep(5)
class PollClient(threading.Thread):
# to do so raises the same exception."
pass
- def subscribe(self, filters):
+ def subscribe(self, f):
self.on_event({'status': 200})
- self.filters.append(filters)
+ self.filters.append(f)
- def unsubscribe(self, filters):
- del self.filters[self.filters.index(filters)]
+ def unsubscribe(self, f):
+ del self.filters[self.filters.index(f)]
def _subscribe_websocket(api, filters, on_event, last_log_id=None):
import arvados
import arvados.events
from datetime import datetime, timedelta, tzinfo
+import logging
+import logging.handlers
import mock
import Queue
import run_test_server
+import StringIO
+import tempfile
import threading
import time
import unittest
self.ws = None
def tearDown(self):
- if self.ws:
- self.ws.close()
+ try:
+ if self.ws:
+ self.ws.close()
+ except Exception as e:
+ print("Error in teardown: ", e)
super(WebsocketTest, self).tearDown()
run_test_server.reset()
def isotz(self, offset):
"""Convert minutes-east-of-UTC to ISO8601 time zone designator"""
return '{:+03d}{:02d}'.format(offset/60, offset%60)
+
+ # Test websocket reconnection on (un)execpted close
+ def _test_websocket_reconnect(self, close_unexpected):
+ run_test_server.authorize_with('active')
+ events = Queue.Queue(100)
+
+ logstream = StringIO.StringIO()
+ rootLogger = logging.getLogger()
+ streamHandler = logging.StreamHandler(logstream)
+ rootLogger.addHandler(streamHandler)
+
+ filters = [['object_uuid', 'is_a', 'arvados#human']]
+ filters.append(['created_at', '>=', self.localiso(self.TIME_PAST)])
+ self.ws = arvados.events.subscribe(
+ arvados.api('v1'), filters,
+ events.put_nowait,
+ poll_fallback=False,
+ last_log_id=None)
+ self.assertIsInstance(self.ws, arvados.events.EventClient)
+ self.assertEqual(200, events.get(True, 5)['status'])
+
+ # create obj
+ human = arvados.api('v1').humans().create(body={}).execute()
+
+ # expect an event
+ self.assertIn(human['uuid'], events.get(True, 5)['object_uuid'])
+ with self.assertRaises(Queue.Empty):
+ self.assertEqual(events.get(True, 2), None)
+
+ # close (im)properly
+ if close_unexpected:
+ self.ws.close_connection()
+ else:
+ self.ws.close()
+
+ # create one more obj
+ human2 = arvados.api('v1').humans().create(body={}).execute()
+
+ # (un)expect the object creation event
+ if close_unexpected:
+ log_object_uuids = []
+ for i in range(0, 2):
+ event = events.get(True, 5)
+ if event.get('object_uuid') != None:
+ log_object_uuids.append(event['object_uuid'])
+ with self.assertRaises(Queue.Empty):
+ self.assertEqual(events.get(True, 2), None)
+ self.assertNotIn(human['uuid'], log_object_uuids)
+ self.assertIn(human2['uuid'], log_object_uuids)
+ else:
+ with self.assertRaises(Queue.Empty):
+ self.assertEqual(events.get(True, 2), None)
+
+ # verify log message to ensure that an (un)expected close
+ log_messages = logstream.getvalue()
+ closeLogFound = log_messages.find("Unexpected close. Reconnecting.")
+ retryLogFound = log_messages.find("Error during websocket reconnect. Will retry")
+ if close_unexpected:
+ self.assertNotEqual(closeLogFound, -1)
+ else:
+ self.assertEqual(closeLogFound, -1)
+ rootLogger.removeHandler(streamHandler)
+
+ def test_websocket_reconnect_on_unexpected_close(self):
+ self._test_websocket_reconnect(True)
+
+ def test_websocket_no_reconnect_on_close_by_user(self):
+ self._test_websocket_reconnect(False)
+
+ # Test websocket reconnection retry
+ @mock.patch('arvados.events._EventClient.connect')
+ def test_websocket_reconnect_retry(self, event_client_connect):
+ event_client_connect.side_effect = [None, Exception('EventClient.connect error'), None]
+
+ logstream = StringIO.StringIO()
+ rootLogger = logging.getLogger()
+ streamHandler = logging.StreamHandler(logstream)
+ rootLogger.addHandler(streamHandler)
+
+ run_test_server.authorize_with('active')
+ events = Queue.Queue(100)
+
+ filters = [['object_uuid', 'is_a', 'arvados#human']]
+ self.ws = arvados.events.subscribe(
+ arvados.api('v1'), filters,
+ events.put_nowait,
+ poll_fallback=False,
+ last_log_id=None)
+ self.assertIsInstance(self.ws, arvados.events.EventClient)
+
+ # simulate improper close
+ self.ws.on_closed()
+
+ # verify log messages to ensure retry happened
+ log_messages = logstream.getvalue()
+ found = log_messages.find("Error 'EventClient.connect error' during websocket reconnect. Will retry")
+ self.assertNotEqual(found, -1)
+ rootLogger.removeHandler(streamHandler)
('share/doc/arvados-docker-cleaner', ['agpl-3.0.txt']),
],
install_requires=[
- 'docker-py',
+ 'docker-py==1.7.2',
],
tests_require=[
'pbr<1.7.0',
self.flush()
src.flush()
+ def clear(self, force=False):
+ r = super(CollectionDirectoryBase, self).clear(force)
+ self.collection = None
+ return r
+
class CollectionDirectory(CollectionDirectoryBase):
"""Represents the root of a directory tree representing a collection."""
--- /dev/null
+import arvados
+import arvados.collection
+import arvados_fuse
+import arvados_fuse.command
+import json
+import logging
+import os
+import tempfile
+import unittest
+
+from .integration_test import IntegrationTest
+from .mount_test_base import MountTestBase
+
+class TmpCollectionTest(IntegrationTest):
+ mnt_args = ["--directory-cache=0"]
+
+ @IntegrationTest.mount(argv=mnt_args)
+ def test_cache_spill(self):
+ pdh = []
+ for i in range(0, 8):
+ cw = arvados.collection.Collection()
+ f = cw.open("blurg%i" % i, "w")
+ f.write("bloop%i" % i)
+
+ cw.mkdirs("dir%i" % i)
+ f = cw.open("dir%i/blurg" % i, "w")
+ f.write("dirbloop%i" % i)
+
+ cw.save_new()
+ pdh.append(cw.portable_data_hash())
+ self.pool_test(self.mnt, pdh)
+
+ @staticmethod
+ def _test_cache_spill(self, mnt, pdh):
+ for i,v in enumerate(pdh):
+ j = os.path.join(mnt, "by_id", v, "blurg%i" % i)
+ self.assertTrue(os.path.exists(j))
+ j = os.path.join(mnt, "by_id", v, "dir%i/blurg" % i)
+ self.assertTrue(os.path.exists(j))
+
+ for i,v in enumerate(pdh):
+ j = os.path.join(mnt, "by_id", v, "blurg%i" % i)
+ self.assertTrue(os.path.exists(j))
+ j = os.path.join(mnt, "by_id", v, "dir%i/blurg" % i)
+ self.assertTrue(os.path.exists(j))
return orig_func(self, *args, **kwargs)
return stop_wrapper
- @ComputeNodeStateChangeBase._finish_on_exception
+ def _cancel_on_exception(orig_func):
+ @functools.wraps(orig_func)
+ def finish_wrapper(self, *args, **kwargs):
+ try:
+ return orig_func(self, *args, **kwargs)
+ except Exception as error:
+ self._logger.error("Actor error %s", error)
+ self._later.cancel_shutdown("Unhandled exception %s" % error)
+ return finish_wrapper
+
+ @_cancel_on_exception
@_stop_if_window_closed
@RetryMixin._retry()
def shutdown_node(self):
else:
return "node is not idle."
+ def resume_node(self):
+ pass
+
def consider_shutdown(self):
try:
next_opening = self._shutdowns.next_opening()
import time
from . import \
- ComputeNodeSetupActor, ComputeNodeUpdateActor, ComputeNodeMonitorActor
+ ComputeNodeSetupActor, ComputeNodeUpdateActor
from . import ComputeNodeShutdownActor as ShutdownActorBase
+from . import ComputeNodeMonitorActor as MonitorActorBase
from .. import RetryMixin
-class ComputeNodeShutdownActor(ShutdownActorBase):
+class SlurmMixin(object):
SLURM_END_STATES = frozenset(['down\n', 'down*\n',
'drain\n', 'drain*\n',
'fail\n', 'fail*\n'])
SLURM_DRAIN_STATES = frozenset(['drain\n', 'drng\n'])
+ def _set_node_state(self, nodename, state, *args):
+ cmd = ['scontrol', 'update', 'NodeName=' + nodename,
+ 'State=' + state]
+ cmd.extend(args)
+ subprocess.check_output(cmd)
+
+ def _get_slurm_state(self, nodename):
+ return subprocess.check_output(['sinfo', '--noheader', '-o', '%t', '-n', nodename])
+
+
+class ComputeNodeShutdownActor(SlurmMixin, ShutdownActorBase):
def on_start(self):
arv_node = self._arvados_node()
if arv_node is None:
self._logger.info("Draining SLURM node %s", self._nodename)
self._later.issue_slurm_drain()
- def _set_node_state(self, state, *args):
- cmd = ['scontrol', 'update', 'NodeName=' + self._nodename,
- 'State=' + state]
- cmd.extend(args)
- subprocess.check_output(cmd)
-
- def _get_slurm_state(self):
- return subprocess.check_output(['sinfo', '--noheader', '-o', '%t', '-n', self._nodename])
-
@RetryMixin._retry((subprocess.CalledProcessError,))
def cancel_shutdown(self, reason):
if self._nodename:
- if self._get_slurm_state() in self.SLURM_DRAIN_STATES:
+ if self._get_slurm_state(self._nodename) in self.SLURM_DRAIN_STATES:
# Resume from "drng" or "drain"
- self._set_node_state('RESUME')
+ self._set_node_state(self._nodename, 'RESUME')
else:
# Node is in a state such as 'idle' or 'alloc' so don't
# try to resume it because that will just raise an error.
@RetryMixin._retry((subprocess.CalledProcessError,))
@ShutdownActorBase._stop_if_window_closed
def issue_slurm_drain(self):
- self._set_node_state('DRAIN', 'Reason=Node Manager shutdown')
+ self._set_node_state(self._nodename, 'DRAIN', 'Reason=Node Manager shutdown')
self._logger.info("Waiting for SLURM node %s to drain", self._nodename)
self._later.await_slurm_drain()
@RetryMixin._retry((subprocess.CalledProcessError,))
@ShutdownActorBase._stop_if_window_closed
def await_slurm_drain(self):
- output = self._get_slurm_state()
+ output = self._get_slurm_state(self._nodename)
if output in self.SLURM_END_STATES:
self._later.shutdown_node()
else:
self._timer.schedule(time.time() + 10,
self._later.await_slurm_drain)
+
+
+class ComputeNodeMonitorActor(SlurmMixin, MonitorActorBase):
+
+ def shutdown_eligible(self):
+ if self.arvados_node is not None:
+ state = self._get_slurm_state(self.arvados_node['hostname'])
+ # Automatically eligible for shutdown if it's down or failed, but
+ # not drain to avoid a race condition with resume_node().
+ if state in self.SLURM_END_STATES:
+ if state in self.SLURM_DRAIN_STATES:
+ return "node is draining"
+ else:
+ return True
+ return super(ComputeNodeMonitorActor, self).shutdown_eligible()
+
+ def resume_node(self):
+ try:
+ if (self.arvados_node is not None and
+ self._get_slurm_state(self.arvados_node['hostname']) in self.SLURM_DRAIN_STATES):
+ # Resume from "drng" or "drain"
+ self._set_node_state(self.arvados_node['hostname'], 'RESUME')
+ except Exception as error:
+ self._logger.warn(
+ "Exception reenabling node: %s", error, exc_info=error)
key = NodeAuthSSHKey(ssh_file.read())
return 'auth', key
- def search_for(self, term, list_method, key=attrgetter('id'), **kwargs):
+ def search_for_now(self, term, list_method, key=attrgetter('id'), **kwargs):
"""Return one matching item from a list of cloud objects.
Raises ValueError if the number of matching objects is not exactly 1.
value search for a `term` match on each item. Returns the
object's 'id' attribute by default.
"""
+ items = getattr(self.real, list_method)(**kwargs)
+ results = [item for item in items if key(item) == term]
+ count = len(results)
+ if count != 1:
+ raise ValueError("{} returned {} results for {!r}".format(
+ list_method, count, term))
+ return results[0]
+
+ def search_for(self, term, list_method, key=attrgetter('id'), **kwargs):
+ """Return one cached matching item from a list of cloud objects.
+
+ See search_for_now() for details of arguments and exceptions.
+ This method caches results, so it's good to find static cloud objects
+ like node sizes, regions, etc.
+ """
cache_key = (list_method, term)
if cache_key not in self.SEARCH_CACHE:
- items = getattr(self.real, list_method)(**kwargs)
- results = [item for item in items
- if key(item) == term]
- count = len(results)
- if count != 1:
- raise ValueError("{} returned {} results for '{}'".format(
- list_method, count, term))
- self.SEARCH_CACHE[cache_key] = results[0]
+ self.SEARCH_CACHE[cache_key] = self.search_for_now(
+ term, list_method, key, **kwargs)
return self.SEARCH_CACHE[cache_key]
def list_nodes(self, **kwargs):
l.update(kwargs)
return self.real.list_nodes(**l)
+ def create_cloud_name(self, arvados_node):
+ """Return a cloud node name for the given Arvados node record.
+
+ Subclasses must override this method. It should return a string
+ that can be used as the name for a newly-created cloud node,
+ based on identifying information in the Arvados node record.
+
+ Arguments:
+ * arvados_node: This Arvados node record to seed the new cloud node.
+ """
+ raise NotImplementedError("BaseComputeNodeDriver.create_cloud_name")
+
def arvados_create_kwargs(self, size, arvados_node):
"""Return dynamic keyword arguments for create_node.
kwargs.update(self.arvados_create_kwargs(size, arvados_node))
kwargs['size'] = size
return self.real.create_node(**kwargs)
- except self.CLOUD_ERRORS:
+ except self.CLOUD_ERRORS as create_error:
# Workaround for bug #6702: sometimes the create node request
# succeeds but times out and raises an exception instead of
# returning a result. If this happens, we get stuck in a retry
# loop forever because subsequent create_node attempts will fail
# due to node name collision. So check if the node we intended to
# create shows up in the cloud node list and return it if found.
- node = self.search_for(kwargs['name'], 'list_nodes', self._name_key)
- if node:
- return node
- else:
- # something else went wrong, re-raise the exception
- raise
+ try:
+ return self.search_for_now(kwargs['name'], 'list_nodes', self._name_key)
+ except ValueError:
+ raise create_error
def post_create_node(self, cloud_node):
# ComputeNodeSetupActor calls this method after the cloud node is
auth_kwargs, list_kwargs, create_kwargs,
driver_class)
+ def create_cloud_name(self, arvados_node):
+ uuid_parts = arvados_node['uuid'].split('-', 2)
+ return 'compute-{parts[2]}-{parts[0]}'.format(parts=uuid_parts)
+
def arvados_create_kwargs(self, size, arvados_node):
- cluster_id, _, node_id = arvados_node['uuid'].split('-')
- name = 'compute-{}-{}'.format(node_id, cluster_id)
tags = {
'booted_at': time.strftime(ARVADOS_TIMEFMT, time.gmtime()),
'arv-ping-url': self._make_ping_url(arvados_node)
}
tags.update(self.tags)
+ name = self.create_cloud_name(arvados_node)
customdata = """#!/bin/sh
mkdir -p /var/tmp/arv-node-data/meta-data
echo %s > /var/tmp/arv-node-data/arv-ping-url
def _init_subnet_id(self, subnet_id):
return 'ex_subnet', self.search_for(subnet_id, 'ex_list_subnets')
+ create_cloud_name = staticmethod(arvados_node_fqdn)
+
def arvados_create_kwargs(self, size, arvados_node):
- return {'name': arvados_node_fqdn(arvados_node),
+ return {'name': self.create_cloud_name(arvados_node),
'ex_userdata': self._make_ping_url(arvados_node)}
def post_create_node(self, cloud_node):
self.create_kwargs['ex_metadata']['sshKeys'] = (
'root:' + ssh_file.read().strip())
+ def create_cloud_name(self, arvados_node):
+ uuid_parts = arvados_node['uuid'].split('-', 2)
+ return 'compute-{parts[2]}-{parts[0]}'.format(parts=uuid_parts)
+
def arvados_create_kwargs(self, size, arvados_node):
- cluster_id, _, node_id = arvados_node['uuid'].split('-')
- name = 'compute-{}-{}'.format(node_id, cluster_id)
+ name = self.create_cloud_name(arvados_node)
disks = [
{'autoDelete': True,
'boot': True,
def _update_poll_time(self, poll_key):
self.last_polls[poll_key] = time.time()
+ def _resume_node(self, node_record):
+ node_record.actor.resume_node()
+
def _pair_nodes(self, node_record, arvados_node):
self._logger.info("Cloud node %s is now paired with Arvados node %s",
node_record.cloud_node.name, arvados_node['uuid'])
if cloud_rec.actor.offer_arvados_pair(arv_node).get():
self._pair_nodes(cloud_rec, arv_node)
break
+ for rec in self.cloud_nodes.nodes.itervalues():
+ # crunch-dispatch turns all slurm states that are not either "idle"
+ # or "alloc" into "down", but in case that behavior changes, assume
+ # any state that is not "idle" or "alloc" could be a state we want
+ # to try to resume from.
+ if (rec.arvados_node is not None and
+ rec.arvados_node["info"].get("slurm_state") not in ("idle", "alloc") and
+ rec.cloud_node.id not in self.shutdowns):
+ self._resume_node(rec)
def _nodes_booting(self, size):
s = sum(1
for c in self.cloud_nodes.nodes.itervalues()
if size is None or c.cloud_node.size.id == size.id)
+ def _nodes_down(self, size):
+ # Make sure to iterate over self.cloud_nodes because what we're
+ # counting here are compute nodes that are reported by the cloud
+ # provider but are considered "down" by Arvados.
+ return sum(1 for down in
+ pykka.get_all(rec.actor.in_state('down') for rec in
+ self.cloud_nodes.nodes.itervalues()
+ if size is None or rec.cloud_node.size.id == size.id)
+ if down)
+
def _nodes_up(self, size):
- up = self._nodes_booting(size) + self._nodes_booted(size)
+ up = (self._nodes_booting(size) + self._nodes_booted(size)) - self._nodes_down(size)
return up
def _total_price(self):
def test_shutdown_without_arvados_node(self):
self.make_actor(start_time=0)
self.shutdowns._set_state(True, 600)
- self.assertTrue(self.node_actor.shutdown_eligible().get(self.TIMEOUT))
+ self.assertIs(True, self.node_actor.shutdown_eligible().get(self.TIMEOUT))
def test_no_shutdown_missing(self):
arv_node = testutil.arvados_node_mock(10, job_uuid=None,
self.make_actor(11, arv_node)
self.shutdowns._set_state(True, 600)
self.cloud_client.broken.return_value = True
- self.assertTrue(self.node_actor.shutdown_eligible().get(self.TIMEOUT))
+ self.assertIs(True, self.node_actor.shutdown_eligible().get(self.TIMEOUT))
def test_no_shutdown_when_window_closed(self):
self.make_actor(3, testutil.arvados_node_mock(3, job_uuid=None))
from __future__ import absolute_import, print_function
import subprocess
+import time
import unittest
import mock
proc_mock.return_value = 'drain\n'
super(SLURMComputeNodeShutdownActorTestCase,
self).test_arvados_node_cleaned_after_shutdown()
+
+class SLURMComputeNodeMonitorActorTestCase(testutil.ActorTestMixin,
+ unittest.TestCase):
+
+ def make_mocks(self, node_num):
+ self.shutdowns = testutil.MockShutdownTimer()
+ self.shutdowns._set_state(False, 300)
+ self.timer = mock.MagicMock(name='timer_mock')
+ self.updates = mock.MagicMock(name='update_mock')
+ self.cloud_mock = testutil.cloud_node_mock(node_num)
+ self.subscriber = mock.Mock(name='subscriber_mock')
+ self.cloud_client = mock.MagicMock(name='cloud_client')
+ self.cloud_client.broken.return_value = False
+
+ def make_actor(self, node_num=1, arv_node=None, start_time=None):
+ if not hasattr(self, 'cloud_mock'):
+ self.make_mocks(node_num)
+ if start_time is None:
+ start_time = time.time()
+ self.node_actor = slurm_dispatch.ComputeNodeMonitorActor.start(
+ self.cloud_mock, start_time, self.shutdowns,
+ testutil.cloud_node_fqdn, self.timer, self.updates, self.cloud_client,
+ arv_node, boot_fail_after=300).proxy()
+ self.node_actor.subscribe(self.subscriber).get(self.TIMEOUT)
+
+ @mock.patch("subprocess.check_output")
+ def test_resume_node(self, check_output):
+ arv_node = testutil.arvados_node_mock()
+ self.make_actor(arv_node=arv_node)
+ check_output.return_value = "drain\n"
+ self.node_actor.resume_node().get(self.TIMEOUT)
+ self.assertIn(mock.call(['sinfo', '--noheader', '-o', '%t', '-n', arv_node['hostname']]), check_output.call_args_list)
+ self.assertIn(mock.call(['scontrol', 'update', 'NodeName=' + arv_node['hostname'], 'State=RESUME']), check_output.call_args_list)
+
+ @mock.patch("subprocess.check_output")
+ def test_no_resume_idle_node(self, check_output):
+ arv_node = testutil.arvados_node_mock()
+ self.make_actor(arv_node=arv_node)
+ check_output.return_value = "idle\n"
+ self.node_actor.resume_node().get(self.TIMEOUT)
+ self.assertIn(mock.call(['sinfo', '--noheader', '-o', '%t', '-n', arv_node['hostname']]), check_output.call_args_list)
+ self.assertNotIn(mock.call(['scontrol', 'update', 'NodeName=' + arv_node['hostname'], 'State=RESUME']), check_output.call_args_list)
+
+ @mock.patch("subprocess.check_output")
+ def test_resume_node_exception(self, check_output):
+ arv_node = testutil.arvados_node_mock()
+ self.make_actor(arv_node=arv_node)
+ check_output.side_effect = Exception()
+ self.node_actor.resume_node().get(self.TIMEOUT)
+ self.assertIn(mock.call(['sinfo', '--noheader', '-o', '%t', '-n', arv_node['hostname']]), check_output.call_args_list)
+ self.assertNotIn(mock.call(['scontrol', 'update', 'NodeName=' + arv_node['hostname'], 'State=RESUME']), check_output.call_args_list)
+
+ @mock.patch("subprocess.check_output")
+ def test_shutdown_down_node(self, check_output):
+ check_output.return_value = "down\n"
+ self.make_actor(arv_node=testutil.arvados_node_mock())
+ self.assertIs(True, self.node_actor.shutdown_eligible().get(self.TIMEOUT))
+
+ @mock.patch("subprocess.check_output")
+ def test_no_shutdown_drain_node(self, check_output):
+ check_output.return_value = "drain\n"
+ self.make_actor(arv_node=testutil.arvados_node_mock())
+ self.assertEquals('node is draining', self.node_actor.shutdown_eligible().get(self.TIMEOUT))
self.last_setup.arvados_node.get.return_value = arv_node
return self.last_setup
- def test_no_new_node_when_booted_node_not_usable(self):
+ def test_new_node_when_booted_node_not_usable(self):
cloud_node = testutil.cloud_node_mock(4)
arv_node = testutil.arvados_node_mock(4, crunch_worker_state='down')
setup = self.start_node_boot(cloud_node, arv_node)
self.daemon.update_server_wishlist(
[testutil.MockSize(1)]).get(self.TIMEOUT)
self.stop_proxy(self.daemon)
- self.assertEqual(1, self.node_setup.start.call_count)
+ self.assertEqual(2, self.node_setup.start.call_count)
def test_no_duplication_when_booting_node_listed_fast(self):
# Test that we don't start two ComputeNodeMonitorActors when
# test for that.
self.assertEqual(2, sizecounts[small.id])
self.assertEqual(1, sizecounts[big.id])
+
+ @mock.patch("arvnodeman.daemon.NodeManagerDaemonActor._resume_node")
+ def test_resume_drained_nodes(self, resume_node):
+ cloud_node = testutil.cloud_node_mock(1)
+ arv_node = testutil.arvados_node_mock(1, info={"ec2_instance_id": "1", "slurm_state": "down"})
+ self.make_daemon([cloud_node], [arv_node])
+ resume_node.assert_called_with(self.daemon.cloud_nodes.get(self.TIMEOUT).nodes.values()[0])
+ self.stop_proxy(self.daemon)
+
+ @mock.patch("arvnodeman.daemon.NodeManagerDaemonActor._resume_node")
+ def test_no_resume_shutdown_nodes(self, resume_node):
+ cloud_node = testutil.cloud_node_mock(1)
+ arv_node = testutil.arvados_node_mock(1, info={"ec2_instance_id": "1", "slurm_state": "down"})
+
+ self.make_daemon([cloud_node], [])
+
+ self.node_shutdown = mock.MagicMock(name='shutdown_mock')
+ self.daemon.shutdowns.get(self.TIMEOUT)[cloud_node.id] = self.node_shutdown
+
+ self.daemon.update_arvados_nodes([arv_node]).get(self.TIMEOUT)
+ self.stop_proxy(self.daemon)
+ resume_node.assert_not_called()
import threading
import time
+import libcloud.common.types as cloud_types
import mock
import pykka
self.assertTrue(self.driver_mock.called)
self.assertIs(driver.real, driver_mock2)
+ def test_create_can_find_node_after_timeout(self):
+ driver = self.new_driver()
+ arv_node = arvados_node_mock()
+ cloud_node = cloud_node_mock()
+ cloud_node.name = driver.create_cloud_name(arv_node)
+ create_method = self.driver_mock().create_node
+ create_method.side_effect = cloud_types.LibcloudError("fake timeout")
+ list_method = self.driver_mock().list_nodes
+ list_method.return_value = [cloud_node]
+ actual = driver.create_node(MockSize(1), arv_node)
+ self.assertIs(cloud_node, actual)
+
+ def test_create_can_raise_exception_after_timeout(self):
+ driver = self.new_driver()
+ arv_node = arvados_node_mock()
+ create_method = self.driver_mock().create_node
+ create_method.side_effect = cloud_types.LibcloudError("fake timeout")
+ list_method = self.driver_mock().list_nodes
+ list_method.return_value = []
+ with self.assertRaises(cloud_types.LibcloudError) as exc_test:
+ driver.create_node(MockSize(1), arv_node)
+ self.assertIs(create_method.side_effect, exc_test.exception)
+
+
class RemotePollLoopActorTestMixin(ActorTestMixin):
def build_monitor(self, *args, **kwargs):
self.timer = mock.MagicMock(name='timer_mock')
VAR_DATA="$ARVBOX_DATA/var"
PASSENGER="$ARVBOX_DATA/passenger"
GEMS="$ARVBOX_DATA/gems"
+PIPCACHE="$ARVBOX_DATA/pip"
+GOSTUFF="$ARVBOX_DATA/gopath"
getip() {
docker inspect $ARVBOX_CONTAINER | grep \"IPAddress\" | head -n1 | tr -d ' ":,\n' | cut -c10-
updateconf
wait_for_arvbox
else
- mkdir -p "$PG_DATA" "$VAR_DATA" "$PASSENGER" "$GEMS"
+ mkdir -p "$PG_DATA" "$VAR_DATA" "$PASSENGER" "$GEMS" "$PIPCACHE" "$GOSTUFF"
+
if ! test -d "$ARVADOS_ROOT" ; then
git clone https://github.com/curoverse/arvados.git "$ARVADOS_ROOT"
"--volume=$VAR_DATA:/var/lib/arvados:rw" \
"--volume=$PASSENGER:/var/lib/passenger:rw" \
"--volume=$GEMS:/var/lib/gems:rw" \
+ "--volume=$PIPCACHE:/var/lib/pip:rw" \
+ "--volume=$GOSTUFF:/var/lib/gopath:rw" \
arvados/arvbox-dev \
/usr/local/bin/runsvinit -svdir=/etc/test-service
"--volume=$VAR_DATA:/var/lib/arvados:rw" \
"--volume=$PASSENGER:/var/lib/passenger:rw" \
"--volume=$GEMS:/var/lib/gems:rw" \
+ "--volume=$PIPCACHE:/var/lib/pip:rw" \
+ "--volume=$GOSTUFF:/var/lib/gopath:rw" \
$PUBLIC \
arvados/arvbox-dev
updateconf
RUN sudo -u arvbox /var/lib/arvbox/service/workbench/run-service --only-deps
RUN sudo -u arvbox /var/lib/arvbox/service/doc/run-service --only-deps
RUN sudo -u arvbox /var/lib/arvbox/service/vm/run-service --only-deps
+RUN sudo -u arvbox /var/lib/arvbox/service/keep-web/run-service --only-deps
+RUN sudo -u arvbox /var/lib/arvbox/service/keepproxy/run-service --only-deps
+RUN sudo -u arvbox /var/lib/arvbox/service/arv-git-httpd/run-service --only-deps
+RUN sudo -u arvbox /usr/local/lib/arvbox/keep-setup.sh --only-deps
RUN sudo -u arvbox /var/lib/arvbox/service/sdk/run-service
else
frozen=""
fi
- if ! flock /var/lib/arvados/gems.lock bundle install --path $GEM_HOME --local --no-deployment $frozen "$@" ; then
- flock /var/lib/arvados/gems.lock bundle install --path $GEM_HOME --no-deployment $frozen "$@"
+ if ! flock /var/lib/gems/gems.lock bundle install --path $GEM_HOME --local --no-deployment $frozen "$@" ; then
+ flock /var/lib/gems/gems.lock bundle install --path $GEM_HOME --no-deployment $frozen "$@"
fi
}
pip_install() {
- pushd /var/lib/arvados/pip
+ pushd /var/lib/pip
for p in $(ls http*.tar.gz) ; do
if test -f $p ; then
ln -sf $p $(echo $p | sed 's/.*%2F\(.*\)/\1/')
done
popd
- if ! pip install --no-index --find-links /var/lib/arvados/pip $1 ; then
+ if ! pip install --no-index --find-links /var/lib/pip $1 ; then
pip install $1
fi
}
. /usr/local/lib/arvbox/common.sh
-mkdir -p /var/lib/arvados/gostuff
-cd /var/lib/arvados/gostuff
+mkdir -p /var/lib/gopath
+cd /var/lib/gopath
export GOPATH=$PWD
mkdir -p "$GOPATH/src/git.curoverse.com"
ln -sfn "/usr/src/arvados" "$GOPATH/src/git.curoverse.com/arvados.git"
-flock /var/lib/arvados/gostuff.lock go get -t "git.curoverse.com/arvados.git/services/crunchstat"
-flock /var/lib/arvados/gostuff.lock go get -t "git.curoverse.com/arvados.git/sdk/go/crunchrunner"
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/services/crunchstat"
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/sdk/go/crunchrunner"
install bin/crunchstat bin/crunchrunner /usr/local/bin
export ARVADOS_API_HOST=$localip:${services[api]}
. /usr/local/lib/arvbox/common.sh
-mkdir -p /var/lib/arvados/gostuff
-cd /var/lib/arvados/gostuff
+mkdir -p /var/lib/gopath
+cd /var/lib/gopath
export GOPATH=$PWD
mkdir -p "$GOPATH/src/git.curoverse.com"
ln -sfn "/usr/src/arvados" "$GOPATH/src/git.curoverse.com/arvados.git"
-flock /var/lib/arvados/gostuff.lock go get -t "git.curoverse.com/arvados.git/services/keepstore"
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/services/keepstore"
install bin/keepstore /usr/local/bin
+if test "$1" = "--only-deps" ; then
+ exit
+fi
+
mkdir -p /var/lib/arvados/$1
export ARVADOS_API_HOST=$localip:${services[api]}
-listen=:$2 \
-enforce-permissions=true \
-blob-signing-key-file=/var/lib/arvados/blob_signing_key \
+ -data-manager-token-file=/var/lib/arvados/superuser_token \
-max-buffers=20 \
-volume=/var/lib/arvados/$1
#!/bin/bash
exec 2>&1
-set -eux -o pipefail
+set -ex -o pipefail
. /usr/local/lib/arvbox/common.sh
-mkdir -p /var/lib/arvados/gostuff
-cd /var/lib/arvados/gostuff
+mkdir -p /var/lib/gopath
+cd /var/lib/gopath
export GOPATH=$PWD
mkdir -p "$GOPATH/src/git.curoverse.com"
ln -sfn "/usr/src/arvados" "$GOPATH/src/git.curoverse.com/arvados.git"
-flock /var/lib/arvados/gostuff.lock go get -t "git.curoverse.com/arvados.git/services/arv-git-httpd"
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/services/arv-git-httpd"
install bin/arv-git-httpd /usr/local/bin
+if test "$1" = "--only-deps" ; then
+ exit
+fi
+
export ARVADOS_API_HOST=$localip:${services[api]}
export ARVADOS_API_HOST_INSECURE=1
export GITOLITE_HTTP_HOME=/var/lib/arvados/git
. /usr/local/lib/arvbox/common.sh
-mkdir -p /var/lib/arvados/gostuff
-cd /var/lib/arvados/gostuff
+mkdir -p /var/lib/gopath
+cd /var/lib/gopath
export GOPATH=$PWD
mkdir -p "$GOPATH/src/git.curoverse.com"
ln -sfn "/usr/src/arvados" "$GOPATH/src/git.curoverse.com/arvados.git"
-flock /var/lib/arvados/gostuff.lock go get -t "git.curoverse.com/arvados.git/services/crunch-run"
-flock /var/lib/arvados/gostuff.lock go get -t "git.curoverse.com/arvados.git/services/crunch-dispatch-local"
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/services/crunch-run"
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/services/crunch-dispatch-local"
install bin/crunch-run bin/crunch-dispatch-local /usr/local/bin
export ARVADOS_API_HOST=$localip:${services[api]}
#!/bin/bash
exec 2>&1
-set -eux -o pipefail
+set -ex -o pipefail
. /usr/local/lib/arvbox/common.sh
-mkdir -p /var/lib/arvados/gostuff
-cd /var/lib/arvados/gostuff
+mkdir -p /var/lib/gopath
+cd /var/lib/gopath
export GOPATH=$PWD
mkdir -p "$GOPATH/src/git.curoverse.com"
ln -sfn "/usr/src/arvados" "$GOPATH/src/git.curoverse.com/arvados.git"
-flock /var/lib/arvados/gostuff.lock go get -t "git.curoverse.com/arvados.git/services/keep-web"
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/services/keep-web"
install bin/keep-web /usr/local/bin
+if test "$1" = "--only-deps" ; then
+ exit
+fi
+
export ARVADOS_API_HOST=$localip:${services[api]}
export ARVADOS_API_HOST_INSECURE=1
export ARVADOS_API_TOKEN=$(cat /var/lib/arvados/superuser_token)
exec 2>&1
sleep 2
-set -eux -o pipefail
+set -ex -o pipefail
. /usr/local/lib/arvbox/common.sh
-mkdir -p /var/lib/arvados/gostuff
-cd /var/lib/arvados/gostuff
+mkdir -p /var/lib/gopath
+cd /var/lib/gopath
export GOPATH=$PWD
mkdir -p "$GOPATH/src/git.curoverse.com"
ln -sfn "/usr/src/arvados" "$GOPATH/src/git.curoverse.com/arvados.git"
-flock /var/lib/arvados/gostuff.lock go get -t "git.curoverse.com/arvados.git/services/keepproxy"
+flock /var/lib/gopath/gopath.lock go get -t "git.curoverse.com/arvados.git/services/keepproxy"
install bin/keepproxy /usr/local/bin
+if test "$1" = "--only-deps" ; then
+ exit
+fi
+
export ARVADOS_API_HOST=$localip:${services[api]}
export ARVADOS_API_HOST_INSECURE=1
export ARVADOS_API_TOKEN=$(cat /var/lib/arvados/superuser_token)
. /usr/local/lib/arvbox/common.sh
-mkdir -p ~/.pip /var/lib/arvados/pip
+mkdir -p ~/.pip /var/lib/pip
cat > ~/.pip/pip.conf <<EOF
[global]
-download_cache = /var/lib/arvados/pip
+download_cache = /var/lib/pip
EOF
cd /usr/src/arvados/sdk/cli