blob: 9895bb527852402e3d55182e6ddcfc7ebaf5f0a1 [file] [edit]
// 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 (
"context"
"fmt"
"net"
"strconv"
"strings"
"time"
"go.fuchsia.dev/fuchsia/src/testing/host-target-testing/ffx"
"go.fuchsia.dev/fuchsia/tools/botanist/targets"
"go.fuchsia.dev/fuchsia/tools/lib/logger"
)
type DeviceResolver interface {
// NodeName returns a nodename for a device.
NodeName() string
// Resolve the device's nodename into an ssh address.
ResolveSshAddress(ctx context.Context) (string, error)
// Block until the device appears to be in fastboot.
WaitToFindDeviceInFastboot(ctx context.Context) (string, error)
// Block until the device appears to be in netboot.
WaitToFindDeviceInNetboot(ctx context.Context) (string, error)
}
// ConstantHostResolver returns a fixed hostname for the specified nodename.
type ConstantHostResolver struct {
nodeName string
host string
sshPort int
}
// NewConstantHostResolver constructs a fixed host.
func NewConstantHostResolver(
ctx context.Context,
nodeName string,
host string,
sshPort int,
) ConstantHostResolver {
return ConstantHostResolver{
nodeName: nodeName,
host: host,
sshPort: sshPort,
}
}
func (r ConstantHostResolver) NodeName() string {
return r.nodeName
}
func (r ConstantHostResolver) ResolveSshAddress(ctx context.Context) (string, error) {
host := r.host
if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
host = host[1 : len(host)-1]
}
return net.JoinHostPort(host, strconv.Itoa(r.sshPort)), nil
}
func (r ConstantHostResolver) WaitToFindDeviceInFastboot(ctx context.Context) (string, error) {
// We have no way to tell if the device is in fastboot, so just exit.
logger.Warningf(ctx, "ConstantHostResolver cannot tell if device is in fastboot, assuming nodename is %s", r.nodeName)
return r.nodeName, nil
}
func (r ConstantHostResolver) WaitToFindDeviceInNetboot(ctx context.Context) (string, error) {
// We have no way to tell if the device is in netboot, so just exit.
logger.Warningf(ctx, "ConstantHostResolver cannot tell if device is in netboot, assuming nodename is %s", r.nodeName)
return r.nodeName, nil
}
// MdnsResolver resolves a nodename into a hostname using mDNS.
type MdnsResolver struct {
nodeName string
sshPort int
}
// NewMdnsResolver constructs a new `MdnsResolver` for the specific nodename.
func NewMdnsResolver(
ctx context.Context,
nodeName string,
sshPort int,
) *MdnsResolver {
return &MdnsResolver{
nodeName: nodeName,
sshPort: sshPort,
}
}
func (r *MdnsResolver) NodeName() string {
return r.nodeName
}
func (r *MdnsResolver) ResolveSshAddress(ctx context.Context) (string, error) {
ipv4Addr, ipv6Addr, err := targets.ResolveIP(ctx, r.nodeName)
if err != nil {
return "", err
}
var ip string
if ipv6Addr.IP != nil {
ip = ipv6Addr.String()
} else if ipv4Addr != nil {
ip = ipv4Addr.String()
} else {
return "", fmt.Errorf("cannot resolve target ssh address via mDNS lookup")
}
return net.JoinHostPort(ip, strconv.Itoa(r.sshPort)), nil
}
func (r *MdnsResolver) WaitToFindDeviceInFastboot(ctx context.Context) (string, error) {
// We have no way to tell if the device is in fastboot, so just exit.
logger.Warningf(ctx, "MdnsResolver cannot tell if device is in fastboot, assuming nodename is %s", r.nodeName)
return r.nodeName, nil
}
func (r *MdnsResolver) WaitToFindDeviceInNetboot(ctx context.Context) (string, error) {
// We have no way to tell if the device is in netboot, so just exit.
logger.Warningf(ctx, "MdnsResolver cannot tell if device is in netboot, assuming nodename is %s", r.nodeName)
return r.nodeName, nil
}
// FfxResolver uses `ffx target list` to resolve a nodename into a hostname.
type FfxResolver struct {
ffx *ffx.FFXTool
nodeName string
}
// NewFfxResolver constructs a new `FfxResolver` for the specific nodename.
func NewFfxResolver(
ctx context.Context,
ffxInst *ffx.FFXTool,
nodeName string,
address string,
) (DeviceResolver, error) {
if nodeName == "" && address != "" {
ffxInst.SetTarget(address)
if err := ffxInst.TargetWait(ctx, address); err != nil {
return nil, fmt.Errorf("failed waiting for target with address %s: %w", address, err)
}
entries, errList := ffxInst.TargetList(ctx, address, 0)
if errList != nil {
return nil, fmt.Errorf("failed to list devices: %w", errList)
}
if len(entries) == 0 {
return nil, fmt.Errorf("failed to find target with address: %s after wait", address)
}
logger.Infof(ctx, "resolved device name %v from address %v", entries[0].NodeName, address)
nodeName = entries[0].NodeName
}
if nodeName == "" {
if err := ffxInst.TargetWait(ctx, ""); err != nil {
return nil, fmt.Errorf("failed waiting for target: %w", err)
}
entries, listErr := ffxInst.TargetList(ctx, "", 0)
if listErr != nil {
return nil, fmt.Errorf("failed to list devices: %w", listErr)
}
if len(entries) != 1 {
return nil, fmt.Errorf("cannot use empty nodename with multiple devices: %v", entries)
}
nodeName = entries[0].NodeName
}
return &FfxResolver{
ffx: ffxInst,
nodeName: nodeName,
}, nil
}
func (r *FfxResolver) NodeName() string {
return r.nodeName
}
func (r *FfxResolver) ResolveName(ctx context.Context) (string, error) {
nodeName := r.NodeName()
logger.Infof(ctx, "resolving the nodename %v hostname", nodeName)
targets, err := r.ffx.TargetList(ctx, nodeName, 0)
if err != nil {
return "", err
}
logger.Infof(ctx, "resolved the nodename %v to %v", nodeName, targets)
if len(targets) == 0 {
return "", fmt.Errorf("no addresses found for nodename: %v", nodeName)
}
if len(targets) > 1 {
return "", fmt.Errorf("multiple addresses found for nodename %v: %v", nodeName, targets)
}
target := targets[0]
if len(target.Addresses) == 0 {
return "", fmt.Errorf("no address found for nodename %v: %v", nodeName, target)
}
for _, v := range target.Addresses {
if v.Type == "Ip" {
return v.IP, nil
}
}
return "", fmt.Errorf("no IP address found for nodename %v: %v", nodeName, target)
}
func (r *FfxResolver) ResolveSshAddress(ctx context.Context) (string, error) {
nodeName := r.NodeName()
logger.Infof(ctx, "resolving the nodename %v ssh address", nodeName)
targets, err := r.ffx.TargetList(ctx, nodeName, 0)
if err != nil {
return "", fmt.Errorf("failed to list devices: %w", err)
}
for _, target := range targets {
for _, addr := range target.Addresses {
if addr.Type == "Ip" {
if addr.SSHPort != 0 {
return net.JoinHostPort(addr.IP, strconv.Itoa(int(addr.SSHPort))), nil
}
return net.JoinHostPort(addr.IP, "22"), nil
}
}
}
return "", fmt.Errorf("no IP address found for nodename %v", nodeName)
}
func (r *FfxResolver) WaitToFindDeviceInFastboot(ctx context.Context) (string, error) {
nodeName := r.NodeName()
// Wait for the device to be listening in netboot.
logger.Infof(ctx, "waiting for the device to be listening on the nodename: %v", nodeName)
attempt := 0
for {
attempt += 1
entries, err := r.ffx.TargetList(ctx, "", 12*time.Second)
if err == nil {
for _, entry := range entries {
logger.Infof(ctx, "device %s is in %v", entry.NodeName, entry.TargetState)
if entry.NodeName == nodeName && entry.TargetState == "Fastboot" {
logger.Infof(ctx, "device %v is listening on %v", entry.NodeName, entry)
if entry.Serial != "" {
return entry.Serial, nil
}
if len(entry.Addresses) > 0 {
addr := entry.Addresses[0].IP
if strings.Contains(addr, ":") && !strings.HasPrefix(addr, "[") {
addr = fmt.Sprintf("[%s]", addr)
}
return addr, nil
}
return entry.NodeName, nil
}
}
logger.Infof(ctx, "attempt %d waiting for device to boot into fastboot", attempt)
time.Sleep(5 * time.Second)
} else {
logger.Infof(ctx, "attempt %d failed to resolve nodename %v: %v", attempt, nodeName, err)
time.Sleep(5 * time.Second)
}
}
}
func (r *FfxResolver) WaitToFindDeviceInNetboot(ctx context.Context) (string, error) {
// Exit early if ffx is not configured to listen for devices in zedboot.
// With strict mode, zedboot discovery is generally off or unneeded unless extra config
// is explicitly provided upfront. Let's stub logically if unsupported.
supported, err := r.ffx.SupportsZedbootDiscovery(ctx)
if err == nil && !supported {
logger.Warningf(ctx, "ffx not configured to listen for devices in zedboot, assuming nodename is %s", r.nodeName)
return r.nodeName, nil
}
nodeName := r.NodeName()
// Wait for the device to be listening in netboot.
logger.Infof(ctx, "waiting for the to be listening on the nodename: %v", nodeName)
attempt := 0
for {
attempt += 1
entries, err := r.ffx.TargetList(ctx, nodeName, 0)
if err == nil {
for _, entry := range entries {
logger.Infof(ctx, "device is in %v", entry.TargetState)
if entry.TargetState == "Zedboot (R)" {
logger.Infof(ctx, "device %v is listening on %v", entry.NodeName, entry)
return entry.NodeName, nil
}
}
logger.Infof(ctx, "attempt %d waiting for device to boot into zedboot", attempt)
time.Sleep(5 * time.Second)
} else {
logger.Infof(ctx, "attempt %d failed to resolve nodename %v: %v", attempt, nodeName, err)
time.Sleep(5 * time.Second)
}
}
}