config/package_resource.go (505 lines of code) (raw):

// Copyright 2020 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 config import ( "context" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "os" "path/filepath" "strings" "time" "github.com/GoogleCloudPlatform/osconfig/agentconfig" "github.com/GoogleCloudPlatform/osconfig/clog" "github.com/GoogleCloudPlatform/osconfig/packages" "github.com/GoogleCloudPlatform/osconfig/util" "cloud.google.com/go/osconfig/agentendpoint/apiv1/agentendpointpb" "google.golang.org/protobuf/proto" ) var ( packageInfoCacheFile = filepath.Join(agentconfig.CacheDir(), "config_package_info.cache") // Clear out the entry if the last lookup is > 7 days ago. packageInfoCacheTimeout = -168 * time.Hour packageInfoCacheStore packageInfoCache ) type packageResouce struct { *agentendpointpb.OSPolicy_Resource_PackageResource managedPackage ManagedPackage } // AptPackage describes an apt package resource. type AptPackage struct { PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_APT DesiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState } // DebPackage describes a deb package resource. type DebPackage struct { PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_Deb name, localPath string } // GooGetPackage describes a googet package resource. type GooGetPackage struct { PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_GooGet DesiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState } // MSIPackage describes an msi package resource. type MSIPackage struct { PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_MSI productName, productCode, localPath string } // YumPackage describes a yum package resource. type YumPackage struct { PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_YUM DesiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState } // ZypperPackage describes a zypper package resource. type ZypperPackage struct { PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_Zypper DesiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState } // RPMPackage describes an rpm package resource. type RPMPackage struct { PackageResource *agentendpointpb.OSPolicy_Resource_PackageResource_RPM name, localPath string } // ManagedPackage is the package that this PackageResource manages. type ManagedPackage struct { Apt *AptPackage Deb *DebPackage GooGet *GooGetPackage MSI *MSIPackage Yum *YumPackage Zypper *ZypperPackage RPM *RPMPackage tempDir string } func (p *packageResouce) validateFile(file *agentendpointpb.OSPolicy_Resource_File) error { if file.GetLocalPath() != "" { if !util.Exists(file.GetLocalPath()) { return fmt.Errorf("%q does not exist", file.GetLocalPath()) } } return nil } type packageInfoCache map[string]packageInfo type packageInfo struct { PkgInfo *packages.PkgInfo LastLookup time.Time } func getPackageInfoCacheKey(pkgFile *agentendpointpb.OSPolicy_Resource_File) (string, error) { // We use the base64 encoded binary proto as the key. raw, err := proto.Marshal(pkgFile) if err != nil { return "", err } return base64.RawStdEncoding.EncodeToString(raw), nil } func loadPackageInfoCache(ctx context.Context) { if packageInfoCacheStore != nil { return } data, err := ioutil.ReadFile(packageInfoCacheFile) if err != nil { // Just ignore the error and return early // The error mode here is to just always redownload the file. clog.Debugf(ctx, "Error reading the package info cache: %v", err) packageInfoCacheStore = packageInfoCache{} return } var cache packageInfoCache if err := json.Unmarshal(data, &cache); err != nil { clog.Debugf(ctx, "Error unmarshaling the package info cache: %v", err) packageInfoCacheStore = packageInfoCache{} return } packageInfoCacheStore = cache } func getPackageInfoFromCache(ctx context.Context, pkgFile *agentendpointpb.OSPolicy_Resource_File) *packages.PkgInfo { loadPackageInfoCache(ctx) key, err := getPackageInfoCacheKey(pkgFile) if err != nil { // Just ignore the error and return early // The error mode here is just always redownload the file. clog.Debugf(ctx, "Error creating the package info cache key: %v", err) return nil } packageInfo, ok := packageInfoCacheStore[key] if !ok { return nil } return packageInfo.PkgInfo } func updatePackageInfoCache(ctx context.Context, info *packages.PkgInfo, pkgFile *agentendpointpb.OSPolicy_Resource_File) { loadPackageInfoCache(ctx) for k, v := range packageInfoCacheStore { if time.Now().Add(packageInfoCacheTimeout).After(v.LastLookup) { delete(packageInfoCacheStore, k) } } key, err := getPackageInfoCacheKey(pkgFile) if err != nil { // Just ignore the error and return early // The error mode here is to just always redownload the file. clog.Warningf(ctx, "Error creating the package info cache: %v", err) return } packageInfoCacheStore[key] = packageInfo{PkgInfo: info, LastLookup: time.Now()} } func savePackageInfoCache(ctx context.Context) error { if packageInfoCacheStore == nil { return nil } data, err := json.Marshal(packageInfoCacheStore) if err != nil { return err } packageInfoCacheStore = nil return ioutil.WriteFile(packageInfoCacheFile, data, 0644) } func (p *packageResouce) validate(ctx context.Context) (*ManagedResources, error) { switch p.GetSystemPackage().(type) { case *agentendpointpb.OSPolicy_Resource_PackageResource_Apt: pr := p.GetApt() if !packages.AptExists { return nil, fmt.Errorf("cannot manage Apt package %q because apt-get does not exist on the system", pr.GetName()) } p.managedPackage.Apt = &AptPackage{DesiredState: p.GetDesiredState(), PackageResource: pr} case *agentendpointpb.OSPolicy_Resource_PackageResource_Deb_: pr := p.GetDeb() if !packages.DpkgExists { return nil, fmt.Errorf("cannot manage Deb package because dpkg does not exist on the system") } if p.GetDesiredState() != agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED { return nil, fmt.Errorf("desired state of %q not applicable for deb package", p.GetDesiredState()) } if err := p.validateFile(pr.GetSource()); err != nil { return nil, err } var localPath string var err error source := p.GetDeb().GetSource() info := getPackageInfoFromCache(ctx, source) if info == nil { localPath, err = p.download(ctx, "pkg.deb", source) if err != nil { return nil, err } info, err = packages.DebPkgInfo(ctx, localPath) if err != nil { return nil, err } } // Always update the cache to update the timestamps. updatePackageInfoCache(ctx, info, source) p.managedPackage.Deb = &DebPackage{PackageResource: pr, localPath: localPath, name: info.Name} case *agentendpointpb.OSPolicy_Resource_PackageResource_Googet: pr := p.GetGooget() if !packages.GooGetExists { return nil, fmt.Errorf("cannot manage GooGet package %q because googet does not exist on the system", pr.GetName()) } p.managedPackage.GooGet = &GooGetPackage{DesiredState: p.GetDesiredState(), PackageResource: pr} case *agentendpointpb.OSPolicy_Resource_PackageResource_Msi: pr := p.GetMsi() if !packages.MSIExists { return nil, fmt.Errorf("cannot manage MSI package because msiexec does not exist on the system") } if p.GetDesiredState() != agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED { return nil, fmt.Errorf("desired state of %q not applicable for MSI package", p.GetDesiredState()) } if err := p.validateFile(pr.GetSource()); err != nil { return nil, err } var localPath string var err error source := p.GetMsi().GetSource() info := getPackageInfoFromCache(ctx, source) if info == nil { localPath, err = p.download(ctx, "pkg.msi", source) if err != nil { return nil, err } productName, productCode, err := packages.MSIInfo(localPath) if err != nil { return nil, err } // We store productCode as version in the packageinfo struct. info = &packages.PkgInfo{Name: productName, Version: productCode} } // Always update the cache to update the timestamps. updatePackageInfoCache(ctx, info, source) p.managedPackage.MSI = &MSIPackage{PackageResource: pr, localPath: localPath, productName: info.Name, productCode: info.Version} case *agentendpointpb.OSPolicy_Resource_PackageResource_Yum: pr := p.GetYum() if !packages.YumExists { return nil, fmt.Errorf("cannot manage Yum package %q because yum does not exist on the system", pr.GetName()) } p.managedPackage.Yum = &YumPackage{DesiredState: p.GetDesiredState(), PackageResource: pr} case *agentendpointpb.OSPolicy_Resource_PackageResource_Zypper_: pr := p.GetZypper() if !packages.ZypperExists { return nil, fmt.Errorf("cannot manage Zypper package %q because zypper does not exist on the system", pr.GetName()) } p.managedPackage.Zypper = &ZypperPackage{DesiredState: p.GetDesiredState(), PackageResource: pr} case *agentendpointpb.OSPolicy_Resource_PackageResource_Rpm: pr := p.GetRpm() if !packages.RPMExists { return nil, fmt.Errorf("cannot manage RPM package because rpm does not exist on the system") } if p.GetDesiredState() != agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED { return nil, fmt.Errorf("desired state of %q not applicable for rpm package", p.GetDesiredState()) } if err := p.validateFile(pr.GetSource()); err != nil { return nil, err } var localPath string var err error source := p.GetRpm().GetSource() info := getPackageInfoFromCache(ctx, source) if info == nil { localPath, err = p.download(ctx, "pkg.rpm", source) if err != nil { return nil, err } info, err = packages.RPMPkgInfo(ctx, localPath) if err != nil { return nil, err } } // Always update the cache to update the timestamps. updatePackageInfoCache(ctx, info, source) p.managedPackage.RPM = &RPMPackage{PackageResource: pr, localPath: localPath, name: info.Name} default: return nil, fmt.Errorf("SystemPackage field not set or references unknown package manager: %v", p.GetSystemPackage()) } return &ManagedResources{Packages: []ManagedPackage{p.managedPackage}}, nil } type packageCache struct { cache map[string]struct{} refreshed time.Time } var ( aptInstalled = &packageCache{} debInstalled = &packageCache{} gooInstalled = &packageCache{} yumInstalled = &packageCache{} zypperInstalled = &packageCache{} rpmInstalled = &packageCache{} ) func populateInstalledCache(ctx context.Context, mp ManagedPackage) error { var cache *packageCache var refreshFunc func(context.Context) ([]*packages.PkgInfo, error) var err error switch { case mp.Apt != nil: cache = aptInstalled refreshFunc = packages.InstalledDebPackages case mp.Deb != nil: cache = debInstalled refreshFunc = packages.InstalledDebPackages case mp.GooGet != nil: cache = gooInstalled refreshFunc = packages.InstalledGooGetPackages case mp.MSI != nil: // We just query per each MSI. return nil // TODO: implement yum functions case mp.Yum != nil: cache = yumInstalled refreshFunc = packages.InstalledRPMPackages // TODO: implement zypper functions case mp.Zypper != nil: cache = zypperInstalled refreshFunc = packages.InstalledRPMPackages case mp.RPM != nil: cache = rpmInstalled refreshFunc = packages.InstalledRPMPackages default: return fmt.Errorf("unknown or unpopulated ManagedPackage package type: %+v", mp) } // Cache already populated within the last 3 min. if cache.cache != nil && cache.refreshed.After(time.Now().Add(-3*time.Minute)) { return nil } pis, err := refreshFunc(ctx) if err != nil { return err } cache.cache = map[string]struct{}{} for _, pkg := range pis { cache.cache[pkg.Name] = struct{}{} } cache.refreshed = time.Now() return nil } // TODO: use a persistent cache for downloaded files so we dont need to redownload them each time func (p *packageResouce) download(ctx context.Context, name string, file *agentendpointpb.OSPolicy_Resource_File) (string, error) { var path string perms := os.FileMode(0644) switch { case file.GetLocalPath() != "": path = file.GetLocalPath() default: tmpDir, err := ioutil.TempDir("", "osconfig_package_resource_") if err != nil { return "", fmt.Errorf("failed to create temp dir: %s", err) } p.managedPackage.tempDir = tmpDir path = filepath.Join(p.managedPackage.tempDir, name) if _, err := downloadFile(ctx, path, perms, file); err != nil { return "", err } } return path, nil } func (p *packageResouce) checkState(ctx context.Context) (inDesiredState bool, err error) { if err := populateInstalledCache(ctx, p.managedPackage); err != nil { return false, err } var desiredState agentendpointpb.OSPolicy_Resource_PackageResource_DesiredState var pkgIns bool switch { case p.managedPackage.Apt != nil: desiredState = p.managedPackage.Apt.DesiredState _, pkgIns = aptInstalled.cache[p.managedPackage.Apt.PackageResource.GetName()] case p.managedPackage.Deb != nil: desiredState = agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED _, pkgIns = debInstalled.cache[p.managedPackage.Deb.name] case p.managedPackage.GooGet != nil: desiredState = p.managedPackage.GooGet.DesiredState _, pkgIns = gooInstalled.cache[p.managedPackage.GooGet.PackageResource.GetName()] case p.managedPackage.MSI != nil: desiredState = agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED pkgIns, err = packages.MSIInstalled(p.managedPackage.MSI.productCode) if err != nil { return false, err } case p.managedPackage.Yum != nil: desiredState = p.managedPackage.Yum.DesiredState _, pkgIns = yumInstalled.cache[p.managedPackage.Yum.PackageResource.GetName()] case p.managedPackage.Zypper != nil: desiredState = p.managedPackage.Zypper.DesiredState _, pkgIns = zypperInstalled.cache[p.managedPackage.Zypper.PackageResource.GetName()] case p.managedPackage.RPM != nil: desiredState = agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED _, pkgIns = rpmInstalled.cache[p.managedPackage.RPM.name] } switch desiredState { case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED: if pkgIns { return true, nil } case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED: if !pkgIns { return true, nil } default: return false, fmt.Errorf("DesiredState field not set or references state: %q", desiredState) } return false, nil } func (p *packageResouce) enforceState(ctx context.Context) (inDesiredState bool, err error) { var ( installing = "installing" removing = "removing" enforcePackage struct { actionFunc func() error installedCache *packageCache name string action string packageType string } ) switch { case p.managedPackage.Apt != nil: enforcePackage.name = p.managedPackage.Apt.PackageResource.GetName() enforcePackage.packageType = "apt" enforcePackage.installedCache = aptInstalled switch p.managedPackage.Apt.DesiredState { case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED: enforcePackage.action, enforcePackage.actionFunc = installing, func() error { if _, err := packages.AptUpdate(ctx); err != nil { return err } return packages.InstallAptPackages(ctx, []string{enforcePackage.name}) } case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED: enforcePackage.action, enforcePackage.actionFunc = removing, func() error { return packages.RemoveAptPackages(ctx, []string{enforcePackage.name}) } } case p.managedPackage.Deb != nil: enforcePackage.name = p.managedPackage.Deb.name enforcePackage.packageType = "deb" enforcePackage.installedCache = debInstalled enforcePackage.action = installing // Check if we have not pulled the package yet. if p.managedPackage.Deb.localPath == "" { localPath, err := p.download(ctx, "pkg.deb", p.GetDeb().GetSource()) if err != nil { return false, err } p.managedPackage.Deb.localPath = localPath } if p.GetDeb().GetPullDeps() { enforcePackage.actionFunc = func() error { return packages.InstallAptPackages(ctx, []string{p.managedPackage.Deb.localPath}) } } else { enforcePackage.actionFunc = func() error { return packages.DpkgInstall(ctx, p.managedPackage.Deb.localPath) } } case p.managedPackage.GooGet != nil: enforcePackage.name = p.managedPackage.GooGet.PackageResource.GetName() enforcePackage.packageType = "googet" enforcePackage.installedCache = gooInstalled switch p.managedPackage.GooGet.DesiredState { case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED: enforcePackage.action, enforcePackage.actionFunc = installing, func() error { return packages.InstallGooGetPackages(ctx, []string{enforcePackage.name}) } case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED: enforcePackage.action, enforcePackage.actionFunc = removing, func() error { return packages.RemoveGooGetPackages(ctx, []string{enforcePackage.name}) } } case p.managedPackage.MSI != nil: enforcePackage.name = p.managedPackage.MSI.productName enforcePackage.packageType = "msi" enforcePackage.action = installing enforcePackage.installedCache = &packageCache{} // No package cache for msi. // Check if we have not pulled the package yet. if p.managedPackage.MSI.localPath == "" { localPath, err := p.download(ctx, "pkg.msi", p.GetMsi().GetSource()) if err != nil { return false, err } p.managedPackage.MSI.localPath = localPath } enforcePackage.actionFunc = func() error { return packages.InstallMSIPackage(ctx, p.managedPackage.MSI.localPath, p.managedPackage.MSI.PackageResource.GetProperties()) } case p.managedPackage.Yum != nil: enforcePackage.name = p.managedPackage.Yum.PackageResource.GetName() enforcePackage.packageType = "yum" enforcePackage.installedCache = yumInstalled switch p.managedPackage.Yum.DesiredState { case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED: enforcePackage.action, enforcePackage.actionFunc = installing, func() error { return packages.InstallYumPackages(ctx, []string{enforcePackage.name}) } case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED: enforcePackage.action, enforcePackage.actionFunc = removing, func() error { return packages.RemoveYumPackages(ctx, []string{enforcePackage.name}) } } case p.managedPackage.Zypper != nil: enforcePackage.name = p.managedPackage.Zypper.PackageResource.GetName() enforcePackage.packageType = "zypper" enforcePackage.installedCache = zypperInstalled switch p.managedPackage.Zypper.DesiredState { case agentendpointpb.OSPolicy_Resource_PackageResource_INSTALLED: enforcePackage.action, enforcePackage.actionFunc = installing, func() error { return packages.InstallZypperPackages(ctx, []string{enforcePackage.name}) } case agentendpointpb.OSPolicy_Resource_PackageResource_REMOVED: enforcePackage.action, enforcePackage.actionFunc = removing, func() error { return packages.RemoveZypperPackages(ctx, []string{enforcePackage.name}) } } case p.managedPackage.RPM != nil: enforcePackage.name = p.managedPackage.RPM.name enforcePackage.packageType = "rpm" enforcePackage.installedCache = rpmInstalled enforcePackage.action = installing // Check if we have not pulled the package yet. if p.managedPackage.RPM.localPath == "" { localPath, err := p.download(ctx, "pkg.rpm", p.GetRpm().GetSource()) if err != nil { return false, err } p.managedPackage.RPM.localPath = localPath } if p.GetRpm().GetPullDeps() { switch { case packages.YumExists: enforcePackage.actionFunc = func() error { return packages.InstallYumPackages(ctx, []string{p.managedPackage.RPM.localPath}) } case packages.ZypperExists: enforcePackage.actionFunc = func() error { return packages.InstallZypperPackages(ctx, []string{p.managedPackage.RPM.localPath}) } default: return false, fmt.Errorf("cannot install rpm %q with 'PullDeps' option as neither yum or zypper exist on system", enforcePackage.name) } } else { enforcePackage.actionFunc = func() error { return packages.RPMInstall(ctx, p.managedPackage.RPM.localPath) } } } clog.Infof(ctx, "%s %s package %q", strings.Title(enforcePackage.action), enforcePackage.packageType, enforcePackage.name) // Reset the cache as we are taking action on. enforcePackage.installedCache.cache = nil if err := enforcePackage.actionFunc(); err != nil { return false, fmt.Errorf("error %s %s package %q", enforcePackage.action, enforcePackage.packageType, enforcePackage.name) } return true, nil } func (p *packageResouce) populateOutput(rCompliance *agentendpointpb.OSPolicyResourceCompliance) {} func (p *packageResouce) cleanup(ctx context.Context) error { // Save cache and clear the variable. if err := savePackageInfoCache(ctx); err != nil { clog.Warningf(ctx, "Error saving the package info cache: %v", err) } if p.managedPackage.tempDir != "" { return os.RemoveAll(p.managedPackage.tempDir) } return nil }