| package powercycle |
| |
| import ( |
| "context" |
| "os" |
| "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: %s", name, err) |
| 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: %s", name, err) |
| 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: %s", name, err) |
| 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 := os.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 |
| } |