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
}