[botanist] Introduce Target interface
This change introduces the Target interface, for which both QEMUTarget
and DeviceTarget are implementations. `botanist run` is modified to deal
abstractly with a Target (possibly a QEMU instance).
Test: ran locally with a QEMU config, as well as against my NUC.
Change-Id: Iba0e6b1c23925c7560fa02445ea76985a4989791
diff --git a/botanist/target/device.go b/botanist/target/device.go
index f496023..a299a21 100644
--- a/botanist/target/device.go
+++ b/botanist/target/device.go
@@ -49,23 +49,6 @@
IPv4Addr string `json:"ipv4"`
}
-// DeviceOptions represents lifecycle options for a target.
-type DeviceOptions struct {
- // Netboot gives whether to netboot or pave. Netboot here is being used in the
- // colloquial sense of only sending netsvc a kernel to mexec. If false, the target
- // will be paved.
- Netboot bool
-
- // SSHKey is a private SSH key file. If provided, the corresponding authorized key
- // will be paved.
- SSHKey string
-
- // Fastboot is a path to the fastboot binary. If provided, it will be assumed that
- // the device is waiting in fastboot mode, and it will be attempted to 'continue'
- // it into zedboot.
- Fastboot string
-}
-
// LoadDeviceConfigs unmarshalls a slice of device configs from a given file.
func LoadDeviceConfigs(path string) ([]DeviceConfig, error) {
data, err := ioutil.ReadFile(path)
@@ -82,13 +65,13 @@
// DeviceTarget represents a target device.
type DeviceTarget struct {
- config *DeviceConfig
- opts *DeviceOptions
+ config DeviceConfig
+ opts Options
signers []ssh.Signer
}
// NewDeviceTarget returns a new device target with a given configuration.
-func NewDeviceTarget(config DeviceConfig, opts DeviceOptions) (*DeviceTarget, error) {
+func NewDeviceTarget(config DeviceConfig, opts Options) (*DeviceTarget, error) {
// If an SSH key is specified in the options, prepend it the configs list so that it
// corresponds to the authorized key that would be paved.
if opts.SSHKey != "" {
@@ -99,8 +82,8 @@
return nil, fmt.Errorf("could not parse out signers from private keys: %v", err)
}
return &DeviceTarget{
- config: &config,
- opts: &opts,
+ config: config,
+ opts: opts,
signers: signers,
}, nil
}
@@ -112,8 +95,7 @@
// IPv6 returns the link-local IPv6 address of the node.
func (t *DeviceTarget) IPv6Addr() (*net.UDPAddr, error) {
- addr, err := netutil.GetNodeAddress(context.Background(), t.Nodename(), false)
- return addr, err
+ return netutil.GetNodeAddress(context.Background(), t.Nodename(), false)
}
// IPv4Addr returns the IPv4 address of the node. If not provided in the config, then it
@@ -122,8 +104,7 @@
if t.config.Network.IPv4Addr != "" {
return net.ParseIP(t.config.Network.IPv4Addr), nil
}
- addr, err := botanist.ResolveIPv4(context.Background(), t.Nodename(), netstackTimeout)
- return addr, err
+ return botanist.ResolveIPv4(context.Background(), t.Nodename(), netstackTimeout)
}
// SSHKey returns the private SSH key path associated with the authorized key to be paved.
@@ -195,6 +176,16 @@
return nil
}
+// Stop stops the device.
+func (t *DeviceTarget) Stop(ctx context.Context) error {
+ return ErrUnimplemented
+}
+
+// Wait waits for the device target to stop.
+func (t *DeviceTarget) Wait(ctx context.Context) error {
+ return ErrUnimplemented
+}
+
func parseOutSigners(keyPaths []string) ([]ssh.Signer, error) {
if len(keyPaths) == 0 {
return nil, errors.New("must supply SSH keys in the config")
diff --git a/botanist/target/errors.go b/botanist/target/errors.go
new file mode 100644
index 0000000..bdb49f7
--- /dev/null
+++ b/botanist/target/errors.go
@@ -0,0 +1,14 @@
+// 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 target
+
+import (
+ "errors"
+)
+
+var (
+ // ErrUnimplemented is an error for unimplemented methods.
+ ErrUnimplemented error = errors.New("method unimplemented")
+)
diff --git a/botanist/target/options.go b/botanist/target/options.go
new file mode 100644
index 0000000..9546bb7
--- /dev/null
+++ b/botanist/target/options.go
@@ -0,0 +1,23 @@
+// 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 target
+
+// Options represents lifecycle options for a target. The options will not necessarily make
+// sense for all target types.
+type Options struct {
+ // Netboot gives whether to netboot or pave. Netboot here is being used in the
+ // colloquial sense of only sending netsvc a kernel to mexec. If false, the target
+ // will be paved. Ignored for QEMUTarget.
+ Netboot bool
+
+ // SSHKey is a private SSH key file, corresponding to an authorized key to be paved or
+ // to one baked into a boot image.
+ SSHKey string
+
+ // Fastboot is a path to the fastboot binary. If provided, it will be assumed that
+ // the device is waiting in fastboot mode, and it will be attempted to 'continue'
+ // it into zedboot. Ignored for QEMUTarget.
+ Fastboot string
+}
diff --git a/botanist/target/qemu.go b/botanist/target/qemu.go
index 1750505..050c10f 100644
--- a/botanist/target/qemu.go
+++ b/botanist/target/qemu.go
@@ -10,6 +10,7 @@
"io"
"io/ioutil"
"log"
+ "net"
"os"
"os/exec"
"path/filepath"
@@ -22,6 +23,16 @@
// qemuSystemPrefix is the prefix of the QEMU binary name, which is of the
// form qemu-system-<QEMU arch suffix>.
qemuSystemPrefix = "qemu-system"
+
+ // DefaultInterfaceName is the name given to the emulated tap interface.
+ defaultInterfaceName = "qemu"
+
+ // DefaultMACAddr is the default MAC address given to a QEMU target.
+ defaultMACAddr = "52:54:00:63:5e:7a"
+
+ // DefaultNodename is the default nodename given to an target with the default QEMU MAC address.
+ defaultNodename = "step-atom-yard-juicy"
+
)
// qemuTargetMapping maps the Fuchsia target name to the name recognized by QEMU.
@@ -63,6 +74,7 @@
// QEMUTarget is a QEMU target.
type QEMUTarget struct {
config QEMUConfig
+ opts Options
c chan error
@@ -71,24 +83,40 @@
}
// NewQEMUTarget returns a new QEMU target with a given configuration.
-func NewQEMUTarget(config QEMUConfig) *QEMUTarget {
+func NewQEMUTarget(config QEMUConfig, opts Options) *QEMUTarget {
return &QEMUTarget{
config: config,
+ opts: opts,
c: make(chan error),
}
}
+// Nodename returns the name of the target node.
+func (t *QEMUTarget) Nodename() string {
+ return defaultNodename
+}
+
+// IPv4Addr returns a nil address, as DHCP is not currently configured.
+func (t *QEMUTarget) IPv4Addr() (net.IP, error) {
+ return nil, nil
+}
+
+// SSHKey returns the private SSH key path associated with the authorized key to be pavet.
+func (t *QEMUTarget) SSHKey() string {
+ return t.opts.SSHKey
+}
+
// Start starts the QEMU target.
-func (d *QEMUTarget) Start(ctx context.Context, images build.Images, args []string) error {
- qemuTarget, ok := qemuTargetMapping[d.config.Target]
+func (t *QEMUTarget) Start(ctx context.Context, images build.Images, args []string) error {
+ qemuTarget, ok := qemuTargetMapping[t.config.Target]
if !ok {
- return fmt.Errorf("invalid target %q", d.config.Target)
+ return fmt.Errorf("invalid target %q", t.config.Target)
}
- if d.config.Path == "" {
+ if t.config.Path == "" {
return fmt.Errorf("directory must be set")
}
- qemuSystem := filepath.Join(d.config.Path, fmt.Sprintf("%s-%s", qemuSystemPrefix, qemuTarget))
+ qemuSystem := filepath.Join(t.config.Path, fmt.Sprintf("%s-%s", qemuSystemPrefix, qemuTarget))
if _, err := os.Stat(qemuSystem); err != nil {
return fmt.Errorf("could not find qemu-system binary %q: %v", qemuSystem, err)
}
@@ -109,11 +137,11 @@
File: storageFull.Path,
})
}
- if d.config.MinFS != nil {
- if _, err := os.Stat(d.config.MinFS.Image); err != nil {
- return fmt.Errorf("could not find minfs image %q: %v", d.config.MinFS.Image, err)
+ if t.config.MinFS != nil {
+ if _, err := os.Stat(t.config.MinFS.Image); err != nil {
+ return fmt.Errorf("could not find minfs image %q: %v", t.config.MinFS.Image, err)
}
- file, err := filepath.Abs(d.config.MinFS.Image)
+ file, err := filepath.Abs(t.config.MinFS.Image)
if err != nil {
return err
}
@@ -126,22 +154,26 @@
drives = append(drives, qemu.Drive{
ID: "testdisk",
File: file,
- Addr: d.config.MinFS.PCIAddress,
+ Addr: t.config.MinFS.PCIAddress,
})
}
networks := []qemu.Netdev{
qemu.Netdev{
ID: "net0",
+ Tap: &qemu.NetdevTap{
+ Name: defaultInterfaceName,
+ },
+ MAC: defaultMACAddr,
},
}
config := qemu.Config{
Binary: qemuSystem,
Target: qemuTarget,
- CPU: d.config.CPU,
- Memory: d.config.Memory,
- KVM: d.config.KVM,
+ CPU: t.config.CPU,
+ Memory: t.config.Memory,
+ KVM: t.config.KVM,
Kernel: qemuKernel.Path,
Initrd: zirconA.Path,
Drives: drives,
@@ -154,7 +186,7 @@
args = append(args, "devmgr.suspend-timeout-debug=true")
// Do not print colors.
args = append(args, "TERM=dumb")
- if d.config.Target == "x64" {
+ if t.config.Target == "x64" {
// Necessary to redirect to stdout.
args = append(args, "kernel.serial=legacy")
}
@@ -172,7 +204,7 @@
return err
}
- d.cmd = &exec.Cmd{
+ t.cmd = &exec.Cmd{
Path: invocation[0],
Args: invocation,
Dir: workdir,
@@ -181,7 +213,7 @@
}
log.Printf("QEMU invocation:\n%s", invocation)
- if err := d.cmd.Start(); err != nil {
+ if err := t.cmd.Start(); err != nil {
os.RemoveAll(workdir)
return fmt.Errorf("failed to start: %v", err)
}
@@ -190,20 +222,25 @@
// method is invoked or not.
go func() {
defer os.RemoveAll(workdir)
- d.c <- qemu.CheckExitCode(d.cmd.Wait())
+ t.c <- qemu.CheckExitCode(t.cmd.Wait())
}()
return nil
}
+// Wait waits for the QEMU target to stop.
+func (t *QEMUTarget) Restart(ctx context.Context) error {
+ return ErrUnimplemented
+}
+
// Stop stops the QEMU target.
-func (d *QEMUTarget) Stop(ctx context.Context) error {
- return d.cmd.Process.Kill()
+func (t *QEMUTarget) Stop(ctx context.Context) error {
+ return t.cmd.Process.Kill()
}
// Wait waits for the QEMU target to stop.
-func (d *QEMUTarget) Wait(ctx context.Context) error {
- return <-d.c
+func (t *QEMUTarget) Wait(ctx context.Context) error {
+ return <-t.c
}
func overwriteFileWithCopy(path string) error {
diff --git a/cmd/botanist/qemu.go b/cmd/botanist/qemu.go
index c3164d0..a76a5b2 100644
--- a/cmd/botanist/qemu.go
+++ b/cmd/botanist/qemu.go
@@ -90,8 +90,7 @@
}
}
- t := target.NewQEMUTarget(config)
-
+ t := target.NewQEMUTarget(config, target.Options{})
if err := t.Start(ctx, imgs, cmdlineArgs); err != nil {
return err
}
diff --git a/cmd/botanist/run.go b/cmd/botanist/run.go
index 4315505..a3839d2 100644
--- a/cmd/botanist/run.go
+++ b/cmd/botanist/run.go
@@ -5,10 +5,12 @@
import (
"context"
+ "encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
+ "net"
"os"
"time"
@@ -23,13 +25,39 @@
"github.com/google/subcommands"
)
-const netstackTimeout time.Duration = 1 * time.Minute
+const (
+ netstackTimeout time.Duration = 1 * time.Minute
+)
+
+// Target represents a fuchsia instance.
+type Target interface {
+ // Nodename returns the name of the target node.
+ Nodename() string
+
+ // IPv4Addr returns the IPv4 address of the target.
+ IPv4Addr() (net.IP, error)
+
+ // SSHKey returns the private key corresponding an authorized SSH key of the target.
+ SSHKey() string
+
+ // Start starts the target.
+ Start(ctx context.Context, images build.Images, args []string) error
+
+ // Restart restarts the target.
+ Restart(ctx context.Context) error
+
+ // Stop stops the target.
+ Stop(ctx context.Context) error
+
+ // Wait waits for the target to finish running.
+ Wait(ctx context.Context) error
+}
// RunCommand is a Command implementation for booting a device and running a
// given command locally.
type RunCommand struct {
- // DeviceFile is the path to a file of device config.
- deviceFile string
+ // ConfigFile is the path to the target configurations.
+ configFile string
// ImageManifests is a list of paths to image manifests (e.g., images.json)
imageManifests command.StringsFlag
@@ -77,7 +105,7 @@
}
func (r *RunCommand) SetFlags(f *flag.FlagSet) {
- f.StringVar(&r.deviceFile, "device", "/etc/botanist/config.json", "path to file of device config")
+ f.StringVar(&r.configFile, "config", "/etc/botanist/device.json", "path to file of device config")
f.Var(&r.imageManifests, "images", "paths to image manifests")
f.BoolVar(&r.netboot, "netboot", false, "if set, botanist will not pave; but will netboot instead")
f.StringVar(&r.fastboot, "fastboot", "", "path to the fastboot tool; if set, the device will be flashed into Zedboot. A zircon-r must be supplied via -images")
@@ -89,11 +117,11 @@
f.StringVar(&r.sshKey, "ssh", "", "file containing a private SSH user key; if not provided, a private key will be generated.")
}
-func (r *RunCommand) runCmd(ctx context.Context, args []string, device *target.DeviceTarget, syslog io.Writer) error {
- nodename := device.Nodename()
+func (r *RunCommand) runCmd(ctx context.Context, args []string, t Target, syslog io.Writer) error {
+ nodename := t.Nodename()
// If having paved, SSH in and stream syslogs back to a file sink.
if !r.netboot && syslog != nil {
- p, err := ioutil.ReadFile(device.SSHKey())
+ p, err := ioutil.ReadFile(t.SSHKey())
if err != nil {
return err
}
@@ -115,7 +143,7 @@
}()
}
- ip, err := device.IPv4Addr()
+ ip, err := t.IPv4Addr()
if err == nil {
logger.Infof(ctx, "IPv4 address of %s found: %s", nodename, ip)
} else {
@@ -126,7 +154,7 @@
os.Environ(),
fmt.Sprintf("FUCHSIA_NODENAME=%s", nodename),
fmt.Sprintf("FUCHSIA_IPV4_ADDR=%v", ip),
- fmt.Sprintf("FUCHSIA_SSH_KEY=%s", device.SSHKey()),
+ fmt.Sprintf("FUCHSIA_SSH_KEY=%s", t.SSHKey()),
)
ctx, cancel := context.WithTimeout(ctx, r.timeout)
@@ -169,21 +197,34 @@
return fmt.Errorf("failed to load images: %v", err)
}
- configs, err := target.LoadDeviceConfigs(r.deviceFile)
- if err != nil {
- return fmt.Errorf("failed to load target config file %q", r.deviceFile)
- } else if len(configs) != 1 {
- return fmt.Errorf("`botanist run` only supports configuration for a single target")
- }
- opts := target.DeviceOptions{
+ opts := target.Options{
Netboot: r.netboot,
Fastboot: r.fastboot,
SSHKey: r.sshKey,
}
- device, err := target.NewDeviceTarget(configs[0], opts)
+
+ data, err := ioutil.ReadFile(r.configFile)
if err != nil {
- return err
+ return fmt.Errorf("could not open config file: %v", err)
}
+ var objs []json.RawMessage
+ if err := json.Unmarshal(data, &objs); err != nil {
+ return fmt.Errorf("could not unmarshal config file as a JSON list: %v", err)
+ }
+
+ var targets []Target
+ for _, obj := range objs {
+ t, err := DeriveTarget(obj, opts)
+ if err != nil {
+ return err
+ }
+ targets = append(targets, t)
+ }
+
+ if len(targets) != 1 {
+ return fmt.Errorf("`botanist run` only supports configuration for a single target")
+ }
+ t := targets[0]
var syslog io.WriteCloser
if r.syslogFile != "" {
@@ -195,20 +236,25 @@
}
defer func() {
- logger.Debugf(ctx, "rebooting the node %q\n", device.Nodename())
- device.Restart(ctx)
+ logger.Debugf(ctx, "stopping or rebooting the node %q\n", t.Nodename())
+ if err := t.Stop(ctx); err == target.ErrUnimplemented {
+ t.Restart(ctx)
+ }
}()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
errs := make(chan error)
go func() {
- if err := device.Start(ctx, imgs, r.zirconArgs); err != nil {
+ if err := t.Start(ctx, imgs, r.zirconArgs); err != nil {
+ errs <- err
+ }
+ if err := t.Wait(ctx); err != nil && err != target.ErrUnimplemented {
errs <- err
}
}()
go func() {
- errs <- r.runCmd(ctx, args, device, syslog)
+ errs <- r.runCmd(ctx, args, t, syslog)
}()
select {
@@ -230,3 +276,31 @@
}
return subcommands.ExitSuccess
}
+
+func DeriveTarget(obj []byte, opts target.Options) (Target, error) {
+ type typed struct {
+ Type string `json:"type"`
+ }
+ var x typed
+
+ if err := json.Unmarshal(obj, &x); err != nil {
+ return nil, fmt.Errorf("object in list has no \"type\" field: %v", err)
+ }
+ switch x.Type {
+ case "qemu":
+ var cfg target.QEMUConfig
+ if err := json.Unmarshal(obj, &cfg); err != nil {
+ return nil, fmt.Errorf("invalid QEMU config found: %v", err)
+ }
+ return target.NewQEMUTarget(cfg, opts), nil
+ case "device":
+ var cfg target.DeviceConfig
+ if err := json.Unmarshal(obj, &cfg); err != nil {
+ return nil, fmt.Errorf("invalid device config found: %v", err)
+ }
+ t, err := target.NewDeviceTarget(cfg, opts)
+ return t, err
+ default:
+ return nil, fmt.Errorf("unknown type found: %q", x.Type)
+ }
+}
diff --git a/cmd/botanist/zedboot.go b/cmd/botanist/zedboot.go
index 13de934..052e32f 100644
--- a/cmd/botanist/zedboot.go
+++ b/cmd/botanist/zedboot.go
@@ -198,7 +198,7 @@
if err != nil {
return fmt.Errorf("failed to load target config file %q", cmd.configFile)
}
- opts := target.DeviceOptions{
+ opts := target.Options{
Netboot: cmd.netboot,
Fastboot: cmd.fastboot,
}
diff --git a/cmd/health_checker/main.go b/cmd/health_checker/main.go
index 48190fe..add3f95 100644
--- a/cmd/health_checker/main.go
+++ b/cmd/health_checker/main.go
@@ -109,7 +109,7 @@
var devices []*target.DeviceTarget
for _, config := range configs {
- device, err := target.NewDeviceTarget(config, target.DeviceOptions{})
+ device, err := target.NewDeviceTarget(config, target.Options{})
if err != nil {
log.Fatal(err)
}