17344: After init/startup, check API and show wb2 URL.
[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         wwwuser, err := user.Lookup("www-data")
115         if err != nil {
116                 err = fmt.Errorf("user.Lookup(%q): %w", "www-data", err)
117                 return 1
118         }
119         wwwgid, err := strconv.Atoi(wwwuser.Gid)
120         if err != nil {
121                 return 1
122         }
123         initcmd.PostgreSQLPassword = initcmd.RandomHex(32)
124
125         err = os.Mkdir("/var/lib/arvados/keep", 0600)
126         if err != nil && !os.IsExist(err) {
127                 err = fmt.Errorf("mkdir /var/lib/arvados/keep: %w", err)
128                 return 1
129         }
130         fmt.Fprintln(stderr, "created /var/lib/arvados/keep")
131
132         err = os.Mkdir(confdir, 0750)
133         if err != nil && !os.IsExist(err) {
134                 err = fmt.Errorf("mkdir %s: %w", confdir, err)
135                 return 1
136         }
137         err = os.Chown(confdir, 0, wwwgid)
138         if err != nil {
139                 err = fmt.Errorf("chown 0:%d %s: %w", wwwgid, confdir, err)
140                 return 1
141         }
142         f, err := os.OpenFile(conffile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
143         if err != nil {
144                 err = fmt.Errorf("open %s: %w", conffile, err)
145                 return 1
146         }
147         tmpl, err := template.New("config").Parse(`Clusters:
148   {{.ClusterID}}:
149     Services:
150       Controller:
151         InternalURLs:
152           "http://0.0.0.0:9000/": {}
153         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4440/" ) }}
154       RailsAPI:
155         InternalURLs:
156           "http://0.0.0.0:9001/": {}
157       Websocket:
158         InternalURLs:
159           "http://0.0.0.0:8005/": {}
160         ExternalURL: {{printf "%q" ( print "wss://" .Domain ":4446/" ) }}
161       Keepbalance:
162         InternalURLs:
163           "http://0.0.0.0:9019/": {}
164       GitHTTP:
165         InternalURLs:
166           "http://0.0.0.0:9005/": {}
167         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4445/" ) }}
168       DispatchCloud:
169         InternalURLs:
170           "http://0.0.0.0:9006/": {}
171       Keepproxy:
172         InternalURLs:
173           "http://0.0.0.0:9007/": {}
174         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4447/" ) }}
175       WebDAV:
176         InternalURLs:
177           "http://0.0.0.0:9008/": {}
178         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4448/" ) }}
179       WebDAVDownload:
180         InternalURLs:
181           "http://0.0.0.0:9009/": {}
182         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4449/" ) }}
183       Keepstore:
184         InternalURLs:
185           "http://0.0.0.0:9010/": {}
186       Composer:
187         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4459/composer" ) }}
188       Workbench1:
189         InternalURLs:
190           "http://0.0.0.0:9002/": {}
191         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4442/" ) }}
192       Workbench2:
193         InternalURLs:
194           "http://0.0.0.0:9003/": {}
195         ExternalURL: {{printf "%q" ( print "https://" .Domain "/" ) }}
196       Health:
197         InternalURLs:
198           "http://0.0.0.0:9011/": {}
199     Collections:
200       BlobSigningKey: {{printf "%q" ( .RandomHex 50 )}}
201       {{if eq .TLS "insecure"}}
202       TrustAllContent: true
203       {{end}}
204     Containers:
205       DispatchPrivateKey: {{printf "%q" .GenerateSSHPrivateKey}}
206       CloudVMs:
207         Enable: true
208         Driver: loopback
209     ManagementToken: {{printf "%q" ( .RandomHex 50 )}}
210     PostgreSQL:
211       Connection:
212         dbname: arvados
213         host: localhost
214         user: arvados
215         password: {{printf "%q" .PostgreSQLPassword}}
216     SystemRootToken: {{printf "%q" ( .RandomHex 50 )}}
217     TLS:
218       {{if eq .TLS "insecure"}}
219       Insecure: true
220       {{else if eq .TLS "auto"}}
221       Automatic: true
222       {{else if eq .TLS "acme"}}
223       Certificate: {{printf "%q" (print "/var/lib/acme/live/" .Domain "/cert")}}
224       Key: {{printf "%q" (print "/var/lib/acme/live/" .Domain "/privkey")}}
225       {{else}}
226       {}
227       {{end}}
228     Volumes:
229       {{.ClusterID}}-nyw5e-000000000000000:
230         Driver: Directory
231         DriverParameters:
232           Root: /var/lib/arvados/keep
233         Replication: 2
234     Workbench:
235       SecretKeyBase: {{printf "%q" ( .RandomHex 50 )}}
236     {{if .LoginPAM}}
237     Login:
238       PAM:
239         Enable: true
240     {{else if .LoginTest}}
241     Login:
242       Test:
243         Enable: true
244         Users:
245           admin:
246             Email: {{printf "%q" .AdminEmail}}
247             Password: admin
248     {{else if .LoginGoogle}}
249     Login:
250       Google:
251         Enable: true
252         ClientID: {{printf "%q" .LoginGoogleClientID}}
253         ClientSecret: {{printf "%q" .LoginGoogleClientSecret}}
254     {{end}}
255     Users:
256       AutoAdminUserWithEmail: {{printf "%q" .AdminEmail}}
257 `)
258         if err != nil {
259                 return 1
260         }
261         err = tmpl.Execute(f, initcmd)
262         if err != nil {
263                 err = fmt.Errorf("%s: tmpl.Execute: %w", conffile, err)
264                 return 1
265         }
266         err = f.Close()
267         if err != nil {
268                 err = fmt.Errorf("%s: close: %w", conffile, err)
269                 return 1
270         }
271         fmt.Fprintln(stderr, "created", conffile)
272
273         ldr := config.NewLoader(nil, logger)
274         ldr.SkipLegacy = true
275         cfg, err := ldr.Load()
276         if err != nil {
277                 err = fmt.Errorf("%s: %w", conffile, err)
278                 return 1
279         }
280         cluster, err := cfg.GetCluster("")
281         if err != nil {
282                 return 1
283         }
284
285         err = initcmd.createDB(ctx, cluster.PostgreSQL.Connection, stderr)
286         if err != nil {
287                 return 1
288         }
289
290         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")
291         cmd.Dir = "/var/lib/arvados/railsapi"
292         cmd.Stdout = stderr
293         cmd.Stderr = stderr
294         err = cmd.Run()
295         if err != nil {
296                 err = fmt.Errorf("rake db:setup failed: %w", err)
297                 return 1
298         }
299         fmt.Fprintln(stderr, "initialized database")
300
301         if initcmd.Start {
302                 fmt.Fprintln(stderr, "starting systemd service")
303                 cmd := exec.CommandContext(ctx, "systemctl", "start", "arvados")
304                 cmd.Dir = "/"
305                 cmd.Stdout = stderr
306                 cmd.Stderr = stderr
307                 err = cmd.Run()
308                 if err != nil {
309                         err = fmt.Errorf("%v: %w", cmd.Args, err)
310                         return 1
311                 }
312
313                 fmt.Fprintln(stderr, "checking controller API endpoint")
314                 u := url.URL(cluster.Services.Controller.ExternalURL)
315                 conn := rpc.NewConn(cluster.ClusterID, &u, cluster.TLS.Insecure, rpc.PassthroughTokenProvider)
316                 ctx := auth.NewContext(context.Background(), auth.NewCredentials(cluster.SystemRootToken))
317                 _, err = conn.UserGetCurrent(ctx, arvados.GetOptions{})
318                 if err != nil {
319                         err = fmt.Errorf("API request failed: %w", err)
320                         return 1
321                 }
322         }
323
324         fmt.Fprintln(stderr, "Log in to workbench2 at", cluster.Services.Workbench2.ExternalURL.String())
325
326         return 0
327 }
328
329 func (initcmd *initCommand) GenerateSSHPrivateKey() (string, error) {
330         privkey, err := rsa.GenerateKey(rand.Reader, 4096)
331         if err != nil {
332                 return "", err
333         }
334         err = privkey.Validate()
335         if err != nil {
336                 return "", err
337         }
338         return string(pem.EncodeToMemory(&pem.Block{
339                 Type:  "RSA PRIVATE KEY",
340                 Bytes: x509.MarshalPKCS1PrivateKey(privkey),
341         })), nil
342 }
343
344 func (initcmd *initCommand) RandomHex(chars int) string {
345         b := make([]byte, chars/2)
346         _, err := rand.Read(b)
347         if err != nil {
348                 panic(err)
349         }
350         return fmt.Sprintf("%x", b)
351 }
352
353 func (initcmd *initCommand) createDB(ctx context.Context, dbconn arvados.PostgreSQLConnection, stderr io.Writer) error {
354         for _, sql := range []string{
355                 `CREATE USER ` + pq.QuoteIdentifier(dbconn["user"]) + ` WITH SUPERUSER ENCRYPTED PASSWORD ` + pq.QuoteLiteral(dbconn["password"]),
356                 `CREATE DATABASE ` + pq.QuoteIdentifier(dbconn["dbname"]) + ` WITH TEMPLATE template0 ENCODING 'utf8'`,
357                 `CREATE EXTENSION IF NOT EXISTS pg_trgm`,
358         } {
359                 cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "-c", sql)
360                 cmd.Dir = "/"
361                 cmd.Stdout = stderr
362                 cmd.Stderr = stderr
363                 err := cmd.Run()
364                 if err != nil {
365                         return fmt.Errorf("error setting up arvados user/database: %w", err)
366                 }
367         }
368         return nil
369 }