On coral devices futility leaves the EC in RO

Add a reboot into the futility.py script in case we lose the ethernet
and don't get it back by the time futility finishes.

BUG=b:331205004
TEST=On leased duts

Change-Id: Ie075e5e209a5288aa1bc959c3f9ddd3113afa57c
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/5545058
Commit-Queue: Kshitij Shah <tij@google.com>
Auto-Submit: Jeremy Bettis <jbettis@chromium.org>
Commit-Queue: Jeremy Bettis <jbettis@chromium.org>
Reviewed-by: Kshitij Shah <tij@google.com>
Tested-by: Jeremy Bettis <jbettis@chromium.org>
diff --git a/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/README.md b/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/README.md
index b2227a3..63270d8 100644
--- a/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/README.md
+++ b/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/README.md
@@ -25,7 +25,7 @@
 ```
 BOARD=myBoard
 MODEL=myModel
-D=192.168.0.0
+DUT_HOSTNAME=192.168.0.0
 ```
 
 Outside chroot
@@ -52,9 +52,14 @@
 
 Then run the local-cft binary to run the cros-dut server.
 ```
-./chroot/usr/bin/local-cft -board ${BOARD?} -host ${D?} -stayalive -port cros-dut=8123
+./chroot/usr/bin/local-cft -board ${BOARD?} -host ${DUT_HOSTNAME?} -stayalive -port cros-dut=8123
 # If you made changes in another service like cros-dut run
-# ./chroot/usr/bin/local-cft -board ${BOARD?} -host ${D?} -stayalive -port cros-dut=8123 --chroot ~/chromiumos/chroot --localservices cros-dut
+# ./chroot/usr/bin/local-cft -board ${BOARD?} -host ${DUT_HOSTNAME?} -stayalive -port cros-dut=8123 --chroot ~/chromiumos/chroot --localservices cros-dut
+```
+
+To find the cache server on a lab machine:
+```
+ssh $DUT_HOSTNAME 'for devserver in 192.168.100.1 100.115.168.190 10.128.176.210 100.115.21.212 100.115.245.199 100.115.245.200 100.115.219.131 100.115.219.132 100.115.219.133 100.115.219.134 100.115.219.137; do if curl -f --connect-timeout 3 "http://${devserver?}:8082/check_health" >/dev/null ; then echo CACHE_SERVER=${devserver?} ; fi; done'
 ```
 
 Inside chroot
diff --git a/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/cros_fw_provision_test.go b/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/cros_fw_provision_test.go
index d76981f..be21849 100644
--- a/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/cros_fw_provision_test.go
+++ b/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/cros_fw_provision_test.go
@@ -440,7 +440,7 @@
 							"with open(\"/var/tmp/someTempDir/futility.log\", \"wb\", buffering=0) as outFile:\n"+
 							"  rc = subprocess.run([\"futility\", %s], stdout=outFile, stderr=outFile, check=False, bufsize=0)\n"+
 							"  outFile.write(f\"EXIT CODE: {rc.returncode}\\n\".encode(\"utf-8\"))\n  outFile.flush()\n"+
-							"subprocess.run([\"sync\", \"-d\", \"/var/tmp\", \"/usr/local/tmp\"])\n", expectedFutilityPyArgs)),
+							"  subprocess.run([\"sync\", \"-d\", \"/var/tmp\", \"/usr/local/tmp\"])\n  subprocess.run([\"reboot\"])\n", expectedFutilityPyArgs)),
 					}}).Return(newResponse(&api.ExecCommandResponse{ExitInfo: &api.ExecCommandResponse_ExitInfo{}}), nil),
 					dsc.EXPECT().ExecCommand(gomock.Any(), &rpcMsg{msg: &api.ExecCommandRequest{
 						Command: "bash",
@@ -533,14 +533,18 @@
 				Command: "rm",
 				Args:    []string{"-rf", "/var/tmp/someTempDir"},
 			}}).Return(newResponse(&api.ExecCommandResponse{ExitInfo: &api.ExecCommandResponse_ExitInfo{}}), nil),
-			dsc.EXPECT().Restart(gomock.Any(), gomock.Any()).Return(&longrunning.Operation{Done: true}, nil),
-			dsc.EXPECT().ExecCommand(gomock.Any(), &rpcMsg{msg: &api.ExecCommandRequest{
-				Command: "true",
-			}}).Return(nil, fmt.Errorf("ssh timeout")),
-			dsc.EXPECT().ExecCommand(gomock.Any(), &rpcMsg{msg: &api.ExecCommandRequest{
-				Command: "true",
-			}}).Return(newResponse(&api.ExecCommandResponse{ExitInfo: &api.ExecCommandResponse_ExitInfo{}}), nil),
 		)
