Support to probe a live device for identity

For the local testing use case and the lab device onboarding, this
implements basic probing support against a live device.

These ids can then be used to reverse look up hardware and testing
config bits.

BUG=b:188712103
TEST=unit

Change-Id: I0fe99b999abd5e3b077113444d6d370ad6519023
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/3032206
Tested-by: C Shapiro <shapiroc@chromium.org>
Auto-Submit: C Shapiro <shapiroc@chromium.org>
Reviewed-by: Jaques Clapauch <jaquesc@google.com>
Commit-Queue: Jaques Clapauch <jaquesc@google.com>
diff --git a/src/chromiumos/test/dut/cmd/dutserver/dutssh/commands.go b/src/chromiumos/test/dut/cmd/dutserver/dutssh/commands.go
index d1bd6d7..be7d5d8 100644
--- a/src/chromiumos/test/dut/cmd/dutserver/dutssh/commands.go
+++ b/src/chromiumos/test/dut/cmd/dutserver/dutssh/commands.go
@@ -9,6 +9,21 @@
 	"strings"
 )
 
+type CmdResult struct {
+	ReturnCode int32
+	StdOut     string
+	StdErr     string
+}
+
+// Simple interface abstracting away many details around SSH/streaming for
+// clients that execute many simple/quick commands.
+// This insulate clients from the full complexity of DutServer and also
+// makes it easier to test logic that's focused on command execution results.
+// E.g. Identity scanning
+type CmdExecutor interface {
+	RunCmd(cmd string) (*CmdResult, error)
+}
+
 // Formatters for commands
 
 func PathExistsCommand(path string) string {
diff --git a/src/chromiumos/test/dut/internal/dutidentity.go b/src/chromiumos/test/dut/internal/dutidentity.go
new file mode 100644
index 0000000..bbe81bb
--- /dev/null
+++ b/src/chromiumos/test/dut/internal/dutidentity.go
@@ -0,0 +1,85 @@
+// Copyright 2021 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 internal
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"chromiumos/test/dut/cmd/dutserver/dutssh"
+
+	hwdesign "go.chromium.org/chromiumos/config/go/api"
+	"go.chromium.org/chromiumos/config/go/test/api"
+)
+
+// cros_config returns an error for non-existent properties, so we'll ignore
+// this and return empty string since many of the identity attributes are optional.
+func crosConfigIdentity(c dutssh.CmdExecutor, property string) string {
+	result, _ := c.RunCmd(fmt.Sprintf("cros_config /identity %s", property))
+	return strings.TrimSpace(result.StdOut)
+}
+
+// DetectDeviceConfigID uses cros_config to probe a live device and retrieve
+// unique device config identifiers, which can then be used to looking up config details.
+// A reverse lookup effectively when device identity isn't known up front.
+// This supports the local device use-case and also initial device onboarding in managed labs.
+func DetectDeviceConfigID(c dutssh.CmdExecutor) *api.DetectDeviceConfigIdResponse {
+	designScanConfig := &hwdesign.DesignConfigId_ScanConfig{}
+	var failure string
+	if match := crosConfigIdentity(c, "smbios-name-match"); len(match) > 0 {
+		designScanConfig.FirmwareNameMatch = &hwdesign.DesignConfigId_ScanConfig_SmbiosNameMatch{
+			SmbiosNameMatch: match,
+		}
+	} else if match := crosConfigIdentity(c, "device-tree-compatible-match"); len(match) > 0 {
+		designScanConfig.FirmwareNameMatch = &hwdesign.DesignConfigId_ScanConfig_DeviceTreeCompatibleMatch{
+			DeviceTreeCompatibleMatch: match,
+		}
+	} else {
+		failure = "Failed to scan firmware identity for X86 (smbios-name-match) and ARM (device-tree-compatible-match)"
+	}
+
+	// FirmwareNameMatch is the only required bit ... all optional from here on
+	if skuIDStr := crosConfigIdentity(c, "sku-id"); len(skuIDStr) > 0 {
+		if skuID, err := strconv.ParseUint(skuIDStr, 10, 32); err == nil {
+			designScanConfig.FirmwareSku = uint32(skuID)
+		} else {
+			failure = fmt.Sprintf("Unexpected value '%s' (non uint32) for sku-id", skuIDStr)
+		}
+	}
+
+	brandScanConfig := &hwdesign.DeviceBrandId_ScanConfig{}
+	if wlTag := crosConfigIdentity(c, "whitelabel-tag"); len(wlTag) > 0 {
+		brandScanConfig.WhitelabelTag = wlTag
+	}
+
+	mfgScanConfig := &hwdesign.MfgConfigId_ScanConfig{}
+	hwidResult, _ := c.RunCmd("crossystem hwid")
+	hwid := strings.TrimSpace(hwidResult.StdOut)
+	if len(hwid) > 0 {
+		mfgScanConfig.Hwid = hwid
+	}
+
+	resp := &api.DetectDeviceConfigIdResponse{}
+	if len(failure) == 0 {
+		resp.Result = &api.DetectDeviceConfigIdResponse_Success_{
+			Success: &api.DetectDeviceConfigIdResponse_Success{
+				DetectedScanConfig: &hwdesign.DeviceConfigId_ScanConfig{
+					DesignScanConfig: designScanConfig,
+					BrandScanConfig:  brandScanConfig,
+					MfgScanConfig:    mfgScanConfig,
+				},
+			},
+		}
+	} else {
+		resp.Result = &api.DetectDeviceConfigIdResponse_Failure_{
+			Failure: &api.DetectDeviceConfigIdResponse_Failure{
+				ErrorMessage: failure,
+			},
+		}
+	}
+
+	return resp
+}
diff --git a/src/chromiumos/test/dut/internal/dutidentity_test.go b/src/chromiumos/test/dut/internal/dutidentity_test.go
new file mode 100644
index 0000000..a381c81
--- /dev/null
+++ b/src/chromiumos/test/dut/internal/dutidentity_test.go
@@ -0,0 +1,115 @@
+// Copyright 2021 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 internal
+
+import (
+	"fmt"
+	"testing"
+
+	"chromiumos/test/dut/cmd/dutserver/dutssh"
+)
+
+type FakeCmdExecutor struct {
+	CmdResults map[string]*dutssh.CmdResult
+}
+
+func (e FakeCmdExecutor) RunCmd(cmd string) (*dutssh.CmdResult, error) {
+	result, exists := e.CmdResults[cmd]
+	if exists {
+		return result, nil
+	}
+	// cros_config returns 1 for non-existent properties, so mimic that
+	return cmdResult("", 1), nil
+}
+
+func cmdResult(stdout string, returnCode int32) *dutssh.CmdResult {
+	return &dutssh.CmdResult{
+		StdOut:     stdout,
+		ReturnCode: returnCode,
+	}
+}
+
+func TestFirmwareNameOnlyX86(t *testing.T) {
+	fakeFwName := "Fake"
+	fakeCmdExecutor := FakeCmdExecutor{
+		map[string]*dutssh.CmdResult{
+			"cros_config /identity smbios-name-match": cmdResult(fakeFwName, 0),
+		},
+	}
+
+	result := DetectDeviceConfigID(fakeCmdExecutor).GetSuccess().DetectedScanConfig
+	if result.DesignScanConfig.GetSmbiosNameMatch() != fakeFwName {
+		t.Fatalf("Expected: %s, got: %s", fakeFwName, result.DesignScanConfig.GetSmbiosNameMatch())
+	}
+}
+
+func TestFirmwareNameOnlyArm(t *testing.T) {
+	fakeFwName := "Fake"
+	fakeCmdExecutor := FakeCmdExecutor{
+		map[string]*dutssh.CmdResult{
+			"cros_config /identity device-tree-compatible-match": cmdResult(fakeFwName, 0),
+		},
+	}
+
+	result := DetectDeviceConfigID(fakeCmdExecutor).GetSuccess().DetectedScanConfig
+	if result.DesignScanConfig.GetDeviceTreeCompatibleMatch() != fakeFwName {
+		t.Fatalf("Expected: %s, got: %s", fakeFwName, result.DesignScanConfig.GetSmbiosNameMatch())
+	}
+}
+
+func TestOptionalIdentifers(t *testing.T) {
+	fakeFwName := "Fake"
+	skuID := 87
+	wlTag := "wlTag"
+	hwid := "FFFF FFFF FFFF"
+	fakeCmdExecutor := FakeCmdExecutor{
+		map[string]*dutssh.CmdResult{
+			"cros_config /identity smbios-name-match": cmdResult(fakeFwName, 0),
+			"cros_config /identity sku-id":            cmdResult(fmt.Sprintf("%d", skuID), 0),
+			"cros_config /identity whitelabel-tag":    cmdResult(wlTag, 0),
+			"crossystem hwid":                         cmdResult(hwid, 0),
+		},
+	}
+
+	result := DetectDeviceConfigID(fakeCmdExecutor).GetSuccess().DetectedScanConfig
+	if result.DesignScanConfig.GetSmbiosNameMatch() != fakeFwName {
+		t.Fatalf("Expected: %s, got: %s", fakeFwName, result.DesignScanConfig.GetSmbiosNameMatch())
+	}
+	if result.DesignScanConfig.GetFirmwareSku() != uint32(skuID) {
+		t.Fatalf("Expected: %d, got: %d", skuID, result.DesignScanConfig.GetFirmwareSku())
+	}
+	if result.BrandScanConfig.GetWhitelabelTag() != wlTag {
+		t.Fatalf("Expected: %s, got: %s", wlTag, result.BrandScanConfig.GetWhitelabelTag())
+	}
+	if result.MfgScanConfig.GetHwid() != hwid {
+		t.Fatalf("Expected: %s, got: %s", hwid, result.MfgScanConfig.GetHwid())
+	}
+}
+
+func TestNoFirmwareNameErrors(t *testing.T) {
+	fakeCmdExecutor := FakeCmdExecutor{
+		map[string]*dutssh.CmdResult{},
+	}
+
+	errorMessage := DetectDeviceConfigID(fakeCmdExecutor).GetFailure().ErrorMessage
+	if len(errorMessage) == 0 {
+		t.Fatalf("Expected failure for missing fw name")
+	}
+}
+
+func TestInvalidSkuFormatErrors(t *testing.T) {
+	fakeFwName := "Fake"
+	invalidSku := "NaN"
+	fakeCmdExecutor := FakeCmdExecutor{
+		map[string]*dutssh.CmdResult{
+			"cros_config /identity smbios-name-match": cmdResult(fakeFwName, 0),
+			"cros_config /identity sku-id":            cmdResult(invalidSku, 0),
+		},
+	}
+
+	errorMessage := DetectDeviceConfigID(fakeCmdExecutor).GetFailure().ErrorMessage
+	if len(errorMessage) == 0 {
+		t.Fatalf("Expected failure for invalid sku format")
+	}
+}