metropolis/cli/metroctl: refactor to use RunE instead of log.Fatal

Change-Id: Id5ca65980816e1715a8f08afcdf712292117012a
Reviewed-on: https://review.monogon.dev/c/monogon/+/3441
Reviewed-by: Lorenz Brun <lorenz@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/cli/metroctl/cmd_install_ssh.go b/metropolis/cli/metroctl/cmd_install_ssh.go
index 56bab4c..8c6f6ad 100644
--- a/metropolis/cli/metroctl/cmd_install_ssh.go
+++ b/metropolis/cli/metroctl/cmd_install_ssh.go
@@ -30,7 +30,129 @@
 	Short:   "Installs Metropolis on a Linux system accessible via SSH.",
 	Example: "metroctl install --bundle=metropolis-v0.1.zip --takeover=takeover ssh --disk=nvme0n1 root@ssh-enabled-server.example",
 	Args:    cobra.ExactArgs(1), // One positional argument: the target
-	RunE:    doSSH,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		user, address, err := parseSSHAddr(args[0])
+		if err != nil {
+			return err
+		}
+
+		diskName, err := cmd.Flags().GetString("disk")
+		if err != nil {
+			return err
+		}
+
+		if len(diskName) == 0 {
+			return fmt.Errorf("flag disk is required")
+		}
+
+		var authMethods []xssh.AuthMethod
+		if aconn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
+			defer aconn.Close()
+			a := agent.NewClient(aconn)
+			authMethods = append(authMethods, xssh.PublicKeysCallback(a.Signers))
+		} else {
+			log.Printf("error while establishing ssh agent connection: %v", err)
+			log.Println("ssh agent authentication will not be available.")
+		}
+
+		if term.IsTerminal(int(os.Stdin.Fd())) {
+			authMethods = append(authMethods,
+				xssh.PasswordCallback(func() (string, error) {
+					fmt.Printf("%s@%s's password: ", user, address)
+					b, err := terminal.ReadPassword(syscall.Stdin)
+					if err != nil {
+						return "", err
+					}
+					fmt.Println()
+					return string(b), nil
+				}),
+				xssh.KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
+					answers := make([]string, 0, len(questions))
+					for i, q := range questions {
+						fmt.Print(q)
+						if echos[i] {
+							if _, err := fmt.Scan(&questions[i]); err != nil {
+								return nil, err
+							}
+						} else {
+							b, err := terminal.ReadPassword(syscall.Stdin)
+							if err != nil {
+								return nil, err
+							}
+							fmt.Println()
+							answers = append(answers, string(b))
+						}
+					}
+					return answers, nil
+				}),
+			)
+		} else {
+			log.Println("stdin is not interactive. password authentication will not be available.")
+		}
+
+		cl := ssh.DirectClient{
+			Username:    user,
+			AuthMethods: authMethods,
+		}
+
+		ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
+		conn, err := cl.Dial(ctx, address, 5*time.Second)
+		if err != nil {
+			return fmt.Errorf("error while establishing ssh connection: %w", err)
+		}
+
+		params, err := makeNodeParams()
+		if err != nil {
+			return err
+		}
+		rawParams, err := proto.Marshal(params)
+		if err != nil {
+			return fmt.Errorf("error while marshaling node params: %w", err)
+		}
+
+		const takeoverTargetPath = "/root/takeover"
+		const bundleTargetPath = "/root/bundle.zip"
+		bundle, err := external("bundle", "_main/metropolis/node/bundle.zip", bundlePath)
+		if err != nil {
+			return err
+		}
+		takeover, err := external("takeover", "_main/metropolis/cli/takeover/takeover_bin_/takeover_bin", bundlePath)
+		if err != nil {
+			return err
+		}
+
+		barUploader := func(r fat32.SizedReader, targetPath string) {
+			bar := progressbar.DefaultBytes(
+				r.Size(),
+				targetPath,
+			)
+			defer bar.Close()
+
+			proxyReader := progressbar.NewReader(r, bar)
+			defer proxyReader.Close()
+
+			if err := conn.Upload(ctx, targetPath, &proxyReader); err != nil {
+				log.Fatalf("error while uploading %q: %v", targetPath, err)
+			}
+		}
+
+		log.Println("Uploading required binaries to target host.")
+		barUploader(takeover, takeoverTargetPath)
+		barUploader(bundle, bundleTargetPath)
+
+		// Start the agent and wait for the agent's output to arrive.
+		log.Printf("Starting the takeover executable at path %q.", takeoverTargetPath)
+		_, stderr, err := conn.Execute(ctx, fmt.Sprintf("%s -disk %s", takeoverTargetPath, diskName), rawParams)
+		stderrStr := strings.TrimSpace(string(stderr))
+		if stderrStr != "" {
+			log.Printf("Agent stderr: %q", stderrStr)
+		}
+		if err != nil {
+			return fmt.Errorf("while starting the takeover executable: %w", err)
+		}
+
+		return nil
+	},
 }
 
 func parseAddrOptionalPort(addr string) (string, string, error) {
@@ -79,121 +201,6 @@
 	return user, net.JoinHostPort(addr, port), nil
 }
 
