diff --git a/config/crds/troubleshoot.sh_collectors.yaml b/config/crds/troubleshoot.sh_collectors.yaml index ba955f220..931b53d68 100644 --- a/config/crds/troubleshoot.sh_collectors.yaml +++ b/config/crds/troubleshoot.sh_collectors.yaml @@ -17393,6 +17393,13 @@ spec: - CIDRRangeAlloc - desiredCIDR type: object + sysctl: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object systemPackages: properties: amzn: diff --git a/config/crds/troubleshoot.sh_hostcollectors.yaml b/config/crds/troubleshoot.sh_hostcollectors.yaml index 701301bc7..df14701be 100644 --- a/config/crds/troubleshoot.sh_hostcollectors.yaml +++ b/config/crds/troubleshoot.sh_hostcollectors.yaml @@ -1719,6 +1719,13 @@ spec: - CIDRRangeAlloc - desiredCIDR type: object + sysctl: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object systemPackages: properties: amzn: diff --git a/config/crds/troubleshoot.sh_hostpreflights.yaml b/config/crds/troubleshoot.sh_hostpreflights.yaml index 236862169..120852ea9 100644 --- a/config/crds/troubleshoot.sh_hostpreflights.yaml +++ b/config/crds/troubleshoot.sh_hostpreflights.yaml @@ -1719,6 +1719,13 @@ spec: - CIDRRangeAlloc - desiredCIDR type: object + sysctl: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object systemPackages: properties: amzn: diff --git a/config/crds/troubleshoot.sh_supportbundles.yaml b/config/crds/troubleshoot.sh_supportbundles.yaml index 7430d68f3..e6ed0e2a2 100644 --- a/config/crds/troubleshoot.sh_supportbundles.yaml +++ b/config/crds/troubleshoot.sh_supportbundles.yaml @@ -20366,6 +20366,13 @@ spec: - CIDRRangeAlloc - desiredCIDR type: object + sysctl: + properties: + collectorName: + type: string + exclude: + type: BoolString + type: object systemPackages: properties: amzn: diff --git a/examples/collect/host/sysctl.yaml b/examples/collect/host/sysctl.yaml new file mode 100644 index 000000000..771b65495 --- /dev/null +++ b/examples/collect/host/sysctl.yaml @@ -0,0 +1,8 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostCollector +metadata: + name: sysctl +spec: + collectors: + - sysctl: + collectorName: sysctl diff --git a/examples/preflight/host/sysctl.yaml b/examples/preflight/host/sysctl.yaml new file mode 100644 index 000000000..c7a7170f3 --- /dev/null +++ b/examples/preflight/host/sysctl.yaml @@ -0,0 +1,10 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: sysctl +spec: + collectors: + - sysctl: + collectorName: sysctl + #TODO add analyzer once implemented + analyzers: [] diff --git a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go index 6cdbb7bc7..383d6102a 100644 --- a/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go +++ b/pkg/apis/troubleshoot/v1beta2/hostcollector_shared.go @@ -231,6 +231,10 @@ type HostDNS struct { Hostnames []string `json:"hostnames" yaml:"hostnames"` } +type HostSysctl struct { + HostCollectorMeta `json:",inline" yaml:",inline"` +} + type HostCollect struct { CPU *CPU `json:"cpu,omitempty" yaml:"cpu,omitempty"` Memory *Memory `json:"memory,omitempty" yaml:"memory,omitempty"` @@ -260,6 +264,7 @@ type HostCollect struct { HostCGroups *HostCGroups `json:"cgroups,omitempty" yaml:"cgroups,omitempty"` HostDNS *HostDNS `json:"dns,omitempty" yaml:"dns,omitempty"` NetworkNamespaceConnectivity *HostNetworkNamespaceConnectivity `json:"networkNamespaceConnectivity,omitempty" yaml:"networkNamespaceConnectivity,omitempty"` + HostSysctl *HostSysctl `json:"sysctl,omitempty" yaml:"sysctl,omitempty"` } // GetName gets the name of the collector diff --git a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go index a7e810864..74d0ddb92 100644 --- a/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go +++ b/pkg/apis/troubleshoot/v1beta2/zz_generated.deepcopy.go @@ -2160,6 +2160,11 @@ func (in *HostCollect) DeepCopyInto(out *HostCollect) { *out = new(HostNetworkNamespaceConnectivity) (*in).DeepCopyInto(*out) } + if in.HostSysctl != nil { + in, out := &in.HostSysctl, &out.HostSysctl + *out = new(HostSysctl) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostCollect. @@ -2686,6 +2691,22 @@ func (in *HostServicesAnalyze) DeepCopy() *HostServicesAnalyze { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HostSysctl) DeepCopyInto(out *HostSysctl) { + *out = *in + in.HostCollectorMeta.DeepCopyInto(&out.HostCollectorMeta) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HostSysctl. +func (in *HostSysctl) DeepCopy() *HostSysctl { + if in == nil { + return nil + } + out := new(HostSysctl) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *HostSystemPackages) DeepCopyInto(out *HostSystemPackages) { *out = *in diff --git a/pkg/collect/host_collector.go b/pkg/collect/host_collector.go index ff93e6655..aa73a2f65 100644 --- a/pkg/collect/host_collector.go +++ b/pkg/collect/host_collector.go @@ -101,6 +101,8 @@ func GetHostCollector(collector *troubleshootv1beta2.HostCollect, bundlePath str return &CollectHostDNS{collector.HostDNS, bundlePath}, true case collector.NetworkNamespaceConnectivity != nil: return &CollectHostNetworkNamespaceConnectivity{collector.NetworkNamespaceConnectivity, bundlePath}, true + case collector.HostSysctl != nil: + return &CollectHostSysctl{collector.HostSysctl, bundlePath}, true default: return nil, false } diff --git a/pkg/collect/host_sysctl.go b/pkg/collect/host_sysctl.go new file mode 100644 index 000000000..08aeb8803 --- /dev/null +++ b/pkg/collect/host_sysctl.go @@ -0,0 +1,88 @@ +package collect + +import ( + "bufio" + "bytes" + "encoding/json" + "os/exec" + "regexp" + + "github.com/pkg/errors" + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "k8s.io/klog/v2" +) + +// Ensure `CollectHostSysctl` implements `HostCollector` interface at compile time. +var _ HostCollector = (*CollectHostSysctl)(nil) + +// Helper var to allow stubbing `exec.Command` for tests +var execCommand = exec.Command + +const HostSysctlPath = `host-collectors/system/sysctl.json` + +type CollectHostSysctl struct { + hostCollector *troubleshootv1beta2.HostSysctl + BundlePath string +} + +func (c *CollectHostSysctl) Title() string { + return hostCollectorTitleOrDefault(c.hostCollector.HostCollectorMeta, "Sysctl") +} + +func (c *CollectHostSysctl) IsExcluded() (bool, error) { + return isExcluded(c.hostCollector.Exclude) +} + +func (c *CollectHostSysctl) Collect(progressChan chan<- interface{}) (map[string][]byte, error) { + klog.V(2).Info("Running sysctl collector") + cmd := execCommand("sysctl", "-a") + out, err := cmd.Output() + if err != nil { + klog.V(2).ErrorS(err, "failed to run sysctl") + if exitErr, ok := err.(*exec.ExitError); ok { + return nil, errors.Wrapf(err, "failed to run sysctl exit-code=%d stderr=%s", exitErr.ExitCode(), exitErr.Stderr) + } else { + return nil, errors.Wrap(err, "failed to run sysctl") + } + } + values := parseSysctlParameters(out) + + payload, err := json.Marshal(values) + if err != nil { + klog.V(2).ErrorS(err, "failed to marshal data to json") + return nil, errors.Wrap(err, "failed to marshal data to json") + } + + output := NewResult() + output.SaveResult(c.BundlePath, HostSysctlPath, bytes.NewBuffer(payload)) + klog.V(2).Info("Finished writing JSON output") + return output, nil +} + +// Linux sysctl outputs = where in Darwin you get : +// where can be a string, number or multiple space separated strings +var sysctlLineRegex = regexp.MustCompile(`(\S+)\s*(=|:)\s*(.*)$`) + +func parseSysctlParameters(output []byte) map[string]string { + scanner := bufio.NewScanner(bytes.NewReader(output)) + + result := map[string]string{} + for scanner.Scan() { + l := scanner.Text() + // <1:key> <2:separator> <3:value> + matches := sysctlLineRegex.FindStringSubmatch(l) + + switch len(matches) { + // there are no matches for the value and separator, ignore and log + case 0, 1, 2: + klog.V(2).Infof("skipping sysctl line since we found no matches for it: %s", l) + // key exists but value could be empty, register as an empty string value but log something for reference + case 3: + klog.V(2).Infof("found no value for sysctl line, keeping it with an empty value: %s", l) + result[matches[1]] = "" + default: + result[matches[1]] = matches[3] + } + } + return result +} diff --git a/pkg/collect/host_sysctl_test.go b/pkg/collect/host_sysctl_test.go new file mode 100644 index 000000000..328020fff --- /dev/null +++ b/pkg/collect/host_sysctl_test.go @@ -0,0 +1,173 @@ +package collect + +import ( + "encoding/json" + "os/exec" + "testing" + + troubleshootv1beta2 "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "github.com/replicatedhq/troubleshoot/pkg/multitype" + "github.com/stretchr/testify/require" +) + +type execStub struct { + cmd *exec.Cmd + name string + args []string +} + +func (s *execStub) testExecCommand(name string, args ...string) *exec.Cmd { + s.name = name + s.args = args + return s.cmd +} + +func setExecStub(c *exec.Cmd) { + e := &execStub{ + cmd: c, + } + execCommand = e.testExecCommand +} + +func TestCollectHostSysctl_Error(t *testing.T) { + req := require.New(t) + setExecStub(exec.Command("sh", "-c", "exit 1")) + + tmpDir := t.TempDir() + c := &CollectHostSysctl{ + BundlePath: tmpDir, + } + + _, err := c.Collect(nil) + req.ErrorContains(err, "failed to run sysctl exit-code=1") +} + +func TestCollectHostSysctl_(t *testing.T) { + tests := []struct { + name string + cmdOut string + expected map[string]string + }{ + { + name: "linux", + cmdOut: ` + net.ipv4.conf.all.arp_evict_nocarrier = 1 + net.ipv4.conf.all.arp_filter = 0 + net.ipv4.conf.all.arp_ignore = 0 + `, + expected: map[string]string{ + "net.ipv4.conf.all.arp_evict_nocarrier": "1", + "net.ipv4.conf.all.arp_filter": "0", + "net.ipv4.conf.all.arp_ignore": "0", + }, + }, + { + name: "darwin", + cmdOut: ` + kern.prng.pool_31.max_sample_count: 16420665 + kern.crypto.sha1: SHA1_VNG_ARM + kern.crypto.sha512: SHA512_VNG_ARM_HW + kern.crypto.aes.ecb.encrypt: AES_ECB_ARM + kern.monotonicclock: 4726514 + kern.monotonicclock_usecs: 4726514658233 13321990885027 + `, + expected: map[string]string{ + "kern.prng.pool_31.max_sample_count": "16420665", + "kern.crypto.sha1": "SHA1_VNG_ARM", + "kern.crypto.sha512": "SHA512_VNG_ARM_HW", + "kern.crypto.aes.ecb.encrypt": "AES_ECB_ARM", + "kern.monotonicclock": "4726514", + "kern.monotonicclock_usecs": "4726514658233 13321990885027", + }, + }, + { + name: "skip non valid entries and keep empty values", + cmdOut: ` + net.ipv4.conf.all.arp_ignore = + kern.prng.pool_31.max_sample_count: + not-valid + net.ipv4.conf.all.arp_filter = 0 + `, + expected: map[string]string{ + "net.ipv4.conf.all.arp_ignore": "", + "kern.prng.pool_31.max_sample_count": "", + "net.ipv4.conf.all.arp_filter": "0", + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + req := require.New(t) + + setExecStub(exec.Command("echo", "-n", test.cmdOut)) // #nosec G204 + + tmpDir := t.TempDir() + c := &CollectHostSysctl{ + BundlePath: tmpDir, + } + + out, err := c.Collect(nil) + req.NoError(err) + res := CollectorResult(out) + reader, err := res.GetReader(tmpDir, HostSysctlPath) + req.NoError(err) + + parameters := map[string]string{} + err = json.NewDecoder(reader).Decode(¶meters) + req.NoError(err) + + req.Equal(test.expected, parameters) + }) + } +} + +func TestCollectHostSysctl_Title(t *testing.T) { + req := require.New(t) + + // Default title is set + c := &CollectHostSysctl{ + hostCollector: &troubleshootv1beta2.HostSysctl{ + HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{}, + }, + } + req.Equal("Sysctl", c.Title()) + + // Configured title is set + c = &CollectHostSysctl{ + hostCollector: &troubleshootv1beta2.HostSysctl{ + HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{ + CollectorName: "foobar", + }, + }, + } + req.Equal("foobar", c.Title()) +} + +func TestCollectHostSysctl_IsExcluded(t *testing.T) { + req := require.New(t) + + // Exclude is true + c := &CollectHostSysctl{ + hostCollector: &troubleshootv1beta2.HostSysctl{ + HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{ + Exclude: multitype.FromBool(true), + }, + }, + } + isExcluded, err := c.IsExcluded() + req.NoError(err) + req.Equal(true, isExcluded) + + // Exclude is false + c = &CollectHostSysctl{ + hostCollector: &troubleshootv1beta2.HostSysctl{ + HostCollectorMeta: troubleshootv1beta2.HostCollectorMeta{ + Exclude: multitype.FromBool(false), + }, + }, + } + isExcluded, err = c.IsExcluded() + req.NoError(err) + req.Equal(false, isExcluded) +} diff --git a/schemas/collector-troubleshoot-v1beta2.json b/schemas/collector-troubleshoot-v1beta2.json index 200c262b0..bd5d00b34 100644 --- a/schemas/collector-troubleshoot-v1beta2.json +++ b/schemas/collector-troubleshoot-v1beta2.json @@ -15124,6 +15124,17 @@ } } }, + "sysctl": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, "systemPackages": { "type": "object", "properties": { diff --git a/schemas/supportbundle-troubleshoot-v1beta2.json b/schemas/supportbundle-troubleshoot-v1beta2.json index 9d805017e..f6dcd8137 100644 --- a/schemas/supportbundle-troubleshoot-v1beta2.json +++ b/schemas/supportbundle-troubleshoot-v1beta2.json @@ -19712,6 +19712,17 @@ } } }, + "sysctl": { + "type": "object", + "properties": { + "collectorName": { + "type": "string" + }, + "exclude": { + "oneOf": [{"type": "string"},{"type": "boolean"}] + } + } + }, "systemPackages": { "type": "object", "properties": {