Files
troubleshoot/pkg/analyze/host_system_packages.go

232 lines
5.6 KiB
Go

package analyzer
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"strconv"
"strings"
"unicode"
"text/template"
"github.com/pkg/errors"
troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2"
"github.com/replicatedhq/troubleshoot/pkg/collect"
)
type AnalyzeHostSystemPackages struct {
hostAnalyzer *troubleshootv1beta2.SystemPackagesAnalyze
}
func (a *AnalyzeHostSystemPackages) Title() string {
return hostAnalyzerTitleOrDefault(a.hostAnalyzer.AnalyzeMeta, "System Packages")
}
func (a *AnalyzeHostSystemPackages) IsExcluded() (bool, error) {
return isExcluded(a.hostAnalyzer.Exclude)
}
func (a *AnalyzeHostSystemPackages) Analyze(getCollectedFileContents func(string) ([]byte, error)) ([]*AnalyzeResult, error) {
hostAnalyzer := a.hostAnalyzer
packagesFileName := "host-collectors/system/packages.json"
if a.hostAnalyzer.CollectorName != "" {
packagesFileName = fmt.Sprintf("host-collectors/system/%s-packages.json", a.hostAnalyzer.CollectorName)
}
contents, err := getCollectedFileContents(packagesFileName)
if err != nil {
return nil, errors.Wrap(err, "failed to get collected file")
}
var info collect.SystemPackagesInfo
if err := json.Unmarshal(contents, &info); err != nil {
return nil, errors.Wrap(err, "failed to unmarshal system packages info")
}
allResults := []*AnalyzeResult{}
for _, pkg := range info.Packages {
templateMap := getSystemPackageTemplateMap(pkg, info.OS, info.OSVersion)
for _, outcome := range hostAnalyzer.Outcomes {
r := AnalyzeResult{}
when := ""
if outcome.Fail != nil {
r.IsFail = true
r.Message = outcome.Fail.Message
r.URI = outcome.Fail.URI
when = outcome.Fail.When
} else if outcome.Warn != nil {
r.IsWarn = true
r.Message = outcome.Warn.Message
r.URI = outcome.Warn.URI
when = outcome.Warn.When
} else if outcome.Pass != nil {
r.IsPass = true
r.Message = outcome.Pass.Message
r.URI = outcome.Pass.URI
when = outcome.Pass.When
} else {
println("error: found an empty outcome in a systemPackages analyzer") // don't stop
continue
}
match, err := compareSystemPackagesConditionalToActual(when, templateMap)
if err != nil {
return nil, errors.Wrapf(err, "failed to compare %s", when)
}
if !match {
continue
}
tmpl := template.New("package")
// template the title
titleTmpl, err := tmpl.Parse(a.Title())
if err != nil {
return nil, errors.Wrap(err, "failed to create new title template")
}
var t bytes.Buffer
err = titleTmpl.Execute(&t, templateMap)
if err != nil {
return nil, errors.Wrap(err, "failed to execute title template")
}
r.Title = t.String()
// template the message
msgTmpl, err := tmpl.Parse(r.Message)
if err != nil {
return nil, errors.Wrap(err, "failed to create new message template")
}
var m bytes.Buffer
err = msgTmpl.Execute(&m, templateMap)
if err != nil {
return nil, errors.Wrap(err, "failed to execute message template")
}
r.Message = m.String()
// add to results, break and check the next pod
allResults = append(allResults, &r)
break
}
}
return allResults, nil
}
func getSystemPackageTemplateMap(pkg collect.SystemPackage, osName string, osVersion string) map[string]interface{} {
osVersionParts := strings.Split(osVersion, ".")
osVersionMajor, _ := strconv.ParseInt(osVersionParts[0], 10, 64)
var osVersionMinor int64
if len(osVersionParts) > 1 {
osVersionMinor, _ = strconv.ParseInt(osVersionParts[1], 10, 64)
}
m := map[string]interface{}{
"OS": osName,
"OSVersion": osVersion,
"OSVersionMajor": osVersionMajor,
"OSVersionMinor": osVersionMinor,
"Name": pkg.Name,
"Error": pkg.Error,
"ExitCode": pkg.ExitCode,
"IsInstalled": isSystemPackageInstalled(pkg),
}
for k, v := range getSystemPackageDetailsMap(pkg) {
m[k] = v
}
return m
}
func isSystemPackageInstalled(pkg collect.SystemPackage) bool {
if pkg.Error != "" {
return false
}
if pkg.ExitCode != "0" {
return false
}
if strings.Contains(pkg.Details, "not installed") {
return false
}
if strings.Contains(pkg.Details, "No matching Packages") {
return false
}
return true
}
func getSystemPackageDetailsMap(pkg collect.SystemPackage) map[string]string {
// TODO: handle multiline values
m := map[string]string{}
buffer := bytes.NewBuffer([]byte(pkg.Details))
scanner := bufio.NewScanner(buffer)
for scanner.Scan() {
line := scanner.Text()
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
continue
}
key := parts[0]
// key should start with an upper case
if !unicode.IsUpper([]rune(key)[0]) {
continue
}
// key shouldn't start with a space
if unicode.IsSpace([]rune(key)[0]) {
continue
}
// sanitize the key and value
key = strings.ReplaceAll(key, " ", "")
key = strings.ReplaceAll(key, "-", "")
value := strings.TrimSpace(parts[1])
if key == "" || value == "" {
continue
}
m[key] = value
}
return m
}
func compareSystemPackagesConditionalToActual(conditional string, templateMap map[string]interface{}) (res bool, err error) {
if conditional == "" {
return true, nil
}
tmpl := template.New("conditional")
conditionalTmpl, err := tmpl.Parse(conditional)
if err != nil {
return false, errors.Wrap(err, "failed to create new when template")
}
var when bytes.Buffer
err = conditionalTmpl.Execute(&when, templateMap)
if err != nil {
return false, errors.Wrap(err, "failed to execute when template")
}
t, err := strconv.ParseBool(when.String())
if err != nil {
return false, errors.Wrap(err, "failed to parse templated when expression as a boolean")
}
return t, nil
}