[ARVADOS] updated: 059a62b51d90f82e4286835d26d8bf523a54132b

Git user git at public.curoverse.com
Fri Apr 28 17:04:35 EDT 2017


Summary of changes:
 .../app/assets/javascripts/edit_collection.js      |  45 ++
 .../app/assets/javascripts/selection.js.erb        |   7 +
 .../app/assets/stylesheets/collections.css.scss    |   6 +
 .../app/controllers/actions_controller.rb          |  37 +-
 .../app/controllers/application_controller.rb      |  56 +-
 .../app/controllers/collections_controller.rb      |  52 ++
 .../controllers/container_requests_controller.rb   |  27 +-
 .../app/controllers/keep_disks_controller.rb       |   1 +
 .../workbench/app/controllers/search_controller.rb |   3 +
 apps/workbench/app/controllers/users_controller.rb |   1 +
 apps/workbench/app/helpers/application_helper.rb   |   6 +-
 apps/workbench/app/helpers/provenance_helper.rb    |  48 ++
 apps/workbench/app/models/arvados_base.rb          |   4 +
 apps/workbench/app/models/arvados_resource_list.rb |   8 +
 apps/workbench/app/models/collection.rb            |   2 +-
 apps/workbench/app/models/job.rb                   |   2 +-
 apps/workbench/app/models/pipeline_instance.rb     |   7 +-
 apps/workbench/app/models/proxy_work_unit.rb       |   1 +
 apps/workbench/app/models/workflow.rb              |   8 +
 .../app/views/application/_content.html.erb        |   2 +-
 .../collections/_extra_tab_line_buttons.html.erb   |   3 +
 .../app/views/collections/_show_files.html.erb     |  36 +-
 .../_extra_tab_line_buttons.html.erb               |   2 +-
 .../_show_provenance.html.erb                      |   4 +-
 .../app/views/projects/_show_dashboard.html.erb    |  12 +-
 .../app/views/work_units/_show_child.html.erb      |   4 +-
 .../app/views/workflows/_show_recent.html.erb      |  65 ++
 apps/workbench/config/routes.rb                    |   1 +
 .../controllers/collections_controller_test.rb     | 132 +++-
 .../container_requests_controller_test.rb          |  21 +
 .../test/controllers/disabled_api_test.rb          |  11 +
 .../test/integration/collection_upload_test.rb     |  14 +
 .../workbench/test/integration/collections_test.rb | 122 ++++
 .../test/integration/container_requests_test.rb    |  12 +
 apps/workbench/test/integration/work_units_test.rb |  24 +
 apps/workbench/test/integration_helper.rb          |  15 +
 .../test/unit/arvados_resource_list_test.rb        |   8 +
 apps/workbench/test/unit/link_test.rb              |   3 +
 apps/workbench/test/unit/pipeline_instance_test.rb |   3 +
 apps/workbench/test/unit/work_unit_test.rb         |   3 +
 build/build.list                                   |  12 +-
 .../package-test-dockerfiles/ubuntu1204/Dockerfile |   2 +-
 build/rails-package-scripts/postinst.sh            |   4 +-
 build/run-build-packages-one-target.sh             |   2 +-
 build/run-build-packages-sso.sh                    |   3 +
 build/run-build-packages.sh                        |   4 +-
 build/run-library.sh                               |   2 +
 build/run-tests.sh                                 |  17 +-
 cmd/arvados-admin/.gitignore                       |   1 -
 cmd/arvados-admin/main.go                          |  26 -
 cmd/arvados-admin/setup_docker_compose_test.go     |  23 -
 doc/_config.yml                                    |   6 +
 doc/_includes/_compute_ping_rb.liquid              | 285 ++++++++
 doc/_includes/_navbar_top.liquid                   |   2 +-
 .../methods/container_requests.html.textile.liquid |   2 +
 .../install-compute-node.html.textile.liquid       |   6 +
 .../install-dispatch.html.textile.liquid           |  15 +
 .../install-slurm.html.textile.liquid}             |  96 +--
 doc/install/install-api-server.html.textile.liquid |  86 +--
 .../install-compute-ping.html.textile.liquid       |   9 +
 .../install-keep-balance.html.textile.liquid       |  15 +-
 doc/install/install-keepstore.html.textile.liquid  |   4 +-
 .../install-nodemanager.html.textile.liquid        | 592 ++++++++++++++++
 doc/install/install-ws.html.textile.liquid         |  12 +-
 doc/install/migrate-docker19.html.textile.liquid   |  34 +
 doc/user/cwl/cwl-extensions.html.textile.liquid    |  17 +
 docker/migrate-docker19/Dockerfile                 |  31 +
 docker/migrate-docker19/build.sh                   |   2 +
 .../docker/run => docker/migrate-docker19/dnd.sh   |  15 +-
 docker/migrate-docker19/migrate.sh                 | 110 +++
 {cmd => lib/cmd}/dispatch.go                       |   0
 lib/configure/configure.go                         |  32 +
 sdk/cli/arvados-cli.gemspec                        |   4 +-
 sdk/cli/bin/crunch-job                             |  32 +-
 sdk/cli/test/binstub_arv-mount/arv-mount           |   1 +
 .../test/binstub_clean_fail/{mount => arv-mount}   |   0
 sdk/cli/test/test_arv-collection-create.rb         |   1 +
 sdk/cli/test/test_arv-keep-get.rb                  |   4 +-
 sdk/cli/test/test_crunch-job.rb                    |   4 +-
 sdk/cwl/arvados_cwl/__init__.py                    |  70 +-
 sdk/cwl/arvados_cwl/arv-cwl-schema.yml             |  31 +
 sdk/cwl/arvados_cwl/arvcontainer.py                |  71 +-
 sdk/cwl/arvados_cwl/arvdocker.py                   |   8 +-
 sdk/cwl/arvados_cwl/arvjob.py                      |   8 +-
 sdk/cwl/arvados_cwl/arvworkflow.py                 |   5 +-
 sdk/cwl/arvados_cwl/crunch_script.py               |  15 +-
 sdk/cwl/arvados_cwl/fsaccess.py                    |  39 +-
 sdk/cwl/arvados_cwl/pathmapper.py                  | 106 +--
 sdk/cwl/arvados_cwl/runner.py                      |  21 +-
 sdk/cwl/setup.py                                   |   7 +-
 sdk/cwl/test_with_arvbox.sh                        |   2 +
 sdk/cwl/tests/arvados-tests.sh                     |   2 +-
 sdk/cwl/tests/arvados-tests.yml                    |  74 ++
 sdk/cwl/tests/cat.cwl                              |   8 +
 sdk/cwl/tests/dir-job2.yml                         |   3 +
 ...dir-test-input.cwl => keep-dir-test-input2.cwl} |   3 +
 ...dir-test-input.cwl => keep-dir-test-input3.cwl} |   3 +
 sdk/cwl/tests/listing-job.yml                      |   3 +
 sdk/cwl/tests/octo.yml                             |   3 +
 .../cwl/tests/octothorpe/item #1.txt               |   0
 sdk/cwl/tests/test_container.py                    | 171 ++++-
 sdk/cwl/tests/test_fsaccess.py                     |  28 +
 sdk/cwl/tests/test_job.py                          |   9 +-
 sdk/cwl/tests/test_pathmapper.py                   |  25 +-
 sdk/cwl/tests/test_submit.py                       |  41 +-
 .../cwl/tests/tmp1/tmp2/tmp3}/.gitkeep             |   0
 sdk/cwl/tests/wf/listing_deep.cwl                  |  15 +
 sdk/cwl/tests/wf/listing_none.cwl                  |  15 +
 sdk/cwl/tests/wf/listing_shallow.cwl               |  15 +
 sdk/cwl/tests/wf/scatter2.cwl                      |   1 +
 sdk/cwl/tests/wf/scatter2_subwf.cwl                |   5 +-
 sdk/go/arvados/client.go                           |   7 +
 sdk/go/arvados/collection.go                       |   4 +-
 sdk/go/arvados/log.go                              |  12 +-
 sdk/go/arvados/workflow.go                         |  22 +
 sdk/go/arvadosclient/arvadosclient_test.go         |   7 +-
 sdk/go/arvadostest/fixtures.go                     |  26 +-
 sdk/go/crunchrunner/crunchrunner.go                |  66 +-
 sdk/go/crunchrunner/crunchrunner_test.go           |  51 +-
 sdk/go/dispatch/throttle_test.go                   |  29 +-
 sdk/go/keepclient/discover.go                      |  15 +-
 sdk/go/keepclient/keepclient.go                    |  22 +-
 sdk/go/keepclient/keepclient_test.go               |  20 +-
 sdk/go/keepclient/support.go                       | 108 +--
 sdk/python/arvados/__init__.py                     |   1 -
 sdk/python/arvados/_ranges.py                      |   8 +-
 sdk/python/arvados/api.py                          |  15 +-
 sdk/python/arvados/arvfile.py                      | 225 ++++---
 sdk/python/arvados/cache.py                        |  71 ++
 sdk/python/arvados/collection.py                   |   1 +
 sdk/python/arvados/commands/get.py                 | 282 ++++++++
 sdk/python/arvados/commands/keepdocker.py          |  92 +--
 sdk/python/arvados/commands/ls.py                  |  57 +-
 sdk/python/arvados/commands/migrate19.py           | 280 ++++++++
 sdk/python/arvados/commands/put.py                 |  29 +-
 sdk/python/arvados/commands/run.py                 |  88 ++-
 sdk/python/arvados/keep.py                         |  19 +-
 sdk/python/bin/arv-get                             | 235 +------
 sdk/python/bin/arv-migrate-docker19                |   4 +-
 sdk/python/setup.py                                |  12 +-
 sdk/python/tests/run_test_server.py                |  24 +-
 sdk/python/tests/test_arv_get.py                   | 137 ++++
 sdk/python/tests/test_arv_keepdocker.py            |  44 +-
 sdk/python/tests/test_arv_ls.py                    |  11 +-
 sdk/python/tests/test_arvfile.py                   | 134 +++-
 sdk/python/tests/test_cache.py                     |  95 +++
 sdk/python/tests/test_collections.py               |  60 +-
 sdk/python/tests/test_stream.py                    |   7 +-
 sdk/ruby/arvados.gemspec                           |   2 +-
 sdk/ruby/lib/arvados.rb                            |  23 +-
 sdk/ruby/lib/arvados/keep.rb                       |   6 +-
 {lib => server}/agent/agent.go                     |  14 +
 server/arvados-server/.gitignore                   |   1 +
 server/arvados-server/docker_compose_test.go       |  43 ++
 server/arvados-server/main.go                      |  29 +
 .../arvados-server/setup_test.go                   |   6 +-
 .../arvados-server}/test-docker-compose/agent.yml  |   0
 .../test-docker-compose/docker-compose.yml         |  24 +-
 .../test-docker-compose/encrypt-key.txt            |   0
 .../test-docker-compose/master-token.txt           |   0
 .../arvados-server/test-setup}/Dockerfile          |   2 +-
 server/setup/arvados_services.go                   | 242 +++++++
 server/setup/cert.go                               |  24 +
 {lib => server}/setup/check.go                     |   0
 {lib => server}/setup/command.go                   |   0
 server/setup/configure.go                          |  65 ++
 {lib => server}/setup/consul.go                    |  62 +-
 server/setup/curoverse_package.go                  |  36 +
 {lib => server}/setup/daemon.go                    |   2 +-
 {lib => server}/setup/download.go                  |   0
 {lib => server}/setup/os_package.go                |   0
 {lib => server}/setup/runit.go                     |   0
 {lib => server}/setup/setup.go                     |  54 +-
 {lib => server}/setup/systemd.go                   |   0
 server/setup/tmpl_nginx_api.go                     |  95 +++
 {lib => server}/setup/vault.go                     |  15 +-
 {lib => server}/setup/write_file.go                |   0
 services/api/.gitignore                            |   3 +-
 services/api/Gemfile                               |  53 +-
 services/api/Gemfile.lock                          | 279 ++++----
 services/api/Rakefile                              |  16 +-
 .../api/app/controllers/application_controller.rb  |  66 +-
 .../v1/api_client_authorizations_controller.rb     |   4 +-
 .../controllers/arvados/v1/groups_controller.rb    |  16 +
 .../arvados/v1/keep_disks_controller.rb            |  32 +-
 .../app/controllers/arvados/v1/nodes_controller.rb |  16 +-
 .../controllers/arvados/v1/schema_controller.rb    |   7 +-
 .../app/controllers/arvados/v1/users_controller.rb |   2 +-
 .../api/app/controllers/database_controller.rb     |   4 +-
 .../app/controllers/user_sessions_controller.rb    |   3 +-
 .../api/app/models/api_client_authorization.rb     |   8 +-
 services/api/app/models/arvados_model.rb           | 210 ++++--
 services/api/app/models/collection.rb              | 145 ++--
 services/api/app/models/container.rb               |  98 ++-
 services/api/app/models/container_request.rb       | 234 ++-----
 services/api/app/models/job.rb                     |  13 +-
 services/api/app/models/link.rb                    |   5 -
 services/api/app/models/log.rb                     |   6 +-
 services/api/app/models/node.rb                    |  20 +-
 services/api/app/models/user.rb                    |   8 +-
 services/api/app/models/virtual_machine.rb         |   6 +-
 services/api/config/application.default.yml        |  55 +-
 services/api/config/application.rb                 |  29 +-
 .../api/config/environments/development.rb.example |   3 -
 .../api/config/environments/production.rb.example  |   2 +-
 services/api/config/environments/test.rb.example   |   5 +-
 services/api/config/initializers/eventbus.rb       |  38 +-
 services/api/config/initializers/load_config.rb    |   1 +
 services/api/config/initializers/lograge.rb        |   4 +-
 .../api/config/initializers/noop_deep_munge.rb     |   1 +
 .../config/initializers/permit_all_parameters.rb   |   1 +
 services/api/config/initializers/time_format.rb    |   2 +
 services/api/config/routes.rb                      |  28 +-
 .../20170301225558_no_downgrade_after_json.rb      |   9 +
 ...0170319063406_serialized_columns_accept_null.rb |   5 +
 ..._add_portable_data_hash_index_to_collections.rb |   5 +
 ...0012505_add_output_ttl_to_container_requests.rb |   5 +
 ...1_add_created_by_job_task_index_to_job_tasks.rb |   5 +
 ...0170419173712_add_object_owner_index_to_logs.rb |   5 +
 ...esting_container_index_to_container_requests.rb |   5 +
 services/api/db/structure.sql                      | 182 +++--
 services/api/lib/audit_logs.rb                     |  65 ++
 services/api/lib/can_be_an_owner.rb                |   2 +-
 services/api/lib/create_superuser_token.rb         |  17 +-
 services/api/lib/crunch_dispatch.rb                |  18 +-
 services/api/lib/eventbus.rb                       | 357 ----------
 services/api/lib/has_uuid.rb                       |  14 +-
 services/api/lib/load_param.rb                     |  10 +-
 services/api/lib/safe_json.rb                      |   8 +
 services/api/lib/serializers.rb                    |  61 ++
 .../api/lib/tasks/delete_old_container_logs.rake   |   2 +-
 services/api/lib/tasks/delete_old_job_logs.rake    |   2 +-
 services/api/lib/whitelist_update.rb               |  15 +-
 services/api/test/fixtures/collections.yml         |  16 +-
 services/api/test/fixtures/logs.yml                |   4 +
 .../arvados/v1/collections_controller_test.rb      |   6 +-
 .../api/test/functional/arvados/v1/filters_test.rb |  52 ++
 .../functional/arvados/v1/nodes_controller_test.rb |  10 +-
 .../functional/arvados/v1/users_controller_test.rb |  25 +-
 .../arvados/v1/virtual_machines_controller_test.rb |   4 +-
 .../test/functional/database_controller_test.rb    |   2 +-
 .../api_client_authorizations_scopes_test.rb       |   6 +-
 .../integration/collections_performance_test.rb    |   5 +-
 .../api/test/integration/crunch_dispatch_test.rb   |   2 +-
 .../api/test/integration/database_reset_test.rb    |   2 -
 services/api/test/integration/errors_test.rb       |   2 +-
 services/api/test/integration/pipeline_test.rb     |   2 +-
 .../api/test/integration/reader_tokens_test.rb     |   2 +-
 services/api/test/integration/websocket_test.rb    | 742 ---------------------
 services/api/test/test_helper.rb                   |  26 +-
 services/api/test/unit/arvados_model_test.rb       |  76 ++-
 services/api/test/unit/collection_test.rb          |  23 +-
 services/api/test/unit/container_request_test.rb   | 235 +++++--
 services/api/test/unit/container_test.rb           |  47 +-
 .../api/test/unit/create_superuser_token_test.rb   |   1 +
 services/api/test/unit/crunch_dispatch_test.rb     |   8 +-
 services/api/test/unit/fail_jobs_test.rb           |   8 +-
 services/api/test/unit/job_test.rb                 |  52 +-
 services/api/test/unit/log_test.rb                 |  78 +++
 services/api/test/unit/node_test.rb                |  12 +
 services/api/test/unit/seralizer_test.rb           |  22 +
 services/api/test/unit/user_test.rb                |  12 +-
 services/api/test/unit/workflow_test.rb            |   8 +-
 services/arv-git-httpd/arvados-git-httpd.service   |   5 +
 .../crunch-dispatch-slurm.service                  |   5 +
 services/crunch-run/crunchrun.go                   | 471 ++++++++++---
 services/crunch-run/crunchrun_test.go              | 406 ++++++++---
 services/crunch-run/logging.go                     |  38 +-
 .../dockercleaner/arvados-docker-cleaner.service   |   4 +
 services/fuse/arvados_fuse/command.py              |  46 +-
 services/fuse/arvados_fuse/unmount.py              | 108 +++
 services/fuse/tests/test_crunchstat.py             |  13 +
 services/fuse/tests/test_mount.py                  |  17 +-
 services/fuse/tests/test_mount_type.py             |  23 +
 services/fuse/tests/test_tmp_collection.py         |  13 +
 services/fuse/tests/test_unmount.py                |  87 +++
 services/keep-balance/keep-balance.service         |   4 +
 services/keep-web/handler.go                       |  26 +-
 services/keep-web/handler_test.go                  |  60 ++
 services/keep-web/keep-web.service                 |   5 +
 services/keepproxy/keepproxy.go                    | 118 ++--
 services/keepproxy/keepproxy.service               |   5 +
 services/keepproxy/keepproxy_test.go               |  59 +-
 services/keepproxy/proxy_client.go                 |  19 +
 services/keepstore/keepstore.service               |   5 +
 .../arvnodeman/computenode/dispatch/__init__.py    |  14 +-
 .../arvnodeman/computenode/dispatch/slurm.py       |   2 +-
 .../arvnodeman/computenode/driver/__init__.py      |  17 +
 .../arvnodeman/computenode/driver/gce.py           |  12 +-
 services/nodemanager/arvnodeman/config.py          |   5 +-
 services/nodemanager/arvnodeman/daemon.py          |  67 +-
 services/nodemanager/arvnodeman/jobqueue.py        |   8 +-
 services/nodemanager/arvnodeman/launcher.py        |   6 +-
 services/nodemanager/arvnodeman/status.py          |  65 ++
 services/nodemanager/doc/azure.example.cfg         |  16 +
 services/nodemanager/doc/ec2.example.cfg           |  16 +
 services/nodemanager/doc/gce.example.cfg           |  16 +
 services/nodemanager/doc/local.example.cfg         |   4 +
 services/nodemanager/setup.py                      |  22 +-
 .../tests/test_computenode_driver_gce.py           |  13 +-
 services/nodemanager/tests/test_config.py          |   4 +
 services/nodemanager/tests/test_daemon.py          |  29 +-
 services/nodemanager/tests/test_failure.py         |   2 +-
 services/nodemanager/tests/test_jobqueue.py        |  16 +
 services/nodemanager/tests/test_status.py          |  55 ++
 services/ws/arvados-ws.service                     |   5 +
 services/ws/event_source.go                        |  38 +-
 services/ws/event_source_test.go                   | 108 +++
 services/ws/event_test.go                          |  21 +
 services/{keepstore => ws}/gocheck_test.go         |   3 +-
 services/ws/main.go                                |  32 +-
 services/ws/permission.go                          |  28 +-
 services/ws/permission_test.go                     |  71 ++
 services/ws/server.go                              |  71 ++
 services/ws/server_test.go                         |  61 ++
 services/ws/session_v0_test.go                     | 238 +++++++
 tools/arvbox/bin/arvbox                            |  78 ++-
 .../arvbox/docker/service/websockets/run-service   |  39 +-
 tools/keep-exercise/keep-exercise.go               |   3 +-
 319 files changed, 8910 insertions(+), 3720 deletions(-)
 create mode 100644 apps/workbench/app/assets/javascripts/edit_collection.js
 create mode 100644 apps/workbench/app/views/collections/_extra_tab_line_buttons.html.erb
 copy apps/workbench/app/views/{jobs => container_requests}/_show_provenance.html.erb (52%)
 create mode 100644 apps/workbench/app/views/workflows/_show_recent.html.erb
 delete mode 100644 cmd/arvados-admin/.gitignore
 delete mode 100644 cmd/arvados-admin/main.go
 delete mode 100644 cmd/arvados-admin/setup_docker_compose_test.go
 create mode 100644 doc/_includes/_compute_ping_rb.liquid
 copy doc/install/{install-crunch-dispatch.html.textile.liquid => crunch2-slurm/install-slurm.html.textile.liquid} (53%)
 create mode 100644 doc/install/install-compute-ping.html.textile.liquid
 create mode 100644 doc/install/install-nodemanager.html.textile.liquid
 create mode 100644 doc/install/migrate-docker19.html.textile.liquid
 create mode 100644 docker/migrate-docker19/Dockerfile
 create mode 100755 docker/migrate-docker19/build.sh
 copy tools/arvbox/lib/arvbox/docker/service/docker/run => docker/migrate-docker19/dnd.sh (90%)
 create mode 100755 docker/migrate-docker19/migrate.sh
 rename {cmd => lib/cmd}/dispatch.go (100%)
 create mode 100644 lib/configure/configure.go
 create mode 100755 sdk/cli/test/binstub_arv-mount/arv-mount
 rename sdk/cli/test/binstub_clean_fail/{mount => arv-mount} (100%)
 create mode 100644 sdk/cwl/tests/cat.cwl
 create mode 100644 sdk/cwl/tests/dir-job2.yml
 copy sdk/cwl/tests/{keep-dir-test-input.cwl => keep-dir-test-input2.cwl} (77%)
 copy sdk/cwl/tests/{keep-dir-test-input.cwl => keep-dir-test-input3.cwl} (76%)
 create mode 100644 sdk/cwl/tests/listing-job.yml
 create mode 100644 sdk/cwl/tests/octo.yml
 rename services/api/log/.gitkeep => sdk/cwl/tests/octothorpe/item #1.txt (100%)
 create mode 100644 sdk/cwl/tests/test_fsaccess.py
 copy {services/api/vendor/plugins => sdk/cwl/tests/tmp1/tmp2/tmp3}/.gitkeep (100%)
 create mode 100644 sdk/cwl/tests/wf/listing_deep.cwl
 create mode 100644 sdk/cwl/tests/wf/listing_none.cwl
 create mode 100644 sdk/cwl/tests/wf/listing_shallow.cwl
 create mode 100644 sdk/go/arvados/workflow.go
 create mode 100644 sdk/python/arvados/cache.py
 create mode 100755 sdk/python/arvados/commands/get.py
 create mode 100644 sdk/python/arvados/commands/migrate19.py
 create mode 100644 sdk/python/tests/test_arv_get.py
 create mode 100644 sdk/python/tests/test_cache.py
 rename {lib => server}/agent/agent.go (90%)
 create mode 100644 server/arvados-server/.gitignore
 create mode 100644 server/arvados-server/docker_compose_test.go
 create mode 100644 server/arvados-server/main.go
 rename cmd/arvados-admin/setup_debian8_test.go => server/arvados-server/setup_test.go (73%)
 rename {cmd/arvados-admin => server/arvados-server}/test-docker-compose/agent.yml (100%)
 rename {cmd/arvados-admin => server/arvados-server}/test-docker-compose/docker-compose.yml (60%)
 rename {cmd/arvados-admin => server/arvados-server}/test-docker-compose/encrypt-key.txt (100%)
 rename {cmd/arvados-admin => server/arvados-server}/test-docker-compose/master-token.txt (100%)
 rename {cmd/arvados-admin/test-debian8 => server/arvados-server/test-setup}/Dockerfile (85%)
 create mode 100644 server/setup/arvados_services.go
 create mode 100644 server/setup/cert.go
 rename {lib => server}/setup/check.go (100%)
 rename {lib => server}/setup/command.go (100%)
 create mode 100644 server/setup/configure.go
 rename {lib => server}/setup/consul.go (67%)
 create mode 100644 server/setup/curoverse_package.go
 rename {lib => server}/setup/daemon.go (97%)
 rename {lib => server}/setup/download.go (100%)
 rename {lib => server}/setup/os_package.go (100%)
 rename {lib => server}/setup/runit.go (100%)
 rename {lib => server}/setup/setup.go (68%)
 rename {lib => server}/setup/systemd.go (100%)
 create mode 100644 server/setup/tmpl_nginx_api.go
 rename {lib => server}/setup/vault.go (94%)
 rename {lib => server}/setup/write_file.go (100%)
 create mode 100644 services/api/config/initializers/permit_all_parameters.rb
 create mode 100644 services/api/db/migrate/20170301225558_no_downgrade_after_json.rb
 create mode 100644 services/api/db/migrate/20170319063406_serialized_columns_accept_null.rb
 create mode 100644 services/api/db/migrate/20170328215436_add_portable_data_hash_index_to_collections.rb
 create mode 100644 services/api/db/migrate/20170330012505_add_output_ttl_to_container_requests.rb
 create mode 100644 services/api/db/migrate/20170419173031_add_created_by_job_task_index_to_job_tasks.rb
 create mode 100644 services/api/db/migrate/20170419173712_add_object_owner_index_to_logs.rb
 create mode 100644 services/api/db/migrate/20170419175801_add_requesting_container_index_to_container_requests.rb
 create mode 100644 services/api/lib/audit_logs.rb
 delete mode 100644 services/api/lib/eventbus.rb
 create mode 100644 services/api/lib/safe_json.rb
 create mode 100644 services/api/lib/serializers.rb
 delete mode 100644 services/api/test/integration/websocket_test.rb
 create mode 100644 services/api/test/unit/seralizer_test.rb
 create mode 100644 services/fuse/arvados_fuse/unmount.py
 create mode 100644 services/fuse/tests/test_crunchstat.py
 create mode 100644 services/fuse/tests/test_mount_type.py
 create mode 100644 services/fuse/tests/test_unmount.py
 create mode 100644 services/keepproxy/proxy_client.go
 create mode 100644 services/nodemanager/arvnodeman/status.py
 create mode 100644 services/nodemanager/tests/test_status.py
 create mode 100644 services/ws/event_source_test.go
 create mode 100644 services/ws/event_test.go
 copy services/{keepstore => ws}/gocheck_test.go (76%)
 create mode 100644 services/ws/permission_test.go
 create mode 100644 services/ws/server.go
 create mode 100644 services/ws/server_test.go
 create mode 100644 services/ws/session_v0_test.go

  discards  e96966556520e3d7cd27ad253418656892355c0d (commit)
  discards  c78289e5956a55c7540c2a3f6a543f16b1eec7c2 (commit)
       via  059a62b51d90f82e4286835d26d8bf523a54132b (commit)
       via  87f40ab7ba0a3f0fc1f3648c917ff929e6e633b8 (commit)
       via  1522e9dc7cd4ccf9a636a8d53091f05560accfb2 (commit)
       via  ff310f43338a2443f9b11b7a55e619dc1961d234 (commit)
       via  55466cec28927431c15e61078b9e56225ce77c72 (commit)
       via  957ec5ff80456706aa3bd709669e4a17251f6791 (commit)
       via  fb08faf4b134a09eee928b596382cf17c75a8c89 (commit)
       via  a328dd7d4d396d1c2f67d3a74989f0194b04e714 (commit)
       via  887ca969a3fa97f2181a089ac22fac1ff1e3ed25 (commit)
       via  1883fbbdc269fa0bdf3099984160e02421bc94b1 (commit)
       via  e56ae6aad06c37d5512537047871d7363dd97620 (commit)
       via  e39a7d5704932cbec606e85cdca50aa38a1ed053 (commit)
       via  88a25d414025c64231f977a9383fd6a69cf6246a (commit)
       via  72900c01e197d602e79fda8d306b17fd1e32a3ea (commit)
       via  6a7edde0c5b906c58a7e739f2d0c612c67f63dc5 (commit)
       via  41a7f3914a6ced0fd374bd903470fb4fc91ea5e4 (commit)
       via  f5f5de0ae41e12738a380c422417d5a5e5af7f09 (commit)
       via  e8317521741c3814e824e209f75edf23636e32ff (commit)
       via  318c49002aea966128a9d37ab29e601a104d79bb (commit)
       via  b075d1be1377760f5d8497a29f63c8e416cd5378 (commit)
       via  8c5f2973a5c5f042d1d12aef1c470b37519fd416 (commit)
       via  edfc619e6189c5407d16798c75aaace08a13536d (commit)
       via  90209af8fa35bc99c9821db0c815404d1234ef31 (commit)
       via  88c241d7c4fbfadb951f370bf2706db687adad75 (commit)
       via  137ebf94ff14837c9df773533ea86e821469bda9 (commit)
       via  260e85a9d9cf2c20313bcf2edb63da57bdd3a69f (commit)
       via  30146198f24c70941d95af714e036ff3c451626c (commit)
       via  95e2bdda5afc3ffc6afb2f08ea6d7cba8f8d62f1 (commit)
       via  629557aa041a80f0704b02e7c679b2f01d9c0be2 (commit)
       via  f2f8340b18430738a9527f05e707dd8f03508cc0 (commit)
       via  ad7294edfcc59c3e67548328a88c9e689c3ae2cf (commit)
       via  3ef580c47029ff0fbf959b044f29c183f41cb609 (commit)
       via  4ccbea9ef440a7e4252b0df5e710dcb767831c60 (commit)
       via  c043e133b2646037ed630d571e91dbf77344f855 (commit)
       via  93c92875aaebe5b06f8dbfe2822b59a772895c08 (commit)
       via  bf5d77baad2071af6eea514c76b4892cec4974a0 (commit)
       via  04bd6b08b9ac13d29ac05c9281850d430d71066d (commit)
       via  840b855ff0317e66f4176ae0f23e9785f72267b4 (commit)
       via  1220e2184449ccab288fa41de4749fb029cd317b (commit)
       via  17b80c32a5b177ee8c5f32b81dd0889f3399eee8 (commit)
       via  9905d124877e5695053cce388d3680c667815de5 (commit)
       via  35c2572761bb060aa1c12f417f97aa9e1ccbe7eb (commit)
       via  a93d95d85ea15c7afd70657abf60635c29043c89 (commit)
       via  f4661a02245a35f8d223693a5aecaae87083fb16 (commit)
       via  52c6f13db207030bdbe063665c0dd524007db828 (commit)
       via  0ecea550fde014578e71004360b700cdfeae4909 (commit)
       via  7116da151dc8bfd5ac1a9b016b2ed6e4c35572f7 (commit)
       via  bef5d901f00648e703bb6a3ad58fa481a610ffd7 (commit)
       via  b04638275cff9b393e1bc04136d44f361b999cf8 (commit)
       via  c39ba5193005a4e9f619901f8348f11fada88df0 (commit)
       via  2333472a4f517a227278f028bbbc4e72687c0e71 (commit)
       via  5180238a10bd15302a1c15b9a428f2fdeeabdf4e (commit)
       via  bfc9660a8b2467893baf131b20e83e76c41ae438 (commit)
       via  aed7702a67426dfd9d24b512c90df8e909162179 (commit)
       via  9aa83ad7b4de05dd2818885ed34111d4dcf322ea (commit)
       via  3c34e713aa343d56c7cea00a9c998b06dbf411d6 (commit)
       via  65e339856daf4b5c3a4a810cd3a5f1a8e386dc8c (commit)
       via  151df8c3b177e4971bfbdf68c87d89599dbe0812 (commit)
       via  e7284afa8ccb95994dcd2009015cafc6180e7187 (commit)
       via  82c40aa7d30cd8e68e2a1bdc0bd8bf03cdfea029 (commit)
       via  38a4b3c43a8a6cff5a00624436b8eaa5cbfbc76b (commit)
       via  f79536fda9dc40f480383caa69a35663702b2ba4 (commit)
       via  c14ae0edcb3c386e50f46218184e8dabcbc20a37 (commit)
       via  04951581a941697d68cdaf9af6661c3c412f1bce (commit)
       via  de99c0b2effdd43d3843f15475cee84dbde8add8 (commit)
       via  e3ac17f8a8aa439e21a8bf56a571f91a671313f7 (commit)
       via  244c47436f294638271eef637997fd00f7ca49f5 (commit)
       via  67ba19113789346005aa61d4234bc33c8677a85c (commit)
       via  9609f9a5a4776671f571f765a179506d26df56da (commit)
       via  03188ad6eb14ee3dcd6bdf74198624c9358936c5 (commit)
       via  692a66affa483483d67931a6b095b361bfd06d24 (commit)
       via  243701b66914b69c9f9a94364e84713c8fcbc1da (commit)
       via  ea40bcae7a5d247ba8667c8c866339c2a8424464 (commit)
       via  cf311e8e16ba74467c77b5353afedc29b40a6a41 (commit)
       via  20364bc1aed2dcad71b006f4314ba0720d20ae05 (commit)
       via  72a377c59d972dd64de6e500c140738ff4b3dea6 (commit)
       via  fc2eaa20275146369e451da6cf14f4461c68117c (commit)
       via  bf55ee80e71c6503292a44fb377927f2ed908f9b (commit)
       via  9cab6a09cca4153104694f0dd4644c5aa5f54b22 (commit)
       via  cbcb0fc8c3ebc85bf81ba9d50795d62db75efa6c (commit)
       via  e507e67ee50852fc44011127eb42f535fe2ae493 (commit)
       via  d64ed33e94700f8204ec8089c7b235cff918f9f7 (commit)
       via  88c26c6b2dd8752ec1ff8196f4af11369ea6adbb (commit)
       via  91f1e7009305109db0bfb6405adbd11357745bfc (commit)
       via  2001423a6eb7937a689414f3fa62be5b124812c1 (commit)
       via  3a88a5d8753593be89373c5b124fc5a8398457a0 (commit)
       via  defb299deab3be85644a7880cba492a73c2e7e62 (commit)
       via  5152e0bf159151eaf04d31b788d57bebf7ab089b (commit)
       via  a3488ff35d219b943f3f52c07914fddf2009a140 (commit)
       via  574dc168c559dd680d26be1306c4a3d8b936c6e8 (commit)
       via  da32ae2a73012ce55cb89b2de9a4716b2800eee1 (commit)
       via  2f06f0e4d5b9ef371463c65dd5cae19f5b385a17 (commit)
       via  29d77300205e485be6595d9cd9276c246f3f89b7 (commit)
       via  af78d389b0ad37858108b69fe5100443dc88eb4a (commit)
       via  0035033f6bf6db955b116e2e0cc052bc5c79d80e (commit)
       via  49a841a5179307f0c8d84f647a71f44fb2b4b26d (commit)
       via  bafff6941293d4cc12cda4209f1d7a2b10274ea3 (commit)
       via  cb47df6cc327a59efaa16170f840c2745162bc86 (commit)
       via  a734789218122d8ab0d8f766bac4d69c04db91bf (commit)
       via  00573cf7d28472bf926e8f610a256cd991879c8b (commit)
       via  4431b9c71fc08955a89d1af70de3a8d5174c6e9f (commit)
       via  f61c7d9445c216c2364734fb1da2bea00cfb581d (commit)
       via  9ea73e2a66dcb1311732d43ea052412546c625fe (commit)
       via  224098499104939fccdb07b39407937c61983687 (commit)
       via  d3a82af33386c99c0fd3d6471df7e6696560089c (commit)
       via  f70c457c73f1bff6314f8c3eb4ad7be0c44a5f1a (commit)
       via  1b290e512496287e389424f3950f660f83c1c59b (commit)
       via  cb4f8b337c56dd3e7a648dda86031ceba65ee6c4 (commit)
       via  d72f1d79c0a53056a7cecad6c65fd57a183059b0 (commit)
       via  c03283d4da26ac9a6551dd00b25882b14c258da3 (commit)
       via  e1bc430748edced07f43f6c8efd73bbf2d828ca0 (commit)
       via  76e3d80bcc1548daba7bca97711684e5d68c1624 (commit)
       via  777e716728a7da63ece00df7e5bb8be7f9a2a1a3 (commit)
       via  a6be53f6913862b3884c57e9e93a891f3cfec471 (commit)
       via  95ef2a6f4145d8c6058738b6de0bce098e3423d6 (commit)
       via  8be16cfc7e163cc96995be891e53050febfb1fca (commit)
       via  828161a7527c535e5c657cd17b79cd562475fe82 (commit)
       via  2c094e28cc4f19df4f6d0bc81355e7fc19e8c493 (commit)
       via  a779382603d2da2ec38ceb8a21262cc4f151f077 (commit)
       via  26eb8f4cba5dd20ae49bf509a9a772ee4fe8b396 (commit)
       via  de083a9fec0ca08afda5a9369c6cd32dbdcd0965 (commit)
       via  f5d09a4904e609b5df20edd0194a9f1ade40c28a (commit)
       via  be2a34c57eab7cc184c08ec1059554cb97911276 (commit)
       via  533bf8f1120c0f3fd74a31dfda990cc832b2aa51 (commit)
       via  96b6e26749f3a7076b585758d44244777cc628c6 (commit)
       via  f08a738f1565b2dabde28dc9e8d1bad9622d5529 (commit)
       via  466e0e2623bb85870943f48fe1ab3016dc32d652 (commit)
       via  c1ea9a2fdccc5e24554f8cf28296cbc0764c8bbc (commit)
       via  f9a1468065776ee71eb43c45e6813bfae69fe0e0 (commit)
       via  0d5be4fa36006459e1579f087b695904e4f32ee3 (commit)
       via  2f46fb5769f41ac9cddf8c46bd0d2ab094375e82 (commit)
       via  77d9c05d89dabc9e9e9a15f46cd12c8ad61ed64e (commit)
       via  909313b20ed5055d62113f0eea123f362869c3f4 (commit)
       via  4f68551a87ff2cb88c23a2942abe2676a33f7cbb (commit)
       via  e7876a3ac520b128be7836e30172079ab2af5e45 (commit)
       via  f49bc2790a18de14e22e69fb39f93805488fdbb3 (commit)
       via  ab9a73d2c0b567d3c05d1d4d8463633a69eafda2 (commit)
       via  3f5c2b95de8483e277a472846ad7e5b2ccb1f79a (commit)
       via  86a6e4fe9347570090d1d1471f98675c3fb758f4 (commit)
       via  157847615412907e87d24fb1767470d0157a4c30 (commit)
       via  1b226fee3268f50ef670289fc5931a00866d69cd (commit)
       via  4405570376295039b4c00577535a394eb011f8ad (commit)
       via  2f52a6e7d9945f51d33b047466c4d927d494bb32 (commit)
       via  0654fa160f872fe20ea4ada42a655f9d154c0833 (commit)
       via  330f544dd257a43640eefc64b825b868f6e5be21 (commit)
       via  351f06f68e8b079377bea195c67fed171b1e4b73 (commit)
       via  75184884ed798b474e8b9e254045dc0f4354379e (commit)
       via  2ebd44960d1e7a0431d6ad612298012627c5019c (commit)
       via  a869fa401696bc481380be239850ccf3edd808f6 (commit)
       via  9dae3e6dde2993bc44e90539141b442c899cf114 (commit)
       via  1c1c11c2c2d6cfd1d379cdbb166498514768be4e (commit)
       via  1d0d28c791e88f817b1c01560ee8efd70bcab41e (commit)
       via  3ceb42c882b32fb9e4ef79679576254f98cdfb3d (commit)
       via  e3c48eaab3bbf538a03e575aab09cef59ee69f18 (commit)
       via  ed46350a4b4cd947d126b5e1e9a0514aa0b93532 (commit)
       via  921d4321201be7542f9198202f75ba39c3f83d3f (commit)
       via  1affdcd3cd34424a817ae350fd9ca236927fd538 (commit)
       via  38fcd08dda022d0167840fbb65222fe99b75fcf5 (commit)
       via  33ec0939cd5853d1c14a621c551ac2705efd1cd7 (commit)
       via  b594678083d6cd7efe425bb4be63a444f22ce153 (commit)
       via  7b31dd40b31bdbf7473b50fd407cd6f636657c69 (commit)
       via  8fd18fc4a32797fc3c6255099fc253d7aede12b5 (commit)
       via  ae69864e07d416d10879e00da416918ac021893f (commit)
       via  7ce46899b5ed250aa909fa240b6be3e01637fb79 (commit)
       via  b50a3bcb38cf9e79416d8cc9b0d8b66249b3d473 (commit)
       via  c3ae1e2f54d4199a9521bf3d4d515bcbb0711989 (commit)
       via  6da9f3666bc0ddee2d0be079cc30ba8b82804706 (commit)
       via  dfd3260e8b32ac4b9640e3e33bf8cf1e581bd917 (commit)
       via  42e6998c32f8d5553133e4b0b3ea02cd0c6f5554 (commit)
       via  4bb024ae6903552c29613c851312f934dc6174b9 (commit)
       via  a03ce4056e503710caa1e95d315b92fb74c96abf (commit)
       via  77f5a84ccc2b14438286ed05c6af183b8d8be605 (commit)
       via  4a35e06b098bdd44a24fcaf77921aea5f371c84b (commit)
       via  313415e33ca374898a371b9f22fc6546793f4688 (commit)
       via  2c0c6e6ba25ec56bab8c224865a7979af577adaa (commit)
       via  07c92074fe2b493dc8cd0abdad154e5b36d4adfc (commit)
       via  1654f776b55bbb861f72c0dd3118ad1beec5db31 (commit)
       via  988e9052575bc93db2909f3b9ef576f1043eddcb (commit)
       via  3cc77494ac02e0f64a814cd4a898662fb7b329af (commit)
       via  17f22a2b220c38ebae5247d09f8c96ceec151957 (commit)
       via  ff3bb22d4b2bff5666907a6eeb6cd68cd3cbe22b (commit)
       via  b651984ff68c6f8add9b99427b205a2cb7c87462 (commit)
       via  55c34fb97edfba5a014eb43b965130cf11dbda8a (commit)
       via  28e59edcec4af09f78be965fa3abfaa9ee3f8dff (commit)
       via  4af850d6898dea0c5f460f6acbc52f2e361cbf1d (commit)
       via  736a629e16c388a5afecc97268bd7eadfd7ce3c2 (commit)
       via  5ee2b9600ced7db8fefe141c837a894f33a1d129 (commit)
       via  c820bfc91be7635739bad0857ba3a385d1334b6a (commit)
       via  b9236fbe81426446e1b541a45e219bbe513fe8d0 (commit)
       via  fb1c8e81a200c11b1130f3a9af586f1bbf8c19b3 (commit)
       via  b351a877ff2d708acc5fe577d5ed5cc9b0469dde (commit)
       via  0b04efefcc49b05df10a202cff5e423bbb587923 (commit)
       via  65121f8db54a1ed15207d050e1f48c5fc26d646b (commit)
       via  1a4a2f3219906220c0b4d7fd9b90325fa529408a (commit)
       via  4437774e863465c0daa41dfd9716174e18d93122 (commit)
       via  a12ed889f9d0106ea26d0e2d6ff1f74e9ab14aac (commit)
       via  c507b0b072ad62c0087d059aedeaae8bae9b715f (commit)
       via  38141f7261a7a2fa29f9cf3281cc46c1d845c1c3 (commit)
       via  acb0bd431ced4814b37f703ca63f2b39ad29c5df (commit)
       via  0568c2d42703a7b839f2661968c05a23753f67c3 (commit)
       via  d6703f161c75a780ff645dc1ec980fca8a3e315b (commit)
       via  bebac87e773d73788de9273d65ca92db6878b2e7 (commit)
       via  e8e9262a0f5e1e5908d338ac1655a2cdfaecf23a (commit)
       via  6fe136c2c7419dfb38a9af42898682701646b518 (commit)
       via  f34f20a9ac4b7ade7eeb05157a3b190d8a98c37e (commit)
       via  154ae0d4b13329bae1033ab99095c00d0b0f66e2 (commit)
       via  ff8d14acd42952a21f5428e96d86e4a54c41be9a (commit)
       via  433c48edb1523809bb8d43e4fc40ebe1a177e103 (commit)
       via  ae7f5a9c869927336dc81cf0552b955457a09647 (commit)
       via  6828728001af20d8b75d841ead727c47d6ee2c96 (commit)
       via  37313363a84fc41dcb87a8c9aa8b8502aaac256c (commit)
       via  de1e1bf2e6cd455135f754878966711db4dae9ec (commit)
       via  d040869ca4926c0eb39e072bdabf139bc1f25dd3 (commit)
       via  fbc867e0b02a0482d00000d76c5d0d343b7f252e (commit)
       via  c6a8839b1888d2eeb302ddfc675772428b8895a9 (commit)
       via  78ca1460d58172f4555409298968583cd2ee70a1 (commit)
       via  0708d4a0b6db9426b9bb5addc1c592d526bd4ecd (commit)
       via  25ab6bebfd55db837aa049e5c0fdb2ce28f98f30 (commit)
       via  201ddbd84144a10bbcbc72a3525bb0c4e743570d (commit)
       via  692ef5fd300fef4ef1b724016c1600dbcfc8a070 (commit)
       via  d7d00f5140b7622fdc55a4058be73b89d587b7b2 (commit)
       via  7bd9af41d106590856e4d8c317502d1f4f56aefd (commit)
       via  5cd99e52e342db19c1a77369a5a8c027ee04feaf (commit)
       via  33b9f9bd117931bae54f45566fb77e654dcbe263 (commit)
       via  6b0632d7706cbb344331bf5d291cafd541070cbb (commit)
       via  820d3244a25aa8b4658ab79cd8368492aeddc739 (commit)
       via  acefccc4d506ed4bd3f51d3d88bc3a826b28be76 (commit)
       via  b3d286cda65b90e4dd0aaef88f085f45ea855ed5 (commit)
       via  2b27132d13fd72f29dbab2297f0d2dc1c110eed1 (commit)
       via  46fac76c9e6e325afcce3e17d51a7c7eb7340e11 (commit)
       via  3306ff5835f05b9d1e63a8254ec9d5e6ab41f968 (commit)
       via  32430fd402dc70b32c945c72b0bb73abbe00a913 (commit)
       via  f92654c53d54dac198078d42d810b65383179073 (commit)
       via  01da808f446e944873c611b6d229e01980c80f9c (commit)
       via  fe0751fd604792c7a0884e3533d8acb03b6038d9 (commit)
       via  126a8a239853e46cef3d4b7e5acf7620c0bd1f36 (commit)
       via  07e060db9abde587a4c56ab47b9c86d3c5a0ba7d (commit)
       via  11e4219911b6a3ff8696c7a0401ce4dcaac64d5f (commit)
       via  71f0b2ccf5f82c5ff86e51c06060ef4a9234e6d3 (commit)
       via  fdc7164e11eb019122b6deb9054decb9fe0be0ce (commit)
       via  4b8e618c36fae9231601b9012fc18c5c2ee43a90 (commit)
       via  189495ad597e9b5b167bd47a1ef2b25393e39692 (commit)
       via  e1e05845b74ce70712e414830f992ce57d7a8453 (commit)
       via  78ff2a600b29f05f522f8e8818967dac88394fd6 (commit)
       via  7fd4aa96f997a133d31b3df88a8d2f4820c5b881 (commit)
       via  f14965fac774465c0179f5e318c9df3df9b6a05d (commit)
       via  a774873482ade7b9dd0f9a607c97b848b77fb6f2 (commit)
       via  2b2c23bf8e997735dbf6c493b168ee3a3ff52be5 (commit)
       via  dfa41e508c0185d8b933b5d1af0aa5f473a10f3c (commit)
       via  fb7bb4c8f17a49abab40e42b7a0101cac7478d60 (commit)
       via  8b4d5991f9d5691b9fa2898d6f60eef8dbfdf987 (commit)
       via  823ef13277f4c5fe6711dc320bf9cd2f76492c2e (commit)
       via  8cd68a569df236ff8e150a556cd8cd69169b113f (commit)
       via  387376e59b6624703117b2ffc296211712aaf970 (commit)
       via  c33eb3191d36b7e39023db0c0f1790a1bad2c4f0 (commit)
       via  922e79bea1cb6d4df7cf1db48c4b73ebac03b64e (commit)
       via  ca19a29fa8cf99f87c074a68db25fafde35ec917 (commit)
       via  7e9156ae4a449a9a762561ef611d971d9bb763e1 (commit)
       via  8d9b12f2a87ffd7183d3a36ca32ee1c7e701a0e2 (commit)
       via  d42ec212e992a83f8e7fc48b59fb3daf58a62787 (commit)
       via  c066a2e6d064a270638baea8f8b0d106f5903e0f (commit)
       via  c36272a5f83ba70980160c4cb205bdfb8a1c660b (commit)
       via  045bace65c2395b6efe9f3d8c93bec74196f58e1 (commit)
       via  7c6852e1b3675b3c1de7e9792373333cb752d40d (commit)
       via  dfd8c4bbd6f126b90d436e9d242cd30e15e70d2e (commit)
       via  de4df7f80c531ab16e59ea36671a8efa9e6ff33d (commit)
       via  126dd750f48654cb3b1a4e53c5b7d337003e112f (commit)
       via  30dbddd3b311653652fa731dbff950aba0712301 (commit)
       via  ab314b9ea3618c71556a6a5f6dd7c769beaf2737 (commit)
       via  390af6a13f7c8974329aecc2f23fbfa81f8e298b (commit)
       via  9090c60b28de593b8bb2ce606a9ab35b62b57608 (commit)
       via  84ad215752fde4291070143411a945fa7a94241c (commit)
       via  c9a361f7fd3b1cf7f4959e9b0292d0f495d82771 (commit)
       via  05d453ec38b10a022ea6db77867957e7115b9b35 (commit)
       via  09dcf71e59907c2eaf4b94918c63da07193481a4 (commit)
       via  099a8c62fcb0905855ddf243a3deddc7398c3c10 (commit)
       via  13c47e13840caa688711a30a80a35637714e034a (commit)
       via  e47a4660df55a7f45bcd3df5d4c041e13f194ac7 (commit)
       via  b7a664f09052ac048e506bed9bb48b54bc2a9bd4 (commit)
       via  b3fe30840e96e7fa77de047fae2488d703a49d89 (commit)
       via  2d0872f936df8c357bbbf9ec2aa8e35cccb2c5bd (commit)
       via  ce4970b9546b586c8caa8416ea13dab8c4866d59 (commit)
       via  d31988b6da2d779a8f3ba4ba3c40760f9729c59f (commit)
       via  0dea965e36e7ac52a789e31b45fc70f1925c4190 (commit)
       via  fe446e10da189d3d3e0ea5f19061389cc2200a08 (commit)
       via  bbacca53ce2e9eabe3c8e02cee8d3650864bd3f1 (commit)
       via  727c1e475afcb1a8fda1f9b689bb3e941ae93cec (commit)
       via  cebc9af013fbe65d83cb4a60813e3d1871f7ca3a (commit)
       via  4e3263eaac83ede993110f1842c557c28be765e1 (commit)
       via  86ec536f01179ecc1bca0f32a4f41ad307b23c7e (commit)
       via  4afa501317cc04596377bcff8a7b2fccf7ab8d8d (commit)
       via  ae8aaa4c55762222c837fcce8e9ad6800ff8b128 (commit)
       via  b8000c3cb38b77c5c429e0fd591a43f5eeee64d1 (commit)
       via  2e32ef1657b439c0398e66930c3d17437032fb1a (commit)
       via  8ada36c931712304c4b2c70bdcbc316b1ad2c4e2 (commit)
       via  4600343d1bff7ac4f7b9f08486541444c31af8b6 (commit)
       via  fd7767148061d21b77bd97a8856191d6c9bbc077 (commit)
       via  6722b420effcab24dffd9b47fa277f8830bd4cca (commit)
       via  a8378b8deaa2bbf9d2c154d9d9bb072538c288cc (commit)
       via  2aef6ca08d80c0fd25d74ddb9ab52cf535a33d3e (commit)
       via  5aeebd0cdbabb1cfc815b21b26fb87622b05f6fe (commit)
       via  e8cc0d73309236d6efc243371969f83808e42d30 (commit)
       via  1667f9860de21d29bbe32bb827db29eca62d9aeb (commit)
       via  c56743e301b49163a56482c13e49a01c9a0fd7dc (commit)
       via  ae970cb115251915c0a8e1052b23acdd2ab70fee (commit)
       via  a67faeeb159323d35d2a3229c7b5d014dc175767 (commit)
       via  82ff0337a99b7aaed626a624633b8c068dc5e142 (commit)
       via  950ae9635334cd1ca6a2738b185f6481cc3d771f (commit)
       via  83203f5c739ee0b0199e76babccb60e832a0de8e (commit)
       via  7772dca4096e64b1033fb9f42e604699ce07f782 (commit)
       via  514cd364c3cb27b633c1368cd06d6a54927c98a8 (commit)
       via  366780077def317818759e999263577d8a9f5064 (commit)
       via  e47fe8663303df51081a77646e061b11aa892df1 (commit)
       via  f0a34ecb9b41b7d4e35575803ba3dc283f369724 (commit)
       via  8d2dd8003b6e865033e372b5db76fc2244378964 (commit)
       via  dc6c3fccb583ae98eee808addb526c45ebdbf2c6 (commit)
       via  bc8a4a8863f147e4c0b95d0aa3e2f3e549637ee4 (commit)
       via  262d1e44ddaf3f014423a4acf96a3a6200279820 (commit)
       via  a54e88868ac259443e2cd8d5f6fddb4b8154acb9 (commit)
       via  432e71aea50074b1674e96fb1a03cf512952ab75 (commit)
       via  6b2005a2fa3e7c040f8db222010fa870f87336bf (commit)
       via  5976c751048100e813edd638632927794df072f2 (commit)
       via  b50e323ec94e9c7355c6f1c5b93f488540d08ec3 (commit)
       via  6498c7751cd0305a28494df45a70965ccc6c3737 (commit)
       via  49510014c88646ef4320b137240966ba8f6d5108 (commit)
       via  5689f1de6e1a263237303b73013dcebb4ad77e1f (commit)
       via  659b49449d313cdb218a46ca36ac124674a4d00b (commit)
       via  2fd606b328508babad9af6c0a30c159568b525c2 (commit)
       via  2741b54c38ed1e32cc9f0129614a00d84f51bca8 (commit)
       via  8b278cdd80f8969a954a2c789281f8f63195e894 (commit)
       via  077878d94771c25c25edfe01a98a523898916d9e (commit)
       via  05c97120decfeeb9ba2faa20561ee1f37c8293ba (commit)
       via  bbe8547e183a66fd551adffe59fbd4f5c146677c (commit)
       via  f8084a54742def1915c9f4be290f9076033af152 (commit)
       via  37e995fb7423b2b353c599e2a1b00bda7c29ee6f (commit)
       via  79e53c0eed77396cb37f60b48be0c60fe7e0ab89 (commit)
       via  b77893f2a8ae755f22615054f2c267d990995e1c (commit)
       via  e20dfe63dea30fc45bd4fa50aa9d6b5ab1040c99 (commit)
       via  f31475dfeb37c0e4d6b5244cba3bbd06e323b8e8 (commit)
       via  db7c2ba7523fbce1dc47ea1a715a738a28b3bb3b (commit)
       via  433cd703220880784c8dab316501d1c74b09e37b (commit)
       via  9c99316ba4a62ab15b383a8e3b2ec6f4d7165a3e (commit)
       via  03589784d8bc566305e1ea3473a8b37941147517 (commit)
       via  fd42d69ffa558918ec40f9aeaceb2cbf93de1e8a (commit)
       via  99ee47018fcb7cff2adc9184e2063eeba9d55798 (commit)
       via  2accec1f7ca3e2dd1e7fe9ee28bb6a8a6cb74abc (commit)
       via  de283bd8dd383a4177bb714212cf7c59f38a23d8 (commit)
       via  b30e81ee46c196476a03bc244fbc4419573ba34e (commit)
       via  da8c9048bf4f3a36f5691aa52e56e2db672dff3a (commit)
       via  47178c71032b476e34fd0c6b10065e1b96a568f1 (commit)
       via  bf03dd4f523463b896428527fa0685631e9ac649 (commit)
       via  996b635700d7270229200a56d2c2b9f7c96a84fb (commit)
       via  b60a21fe3482e1f7ca1815e2a90e276a8c377636 (commit)
       via  2c69d491fbbd1461bb5a6dd9c0f0dd493081b45e (commit)
       via  9f34ff6f6a6ea1d384528a093bad1c72bd2cea89 (commit)
       via  73dbf5bf94b162e5e9ed74cb60f17ee2c7018059 (commit)
       via  386faadf691e444b71d6c96e7c00792d9a0ba2c7 (commit)
       via  8513e042b0033599146546bd3a2ad903c67c9ff5 (commit)
       via  1230d8a106c5c62edcbb9fcf6d1b94585e5596b2 (commit)
       via  54837bcae94d30f37f19e4ee436c2bb96bd8f21a (commit)
       via  b1aa6c85c3db947f0963b1bddb2784c115f5c97d (commit)
       via  98329b75276412e605d54b3a0f907a5cf204384f (commit)
       via  91118e3af51c7fa655f14f2da99216f8ff0e11e0 (commit)
       via  bdaa9de9882fee122fd2274d92ea500113df8195 (commit)
       via  d363323bfea33aee9182e71fd93387b0c4f944a2 (commit)
       via  449d780f0a470f67d1a28fe1459a8bab79562069 (commit)
       via  eabc13f27dfb178069546202bca772f893bac81c (commit)
       via  dee634b21e48e467b0d7b5ef1b2965c1df5e41d1 (commit)
       via  f074ef8181c2b93cbfcd870eb67c8c95cb0e442a (commit)
       via  2ed5325b37efa7aa9d38a60c1d5c8b4980df8489 (commit)
       via  55c719bff1b34d037506639fd4cf7f0a74f4c3cb (commit)
       via  6058f336a4562b6d6780c4fcfc4d28b6b2a51948 (commit)
       via  1d7577d08b6ca646fcca0e941ce614c37051264e (commit)
       via  205b07766565f3ce7b80bcdfee88f83af451fb01 (commit)
       via  0eb521827f6a64e567722527f36f0b4d130af504 (commit)
       via  5fffad607e39e331f1051c35c5b70ecd5a30939a (commit)
       via  355fe24bf5a4ac9e45372333e8bbc8e39b5b86a1 (commit)
       via  25296def9590c8a29bdcf75c78a03444d39ca6b6 (commit)
       via  c04608e40b971b2fc342db8e5e44817b568a3a4c (commit)
       via  a13833894cda6bc1c872b3eb4215dec5c67aad62 (commit)
       via  7eb2eff1864696bb51b2f4913499da5b62fb74ec (commit)
       via  6ea807b2caf6c934f170b2e4d89c23c4a08ca69c (commit)
       via  c7515954731c6b7c0d8fb241ec4316a5e6d62d0c (commit)
       via  dbb0d22a96fa2de606430d78c05c254598f501df (commit)
       via  918e16de43e993894dcaffbe0cafa0fc1d2f16b3 (commit)
       via  a72205728f94f5261b657766e01f5767dc15d4b5 (commit)
       via  e1e0bec5d9828fff1b9269322d415789675e6fab (commit)
       via  f4a7c5e6b1e6e30ad8a31a704642fcc360239e03 (commit)
       via  f63c160fd1cec8dddc673900f5cda824bed30d88 (commit)
       via  605d11f27e73ce60794fa9858aeed8ed961261e8 (commit)
       via  fa8c5cdb36fa89af92087d6465b207d65ffc9618 (commit)
       via  82697fea93b1c87cdee27d2b9a76c1b7ac07497e (commit)
       via  a15fc58f280171660afd2b9f1938f0a6bc5ede4a (commit)
       via  c7143dc92806d7b0c6f434e91744a04e5283284d (commit)
       via  005595fb1a7da851a863118fba7e3d00026fb3a3 (commit)
       via  273a233818ae39e843fab0276f9e381da6645d28 (commit)
       via  533a50bbcee4ece451a65e87066563a5b33ef150 (commit)
       via  c97408a51dab3e968c76cf8c1d873fbe0f99a0dd (commit)
       via  4812b5639cfaf724540786fcd331aaa227635c77 (commit)
       via  101e3227b25c16874fa73660bfd7e338fbfe0da2 (commit)
       via  9dae71315a83504493952f9039de958594e45f31 (commit)
       via  a1b0149879593df786807afbefb26d4687ebe161 (commit)
       via  b6423b5e5261f1ddaa88db2548e2190f82de21e8 (commit)
       via  cb0b666f86ce87d35667e8e23e3775b703a37841 (commit)
       via  a4a3227c634d8cfd54a23205be727e910032cbe9 (commit)
       via  47c56297f949a1bebaa758cb8ce1684ffed22744 (commit)
       via  2e5c44a64ea51f7ae61e286f2c88b057af1bc22f (commit)
       via  660a6143ecf1e777f33bd84183ba9e821e1d7a8e (commit)
       via  f7d32ebcf2e0641cf92d079387dc325d65e35879 (commit)
       via  07e4083ea451913b988d77e8e4c926da8ad844a4 (commit)
       via  dd08615e98e656ae07c6ca26eaa635f6ce8f01b7 (commit)
       via  594e00f9311da95f73843f55b6e1c7c3ad55d8df (commit)
       via  6fe6fb4a307dade78b4f64a3f0f06a9caa8a056d (commit)
       via  c7a44e530c2ecb036ac46d71a0306c6f64c439ee (commit)
       via  2ab1b540a710fd454f33e9ba22db601e67f3d217 (commit)
       via  cad4856b84ff09f8d66faf16cc02ce68ef87995f (commit)
       via  345dfd5a605c9171182cd8acb2918e899f64160b (commit)
       via  766ddd6d958826049c2811f6d058480246e423a6 (commit)
       via  d7c84d69bb62d61bc671b2d5e0ad4ed42dbeb7c0 (commit)
       via  2db0c3a44d8f12e3a566c796bdecaa1de150b155 (commit)
       via  a58e9ca25aaa0963545f256985eb75b2f840e80f (commit)
       via  f36162457a771824059fefa098a3ffb89c59263f (commit)
       via  b3fa9983ac0b7b38a5b3787af56a7bb1502ae3be (commit)
       via  a98594f82f1786b09d9d1e0f57a651d4eae3474a (commit)
       via  aea8596a74dabdc894fc1139c4d8dc195e6c86b2 (commit)
       via  ca812f58e63bd4673bb62aa8528e07d6020bfc9a (commit)
       via  c106221bf8c90fd80ca2c19a578fd7faec985229 (commit)
       via  2ac112e2364b009166e0f67a3455fffb3cccc16c (commit)

