Revert "[devices] Model devices as FSMs"
This reverts commit c9f435d4de72ab4cb8e01ac70621a0e04dc14647.
Reason for revert: This code is moving to google3.
Original change's description:
> [devices] Model devices as FSMs
>
> This change does several things:
> 1) Model Fuchsia devices as state machines
> 2) Modify health checker and catalyst to use the new device interface.
>
> Change-Id: Ic3725be39d007223dd76325bd3de1e3532da7a2c
TBR=nmulcahey@google.com,rudymathu@google.com
# Not skipping CQ checks because original CL landed > 1 day ago.
Change-Id: I4452c72d71cd420427537db2460caa210dcebf73
diff --git a/cmd/catalyst/catalyst_test.go b/cmd/catalyst/catalyst_test.go
index 5e887d0..2f5f115 100644
--- a/cmd/catalyst/catalyst_test.go
+++ b/cmd/catalyst/catalyst_test.go
@@ -7,7 +7,6 @@
import (
"context"
"fmt"
- "log"
"math/rand"
"net"
"net/http"
@@ -84,29 +83,43 @@
checkExitCode(t, exitCode, successExitCode)
}
-func createMockDevice(name string, mac string, cmd []string) devicePkg.Device {
+func createMockDevice(name string, mac string, cmd []string) *devicePkg.DeviceTarget {
+ target := &devicePkg.DeviceTarget{BootserverCmd: cmd}
config := devicePkg.DeviceConfig{
- Network: &devicePkg.NetworkProperties{
+ Network: devicePkg.NetworkProperties{
Nodename: name,
IPv4Addr: "000.000.00.0",
Mac: mac,
},
- Power: &devicePkg.PowerClient{
- Host: "000.000.00.0",
- Username: "user",
- Password: "Pwd",
- },
}
- target, err := devicePkg.NewNuc(config, cmd)
- if err != nil {
- log.Fatalf("creating mock device failed: %v", err)
- }
+ target.SetConfig(config)
return target
}
+// Test running runBootservers with successful results using mock devices.
+func TestBootserverSuccess(t *testing.T) {
+ cmd := constructDummyCmd(false)
+ devices := []*devicePkg.DeviceTarget{
+ createMockDevice("dummy1", "00:00:00:00:00", cmd),
+ createMockDevice("dummy2", "00:00:00:00:01", cmd),
+ }
+ exitCode := runBootservers(context.Background(), devices)
+ checkExitCode(t, exitCode, successExitCode)
+}
+
+// Test running runBootservers with failed results using mock devices.
+func TestBootserverFailure(t *testing.T) {
+ devices := []*devicePkg.DeviceTarget{
+ createMockDevice("dummy1", "00:00:00:00:00", constructDummyCmd(false)),
+ createMockDevice("dummy2", "00:00:00:00:01", constructDummyCmd(true)),
+ }
+ exitCode := runBootservers(context.Background(), devices)
+ checkExitCode(t, exitCode, failureExitCode)
+}
+
// Test that NUC server does not start up when port environment variable isn't set.
func TestNUCServerNonexistence(t *testing.T) {
- var devices []devicePkg.Device
+ var devices []*devicePkg.DeviceTarget
srv, err := runNUCServer(context.Background(), devices, false)
if srv != nil {
t.Errorf("NUC server was created without port set.")
@@ -117,7 +130,7 @@
// Test that NUC server is disabled when flag is set and port environment variable is set.
func TestNUCServerDisabled(t *testing.T) {
- var devices []devicePkg.Device
+ var devices []*devicePkg.DeviceTarget
os.Setenv(portEnvVar, "8000")
srv, err := runNUCServer(context.Background(), devices, true)
if srv != nil {
@@ -130,7 +143,7 @@
// Test that NUC server is started up when port environment variable is set.
func TestNUCServerExistence(t *testing.T) {
- var devices []devicePkg.Device
+ var devices []*devicePkg.DeviceTarget
os.Setenv(portEnvVar, "8000")
srv, err := runNUCServer(context.Background(), devices, false)
os.Setenv(portEnvVar, "")
@@ -147,7 +160,7 @@
func TestNUCServerEndpoints(t *testing.T) {
// Create mock devices
cmd := constructDummyCmd(false)
- devices := []devicePkg.Device{
+ devices := []*devicePkg.DeviceTarget{
createMockDevice("dummy1", "00:00:00:00:00", cmd),
createMockDevice("dummy2", "00:00:00:00:01", cmd),
}
diff --git a/cmd/catalyst/main.go b/cmd/catalyst/main.go
index 5ecd219..6b3541c 100644
--- a/cmd/catalyst/main.go
+++ b/cmd/catalyst/main.go
@@ -79,9 +79,7 @@
return cmd.ProcessState.ExitCode()
}
-// runNUCServer runs an http server that delivers chainloaders/images to NUCs based on their mac.
-// These chainloaders deliver a build's version of zedboot and an exit chainloader (to allow reboot from disk).
-func runNUCServer(ctx context.Context, devices []devicePkg.Device, disabled bool) (*http.Server, error) {
+func runNUCServer(ctx context.Context, devices []*devicePkg.DeviceTarget, disabled bool) (*http.Server, error) {
// Parse the port from the environment variable.
portStr := os.Getenv(portEnvVar)
if portStr == "" || disabled {
@@ -126,36 +124,48 @@
return srv, nil
}
-// prepDevices puts all devices into TaskState.
-func prepDevices(ctx context.Context, devices []devicePkg.Device) error {
- // Put each device into TaskState.
- errorChannel := make(chan error)
+func runBootservers(ctx context.Context, devices []*devicePkg.DeviceTarget) int {
+ // Execute bootserver for each node that isn't a NUC
+ exitCodes := make(chan int)
+ numSubprocesses := 0
for _, device := range devices {
- go func(device devicePkg.Device) {
- errorChannel <- device.ToTaskState(ctx)
- }(device)
- }
- for i := 0; i < len(devices); i++ {
- if err := <-errorChannel; err != nil {
- return err
+ if device.Type() != "nuc" {
+ go func(device *devicePkg.DeviceTarget) {
+ exitCodes <- runSubprocess(ctx, device.BootserverCmd)
+ }(device)
+ numSubprocesses += 1
+ } else {
+ // If this is a NUC, rebooting will allow iPXE to retrieve the
+ // new version of zedboot.
+ device.Powercycle(ctx)
}
}
- return nil
+
+ // Wait for all of the bootservers to finish running and ensure success
+ numErrs := 0
+ for i := 0; i < numSubprocesses; i++ {
+ if exitCode := <-exitCodes; exitCode != 0 {
+ log.Printf("bootserver exited with exit code: %d\n", exitCode)
+ numErrs += 1
+ }
+ }
+
+ if numErrs > 0 {
+ return failureExitCode
+ }
+ return 0
}
-// hasNUC returns true iff this testbed contains a NUC.
-func hasNUC(devices []devicePkg.Device) bool {
+func hasNUC(devices []*devicePkg.DeviceTarget) bool {
for _, device := range devices {
- if _, ok := device.(*devicePkg.Nuc); ok {
+ if device.Type() == "nuc" {
return true
}
}
return false
}
-// downloadZedboot retrieves zedboot.zbi from GCS so that we can use the proper image
-// in NUCs.
-func downloadZedboot(ctx context.Context, imageURL *url.URL, devices []devicePkg.Device) error {
+func downloadZedboot(ctx context.Context, imageURL *url.URL, devices []*devicePkg.DeviceTarget) error {
// Ensure that we have a NUC that needs a local zedboot.zbi.
if !hasNUC(devices) {
return nil
@@ -211,12 +221,8 @@
"-n",
}
- // Create devicePkg.Devices for each of the devices in the config file
- configs, err := devicePkg.LoadDeviceConfigs(deviceConfigPath)
- if err != nil {
- return failureExitCode, err
- }
- devices, err := devicePkg.CreateDevices(ctx, configs, bootserverCmdStub)
+ // Create devicePkg.DeviceTargets for each of the devices in the config file
+ devices, err := devicePkg.CreateDeviceTargets(ctx, deviceConfigPath, bootserverCmdStub)
if err != nil {
return failureExitCode, err
}
@@ -251,8 +257,8 @@
}
}()
- if err := prepDevices(ctx, devices); err != nil {
- return failureExitCode, err
+ if exitCode := runBootservers(ctx, devices); exitCode != 0 {
+ return exitCode, nil
}
time.Sleep(rebootDuration)
diff --git a/cmd/health_checker/main.go b/cmd/health_checker/main.go
index b560a36..61cca5b 100644
--- a/cmd/health_checker/main.go
+++ b/cmd/health_checker/main.go
@@ -8,6 +8,7 @@
"encoding/json"
"flag"
"fmt"
+ "io"
"log"
"net"
"os"
@@ -34,9 +35,9 @@
)
const (
- healthyState = "healthy"
- unhealthyState = "unhealthy"
- logFile = "/tmp/health_checker.log"
+ healthyState = "healthy"
+ unhealthyState = "unhealthy"
+ logFile = "/tmp/health_checker.log"
)
// DeviceHealthProperties contains health properties of a hardware device.
@@ -91,17 +92,20 @@
// response is received - is a no-op if the device doesn't have serial
// this is also a no-op for everything other than NUC, as the check seems flaky
// on astros/sherlocks
-func checkSerial(device devicePkg.Device) error {
- if _, ok := device.(*devicePkg.Nuc); !ok {
+func checkSerial(device *devicePkg.DeviceTarget) error {
+ if device.Type() != "nuc" {
return nil
}
- cmdString := "echo hello"
+ if device.Serial() == nil {
+ return nil
+ }
+ cmdString := "\necho hello\n"
resultString := "\r\n$ echo hello\r\nhello"
- if err := device.SendSerialCommand(context.Background(), cmdString); err != nil {
+ if _, err := io.WriteString(device.Serial(), cmdString); err != nil {
return err
}
buffer := make([]byte, len(resultString))
- if err := device.ReadSerialData(context.Background(), buffer); err != nil {
+ if _, err := io.ReadAtLeast(device.Serial(), buffer, len(resultString)); err != nil {
return err
}
if string(buffer) != resultString {
@@ -113,8 +117,8 @@
// checkBroadcasting ensures that broadcast packets are being sent by the device
// is a no-op on NUCs
-func checkBroadcasting(n *netboot.Client, device devicePkg.Device) error {
- if _, ok := device.(*devicePkg.ArmCDCether); !ok {
+func checkBroadcasting(n *netboot.Client, device *devicePkg.DeviceTarget) error {
+ if device.Type() == "nuc" {
return nil
}
if _, err := n.Beacon(); err != nil {
@@ -123,7 +127,7 @@
return nil
}
-func checkHealth(n *netboot.Client, device devicePkg.Device) HealthCheckResult {
+func checkHealth(n *netboot.Client, device *devicePkg.DeviceTarget) HealthCheckResult {
nodename := device.Nodename()
log.Printf("Checking health for %s", nodename)
// Check the device is in zedboot.
@@ -169,11 +173,7 @@
flag.Parse()
client := netboot.NewClient(timeout)
ctx := context.Background()
- configs, err := devicePkg.LoadDeviceConfigs(configFile)
- if err != nil {
- log.Fatal(err)
- }
- devices, err := devicePkg.CreateDevices(ctx, configs, nil)
+ devices, err := devicePkg.CreateDeviceTargets(ctx, configFile, nil)
if err != nil {
log.Fatal(err)
}
@@ -188,18 +188,15 @@
if forceReboot {
for _, device := range devices {
log.Printf("attempting forced device restart for: %s", device.Nodename())
- if err := device.Powercycle(ctx); err == nil {
- time.Sleep(1 * time.Minute)
- } else {
- log.Printf("powercycle failed: %v", err)
- }
- if device.HasSerial() {
- if err := device.SoftReboot(ctx, "R", "serial"); err != nil {
- log.Printf("serial reboot failed: %v", err)
+ if device.Serial() == nil {
+ log.Printf("device does not have serial, powercycling")
+ if err := device.Powercycle(ctx); err != nil {
+ log.Printf("powercycle failed: %s", err.Error())
}
} else {
- if err := device.SoftReboot(ctx, "R", "ssh"); err != nil {
- log.Printf("ssh reboot failed: %v", err)
+ log.Printf("device has serial, restarting")
+ if err := device.Restart(ctx); err != nil {
+ log.Printf("forced restart failed with error: %s", err.Error())
}
}
log.Printf("forced restart for device %s is complete", device.Nodename())
@@ -212,19 +209,16 @@
checkResult := checkHealth(client, device)
log.Printf("state=%s, error_msg=%s", checkResult.State, checkResult.ErrorMsg)
if checkResult.State == unhealthyState && rebootIfUnhealthy {
- if err := device.Powercycle(ctx); err == nil {
- time.Sleep(1 * time.Minute)
- } else {
- log.Printf("powercycle failed: %v", err)
- }
- if device.HasSerial() {
- if err := device.SoftReboot(ctx, "R", "serial"); err != nil {
- log.Printf("serial reboot failed: %v", err)
+ if err := device.Powercycle(ctx); err != nil {
+ log.Printf("powercycle call failed with error: %s", err.Error())
+ if err := device.Restart(ctx); err != nil {
+ log.Printf("restart call failed with error: %s", err.Error())
+ checkResult.ErrorMsg += "; Failed to perform powercycle and restart"
+ } else {
+ checkResult.ErrorMsg += "; Failed to perform powercycle; restart succeeded"
}
} else {
- if err := device.SoftReboot(ctx, "R", "ssh"); err != nil {
- log.Printf("ssh reboot failed: %v", err)
- }
+ log.Printf("powercycle call succeeded for %s", device.Nodename())
}
}
checkResultSlice = append(checkResultSlice, checkResult)
diff --git a/devices/amt.go b/devices/amt.go
new file mode 100644
index 0000000..40cc5b4
--- /dev/null
+++ b/devices/amt.go
@@ -0,0 +1,98 @@
+// 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 devices
+
+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
+}
diff --git a/devices/arm_cdcether.go b/devices/arm_cdcether.go
deleted file mode 100644
index ac45ed9..0000000
--- a/devices/arm_cdcether.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package devices
-
-import (
- "context"
- "fmt"
- "log"
- "os"
- "os/exec"
- "strconv"
- "syscall"
- "time"
-
- "go.fuchsia.dev/infra/devices/power"
- "go.fuchsia.dev/tools/netboot"
-)
-
-// ArmCDCether represents a generic arm cdcether product. It currently encompasses
-// 1) Sherlock
-// 2) Astro
-// 3) Nelson
-type ArmCDCether struct {
- fuchsiaDevice
-}
-
-// NewArmCDCether initializes Astros, Sherlocks, and Nelsons as Arm CDCether objects.
-func NewArmCDCether(config DeviceConfig, bootserverCmdStub []string) (*ArmCDCether, error) {
- var powerManager powerManager = nil
- if config.Power != nil {
- port, err := strconv.Atoi(config.Power.PDUPort)
- if err != nil {
- return nil, err
- }
- powerManager = power.NewPDUPowerManager(
- config.Power.PDUIp,
- config.Power.Username,
- config.Power.Password,
- port,
- )
- }
-
- sd := &ArmCDCether{
- fuchsiaDevice{
- power: powerManager,
- },
- }
- if err := initFuchsiaDevice(&sd.fuchsiaDevice, config, bootserverCmdStub); err != nil {
- return nil, err
- }
-
- // All transitions in a ArmCDCether attempt to get it into zedboot.
- transitionMap := map[DeviceState]*Transition{
- Initial: &Transition{
- PerformAction: sd.sshRebootRecovery,
- Validate: sd.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: "ssh_failed",
- },
- "ssh_failed": &Transition{
- PerformAction: sd.serialRebootRecovery,
- Validate: sd.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: "serial_failed",
- },
- "serial_failed": &Transition{
- PerformAction: sd.PowercycleIntoRecovery,
- Validate: sd.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: Unrecoverable,
- },
- }
- sd.transitionMap = transitionMap
- return sd, nil
-}
-
-// PowercycleIntoRecovery powercycles the device and then sends a serial command
-// to boot into the recovery partition.
-func (s *ArmCDCether) PowercycleIntoRecovery(ctx context.Context) error {
- if err := s.Powercycle(ctx); err != nil {
- return err
- }
- time.Sleep(powercycleWait)
- return s.SoftReboot(ctx, "R", "serial")
-}
-
-// checkTransitionSuccess is the validation function used by all transitions
-// performed by a ArmCDCether. It verfies that the devices is in zedboot and
-// broadcasting.
-func (s *ArmCDCether) checkTransitionSuccess(ctx context.Context) error {
- // Give the device time to transition.
- time.Sleep(1 * time.Minute)
- client := netboot.NewClient(netbootTimeout)
- if err := checkBroadcasting(client); err != nil {
- return err
- }
- return deviceInZedboot(client, s.nodename)
-}
-
-// checkBroadcasting ensures that the ArmCDCether is sending out advertising
-// packets.
-func checkBroadcasting(n *netboot.Client) error {
- if _, err := n.Beacon(); err != nil {
- return err
- }
- return nil
-}
-
-// ToTaskState puts the ArmCDCether into a state in which it can run tasks.
-func (s *ArmCDCether) ToTaskState(ctx context.Context) error {
- cmd := exec.Command(s.BootserverCmd[0], s.BootserverCmd[1:]...)
-
- // Spin up handler to exit subprocess cleanly on sigterm.
- processDone := make(chan bool, 1)
- go func() {
- select {
- case <-processDone:
- case <-ctx.Done():
- if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
- log.Printf("exited cmd with error %v", err)
- }
- }
- }()
-
- // Ensure the context still exists before running the subprocess.
- if ctx.Err() != nil {
- return nil
- }
-
- // Run the bootserver.
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Run()
- processDone <- true
- if !cmd.ProcessState.Success() {
- return fmt.Errorf("bootserver for %s exited with exit code %d", s.nodename, cmd.ProcessState.ExitCode())
- }
- return nil
-}
diff --git a/devices/config.go b/devices/config.go
deleted file mode 100644
index 81624da..0000000
--- a/devices/config.go
+++ /dev/null
@@ -1,150 +0,0 @@
-package devices
-
-import (
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io/ioutil"
-
- "golang.org/x/crypto/ssh"
-)
-
-// DeviceConfig contains the static properties of a target device.
-type DeviceConfig struct {
- // Type tells us what kind of machine this is.
- Type string `json:"type"`
-
- // Network is the network properties of the target.
- Network *NetworkProperties `json:"network"`
-
- // Power is the attached power management configuration.
- Power *PowerClient `json:"power,omitempty"`
-
- // SSHKeys are the default system keys to be used with the device.
- SSHKeys []string `json:"keys,omitempty"`
-
- // Serial is the path to the device file for serial i/o.
- Serial string `json:"serial,omitempty"`
-}
-
-// NetworkProperties are the static network properties of a target.
-type NetworkProperties struct {
- // Nodename is the hostname of the device that we want to boot on.
- Nodename string `json:"nodename"`
-
- // IPv4Addr is the IPv4 address, if statically given. If not provided, it may be
- // resolved via the netstack's MDNS server.
- IPv4Addr string `json:"ipv4"`
-
- // Mac is the mac address of the device
- Mac string `json:"mac"`
-
- // The network interface used to communicate with this device.
- Interface string `json:"interface"`
-}
-
-// PowerClient represents a power management configuration for a particular device.
-type PowerClient struct {
- // Type is the type of manager to use.
- Type string `json:"type"`
-
- // Host is the network hostname of the manager
- Host string `json:"host"`
-
- // Username is the username used to log in to the manager.
- Username string `json:"username"`
-
- // Password is the password used to log in to the manager..
- Password string `json:"password"`
-
- // PDUIp is the IP address of the pdu
- PDUIp string `json:"pduIp"`
-
- // PDUPort is the port the PDU uses to connect to this device.
- PDUPort string `json:"pduPort"`
-}
-
-// Device represents a generic device in our infrastructure.
-// TODO(rudymathu) This interface should eventually be pared down to:
-// State()
-// Nodename()
-// Transition()
-type Device interface {
- State() DeviceState
- Nodename() string
- Mac() string
- Interface() string
- HasSerial() bool
- Transition(context.Context) error
- ToTaskState(context.Context) error
- Powercycle(context.Context) error
- SoftReboot(context.Context, string, string) error
- SendSerialCommand(context.Context, string) error
- ReadSerialData(context.Context, []byte) error
-}
-
-// LoadDeviceConfigs unmarshalls a slice of device configs from a given file.
-func LoadDeviceConfigs(path string) ([]DeviceConfig, error) {
- data, err := ioutil.ReadFile(path)
- if err != nil {
- return nil, fmt.Errorf("failed to read device properties file %q", path)
- }
-
- var configs []DeviceConfig
- if err := json.Unmarshal(data, &configs); err != nil {
- return nil, fmt.Errorf("failed to unmarshal configs: %v", err)
- }
- return configs, nil
-}
-
-// CreateDevices loads configs from the given file and creates an appropriate Device for each one.
-func CreateDevices(ctx context.Context, deviceConfigs []DeviceConfig, bootserverCmdStub []string) ([]Device, error) {
- var devices []Device
- for _, deviceConfig := range deviceConfigs {
- var device Device
- var err error
- switch deviceConfig.Type {
- case "Intel NUC Kit NUC7i5DNHE":
- device, err = NewNuc(deviceConfig, bootserverCmdStub)
- case "Khadas Vim2 Max":
- device, err = NewVim(deviceConfig, bootserverCmdStub)
- case "Astro":
- fallthrough
- case "Sherlock":
- fallthrough
- case "Nelson":
- device, err = NewArmCDCether(deviceConfig, bootserverCmdStub)
- }
- if err != nil {
- return nil, err
- }
- devices = append(devices, device)
- }
- return devices, nil
-}
-
-// parseOutSigners parses out ssh signers from the given key files.
-func parseOutSigners(keyPaths []string) ([]ssh.Signer, error) {
- if len(keyPaths) == 0 {
- return nil, errors.New("must supply SSH keys in the config")
- }
- var keys [][]byte
- for _, keyPath := range keyPaths {
- p, err := ioutil.ReadFile(keyPath)
- if err != nil {
- return nil, fmt.Errorf("could not read SSH key file %q: %v", keyPath, err)
- }
- keys = append(keys, p)
- }
-
- var signers []ssh.Signer
- for _, p := range keys {
- signer, err := ssh.ParsePrivateKey(p)
- if err != nil {
- return nil, err
- }
- signers = append(signers, signer)
- }
- return signers, nil
-}
diff --git a/devices/device.go b/devices/device.go
index 1069375..1c3b651 100644
--- a/devices/device.go
+++ b/devices/device.go
@@ -2,279 +2,217 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-// Package devices models devices as state machines, and exposes capabilities for
-// other utilites to work with.
package devices
import (
"context"
+ "encoding/json"
+ "errors"
"fmt"
"io"
+ "io/ioutil"
+ "log"
"net"
- "os/exec"
"time"
"go.fuchsia.dev/fuchsia/tools/net/serial"
- "go.fuchsia.dev/fuchsia/tools/net/sshutil"
- "go.fuchsia.dev/tools/netboot"
+
"golang.org/x/crypto/ssh"
)
-// DeviceState represents a physical state the device can be in.
-// It is used when performing device recovery.
-type DeviceState string
-
-// Transition specifies an action to perform, a validation function to
-// determine if the action succeeded or failed, and the appropriate
-// states to put the device in.
-type Transition struct {
- PerformAction func(context.Context) error
- Validate func(context.Context) error
- SuccessState DeviceState
- FailureState DeviceState
-}
-
const (
- // Healthy refers to the state in which the device is booted into zedboot.
- Healthy DeviceState = "zedboot"
- // Devices are marked Unrecoverable if all recovery attempts fail.
- Unrecoverable DeviceState = "unrecoverable"
- // All devices are created in the Inital state. This is the entrypoint into the
- // state machine.
- Initial DeviceState = "unknown"
- // Duration to wait before sending dm reboot-recovery after powercycle.
- powercycleWait = 1 * time.Minute
- // netbootTimeout is the duration to wait when creating a new netboot client.
- netbootTimeout = 10 * time.Second
+ // The duration we allow for the netstack to come up when booting.
+ netstackTimeout = 90 * time.Second
)
-// powerManager describes any object that gives a device powercycle functionality.
-// Different devices use different power management systems, so each device is expected
-// to provide a concrete implementation of this.
-type powerManager interface {
- Powercycle(context.Context) error
+type DeviceType string
+
+// DeviceConfig contains the static properties of a target device.
+type DeviceConfig struct {
+ // Type tells us what kind of machine this is.
+ Type string `json:"type"`
+
+ // Network is the network properties of the target.
+ Network NetworkProperties `json:"network"`
+
+ // Power is the attached power management configuration.
+ Power *PowerClient `json:"power,omitempty"`
+
+ // SSHKeys are the default system keys to be used with the device.
+ SSHKeys []string `json:"keys,omitempty"`
+
+ // Serial is the path to the device file for serial i/o.
+ Serial string `json:"serial,omitempty"`
}
-// fuchsiaDevice represents an abstract fuchsia device. It should not be
-// directly instantiated.
-type fuchsiaDevice struct {
- mac string
- nodename string
- networkIf string
- state DeviceState
- transitionMap map[DeviceState]*Transition
+// NetworkProperties are the static network properties of a target.
+type NetworkProperties struct {
+ // Nodename is the hostname of the device that we want to boot on.
+ Nodename string `json:"nodename"`
+
+ // IPv4Addr is the IPv4 address, if statically given. If not provided, it may be
+ // resolved via the netstack's MDNS server.
+ IPv4Addr string `json:"ipv4"`
+
+ // Mac is the mac address of the device
+ Mac string `json:"mac"`
+}
+
+// LoadDeviceConfigs unmarshalls a slice of device configs from a given file.
+func LoadDeviceConfigs(path string) ([]DeviceConfig, error) {
+ data, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, fmt.Errorf("failed to read device properties file %q", path)
+ }
+
+ var configs []DeviceConfig
+ if err := json.Unmarshal(data, &configs); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal configs: %v", err)
+ }
+ return configs, nil
+}
+
+// DeviceTarget represents a target device.
+type DeviceTarget struct {
+ deviceType DeviceType
+ config DeviceConfig
signers []ssh.Signer
serial io.ReadWriteCloser
BootserverCmd []string
- power powerManager
}
-// initFuchsiaDevice initializes the fields in fuchsiaDevice using the given config.
-// Concrete implementations of fuchsiaDevice can use this to avoid code duplication.
-func initFuchsiaDevice(device *fuchsiaDevice, config DeviceConfig, bootserverCmdStub []string) error {
- var signers []ssh.Signer
- var err error
- if config.SSHKeys != nil {
- signers, err = parseOutSigners(config.SSHKeys)
+// CreateDeviceTargets loads configs from the given file and creates DeviceTargets for each one
+func CreateDeviceTargets(ctx context.Context, path string, bootserverCmdStub []string) ([]*DeviceTarget, error) {
+ deviceConfigs, err := LoadDeviceConfigs(path)
+ if err != nil {
+ return nil, err
+ }
+ var devices []*DeviceTarget
+ for _, deviceConfig := range deviceConfigs {
+ device, err := NewDeviceTarget(ctx, deviceConfig, bootserverCmdStub)
if err != nil {
- return fmt.Errorf("could not parse out signers from private keys: %v", err)
+ return nil, err
}
+ devices = append(devices, device)
+ }
+ return devices, nil
+}
+
+// NewDeviceTarget returns a new device target with a given configuration.
+func NewDeviceTarget(ctx context.Context, config DeviceConfig, bootserverCmdStub []string) (*DeviceTarget, error) {
+ signers, err := parseOutSigners(config.SSHKeys)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse out signers from private keys: %v", err)
}
var s io.ReadWriteCloser
if config.Serial != "" {
s, err = serial.Open(config.Serial)
if err != nil {
- return fmt.Errorf("could not open serial line: %s", config.Serial)
+ log.Printf("unable to open %s: %v", config.Serial, err)
}
}
- device.nodename = config.Network.Nodename
- device.networkIf = config.Network.Interface
- device.state = Initial
- device.signers = signers
- device.serial = s
- device.mac = config.Network.Mac
- device.BootserverCmd = append(bootserverCmdStub, device.nodename)
- return nil
+ deviceType, err := ConvertConfigType(config)
+ if err != nil {
+ return nil, err
+ }
+ return &DeviceTarget{
+ deviceType: deviceType,
+ config: config,
+ signers: signers,
+ serial: s,
+ BootserverCmd: append(bootserverCmdStub, config.Network.Nodename),
+ }, nil
}
-// Mac returns the mac address of this device.
-func (f *fuchsiaDevice) Mac() string {
- return f.mac
+// ConvertConfigType converts a device type from the configs into a more
+// manageable representation.
+func ConvertConfigType(config DeviceConfig) (DeviceType, error) {
+ switch config.Type {
+ case "Intel NUC Kit NUC7i5DNHE":
+ return "nuc", nil
+ case "Khadas Vim2 Max":
+ return "vim2", nil
+ case "Astro":
+ return "astro", nil
+ case "Sherlock":
+ return "sherlock", nil
+ case "Nelson":
+ return "nelson", nil
+ }
+ return "", fmt.Errorf("Invalid device type in configs: %s", config.Type)
}
-// Interface returns the network interface of this device.
-func (f *fuchsiaDevice) Interface() string {
- return f.networkIf
+// SetConfig sets the config field of the given DeviceTarget
+func (t *DeviceTarget) SetConfig(config DeviceConfig) {
+ t.config = config
}
-// HasSerial returns true if this device has a serial line.
-func (f *fuchsiaDevice) HasSerial() bool {
- return f.serial != nil
+// Nodename returns the name of the node.
+func (t *DeviceTarget) Nodename() string {
+ return t.config.Network.Nodename
}
-// Powercycle uses an out of band method to reboot a fuchsia device.
-// Is a no-op if no out of band is possible.
-// TODO(rudymathu): remove the no-op as soon as all devices have an out of band
-// reboot method.
-func (f *fuchsiaDevice) Powercycle(ctx context.Context) error {
- if f.power != nil {
- return f.power.Powercycle(ctx)
+// IPv4Addr returns the IPv4 address of the node from the config
+func (t *DeviceTarget) IPv4Addr() (net.IP, error) {
+ return net.ParseIP(t.config.Network.IPv4Addr), nil
+}
+
+// Mac returns the Mac address of the node from the config
+func (t *DeviceTarget) Mac() string {
+ return t.config.Network.Mac
+}
+
+// Serial returns the serial device associated with the target for serial i/o.
+func (t *DeviceTarget) Serial() io.ReadWriteCloser {
+ return t.serial
+}
+
+// Type returns the type of the device.
+func (t *DeviceTarget) Type() DeviceType {
+ return t.deviceType
+}
+
+// Restart restarts the target.
+func (t *DeviceTarget) Restart(ctx context.Context) error {
+ if t.serial != nil {
+ defer t.serial.Close()
+ }
+ if t.config.Power != nil {
+ if err := t.config.Power.RebootDevice(t.signers, t.Nodename(), t.serial); err != nil {
+ return fmt.Errorf("failed to reboot the device: %v", err)
+ }
}
return nil
}
-// SendSSHCommand lets us send a command via SSH to the fuchsia device.
-func (f *fuchsiaDevice) SendSSHCommand(ctx context.Context, command string) error {
- if f.signers == nil {
- return fmt.Errorf("device %s does not support SSH", f.nodename)
- }
- config, err := sshutil.DefaultSSHConfigFromSigners(f.signers...)
- if err != nil {
- return err
- }
-
- client, err := sshutil.ConnectToNode(ctx, f.nodename, config)
- if err != nil {
- return err
- }
-
- defer client.Close()
-
- session, err := client.NewSession()
- if err != nil {
- return err
- }
-
- defer session.Close()
-
- // Invoke `dm reboot-recovery`
- err = session.Start(command)
- if err != nil {
- return err
- }
-
- done := make(chan error)
- go func() {
- done <- session.Wait()
- }()
-
- select {
- case err := <-done:
- return err
- case <-time.After(10 * time.Second):
- return nil
- }
-}
-
-// SendSerialCommand lets us send a command via serial to the fuchsia device.
-func (f *fuchsiaDevice) SendSerialCommand(ctx context.Context, command string) error {
- if f.serial == nil {
- return fmt.Errorf("device %s does not support serial", f.nodename)
- }
- _, err := io.WriteString(f.serial, fmt.Sprintf("\n%s\n", command))
- return err
-}
-
-// ReadSerialData fills the given buffer with bytes from the serial line.
-func (f *fuchsiaDevice) ReadSerialData(ctx context.Context, buf []byte) error {
- _, err := io.ReadAtLeast(f.serial, buf, len(buf))
- return err
-}
-
-// SoftReboot is a convenience function that reboots into the specified partition
-// using the specified method.
-func (f *fuchsiaDevice) SoftReboot(ctx context.Context, partition string, method string) error {
- command := ""
- if partition == "A" {
- command = "dm reboot"
- } else if partition == "R" {
- command = "dm reboot-recovery"
- } else {
- return fmt.Errorf("rebooting into partition %s is not supported", partition)
- }
-
- if method == "ssh" {
- return f.SendSSHCommand(ctx, command)
- } else if method == "serial" {
- return f.SendSerialCommand(ctx, command)
- }
- return fmt.Errorf("reboot method %s is not supported", method)
-}
-
-// State returns the current state of the fuchsia device.
-func (f *fuchsiaDevice) State() DeviceState {
- return f.state
-}
-
-// Nodename returns the nodename of the fuchsia device.
-func (f *fuchsiaDevice) Nodename() string {
- return f.nodename
-}
-
-// Transition tries to move the device into the next state. It does so by:
-// 1) Getting the transition associated with the current state.
-// 2) Performing the action specified by the transition.
-// 3) Validating if the action worked, and moving the device into the corresponding
-// failure or success state.
-func (f *fuchsiaDevice) Transition(ctx context.Context) error {
- transition := f.transitionMap[f.state]
- if err := transition.PerformAction(ctx); err != nil {
- f.state = transition.FailureState
- return err
- }
- if err := transition.Validate(ctx); err != nil {
- f.state = transition.FailureState
- return err
- }
- f.state = transition.SuccessState
- return nil
-}
-
-func (f *fuchsiaDevice) serialRebootRecovery(ctx context.Context) error {
- return f.SoftReboot(ctx, "R", "serial")
-}
-
-func (f *fuchsiaDevice) sshRebootRecovery(ctx context.Context) error {
- return f.SoftReboot(ctx, "R", "ssh")
-}
-
-// pingZedboot creates a netboot client and attempts to ping the zedboot ipv6
-// address of the device with the given nodename.
-func pingZedboot(n *netboot.Client, nodename string) error {
- netsvcAddr, err := n.Discover(nodename, false)
- if err != nil {
- return fmt.Errorf("Failed to discover netsvc addr: %v.", err)
- }
- netsvcIpAddr := &net.IPAddr{IP: netsvcAddr.IP, Zone: netsvcAddr.Zone}
- cmd := exec.Command("ping", "-6", netsvcIpAddr.String(), "-c", "1")
- if _, err = cmd.Output(); err != nil {
- return fmt.Errorf("Failed to ping netsvc addr %s: %v.", netsvcIpAddr, err)
+// Powercycle uses an out of band method to powercycle the target.
+func (t *DeviceTarget) Powercycle(ctx context.Context) error {
+ if t.config.Power != nil {
+ return t.config.Power.Powercycle(t.Nodename(), t.Mac(), t.serial)
}
return nil
}
-// ensureNotFuchsia creates a netboot client and ensures it cannot ping the
-// fuchsia ipv6 address of the device with the given nodename.
-func ensureNotFuchsia(n *netboot.Client, nodename string) error {
- fuchsiaAddr, err := n.Discover(nodename, true)
- if err != nil {
- return fmt.Errorf("Failed to discover fuchsia addr: %v.", err)
+func parseOutSigners(keyPaths []string) ([]ssh.Signer, error) {
+ if len(keyPaths) == 0 {
+ return nil, errors.New("must supply SSH keys in the config")
}
- fuchsiaIpAddr := &net.IPAddr{IP: fuchsiaAddr.IP, Zone: fuchsiaAddr.Zone}
- cmd := exec.Command("ping", "-6", fuchsiaIpAddr.String(), "-c", "1")
- if _, err = cmd.Output(); err == nil {
- return fmt.Errorf("Device is in Fuchsia, should be in Zedboot.")
+ var keys [][]byte
+ for _, keyPath := range keyPaths {
+ p, err := ioutil.ReadFile(keyPath)
+ if err != nil {
+ return nil, fmt.Errorf("could not read SSH key file %q: %v", keyPath, err)
+ }
+ keys = append(keys, p)
}
- return nil
-}
-// deviceInZedboot ensures that the device with the given nodename is in zedboot.
-func deviceInZedboot(n *netboot.Client, nodename string) error {
- if err := pingZedboot(n, nodename); err != nil {
- return err
+ var signers []ssh.Signer
+ for _, p := range keys {
+ signer, err := ssh.ParsePrivateKey(p)
+ if err != nil {
+ return nil, err
+ }
+ signers = append(signers, signer)
}
- if err := ensureNotFuchsia(n, nodename); err != nil {
- return err
- }
- return nil
+ return signers, nil
}
diff --git a/devices/devices_test.go b/devices/devices_test.go
deleted file mode 100644
index 1cfd2ef..0000000
--- a/devices/devices_test.go
+++ /dev/null
@@ -1,119 +0,0 @@
-package devices
-
-import (
- "context"
- "testing"
-
- "github.com/google/go-cmp/cmp"
-)
-
-var (
- testDeviceConfigs = []DeviceConfig{
- DeviceConfig{
- Type: "Astro",
- Network: &NetworkProperties{
- Nodename: "astro-test-device",
- IPv4Addr: "255.255.255.255",
- Mac: "00:00:00:00:00:00",
- },
- Power: &PowerClient{
- Type: "PDU",
- Host: "255.255.255.255",
- Username: "username",
- Password: "password",
- PDUIp: "255.255.255.255",
- PDUPort: "1",
- },
- },
- DeviceConfig{
- Type: "Intel NUC Kit NUC7i5DNHE",
- Network: &NetworkProperties{
- Nodename: "nuc-test-device",
- IPv4Addr: "255.255.255.255",
- Mac: "00:00:00:00:00:00",
- },
- Power: &PowerClient{
- Type: "AMT",
- Host: "255.255.255.255",
- Username: "username",
- Password: "password",
- },
- },
- DeviceConfig{
- Type: "Khadas Vim2 Max",
- Network: &NetworkProperties{
- Nodename: "vim-test-device",
- IPv4Addr: "255.255.255.255",
- Mac: "00:00:00:00:00:00",
- },
- Power: &PowerClient{
- Type: "WOL",
- Host: "255.255.255.255",
- },
- },
- }
-)
-
-// deviceSanityCheck identifies basic differences between a Device and its source DeviceConfig.
-// This function does not check every property; it is meant to be a sanity check.
-func deviceSanityCheck(t *testing.T, actual Device, expected DeviceConfig) {
- if actual.Nodename() != expected.Network.Nodename {
- t.Errorf("nodenames not equal; expected: %s, actual: %s", expected.Network.Nodename, actual.Nodename())
- }
- if actual.Mac() != expected.Network.Mac {
- t.Errorf("macs not equal; expected: %s, actual: %s", expected.Network.Mac, actual.Mac())
- }
- if actual.HasSerial() {
- t.Errorf("test device should not have serial")
- }
-}
-
-func TestLoadDeviceConfigs(t *testing.T) {
- // Test that loading a nonexistent config fails.
- if _, err := LoadDeviceConfigs("./test_data/nonexistent.json"); err == nil {
- t.Error("loading nonexistent config did not fail")
- }
-
- // Test that loading misformatted json files fails.
- if _, err := LoadDeviceConfigs("./test_data/invalid.json"); err == nil {
- t.Error("loading invalid json did not produce error")
- }
-
- // Test that loading correctly formatted config files succeeds
- configs, err := LoadDeviceConfigs("./test_data/config.json")
- if err != nil {
- t.Fatalf("loading device configs failed: %v", err)
- }
- if diff := cmp.Diff(configs, testDeviceConfigs); diff != "" {
- t.Errorf("parsed configs do not match expected: (-want +got):\n%s", diff)
- }
-
-}
-
-func TestCreateDevices(t *testing.T) {
- ctx := context.Background()
- devices, err := CreateDevices(ctx, testDeviceConfigs, []string{"test"})
- if err != nil {
- t.Fatalf("creating devices from configs failed: %v", err)
- }
-
- if len(devices) != 3 {
- t.Fatalf("expected three devices to be created, got %d", len(devices))
- }
-
- // Check the types of each device.
- if _, ok := devices[0].(*ArmCDCether); !ok {
- t.Errorf("device one is not an arm cdcether device: %v", devices[0])
- }
- if _, ok := devices[1].(*Nuc); !ok {
- t.Errorf("device one is not a nuc: %v", devices[1])
- }
- if _, ok := devices[2].(*Vim); !ok {
- t.Errorf("device one is not a vim: %v", devices[2])
- }
-
- // Perform a quick sanity check on the devices.
- for i, device := range devices {
- deviceSanityCheck(t, device, testDeviceConfigs[i])
- }
-}
diff --git a/devices/nuc.go b/devices/nuc.go
deleted file mode 100644
index ad62411..0000000
--- a/devices/nuc.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package devices
-
-import (
- "context"
- "fmt"
- "io"
- "time"
-
- "go.fuchsia.dev/infra/devices/power"
- "go.fuchsia.dev/tools/netboot"
-)
-
-// Nuc represents an Intel NUC
-type Nuc struct {
- fuchsiaDevice
-}
-
-// NewNuc initializes a NUC for use by other infra utilites.
-func NewNuc(config DeviceConfig, bootserverCmdStub []string) (*Nuc, error) {
- n := &Nuc{
- fuchsiaDevice{
- power: power.NewAMTPowerManager(
- config.Power.Host,
- config.Power.Username,
- config.Power.Password,
- ),
- },
- }
- if err := initFuchsiaDevice(&n.fuchsiaDevice, config, bootserverCmdStub); err != nil {
- return nil, err
- }
-
- // All transitions in a NUC attempt to get it into Zedboot.
- transitionMap := map[DeviceState]*Transition{
- Initial: &Transition{
- PerformAction: n.sshRebootRecovery,
- Validate: n.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: "ssh_failed",
- },
- "ssh_failed": &Transition{
- PerformAction: n.serialRebootRecovery,
- Validate: n.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: "serial_failed",
- },
- "serial_failed": &Transition{
- PerformAction: n.Powercycle,
- Validate: n.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: Unrecoverable,
- },
- }
- n.transitionMap = transitionMap
- return n, nil
-}
-
-// checkSerial performs an echo test over serial.
-func (n *Nuc) checkSerial(ctx context.Context) error {
- if n.serial == nil {
- return nil
- }
- cmdString := "echo hello"
- resultString := "\r\n$ echo hello\r\nhello"
-
- n.SendSerialCommand(ctx, cmdString)
- buffer := make([]byte, len(resultString))
- if _, err := io.ReadAtLeast(n.serial, buffer, len(resultString)); err != nil {
- return err
- }
- if string(buffer) != resultString {
- return fmt.Errorf("serial test got unexpected output: %s", string(buffer))
- }
- return nil
-}
-
-// checkTransitionSuccess is the validation function used by all transitions
-// on a NUC. It checks that serial works and the NUC is in Zedboot.
-func (n *Nuc) checkTransitionSuccess(ctx context.Context) error {
- // Give the device time to transition.
- time.Sleep(1 * time.Minute)
- client := netboot.NewClient(netbootTimeout)
- if err := n.checkSerial(ctx); err != nil {
- return err
- }
- return deviceInZedboot(client, n.nodename)
-}
-
-// ToTaskState moves a NUC into a state in which it can run a test task.
-func (n *Nuc) ToTaskState(ctx context.Context) error {
- // This assumes that there is a NUC reboot server running to
- // deliver the build's version of zedboot.
- return n.Powercycle(ctx)
-}
diff --git a/devices/pdu.go b/devices/pdu.go
new file mode 100644
index 0000000..e7e4096
--- /dev/null
+++ b/devices/pdu.go
@@ -0,0 +1,206 @@
+// 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 devices
+
+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
+}
diff --git a/devices/power.go b/devices/power.go
new file mode 100644
index 0000000..72bffd3
--- /dev/null
+++ b/devices/power.go
@@ -0,0 +1,176 @@
+// 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 devices
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "log"
+ "strconv"
+ "time"
+
+ "go.fuchsia.dev/fuchsia/tools/net/sshutil"
+ "golang.org/x/crypto/ssh"
+)
+
+// TODO(IN-977) Clean this up per suggestions in go/fxr/251550
+
+const (
+ // 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"
+
+ // Duration to wait before sending dm reboot-recovery after powercycle
+ powercycleWait = 1 * time.Minute
+)
+
+// PowerClient represents a power management configuration for a particular device.
+type PowerClient struct {
+ // Type is the type of manager to use.
+ Type string `json:"type"`
+
+ // Host is the network hostname of the manager
+ Host string `json:"host"`
+
+ // Username is the username used to log in to the manager.
+ Username string `json:"username"`
+
+ // Password is the password used to log in to the manager..
+ Password string `json:"password"`
+
+ // PDUIp is the IP address of the pdu
+ PDUIp string `json:"pduIp"`
+
+ // PDUPort is the port the PDU uses to connect to this device.
+ PDUPort string `json:"pduPort"`
+}
+
+type Rebooter interface {
+ reboot() error
+}
+
+type SshRebooter struct {
+ nodename string
+ signers []ssh.Signer
+}
+
+type SerialRebooter struct {
+ serial io.ReadWriter
+}
+
+// RebootDevice attempts to reboot the specified device into recovery, and
+// additionally uses the given configuration to reboot the device if specified.
+func (c PowerClient) RebootDevice(signers []ssh.Signer, nodename string, serial io.ReadWriter) error {
+ var rebooter Rebooter
+ log.Printf("Attempting to soft reboot device %s", nodename)
+ if serial == nil {
+ log.Printf("Using SSH to reboot.")
+ rebooter = NewSSHRebooter(nodename, signers)
+ } else {
+ log.Printf("Using serial to reboot.")
+ rebooter = NewSerialRebooter(serial)
+ }
+ return rebooter.reboot()
+}
+
+// Powercycle the device using an out of band method
+func (c PowerClient) Powercycle(nodename string, mac string, serial io.ReadWriter) error {
+ log.Printf("Attempting to powercycle device %s", nodename)
+ switch c.Type {
+ case "AMT":
+ log.Printf("Using AMT to powercycle.")
+ return AMTReboot(c.Host, c.Username, c.Password)
+ case "WOL":
+ if serial == nil {
+ return errors.New(fmt.Sprintf("WOL Reboot requires serial connection."))
+ }
+ log.Printf("Using WOL to powercycle")
+ if err := WOLReboot(botBroadcastAddr, botInterface, mac); err != nil {
+ return err
+ }
+ case "PDU":
+ if serial == nil {
+ return errors.New(fmt.Sprintf("PDU Reboot requires serial connection."))
+ }
+ log.Printf("Using PDU to powercycle")
+ port, err := strconv.Atoi(c.PDUPort)
+ if err != nil {
+ return err
+ }
+ if err := PDUReboot(port, c.PDUIp, c.Username, c.Password); err != nil {
+ return err
+ }
+ default:
+ return errors.New(fmt.Sprintf("%v does not have AMT, WOL, or PDU support. Cannot powercycle.", nodename))
+ }
+ // Send dm reboot-recovery to the device so that it always boots into zedboot.
+ log.Printf("Powercycle complete; using serial to send dm reboot recovery.")
+ time.Sleep(powercycleWait)
+ rebooter := NewSerialRebooter(serial)
+ return rebooter.reboot()
+}
+
+func NewSerialRebooter(serial io.ReadWriter) *SerialRebooter {
+ return &SerialRebooter{
+ serial: serial,
+ }
+}
+
+func (s *SerialRebooter) reboot() error {
+ _, err := io.WriteString(s.serial, "\ndm reboot-recovery\n")
+ return err
+}
+
+func NewSSHRebooter(nodename string, signers []ssh.Signer) *SshRebooter {
+ return &SshRebooter{
+ nodename: nodename,
+ signers: signers,
+ }
+}
+
+func (s *SshRebooter) reboot() error {
+ config, err := sshutil.DefaultSSHConfigFromSigners(s.signers...)
+ if err != nil {
+ return err
+ }
+
+ client, err := sshutil.ConnectToNode(context.Background(), s.nodename, config)
+ if err != nil {
+ return err
+ }
+
+ defer client.Close()
+
+ session, err := client.NewSession()
+ if err != nil {
+ return err
+ }
+
+ defer session.Close()
+
+ // Invoke `dm reboot-recovery`
+ err = session.Start("dm reboot-recovery")
+ if err != nil {
+ return err
+ }
+
+ done := make(chan error)
+ go func() {
+ done <- session.Wait()
+ }()
+
+ select {
+ case err := <-done:
+ return err
+ case <-time.After(10*time.Second):
+ return nil
+ }
+}
diff --git a/devices/power/amt.go b/devices/power/amt.go
index d6268c7..7f85b6f 100644
--- a/devices/power/amt.go
+++ b/devices/power/amt.go
@@ -5,7 +5,6 @@
package power
import (
- "context"
"fmt"
"io/ioutil"
"net/http"
@@ -112,6 +111,6 @@
}
}
-func (a *AMTPowerManager) Powercycle(ctx context.Context) error {
+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
index 35f32ed..c03d5fd 100644
--- a/devices/power/pdu.go
+++ b/devices/power/pdu.go
@@ -6,7 +6,6 @@
import (
"bytes"
- "context"
"crypto/tls"
"encoding/json"
"fmt"
@@ -222,6 +221,6 @@
}
}
-func (p *PDUPowerManager) Powercycle(ctx context.Context) error {
+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
index fe0320c..f6333fd 100644
--- a/devices/power/wol.go
+++ b/devices/power/wol.go
@@ -5,7 +5,6 @@
package power
import (
- "context"
"errors"
"fmt"
"net"
@@ -129,6 +128,6 @@
}
}
-func (w *WOLPowerManager) Powercycle(ctx context.Context) error {
+func (w *WOLPowerManager) Powercycle() error {
return WOLReboot(w.botBroadcastAddr, w.botInterface, w.mac)
}
diff --git a/devices/test_data/config.json b/devices/test_data/config.json
deleted file mode 100644
index 94793c9..0000000
--- a/devices/test_data/config.json
+++ /dev/null
@@ -1,44 +0,0 @@
-[
- {
- "type": "Astro",
- "network": {
- "nodename": "astro-test-device",
- "ipv4": "255.255.255.255",
- "mac": "00:00:00:00:00:00"
- },
- "power": {
- "host": "255.255.255.255",
- "password": "password",
- "pduIp": "255.255.255.255",
- "pduPort": "1",
- "type": "PDU",
- "username": "username"
- }
- },
- {
- "type": "Intel NUC Kit NUC7i5DNHE",
- "network": {
- "nodename": "nuc-test-device",
- "ipv4": "255.255.255.255",
- "mac": "00:00:00:00:00:00"
- },
- "power": {
- "username": "username",
- "host": "255.255.255.255",
- "password": "password",
- "type": "AMT"
- }
- },
- {
- "type": "Khadas Vim2 Max",
- "network": {
- "nodename": "vim-test-device",
- "ipv4": "255.255.255.255",
- "mac": "00:00:00:00:00:00"
- },
- "power": {
- "host": "255.255.255.255",
- "type": "WOL"
- }
- }
-]
diff --git a/devices/test_data/invalid.json b/devices/test_data/invalid.json
deleted file mode 100644
index d232445..0000000
--- a/devices/test_data/invalid.json
+++ /dev/null
@@ -1,25 +0,0 @@
-[
- {
- "type": "Astro",
- "network": {
- "nodename": "astro-test-device",
- "ipv4": "255.255.255.255",
- "mac": "00:00:00:00:00:00"
- },
- "power":
- "host": "255.255.255.255",
- "password": "password",
- "pduPort": "1",
- "type": "PDU",
- "username": "username"
- },
- ]
- },
- {
- "type": "Intel NUC Kit NUC7i5DNHE",
- "network": {
- "nodename": "nuc-test-device",
- "ipv4": "255.255.255.255",
- ]
- }
-]
diff --git a/devices/vim.go b/devices/vim.go
deleted file mode 100644
index 5076f94..0000000
--- a/devices/vim.go
+++ /dev/null
@@ -1,96 +0,0 @@
-package devices
-
-import (
- "context"
- "fmt"
- "log"
- "os"
- "os/exec"
- "syscall"
- "time"
-
- "go.fuchsia.dev/infra/devices/power"
- "go.fuchsia.dev/tools/netboot"
-)
-
-// Vim represents a VIM2 (and eventually a VIM3)
-type Vim struct {
- fuchsiaDevice
-}
-
-// NewVim initializes a Vim for use by other infra utilities.
-func NewVim(config DeviceConfig, bootserverCmdStub []string) (*Vim, error) {
- v := &Vim{
- fuchsiaDevice{
- power: power.NewWOLPowerManager(config.Network.Mac),
- },
- }
- if err := initFuchsiaDevice(&v.fuchsiaDevice, config, bootserverCmdStub); err != nil {
- return nil, err
- }
-
- // All transitions in Vim attempt to get the device into zedboot.
- transitionMap := map[DeviceState]*Transition{
- Initial: &Transition{
- PerformAction: v.sshRebootRecovery,
- Validate: v.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: "ssh_failed",
- },
- "ssh_failed": &Transition{
- PerformAction: v.serialRebootRecovery,
- Validate: v.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: "serial_failed",
- },
- "serial_failed": &Transition{
- PerformAction: v.Powercycle,
- Validate: v.checkTransitionSuccess,
- SuccessState: Healthy,
- FailureState: Unrecoverable,
- },
- }
- v.transitionMap = transitionMap
- return v, nil
-}
-
-// checkTransitionSuccess is the validation function used by all transitions
-// performed by Vim. It checks that the device is in zedboot.
-func (v *Vim) checkTransitionSuccess(ctx context.Context) error {
- // Give the device time to transition.
- time.Sleep(1 * time.Minute)
- client := netboot.NewClient(netbootTimeout)
- return deviceInZedboot(client, v.nodename)
-}
-
-// ToTaskState puts the Vim into a state in which it can run a test task.
-func (v *Vim) ToTaskState(ctx context.Context) error {
- cmd := exec.Command(v.BootserverCmd[0], v.BootserverCmd[1:]...)
-
- // Spin up handler to exit subprocess cleanly on sigterm.
- processDone := make(chan bool, 1)
- go func() {
- select {
- case <-processDone:
- case <-ctx.Done():
- if err := cmd.Process.Signal(syscall.SIGTERM); err != nil {
- log.Printf("exited cmd with error %v", err)
- }
- }
- }()
-
- // Ensure the context still exists before running the subprocess.
- if ctx.Err() != nil {
- return nil
- }
-
- // Run the bootserver.
- cmd.Stdout = os.Stdout
- cmd.Stderr = os.Stderr
- cmd.Run()
- processDone <- true
- if !cmd.ProcessState.Success() {
- return fmt.Errorf("bootserver for %s exited with exit code %d", v.nodename, cmd.ProcessState.ExitCode())
- }
- return nil
-}
diff --git a/devices/wol.go b/devices/wol.go
new file mode 100644
index 0000000..cb3a861
--- /dev/null
+++ b/devices/wol.go
@@ -0,0 +1,107 @@
+// 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 devices
+
+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
+)
+
+// 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
+}
diff --git a/go.mod b/go.mod
index b68159f..817aa63 100644
--- a/go.mod
+++ b/go.mod
@@ -10,7 +10,6 @@
github.com/docker/go-units v0.3.3 // indirect
github.com/gogo/protobuf v1.1.1 // indirect
github.com/golang/protobuf v1.3.2
- github.com/google/go-cmp v0.4.0
github.com/google/subcommands v1.0.1
github.com/google/uuid v1.1.1
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
diff --git a/go.sum b/go.sum
index ce78550..f92df4b 100644
--- a/go.sum
+++ b/go.sum
@@ -40,8 +40,6 @@
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
-github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
-github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -175,7 +173,6 @@
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0 h1:VGGbLNyPF7dvYHhcUGYBBGCRDDK0RRJAI6KCvo0CL+E=