packages/zypper.go (282 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/exec" "regexp" "runtime" "strconv" "strings" "github.com/GoogleCloudPlatform/osconfig/clog" "github.com/GoogleCloudPlatform/osconfig/osinfo" "github.com/GoogleCloudPlatform/osconfig/util" ) var ( zypper string // zypperInstallArgs is zypper command to install patches, packages zypperInstallArgs = []string{"--gpg-auto-import-keys", "--non-interactive", "install", "--auto-agree-with-licenses"} zypperRemoveArgs = []string{"--non-interactive", "remove"} zypperListUpdatesArgs = []string{"--gpg-auto-import-keys", "-q", "list-updates"} zypperListPatchesArgs = []string{"--gpg-auto-import-keys", "-q", "list-patches"} zypperPatchInfoArgs = []string{"info", "-t", "patch"} ) func init() { if runtime.GOOS != "windows" { zypper = "/usr/bin/zypper" } ZypperExists = util.Exists(zypper) } type zypperListPatchOpts struct { categories []string severities []string withOptional bool all bool } // ZypperListOption is patch list options type ZypperListOption func(opts *zypperListPatchOpts) // ZypperListPatchCategories is zypper list option to provide category filter func ZypperListPatchCategories(categories []string) ZypperListOption { return func(args *zypperListPatchOpts) { args.categories = categories } } // ZypperListPatchSeverities is zypper list option to provide severity filter func ZypperListPatchSeverities(severities []string) ZypperListOption { return func(args *zypperListPatchOpts) { args.severities = severities } } // ZypperListPatchWithOptional is zypper list option to also list optional patches func ZypperListPatchWithOptional(withOptional bool) ZypperListOption { return func(args *zypperListPatchOpts) { args.withOptional = withOptional } } // ZypperListPatchAll is zypper list option to all all patches func ZypperListPatchAll(all bool) ZypperListOption { return func(args *zypperListPatchOpts) { args.all = all } } // InstallZypperPackages Installs zypper packages func InstallZypperPackages(ctx context.Context, pkgs []string) error { // TODO: Add retries when the it fails with exit code: 7 - which means zypper is locked by another process id. _, err := run(ctx, zypper, append(zypperInstallArgs, pkgs...)) return err } // ZypperInstall installs zypper patches and packages func ZypperInstall(ctx context.Context, patches []*ZypperPatch, pkgs []*PkgInfo) error { args := zypperInstallArgs // https://www.mankier.com/8/zypper#Concepts-Package_Types use patch install // for single patch and package installs for _, patch := range patches { args = append(args, "patch:"+patch.Name) } for _, pkg := range pkgs { args = append(args, "package:"+pkg.Name) } stdout, stderr, err := runner.Run(ctx, exec.CommandContext(ctx, zypper, args...)) // https://en.opensuse.org/SDB:Zypper_manual#EXIT_CODES if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { // ZYPPER_EXIT_INF_REBOOT_NEEDED if exitErr.ExitCode() == 102 { err = nil } } else { err = fmt.Errorf("error running %s with args %q: %v, stdout: %q, stderr: %q", zypper, args, err, stdout, stderr) } } return err } // RemoveZypperPackages installed Zypper packages. func RemoveZypperPackages(ctx context.Context, pkgs []string) error { _, err := run(ctx, zypper, append(zypperRemoveArgs, pkgs...)) return err } func parseZypperUpdates(data []byte) []*PkgInfo { /* S | Repository | Name | Current Version | Available Version | Arch --+---------------------+------------------------+-----------------+-------------------+------- v | SLES12-SP3-Updates | at | 3.1.14-7.3 | 3.1.14-8.3.1 | x86_64 v | SLES12-SP3-Updates | autoyast2-installation | 3.2.17-1.3 | 3.2.22-2.9.2 | noarch ... */ lines := bytes.Split(bytes.TrimSpace(data), []byte("\n")) var pkgs []*PkgInfo for _, ln := range lines { pkg := bytes.Split(ln, []byte("|")) if len(pkg) != 6 || string(bytes.TrimSpace(pkg[0])) != "v" { continue } name := string(bytes.TrimSpace(pkg[2])) arch := string(bytes.TrimSpace(pkg[5])) ver := string(bytes.TrimSpace(pkg[4])) pkgs = append(pkgs, &PkgInfo{Name: name, Arch: osinfo.NormalizeArchitecture(arch), Version: ver}) } return pkgs } // ZypperUpdates queries for all available zypper updates. func ZypperUpdates(ctx context.Context) ([]*PkgInfo, error) { out, err := run(ctx, zypper, zypperListUpdatesArgs) if err != nil { return nil, err } return parseZypperUpdates(out), nil } func parseZypperPatches(ctx context.Context, data []byte) ([]*ZypperPatch, []*ZypperPatch) { /* Repository | Name | Category | Severity | Interactive | Status | Summary ------------------------------------+---------------------------------------------+-------------+-----------+-------------+------------+------------------------------------------------------------ SLE-Module-Basesystem15-SP1-Updates | SUSE-SLE-Module-Basesystem-15-SP1-2019-1206 | security | low | --- | applied | Security update for bzip2 SLE-Module-Basesystem15-SP1-Updates | SUSE-SLE-Module-Basesystem-15-SP1-2019-1221 | security | moderate | --- | applied | Security update for libxslt SLE-Module-Basesystem15-SP1-Updates | SUSE-SLE-Module-Basesystem-15-SP1-2019-1229 | recommended | moderate | --- | not needed | Recommended update for sensors SLE-Module-Basesystem15-SP1-Updates | SUSE-SLE-Module-Basesystem-15-SP1-2019-1258 | recommended | moderate | --- | needed | Recommended update for postfix */ lines := bytes.Split(bytes.TrimSpace(data), []byte("\n")) var installed []*ZypperPatch var available []*ZypperPatch for _, ln := range lines { patch, status, err := parseZypperPatch(ln) if err != nil { clog.Debugf(ctx, "skipping a line from zypper patch output: %s", err) continue } switch status { case "needed": available = append(available, patch) case "applied": installed = append(installed, patch) default: continue } } return installed, available } func parseZypperPatch(tableLine []byte) (*ZypperPatch, string, error) { patch := bytes.Split(tableLine, []byte("|")) if len(patch) < 7 || len(patch) > 8 { return nil, "", fmt.Errorf("not parsable zypper patch line; expected 7 or 8 segments, got - %d; this usually isn't an error; line: %s", len(patch), string(tableLine)) } name := string(bytes.TrimSpace(patch[1])) category := string(bytes.TrimSpace(patch[2])) severity := string(bytes.TrimSpace(patch[3])) status := string(bytes.TrimSpace(patch[5])) summary := string(bytes.TrimSpace(patch[6])) if len(patch) == 8 { summary = string(bytes.TrimSpace(patch[7])) } return &ZypperPatch{Name: name, Category: category, Severity: severity, Summary: summary}, status, nil } func zypperPatches(ctx context.Context, opts ...ZypperListOption) ([]byte, error) { zOpts := &zypperListPatchOpts{ categories: nil, severities: nil, withOptional: false, all: false, } for _, opt := range opts { opt(zOpts) } args := zypperListPatchesArgs for _, c := range zOpts.categories { args = append(args, "--category="+c) } for _, s := range zOpts.severities { args = append(args, "--severity="+s) } if zOpts.withOptional { args = append(args, "--with-optional") } // As per zypper's current implementation, // --all is ignored if we have any filters on any other // field. if zOpts.all || (len(zOpts.severities)+len(zOpts.categories)) <= 0 { args = append(args, "--all") } return run(ctx, zypper, args) } // ZypperPatches queries for all available zypper patches. func ZypperPatches(ctx context.Context, opts ...ZypperListOption) ([]*ZypperPatch, error) { out, err := zypperPatches(ctx, opts...) if err != nil { return nil, err } _, patches := parseZypperPatches(ctx, out) return patches, nil } // ZypperInstalledPatches queries for all installed zypper patches. func ZypperInstalledPatches(ctx context.Context, opts ...ZypperListOption) ([]*ZypperPatch, error) { out, err := zypperPatches(ctx, opts...) if err != nil { return nil, err } patches, _ := parseZypperPatches(ctx, out) return patches, nil } func zypperPatchInfo(ctx context.Context, patches []string) ([]byte, error) { args := zypperPatchInfoArgs for _, name := range patches { args = append(args, name) } return run(ctx, zypper, args) } func parseZypperPatchInfo(out []byte) (map[string][]string, error) { /* Loading repository data... Reading installed packages... Information for patch SUSE-SLE-SERVER-12-SP4-2019-2974: ------------------------------------------------------- Repository : SLES12-SP4-Updates Name : SUSE-SLE-SERVER-12-SP4-2019-2974 Version : 1 Arch : noarch Vendor : maint-coord@suse.de Status : needed Category : recommended Severity : important Created On : Thu Nov 14 13:17:48 2019 Interactive : --- Summary : Recommended update for irqbalance Description : This update for irqbalance fixes the following issues: - Irqbalanced spreads the IRQs between the available virtual machines. (bsc#1119465, bsc#1154905) Provides : patch:SUSE-SLE-SERVER-12-SP4-2019-2974 = 1 Conflicts : [2] irqbalance.src < 1.1.0-9.3.1 irqbalance.x86_64 < 1.1.0-9.3.1 */ patchInfo := make(map[string][]string) var validConflictLine = regexp.MustCompile(`\s*Conflicts\s*:\s*\[\d*\]\s*`) var conflictLineExtract = regexp.MustCompile(`\[[0-9]+\]`) var nameLine = regexp.MustCompile(`\s*Name\s*:\s*`) lines := bytes.Split(bytes.TrimSpace(out), []byte("\n")) i := 0 for { // find the name line for ; i < len(lines); i++ { b := nameLine.Find([]byte(lines[i])) if b != nil { break } } if i >= len(lines) { // we do not have any more patch info blobs break } parts := strings.Split(string(lines[i]), ":") i++ if len(parts) != 2 { return nil, fmt.Errorf("invalid name output") } patchName := strings.Trim(parts[1], " ") for ; i < len(lines); i++ { b := validConflictLine.Find([]byte(lines[i])) if b != nil { // Conflicts : [2] break } } if i >= len(lines) { // did not find conflicting packages // this should not happen return nil, nil } matches := conflictLineExtract.FindAllString(string(lines[i]), -1) if len(matches) != 1 { return nil, fmt.Errorf("invalid patch info") } // get the number of package lines to parse conflicts := strings.Trim(matches[0], "[") conflicts = strings.Trim(conflicts, "]") conflictLines, err := strconv.Atoi(conflicts) if err != nil { return nil, fmt.Errorf("invalid patch info: invalid conflict info") } ctr := i + 1 ctrEnd := ctr + conflictLines for ; ctr < ctrEnd; ctr++ { //libsolv.src < 0.6.36-2.27.19.8 //libsolv-tools.x86_64 < 0.6.36-2.27.19.8 //libzypp.src < 16.20.2-27.60.4 //libzypp.x86_64 < 16.20.2-27.60.4 //perl-solv.x86_64 < 0.6.36-2.27.19.8 //python-solv.x86_64 < 0.6.36-2.27.19.8 //zypper.src < 1.13.54-18.40.2 //zypper.x86_64 < 1.13.54-18.40.2 //zypper-log < 1.13.54-18.40.2 //srcpackage:ruby2.5 < 2.5.9-150000.4.29.1 //ruby2.5.noarch < 2.5.9-150000.4.29.1 //ruby2.5.x86_64 < 2.5.9-150000.4.29.1 //srcpackage:zypper //zypper-log < 1.14.64-150400.3.32.1 //zypper-needs-restarting < 1.14.64-150400.3.32.1 parts := strings.Split(string(lines[ctr]), "<") if len(parts) != 2 { return nil, fmt.Errorf("invalid package info, can't parse line: %v", string(lines[ctr])) } nameArch := parts[0] pkgName := "" if strings.Contains(nameArch, "srcpackage:") { colonIdx := strings.Index(nameArch, ":") pkgName = strings.Trim(nameArch[colonIdx+1:], " ") if len(pkgName) == 0 { return nil, fmt.Errorf("invalid package info, can't parse line: %v", string(lines[ctr])) } } else { // Get the last index to handle the case if pkg has float version // (e.g. `ruby2.5.noarch < 2.5.9-150000.4.29.1`) lastDotIdx := strings.LastIndex(nameArch, ".") // In case if there's NO dot exist, then the package name doen't contain // the architecture details (`e.g. zypper-log < 1.14.64-150400.3.32.1`) if lastDotIdx == -1 { pkgName = strings.Trim(nameArch, " ") } else { pkgName = strings.Trim(nameArch[:lastDotIdx], " ") } } patches, ok := patchInfo[pkgName] if !ok { patches = make([]string, 0) } patches = append(patches, patchName) patchInfo[pkgName] = patches } // set i for next patch information blob i = ctrEnd } // TODO: instead of returning a map of <string, []string> // make it more concrete type returns with more information // about the package and patch if len(patchInfo) == 0 { return nil, fmt.Errorf("invalid patch information, did not find patch blobs") } return patchInfo, nil } // ZypperPackagesInPatch returns the list of patches, a package upgrade belongs to func ZypperPackagesInPatch(ctx context.Context, patches []*ZypperPatch) (map[string][]string, error) { if len(patches) == 0 { return make(map[string][]string), nil } var patchNames []string for _, patch := range patches { patchNames = append(patchNames, patch.Name) } out, err := zypperPatchInfo(ctx, patchNames) if err != nil { return nil, err } return parseZypperPatchInfo(out) }