| 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 |
| } |