16552: change default db name to just arvados.
[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         "os"
17         "os/exec"
18         "os/user"
19         "regexp"
20         "strconv"
21         "strings"
22         "text/template"
23
24         "git.arvados.org/arvados.git/lib/cmd"
25         "git.arvados.org/arvados.git/lib/config"
26         "git.arvados.org/arvados.git/sdk/go/arvados"
27         "git.arvados.org/arvados.git/sdk/go/ctxlog"
28         "github.com/lib/pq"
29 )
30
31 var InitCommand cmd.Handler = &initCommand{}
32
33 type initCommand struct {
34         ClusterID          string
35         Domain             string
36         PostgreSQLPassword string
37         Login              string
38         TLS                string
39         Start              bool
40
41         LoginPAM                bool
42         LoginTest               bool
43         LoginGoogle             bool
44         LoginGoogleClientID     string
45         LoginGoogleClientSecret string
46 }
47
48 func (initcmd *initCommand) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
49         logger := ctxlog.New(stderr, "text", "info")
50         ctx := ctxlog.Context(context.Background(), logger)
51         ctx, cancel := context.WithCancel(ctx)
52         defer cancel()
53
54         var err error
55         defer func() {
56                 if err != nil {
57                         logger.WithError(err).Info("exiting")
58                 }
59         }()
60
61         hostname, err := os.Hostname()
62         if err != nil {
63                 err = fmt.Errorf("Hostname(): %w", err)
64                 return 1
65         }
66
67         flags := flag.NewFlagSet(prog, flag.ContinueOnError)
68         flags.SetOutput(stderr)
69         versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
70         flags.StringVar(&initcmd.ClusterID, "cluster-id", "", "cluster `id`, like x1234 for a dev cluster")
71         flags.StringVar(&initcmd.Domain, "domain", hostname, "cluster public DNS `name`, like x1234.arvadosapi.com")
72         flags.StringVar(&initcmd.Login, "login", "", "login `backend`: test, pam, 'google {client-id} {client-secret}', or ''")
73         flags.StringVar(&initcmd.TLS, "tls", "none", "tls certificate `source`: acme, auto, insecure, or none")
74         flags.BoolVar(&initcmd.Start, "start", true, "start systemd service after creating config")
75         if ok, code := cmd.ParseFlags(flags, prog, args, "", stderr); !ok {
76                 return code
77         } else if *versionFlag {
78                 return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
79         } else if !regexp.MustCompile(`^[a-z][a-z0-9]{4}`).MatchString(initcmd.ClusterID) {
80                 err = fmt.Errorf("cluster ID %q is invalid; must be an ASCII letter followed by 4 alphanumerics (try -help)", initcmd.ClusterID)
81                 return 1
82         }
83
84         if fields := strings.Fields(initcmd.Login); len(fields) == 3 && fields[0] == "google" {
85                 initcmd.LoginGoogle = true
86                 initcmd.LoginGoogleClientID = fields[1]
87                 initcmd.LoginGoogleClientSecret = fields[2]
88         } else if initcmd.Login == "test" {
89                 initcmd.LoginTest = true
90         } else if initcmd.Login == "pam" {
91                 initcmd.LoginPAM = true
92         } else if initcmd.Login == "" {
93                 // none; login will show an error page
94         } else {
95                 err = fmt.Errorf("invalid argument to -login: %q: should be 'test', 'pam', 'google {client-id} {client-secret}', or empty")
96                 return 1
97         }
98
99         confdir := "/etc/arvados"
100         conffile := confdir + "/config.yml"
101         if _, err = os.Stat(conffile); err == nil {
102                 err = fmt.Errorf("config file %s already exists; delete it first if you really want to start over", conffile)
103                 return 1
104         }
105
106         wwwuser, err := user.Lookup("www-data")
107         if err != nil {
108                 err = fmt.Errorf("user.Lookup(%q): %w", "www-data", err)
109                 return 1
110         }
111         wwwgid, err := strconv.Atoi(wwwuser.Gid)
112         if err != nil {
113                 return 1
114         }
115         initcmd.PostgreSQLPassword = initcmd.RandomHex(32)
116
117         err = os.Mkdir("/var/lib/arvados/keep", 0600)
118         if err != nil && !os.IsExist(err) {
119                 err = fmt.Errorf("mkdir /var/lib/arvados/keep: %w", err)
120                 return 1
121         }
122         fmt.Fprintln(stderr, "created /var/lib/arvados/keep")
123
124         err = os.Mkdir(confdir, 0750)
125         if err != nil && !os.IsExist(err) {
126                 err = fmt.Errorf("mkdir %s: %w", confdir, err)
127                 return 1
128         }
129         err = os.Chown(confdir, 0, wwwgid)
130         if err != nil {
131                 err = fmt.Errorf("chown 0:%d %s: %w", wwwgid, confdir, err)
132                 return 1
133         }
134         f, err := os.OpenFile(conffile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
135         if err != nil {
136                 err = fmt.Errorf("open %s: %w", conffile, err)
137                 return 1
138         }
139         tmpl, err := template.New("config").Parse(`Clusters:
140   {{.ClusterID}}:
141     Services:
142       Controller:
143         InternalURLs:
144           "http://0.0.0.0:9000/": {}
145         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4440/" ) }}
146       RailsAPI:
147         InternalURLs:
148           "http://0.0.0.0:9001/": {}
149       Websocket:
150         InternalURLs:
151           "http://0.0.0.0:8005/": {}
152         ExternalURL: {{printf "%q" ( print "wss://" .Domain ":4436/" ) }}
153       Keepbalance:
154         InternalURLs:
155           "http://0.0.0.0:9019/": {}
156       GitHTTP:
157         InternalURLs:
158           "http://0.0.0.0:9005/": {}
159         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4445/" ) }}
160       DispatchCloud:
161         InternalURLs:
162           "http://0.0.0.0:9006/": {}
163       Keepproxy:
164         InternalURLs:
165           "http://0.0.0.0:9007/": {}
166         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4447/" ) }}
167       WebDAV:
168         InternalURLs:
169           "http://0.0.0.0:9008/": {}
170         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4448/" ) }}
171       WebDAVDownload:
172         InternalURLs:
173           "http://0.0.0.0:9009/": {}
174         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4449/" ) }}
175       Keepstore:
176         InternalURLs:
177           "http://0.0.0.0:9010/": {}
178       Composer:
179         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4459/composer" ) }}
180       Workbench1:
181         InternalURLs:
182           "http://0.0.0.0:9002/": {}
183         ExternalURL: {{printf "%q" ( print "https://" .Domain ":4442/" ) }}
184       Workbench2:
185         InternalURLs:
186           "http://0.0.0.0:9003/": {}
187         ExternalURL: {{printf "%q" ( print "https://" .Domain "/" ) }}
188       Health:
189         InternalURLs:
190           "http://0.0.0.0:9011/": {}
191     Collections:
192       BlobSigningKey: {{printf "%q" ( .RandomHex 50 )}}
193       {{if eq .TLS "insecure"}}
194       TrustAllContent: true
195       {{end}}
196     Containers:
197       DispatchPrivateKey: {{printf "%q" .GenerateSSHPrivateKey}}
198       CloudVMs:
199         Enable: true
200         Driver: loopback
201     ManagementToken: {{printf "%q" ( .RandomHex 50 )}}
202     PostgreSQL:
203       Connection:
204         dbname: arvados
205         host: localhost
206         user: arvados
207         password: {{printf "%q" .PostgreSQLPassword}}
208     SystemRootToken: {{printf "%q" ( .RandomHex 50 )}}
209     TLS:
210       {{if eq .TLS "insecure"}}
211       Insecure: true
212       {{else if eq .TLS "auto"}}
213       Automatic: true
214       {{else if eq .TLS "acme"}}
215       Certificate: {{printf "%q" (print "/var/lib/acme/live/" .Domain "/cert")}}
216       Key: {{printf "%q" (print "/var/lib/acme/live/" .Domain "/privkey")}}
217       {{else}}
218       {}
219       {{end}}
220     Volumes:
221       {{.ClusterID}}-nyw5e-000000000000000:
222         Driver: Directory
223         DriverParameters:
224           Root: /var/lib/arvados/keep
225         Replication: 2
226     Workbench:
227       SecretKeyBase: {{printf "%q" ( .RandomHex 50 )}}
228     {{if .LoginPAM}}
229     Login:
230       PAM:
231         Enable: true
232     {{else if .LoginTest}}
233     Login:
234       Test:
235         Enable: true
236         Users:
237           admin:
238             Email: admin@example.com
239             Password: admin
240     {{else if .LoginGoogle}}
241     Login:
242       Google:
243         Enable: true
244         ClientID: {{printf "%q" .LoginGoogleClientID}}
245         ClientSecret: {{printf "%q" .LoginGoogleClientSecret}}
246     {{end}}
247     {{if .LoginTest}}
248     Users:
249       AutoAdminUserWithEmail: admin@example.com
250     {{end}}
251 `)
252         if err != nil {
253                 return 1
254         }
255         err = tmpl.Execute(f, initcmd)
256         if err != nil {
257                 err = fmt.Errorf("%s: tmpl.Execute: %w", conffile, err)
258                 return 1
259         }
260         err = f.Close()
261         if err != nil {
262                 err = fmt.Errorf("%s: close: %w", conffile, err)
263                 return 1
264         }
265         fmt.Fprintln(stderr, "created", conffile)
266
267         ldr := config.NewLoader(nil, logger)
268         ldr.SkipLegacy = true
269         cfg, err := ldr.Load()
270         if err != nil {
271                 err = fmt.Errorf("%s: %w", conffile, err)
272                 return 1
273         }
274         cluster, err := cfg.GetCluster("")
275         if err != nil {
276                 return 1
277         }
278
279         err = initcmd.createDB(ctx, cluster.PostgreSQL.Connection, stderr)
280         if err != nil {
281                 return 1
282         }
283
284         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")
285         cmd.Dir = "/var/lib/arvados/railsapi"
286         cmd.Stdout = stderr
287         cmd.Stderr = stderr
288         err = cmd.Run()
289         if err != nil {
290                 err = fmt.Errorf("rake db:setup failed: %w", err)
291                 return 1
292         }
293         fmt.Fprintln(stderr, "initialized database")
294
295         if initcmd.Start {
296                 fmt.Fprintln(stderr, "starting systemd service")
297                 cmd := exec.CommandContext(ctx, "systemctl", "start", "--no-block", "arvados")
298                 cmd.Dir = "/"
299                 cmd.Stdout = stderr
300                 cmd.Stderr = stderr
301                 err = cmd.Run()
302                 if err != nil {
303                         err = fmt.Errorf("%v: %w", cmd.Args, err)
304                         return 1
305                 }
306         }
307
308         return 0
309 }
310
311 func (initcmd *initCommand) GenerateSSHPrivateKey() (string, error) {
312         privkey, err := rsa.GenerateKey(rand.Reader, 4096)
313         if err != nil {
314                 return "", err
315         }
316         err = privkey.Validate()
317         if err != nil {
318                 return "", err
319         }
320         return string(pem.EncodeToMemory(&pem.Block{
321                 Type:  "RSA PRIVATE KEY",
322                 Bytes: x509.MarshalPKCS1PrivateKey(privkey),
323         })), nil
324 }
325
326 func (initcmd *initCommand) RandomHex(chars int) string {
327         b := make([]byte, chars/2)
328         _, err := rand.Read(b)
329         if err != nil {
330                 panic(err)
331         }
332         return fmt.Sprintf("%x", b)
333 }
334
335 func (initcmd *initCommand) createDB(ctx context.Context, dbconn arvados.PostgreSQLConnection, stderr io.Writer) error {
336         for _, sql := range []string{
337                 `CREATE USER ` + pq.QuoteIdentifier(dbconn["user"]) + ` WITH SUPERUSER ENCRYPTED PASSWORD ` + pq.QuoteLiteral(dbconn["password"]),
338                 `CREATE DATABASE ` + pq.QuoteIdentifier(dbconn["dbname"]) + ` WITH TEMPLATE template0 ENCODING 'utf8'`,
339                 `CREATE EXTENSION IF NOT EXISTS pg_trgm`,
340         } {
341                 cmd := exec.CommandContext(ctx, "sudo", "-u", "postgres", "psql", "-c", sql)
342                 cmd.Stdout = stderr
343                 cmd.Stderr = stderr
344                 err := cmd.Run()
345                 if err != nil {
346                         return fmt.Errorf("error setting up arvados user/database: %w", err)
347                 }
348         }
349         return nil
350 }