+		if !testCase.ec_ro || testCase.updateRw {
+			gomock.InOrder(
+				dsc.EXPECT().Restart(gomock.Any(), gomock.Any()).Return(&longrunning.Operation{Done: true}, nil),
+				dsc.EXPECT().ExecCommand(gomock.Any(), &rpcMsg{msg: &api.ExecCommandRequest{
+					Command: "true",
+				}}).Return(nil, fmt.Errorf("ssh timeout")),
+				dsc.EXPECT().ExecCommand(gomock.Any(), &rpcMsg{msg: &api.ExecCommandRequest{
+					Command: "true",
+				}}).Return(newResponse(&api.ExecCommandResponse{ExitInfo: &api.ExecCommandResponse_ExitInfo{}}), nil),
+			)
+		}
 		if testCase.updateRo && testCase.updateRw {
 			gomock.InOrder(
 				dsc.EXPECT().ExecCommand(gomock.Any(), &rpcMsg{msg: &api.ExecCommandRequest{
diff --git a/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/service/firmwareservice.go b/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/service/firmwareservice.go
index 0473b22..4c29c09 100644
--- a/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/service/firmwareservice.go
+++ b/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/service/firmwareservice.go
@@ -80,6 +80,9 @@
 
 	CacheServer      url.URL
 	ExpectedVersions FirmwareVersions
+
+	// RestartRequired indicates that a firmware update was performed, but the DUT has not yet rebooted.
+	RestartRequired bool
 }
 
 type versionJSON struct {
@@ -561,6 +564,7 @@
 		// TODO(sfrolov): extra args from fw-config.json
 	}
 
+	fws.RestartRequired = true
 	connection := fws.GetConnectionToFlashingDevice()
 	if ecImagePath != "" {
 		// If we are flashing EC, we might lose SSH access, so use nohup to run futility in a temporary python script, and poll for results
@@ -583,7 +587,8 @@
 		scriptBody.WriteString(`], stdout=outFile, stderr=outFile, check=False, bufsize=0)
   outFile.write(f"EXIT CODE: {rc.returncode}\n".encode("utf-8"))
   outFile.flush()
-subprocess.run(["sync", "-d", "/var/tmp", "/usr/local/tmp"])
+  subprocess.run(["sync", "-d", "/var/tmp", "/usr/local/tmp"])
+  subprocess.run(["reboot"])
 `)
 		_, _, err := RunDUTCommand(ctx, fws.DUTServer, time.Minute, "cat", []string{">", pyScript}, []byte(scriptBody.String()))
 		if err != nil {
@@ -610,6 +615,7 @@
 			if m != nil {
 				log.Printf("Futility output:\n%s", buf)
 				if m[1] == "0" {
+					fws.RestartRequired = false
 					return nil
 				}
 				return errors.Errorf("futility failed: %q", buf)
diff --git a/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/state-machine/postinstallstate.go b/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/state-machine/postinstallstate.go
index 0b0be39..87d2dec 100644
--- a/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/state-machine/postinstallstate.go
+++ b/src/go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/state-machine/postinstallstate.go
@@ -8,9 +8,10 @@
 
 import (
 	"context"
-	firmwareservice "go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/service"
 	"log"
 
+	firmwareservice "go.chromium.org/chromiumos/test/provision/v2/cros-fw-provision/service"
+
 	"github.com/pkg/errors"
 	"go.chromium.org/chromiumos/config/go/test/api"
 )
@@ -24,9 +25,11 @@
 func (s FirmwarePostInstallState) Execute(ctx context.Context, log *log.Logger) (*api.FirmwareProvisionResponse, api.InstallResponse_Status, error) {
 	fwMetadata := s.service.GetVersions()
 	s.service.DeleteArchiveDirectories()
-	err := s.service.RestartDut(ctx, false)
-	if err != nil {
-		return fwMetadata, api.InstallResponse_STATUS_DUT_UNREACHABLE_POST_FIRMWARE_UPDATE, firmwareservice.UnreachablePostProvisionErr(err)
+	if s.service.RestartRequired {
+		err := s.service.RestartDut(ctx, false)
+		if err != nil {
+			return fwMetadata, api.InstallResponse_STATUS_DUT_UNREACHABLE_POST_FIRMWARE_UPDATE, firmwareservice.UnreachablePostProvisionErr(err)
+		}
 	}
 
 	versions, err := s.service.ActiveFirmwareVersions(ctx)