policies/recipes/steps.go (95 lines of code) (raw):

// Copyright 2019 Google Inc. All Rights Reserved. // // 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 recipes import ( "archive/tar" "archive/zip" "compress/bzip2" "compress/gzip" "context" "errors" "fmt" "io" "os" "os/exec" "path/filepath" "runtime" "strconv" "strings" "time" "github.com/GoogleCloudPlatform/osconfig/clog" "github.com/GoogleCloudPlatform/osconfig/packages" "github.com/GoogleCloudPlatform/osconfig/util" "github.com/ulikunitz/xz" "github.com/ulikunitz/xz/lzma" "cloud.google.com/go/osconfig/agentendpoint/apiv1beta/agentendpointpb" ) var extensionMap = map[agentendpointpb.SoftwareRecipe_Step_RunScript_Interpreter]string{ agentendpointpb.SoftwareRecipe_Step_RunScript_INTERPRETER_UNSPECIFIED: ".bat", agentendpointpb.SoftwareRecipe_Step_RunScript_SHELL: ".bat", agentendpointpb.SoftwareRecipe_Step_RunScript_POWERSHELL: ".ps1", } func stepCopyFile(step *agentendpointpb.SoftwareRecipe_Step_CopyFile, artifacts map[string]string, runEnvs []string, stepDir string) error { dest, err := util.NormPath(step.Destination) if err != nil { return err } permissions, err := parsePermissions(step.Permissions) if err != nil { return err } if _, err := os.Stat(dest); err != nil { if !os.IsNotExist(err) { return err } } else { // file exists if !step.Overwrite { return fmt.Errorf("file already exists at path %q and Overwrite = false", step.Destination) } if err := os.Chmod(dest, permissions); err != nil { return err } } artifact := step.GetArtifactId() src, ok := artifacts[artifact] if !ok { return fmt.Errorf("could not find location for artifact %q", artifact) } reader, err := os.Open(src) if err != nil { return err } defer reader.Close() _, err = util.AtomicWriteFileStream(reader, "", dest, permissions) return err } func parsePermissions(s string) (os.FileMode, error) { if s == "" { return 0755, nil } i, err := strconv.ParseUint(s, 8, 32) if err != nil { return 0, err } return os.FileMode(i), nil } func stepExtractArchive(ctx context.Context, step *agentendpointpb.SoftwareRecipe_Step_ExtractArchive, artifacts map[string]string, runEnvs []string, stepDir string) error { artifact := step.GetArtifactId() filename, ok := artifacts[artifact] if !ok { return fmt.Errorf("%q not found in artifact map", artifact) } switch typ := step.GetType(); typ { case agentendpointpb.SoftwareRecipe_Step_ExtractArchive_ZIP: return extractZip(filename, step.Destination) case agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_GZIP, agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_BZIP, agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_LZMA, agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_XZ, agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR: return extractTar(ctx, filename, step.Destination, typ) default: return fmt.Errorf("Unrecognized archive type %q", typ) } } func zipIsDir(name string) bool { if os.PathSeparator == '\\' { return strings.HasSuffix(name, `\`) || strings.HasSuffix(name, "/") } return strings.HasSuffix(name, "/") } func extractZip(zipPath string, dst string) error { zr, err := zip.OpenReader(zipPath) if err != nil { return err } defer zr.Close() // Check for conflicts for _, f := range zr.File { filen, err := util.NormPath(util.SanitizePath(filepath.Join(dst, f.Name))) if err != nil { return err } stat, err := os.Stat(filen) if os.IsNotExist(err) { continue } if err != nil { return err } if zipIsDir(f.Name) && stat.IsDir() { // it's ok if directories already exist continue } return fmt.Errorf("file exists: %s", filen) } // Create files. for _, f := range zr.File { filen, err := util.NormPath(util.SanitizePath(filepath.Join(dst, f.Name))) if err != nil { return err } if zipIsDir(f.Name) { mode := f.Mode() if mode == 0 { mode = 0755 } if err := os.MkdirAll(filen, mode); err != nil { return err } // Setting to correct permissions in case the directory has already been created if err := os.Chmod(filen, mode); err != nil { return err } continue } filedir := filepath.Dir(filen) if err = os.MkdirAll(filedir, 0755); err != nil { return err } reader, err := f.Open() if err != nil { return err } mode := f.Mode() if mode == 0 { mode = 0755 } dst, err := os.OpenFile(filen, os.O_RDWR|os.O_CREATE, mode) if err != nil { return err } if _, err = io.Copy(dst, reader); err != nil { dst.Close() return err } reader.Close() if err := dst.Close(); err != nil { return err } if err := os.Chtimes(filen, time.Now(), f.ModTime()); err != nil { return err } } return nil } func decompress(reader io.Reader, archiveType agentendpointpb.SoftwareRecipe_Step_ExtractArchive_ArchiveType) (io.Reader, error) { switch archiveType { case agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_GZIP: // *gzip.Reader is a io.ReadCloser but it isn't necesary to call Close() on it. return gzip.NewReader(reader) case agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_BZIP: return bzip2.NewReader(reader), nil case agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_LZMA: return lzma.NewReader2(reader) case agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR_XZ: return xz.NewReader(reader) case agentendpointpb.SoftwareRecipe_Step_ExtractArchive_TAR: return reader, nil default: return nil, fmt.Errorf("Unrecognized archive type %q when trying to decompress tar", archiveType) } } func checkForConflicts(tr *tar.Reader, dst string) error { for { header, err := tr.Next() if err == io.EOF { break } if err != nil { return err } filen, err := util.NormPath(filepath.Join(dst, header.Name)) if err != nil { return err } stat, err := os.Stat(filen) if os.IsNotExist(err) { continue } if err != nil { return err } if header.Typeflag == tar.TypeDir && stat.IsDir() { // it's ok if directories already exist continue } return fmt.Errorf("file exists: %s", filen) } return nil } func extractTar(ctx context.Context, tarName string, dst string, archiveType agentendpointpb.SoftwareRecipe_Step_ExtractArchive_ArchiveType) error { file, err := os.Open(tarName) if err != nil { return err } defer file.Close() decompressed, err := decompress(file, archiveType) if err != nil { return err } tr := tar.NewReader(decompressed) if err := checkForConflicts(tr, dst); err != nil { return err } file.Seek(0, 0) decompressed, err = decompress(file, archiveType) if err != nil { return err } tr = tar.NewReader(decompressed) for { var err error header, err := tr.Next() if err == io.EOF { break } if err != nil { return err } filen, err := util.NormPath(filepath.Join(dst, util.SanitizePath(header.Name))) if err != nil { return err } filedir := filepath.Dir(filen) if err := os.MkdirAll(filedir, 0700); err != nil { return err } switch header.Typeflag { case tar.TypeDir: if err := os.MkdirAll(filen, os.FileMode(header.Mode)); err != nil { return err } // Setting to correct permissions in case the directory has already been created if err := os.Chmod(filen, os.FileMode(header.Mode)); err != nil { return err } if err := chown(filen, header.Uid, header.Gid); err != nil { return err } case tar.TypeReg, tar.TypeRegA: dst, err := os.OpenFile(filen, os.O_RDWR|os.O_CREATE, os.FileMode(header.Mode)) if err != nil { return err } if _, err := io.Copy(dst, tr); err != nil { dst.Close() return err } if err := dst.Close(); err != nil { return err } case tar.TypeLink: if err := os.Link(header.Linkname, filen); err != nil { return err } continue case tar.TypeSymlink: if err := os.Symlink(header.Linkname, filen); err != nil { return err } continue case tar.TypeChar: if err := mkCharDevice(filen, uint32(header.Devmajor), uint32(header.Devminor)); err != nil { return err } if runtime.GOOS != "windows" { if err := os.Chmod(filen, os.FileMode(header.Mode)); err != nil { return err } } case tar.TypeBlock: if err := mkBlockDevice(filen, uint32(header.Devmajor), uint32(header.Devminor)); err != nil { return err } if runtime.GOOS != "windows" { if err := os.Chmod(filen, os.FileMode(header.Mode)); err != nil { return err } } case tar.TypeFifo: if err := mkFifo(filen, uint32(header.Mode)); err != nil { return err } default: clog.Infof(ctx, "Unknown file type for tar entry %s\n", filen) continue } if err := chown(filen, header.Uid, header.Gid); err != nil { return err } if err := os.Chtimes(filen, header.AccessTime, header.ModTime); err != nil { return err } } return nil } func stepInstallMsi(ctx context.Context, step *agentendpointpb.SoftwareRecipe_Step_InstallMsi, artifacts map[string]string, runEnvs []string, stepDir string) error { if runtime.GOOS != "windows" { return errors.New("SoftwareRecipe_Step_InstallMsi only applicable on Windows") } artifact := step.GetArtifactId() path, ok := artifacts[artifact] if !ok { return fmt.Errorf("%q not found in artifact map", artifact) } args := step.Flags if len(args) == 0 { args = []string{"/i", "/qn", "/norestart"} } args = append(args, path) exitCodes := step.AllowedExitCodes if len(exitCodes) == 0 { exitCodes = []int32{0, 1641, 3010} } return executeCommand(ctx, "C:\\Windows\\System32\\msiexec.exe", args, stepDir, runEnvs, exitCodes) } func stepInstallDpkg(ctx context.Context, step *agentendpointpb.SoftwareRecipe_Step_InstallDpkg, artifacts map[string]string) error { if !packages.DpkgExists { return fmt.Errorf("dpkg does not exist on system") } artifact := step.GetArtifactId() path, ok := artifacts[artifact] if !ok { return fmt.Errorf("%q not found in artifact map", artifact) } return packages.DpkgInstall(ctx, path) } func stepInstallRpm(ctx context.Context, step *agentendpointpb.SoftwareRecipe_Step_InstallRpm, artifacts map[string]string) error { if !packages.RPMExists { return fmt.Errorf("rpm does not exist on system") } artifact := step.GetArtifactId() path, ok := artifacts[artifact] if !ok { return fmt.Errorf("%q not found in artifact map", artifact) } return packages.RPMInstall(ctx, path) } func stepExecFile(ctx context.Context, step *agentendpointpb.SoftwareRecipe_Step_ExecFile, artifacts map[string]string, runEnvs []string, stepDir string) error { var path string switch { case step.GetArtifactId() != "": var ok bool artifact := step.GetArtifactId() path, ok = artifacts[artifact] if !ok { return fmt.Errorf("%q not found in artifact map", artifact) } // By default artifacts are created with 0644 if err := os.Chmod(path, 0755); err != nil { return fmt.Errorf("error setting execute permissions on artifact %s: %v", step.GetArtifactId(), err) } case step.GetLocalPath() != "": path = step.GetLocalPath() default: return fmt.Errorf("can't determine location type") } return executeCommand(ctx, path, step.Args, stepDir, runEnvs, []int32{0}) } func stepRunScript(ctx context.Context, step *agentendpointpb.SoftwareRecipe_Step_RunScript, artifacts map[string]string, runEnvs []string, stepDir string) error { var extension string if runtime.GOOS == "windows" { extension = extensionMap[step.Interpreter] } scriptPath := filepath.Join(stepDir, "recipe_script_source"+extension) if err := util.AtomicWrite(scriptPath, []byte(step.Script), 0755); err != nil { return err } var cmd string var args []string switch step.Interpreter { case agentendpointpb.SoftwareRecipe_Step_RunScript_INTERPRETER_UNSPECIFIED: cmd = scriptPath case agentendpointpb.SoftwareRecipe_Step_RunScript_SHELL: if runtime.GOOS == "windows" { cmd = scriptPath } else { args = append(args, scriptPath) cmd = "/bin/sh" } case agentendpointpb.SoftwareRecipe_Step_RunScript_POWERSHELL: if runtime.GOOS != "windows" { return fmt.Errorf("interpreter %q can only be used on Windows systems", step.Interpreter) } args = append(args, "-File", scriptPath) cmd = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\PowerShell.exe" default: return fmt.Errorf("unsupported interpreter %q", step.Interpreter) } return executeCommand(ctx, cmd, args, stepDir, runEnvs, step.AllowedExitCodes) } func executeCommand(ctx context.Context, cmd string, args []string, workDir string, runEnvs []string, allowedExitCodes []int32) error { cmdObj := exec.Command(cmd, args...) cmdObj.Dir = workDir defaultEnv, err := createDefaultEnvironment() if err != nil { return fmt.Errorf("error creating default environment: %v", err) } cmdObj.Env = append(cmdObj.Env, defaultEnv...) cmdObj.Env = append(cmdObj.Env, runEnvs...) o, err := cmdObj.CombinedOutput() clog.Infof(ctx, "Combined output for %q command:\n%s", cmd, o) if err == nil { return nil } if v, ok := err.(*exec.ExitError); ok && len(allowedExitCodes) != 0 { result := int32(v.ExitCode()) for _, code := range allowedExitCodes { if result == code { return nil } } } return err } func chown(file string, uid, gid int) error { // os.Chown unsupported on windows if runtime.GOOS == "windows" { return nil } return os.Chown(file, uid, gid) }