-func doSSH(cmd *cobra.Command, args []string) error {
-	user, address, err := parseSSHAddr(args[0])
-	if err != nil {
-		return err
-	}
-
-	diskName, err := cmd.Flags().GetString("disk")
-	if err != nil {
-		return err
-	}
-
-	if len(diskName) == 0 {
-		return fmt.Errorf("flag disk is required")
-	}
-
-	var authMethods []xssh.AuthMethod
-	if aconn, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil {
-		defer aconn.Close()
-		a := agent.NewClient(aconn)
-		authMethods = append(authMethods, xssh.PublicKeysCallback(a.Signers))
-	} else {
-		log.Printf("error while establishing ssh agent connection: %v", err)
-		log.Println("ssh agent authentication will not be available.")
-	}
-
-	if term.IsTerminal(int(os.Stdin.Fd())) {
-		authMethods = append(authMethods,
-			xssh.PasswordCallback(func() (string, error) {
-				fmt.Printf("%s@%s's password: ", user, address)
-				b, err := terminal.ReadPassword(syscall.Stdin)
-				if err != nil {
-					return "", err
-				}
-				fmt.Println()
-				return string(b), nil
-			}),
-			xssh.KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
-				answers := make([]string, 0, len(questions))
-				for i, q := range questions {
-					fmt.Print(q)
-					if echos[i] {
-						if _, err := fmt.Scan(&questions[i]); err != nil {
-							return nil, err
-						}
-					} else {
-						b, err := terminal.ReadPassword(syscall.Stdin)
-						if err != nil {
-							return nil, err
-						}
-						fmt.Println()
-						answers = append(answers, string(b))
-					}
-				}
-				return answers, nil
-			}),
-		)
-	} else {
-		log.Println("stdin is not interactive. password authentication will not be available.")
-	}
-
-	cl := ssh.DirectClient{
-		Username:    user,
-		AuthMethods: authMethods,
-	}
-
-	ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt)
-	conn, err := cl.Dial(ctx, address, 5*time.Second)
-	if err != nil {
-		return fmt.Errorf("error while establishing ssh connection: %w", err)
-	}
-
-	params := makeNodeParams()
-	rawParams, err := proto.Marshal(params)
-	if err != nil {
-		return fmt.Errorf("error while marshaling node params: %w", err)
-	}
-
-	const takeoverTargetPath = "/root/takeover"
-	const bundleTargetPath = "/root/bundle.zip"
-	bundle := external("bundle", "_main/metropolis/node/bundle.zip", bundlePath)
-	takeover := external("takeover", "_main/metropolis/cli/takeover/takeover_bin_/takeover_bin", bundlePath)
-
-	barUploader := func(r fat32.SizedReader, targetPath string) {
-		bar := progressbar.DefaultBytes(
-			r.Size(),
-			targetPath,
-		)
-		defer bar.Close()
-
-		proxyReader := progressbar.NewReader(r, bar)
-		defer proxyReader.Close()
-
-		if err := conn.Upload(ctx, targetPath, &proxyReader); err != nil {
-			log.Fatalf("error while uploading %q: %v", targetPath, err)
-		}
-	}
-
-	log.Println("Uploading required binaries to target host.")
-	barUploader(takeover, takeoverTargetPath)
-	barUploader(bundle, bundleTargetPath)
-
-	// Start the agent and wait for the agent's output to arrive.
-	log.Printf("Starting the takeover executable at path %q.", takeoverTargetPath)
-	_, stderr, err := conn.Execute(ctx, fmt.Sprintf("%s -disk %s", takeoverTargetPath, diskName), rawParams)
-	stderrStr := strings.TrimSpace(string(stderr))
-	if stderrStr != "" {
-		log.Printf("Agent stderr: %q", stderrStr)
-	}
-	if err != nil {
-		return fmt.Errorf("while starting the takeover executable: %w", err)
-	}
-
-	return nil
-}
-
 func init() {
 	sshCmd.Flags().String("disk", "", "Which disk Metropolis should be installed to")
 	sshCmd.Flags().String("takeover", "", "Path to the Metropolis takeover binary")