blob: ac45ed98663171fd49a6ca5fe8e3713caf5c801a [file] [log] [blame]
package devices
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"strconv"
"syscall"
"time"
"go.fuchsia.dev/infra/devices/power"
"go.fuchsia.dev/tools/netboot"
)
// ArmCDCether represents a generic arm cdcether product. It currently encompasses
// 1) Sherlock
// 2) Astro
// 3) Nelson
type ArmCDCether struct {
fuchsiaDevice
}
// NewArmCDCether initializes Astros, Sherlocks, and Nelsons as Arm CDCether objects.
func NewArmCDCether(config DeviceConfig, bootserverCmdStub []string) (*ArmCDCether, error) {
var powerManager powerManager = nil
if config.Power != nil {
port, err := strconv.Atoi(config.Power.PDUPort)
if err != nil {
return nil, err
}
powerManager = power.NewPDUPowerManager(
config.Power.PDUIp,
config.Power.Username,
config.Power.Password,
port,
)
}
sd := &ArmCDCether{
fuchsiaDevice{
power: powerManager,
},
}
if err := initFuchsiaDevice(&sd.fuchsiaDevice, config, bootserverCmdStub); err != nil {
return nil, err
}
// All transitions in a ArmCDCether attempt to get it into zedboot.
transitionMap := map[DeviceState]*Transition{
Initial: &Transition{
PerformAction: sd.sshRebootRecovery,
Validate: sd.checkTransitionSuccess,
SuccessState: Healthy,
FailureState: "ssh_failed",
},
"ssh_failed": &Transition{
PerformAction: sd.serialRebootRecovery,
Validate: sd.checkTransitionSuccess,
SuccessState: Healthy,
FailureState: "serial_failed",
},
"serial_failed": &Transition{
PerformAction: sd.PowercycleIntoRecovery,
Validate: sd.checkTransitionSuccess,
SuccessState: Healthy,
FailureState: Unrecoverable,
},
}
sd.transitionMap = transitionMap
return sd, nil
}
// PowercycleIntoRecovery powercycles the device and then sends a serial command
// to boot into the recovery partition.
func (s *ArmCDCether) PowercycleIntoRecovery(ctx context.Context) error {
if err := s.Powercycle(ctx); err != nil {
return err
}
time.Sleep(powercycleWait)
return s.SoftReboot(ctx, "R", "serial")
}
// checkTransitionSuccess is the validation function used by all transitions
// performed by a ArmCDCether. It verfies that the devices is in zedboot and
// broadcasting.
func (s *ArmCDCether) checkTransitionSuccess(ctx context.Context) error {
// Give the device time to transition.
time.Sleep(1 * time.Minute)
client := netboot.NewClient(netbootTimeout)
if err := checkBroadcasting(client); err != nil {
return err
}
return deviceInZedboot(client, s.nodename)
}
// checkBroadcasting ensures that the ArmCDCether is sending out advertising
// packets.
func checkBroadcasting(n *netboot.Client) error {
if _, err := n.Beacon(); err != nil {
return err
}
return nil
}
// ToTaskState puts the ArmCDCether into a state in which it can run tasks.
func (s *ArmCDCether) ToTaskState(ctx context.Context) error {
cmd := exec.Command(s.BootserverCmd[0], s.BootserverCmd[1:]...)
// Spin up handler to exit subprocess cleanly on sigterm.
processDone := make(chan bool, 1)
go func() {
select {
case <-processDone:
case <-ctx.Done():
if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
log.Printf("exited cmd with error %v", err)
}
}
}()
// Ensure the context still exists before running the subprocess.
if ctx.Err() != nil {
return nil
}
// Run the bootserver.
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Run()
processDone <- true
if !cmd.ProcessState.Success() {
return fmt.Errorf("bootserver for %s exited with exit code %d", s.nodename, cmd.ProcessState.ExitCode())
}
return nil
}