blob: 40bc9d7c33edcefae26060ef6904f2b63ef583c3 [file] [log] [blame]
package powercycle
import (
"context"
"fmt"
"os"
"strings"
"time"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
)
const (
// Amount of time to wait between turning a port on and off again.
powerOffDelayEdgeSwitch = 5 * time.Second
// values for the poe opmode
edgeSwitchOff = "shutdown"
edgeSwitchOn = "auto"
powerCyclePasswordEnvVar = "POWERCYCLE_PASSWORD"
)
// EdgeSwitchConfig contains configuration options for a single EdgeSwitch. Authentication is
// handled via a provided password. See go/skolo-powercycle-setup for more.
type EdgeSwitchConfig struct {
// IP address of the device, i.e. 192.168.1.33
Address string `json:"address"`
// User of the ssh connection.
User string `json:"user"`
// Password for User. This can also be set by the environment variable "POWERCYCLE_PASSWORD".
Password string `json:"password"`
// Mapping between device id and port on the power strip.
DevPortMap map[DeviceID]int `json:"ports"`
}
// Validate returns an error if the configuration is not complete.
func (c *EdgeSwitchConfig) Validate() error {
if c.User == "" || c.Address == "" {
return skerr.Fmt("You must specify a user and ip address.")
}
if c.getPassword() == "" {
return skerr.Fmt("You must specify the password.")
}
return nil
}
// getPassword returns the password.
func (c *EdgeSwitchConfig) getPassword() string {
if c.Password != "" {
return c.Password
}
return strings.TrimSpace(os.Getenv(powerCyclePasswordEnvVar))
}
// edgeSwitchClient implements the Client interface.
type edgeSwitchClient struct {
conf *EdgeSwitchConfig
portDevMap map[int]DeviceID
devIDs []DeviceID
runner CommandRunner
}
// newEdgeSwitchController connects to the EdgeSwitch identified by the given configuration and
// returns a new instance of edgeSwitchClient.
func newEdgeSwitchController(ctx context.Context, conf *EdgeSwitchConfig, connect bool) (*edgeSwitchClient, error) {
if err := conf.Validate(); err != nil {
return nil, skerr.Wrap(err)
}
target := fmt.Sprintf("%s@%s", conf.User, conf.Address)
// The -T removes a warning SSH gives because we are not invoking it over TTY.
// The -o StrictHostKeyChecking=no is added because pods don't have authorized_keys files.
runner := PasswordSSHCommandRunner(conf.getPassword(), "-T", target, "-o", "StrictHostKeyChecking=no")
if connect {
out, _ := runner.ExecCmds(ctx, "help")
// When using sshpass, we always seem to get exit code 255 (from ssh) and any actual errors are
// in stderr. So, we check the returned output for evidence that things actually worked
if !strings.Contains(out, "HELP") {
return nil, skerr.Fmt("smoke test on edge switch %s failed; output: %s", target, out)
}
sklog.Infof("connected successfully to edge switch %s", target)
}
ret := &edgeSwitchClient{
conf: conf,
runner: runner,
}
// Build the dev-port mappings. Ensure each device and port occur only once.
ret.portDevMap = make(map[int]DeviceID, len(conf.DevPortMap))
for id, port := range conf.DevPortMap {
if _, ok := ret.portDevMap[port]; ok {
return nil, skerr.Fmt("Port '%d' specified more than once.", port)
}
ret.portDevMap[port] = id
ret.devIDs = append(ret.devIDs, id)
}
sortIDs(ret.devIDs)
return ret, nil
}
// DeviceIDs implements the Client interface.
func (e *edgeSwitchClient) DeviceIDs() []DeviceID {
return e.devIDs
}
// PowerCycle implements the Client interface.
func (e *edgeSwitchClient) PowerCycle(ctx context.Context, id DeviceID, delayOverride time.Duration) error {
delay := powerOffDelayEdgeSwitch
if delayOverride > 0 {
delay = delayOverride
}
port, ok := e.conf.DevPortMap[id]
if !ok {
return skerr.Fmt("Invalid id: %s", id)
}
if ok := softPowerCycle(ctx, id); ok {
sklog.Infof("Was able to powercycle %s via SSH", id)
return nil
}
sklog.Infof("soft powercycle of %s failed, going to turn off POE port %d", id, port)
if err := e.setPortValue(ctx, port, edgeSwitchOff); err != nil {
return skerr.Wrapf(err, "turning port %d off", port)
}
sklog.Infof("Switched port %d off. Waiting for %s.", port, delay)
time.Sleep(delay)
if err := e.setPortValue(ctx, port, edgeSwitchOn); err != nil {
return skerr.Wrapf(err, "turning port %d back on", port)
}
sklog.Infof("Switched port %d on.", port)
return nil
}
// softPowerCycle attempts to SSH into the machine using the jumphost's private/public key and
// reboot it. This should help the jarring behavior seen when a bot is hard-rebooted frequently.
func softPowerCycle(ctx context.Context, machineName DeviceID) bool {
// We rely on a dns lookup for the bot id ("e.g. skia-rpi-001") for this to work.
// The router or the host can have it in /etc/host.
machineRunner := PublicKeySSHCommandRunner("-T", string(machineName))
// First try to run a trivial command to see if we can access the machine via SSH.
if _, err := machineRunner.ExecCmds(ctx, "time"); err != nil {
return false
}
// Do not bother checking error - this always fails because the command doesn't return after
// reboot.
out, _ := machineRunner.ExecCmds(ctx, "sudo /sbin/reboot -f")
sklog.Infof("Soft reboot should have succeeded. See logs: %s", out)
return true
}
func (e *edgeSwitchClient) setPortValue(ctx context.Context, port int, value string) error {
out, _ := e.runner.ExecCmds(ctx,
"enable",
"configure",
fmt.Sprintf("interface 0/%d", port),
fmt.Sprintf("poe opmode %s", value),
)
// When using sshpass, we always seem to get exit code 255 (from ssh) and any actual errors are
// in stderr. So, we check the returned output for evidence that things actually worked
if !strings.Contains(out, value) {
return skerr.Fmt("Error while setting port value - got output %s", out)
}
sklog.Debugf("output while setting port %d to %s:\n%s\n", port, value, out)
return nil
}