blob: 16e826a371a9aa5982f8c18ffd6ec6d1276c073a [file] [log] [blame]
// Copyright 2018 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 main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"text/template"
"cos.googlesource.com/cos/tools.git/src/pkg/config"
"cos.googlesource.com/cos/tools.git/src/pkg/fs"
"cos.googlesource.com/cos/tools.git/src/pkg/provisioner"
"cos.googlesource.com/cos/tools.git/src/pkg/utils"
"cloud.google.com/go/storage"
"github.com/google/subcommands"
"google.golang.org/api/iterator"
)
const (
gpuScript = "install_gpu.sh"
installerContainer = "gcr.io/cos-cloud/cos-gpu-installer:v20220608"
)
// TODO(b/121332360): Move most GPU functionality to cos-gpu-installer
var (
validGPUs = []string{"nvidia-tesla-k80", "nvidia-tesla-p100", "nvidia-tesla-v100"}
)
// InstallGPU implements subcommands.Command for the "install-gpu" command.
// This command configures the current image build process to customize the result image
// with GPU drivers.
type InstallGPU struct {
NvidiaDriverVersion string
NvidiaDriverMd5sum string
NvidiaInstallDirHost string
gpuType string
getValidDrivers bool
gpuDataDir string
}
// Name implements subcommands.Command.Name.
func (*InstallGPU) Name() string {
return "install-gpu"
}
// Synopsis implements subcommands.Command.Synopsis.
func (*InstallGPU) Synopsis() string {
return "Configure the image build with GPU drivers."
}
// Usage implements subcommands.Command.Usage.
func (*InstallGPU) Usage() string {
return `install-gpu [flags]
`
}
// SetFlags implements subcommands.Command.SetFlags.
func (i *InstallGPU) SetFlags(f *flag.FlagSet) {
f.StringVar(&i.NvidiaDriverVersion, "version", "", "Driver version to install. Can also be the name of an nvidia installer present in the "+
"directory specified by '-deps-dir'; e.g., NVIDIA-Linux-x86_64-450.51.06.run.")
f.StringVar(&i.NvidiaDriverMd5sum, "md5sum", "", "Md5sum of the driver to install.")
f.StringVar(&i.NvidiaInstallDirHost, "install-dir", "/var/lib/nvidia",
"Location to install drivers on the image.")
f.StringVar(
&i.gpuType, "gpu-type", "nvidia-tesla-p100",
fmt.Sprintf("The type of GPU to verify drivers for. Must be one of: %v", validGPUs))
f.BoolVar(
&i.getValidDrivers, "get-valid-drivers", false,
"Print the list of supported GPU driver versions. If this flag is given, no other actions will be taken.")
f.StringVar(&i.gpuDataDir, "deps-dir", "", "If provided, the local directory to search for cos-gpu-installer data dependencies. "+
"The exact data dependencies that must be present in this directory depends on the version of cos-gpu-installer "+
"used by cos-customizer. Do not expect this flag to be stable; it exists for compatibility with pre-release COS images.")
}
func validDriverVersions(ctx context.Context, gcsClient *storage.Client) (map[string]bool, error) {
// We gather the set of valid drivers from the set of drivers provided by Nvidia in their GCS bucket.
// Nominally, paths we care about in this bucket look like 'tesla/<version>/<binaries>'. Version 390.46 has
// a deprecated path structure, and since it's supported by cos-gpu-installer, we special case that here.
validDrivers := map[string]bool{"390.46": true}
query := &storage.Query{Prefix: "tesla/"}
it := gcsClient.Bucket("nvidia-drivers-us-public").Objects(ctx, query)
for {
objAttrs, err := it.Next()
if err == iterator.Done {
break
}
if err != nil {
return nil, err
}
// Example object: tesla/396.26/NVIDIA-Linux-x86_64-396.26-diagnostic.run
if splitPath := strings.SplitN(objAttrs.Name, "/", 3); len(splitPath) > 1 {
validDrivers[splitPath[1]] = true
}
}
return validDrivers, nil
}
func (i *InstallGPU) validate(ctx context.Context, gcsClient *storage.Client, files *fs.Files, provConfig *provisioner.Config) error {
isValidGPU := false
for _, g := range validGPUs {
if i.gpuType == g {
isValidGPU = true
break
}
}
if !isValidGPU {
return fmt.Errorf("%q is an invalid GPU type. Must be one of: %v", i.gpuType, validGPUs)
}
if i.NvidiaDriverVersion == "" {
return fmt.Errorf("version must be set")
}
var gpuAlreadyConf bool
for _, s := range provConfig.Steps {
if s.Type == "InstallGPU" {
gpuAlreadyConf = true
break
}
}
if gpuAlreadyConf {
return fmt.Errorf("install-gpu can only be invoked once in an image build process. Only one driver version can be installed on the image")
}
if strings.HasSuffix(i.NvidiaDriverVersion, ".run") {
log.Printf("driver version is set to %q, which looks like an nvidia installer file", i.NvidiaDriverVersion)
if i.gpuDataDir == "" {
return errors.New(`"-deps-dir" must be set when the version is specified as an nvidia installer file`)
}
fileName := filepath.Join(i.gpuDataDir, i.NvidiaDriverVersion)
info, err := os.Stat(fileName)
if os.IsNotExist(err) {
return fmt.Errorf("nvidia installer file at %q does not exist", fileName)
}
if info.IsDir() {
return fmt.Errorf("nvidia installer file at %q is a directory", fileName)
}
} else {
validDrivers, err := validDriverVersions(ctx, gcsClient)
if err != nil {
return err
}
if !validDrivers[i.NvidiaDriverVersion] {
var drivers []string
for d := range validDrivers {
drivers = append(drivers, d)
}
return fmt.Errorf("driver version %s is not valid; valid driver versions are: %v", i.NvidiaDriverVersion, drivers)
}
}
return nil
}
func (i *InstallGPU) templateScript(scriptPath string) error {
setCOSDownloadGCS := ""
if i.gpuDataDir != "" {
setCOSDownloadGCS = "true"
}
data := struct {
NvidiaDriverVersion string
NvidiaDriverMd5sum string
NvidiaInstallDirHost string
SetCOSDownloadGCS string
}{
NvidiaDriverVersion: utils.QuoteForShell(i.NvidiaDriverVersion),
NvidiaDriverMd5sum: utils.QuoteForShell(i.NvidiaDriverMd5sum),
NvidiaInstallDirHost: utils.QuoteForShell(i.NvidiaInstallDirHost),
SetCOSDownloadGCS: utils.QuoteForShell(setCOSDownloadGCS),
}
tmpl, err := template.New(filepath.Base(scriptPath)).ParseFiles(scriptPath)
if err != nil {
return err
}
w, err := os.Create(scriptPath)
if err != nil {
return err
}
defer w.Close()
return tmpl.Execute(w, data)
}
func (i *InstallGPU) updateBuildConfig(configPath string) error {
buildConfig := &config.Build{}
configFile, err := os.OpenFile(configPath, os.O_RDWR, 0666)
if err != nil {
return err
}
defer configFile.Close()
if err := config.Load(configFile, buildConfig); err != nil {
return err
}
buildConfig.GPUType = i.gpuType
if i.gpuDataDir != "" {
files, err := ioutil.ReadDir(i.gpuDataDir)
if err != nil {
return fmt.Errorf("error reading dir %q: %v", i.gpuDataDir, err)
}
for _, f := range files {
if f.Mode().IsRegular() {
buildConfig.GCSFiles = append(buildConfig.GCSFiles, filepath.Join(i.gpuDataDir, f.Name()))
}
}
}
if _, err := configFile.Seek(0, 0); err != nil {
return err
}
return config.SaveConfigToFile(configFile, buildConfig)
}
func (i *InstallGPU) updateProvConfig(provConfig *provisioner.Config) error {
buf, err := json.Marshal(&provisioner.InstallGPUStep{
NvidiaDriverVersion: i.NvidiaDriverVersion,
NvidiaDriverMD5Sum: i.NvidiaDriverMd5sum,
NvidiaInstallDirHost: i.NvidiaInstallDirHost,
NvidiaInstallerContainer: installerContainer,
// GCSDepsPrefix will be converted into a gs:// address by the preloader
// package.
GCSDepsPrefix: i.gpuDataDir,
})
if err != nil {
return err
}
provConfig.Steps = append(provConfig.Steps, provisioner.StepConfig{
Type: "InstallGPU",
Args: json.RawMessage(buf),
})
return nil
}
// Execute implements subcommands.Command.Execute. It configures the current image build process to
// customize the result image with GPU drivers.
func (i *InstallGPU) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
if f.NArg() != 0 {
f.Usage()
return subcommands.ExitUsageError
}
if len(args) < 2 {
log.Panic("InstallGPU expects two arguments; *fs.Files and ServiceClients")
}
files, ok := args[0].(*fs.Files)
if !ok {
log.Panic("InstallGPU expects two arguments; *fs.Files and ServiceClients")
}
serviceClients, ok := args[1].(ServiceClients)
if !ok {
log.Panic("InstallGPU expects two arguments; *fs.Files and ServiceClients")
}
_, gcsClient, err := serviceClients(ctx, false)
if err != nil {
log.Println(err)
return subcommands.ExitFailure
}
defer gcsClient.Close()
if i.getValidDrivers {
validDrivers, err := validDriverVersions(ctx, gcsClient)
if err != nil {
log.Println(err)
return subcommands.ExitFailure
}
var drivers []string
for d := range validDrivers {
drivers = append(drivers, d)
}
log.Printf("Valid driver versions are: %v\n", drivers)
return subcommands.ExitSuccess
}
var provConfig provisioner.Config
if err := config.LoadFromFile(files.ProvConfig, &provConfig); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := i.validate(ctx, gcsClient, files, &provConfig); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := i.updateBuildConfig(files.BuildConfig); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := i.updateProvConfig(&provConfig); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
if err := config.SaveConfigToPath(files.ProvConfig, &provConfig); err != nil {
log.Println(err)
return subcommands.ExitFailure
}
return subcommands.ExitSuccess
}