[acts] Added multidevice support to Botanist.
Added multi-device support in both run.go and zedboot.go.
This enables multi-fuchsia device tests (such as a subset
of BT ACTS tests) to be run in our infra.
Bug: IN-890 #comment
Change-Id: I7d3496695d2155899752fc079248a6e311303ee5
diff --git a/botanist/common.go b/botanist/common.go
index 43b017a..db7fd3d 100644
--- a/botanist/common.go
+++ b/botanist/common.go
@@ -8,8 +8,8 @@
"context"
"encoding/json"
"fmt"
+ "io/ioutil"
"net"
- "os"
"strings"
"time"
@@ -63,12 +63,23 @@
SSHKeys []string `json:"keys,omitempty"`
}
-// LoadDeviceProperties unmarshalls the DeviceProperties found in a given file.
-func LoadDeviceProperties(propertiesFile string, properties *DeviceProperties) error {
- file, err := os.Open(propertiesFile)
+// LoadDeviceProperties unmarshalls a slice of DeviceProperties from a given file.
+// For backwards compatibility, it supports unmarshalling a single DeviceProperties object also
+// TODO(IN-1028): Update all botanist configs to use JSON list format
+func LoadDeviceProperties(path string) ([]DeviceProperties, error) {
+ data, err := ioutil.ReadFile(path)
if err != nil {
- return err
+ return nil, fmt.Errorf("failed to read device properties file %q", path)
}
- return json.NewDecoder(file).Decode(properties)
+ var propertiesSlice []DeviceProperties
+
+ if err := json.Unmarshal(data, &propertiesSlice); err != nil {
+ var properties DeviceProperties
+ if err := json.Unmarshal(data, &properties); err != nil {
+ return nil, err
+ }
+ propertiesSlice = append(propertiesSlice, properties)
+ }
+ return propertiesSlice, nil
}
diff --git a/botanist/common_test.go b/botanist/common_test.go
new file mode 100644
index 0000000..94f5785
--- /dev/null
+++ b/botanist/common_test.go
@@ -0,0 +1,56 @@
+// 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 botanist
+
+import (
+ "io/ioutil"
+ "os"
+ "testing"
+)
+
+func TestLoadDevicePropertiesSlice(t *testing.T) {
+ tests := []struct {
+ name string
+ jsonStr string
+ expectedLen int
+ expectErr bool
+ }{
+ // Valid configs.
+ {"ValidListConfig", `[{"nodename":"upper-drank-wick-creek"},{"nodename":"siren-swoop-wick-hasty"}]`, 2, false},
+ {"ValidSingleConfig", `{"nodename":"upper-drank-wick-creek"}`, 1, false},
+ // Invalid configs.
+ {"InvalidListConfig", `{{"nodename":"upper-drank-wick-creek"},{"nodename":"siren-swoop-wick-hasty"}}`, 0, true},
+ {"InvalidSingleConfig", `{"upper-drank-wick-creek"}`, 0, true},
+ }
+ for _, test := range tests {
+ tmpfile, err := ioutil.TempFile(os.TempDir(), "common_test")
+ if err != nil {
+ t.Fatalf("Failed to create test device properties file: %s", err)
+ }
+ defer os.Remove(tmpfile.Name())
+
+ content := []byte(test.jsonStr)
+ if _, err := tmpfile.Write(content); err != nil {
+ t.Fatalf("Failed to write to test device properties file: %s", err)
+ }
+
+ propertiesSlice, err := LoadDeviceProperties(tmpfile.Name())
+
+ if test.expectErr && err == nil {
+ t.Errorf("Test%v: Exepected errors; no errors found", test.name)
+ }
+
+ if !test.expectErr && err != nil {
+ t.Errorf("Test%v: Exepected no errors; found error - %v", test.name, err)
+ }
+
+ if len(propertiesSlice) != test.expectedLen {
+ t.Errorf("Test%v: Expected %d nodes; found %d", test.name, test.expectedLen, len(propertiesSlice))
+ }
+
+ if err := tmpfile.Close(); err != nil {
+ t.Fatal(err)
+ }
+ }
+}
diff --git a/cmd/botanist/run.go b/cmd/botanist/run.go
index 09ed928..e8141e7 100644
--- a/cmd/botanist/run.go
+++ b/cmd/botanist/run.go
@@ -191,10 +191,13 @@
return fmt.Errorf("failed to load images: %v", err)
}
- var properties botanist.DeviceProperties
- if err := botanist.LoadDeviceProperties(r.deviceFile, &properties); err != nil {
- return fmt.Errorf("failed to open device properties file \"%v\": %v", r.deviceFile, err)
+ propertiesSlice, err := botanist.LoadDeviceProperties(r.deviceFile)
+ if err != nil {
+ return fmt.Errorf("failed to load device properties file %q", r.deviceFile)
+ } else if len(propertiesSlice) != 1 {
+ return fmt.Errorf("expected 1 entry in the device properties file; found %d", len(propertiesSlice))
}
+ properties := propertiesSlice[0]
// Merge config file and command-line keys.
privKeyPaths := properties.SSHKeys
diff --git a/cmd/botanist/zedboot.go b/cmd/botanist/zedboot.go
index 6198e20..77ecfa2 100644
--- a/cmd/botanist/zedboot.go
+++ b/cmd/botanist/zedboot.go
@@ -208,32 +208,51 @@
return cmd.tarHostCmdArtifacts(summaryBuffer.Bytes(), stdoutBuf.Bytes(), tmpDir)
}
-func (cmd *ZedbootCommand) runTests(ctx context.Context, imgs build.Images, nodename string, cmdlineArgs []string, signers []ssh.Signer) error {
- // Set up log listener and dump kernel output to stdout.
- l, err := netboot.NewLogListener(nodename)
- if err != nil {
- return fmt.Errorf("cannot listen: %v", err)
- }
- go func() {
- defer l.Close()
- logger.Debugf(ctx, "starting log listener\n")
- for {
- data, err := l.Listen()
- if err != nil {
- continue
- }
- fmt.Print(data)
- select {
- case <-ctx.Done():
- return
- default:
- }
- }
- }()
+func (cmd *ZedbootCommand) runTests(ctx context.Context, imgs build.Images, nodes []botanist.DeviceProperties, cmdlineArgs []string, signers []ssh.Signer) error {
+ var err error
- addr, err := botanist.GetNodeAddress(ctx, nodename, false)
- if err != nil {
- return err
+ // Set up log listener and dump kernel output to stdout.
+ for _, node := range nodes {
+ l, err := netboot.NewLogListener(node.Nodename)
+ if err != nil {
+ return fmt.Errorf("cannot listen: %v\n", err)
+ }
+ go func(nodename string) {
+ defer l.Close()
+ logger.Debugf(ctx, "starting log listener for <<%s>>\n", nodename)
+ for {
+ data, err := l.Listen()
+ if err != nil {
+ continue
+ }
+ if len(nodes) == 1 {
+ fmt.Print(data)
+ } else {
+ // Print each line with nodename prepended when there are multiple nodes
+ lines := strings.Split(data, "\n")
+ for _, line := range lines {
+ if len(line) > 0 {
+ fmt.Printf("<<%s>> %s\n", nodename, line)
+ }
+ }
+ }
+
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ }
+ }
+ }(node.Nodename)
+ }
+
+ var addrs []*net.UDPAddr
+ for _, node := range nodes {
+ addr, err := botanist.GetNodeAddress(ctx, node.Nodename, false)
+ if err != nil {
+ return err
+ }
+ addrs = append(addrs, addr)
}
// Boot fuchsia.
@@ -243,8 +262,10 @@
} else {
bootMode = botanist.ModePave
}
- if err = botanist.Boot(ctx, addr, bootMode, imgs, cmdlineArgs, signers); err != nil {
- return err
+ for _, addr := range addrs {
+ if err = botanist.Boot(ctx, addr, bootMode, imgs, cmdlineArgs, signers); err != nil {
+ return err
+ }
}
// Handle host commands
@@ -254,7 +275,11 @@
}
logger.Debugf(ctx, "waiting for %q\n", cmd.summaryFilename)
+ if len(addrs) != 1 {
+ return fmt.Errorf("Non-host tests should have exactly 1 node defined in config, found %v", len(addrs))
+ }
+ addr := addrs[0]
// Poll for summary.json; this relies on runtest being executed using
// autorun and it eventually producing the summary.json file.
t := tftp.NewClient()
@@ -345,32 +370,41 @@
}
func (cmd *ZedbootCommand) execute(ctx context.Context, cmdlineArgs []string) error {
- var properties botanist.DeviceProperties
- if err := botanist.LoadDeviceProperties(cmd.propertiesFile, &properties); err != nil {
- return fmt.Errorf("failed to open device properties file \"%v\"", cmd.propertiesFile)
+ propertiesSlice, err := botanist.LoadDeviceProperties(cmd.propertiesFile)
+
+ if err != nil {
+ return fmt.Errorf("failed to load device properties file %q", cmd.propertiesFile)
}
+ processedKeys := make(map[string]bool)
var signers []ssh.Signer
- for _, keyPath := range properties.SSHKeys {
- p, err := ioutil.ReadFile(keyPath)
- if err != nil {
- return err
+ for _, properties := range propertiesSlice {
+ for _, keyPath := range properties.SSHKeys {
+ if !processedKeys[keyPath] {
+ processedKeys[keyPath] = true
+ p, err := ioutil.ReadFile(keyPath)
+ if err != nil {
+ return err
+ }
+ s, err := ssh.ParsePrivateKey(p)
+ if err != nil {
+ return err
+ }
+ signers = append(signers, s)
+ }
}
- s, err := ssh.ParsePrivateKey(p)
- if err != nil {
- return err
- }
- signers = append(signers, s)
}
- if properties.PDU != nil {
- defer func() {
- logger.Debugf(ctx, "rebooting the node %q\n", properties.Nodename)
+ for _, properties := range propertiesSlice {
+ if properties.PDU != nil {
+ defer func() {
+ logger.Debugf(ctx, "rebooting the node %q\n", properties.Nodename)
- if err := botanist.RebootDevice(properties.PDU, signers, properties.Nodename); err != nil {
- logger.Errorf(ctx, "failed to reboot the device: %v\n", err)
- }
- }()
+ if err := botanist.RebootDevice(properties.PDU, signers, properties.Nodename); err != nil {
+ logger.Errorf(ctx, "failed to reboot the device: %v", err)
+ }
+ }()
+ }
}
ctx, cancel := context.WithCancel(ctx)
@@ -407,7 +441,7 @@
return
}
}
- errs <- cmd.runTests(ctx, imgs, properties.Nodename, cmdlineArgs, signers)
+ errs <- cmd.runTests(ctx, imgs, propertiesSlice, cmdlineArgs, signers)
}()
select {