blob: 020b3e9cd03d43e7bc8b7f858a41ad42f19e65f4 [file] [log] [blame]
package powercycle
import (
"context"
"net/http"
"strconv"
"time"
"go.skia.org/infra/go/httputils"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
)
const (
synaccessUser = "admin"
synaccessPassword = "admin"
powerOffDelaySynaccess = 10 * time.Second
)
type SynaccessConfig struct {
// IP address of the device, i.e. 192.168.1.10
Address string `json:"address"`
// Mapping between device id and port on the PDU. These should be the labels on the physical
// device (i.e. 1-indexed).
DevPortMap map[DeviceID]int `json:"ports"`
}
type synaccessClient struct {
name string
conf *SynaccessConfig
httpClient *http.Client
}
// newSynaccessController creates a new client to talk to the Synaccess PDUs. If connect is true,
// it makes a request to test the IP address.
func newSynaccessController(ctx context.Context, name string, conf *SynaccessConfig, connect bool) (*synaccessClient, error) {
client := &synaccessClient{
name: name,
conf: conf,
httpClient: httputils.DefaultClientConfig().Client(),
}
if connect {
err := client.ping(ctx)
if err != nil {
return nil, skerr.Wrapf(err, "Contacting Synaccess device on port %s", conf.Address)
}
}
return client, nil
}
// DeviceIDs implementst the Controller interface.
func (s *synaccessClient) DeviceIDs() []DeviceID {
var rv []DeviceID
for d := range s.conf.DevPortMap {
rv = append(rv, d)
}
return rv
}
// PowerCycle sends up to two HTTP requests to the PDU's API to powercycle the given device.
// It returns an error only if the request fails to connect (it does not fail if it gets a non-2XX
// response).
func (s *synaccessClient) PowerCycle(ctx context.Context, id DeviceID, delayOverride time.Duration) error {
port, ok := s.conf.DevPortMap[id]
if !ok {
return skerr.Fmt("No mapping exists for %s", id)
}
// The API for the Synaccess series of PDU is a stateless HTTP based system.
// https://github.com/synaccess-networks/API-Examples/blob/master/examples.sh
// The $A3 one seems more consistent across models than the other API.s
turnOffCmd := s.conf.Address + "/cmd.cgi?" +
"$A3" + // Command for setting an outlet to off or on
"%20" + // URL encoded space
strconv.Itoa(port) +
"%20" + // URL encoded space
"0" // turn port off
if err := s.doGetAndIgnoreResponse(ctx, turnOffCmd); err != nil {
return skerr.Wrapf(err, "turning off port %d", port)
}
delay := powerOffDelaySynaccess
if delayOverride > 0 {
delay = delayOverride
}
sklog.Infof("Switched %s port %d off. Waiting for %s.", s.name, port, delay)
time.Sleep(delay)
turnOnCmd := s.conf.Address + "/cmd.cgi?" +
"$A3" + // Command for setting an outlet to off or on
"%20" + // URL encoded space
strconv.Itoa(port) +
"%20" + // URL encoded space
"1" // turn port off
if err := s.doGetAndIgnoreResponse(ctx, turnOnCmd); err != nil {
return skerr.Wrapf(err, "turning on port %d", port)
}
sklog.Infof("Switched %s port %d on.", s.name, port)
return nil
}
// ping makes a GET request to the IP address, which should return an index.html (some sort of
// dashboard). We ignore the return value and just error if we cannot connect.
func (s *synaccessClient) ping(ctx context.Context) error {
return skerr.Wrap(s.doGetAndIgnoreResponse(ctx, s.conf.Address))
}
// doGetAndIgnoreResponse makes a GET request to the specified URL and returns an error if the
// connection could not be made.
func (s *synaccessClient) doGetAndIgnoreResponse(ctx context.Context, url string) error {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return skerr.Wrap(err)
}
req = req.WithContext(ctx)
req.SetBasicAuth(synaccessUser, synaccessPassword)
// We only care if we could not connect. The actual response is not useful.
_, err = s.httpClient.Do(req)
return skerr.Wrapf(err, "Making request to %s", url)
}