| // Copyright 2025 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. | |
| package preloader | |
| import ( | |
| "context" | |
| "crypto/md5" | |
| "fmt" | |
| "io" | |
| "io/fs" | |
| "log" | |
| "net/http" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "cos.googlesource.com/cos/tools.git/src/pkg/config" | |
| "cos.googlesource.com/cos/tools.git/src/pkg/cos" | |
| cosfs "cos.googlesource.com/cos/tools.git/src/pkg/fs" | |
| "cos.googlesource.com/cos/tools.git/src/pkg/gpubuild" | |
| "cos.googlesource.com/cos/tools.git/src/pkg/gpuconfig" | |
| "cos.googlesource.com/cos/tools.git/src/pkg/gpuconfig/pb" | |
| "cos.googlesource.com/cos/tools.git/src/pkg/utils" | |
| "cloud.google.com/go/storage" | |
| "google.golang.org/protobuf/proto" | |
| ) | |
| func extractTarball(srcPath, destDir string) error { | |
| cmd := exec.Command("tar", "xf", srcPath, "-C", destDir) | |
| cmd.Stdout = os.Stdout | |
| cmd.Stderr = os.Stderr | |
| return cmd.Run() | |
| } | |
| type envReaderCreator func(context.Context) (*cos.EnvReader, error) | |
| // cachedEnvReader returns a closure that creates an EnvReader from a VMImage. | |
| // Since creating an EnvReader is very expensive in this context (it boots a VM | |
| // from the image to read data from it), we use a closure to cache the resulting | |
| // envReader. | |
| // | |
| // We could create the EnvReader once and pass it around everywhere. However, | |
| // under specific conditions, the EnvReader won't need to be created at all, | |
| // so this closure allows us to lazily create the EnvReader when it is needed. | |
| func cachedEnvReader(input *config.Image, gcs *gcsManager, files *cosfs.Files, buildSpec *config.Build) envReaderCreator { | |
| var envReader *cos.EnvReader | |
| return func(ctx context.Context) (*cos.EnvReader, error) { | |
| var err error | |
| if envReader != nil { | |
| return envReader, nil | |
| } | |
| envReader, err = cos.NewEnvReaderFromVMImage(input.Name, input.Project, cos.EnvReaderVMImageConfig{ | |
| DaisyBin: files.DaisyBin, | |
| DaisyWorkflow: "/data/read_cos_env.wf.json", | |
| Project: buildSpec.Project, | |
| Zone: buildSpec.Zone, | |
| GCSDir: gcs.managedDirURL(), | |
| DiskType: buildSpec.DiskType, | |
| MachineType: buildSpec.MachineType, | |
| Network: buildSpec.Network, | |
| SubNet: buildSpec.Subnet, | |
| ServiceAccount: buildSpec.ServiceAccount, | |
| }) | |
| if err != nil { | |
| return nil, fmt.Errorf("failed to read env from VM image: %v", err) | |
| } | |
| return envReader, nil | |
| } | |
| } | |
| func installToolchain(ctx context.Context, envReaderFunc envReaderCreator, gcsClient *storage.Client, buildSpec *config.Build, toolchainDir string) error { | |
| if buildSpec.Toolchain != nil { | |
| log.Println("Extracting toolchain package") | |
| if err := extractTarball(buildSpec.Toolchain.ToolchainTarPath, toolchainDir); err != nil { | |
| return err | |
| } | |
| if err := extractTarball(buildSpec.Toolchain.KernelHeadersPath, toolchainDir); err != nil { | |
| return err | |
| } | |
| return nil | |
| } | |
| log.Println("No toolchain package provided; getting toolchain from input image") | |
| envReader, err := envReaderFunc(ctx) | |
| if err != nil { | |
| return err | |
| } | |
| downloader := cos.NewGCSDownloader(gcsClient, envReader, "", "", "", "", false) | |
| if err := cos.InstallCrossToolchain(ctx, downloader, toolchainDir); err != nil { | |
| return err | |
| } | |
| // InstallCrossToolchain has an undesirable side effect of setting the SYSROOT | |
| // environment variable. It's unclear if removing this behavior from | |
| // InstallCrossToolchain is safe for all usages. | |
| if err := os.Unsetenv("SYSROOT"); err != nil { | |
| log.Printf("WARNING: unable to unset SYSROOT: %v", err) | |
| } | |
| return nil | |
| } | |
| func getArchFromToolchain(toolchainDir string) (string, error) { | |
| x86Matches, err := filepath.Glob(filepath.Join(toolchainDir, "bin/x86_64-cros-linux-gnu-*")) | |
| if err != nil { | |
| return "", err | |
| } | |
| if len(x86Matches) != 0 { | |
| return "x86_64", nil | |
| } | |
| aarch64Matches, err := filepath.Glob(filepath.Join(toolchainDir, "bin/aarch64-cros-linux-gnu-*")) | |
| if err != nil { | |
| return "", err | |
| } | |
| if len(aarch64Matches) != 0 { | |
| return "aarch64", nil | |
| } | |
| return "", fmt.Errorf("could not identify arch from toolchain at %q; are programs missing from the toolchain?") | |
| } | |
| func downloadRunfile(version, arch, md5Sum, outDir string) (_ string, err error) { | |
| outName := fmt.Sprintf("NVIDIA-Linux-%s-%s.run", arch, version) | |
| url := fmt.Sprintf("https://us.download.nvidia.com/tesla/%s/%s", version, outName) | |
| log.Printf("Downloading runfile from %q", url) | |
| outPath := filepath.Join(outDir, outName) | |
| outFile, err := os.Create(outPath) | |
| if err != nil { | |
| return "", err | |
| } | |
| defer utils.CheckClose(outFile, "error closing downloaded runfile", &err) | |
| resp, err := http.Get(url) | |
| if err != nil { | |
| return "", err | |
| } | |
| defer utils.CheckClose(resp.Body, "error closing runfile HTTP body", &err) | |
| if resp.StatusCode != http.StatusOK { | |
| return "", fmt.Errorf("unhealthy status downloading Nvidia runfile: %v", resp.Status) | |
| } | |
| if md5Sum != "" { | |
| h := md5.New() | |
| mw := io.MultiWriter(outFile, h) | |
| if _, err := io.Copy(mw, resp.Body); err != nil { | |
| return "", err | |
| } | |
| got := fmt.Sprintf("%x", h.Sum(nil)) | |
| if got != md5Sum { | |
| return "", fmt.Errorf("md5sum mismatch when downloading runfile from %q; got %q, want %q", url, got, md5Sum) | |
| } | |
| } else { | |
| if _, err := io.Copy(outFile, resp.Body); err != nil { | |
| return "", err | |
| } | |
| } | |
| return outPath, nil | |
| } | |
| func listModules(dir string) ([]string, error) { | |
| var result []string | |
| if err := filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { | |
| if err != nil { | |
| return err | |
| } | |
| if filepath.Ext(path) == ".ko" { | |
| result = append(result, filepath.Base(path)) | |
| } | |
| return nil | |
| }); err != nil { | |
| return nil, err | |
| } | |
| return result, nil | |
| } | |
| func setDiff(a, b []string) []string { | |
| aMap := map[string]bool{} | |
| for _, e := range a { | |
| aMap[e] = true | |
| } | |
| for _, e := range b { | |
| if _, ok := aMap[e]; ok { | |
| delete(aMap, e) | |
| } | |
| } | |
| var result []string | |
| for k, _ := range aMap { | |
| result = append(result, k) | |
| } | |
| return result | |
| } | |
| func mergeDriverPackages(generatedDriver *gpubuild.DriverPackage, preBuiltPath, outDir string) (*gpubuild.DriverPackage, error) { | |
| log.Println("Merging any missing precompiled modules from precompiled package into custom package") | |
| nvidiaGenerated, err := os.MkdirTemp("", "nvidia-generated") | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer os.RemoveAll(nvidiaGenerated) | |
| if err := extractTarball(generatedDriver.Path, nvidiaGenerated); err != nil { | |
| return nil, err | |
| } | |
| nvidiaPreBuilt, err := os.MkdirTemp("", "prebuilt-generated") | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer os.RemoveAll(nvidiaPreBuilt) | |
| if err := extractTarball(preBuiltPath, nvidiaPreBuilt); err != nil { | |
| return nil, err | |
| } | |
| generatedModules, err := listModules(nvidiaGenerated) | |
| if err != nil { | |
| return nil, err | |
| } | |
| log.Printf("Generated package includes: %v", generatedModules) | |
| preBuiltModules, err := listModules(nvidiaPreBuilt) | |
| if err != nil { | |
| return nil, err | |
| } | |
| log.Printf("Prebuilt package includes: %v", preBuiltModules) | |
| notGeneratedModules := setDiff(preBuiltModules, generatedModules) | |
| log.Printf("Adding the following to generated package: %v", notGeneratedModules) | |
| for _, module := range notGeneratedModules { | |
| src := filepath.Join(nvidiaPreBuilt, "drivers", module) | |
| dst := filepath.Join(nvidiaGenerated, "drivers", module) | |
| if err := utils.CopyFile(src, dst); err != nil { | |
| return nil, err | |
| } | |
| } | |
| result := &gpubuild.DriverPackage{ | |
| Path: filepath.Join(outDir, fmt.Sprintf("nvidia-drivers-%s.tgz", generatedDriver.Version)), | |
| Version: generatedDriver.Version, | |
| } | |
| if err := cosfs.TarDir(nvidiaGenerated, result.Path); err != nil { | |
| return nil, err | |
| } | |
| return result, nil | |
| } | |
| // installNonRunfileModules searches for kernel modules in any existing | |
| // precompiled packages that are not present in the runfile we just compiled. If | |
| // it finds anything, it adds it to the generated package we just compiled. This | |
| // helps with supporting gdrcopy. | |
| func installNonRunfileModules(ctx context.Context, envReaderFunc envReaderCreator, gcsClient *storage.Client, gpuConfig *config.GPUConfig, generatedDriver *gpubuild.DriverPackage, outDir string) (*gpubuild.DriverPackage, error) { | |
| if gpuConfig.PrePackagedDriversPath != "" { | |
| return mergeDriverPackages(generatedDriver, gpuConfig.PrePackagedDriversPath, outDir) | |
| } | |
| log.Println("No precompiled driver package provided, looking for an existing driver package for the given VM image") | |
| envReader, err := envReaderFunc(ctx) | |
| if err != nil { | |
| return nil, err | |
| } | |
| downloader := cos.NewGCSDownloader(gcsClient, envReader, "", "", "", "", false) | |
| precompiled := fmt.Sprintf("nvidia-drivers-%s.tgz", generatedDriver.Version) | |
| if exists, err := downloader.ArtifactExists(ctx, precompiled); err != nil || !exists { | |
| if err != nil { | |
| log.Printf("WARNING: error searching for non-runfile modules for version %q: %v", generatedDriver.Version, err) | |
| } | |
| log.Printf("WARNING: could not find non-runfile modules for version %q, will not install them", generatedDriver.Version) | |
| return generatedDriver, nil | |
| } | |
| tmpDir, err := os.MkdirTemp("", "non-runfile-modules") | |
| if err != nil { | |
| return nil, err | |
| } | |
| defer os.RemoveAll(tmpDir) | |
| if err := downloader.DownloadArtifact(ctx, tmpDir, precompiled); err != nil { | |
| return nil, err | |
| } | |
| precompiledPath := filepath.Join(tmpDir, precompiled) | |
| return mergeDriverPackages(generatedDriver, precompiledPath, outDir) | |
| } | |
| func downloadImexDriver(ctx context.Context, envReaderFunc envReaderCreator, gcsClient *storage.Client, generatedDriver *gpubuild.DriverPackage, arch, outDir string) (string, error) { | |
| if arch != "aarch64" { | |
| log.Printf("Arch is %s, not searching for IMEX drivers", arch) | |
| return "", nil | |
| } | |
| log.Println("Searching for IMEX driver to install") | |
| envReader, err := envReaderFunc(ctx) | |
| if err != nil { | |
| return "", err | |
| } | |
| downloader := cos.NewGCSDownloader(gcsClient, envReader, "", "", "", "", false) | |
| result, err := downloader.DownloadImexDriver(ctx, outDir, generatedDriver.Version) | |
| if err != nil { | |
| log.Printf("WARNING: could not download IMEX driver for %s", generatedDriver.Version) | |
| return "", nil | |
| } | |
| return result, nil | |
| } | |
| func updateVersionInfo(versionsProto []byte, driverVersion, outDir string) (string, error) { | |
| driverVersionInfo, err := gpuconfig.ParseGPUDriverVersionInfoList(versionsProto) | |
| if err != nil { | |
| return "", err | |
| } | |
| for _, gpuVersionInfo := range driverVersionInfo.GetGpuDriverVersionInfo() { | |
| found := false | |
| for _, v := range gpuVersionInfo.SupportedDriverVersions { | |
| if v.Version == driverVersion { | |
| found = true | |
| break | |
| } | |
| } | |
| if !found { | |
| gpuVersionInfo.SupportedDriverVersions = append(gpuVersionInfo.SupportedDriverVersions, &pb.DriverVersion{Version: driverVersion}) | |
| } | |
| } | |
| resultBytes, err := proto.Marshal(driverVersionInfo) | |
| if err != nil { | |
| return "", err | |
| } | |
| result := filepath.Join(outDir, "gpu_driver_versions.bin") | |
| if err := os.WriteFile(result, resultBytes, 0644); err != nil { | |
| return "", err | |
| } | |
| return result, nil | |
| } | |
| // installDriverVersionsProto adds the version we just compiled to the | |
| // gpu_driver_versions.bin associated with the input image. For each GPU type, | |
| // add the version to it, if the version is not present at all for that GPU | |
| // type. | |
| // | |
| // Some older COS images do not have a gpu_driver_versions.bin, and those | |
| // versions do not require one. Do not install anything in this case and return | |
| // an empty string. | |
| func installDriverVersionsProto(ctx context.Context, envReaderFunc envReaderCreator, gcsClient *storage.Client, gpuConfig *config.GPUConfig, driverVersion, outDir string) (string, error) { | |
| if gpuConfig.VersionsProtoPath != "" { | |
| log.Println("Using provided gpu_driver_versions.bin") | |
| versionsProto, err := os.ReadFile(gpuConfig.VersionsProtoPath) | |
| if err != nil { | |
| return "", err | |
| } | |
| return updateVersionInfo(versionsProto, driverVersion, outDir) | |
| } | |
| log.Println("No provided gpu_driver_versions.bin, looking for the one associated with the input image") | |
| envReader, err := envReaderFunc(ctx) | |
| if err != nil { | |
| return "", err | |
| } | |
| downloader := cos.NewGCSDownloader(gcsClient, envReader, "", "", "", "", false) | |
| if exists, err := downloader.ArtifactExists(ctx, "gpu_driver_versions.bin"); err != nil || !exists { | |
| if err != nil { | |
| log.Printf("WARNING: error searching for gpu_driver_versions.bin for input COS image: %v", err) | |
| } | |
| log.Println("WARNING: could not find gpu_driver_versions.bin for input image, will not provide one to cos-gpu-installer") | |
| return "", nil | |
| } | |
| tmpDir, err := os.MkdirTemp("", "image-driver-versions") | |
| if err != nil { | |
| return "", err | |
| } | |
| defer os.RemoveAll(tmpDir) | |
| if err := downloader.DownloadArtifact(ctx, tmpDir, "gpu_driver_versions.bin"); err != nil { | |
| return "", err | |
| } | |
| versionsProto, err := os.ReadFile(filepath.Join(tmpDir, "gpu_driver_versions.bin")) | |
| if err != nil { | |
| return "", err | |
| } | |
| return updateVersionInfo(versionsProto, driverVersion, outDir) | |
| } | |
| func buildGPUPackage(ctx context.Context, input *config.Image, gcs *gcsManager, files *cosfs.Files, buildSpec *config.Build, workDir string) error { | |
| log.Println("Building GPU package") | |
| envReaderFunc := cachedEnvReader(input, gcs, files, buildSpec) | |
| toolchainDir := filepath.Join(workDir, "toolchain") | |
| if err := os.Mkdir(toolchainDir, 0755); err != nil { | |
| return err | |
| } | |
| if err := installToolchain(ctx, envReaderFunc, gcs.gcsClient, buildSpec, toolchainDir); err != nil { | |
| return fmt.Errorf("unable to install toolchain: %v", err) | |
| } | |
| arch, err := getArchFromToolchain(toolchainDir) | |
| if err != nil { | |
| return err | |
| } | |
| var runfile string | |
| if filepath.Ext(buildSpec.GPUConfig.Version) == ".run" { | |
| runfile = buildSpec.GPUConfig.Version | |
| } else { | |
| runfile, err = downloadRunfile(buildSpec.GPUConfig.Version, arch, buildSpec.GPUConfig.MD5Sum, workDir) | |
| if err != nil { | |
| return err | |
| } | |
| } | |
| if err := os.Chmod(runfile, 0755); err != nil { | |
| return err | |
| } | |
| driverPackage, err := gpubuild.Compile(runfile, toolchainDir, workDir) | |
| if err != nil { | |
| return err | |
| } | |
| mergedDir := filepath.Join(workDir, "merged") | |
| if err := os.Mkdir(mergedDir, 0755); err != nil { | |
| return err | |
| } | |
| mergedPackage, err := installNonRunfileModules(ctx, envReaderFunc, gcs.gcsClient, buildSpec.GPUConfig, driverPackage, mergedDir) | |
| if err != nil { | |
| return err | |
| } | |
| imexPath, err := downloadImexDriver(ctx, envReaderFunc, gcs.gcsClient, driverPackage, arch, mergedDir) | |
| if err != nil { | |
| return err | |
| } | |
| generatedProtoPath, err := installDriverVersionsProto(ctx, envReaderFunc, gcs.gcsClient, buildSpec.GPUConfig, driverPackage.Version, mergedDir) | |
| if err != nil { | |
| return err | |
| } | |
| buildSpec.GCSFiles = append(buildSpec.GCSFiles, runfile, mergedPackage.Path) | |
| if imexPath != "" { | |
| buildSpec.GCSFiles = append(buildSpec.GCSFiles, imexPath) | |
| } | |
| if generatedProtoPath != "" { | |
| buildSpec.GCSFiles = append(buildSpec.GCSFiles, generatedProtoPath) | |
| } | |
| buildSpec.GPUConfig.Version = driverPackage.Version | |
| return nil | |
| } |