| // Copyright 2018 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package devices |
| |
| import ( |
| "context" |
| "errors" |
| "fmt" |
| "io" |
| "log" |
| "strconv" |
| "time" |
| |
| "go.fuchsia.dev/fuchsia/tools/net/sshutil" |
| "golang.org/x/crypto/ssh" |
| ) |
| |
| // TODO(IN-977) Clean this up per suggestions in go/fxr/251550 |
| |
| const ( |
| // Controller machines use 192.168.42.1/24 for swarming bots |
| // This will broadcast to that entire subnet. |
| botBroadcastAddr = "192.168.42.255:9" |
| |
| // Controller machines have multiple interfaces, currently |
| // 'eno2' is used for swarming bots. |
| botInterface = "eno2" |
| |
| // Duration to wait before sending dm reboot-recovery after powercycle |
| powercycleWait = 1 * time.Minute |
| ) |
| |
| // PowerClient represents a power management configuration for a particular device. |
| type PowerClient struct { |
| // Type is the type of manager to use. |
| Type string `json:"type"` |
| |
| // Host is the network hostname of the manager |
| Host string `json:"host"` |
| |
| // Username is the username used to log in to the manager. |
| Username string `json:"username"` |
| |
| // Password is the password used to log in to the manager.. |
| Password string `json:"password"` |
| |
| // PDUIp is the IP address of the pdu |
| PDUIp string `json:"pduIp"` |
| |
| // PDUPort is the port the PDU uses to connect to this device. |
| PDUPort string `json:"pduPort"` |
| } |
| |
| 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 PowerClient) RebootDevice(signers []ssh.Signer, nodename string, serial io.ReadWriter) error { |
| var rebooter Rebooter |
| log.Printf("Attempting to soft reboot device %s", nodename) |
| if serial == nil { |
| log.Printf("Using SSH to reboot.") |
| rebooter = NewSSHRebooter(nodename, signers) |
| } else { |
| log.Printf("Using serial to reboot.") |
| rebooter = NewSerialRebooter(serial) |
| } |
| return rebooter.reboot() |
| } |
| |
| // Powercycle the device using an out of band method |
| func (c PowerClient) Powercycle(nodename, mac string, serial io.ReadWriter, signers []ssh.Signer) error { |
| log.Printf("Attempting to powercycle device %s", nodename) |
| switch c.Type { |
| case "AMT": |
| log.Printf("Using AMT to powercycle.") |
| return AMTReboot(c.Host, c.Username, c.Password) |
| case "WOL": |
| if serial == nil { |
| return errors.New(fmt.Sprintf("WOL Reboot requires serial connection.")) |
| } |
| log.Printf("Using WOL to powercycle") |
| if err := WOLReboot(botBroadcastAddr, botInterface, mac); err != nil { |
| return err |
| } |
| case "PDU": |
| if serial == nil { |
| return errors.New(fmt.Sprintf("PDU Reboot requires serial connection.")) |
| } |
| log.Printf("Using PDU to powercycle") |
| port, err := strconv.Atoi(c.PDUPort) |
| if err != nil { |
| return err |
| } |
| if err := PDUReboot(port, c.PDUIp, c.Username, c.Password); err != nil { |
| return err |
| } |
| case "ACTS_PDU": |
| port, err := strconv.Atoi(c.PDUPort) |
| if err != nil { |
| return err |
| } |
| conn := NewActsPDUConn(c.PDUIp, c.Username, c.Password) |
| if err := conn.Reboot(port); err != nil { |
| return err |
| } |
| // TODO(rudymathu): Remove this dependency on ssh when all connectivity testbeds have |
| // serial connections. |
| if serial == nil { |
| log.Printf("Powercycle complete; using ssh to send dm reboot recovery.") |
| time.Sleep(powercycleWait) |
| rebooter := NewSSHRebooter(nodename, signers) |
| return rebooter.reboot() |
| } |
| default: |
| return errors.New(fmt.Sprintf("%v does not have AMT, WOL, or PDU support. Cannot powercycle.", nodename)) |
| } |
| // Send dm reboot-recovery to the device so that it always boots into zedboot. |
| log.Printf("Powercycle complete; using serial to send dm reboot recovery.") |
| time.Sleep(powercycleWait) |
| rebooter := NewSerialRebooter(serial) |
| return rebooter.reboot() |
| } |
| |
| // PowercycleIntoFastboot powercycles an Astro or Sherlock into fastboot. |
| func (c PowerClient) PowercycleIntoFastboot(nodename string, mac string, serial io.ReadWriter) error { |
| if c.Type != "PDU" { |
| return errors.New("fastboot not supported for device") |
| } |
| if serial == nil { |
| return errors.New("PDU Reboot requires serial connection.") |
| } |
| log.Printf("Using PDU to powercycle") |
| port, err := strconv.Atoi(c.PDUPort) |
| if err != nil { |
| return err |
| } |
| if err := PDUReboot(port, c.PDUIp, c.Username, c.Password); err != nil { |
| return err |
| } |
| |
| // Spam the serial line to get into fastboot mode. |
| // We spam the line so much because it gives us the greatest chance of entering fastboot. |
| for i := 1; i < 15000; i++ { |
| if _, err := io.WriteString(serial, "\nf\n"); err != nil { |
| return err |
| } |
| time.Sleep(1 * time.Millisecond) |
| } |
| time.Sleep(1 * time.Second) |
| // This serial command instructs Sherlocks to enter fastboot mode. |
| if _, err := io.WriteString(serial, "\nfastboot 0\n"); err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| func NewSerialRebooter(serial io.ReadWriter) *SerialRebooter { |
| return &SerialRebooter{ |
| serial: serial, |
| } |
| } |
| |
| func (s *SerialRebooter) reboot() error { |
| _, err := io.WriteString(s.serial, "\ndm reboot-recovery\n") |
| return err |
| } |
| |
| func NewSSHRebooter(nodename string, signers []ssh.Signer) *SshRebooter { |
| return &SshRebooter{ |
| nodename: nodename, |
| signers: signers, |
| } |
| } |
| |
| func (s *SshRebooter) reboot() error { |
| config, err := sshutil.DefaultSSHConfigFromSigners(s.signers...) |
| if err != nil { |
| return err |
| } |
| |
| client, err := sshutil.ConnectToNodeDeprecated(context.Background(), s.nodename, config) |
| if err != nil { |
| return err |
| } |
| |
| defer client.Close() |
| |
| session, err := client.NewSession() |
| if err != nil { |
| return err |
| } |
| |
| defer session.Close() |
| |
| // Invoke `dm reboot-recovery` |
| err = session.Start("dm reboot-recovery") |
| if err != nil { |
| return err |
| } |
| |
| done := make(chan error) |
| go func() { |
| done <- session.Wait() |
| }() |
| |
| select { |
| case err := <-done: |
| return err |
| case <-time.After(10 * time.Second): |
| return nil |
| } |
| } |