[serial][reboot] Add serial rebooting.
This adds support for rebooting over serial and gives preference to it
if available.
Change-Id: Ib5c51afce89aaafcb794348edc380341037ec7b9
diff --git a/botanist/power/power.go b/botanist/power/power.go
index 4884420..dc35b0e 100644
--- a/botanist/power/power.go
+++ b/botanist/power/power.go
@@ -6,6 +6,7 @@
import (
"context"
+ "io"
"fuchsia.googlesource.com/tools/botanist/power/amt"
"fuchsia.googlesource.com/tools/botanist/power/wol"
@@ -45,11 +46,30 @@
Password string `json:"password"`
}
+type Rebooter interface {
+ reboot() error
+}
+
+type SshRebooter struct {
+ nodename string
+ signers []ssh.Signer
+}
+
+type SerialRebooter struct {
+ serial io.ReadWriter
+}
+
// RebootDevice attempts to reboot the specified device into recovery, and
// additionally uses the given configuration to reboot the device if specified.
-func (c Client) RebootDevice(signers []ssh.Signer, nodename string) error {
+func (c Client) RebootDevice(signers []ssh.Signer, nodename string, serial io.ReadWriter) error {
+ var rebooter Rebooter
+ if serial != nil {
+ rebooter = NewSerialRebooter(serial)
+ } else {
+ rebooter = NewSSHRebooter(nodename, signers)
+ }
// Always attempt to soft reboot the device to recovery.
- err := rebootRecovery(nodename, signers)
+ err := rebooter.reboot()
if err != nil {
logger.Warningf(context.Background(), "soft reboot failed: %v", err)
}
@@ -65,20 +85,32 @@
}
}
-func rebootRecovery(nodeName string, signers []ssh.Signer) error {
- // Invoke `dm reboot-recovery` with a 2 second delay in the background, then exit the SSH shell.
- // This prevents the SSH connection hanging waiting for `dm reboot-recovery to return.`
- return sendCommand(nodeName, "{ sleep 2; dm reboot-recovery; } >/dev/null & exit", signers)
+func NewSerialRebooter(serial io.ReadWriter) *SerialRebooter {
+ return &SerialRebooter{
+ serial: serial,
+ }
}
-func sendCommand(nodeName, command string, signers []ssh.Signer) error {
- config, err := sshutil.DefaultSSHConfigFromSigners(signers...)
+func NewSSHRebooter(nodename string, signers []ssh.Signer) *SshRebooter {
+ return &SshRebooter{
+ nodename: nodename,
+ signers: signers,
+ }
+}
+
+func (s *SerialRebooter) reboot() error {
+ _, err := io.WriteString(s.serial, "\ndm reboot-recovery\n")
+ return err
+}
+
+func (s *SshRebooter) reboot() error {
+ config, err := sshutil.DefaultSSHConfigFromSigners(s.signers...)
if err != nil {
return err
}
ctx := context.Background()
- client, err := sshutil.ConnectToNode(ctx, nodeName, config)
+ client, err := sshutil.ConnectToNode(ctx, s.nodename, config)
if err != nil {
return err
}
@@ -92,7 +124,9 @@
defer session.Close()
- err = session.Start(command)
+ // Invoke `dm reboot-recovery` with a 2 second delay in the background, then exit the SSH shell.
+ // This prevents the SSH connection from hanging waiting for `dm reboot-recovery` to return.
+ err = session.Start("{ sleep 2; dm reboot-recovery; } >/dev/null & exit")
if err != nil {
return err
}
diff --git a/botanist/target/device.go b/botanist/target/device.go
index 5840da2..6c14c9b 100644
--- a/botanist/target/device.go
+++ b/botanist/target/device.go
@@ -9,6 +9,7 @@
"encoding/json"
"errors"
"fmt"
+ "io"
"io/ioutil"
"net"
"time"
@@ -16,8 +17,10 @@
"fuchsia.googlesource.com/tools/botanist"
"fuchsia.googlesource.com/tools/botanist/power"
"fuchsia.googlesource.com/tools/build"
+ "fuchsia.googlesource.com/tools/logger"
"fuchsia.googlesource.com/tools/netboot"
"fuchsia.googlesource.com/tools/netutil"
+ "fuchsia.googlesource.com/tools/serial"
"golang.org/x/crypto/ssh"
)
@@ -71,10 +74,11 @@
config DeviceConfig
opts Options
signers []ssh.Signer
+ serial io.ReadWriteCloser
}
// NewDeviceTarget returns a new device target with a given configuration.
-func NewDeviceTarget(config DeviceConfig, opts Options) (*DeviceTarget, error) {
+func NewDeviceTarget(ctx context.Context, config DeviceConfig, opts Options) (*DeviceTarget, error) {
// 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 opts.SSHKey != "" {
@@ -84,10 +88,21 @@
if err != nil {
return nil, fmt.Errorf("could not parse out signers from private keys: %v", err)
}
+ var s io.ReadWriteCloser
+ if config.Serial != "" {
+ s, err = serial.Open(config.Serial)
+ if err != nil {
+ // TODO(IN-????): This should be returned as an error, but we don't want to fail any
+ // test runs for misconfigured serial until it is actually required to complete certain
+ // tasks.
+ logger.Errorf(ctx, "unable to open %s: %v", config.Serial, err)
+ }
+ }
return &DeviceTarget{
config: config,
opts: opts,
signers: signers,
+ serial: s,
}, nil
}
@@ -106,8 +121,8 @@
}
// Serial returns the serial device associated with the target for serial i/o.
-func (t *DeviceTarget) Serial() string {
- return t.config.Serial
+func (t *DeviceTarget) Serial() io.ReadWriteCloser {
+ return t.serial
}
// SSHKey returns the private SSH key path associated with the authorized key to be paved.
@@ -155,8 +170,11 @@
// Restart restarts the target.
func (t *DeviceTarget) Restart(ctx context.Context) error {
+ if t.serial != nil {
+ defer t.serial.Close()
+ }
if t.config.Power != nil {
- if err := t.config.Power.RebootDevice(t.signers, t.Nodename()); err != nil {
+ if err := t.config.Power.RebootDevice(t.signers, t.Nodename(), t.serial); err != nil {
return fmt.Errorf("failed to reboot the device: %v", err)
}
}
diff --git a/botanist/target/qemu.go b/botanist/target/qemu.go
index 77f368a..4ce32e7 100644
--- a/botanist/target/qemu.go
+++ b/botanist/target/qemu.go
@@ -101,8 +101,8 @@
}
// Serial returns the serial device associated with the target for serial i/o.
-func (t *QEMUTarget) Serial() string {
- return ""
+func (t *QEMUTarget) Serial() io.ReadWriteCloser {
+ return nil
}
// SSHKey returns the private SSH key path associated with the authorized key to be pavet.
diff --git a/cmd/botanist/run.go b/cmd/botanist/run.go
index bb6cb46..60a425d 100644
--- a/cmd/botanist/run.go
+++ b/cmd/botanist/run.go
@@ -20,7 +20,6 @@
"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"
@@ -39,7 +38,7 @@
IPv4Addr() (net.IP, error)
// Serial returns the serial device associated with the target for serial i/o.
- Serial() string
+ Serial() io.ReadWriteCloser
// SSHKey returns the private key corresponding an authorized SSH key of the target.
SSHKey() string
@@ -216,7 +215,7 @@
var targets []Target
for _, obj := range objs {
- t, err := DeriveTarget(obj, opts)
+ t, err := DeriveTarget(ctx, obj, opts)
if err != nil {
return err
}
@@ -237,36 +236,32 @@
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 t.Serial() != nil {
+ if r.serialLogFile != "" {
+ serialLog, err := os.Create(r.serialLogFile)
if err != nil {
- logger.Errorf(ctx, "failed to write serial log: %v", err)
+ return err
}
- }()
+ defer serialLog.Close()
- // Modify the zirconArgs passed to the kernel on boot to enable serial logging on x64.
+ // 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(t.Serial(), "\ndlog\n"); err != nil {
+ logger.Errorf(ctx, "failed to tail zedboot dlog: %v", err)
+ }
+
+ go func() {
+ _, err := io.Copy(serialLog, t.Serial())
+ if err != nil {
+ logger.Errorf(ctx, "failed to write serial log: %v", err)
+ }
+ }()
+ r.zirconArgs = append(r.zirconArgs, "kernel.bypass-debuglog=true")
+ }
+ // Modify the zirconArgs passed to the kernel on boot to enable serial 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")
+ r.zirconArgs = append(r.zirconArgs, "kernel.serial=legacy")
}
defer func() {
@@ -311,7 +306,7 @@
return subcommands.ExitSuccess
}
-func DeriveTarget(obj []byte, opts target.Options) (Target, error) {
+func DeriveTarget(ctx context.Context, obj []byte, opts target.Options) (Target, error) {
type typed struct {
Type string `json:"type"`
}
@@ -332,7 +327,7 @@
if err := json.Unmarshal(obj, &cfg); err != nil {
return nil, fmt.Errorf("invalid device config found: %v", err)
}
- t, err := target.NewDeviceTarget(cfg, opts)
+ t, err := target.NewDeviceTarget(ctx, cfg, opts)
return t, err
default:
return nil, fmt.Errorf("unknown type found: %q", x.Type)
diff --git a/cmd/botanist/zedboot.go b/cmd/botanist/zedboot.go
index 5caaea1..42d85c1 100644
--- a/cmd/botanist/zedboot.go
+++ b/cmd/botanist/zedboot.go
@@ -200,7 +200,7 @@
var devices []*target.DeviceTarget
for _, config := range configs {
- device, err := target.NewDeviceTarget(config, opts)
+ device, err := target.NewDeviceTarget(ctx, config, opts)
if err != nil {
return err
}
diff --git a/cmd/health_checker/main.go b/cmd/health_checker/main.go
index add3f95..5ea1a90 100644
--- a/cmd/health_checker/main.go
+++ b/cmd/health_checker/main.go
@@ -109,7 +109,7 @@
var devices []*target.DeviceTarget
for _, config := range configs {
- device, err := target.NewDeviceTarget(config, target.Options{})
+ device, err := target.NewDeviceTarget(context.Background(), config, target.Options{})
if err != nil {
log.Fatal(err)
}