blob: 98fb50d77157c551cb4091b9ef32b13bbbebf438 [file] [log] [blame]
package powercycle
import (
"context"
"io/ioutil"
"sort"
"time"
"github.com/flynn/json5"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/machine/go/machine"
"go.skia.org/infra/machine/go/machineserver/rpc"
)
// ControllerInitCB is a callback that is called for every Controller as it
// finishes initialization. The success or failure of that initialization is
// stored in the rpc.UpdatePowerCycleStateRequest.
type ControllerInitCB func(rpc.UpdatePowerCycleStateRequest) error
// DeviceID is a unique identifier for a given machine or attached device.
type DeviceID string
// DeviceIn returns true if the given id is in the slice of DeviceID.
func DeviceIn(id DeviceID, ids []DeviceID) bool {
for _, other := range ids {
if other == id {
return true
}
}
return false
}
func sortIDs(ids []DeviceID) {
sort.Slice(ids, func(i, j int) bool {
return ids[i] < ids[j]
})
}
// Controller abstracts a set of devices that can all be controlled together.
type Controller interface {
// DeviceIDs returns a list of strings that uniquely identify the devices that can be controlled
// through this group.
DeviceIDs() []DeviceID
// PowerCycle turns the device off for a reasonable amount of time (i.e. 10 seconds) and then
// turns it back on. If delayOverride is larger than zero it overrides the default delay between
// turning the port off and on again.
PowerCycle(ctx context.Context, id DeviceID, delayOverride time.Duration) error
}
// controllerName is a human readable name (hopefully a physical label) for a given Controller.
// It is not really used by the code - this type is primarily for self-documentation purposes.
type controllerName string
// config is the overall structure to aggregate configuration options for different device types.
type config struct {
// MPower aggregates all mPower configurations.
MPower map[controllerName]*mPowerConfig `json:"mpower"`
// EdgeSwitch aggregates all EdgeSwitch configurations.
EdgeSwitch map[controllerName]*EdgeSwitchConfig `json:"edgeswitch"`
// SynaccessPDU aggregates all PDUs produced by Synaccess (https://www.synaccess-net.com/)
SynaccessPDU map[controllerName]*SynaccessConfig `json:"synaccess"`
}
// multiController allows us to combine multiple Controller implementations into one.
type multiController struct {
controllerForID map[DeviceID]Controller
}
// add adds a new Controller.
func (a *multiController) add(client Controller) error {
for _, id := range client.DeviceIDs() {
if _, ok := a.controllerForID[id]; ok {
return skerr.Fmt("Device '%s' already exists.", id)
}
a.controllerForID[id] = client
}
return nil
}
// DeviceIDs implements the Controller interface.
func (a *multiController) DeviceIDs() []DeviceID {
ret := make([]DeviceID, 0, len(a.controllerForID))
for id := range a.controllerForID {
ret = append(ret, id)
}
sortIDs(ret)
return ret
}
// PowerCycle implements the Controller interface.
func (a *multiController) PowerCycle(ctx context.Context, id DeviceID, delayOverride time.Duration) error {
ctrl, ok := a.controllerForID[id]
if !ok {
return skerr.Fmt("Unknown device id: %s", id)
}
return ctrl.PowerCycle(ctx, id, delayOverride)
}
func updatePowerCycleStateRequestFromController(c Controller, state machine.PowerCycleState) rpc.UpdatePowerCycleStateRequest {
ret := rpc.UpdatePowerCycleStateRequest{Machines: []rpc.PowerCycleStateForMachine{}}
if c == nil {
return ret
}
for _, deviceID := range c.DeviceIDs() {
ret.Machines = append(ret.Machines, rpc.PowerCycleStateForMachine{
MachineID: string(deviceID),
PowerCycleState: state,
})
}
return ret
}
// controllerFromConfig creates a Controll from the given config. If connect is
// true, an attempt will be made to connect to the subclients and errors will be
// returned if they are not accessible.
func controllerFromConfig(ctx context.Context, conf config, connect bool, controllerInitCallback ControllerInitCB) (Controller, error) {
ret := &multiController{
controllerForID: map[DeviceID]Controller{},
}
// Add the mpower devices.
for name, c := range conf.MPower {
mp, err := newMPowerController(ctx, c, connect)
if err != nil {
sklog.Errorf("failed to initialize %s", name)
if err := controllerInitCallback(updatePowerCycleStateRequestFromController(mp, machine.InError)); err != nil {
return nil, skerr.Wrap(err)
}
continue
}
// TODO(kjlubick) add test for duplicate device names.
if err := ret.add(mp); err != nil {
return nil, skerr.Wrapf(err, "incorporating %s", name)
}
if err := controllerInitCallback(updatePowerCycleStateRequestFromController(mp, machine.Available)); err != nil {
return nil, skerr.Wrap(err)
}
}
// Add the EdgeSwitch devices.
for name, c := range conf.EdgeSwitch {
es, err := newEdgeSwitchController(ctx, c, connect)
if err != nil {
sklog.Errorf("failed to initialize %s", name)
if err := controllerInitCallback(updatePowerCycleStateRequestFromController(es, machine.InError)); err != nil {
return nil, skerr.Wrap(err)
}
continue
}
if err := ret.add(es); err != nil {
return nil, skerr.Wrapf(err, "incorporating %s", name)
}
if err := controllerInitCallback(updatePowerCycleStateRequestFromController(es, machine.Available)); err != nil {
return nil, skerr.Wrap(err)
}
}
for name, c := range conf.SynaccessPDU {
es, err := newSynaccessController(ctx, string(name), c, connect)
if err != nil {
sklog.Errorf("failed to initialize %s", name)
if err := controllerInitCallback(updatePowerCycleStateRequestFromController(es, machine.InError)); err != nil {
return nil, skerr.Wrap(err)
}
continue
}
if err := ret.add(es); err != nil {
return nil, skerr.Wrapf(err, "incorporating %s", name)
}
if err := controllerInitCallback(updatePowerCycleStateRequestFromController(es, machine.Available)); err != nil {
return nil, skerr.Wrap(err)
}
}
return ret, nil
}
// ControllerFromJSON5 parses a JSON5 file and instantiates the defined devices.
// If connect is true, an attempt will be made to connect to the subclients and
// errors will be returned if they are not accessible. The ControllerInitCB is
// called once for each controller with the state for each machine it controls
func ControllerFromJSON5(ctx context.Context, path string, connect bool, cb ControllerInitCB) (Controller, error) {
conf, err := readConfig(path)
if err != nil {
return nil, skerr.Wrap(err)
}
return controllerFromConfig(ctx, conf, connect, cb)
}
// ControllerFromJSON5Bytes parses a JSON5 file and instantiates the defined
// devices. If connect is true, an attempt will be made to connect to the
// subclients and errors will be returned if they are not accessible. The
// ControllerInitCB is called once for each controller with the state for each
// machine it controls.
func ControllerFromJSON5Bytes(ctx context.Context, configFileBytes []byte, connect bool, controllerInitCallback ControllerInitCB) (Controller, error) {
var conf config
if err := json5.Unmarshal(configFileBytes, &conf); err != nil {
return nil, skerr.Wrapf(err, "reading JSON5 bytes")
}
return controllerFromConfig(ctx, conf, connect, controllerInitCallback)
}
func readConfig(path string) (config, error) {
conf := config{}
jsonBytes, err := ioutil.ReadFile(path)
if err != nil {
return conf, skerr.Wrapf(err, "reading %s", path)
}
if err := json5.Unmarshal(jsonBytes, &conf); err != nil {
return conf, skerr.Wrapf(err, "reading JSON5 from %s", path)
}
return conf, nil
}