This update added new revisions after undoing existing revisions.  That is
to say, the old revision is not a strict subset of the new revision.  This
situation occurs when you --force push a change and generate a repository
containing something like this:

 * -- * -- B -- O -- O -- O (e96966556520e3d7cd27ad253418656892355c0d)
            \
             N -- N -- N (059a62b51d90f82e4286835d26d8bf523a54132b)

When this happens we assume that you've already had alert emails for all
of the O revisions, and so we here report only the revisions in the N
branch from the common base, B.

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 059a62b51d90f82e4286835d26d8bf523a54132b
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Apr 28 17:04:20 2017 -0400

    11183: In docker-compose test, wait for checks only on head node.

diff --git a/server/arvados-server/docker_compose_test.go b/server/arvados-server/docker_compose_test.go
index f80577d..089db46 100644
--- a/server/arvados-server/docker_compose_test.go
+++ b/server/arvados-server/docker_compose_test.go
@@ -1,26 +1,43 @@
 package main
 
 import (
+	"log"
 	"os"
 	"os/exec"
 	"testing"
 )
 
 func TestDockerCompose(t *testing.T) {
+	run := func(cmdline []string) error {
+		log.Printf("TestDockerCompose: %q", cmdline)
+		cmd := exec.Command(cmdline[0], cmdline[1:]...)
+		cmd.Stdout = os.Stderr
+		cmd.Stderr = os.Stderr
+		return cmd.Run()
+	}
 	for _, cmdline := range [][]string{
 		{"go", "build"},
 		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "-p", "arvados_setup_test", "down", "-v"},
-		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "-p", "arvados_setup_test", "up"},
+		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "-p", "arvados_setup_test", "up", "-d"},
+		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "-p", "arvados_setup_test", "logs", "--timestamps", "--follow"},
 		{"docker", "wait", "arvadossetuptest_sys0_1"},
