blob: 00a5c95fb995a4f5f1b93e4e019537639070e4d9 [file] [log] [blame]
// Copyright 2022 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package internal
import (
"context"
"fmt"
"log"
"time"
"chromium.googlesource.com/chromiumos/platform/dev-util.git/contrib/fflash/internal/ssh"
)
const BootIdFile = "/proc/sys/kernel/random/boot_id"
// GetBootId returns the unique boot ID of c's remote host.
func GetBootId(ctx context.Context, c *ssh.Client) (string, error) {
return c.RunSimpleOutput("cat " + BootIdFile)
}
// GetRebootingBootId attempts to connect to host using the system SSH command.
// Then returns the unique boot ID of the host.
func GetRebootingBootId(ctx context.Context, dialer *ssh.Dialer, host string) (string, error) {
ctx, cancel := context.WithTimeout(
ctx,
dialer.SSHOptions().ConnectTimeout+
2*time.Second, // Give 2 secs for the overhead of the SSH command itself.
)
defer cancel()
cmd := dialer.DefaultCommand(ctx)
cmd.Args = append(cmd.Args, host, "cat", BootIdFile)
out, err := cmd.Output()
return string(out), err
}
// WaitReboot waits the ssh host to reboot
func WaitReboot(ctx context.Context, dialer *ssh.Dialer, host string, oldBootID string) error {
ctx, cancel := context.WithTimeout(ctx, 10*time.Minute)
defer cancel()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
BootID, err := GetRebootingBootId(ctx, dialer, host)
if err != nil {
continue
}
if BootID != oldBootID {
return nil
}
case <-ctx.Done():
return fmt.Errorf("failed to wait for DUT reboot: %s", ctx.Err())
}
}
}
// Reboot the device connected with client and wait for the host to be online
func Reboot(ctx context.Context, dialer *ssh.Dialer, client *ssh.Client, host, oldBootID string) error {
log.Println("rebooting host:", host)
if _, err := client.RunSimpleOutput("sh -c 'nohup sleep 1 && reboot &'"); err != nil {
return fmt.Errorf("failed to schedule reboot")
}
client.Close()
return WaitReboot(ctx, dialer, host, oldBootID)
}
// checkBootExpectations checks that the DUT host reboots to wantActiveRootfs.
// On success a new ssh.Client is returned.
func checkBootExpectations(ctx context.Context, dialer *ssh.Dialer, host, wantActiveRootfs string) (*ssh.Client, error) {
client, err := dialer.DialWithSystemSSH(ctx, host)
if err != nil {
return nil, fmt.Errorf("DialWithSystemSSH(): %w", err)
}
newParts, err := DetectPartitions(client)
if err != nil {
client.Close()
return nil, err
}
log.Println("DUT rebooted to:", newParts.ActiveRootfs())
if newParts.ActiveRootfs() != wantActiveRootfs {
client.Close()
return nil, fmt.Errorf("DUT did not boot to %s", wantActiveRootfs)
}
// Check that the DUT is alive for 5 seconds. If it remains alive then assume
// there are no remaining pending reboots.
if _, err := client.RunSimpleOutput("sleep 5"); err != nil {
client.Close()
return nil, fmt.Errorf("cannot execute ssh command. Maybe DUT rebooted again?")
}
return client, nil
}
// CheckedReboot reboots the dut with Reboot() and checkBootExpectations().
// A new SSH client is provided to interact with the rebooted host.
// The passed in client is closed.
func CheckedReboot(ctx context.Context, dialer *ssh.Dialer, client *ssh.Client, host, wantActiveRootfs string) (*ssh.Client, error) {
defer client.Close()
bootID, err := GetBootId(ctx, client)
if err != nil {
return nil, fmt.Errorf("cannot get current boot ID of DUT %s", bootID)
}
if err := Reboot(ctx, dialer, client, host, bootID); err != nil {
return nil, err
}
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for i := 1; ; i++ {
select {
case <-ticker.C:
log.Printf("checking boot expectations #%d", i)
newClient, err := checkBootExpectations(ctx, dialer, host, wantActiveRootfs)
if err == nil {
return newClient, nil
}
log.Printf("failed checking boot expectations: %s", err)
case <-ctx.Done():
return nil, fmt.Errorf("failed to wait for DUT reboot: %s", ctx.Err())
}
}
}