cmd, lib
authorTom Clegg <tom@curoverse.com>
Mon, 20 Feb 2017 09:28:07 +0000 (04:28 -0500)
committerTom Clegg <tom@curoverse.com>
Mon, 20 Feb 2017 09:28:07 +0000 (04:28 -0500)
16 files changed:
cmd/arvados-admin/main.go [new file with mode: 0644]
cmd/arvados-admin/setup_debian8_test.go [new file with mode: 0644]
cmd/arvados-admin/test-debian8/Dockerfile [new file with mode: 0644]
cmd/dispatch.go [new file with mode: 0644]
lib/agent/agent.go [new file with mode: 0644]
lib/setup/check.go [new file with mode: 0644]
lib/setup/command.go [new file with mode: 0644]
lib/setup/consul.go [new file with mode: 0644]
lib/setup/daemon.go [new file with mode: 0644]
lib/setup/download.go [new file with mode: 0644]
lib/setup/os_package.go [new file with mode: 0644]
lib/setup/runit.go [new file with mode: 0644]
lib/setup/setup.go [new file with mode: 0644]
lib/setup/systemd.go [new file with mode: 0644]
lib/setup/write_file.go [new file with mode: 0644]
services/boot/testimage_runit/Dockerfile

diff --git a/cmd/arvados-admin/main.go b/cmd/arvados-admin/main.go
new file mode 100644 (file)
index 0000000..de532cb
--- /dev/null
@@ -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 (file)
index 0000000..671f8c7
--- /dev/null
@@ -0,0 +1,42 @@
+package main
+
+import (
+       "log"
+       "net"
+       "os"
+       "os/exec"
+       "testing"
+)
+
+func TestSetupDebian8(t *testing.T) {
+       cwd, err := os.Getwd()
+       if err != nil {
+               t.Fatal(err)
+       }
+       ln, err := net.Listen("tcp", ":")
+       if err != nil {
+               t.Fatal(err)
+       }
+       _, port, err := net.SplitHostPort(ln.Addr().String())
+       if err != nil {
+               t.Fatal(err)
+       }
+       err = ln.Close()
+       if err != nil {
+               t.Fatal(err)
+       }
+       log.Printf("Publishing consul webgui at %v", ln.Addr())
+       for _, cmdline := range [][]string{
+               {"go", "build"},
+               {"docker", "build", "--tag=arvados-admin-debian8-test", "test-debian8"},
+               {"docker", "run", "--rm", "--publish=" + port + ":18500", "--cap-add=IPC_LOCK", "--cap-add=SYS_ADMIN", "--volume=/sys/fs/cgroup", "--volume=" + cwd + "/arvados-admin:/usr/bin/arvados-admin:ro", "--volume=/var/cache/arvados:/var/cache/arvados:ro", "arvados-admin-debian8-test"},
+       } {
+               cmd := exec.Command(cmdline[0], cmdline[1:]...)
+               cmd.Stdout = os.Stderr
+               cmd.Stderr = os.Stderr
+               err = cmd.Run()
+               if err != nil {
+                       t.Fatal(err)
+               }
+       }
+}
diff --git a/cmd/arvados-admin/test-debian8/Dockerfile b/cmd/arvados-admin/test-debian8/Dockerfile
new file mode 100644 (file)
index 0000000..645aec2
--- /dev/null
@@ -0,0 +1,14 @@
+FROM debian:8
+RUN apt-get update
+
+# preload (but don't install) packages arvados-boot might decide to install
+RUN DEBIAN_FRONTEND=noninteractive apt-get -dy install --no-install-recommends ca-certificates locales nginx postgresql runit
+
+RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends runit locales
+
+RUN ["bash", "-c", "echo en_US.utf8 UTF-8 | tee -a /etc/locale.gen && locale-gen -a && \
+    (echo LANG=en_US.UTF-8; echo LC_ALL=en_US.UTF-8) > /etc/default/locale"]
+
+RUN DEBIAN_FRONTEND=noninteractive apt-get -y install --no-install-recommends ca-certificates locales nginx postgresql runit
+
+CMD ["bash", "-c", "runsvdir /etc/sv & arvados-admin setup && arvados-admin setup"]
diff --git a/cmd/dispatch.go b/cmd/dispatch.go
new file mode 100644 (file)
index 0000000..2fc2a70
--- /dev/null
@@ -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 (file)
index 0000000..52a3d83
--- /dev/null
@@ -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 (file)
index 0000000..2b12c1f
--- /dev/null
@@ -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 (file)
index 0000000..79c28b7
--- /dev/null
@@ -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 (file)
index 0000000..0ebff83
--- /dev/null
@@ -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 (file)
index 0000000..3985259
--- /dev/null
@@ -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 (file)
index 0000000..550f6cb
--- /dev/null
@@ -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 (file)
index 0000000..106945b
--- /dev/null
@@ -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 (file)
index 0000000..3e9f347
--- /dev/null
@@ -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 (file)
index 0000000..935db1a
--- /dev/null
@@ -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 (file)
index 0000000..bc3502f
--- /dev/null
@@ -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 (file)
index 0000000..b50079c
--- /dev/null
@@ -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)
+}
index aaf5b88f3d2e388083095e607b6213a0c75133ce..e60686838addbe1a029a6c3b3003ad6081025939 100644 (file)
@@ -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"]