-		{"docker", "wait", "arvadossetuptest_sys1_1"},
-		{"docker", "wait", "arvadossetuptest_sys2_1"},
 	} {
-		cmd := exec.Command(cmdline[0], cmdline[1:]...)
-		cmd.Stdout = os.Stderr
-		cmd.Stderr = os.Stderr
-		err := cmd.Run()
+		if cmdline[len(cmdline)-1] == "--follow" {
+			go run(cmdline)
+			continue
+		}
+		err := run(cmdline)
 		if err != nil {
 			t.Fatal(err)
 		}
 	}
+	defer func() {
+		for _, cmdline := range [][]string{
+			{"docker", "stop", "arvadossetuptest_sys0_1"},
+			{"docker", "stop", "arvadossetuptest_sys1_1"},
+			{"docker", "stop", "arvadossetuptest_sys2_1"},
+		} {
+			run(cmdline)
+		}
+	}()
 }
diff --git a/server/arvados-server/test-docker-compose/docker-compose.yml b/server/arvados-server/test-docker-compose/docker-compose.yml
index 73ba0f3..d18a954 100644
--- a/server/arvados-server/test-docker-compose/docker-compose.yml
+++ b/server/arvados-server/test-docker-compose/docker-compose.yml
@@ -12,6 +12,8 @@ services:
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
     command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup -init-vault=true -run-api -wait"]
+    ports:
+      - "18500"
   sys1:
     build: ../test-setup
     cap_add:
