Repository: replicatedhq/troubleshoot
Pull request #321 (2021-02-15 18:57:58)

Analyze ipv4 interfaces


Patch:
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/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
+}


Test patch:
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)
+		})
+	}
+}