blob: 72bffd321f61aee1af726973b5f8289541bda9fa [file] [log] [blame]
// 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 string, mac string, serial io.ReadWriter) 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
}
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()
}
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.ConnectToNode(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
}
}