[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)
+}