blob: d4a6d655dcf883c1c89a2c54013f81f5c5ae6e45 [file]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package amt
import (
"context"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/beevik/etree"
dac "github.com/xinsnake/go-http-digest-auth-client"
"go.chromium.org/luci/common/errors"
"infra/cros/recovery/internal/log"
)
// Map of human-readable states to AMT power states.
var powerStateMap = map[string]int{
"on": 2,
"off": 8,
}
// TODO(josephsussman): Check the namespace URI.
func findIntInResponse(response string, tagname string) (int, error) {
doc := etree.NewDocument()
err := doc.ReadFromString(response)
if err != nil {
return 0, errors.Reason("failed to parse response").Err()
}
elem := doc.FindElement(fmt.Sprintf("//%s", tagname))
value, err := strconv.Atoi(elem.Text())
if err != nil {
return 0, errors.Reason("failed to convert to int").Err()
}
return value, nil
}
func findReturnValue(response string) (int, error) {
return findIntInResponse(response, "ReturnValue")
}
func findPowerState(response string) (int, error) {
return findIntInResponse(response, "PowerState")
}
// AMTClient holds WS-Management connection data.
type AMTClient struct {
uri, username, password string
}
// NewAMTClient returns a new AMTClient instance.
func NewAMTClient(hostname string, username string, password string) AMTClient {
protocol := "http"
port := 16992
uri := fmt.Sprintf("%s://%s:%d/wsman", protocol, hostname, port)
return AMTClient{uri, username, password}
}
func (c AMTClient) post(ctx context.Context, request string) (string, error) {
log.Debugf(ctx, "Posting HTTP request: %s", request)
t := dac.NewTransport(c.username, c.password)
r, err := http.NewRequest("POST", c.uri, strings.NewReader(request))
if err != nil {
return "", errors.Reason("failed to create the request").Err()
}
r.Header.Add("Content-Type", "application/soap+xml;charset=UTF-8")
resp, err := t.RoundTrip(r)
log.Debugf(ctx, "Received HTTP status code: %d", resp.StatusCode)
if err != nil {
return "", errors.Reason("failed to post the data").Err()
}
// Work around the following linter error:
// Error return value of `resp.Body.Close` is not checked (errcheck)
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
return "", errors.Reason("responded with status %d", resp.StatusCode).Err()
}
body, _ := io.ReadAll(resp.Body)
log.Debugf(ctx, "Received HTTP response: %s", body)
return string(body), nil
}
// AMTPresent returns true if the client URI is accessible.
func (c AMTClient) AMTPresent(ctx context.Context) (bool, error) {
client := http.Client{
Timeout: 500 * time.Millisecond,
}
resp, err := client.Get(c.uri)
if err != nil {
return false, err
}
// Is the URI is not found then AMT is not enabled.
if resp.StatusCode == http.StatusNotFound {
return false, nil
} else if resp.StatusCode == http.StatusUnauthorized {
return true, nil
} else {
return false, errors.Reason("responded with status %d", resp.StatusCode).Err()
}
}
// GetPowerState returns the power state as an int.
func (c AMTClient) GetPowerState(ctx context.Context) (int, error) {
resp, err := c.post(ctx, createReadAMTPowerStateRequest(c.uri))
if err != nil {
return 0, err
}
state, err := findPowerState(resp)
if err != nil {
return 0, err
}
log.Debugf(ctx, "Got power state: %d", state)
return state, nil
}
// PowerOn powers on the DUT using Intel AMT (vPro).
func (c AMTClient) PowerOn(ctx context.Context) error {
resp, err := c.post(ctx, createUpdateAMTPowerStateRequest(c.uri, powerStateMap["on"]))
if err != nil {
return err
}
rvalue, err := findReturnValue(resp)
if err != nil {
return err
}
if rvalue != 0 {
return errors.Reason("power on failed with: %d", rvalue).Err()
}
return nil
}
// PowerOff powers off the DUT using Intel AMT (vPro).
func (c AMTClient) PowerOff(ctx context.Context) error {
resp, err := c.post(ctx, createUpdateAMTPowerStateRequest(c.uri, powerStateMap["off"]))
if err != nil {
return err
}
rvalue, err := findReturnValue(resp)
if err != nil {
return err
}
if rvalue != 0 {
return errors.Reason("power off failed with: %d", rvalue).Err()
}
return nil
}