| // Copyright 2021 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 utils provides utility functions. | |
| package utils | |
| import ( | |
| "archive/tar" | |
| "bufio" | |
| "crypto/sha256" | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "io/ioutil" | |
| "log" | |
| "net/http" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "strings" | |
| "syscall" | |
| "time" | |
| "github.com/golang/glog" | |
| "github.com/pkg/errors" | |
| ) | |
| var ( | |
| downloadRetries = 3 | |
| lockFile = "/root/tmp/cos_gpu_installer_lock" | |
| ) | |
| type serviceAccountToken struct { | |
| Token string `json:"access_token"` | |
| Expire int `json:"expires_in"` | |
| TokenType string `json:"token_type"` | |
| } | |
| type listStorageObjectsResponse struct { | |
| Kind string `json:"kind"` | |
| Items []struct { | |
| Kind string `json:"kind"` | |
| ID string `json:"id"` | |
| SelfLink string `json:"selfLink"` | |
| MediaLink string `json:"mediaLink"` | |
| Name string `json:"name"` | |
| Bucket string `json:"bucket"` | |
| Generation string `json:"generation"` | |
| Metageneration string `json:"metageneration"` | |
| ContentType string `json:"contentType"` | |
| StorageClass string `json:"storageClass"` | |
| Size string `json:"size"` | |
| Md5Hash string `json:"md5Hash"` | |
| Crc32c string `json:"crc32c"` | |
| Etag string `json:"etag"` | |
| TimeCreated string `json:"timeCreated"` | |
| Updated string `json:"updated"` | |
| TimeStorageClassUpdated string `json:"timeStorageClassUpdated"` | |
| } `json:"items"` | |
| } | |
| // Flock exclusively locks a special file on the host to make sure only one calling process is running at any time. | |
| func Flock() *os.File { | |
| // TODO(mikewu): generalize Flock to make it useful for other use cases. | |
| f, err := os.OpenFile(lockFile, os.O_RDONLY|os.O_CREATE, 0666) | |
| if err != nil { | |
| glog.Exitf("Failed to open lock file: %v", err) | |
| } | |
| if err := syscall.Flock(int(f.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil { | |
| glog.Exitf("File %s is locked. Other process might be running.", lockFile) | |
| } | |
| return f | |
| } | |
| // DownloadContentFromURL downloads file from a given URL. | |
| func DownloadContentFromURL(url, outputPath, infoStr string) error { | |
| url = strings.TrimSpace(url) | |
| glog.Infof("Downloading %s from %s", infoStr, url) | |
| req, err := http.NewRequest("GET", url, nil) | |
| if err != nil { | |
| return errors.Wrapf(err, "failed to download %s from %s", infoStr, url) | |
| } | |
| // TODO(mikewu): Consider using GCS GO package. | |
| if strings.HasPrefix(url, "https://storage.googleapis.com") { | |
| // TODO(mikewu): Consider using sgauth (https://github.com/google/oauth2l/tree/master/sgauth). | |
| token, err := GetDefaultVMToken() | |
| if err != nil { | |
| return errors.Wrap(err, "failed to get VM token") | |
| } | |
| req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) | |
| } | |
| outputFile, err := os.Create(outputPath) | |
| if err != nil { | |
| return errors.Wrapf(err, "failed to create file %s", outputPath) | |
| } | |
| defer outputFile.Close() | |
| client := &http.Client{} | |
| var response *http.Response | |
| retries := downloadRetries | |
| for retries > 0 { | |
| response, err = client.Do(req) | |
| if err != nil { | |
| glog.Errorf("Failed to download %s: %v", infoStr, err) | |
| retries-- | |
| time.Sleep(time.Second) | |
| glog.V(2).Info("Retry...") | |
| } else { | |
| break | |
| } | |
| } | |
| if response == nil { | |
| return errors.Wrapf(err, "failed to download %s", infoStr) | |
| } | |
| defer response.Body.Close() | |
| if response.StatusCode != 200 { | |
| return errors.Errorf("failed to download %s, status: %s", infoStr, response.Status) | |
| } | |
| if _, err := io.Copy(outputFile, response.Body); err != nil { | |
| return errors.Wrapf(err, "failed to download %s", infoStr) | |
| } | |
| glog.V(2).Infof("Successfully downloaded %s from %s", infoStr, url) | |
| return nil | |
| } | |
| // DownloadFromGCS downloads an object from the given GCS path. | |
| func DownloadFromGCS(destDir, gcsBucket, gcsPath string) error { | |
| downloadURL := fmt.Sprintf("https://storage.googleapis.com/%s/%s", gcsBucket, gcsPath) | |
| filename := filepath.Base(gcsPath) | |
| outputPath := filepath.Join(destDir, filename) | |
| return DownloadContentFromURL(downloadURL, outputPath, filename) | |
| } | |
| // ListGCSBucket lists the objects whose names begin with the given prefix in the given GCS bucket. | |
| func ListGCSBucket(bucket, prefix string) ([]string, error) { | |
| glog.V(2).Infof("Listing objects from GCS bucket %s with prefix %s", bucket, prefix) | |
| url := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o?prefix=%s", bucket, prefix) | |
| dir, err := ioutil.TempDir("", "bucketlist") | |
| if err != nil { | |
| return nil, errors.Wrap(err, "failed to create tempdir") | |
| } | |
| defer os.RemoveAll(dir) | |
| tmpfile := filepath.Join(dir, "bucketlist") | |
| if err := DownloadContentFromURL(url, tmpfile, "bucketlist"); err != nil { | |
| return nil, errors.Wrapf(err, "failed to downoad url %s", url) | |
| } | |
| content, err := ioutil.ReadFile(tmpfile) | |
| if err != nil { | |
| return nil, errors.Wrapf(err, "failed to read file %s", tmpfile) | |
| } | |
| var jsonContent listStorageObjectsResponse | |
| if err := json.Unmarshal(content, &jsonContent); err != nil { | |
| return nil, errors.Wrapf(err, "failed to parse json string %s", string(content)) | |
| } | |
| var objects []string | |
| for _, item := range jsonContent.Items { | |
| objects = append(objects, item.Name) | |
| } | |
| return objects, nil | |
| } | |
| // GetDefaultVMToken returns the default GCE service account of the COS VM the program is running on. | |
| func GetDefaultVMToken() (string, error) { | |
| tokenStr, err := GetGCEMetadata("service-accounts/default/token") | |
| if err != nil { | |
| return "", errors.Wrap(err, "failed to get default VM token") | |
| } | |
| token, err := parseVMToken(tokenStr) | |
| if err != nil { | |
| return "", errors.Wrap(err, "failed to parse VM token") | |
| } | |
| return token.Token, nil | |
| } | |
| // GetGCEMetadata queries GCE metadata server to get the value of a given metadata key. | |
| func GetGCEMetadata(metadataPath string) (string, error) { | |
| req, err := http.NewRequest("GET", "http://metadata.google.internal/computeMetadata/v1/instance/"+metadataPath, nil) | |
| if err != nil { | |
| return "", errors.Wrap(err, "failed to get GCE metadata") | |
| } | |
| req.Header.Add("Metadata-Flavor", "Google") | |
| client := &http.Client{} | |
| resp, err := client.Do(req) | |
| if err != nil { | |
| return "", errors.Wrap(err, "failed to get GCE metadata") | |
| } | |
| defer resp.Body.Close() | |
| body, err := ioutil.ReadAll(resp.Body) | |
| if err != nil { | |
| return "", errors.Wrap(err, "failed to get GCE metadata") | |
| } | |
| return string(body), nil | |
| } | |
| // IsDirEmpty returns whether a given directory is empty. | |
| func IsDirEmpty(dirName string) (bool, error) { | |
| dir, err := os.Open(dirName) | |
| if err != nil { | |
| return false, err | |
| } | |
| defer dir.Close() | |
| _, err = dir.Readdir(1) | |
| if err == io.EOF { | |
| return true, nil | |
| } | |
| return false, err | |
| } | |
| func loadEnvFromReader(reader io.Reader) (map[string]string, error) { | |
| envs := make(map[string]string) | |
| scanner := bufio.NewScanner(reader) | |
| for scanner.Scan() { | |
| line := scanner.Text() | |
| trimmedLine := strings.TrimSpace(line) | |
| parts := strings.SplitN(trimmedLine, "=", 2) | |
| if len(parts) != 2 { | |
| return nil, fmt.Errorf("Unrecognized format: %s", trimmedLine) | |
| } | |
| envs[parts[0]] = strings.Trim(parts[1], `"'`) | |
| } | |
| return envs, nil | |
| } | |
| // LoadEnvFromFile reads an env file from fs into memory as a map. | |
| func LoadEnvFromFile(prefix, filePath string) (map[string]string, error) { | |
| path := filepath.Join(prefix, filePath) | |
| f, err := os.Open(path) | |
| if err != nil { | |
| return nil, errors.Wrapf(err, "failed to read file %s", path) | |
| } | |
| defer f.Close() | |
| return loadEnvFromReader(f) | |
| } | |
| func LoadEnvFromString(s string) (map[string]string, error) { | |
| return loadEnvFromReader(strings.NewReader(s)) | |
| } | |
| // CreateTarFile creates a tar archive file given a map of {filename: content}. | |
| func CreateTarFile(tarFilename string, files map[string][]byte) error { | |
| tarFile, err := os.Create(tarFilename) | |
| if err != nil { | |
| return err | |
| } | |
| defer tarFile.Close() | |
| tw := tar.NewWriter(tarFile) | |
| defer tw.Close() | |
| for name, body := range files { | |
| header := &tar.Header{ | |
| Name: name, | |
| Mode: 0644, | |
| Size: int64(len(body)), | |
| } | |
| if err := tw.WriteHeader(header); err != nil { | |
| return err | |
| } | |
| if _, err := tw.Write(body); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| // RunCommandAndLogOutput runs the given command and logs the stdout and stderr in parallel. | |
| func RunCommandAndLogOutput(cmd *exec.Cmd, expectError bool) error { | |
| errLogger := glog.Error | |
| if expectError { | |
| errLogger = glog.V(2).Info | |
| } | |
| cmd.Stdout = &loggingWriter{logger: glog.V(2).Info} | |
| cmd.Stderr = &loggingWriter{logger: errLogger} | |
| err := cmd.Run() | |
| if _, ok := err.(*exec.ExitError); ok && expectError { | |
| glog.Warningf("command %s didn't complete successfully: %v", cmd.Path, err) | |
| return nil | |
| } | |
| return err | |
| } | |
| // CopyFile copies a file from src to dest. | |
| func CopyFile(src, dest string) error { | |
| srcfile, err := os.Open(src) | |
| if err != nil { | |
| return errors.Wrapf(err, "failed to open file %s", src) | |
| } | |
| defer srcfile.Close() | |
| destfile, err := os.Create(dest) | |
| if err != nil { | |
| return errors.Wrapf(err, "failed to create file %s", dest) | |
| } | |
| defer destfile.Close() | |
| if _, err := io.Copy(destfile, srcfile); err != nil { | |
| return errors.Wrapf(err, "failed to copy file from %s to %s", dest, src) | |
| } | |
| if err := srcfile.Close(); err != nil { | |
| return errors.Wrapf(err, "failed to close source file %s", src) | |
| } | |
| if err := destfile.Close(); err != nil { | |
| return errors.Wrapf(err, "failed to close destination file %s", dest) | |
| } | |
| return nil | |
| } | |
| // MoveFile moves a file from src to dest. | |
| // Avoid to use os.Rename as the src and dst may on different filesystems, | |
| // e.g. (container temp fs -> host mounted volume). | |
| func MoveFile(src, dest string) error { | |
| if err := CopyFile(src, dest); err != nil { | |
| return errors.Wrapf(err, "failed to move file from %s to %s", src, dest) | |
| } | |
| if err := os.Remove(src); err != nil { | |
| return errors.Wrapf(err, "failed to remove file %s", src) | |
| } | |
| return nil | |
| } | |
| func parseVMToken(tokenStr string) (*serviceAccountToken, error) { | |
| var token serviceAccountToken | |
| if err := json.Unmarshal([]byte(tokenStr), &token); err != nil { | |
| return nil, err | |
| } | |
| return &token, nil | |
| } | |
| type loggingWriter struct { | |
| logger func(...interface{}) | |
| buf []byte | |
| } | |
| func (l *loggingWriter) Write(p []byte) (int, error) { | |
| for _, b := range p { | |
| if b == '\n' { | |
| l.logger(string(l.buf[:])) | |
| l.buf = nil | |
| continue | |
| } | |
| l.buf = append(l.buf, b) | |
| } | |
| return len(p), nil | |
| } | |
| // CheckClose closes an io.Closer and checks its error. Useful for checking the | |
| // errors on deferred Close() behaviors. | |
| func CheckClose(closer io.Closer, errMsgOnClose string, err *error) { | |
| if closeErr := closer.Close(); closeErr != nil { | |
| var fullErr error | |
| if errMsgOnClose != "" { | |
| fullErr = fmt.Errorf("%s: %v", errMsgOnClose, closeErr) | |
| } else { | |
| fullErr = closeErr | |
| } | |
| if *err == nil { | |
| *err = fullErr | |
| } else { | |
| log.Println(fullErr) | |
| } | |
| } | |
| } | |
| // RemoveDir removes the directory at inputPath and checks its error. Useful for checking the | |
| // errors on deferred remove(). | |
| func RemoveDir(inputPath string, errMsgOnRemove string, err *error) { | |
| if removeErr := os.RemoveAll(inputPath); removeErr != nil { | |
| var fullErr error | |
| if errMsgOnRemove != "" { | |
| fullErr = fmt.Errorf("%s: %v", errMsgOnRemove, fullErr) | |
| } else { | |
| fullErr = removeErr | |
| } | |
| if *err == nil { | |
| *err = fullErr | |
| } else { | |
| log.Println(fullErr) | |
| } | |
| } | |
| } | |
| func runCommand(args []string, dir string, env []string) error { | |
| cmd := exec.Command(args[0], args[1:]...) | |
| cmd.Stdout = os.Stdout | |
| cmd.Stderr = os.Stderr | |
| cmd.Dir = dir | |
| cmd.Env = env | |
| return cmd.Run() | |
| } | |
| // RunCommandWithExitCode runs a command using exec.Command. The command runs in the working | |
| // directory "dir" with environment "env" and outputs to stdout and stderr. Further it returns the exit code of the executed command. | |
| func RunCommandWithExitCode(args []string, dir string, env []string) (int, error) { | |
| if err := runCommand(args, dir, env); err != nil { | |
| if exitError, ok := err.(*exec.ExitError); ok { | |
| return exitError.ExitCode(), err | |
| } else { | |
| log.Fatal(fmt.Errorf(`error in cmd "%v", unable to parse exit error code, see stderr for details: %v`, args, err)) | |
| } | |
| } | |
| return 0, nil | |
| } | |
| // RunCommand runs a command using exec.Command. The command runs in the working | |
| // directory "dir" with environment "env" and outputs to stdout and stderr. | |
| func RunCommand(args []string, dir string, env []string) error { | |
| if err := runCommand(args, dir, env); err != nil { | |
| return fmt.Errorf(`error in cmd "%v", see stderr for details: %v`, args, err) | |
| } | |
| return nil | |
| } | |
| // QuoteForShell quotes a string for use in a bash shell. | |
| func QuoteForShell(str string) string { | |
| return fmt.Sprintf("'%s'", strings.Replace(str, "'", "'\"'\"'", -1)) | |
| } | |
| // StringSliceContains returns "true" if elem is in arr, "false" otherwise. | |
| func StringSliceContains(arr []string, elem string) bool { | |
| for _, s := range arr { | |
| if s == elem { | |
| return true | |
| } | |
| } | |
| return false | |
| } | |
| func CheckFileExists(path string) (bool, error) { | |
| if _, err := os.Stat(path); err != nil { | |
| if errors.Is(err, os.ErrNotExist) { | |
| return false, nil | |
| } | |
| return false, fmt.Errorf("failed to stat %s, err: %v", path, err) | |
| } | |
| return true, nil | |
| } | |
| // Cut slices s around the first instance of sep, | |
| // returning the text before and after sep. | |
| // The found result reports whether sep appears in s. | |
| // If sep does not appear in s, cut returns s, "", false. | |
| func Cut(s, sep string) (before, after string, found bool) { | |
| if i := strings.Index(s, sep); i >= 0 { | |
| return s[:i], s[i+len(sep):], true | |
| } | |
| return s, "", false | |
| } | |
| // ComputeChecksum computes a string SHA-256 checksum for the provided | |
| // file depending on the file extension. | |
| func ComputeChecksum(path string) (string, error) { | |
| var hash string | |
| var err error | |
| if filepath.Ext(path) == "tar" { | |
| hash, err = computeTarChecksum(path) | |
| } else { | |
| hash, err = computeFileChecksum(path) | |
| } | |
| if err != nil { | |
| return "", fmt.Errorf("error computing checksum for file %s: %v", path, err) | |
| } | |
| return hash, nil | |
| } | |
| func computeTarChecksum(tarFilePath string) (string, error) { | |
| file, err := os.Open(tarFilePath) | |
| if err != nil { | |
| return "", fmt.Errorf("error opening tar file: %v", err) | |
| } | |
| defer file.Close() | |
| fileHash := sha256.New() | |
| reader := tar.NewReader(file) | |
| for { | |
| header, err := reader.Next() | |
| if err == io.EOF { | |
| break | |
| } | |
| if err != nil { | |
| return "", fmt.Errorf("error reading tar headers: %v", err) | |
| } | |
| if header.Typeflag == tar.TypeReg { | |
| _, err := io.Copy(fileHash, reader) | |
| if err != nil { | |
| return "", fmt.Errorf("error reading tar file: %v", err) | |
| } | |
| } | |
| } | |
| return fmt.Sprintf("%x", fileHash.Sum(nil)), nil | |
| } | |
| func computeFileChecksum(path string) (string, error) { | |
| file, err := os.Open(path) | |
| if err != nil { | |
| return "", fmt.Errorf("error opening file: %v", err) | |
| } | |
| defer file.Close() | |
| hash := sha256.New() | |
| if _, err := io.Copy(hash, file); err != nil { | |
| return "", err | |
| } | |
| return fmt.Sprintf("%x", hash.Sum(nil)), nil | |
| } |