[botanist][serial] Allow reading of serial from DUTs.

This allows reading logs over serial iff the Serial key is specified in
the botanist config, and the invocation specifies a path to write a
serial log.

Change-Id: I34cf5a5be4964e6755e5a5c18b776b572be84ed7
diff --git a/botanist/target/device.go b/botanist/target/device.go
index dadafdb..01e7281 100644
--- a/botanist/target/device.go
+++ b/botanist/target/device.go
@@ -37,6 +37,9 @@
 
 	// SSHKeys are the default system keys to be used with the device.
 	SSHKeys []string `json:"keys,omitempty"`
+
+	// Serial is the path to the device file for serial i/o.
+	Serial string `json:"serial,omitempty`
 }
 
 // NetworkProperties are the static network properties of a target.
@@ -102,6 +105,11 @@
 	return botanist.ResolveIPv4(context.Background(), t.Nodename(), netstackTimeout)
 }
 
+// Serial returns the serial device associated with the target for serial i/o.
+func (t *DeviceTarget) Serial() string {
+	return t.config.Serial
+}
+
 // SSHKey returns the private SSH key path associated with the authorized key to be paved.
 func (t *DeviceTarget) SSHKey() string {
 	return t.config.SSHKeys[0]
diff --git a/botanist/target/qemu.go b/botanist/target/qemu.go
index 7b8f816..77f368a 100644
--- a/botanist/target/qemu.go
+++ b/botanist/target/qemu.go
@@ -100,6 +100,11 @@
 	return nil, nil
 }
 
+// Serial returns the serial device associated with the target for serial i/o.
+func (t *QEMUTarget) Serial() string {
+	return ""
+}
+
 // SSHKey returns the private SSH key path associated with the authorized key to be pavet.
 func (t *QEMUTarget) SSHKey() string {
 	return t.opts.SSHKey
diff --git a/cmd/botanist/run.go b/cmd/botanist/run.go
index a3839d2..fdd9421 100644
--- a/cmd/botanist/run.go
+++ b/cmd/botanist/run.go
@@ -20,6 +20,7 @@
 	"fuchsia.googlesource.com/tools/command"
 	"fuchsia.googlesource.com/tools/logger"
 	"fuchsia.googlesource.com/tools/runner"
+	"fuchsia.googlesource.com/tools/serial"
 	"fuchsia.googlesource.com/tools/sshutil"
 
 	"github.com/google/subcommands"
@@ -37,6 +38,9 @@
 	// IPv4Addr returns the IPv4 address of the target.
 	IPv4Addr() (net.IP, error)
 
+	// Serial returns the serial device associated with the target for serial i/o.
+	Serial() string
+
 	// SSHKey returns the private key corresponding an authorized SSH key of the target.
 	SSHKey() string
 
@@ -84,8 +88,11 @@
 	// SysloggerFile, if nonempty, is the file to where the system's logs will be written.
 	syslogFile string
 
-	// sshKey is the path to a private SSH user key.
+	// SshKey is the path to a private SSH user key.
 	sshKey string
+
+	// SerialLogFile, if nonempty, is the file where the system's serial logs will be written.
+	serialLogFile string
 }
 
 func (*RunCommand) Name() string {
@@ -115,6 +122,7 @@
 	f.StringVar(&r.cmdStderr, "stderr", "", "file to redirect the command's stderr into; if unspecified, it will be redirected to the process' stderr")
 	f.StringVar(&r.syslogFile, "syslog", "", "file to write the systems logs to")
 	f.StringVar(&r.sshKey, "ssh", "", "file containing a private SSH user key; if not provided, a private key will be generated.")
+	f.StringVar(&r.serialLogFile, "serial-log", "", "file to write the serial logs to.")
 }
 
 func (r *RunCommand) runCmd(ctx context.Context, args []string, t Target, syslog io.Writer) error {
@@ -235,6 +243,38 @@
 		defer syslog.Close()
 	}
 
+	if t.Serial() != "" && r.serialLogFile != "" {
+		serialLog, err := os.Create(r.serialLogFile)
+		if err != nil {
+			return err
+		}
+		defer serialLog.Close()
+
+		serialDevice, err := serial.Open(t.Serial())
+		if err != nil {
+			return fmt.Errorf("unable to open %s: %v", t.Serial(), err)
+		}
+		defer serialDevice.Close()
+
+		// Here we invoke the `dlog` command over serial to tail the existing log buffer into the
+		// output file.  This should give us everything since Zedboot boot, and new messages should
+		// be written to directly to the serial port without needing to tail with `dlog -f`.
+		if _, err = io.WriteString(serialDevice, "\ndlog\n"); err != nil {
+			logger.Errorf(ctx, "failed to tail zedboot dlog: %v", err)
+		}
+
+		go func() {
+			_, err := io.Copy(serialLog, serialDevice)
+			if err != nil {
+				logger.Errorf(ctx, "failed to write serial log: %v", err)
+			}
+		}()
+
+		// Modify the zirconArgs passed to the kernel on boot to enable serial logging on x64.
+		// arm64 devices should already be enabling kernel.serial at compile time.
+		r.zirconArgs = append(r.zirconArgs, "kernel.bypass-debuglog=true", "kernel.serial=legacy")
+	}
+
 	defer func() {
 		logger.Debugf(ctx, "stopping or rebooting the node %q\n", t.Nodename())
 		if err := t.Stop(ctx); err == target.ErrUnimplemented {