blob: 32e8862dd55ea2e102f280d31b3c0d1e8c0d7879 [file]
// Copyright 2017 The Chromium OS 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 dut provides a connection to a DUT ("Device Under Test")
// for use by remote tests.
package dut
import (
"context"
"fmt"
"net"
"strings"
"time"
"chromiumos/tast/errors"
"chromiumos/tast/internal/linuxssh"
"chromiumos/tast/internal/testingutil"
"chromiumos/tast/ssh"
)
const (
pingTimeout = time.Second
pingRetryDelay = time.Second
connectTimeout = 10 * time.Second
reconnectRetryDelay = time.Second
)
// Suffix names for forward compatibility.
const (
// CompanionSuffixPcap is a companion suffix for the pcap.
CompanionSuffixPcap = "-pcap"
// CompanionSuffixRouter is a companion suffix for the router.
CompanionSuffixRouter = "-router"
// CompanionSuffixTablet is a companion suffix for the tablet.
CompanionSuffixTablet = "-tablet"
)
// DUT represents a "Device Under Test" against which remote tests are run.
type DUT struct {
sopt ssh.Options
hst *ssh.Conn
beforeReboot func(context.Context, *DUT) error
}
// New returns a new DUT usable for communication with target
// (of the form "[<user>@]host[:<port>]") using the SSH key at keyFile or
// keys located in keyDir.
// The DUT does not start out in a connected state; Connect must be called.
func New(target, keyFile, keyDir string, beforeReboot func(context.Context, *DUT) error) (*DUT, error) {
d := DUT{beforeReboot: beforeReboot}
if err := ssh.ParseTarget(target, &d.sopt); err != nil {
return nil, err
}
d.sopt.ConnectTimeout = connectTimeout
d.sopt.KeyFile = keyFile
d.sopt.KeyDir = keyDir
return &d, nil
}
// Conn returns the connection to the DUT, or nil if there is no connection.
// The ownership of the connection is managed by *DUT, so don't call Close for
// the connection. To disconnect, call DUT.Disconnect.
// Storing the returned value into a variable is not recommended, because
// after reconnection (e.g. by Reboot), the instance previous Conn returned
// is stale. Always call Conn to get the present connection.
//
// Examples:
// linuxssh.GetFile(ctx, d.Conn(), src, dst)
// d.Conn().Command("uptime")
func (d *DUT) Conn() *ssh.Conn {
return d.hst
}
// Close releases the DUT's resources.
func (d *DUT) Close(ctx context.Context) error {
return d.Disconnect(ctx)
}
// Connected returns true if a usable connection to the DUT is held.
func (d *DUT) Connected(ctx context.Context) bool {
if d.hst == nil {
return false
}
if err := d.hst.Ping(ctx, pingTimeout); err != nil {
return false
}
return true
}
// Connect establishes a connection to the DUT. If a connection already
// exists, it is closed first.
func (d *DUT) Connect(ctx context.Context) error {
d.Disconnect(ctx)
var err error
d.hst, err = ssh.New(ctx, &d.sopt)
return err
}
// Disconnect closes the current connection to the DUT. It is a no-op if
// no connection is currently established.
func (d *DUT) Disconnect(ctx context.Context) error {
if d.hst == nil {
return nil
}
defer func() { d.hst = nil }()
return d.hst.Close(ctx)
}
// Command returns the Cmd struct to execute the named program with the given arguments.
//
// See https://godoc.org/chromium.googlesource.com/chromiumos/platform/tast.git/src/chromiumos/tast/host#Command
//
// DEPRECATED: use d.Conn().Command
func (d *DUT) Command(name string, args ...string) *ssh.Cmd {
// It is fine even if d.hst is nil; subsequent method calls will just fail.
return d.hst.Command(name, args...)
}
// GetFile copies a file or directory from the DUT to the local machine.
// dst is the full destination name for the file or directory being copied, not
// a destination directory into which it will be copied. dst will be replaced
// if it already exists.
//
// DEPRECATED: use linuxssh.GetFile(ctx, d.Conn(), src, dst)
func (d *DUT) GetFile(ctx context.Context, src, dst string) error {
return linuxssh.GetFile(ctx, d.hst, src, dst)
}
// WaitUnreachable waits for the DUT to become unreachable.
// It requires that a connection is already established to the DUT.
func (d *DUT) WaitUnreachable(ctx context.Context) error {
if d.hst == nil {
return errors.New("not connected")
}
for {
if err := d.hst.Ping(ctx, pingTimeout); err != nil {
// Return the context's error instead of the one returned by Ping:
// we should return an error if the context's deadline expired,
// while returning nil if only Ping returned an error.
return ctx.Err()
}
select {
case <-time.After(pingRetryDelay):
break
case <-ctx.Done():
return ctx.Err()
}
}
}
// WaitConnect connects to the DUT, waiting for it to become reachable.
// If a connection already exists, it is closed first.
func (d *DUT) WaitConnect(ctx context.Context) error {
for {
err := d.Connect(ctx)
if err == nil {
return nil
}
select {
case <-time.After(reconnectRetryDelay):
break
case <-ctx.Done():
if err.Error() == ctx.Err().Error() {
return err
}
return fmt.Errorf("%v (%v)", ctx.Err(), err)
}
}
}
// Reboot reboots the DUT.
func (d *DUT) Reboot(ctx context.Context) error {
if d.beforeReboot != nil {
if err := d.beforeReboot(ctx, d); err != nil {
return errors.Wrap(err, "failed while running pre-reboot function")
}
}
readBootID := func(ctx context.Context) (string, error) {
out, err := d.Command("cat", "/proc/sys/kernel/random/boot_id").Output(ctx)
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
initID, err := readBootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to read initial boot_id")
}
// Run the reboot command with a short timeout. This command can block for long time
// if the network interface of the DUT goes down before the SSH command finishes.
rebootCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
d.Command("reboot").Run(rebootCtx) // ignore the error
if err := testingutil.Poll(ctx, func(ctx context.Context) error {
// Set a short timeout to the iteration in case of any SSH operations
// blocking for long time. For example, the network interface of the DUT
// might go down in the middle of readBootID and it might block for
// long time.
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
if err := d.WaitConnect(ctx); err != nil {
return errors.Wrap(err, "failed to connect to DUT")
}
curID, err := readBootID(ctx)
if err != nil {
return errors.Wrap(err, "failed to read boot_id")
}
if curID == initID {
return errors.New("boot_id did not change")
}
return nil
}, &testingutil.PollOptions{Timeout: 3 * time.Minute}); err != nil {
return errors.Wrap(err, "failed to wait for DUT to reboot")
}
return nil
}
// KeyFile returns the path to the SSH private key used to connect to the DUT.
// This is provided for tests that may need to establish SSH connections to additional hosts
// (e.g. a host running a servod instance).
func (d *DUT) KeyFile() string { return d.sopt.KeyFile }
// KeyDir returns the path to the directory containing SSH private keys used to connect to the DUT.
// This is provided for tests that may need to establish SSH connections to additional hosts
// (e.g. a host running a servod instance).
func (d *DUT) KeyDir() string { return d.sopt.KeyDir }
// HostName returns the a string representing the "<dut_hostname>:<ssh_port>" used to connect to the DUT.
// This is provided for tests that may need to establish direct SSH connections to hosts.
// (e.g. syzkaller connecting to a host).
func (d *DUT) HostName() string { return d.sopt.Hostname }
// ErrCompanionHostname is the error of deriving default companion device hostname from dut's hostname.
// e.g. when DUT is connected with IP address.
var ErrCompanionHostname = errors.New("cannot derive default companion device hostname")
// CompanionDeviceHostname derives the hostname of companion device from test target
// with the convention in Autotest.
// (see server/cros/dnsname_mangler.py in Autotest)
func (d *DUT) CompanionDeviceHostname(suffix string) (string, error) {
dutHost := d.sopt.Hostname
// Try split out port part.
if host, _, err := net.SplitHostPort(dutHost); err == nil {
dutHost = host
}
if ip := net.ParseIP(dutHost); ip != nil {
// We don't mangle IP address. Return error.
return "", ErrCompanionHostname
}
// Companion device hostname convention: append suffix after the first sub-domain string.
hostname := strings.SplitN(dutHost, ".", 2)
hostname[0] = hostname[0] + suffix
return strings.Join(hostname, "."), nil
}
// connectCompanionDevice connects to a companion device in test environment. e.g. WiFi AP.
// It reuses SSH key from DUT for establishing SSH connection to a companion device.
// Note that it uses the derived hostname (from the DUT's hostname) with default port to
// establish a connection. If the router/pcap runs in a non-default port, router/pcap
// target should be specified in test variables.
func (d *DUT) connectCompanionDevice(ctx context.Context, suffix string) (*ssh.Conn, error) {
var sopt ssh.Options
hostname, err := d.CompanionDeviceHostname(suffix)
if err != nil {
return nil, err
}
// Let ParseTarget derive default user and port for us.
if err := ssh.ParseTarget(hostname, &sopt); err != nil {
return nil, ErrCompanionHostname
}
sopt.ConnectTimeout = connectTimeout
// Companion devices use the same key as DUT.
sopt.KeyFile = d.sopt.KeyFile
sopt.KeyDir = d.sopt.KeyDir
return ssh.New(ctx, &sopt)
}
// DefaultWifiRouterHost connects to the default WiFi router and returns SSH object.
// DEPRECATED: Connect using CompanionDeviceHostname() instead.
func (d *DUT) DefaultWifiRouterHost(ctx context.Context) (*ssh.Conn, error) {
return d.connectCompanionDevice(ctx, CompanionSuffixRouter)
}
// DefaultWifiPcapHost connects to the default WiFi pcap router and returns SSH object.
// DEPRECATED: Connect using CompanionDeviceHostname() instead.
func (d *DUT) DefaultWifiPcapHost(ctx context.Context) (*ssh.Conn, error) {
return d.connectCompanionDevice(ctx, CompanionSuffixPcap)
}
// WifiPeerHost connects to the WiFi peer (specified by index) and returns SSH object.
func (d *DUT) WifiPeerHost(ctx context.Context, index int) (*ssh.Conn, error) {
return d.connectCompanionDevice(ctx, fmt.Sprintf("-wifipeer%d", index))
}
// DefaultCameraboxChart connects to paired chart tablet in camerabox setup and returns SSH object.
func (d *DUT) DefaultCameraboxChart(ctx context.Context) (*ssh.Conn, error) {
return d.connectCompanionDevice(ctx, CompanionSuffixTablet)
}
// NewSecondaryDevice creates a DUT for a secondary target, sharing the same SSH info.
// TODO(crbug/1129234): Remove this when full secondary DUT support is added.
func (d *DUT) NewSecondaryDevice(target string) (*DUT, error) {
d2 := DUT{beforeReboot: d.beforeReboot}
if err := ssh.ParseTarget(target, &d2.sopt); err != nil {
return nil, err
}
d2.sopt.ConnectTimeout = connectTimeout
d2.sopt.KeyFile = d.sopt.KeyFile
d2.sopt.KeyDir = d.sopt.KeyDir
return &d2, nil
}