Merge branch '17344-easy-demo'
authorTom Clegg <tom@curii.com>
Thu, 21 Jul 2022 14:36:35 +0000 (10:36 -0400)
committerTom Clegg <tom@curii.com>
Thu, 21 Jul 2022 14:36:35 +0000 (10:36 -0400)
refs #17344

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@curii.com>

cmd/arvados-client/cmd.go
cmd/arvados-client/sudo.go [new file with mode: 0644]
cmd/arvados-package/fpm.go
doc/install/automatic.html.textile.liquid
lib/boot/passenger.go
lib/cli/external.go
lib/diagnostics/cmd.go
lib/install/arvados.service
lib/install/deps.go
lib/install/init.go

index cb15462119d4d1d3368382e1cbbcceafba40464f..c10783c978be6c48e6f7aea0c9ad1a959303b656 100644 (file)
@@ -61,6 +61,7 @@ var (
                "shell":                shellCommand{},
                "connect-ssh":          connectSSHCommand{},
                "diagnostics":          diagnostics.Command{},
+               "sudo":                 sudoCommand{},
        })
 )
 
diff --git a/cmd/arvados-client/sudo.go b/cmd/arvados-client/sudo.go
new file mode 100644 (file)
index 0000000..94c2d27
--- /dev/null
@@ -0,0 +1,48 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: Apache-2.0
+
+package main
+
+import (
+       "flag"
+       "fmt"
+       "io"
+       "os"
+
+       "git.arvados.org/arvados.git/lib/cmd"
+       "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+)
+
+// sudoCommand runs another command using API connection info and
+// SystemRootToken from the system config file instead of the caller's
+// environment vars.
+type sudoCommand struct{}
+
+func (sudoCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
+       ldr := config.NewLoader(stdin, ctxlog.New(stderr, "text", "info"))
+       flags := flag.NewFlagSet(prog, flag.ContinueOnError)
+       ldr.SetupFlags(flags)
+       if ok, code := cmd.ParseFlags(flags, prog, args, "subcommand ...", stderr); !ok {
+               return code
+       }
+       cfg, err := ldr.Load()
+       if err != nil {
+               fmt.Fprintln(stderr, err)
+               return 1
+       }
+       cluster, err := cfg.GetCluster("")
+       if err != nil {
+               fmt.Fprintln(stderr, err)
+               return 1
+       }
+       os.Setenv("ARVADOS_API_HOST", cluster.Services.Controller.ExternalURL.Host)
+       os.Setenv("ARVADOS_API_TOKEN", cluster.SystemRootToken)
+       if cluster.TLS.Insecure {
+               os.Setenv("ARVADOS_API_HOST_INSECURE", "1")
+       } else {
+               os.Unsetenv("ARVADOS_API_HOST_INSECURE")
+       }
+       return handler.RunCommand(prog, flags.Args(), stdin, stdout, stderr)
+}
index 9a20ec76fc5c8058423403ed9e6af6d9c7517b62..64d0adabe39190452891db68ee6eeb91d20717ee 100644 (file)
@@ -34,15 +34,7 @@ func fpm(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writ
                return fmt.Errorf("arvados-server install failed: exit code %d", exitcode)
        }
 
-       cmd := exec.Command("/var/lib/arvados/bin/gem", "install", "--user", "--no-document", "fpm")
-       cmd.Stdout = stdout
-       cmd.Stderr = stderr
-       err := cmd.Run()
-       if err != nil {
-               return fmt.Errorf("gem install fpm: %w", err)
-       }
-
-       cmd = exec.Command("/var/lib/arvados/bin/gem", "env", "gempath")
+       cmd := exec.Command("/var/lib/arvados/bin/gem", "env", "gempath")
        cmd.Stderr = stderr
        buf, err := cmd.Output() // /root/.gem/ruby/2.7.0:...
        if err != nil || len(buf) == 0 {
@@ -50,6 +42,17 @@ func fpm(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writ
        }
        gempath := string(bytes.TrimRight(bytes.Split(buf, []byte{':'})[0], "\n"))
 
+       cmd = exec.Command("/var/lib/arvados/bin/gem", "install", "--user", "--no-document", "fpm")
+       cmd.Stdout = stdout
+       cmd.Stderr = stderr
+       // Avoid "WARNING: You don't have [...] in your PATH, gem
+       // executables will not run"
+       cmd.Env = append(os.Environ(), "PATH="+os.Getenv("PATH")+":"+gempath)
+       err = cmd.Run()
+       if err != nil {
+               return fmt.Errorf("gem install fpm: %w", err)
+       }
+
        if _, err := os.Stat(gempath + "/gems/fpm-1.11.0/lib/fpm/package/deb.rb"); err == nil {
                // Workaround for fpm bug https://github.com/jordansissel/fpm/issues/1739
                cmd = exec.Command("sed", "-i", `/require "digest"/a require "zlib"`, gempath+"/gems/fpm-1.11.0/lib/fpm/package/deb.rb")
@@ -97,16 +100,28 @@ func fpm(ctx context.Context, opts opts, stdin io.Reader, stdout, stderr io.Writ
                "--verbose",
                "--deb-use-file-permissions",
                "--rpm-use-file-permissions",
-               "--deb-systemd", "/etc/systemd/system/multi-user.target.wants/arvados.service",
+               "--deb-systemd", "/lib/systemd/system/arvados.service",
                "--deb-systemd-enable",
                "--no-deb-systemd-auto-start",
                "--no-deb-systemd-restart-after-upgrade",
-               "/lib/systemd/system/arvados.service",
+               "--deb-suggests", "postgresql",
+               "--deb-suggests", "docker.io",
                "/usr/bin/arvados-client",
                "/usr/bin/arvados-server",
                "/usr/bin/arv",
+               "/usr/bin/arv-ruby",
                "/usr/bin/arv-tag",
                "/var/lib/arvados",
+               "/usr/bin/arv-copy",
+               "/usr/bin/arv-federation-migrate",
+               "/usr/bin/arv-get",
+               "/usr/bin/arv-keepdocker",
+               "/usr/bin/arv-ls",
+               "/usr/bin/arv-migrate-docker19",
+               "/usr/bin/arv-normalize",
+               "/usr/bin/arv-put",
+               "/usr/bin/arv-ws",
+               "/usr/bin/arv-mount",
                "/var/www/.gem",
                "/var/www/.passenger",
                "/var/www/.bundle",
index d72f8f69eedf70ce6bfa4e32e16f9f06451fb8cf..398ebc20e024c745f4ba49a0b5ecab260ce215e8 100644 (file)
@@ -22,26 +22,74 @@ h2. Prerequisites
 You will need:
 * a server host running Debian 10 (buster) or Debian 11 (bullseye).
 * a unique 5-character ID like @x9999@ for your cluster (first character should be @[a-w]@ for a long-lived / production cluster; all characters are @[a-z0-9]@).
-* a DNS name like @x9999.example.com@ that resolves to your server host (or a load balancer / proxy that passes HTTP and HTTPS requests through to your server host).
-* a Google account (use it in place of <code>example@gmail.com.example</code> in the instructions below).
+* a DNS name like @x9999.example.com@ that resolves to your server host (or a load balancer / proxy that passes HTTP requests on port 80[1] and HTTPS requests on ports 443 and 4440-4460 through to the same port on your server host).
+* a firewall setup that allows incoming connections to ports 80[1], 443, and 4440-4460.
+
+fn1. Port 80 is only used to obtain TLS certificates automatically from Let's Encrypt. It is not needed if you have another way to provision certificates.
+
+h2. Options
+
+Arvados needs a PostgreSQL database. To get started quickly, install the postgresql-server package on your server host.
+
+<pre>
+# apt install postgresql
+</pre>
+
+Arvados normally uses cloud VMs or a Slurm/LSF cluster to run containers. To get started quickly, install Docker on your system host. The @arvados-server init@ command, as shown below, will configure Arvados to run containers on the system host.
+
+<pre>
+# apt install docker.io
+</pre>
+
+Arvados needs a login backend. To get started quickly, add a user account on your server host and assign a password. The @arvados-server init ... -login pam@ option, as shown below, will configure Arvados so you can log in with this username and password.
+
+<pre>
+# adduser exampleUserName
+</pre>
 
 h2. Initialize the cluster
 
 <pre>
-# echo > /etc/apt/sources.list.d/arvados.list "deb http://apt.arvados.org/buster buster main"
-# apt-get update
-# apt-get install arvados-server-easy
-# arvados-server init -cluster-id x9999 -domain x9999.example.com -tls acme -admin-email example@gmail.com.example
+# echo > /etc/apt/sources.list.d/arvados.list "deb http://apt.arvados.org/$(lsb_release -sc) $(lsb_release -sc) main"
+# apt update
+# apt install arvados-server-easy
+# arvados-server init -cluster-id x9999 -domain x9999.example.com -tls acme -login pam
 </pre>
 
-When the "init" command is finished, navigate to the link shown in the terminal (e.g., @https://x9999.example.com/token?api_token=zzzzzzzzzzzzzzzzzzzzzz@). This will log you in to your admin account.
+When the "init" command is finished, navigate to the link shown in the terminal (e.g., @https://x9999.example.com/@) and log in with the account you created above.
 
-h2. Enable login
+Activate your new Arvados user account. Copy the UUID (looks like @x9999-tpzed-xxxxxxxxxxxxxxx@) from your browser's location bar and run:
 
-Follow the instructions to "set up Google login":{{site.baseurl}}/install/setup-login.html or another authentication option.
+<pre>
+# arv sudo user setup --uuid x9999-tpzed-xxxxxxxxxxxxxxx
+</pre>
+
+Run the diagnostics tool to ensure everything is working.
+
+<pre>
+# arv sudo diagnostics
+</pre>
+
+h2. Customize the cluster
+
+Things you should plan to update before using your cluster in production:
+* "Set up Google login":{{site.baseurl}}/install/setup-login.html or another authentication option.
+* "Set up a wildcard TLS certificate and DNS name,":{{site.baseurl}}/install/install-manual-prerequisites.html#dnstls or enable @TrustAllContent@ mode.
+* Update storage configuration to use a cloud storage bucket ("S3":{{site.baseurl}}/install/configure-s3-object-storage.html or "Azure":{{site.baseurl}}/install/configure-azure-blob-storage.html) instead of the local filesystem.
+* Update "CloudVMs configuration":{{site.baseurl}}/install/crunch2-cloud/install-dispatch-cloud.html to use a cloud provider to bring up VMs on demand instead of running containers on the server host.
+
+h2. Updating configuration
+
+After updating your configuration file (@/etc/arvados/config.yml@), notify the server:
+
+<pre>
+# systemctl reload arvados-server
+</pre>
 
-After updating your configuration file (@/etc/arvados/config.yml@), restart the server to make your changes take effect:
+Optionally, add "AutoReloadConfig: true" at the top of @/etc/arvados/config.yml@. Arvados will automatically reload the config file when it changes.
 
 <pre>
-# systemctl restart arvados-server
+AutoReloadConfig: true
+Clusters:
+  [...]
 </pre>
index e0eec5ee4a047a27fba930d3f2b60346caa1e19c..5367337e81a1d0e605160c9c0dd789c9282a21f2 100644 (file)
@@ -10,6 +10,7 @@ import (
        "fmt"
        "os"
        "path/filepath"
+       "runtime"
        "strings"
        "sync"
 
@@ -96,7 +97,7 @@ func (runner installPassenger) Run(ctx context.Context, fail func(error), super
        if err != nil {
                return err
        }
-       err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "install", "--jobs", "4")
+       err = super.RunProgram(ctx, appdir, runOptions{}, "bundle", "install", "--jobs", fmt.Sprintf("%d", runtime.NumCPU()))
        if err != nil {
                return err
        }
index 7d9bb6f200253405b15fbd3edaed82132efe61e4..54dfd9a91f9fff8429da645d39ee9029e5571335 100644 (file)
@@ -57,7 +57,7 @@ func (cmd apiCallCmd) RunCommand(prog string, args []string, stdin io.Reader, st
                return 2
        }
        model := split[len(split)-1]
-       return externalCmd{"arv"}.RunCommand("arv", legacyFlagsToFront(model, args), stdin, stdout, stderr)
+       return rubyArvCmd{model}.RunCommand(prog, args, stdin, stdout, stderr)
 }
 
 type rubyArvCmd struct {
@@ -65,7 +65,18 @@ type rubyArvCmd struct {
 }
 
 func (rc rubyArvCmd) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
-       return externalCmd{"arv"}.RunCommand("arv", legacyFlagsToFront(rc.subcommand, args), stdin, stdout, stderr)
+       wrapprog := "arv-ruby"
+       if _, err := exec.LookPath(wrapprog); err != nil && !strings.Contains(prog, "arv ") {
+               // arv-ruby isn't in PATH (i.e., installation method
+               // wasn't a recent "arvados-server install", which
+               // symlinks /usr/bin/arv-ruby ->
+               // /var/lib/arvados/bin/arv), so fall back to looking
+               // for the arvados-cli program as "arv". (But don't do
+               // this if we are being run as "arv" -- that would
+               // probably cause a recursive fork bomb.)
+               wrapprog = "arv"
+       }
+       return externalCmd{wrapprog}.RunCommand(wrapprog, legacyFlagsToFront(rc.subcommand, args), stdin, stdout, stderr)
 }
 
 type externalCmd struct {
@@ -90,7 +101,7 @@ func (ec externalCmd) RunCommand(prog string, args []string, stdin io.Reader, st
                return 1
        case *exec.Error:
                fmt.Fprintln(stderr, err)
-               if ec.prog == "arv" {
+               if ec.prog == "arv" || ec.prog == "arv-ruby" {
                        fmt.Fprint(stderr, rubyInstallHints)
                } else if strings.HasPrefix(ec.prog, "arv-") {
                        fmt.Fprint(stderr, pythonInstallHints)
index 71fe1c5dc60c8501353da59f5f49df31a2f7f805..3455d3307e71bff663aa44ea3023bf7b71efca26 100644 (file)
@@ -30,6 +30,7 @@ func (Command) RunCommand(prog string, args []string, stdin io.Reader, stdout, s
        f := flag.NewFlagSet(prog, flag.ContinueOnError)
        f.StringVar(&diag.projectName, "project-name", "scratch area for diagnostics", "name of project to find/create in home project and use for temporary/test objects")
        f.StringVar(&diag.logLevel, "log-level", "info", "logging level (debug, info, warning, error)")
+       f.StringVar(&diag.dockerImage, "docker-image", "alpine:latest", "image to use when running a test container")
        f.BoolVar(&diag.checkInternal, "internal-client", false, "check that this host is considered an \"internal\" client")
        f.BoolVar(&diag.checkExternal, "external-client", false, "check that this host is considered an \"external\" client")
        f.IntVar(&diag.priority, "priority", 500, "priority for test container (1..1000, or 0 to skip)")
@@ -60,6 +61,7 @@ type diagnoser struct {
        logLevel      string
        priority      int
        projectName   string
+       dockerImage   string
        checkInternal bool
        checkExternal bool
        timeout       time.Duration
@@ -132,6 +134,7 @@ func (diag *diagnoser) runtests() {
 
        var cluster arvados.Cluster
        cfgpath := "arvados/v1/config"
+       cfgOK := false
        diag.dotest(20, fmt.Sprintf("getting exported config from https://%s/%s", client.APIHost, cfgpath), func() error {
                ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(diag.timeout))
                defer cancel()
@@ -140,6 +143,7 @@ func (diag *diagnoser) runtests() {
                        return err
                }
                diag.debugf("Collections.BlobSigning = %v", cluster.Collections.BlobSigning)
+               cfgOK = true
                return nil
        })
 
@@ -155,6 +159,11 @@ func (diag *diagnoser) runtests() {
                return nil
        })
 
+       if !cfgOK {
+               diag.errorf("cannot proceed without cluster config -- aborting without running any further tests")
+               return
+       }
+
        // uncomment to create some spurious errors
        // cluster.Services.WebDAVDownload.ExternalURL.Host = "0.0.0.0:9"
 
@@ -388,29 +397,35 @@ func (diag *diagnoser) runtests() {
        })
 
        davurl := cluster.Services.WebDAV.ExternalURL
+       davWildcard := strings.HasPrefix(davurl.Host, "*--") || strings.HasPrefix(davurl.Host, "*.")
        diag.dotest(110, fmt.Sprintf("checking WebDAV ExternalURL wildcard (%s)", davurl), func() error {
                if davurl.Host == "" {
                        return fmt.Errorf("host missing - content previews will not work")
                }
-               if !strings.HasPrefix(davurl.Host, "*--") && !strings.HasPrefix(davurl.Host, "*.") && !cluster.Collections.TrustAllContent {
+               if !davWildcard && !cluster.Collections.TrustAllContent {
                        diag.warnf("WebDAV ExternalURL has no leading wildcard and TrustAllContent==false - content previews will not work")
                }
                return nil
        })
 
        for i, trial := range []struct {
-               needcoll bool
-               status   int
-               fileurl  string
+               needcoll     bool
+               needWildcard bool
+               status       int
+               fileurl      string
        }{
-               {false, http.StatusNotFound, strings.Replace(davurl.String(), "*", "d41d8cd98f00b204e9800998ecf8427e-0", 1) + "foo"},
-               {false, http.StatusNotFound, strings.Replace(davurl.String(), "*", "d41d8cd98f00b204e9800998ecf8427e-0", 1) + "testfile"},
-               {false, http.StatusNotFound, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=d41d8cd98f00b204e9800998ecf8427e+0/_/foo"},
-               {false, http.StatusNotFound, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=d41d8cd98f00b204e9800998ecf8427e+0/_/testfile"},
-               {true, http.StatusOK, strings.Replace(davurl.String(), "*", strings.Replace(collection.PortableDataHash, "+", "-", -1), 1) + "testfile"},
-               {true, http.StatusOK, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=" + collection.UUID + "/_/testfile"},
+               {false, false, http.StatusNotFound, strings.Replace(davurl.String(), "*", "d41d8cd98f00b204e9800998ecf8427e-0", 1) + "foo"},
+               {false, false, http.StatusNotFound, strings.Replace(davurl.String(), "*", "d41d8cd98f00b204e9800998ecf8427e-0", 1) + "testfile"},
+               {false, false, http.StatusNotFound, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=d41d8cd98f00b204e9800998ecf8427e+0/_/foo"},
+               {false, false, http.StatusNotFound, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=d41d8cd98f00b204e9800998ecf8427e+0/_/testfile"},
+               {true, true, http.StatusOK, strings.Replace(davurl.String(), "*", strings.Replace(collection.PortableDataHash, "+", "-", -1), 1) + "testfile"},
+               {true, false, http.StatusOK, cluster.Services.WebDAVDownload.ExternalURL.String() + "c=" + collection.UUID + "/_/testfile"},
        } {
                diag.dotest(120+i, fmt.Sprintf("downloading from webdav (%s)", trial.fileurl), func() error {
+                       if trial.needWildcard && !davWildcard {
+                               diag.warnf("skipping collection-id-in-vhost test because WebDAV ExternalURL has no leading wildcard")
+                               return nil
+                       }
                        ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(diag.timeout))
                        defer cancel()
                        if trial.needcoll && collection.UUID == "" {
@@ -450,9 +465,10 @@ func (diag *diagnoser) runtests() {
                        return err
                }
                if len(vmlist.Items) < 1 {
-                       return fmt.Errorf("no VMs found")
+                       diag.warnf("no VMs found")
+               } else {
+                       vm = vmlist.Items[0]
                }
-               vm = vmlist.Items[0]
                return nil
        })
 
@@ -460,7 +476,8 @@ func (diag *diagnoser) runtests() {
                ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(diag.timeout))
                defer cancel()
                if vm.UUID == "" {
-                       return fmt.Errorf("skipping, no vm available")
+                       diag.warnf("skipping, no vm available")
+                       return nil
                }
                webshelltermurl := cluster.Services.Workbench1.ExternalURL.String() + "virtual_machines/" + vm.UUID + "/webshell/testusername"
                diag.debugf("url %s", webshelltermurl)
@@ -488,7 +505,8 @@ func (diag *diagnoser) runtests() {
                ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(diag.timeout))
                defer cancel()
                if vm.UUID == "" {
-                       return fmt.Errorf("skipping, no vm available")
+                       diag.warnf("skipping, no vm available")
+                       return nil
                }
                u := cluster.Services.WebShell.ExternalURL
                webshellurl := u.String() + vm.Hostname + "?"
@@ -545,7 +563,7 @@ func (diag *diagnoser) runtests() {
                err := client.RequestAndDecodeContext(ctx, &cr, "POST", "arvados/v1/container_requests", nil, map[string]interface{}{"container_request": map[string]interface{}{
                        "owner_uuid":      project.UUID,
                        "name":            fmt.Sprintf("diagnostics container request %s", timestamp),
-                       "container_image": "arvados/jobs",
+                       "container_image": diag.dockerImage,
                        "command":         []string{"echo", timestamp},
                        "use_existing":    false,
                        "output_path":     "/mnt/output",
index cb411c63a5407aec56a95735343752ee1b560957..3b68f31e9fac07208c8b4dcff46ee58f4e99deda 100644 (file)
@@ -15,10 +15,11 @@ StartLimitIntervalSec=0
 Type=notify
 EnvironmentFile=-/etc/arvados/environment
 ExecStart=/usr/bin/arvados-server boot
-# Set a reasonable default for the open file limit
-LimitNOFILE=65536
+ExecReload=/usr/bin/arvados-server config-check
+ExecReload=kill -HUP $MAINPID
 Restart=always
 RestartSec=1
+LimitNOFILE=65536
 
 # systemd<=219 (centos:7, debian:8, ubuntu:trusty) obeys StartLimitInterval in the [Service] section
 StartLimitInterval=0
index 6e76b82f99b7593c29878b9e71370e83dd1092b7..27b8d1dc8aedb70c9c5720ace830bc3bbaabd10a 100644 (file)
@@ -17,6 +17,7 @@ import (
        "os/exec"
        "os/user"
        "path/filepath"
+       "runtime"
        "strconv"
        "strings"
        "syscall"
@@ -189,6 +190,7 @@ func (inst *installCommand) RunCommand(prog string, args []string, stdin io.Read
                        "uuid-dev",
                        "wget",
                        "xvfb",
+                       "zlib1g-dev", // services/api
                )
                if test {
                        if osv.Debian && osv.Major <= 10 {
@@ -203,11 +205,13 @@ func (inst *installCommand) RunCommand(prog string, args []string, stdin io.Read
                }
                switch {
                case osv.Debian && osv.Major >= 11:
-                       pkgs = append(pkgs, "libcurl4", "perl-modules-5.32")
+                       pkgs = append(pkgs, "g++", "libcurl4", "libcurl4-openssl-dev", "perl-modules-5.32")
                case osv.Debian && osv.Major >= 10:
-                       pkgs = append(pkgs, "libcurl4", "perl-modules")
-               default:
-                       pkgs = append(pkgs, "libcurl3", "perl-modules")
+                       pkgs = append(pkgs, "g++", "libcurl4", "libcurl4-openssl-dev", "perl-modules")
+               case osv.Debian || osv.Ubuntu:
+                       pkgs = append(pkgs, "g++", "libcurl3", "libcurl3-openssl-dev", "perl-modules")
+               case osv.Centos:
+                       pkgs = append(pkgs, "gcc", "gcc-c++", "libcurl-devel", "postgresql-devel")
                }
                cmd := exec.CommandContext(ctx, "apt-get")
                if inst.EatMyData {
@@ -563,7 +567,6 @@ yarn install
                for _, srcdir := range []string{
                        "cmd/arvados-client",
                        "cmd/arvados-server",
-                       "services/crunch-dispatch-slurm",
                } {
                        fmt.Fprintf(stderr, "building %s...\n", srcdir)
                        cmd := exec.Command("go", "install", "-ldflags", "-X git.arvados.org/arvados.git/lib/cmd.version="+inst.PackageVersion+" -X main.version="+inst.PackageVersion+" -s -w")
@@ -587,6 +590,33 @@ yarn install
                        return 1
                }
 
+               // Install python SDK and arv-mount in
+               // /var/lib/arvados/lib/python.
+               //
+               // setup.py writes a file in the source directory in
+               // order to include the version number in the package
+               // itself.  We don't want to write to the source tree
+               // (in "arvados-package" context it's mounted
+               // readonly) so we run setup.py in a temporary copy of
+               // the source dir.
+               if err = inst.runBash(`
+v=/var/lib/arvados/lib/python
+tmp=/var/lib/arvados/tmp/python
+python3 -m venv "$v"
+. "$v/bin/activate"
+pip3 install --no-cache-dir 'setuptools>=18.5' 'pip>=7'
+export ARVADOS_BUILDING_VERSION="`+inst.PackageVersion+`"
+for src in "`+inst.SourcePath+`/sdk/python" "`+inst.SourcePath+`/services/fuse"; do
+  rsync -a --delete-after "$src/" "$tmp/"
+  cd "$tmp"
+  python3 setup.py install
+  cd ..
+  rm -rf "$tmp"
+done
+`, stdout, stderr); err != nil {
+                       return 1
+               }
+
                // Install Rails apps to /var/lib/arvados/{railsapi,workbench1}/
                for dstdir, srcdir := range map[string]string{
                        "railsapi":   "services/api",
@@ -615,7 +645,10 @@ yarn install
                                {"touch", "log/production.log"},
                                {"chown", "-R", "--from=root", "www-data:www-data", "/var/www/.bundle", "/var/www/.gem", "/var/www/.npm", "/var/www/.passenger", "log", "tmp", "vendor", ".bundle", "Gemfile.lock", "config.ru", "config/environment.rb"},
                                {"sudo", "-u", "www-data", "/var/lib/arvados/bin/gem", "install", "--user", "--conservative", "--no-document", "bundler:" + bundlerversion},
-                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "install", "--deployment", "--jobs", "8", "--path", "/var/www/.gem", "--without", "development test diagnostics performance"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "config", "set", "--local", "deployment", "true"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "config", "set", "--local", "path", "/var/www/.gem"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "config", "set", "--local", "without", "development test diagnostics performance"},
+                               {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "install", "--jobs", fmt.Sprintf("%d", runtime.NumCPU())},
 
                                {"chown", "www-data:www-data", ".", "public/assets"},
                                // {"sudo", "-u", "www-data", "/var/lib/arvados/bin/bundle", "config", "set", "--local", "system", "true"},
@@ -675,27 +708,56 @@ rsync -a --delete-after build/ /var/lib/arvados/workbench2/
                if err != nil {
                        return 1
                }
-               // This is equivalent to "systemd enable", but does
-               // not depend on the systemctl program being
-               // available.
-               symlink := "/etc/systemd/system/multi-user.target.wants/arvados.service"
-               err = os.Remove(symlink)
-               if err != nil && !errors.Is(err, os.ErrNotExist) {
-                       return 1
-               }
-               err = os.Symlink("/lib/systemd/system/arvados.service", symlink)
-               if err != nil {
-                       return 1
+               if prod {
+                       // (fpm will do this for us in the pkg case)
+                       // This is equivalent to "systemd enable", but
+                       // does not depend on the systemctl program
+                       // being available:
+                       symlink := "/etc/systemd/system/multi-user.target.wants/arvados.service"
+                       err = os.Remove(symlink)
+                       if err != nil && !errors.Is(err, os.ErrNotExist) {
+                               return 1
+                       }
+                       err = os.Symlink("/lib/systemd/system/arvados.service", symlink)
+                       if err != nil {
+                               return 1
+                       }
                }
 
-               // Symlink user-facing programs /usr/bin/x ->
-               // /var/lib/arvados/bin/x
-               for _, prog := range []string{"arvados-client", "arvados-server", "arv", "arv-tag"} {
-                       err = os.Remove("/usr/bin/" + prog)
+               // Add symlinks in /usr/bin for user-facing programs
+               for _, srcdst := range [][]string{
+                       // go
+                       {"bin/arvados-client"},
+                       {"bin/arvados-client", "arv"},
+                       {"bin/arvados-server"},
+                       // sdk/cli
+                       {"bin/arv", "arv-ruby"},
+                       {"bin/arv-tag"},
+                       // sdk/python
+                       {"lib/python/bin/arv-copy"},
+                       {"lib/python/bin/arv-federation-migrate"},
+                       {"lib/python/bin/arv-get"},
+                       {"lib/python/bin/arv-keepdocker"},
+                       {"lib/python/bin/arv-ls"},
+                       {"lib/python/bin/arv-migrate-docker19"},
+                       {"lib/python/bin/arv-normalize"},
+                       {"lib/python/bin/arv-put"},
+                       {"lib/python/bin/arv-ws"},
+                       // services/fuse
+                       {"lib/python/bin/arv-mount"},
+               } {
+                       src := "/var/lib/arvados/" + srcdst[0]
+                       if _, err = os.Stat(src); err != nil {
+                               return 1
+                       }
+                       dst := srcdst[len(srcdst)-1]
+                       _, dst = filepath.Split(dst)
+                       dst = "/usr/bin/" + dst
+                       err = os.Remove(dst)
                        if err != nil && !errors.Is(err, os.ErrNotExist) {
                                return 1
                        }
-                       err = os.Symlink("/var/lib/arvados/bin/"+prog, "/usr/bin/"+prog)
+                       err = os.Symlink(src, dst)
                        if err != nil {
                                return 1
                        }
@@ -798,7 +860,7 @@ func prodpkgs(osv osversion) []string {
                "libcurl3-gnutls",
                "libxslt1.1",
                "nginx",
-               "python",
+               "python3",
                "sudo",
        }
        if osv.Debian || osv.Ubuntu {
@@ -808,21 +870,12 @@ func prodpkgs(osv osversion) []string {
                        pkgs = append(pkgs, "python3-distutils") // sdk/cwl
                }
                return append(pkgs,
-                       "g++",
-                       "libcurl4-openssl-dev", // services/api
-                       "libpq-dev",
-                       "libpython2.7", // services/fuse
                        "mime-support", // keep-web
-                       "zlib1g-dev",   // services/api
                )
        } else if osv.Centos {
                return append(pkgs,
                        "fuse-libs", // services/fuse
-                       "gcc",
-                       "gcc-c++",
-                       "libcurl-devel",    // services/api
-                       "mailcap",          // keep-web
-                       "postgresql-devel", // services/api
+                       "mailcap",   // keep-web
                )
        } else {
                panic("os version not supported")
index d322e753ebd912152f67c7922bee0cb7998bfe9e..36501adf81e60c6b4f9c8f7919f9ee57c5c13581 100644 (file)
@@ -13,17 +13,24 @@ import (
        "flag"
        "fmt"
        "io"
+       "net"
+       "net/http"
+       "net/url"
        "os"
        "os/exec"
        "os/user"
        "regexp"
        "strconv"
        "strings"
+       "sync/atomic"
        "text/template"
+       "time"
 
        "git.arvados.org/arvados.git/lib/cmd"
        "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/lib/controller/rpc"
        "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/auth"
        "git.arvados.org/arvados.git/sdk/go/ctxlog"
        "github.com/lib/pq"
 )
@@ -31,14 +38,20 @@ import (
 var InitCommand cmd.Handler = &initCommand{}
 
 type initCommand struct {
-       ClusterID          string
-       Domain             string
-       PostgreSQLPassword string
-       Login              string
-       TLS                string
-       AdminEmail         string
-       Start              bool
+       ClusterID  string
+       Domain     string
+       CreateDB   bool
+       Login      string
+       TLS        string
+       AdminEmail string
+       Start      bool
 
+       PostgreSQL struct {
+               Host     string
+               User     string
+               Password string
+               DB       string
+       }
        LoginPAM                bool
        LoginTest               bool
        LoginGoogle             bool
@@ -70,6 +83,7 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
        flags.SetOutput(stderr)
        versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
        flags.StringVar(&initcmd.ClusterID, "cluster-id", "", "cluster `id`, like x1234 for a dev cluster")
+       flags.BoolVar(&initcmd.CreateDB, "create-db", true, "create an 'arvados' postgresql role and database using 'sudo -u postgres psql ...' (if false, use existing database specified by POSTGRES_HOST, POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB env vars, and assume 'CREATE EXTENSION IF NOT EXISTS pg_trgm' has already been done)")
        flags.StringVar(&initcmd.Domain, "domain", hostname, "cluster public DNS `name`, like x1234.arvadosapi.com")
        flags.StringVar(&initcmd.Login, "login", "", "login `backend`: test, pam, 'google {client-id} {client-secret}', or ''")
        flags.StringVar(&initcmd.AdminEmail, "admin-email", "", "give admin privileges to user with given `email`")
@@ -119,6 +133,52 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                return 1
        }
 
+       ports := []int{443}
+       for i := 4440; i < 4460; i++ {
+               ports = append(ports, i)
+       }
+       if initcmd.TLS == "acme" {
+               ports = append(ports, 80)
+       }
+       for _, port := range ports {
+               err = initcmd.checkPort(ctx, fmt.Sprintf("%d", port))
+               if err != nil {
+                       return 1
+               }
+       }
+
+       if initcmd.CreateDB {
+               // Do the "create extension" thing early. This way, if
+               // there's no local postgresql server (a likely
+               // failure mode), we can bail out without any side
+               // effects, and the user can start over easily.
+               fmt.Fprintln(stderr, "installing pg_trgm postgresql extension...")
+               cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "--quiet",
+                       "-c", `CREATE EXTENSION IF NOT EXISTS pg_trgm`)
+               cmd.Dir = "/"
+               cmd.Stdout = stdout
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       err = fmt.Errorf("error preparing postgresql server: %w", err)
+                       return 1
+               }
+               fmt.Fprintln(stderr, "...done")
+               initcmd.PostgreSQL.Host = "localhost"
+               initcmd.PostgreSQL.User = "arvados"
+               initcmd.PostgreSQL.Password = initcmd.RandomHex(32)
+               initcmd.PostgreSQL.DB = "arvados"
+       } else {
+               initcmd.PostgreSQL.Host = os.Getenv("POSTGRES_HOST")
+               initcmd.PostgreSQL.User = os.Getenv("POSTGRES_USER")
+               initcmd.PostgreSQL.Password = os.Getenv("POSTGRES_PASSWORD")
+               initcmd.PostgreSQL.DB = os.Getenv("POSTGRES_DB")
+               if initcmd.PostgreSQL.Host == "" || initcmd.PostgreSQL.User == "" || initcmd.PostgreSQL.Password == "" || initcmd.PostgreSQL.DB == "" {
+                       err = fmt.Errorf("missing $POSTGRES_* env var(s) for -create-db=false; see %s -help", prog)
+                       return 1
+               }
+       }
+
        wwwuser, err := user.Lookup("www-data")
        if err != nil {
                err = fmt.Errorf("user.Lookup(%q): %w", "www-data", err)
@@ -128,15 +188,16 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
        if err != nil {
                return 1
        }
-       initcmd.PostgreSQLPassword = initcmd.RandomHex(32)
 
+       fmt.Fprintln(stderr, "creating data storage directory /var/lib/arvados/keep ...")
        err = os.Mkdir("/var/lib/arvados/keep", 0600)
        if err != nil && !os.IsExist(err) {
                err = fmt.Errorf("mkdir /var/lib/arvados/keep: %w", err)
                return 1
        }
-       fmt.Fprintln(stderr, "created /var/lib/arvados/keep")
+       fmt.Fprintln(stderr, "...done")
 
+       fmt.Fprintln(stderr, "creating config file", conffile, "...")
        err = os.Mkdir(confdir, 0750)
        if err != nil && !os.IsExist(err) {
                err = fmt.Errorf("mkdir %s: %w", confdir, err)
@@ -147,9 +208,9 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                err = fmt.Errorf("chown 0:%d %s: %w", wwwgid, confdir, err)
                return 1
        }
-       f, err := os.OpenFile(conffile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
+       f, err := os.OpenFile(conffile+".tmp", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
        if err != nil {
-               err = fmt.Errorf("open %s: %w", conffile, err)
+               err = fmt.Errorf("open %s: %w", conffile+".tmp", err)
                return 1
        }
        tmpl, err := template.New("config").Parse(`Clusters:
@@ -217,10 +278,10 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
     ManagementToken: {{printf "%q" ( .RandomHex 50 )}}
     PostgreSQL:
       Connection:
-        dbname: arvados
-        host: localhost
-        user: arvados
-        password: {{printf "%q" .PostgreSQLPassword}}
+        dbname: {{printf "%q" .PostgreSQL.DB}}
+        host: {{printf "%q" .PostgreSQL.Host}}
+        user: {{printf "%q" .PostgreSQL.User}}
+        password: {{printf "%q" .PostgreSQL.Password}}
     SystemRootToken: {{printf "%q" ( .RandomHex 50 )}}
     TLS:
       {{if eq .TLS "insecure"}}
@@ -269,18 +330,24 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
        }
        err = tmpl.Execute(f, initcmd)
        if err != nil {
-               err = fmt.Errorf("%s: tmpl.Execute: %w", conffile, err)
+               err = fmt.Errorf("%s: tmpl.Execute: %w", conffile+".tmp", err)
                return 1
        }
        err = f.Close()
        if err != nil {
-               err = fmt.Errorf("%s: close: %w", conffile, err)
+               err = fmt.Errorf("%s: close: %w", conffile+".tmp", err)
                return 1
        }
-       fmt.Fprintln(stderr, "created", conffile)
+       err = os.Rename(conffile+".tmp", conffile)
+       if err != nil {
+               err = fmt.Errorf("rename %s -> %s: %w", conffile+".tmp", conffile, err)
+               return 1
+       }
+       fmt.Fprintln(stderr, "...done")
 
        ldr := config.NewLoader(nil, logger)
        ldr.SkipLegacy = true
+       ldr.Path = conffile // load the file we just wrote, even if $ARVADOS_CONFIG is set
        cfg, err := ldr.Load()
        if err != nil {
                err = fmt.Errorf("%s: %w", conffile, err)
@@ -291,11 +358,14 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                return 1
        }
 
+       fmt.Fprintln(stderr, "creating postresql user and database...")
        err = initcmd.createDB(ctx, cluster.PostgreSQL.Connection, stderr)
        if err != nil {
                return 1
        }
+       fmt.Fprintln(stderr, "...done")
 
+       fmt.Fprintln(stderr, "initializing database...")
        cmd := exec.CommandContext(ctx, "sudo", "-u", "www-data", "-E", "HOME=/var/www", "PATH=/var/lib/arvados/bin:"+os.Getenv("PATH"), "/var/lib/arvados/bin/bundle", "exec", "rake", "db:setup")
        cmd.Dir = "/var/lib/arvados/railsapi"
        cmd.Stdout = stderr
@@ -305,11 +375,11 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                err = fmt.Errorf("rake db:setup failed: %w", err)
                return 1
        }
-       fmt.Fprintln(stderr, "initialized database")
+       fmt.Fprintln(stderr, "...done")
 
        if initcmd.Start {
-               fmt.Fprintln(stderr, "starting systemd service")
-               cmd := exec.CommandContext(ctx, "systemctl", "start", "--no-block", "arvados")
+               fmt.Fprintln(stderr, "starting systemd service...")
+               cmd := exec.CommandContext(ctx, "systemctl", "start", "arvados")
                cmd.Dir = "/"
                cmd.Stdout = stderr
                cmd.Stderr = stderr
@@ -318,8 +388,50 @@ func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Read
                        err = fmt.Errorf("%v: %w", cmd.Args, err)
                        return 1
                }
+               fmt.Fprintln(stderr, "...done")
+
+               fmt.Fprintln(stderr, "checking controller API endpoint...")
+               u := url.URL(cluster.Services.Controller.ExternalURL)
+               conn := rpc.NewConn(cluster.ClusterID, &u, cluster.TLS.Insecure, rpc.PassthroughTokenProvider)
+               ctx := auth.NewContext(context.Background(), auth.NewCredentials(cluster.SystemRootToken))
+               _, err = conn.UserGetCurrent(ctx, arvados.GetOptions{})
+               if err != nil {
+                       err = fmt.Errorf("API request failed: %w", err)
+                       return 1
+               }
+               fmt.Fprintln(stderr, "...looks good")
+       }
+
+       if out, err := exec.CommandContext(ctx, "docker", "version").CombinedOutput(); err == nil && strings.Contains(string(out), "\nServer:\n") {
+               fmt.Fprintln(stderr, "loading alpine docker image for diagnostics...")
+               cmd := exec.CommandContext(ctx, "docker", "pull", "alpine")
+               cmd.Stdout = stderr
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       err = fmt.Errorf("%v: %w", cmd.Args, err)
+                       return 1
+               }
+               cmd = exec.CommandContext(ctx, "arv", "sudo", "keep", "docker", "alpine")
+               cmd.Stdout = stderr
+               cmd.Stderr = stderr
+               err = cmd.Run()
+               if err != nil {
+                       err = fmt.Errorf("%v: %w", cmd.Args, err)
+                       return 1
+               }
+               fmt.Fprintln(stderr, "...done")
+       } else {
+               fmt.Fprintln(stderr, "docker is not installed -- skipping step of downloading 'alpine' image")
        }
 
+       fmt.Fprintf(stderr, `
+Setup complete. Next steps:
+* run 'arv sudo diagnostics'
+* log in to workbench2 at %s
+* see documentation at https://doc.arvados.org/install/automatic.html
+`, cluster.Services.Workbench2.ExternalURL.String())
+
        return 0
 }
 
@@ -348,19 +460,98 @@ func (initcmd *initCommand) RandomHex(chars int) string {
 }
 
 func (initcmd *initCommand) createDB(ctx context.Context, dbconn arvados.PostgreSQLConnection, stderr io.Writer) error {
-       for _, sql := range []string{
-               `CREATE USER ` + pq.QuoteIdentifier(dbconn["user"]) + ` WITH SUPERUSER ENCRYPTED PASSWORD ` + pq.QuoteLiteral(dbconn["password"]),
-               `CREATE DATABASE ` + pq.QuoteIdentifier(dbconn["dbname"]) + ` WITH TEMPLATE template0 ENCODING 'utf8'`,
-               `CREATE EXTENSION IF NOT EXISTS pg_trgm`,
-       } {
-               cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "-c", sql)
-               cmd.Dir = "/"
-               cmd.Stdout = stderr
-               cmd.Stderr = stderr
-               err := cmd.Run()
-               if err != nil {
-                       return fmt.Errorf("error setting up arvados user/database: %w", err)
-               }
+       cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "--quiet",
+               "-c", `CREATE USER `+pq.QuoteIdentifier(dbconn["user"])+` WITH SUPERUSER ENCRYPTED PASSWORD `+pq.QuoteLiteral(dbconn["password"]),
+               "-c", `CREATE DATABASE `+pq.QuoteIdentifier(dbconn["dbname"])+` WITH TEMPLATE template0 ENCODING 'utf8'`,
+       )
+       cmd.Dir = "/"
+       cmd.Stdout = stderr
+       cmd.Stderr = stderr
+       err := cmd.Run()
+       if err != nil {
+               return fmt.Errorf("error setting up arvados user/database: %w", err)
+       }
+       return nil
+}
+
+// Confirm that http://{initcmd.Domain}:{port} reaches a server that
+// we run on {port}.
+//
+// If port is "80", listening fails, and Nginx appears to be using the
+// debian-packaged default configuration that listens on port 80,
+// disable that Nginx config and try again.
+//
+// (Typically, the reason Nginx is installed is so that Arvados can
+// run an Nginx child process; the default Nginx service using config
+// from /etc/nginx is just an unfortunate side effect of installing
+// Nginx by way of the Debian package.)
+func (initcmd *initCommand) checkPort(ctx context.Context, port string) error {
+       err := initcmd.checkPortOnce(ctx, port)
+       if err == nil || port != "80" {
+               // success, or poking Nginx in the eye won't help
+               return err
+       }
+       d, err2 := os.Open("/etc/nginx/sites-enabled/.")
+       if err2 != nil {
+               return err
+       }
+       fis, err2 := d.Readdir(-1)
+       if err2 != nil || len(fis) != 1 {
+               return err
+       }
+       if target, err2 := os.Readlink("/etc/nginx/sites-enabled/default"); err2 != nil || target != "/etc/nginx/sites-available/default" {
+               return err
+       }
+       err2 = os.Remove("/etc/nginx/sites-enabled/default")
+       if err2 != nil {
+               return err
+       }
+       exec.CommandContext(ctx, "nginx", "-s", "reload").Run()
+       time.Sleep(time.Second)
+       return initcmd.checkPortOnce(ctx, port)
+}
+
+// Start an http server on 0.0.0.0:{port} and confirm that
+// http://{initcmd.Domain}:{port} reaches that server.
+func (initcmd *initCommand) checkPortOnce(ctx context.Context, port string) error {
+       b := make([]byte, 128)
+       _, err := rand.Read(b)
+       if err != nil {
+               return err
+       }
+       token := fmt.Sprintf("%x", b)
+
+       srv := http.Server{
+               Addr: net.JoinHostPort("", port),
+               Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+                       fmt.Fprint(w, token)
+               })}
+       var errServe atomic.Value
+       go func() {
+               errServe.Store(srv.ListenAndServe())
+       }()
+       defer srv.Close()
+       url := "http://" + net.JoinHostPort(initcmd.Domain, port) + "/probe"
+       req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+       if err != nil {
+               return err
+       }
+       resp, err := http.DefaultClient.Do(req)
+       if err == nil {
+               defer resp.Body.Close()
+       }
+       if errServe, _ := errServe.Load().(error); errServe != nil {
+               // If server already exited, return that error
+               // (probably "can't listen"), not the request error.
+               return errServe
+       }
+       if err != nil {
+               return err
+       }
+       buf := make([]byte, len(token))
+       n, err := io.ReadFull(resp.Body, buf)
+       if string(buf[:n]) != token {
+               return fmt.Errorf("listened on port %s but %s connected to something else, returned %q, err %v", port, url, buf[:n], err)
        }
        return nil
 }