[devices] Create power package
Create a power package in the infra/infra repo in expectation for
supporting stateful health checks. This isn't used to build any
infra utility, so should be a no-op functionally.
Change-Id: I98fcf3ff4b7a84b470d95003851cf2a8ba39193b
diff --git a/devices/power/amt.go b/devices/power/amt.go
new file mode 100644
index 0000000..7f85b6f
--- /dev/null
+++ b/devices/power/amt.go
@@ -0,0 +1,116 @@
+// 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 power
+
+import (
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+
+ "github.com/google/uuid"
+ "go.fuchsia.dev/fuchsia/tools/net/digest"
+)
+
+const (
+ // https://software.intel.com/en-us/node/645995
+ PowerStateOn = 2
+ PowerStateLightSleep = 3
+ PowerStateDeepSleep = 4
+ PowerStatePowerCycleSoft = 5
+ PowerStateOffHard = 6
+ PowerStateHibernate = 7
+ PowerStateOffSoft = 8
+ PowerStatePowerCycleHard = 9
+ PowerStateMasterBusReset = 10
+)
+
+// Printf string with placeholders for destination uri, message uuid
+const payloadTmpl = `
+<?xml version="1.0" encoding="UTF-8"?>
+<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" xmlns:wsman="http://schemas.dmtf.org/wbem/wsman/1/wsman.xsd" xmlns:pms="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService">
+<s:Header>
+ <wsa:Action s:mustUnderstand="true">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService/RequestPowerStateChange</wsa:Action>
+ <wsa:To s:mustUnderstand="true">%s</wsa:To>
+ <wsman:ResourceURI s:mustUnderstand="true">http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_PowerManagementService</wsman:ResourceURI>
+ <wsa:MessageID s:mustUnderstand="true">uuid:%s</wsa:MessageID>
+ <wsa:ReplyTo><wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address></wsa:ReplyTo>
+ <wsman:SelectorSet>
+ <wsman:Selector Name="Name">Intel(r) AMT Power Management Service</wsman:Selector>
+ <wsman:Selector Name="SystemName">Intel(r) AMT</wsman:Selector>
+ <wsman:Selector Name="CreationClassName">CIM_PowerManagementService</wsman:Selector>
+ <wsman:Selector Name="SystemCreationClassName">CIM_ComputerSystem</wsman:Selector>
+ </wsman:SelectorSet>
+</s:Header>
+<s:Body>
+ <pms:RequestPowerStateChange_INPUT>
+ <pms:PowerState>%d</pms:PowerState>
+ <pms:ManagedElement>
+ <wsa:Address>http://schemas.xmlsoap.org/ws/2004/08/addressing/role/anonymous</wsa:Address>
+ <wsa:ReferenceParameters>
+ <wsman:ResourceURI>http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ComputerSystem</wsman:ResourceURI>
+ <wsman:SelectorSet>
+ <wsman:Selector Name="Name">ManagedSystem</wsman:Selector>
+ <wsman:Selector Name="CreationClassName">CIM_ComputerSystem</wsman:Selector>
+ </wsman:SelectorSet>
+ </wsa:ReferenceParameters>
+ </pms:ManagedElement>
+ </pms:RequestPowerStateChange_INPUT>
+</s:Body>
+</s:Envelope>
+`
+
+// Reboot sends a Master Bus Reset to an AMT compatible device at host:port.
+func AMTReboot(host, username, password string) error {
+ // AMT over http always uses port 16992
+ uri, err := url.Parse(fmt.Sprintf("http://%s:16992/wsman", host))
+ if err != nil {
+ return err
+ }
+ // Generate MessageID
+ uuid := uuid.New()
+ payload := fmt.Sprintf(payloadTmpl, uri.String(), uuid, PowerStatePowerCycleSoft)
+
+ t := digest.NewTransport(username, password)
+ req, err := http.NewRequest("POST", uri.String(), strings.NewReader(payload))
+ if err != nil {
+ return err
+ }
+ res, err := t.RoundTrip(req)
+ if err != nil {
+ return err
+ }
+ defer res.Body.Close()
+
+ body, err := ioutil.ReadAll(res.Body)
+ if err != nil {
+ return err
+ }
+ returnValue := string(strings.Split(string(body), "ReturnValue>")[1][0])
+ if returnValue != "0" {
+ return fmt.Errorf("amt reboot ReturnValue=%s", returnValue)
+ }
+
+ return nil
+}
+
+type AMTPowerManager struct {
+ host string
+ user string
+ pwd string
+}
+
+func NewAMTPowerManager(host string, user string, pwd string) *AMTPowerManager {
+ return &AMTPowerManager{
+ host: host,
+ user: user,
+ pwd: pwd,
+ }
+}
+
+func (a *AMTPowerManager) Powercycle() error {
+ return AMTReboot(a.host, a.user, a.pwd)
+}
diff --git a/devices/power/pdu.go b/devices/power/pdu.go
new file mode 100644
index 0000000..c03d5fd
--- /dev/null
+++ b/devices/power/pdu.go
@@ -0,0 +1,226 @@
+// Copyright 2019 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 power
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+)
+
+// PDUConn encapsultes PDU connection settings.
+type PDUConn struct {
+ // Client is the client used to perform all requests.
+ Client *http.Client
+
+ // Host is the network hostname of the PDU.
+ Host string
+
+ // Username is the username by which we can log in to the PDU.
+ Username string
+
+ // Password is the password by which we can log in to the PDU.
+ Password string
+
+ // token is the authentication token obtained by Login.
+ token string
+}
+
+type message struct {
+ ID int `json:"msgid,omitempty"`
+ Reply string `json:"reply,omitempty"`
+ Data json.RawMessage `json:"data,omitempty"`
+ Error struct {
+ Name string `json:"name"`
+ Message string `json:"message"`
+ Status int `json:"status"`
+ } `json:"error,omitempty"`
+}
+
+// NewPDUConn initializes and returns a new PDUConn struct.
+func NewPDUConn(host string, uname string, pwd string) *PDUConn {
+ return &PDUConn{
+ Client: &http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ },
+ },
+ Host: host,
+ Username: uname,
+ Password: pwd,
+ }
+}
+
+// Login performs a login and obtains authorization token used for other calls.
+func (c *PDUConn) Login() error {
+ q := make(url.Values)
+ q.Set("username", c.Username)
+ q.Set("password", c.Password)
+
+ u := url.URL{
+ Scheme: "https",
+ Host: c.Host,
+ Path: "/api/AuthenticationControllers/login",
+ RawQuery: q.Encode(),
+ }
+
+ l := struct {
+ Username string `json:"username"`
+ Password string `json:"password"`
+ }{
+ Username: c.Username,
+ Password: c.Password,
+ }
+
+ b := new(bytes.Buffer)
+ if err := json.NewEncoder(b).Encode(l); err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("POST", u.String(), b)
+ req.Header.Add("Accept", "application/json")
+ req.Header.Set("Content-Type", "application/json")
+ if err != nil {
+ return err
+ }
+ resp, err := c.Client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ var msg message
+ err = json.NewDecoder(resp.Body).Decode(&msg)
+
+ if resp.StatusCode != http.StatusCreated {
+ if err != nil {
+ return fmt.Errorf("request failed: %d", resp.StatusCode)
+ } else {
+ return fmt.Errorf("request failed: %s", msg.Error.Message)
+ }
+ }
+
+ if token, ok := resp.Header["Authorization"]; ok {
+ c.token = token[0]
+ }
+
+ return nil
+}
+
+// PDUReboot powercycles the given outlet.
+func PDUReboot(outlet int, host string, uname string, pwd string) error {
+ conn := NewPDUConn(host, uname, pwd)
+ if err := conn.Login(); err != nil {
+ return err
+ }
+ defer conn.Logout()
+ return conn.Loads(outlet, "Cycle")
+}
+
+// Loads changes the outlet state.
+func (c *PDUConn) Loads(outlet int, state string) error {
+ u := url.URL{
+ Scheme: "https",
+ Host: c.Host,
+ Path: fmt.Sprintf("/api/device/loads/%d", outlet),
+ }
+
+ s := struct {
+ LoadFireState string `json:"loadFireState"`
+ }{
+ LoadFireState: state,
+ }
+
+ b := new(bytes.Buffer)
+ if err := json.NewEncoder(b).Encode(s); err != nil {
+ return err
+ }
+
+ req, err := http.NewRequest("PUT", u.String(), b)
+ if err != nil {
+ return err
+ }
+ req.Header.Add("Accept", "application/json")
+ req.Header.Add("Authorization", c.token)
+ req.Header.Set("Content-Type", "application/json")
+ resp, err := c.Client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ var msg message
+ err = json.NewDecoder(resp.Body).Decode(&msg)
+
+ if resp.StatusCode != http.StatusOK {
+ if err != nil {
+ return fmt.Errorf("request failed: %d", resp.StatusCode)
+ } else {
+ return fmt.Errorf("request failed: %s", msg.Error.Message)
+ }
+ }
+
+ return nil
+}
+
+// Logout performs a logout and discards the authorization token.
+func (c *PDUConn) Logout() error {
+ u := url.URL{
+ Scheme: "https",
+ Host: c.Host,
+ Path: "/api/AuthenticationControllers/logout",
+ }
+
+ req, err := http.NewRequest("POST", u.String(), nil)
+ req.Header.Add("Accept", "application/json")
+ req.Header.Add("Authorization", c.token)
+ req.Header.Set("Content-Type", "application/json")
+ if err != nil {
+ return err
+ }
+ resp, err := c.Client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ var msg message
+ err = json.NewDecoder(resp.Body).Decode(&msg)
+
+ if resp.StatusCode != http.StatusOK {
+ if err != nil {
+ return fmt.Errorf("request failed: %d", resp.StatusCode)
+ } else {
+ return fmt.Errorf("request failed: %s", msg.Error.Message)
+ }
+ }
+
+ c.token = ""
+
+ return nil
+}
+
+type PDUPowerManager struct {
+ devicePort int
+ pduIp string
+ pduUser string
+ pduPwd string
+}
+
+func NewPDUPowerManager(ip string, user string, pwd string, port int) *PDUPowerManager {
+ return &PDUPowerManager{
+ devicePort: port,
+ pduIp: ip,
+ pduUser: user,
+ pduPwd: pwd,
+ }
+}
+
+func (p *PDUPowerManager) Powercycle() error {
+ return PDUReboot(p.devicePort, p.pduIp, p.pduUser, p.pduPwd)
+}
diff --git a/devices/power/wol.go b/devices/power/wol.go
new file mode 100644
index 0000000..f6333fd
--- /dev/null
+++ b/devices/power/wol.go
@@ -0,0 +1,133 @@
+// 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 power
+
+import (
+ "errors"
+ "fmt"
+ "net"
+ "regexp"
+ "time"
+)
+
+var (
+ macAddrRegex = regexp.MustCompile(`(?i)^([0-9A-F]{2}[:-]){5}([0-9A-F]{2})$`)
+ // Magic Packet header is 0xFF repeated 6 times.
+ magicPacketHeader = []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
+)
+
+const (
+ magicPacketLength = 102
+
+ // 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"
+)
+
+// Reboot sends a WakeOnLAN magic packet {magicPacketHeader + macAddr x 16}
+// using the specified network interface to the broadcast address
+func WOLReboot(broadcastAddr, interfaceName, macAddr string) error {
+ if !macAddrRegex.Match([]byte(macAddr)) {
+ return fmt.Errorf("Invalid MAC: %s", macAddr)
+ }
+
+ remoteHwAddr, err := net.ParseMAC(macAddr)
+ if err != nil {
+ return err
+ }
+
+ localAddr, err := getUDPAddrFromIFace(interfaceName)
+ if err != nil {
+ return err
+ }
+ remoteAddr, err := net.ResolveUDPAddr("udp", broadcastAddr)
+ if err != nil {
+ return err
+ }
+
+ return sendMagicPacket(localAddr, remoteAddr, remoteHwAddr)
+}
+
+func getUDPAddrFromIFace(ifaceName string) (*net.UDPAddr, error) {
+ iface, err := net.InterfaceByName(ifaceName)
+ if err != nil {
+ return nil, err
+ }
+
+ addrs, err := iface.Addrs()
+ if err != nil {
+ return nil, err
+ }
+
+ for _, addr := range addrs {
+ if ipAddr, ok := addr.(*net.IPNet); ok {
+ // Need an IPv4, non-loopback address to send on
+ if !ipAddr.IP.IsLoopback() && ipAddr.IP.To4() != nil {
+ return &net.UDPAddr{
+ IP: ipAddr.IP,
+ }, nil
+ }
+ }
+ }
+
+ return nil, errors.New("No UDPAddr found on interface")
+}
+
+func sendMagicPacket(localAddr, remoteAddr *net.UDPAddr, remoteHwAddr net.HardwareAddr) error {
+ packet := magicPacketHeader
+ for i := 0; i < 16; i++ {
+ packet = append(packet, remoteHwAddr...)
+ }
+
+ if len(packet) != magicPacketLength {
+ return fmt.Errorf("Wake-On-LAN packet incorrect length: %d", len(packet))
+ }
+
+ conn, err := net.DialUDP("udp", localAddr, remoteAddr)
+ if err != nil {
+ return err
+ }
+ defer conn.Close()
+
+ // Attempt to send the Magic Packet TEN times in a row. The UDP packet sometimes
+ // does not make it to the DUT and this is the simplest way to increase the chance
+ // the device reboots.
+ for i := 0; i < 10; i++ {
+ n, err := conn.Write(packet)
+
+ if n != magicPacketLength {
+ return errors.New("Failed to send correct Wake-On-LAN packet length")
+ }
+
+ if err != nil {
+ return err
+ }
+ time.Sleep(1 * time.Second)
+ }
+
+ return nil
+}
+
+type WOLPowerManager struct {
+ botBroadcastAddr string
+ botInterface string
+ mac string
+}
+
+func NewWOLPowerManager(mac string) *WOLPowerManager {
+ return &WOLPowerManager{
+ botBroadcastAddr: botBroadcastAddr,
+ botInterface: botInterface,
+ mac: mac,
+ }
+}
+
+func (w *WOLPowerManager) Powercycle() error {
+ return WOLReboot(w.botBroadcastAddr, w.botInterface, w.mac)
+}