cmd/manager/main.go (471 lines of code) (raw):

// Copyright 2024 Google LLC. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Manager is a cli interface to the orchestration provided by the imagetest // library. Run this binary with the desired flags to run a test workflow on an // image. package main import ( "context" "encoding/xml" "flag" "fmt" "io" "log" "os" "path/filepath" "regexp" "strings" "sync" "cloud.google.com/go/storage" "github.com/GoogleCloudPlatform/cloud-image-tests" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/acceleratorconfig" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/acceleratorrdma" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/compatmanager" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/cvm" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/disk" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/guestagent" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/hostnamevalidation" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/hotattach" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/imageboot" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/licensevalidation" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/livemigrate" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/loadbalancer" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/lssd" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/mdsmtls" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/mdsroutes" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/metadata" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/network" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/networkinterfacenaming" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/networkperf" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/oslogin" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/packagemanager" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/packageupgrade" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/packagevalidation" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/pluginmanager" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/security" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/shapevalidation" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/sql" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/ssh" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/storageperf" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/suspendresume" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/vmspec" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/windowscontainers" "github.com/GoogleCloudPlatform/cloud-image-tests/test_suites/winrm" "github.com/GoogleCloudPlatform/compute-daisy/compute" "google.golang.org/api/iterator" "google.golang.org/api/option" ) var ( project = flag.String("project", "", "project to use for test runner") testProjects = flag.String("test_projects", "", "comma separated list of projects to be used for tests. defaults to the test runner project") zone = flag.String("zone", "us-central1-a", "zone to be used for tests") printwf = flag.Bool("print", false, "print out the parsed test workflows and exit") validate = flag.Bool("validate", false, "validate all the test workflows and exit") outPath = flag.String("out_path", "junit.xml", "junit xml path") gcsPath = flag.String("gcs_path", "", "GCS Path for Daisy working directory") writeLocalArtifacts = flag.String("write_local_artifacts", "", "Local path to download test artifacts from gcs.") localPath = flag.String("local_path", "", "path where test output files are stored, can be modified for local testing") images = flag.String("images", "", "comma separated list of images to test") timeout = flag.String("timeout", "45m", "timeout for the test suite") computeEndpointOverride = flag.String("compute_endpoint_override", "", "compute client endpoint override") parallelCount = flag.Int("parallel_count", 5, "TestParallelCount") parallelStagger = flag.String("parallel_stagger", "60s", "parseable time.Duration to stagger each parallel test") filter = flag.String("filter", "", "only run test suites matching filter") exclude = flag.String("exclude", "", "skip test suites matching filter") testExcludeFilter = flag.String("exclude_discrete_tests", "", "skip individual tests within suites that match the regexp filter") machineType = flag.String("machine_type", "", "deprecated, use -x86_shape and/or -arm64_shape instead") x86Shape = flag.String("x86_shape", "n1-standard-1", "default x86(-32 and -64) vm shape for tests not requiring a specific shape") arm64Shape = flag.String("arm64_shape", "t2a-standard-1", "default arm64 vm shape for tests not requiring a specific shape") setExitStatus = flag.Bool("set_exit_status", true, "Exit with non-zero exit code if test suites are failing") useReservations = flag.Bool("use_reservations", false, "Whether to consume reservations when creating VMs. Will consume any reservation if reservation_urls is unspecified.") reservationURLs = flag.String("reservation_urls", "", "Comma separated list of partial URLs for reservations to consume.") acceleratorType = flag.String("accelerator_type", "", "Accelerator type to be used for accelerator tests") ) var ( projectMap = map[string]string{ "almalinux": "almalinux-cloud", "centos": "centos-cloud", "cos": "cos-cloud", "debian": "debian-cloud", "fedora-cloud": "fedora-cloud", "fedora-coreos": "fedora-coreos-cloud", "opensuse": "opensuse-cloud", "rhel": "rhel-cloud", "rhel-sap": "rhel-sap-cloud", "rocky-linux": "rocky-linux-cloud", "sles": "suse-cloud", "sles-sap": "suse-sap-cloud", "sql-": "windows-sql-cloud", "ubuntu": "ubuntu-os-cloud", "ubuntu-pro": "ubuntu-os-pro-cloud", "windows": "windows-cloud", } ) type logWriter struct { log *log.Logger } func (l *logWriter) Write(b []byte) (int, error) { l.log.Print(string(b)) return len(b), nil } func main() { flag.Parse() if *project == "" || *zone == "" || *images == "" { log.Fatal("Must provide project, zone and images arguments") return } var testProjectsReal []string if *testProjects == "" { testProjectsReal = append(testProjectsReal, *project) } else { testProjectsReal = strings.Split(*testProjects, ",") } log.Printf("Running in project %s zone %s. Tests will run in projects: %s", *project, *zone, testProjectsReal) if *gcsPath != "" { log.Printf("gcs_path set to %s", *gcsPath) } var filterRegex *regexp.Regexp if *filter != "" { var err error filterRegex, err = regexp.Compile(*filter) if err != nil { log.Fatal("-filter flag not valid:", err) } log.Printf("using -filter %s", *filter) } var excludeRegex *regexp.Regexp if *exclude != "" { var err error excludeRegex, err = regexp.Compile(*exclude) if err != nil { log.Fatal("-exclude flag not valid:", err) } log.Printf("using -exclude %s", *exclude) } if *testExcludeFilter != "" { log.Printf("Using -exclude_discrete_tests %s", *testExcludeFilter) } if *machineType != "" { log.Printf("The -machine_type flag is deprecated, please use -x86_shape and -arm64_shape instead. Retaining legacy behavior while this is set.") *x86Shape = *machineType *arm64Shape = *machineType } var reservationURLSlice []string if *reservationURLs != "" { reservationURLSlice = strings.Split(*reservationURLs, ",") } // Setup tests. testPackages := []struct { name string setupFunc func(*imagetest.TestWorkflow) error }{ { acceleratorconfig.Name, acceleratorconfig.TestSetup, }, { acceleratorrdma.Name, acceleratorrdma.TestSetup, }, { cvm.Name, cvm.TestSetup, }, { livemigrate.Name, livemigrate.TestSetup, }, { suspendresume.Name, suspendresume.TestSetup, }, { networkperf.Name, networkperf.TestSetup, }, { networkinterfacenaming.Name, networkinterfacenaming.TestSetup, }, { loadbalancer.Name, loadbalancer.TestSetup, }, { guestagent.Name, guestagent.TestSetup, }, { hostnamevalidation.Name, hostnamevalidation.TestSetup, }, { imageboot.Name, imageboot.TestSetup, }, { licensevalidation.Name, licensevalidation.TestSetup, }, { network.Name, network.TestSetup, }, { security.Name, security.TestSetup, }, { hotattach.Name, hotattach.TestSetup, }, { lssd.Name, lssd.TestSetup, }, { disk.Name, disk.TestSetup, }, { shapevalidation.Name, shapevalidation.TestSetup, }, { packagemanager.Name, packagemanager.TestSetup, }, { packageupgrade.Name, packageupgrade.TestSetup, }, { packagevalidation.Name, packagevalidation.TestSetup, }, { storageperf.Name, storageperf.TestSetup, }, { ssh.Name, ssh.TestSetup, }, { winrm.Name, winrm.TestSetup, }, { sql.Name, sql.TestSetup, }, { metadata.Name, metadata.TestSetup, }, { oslogin.Name, oslogin.TestSetup, }, { mdsmtls.Name, mdsmtls.TestSetup, }, { mdsroutes.Name, mdsroutes.TestSetup, }, { windowscontainers.Name, windowscontainers.TestSetup, }, { vmspec.Name, vmspec.TestSetup, }, { pluginmanager.Name, pluginmanager.TestSetup, }, { compatmanager.Name, compatmanager.TestSetup, }, } ctx := context.Background() var computeclient compute.Client var err error if *computeEndpointOverride != "" { log.Printf("Using compute endpoint %q", *computeEndpointOverride) computeclient, err = compute.NewClient(ctx, option.WithEndpoint(*computeEndpointOverride)) } else { computeclient, err = compute.NewClient(ctx) } if err != nil { log.Fatalf("Could not create compute client:%v", err) } var testWorkflows []*imagetest.TestWorkflow for _, testPackage := range testPackages { if filterRegex != nil && !filterRegex.MatchString(testPackage.name) { continue } if excludeRegex != nil && excludeRegex.MatchString(testPackage.name) { continue } for _, image := range strings.Split(*images, ",") { if !strings.Contains(image, "/") { // Find the project of the image. project := "" for k := range projectMap { if strings.Contains(k, "sap") { // sap follows a slightly different naming convention. imageName := strings.Split(k, "-")[0] if strings.HasPrefix(image, imageName) && strings.Contains(image, "sap") { project = projectMap[k] break } } if strings.HasPrefix(image, k) { project = projectMap[k] break } } if project == "" { log.Fatalf("unknown image %s", image) } // Check whether the image is an image family or a specific image version. isMatch, err := regexp.MatchString(".*v([0-9]+)", image) if err != nil { log.Fatalf("failed regex: %v", err) } if isMatch { image = fmt.Sprintf("projects/%s/global/images/%s", project, image) } else { image = fmt.Sprintf("projects/%s/global/images/family/%s", project, image) } } log.Printf("Add test workflow for test %s on image %s", testPackage.name, image) test, err := imagetest.NewTestWorkflow(&imagetest.TestWorkflowOpts{ Client: computeclient, ComputeEndpointOverride: *computeEndpointOverride, Name: testPackage.name, Image: image, Timeout: *timeout, Project: *project, Zone: *zone, ExcludeFilter: *testExcludeFilter, X86Shape: *x86Shape, ARM64Shape: *arm64Shape, UseReservations: *useReservations, ReservationURLs: reservationURLSlice, AcceleratorType: *acceleratorType, }) if err != nil { log.Fatalf("Failed to create test workflow: %v", err) } testWorkflows = append(testWorkflows, test) if err := testPackage.setupFunc(test); err != nil { log.Fatalf("%s.TestSetup for %s failed: %v", testPackage.name, image, err) } } } if len(testWorkflows) == 0 { log.Fatalf("No workflows to run!") } log.Println("Done with setup") storageclient, err := storage.NewClient(ctx) if err != nil { log.Fatalf("failed to set up storage client: %v", err) } if *printwf { imagetest.PrintTests(ctx, storageclient, testWorkflows, *project, *zone, *gcsPath, *localPath) return } if *validate { if err := imagetest.ValidateTests(ctx, storageclient, testWorkflows, *project, *zone, *gcsPath, *localPath); err != nil { log.Printf("Validate failed: %v\n", err) } return } suites, err := imagetest.RunTests(ctx, storageclient, testWorkflows, *project, *zone, *gcsPath, *localPath, *parallelCount, *parallelStagger, testProjectsReal) if err != nil { log.Fatalf("Failed to run tests: %v", err) } if *writeLocalArtifacts != "" { var wg sync.WaitGroup for _, twf := range testWorkflows { bkt := strings.TrimSuffix(strings.TrimPrefix(regexp.MustCompile(`gs://[a-z0-9][a-z0-9-_.]{2,62}[a-z0-9]/?`).FindString(twf.GCSPath), "gs://"), "/") if bkt == "" { log.Printf("could not find gcs bucket from %s for workflow %s", twf.GCSPath, twf.Name) continue } gcsSubfolder := strings.TrimPrefix(twf.GCSPath, "gs://"+bkt+"/") wg.Add(1) go func(bucket, folder, dstDir string) { defer wg.Done() if err := downloadFolder(ctx, storageclient, bucket, folder, dstDir); err != nil { log.Printf("failed to download test artifacts from folder %s in bucket %s to %s: %v\n", folder, bucket, dstDir, err) } }(bkt, gcsSubfolder, *writeLocalArtifacts) } wg.Wait() } bytes, err := xml.MarshalIndent(suites, "", "\t") if err != nil { log.Fatalf("failed to marshall result: %v", err) } bytes = []byte(fmt.Sprintf("%s%s", xml.Header, bytes)) var outFile *os.File if artifacts := os.Getenv("ARTIFACTS"); artifacts != "" { outFile, err = os.Create(artifacts + "/junit.xml") } else { outFile, err = os.Create(*outPath) } if err != nil { log.Fatalf("failed to create output file: %v", err) } defer outFile.Close() outFile.Write(bytes) outFile.Write([]byte{'\n'}) fmt.Printf("%s\n", bytes) if *setExitStatus && (suites.Errors != 0 || suites.Failures != 0) { log.Fatalf("test suite has error or failure") } } func downloadFolder(ctx context.Context, client *storage.Client, bucket, folder, dstDir string) error { // Create the destination directory if it doesn't exist. if err := os.MkdirAll(dstDir, 0755); err != nil { return err } // List all objects in the folder. query := &storage.Query{ Prefix: folder, } objs := client.Bucket(bucket).Objects(ctx, query) // Download each object to the destination directory. for { obj, err := objs.Next() if err == iterator.Done { break } if err != nil { return err } dstFile := filepath.Join(dstDir, obj.Name) if strings.Contains(dstFile, "/sources/") { continue } // Remote path might contain subfolders, create them locally too. fileDstDir := filepath.Dir(dstFile) if err := os.MkdirAll(fileDstDir, 0755); err != nil { log.Printf("failed to create %s: %v", fileDstDir, err) continue } file, err := os.Create(dstFile) if err != nil { log.Printf("failed to write %s: %v", dstFile, err) continue } objReader, err := client.Bucket(bucket).Object(obj.Name).NewReader(ctx) if err != nil { log.Printf("failed to make reader for %s: %v", obj.Name, err) continue } if _, err := io.Copy(file, objReader); err != nil { log.Printf("failed to copy %s to disk: %v", obj.Name, err) continue } if err := objReader.Close(); err != nil { log.Printf("failed to close %s reader: %v", obj.Name, err) continue } } return nil }