@@ -23,7 +25,9 @@ services:
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup -wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup && wait"]
+    ports:
+      - "18500"
   sys2:
     build: ../test-setup
     cap_add:
@@ -35,6 +39,8 @@ services:
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup -wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup && wait"]
+    ports:
+      - "18500"
 volumes:
   vault:
diff --git a/server/setup/setup.go b/server/setup/setup.go
index 8886946..a476268 100644
--- a/server/setup/setup.go
+++ b/server/setup/setup.go
@@ -68,10 +68,17 @@ func (s *Setup) Run() error {
 			return err
 		}
 	}
+	if s.Wait {
+		return s.wait()
+	} else {
+		return nil
+	}
+}
 
+func (s *Setup) wait() error {
 	checkStatus := map[string]string{}
-	wait := 2 * time.Second
-	for ok := false; s.Wait && !ok; time.Sleep(wait) {
+	sleep := 2 * time.Second
+	for {
 		cc, err := s.ConsulMaster()
 		if err != nil {
 			log.Printf("setup: consulMaster(): %s", err)
@@ -83,14 +90,14 @@ func (s *Setup) Run() error {
 			log.Printf("setup: consul.Catalog().Service(): %s", err)
 			continue
 		} else if len(apiSvcs) == 0 {
-			if wait <= 2*time.Second {
-				wait = wait * 2
+			if sleep <= 2*time.Second {
+				sleep = sleep * 2
 				log.Printf("setup: waiting for arvados-api service to appear")
 			}
 			continue
 		}
 
-		ok = true
+		ok := true
 		svcs, _, err := cc.Catalog().Services(nil)
 		if err != nil {
 			log.Printf("setup: consul.Catalog().Services(): %s", err)
@@ -115,16 +122,10 @@ func (s *Setup) Run() error {
 		}
 		if ok {
 			log.Printf("All services are passing: %+v", svcs)
-			// Wait to ensure any other "setup -wait"
-			// processes have a chance to see the
-			// all-passing state before we return (if this
-			// is a test or image-building scenario, the
-			// whole system might shut down and stop
-			// passing as soon as we return).
-			time.Sleep(2 * wait)
+			return nil
 		}
+		time.Sleep(sleep)
 	}
-	return nil
 }
 
 func (s *Setup) makeDirs() error {

commit 87f40ab7ba0a3f0fc1f3648c917ff929e6e633b8
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Apr 28 15:52:07 2017 -0400

    11183: Fix "just setup" test.

diff --git a/server/arvados-server/setup_docker_compose_test.go b/server/arvados-server/docker_compose_test.go
similarity index 93%
rename from server/arvados-server/setup_docker_compose_test.go
rename to server/arvados-server/docker_compose_test.go
index a8f405d..f80577d 100644
--- a/server/arvados-server/setup_docker_compose_test.go
+++ b/server/arvados-server/docker_compose_test.go
@@ -6,7 +6,7 @@ import (
 	"testing"
 )
 
-func TestSetupDockerCompose(t *testing.T) {
+func TestDockerCompose(t *testing.T) {
 	for _, cmdline := range [][]string{
 		{"go", "build"},
 		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "-p", "arvados_setup_test", "down", "-v"},
diff --git a/server/arvados-server/main.go b/server/arvados-server/main.go
index 4159438..cfa0b17 100644
--- a/server/arvados-server/main.go
+++ b/server/arvados-server/main.go
@@ -13,6 +13,7 @@ import (
 
 var cmds = map[string]cmd.Command{
 	"agent":     agent.Command(),
+	"init":      setup.Command(),
 	"setup":     setup.Command(),
 	"configure": configure.Command(),
 }
diff --git a/server/arvados-server/setup_debian8_test.go b/server/arvados-server/setup_test.go
similarity index 86%
rename from server/arvados-server/setup_debian8_test.go
rename to server/arvados-server/setup_test.go
index 7bcd48e..cd93837 100644
--- a/server/arvados-server/setup_debian8_test.go
+++ b/server/arvados-server/setup_test.go
@@ -8,7 +8,7 @@ import (
 	"testing"
 )
 
-func TestSetupDebian8(t *testing.T) {
+func TestSetup(t *testing.T) {
 	cwd, err := os.Getwd()
 	if err != nil {
 		t.Fatal(err)
@@ -28,8 +28,8 @@ func TestSetupDebian8(t *testing.T) {
 	log.Printf("Publishing consul webgui at %v", ln.Addr())
 	for _, cmdline := range [][]string{
 		{"go", "build"},
-		{"docker", "build", "--tag=arvados-server-debian8-test", "test-debian8"},
-		{"docker", "run", "--rm", "--publish=" + port + ":18500", "--cap-add=IPC_LOCK", "--cap-add=SYS_ADMIN", "--volume=/sys/fs/cgroup", "--volume=" + cwd + "/arvados-server:/usr/bin/arvados-server:ro", "--volume=/var/cache/arvados:/var/cache/arvados:ro", "arvados-server-debian8-test"},
+		{"docker", "build", "--tag=arvados-setup-test", "test-setup"},
+		{"docker", "run", "--rm", "--publish=" + port + ":18500", "--cap-add=IPC_LOCK", "--cap-add=SYS_ADMIN", "--volume=/sys/fs/cgroup", "--volume=" + cwd + "/arvados-server:/usr/bin/arvados-server:ro", "--volume=/var/cache/arvados:/var/cache/arvados:ro", "arvados-setup-test"},
 	} {
 		cmd := exec.Command(cmdline[0], cmdline[1:]...)
 		cmd.Stdout = os.Stderr
diff --git a/server/arvados-server/test-docker-compose/docker-compose.yml b/server/arvados-server/test-docker-compose/docker-compose.yml
index 8c362e9..73ba0f3 100644
--- a/server/arvados-server/test-docker-compose/docker-compose.yml
+++ b/server/arvados-server/test-docker-compose/docker-compose.yml
@@ -1,7 +1,7 @@
 version: '2'
 services:
   sys0:
-    build: ../test-debian8
+    build: ../test-setup
     cap_add:
       - IPC_LOCK
       - SYS_ADMIN
@@ -13,7 +13,7 @@ services:
       - vault:/var/lib/arvados/vault
     command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup -init-vault=true -run-api -wait"]
   sys1:
-    build: ../test-debian8
+    build: ../test-setup
     cap_add:
       - IPC_LOCK
       - SYS_ADMIN
@@ -25,7 +25,7 @@ services:
       - vault:/var/lib/arvados/vault
     command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup -wait"]
   sys2:
-    build: ../test-debian8
+    build: ../test-setup
     cap_add:
       - IPC_LOCK
       - SYS_ADMIN
diff --git a/server/arvados-server/test-debian8/Dockerfile b/server/arvados-server/test-setup/Dockerfile
similarity index 91%
rename from server/arvados-server/test-debian8/Dockerfile
rename to server/arvados-server/test-setup/Dockerfile
index 09261e5..04c4640 100644
--- a/server/arvados-server/test-debian8/Dockerfile
+++ b/server/arvados-server/test-setup/Dockerfile
@@ -11,4 +11,4 @@ RUN ["bash", "-c", "echo en_US.utf8 UTF-8 | tee -a /etc/locale.gen && locale-gen
 
 RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends ca-certificates locales nginx postgresql runit
 
-CMD ["bash", "-c", "runsvdir /etc/sv & service postgresql start & arvados-server setup -init-vault && arvados-server setup -wait"]
+CMD ["bash", "-c", "runsvdir /etc/sv & service postgresql start & arvados-server init -init-vault"]

commit 1522e9dc7cd4ccf9a636a8d53091f05560accfb2
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Apr 28 13:39:42 2017 -0400

    11183: Rearrange source tree

diff --git a/build/run-build-packages-one-target.sh b/build/run-build-packages-one-target.sh
index 78adfbb..b4fca9c 100755
--- a/build/run-build-packages-one-target.sh
+++ b/build/run-build-packages-one-target.sh
@@ -129,7 +129,7 @@ popd
 
 if test -z "$packages" ; then
     packages="arvados-api-server
-        arvados-admin
+        arvados-server
         arvados-docker-cleaner
         arvados-git-httpd
         arvados-node-manager
diff --git a/build/run-build-packages.sh b/build/run-build-packages.sh
index 5c6875d..06d36e3 100755
--- a/build/run-build-packages.sh
+++ b/build/run-build-packages.sh
@@ -338,7 +338,7 @@ package_go_binary sdk/go/crunchrunner crunchrunner \
     "Crunchrunner executes a command inside a container and uploads the output"
 package_go_binary services/arv-git-httpd arvados-git-httpd \
     "Provide authenticated http access to Arvados-hosted git repositories"
-package_go_binary cmd/arvados-admin arvados-admin \
+package_go_binary server/arvados-server arvados-server \
     "Arvados cluster administration tool"
 package_go_binary services/crunch-dispatch-local crunch-dispatch-local \
     "Dispatch Crunch containers on the local system"
diff --git a/build/run-tests.sh b/build/run-tests.sh
index e2e6f34..fd52e95 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -101,10 +101,10 @@ tools/crunchstat-summary
 tools/keep-exercise
 tools/keep-rsync
 tools/keep-block-check
-lib/agent
 lib/crunchstat
-lib/setup
-cmd/arvados-admin
+server/agent
+server/setup
+server/arvados-server
 
 (*) apps/workbench is shorthand for apps/workbench_units +
     apps/workbench_functionals + apps/workbench_integration
@@ -796,9 +796,7 @@ gostuff=(
     sdk/go/streamer
     sdk/go/crunchrunner
     sdk/go/stats
-    lib/agent
     lib/crunchstat
-    lib/setup
     services/arv-git-httpd
     services/crunchstat
     services/keep-web
@@ -814,7 +812,9 @@ gostuff=(
     tools/keep-block-check
     tools/keep-exercise
     tools/keep-rsync
-    cmd/arvados-admin
+    server/agent
+    server/setup
+    server/arvados-server
     )
 for g in "${gostuff[@]}"
 do
diff --git a/cmd/arvados-admin/.gitignore b/cmd/arvados-admin/.gitignore
deleted file mode 100644
index 87fbbf4..0000000
--- a/cmd/arvados-admin/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-arvados-admin
diff --git a/cmd/dispatch.go b/lib/cmd/dispatch.go
similarity index 100%
rename from cmd/dispatch.go
rename to lib/cmd/dispatch.go
diff --git a/lib/configure/configure.go b/lib/configure/configure.go
index 2f78534..88c5399 100644
--- a/lib/configure/configure.go
+++ b/lib/configure/configure.go
@@ -4,8 +4,8 @@ import (
 	"flag"
 	"os"
 
-	"git.curoverse.com/arvados.git/lib/setup"
 	"git.curoverse.com/arvados.git/sdk/go/config"
+	"git.curoverse.com/arvados.git/server/setup"
 )
 
 func Command() *Configure {
diff --git a/lib/agent/agent.go b/server/agent/agent.go
similarity index 100%
rename from lib/agent/agent.go
rename to server/agent/agent.go
diff --git a/server/arvados-server/.gitignore b/server/arvados-server/.gitignore
new file mode 100644
index 0000000..b6bb7f0
--- /dev/null
+++ b/server/arvados-server/.gitignore
@@ -0,0 +1 @@
+arvados-server
diff --git a/cmd/arvados-admin/main.go b/server/arvados-server/main.go
similarity index 75%
rename from cmd/arvados-admin/main.go
rename to server/arvados-server/main.go
index d4b25a6..4159438 100644
--- a/cmd/arvados-admin/main.go
+++ b/server/arvados-server/main.go
@@ -5,10 +5,10 @@ import (
 	"fmt"
 	"os"
 
-	"git.curoverse.com/arvados.git/cmd"
-	"git.curoverse.com/arvados.git/lib/agent"
+	"git.curoverse.com/arvados.git/lib/cmd"
 	"git.curoverse.com/arvados.git/lib/configure"
-	"git.curoverse.com/arvados.git/lib/setup"
+	"git.curoverse.com/arvados.git/server/agent"
+	"git.curoverse.com/arvados.git/server/setup"
 )
 
 var cmds = map[string]cmd.Command{
diff --git a/cmd/arvados-admin/setup_debian8_test.go b/server/arvados-server/setup_debian8_test.go
similarity index 77%
rename from cmd/arvados-admin/setup_debian8_test.go
rename to server/arvados-server/setup_debian8_test.go
index 671f8c7..7bcd48e 100644
--- a/cmd/arvados-admin/setup_debian8_test.go
+++ b/server/arvados-server/setup_debian8_test.go
@@ -28,8 +28,8 @@ func TestSetupDebian8(t *testing.T) {
 	log.Printf("Publishing consul webgui at %v", ln.Addr())
 	for _, cmdline := range [][]string{
 		{"go", "build"},
-		{"docker", "build", "--tag=arvados-admin-debian8-test", "test-debian8"},
-		{"docker", "run", "--rm", "--publish=" + port + ":18500", "--cap-add=IPC_LOCK", "--cap-add=SYS_ADMIN", "--volume=/sys/fs/cgroup", "--volume=" + cwd + "/arvados-admin:/usr/bin/arvados-admin:ro", "--volume=/var/cache/arvados:/var/cache/arvados:ro", "arvados-admin-debian8-test"},
+		{"docker", "build", "--tag=arvados-server-debian8-test", "test-debian8"},
+		{"docker", "run", "--rm", "--publish=" + port + ":18500", "--cap-add=IPC_LOCK", "--cap-add=SYS_ADMIN", "--volume=/sys/fs/cgroup", "--volume=" + cwd + "/arvados-server:/usr/bin/arvados-server:ro", "--volume=/var/cache/arvados:/var/cache/arvados:ro", "arvados-server-debian8-test"},
 	} {
 		cmd := exec.Command(cmdline[0], cmdline[1:]...)
 		cmd.Stdout = os.Stderr
diff --git a/cmd/arvados-admin/setup_docker_compose_test.go b/server/arvados-server/setup_docker_compose_test.go
similarity index 100%
rename from cmd/arvados-admin/setup_docker_compose_test.go
rename to server/arvados-server/setup_docker_compose_test.go
diff --git a/cmd/arvados-admin/test-debian8/Dockerfile b/server/arvados-server/test-debian8/Dockerfile
similarity index 91%
rename from cmd/arvados-admin/test-debian8/Dockerfile
rename to server/arvados-server/test-debian8/Dockerfile
index 7db1224..09261e5 100644
--- a/cmd/arvados-admin/test-debian8/Dockerfile
+++ b/server/arvados-server/test-debian8/Dockerfile
@@ -11,4 +11,4 @@ RUN ["bash", "-c", "echo en_US.utf8 UTF-8 | tee -a /etc/locale.gen && locale-gen
 
 RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends ca-certificates locales nginx postgresql runit
 
-CMD ["bash", "-c", "runsvdir /etc/sv & service postgresql start & arvados-admin setup -init-vault && arvados-admin setup -wait"]
+CMD ["bash", "-c", "runsvdir /etc/sv & service postgresql start & arvados-server setup -init-vault && arvados-server setup -wait"]
diff --git a/cmd/arvados-admin/test-docker-compose/agent.yml b/server/arvados-server/test-docker-compose/agent.yml
similarity index 100%
rename from cmd/arvados-admin/test-docker-compose/agent.yml
rename to server/arvados-server/test-docker-compose/agent.yml
diff --git a/cmd/arvados-admin/test-docker-compose/docker-compose.yml b/server/arvados-server/test-docker-compose/docker-compose.yml
similarity index 70%
rename from cmd/arvados-admin/test-docker-compose/docker-compose.yml
rename to server/arvados-server/test-docker-compose/docker-compose.yml
index 85390ce..8c362e9 100644
--- a/cmd/arvados-admin/test-docker-compose/docker-compose.yml
+++ b/server/arvados-server/test-docker-compose/docker-compose.yml
@@ -6,35 +6,35 @@ services:
       - IPC_LOCK
       - SYS_ADMIN
     volumes:
-      - ../arvados-admin:/usr/bin/arvados-admin:ro
+      - ../arvados-server:/usr/bin/arvados-server:ro
       - ./agent.yml:/etc/arvados/agent/agent.yml:ro
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -init-vault=true -run-api -wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup -init-vault=true -run-api -wait"]
   sys1:
     build: ../test-debian8
     cap_add:
       - IPC_LOCK
       - SYS_ADMIN
     volumes:
-      - ../arvados-admin:/usr/bin/arvados-admin:ro
+      - ../arvados-server:/usr/bin/arvados-server:ro
       - ./agent.yml:/etc/arvados/agent/agent.yml:ro
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup -wait"]
   sys2:
     build: ../test-debian8
     cap_add:
       - IPC_LOCK
       - SYS_ADMIN
     volumes:
-      - ../arvados-admin:/usr/bin/arvados-admin:ro
+      - ../arvados-server:/usr/bin/arvados-server:ro
       - ./agent.yml:/etc/arvados/agent/agent.yml:ro
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-server setup -wait"]
 volumes:
   vault:
diff --git a/cmd/arvados-admin/test-docker-compose/encrypt-key.txt b/server/arvados-server/test-docker-compose/encrypt-key.txt
similarity index 100%
rename from cmd/arvados-admin/test-docker-compose/encrypt-key.txt
rename to server/arvados-server/test-docker-compose/encrypt-key.txt
diff --git a/cmd/arvados-admin/test-docker-compose/master-token.txt b/server/arvados-server/test-docker-compose/master-token.txt
similarity index 100%
rename from cmd/arvados-admin/test-docker-compose/master-token.txt
rename to server/arvados-server/test-docker-compose/master-token.txt
diff --git a/lib/setup/arvados_services.go b/server/setup/arvados_services.go
similarity index 98%
rename from lib/setup/arvados_services.go
rename to server/setup/arvados_services.go
index ca3b68f..39b401a 100644
--- a/lib/setup/arvados_services.go
+++ b/server/setup/arvados_services.go
@@ -221,7 +221,7 @@ func (s *Setup) installArvadosServices() error {
 		})
 	}
 	// f = append(f, func() error {
-	// 	return s.consulTemplateExec("arvados-agent", "./agent.json", tmplArvadosAgent, 0640, "arvados-admin", "agent", "-config", "./agent.json")
+	// 	return s.consulTemplateExec("arvados-agent", "./agent.json", tmplArvadosAgent, 0640, "arvados-server", "agent", "-config", "./agent.json")
 	// })
 	for _, f := range todo {
 		err := f()
diff --git a/lib/setup/cert.go b/server/setup/cert.go
similarity index 100%
rename from lib/setup/cert.go
rename to server/setup/cert.go
diff --git a/lib/setup/check.go b/server/setup/check.go
similarity index 100%
rename from lib/setup/check.go
rename to server/setup/check.go
diff --git a/lib/setup/command.go b/server/setup/command.go
similarity index 100%
rename from lib/setup/command.go
rename to server/setup/command.go
diff --git a/lib/setup/configure.go b/server/setup/configure.go
similarity index 100%
rename from lib/setup/configure.go
rename to server/setup/configure.go
diff --git a/lib/setup/consul.go b/server/setup/consul.go
similarity index 100%
rename from lib/setup/consul.go
rename to server/setup/consul.go
diff --git a/lib/setup/curoverse_package.go b/server/setup/curoverse_package.go
similarity index 100%
rename from lib/setup/curoverse_package.go
rename to server/setup/curoverse_package.go
diff --git a/lib/setup/daemon.go b/server/setup/daemon.go
similarity index 100%
rename from lib/setup/daemon.go
rename to server/setup/daemon.go
diff --git a/lib/setup/download.go b/server/setup/download.go
similarity index 100%
rename from lib/setup/download.go
rename to server/setup/download.go
diff --git a/lib/setup/os_package.go b/server/setup/os_package.go
similarity index 100%
rename from lib/setup/os_package.go
rename to server/setup/os_package.go
diff --git a/lib/setup/runit.go b/server/setup/runit.go
similarity index 100%
rename from lib/setup/runit.go
rename to server/setup/runit.go
diff --git a/lib/setup/setup.go b/server/setup/setup.go
similarity index 98%
rename from lib/setup/setup.go
rename to server/setup/setup.go
index fdc7fb7..8886946 100644
--- a/lib/setup/setup.go
+++ b/server/setup/setup.go
@@ -7,8 +7,8 @@ import (
 	"os"
 	"time"
 
-	"git.curoverse.com/arvados.git/lib/agent"
 	"git.curoverse.com/arvados.git/sdk/go/config"
+	"git.curoverse.com/arvados.git/server/agent"
 )
 
 func Command() *Setup {
diff --git a/lib/setup/systemd.go b/server/setup/systemd.go
similarity index 100%
rename from lib/setup/systemd.go
rename to server/setup/systemd.go
diff --git a/lib/setup/tmpl_nginx_api.go b/server/setup/tmpl_nginx_api.go
similarity index 100%
rename from lib/setup/tmpl_nginx_api.go
rename to server/setup/tmpl_nginx_api.go
diff --git a/lib/setup/vault.go b/server/setup/vault.go
similarity index 100%
rename from lib/setup/vault.go
rename to server/setup/vault.go
diff --git a/lib/setup/write_file.go b/server/setup/write_file.go
similarity index 100%
rename from lib/setup/write_file.go
rename to server/setup/write_file.go

commit ff310f43338a2443f9b11b7a55e619dc1961d234
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Apr 28 13:11:42 2017 -0400

    11183: Fix setup-already-done check.

diff --git a/lib/setup/configure.go b/lib/setup/configure.go
index 9c22b3e..7f63aa8 100644
--- a/lib/setup/configure.go
+++ b/lib/setup/configure.go
@@ -16,8 +16,10 @@ func (s *Setup) maybeConfigure() error {
 	}
 	kv := cc.KV()
 
-	_, _, err = kv.Get("arvados/service/API/port", nil)
-	if err == nil {
+	cur, _, err := kv.Get("arvados/service/API/port", nil)
+	if err != nil {
+		return err
+	} else if cur != nil {
 		// already configured
 		return nil
 	}
@@ -48,8 +50,8 @@ func (s *Setup) Reconfigure() error {
 
 		cur, _, err := kv.Get(pair.Key, nil)
 		if err != nil {
-			log.Print(err)
-		} else if bytes.Compare(cur.Value, pair.Value) == 0 {
+			return err
+		} else if cur != nil && bytes.Compare(cur.Value, pair.Value) == 0 {
 			continue
 		}
 

commit 55466cec28927431c15e61078b9e56225ce77c72
Author: Tom Clegg <tom at curoverse.com>
Date:   Sun Apr 9 03:57:43 2017 -0400

    11183: Avoid reusing httpclient via vault config to prevent "protocol https already registered" error from ConfigureTransport.

diff --git a/lib/setup/setup.go b/lib/setup/setup.go
index f578d20..fdc7fb7 100644
--- a/lib/setup/setup.go
+++ b/lib/setup/setup.go
@@ -9,7 +9,6 @@ import (
 
 	"git.curoverse.com/arvados.git/lib/agent"
 	"git.curoverse.com/arvados.git/sdk/go/config"
-	vaultAPI "github.com/hashicorp/vault/api"
 )
 
 func Command() *Setup {
@@ -35,7 +34,6 @@ type Setup struct {
 
 	encryptKey  string
 	masterToken string
-	vaultCfg    *vaultAPI.Config
 }
 
 func (s *Setup) ParseFlags(args []string) error {
diff --git a/lib/setup/vault.go b/lib/setup/vault.go
index 2a25578..4b134fb 100644
--- a/lib/setup/vault.go
+++ b/lib/setup/vault.go
@@ -19,9 +19,6 @@ func (s *Setup) installVault() error {
 	if err := s.consulInit(); err != nil {
 		return err
 	}
-	if err := s.vaultInit(); err != nil {
-		return err
-	}
 	if s.vaultCheck() == nil {
 		return nil
 	}
@@ -215,14 +212,10 @@ func (s *Setup) vaultBootstrap() error {
 	return nil
 }
 
-func (s *Setup) vaultInit() error {
-	s.vaultCfg = vaultAPI.DefaultConfig()
-	s.vaultCfg.Address = fmt.Sprintf("http://%s:%d", s.LANHost, s.Ports.VaultServer)
-	return nil
-}
-
 func (s *Setup) vaultClient() (*vaultAPI.Client, error) {
-	return vaultAPI.NewClient(s.vaultCfg)
+	cfg := vaultAPI.DefaultConfig()
+	cfg.Address = fmt.Sprintf("http://%s:%d", s.LANHost, s.Ports.VaultServer)
+	return vaultAPI.NewClient(cfg)
 }
 
 func (s *Setup) vaultCheck() error {

commit 957ec5ff80456706aa3bd709669e4a17251f6791
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu Mar 23 15:54:49 2017 -0400

    11183: Less noise while waiting for health checks.

diff --git a/lib/setup/setup.go b/lib/setup/setup.go
index a4c8c43..f578d20 100644
--- a/lib/setup/setup.go
+++ b/lib/setup/setup.go
@@ -71,6 +71,7 @@ func (s *Setup) Run() error {
 		}
 	}
 
+	checkStatus := map[string]string{}
 	wait := 2 * time.Second
 	for ok := false; s.Wait && !ok; time.Sleep(wait) {
 		cc, err := s.ConsulMaster()
@@ -90,7 +91,6 @@ func (s *Setup) Run() error {
 			}
 			continue
 		}
-		log.Printf("arvados-api service: %#v", apiSvcs)
 
 		ok = true
 		svcs, _, err := cc.Catalog().Services(nil)
@@ -106,10 +106,13 @@ func (s *Setup) Run() error {
 			}
 
 			for _, check := range checks {
+				if checkStatus[check.CheckID] != check.Status {
+					log.Printf("setup: node %q service %q check %q state %q", check.Node, check.ServiceName, check.CheckID, check.Status)
+				}
 				if check.Status != "passing" {
-					log.Printf("waiting for node %q service %q check %q state %q", check.Node, check.ServiceName, check.CheckID, check.Status)
 					ok = false
 				}
+				checkStatus[check.CheckID] = check.Status
 			}
 		}
 		if ok {

commit fb08faf4b134a09eee928b596382cf17c75a8c89
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Mar 15 02:22:03 2017 -0400

    11183: More consul setup.

diff --git a/cmd/arvados-admin/main.go b/cmd/arvados-admin/main.go
index de532cb..d4b25a6 100644
--- a/cmd/arvados-admin/main.go
+++ b/cmd/arvados-admin/main.go
@@ -7,12 +7,14 @@ import (
 
 	"git.curoverse.com/arvados.git/cmd"
 	"git.curoverse.com/arvados.git/lib/agent"
+	"git.curoverse.com/arvados.git/lib/configure"
 	"git.curoverse.com/arvados.git/lib/setup"
 )
 
 var cmds = map[string]cmd.Command{
-	"agent": agent.Command(),
-	"setup": setup.Command(),
+	"agent":     agent.Command(),
+	"setup":     setup.Command(),
+	"configure": configure.Command(),
 }
 
 func main() {
diff --git a/cmd/arvados-admin/setup_docker_compose_test.go b/cmd/arvados-admin/setup_docker_compose_test.go
index e88dc16..a8f405d 100644
--- a/cmd/arvados-admin/setup_docker_compose_test.go
+++ b/cmd/arvados-admin/setup_docker_compose_test.go
@@ -9,8 +9,11 @@ import (
 func TestSetupDockerCompose(t *testing.T) {
 	for _, cmdline := range [][]string{
 		{"go", "build"},
-		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "down", "-v"},
-		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "up"},
+		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "-p", "arvados_setup_test", "down", "-v"},
+		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "-p", "arvados_setup_test", "up"},
+		{"docker", "wait", "arvadossetuptest_sys0_1"},
+		{"docker", "wait", "arvadossetuptest_sys1_1"},
+		{"docker", "wait", "arvadossetuptest_sys2_1"},
 	} {
 		cmd := exec.Command(cmdline[0], cmdline[1:]...)
 		cmd.Stdout = os.Stderr
diff --git a/lib/configure/configure.go b/lib/configure/configure.go
new file mode 100644
index 0000000..2f78534
--- /dev/null
+++ b/lib/configure/configure.go
@@ -0,0 +1,32 @@
+package configure
+
+import (
+	"flag"
+	"os"
+
+	"git.curoverse.com/arvados.git/lib/setup"
+	"git.curoverse.com/arvados.git/sdk/go/config"
+)
+
+func Command() *Configure {
+	return &Configure{
+		Setup: setup.Command(),
+	}
+}
+
+type Configure struct {
+	*setup.Setup
+}
+
+func (c *Configure) ParseFlags(args []string) error {
+	fs := flag.NewFlagSet("configure", flag.ContinueOnError)
+	return fs.Parse(args)
+}
+
+func (c *Configure) Run() error {
+	err := config.LoadFile(c, c.DefaultConfigFile())
+	if err != nil && !os.IsNotExist(err) {
+		return err
+	}
+	return c.Setup.Reconfigure()
+}
diff --git a/lib/setup/arvados_services.go b/lib/setup/arvados_services.go
index f15e693..ca3b68f 100644
--- a/lib/setup/arvados_services.go
+++ b/lib/setup/arvados_services.go
@@ -195,17 +195,17 @@ func (s *Setup) installArvadosServices() error {
 			func() error {
 				return os.Symlink("../sites-available/arvados-api.conf", "/etc/nginx/sites-enabled/arvados-api.conf")
 			})
-		c, err := s.consulMaster()
+		c, err := s.ConsulMaster()
 		if err != nil {
 			return err
 		}
 		err = c.Agent().ServiceRegister(&consul.AgentServiceRegistration{
 			ID:   "arvados-api:" + s.LANHost,
-			Name: "api",
+			Name: "arvados-api",
 			Port: s.Agent.Ports.API,
 			Checks: consul.AgentServiceChecks{
 				{
-					HTTP:     fmt.Sprintf("http://%s:%d/discovery/v1/apis/arvados/v1/rest", s.LANHost, s.Agent.Ports.API),
+					HTTP:     fmt.Sprintf("http://127.0.0.1:%d/discovery/v1/apis/arvados/v1/rest", s.Agent.Ports.API),
 					Interval: "15s",
 				},
 			},
diff --git a/lib/setup/configure.go b/lib/setup/configure.go
new file mode 100644
index 0000000..9c22b3e
--- /dev/null
+++ b/lib/setup/configure.go
@@ -0,0 +1,63 @@
+package setup
+
+import (
+	"bytes"
+	"encoding/json"
+	"log"
+	"strconv"
+
+	consul "github.com/hashicorp/consul/api"
+)
+
+func (s *Setup) maybeConfigure() error {
+	cc, err := s.ConsulMaster()
+	if err != nil {
+		return err
+	}
+	kv := cc.KV()
+
+	_, _, err = kv.Get("arvados/service/API/port", nil)
+	if err == nil {
+		// already configured
+		return nil
+	}
+	return s.Reconfigure()
+}
+
+func (s *Setup) Reconfigure() error {
+	cc, err := s.ConsulMaster()
+	if err != nil {
+		return err
+	}
+	kv := cc.KV()
+
+	var portmap map[string]int
+	buf, err := json.Marshal(s.Ports)
+	if err != nil {
+		return err
+	}
+	err = json.Unmarshal(buf, &portmap)
+	if err != nil {
+		return err
+	}
+	for name, port := range portmap {
+		pair := &consul.KVPair{
+			Key:   "arvados/service/" + name + "/port",
+			Value: []byte(strconv.Itoa(port)),
+		}
+
+		cur, _, err := kv.Get(pair.Key, nil)
+		if err != nil {
+			log.Print(err)
+		} else if bytes.Compare(cur.Value, pair.Value) == 0 {
+			continue
+		}
+
+		log.Printf("%q => %q", pair.Key, pair.Value)
+		_, err = kv.Put(pair, nil)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/lib/setup/consul.go b/lib/setup/consul.go
index e1a70d0..0af119b 100644
--- a/lib/setup/consul.go
+++ b/lib/setup/consul.go
@@ -143,15 +143,18 @@ func (s *Setup) installConsul() error {
 
 var consulCfg = api.DefaultConfig()
 
-func (s *Setup) consulMaster() (*api.Client, error) {
-	masterToken, err := ioutil.ReadFile(path.Join(s.DataDir, "master-token.txt"))
-	if err != nil {
-		return nil, err
+func (s *Setup) ConsulMaster() (*api.Client, error) {
+	if s.masterToken == "" {
+		t, err := ioutil.ReadFile(path.Join(s.DataDir, "master-token.txt"))
+		if err != nil {
+			return nil, err
+		}
+		s.masterToken = string(t)
 	}
 	ccfg := api.DefaultConfig()
 	ccfg.Address = fmt.Sprintf("127.0.0.1:%d", s.Ports.ConsulHTTP)
 	ccfg.Datacenter = s.ClusterID
-	ccfg.Token = string(masterToken)
+	ccfg.Token = s.masterToken
 	return api.NewClient(ccfg)
 }
 
@@ -185,7 +188,7 @@ func (s *Setup) consulInit() error {
 }
 
 func (s *Setup) consulCheck() error {
-	consul, err := s.consulMaster()
+	consul, err := s.ConsulMaster()
 	if err != nil {
 		return err
 	}
@@ -195,7 +198,7 @@ func (s *Setup) consulCheck() error {
 
 // OnlyNode returns true if this is the only consul node.
 func (s *Setup) OnlyNode() (bool, error) {
-	c, err := s.consulMaster()
+	c, err := s.ConsulMaster()
 	if err != nil {
 		return false, err
 	}
diff --git a/lib/setup/daemon.go b/lib/setup/daemon.go
index 3985259..a7b902f 100644
--- a/lib/setup/daemon.go
+++ b/lib/setup/daemon.go
@@ -33,7 +33,7 @@ func (s *Setup) installService(d daemon) error {
 	if d.noRegister {
 		return nil
 	}
-	consul, err := s.consulMaster()
+	consul, err := s.ConsulMaster()
 	if err != nil {
 		return err
 	}
diff --git a/lib/setup/setup.go b/lib/setup/setup.go
index cdbb5db..a4c8c43 100644
--- a/lib/setup/setup.go
+++ b/lib/setup/setup.go
@@ -61,6 +61,7 @@ func (s *Setup) Run() error {
 		s.installConsul,
 		s.installConsulTemplate,
 		s.installVault,
+		s.maybeConfigure,
 		s.generateSelfSignedCert,
 		s.installArvadosServices,
 	} {
@@ -72,7 +73,7 @@ func (s *Setup) Run() error {
 
 	wait := 2 * time.Second
 	for ok := false; s.Wait && !ok; time.Sleep(wait) {
-		cc, err := s.consulMaster()
+		cc, err := s.ConsulMaster()
 		if err != nil {
 			log.Printf("setup: consulMaster(): %s", err)
 			continue
diff --git a/lib/setup/tmpl_nginx_api.go b/lib/setup/tmpl_nginx_api.go
index c2c07bf..6a448f7 100644
--- a/lib/setup/tmpl_nginx_api.go
+++ b/lib/setup/tmpl_nginx_api.go
@@ -1,7 +1,7 @@
 package setup
 
 const tmplNginxAPI = `server {
-  listen 127.0.0.1:8000;
+  listen 127.0.0.1:{{ key "arvados/service/API/port" }};
   server_name localhost-api;
 
   root /var/www/arvados-api/current/public;
@@ -21,11 +21,11 @@ const tmplNginxAPI = `server {
 }
 
 upstream api {
-  server     127.0.0.1:8000  fail_timeout=10s;
+  server     127.0.0.1:{{ key "arvados/service/API/port" }} fail_timeout=10s;
 }
 
 upstream websockets {
-  server     127.0.0.1:8100  fail_timeout=10s;
+  server     127.0.0.1:{{ key "arvados/service/Websocket/port" }} fail_timeout=10s;
 }
 
 proxy_http_version 1.1;
diff --git a/lib/setup/vault.go b/lib/setup/vault.go
index 8443764..2a25578 100644
--- a/lib/setup/vault.go
+++ b/lib/setup/vault.go
@@ -154,7 +154,7 @@ func (s *Setup) vaultBootstrap() error {
 
 	if s.InitVault {
 		// Use master token to create a management token
-		master, err := s.consulMaster()
+		master, err := s.ConsulMaster()
 		if err != nil {
 			return err
 		}

commit a328dd7d4d396d1c2f67d3a74989f0194b04e714
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Mar 14 19:09:07 2017 -0400

    11183: Start API server

diff --git a/cmd/arvados-admin/test-debian8/Dockerfile b/cmd/arvados-admin/test-debian8/Dockerfile
index 6886fd3..7db1224 100644
--- a/cmd/arvados-admin/test-debian8/Dockerfile
+++ b/cmd/arvados-admin/test-debian8/Dockerfile
@@ -11,4 +11,4 @@ RUN ["bash", "-c", "echo en_US.utf8 UTF-8 | tee -a /etc/locale.gen && locale-gen
 
 RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends ca-certificates locales nginx postgresql runit
 
-CMD ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -init-vault && arvados-admin setup -wait"]
+CMD ["bash", "-c", "runsvdir /etc/sv & service postgresql start & arvados-admin setup -init-vault && arvados-admin setup -wait"]
diff --git a/cmd/arvados-admin/test-docker-compose/docker-compose.yml b/cmd/arvados-admin/test-docker-compose/docker-compose.yml
index aa5cb48..85390ce 100644
--- a/cmd/arvados-admin/test-docker-compose/docker-compose.yml
+++ b/cmd/arvados-admin/test-docker-compose/docker-compose.yml
@@ -11,7 +11,7 @@ services:
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -init-vault=true -wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -init-vault=true -run-api -wait"]
   sys1:
     build: ../test-debian8
     cap_add:
diff --git a/lib/agent/agent.go b/lib/agent/agent.go
index 2456423..d16917a 100644
--- a/lib/agent/agent.go
+++ b/lib/agent/agent.go
@@ -42,6 +42,13 @@ type PortsConfig struct {
 	NomadRPC      int
 	NomadSerf     int
 	VaultServer   int
+
+	API       int
+	GitHTTP   int
+	KeepWeb   int
+	Keepproxy int
+	Keepstore int
+	Websocket int
 }
 
 type RepoConfig struct {
@@ -87,6 +94,13 @@ func Command() *Agent {
 			NomadRPC:      14647,
 			NomadSerf:     14648,
 			VaultServer:   18200,
+
+			API:       8000,
+			GitHTTP:   9001,
+			KeepWeb:   9002,
+			Keepproxy: 25100,
+			Keepstore: 25107,
+			Websocket: 8100,
 		},
 		DataDir:    "/var/lib/arvados",
 		UsrDir:     "/usr/local/arvados",
diff --git a/lib/setup/arvados_services.go b/lib/setup/arvados_services.go
new file mode 100644
index 0000000..f15e693
--- /dev/null
+++ b/lib/setup/arvados_services.go
@@ -0,0 +1,242 @@
+package setup
+
+import (
+	"bytes"
+	"crypto/rand"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"os/user"
+	"path"
+	"strconv"
+	"time"
+
+	consul "github.com/hashicorp/consul/api"
+)
+
+func (s *Setup) installPassenger() error {
+	if _, err := os.Stat("/etc/nginx/conf.d/passenger.conf"); err == nil {
+		return nil
+	}
+
+	err := command("apt-get", "install", "-y", "apt-transport-https", "ca-certificates").Run()
+	if err != nil {
+		return err
+	}
+	err = atomicWriteFile("/etc/apt/sources.list.d/passenger.list", []byte("deb https://oss-binaries.phusionpassenger.com/apt/passenger jessie main\n"), 0644)
+	if err != nil {
+		return err
+	}
+	err = command("apt-get", "update").Run()
+	if err != nil {
+		return err
+	}
+	err = command("apt-get", "install", "-y", "nginx-extras", "passenger").Run()
+	if err != nil {
+		return err
+	}
+	err = os.Symlink("../passenger.conf", "/etc/nginx/conf.d/passenger.conf")
+	if err != nil {
+		return err
+	}
+	return command("service", "nginx", "restart").Run()
+}
+
+func (s *Setup) installArvadosServices() error {
+	var todo []func() error
+	var cvPkgs []string
+	if s.RunAPI {
+		err := (&osPackage{Debian: "curl"}).install()
+		if err != nil {
+			return err
+		}
+		resp, err := http.Get("https://get.rvm.io")
+		if err != nil {
+			return err
+		}
+		rvmCmd := command("bash", "-s", "stable", "--ruby=2.3")
+		rvmCmd.Stdin = resp.Body
+
+		todo = append(todo,
+			command("bash", "-c", `gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3`).Run,
+			rvmCmd.Run,
+		)
+		todo = append(todo,
+			command("apt-get", "install", "-y", "--no-install-recommends",
+				"bison", "build-essential", "libcurl4-openssl-dev", "git").Run,
+		)
+
+		err = (&osPackage{Debian: "postgresql"}).install()
+		if err != nil {
+			return err
+		}
+
+		wwwUser, err := user.Lookup("www-data")
+		if err != nil {
+			return err
+		}
+		wwwGID, err := strconv.Atoi(wwwUser.Gid)
+		if err != nil {
+			return err
+		}
+
+		gitRepoDir := path.Join(s.Agent.DataDir, "git", "repositories")
+		{
+			if err := os.MkdirAll(gitRepoDir, 0755); err != nil {
+				return err
+			}
+			err = os.Chown(gitRepoDir, 0, wwwGID)
+			if err != nil {
+				return err
+			}
+		}
+
+		appYml := "/etc/arvados/api/application.yml"
+		{
+			secretToken, err := s.newSecret(32)
+			if err != nil {
+				return err
+			}
+			blobSigningKey, err := s.newSecret(32)
+			if err != nil {
+				return err
+			}
+			err = atomicWriteJSON(appYml, map[string]interface{}{
+				"production": map[string]interface{}{
+					"uuid_prefix":          s.Agent.ClusterID,
+					"secret_token":         secretToken,
+					"blob_signing_key":     blobSigningKey,
+					"sso_app_secret":       "TODO",
+					"sso_app_id":           "TODO",
+					"sso_provider_url":     "https://TODO/",
+					"workbench_address":    "https://TODO/",
+					"websocket_address":    "wss://TODO/",
+					"git_repositories_dir": gitRepoDir,
+					"git_internal_dir":     path.Join(s.Agent.DataDir, "internal.git"),
+				}}, 0640)
+			err = os.Chown(appYml, 0, wwwGID)
+			if err != nil {
+				return err
+			}
+		}
+
+		dbYml := "/etc/arvados/api/database.yml"
+		if _, err = os.Stat(dbYml); err != nil && !os.IsNotExist(err) {
+			return err
+		} else if os.IsNotExist(err) {
+			saidWaiting := false
+		waitPg:
+			for {
+				err := command("pg_isready").Run()
+				switch err := err.(type) {
+				case nil:
+					break waitPg
+				case *exec.ExitError:
+					if !saidWaiting {
+						err := command("service", "postgresql", "start").Run()
+						if err != nil {
+							return err
+						}
+						log.Print("waiting for postgres to be ready")
+						saidWaiting = true
+					}
+					time.Sleep(time.Second)
+					continue
+				default:
+					return err
+				}
+			}
+			password, err := s.newSecret(16)
+			if err != nil {
+				return err
+			}
+			// TODO: write password to a file here, so if
+			// running "create user" succeeds but writing
+			// database.yml fails, we can recover on a
+			// subsequent attempt.
+
+			sql := fmt.Sprintf("create user arvados with createdb encrypted password '%s'", password)
+			cmd := command("su", "-c", "psql", "postgres")
+			cmd.Stdin = bytes.NewBufferString(sql)
+			err = cmd.Run()
+			if err != nil {
+				return err
+			}
+			err = atomicWriteJSON(dbYml, map[string]interface{}{
+				"production": map[string]interface{}{
+					"adapter":  "postgresql",
+					"template": "template0",
+					"encoding": "utf8",
+					"database": "arvados_" + s.Agent.ClusterID,
+					"username": "arvados",
+					"password": password,
+					"host":     "localhost",
+				}}, 0640)
+			if err != nil {
+				return err
+			}
+			err = os.Chown(dbYml, 0, wwwGID)
+			if err != nil {
+				return err
+			}
+		}
+		cvPkgs = append(cvPkgs,
+			"arvados-api-server",
+		)
+		todo = append(todo,
+			command("apt-key", "adv", "--keyserver", "hkp://keyserver.ubuntu.com:80", "--recv-keys", "561F9B9CAC40B2F7").Run,
+			s.installPassenger,
+			func() error {
+				return s.consulTemplateTrigger("arvados-api", "/etc/nginx/sites-available/arvados-api.conf", tmplNginxAPI, 0640, "service nginx reload")
+			},
+			func() error {
+				return os.Symlink("../sites-available/arvados-api.conf", "/etc/nginx/sites-enabled/arvados-api.conf")
+			})
+		c, err := s.consulMaster()
+		if err != nil {
+			return err
+		}
+		err = c.Agent().ServiceRegister(&consul.AgentServiceRegistration{
+			ID:   "arvados-api:" + s.LANHost,
+			Name: "api",
+			Port: s.Agent.Ports.API,
+			Checks: consul.AgentServiceChecks{
+				{
+					HTTP:     fmt.Sprintf("http://%s:%d/discovery/v1/apis/arvados/v1/rest", s.LANHost, s.Agent.Ports.API),
+					Interval: "15s",
+				},
+			},
+		})
+		if err != nil {
+			return err
+		}
+	}
+	for _, pkg := range cvPkgs {
+		pkg := pkg
+		todo = append(todo, func() error {
+			return s.installCuroversePackage(pkg)
+		})
+	}
+	// f = append(f, func() error {
+	// 	return s.consulTemplateExec("arvados-agent", "./agent.json", tmplArvadosAgent, 0640, "arvados-admin", "agent", "-config", "./agent.json")
+	// })
+	for _, f := range todo {
+		err := f()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *Setup) newSecret(n int) (string, error) {
+	buf := &bytes.Buffer{}
+	_, err := io.CopyN(buf, rand.Reader, 16)
+	if err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("%x", buf), nil
+}
diff --git a/lib/setup/cert.go b/lib/setup/cert.go
new file mode 100644
index 0000000..b54735e
--- /dev/null
+++ b/lib/setup/cert.go
@@ -0,0 +1,24 @@
+package setup
+
+import (
+	"os"
+	"path"
+)
+
+func (s *Setup) generateSelfSignedCert() error {
+	path := path.Join(s.DataDir, "certs")
+	if err := os.MkdirAll(path, 0700); err != nil {
+		return err
+	}
+	for _, f := range []func() error{
+		command("openssl", "genrsa", "-out", path+"/selfsigned.key", "2048").Run,
+		command("openssl", "req", "-new", "-batch", "-subj", "/C=US/ST=MA/O=Example, Inc./CN=example.com", "-key", path+"/selfsigned.key", "-out", path+"/selfsigned.csr").Run,
+		command("openssl", "x509", "-req", "-days", "3650", "-in", path+"/selfsigned.csr", "-signkey", path+"/selfsigned.key", "-out", path+"/selfsigned.crt").Run,
+	} {
+		err := f()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/lib/setup/consul.go b/lib/setup/consul.go
index 9cc2f29..e1a70d0 100644
--- a/lib/setup/consul.go
+++ b/lib/setup/consul.go
@@ -13,6 +13,51 @@ import (
 	"github.com/hashicorp/consul/api"
 )
 
+func (s *Setup) consulTemplateTrigger(name, dst, tmpl string, mode os.FileMode, reload string) error {
+	atomicWriteFile(dst+".tmpl", []byte(tmpl), mode)
+
+	svdir := "/etc/sv/" + name
+	cfgPath := svdir + "/consul.json"
+	atomicWriteJSON(cfgPath, map[string]interface{}{
+		"consul": map[string]interface{}{
+			"address": fmt.Sprintf("%s:%d", s.LANHost, s.Agent.Ports.ConsulHTTP),
+			"token":   s.masterToken,
+		},
+		"vault": map[string]interface{}{
+			"address": fmt.Sprintf("http://%s:%d", s.LANHost, s.Agent.Ports.VaultServer),
+		},
+	}, 0600)
+
+	ct := path.Join(s.UsrDir, "bin", "consul-template")
+	args := []string{
+		"-config", cfgPath, "-template", dst + ".tmpl:" + dst + ":" + reload,
+	}
+	script := fmt.Sprintf("#!/bin/sh\nexec %q ", ct)
+	for _, a := range args {
+		script = script + fmt.Sprintf(" %q", a)
+	}
+	script = script + "\n"
+
+	atomicWriteFile(svdir+"/run", []byte(script), 0755)
+	err := command("sv", "term", svdir).Run()
+	if _, ok := err.(*exec.ExitError); err != nil && !ok {
+		// "sv could not send term" is ok, but "sv not found" is not ok
+		return err
+	}
+	return nil
+}
+
+func (s *Setup) installConsulTemplate() error {
+	prog := path.Join(s.UsrDir, "bin", "consul-template")
+	return (&download{
+		URL:        "https://releases.hashicorp.com/consul-template/0.18.1/consul-template_0.18.1_linux_amd64.zip",
+		Dest:       prog,
+		Size:       6932736,
+		Mode:       0755,
+		PreloadDir: s.PreloadDir,
+	}).install()
+}
+
 func (s *Setup) installConsul() error {
 	prog := path.Join(s.UsrDir, "bin", "consul")
 	err := (&download{
diff --git a/lib/setup/curoverse_package.go b/lib/setup/curoverse_package.go
new file mode 100644
index 0000000..3f7f529
--- /dev/null
+++ b/lib/setup/curoverse_package.go
@@ -0,0 +1,36 @@
+package setup
+
+import (
+	"fmt"
+	"os"
+)
+
+func (s *Setup) installCuroversePackage(name string) error {
+	rel := s.Agent.ArvadosAptRepo.Release
+	if rel == "" {
+		return fmt.Errorf("os release not known: cannot add arvados package repo")
+	}
+	listFn := "/etc/apt/sources.list.d/arvados.list"
+	{
+		err := command("apt-key", "adv", "--keyserver", "pool.sks-keyservers.net", "--recv", "1078ECD7").Run()
+		if err != nil {
+			return err
+		}
+		_, err = os.Stat(listFn)
+		if os.IsNotExist(err) {
+			err = atomicWriteFile(listFn, []byte(fmt.Sprintf("deb http://apt.arvados.org/ %s main\n", rel)), 0644)
+		}
+		if err != nil {
+			return err
+		}
+		err = command("apt-get", "update").Run()
+		if err != nil {
+			os.Remove(listFn)
+			return err
+		}
+	}
+	return (&osPackage{
+		Debian: name,
+		RedHat: name,
+	}).install()
+}
diff --git a/lib/setup/setup.go b/lib/setup/setup.go
index 25c4d25..cdbb5db 100644
--- a/lib/setup/setup.go
+++ b/lib/setup/setup.go
@@ -30,6 +30,7 @@ type Setup struct {
 	InitVault  bool
 	LANHost    string
 	PreloadDir string
+	RunAPI     bool
 	Wait       bool
 
 	encryptKey  string
@@ -42,6 +43,7 @@ func (s *Setup) ParseFlags(args []string) error {
 	fs.StringVar(&s.ClusterID, "cluster-id", s.ClusterID, "five-character cluster ID")
 	fs.BoolVar(&s.InitVault, "init-vault", s.InitVault, "initialize the vault if needed")
 	fs.BoolVar(&s.Unseal, "unseal", s.Unseal, "unseal the vault automatically")
+	fs.BoolVar(&s.RunAPI, "run-api", s.RunAPI, "run API server on this node")
 	fs.BoolVar(&s.Wait, "wait", s.Wait, "wait for all nodes to come up before exiting")
 	return fs.Parse(args)
 }
@@ -57,7 +59,10 @@ func (s *Setup) Run() error {
 		(&osPackage{Debian: "nginx"}).install,
 		s.installRunit,
 		s.installConsul,
+		s.installConsulTemplate,
 		s.installVault,
+		s.generateSelfSignedCert,
+		s.installArvadosServices,
 	} {
 		err := f()
 		if err != nil {
@@ -72,6 +77,20 @@ func (s *Setup) Run() error {
 			log.Printf("setup: consulMaster(): %s", err)
 			continue
 		}
+
+		apiSvcs, _, err := cc.Catalog().Service("arvados-api", "", nil)
+		if err != nil {
+			log.Printf("setup: consul.Catalog().Service(): %s", err)
+			continue
+		} else if len(apiSvcs) == 0 {
+			if wait <= 2*time.Second {
+				wait = wait * 2
+				log.Printf("setup: waiting for arvados-api service to appear")
+			}
+			continue
+		}
+		log.Printf("arvados-api service: %#v", apiSvcs)
+
 		ok = true
 		svcs, _, err := cc.Catalog().Services(nil)
 		if err != nil {
diff --git a/lib/setup/tmpl_nginx_api.go b/lib/setup/tmpl_nginx_api.go
new file mode 100644
index 0000000..c2c07bf
--- /dev/null
+++ b/lib/setup/tmpl_nginx_api.go
@@ -0,0 +1,95 @@
+package setup
+
+const tmplNginxAPI = `server {
+  listen 127.0.0.1:8000;
+  server_name localhost-api;
+
+  root /var/www/arvados-api/current/public;
+  index  index.html index.htm index.php;
+
+  passenger_enabled on;
+  # if using RVM:
+  passenger_ruby /usr/local/rvm/wrappers/default/ruby;
+
+  # This value effectively limits the size of API objects users can
+  # create, especially collections.  If you change this, you should
+  # also ensure the following settings match it:
+  # * "client_max_body_size" in the server section below
+  # * "client_max_body_size" in the Workbench Nginx configuration (twice)
+  # * "max_request_size" in the API server's application.yml file
+  client_max_body_size 128m;
+}
+
+upstream api {
+  server     127.0.0.1:8000  fail_timeout=10s;
+}
+
+upstream websockets {
+  server     127.0.0.1:8100  fail_timeout=10s;
+}
+
+proxy_http_version 1.1;
+
+# When Keep clients request a list of Keep services from the API server, the
+# server will automatically return the list of available proxies if
+# the request headers include X-External-Client: 1.  Following the example
+# here, at the end of this section, add a line for each netmask that has
+# direct access to Keep storage daemons to set this header value to 0.
+geo $external_client {
+  default        1;
+  10.0.0.0/8     0;
+  172.16.0.0/12  0;
+  192.168.0.0/16 0;
+}
+
+server {
+  listen       0.0.0.0:443 ssl;
+  server_name  uuid_prefix.your.domain;
+
+  ssl on;
+  ssl_certificate     {{ keyOrDefault "arvados/api/sslCertPath" "/var/lib/arvados/certs/selfsigned.crt" }};
+  ssl_certificate_key {{ keyOrDefault "arvados/api/sslKeyPath" "/var/lib/arvados/certs/selfsigned.key" }};
+
+  index  index.html index.htm index.php;
+
+  # Refer to the comment about this setting in the server section above.
+  client_max_body_size 128m;
+
+  location / {
+    proxy_pass            http://api;
+    proxy_redirect        off;
+    proxy_connect_timeout 90s;
+    proxy_read_timeout    300s;
+
+    proxy_set_header      X-Forwarded-Proto https;
+    proxy_set_header      Host $http_host;
+    proxy_set_header      X-External-Client $external_client;
+    proxy_set_header      X-Real-IP $remote_addr;
+    proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
+  }
+}
+
+server {
+  listen       0.0.0.0:443 ssl;
+  server_name  ws.uuid_prefix.your.domain;
+
+  ssl on;
+  ssl_certificate     {{ keyOrDefault "arvados/ws/sslCertPath" "/var/lib/arvados/certs/selfsigned.crt" }};
+  ssl_certificate_key {{ keyOrDefault "arvados/ws/sslKeyPath" "/var/lib/arvados/certs/selfsigned.key" }};
+
+  index  index.html index.htm index.php;
+
+  location / {
+    proxy_pass            http://websockets;
+    proxy_redirect        off;
+    proxy_connect_timeout 90s;
+    proxy_read_timeout    300s;
+
+    proxy_set_header      Upgrade $http_upgrade;
+    proxy_set_header      Connection "upgrade";
+    proxy_set_header      Host $host;
+    proxy_set_header      X-Real-IP $remote_addr;
+    proxy_set_header      X-Forwarded-For $proxy_add_x_forwarded_for;
+  }
+}
+`

commit 887ca969a3fa97f2181a089ac22fac1ff1e3ed25
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu Mar 2 01:04:27 2017 -0500

    11183: Add "setup -wait" flag

diff --git a/cmd/arvados-admin/test-debian8/Dockerfile b/cmd/arvados-admin/test-debian8/Dockerfile
index 645aec2..6886fd3 100644
--- a/cmd/arvados-admin/test-debian8/Dockerfile
+++ b/cmd/arvados-admin/test-debian8/Dockerfile
@@ -11,4 +11,4 @@ RUN ["bash", "-c", "echo en_US.utf8 UTF-8 | tee -a /etc/locale.gen && locale-gen
 
 RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends ca-certificates locales nginx postgresql runit
 
-CMD ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup && arvados-admin setup"]
+CMD ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -init-vault && arvados-admin setup -wait"]
diff --git a/cmd/arvados-admin/test-docker-compose/docker-compose.yml b/cmd/arvados-admin/test-docker-compose/docker-compose.yml
index 34e7cd4..aa5cb48 100644
--- a/cmd/arvados-admin/test-docker-compose/docker-compose.yml
+++ b/cmd/arvados-admin/test-docker-compose/docker-compose.yml
@@ -11,7 +11,7 @@ services:
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -unseal=true -init-vault=true && wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -init-vault=true -wait"]
   sys1:
     build: ../test-debian8
     cap_add:
@@ -23,7 +23,7 @@ services:
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -unseal=true && wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -wait"]
   sys2:
     build: ../test-debian8
     cap_add:
@@ -35,6 +35,6 @@ services:
       - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
       - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
       - vault:/var/lib/arvados/vault
-    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -unseal=true && wait"]
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -wait"]
 volumes:
   vault:
diff --git a/lib/agent/agent.go b/lib/agent/agent.go
index 52a3d83..2456423 100644
--- a/lib/agent/agent.go
+++ b/lib/agent/agent.go
@@ -91,6 +91,7 @@ func Command() *Agent {
 		DataDir:    "/var/lib/arvados",
 		UsrDir:     "/usr/local/arvados",
 		RunitSvDir: "/etc/sv",
+		Unseal:     true,
 	}
 }
 
diff --git a/lib/setup/setup.go b/lib/setup/setup.go
index 379ccb2..25c4d25 100644
--- a/lib/setup/setup.go
+++ b/lib/setup/setup.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"log"
 	"os"
+	"time"
 
 	"git.curoverse.com/arvados.git/lib/agent"
 	"git.curoverse.com/arvados.git/sdk/go/config"
@@ -29,6 +30,7 @@ type Setup struct {
 	InitVault  bool
 	LANHost    string
 	PreloadDir string
+	Wait       bool
 
 	encryptKey  string
 	masterToken string
@@ -40,6 +42,7 @@ func (s *Setup) ParseFlags(args []string) error {
 	fs.StringVar(&s.ClusterID, "cluster-id", s.ClusterID, "five-character cluster ID")
 	fs.BoolVar(&s.InitVault, "init-vault", s.InitVault, "initialize the vault if needed")
 	fs.BoolVar(&s.Unseal, "unseal", s.Unseal, "unseal the vault automatically")
+	fs.BoolVar(&s.Wait, "wait", s.Wait, "wait for all nodes to come up before exiting")
 	return fs.Parse(args)
 }
 
@@ -61,6 +64,45 @@ func (s *Setup) Run() error {
 			return err
 		}
 	}
+
+	wait := 2 * time.Second
+	for ok := false; s.Wait && !ok; time.Sleep(wait) {
+		cc, err := s.consulMaster()
+		if err != nil {
+			log.Printf("setup: consulMaster(): %s", err)
+			continue
+		}
+		ok = true
+		svcs, _, err := cc.Catalog().Services(nil)
+		if err != nil {
+			log.Printf("setup: consul.Catalog().Services(): %s", err)
+			continue
+		}
+		for svc := range svcs {
+			checks, _, err := cc.Health().Checks(svc, nil)
+			if err != nil {
+				log.Printf("setup: consul.Health().Checks(%q): %s", svc, err)
+				continue
+			}
+
+			for _, check := range checks {
+				if check.Status != "passing" {
+					log.Printf("waiting for node %q service %q check %q state %q", check.Node, check.ServiceName, check.CheckID, check.Status)
+					ok = false
+				}
+			}
+		}
+		if ok {
+			log.Printf("All services are passing: %+v", svcs)
+			// Wait to ensure any other "setup -wait"
+			// processes have a chance to see the
+			// all-passing state before we return (if this
+			// is a test or image-building scenario, the
+			// whole system might shut down and stop
+			// passing as soon as we return).
+			time.Sleep(2 * wait)
+		}
+	}
 	return nil
 }
 

commit 1883fbbdc269fa0bdf3099984160e02421bc94b1
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Feb 28 15:52:18 2017 -0500

    11183: Add "arvados-admin setup"

diff --git a/build/package-build-dockerfiles/debian8/Dockerfile b/build/package-build-dockerfiles/debian8/Dockerfile
index d4e77c9..36bba41 100644
--- a/build/package-build-dockerfiles/debian8/Dockerfile
+++ b/build/package-build-dockerfiles/debian8/Dockerfile
@@ -2,7 +2,7 @@ FROM debian:jessie
 MAINTAINER Ward Vandewege <ward at curoverse.com>
 
 # Install dependencies and set up system.
-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
+RUN apt-get update && 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 && apt-get clean
 
 # Install RVM
 RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
diff --git a/build/package-build-dockerfiles/ubuntu1204/Dockerfile b/build/package-build-dockerfiles/ubuntu1204/Dockerfile
index daeabc9..3a03b9a 100644
--- a/build/package-build-dockerfiles/ubuntu1204/Dockerfile
+++ b/build/package-build-dockerfiles/ubuntu1204/Dockerfile
@@ -2,7 +2,7 @@ FROM ubuntu:precise
 MAINTAINER Ward Vandewege <ward at curoverse.com>
 
 # Install dependencies and set up system.
-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
+RUN apt-get update && 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 && apt-get clean
 
 # Install RVM
 RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
diff --git a/build/package-build-dockerfiles/ubuntu1404/Dockerfile b/build/package-build-dockerfiles/ubuntu1404/Dockerfile
index aa92ad2..13e9bc6 100644
--- a/build/package-build-dockerfiles/ubuntu1404/Dockerfile
+++ b/build/package-build-dockerfiles/ubuntu1404/Dockerfile
@@ -1,8 +1,8 @@
 FROM ubuntu:trusty
-MAINTAINER Brett Smith <brett at curoverse.com>
+MAINTAINER Ward Vandewege <ward at curoverse.com>
 
 # Install dependencies and set up system.
-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
+RUN apt-get update && 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 && apt-get clean
 
 # Install RVM
 RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
diff --git a/build/package-build-dockerfiles/ubuntu1604/Dockerfile b/build/package-build-dockerfiles/ubuntu1604/Dockerfile
index fec55e6..d85e8b2 100644
--- a/build/package-build-dockerfiles/ubuntu1604/Dockerfile
+++ b/build/package-build-dockerfiles/ubuntu1604/Dockerfile
@@ -2,7 +2,7 @@ FROM ubuntu:xenial
 MAINTAINER Ward Vandewege <ward at curoverse.com>
 
 # Install dependencies and set up system.
-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
+RUN apt-get update && 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 && apt-get clean
 
 # Install RVM
 RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \
diff --git a/build/run-build-packages-all-targets.sh b/build/run-build-packages-all-targets.sh
index a4dd9a6..2d16147 100755
--- a/build/run-build-packages-all-targets.sh
+++ b/build/run-build-packages-all-targets.sh
@@ -38,7 +38,7 @@ fi
 set -e
 
 PARSEDOPTS=$(getopt --name "$0" --longoptions \
-    help,test-packages,debug,command:,only-test: \
+    help,test-packages,debug,command:,only-test:,only-build: \
     -- "" "$@")
 if [ $? -ne 0 ]; then
     exit 1
@@ -66,6 +66,9 @@ while [ $# -gt 0 ]; do
         --test-packages)
             TEST_PACKAGES="--test-packages"
             ;;
+        --only-build)
+            ONLY_BUILD="$1 $2"; shift
+            ;;
         --only-test)
             ONLY_TEST="$1 $2"; shift
             ;;
@@ -84,7 +87,7 @@ cd $(dirname $0)
 FINAL_EXITCODE=0
 
 for dockerfile_path in $(find -name Dockerfile | grep package-build-dockerfiles); do
-    if ./run-build-packages-one-target.sh --target "$(basename $(dirname "$dockerfile_path"))" --command "$COMMAND" $DEBUG $TEST_PACKAGES $ONLY_TEST ; then
+    if ./run-build-packages-one-target.sh --target "$(basename $(dirname "$dockerfile_path"))" --command "$COMMAND" $DEBUG $TEST_PACKAGES $ONLY_TEST $ONLY_BUILD ; then
         true
     else
         FINAL_EXITCODE=$?
diff --git a/build/run-build-packages-one-target.sh b/build/run-build-packages-one-target.sh
index 685ca51..78adfbb 100755
--- a/build/run-build-packages-one-target.sh
+++ b/build/run-build-packages-one-target.sh
@@ -129,6 +129,7 @@ popd
 
 if test -z "$packages" ; then
     packages="arvados-api-server
+        arvados-admin
         arvados-docker-cleaner
         arvados-git-httpd
         arvados-node-manager
diff --git a/build/run-build-packages.sh b/build/run-build-packages.sh
index 777cd3c..5c6875d 100755
--- a/build/run-build-packages.sh
+++ b/build/run-build-packages.sh
@@ -338,6 +338,8 @@ package_go_binary sdk/go/crunchrunner crunchrunner \
     "Crunchrunner executes a command inside a container and uploads the output"
 package_go_binary services/arv-git-httpd arvados-git-httpd \
     "Provide authenticated http access to Arvados-hosted git repositories"
+package_go_binary cmd/arvados-admin arvados-admin \
+    "Arvados cluster administration tool"
 package_go_binary services/crunch-dispatch-local crunch-dispatch-local \
     "Dispatch Crunch containers on the local system"
 package_go_binary services/crunch-dispatch-slurm crunch-dispatch-slurm \
diff --git a/build/run-library.sh b/build/run-library.sh
index 3d61962..4f259f9 100755
--- a/build/run-library.sh
+++ b/build/run-library.sh
@@ -114,6 +114,8 @@ package_go_binary() {
         fi
     fi
 
+    go generate || return 1
+
     cd $WORKSPACE/packages/$TARGET
     test_package_presence $prog $version go
 
diff --git a/build/run-tests.sh b/build/run-tests.sh
index afaa834..e2e6f34 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -101,6 +101,10 @@ tools/crunchstat-summary
 tools/keep-exercise
 tools/keep-rsync
 tools/keep-block-check
+lib/agent
+lib/crunchstat
+lib/setup
+cmd/arvados-admin
 
 (*) apps/workbench is shorthand for apps/workbench_units +
     apps/workbench_functionals + apps/workbench_integration
@@ -198,6 +202,9 @@ sanity_checks() {
     echo -n 'gitolite: '
     which gitolite \
         || fatal "No gitolite. Try: apt-get install gitolite3"
+    echo -n 'docker-compose: '
+    which docker-compose \
+        || fatal "No docker-compose. Try: sudo curl -L https://github.com/docker/compose/releases/download/1.11.2/docker-compose-`uname -s`-`uname -m` --output /usr/local/bin/docker-compose && sudo chmod +x /usr/local/bin/docker-compose"
 }
 
 rotate_logfile() {
@@ -547,12 +554,14 @@ do_test_once() {
     then
         covername="coverage-$(echo "$1" | sed -e 's/\//_/g')"
         coverflags=("-covermode=count" "-coverprofile=$WORKSPACE/tmp/.$covername.tmp")
+        gopkgpath="git.curoverse.com/arvados.git/$1"
         # We do "go get -t" here to catch compilation errors
         # before trying "go test". Otherwise, coverage-reporting
         # mode makes Go show the wrong line numbers when reporting
         # compilation errors.
         go get -t "git.curoverse.com/arvados.git/$1" && \
             cd "$WORKSPACE/$1" && \
+            go generate && \
             [[ -z "$(gofmt -e -d . | tee /dev/stderr)" ]] && \
             if [[ -n "${testargs[$1]}" ]]
         then
@@ -562,7 +571,7 @@ do_test_once() {
         else
             # The above form gets verbose even when testargs is
             # empty, so use this form in such cases:
-            go test ${short:+-short} ${coverflags[@]} "git.curoverse.com/arvados.git/$1"
+            go test ${short:+-short} ${coverflags[@]} .
         fi
         result=${result:-$?}
         if [[ -f "$WORKSPACE/tmp/.$covername.tmp" ]]
@@ -594,6 +603,9 @@ do_test_once() {
     else
         "test_$1"
     fi
+    if [[ -e "${WORKSPACE}/${1}/package.json" ]]; then
+        cd "${WORKSPACE}/${1}" && npm test || result=1
+    fi
     result=${result:-$?}
     checkexit $result "$1 tests"
     title "End of $1 tests (`timer`)"
@@ -611,9 +623,15 @@ do_install() {
 do_install_once() {
     title "Running $1 install"
     timer_reset
+    cd "${WORKSPACE}/${1}" || return 1
+    if [[ -e "${WORKSPACE}/${1}/package.json" ]]; then
+        npm install || return 1
+    fi
     if [[ "$2" == "go" ]]
     then
-        go get -t "git.curoverse.com/arvados.git/$1"
+        go get -d -t "git.curoverse.com/arvados.git/$1" \
+            && go generate \
+            && go get "git.curoverse.com/arvados.git/$1"
     elif [[ "$2" == "pip" ]]
     then
         # $3 can name a path directory for us to use, including trailing
@@ -628,8 +646,7 @@ do_install_once() {
         # install" ensures that the dependencies are met, the second "pip
         # install" ensures that we've actually installed the local package
         # we just built.
-        cd "$WORKSPACE/$1" \
-            && "${3}python" setup.py sdist rotate --keep=1 --match .tar.gz \
+        "${3}python" setup.py sdist rotate --keep=1 --match .tar.gz \
             && cd "$WORKSPACE" \
             && "${3}pip" install --quiet "$WORKSPACE/$1/dist"/*.tar.gz \
             && "${3}pip" install --quiet --no-deps --ignore-installed "$WORKSPACE/$1/dist"/*.tar.gz
@@ -779,7 +796,9 @@ gostuff=(
     sdk/go/streamer
     sdk/go/crunchrunner
     sdk/go/stats
+    lib/agent
     lib/crunchstat
+    lib/setup
     services/arv-git-httpd
     services/crunchstat
     services/keep-web
@@ -791,9 +810,11 @@ gostuff=(
     services/crunch-dispatch-slurm
     services/crunch-run
     services/ws
+    services/boot
     tools/keep-block-check
     tools/keep-exercise
     tools/keep-rsync
+    cmd/arvados-admin
     )
 for g in "${gostuff[@]}"
 do
diff --git a/cmd/arvados-admin/.gitignore b/cmd/arvados-admin/.gitignore
new file mode 100644
index 0000000..87fbbf4
--- /dev/null
+++ b/cmd/arvados-admin/.gitignore
@@ -0,0 +1 @@
+arvados-admin
diff --git a/cmd/arvados-admin/main.go b/cmd/arvados-admin/main.go
new file mode 100644
index 0000000..de532cb
--- /dev/null
+++ b/cmd/arvados-admin/main.go
@@ -0,0 +1,26 @@
+package main
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"git.curoverse.com/arvados.git/cmd"
+	"git.curoverse.com/arvados.git/lib/agent"
+	"git.curoverse.com/arvados.git/lib/setup"
+)
+
+var cmds = map[string]cmd.Command{
+	"agent": agent.Command(),
+	"setup": setup.Command(),
+}
+
+func main() {
+	err := cmd.Dispatch(cmds, os.Args[0], os.Args[1:])
+	if err != nil {
+		if err != flag.ErrHelp {
+			fmt.Fprintf(os.Stderr, "%s\n", err)
+		}
+		os.Exit(1)
+	}
+}
diff --git a/cmd/arvados-admin/setup_debian8_test.go b/cmd/arvados-admin/setup_debian8_test.go
new file mode 100644
index 0000000..671f8c7
--- /dev/null
+++ b/cmd/arvados-admin/setup_debian8_test.go
@@ -0,0 +1,42 @@
+package main
+
+import (
+	"log"
+	"net"
+	"os"
+	"os/exec"
+	"testing"
+)
+
+func TestSetupDebian8(t *testing.T) {
+	cwd, err := os.Getwd()
+	if err != nil {
+		t.Fatal(err)
+	}
+	ln, err := net.Listen("tcp", ":")
+	if err != nil {
+		t.Fatal(err)
+	}
+	_, port, err := net.SplitHostPort(ln.Addr().String())
+	if err != nil {
+		t.Fatal(err)
+	}
+	err = ln.Close()
+	if err != nil {
+		t.Fatal(err)
+	}
+	log.Printf("Publishing consul webgui at %v", ln.Addr())
+	for _, cmdline := range [][]string{
+		{"go", "build"},
+		{"docker", "build", "--tag=arvados-admin-debian8-test", "test-debian8"},
+		{"docker", "run", "--rm", "--publish=" + port + ":18500", "--cap-add=IPC_LOCK", "--cap-add=SYS_ADMIN", "--volume=/sys/fs/cgroup", "--volume=" + cwd + "/arvados-admin:/usr/bin/arvados-admin:ro", "--volume=/var/cache/arvados:/var/cache/arvados:ro", "arvados-admin-debian8-test"},
+	} {
+		cmd := exec.Command(cmdline[0], cmdline[1:]...)
+		cmd.Stdout = os.Stderr
+		cmd.Stderr = os.Stderr
+		err = cmd.Run()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+}
diff --git a/cmd/arvados-admin/setup_docker_compose_test.go b/cmd/arvados-admin/setup_docker_compose_test.go
new file mode 100644
index 0000000..e88dc16
--- /dev/null
+++ b/cmd/arvados-admin/setup_docker_compose_test.go
@@ -0,0 +1,23 @@
+package main
+
+import (
+	"os"
+	"os/exec"
+	"testing"
+)
+
+func TestSetupDockerCompose(t *testing.T) {
+	for _, cmdline := range [][]string{
+		{"go", "build"},
+		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "down", "-v"},
+		{"docker-compose", "--file", "test-docker-compose/docker-compose.yml", "up"},
+	} {
+		cmd := exec.Command(cmdline[0], cmdline[1:]...)
+		cmd.Stdout = os.Stderr
+		cmd.Stderr = os.Stderr
+		err := cmd.Run()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+}
diff --git a/cmd/arvados-admin/test-debian8/Dockerfile b/cmd/arvados-admin/test-debian8/Dockerfile
new file mode 100644
index 0000000..645aec2
--- /dev/null
+++ b/cmd/arvados-admin/test-debian8/Dockerfile
@@ -0,0 +1,14 @@
+FROM debian:8
+RUN apt-get update
+
+# preload (but don't install) packages arvados-boot might decide to install
+RUN DEBIAN_FRONTEND=noninteractive apt-get -dy install --no-install-recommends ca-certificates locales nginx postgresql runit
+
+RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends runit locales
+
+RUN ["bash", "-c", "echo en_US.utf8 UTF-8 | tee -a /etc/locale.gen && locale-gen -a && \
+    (echo LANG=en_US.UTF-8; echo LC_ALL=en_US.UTF-8) > /etc/default/locale"]
+
+RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends ca-certificates locales nginx postgresql runit
+
+CMD ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup && arvados-admin setup"]
diff --git a/cmd/arvados-admin/test-docker-compose/agent.yml b/cmd/arvados-admin/test-docker-compose/agent.yml
new file mode 100644
index 0000000..46f4de7
--- /dev/null
+++ b/cmd/arvados-admin/test-docker-compose/agent.yml
@@ -0,0 +1,4 @@
+ControlHosts:
+  - sys0
+  - sys1
+  - sys2
diff --git a/cmd/arvados-admin/test-docker-compose/docker-compose.yml b/cmd/arvados-admin/test-docker-compose/docker-compose.yml
new file mode 100644
index 0000000..34e7cd4
--- /dev/null
+++ b/cmd/arvados-admin/test-docker-compose/docker-compose.yml
@@ -0,0 +1,40 @@
+version: '2'
+services:
+  sys0:
+    build: ../test-debian8
+    cap_add:
+      - IPC_LOCK
+      - SYS_ADMIN
+    volumes:
+      - ../arvados-admin:/usr/bin/arvados-admin:ro
+      - ./agent.yml:/etc/arvados/agent/agent.yml:ro
+      - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
+      - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
+      - vault:/var/lib/arvados/vault
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -unseal=true -init-vault=true && wait"]
+  sys1:
+    build: ../test-debian8
+    cap_add:
+      - IPC_LOCK
+      - SYS_ADMIN
+    volumes:
+      - ../arvados-admin:/usr/bin/arvados-admin:ro
+      - ./agent.yml:/etc/arvados/agent/agent.yml:ro
+      - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
+      - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
+      - vault:/var/lib/arvados/vault
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -unseal=true && wait"]
+  sys2:
+    build: ../test-debian8
+    cap_add:
+      - IPC_LOCK
+      - SYS_ADMIN
+    volumes:
+      - ../arvados-admin:/usr/bin/arvados-admin:ro
+      - ./agent.yml:/etc/arvados/agent/agent.yml:ro
+      - ./encrypt-key.txt:/var/lib/arvados/encrypt-key.txt:ro
+      - ./master-token.txt:/var/lib/arvados/master-token.txt:ro
+      - vault:/var/lib/arvados/vault
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -unseal=true && wait"]
+volumes:
+  vault:
diff --git a/cmd/arvados-admin/test-docker-compose/encrypt-key.txt b/cmd/arvados-admin/test-docker-compose/encrypt-key.txt
new file mode 100644
index 0000000..507ff36
--- /dev/null
+++ b/cmd/arvados-admin/test-docker-compose/encrypt-key.txt
@@ -0,0 +1 @@
+qigR/fVUccR07/J56MsloA==
diff --git a/cmd/arvados-admin/test-docker-compose/master-token.txt b/cmd/arvados-admin/test-docker-compose/master-token.txt
new file mode 100644
index 0000000..f12bf2c
--- /dev/null
+++ b/cmd/arvados-admin/test-docker-compose/master-token.txt
@@ -0,0 +1 @@
+2f79a06949ba76666308f5c821f234c9c038664df2b8662b587b9500ef4853a1
\ No newline at end of file
diff --git a/cmd/dispatch.go b/cmd/dispatch.go
new file mode 100644
index 0000000..2fc2a70
--- /dev/null
+++ b/cmd/dispatch.go
@@ -0,0 +1,59 @@
+package cmd
+
+import (
+	"flag"
+	"fmt"
+	"os"
+	"sort"
+
+	"git.curoverse.com/arvados.git/sdk/go/config"
+)
+
+// A Command is a subcommand that can be invoked by Dispatch.
+type Command interface {
+	DefaultConfigFile() string
+	ParseFlags([]string) error
+	Run() error
+}
+
+// Dispatch parses flags from args, chooses an entry in cmds using the
+// next argument after the parsed flags, loads the command's
+// configuration file if it exists, passes any additional flags to the
+// command's ParseFlags method, and -- if all of those steps complete
+// without errors -- runs the command.
+func Dispatch(cmds map[string]Command, prog string, args []string) error {
+	fs := flag.NewFlagSet(prog, flag.ContinueOnError)
+	err := fs.Parse(args)
+	if err != nil {
+		return err
+	}
+
+	subcmd := fs.Arg(0)
+	cmd, ok := cmds[subcmd]
+	if !ok {
+		if subcmd != "" && subcmd != "help" {
+			return fmt.Errorf("unrecognized subcommand %q", subcmd)
+		}
+		var subcmds []string
+		for s := range cmds {
+			subcmds = append(subcmds, s)
+		}
+		sort.Sort(sort.StringSlice(subcmds))
+		return fmt.Errorf("available subcommands: %q", subcmds)
+	}
+
+	err = config.LoadFile(cmd, cmd.DefaultConfigFile())
+	if err != nil && !os.IsNotExist(err) {
+		return err
+	}
+	if fs.NArg() > 1 {
+		args = fs.Args()[1:]
+	} else {
+		args = nil
+	}
+	err = cmd.ParseFlags(args)
+	if err != nil {
+		return err
+	}
+	return cmd.Run()
+}
diff --git a/lib/agent/agent.go b/lib/agent/agent.go
new file mode 100644
index 0000000..52a3d83
--- /dev/null
+++ b/lib/agent/agent.go
@@ -0,0 +1,107 @@
+package agent
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"strings"
+)
+
+type Agent struct {
+	// 5 alphanumeric chars. Must be either xx*, yy*, zz*, or
+	// globally unique.
+	ClusterID string
+
+	// "runit" or "systemd"
+	DaemonSupervisor string
+
+	// Hostnames or IP addresses of control hosts. Use at least 3
+	// in production. System functions only when a majority are
+	// alive.
+	ControlHosts []string
+	Ports        PortsConfig
+	DataDir      string
+	UsrDir       string
+	RunitSvDir   string
+
+	ArvadosAptRepo RepoConfig
+
+	// Unseal the vault automatically at startup
+	Unseal bool
+}
+
+type PortsConfig struct {
+	ConsulDNS     int
+	ConsulHTTP    int
+	ConsulHTTPS   int
+	ConsulRPC     int
+	ConsulSerfLAN int
+	ConsulSerfWAN int
+	ConsulServer  int
+	NomadHTTP     int
+	NomadRPC      int
+	NomadSerf     int
+	VaultServer   int
+}
+
+type RepoConfig struct {
+	Enabled bool
+	URL     string
+	Release string
+}
+
+func Command() *Agent {
+	var repoConf RepoConfig
+	if rel, err := ioutil.ReadFile("/etc/os-release"); err == nil {
+		rel := string(rel)
+		for _, try := range []string{"jessie", "precise", "xenial"} {
+			if !strings.Contains(rel, try) {
+				continue
+			}
+			repoConf = RepoConfig{
+				Enabled: true,
+				URL:     "http://apt.arvados.org/",
+				Release: try,
+			}
+			break
+		}
+	}
+	ds := "runit"
+	if _, err := os.Stat("/run/systemd/system"); err == nil {
+		ds = "systemd"
+	}
+	return &Agent{
+		ClusterID:        "zzzzz",
+		DaemonSupervisor: ds,
+		ArvadosAptRepo:   repoConf,
+		ControlHosts:     []string{"127.0.0.1"},
+		Ports: PortsConfig{
+			ConsulDNS:     18600,
+			ConsulHTTP:    18500,
+			ConsulHTTPS:   -1,
+			ConsulRPC:     18400,
+			ConsulSerfLAN: 18301,
+			ConsulSerfWAN: 18302,
+			ConsulServer:  18300,
+			NomadHTTP:     14646,
+			NomadRPC:      14647,
+			NomadSerf:     14648,
+			VaultServer:   18200,
+		},
+		DataDir:    "/var/lib/arvados",
+		UsrDir:     "/usr/local/arvados",
+		RunitSvDir: "/etc/sv",
+	}
+}
+
+func (*Agent) ParseFlags(args []string) error {
+	return nil
+}
+
+func (a *Agent) Run() error {
+	return fmt.Errorf("not implemented: %T.Run()", a)
+}
+
+func (*Agent) DefaultConfigFile() string {
+	return "/etc/arvados/agent/agent.yml"
+}
diff --git a/lib/setup/check.go b/lib/setup/check.go
new file mode 100644
index 0000000..2b12c1f
--- /dev/null
+++ b/lib/setup/check.go
@@ -0,0 +1,12 @@
+package setup
+
+import "time"
+
+func waitCheck(timeout time.Duration, check func() error) error {
+	deadline := time.Now().Add(timeout)
+	var err error
+	for err = check(); err != nil && !time.Now().After(deadline); err = check() {
+		time.Sleep(time.Second)
+	}
+	return err
+}
diff --git a/lib/setup/command.go b/lib/setup/command.go
new file mode 100644
index 0000000..79c28b7
--- /dev/null
+++ b/lib/setup/command.go
@@ -0,0 +1,26 @@
+package setup
+
+import (
+	"os"
+	"os/exec"
+)
+
+func command(prog string, args ...string) *exec.Cmd {
+	cmd := exec.Command(prog, args...)
+	cmd.Stderr = os.Stderr
+	cmd.Stdout = os.Stderr
+	return cmd
+}
+
+func runStatusCmd(prog string, args ...string) (bool, error) {
+	cmd := command(prog, args...)
+	err := cmd.Run()
+	switch err.(type) {
+	case *exec.ExitError:
+		return false, nil
+	case nil:
+		return true, nil
+	default:
+		return false, err
+	}
+}
diff --git a/lib/setup/consul.go b/lib/setup/consul.go
new file mode 100644
index 0000000..9cc2f29
--- /dev/null
+++ b/lib/setup/consul.go
@@ -0,0 +1,167 @@
+package setup
+
+import (
+	"crypto/rand"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"os/exec"
+	"path"
+	"strings"
+	"time"
+
+	"github.com/hashicorp/consul/api"
+)
+
+func (s *Setup) installConsul() error {
+	prog := path.Join(s.UsrDir, "bin", "consul")
+	err := (&download{
+		URL:        "https://releases.hashicorp.com/consul/0.7.5/consul_0.7.5_linux_amd64.zip",
+		Dest:       prog,
+		Size:       36003713,
+		Mode:       0755,
+		PreloadDir: s.PreloadDir,
+	}).install()
+	if err != nil {
+		return err
+	}
+
+	if err := s.consulInit(); err != nil {
+		return err
+	}
+	if s.consulCheck() == nil {
+		return nil
+	}
+
+	dataDir := path.Join(s.DataDir, "consul")
+	if err := os.MkdirAll(dataDir, 0700); err != nil {
+		return err
+	}
+
+	cf := path.Join(s.DataDir, "consul-config.json")
+	{
+		c := map[string]interface{}{
+			"acl_agent_token":       s.masterToken,
+			"acl_datacenter":        s.ClusterID,
+			"acl_default_policy":    "deny",
+			"acl_enforce_version_8": true,
+			"acl_master_token":      s.masterToken,
+			"bootstrap_expect":      len(s.ControlHosts),
+			"client_addr":           "0.0.0.0",
+			"data_dir":              dataDir,
+			"datacenter":            s.ClusterID,
+			"encrypt":               s.encryptKey,
+			"server":                true,
+			"ui":                    true,
+			"ports": map[string]int{
+				"dns":      s.Ports.ConsulDNS,
+				"http":     s.Ports.ConsulHTTP,
+				"https":    s.Ports.ConsulHTTPS,
+				"rpc":      s.Ports.ConsulRPC,
+				"serf_lan": s.Ports.ConsulSerfLAN,
+				"serf_wan": s.Ports.ConsulSerfWAN,
+				"server":   s.Ports.ConsulServer,
+			},
+		}
+		err = atomicWriteJSON(cf, c, 0600)
+		if err != nil {
+			return err
+		}
+	}
+
+	err = s.installService(daemon{
+		name:       "arvados-consul",
+		prog:       prog,
+		args:       []string{"agent", "-config-file=" + cf},
+		noRegister: true,
+	})
+	if err != nil {
+		return err
+	}
+	if err = waitCheck(20*time.Second, s.consulCheck); err != nil {
+		return err
+	}
+	if len(s.ControlHosts) > 1 {
+		args := []string{"join"}
+		args = append(args, fmt.Sprintf("-rpc-addr=127.0.0.1:%d", s.Ports.ConsulRPC))
+		args = append(args, s.ControlHosts...)
+		cmd := exec.Command(prog, args...)
+		cmd.Stdout = os.Stderr
+		cmd.Stderr = os.Stderr
+		err := cmd.Run()
+		if err != nil {
+			return fmt.Errorf("consul join: %s", err)
+		}
+	}
+	return nil
+}
+
+var consulCfg = api.DefaultConfig()
+
+func (s *Setup) consulMaster() (*api.Client, error) {
+	masterToken, err := ioutil.ReadFile(path.Join(s.DataDir, "master-token.txt"))
+	if err != nil {
+		return nil, err
+	}
+	ccfg := api.DefaultConfig()
+	ccfg.Address = fmt.Sprintf("127.0.0.1:%d", s.Ports.ConsulHTTP)
+	ccfg.Datacenter = s.ClusterID
+	ccfg.Token = string(masterToken)
+	return api.NewClient(ccfg)
+}
+
+func (s *Setup) consulInit() error {
+	prog := path.Join(s.UsrDir, "bin", "consul")
+	keyPath := path.Join(s.DataDir, "encrypt-key.txt")
+	key, err := ioutil.ReadFile(keyPath)
+	if os.IsNotExist(err) {
+		key, err = exec.Command(prog, "keygen").CombinedOutput()
+		if err != nil {
+			return err
+		}
+		err = atomicWriteFile(keyPath, key, 0400)
+	}
+	if err != nil {
+		return err
+	}
+	s.encryptKey = strings.TrimSpace(string(key))
+
+	tokPath := path.Join(s.DataDir, "master-token.txt")
+	if tok, err := ioutil.ReadFile(tokPath); err != nil {
+		s.masterToken = generateToken()
+		err = atomicWriteFile(tokPath, []byte(s.masterToken), 0600)
+		if err != nil {
+			return err
+		}
+	} else {
+		s.masterToken = string(tok)
+	}
+	return nil
+}
+
+func (s *Setup) consulCheck() error {
+	consul, err := s.consulMaster()
+	if err != nil {
+		return err
+	}
+	_, err = consul.Catalog().Datacenters()
+	return err
+}
+
+// OnlyNode returns true if this is the only consul node.
+func (s *Setup) OnlyNode() (bool, error) {
+	c, err := s.consulMaster()
+	if err != nil {
+		return false, err
+	}
+	nodes, _, err := c.Catalog().Nodes(nil)
+	return len(nodes) == 1, err
+}
+
+func generateToken() string {
+	var r [16]byte
+	if _, err := rand.Read(r[:]); err != nil {
+		panic(err)
+	}
+	return fmt.Sprintf("%x", r)
+}
diff --git a/lib/setup/daemon.go b/lib/setup/daemon.go
new file mode 100644
index 0000000..3985259
--- /dev/null
+++ b/lib/setup/daemon.go
@@ -0,0 +1,75 @@
+package setup
+
+import (
+	"log"
+	"math/rand"
+	"os"
+
+	"github.com/hashicorp/consul/api"
+)
+
+type daemon struct {
+	name       string
+	prog       string // program to run (absolute path) -- if blank, use name
+	args       []string
+	noRegister bool
+}
+
+func (s *Setup) installService(d daemon) error {
+	if d.prog == "" {
+		d.prog = d.name
+	}
+	if _, err := os.Stat(d.prog); err != nil {
+		return err
+	}
+	sup := s.superviseDaemon(d)
+	if ok, err := sup.Running(); err != nil {
+		return err
+	} else if !ok {
+		if err := sup.Start(); err != nil {
+			return err
+		}
+	}
+	if d.noRegister {
+		return nil
+	}
+	consul, err := s.consulMaster()
+	if err != nil {
+		return err
+	}
+	agent := consul.Agent()
+	svcs, err := agent.Services()
+	if err != nil {
+		return err
+	}
+	if svc, ok := svcs[d.name]; ok {
+		log.Printf("%q is registered: %#v", d.name, svc)
+		return nil
+	}
+	return agent.ServiceRegister(&api.AgentServiceRegistration{
+		ID:   d.name,
+		Name: d.name,
+		Port: availablePort(),
+	})
+}
+
+type supervisor interface {
+	Running() (bool, error)
+	Start() error
+}
+
+func (s *Setup) superviseDaemon(d daemon) supervisor {
+	switch s.DaemonSupervisor {
+	case "runit":
+		return &runitService{daemon: d, etcsv: s.RunitSvDir}
+	case "systemd":
+		return &systemdSupervisor{daemon: d}
+	default:
+		log.Fatalf("unknown DaemonSupervisor %q", s.DaemonSupervisor)
+		return nil
+	}
+}
+
+func availablePort() int {
+	return rand.Intn(10000) + 20000
+}
diff --git a/lib/setup/download.go b/lib/setup/download.go
new file mode 100644
index 0000000..550f6cb
--- /dev/null
+++ b/lib/setup/download.go
@@ -0,0 +1,130 @@
+package setup
+
+import (
+	"archive/zip"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"path"
+	"strings"
+)
+
+type download struct {
+	URL        string
+	Dest       string
+	Size       int64
+	Mode       os.FileMode
+	Hash       string
+	PreloadDir string
+}
+
+func (d *download) install() error {
+	fi, err := os.Stat(d.Dest)
+	if os.IsNotExist(err) {
+		// fall through to fix
+	} else if err != nil {
+		return err
+	} else if d.Size > 0 && fi.Size() != d.Size {
+		err = fmt.Errorf("Size mismatch: %q is %d bytes, expected %d", d.Dest, fi.Size(), d.Size)
+	} else if d.Mode > 0 && fi.Mode() != d.Mode {
+		err = fmt.Errorf("Mode mismatch: %q is %s, expected %s", d.Dest, fi.Mode(), d.Mode)
+	} else {
+		return nil
+	}
+
+	out, err := ioutil.TempFile(path.Dir(d.Dest), path.Base(d.Dest))
+	if err != nil {
+		return err
+	}
+	defer func() {
+		if out != nil {
+			os.Remove(out.Name())
+			out.Close()
+		}
+	}()
+
+	var size int64
+	{
+		got := false
+		if d.PreloadDir != "" {
+			fn := path.Join(d.PreloadDir, path.Base(d.URL))
+			f, err := os.Open(fn)
+			defer f.Close()
+			if err == nil {
+				size, err = io.Copy(out, f)
+				if err != nil {
+					return err
+				}
+				got = true
+			}
+		}
+		if !got {
+			resp, err := http.Get(d.URL)
+			if err != nil {
+				return err
+			}
+			size, err = io.Copy(out, resp.Body)
+			resp.Body.Close()
+			if err != nil {
+				return err
+			}
+		}
+	}
+
+	if strings.HasSuffix(d.URL, ".zip") && !strings.HasSuffix(d.Dest, ".zip") {
+		r, err := zip.NewReader(out, size)
+		if err != nil {
+			return err
+		}
+		defer os.Remove(out.Name())
+		out = nil
+
+		found := false
+		for _, f := range r.File {
+			if !strings.HasSuffix(d.Dest, "/"+f.Name) {
+				continue
+			}
+			rc, err := f.Open()
+			if err != nil {
+				return err
+			}
+			defer rc.Close()
+
+			out, err = ioutil.TempFile(path.Dir(d.Dest), path.Base(d.Dest))
+			if err != nil {
+				return err
+			}
+
+			size, err = io.Copy(out, rc)
+			if err != nil {
+				return err
+			}
+			found = true
+			break
+		}
+		if !found {
+			return fmt.Errorf("File not found in archive")
+		}
+	}
+
+	if d.Size > 0 && d.Size != size {
+		return fmt.Errorf("Size mismatch: got %d bytes, expected %d", size, d.Size)
+	} else if d.Size == 0 {
+		log.Printf("%v: size was %d", d, size)
+	}
+	if err = out.Close(); err != nil {
+		return err
+	}
+	if err = os.Chmod(out.Name(), d.Mode); err != nil {
+		return err
+	}
+	err = os.Rename(out.Name(), d.Dest)
+	if err == nil {
+		// skip deferred os.Remove(out.Name())
+		out = nil
+	}
+	return err
+}
diff --git a/lib/setup/os_package.go b/lib/setup/os_package.go
new file mode 100644
index 0000000..106945b
--- /dev/null
+++ b/lib/setup/os_package.go
@@ -0,0 +1,57 @@
+package setup
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+)
+
+type osPackage struct {
+	Debian string
+	RedHat string
+}
+
+var (
+	osPackageMutex     sync.Mutex
+	osPackageDidUpdate bool
+)
+
+func (pkg *osPackage) install() error {
+	osPackageMutex.Lock()
+	defer osPackageMutex.Unlock()
+
+	if _, err := os.Stat("/var/lib/dpkg/info/" + pkg.Debian + ".list"); err == nil {
+		return nil
+	}
+	if !osPackageDidUpdate {
+		d, err := os.Open("/var/lib/apt/lists")
+		if err != nil {
+			return err
+		}
+		defer d.Close()
+		if files, err := d.Readdir(4); len(files) < 4 || err != nil {
+			err = pkg.aptGet("update")
+			if err != nil {
+				return err
+			}
+			osPackageDidUpdate = true
+		}
+	}
+	return pkg.aptGet("install", "-y", "--no-install-recommends", pkg.Debian)
+}
+
+func (*osPackage) aptGet(args ...string) error {
+	cmd := command("apt-get", args...)
+	for _, kv := range os.Environ() {
+		if !strings.HasPrefix(kv, "DEBIAN_FRONTEND=") {
+			cmd.Env = append(cmd.Env, kv)
+		}
+	}
+	cmd.Env = append(cmd.Env, "DEBIAN_FRONTEND=noninteractive")
+	err := cmd.Run()
+	if err != nil {
+		return fmt.Errorf("%s: %s", cmd.Args, err)
+	}
+	return nil
+}
diff --git a/lib/setup/runit.go b/lib/setup/runit.go
new file mode 100644
index 0000000..3e9f347
--- /dev/null
+++ b/lib/setup/runit.go
@@ -0,0 +1,41 @@
+package setup
+
+import (
+	"bytes"
+	"fmt"
+	"os"
+	"path"
+)
+
+func (s *Setup) installRunit() error {
+	if s.DaemonSupervisor != "runit" {
+		return nil
+	}
+	return (&osPackage{Debian: "runit"}).install()
+}
+
+type runitService struct {
+	daemon
+	etcsv string
+}
+
+func (r *runitService) Start() error {
+	script := &bytes.Buffer{}
+	fmt.Fprintf(script, "#!/bin/sh\n\nexec %q", r.prog)
+	for _, arg := range r.args {
+		fmt.Fprintf(script, " %q", arg)
+	}
+	fmt.Fprintf(script, " 2>&1\n")
+	return atomicWriteFile(path.Join(r.svdir(), "run"), script.Bytes(), 0755)
+}
+
+func (r *runitService) Running() (bool, error) {
+	if _, err := os.Stat(r.svdir()); err != nil && os.IsNotExist(err) {
+		return false, nil
+	}
+	return runStatusCmd("sv", "stat", r.svdir())
+}
+
+func (r *runitService) svdir() string {
+	return path.Join(r.etcsv, r.name)
+}
diff --git a/lib/setup/setup.go b/lib/setup/setup.go
new file mode 100644
index 0000000..379ccb2
--- /dev/null
+++ b/lib/setup/setup.go
@@ -0,0 +1,79 @@
+package setup
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"os"
+
+	"git.curoverse.com/arvados.git/lib/agent"
+	"git.curoverse.com/arvados.git/sdk/go/config"
+	vaultAPI "github.com/hashicorp/vault/api"
+)
+
+func Command() *Setup {
+	hostname, err := os.Hostname()
+	if err != nil {
+		log.Fatalf("hostname: %s", err)
+	}
+
+	return &Setup{
+		Agent:      agent.Command(),
+		LANHost:    hostname,
+		PreloadDir: "/var/cache/arvados",
+	}
+}
+
+type Setup struct {
+	*agent.Agent
+	InitVault  bool
+	LANHost    string
+	PreloadDir string
+
+	encryptKey  string
+	masterToken string
+	vaultCfg    *vaultAPI.Config
+}
+
+func (s *Setup) ParseFlags(args []string) error {
+	fs := flag.NewFlagSet("setup", flag.ContinueOnError)
+	fs.StringVar(&s.ClusterID, "cluster-id", s.ClusterID, "five-character cluster ID")
+	fs.BoolVar(&s.InitVault, "init-vault", s.InitVault, "initialize the vault if needed")
+	fs.BoolVar(&s.Unseal, "unseal", s.Unseal, "unseal the vault automatically")
+	return fs.Parse(args)
+}
+
+func (s *Setup) Run() error {
+	err := config.LoadFile(s, s.DefaultConfigFile())
+	if err != nil && !os.IsNotExist(err) {
+		return err
+	}
+	for _, f := range []func() error{
+		s.makeDirs,
+		(&osPackage{Debian: "ca-certificates"}).install,
+		(&osPackage{Debian: "nginx"}).install,
+		s.installRunit,
+		s.installConsul,
+		s.installVault,
+	} {
+		err := f()
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (s *Setup) makeDirs() error {
+	for _, path := range []string{s.DataDir, s.UsrDir, s.UsrDir + "/bin"} {
+		if fi, err := os.Stat(path); err != nil {
+			err = os.MkdirAll(path, 0755)
+			if err != nil {
+				return err
+			}
+		} else if !fi.IsDir() {
+			return fmt.Errorf("%s: is not a directory", path)
+		}
+	}
+	return nil
+}
diff --git a/lib/setup/systemd.go b/lib/setup/systemd.go
new file mode 100644
index 0000000..bc3502f
--- /dev/null
+++ b/lib/setup/systemd.go
@@ -0,0 +1,20 @@
+package setup
+
+import "fmt"
+
+type systemdSupervisor struct {
+	daemon
+}
+
+func (ss *systemdSupervisor) Start() error {
+	cmd := command("systemd-run", append([]string{"--unit=arvados-" + ss.name, ss.prog}, ss.args...)...)
+	err := cmd.Run()
+	if err != nil {
+		err = fmt.Errorf("systemd-run: %s", err)
+	}
+	return err
+}
+
+func (ss *systemdSupervisor) Running() (bool, error) {
+	return runStatusCmd("systemctl", "status", "arvados-"+ss.name)
+}
diff --git a/lib/setup/vault.go b/lib/setup/vault.go
new file mode 100644
index 0000000..8443764
--- /dev/null
+++ b/lib/setup/vault.go
@@ -0,0 +1,244 @@
+package setup
+
+import (
+	"encoding/base64"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"os"
+	"path"
+	"strings"
+	"time"
+
+	consulAPI "github.com/hashicorp/consul/api"
+	vaultAPI "github.com/hashicorp/vault/api"
+)
+
+func (s *Setup) installVault() error {
+	if err := s.consulInit(); err != nil {
+		return err
+	}
+	if err := s.vaultInit(); err != nil {
+		return err
+	}
+	if s.vaultCheck() == nil {
+		return nil
+	}
+
+	log.Printf("download & install vault")
+	bin := s.UsrDir + "/bin/vault"
+	err := (&download{
+		URL:        "https://releases.hashicorp.com/vault/0.6.4/vault_0.6.4_linux_amd64.zip",
+		Dest:       bin,
+		Size:       52518022,
+		Mode:       0755,
+		PreloadDir: s.PreloadDir,
+	}).install()
+	if err != nil {
+		return err
+	}
+
+	haAddr := fmt.Sprintf("http://%s:%d", s.LANHost, s.Ports.VaultServer)
+
+	cfgPath := path.Join(s.DataDir, "vault.hcl")
+	err = atomicWriteFile(cfgPath, []byte(fmt.Sprintf(`
+		cluster_name = %q
+		backend "consul" {
+			address = "127.0.0.1:%d"
+			redirect_addr = %q
+			cluster_addr = %q
+			path = %q
+			token = %q
+		}
+		listener "tcp" {
+			address = %q
+			tls_disable = 1
+		}
+		`,
+		s.ClusterID,
+		s.Ports.ConsulHTTP,
+		haAddr,
+		haAddr,
+		"vault-"+s.ClusterID+"/",
+		s.masterToken,
+		fmt.Sprintf("%s:%d", s.LANHost, s.Ports.VaultServer),
+	)), 0600)
+	if err != nil {
+		return err
+	}
+
+	args := []string{"server", "-config=" + cfgPath}
+	err = s.installService(daemon{
+		name:       "arvados-vault",
+		prog:       bin,
+		args:       args,
+		noRegister: true,
+	})
+	if err != nil {
+		return err
+	}
+
+	if !s.Unseal {
+		return nil
+	}
+
+	if err := s.vaultBootstrap(); err != nil {
+		return err
+	}
+	return waitCheck(30*time.Second, s.vaultCheck)
+}
+
+func (s *Setup) vaultBootstrap() error {
+	var vault *vaultAPI.Client
+	var initialized bool
+	resp := &vaultAPI.InitResponse{}
+	if err := waitCheck(time.Minute, func() error {
+		var err error
+		vault, err = s.vaultClient()
+		if err != nil {
+			return err
+		}
+		initialized, err = vault.Sys().InitStatus()
+		if err != nil {
+			return err
+		} else if s.InitVault {
+			return nil
+		}
+		_, err = os.Stat(path.Join(s.DataDir, "vault", "mgmt-token.txt"))
+		if err != nil {
+			log.Print("vault is not initialized, waiting")
+			return fmt.Errorf("vault is not initialized")
+		}
+		return nil
+	}); err != nil {
+		return err
+	} else if !initialized && s.InitVault {
+		resp, err = vault.Sys().Init(&vaultAPI.InitRequest{
+			SecretShares:    5,
+			SecretThreshold: 3,
+		})
+		if err != nil {
+			return fmt.Errorf("vault-init: %s", err)
+		}
+		atomicWriteJSON(path.Join(s.DataDir, "vault", "keys.json"), resp, 0400)
+		atomicWriteFile(path.Join(s.DataDir, "vault", "root-token.txt"), []byte(resp.RootToken), 0400)
+	} else {
+		j, err := ioutil.ReadFile(path.Join(s.DataDir, "vault", "keys.json"))
+		if err != nil {
+			return err
+		}
+		err = json.Unmarshal(j, resp)
+		if err != nil {
+			return err
+		}
+	}
+	vault.SetToken(resp.RootToken)
+
+	ok := false
+	for _, key := range resp.Keys {
+		resp, err := vault.Sys().Unseal(key)
+		if err != nil {
+			log.Printf("error: unseal: %s", err)
+			continue
+		}
+		if !resp.Sealed {
+			log.Printf("unseal successful")
+			ok = true
+			break
+		}
+	}
+	if !ok {
+		return fmt.Errorf("vault unseal failed!")
+	}
+
+	if s.InitVault {
+		// Use master token to create a management token
+		master, err := s.consulMaster()
+		if err != nil {
+			return err
+		}
+		mgmtToken, _, err := master.ACL().Create(&consulAPI.ACLEntry{Name: "vault", Type: "management"}, nil)
+		if err != nil {
+			return err
+		}
+
+		// Mount+configure consul backend
+		alreadyMounted := false
+		if err = waitCheck(30*time.Second, func() error {
+			// Typically this first fails "500 node not active but
+			// active node not found" but then succeeds.
+			err := vault.Sys().Mount("consul", &vaultAPI.MountInput{Type: "consul"})
+			if err != nil && strings.Contains(err.Error(), "existing mount at consul") {
+				alreadyMounted = true
+				err = nil
+			}
+			return err
+		}); err != nil {
+			return err
+		}
+		_, err = vault.Logical().Write("consul/config/access", map[string]interface{}{
+			"address": fmt.Sprintf("127.0.0.1:%d", s.Ports.ConsulHTTP),
+			"token":   string(mgmtToken),
+		})
+		if err != nil {
+			return err
+		}
+
+		// Create a role
+		_, err = vault.Logical().Write("consul/roles/write-all", map[string]interface{}{
+			"policy": base64.StdEncoding.EncodeToString([]byte(`key "" { policy = "write" }`)),
+		})
+		if err != nil {
+			return err
+		}
+
+		// Write mgmtToken after bootstrapping is done. If
+		// other nodes share our vault data dir, this is their
+		// signal to try unseal.
+		if err = atomicWriteFile(path.Join(s.DataDir, "vault", "mgmt-token.txt"), []byte(mgmtToken), 0400); err != nil {
+			return err
+		}
+	}
+
+	// Test: generate a new token with the write-all role
+	secret, err := vault.Logical().Read("consul/creds/write-all")
+	if err != nil {
+		return err
+	}
+	token, ok := secret.Data["token"].(string)
+	if !ok {
+		return fmt.Errorf("secret token broken?? %+v", secret)
+	}
+	log.Printf("Vault supplied token with lease duration %s (renewable=%v): %q", time.Duration(secret.LeaseDuration)*time.Second, secret.Renewable, token)
+
+	return nil
+}
+
+func (s *Setup) vaultInit() error {
+	s.vaultCfg = vaultAPI.DefaultConfig()
+	s.vaultCfg.Address = fmt.Sprintf("http://%s:%d", s.LANHost, s.Ports.VaultServer)
+	return nil
+}
+
+func (s *Setup) vaultClient() (*vaultAPI.Client, error) {
+	return vaultAPI.NewClient(s.vaultCfg)
+}
+
+func (s *Setup) vaultCheck() error {
+	vault, err := s.vaultClient()
+	if err != nil {
+		return err
+	}
+	token, err := ioutil.ReadFile(path.Join(s.DataDir, "vault", "root-token.txt"))
+	if err != nil {
+		return err
+	}
+	vault.SetToken(string(token))
+	if init, err := vault.Sys().InitStatus(); err != nil {
+		return err
+	} else if !init {
+		return fmt.Errorf("vault is not initialized")
+	}
+	return nil
+}
diff --git a/lib/setup/write_file.go b/lib/setup/write_file.go
new file mode 100644
index 0000000..b50079c
--- /dev/null
+++ b/lib/setup/write_file.go
@@ -0,0 +1,49 @@
+package setup
+
+import (
+	"encoding/json"
+	"io/ioutil"
+	"os"
+	"path"
+)
+
+func atomicWriteFile(name string, data []byte, mode os.FileMode) error {
+	if err := os.MkdirAll(path.Dir(name), 0755); err != nil {
+		return err
+	}
+	tmp, err := ioutil.TempFile(path.Dir(name), path.Base(name)+"~")
+	if err != nil {
+		return err
+	}
+	defer func() {
+		if tmp != nil {
+			os.Remove(tmp.Name())
+		}
+	}()
+	_, err = tmp.Write(data)
+	if err != nil {
+		return err
+	}
+	err = tmp.Close()
+	if err != nil {
+		return err
+	}
+	err = os.Chmod(tmp.Name(), mode)
+	if err != nil {
+		return err
+	}
+	err = os.Rename(tmp.Name(), name)
+	if err != nil {
+		return err
+	}
+	tmp = nil
+	return nil
+}
+
+func atomicWriteJSON(name string, data interface{}, mode os.FileMode) error {
+	j, err := json.MarshalIndent(data, "", "  ")
+	if err != nil {
+		return err
+	}
+	return atomicWriteFile(name, j, mode)
+}

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list