From 6498c34da5f01a18b732a5d437af78c8429dc215 Mon Sep 17 00:00:00 2001 From: Andrew Reed Date: Mon, 15 Feb 2021 18:41:48 +0000 Subject: [PATCH] Analyze ipv4 interfaces Co-authored-by: Andrew Lavery --- examples/preflight/host-ipv4-interfaces.yaml | 19 +++ pkg/analyze/analyzer.go | 8 + pkg/analyze/host_ipv4interfaces.go | 138 ++++++++++++++++++ pkg/analyze/host_ipv4interfaces_test.go | 101 +++++++++++++ .../v1beta2/hostanalyzer_shared.go | 7 + .../v1beta2/zz_generated.deepcopy.go | 32 ++++ pkg/collect/host_collector.go | 2 + pkg/collect/host_ipv4interfaces.go | 40 +++++ 8 files changed, 347 insertions(+) create mode 100644 examples/preflight/host-ipv4-interfaces.yaml create mode 100644 pkg/analyze/host_ipv4interfaces.go create mode 100644 pkg/analyze/host_ipv4interfaces_test.go create mode 100644 pkg/collect/host_ipv4interfaces.go diff --git a/examples/preflight/host-ipv4-interfaces.yaml b/examples/preflight/host-ipv4-interfaces.yaml new file mode 100644 index 00000000..9aa5cd62 --- /dev/null +++ b/examples/preflight/host-ipv4-interfaces.yaml @@ -0,0 +1,19 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: ipv4Interfaces +spec: + collectors: + - ipv4Interfaces: {} + analyzers: + - ipv4Interfaces: + outcomes: + - fail: + when: "count == 0" + message: No IPv4 interfaces detected + - warn: + when: "count >= 2" + message: Multiple IPv4 interfaces detected + - pass: + when: "count == 1" + message: IPv4 interface detected diff --git a/pkg/analyze/analyzer.go b/pkg/analyze/analyzer.go index a95b578e..a7c68471 100644 --- a/pkg/analyze/analyzer.go +++ b/pkg/analyze/analyzer.go @@ -111,6 +111,14 @@ func HostAnalyze(hostAnalyzer *troubleshootv1beta2.HostAnalyze, getFile getColle } return []*AnalyzeResult{result}, nil } + if hostAnalyzer.IPV4Interfaces != nil { + result, err := analyzeHostIPV4Interfaces(hostAnalyzer.IPV4Interfaces, getFile) + if err != nil { + return nil, err + } + return []*AnalyzeResult{result}, nil + } + return nil, errors.New("invalid analyzer") } diff --git a/pkg/analyze/host_ipv4interfaces.go b/pkg/analyze/host_ipv4interfaces.go new file mode 100644 index 00000000..4c08a763 --- /dev/null +++ b/pkg/analyze/host_ipv4interfaces.go @@ -0,0 +1,138 @@ +package analyzer + +import ( + "encoding/json" + "fmt" + "net" + "strconv" + "strings" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +) + +func analyzeHostIPV4Interfaces(hostAnalyzer *troubleshootv1beta2.IPV4InterfacesAnalyze, getCollectedFileContents func(string) ([]byte, error)) (*AnalyzeResult, error) { + contents, err := getCollectedFileContents("system/ipv4Interfaces.json") + if err != nil { + return nil, errors.Wrap(err, "failed to get collected file") + } + + var ipv4Interfaces []net.Interface + if err := json.Unmarshal(contents, &ipv4Interfaces); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal ipv4Interfaces") + } + + result := AnalyzeResult{} + + title := hostAnalyzer.CheckName + if title == "" { + title = "IPv4 Interfaces" + } + result.Title = title + + for _, outcome := range hostAnalyzer.Outcomes { + if outcome.Fail != nil { + if outcome.Fail.When == "" { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + + isMatch, err := compareHostIPV4InterfacesConditionalToActual(outcome.Fail.When, ipv4Interfaces) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %s", outcome.Fail.When) + } + + if isMatch { + result.IsFail = true + result.Message = outcome.Fail.Message + result.URI = outcome.Fail.URI + + return &result, nil + } + } else if outcome.Warn != nil { + if outcome.Warn.When == "" { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + + isMatch, err := compareHostIPV4InterfacesConditionalToActual(outcome.Warn.When, ipv4Interfaces) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %s", outcome.Warn.When) + } + + if isMatch { + result.IsWarn = true + result.Message = outcome.Warn.Message + result.URI = outcome.Warn.URI + + return &result, nil + } + } else if outcome.Pass != nil { + if outcome.Pass.When == "" { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + + isMatch, err := compareHostIPV4InterfacesConditionalToActual(outcome.Pass.When, ipv4Interfaces) + if err != nil { + return nil, errors.Wrapf(err, "failed to compare %s", outcome.Pass.When) + } + + if isMatch { + result.IsPass = true + result.Message = outcome.Pass.Message + result.URI = outcome.Pass.URI + + return &result, nil + } + } + } + + return &result, nil +} + +func compareHostIPV4InterfacesConditionalToActual(conditional string, ipv4Interfaces []net.Interface) (res bool, err error) { + parts := strings.Split(conditional, " ") + if len(parts) != 3 { + return false, fmt.Errorf("Expected exactly 3 parts in conditional, got %d", len(parts)) + } + + keyword := parts[0] + operator := parts[1] + desired := parts[2] + + if keyword != "count" { + return false, fmt.Errorf(`Only supported keyword is "count", got %q`, keyword) + } + + desiredInt, err := strconv.ParseInt(desired, 10, 64) + if err != nil { + return false, errors.Wrapf(err, "failed to parse %q as int", desired) + } + + actualCount := len(ipv4Interfaces) + + switch operator { + case "<": + return actualCount < int(desiredInt), nil + case "<=": + return actualCount <= int(desiredInt), nil + case ">": + return actualCount > int(desiredInt), nil + case ">=": + return actualCount >= int(desiredInt), nil + case "=", "==", "===": + return actualCount == int(desiredInt), nil + } + + return false, fmt.Errorf("Unknown operator %q. Supported operators are: <, <=, ==, >=, >", operator) +} diff --git a/pkg/analyze/host_ipv4interfaces_test.go b/pkg/analyze/host_ipv4interfaces_test.go new file mode 100644 index 00000000..ed4ee0d2 --- /dev/null +++ b/pkg/analyze/host_ipv4interfaces_test.go @@ -0,0 +1,101 @@ +package analyzer + +import ( + "encoding/json" + "net" + "testing" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAnalyzeIPV4Interfaces(t *testing.T) { + tests := []struct { + name string + interfaces []net.Interface + hostAnalyzer *troubleshootv1beta2.IPV4InterfacesAnalyze + result *AnalyzeResult + expectErr bool + }{ + { + name: "fail when no ipv4 interfaces detected", + interfaces: nil, + hostAnalyzer: &troubleshootv1beta2.IPV4InterfacesAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "count > 0", + Message: "IPv4 interface available", + }, + }, + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "count == 0", + Message: "No IPv4 interfaces detected", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "IPv4 Interfaces", + IsFail: true, + Message: "No IPv4 interfaces detected", + }, + }, + { + name: "pass when ipv4 interfaces detected", + interfaces: []net.Interface{ + { + Index: 1, + MTU: 1460, + HardwareAddr: net.HardwareAddr("42010a80001d"), + Name: "ens4", + }, + }, + hostAnalyzer: &troubleshootv1beta2.IPV4InterfacesAnalyze{ + Outcomes: []*troubleshootv1beta2.Outcome{ + { + Fail: &troubleshootv1beta2.SingleOutcome{ + When: "count == 0", + Message: "No IPv4 interfaces detected", + }, + }, + { + Pass: &troubleshootv1beta2.SingleOutcome{ + When: "count > 0", + Message: "IPv4 interface available", + }, + }, + }, + }, + result: &AnalyzeResult{ + Title: "IPv4 Interfaces", + IsPass: true, + Message: "IPv4 interface available", + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + b, err := json.Marshal(test.interfaces) + if err != nil { + t.Fatal(err) + } + + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + + result, err := analyzeHostIPV4Interfaces(test.hostAnalyzer, getCollectedFileContents) + if test.expectErr { + req.Error(err) + } else { + req.NoError(err) + } + + assert.Equal(t, test.result, result) + }) + } +} diff --git a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go index 739ad868..57d1fd9a 100644 --- a/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/hostanalyzer_shared.go @@ -56,6 +56,11 @@ type TCPConnectAnalyze struct { Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` } +type IPV4InterfacesAnalyze struct { + AnalyzeMeta `json:",inline" yaml:",inline"` + Outcomes []*Outcome `json:"outcomes" yaml:"outcomes"` +} + type HostAnalyze struct { CPU *CPUAnalyze `json:"cpu,omitempty" yaml:"cpu,omitempty"` // @@ -75,4 +80,6 @@ type HostAnalyze struct { BlockDevices *BlockDevicesAnalyze `json:"blockDevices" yaml:"blockDevices"` TCPConnect *TCPConnectAnalyze `json:"tcpConnect" yaml:"tcpConnect"` + + IPV4Interfaces *IPV4InterfacesAnalyze `json:"ipv4Interfaces" yaml:"ipv4Interfaces"` } diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index 44032a8a..ae164531 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -1177,6 +1177,11 @@ func (in *HostAnalyze) DeepCopyInto(out *HostAnalyze) { *out = new(TCPConnectAnalyze) (*in).DeepCopyInto(*out) } + if in.IPV4Interfaces != nil { + in, out := &in.IPV4Interfaces, &out.IPV4Interfaces + *out = new(IPV4InterfacesAnalyze) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostAnalyze. @@ -1470,6 +1475,33 @@ func (in *IPV4Interfaces) DeepCopy() *IPV4Interfaces { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IPV4InterfacesAnalyze) DeepCopyInto(out *IPV4InterfacesAnalyze) { + *out = *in + out.AnalyzeMeta = in.AnalyzeMeta + if in.Outcomes != nil { + in, out := &in.Outcomes, &out.Outcomes + *out = make([]*Outcome, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(Outcome) + (*in).DeepCopyInto(*out) + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IPV4InterfacesAnalyze. +func (in *IPV4InterfacesAnalyze) DeepCopy() *IPV4InterfacesAnalyze { + if in == nil { + return nil + } + out := new(IPV4InterfacesAnalyze) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ImagePullSecret) DeepCopyInto(out *ImagePullSecret) { *out = *in diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go index 9f822782..2b8bc1ea 100644 --- a/pkg/collect/host_collector.go +++ b/pkg/collect/host_collector.go @@ -38,6 +38,8 @@ func (c *HostCollector) RunCollectorSync() (result map[string][]byte, err error) result, err = HostBlockDevices(c) } else if c.Collect.TCPConnect != nil { result, err = HostTCPConnect(c) + } else if c.Collect.IPV4Interfaces != nil { + result, err = HostIPV4Interfaces(c) } else { err = errors.New("no spec found to run") return diff --git a/pkg/collect/host_ipv4interfaces.go b/pkg/collect/host_ipv4interfaces.go new file mode 100644 index 00000000..4c549211 --- /dev/null +++ b/pkg/collect/host_ipv4interfaces.go @@ -0,0 +1,40 @@ +package collect + +import ( + "encoding/json" + "net" + + "github.com/pkg/errors" +) + +func HostIPV4Interfaces(c *HostCollector) (map[string][]byte, error) { + var ipv4Interfaces []net.Interface + + interfaces, err := net.Interfaces() + if err != nil { + return nil, errors.Wrap(err, "list host network interfaces") + } + + for _, iface := range interfaces { + if iface.Flags&net.FlagUp == 0 { + continue + } + if iface.Flags&net.FlagLoopback != 0 { + continue + } + ip, _ := getIPv4FromInterface(&iface) + if ip == nil { + continue + } + ipv4Interfaces = append(ipv4Interfaces, iface) + } + + b, err := json.Marshal(ipv4Interfaces) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal network interfaces") + } + + return map[string][]byte{ + "system/ipv4Interfaces.json": b, + }, nil +}