AutoVM filter updates Do not remove VM scheduling unit if already present BUG=None TEST=UT Change-Id: Iac71e34e59dfed55593a0e8a34805aab9c1ef148 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/dev-util/+/5860131 Tested-by: Varun Srivastav <varunsrivastav@google.com> Auto-Submit: Varun Srivastav <varunsrivastav@google.com> Reviewed-by: Derek Beckett <dbeckett@chromium.org> Commit-Queue: Varun Srivastav <varunsrivastav@google.com>
diff --git a/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/main.go b/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/main.go index 6abadd7..6d08ec2 100644 --- a/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/main.go +++ b/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/main.go
@@ -4,13 +4,9 @@ package main import ( - "encoding/json" - "errors" "fmt" "log" "os" - "os/exec" - "regexp" server "go.chromium.org/chromiumos/test/ctpv2/common/server_template" @@ -28,7 +24,7 @@ success = "SUCCESS" ) -var vmBoards = []string{"betty", "betty-arc-t", "reven-vmtest", "amd64-generic", "betty-kernelnext", "betty-arc-r", "betty-arc-v"} +var vmBoards = []string{"betty", "reven-vmtest", "amd64-generic"} // isTestVMCompatible returns true if the test can be safely executed on a VM. func isTestVMCompatible(testCase *api.CTPTestCase) bool { @@ -55,8 +51,8 @@ return false } -func generateVMSchedulingUnitOptions(board string, version string) []*api.SchedulingUnitOptions { - schedulingUnit := &api.SchedulingUnit{ +func generateVMSchedulingUnit(board string, version string) *api.SchedulingUnit { + return &api.SchedulingUnit{ PrimaryTarget: &api.Target{ SwarmingDef: &api.SwarmingDefinition{ DutInfo: &testapi.Dut{ @@ -72,16 +68,22 @@ "installPath": "gs://chromeos-image-archive/" + board + "-" + release + version, }, } - var schedulingUnits []*api.SchedulingUnit - schedulingUnits = append(schedulingUnits, schedulingUnit) - schedulingUnitOption := &api.SchedulingUnitOptions{ - SchedulingUnits: schedulingUnits, +} + +// updateSchedulingUnitOption updates the list of Scheduling units. Returns nil if no VMlab Scheduling unit found. +func updateSchedulingUnitOption(schedulingUnitOption *api.SchedulingUnitOptions, board string, version string) *api.SchedulingUnitOptions { + var filteredSchedulingUnits []*api.SchedulingUnit + for _, schedulingUnit := range schedulingUnitOption.GetSchedulingUnits() { + // if VM board in scheduling unit then do not remove. This will trigger VMlab flow + if checkBoardInVMBoardsList(getBoard(schedulingUnit)) { + filteredSchedulingUnits = append(filteredSchedulingUnits, schedulingUnit) + } } - - var schedulingUnitOptions []*api.SchedulingUnitOptions - schedulingUnitOptions = append(schedulingUnitOptions, schedulingUnitOption) - - return schedulingUnitOptions + if len(filteredSchedulingUnits) == 0 { + return nil + } + schedulingUnitOption.SchedulingUnits = filteredSchedulingUnits + return schedulingUnitOption } // updateSchedulingUnitOptions updates the schedulingUnitOptions for each test case in internal test plan request. @@ -89,8 +91,27 @@ for _, testCase := range req.GetTestCases() { if isTestVMCompatible(testCase) { - updatedSchedulingUnitOptions := generateVMSchedulingUnitOptions(board, version) - testCase.SchedulingUnitOptions = updatedSchedulingUnitOptions + var updatedSchedulingUnitOptions []*api.SchedulingUnitOptions + for _, schedulingUnitOption := range testCase.GetSchedulingUnitOptions() { + updatedSchedulingUnitOption := updateSchedulingUnitOption(schedulingUnitOption, board, version) + if updatedSchedulingUnitOption == nil { + continue + } + updatedSchedulingUnitOptions = append(updatedSchedulingUnitOptions, updatedSchedulingUnitOption) + } + // if no schedulign unit found from intended scheduling units, then create a VM schedulign unit with available board/version + if len(updatedSchedulingUnitOptions) == 0 { + testCase.SchedulingUnitOptions = []*api.SchedulingUnitOptions{ + { + SchedulingUnits: []*api.SchedulingUnit{ + generateVMSchedulingUnit(board, version), + }, + }, + } + } else { + testCase.SchedulingUnitOptions = updatedSchedulingUnitOptions + } + } else { log.Printf("Skippping as test : %s, isn't VM compatible", testCase.GetName()) } @@ -99,125 +120,6 @@ return nil } -func getBuildReportJSON(gcsPath string) ([]byte, error) { - // Execute the gsutil ls command - log.Printf("Checking image %s status...", gcsPath) - cmd := exec.Command("gsutil", "cat", gcsPath+"/"+buildReportJSON) - log.Printf("Executing command: %s", cmd.String()) - output, err := cmd.CombinedOutput() - if err != nil { - return nil, fmt.Errorf("error executing gsutil command: %v, output: %s", err, string(output)) - } - // Check if the stdout is empty. No build report found - if len(output) == 0 { - return nil, fmt.Errorf("No build report found") - } - return output, nil -} - -// isVMImageAvailable reads build report in gs bucket and infers if the builder has successfully completed. -func isVMImageAvailable(gcsPath string) (bool, error) { - buildReportJSON, err := getBuildReportJSON(gcsPath) - if err != nil { - log.Printf("Error while getting build report: %s", err) - } - // Unmarshal the JSON string into the map - var result map[string]interface{} - err = json.Unmarshal(buildReportJSON, &result) - if err != nil { - return false, fmt.Errorf("error parsing JSON: %v", err) - } - - // safely parse the nested structure - status, ok := result["status"].(map[string]interface{}) - if !ok { - return false, fmt.Errorf("status key not found or is not in expected format") - } - - value, ok := status["value"].(string) - if !ok { - return false, fmt.Errorf("value key not found or is not in expected format") - } - - // check the status value from build report - fmt.Printf("Status is %s\n", value) - if value == success { - log.Printf("Image %s is available", gcsPath) - return true, nil - } - log.Printf("Image %s is not available", gcsPath) - return false, nil -} - -// getImageVersion returns the image version from a given image gs url(installPath) ex-`R128-15964.4.0`. -func getImageVersionFromInstallPath(installPath string) (string, error) { - if installPath == "" { - return "", errors.New("Empty Installpath") - } - // Define a regex pattern to match "R" followed by digits and a hyphen - re := regexp.MustCompile(`/R(.+)$`) - - match := re.FindStringSubmatch(installPath) - if len(match) < 2 { - return "", errors.New("milestone version not found in installPath") - } - - milestone := match[1] - return "R" + milestone, nil -} - -// getTargetedImageVersion fetches the chromeos image version of boards on which tests are to be executed. -// Note: Is same for all boards for a single CTP request. -func getTargetedImageVersion(req *api.InternalTestplan, log *log.Logger) string { - // find the image version from the first valid gcs installPath - for _, tc := range req.TestCases { - for _, units := range tc.GetSchedulingUnitOptions() { - for _, unit := range units.GetSchedulingUnits() { - gcsInstallPath := unit.GetDynamicUpdateLookupTable()[installPath] - imageVersion, err := getImageVersionFromInstallPath(gcsInstallPath) - if err != nil { - // log the error and try for other install paths. - log.Printf("Error while getting image version for gcsInstallPath %s: %s", gcsInstallPath, err) - continue - } - log.Printf("Found image version for the request: %s", imageVersion) - return imageVersion - } - } - } - return "" -} - -// isAnyVMImageAvailable returns true if any corresponding VM image for same version is available. Availability -// means that the builder has successfully completed building image and should be available to lease a VM with it. -func isAnyVMImageAvailable(req *api.InternalTestplan, log *log.Logger) (bool, string, string) { - // fetch image version of intended boards. Same version of VM boards will be used, if available. - targetedVersion := getTargetedImageVersion(req, log) - if targetedVersion == "" { - log.Printf("Failed to find image version from the request. Skipping AutoVM test shifter filter execution.") - return false, "", "" - } - - // check for all valid VM boards for image availability. Stop as soon as any is found available. - log.Printf("Checking availability for VM board images ...") - for _, board := range vmBoards { - // form the gcsImagePath for VM board to check image availability. - gcsImagePath := gsBucketPath + board + "-" + release + targetedVersion - log.Printf("GCS image path for board %s: %s", board, gcsImagePath) - - exists, err := isVMImageAvailable(gcsImagePath) - if err != nil { - log.Printf("Error while checking image availability: %s", err) - continue - } - if exists { - return exists, board, targetedVersion - } - } - log.Printf("No images found available.") - return false, "", "" -} - func executor(req *api.InternalTestplan, log *log.Logger) (*api.InternalTestplan, error) { available, board, version := isAnyVMImageAvailable(req, log)
diff --git a/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/main_test.go b/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/main_test.go index 9e67ea5..d76ce85 100644 --- a/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/main_test.go +++ b/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/main_test.go
@@ -70,6 +70,31 @@ }, }, } + SchedulingUnitDedede := []*api.SchedulingUnit{ + { + PrimaryTarget: &api.Target{ + SwarmingDef: &api.SwarmingDefinition{ + DutInfo: &testapi.Dut{ + DutType: &testapi.Dut_Chromeos{ + Chromeos: &testapi.Dut_ChromeOS{ + DutModel: &testapi.DutModel{ + BuildTarget: "dedede", + }, + }, + }, + }, + SwarmingLabels: []string{ + "label-peripheral_wifi_state:WORKING", + "label-wificell:True", + }, + }, + }, + DynamicUpdateLookupTable: map[string]string{ + "board": "betty", + "installPath": "gs://chromeos-image-archive/dedede-release/R128-15964.4.0", + }, + }, + } var SchedulingUnitOptions []*api.SchedulingUnitOptions for _, board := range boards { @@ -78,7 +103,10 @@ SchedulingUnitOptions = append(SchedulingUnitOptions, &api.SchedulingUnitOptions{SchedulingUnits: SchedulingUnitSkyrim}) case "betty": SchedulingUnitOptions = append(SchedulingUnitOptions, &api.SchedulingUnitOptions{SchedulingUnits: SchedulingUnitBetty}) + case "dedede": + SchedulingUnitOptions = append(SchedulingUnitOptions, &api.SchedulingUnitOptions{SchedulingUnits: SchedulingUnitDedede}) } + } return SchedulingUnitOptions @@ -255,7 +283,7 @@ t.Errorf("Error expected to be nil, got %s", err) } if version != expectedVersion { - t.Errorf("Expected R128-15964.4.0, got %s", version) + t.Errorf("Expected %s, got %s", expectedVersion, version) } } } @@ -344,7 +372,7 @@ } -func Test_UpdateSchedulingUnitOptions_VMCompatibleTest(t *testing.T) { +func Test_UpdateSchedulingUnitOptions_VMCompatibleTest_WithVMSchedulingUnit(t *testing.T) { schedulingUnitOptions := GenerateSchedulingUnitOptions([]string{"skyrim", "betty"}) testCase := GenerateCTPTestCase("test1", true, schedulingUnitOptions) internalTestPlan := &api.InternalTestplan{ @@ -355,13 +383,41 @@ if err != nil { t.Errorf("Expected nil, got %s", err) } + + if len(internalTestPlan.TestCases[0].GetSchedulingUnitOptions()) != 1 { + t.Errorf("Expected 2, got %d", len(internalTestPlan.TestCases[0].SchedulingUnitOptions)) + } + expectedBoard := "betty" + updatedBoard := internalTestPlan.TestCases[0].GetSchedulingUnitOptions()[0].GetSchedulingUnits()[0].GetDynamicUpdateLookupTable()["board"] + if expectedBoard != updatedBoard { + t.Errorf("Expected %s, got %s", expectedBoard, updatedBoard) + } + if len(internalTestPlan.TestCases[0].GetSchedulingUnitOptions()[0].GetSchedulingUnits()) != 1 { + t.Errorf("Expected 1, got %d", len(internalTestPlan.TestCases[0].SchedulingUnitOptions)) + } +} + +func Test_UpdateSchedulingUnitOptions_VMCompatibleTest_WithNoVMSchedulingUnit(t *testing.T) { + schedulingUnitOptions := GenerateSchedulingUnitOptions([]string{"skyrim", "dedede"}) + testCase := GenerateCTPTestCase("test1", true, schedulingUnitOptions) + internalTestPlan := &api.InternalTestplan{ + TestCases: []*api.CTPTestCase{testCase}, + } + + err := updateSchedulingUnitOptions(internalTestPlan, "reven-vmtest", "R128-15964.4.0", log.New(os.Stdout, "DEBUG", log.LstdFlags)) + if err != nil { + t.Errorf("Expected nil, got %s", err) + } + if len(internalTestPlan.TestCases[0].GetSchedulingUnitOptions()) != 1 { t.Errorf("Expected 2, got %d", len(internalTestPlan.TestCases[0].SchedulingUnitOptions)) } expectedBoard := "reven-vmtest" updatedBoard := internalTestPlan.TestCases[0].GetSchedulingUnitOptions()[0].GetSchedulingUnits()[0].GetDynamicUpdateLookupTable()["board"] if expectedBoard != updatedBoard { - t.Errorf("Expected reven-vmtest, got %d", len(internalTestPlan.TestCases[0].SchedulingUnitOptions)) + t.Errorf("Expected %s, got %s", expectedBoard, updatedBoard) } - + if len(internalTestPlan.TestCases[0].GetSchedulingUnitOptions()[0].GetSchedulingUnits()) != 1 { + t.Errorf("Expected 1, got %d", len(internalTestPlan.TestCases[0].SchedulingUnitOptions)) + } }
diff --git a/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/utils.go b/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/utils.go new file mode 100644 index 0000000..cc0e415 --- /dev/null +++ b/src/go.chromium.org/chromiumos/test/ctpv2/autovm_test_shifter_filter/utils.go
@@ -0,0 +1,170 @@ +// Copyright 2024 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package main + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "os/exec" + "regexp" + + "go.chromium.org/chromiumos/config/go/test/api" + labapi "go.chromium.org/chromiumos/config/go/test/lab/api" +) + +// dutModelFromDut returns the dut model for a DUT. +func dutModelFromDut(dut *labapi.Dut) *labapi.DutModel { + if dut == nil { + return nil + } + + switch hw := dut.GetDutType().(type) { + case *labapi.Dut_Chromeos: + return hw.Chromeos.GetDutModel() + case *labapi.Dut_Android_: + return hw.Android.GetDutModel() + case *labapi.Dut_Devboard_: + return hw.Devboard.GetDutModel() + } + return nil +} + +// getBoard returns board value for a scheduling unit. +func getBoard(unit *api.SchedulingUnit) string { + return dutModelFromDut(unit.GetPrimaryTarget().GetSwarmingDef().GetDutInfo()).GetBuildTarget() +} + +func checkBoardInVMBoardsList(board string) bool { + for _, vmBoard := range vmBoards { + if board == vmBoard { + return true + } + } + return false +} + +func getBuildReportJSON(gcsPath string) ([]byte, error) { + // Execute the gsutil ls command + log.Printf("Checking image %s status...", gcsPath) + cmd := exec.Command("gsutil", "cat", gcsPath+"/"+buildReportJSON) + log.Printf("Executing command: %s", cmd.String()) + output, err := cmd.CombinedOutput() + if err != nil { + return nil, fmt.Errorf("error executing gsutil command: %v, output: %s", err, string(output)) + } + // Check if the stdout is empty. No build report found + if len(output) == 0 { + return nil, fmt.Errorf("No build report found") + } + return output, nil +} + +// isVMImageAvailable reads build report in gs bucket and infers if the builder has successfully completed. +func isVMImageAvailable(gcsPath string) (bool, error) { + buildReportJSON, err := getBuildReportJSON(gcsPath) + if err != nil { + log.Printf("Error while getting build report: %s", err) + } + // Unmarshal the JSON string into the map + var result map[string]interface{} + err = json.Unmarshal(buildReportJSON, &result) + if err != nil { + return false, fmt.Errorf("error parsing JSON: %v", err) + } + + // safely parse the nested structure + status, ok := result["status"].(map[string]interface{}) + if !ok { + return false, fmt.Errorf("status key not found or is not in expected format") + } + + value, ok := status["value"].(string) + if !ok { + return false, fmt.Errorf("value key not found or is not in expected format") + } + + // check the status value from build report + log.Printf("Status is %s\n", value) + if value == success { + log.Printf("Image %s is available", gcsPath) + return true, nil + } + log.Printf("Image %s is not available", gcsPath) + return false, nil +} + +// getImageVersion returns the image version from a given image gs url(installPath) ex-`R128-15964.4.0`. +func getImageVersionFromInstallPath(installPath string) (string, error) { + if installPath == "" { + return "", errors.New("Empty Installpath") + } + // Capturing group regex. match[0] contains everything including /R, match[1] contains everything captured as part of (.+) + re := regexp.MustCompile(`/R(.+)$`) + + match := re.FindStringSubmatch(installPath) + if len(match) < 2 { + return "", errors.New("milestone version not found in installPath") + } + + milestone := match[1] + return "R" + milestone, nil +} + +// getTargetedImageVersion fetches the chromeos image version of boards on which tests are to be executed. +// Note: Is same for all boards for a single CTP request. +func getTargetedImageVersion(req *api.InternalTestplan, log *log.Logger) string { + // find the image version from the first valid gcs installPath + for _, tc := range req.TestCases { + for _, units := range tc.GetSchedulingUnitOptions() { + for _, unit := range units.GetSchedulingUnits() { + if unit.GetDynamicUpdateLookupTable() != nil { + gcsInstallPath, exists := unit.GetDynamicUpdateLookupTable()[installPath] + if exists { + imageVersion, err := getImageVersionFromInstallPath(gcsInstallPath) + if err != nil { + log.Printf("Error while getting image version for gcsInstallPath %s: %s", gcsInstallPath, err) + continue + } + log.Printf("Found image version for the request: %s", imageVersion) + return imageVersion + } + } + } + } + } + return "" +} + +// isAnyVMImageAvailable returns true if any corresponding VM image for same version is available. Availability +// means that the builder has successfully completed building image and should be available to lease a VM with it. +func isAnyVMImageAvailable(req *api.InternalTestplan, log *log.Logger) (bool, string, string) { + // fetch image version of intended boards. Same version of VM boards will be used, if available. + targetedVersion := getTargetedImageVersion(req, log) + if targetedVersion == "" { + log.Printf("Failed to find image version from the request. Skipping AutoVM test shifter filter execution.") + return false, "", "" + } + + // check for all valid VM boards for image availability. Stop as soon as any is found available. + log.Printf("Checking availability for VM board images ...") + for _, board := range vmBoards { + // form the gcsImagePath for VM board to check image availability. + gcsImagePath := gsBucketPath + board + "-" + release + targetedVersion + log.Printf("GCS image path for board %s: %s", board, gcsImagePath) + + exists, err := isVMImageAvailable(gcsImagePath) + if err != nil { + log.Printf("Error while checking image availability: %s", err) + continue + } + if exists { + return exists, board, targetedVersion + } + } + log.Printf("No images found available.") + return false, "", "" +}