packages/yum.go (137 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"
"fmt"
"os"
"os/exec"
"runtime"
"slices"
"strings"
"github.com/GoogleCloudPlatform/osconfig/clog"
"github.com/GoogleCloudPlatform/osconfig/osinfo"
"github.com/GoogleCloudPlatform/osconfig/util"
)
var (
yum string
yumInstallArgs = []string{"install", "--assumeyes"}
yumRemoveArgs = []string{"remove", "--assumeyes"}
yumCheckUpdateArgs = []string{"check-update", "--assumeyes"}
yumListUpdatesArgs = []string{"update", "--assumeno", "--cacheonly", "--color=never"}
yumListUpdateMinimalArgs = []string{"update-minimal", "--assumeno", "--cacheonly", "--color=never"}
)
func init() {
if runtime.GOOS != "windows" {
yum = "/usr/bin/yum"
}
YumExists = util.Exists(yum)
}
type yumUpdateOpts struct {
security bool
minimal bool
}
// YumUpdateOption is an option for yum update.
type YumUpdateOption func(*yumUpdateOpts)
// YumUpdateSecurity returns a YumUpdateOption that specifies the --security flag should
// be used.
func YumUpdateSecurity(security bool) YumUpdateOption {
return func(args *yumUpdateOpts) {
args.security = security
}
}
// YumUpdateMinimal returns a YumUpdateOption that specifies the update-minimal
// command should be used.
func YumUpdateMinimal(minimal bool) YumUpdateOption {
return func(args *yumUpdateOpts) {
args.minimal = minimal
}
}
// InstallYumPackages installs yum packages.
func InstallYumPackages(ctx context.Context, pkgs []string) error {
_, err := run(ctx, yum, append(yumInstallArgs, pkgs...))
return err
}
// RemoveYumPackages removes yum packages.
func RemoveYumPackages(ctx context.Context, pkgs []string) error {
_, err := run(ctx, yum, append(yumRemoveArgs, pkgs...))
return err
}
func parseYumUpdates(data []byte) []*PkgInfo {
/*
Last metadata expiration check: 0:11:22 ago on Tue 12 Nov 2019 12:13:38 AM UTC.
Dependencies resolved.
=================================================================================================================================================================================
Package Arch Version Repository Size
=================================================================================================================================================================================
Installing:
kernel x86_64 2.6.32-754.24.3.el6 updates 32 M
Updating:
google-compute-engine noarch 1:20190916.00-g2.el6 google-compute-engine 18 k
kernel-firmware noarch 2.6.32-754.24.3.el6 updates 29 M
libudev x86_64 147-2.74.el6_10 updates 78 k
nspr x86_64 4.21.0-1.el6_10 updates 114 k
google-cloud-sdk noarch 270.0.0-1 google-cloud-sdk 36 M
Transaction Summary
=================================================================================================================================================================================
Upgrade 5 Packages
Total download size: 36 M
Operation aborted.
*/
lines := bytes.Split(bytes.TrimSpace(data), []byte("\n"))
var pkgs []*PkgInfo
var upgrading bool
packagesInstallOrUpdateKeywords := []string{"Upgrading:", "Updating:", "Installing:", "Installing dependencies:", "Installing weak dependencies:"}
for _, ln := range lines {
pkg := bytes.Fields(ln)
if len(pkg) == 0 {
continue
}
// Continue until we see one of the installing/upgrading keywords section.
// Yum has this as Updating, dnf is Upgrading.
if slices.Contains(packagesInstallOrUpdateKeywords, string(bytes.Join(pkg, []byte(" ")))) {
upgrading = true
continue
} else if !upgrading {
continue
}
// A package line should have 6 fields, break unless this is a 'replacing' entry.
if len(pkg) < 6 {
if string(pkg[0]) == "replacing" {
continue
}
break
}
pkgs = append(pkgs, &PkgInfo{Name: string(pkg[0]), Arch: osinfo.NormalizeArchitecture(string(pkg[1])), RawArch: string(pkg[1]), Version: string(pkg[2])})
}
return pkgs
}
func getYumTXFile(data []byte) string {
/* The last lines of a non-complete yum update where the transaction
is saved look like:
Exiting on user command
Your transaction was saved, rerun it with:
yum load-transaction /tmp/yum_save_tx.2022-10-12.20-26.j3auah.yumtx
*/
lines := bytes.Split(bytes.TrimSpace(data), []byte("\n"))
for _, ln := range lines {
flds := bytes.Fields(ln)
if len(flds) == 3 && string(flds[1]) == "load-transaction" && strings.HasPrefix(string(flds[2]), "/tmp/yum_save_tx.") {
return string(flds[2])
}
}
return ""
}
// YumUpdates queries for all available yum updates.
func YumUpdates(ctx context.Context, opts ...YumUpdateOption) ([]*PkgInfo, error) {
// We just use check-update to ensure all repo keys are synced as we run
// update with --assumeno.
stdout, stderr, err := runner.Run(ctx, exec.CommandContext(ctx, yum, yumCheckUpdateArgs...))
// Exit code 0 means no updates, 100 means there are updates.
if err == nil {
return nil, nil
}
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 100 {
err = nil
}
}
// Since we don't get good error codes from 'yum update' exit now if there is an issue.
if err != nil {
return nil, fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", yum, yumCheckUpdateArgs, err, stdout, stderr)
}
return listAndParseYumPackages(ctx, opts...)
}
func listAndParseYumPackages(ctx context.Context, opts ...YumUpdateOption) ([]*PkgInfo, error) {
yumOpts := &yumUpdateOpts{
security: false,
minimal: false,
}
for _, opt := range opts {
opt(yumOpts)
}
args := yumListUpdatesArgs
if yumOpts.minimal {
args = yumListUpdateMinimalArgs
}
if yumOpts.security {
args = append(args, "--security")
}
stdout, stderr, err := ptyrunner.Run(ctx, exec.CommandContext(ctx, yum, args...))
if err != nil {
return nil, fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", yum, args, err, stdout, stderr)
}
if stdout == nil {
return nil, nil
}
// Some versions of yum will leave a transaction file in /tmp when update
// is run with --assumeno. This will attempt to delete that file.
yumTXFile := getYumTXFile(stdout)
if yumTXFile != "" {
clog.Debugf(ctx, "Removing yum tx file: %s", yumTXFile)
if err := os.Remove(yumTXFile); err != nil {
clog.Debugf(ctx, "Error deleting yum tx file %s: %v", yumTXFile, err)
}
}
pkgs := parseYumUpdates(stdout)
if len(pkgs) == 0 {
// This means we could not parse any packages and instead got an error from yum.
return nil, fmt.Errorf("error checking for yum updates, non-zero error code from 'yum update' but no packages parsed, stdout: %q", stdout)
}
return pkgs, nil
}