[ARVADOS] updated: cea3337f1d104150d3314e43bc1c07eef0851bc5

Git user git at public.curoverse.com
Sat Feb 25 19:33:37 EST 2017


Summary of changes:
 .../app/controllers/collections_controller.rb      |   8 +-
 apps/workbench/app/controllers/jobs_controller.rb  |   1 +
 .../controllers/pipeline_instances_controller.rb   |  10 +
 .../app/controllers/projects_controller.rb         |   7 +-
 apps/workbench/app/models/job.rb                   |   2 +-
 apps/workbench/app/models/job_work_unit.rb         |   4 +
 apps/workbench/app/models/pipeline_instance.rb     |   4 +
 apps/workbench/app/models/work_unit.rb             |   4 +
 .../workbench/app/views/jobs/_show_status.html.erb |  32 +-
 .../pipeline_instances/_show_tab_buttons.html.erb  |   5 +-
 .../app/views/work_units/_show_component.html.erb  |   7 +-
 apps/workbench/config/routes.rb                    |   1 +
 .../test/controllers/jobs_controller_test.rb       |  15 +
 apps/workbench/test/integration/work_units_test.rb |  32 +-
 build/build.list                                   |  34 +-
 build/package-build-dockerfiles/Makefile           |   8 +-
 build/package-build-dockerfiles/centos7/Dockerfile |   8 +-
 build/package-build-dockerfiles/debian8/Dockerfile |   8 +-
 .../ubuntu1204/Dockerfile                          |   8 +-
 .../ubuntu1404/Dockerfile                          |   8 +-
 .../{ubuntu1204 => ubuntu1604}/Dockerfile          |  17 +-
 .../{ubuntu1204 => ubuntu1604}/Dockerfile          |  11 +-
 .../ubuntu1604/etc-apt-preferences.d-arvados       |   3 +
 build/package-testing/deb-common-test-packages.sh  |   2 +-
 ...ages-debian7.sh => test-packages-ubuntu1604.sh} |   0
 build/run-build-packages-one-target.sh             |   5 +-
 build/run-build-packages.sh                        |   3 +
 build/run-tests.sh                                 |   5 +
 cmd/arvados-admin/.gitignore                       |   1 +
 cmd/arvados-admin/main.go                          |  26 ++
 cmd/arvados-admin/setup_debian8_test.go            |  42 +++
 cmd/arvados-admin/setup_docker_compose_test.go     |  23 ++
 cmd/arvados-admin/test-debian8/Dockerfile          |  14 +
 cmd/arvados-admin/test-docker-compose/agent.yml    |   4 +
 .../test-docker-compose/docker-compose.yml         |  35 ++
 .../test-docker-compose/encrypt-key.txt            |   1 +
 .../test-docker-compose/master-token.txt           |   1 +
 cmd/dispatch.go                                    |  59 ++++
 doc/_includes/_install_ruby_and_bundler.liquid     |   8 +-
 doc/_includes/_mount_types.liquid                  |  55 ++++
 doc/_includes/_navbar_top.liquid                   |   2 +-
 doc/api/methods.html.textile.liquid                |   2 +
 .../methods/container_requests.html.textile.liquid |   2 +-
 .../install-dispatch.html.textile.liquid           |  12 +
 doc/sdk/cli/reference.html.textile.liquid          |  13 +-
 doc/user/index.html.textile.liquid                 |   2 +-
 services/boot/config.go => lib/agent/agent.go      |  84 ++---
 lib/setup/check.go                                 |  12 +
 lib/setup/command.go                               |  26 ++
 lib/setup/consul.go                                | 153 +++++++++
 lib/setup/daemon.go                                |  75 +++++
 {services/boot => lib/setup}/download.go           |  62 ++--
 {services/boot => lib/setup}/os_package.go         |  17 +-
 lib/setup/runit.go                                 |  41 +++
 lib/setup/setup.go                                 |  65 ++++
 lib/setup/systemd.go                               |  20 ++
 {services/boot => lib/setup}/write_file.go         |   5 +-
 sdk/cli/arvados-cli.gemspec                        |   4 +-
 sdk/cli/bin/crunch-job                             |   9 +
 sdk/cwl/arvados_cwl/__init__.py                    |  94 ++++--
 sdk/cwl/arvados_cwl/arvcontainer.py                | 101 ++++--
 sdk/cwl/arvados_cwl/arvdocker.py                   |   2 +
 sdk/cwl/arvados_cwl/arvjob.py                      |  46 ++-
 sdk/cwl/arvados_cwl/arvworkflow.py                 |  57 ++--
 sdk/cwl/arvados_cwl/done.py                        |  35 +-
 sdk/cwl/arvados_cwl/fsaccess.py                    |  40 ++-
 sdk/cwl/arvados_cwl/pathmapper.py                  |  12 +-
 sdk/cwl/arvados_cwl/runner.py                      | 180 ++++++++---
 sdk/cwl/setup.py                                   |   4 +-
 sdk/cwl/test_with_arvbox.sh                        |   2 +
 sdk/cwl/tests/test_container.py                    | 125 +++++++-
 sdk/cwl/tests/test_job.py                          |  17 +-
 sdk/cwl/tests/test_submit.py                       | 248 ++++++++------
 sdk/cwl/tests/wf/expect_arvworkflow.cwl            |  22 +-
 sdk/cwl/tests/wf/expect_packed.cwl                 |   4 +-
 sdk/cwl/tests/wf/scatter2_subwf.cwl                | 116 ++++---
 .../wf/{expect_packed.cwl => submit_wf_packed.cwl} |   8 +-
 sdk/go/arvados/collection.go                       |   5 +-
 sdk/go/arvados/container.go                        |  15 +-
 sdk/go/arvados/resource_list.go                    |   2 +
 sdk/go/config/dump.go                              |  27 ++
 sdk/go/dispatch/dispatch.go                        | 356 ++++++++++-----------
 sdk/go/dispatch/throttle.go                        |  58 ++++
 sdk/go/dispatch/throttle_test.go                   |  38 +++
 sdk/go/keepclient/block_cache.go                   | 104 ++++++
 sdk/go/keepclient/collectionreader.go              | 321 +++++++------------
 sdk/go/keepclient/collectionreader_test.go         | 108 +++++--
 sdk/go/keepclient/keepclient.go                    |   9 +
 sdk/go/manifest/manifest.go                        | 341 +++++++++++++++++---
 sdk/go/manifest/manifest_test.go                   | 126 +++++++-
 sdk/go/manifest/testdata/long_manifest             |   8 +-
 sdk/go/manifest/testdata/short_manifest            |   7 +-
 sdk/python/arvados/arvfile.py                      |   7 +-
 sdk/python/arvados/collection.py                   |  12 +-
 sdk/python/arvados/commands/keepdocker.py          | 103 ++++++
 sdk/python/arvados/commands/put.py                 |  60 ++--
 sdk/python/bin/arv-migrate-docker19                |   4 +
 sdk/python/setup.py                                |   1 +
 sdk/python/tests/test_arv_keepdocker.py            |  97 ++++++
 sdk/python/tests/test_arvfile.py                   |  24 +-
 sdk/ruby/arvados.gemspec                           |   4 +-
 .../api/app/controllers/application_controller.rb  |  15 +-
 .../app/controllers/arvados/v1/jobs_controller.rb  |   2 +-
 .../arvados/v1/pipeline_instances_controller.rb    |   6 +
 .../controllers/arvados/v1/schema_controller.rb    |  11 +-
 services/api/app/models/collection.rb              |  18 ++
 services/api/app/models/container_request.rb       |   2 +-
 services/api/app/models/job.rb                     |  51 ++-
 services/api/app/models/node.rb                    |  28 +-
 services/api/app/models/pipeline_instance.rb       |  27 ++
 services/api/config/application.default.yml        |  11 +
 .../api/config/initializers/noop_deep_munge.rb     |   9 +
 services/api/config/routes.rb                      |   4 +-
 services/api/test/fixtures/collections.yml         |   2 +-
 services/api/test/fixtures/jobs.yml                | 175 ++++++++++
 services/api/test/fixtures/links.yml               |  56 ++++
 services/api/test/fixtures/pipeline_instances.yml  |  39 +++
 .../v1/pipeline_instances_controller_test.rb       |  20 ++
 .../api/test/functional/arvados/v1/query_test.rb   |  29 ++
 .../api/test/helpers/docker_migration_helper.rb    |  13 +
 services/api/test/integration/noop_deep_munge.rb   |  35 ++
 services/api/test/unit/container_request_test.rb   |  19 ++
 services/api/test/unit/job_test.rb                 | 103 ++++++
 services/api/test/unit/node_test.rb                |  27 ++
 services/arv-git-httpd/main.go                     |   5 +
 services/boot/.gitignore                           |   1 +
 services/boot/arvados_go.go                        |   1 -
 services/boot/arvados_packages.go                  |   8 +-
 services/boot/check.go                             |  16 +
 services/boot/config.go                            |  11 +
 services/boot/consul.go                            |  81 +++--
 services/boot/controller.go                        |  22 +-
 services/boot/debian8_test.go                      |  35 ++
 services/boot/download.go                          |   2 +-
 services/boot/gateway.go                           | 166 ++++++++++
 services/boot/main.go                              |  21 +-
 services/boot/nomad.go                             | 107 +++++++
 services/boot/package.json                         |   2 +-
 services/boot/postgresql.go                        |  30 ++
 services/boot/runit.go                             |  31 +-
 services/boot/supervisor.go                        |  10 +-
 services/boot/testimage_runit/Dockerfile           |  14 +-
 services/boot/vault.go                             | 100 +++++-
 services/boot/webpack.config.js                    |   2 +-
 services/boot/write_file.go                        |   3 +
 .../crunch-dispatch-local/crunch-dispatch-local.go |  34 +-
 .../crunch-dispatch-local_test.go                  |  56 ++--
 .../crunch-dispatch-slurm/crunch-dispatch-slurm.go | 285 +++++++----------
 .../crunch-dispatch-slurm_test.go                  |  93 +++---
 services/crunch-dispatch-slurm/squeue.go           | 164 ++++------
 services/crunch-run/crunchrun.go                   | 118 ++++++-
 services/crunch-run/crunchrun_test.go              | 283 ++++++++++++++--
 services/keep-balance/balance_run_test.go          |   4 +-
 services/keep-balance/collection.go                |  75 +++--
 services/keep-balance/collection_test.go           |  57 ++++
 services/keep-balance/main.go                      |  19 +-
 services/keep-web/handler.go                       |  52 +--
 services/keep-web/handler_test.go                  |  43 ---
 services/keep-web/main.go                          |   6 +
 services/keep-web/ranges_test.go                   |  90 ++++++
 services/keepproxy/keepproxy.go                    |  11 +-
 services/keepstore/keepstore.go                    |   8 +-
 .../arvnodeman/computenode/dispatch/__init__.py    |  40 +--
 .../arvnodeman/computenode/dispatch/slurm.py       |   8 +-
 services/nodemanager/arvnodeman/daemon.py          |  46 +--
 .../nodemanager/tests/test_computenode_dispatch.py |  29 +-
 .../tests/test_computenode_dispatch_slurm.py       |  10 +
 .../tests/test_computenode_driver_azure.py         |   4 +-
 services/nodemanager/tests/test_daemon.py          |  57 ++--
 services/nodemanager/tests/test_failure.py         |   4 +-
 services/nodemanager/tests/testutil.py             |   4 +-
 services/ws/event_source.go                        |  94 ++++--
 services/ws/main.go                                |   8 +-
 tools/arvbox/lib/arvbox/docker/Dockerfile.base     |  23 +-
 tools/arvbox/lib/arvbox/docker/api-setup.sh        |   4 +-
 tools/arvbox/lib/arvbox/docker/createusers.sh      |  15 +-
 176 files changed, 5398 insertions(+), 1793 deletions(-)
 copy build/package-build-dockerfiles/{ubuntu1204 => ubuntu1604}/Dockerfile (54%)
 copy build/package-test-dockerfiles/{ubuntu1204 => ubuntu1604}/Dockerfile (55%)
 create mode 100644 build/package-test-dockerfiles/ubuntu1604/etc-apt-preferences.d-arvados
 rename build/package-testing/{test-packages-debian7.sh => test-packages-ubuntu1604.sh} (100%)
 create mode 100644 cmd/arvados-admin/.gitignore
 create mode 100644 cmd/arvados-admin/main.go
 create mode 100644 cmd/arvados-admin/setup_debian8_test.go
 create mode 100644 cmd/arvados-admin/setup_docker_compose_test.go
 create mode 100644 cmd/arvados-admin/test-debian8/Dockerfile
 create mode 100644 cmd/arvados-admin/test-docker-compose/agent.yml
 create mode 100644 cmd/arvados-admin/test-docker-compose/docker-compose.yml
 create mode 100644 cmd/arvados-admin/test-docker-compose/encrypt-key.txt
 create mode 100644 cmd/arvados-admin/test-docker-compose/master-token.txt
 create mode 100644 cmd/dispatch.go
 copy services/boot/config.go => lib/agent/agent.go (55%)
 create mode 100644 lib/setup/check.go
 create mode 100644 lib/setup/command.go
 create mode 100644 lib/setup/consul.go
 create mode 100644 lib/setup/daemon.go
 copy {services/boot => lib/setup}/download.go (67%)
 copy {services/boot => lib/setup}/os_package.go (80%)
 create mode 100644 lib/setup/runit.go
 create mode 100644 lib/setup/setup.go
 create mode 100644 lib/setup/systemd.go
 copy {services/boot => lib/setup}/write_file.go (89%)
 copy sdk/cwl/tests/wf/{expect_packed.cwl => submit_wf_packed.cwl} (53%)
 create mode 100644 sdk/go/config/dump.go
 create mode 100644 sdk/go/dispatch/throttle.go
 create mode 100644 sdk/go/dispatch/throttle_test.go
 create mode 100644 sdk/go/keepclient/block_cache.go
 create mode 100755 sdk/python/bin/arv-migrate-docker19
 create mode 100644 services/api/config/initializers/noop_deep_munge.rb
 create mode 100644 services/api/test/helpers/docker_migration_helper.rb
 create mode 100644 services/api/test/integration/noop_deep_munge.rb
 create mode 100644 services/boot/check.go
 create mode 100644 services/boot/debian8_test.go
 create mode 100644 services/boot/gateway.go
 create mode 100644 services/boot/nomad.go
 create mode 100644 services/boot/postgresql.go
 create mode 100644 services/keep-balance/collection_test.go
 create mode 100644 services/keep-web/ranges_test.go

       via  cea3337f1d104150d3314e43bc1c07eef0851bc5 (commit)
       via  379726401a04d87bdb9ef795af321ec0791a41ec (commit)
       via  fc33deaecf2f7e4c9b3cd8d2e1f5b56901794042 (commit)
       via  016a995d5a65f9de66c505fe80ab49eebc1a11c4 (commit)
       via  aaa536943dde5b3f1ec59c1bd18929b53dfea308 (commit)
       via  5624fec61db977d386ce03ca333241c74ca251b5 (commit)
       via  4edf67aee4e8d4b351ee08bcb19c051a9bddeacd (commit)
       via  31d76600cdb691251d0823cc6be601d958b4e1a4 (commit)
       via  03306e210c6835c6de1c908d5afe02ba964e7dbc (commit)
       via  6873eca0bf64a2a94ea7536bbc9a5a3788695136 (commit)
       via  6b775ec45db0143c0d476cf2f0fcfb8bdd39a845 (commit)
       via  e86743ca1a4e1431d6d8417083ef1a56461b45e9 (commit)
       via  bee301067b50d0cc00bbc1dc0a0b5c036d747a74 (commit)
       via  f782a2505422ad9c853c4c416640c41f3b1e7e79 (commit)
       via  3f30342290d901408a3b9a640a21e41f011041cd (commit)
       via  c90f6525e949c2314ccc9c2cb980bccbc23e2434 (commit)
       via  6e8c06b81867a3d54e89694f1301ab037d3f5d22 (commit)
       via  72b6a2992c69f557f3b6b850914c5ee914cf862b (commit)
       via  38bf86b7d9e256aaae50870174ae2cf6a84e8656 (commit)
       via  c86cbaa6f286e50900dae3203a42044449e042f7 (commit)
       via  0030aceea911108449d67a785d802e894943b340 (commit)
       via  70f47bfdccdbdcc773225a94015bba2d2975d7f7 (commit)
       via  7c667d8963c7a3cf9acd04c1d938b5273b761228 (commit)
       via  5763c67176e8e34656cd96881074777b14b2dc4a (commit)
       via  53b24cf4fafa5c8ce76eb4abf5e61de944bb4a6a (commit)
       via  232a69cedf3024380546b9e67ad10d6beb357649 (commit)
       via  10b3fe2ae3a37ee473684177aa6e4e9f090a230e (commit)
       via  90982bd47200f6a555074842c817ac2893c2e391 (commit)
       via  01779cbbd49870dbe12478713bf6cd3332d08144 (commit)
       via  2a469c4874895b05ee137e2382fd882680b3feb2 (commit)
       via  091c92aef16f9657cf7b9eb8f8778aafa33f12c1 (commit)
       via  15907692a7a9213ef99a74e5ef1c48a487a8ed8b (commit)
       via  5eace5e8b255fdc7f65081429d0b42e551479e95 (commit)
       via  07f620f078c6f75cf52e9ae65040857ea1980fdc (commit)
       via  dde508ae4d3f9542a16c5ebbe90de534cbdaaabb (commit)
       via  816f3bfd2b6a562fc712b7c2b3eb30157a95cb7e (commit)
       via  e2f3f8e7347a8ded697d3c0127f82325d672e7ab (commit)
       via  08c06bc813d96634b362e8c5736341fb2d874f59 (commit)
       via  d6aab18f9688d46bc1f86f021d68e02e5601cfe7 (commit)
       via  5dfc48c1a1697d9c29eac0061a491e96c9b72441 (commit)
       via  dc82c1c16a9b18e094e60af2c3973e034d5e2068 (commit)
       via  d3719c6ba29daec66eb1dd4003f2580f97da8e41 (commit)
       via  35ae0e8d87dde84dd3a0ab41cef9567ab93b042a (commit)
       via  2e9cb970e94c1ba259c6365ea4b456ca87e85cee (commit)
       via  fbc6c71ef67461cde5ba914e61f3f3b8740a4045 (commit)
       via  397a747639fe360c18f3aa79846fef530b263184 (commit)
       via  a2fe6f9367de3ee93064fbee3f2df78ce84aa318 (commit)
       via  9333c9f65503d86c12776e0bc8bfcb6fc07dc79c (commit)
       via  cff6cd57cbbaa032602ca2fb930c78812c5824dc (commit)
       via  6eded76b722df150ce59a1bddd2fb7e5175a596d (commit)
       via  d6cc6f748c79e9bbd0ba18dd8d32ae210a7b897b (commit)
       via  ef9bbef07f63dd2207e1336e85884aa90434ad29 (commit)
       via  ad464d416de6996a41d0c752124e0d201de0d3fc (commit)
       via  80689aac71f6e3e9b103f0f6b668bd173a76554f (commit)
       via  4ee34d218487d8b330147b185a2c9bcea8d68b09 (commit)
       via  6b1bf77727379c7ffaf1620399c37ea0106a0909 (commit)
       via  ddee3839f8a82b889f84171e2354108cb20f93e0 (commit)
       via  78b94fd03e3c0a2d225d0ab8882ee62dee9f8a11 (commit)
       via  d95e6df5184ab2ec137c8098b47caf9ebcf4e7d9 (commit)
       via  7b4c1b70b7d1ef4c977c57c89c4dc0f479e7e5ae (commit)
       via  4fa9baf6590e63975b6053a049543590db0e2527 (commit)
       via  4c081dd00f65f1e5a8e0cea34276d60ecbb49f40 (commit)
       via  2b297df85b61ac7f2ded512eca7c307d75b1cd8e (commit)
       via  a0195a506af77771434302b96a5e05ecfd3814d8 (commit)
       via  85c684122fd678dc24035d58236dd5734aed50e5 (commit)
       via  1c434a9f9ec70d8f23583bc737a516c3ef0eb91d (commit)
       via  310f7b0266f012355e38f3296bc6defe14a3d25f (commit)
       via  15b49783aa0ac76508986e772b98ffa9d187c57f (commit)
       via  5bf9312a5174f97f00db383836eb7666dc500293 (commit)
       via  2b7834020290b28d797333f90fcb87e5da67d616 (commit)
       via  4fd89ed7b10f0860a6030c25e44d4df45a087b2e (commit)
       via  8ee2c7c4f231a55601fdc90b087e42985b52fb20 (commit)
       via  d5effac093938be2b57d579cc1fecfa90310af02 (commit)
       via  0fc6eaead0bf7691e99d19e74ec33636909001a7 (commit)
       via  8e7c3b36efef6ef6590f4359eb48bf98826d71d4 (commit)
       via  4c1ca6b3e4cadc74df738882d44019c623869329 (commit)
       via  210af1cc7ede88914026fde078e45ef84c187a0c (commit)
       via  bf0945d1136d2578e40909d54e8614085d6f9c34 (commit)
       via  ca56623679bcf5733a3266711f513f8c23f8b0df (commit)
       via  4dbaf8c355f3743bd42ff5b917eda57d3e90abf8 (commit)
       via  108467e2b85c9f3c44b483bc79f68acf6ae3b963 (commit)
       via  ab6a70e86dd041f3b4da167c59e3e91309f14365 (commit)
       via  3f8336875d5938afb6b00289e9d6c9941456b57f (commit)
       via  61476528523e4ee2ebe199807b8aeba1e17074d4 (commit)
       via  668b2ae22b910765d4e7982a9d9b8a82307e4360 (commit)
       via  ef934e66fee76dcf760dd5fc835f27127e4cc791 (commit)
       via  61f0eb808bba87639e5e4b068f444b7de0526bb2 (commit)
       via  126f499d2f084963a5ef5df7cf567a1ed4cb96ed (commit)
       via  674236324476c58ccf20d554acd4427dfb6b9873 (commit)
       via  87107582273d254e98a66a3036bc0fc487edfa68 (commit)
       via  8734a7391a5672eebcdf572d93bae1b3ed1179c9 (commit)
       via  8ee173e80b4954159490c8f9327dab07a4bffcf3 (commit)
       via  837c509a55f56fa653454e7b99e293f8f87ebef2 (commit)
       via  8adad21150d3678aad0f88e5fb30a088145478ee (commit)
       via  aaa45b09de0e9437743fce53d7c0bf8165074b5e (commit)
       via  efee0049baf53efa3be10804abc9bc292f2a60ed (commit)
       via  925a1526383299a1ade38a18e616564ab8c38da4 (commit)
       via  785c967e74a7dab0b29b276162d1d7513ce1cf6b (commit)
       via  ef328d9143c65c72c37194272f90fabc3ec45e0a (commit)
       via  74a9decf66adc216d3d8bbb7ee8f6e3704d9590d (commit)
       via  51f5a4ede44615be700eec7fcbfe3c1e60c842ac (commit)
       via  211884f495248bbaf2ab74ff8d5ae2ed54b97bf2 (commit)
       via  a8b210d2bb5b10e6583abf295b99788b3dff7479 (commit)
       via  e41dc06c9115d0ce30207560b83d6c8dec6fd18a (commit)
       via  8a882eff8b8359d58b56f1bc7cd2da95775d97eb (commit)
       via  471270bc6ec9453fdd4c1faf97b65a8291543c6f (commit)
       via  6a7d7a2fa8e217e1ff9440769f39a2095d5bb837 (commit)
       via  d02bf4d817e50c3c0ee9f5e2dd901c512ea30943 (commit)
       via  9c9b52038aa8b9c15f02567d186539fd8794d0f2 (commit)
       via  519ab4c83aeb44ce91da941ecc191d00b6c6c72b (commit)
       via  1bba7f8fb361186ad040b521d168a73abd8fdd65 (commit)
       via  04a8e2c7d8a8dd3afd891292415cdfaea25fd481 (commit)
       via  e6986ed88126f61ecad0f557c78981961f901044 (commit)
       via  f927cd24852edcd7a8389533e96a16cf6877e58f (commit)
       via  7d29578411f1940ef5cc5ce985766dd81d133aaa (commit)
       via  c3d4f8a585202ec58df5506934b698039c200b68 (commit)
       via  36360d1f2987cea89ce217a2519c01da7456d533 (commit)
       via  3860042ac5783cddd368d8997991b0d1f8e9a111 (commit)
       via  7a53cfc92d4bca452a687db0a6f338e6deb1564a (commit)
       via  5caa73a3bf5a17241bf908c75aa2c3aa427289d1 (commit)
       via  a19ffad966b25b3869e666f749f7c6da187bef68 (commit)
       via  1bc602ad5480b9b1ed78b318e9d3d9749d2b83ab (commit)
       via  e502060ffe4f68d33e2cca8f8d7544ce40d53eb7 (commit)
       via  dcf5452fb14c7025feff04b3003ff68c9f4ec630 (commit)
       via  4f485c1318130c61d81f6553460d5eb063f100cb (commit)
       via  da42861619eb478cd1f01d58a1ebe59f1a25002e (commit)
       via  dc7d01f4d4031962ffd5734ca0c64146a7217e4a (commit)
       via  a5e7c0c8bf829a41c4d02b48d5a9a40d6d225c8a (commit)
       via  ecd8381224f7883db0504eb338d7f1c55fc4349f (commit)
       via  edd5811cd9627cd1c734d9c5b1e863b3d94f746a (commit)
       via  280621308e41e9cc8b8557d27738f186e795780b (commit)
       via  804d23f38fb98ec648349703f74a229f225ee22a (commit)
       via  49aa7305a80547bc7fb8c32f1073d7add2506ece (commit)
       via  65123c5a66fe155d6dad2cee3a1e0b90f7b7f3f2 (commit)
       via  aa05b5d5ee2e42f2e456cca0c2f3cd835ab7cef4 (commit)
       via  fdecbc701188e64dc9e539da2dddbbeded709b42 (commit)
       via  28aea3c6b1888c00a041992282c95fe595f85005 (commit)
       via  436f5c768dbc97135490b6477efd1ff0482a9dda (commit)
       via  71229e23918b698caa7c6c8b62b368d4aef2ab85 (commit)
       via  d6579d95bacbba5682d0c085f139681c93cb005a (commit)
       via  827879be023e90d58eb681b3c930154739a0b27f (commit)
       via  24b137a5b3313778e2db7f5d1e0c82daf0634a9c (commit)
       via  b001425779a0189b357c4e3a47734eda6b77ce2d (commit)
       via  2b37f7a8e843638431dc9e8031c1c2c2a81c1e7e (commit)
       via  1b8106da867aafbe6e4653485d5e644faee4c35d (commit)
       via  dcb7f718a7bf31c6e1227d28cdaac64fa769015b (commit)
       via  95d8984418a5198639000f0a622da74af259b6c5 (commit)
       via  8675eec8206c0e39753741864636c8a05ce02408 (commit)
       via  799b5333c66ccd18fe1b0ee97c2a28be6e07b731 (commit)
       via  9dd66f4d896785e0c6dce839ed1b411dd3c77bf0 (commit)
       via  e34a5060cfc1cc4821b431e8aa6778a31898e0eb (commit)
       via  ae5eb12d3d9ab298a4c36412b4a4d83272574d25 (commit)
       via  7dd717ca530fccae814c8c59a8c242d376fa0cef (commit)
       via  683f5374b0fc516579c1d6dc3379fc900d642322 (commit)
       via  318dd887d108e0664ef22a8d38a34fa18e1b2657 (commit)
       via  a7222f4df954c9ac973d58bb2f27a8f049dbbbc2 (commit)
       via  a3ca6693f522b59ea50d76cadde69b91c0a62cad (commit)
       via  13c27ef91fc2516bdcdee94ba1ff2f08361dc51d (commit)
       via  e356309e05714cd65d88456c563cea606f820394 (commit)
       via  5ba9471d651c9f6235988d3ec680461ba4785453 (commit)
       via  0d7ceb1e3498210d20859516de4ffdd59530b6d7 (commit)
       via  da0aee751d8cb039c9b6b85a03e7d62cb973e3b3 (commit)
       via  a20c77028c182b684a3acb791c7de4183319737e (commit)
       via  1edd524ecd7b6776dd9b20ab81030e7c69c392b4 (commit)
       via  b172bbd9e4c780d9af79774aee81a1ac48b3a857 (commit)
       via  8e569c16ba035b131c148441ca5a590fb49811ac (commit)
       via  38ac8f3e065e74f6af172cee90144701d2f73b58 (commit)
       via  1e7d93d8dcc2d896a71c38910f4fb6ef35408c0d (commit)
       via  7fd60cc870863647127a438a085685d415c37a46 (commit)
       via  aea835bc965d42e225c2641b0210c4b521f6dc4e (commit)
       via  16fe80b0e93ed8c8416b2dcbc0e2ad49bc850738 (commit)
       via  b8de9b3e62e82b806576b237be5f317bf378169f (commit)
       via  39189b90bd2f73d4f4938049ffa4441a967ca24c (commit)
       via  dc5a33cbfc156c839515acb4fa6ea2f9162a0972 (commit)
       via  cd383b7168d9412f4f097438d590e919ff7a97d6 (commit)
       via  ddcaafee5dc10ee0104c108c9648f4d5024a83d9 (commit)
       via  685af7fb2ae3a8ea162edd89eec61fdd4ca376f0 (commit)
       via  ae9f71231ed50eb35097c10c84b0070bcdcd22cf (commit)
       via  01007e0a2c7cf5461ced83339f6abcfb6f9fac72 (commit)
       via  6143bba5421756c78b282ee6c4da793d45a4523e (commit)
       via  036c59ea6b19372e74f7ccacb5dcb2f522f99629 (commit)
       via  7fa95f2db716ebfdb6312fa67b9b07bebb815b39 (commit)
       via  f40364c4d42e111b9da3873afcfaed2b49e7f182 (commit)
       via  2f953026bc4baeccb78ca82acc4d07cad37625b8 (commit)
       via  60d986b8908487c086eb4e402ac69669cb26108b (commit)
       via  c40389e0f064d4ea379d5f5471116936239a467a (commit)
       via  b6e15a581be2e5b2387fe18ecb01714fbc21d3aa (commit)
       via  80f042bec0b25966498324cb6aafb7fc24a36e12 (commit)
       via  8e9a3e39375b4dd689cd85a92e77a5eee03b4908 (commit)
       via  6b91e8201f24d2b4126532d809abba42a5ab959c (commit)
       via  fbacdaec3dbba425155ab6348c7e6b80ff4e710b (commit)
       via  edd77406c5a428af8780ed4dffd512adcc70bc0f (commit)
       via  1cdb5cbf83d34cb1a19c78478a7d5a28738e7bbf (commit)
       via  e8521124b0b8e647c58e99905100171ea5e30abd (commit)
       via  b07d8a573c74e7be56a2fd8c5064032c37d90c67 (commit)
       via  de4ecbc700759ff22e76948a58f7d70e5d3c1464 (commit)
       via  e1fd558686c78d6edfd460b7531ec9b559299889 (commit)
       via  4696633ae5e955c267d50a14e790c93d833f41b0 (commit)
       via  a9ae80084e5b8897408a47fe081481cb913fd260 (commit)
       via  609646134bcd8fc3a7fd500848220741ecc4a9d2 (commit)
       via  49d7a1fbd42359aa0948efd0968349eaf854d6cb (commit)
       via  9cc8eb72a4d8dddd2ffe2014d085517ecd0f6a67 (commit)
       via  89ce36332958f698bc571eca770fb94e579957cd (commit)
       via  d2942208ef4bc31bc965d5f72f254b8d39a1bc9b (commit)
       via  4032e4dc4284391f21f21630bdb7ca88f8329d8e (commit)
       via  0d0092442c009b8b3e387da18f828f3f0518b709 (commit)
       via  a4edc2a4a740d3908b907682a85e227eee80683b (commit)
       via  f65530a60d7b74da723d16eccd84576c53575401 (commit)
       via  be141b88fe871c7e72b19a5cf1e0aa56bf28283a (commit)
       via  43dcf83e8b5332004bddd01be22476e68b1cef0f (commit)
       via  bb037e3872214d2b709964405775f476c6b3b550 (commit)
      from  658183e2bca17f5aa281ab95068906c7150b2b73 (commit)

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 cea3337f1d104150d3314e43bc1c07eef0851bc5
Author: Tom Clegg <tom at curoverse.com>
Date:   Sat Feb 25 19:32:45 2017 -0500

    multiple servers, add docker-compose test

