"shell": shellCommand{},
"connect-ssh": connectSSHCommand{},
"diagnostics": diagnostics.Command{},
+ "sudo": sudoCommand{},
})
)
--- /dev/null
+// 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)
+}
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 {
}
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")
"--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",
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>
"fmt"
"os"
"path/filepath"
+ "runtime"
"strings"
"sync"
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
}
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 {
}
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 {
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)
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)")
logLevel string
priority int
projectName string
+ dockerImage string
checkInternal bool
checkExternal bool
timeout time.Duration
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()
return err
}
diag.debugf("Collections.BlobSigning = %v", cluster.Collections.BlobSigning)
+ cfgOK = true
return nil
})
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"
})
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 == "" {
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
})
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)
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 + "?"
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",
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
"os/exec"
"os/user"
"path/filepath"
+ "runtime"
"strconv"
"strings"
"syscall"
"uuid-dev",
"wget",
"xvfb",
+ "zlib1g-dev", // services/api
)
if test {
if osv.Debian && osv.Major <= 10 {
}
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 {
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")
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",
{"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"},
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
}
"libcurl3-gnutls",
"libxslt1.1",
"nginx",
- "python",
+ "python3",
"sudo",
}
if osv.Debian || osv.Ubuntu {
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")
"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"
)
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
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`")
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)
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)
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:
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"}}
}
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)
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
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
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
}
}
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
}