blob: a4e5742383221074c8c26192c5562bfd896d8219 [file] [log] [blame]
// Copyright 2021 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 device
import (
"bufio"
"bytes"
"context"
"fmt"
"strings"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/util"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
// DeviceFinderMode selects the protocol to use to resolve a nodename to an IP Address.
type DeviceFinderMode int
const (
// Mdns uses the mDNS protocol to resolve a nodename to an IP address.
Mdns DeviceFinderMode = iota
// Netboot uses the netboot protocol to resolve a nodename to an IP address.
Netboot
)
func (m DeviceFinderMode) String() string {
if m == Mdns {
return "mdns"
}
return "netboot"
}
// DeviceFinder wraps the logic around the `device-finder` tool. This tools
// allows the discovery and resolution of device node names to their IP
// addresses.
type DeviceFinder struct {
deviceFinderPath string
}
// NewDeviceFinder constructs a `DeviceFinder` type wrapping the
// `device-finder` path pointed to by the `deviceFinderPath`.
func NewDeviceFinder(deviceFinderPath string) *DeviceFinder {
return &DeviceFinder{
deviceFinderPath: deviceFinderPath,
}
}
// DeviceEntry describes the result from `List` and `ListNetboot`.
type DeviceEntry struct {
Address string
NodeName string
}
// List wraps running `device-finder list ...`. This can be filtered down for specific nodenames.
func (d *DeviceFinder) List(ctx context.Context, mode DeviceFinderMode, nodeNames ...string) ([]DeviceEntry, error) {
args := []string{
"list",
"-ipv4=false",
"-timeout=5s",
"-full",
}
switch mode {
case Mdns:
args = append(args, "-mdns")
case Netboot:
args = append(args, "-netboot")
}
stdout, err := d.run(ctx, args...)
if err != nil {
return nil, fmt.Errorf("failed to list devices: %w", err)
}
var devices []DeviceEntry
s := bufio.NewScanner(bytes.NewReader(stdout))
for s.Scan() {
line := s.Text()
// Device finder (with -full) outputs in the format "<address> <domain>".
entry := strings.Split(line, " ")
if len(entry) != 2 {
return nil, fmt.Errorf("device-finder return unexpected output: %s", stdout)
}
address := entry[0]
nodeName := entry[1]
if address == "" {
return nil, fmt.Errorf("device-finder list returned an empty address")
}
if nodeName == "" {
return nil, fmt.Errorf("device-finder list returned an empty nodename")
}
devices = append(devices, DeviceEntry{
Address: address,
NodeName: nodeName,
})
}
return devices, nil
}
// Resolves nodenames to an IP address.
func (d *DeviceFinder) Resolve(ctx context.Context, mode DeviceFinderMode, nodeNames []string) (DeviceEntry, error) {
logger.Infof(ctx, "resolving the nodenames %v using %v", nodeNames, mode)
entries, err := d.List(ctx, mode)
if err != nil {
return DeviceEntry{}, err
}
var nodeEntries []DeviceEntry
if len(nodeNames) == 0 {
// Resolve to all addresses if no nodename is specified.
for _, entry := range entries {
nodeEntries = append(nodeEntries, entry)
}
} else {
// Otherwise, filter the entry list for the nodenames we are interested in.
for _, entry := range entries {
for _, nodeName := range nodeNames {
if entry.NodeName == nodeName {
nodeEntries = append(nodeEntries, entry)
}
}
}
}
logger.Infof(ctx, "resolved the nodenames %v to %v", nodeNames, entries)
if len(nodeEntries) == 0 {
return DeviceEntry{}, fmt.Errorf("no addresses found for nodenames: %v", nodeNames)
}
if len(nodeEntries) > 1 {
return DeviceEntry{}, fmt.Errorf("multiple addresses found for nodenames %v: %v", nodeNames, entries)
}
return nodeEntries[0], nil
}
func (d *DeviceFinder) run(ctx context.Context, arg ...string) ([]byte, error) {
stdout, stderr, err := util.RunCommand(ctx, d.deviceFinderPath, arg...)
if err != nil {
return []byte{}, fmt.Errorf("device-finder failed: %w: %s", err, string(stderr))
}
return stdout, nil
}