diff --git a/build/run-tests.sh b/build/run-tests.sh
index aafbead..3cbc6b3 100755
--- a/build/run-tests.sh
+++ b/build/run-tests.sh
@@ -196,6 +196,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() {
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..d949604
--- /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"},
+		{"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-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..66378e5
--- /dev/null
+++ b/cmd/arvados-admin/test-docker-compose/docker-compose.yml
@@ -0,0 +1,35 @@
+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
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup -unseal=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
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup && 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
+    command: ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup && wait"]
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/lib/setup/consul.go b/lib/setup/consul.go
index 0ebff83..b1682b2 100644
--- a/lib/setup/consul.go
+++ b/lib/setup/consul.go
@@ -16,9 +16,9 @@ import (
 func (s *Setup) installConsul() error {
 	prog := s.UsrDir + "/bin/consul"
 	err := (&download{
-		URL:        "https://releases.hashicorp.com/consul/0.7.4/consul_0.7.4_linux_amd64.zip",
+		URL:        "https://releases.hashicorp.com/consul/0.7.5/consul_0.7.5_linux_amd64.zip",
 		Dest:       prog,
-		Size:       36003597,
+		Size:       36003713,
 		Mode:       0755,
 		PreloadDir: s.PreloadDir,
 	}).install()
@@ -29,43 +29,45 @@ func (s *Setup) installConsul() error {
 	if err := os.MkdirAll(dataDir, 0700); err != nil {
 		return err
 	}
-	args := []string{"agent"}
-	{
-		cf := path.Join(s.DataDir, "consul-encrypt.json")
-		if _, err := os.Stat(cf); err != nil && !os.IsNotExist(err) {
+
+	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
-		} else if err != nil {
-			key, err := exec.Command(prog, "keygen").CombinedOutput()
-			if err != nil {
-				return err
-			}
-			if err = atomicWriteJSON(cf, map[string]interface{}{
-				"encrypt": strings.TrimSpace(string(key)),
-			}, 0400); err != nil {
-				return err
-			}
 		}
-		args = append(args, "-config-file="+cf)
+		err = atomicWriteFile(keyPath, key, 0400)
 	}
-	{
+	if err != nil {
+		return err
+	}
+	encryptKey := strings.TrimSpace(string(key))
+
+	tokPath := path.Join(s.DataDir, "master-token.txt")
+	if tok, err := ioutil.ReadFile(tokPath); err != nil {
 		s.masterToken = generateToken()
-		// os.Setenv("CONSUL_TOKEN", s.masterToken)
-		err = atomicWriteFile(path.Join(s.DataDir, "master-token.txt"), []byte(s.masterToken), 0600)
+		err = atomicWriteFile(tokPath, []byte(s.masterToken), 0600)
 		if err != nil {
 			return err
 		}
-		cf := path.Join(s.DataDir, "consul-config.json")
-		err = atomicWriteJSON(cf, map[string]interface{}{
-			"acl_datacenter":        s.ClusterID,
-			"acl_default_policy":    "deny",
-			"acl_enforce_version_8": true,
-			"acl_master_token":      s.masterToken,
-			"client_addr":           "0.0.0.0",
-			"bootstrap_expect":      len(s.ControlHosts),
-			"data_dir":              dataDir,
-			"datacenter":            s.ClusterID,
-			"server":                true,
-			"ui":                    true,
+	} else {
+		s.masterToken = string(tok)
+	}
+
+	cf := path.Join(s.DataDir, "consul-config.json")
+	{
+		c := map[string]interface{}{
+			"acl_datacenter":     s.ClusterID,
+			"acl_default_policy": "deny",
+			"acl_master_token":   s.masterToken,
+			"bootstrap_expect":   len(s.ControlHosts),
+			"client_addr":        "0.0.0.0",
+			"data_dir":           dataDir,
+			"datacenter":         s.ClusterID,
+			"encrypt":            encryptKey,
+			"server":             true,
+			"ui":                 true,
 			"ports": map[string]int{
 				"dns":      s.Ports.ConsulDNS,
 				"http":     s.Ports.ConsulHTTP,
@@ -75,23 +77,30 @@ func (s *Setup) installConsul() error {
 				"serf_wan": s.Ports.ConsulSerfWAN,
 				"server":   s.Ports.ConsulServer,
 			},
-		}, 0644)
+		}
+		err = atomicWriteJSON(cf, c, 0600)
 		if err != nil {
 			return err
 		}
-		args = append(args, "-config-file="+cf)
 	}
+
 	err = s.installService(daemon{
 		name:       "arvados-consul",
 		prog:       prog,
-		args:       args,
+		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 {
-		cmd := exec.Command(prog, append([]string{"join"}, s.ControlHosts...)...)
+		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()
@@ -99,7 +108,7 @@ func (s *Setup) installConsul() error {
 			return fmt.Errorf("consul join: %s", err)
 		}
 	}
-	return waitCheck(20*time.Second, s.consulCheck)
+	return nil
 }
 
 var consulCfg = api.DefaultConfig()

commit 379726401a04d87bdb9ef795af321ec0791a41ec
Author: Tom Clegg <tom at curoverse.com>
Date:   Sat Feb 25 17:48:06 2017 -0500

    gitignore

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

commit fc33deaecf2f7e4c9b3cd8d2e1f5b56901794042
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Feb 20 04:28:07 2017 -0500

    cmd, lib

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/services/boot/testimage_runit/Dockerfile b/cmd/arvados-admin/test-debian8/Dockerfile
similarity index 88%
copy from services/boot/testimage_runit/Dockerfile
copy to cmd/arvados-admin/test-debian8/Dockerfile
index aaf5b88..645aec2 100644
--- a/services/boot/testimage_runit/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 & exec arvados-boot"]
+CMD ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup && arvados-admin setup"]
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..0ebff83
--- /dev/null
+++ b/lib/setup/consul.go
@@ -0,0 +1,144 @@
+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 := s.UsrDir + "/bin/consul"
+	err := (&download{
+		URL:        "https://releases.hashicorp.com/consul/0.7.4/consul_0.7.4_linux_amd64.zip",
+		Dest:       prog,
+		Size:       36003597,
+		Mode:       0755,
+		PreloadDir: s.PreloadDir,
+	}).install()
+	if err != nil {
+		return err
+	}
+	dataDir := path.Join(s.DataDir, "consul")
+	if err := os.MkdirAll(dataDir, 0700); err != nil {
+		return err
+	}
+	args := []string{"agent"}
+	{
+		cf := path.Join(s.DataDir, "consul-encrypt.json")
+		if _, err := os.Stat(cf); err != nil && !os.IsNotExist(err) {
+			return err
+		} else if err != nil {
+			key, err := exec.Command(prog, "keygen").CombinedOutput()
+			if err != nil {
+				return err
+			}
+			if err = atomicWriteJSON(cf, map[string]interface{}{
+				"encrypt": strings.TrimSpace(string(key)),
+			}, 0400); err != nil {
+				return err
+			}
+		}
+		args = append(args, "-config-file="+cf)
+	}
+	{
+		s.masterToken = generateToken()
+		// os.Setenv("CONSUL_TOKEN", s.masterToken)
+		err = atomicWriteFile(path.Join(s.DataDir, "master-token.txt"), []byte(s.masterToken), 0600)
+		if err != nil {
+			return err
+		}
+		cf := path.Join(s.DataDir, "consul-config.json")
+		err = atomicWriteJSON(cf, map[string]interface{}{
+			"acl_datacenter":        s.ClusterID,
+			"acl_default_policy":    "deny",
+			"acl_enforce_version_8": true,
+			"acl_master_token":      s.masterToken,
+			"client_addr":           "0.0.0.0",
+			"bootstrap_expect":      len(s.ControlHosts),
+			"data_dir":              dataDir,
+			"datacenter":            s.ClusterID,
+			"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,
+			},
+		}, 0644)
+		if err != nil {
+			return err
+		}
+		args = append(args, "-config-file="+cf)
+	}
+	err = s.installService(daemon{
+		name:       "arvados-consul",
+		prog:       prog,
+		args:       args,
+		noRegister: true,
+	})
+	if err != nil {
+		return err
+	}
+	if len(s.ControlHosts) > 1 {
+		cmd := exec.Command(prog, append([]string{"join"}, s.ControlHosts...)...)
+		cmd.Stdout = os.Stderr
+		cmd.Stderr = os.Stderr
+		err := cmd.Run()
+		if err != nil {
+			return fmt.Errorf("consul join: %s", err)
+		}
+	}
+	return waitCheck(20*time.Second, s.consulCheck)
+}
+
+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) 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..935db1a
--- /dev/null
+++ b/lib/setup/setup.go
@@ -0,0 +1,65 @@
+package setup
+
+import (
+	"flag"
+	"fmt"
+	"os"
+
+	"git.curoverse.com/arvados.git/lib/agent"
+	"git.curoverse.com/arvados.git/sdk/go/config"
+)
+
+func Command() *Setup {
+	return &Setup{
+		Agent:      agent.Command(),
+		PreloadDir: "/var/cache/arvados",
+	}
+}
+
+type Setup struct {
+	*agent.Agent
+	PreloadDir string
+
+	masterToken string
+}
+
+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.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,
+	} {
+		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/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)
+}
diff --git a/services/boot/testimage_runit/Dockerfile b/services/boot/testimage_runit/Dockerfile
index aaf5b88..e606868 100644
--- a/services/boot/testimage_runit/Dockerfile
+++ b/services/boot/testimage_runit/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 & exec arvados-boot"]
+CMD ["bash", "-c", "runsvdir /etc/sv & arvados-boot && arvados-boot"]

commit 016a995d5a65f9de66c505fe80ab49eebc1a11c4
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Feb 17 03:09:34 2017 -0500

    more vault

diff --git a/services/boot/controller.go b/services/boot/controller.go
index 4b3e249..abd0725 100644
--- a/services/boot/controller.go
+++ b/services/boot/controller.go
@@ -17,20 +17,16 @@ func (c *controller) Boot(ctx context.Context) error {
 		},
 		Concurrent{
 			postgresql,
-			Concurrent{
-				&download{
-					URL:  "https://releases.hashicorp.com/consul-template/0.18.0/consul-template_0.18.0_linux_amd64.zip",
-					Dest: path.Join(cfg.UsrDir, "bin", "consul-template"),
-					Size: 6912352,
-					Mode: 0755,
-				},
-				consul,
-			},
-			Concurrent{
-				vault,
-				nomad,
+			&download{
+				URL:  "https://releases.hashicorp.com/consul-template/0.18.0/consul-template_0.18.0_linux_amd64.zip",
+				Dest: path.Join(cfg.UsrDir, "bin", "consul-template"),
+				Size: 6912352,
+				Mode: 0755,
 			},
 		},
+		consul,
+		vault,
+		nomad,
 		// Concurrent{
 		// 	dispatchLocal,
 		// 	dispatchSLURM,
diff --git a/services/boot/nomad.go b/services/boot/nomad.go
index 632f993..0dfdd11 100644
--- a/services/boot/nomad.go
+++ b/services/boot/nomad.go
@@ -3,6 +3,7 @@ package main
 import (
 	"context"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"path"
 	"sync"
@@ -36,6 +37,11 @@ func (nb *nomadBooter) Boot(ctx context.Context) error {
 		return err
 	}
 
+	masterToken, err := ioutil.ReadFile(cfg.masterTokenFile())
+	if err != nil {
+		return err
+	}
+
 	dataDir := path.Join(cfg.DataDir, "nomad")
 	if err := os.MkdirAll(dataDir, 0700); err != nil {
 		return err
@@ -51,6 +57,7 @@ func (nb *nomadBooter) Boot(ctx context.Context) error {
 		},
 		"consul": map[string]interface{}{
 			"address": fmt.Sprintf("127.0.0.1:%d", cfg.Ports.ConsulHTTP),
+			"token":   string(masterToken),
 		},
 		"data_dir":   dataDir,
 		"datacenter": cfg.SiteID,
diff --git a/services/boot/vault.go b/services/boot/vault.go
index f01dbb6..4d063e7 100644
--- a/services/boot/vault.go
+++ b/services/boot/vault.go
@@ -128,31 +128,54 @@ func (vb *vaultBooter) tryInit(ctx context.Context) error {
 		return fmt.Errorf("vault unseal failed!")
 	}
 
+	// Use master token to create a management token
 	master, err := consul.master(ctx)
 	if err != nil {
 		return err
 	}
-	token, _, err := master.ACL().Create(&consulAPI.ACLEntry{Name: "vault", Type: "management"}, nil)
+	mgmtToken, _, err := master.ACL().Create(&consulAPI.ACLEntry{Name: "vault", Type: "management"}, nil)
 	if err != nil {
 		return err
 	}
-	err = waitCheck(ctx, 30*time.Second, func(context.Context) error {
+	if err = atomicWriteFile(path.Join(cfg.DataDir, "vault-mgmt-token.txt"), []byte(mgmtToken), 0400); err != nil {
+		return err
+	}
+
+	// Mount+configure consul backend
+	if err = waitCheck(ctx, 30*time.Second, func(context.Context) error {
+		// Typically this first fails "500 node not active but
+		// active node not found" but then succeeds.
 		return vault.Sys().Mount("consul", &api.MountInput{Type: "consul"})
-	})
-	if err != nil {
+	}); err != nil {
 		return err
 	}
 	_, err = vault.Logical().Write("consul/config/access", map[string]interface{}{
 		"address": fmt.Sprintf("127.0.0.1:%d", cfg.Ports.ConsulHTTP),
-		"token":   string(token),
+		"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" }`)),
 	})
-	return err
+	if err != nil {
+		return err
+	}
+
+	// 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 (vb *vaultBooter) client(ctx context.Context) (*api.Client, error) {

commit aaa536943dde5b3f1ec59c1bd18929b53dfea308
Merge: bee3010 5624fec
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu Feb 16 12:01:17 2017 -0500

    Merge branch 'master' into deploy-agent
    
    Conflicts:
    	build/package-build-dockerfiles/centos7/Dockerfile
    	build/package-build-dockerfiles/debian8/Dockerfile
    	build/package-build-dockerfiles/ubuntu1204/Dockerfile
    	build/package-build-dockerfiles/ubuntu1404/Dockerfile

diff --cc build/package-build-dockerfiles/centos7/Dockerfile
index 8035ef4,08bd473..189a841
--- a/build/package-build-dockerfiles/centos7/Dockerfile
+++ b/build/package-build-dockerfiles/centos7/Dockerfile
@@@ -4,12 -4,9 +4,12 @@@ MAINTAINER Brett Smith <brett at curoverse
  # Install build dependencies provided in base distribution
  RUN yum -q -y install make automake gcc gcc-c++ libyaml-devel patch readline-devel zlib-devel libffi-devel openssl-devel bzip2 libtool bison sqlite-devel rpm-build git perl-ExtUtils-MakeMaker libattr-devel nss-devel libcurl-devel which tar unzip scl-utils centos-release-scl postgresql-devel python-devel python-setuptools fuse-devel xz-libs git
  
 +# Node.js
 +RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && yum install -y nodejs
 +
  # Install golang binary
- ADD generated/go1.7.1.linux-amd64.tar.gz /usr/local/
- RUN ln -s /usr/local/go/bin/go* /usr/local/bin/
+ ADD generated/go1.7.5.linux-amd64.tar.gz /usr/local/
+ RUN ln -s /usr/local/go/bin/go /usr/local/bin/
  
  # Install RVM
  RUN gpg --keyserver pool.sks-keyservers.net --recv-keys D39DC0E3 && \

commit bee301067b50d0cc00bbc1dc0a0b5c036d747a74
Author: Tom Clegg <tom at curoverse.com>
Date:   Wed Feb 15 01:19:15 2017 -0500

    consul acl

diff --git a/services/boot/config.go b/services/boot/config.go
index 50e2850..f301132 100644
--- a/services/boot/config.go
+++ b/services/boot/config.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"path"
 	"strings"
 )
 
@@ -107,3 +108,7 @@ func DefaultConfig() *Config {
 		},
 	}
 }
+
+func (cfg *Config) masterTokenFile() string {
+	return path.Join(cfg.DataDir, "consul-master-token.txt")
+}
diff --git a/services/boot/consul.go b/services/boot/consul.go
index 3c57386..ebf8661 100644
--- a/services/boot/consul.go
+++ b/services/boot/consul.go
@@ -2,7 +2,10 @@ package main
 
 import (
 	"context"
+	"crypto/rand"
+	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"os"
 	"os/exec"
 	"path"
@@ -60,14 +63,23 @@ func (cb *consulBooter) Boot(ctx context.Context) error {
 		args = append(args, "-config-file="+cf)
 	}
 	{
+		masterToken := generateUUID()
+		os.Setenv("CONSUL_TOKEN", masterToken)
+		err = atomicWriteFile(cfg.masterTokenFile(), []byte(masterToken), 0600)
+		if err != nil {
+			return err
+		}
 		cf := path.Join(cfg.DataDir, "consul-ports.json")
 		err = atomicWriteJSON(cf, map[string]interface{}{
-			"client_addr":      "0.0.0.0",
-			"bootstrap_expect": len(cfg.ControlHosts),
-			"data_dir":         dataDir,
-			"datacenter":       cfg.SiteID,
-			"server":           true,
-			"ui":               true,
+			"acl_datacenter":     cfg.SiteID,
+			"acl_default_policy": "deny",
+			"acl_master_token":   masterToken,
+			"client_addr":        "0.0.0.0",
+			"bootstrap_expect":   len(cfg.ControlHosts),
+			"data_dir":           dataDir,
+			"datacenter":         cfg.SiteID,
+			"server":             true,
+			"ui":                 true,
 			"ports": map[string]int{
 				"dns":      cfg.Ports.ConsulDNS,
 				"http":     cfg.Ports.ConsulHTTP,
@@ -109,11 +121,21 @@ func (cb *consulBooter) Boot(ctx context.Context) error {
 
 var consulCfg = api.DefaultConfig()
 
-func (cb *consulBooter) check(ctx context.Context) error {
+func (cb *consulBooter) master(ctx context.Context) (*api.Client, error) {
 	cfg := cfg(ctx)
-	consulCfg.Address = fmt.Sprintf("127.0.0.1:%d", cfg.Ports.ConsulHTTP)
-	consulCfg.Datacenter = cfg.SiteID
-	consul, err := api.NewClient(consulCfg)
+	masterToken, err := ioutil.ReadFile(cfg.masterTokenFile())
+	if err != nil {
+		return nil, err
+	}
+	ccfg := api.DefaultConfig()
+	ccfg.Address = fmt.Sprintf("127.0.0.1:%d", cfg.Ports.ConsulHTTP)
+	ccfg.Datacenter = cfg.SiteID
+	ccfg.Token = string(masterToken)
+	return api.NewClient(ccfg)
+}
+
+func (cb *consulBooter) check(ctx context.Context) error {
+	consul, err := cb.master(ctx)
 	if err != nil {
 		return err
 	}
@@ -121,6 +143,14 @@ func (cb *consulBooter) check(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
+	acls, qmeta, err := consul.ACL().List(nil)
+	if err != nil {
+		return err
+	}
+	e := json.NewEncoder(os.Stderr)
+	e.SetIndent("", "  ")
+	e.Encode(acls)
+	e.Encode(qmeta)
 	return nil
 }
 
@@ -133,3 +163,11 @@ func (cb *consulBooter) OnlyNode() (bool, error) {
 	nodes, _, err := c.Catalog().Nodes(nil)
 	return len(nodes) == 1, err
 }
+
+func generateUUID() string {
+	var r [16]byte
+	if _, err := rand.Read(r[:]); err != nil {
+		panic(err)
+	}
+	return fmt.Sprintf("%x", r)
+}
diff --git a/services/boot/supervisor.go b/services/boot/supervisor.go
index 8a46eb5..87a96c1 100644
--- a/services/boot/supervisor.go
+++ b/services/boot/supervisor.go
@@ -52,10 +52,7 @@ func (s *supervisedService) Boot(ctx context.Context) error {
 			return err
 		}
 	}
-	if err := consul.Boot(ctx); err != nil {
-		return err
-	}
-	consul, err := api.NewClient(consulCfg)
+	consul, err := consul.master(ctx)
 	if err != nil {
 		return err
 	}
diff --git a/services/boot/testimage_runit/Dockerfile b/services/boot/testimage_runit/Dockerfile
index 0541e4d..aaf5b88 100644
--- a/services/boot/testimage_runit/Dockerfile
+++ b/services/boot/testimage_runit/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", "coproc runsvdir /etc/sv; arvados-boot"]
+CMD ["bash", "-c", "runsvdir /etc/sv & exec arvados-boot"]
diff --git a/services/boot/vault.go b/services/boot/vault.go
index 7d042f8..f01dbb6 100644
--- a/services/boot/vault.go
+++ b/services/boot/vault.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"encoding/base64"
 	"fmt"
 	"io/ioutil"
 	"log"
@@ -9,6 +10,7 @@ import (
 	"sync"
 	"time"
 
+	consulAPI "github.com/hashicorp/consul/api"
 	"github.com/hashicorp/vault/api"
 )
 
@@ -40,15 +42,21 @@ func (vb *vaultBooter) Boot(ctx context.Context) error {
 		return err
 	}
 
+	masterToken, err := ioutil.ReadFile(cfg.masterTokenFile())
+	if err != nil {
+		return err
+	}
+
 	cfgPath := path.Join(cfg.DataDir, "vault.hcl")
 	err = atomicWriteFile(cfgPath, []byte(fmt.Sprintf(`backend "consul" {
 		address = "127.0.0.1:%d"
 		path = "vault"
+		token = %q
 	}
 	listener "tcp" {
 		address = "127.0.0.1:%d"
 		tls_disable = 1
-	}`, cfg.Ports.ConsulHTTP, cfg.Ports.VaultServer)), 0644)
+	}`, cfg.Ports.ConsulHTTP, masterToken, cfg.Ports.VaultServer)), 0644)
 	if err != nil {
 		return err
 	}
@@ -101,7 +109,9 @@ func (vb *vaultBooter) tryInit(ctx context.Context) error {
 	}
 	atomicWriteJSON(path.Join(cfg.DataDir, "vault-keys.json"), resp, 0400)
 	atomicWriteFile(path.Join(cfg.DataDir, "vault-root-token.txt"), []byte(resp.RootToken), 0400)
+	vault.SetToken(resp.RootToken)
 
+	ok := false
 	for _, key := range resp.Keys {
 		resp, err := vault.Sys().Unseal(key)
 		if err != nil {
@@ -110,10 +120,39 @@ func (vb *vaultBooter) tryInit(ctx context.Context) error {
 		}
 		if !resp.Sealed {
 			log.Printf("unseal successful")
-			return nil
+			ok = true
+			break
 		}
 	}
-	return fmt.Errorf("vault unseal failed!")
+	if !ok {
+		return fmt.Errorf("vault unseal failed!")
+	}
+
+	master, err := consul.master(ctx)
+	if err != nil {
+		return err
+	}
+	token, _, err := master.ACL().Create(&consulAPI.ACLEntry{Name: "vault", Type: "management"}, nil)
+	if err != nil {
+		return err
+	}
+	err = waitCheck(ctx, 30*time.Second, func(context.Context) error {
+		return vault.Sys().Mount("consul", &api.MountInput{Type: "consul"})
+	})
+	if err != nil {
+		return err
+	}
+	_, err = vault.Logical().Write("consul/config/access", map[string]interface{}{
+		"address": fmt.Sprintf("127.0.0.1:%d", cfg.Ports.ConsulHTTP),
+		"token":   string(token),
+	})
+	if err != nil {
+		return err
+	}
+	_, err = vault.Logical().Write("consul/roles/write-all", map[string]interface{}{
+		"policy": base64.StdEncoding.EncodeToString([]byte(`key "" { policy = "write" }`)),
+	})
+	return err
 }
 
 func (vb *vaultBooter) client(ctx context.Context) (*api.Client, error) {

commit 15907692a7a9213ef99a74e5ef1c48a487a8ed8b
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Feb 13 01:51:09 2017 -0500

    gitignore

diff --git a/services/boot/.gitignore b/services/boot/.gitignore
index 8c8b8e3..72a5a79 100644
--- a/services/boot/.gitignore
+++ b/services/boot/.gitignore
@@ -3,3 +3,4 @@ bindata.tmp
 node_modules
 bindata_assetfs.go
 npm-debug.log
+boot

commit 5eace5e8b255fdc7f65081429d0b42e551479e95
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Feb 13 01:50:06 2017 -0500

    consul 0.7.4, go tests

diff --git a/services/boot/consul.go b/services/boot/consul.go
index 9190aac..3c57386 100644
--- a/services/boot/consul.go
+++ b/services/boot/consul.go
@@ -29,9 +29,9 @@ func (cb *consulBooter) Boot(ctx context.Context) error {
 	cfg := cfg(ctx)
 	bin := cfg.UsrDir + "/bin/consul"
 	err := (&download{
-		URL:  "https://releases.hashicorp.com/consul/0.7.2/consul_0.7.2_linux_amd64.zip",
+		URL:  "https://releases.hashicorp.com/consul/0.7.4/consul_0.7.4_linux_amd64.zip",
 		Dest: bin,
-		Size: 29079005,
+		Size: 36003597,
 		Mode: 0755,
 	}).Boot(ctx)
 	if err != nil {
diff --git a/services/boot/debian8_test.go b/services/boot/debian8_test.go
new file mode 100644
index 0000000..8cfafba
--- /dev/null
+++ b/services/boot/debian8_test.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+	"log"
+	"net"
+	"os"
+	"testing"
+)
+
+func TestDebian8Install(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)
+	}
+	ln.Close()
+	log.Printf("Publishing consul webgui at %v", ln.Addr())
+	for _, cmdline := range [][]string{
+		{"go", "build"},
+		{"docker", "build", "--tag=arvados-boot-test-runit", "testimage_runit"},
+		{"docker", "run", "--rm", "--publish=" + port + ":18500", "--cap-add=IPC_LOCK", "--cap-add=SYS_ADMIN", "--volume=/sys/fs/cgroup", "--volume=" + cwd + "/boot:/usr/bin/arvados-boot:ro", "arvados-boot-test-runit"},
+	} {
+		err = command(cmdline[0], cmdline[1:]...).Run()
+		if err != nil {
+			t.Fatal(err)
+		}
+	}
+}
diff --git a/services/boot/testimage_runit/Dockerfile b/services/boot/testimage_runit/Dockerfile
index 3a6829d..0541e4d 100644
--- a/services/boot/testimage_runit/Dockerfile
+++ b/services/boot/testimage_runit/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", "coproc arvados-boot; runsvdir /etc/sv"]
+CMD ["bash", "-c", "coproc runsvdir /etc/sv; arvados-boot"]

commit 07f620f078c6f75cf52e9ae65040857ea1980fdc
Author: Tom Clegg <tom at curoverse.com>
Date:   Sun Feb 12 12:01:52 2017 -0500

    gateway conf

diff --git a/services/boot/gateway.go b/services/boot/gateway.go
index 7d0ceb0..aae7ad8 100644
--- a/services/boot/gateway.go
+++ b/services/boot/gateway.go
@@ -5,6 +5,8 @@ package main
 import (
 	"context"
 	"fmt"
+	"io/ioutil"
+	"os/exec"
 	"path"
 )
 
@@ -14,73 +16,81 @@ error_log stderr info;          # Yes, must be specified here _and_ cmdline
 events {
 }
 http {
-  access_log {{keyOrDefault "service/gateway/access_log" "/var/log/arvados/gateway.log" | toJSON}} combined;
-  upstream arv-git-http {
-    server localhost:{{GITPORT}};
+  access_log {{keyOrDefault "arvados/service/gateway/access_log" "/var/log/arvados/gateway.log" | toJSON}} combined;
+  upstream git-httpd {
+    {{service "arvados-git-http"}}
+    server {{.Address}}:{{.Port}};
+    {{end}}
   }
   server {
-    {{if keyExists"service/gateway/ports/tlsGit"}}
-    listen *:{{key "service/gateway/ports/tlsGit"}} ssl default_server;
+    {{if keyExists "arvados/port/tlsGit"}}
+    listen *:{{key "arvados/port/tlsGit"}} ssl default_server;
     {{end}}
-    listen *:{{keyOrDefault "service/gateway/ports/tlsGateway" 443}} ssl;
-    server_name git.{{key "service/gateway/domain"}};
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    listen *:{{keyOrDefault "arvados/port/tlsGateway" 443}} ssl;
+    server_name git.{{key "arvados/service/gateway/domain"}};
+    ssl_certificate {{key "arvados/service/gateway/pki/certPath"}};
+    ssl_certificate_key {{key "arvados/service/gateway/pki/keyPath"}};
     location  / {
-      proxy_pass http://arv-git-http;
+      proxy_pass http://git-httpd;
     }
   }
-  upstream keepproxy {
-    server localhost:{{KEEPPROXYPORT}};
+  upstream keep-proxy {
+    {{service "arvados-keepproxy"}}
+    server {{.Address}}:{{.Port}};
+    {{end}}
   }
   server {
-    listen *:{{KEEPPROXYSSLPORT}} ssl default_server;
-    server_name _;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    {{if keyExists "arvados/port/tlsKeepProxy"}}
+    listen *:{{key "arvados/port/tlsKeepProxy"}} ssl default_server;
+    {{end}}
+    listen *:{{keyOrDefault "arvados/port/tlsGateway" 443}} ssl;
+    server_name keep.{{key "arvados/service/gateway/domain"}};
+    ssl_certificate {{key "arvados/service/gateway/pki/certPath"}};
+    ssl_certificate_key {{key "arvados/service/gateway/pki/keyPath"}};
     location  / {
-      proxy_pass http://keepproxy;
+      proxy_pass http://keep-proxy;
     }
   }
   upstream keep-web {
-    server localhost:{{KEEPWEBPORT}};
-  }
-  server {
-    listen *:{{KEEPWEBSSLPORT}} ssl default_server;
-    server_name ~^(?<request_host>.*)$;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
-    location  / {
-      proxy_pass http://keep-web;
-      proxy_set_header Host $request_host:{{KEEPWEBPORT}};
-      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-    }
+    {{service "arvados-keep-web"}}
+    server {{.Address}}:{{.Port}};
+    {{end}}
   }
   server {
-    listen *:{{KEEPWEBDLSSLPORT}} ssl default_server;
-    server_name ~.*;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    {{if keyExists "arvados/port/tlsKeepWeb"}}
+    listen *:{{key "arvados/port/tlsKeepWeb"}} ssl default_server;
+    {{end}}
+    listen *:{{keyOrDefault "arvados/port/tlsGateway" 443}} ssl;
+    server_name download.{{key "arvados/service/gateway/domain"}}
+        collections.{{key "arvados/service/gateway/domain"}}
+        *.collections.{{key "arvados/service/gateway/domain"}}
+        ~.*--collections.{{key "arvados/service/gateway/domain"}};
+        *.collections.{{key "arvados/service/gateway/domain"}};
+    ssl_certificate {{key "arvados/service/gateway/pki/certPath"}};
+    ssl_certificate_key {{key "arvados/service/gateway/pki/keyPath"}};
     location  / {
       proxy_pass http://keep-web;
-      proxy_set_header Host download:{{KEEPWEBPORT}};
+      proxy_set_header Host            $host;
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
-      proxy_redirect //download:{{KEEPWEBPORT}}/ https://$host:{{KEEPWEBDLSSLPORT}}/;
     }
   }
   upstream ws {
-    server localhost:{{WSPORT}};
+    {{service "arvados-ws"}}
+    server {{.Address}}:{{.Port}};
+    {{end}}
   }
   server {
-    listen *:{{WSSPORT}} ssl default_server;
-    server_name ~^(?<request_host>.*)$;
-    ssl_certificate {{SSLCERT}};
-    ssl_certificate_key {{SSLKEY}};
+    {{if keyExists "arvados/port/tlsWS"}}
+    listen *:{{key "arvados/port/tlsWS"}} ssl default_server;
+    {{end}}
+    listen *:{{keyOrDefault "arvados/port/tlsGateway" 443}} ssl;
+    server_name ws.{{key "arvados/service/gateway/domain"}};
+    ssl_certificate {{key "arvados/service/gateway/pki/certPath"}};
+    ssl_certificate_key {{key "arvados/service/gateway/pki/keyPath"}};
     location  / {
       proxy_pass http://ws;
       proxy_set_header Upgrade $http_upgrade;
       proxy_set_header Connection "upgrade";
-      proxy_set_header Host $request_host:{{WSPORT}};
       proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     }
   }
@@ -106,8 +116,11 @@ func (ngb *nginxGatewayBooter) Boot(ctx context.Context) error {
 		return err
 	}
 
-	cfgPath := path.Join(cfg.DataDir, "gateway.consul-template.hcl")
-	if err = atomicWriteJSON(cfgPath+".ctmpl", map[string]interface{}{
+	consulCfg := path.Join(cfg.DataDir, "gateway.consul-template.hcl")
+	if err = atomicWriteJSON(consulCfg+".ctmpl", map[string]interface{}{
+		"exec": map[string]interface{}{
+			"reload_signal": "SIGHUP",
+		},
 		"consul": map[string]interface{}{
 			"address": fmt.Sprintf("0.0.0.0:%d", cfg.Ports.ConsulHTTP),
 		},
@@ -118,27 +131,36 @@ func (ngb *nginxGatewayBooter) Boot(ctx context.Context) error {
 		return err
 	}
 
-	tmplPath := path.Join(cfg.DataDir, "gateway.nginx.conf")
-	if err = atomicWriteFile(tmplPath+".ctmpl", []byte(ngb.tmpl), 0644); err != nil {
+	nginxCfg := path.Join(cfg.DataDir, "gateway.nginx.conf")
+	if err = atomicWriteFile(nginxCfg+".ctmpl", []byte(ngb.tmpl), 0644); err != nil {
+		return err
+	}
+
+	if err := (&osPackage{
+		Debian: "nginx",
+	}).Boot(ctx); err != nil {
+		return err
+	}
+
+	nginxBin, err := exec.LookPath("nginx")
+	if err != nil {
 		return err
 	}
 
-	return Series{
-		&osPackage{
-			Debian: "nginx",
+	return (&supervisedService{
+		name: ngb.name,
+		cmd:  path.Join(cfg.UsrDir, "bin", "consul-template"),
+		args: []string{
+			"-config=" + consulCfg,
+			"-template=" + nginxCfg + ".ctmpl:" + nginxCfg,
+			"-exec",
+			"nginx",
+			"-g", "error_log stderr info;",
+			"-g", "pid " + path.Join(cfg.DataDir, "nginx.pid") + ";",
+			"-c", nginxCfg,
 		},
-		&supervisedService{
-			name: ngb.name,
-			cmd:  path.Join(cfg.UsrDir, "bin", "consul-template"),
-			args: []string{
-				"-config=" + cfgPath,
-				"-template=" + tmplPath + ".ctmpl:" + tmplPath,
-				"-exec",
-				"nginx",
-			},
-			env: map[string]string{
-				"VAULT_TOKEN": rootToken,
-			},
+		env: map[string]string{
+			"VAULT_TOKEN": rootToken,
 		},
-	}.Boot(ctx)
+	}).Boot(ctx)
 }

commit dde508ae4d3f9542a16c5ebbe90de534cbdaaabb
Author: Tom Clegg <tom at curoverse.com>
Date:   Sun Feb 12 12:01:46 2017 -0500

    go vet

diff --git a/services/boot/check.go b/services/boot/check.go
index b71db80..7b5c776 100644
--- a/services/boot/check.go
+++ b/services/boot/check.go
@@ -6,7 +6,8 @@ import (
 )
 
 func waitCheck(ctx context.Context, timeout time.Duration, check func(ctx context.Context) error) error {
-	ctx, _ = context.WithTimeout(ctx, timeout)
+	ctx, cancel := context.WithTimeout(ctx, timeout)
+	defer cancel()
 	var err error
 	for err = check(ctx); err != nil && ctx.Err() == nil; err = check(ctx) {
 		time.Sleep(time.Second)
diff --git a/services/boot/download.go b/services/boot/download.go
index e047aab..83b519a 100644
--- a/services/boot/download.go
+++ b/services/boot/download.go
@@ -97,7 +97,7 @@ func (d *download) Boot(ctx context.Context) error {
 	if d.Size > 0 && d.Size != n {
 		return fmt.Errorf("Size mismatch: got %d bytes, expected %d", n, d.Size)
 	} else if d.Size == 0 {
-		log.Printf("%s: size was %d", d, n)
+		log.Printf("%v: size was %d", d, n)
 	}
 	if err = out.Close(); err != nil {
 		return err

commit 816f3bfd2b6a562fc712b7c2b3eb30157a95cb7e
Author: Tom Clegg <tom at curoverse.com>
Date:   Sat Feb 11 03:48:35 2017 -0500

    dev privileges, db

diff --git a/services/boot/package.json b/services/boot/package.json
index 7c30502..9ef7412 100644
--- a/services/boot/package.json
+++ b/services/boot/package.json
@@ -14,7 +14,7 @@
   "scripts": {
     "dev": "WEBPACK_FLAGS=-d go generate && go get ./... && $GOPATH/bin/boot",
     "dev-as-root": "WEBPACK_FLAGS=-d go generate && go get ./... && sudo $GOPATH/bin/boot",
-    "dev-docker": "WEBPACK_FLAGS=-d go generate && go get ./... && docker build --tag=arvados-boot-test-runit testimage_runit && docker run --rm -it --publish=18500:18500 --cap-add=IPC_LOCK --volume=${GOPATH}/bin/boot:/usr/bin/arvados-boot:ro arvados-boot-test-runit",
+    "dev-docker": "WEBPACK_FLAGS=-d go generate && go get ./... && docker build --tag=arvados-boot-test-runit testimage_runit && docker run --rm -it --publish=18500:18500 --cap-add=IPC_LOCK --cap-add=SYS_ADMIN --volume=/sys/fs/cgroup --volume=${GOPATH}/bin/boot:/usr/bin/arvados-boot:ro arvados-boot-test-runit",
     "test": "./node_modules/.bin/tap 'js/**/*_test.js'",
     "build": "go generate && go get ./...",
     "start": "npm run build && $GOPATH/bin/boot",
diff --git a/services/boot/testimage_runit/Dockerfile b/services/boot/testimage_runit/Dockerfile
index 07e7c3c..3a6829d 100644
--- a/services/boot/testimage_runit/Dockerfile
+++ b/services/boot/testimage_runit/Dockerfile
@@ -9,4 +9,6 @@ RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends ru
 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", "coproc arvados-boot; runsvdir /etc/sv"]

commit 61476528523e4ee2ebe199807b8aeba1e17074d4
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Feb 7 01:40:04 2017 -0500

    postgresql

diff --git a/services/boot/controller.go b/services/boot/controller.go
index 7ddb65b..4b3e249 100644
--- a/services/boot/controller.go
+++ b/services/boot/controller.go
@@ -31,14 +31,14 @@ func (c *controller) Boot(ctx context.Context) error {
 				nomad,
 			},
 		},
-		Concurrent{
-			dispatchLocal,
-			dispatchSLURM,
-			gitHTTP,
-			keepbalance,
-			keepproxy,
-			keepstore,
-			websocket,
-		},
+		// Concurrent{
+		// 	dispatchLocal,
+		// 	dispatchSLURM,
+		// 	gitHTTP,
+		// 	keepbalance,
+		// 	keepproxy,
+		// 	keepstore,
+		// 	websocket,
+		// },
 	}.Boot(ctx)
 }
diff --git a/services/boot/postgresql.go b/services/boot/postgresql.go
index 2be582b..e92d365 100644
--- a/services/boot/postgresql.go
+++ b/services/boot/postgresql.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"os"
 	"time"
 )
 
@@ -11,20 +10,11 @@ var postgresql = &pgBooter{}
 type pgBooter struct {}
 
 func (pb *pgBooter) Boot(ctx context.Context) error {
-	os.Setenv("LANG", "en_US.utf8")
 	// TODO: return nil if this isn't the database host.
 	if pb.check(ctx) == nil {
 		return nil
 	}
 	if err := (&osPackage{
-		Debian: "locales",
-	}).Boot(ctx); err != nil {
-		return err
-	}
-	if err := command("bash", "-c", "echo ${LANG} UTF-8 | tee -a /etc/locale.gen && locale-gen -a").Run(); err != nil {
-		return err
-	}
-	if err := (&osPackage{
 		Debian: "postgresql",
 	}).Boot(ctx); err != nil {
 		return err
diff --git a/services/boot/testimage_runit/Dockerfile b/services/boot/testimage_runit/Dockerfile
index d3aac96..07e7c3c 100644
--- a/services/boot/testimage_runit/Dockerfile
+++ b/services/boot/testimage_runit/Dockerfile
@@ -4,6 +4,9 @@ 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
+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"]
 
 CMD ["bash", "-c", "coproc arvados-boot; runsvdir /etc/sv"]

commit 668b2ae22b910765d4e7982a9d9b8a82307e4360
Author: Tom Clegg <tom at curoverse.com>
Date:   Tue Feb 7 01:19:16 2017 -0500

    new webpack

diff --git a/services/boot/webpack.config.js b/services/boot/webpack.config.js
index 836afea..1693554 100644
--- a/services/boot/webpack.config.js
+++ b/services/boot/webpack.config.js
@@ -3,7 +3,7 @@ module.exports = {
         js: './js',
     },
     output: {
-        directory: 'bindata.tmp',
+        path: 'bindata.tmp',
         filename: 'bindata.tmp/[name].js',
     },
 };

commit 61f0eb808bba87639e5e4b068f444b7de0526bb2
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Feb 6 09:59:22 2017 -0500

    add gateway (part)

diff --git a/services/boot/gateway.go b/services/boot/gateway.go
new file mode 100644
index 0000000..7d0ceb0
--- /dev/null
+++ b/services/boot/gateway.go
@@ -0,0 +1,144 @@
+//+build ignore
+
+package main
+
+import (
+	"context"
+	"fmt"
+	"path"
+)
+
+var gateway = &nginxGatewayBooter{tmpl: `
+daemon off;
+error_log stderr info;          # Yes, must be specified here _and_ cmdline
+events {
+}
+http {
+  access_log {{keyOrDefault "service/gateway/access_log" "/var/log/arvados/gateway.log" | toJSON}} combined;
+  upstream arv-git-http {
+    server localhost:{{GITPORT}};
+  }
+  server {
+    {{if keyExists"service/gateway/ports/tlsGit"}}
+    listen *:{{key "service/gateway/ports/tlsGit"}} ssl default_server;
+    {{end}}
+    listen *:{{keyOrDefault "service/gateway/ports/tlsGateway" 443}} ssl;
+    server_name git.{{key "service/gateway/domain"}};
+    ssl_certificate {{SSLCERT}};
+    ssl_certificate_key {{SSLKEY}};
+    location  / {
+      proxy_pass http://arv-git-http;
+    }
+  }
+  upstream keepproxy {
+    server localhost:{{KEEPPROXYPORT}};
+  }
+  server {
+    listen *:{{KEEPPROXYSSLPORT}} ssl default_server;
+    server_name _;
+    ssl_certificate {{SSLCERT}};
+    ssl_certificate_key {{SSLKEY}};
+    location  / {
+      proxy_pass http://keepproxy;
+    }
+  }
+  upstream keep-web {
+    server localhost:{{KEEPWEBPORT}};
+  }
+  server {
+    listen *:{{KEEPWEBSSLPORT}} ssl default_server;
+    server_name ~^(?<request_host>.*)$;
+    ssl_certificate {{SSLCERT}};
+    ssl_certificate_key {{SSLKEY}};
+    location  / {
+      proxy_pass http://keep-web;
+      proxy_set_header Host $request_host:{{KEEPWEBPORT}};
+      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+  }
+  server {
+    listen *:{{KEEPWEBDLSSLPORT}} ssl default_server;
+    server_name ~.*;
+    ssl_certificate {{SSLCERT}};
+    ssl_certificate_key {{SSLKEY}};
+    location  / {
+      proxy_pass http://keep-web;
+      proxy_set_header Host download:{{KEEPWEBPORT}};
+      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+      proxy_redirect //download:{{KEEPWEBPORT}}/ https://$host:{{KEEPWEBDLSSLPORT}}/;
+    }
+  }
+  upstream ws {
+    server localhost:{{WSPORT}};
+  }
+  server {
+    listen *:{{WSSPORT}} ssl default_server;
+    server_name ~^(?<request_host>.*)$;
+    ssl_certificate {{SSLCERT}};
+    ssl_certificate_key {{SSLKEY}};
+    location  / {
+      proxy_pass http://ws;
+      proxy_set_header Upgrade $http_upgrade;
+      proxy_set_header Connection "upgrade";
+      proxy_set_header Host $request_host:{{WSPORT}};
+      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+  }
+}
+`}
+
+type nginxGatewayBooter struct {
+	tmpl string
+}
+
+func (ngb *nginxGatewayBooter) Boot(ctx context.Context) error {
+	cfg := cfg(ctx)
+
+	if ngb.conf == "" {
+		ngb.conf = ngb.name
+	}
+	if ngb.tmpl == "" {
+		ngb.tmpl = "{}"
+	}
+
+	rootToken, err := ioutil.ReadFile(path.Join(cfg.DataDir, "vault-root-token.txt"))
+	if err != nil {
+		return err
+	}
+
+	cfgPath := path.Join(cfg.DataDir, "gateway.consul-template.hcl")
+	if err = atomicWriteJSON(cfgPath+".ctmpl", map[string]interface{}{
+		"consul": map[string]interface{}{
+			"address": fmt.Sprintf("0.0.0.0:%d", cfg.Ports.ConsulHTTP),
+		},
+		"vault": map[string]string{
+			"address": fmt.Sprintf("http://0.0.0.0:%d", cfg.Ports.VaultServer),
+			"token":   rootToken,
+		}}, 0600); err != nil {
+		return err
+	}
+
+	tmplPath := path.Join(cfg.DataDir, "gateway.nginx.conf")
+	if err = atomicWriteFile(tmplPath+".ctmpl", []byte(ngb.tmpl), 0644); err != nil {
+		return err
+	}
+
+	return Series{
+		&osPackage{
+			Debian: "nginx",
+		},
+		&supervisedService{
+			name: ngb.name,
+			cmd:  path.Join(cfg.UsrDir, "bin", "consul-template"),
+			args: []string{
+				"-config=" + cfgPath,
+				"-template=" + tmplPath + ".ctmpl:" + tmplPath,
+				"-exec",
+				"nginx",
+			},
+			env: map[string]string{
+				"VAULT_TOKEN": rootToken,
+			},
+		},
+	}.Boot(ctx)
+}

commit 126f499d2f084963a5ef5df7cf567a1ed4cb96ed
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Feb 6 05:49:04 2017 -0500

    add postgresql

diff --git a/services/boot/arvados_packages.go b/services/boot/arvados_packages.go
index 5115b46..eee5f88 100644
--- a/services/boot/arvados_packages.go
+++ b/services/boot/arvados_packages.go
@@ -4,16 +4,16 @@ import (
 	"context"
 	"io/ioutil"
 	"os"
-	"sync"
 )
 
 var arvadosRepo = &arvadosRepoBooter{}
 
-type arvadosRepoBooter struct {
-	sync.Mutex
-}
+type arvadosRepoBooter struct {}
 
 func (*arvadosRepoBooter) Boot(ctx context.Context) error {
+	osPackageMutex.Lock()
+	defer osPackageMutex.Unlock()
+
 	cfg := cfg(ctx)
 	repo := cfg.ArvadosAptRepo
 	if !repo.Enabled {
diff --git a/services/boot/check.go b/services/boot/check.go
new file mode 100644
index 0000000..b71db80
--- /dev/null
+++ b/services/boot/check.go
@@ -0,0 +1,15 @@
+package main
+
+import (
+	"context"
+	"time"
+)
+
+func waitCheck(ctx context.Context, timeout time.Duration, check func(ctx context.Context) error) error {
+	ctx, _ = context.WithTimeout(ctx, timeout)
+	var err error
+	for err = check(ctx); err != nil && ctx.Err() == nil; err = check(ctx) {
+		time.Sleep(time.Second)
+	}
+	return err
+}
diff --git a/services/boot/controller.go b/services/boot/controller.go
index 784f09f..7ddb65b 100644
--- a/services/boot/controller.go
+++ b/services/boot/controller.go
@@ -16,17 +16,20 @@ func (c *controller) Boot(ctx context.Context) error {
 			arvadosRepo,
 		},
 		Concurrent{
-			&download{
-				URL:  "https://releases.hashicorp.com/consul-template/0.18.0/consul-template_0.18.0_linux_amd64.zip",
-				Dest: path.Join(cfg.UsrDir, "bin", "consul-template"),
-				Size: 6912352,
-				Mode: 0755,
+			postgresql,
+			Concurrent{
+				&download{
+					URL:  "https://releases.hashicorp.com/consul-template/0.18.0/consul-template_0.18.0_linux_amd64.zip",
+					Dest: path.Join(cfg.UsrDir, "bin", "consul-template"),
+					Size: 6912352,
+					Mode: 0755,
+				},
+				consul,
+			},
+			Concurrent{
+				vault,
+				nomad,
 			},
-			consul,
-		},
-		Concurrent{
-			vault,
-			nomad,
 		},
 		Concurrent{
 			dispatchLocal,
diff --git a/services/boot/postgresql.go b/services/boot/postgresql.go
new file mode 100644
index 0000000..2be582b
--- /dev/null
+++ b/services/boot/postgresql.go
@@ -0,0 +1,40 @@
+package main
+
+import (
+	"context"
+	"os"
+	"time"
+)
+
+var postgresql = &pgBooter{}
+
+type pgBooter struct {}
+
+func (pb *pgBooter) Boot(ctx context.Context) error {
+	os.Setenv("LANG", "en_US.utf8")
+	// TODO: return nil if this isn't the database host.
+	if pb.check(ctx) == nil {
+		return nil
+	}
+	if err := (&osPackage{
+		Debian: "locales",
+	}).Boot(ctx); err != nil {
+		return err
+	}
+	if err := command("bash", "-c", "echo ${LANG} UTF-8 | tee -a /etc/locale.gen && locale-gen -a").Run(); err != nil {
+		return err
+	}
+	if err := (&osPackage{
+		Debian: "postgresql",
+	}).Boot(ctx); err != nil {
+		return err
+	}
+	if err := command("service", "postgresql", "start").Run(); err != nil {
+		return err
+	}
+	return waitCheck(ctx, 30*time.Second, pb.check)
+}
+
+func (pb *pgBooter) check(ctx context.Context) error {
+	return command("pg_isready").Run()
+}
diff --git a/services/boot/testimage_runit/Dockerfile b/services/boot/testimage_runit/Dockerfile
index 1bab296..d3aac96 100644
--- a/services/boot/testimage_runit/Dockerfile
+++ b/services/boot/testimage_runit/Dockerfile
@@ -2,7 +2,7 @@ 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 nginx runit
+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
 

commit 674236324476c58ccf20d554acd4427dfb6b9873
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Feb 6 05:12:59 2017 -0500

    just try init once

diff --git a/services/boot/consul.go b/services/boot/consul.go
index f51d694..9190aac 100644
--- a/services/boot/consul.go
+++ b/services/boot/consul.go
@@ -8,6 +8,7 @@ import (
 	"path"
 	"strings"
 	"sync"
+	"time"
 
 	"github.com/hashicorp/consul/api"
 )
@@ -36,25 +37,25 @@ func (cb *consulBooter) Boot(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-	dataDir := cfg.DataDir + "/consul"
+	dataDir := path.Join(cfg.DataDir, "consul")
 	if err := os.MkdirAll(dataDir, 0700); err != nil {
 		return err
 	}
 	args := []string{"agent"}
 	{
 		cf := path.Join(cfg.DataDir, "consul-encrypt.json")
-		_, err := os.Stat(cf)
-		if os.IsNotExist(err) {
+		if _, err := os.Stat(cf); err != nil && !os.IsNotExist(err) {
+			return err
+		} else if err != nil {
 			key, err := exec.Command(bin, "keygen").CombinedOutput()
 			if err != nil {
 				return err
 			}
-			err = atomicWriteJSON(cf, map[string]interface{}{
+			if err = atomicWriteJSON(cf, map[string]interface{}{
 				"encrypt": strings.TrimSpace(string(key)),
-			}, 0400)
-		}
-		if err != nil {
-			return err
+			}, 0400); err != nil {
+				return err
+			}
 		}
 		args = append(args, "-config-file="+cf)
 	}
@@ -103,7 +104,7 @@ func (cb *consulBooter) Boot(ctx context.Context) error {
 			}
 		}
 	}
-	return cb.check(ctx)
+	return waitCheck(ctx, 30*time.Second, cb.check)
 }
 
 var consulCfg = api.DefaultConfig()
diff --git a/services/boot/main.go b/services/boot/main.go
index ebab009..f145e15 100644
--- a/services/boot/main.go
+++ b/services/boot/main.go
@@ -6,7 +6,6 @@ import (
 	"flag"
 	"log"
 	"os"
-	"time"
 
 	"git.curoverse.com/arvados.git/sdk/go/config"
 )
@@ -28,19 +27,9 @@ func main() {
 	enc.SetIndent("", "  ")
 	enc.Encode(cfg)
 
-	go runWebGUI(cfg)
-	go func() {
-		var ctl Booter = &controller{}
-		ticker := time.NewTicker(5 * time.Second)
-		for {
-			err := ctl.Boot(withCfg(context.Background(), cfg))
-			if err != nil {
-				log.Printf("controller boot failed: %v", err)
-			} else {
-				log.Printf("controller boot OK")
-			}
-			<-ticker.C
-		}
-	}()
-	<-(chan struct{})(nil)
+	var ctl Booter = &controller{}
+	err := ctl.Boot(withCfg(context.Background(), cfg))
+	if err != nil {
+		log.Printf("controller boot failed: %v", err)
+	}
 }
diff --git a/services/boot/nomad.go b/services/boot/nomad.go
index 07c5de8..632f993 100644
--- a/services/boot/nomad.go
+++ b/services/boot/nomad.go
@@ -6,6 +6,7 @@ import (
 	"os"
 	"path"
 	"sync"
+	"time"
 
 	"github.com/hashicorp/nomad/api"
 )
@@ -28,7 +29,7 @@ func (nb *nomadBooter) Boot(ctx context.Context) error {
 	err := (&download{
 		URL:  "https://releases.hashicorp.com/nomad/0.5.4/nomad_0.5.4_linux_amd64.zip",
 		Dest: bin,
-		//Size: 29079005,
+		Size: 34150464,
 		Mode: 0755,
 	}).Boot(ctx)
 	if err != nil {
@@ -79,7 +80,7 @@ func (nb *nomadBooter) Boot(ctx context.Context) error {
 			return fmt.Errorf("starting nomad: %s", err)
 		}
 	}
-	return nb.check(ctx)
+	return waitCheck(ctx, 30*time.Second, nb.check)
 }
 
 var nomadCfg = api.DefaultConfig()
diff --git a/services/boot/testimage_runit/Dockerfile b/services/boot/testimage_runit/Dockerfile
index b65a158..1bab296 100644
--- a/services/boot/testimage_runit/Dockerfile
+++ b/services/boot/testimage_runit/Dockerfile
@@ -1,10 +1,9 @@
 FROM debian:8
 RUN apt-get update
 
-RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends runit
-RUN mkdir /etc/sv/arvados-boot && ln -s /usr/bin/arvados-boot /etc/sv/arvados-boot/run
-
 # 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 nginx
+RUN DEBIAN_FRONTEND=noninteractive apt-get -dy install --no-install-recommends ca-certificates nginx runit
+
+RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends runit
 
-CMD ["sh", "-c", "runsvdir /etc/sv"]
+CMD ["bash", "-c", "coproc arvados-boot; runsvdir /etc/sv"]
diff --git a/services/boot/vault.go b/services/boot/vault.go
index de9cfb3..7d042f8 100644
--- a/services/boot/vault.go
+++ b/services/boot/vault.go
@@ -7,6 +7,7 @@ import (
 	"log"
 	"path"
 	"sync"
+	"time"
 
 	"github.com/hashicorp/vault/api"
 )
@@ -66,29 +67,37 @@ func (vb *vaultBooter) Boot(ctx context.Context) error {
 		}
 	}
 
-	vb.tryInit(ctx)
-	return vb.check(ctx)
+	if err := vb.tryInit(ctx); err != nil {
+		return err
+	}
+	return waitCheck(ctx, 30*time.Second, vb.check)
 }
 
-func (vb *vaultBooter) tryInit(ctx context.Context) {
+func (vb *vaultBooter) tryInit(ctx context.Context) error {
 	cfg := cfg(ctx)
-	vault, err := vb.client(ctx)
-	if err != nil {
-		return
-	}
-	if init, err := vault.Sys().InitStatus(); err != nil {
-		log.Printf("error: vault InitStatus: %s", err)
-		return
+
+	var vault *api.Client
+	var init bool
+	if err := waitCheck(ctx, time.Minute, func(context.Context) error {
+		var err error
+		vault, err = vb.client(ctx)
+		if err != nil {
+			return err
+		}
+		init, err = vault.Sys().InitStatus()
+		return err
+	}); err != nil {
+		return err
 	} else if init {
-		return
+		return nil
 	}
+
 	resp, err := vault.Sys().Init(&api.InitRequest{
 		SecretShares:    5,
 		SecretThreshold: 3,
 	})
 	if err != nil {
-		log.Printf("vault-init: %s", err)
-		return
+		return fmt.Errorf("vault-init: %s", err)
 	}
 	atomicWriteJSON(path.Join(cfg.DataDir, "vault-keys.json"), resp, 0400)
 	atomicWriteFile(path.Join(cfg.DataDir, "vault-root-token.txt"), []byte(resp.RootToken), 0400)
@@ -101,9 +110,10 @@ func (vb *vaultBooter) tryInit(ctx context.Context) {
 		}
 		if !resp.Sealed {
 			log.Printf("unseal successful")
-			break
+			return nil
 		}
 	}
+	return fmt.Errorf("vault unseal failed!")
 }
 
 func (vb *vaultBooter) client(ctx context.Context) (*api.Client, error) {

commit 87107582273d254e98a66a3036bc0fc487edfa68
Author: Tom Clegg <tom at curoverse.com>
Date:   Mon Feb 6 04:27:09 2017 -0500

    add nomad

diff --git a/services/boot/config.go b/services/boot/config.go
index 3ee981d..50e2850 100644
--- a/services/boot/config.go
+++ b/services/boot/config.go
@@ -34,6 +34,9 @@ type portsConfig struct {
 	ConsulSerfLAN int
 	ConsulSerfWAN int
 	ConsulServer  int
+	NomadHTTP     int
+	NomadRPC      int
+	NomadSerf     int
 	VaultServer   int
 }
 
@@ -91,6 +94,9 @@ func DefaultConfig() *Config {
 			ConsulSerfLAN: 18301,
 			ConsulSerfWAN: 18302,
 			ConsulServer:  18300,
+			NomadHTTP:     14646,
+			NomadRPC:      14647,
+			NomadSerf:     14648,
 			VaultServer:   18200,
 		},
 		DataDir:    "/var/lib/arvados",
diff --git a/services/boot/controller.go b/services/boot/controller.go
index 953507f..784f09f 100644
--- a/services/boot/controller.go
+++ b/services/boot/controller.go
@@ -24,7 +24,10 @@ func (c *controller) Boot(ctx context.Context) error {
 			},
 			consul,
 		},
-		vault,
+		Concurrent{
+			vault,
+			nomad,
+		},
 		Concurrent{
 			dispatchLocal,
 			dispatchSLURM,
diff --git a/services/boot/nomad.go b/services/boot/nomad.go
new file mode 100644
index 0000000..07c5de8
--- /dev/null
+++ b/services/boot/nomad.go
@@ -0,0 +1,99 @@
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+	"path"
+	"sync"
+
+	"github.com/hashicorp/nomad/api"
+)
+
+var nomad = &nomadBooter{}
+
+type nomadBooter struct {
+	sync.Mutex
+}
+
+func (nb *nomadBooter) Boot(ctx context.Context) error {
+	nb.Lock()
+	defer nb.Unlock()
+
+	if nb.check(ctx) == nil {
+		return nil
+	}
+	cfg := cfg(ctx)
+	bin := cfg.UsrDir + "/bin/nomad"
+	err := (&download{
+		URL:  "https://releases.hashicorp.com/nomad/0.5.4/nomad_0.5.4_linux_amd64.zip",
+		Dest: bin,
+		//Size: 29079005,
+		Mode: 0755,
+	}).Boot(ctx)
+	if err != nil {
+		return err
+	}
+
+	dataDir := path.Join(cfg.DataDir, "nomad")
+	if err := os.MkdirAll(dataDir, 0700); err != nil {
+		return err
+	}
+
+	cf := path.Join(cfg.DataDir, "nomad.json")
+	err = atomicWriteJSON(cf, map[string]interface{}{
+		"client": map[string]interface{}{
+			"enabled": true,
+			"options": map[string]interface{}{
+				"driver.raw_exec.enable": true,
+			},
+		},
+		"consul": map[string]interface{}{
+			"address": fmt.Sprintf("127.0.0.1:%d", cfg.Ports.ConsulHTTP),
+		},
+		"data_dir":   dataDir,
+		"datacenter": cfg.SiteID,
+		"ports": map[string]int{
+			"http": cfg.Ports.NomadHTTP,
+			"rpc":  cfg.Ports.NomadRPC,
+			"serf": cfg.Ports.NomadSerf,
+		},
+		"server": map[string]interface{}{
+			"enabled":          true,
+			"bootstrap_expect": len(cfg.ControlHosts),
+		},
+	}, 0644)
+	if err != nil {
+		return err
+	}
+
+	supervisor := newSupervisor(ctx, "arvados-nomad", bin, "agent", "-config="+cf)
+	running, err := supervisor.Running(ctx)
+	if err != nil {
+		return err
+	}
+	if !running {
+		defer feedbackf(ctx, "starting nomad service")()
+		err = supervisor.Start(ctx)
+		if err != nil {
+			return fmt.Errorf("starting nomad: %s", err)
+		}
+	}
+	return nb.check(ctx)
+}
+
+var nomadCfg = api.DefaultConfig()
+
+func (nb *nomadBooter) check(ctx context.Context) error {
+	cfg := cfg(ctx)
+	nomadCfg.Address = fmt.Sprintf("http://127.0.0.1:%d", cfg.Ports.NomadHTTP)
+	nomad, err := api.NewClient(nomadCfg)
+	if err != nil {
+		return err
+	}
+	_, err = nomad.Agent().Datacenter()
+	if err != nil {
+		return err
+	}
+	return nil
+}
diff --git a/services/boot/runit.go b/services/boot/runit.go
index ec906ad..a47db32 100644
--- a/services/boot/runit.go
+++ b/services/boot/runit.go
@@ -4,8 +4,6 @@ import (
 	"bytes"
 	"context"
 	"fmt"
-	"io/ioutil"
-	"os"
 	"path"
 )
 
@@ -20,7 +18,7 @@ func (r *runitService) Start(ctx context.Context) error {
 		return err
 	}
 
-	var script bytes.Buffer
+	script := &bytes.Buffer{}
 	fmt.Fprintf(script, "#!/bin/sh\n\nexec %q", r.cmd)
 	for _, arg := range r.args {
 		fmt.Fprintf(script, " %q", arg)

commit 51f5a4ede44615be700eec7fcbfe3c1e60c842ac
Author: Tom Clegg <tom at curoverse.com>
Date:   Fri Feb 3 13:15:02 2017 -0500

    tidy runit

diff --git a/services/boot/runit.go b/services/boot/runit.go
index e5bf520..ec906ad 100644
--- a/services/boot/runit.go
+++ b/services/boot/runit.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"io/ioutil"
@@ -18,29 +19,15 @@ func (r *runitService) Start(ctx context.Context) error {
 	if err := installRunit.Boot(ctx); err != nil {
 		return err
 	}
-	svdir := r.svdir(ctx)
-	if err := os.MkdirAll(svdir, 0755); err != nil {
-		return err
-	}
-	tmp, err := ioutil.TempFile(svdir, "run~")
-	if err != nil {
-		return err
-	}
-	fmt.Fprintf(tmp, "#!/bin/sh\n\nexec %q", r.cmd)
+
+	var script bytes.Buffer
+	fmt.Fprintf(script, "#!/bin/sh\n\nexec %q", r.cmd)
 	for _, arg := range r.args {
-		fmt.Fprintf(tmp, " %q", arg)
+		fmt.Fprintf(script, " %q", arg)
 	}
-	fmt.Fprintf(tmp, " 2>&1\n")
-	tmp.Close()
-	if err := os.Chmod(tmp.Name(), 0755); err != nil {
-		os.Remove(tmp.Name())
-		return err
-	}
-	if err := os.Rename(tmp.Name(), path.Join(svdir, "run")); err != nil {
-		os.Remove(tmp.Name())
-		return err
-	}
-	return nil
+	fmt.Fprintf(script, " 2>&1\n")
+
+	return atomicWriteFile(path.Join(r.svdir(ctx), "run"), script.Bytes(), 0755)
 }
 
 func (r *runitService) Running(ctx context.Context) (bool, error) {
diff --git a/services/boot/supervisor.go b/services/boot/supervisor.go
index 43909e8..8a46eb5 100644
--- a/services/boot/supervisor.go
+++ b/services/boot/supervisor.go
@@ -32,7 +32,7 @@ func newSupervisor(ctx context.Context, name, cmd string, args ...string) superv
 // supervised by systemd/runit/etc and registered with consul
 type supervisedService struct {
 	name string // name to register with consul
-	cmd  string // program to run (absolute path)
+	cmd  string // program to run (absolute path) -- if blank, use name
 	args []string
 }
 
diff --git a/services/boot/write_file.go b/services/boot/write_file.go
index d482f16..2a5de46 100644
--- a/services/boot/write_file.go
+++ b/services/boot/write_file.go
@@ -8,6 +8,9 @@ import (
 )
 
 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

commit 8a882eff8b8359d58b56f1bc7cd2da95775d97eb
Author: Tom Clegg <tom at curoverse.com>
Date:   Thu Feb 2 20:03:09 2017 -0500

    move import

diff --git a/services/boot/arvados_go.go b/services/boot/arvados_go.go
index 3d60dde..c4cd499 100644
--- a/services/boot/arvados_go.go
+++ b/services/boot/arvados_go.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"fmt"
 	"log"
-	"math/rand"
 	"os"
 	"path"
 	"path/filepath"
diff --git a/services/boot/supervisor.go b/services/boot/supervisor.go
index a8d8a8c..43909e8 100644
--- a/services/boot/supervisor.go
+++ b/services/boot/supervisor.go
@@ -3,6 +3,7 @@ package main
 import (
 	"context"
 	"log"
+	"math/rand"
 	"os"
 
 	"github.com/hashicorp/consul/api"
@@ -30,8 +31,8 @@ func newSupervisor(ctx context.Context, name, cmd string, args ...string) superv
 
 // supervised by systemd/runit/etc and registered with consul
 type supervisedService struct {
-	name string		// name to register with consul
-	cmd  string		// program to run (absolute path)
+	name string // name to register with consul
+	cmd  string // program to run (absolute path)
 	args []string
 }
 

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


hooks/post-receive
-- 




More information about the arvados-commits mailing list