packages/apt_deb.go (273 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 packages import ( "bytes" "context" "encoding/json" "fmt" "os" "os/exec" "runtime" "strings" "github.com/GoogleCloudPlatform/osconfig/clog" "github.com/GoogleCloudPlatform/osconfig/osinfo" "github.com/GoogleCloudPlatform/osconfig/util" ) var ( dpkg string dpkgQuery string dpkgDeb string aptGet string dpkgInstallArgs = []string{"--install"} dpkgPackageFieldsMapping = map[string]string{ "package": "${Package}", "architecture": "${Architecture}", "version": "${Version}", "status": "${db:Status-Status}", "source_name": "${source:Package}", "source_version": "${source:Version}", } dpkgQueryArgs = []string{"-W", "-f", formatFieldsMappingToFormattingString(dpkgPackageFieldsMapping)} dpkgRepairArgs = []string{"--configure", "-a"} aptGetInstallArgs = []string{"install", "-y"} aptGetRemoveArgs = []string{"remove", "-y"} aptGetUpdateArgs = []string{"update"} aptGetUpgradeCmd = "upgrade" aptGetFullUpgradeCmd = "full-upgrade" aptGetDistUpgradeCmd = "dist-upgrade" aptGetUpgradableArgs = []string{"--just-print", "-qq"} allowDowngradesArg = "--allow-downgrades" dpkgErr = []byte("dpkg --configure -a") ) func init() { if runtime.GOOS != "windows" { dpkg = "/usr/bin/dpkg" dpkgQuery = "/usr/bin/dpkg-query" dpkgDeb = "/usr/bin/dpkg-deb" aptGet = "/usr/bin/apt-get" } AptExists = util.Exists(aptGet) DpkgExists = util.Exists(dpkg) DpkgQueryExists = util.Exists(dpkgQuery) } // AptUpgradeType is the apt upgrade type. type AptUpgradeType int const ( // AptGetUpgrade specifies apt-get upgrade should be run. AptGetUpgrade AptUpgradeType = iota // AptGetDistUpgrade specifies apt-get dist-upgrade should be run. AptGetDistUpgrade // AptGetFullUpgrade specifies apt-get full-upgrade should be run. AptGetFullUpgrade ) type aptGetUpgradeOpts struct { upgradeType AptUpgradeType showNew bool allowDowngrades bool } // AptGetUpgradeOption is an option for apt-get upgrade. type AptGetUpgradeOption func(*aptGetUpgradeOpts) // AptGetUpgradeType returns a AptGetUpgradeOption that specifies upgrade type. func AptGetUpgradeType(upgradeType AptUpgradeType) AptGetUpgradeOption { return func(args *aptGetUpgradeOpts) { args.upgradeType = upgradeType } } // AptGetUpgradeShowNew returns a AptGetUpgradeOption that indicates whether 'new' packages should be returned. func AptGetUpgradeShowNew(showNew bool) AptGetUpgradeOption { return func(args *aptGetUpgradeOpts) { args.showNew = showNew } } // AptGetUpgradeAllowDowngrades returns a AptGetUpgradeOption that specifies AllowDowngrades. func AptGetUpgradeAllowDowngrades(allowDowngrades bool) AptGetUpgradeOption { return func(args *aptGetUpgradeOpts) { args.allowDowngrades = allowDowngrades } } func dpkgRepair(ctx context.Context, out []byte) bool { // Error code 100 may occur for non repairable errors, just check the output. if !bytes.Contains(out, dpkgErr) { return false } clog.Debugf(ctx, "apt-get error, attempting dpkg repair.") // Ignore error here, just log and rerun apt-get. run(ctx, dpkg, dpkgRepairArgs) return true } type cmdModifier func(*exec.Cmd) func runAptGet(ctx context.Context, args []string, cmdModifiers []cmdModifier) ([]byte, []byte, error) { cmd := exec.CommandContext(ctx, aptGet, args...) for _, modifier := range cmdModifiers { modifier(cmd) } return runner.Run(ctx, cmd) } func runAptGetWithDowngradeRetrial(ctx context.Context, args []string, cmdModifiers []cmdModifier) ([]byte, []byte, error) { stdout, stderr, err := runAptGet(ctx, args, cmdModifiers) if err != nil { if strings.Contains(string(stderr), "E: Packages were downgraded and -y was used without --allow-downgrades.") { cmdModifiers = append(cmdModifiers, func(cmd *exec.Cmd) { cmd.Args = append(cmd.Args, allowDowngradesArg) }) stdout, stderr, err = runAptGet(ctx, args, cmdModifiers) } } return stdout, stderr, err } func parseDpkgDeb(data []byte) (*PkgInfo, error) { /* new Debian package, version 2.0. size 6731954 bytes: control archive=2138 bytes. 498 bytes, 12 lines control 3465 bytes, 31 lines md5sums 2793 bytes, 65 lines * postinst #!/bin/sh 938 bytes, 28 lines * postrm #!/bin/sh 216 bytes, 7 lines * prerm #!/bin/sh Package: google-guest-agent Version: 1:1dummy-g1 Architecture: amd64 Maintainer: Google Cloud Team <gc-team@google.com> Installed-Size: 23279 Depends: init-system-helpers (>= 1.18~) Conflicts: python-google-compute-engine, python3-google-compute-engine Section: misc Priority: optional Description: Google Compute Engine Guest Agent Contains the guest agent and metadata script runner binaries. Git: https://github.com/GoogleCloudPlatform/guest-agent/tree/c3d526e650c4e45ae3258c07836fd72f85fd9fc8 */ lines := bytes.Split(bytes.TrimSpace(data), []byte("\n")) info := &PkgInfo{} for _, ln := range lines { if info.Name != "" && info.Version != "" && info.Arch != "" { break } fields := bytes.Fields(ln) if len(fields) != 2 { continue } if bytes.Contains(fields[0], []byte("Package:")) { // Some packages do not adhere to the Debian Policy and might have mix-cased names // And dpkg will register the package with lower case anyway so use lower-case package name // This is necessary because the compliance check is done between the .deb file descriptor value // and the internal dpkg db which register a lower-cased package name info.Name = strings.ToLower(string(fields[1])) continue } if bytes.Contains(fields[0], []byte("Version:")) { info.Version = string(fields[1]) continue } if bytes.Contains(fields[0], []byte("Architecture:")) { info.Arch = osinfo.NormalizeArchitecture(string(fields[1])) continue } } if info.Name == "" || info.Version == "" || info.Arch == "" { return nil, fmt.Errorf("could not parse dpkg-deb output: %q", data) } return info, nil } // DebPkgInfo gets PkgInfo from a deb package. func DebPkgInfo(ctx context.Context, path string) (*PkgInfo, error) { out, err := run(ctx, dpkgDeb, []string{"-I", path}) if err != nil { return nil, err } return parseDpkgDeb(out) } // InstallAptPackages installs apt packages. func InstallAptPackages(ctx context.Context, pkgs []string) error { args := append(aptGetInstallArgs, pkgs...) cmdModifiers := []cmdModifier{ func(cmd *exec.Cmd) { cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive") }, } stdout, stderr, err := runAptGetWithDowngradeRetrial(ctx, args, cmdModifiers) if err != nil { if dpkgRepair(ctx, stderr) { stdout, stderr, err = runAptGetWithDowngradeRetrial(ctx, args, cmdModifiers) } } if err != nil { err = fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", aptGet, args, err, stdout, stderr) } return err } // RemoveAptPackages removes apt packages. func RemoveAptPackages(ctx context.Context, pkgs []string) error { args := append(aptGetRemoveArgs, pkgs...) cmdModifiers := []cmdModifier{ func(cmd *exec.Cmd) { cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive") }, } stdout, stderr, err := runAptGet(ctx, args, cmdModifiers) if err != nil { if dpkgRepair(ctx, stderr) { stdout, stderr, err = runAptGet(ctx, args, cmdModifiers) } } if err != nil { err = fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", aptGet, args, err, stdout, stderr) } return err } func parseAptUpdates(ctx context.Context, data []byte, showNew bool) []*PkgInfo { /* Inst libldap-common [2.4.45+dfsg-1ubuntu1.2] (2.4.45+dfsg-1ubuntu1.3 Ubuntu:18.04/bionic-updates, Ubuntu:18.04/bionic-security [all]) Inst firmware-linux-free (3.4 Debian:9.9/stable [all]) [] Inst google-cloud-sdk [245.0.0-0] (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all]) Inst linux-image-4.9.0-9-amd64 (4.9.168-1+deb9u2 Debian-Security:9/stable [amd64]) Inst linux-image-amd64 [4.9+80+deb9u6] (4.9+80+deb9u7 Debian:9.9/stable [amd64]) Conf firmware-linux-free (3.4 Debian:9.9/stable [all]) Conf google-cloud-sdk (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all]) Conf linux-image-4.9.0-9-amd64 (4.9.168-1+deb9u2 Debian-Security:9/stable [amd64]) Conf linux-image-amd64 (4.9+80+deb9u7 Debian:9.9/stable [amd64]) */ lines := bytes.Split(bytes.TrimSpace(data), []byte("\n")) var pkgs []*PkgInfo for _, ln := range lines { pkg := bytes.Fields(ln) if len(pkg) < 5 || string(pkg[0]) != "Inst" { continue } // Inst google-cloud-sdk [245.0.0-0] (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all]) pkg = pkg[1:] // ==> google-cloud-sdk [245.0.0-0] (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all]) if bytes.HasPrefix(pkg[1], []byte("[")) { pkg = append(pkg[:1], pkg[2:]...) // ==> google-cloud-sdk (246.0.0-0 cloud-sdk-stretch:cloud-sdk-stretch [all]) } else if !showNew { // This is a newly installed package and not an upgrade, ignore if showNew is false. continue } // Drop trailing "[]" if they exist. if bytes.Contains(pkg[len(pkg)-1], []byte("[]")) { pkg = pkg[:len(pkg)-1] } if !bytes.HasPrefix(pkg[1], []byte("(")) || !bytes.HasSuffix(pkg[len(pkg)-1], []byte(")")) { continue } ver := bytes.Trim(pkg[1], "(") // (246.0.0-0 => 246.0.0-0 arch := bytes.Trim(pkg[len(pkg)-1], "[])") // [all]) => all pkgs = append(pkgs, &PkgInfo{Name: string(pkg[0]), Arch: osinfo.NormalizeArchitecture(string(arch)), Version: string(ver)}) } return pkgs } // AptUpdates returns all the packages that will be installed when running // apt-get [dist-|full-]upgrade. func AptUpdates(ctx context.Context, opts ...AptGetUpgradeOption) ([]*PkgInfo, error) { aptOpts := &aptGetUpgradeOpts{ upgradeType: AptGetUpgrade, showNew: false, allowDowngrades: false, } for _, opt := range opts { opt(aptOpts) } args := aptGetUpgradableArgs switch aptOpts.upgradeType { case AptGetUpgrade: args = append(aptGetUpgradableArgs, aptGetUpgradeCmd) case AptGetDistUpgrade: args = append(aptGetUpgradableArgs, aptGetDistUpgradeCmd) case AptGetFullUpgrade: args = append(aptGetUpgradableArgs, aptGetFullUpgradeCmd) default: return nil, fmt.Errorf("unknown upgrade type: %q", aptOpts.upgradeType) } if _, err := AptUpdate(ctx); err != nil { return nil, err } out, _, err := runAptGetWithDowngradeRetrial(ctx, args, []cmdModifier{ func(cmd *exec.Cmd) { cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive") }, }) if err != nil { return nil, err } return parseAptUpdates(ctx, out, aptOpts.showNew), nil } // AptUpdate runs apt-get update. func AptUpdate(ctx context.Context) ([]byte, error) { stdout, _, err := runAptGet(ctx, aptGetUpdateArgs, []cmdModifier{ func(cmd *exec.Cmd) { cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive") }, }) return stdout, err } // InstalledDebPackages queries for all installed deb packages. func InstalledDebPackages(ctx context.Context) ([]*PkgInfo, error) { out, err := run(ctx, dpkgQuery, dpkgQueryArgs) if err != nil { return nil, err } return parseInstalledDebPackages(ctx, out), nil } func parseInstalledDebPackages(ctx context.Context, data []byte) []*PkgInfo { /* Each line contains an entry in a json format, keep in mind that whole output is not valid json. {"package":"adduser","architecture":"all","version":"3.118ubuntu2","status":"installed","source_name":"adduser","source_version":"3.118ubuntu2"} {"package":"apt-utils","architecture":"amd64","version":"2.0.10","status":"installed","source_name":"apt","source_version":"2.0.10"} {"package":"git","architecture":"amd64","version":"1:2.25.1-1ubuntu3.12","status":"installed","source_name":"git","source_version":"1:2.25.1-1ubuntu3.12"} ... */ entries := bytes.Split(bytes.TrimSpace(data), []byte("\n")) var result []*PkgInfo for _, entry := range entries { var dpkg packageMetadata if err := json.Unmarshal(entry, &dpkg); err != nil { clog.Debugf(ctx, "unable to parse dpkg package info, err %s, raw - %s", err, string(entry)) continue } pkg := pkgInfoFromPackageMetadata(dpkg) if dpkg.Status != "installed" { continue } result = append(result, pkg) } return result } // DpkgInstall installs a deb package. func DpkgInstall(ctx context.Context, path string) error { _, err := run(ctx, dpkg, append(dpkgInstallArgs, path)) return err }