[botanist] Migrate `botanist run` to DeviceTarget

Change-Id: I492d156a5c02ef2eee775dc7378f8309eddffc6d
diff --git a/cmd/botanist/run.go b/cmd/botanist/run.go
index cd111eb..4315505 100644
--- a/cmd/botanist/run.go
+++ b/cmd/botanist/run.go
@@ -17,13 +17,10 @@
 	"fuchsia.googlesource.com/tools/build"
 	"fuchsia.googlesource.com/tools/command"
 	"fuchsia.googlesource.com/tools/logger"
-	"fuchsia.googlesource.com/tools/netboot"
-	"fuchsia.googlesource.com/tools/netutil"
 	"fuchsia.googlesource.com/tools/runner"
 	"fuchsia.googlesource.com/tools/sshutil"
 
 	"github.com/google/subcommands"
-	"golang.org/x/crypto/ssh"
 )
 
 const netstackTimeout time.Duration = 1 * time.Minute
@@ -92,52 +89,15 @@
 	f.StringVar(&r.sshKey, "ssh", "", "file containing a private SSH user key; if not provided, a private key will be generated.")
 }
 
-func (r *RunCommand) runCmd(ctx context.Context, imgs build.Images, nodename string, args []string, privKeyFile string, signers []ssh.Signer, syslog io.Writer) error {
-	// Set up log listener and dump kernel output to stdout.
-	l, err := netboot.NewLogListener(nodename)
-	if err != nil {
-		return fmt.Errorf("cannot listen: %v", err)
-	}
-	go func() {
-		defer l.Close()
-		logger.Debugf(ctx, "starting log listener\n")
-		for {
-			data, err := l.Listen()
-			if err != nil {
-				continue
-			}
-			fmt.Print(data)
-			select {
-			case <-ctx.Done():
-				return
-			default:
-			}
-		}
-	}()
-
-	addr, err := netutil.GetNodeAddress(ctx, nodename, false)
-	if err != nil {
-		return err
-	}
-
-	// Boot fuchsia.
-	var bootMode int
-	if r.netboot {
-		bootMode = botanist.ModeNetboot
-	} else {
-		bootMode = botanist.ModePave
-	}
-	if err = botanist.Boot(ctx, addr, bootMode, imgs, r.zirconArgs, signers); err != nil {
-		return err
-	}
-
+func (r *RunCommand) runCmd(ctx context.Context, args []string, device *target.DeviceTarget, syslog io.Writer) error {
+	nodename := device.Nodename()
 	// If having paved, SSH in and stream syslogs back to a file sink.
 	if !r.netboot && syslog != nil {
-		privKey, err := ioutil.ReadFile(privKeyFile)
+		p, err := ioutil.ReadFile(device.SSHKey())
 		if err != nil {
 			return err
 		}
-		config, err := sshutil.DefaultSSHConfig(privKey)
+		config, err := sshutil.DefaultSSHConfig(p)
 		if err != nil {
 			return err
 		}
@@ -155,7 +115,7 @@
 		}()
 	}
 
-	ip, err := botanist.ResolveIPv4(ctx, nodename, netstackTimeout)
+	ip, err := device.IPv4Addr()
 	if err == nil {
 		logger.Infof(ctx, "IPv4 address of %s found: %s", nodename, ip)
 	} else {
@@ -166,7 +126,7 @@
 		os.Environ(),
 		fmt.Sprintf("FUCHSIA_NODENAME=%s", nodename),
 		fmt.Sprintf("FUCHSIA_IPV4_ADDR=%v", ip),
-		fmt.Sprintf("FUCHSIA_SSH_KEY=%s", privKeyFile),
+		fmt.Sprintf("FUCHSIA_SSH_KEY=%s", device.SSHKey()),
 	)
 
 	ctx, cancel := context.WithTimeout(ctx, r.timeout)
@@ -211,36 +171,18 @@
 
 	configs, err := target.LoadDeviceConfigs(r.deviceFile)
 	if err != nil {
-		return fmt.Errorf("failed to load device config file %q", r.deviceFile)
+		return fmt.Errorf("failed to load target config file %q", r.deviceFile)
 	} else if len(configs) != 1 {
-		return fmt.Errorf("expected 1 entry in the device config file; found %d", len(configs))
+		return fmt.Errorf("`botanist run` only supports configuration for a single target")
 	}
-	config := configs[0]
-
-	// If an SSH key is specified in the options, prepend it the configs list so that it
-	// corresponds to the authorized key that would be paved.
-	if r.sshKey != "" {
-		config.SSHKeys = append([]string{r.sshKey}, config.SSHKeys...)
+	opts := target.DeviceOptions{
+		Netboot:  r.netboot,
+		Fastboot: r.fastboot,
+		SSHKey:   r.sshKey,
 	}
-	if len(config.SSHKeys) == 0 {
-		return fmt.Errorf("SSH keys must be supplied in the config entry in %q or via -ssh", r.deviceFile)
-	}
-	var privKeys [][]byte
-	for _, keyPath := range config.SSHKeys {
-		p, err := ioutil.ReadFile(keyPath)
-		if err != nil {
-			return fmt.Errorf("could not read SSH key file %q: %v", keyPath, err)
-		}
-		privKeys = append(privKeys, p)
-	}
-
-	var signers []ssh.Signer
-	for _, p := range privKeys {
-		signer, err := ssh.ParsePrivateKey(p)
-		if err != nil {
-			return err
-		}
-		signers = append(signers, signer)
+	device, err := target.NewDeviceTarget(configs[0], opts)
+	if err != nil {
+		return err
 	}
 
 	var syslog io.WriteCloser
@@ -252,40 +194,21 @@
 		defer syslog.Close()
 	}
 
-	if config.Power != nil {
-		defer func() {
-			logger.Debugf(ctx, "rebooting the node %q\n", config.Network.Nodename)
-
-			if err := config.Power.RebootDevice(signers, config.Network.Nodename); err != nil {
-				logger.Errorf(ctx, "failed to reboot %q: %v\n", config.Network.Nodename, err)
-			}
-		}()
-	}
+	defer func() {
+		logger.Debugf(ctx, "rebooting the node %q\n", device.Nodename())
+		device.Restart(ctx)
+	}()
 
 	ctx, cancel := context.WithCancel(ctx)
 	defer cancel()
 	errs := make(chan error)
 	go func() {
-		if r.fastboot != "" {
-			zirconR := imgs.Get("zircon-r")
-			if zirconR == nil {
-				errs <- fmt.Errorf("zircon-r not provided")
-				return
-			}
-			// If it can't find any fastboot device, the fastboot
-			// tool will hang waiting, so we add a timeout.
-			// All fastboot operations take less than a second on
-			// a developer workstation, so two minutes to flash and
-			// continue is very generous.
-			ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
-			defer cancel()
-			logger.Debugf(ctx, "flashing to zedboot with fastboot\n")
-			if err := botanist.FastbootToZedboot(ctx, r.fastboot, zirconR.Path); err != nil {
-				errs <- err
-				return
-			}
+		if err := device.Start(ctx, imgs, r.zirconArgs); err != nil {
+			errs <- err
 		}
-		errs <- r.runCmd(ctx, imgs, config.Network.Nodename, args, config.SSHKeys[0], signers, syslog)
+	}()
+	go func() {
+		errs <- r.runCmd(ctx, args, device, syslog)
 	}()
 
 	select {
@@ -293,7 +216,6 @@
 		return err
 	case <-ctx.Done():
 	}
-
 	return nil
 }