17344: Load alpine docker image for diagnostics.
[arvados.git] / lib / install / init.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package install
6
7 import (
8         "context"
9         "crypto/rand"
10         "crypto/rsa"
11         "crypto/x509"
12         "encoding/pem"
13         "flag"
14         "fmt"
15         "io"
16         "net/url"
17         "os"
18         "os/exec"
19         "os/user"
20         "regexp"
21         "strconv"
22         "strings"
23         "text/template"
24
25         "git.arvados.org/arvados.git/lib/cmd"
26         "git.arvados.org/arvados.git/lib/config"
27         "git.arvados.org/arvados.git/lib/controller/rpc"
28         "git.arvados.org/arvados.git/sdk/go/arvados"
29         "git.arvados.org/arvados.git/sdk/go/auth"
30         "git.arvados.org/arvados.git/sdk/go/ctxlog"
31         "github.com/lib/pq"
32 )
33
34 var InitCommand cmd.Handler = &initCommand{}
35
36 type initCommand struct {
37         ClusterID          string
38         Domain             string
39         PostgreSQLPassword string
40         Login              string
41         TLS                string
42         AdminEmail         string
43         Start              bool
44
45         LoginPAM                bool
46         LoginTest               bool
47         LoginGoogle             bool
48         LoginGoogleClientID     string
49         LoginGoogleClientSecret string
50 }
51
52 func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
53         logger := ctxlog.New(stderr, "text", "info")
54         ctx := ctxlog.Context(context.Background(), logger)
55         ctx, cancel := context.WithCancel(ctx)
56         defer cancel()
57
58         var err error
59         defer func() {
60                 if err != nil {
61                         logger.WithError(err).Info("exiting")
62                 }
63         }()
64
65         hostname, err := os.Hostname()
66         if err != nil {
67                 err = fmt.Errorf("Hostname(): %w", err)
68                 return 1
69         }
70
71         flags := flag.NewFlagSet(prog, flag.ContinueOnError)
72         flags.SetOutput(stderr)
73         versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
74         flags.StringVar(&initcmd.ClusterID, "cluster-id", "", "cluster `id`, like x1234 for a dev cluster")
75         flags.StringVar(&initcmd.Domain, "domain", hostname, "cluster public DNS `name`, like x1234.arvadosapi.com")
76         flags.StringVar(&initcmd.Login, "login", "", "login `backend`: test, pam, 'google {client-id} {client-secret}', or ''")
77         flags.StringVar(&initcmd.AdminEmail, "admin-email", "", "give admin privileges to user with given `email`")
78         flags.StringVar(&initcmd.TLS, "tls", "none", "tls certificate `source`: acme, auto, insecure, or none")
79         flags.BoolVar(&initcmd.Start, "start", true, "start systemd service after creating config")
80         if ok, code := cmd.ParseFlags(flags, prog, args, "", stderr); !ok {
81                 return code
82         } else if *versionFlag {
83                 return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
84         } else if !regexp.MustCompile(`^[a-z][a-z0-9]{4}`).MatchString(initcmd.ClusterID) {
85                 err = fmt.Errorf("cluster ID %q is invalid; must be an ASCII letter followed by 4 alphanumerics (try -help)", initcmd.ClusterID)
86                 return 1
87         }
88
89         if fields := strings.Fields(initcmd.Login); len(fields) == 3 && fields[0] == "google" {
90                 initcmd.LoginGoogle = true
91                 initcmd.LoginGoogleClientID = fields[1]
92                 initcmd.LoginGoogleClientSecret = fields[2]
93         } else if initcmd.Login == "test" {
94                 initcmd.LoginTest = true
95                 if initcmd.AdminEmail == "" {
96                         initcmd.AdminEmail = "admin@example.com"
97                 }
98         } else if initcmd.Login == "pam" {
99                 initcmd.LoginPAM = true
100         } else if initcmd.Login == "" {
101                 // none; login will show an error page
102         } else {
103                 err = fmt.Errorf("invalid argument to -login: %q: should be 'test', 'pam', 'google {client-id} {client-secret}', or empty", initcmd.Login)
104                 return 1
105         }
106
107         confdir := "/etc/arvados"
108         conffile := confdir + "/config.yml"
109         if _, err = os.Stat(conffile); err == nil {
110                 err = fmt.Errorf("config file %s already exists; delete it first if you really want to start over", conffile)
111                 return 1
112         }
113
114         // Do the "create extension" thing early. This way, if there's
115         // no local postgresql server (a likely failure mode), we can
116         // bail out without any side effects, and the user can start
117         // over easily.
118         fmt.Fprintln(stderr, "installing pg_trgm postgresql extension...")
119         cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "--quiet",
120                 "-c", `CREATE EXTENSION IF NOT EXISTS pg_trgm`)
121         cmd.Dir = "/"
122         cmd.Stdout = stdout
123         cmd.Stderr = stderr
124         err = cmd.Run()
125         if err != nil {
126                 err = fmt.Errorf("error preparing postgresql server: %w", err)
127                 return 1
128         }
129         fmt.Fprintln(stderr, "...done")
130
131         wwwuser, err := user.Lookup("www-data")
132         if err != nil {
133                 err = fmt.Errorf("user.Lookup(%q): %w", "www-data", err)
134                 return 1
135         }
136         wwwgid, err := strconv.Atoi(wwwuser.Gid)
137         if err != nil {
138                 return 1
139         }
140         initcmd.PostgreSQLPassword = initcmd.RandomHex(32)
141
142         fmt.Fprintln(stderr, "creating data storage directory /var/lib/arvados/keep ...")
143         err = os.Mkdir("/var/lib/arvados/keep", 0600)
144         if err != nil && !os.IsExist(err) {
145                 err = fmt.Errorf("mkdir /var/lib/arvados/keep: %w", err)
146                 return 1
147         }
148         fmt.Fprintln(stderr, "...done")
149
150         fmt.Fprintln(stderr, "creating config file", conffile, "...")
151         err = os.Mkdir(confdir, 0750)
152         if err != nil && !os.IsExist(err) {
153                 err = fmt.Errorf("mkdir %s: %w", confdir, err)
154                 return 1
155         }
156         err = os.Chown(confdir, 0, wwwgid)
157         if err != nil {
158                 err = fmt.Errorf("chown 0:%d %s: %w", wwwgid, confdir, err)
159                 return 1
160         }
161         f, err := os.OpenFile(conffile+".tmp", os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
162         if err != nil {
163                 err = fmt.Errorf("open %s: %w", conffile+".tmp", err)
164                 return 1
165         }
166         tmpl, err := template.New("config").Parse(`Clusters:
167   {{.ClusterID}}:
168     Services:
169       Controller:
170         InternalURLs:
171           "http://0.0.0.0:9000/": {}
172         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4440/" ) }}
173       RailsAPI:
174         InternalURLs:
175           "http://0.0.0.0:9001/": {}
176       Websocket:
177         InternalURLs:
178           "http://0.0.0.0:8005/": {}
179         ExternalURL: {{printf "%q" ( print "wss://" .Domain ":4446/" ) }}
180       Keepbalance:
181         InternalURLs:
182           "http://0.0.0.0:9019/": {}
183       GitHTTP:
184         InternalURLs:
185           "http://0.0.0.0:9005/": {}
186         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4445/" ) }}
187       DispatchCloud:
188         InternalURLs:
189           "http://0.0.0.0:9006/": {}
190       Keepproxy:
191         InternalURLs:
192           "http://0.0.0.0:9007/": {}
193         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4447/" ) }}
194       WebDAV:
195         InternalURLs:
196           "http://0.0.0.0:9008/": {}
197         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4448/" ) }}
198       WebDAVDownload:
199         InternalURLs:
200           "http://0.0.0.0:9009/": {}
201         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4449/" ) }}
202       Keepstore:
203         InternalURLs:
204           "http://0.0.0.0:9010/": {}
205       Composer:
206         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4459/composer" ) }}
207       Workbench1:
208         InternalURLs:
209           "http://0.0.0.0:9002/": {}
210         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4442/" ) }}
211       Workbench2:
212         InternalURLs:
213           "http://0.0.0.0:9003/": {}
214         ExternalURL: {{printf "%q" ( print "https://" .Domain "/" ) }}
215       Health:
216         InternalURLs:
217           "http://0.0.0.0:9011/": {}
218     Collections:
219       BlobSigningKey: {{printf "%q" ( .RandomHex 50 )}}
220       {{if eq .TLS "insecure"}}
221       TrustAllContent: true
222       {{end}}
223     Containers:
224       DispatchPrivateKey: {{printf "%q" .GenerateSSHPrivateKey}}
225       CloudVMs:
226         Enable: true
227         Driver: loopback
228     ManagementToken: {{printf "%q" ( .RandomHex 50 )}}
229     PostgreSQL:
230       Connection:
231         dbname: arvados
232         host: localhost
233         user: arvados
234         password: {{printf "%q" .PostgreSQLPassword}}
235     SystemRootToken: {{printf "%q" ( .RandomHex 50 )}}
236     TLS:
237       {{if eq .TLS "insecure"}}
238       Insecure: true
239       {{else if eq .TLS "auto"}}
240       Automatic: true
241       {{else if eq .TLS "acme"}}
242       Certificate: {{printf "%q" (print "/var/lib/acme/live/" .Domain "/cert")}}
243       Key: {{printf "%q" (print "/var/lib/acme/live/" .Domain "/privkey")}}
244       {{else}}
245       {}
246       {{end}}
247     Volumes:
248       {{.ClusterID}}-nyw5e-000000000000000:
249         Driver: Directory
250         DriverParameters:
251           Root: /var/lib/arvados/keep
252         Replication: 2
253     Workbench:
254       SecretKeyBase: {{printf "%q" ( .RandomHex 50 )}}
255     {{if .LoginPAM}}
256     Login:
257       PAM:
258         Enable: true
259     {{else if .LoginTest}}
260     Login:
261       Test:
262         Enable: true
263         Users:
264           admin:
265             Email: {{printf "%q" .AdminEmail}}
266             Password: admin
267     {{else if .LoginGoogle}}
268     Login:
269       Google:
270         Enable: true
271         ClientID: {{printf "%q" .LoginGoogleClientID}}
272         ClientSecret: {{printf "%q" .LoginGoogleClientSecret}}
273     {{end}}
274     Users:
275       AutoAdminUserWithEmail: {{printf "%q" .AdminEmail}}
276 `)
277         if err != nil {
278                 return 1
279         }
280         err = tmpl.Execute(f, initcmd)
281         if err != nil {
282                 err = fmt.Errorf("%s: tmpl.Execute: %w", conffile+".tmp", err)
283                 return 1
284         }
285         err = f.Close()
286         if err != nil {
287                 err = fmt.Errorf("%s: close: %w", conffile+".tmp", err)
288                 return 1
289         }
290         err = os.Rename(conffile+".tmp", conffile)
291         if err != nil {
292                 err = fmt.Errorf("rename %s -> %s: %w", conffile+".tmp", conffile, err)
293                 return 1
294         }
295         fmt.Fprintln(stderr, "...done")
296
297         ldr := config.NewLoader(nil, logger)
298         ldr.SkipLegacy = true
299         ldr.Path = conffile // load the file we just wrote, even if $ARVADOS_CONFIG is set
300         cfg, err := ldr.Load()
301         if err != nil {
302                 err = fmt.Errorf("%s: %w", conffile, err)
303                 return 1
304         }
305         cluster, err := cfg.GetCluster("")
306         if err != nil {
307                 return 1
308         }
309
310         fmt.Fprintln(stderr, "creating postresql user and database...")
311         err = initcmd.createDB(ctx, cluster.PostgreSQL.Connection, stderr)
312         if err != nil {
313                 return 1
314         }
315         fmt.Fprintln(stderr, "...done")
316
317         fmt.Fprintln(stderr, "initializing database...")
318         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")
319         cmd.Dir = "/var/lib/arvados/railsapi"
320         cmd.Stdout = stderr
321         cmd.Stderr = stderr
322         err = cmd.Run()
323         if err != nil {
324                 err = fmt.Errorf("rake db:setup failed: %w", err)
325                 return 1
326         }
327         fmt.Fprintln(stderr, "...done")
328
329         if initcmd.Start {
330                 fmt.Fprintln(stderr, "starting systemd service...")
331                 cmd := exec.CommandContext(ctx, "systemctl", "start", "arvados")
332                 cmd.Dir = "/"
333                 cmd.Stdout = stderr
334                 cmd.Stderr = stderr
335                 err = cmd.Run()
336                 if err != nil {
337                         err = fmt.Errorf("%v: %w", cmd.Args, err)
338                         return 1
339                 }
340                 fmt.Fprintln(stderr, "...done")
341
342                 fmt.Fprintln(stderr, "checking controller API endpoint...")
343                 u := url.URL(cluster.Services.Controller.ExternalURL)
344                 conn := rpc.NewConn(cluster.ClusterID, &u, cluster.TLS.Insecure, rpc.PassthroughTokenProvider)
345                 ctx := auth.NewContext(context.Background(), auth.NewCredentials(cluster.SystemRootToken))
346                 _, err = conn.UserGetCurrent(ctx, arvados.GetOptions{})
347                 if err != nil {
348                         err = fmt.Errorf("API request failed: %w", err)
349                         return 1
350                 }
351                 fmt.Fprintln(stderr, "...looks good")
352         }
353
354         if out, err := exec.CommandContext(ctx, "docker", "version").CombinedOutput(); err == nil && strings.Contains(string(out), "\nServer:\n") {
355                 fmt.Fprintln(stderr, "loading alpine docker image for diagnostics...")
356                 cmd := exec.CommandContext(ctx, "docker", "pull", "alpine")
357                 cmd.Stdout = stderr
358                 cmd.Stderr = stderr
359                 err = cmd.Run()
360                 if err != nil {
361                         err = fmt.Errorf("%v: %w", cmd.Args, err)
362                         return 1
363                 }
364                 cmd = exec.CommandContext(ctx, "arv", "root", "keep", "docker", "alpine")
365                 cmd.Stdout = stderr
366                 cmd.Stderr = stderr
367                 err = cmd.Run()
368                 if err != nil {
369                         err = fmt.Errorf("%v: %w", cmd.Args, err)
370                         return 1
371                 }
372                 fmt.Fprintln(stderr, "...done")
373         } else {
374                 fmt.Fprintln(stderr, "docker is not installed -- skipping step of downloading 'alpine' image")
375         }
376
377         fmt.Fprintln(stderr, "Setup complete. You should now be able to log in to workbench2 at", cluster.Services.Workbench2.ExternalURL.String())
378
379         return 0
380 }
381
382 func (initcmd *initCommand) GenerateSSHPrivateKey() (string, error) {
383         privkey, err := rsa.GenerateKey(rand.Reader, 4096)
384         if err != nil {
385                 return "", err
386         }
387         err = privkey.Validate()
388         if err != nil {
389                 return "", err
390         }
391         return string(pem.EncodeToMemory(&pem.Block{
392                 Type:  "RSA PRIVATE KEY",
393                 Bytes: x509.MarshalPKCS1PrivateKey(privkey),
394         })), nil
395 }
396
397 func (initcmd *initCommand) RandomHex(chars int) string {
398         b := make([]byte, chars/2)
399         _, err := rand.Read(b)
400         if err != nil {
401                 panic(err)
402         }
403         return fmt.Sprintf("%x", b)
404 }
405
406 func (initcmd *initCommand) createDB(ctx context.Context, dbconn arvados.PostgreSQLConnection, stderr io.Writer) error {
407         cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "--quiet",
408                 "-c", `CREATE USER `+pq.QuoteIdentifier(dbconn["user"])+` WITH SUPERUSER ENCRYPTED PASSWORD `+pq.QuoteLiteral(dbconn["password"]),
409                 "-c", `CREATE DATABASE `+pq.QuoteIdentifier(dbconn["dbname"])+` WITH TEMPLATE template0 ENCODING 'utf8'`,
410         )
411         cmd.Dir = "/"
412         cmd.Stdout = stderr
413         cmd.Stderr = stderr
414         err := cmd.Run()
415         if err != nil {
416                 return fmt.Errorf("error setting up arvados user/database: %w", err)
417         }
418         return nil
419 }