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")