| // 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) |
| } |