mirror of
https://github.com/replicatedhq/troubleshoot.git
synced 2026-02-14 18:29:53 +00:00
feat(host_sysctl): add host sysctl collector (#1676)
* feat(host_sysctl): add host sysctl collector * chore: add examples * Update pkg/collect/host_sysctl.go Co-authored-by: Evans Mungai <evans@replicated.com> * chore: use sysctl package vs exec calls * chore: make linter happy * chore: make schemas * chore: go back to sysctl exec * chore: make linter happy --------- Co-authored-by: Evans Mungai <evans@replicated.com>
This commit is contained in:
@@ -17393,6 +17393,13 @@ spec:
|
||||
- CIDRRangeAlloc
|
||||
- desiredCIDR
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
systemPackages:
|
||||
properties:
|
||||
amzn:
|
||||
|
||||
@@ -1719,6 +1719,13 @@ spec:
|
||||
- CIDRRangeAlloc
|
||||
- desiredCIDR
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
systemPackages:
|
||||
properties:
|
||||
amzn:
|
||||
|
||||
@@ -1719,6 +1719,13 @@ spec:
|
||||
- CIDRRangeAlloc
|
||||
- desiredCIDR
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
systemPackages:
|
||||
properties:
|
||||
amzn:
|
||||
|
||||
@@ -20366,6 +20366,13 @@ spec:
|
||||
- CIDRRangeAlloc
|
||||
- desiredCIDR
|
||||
type: object
|
||||
sysctl:
|
||||
properties:
|
||||
collectorName:
|
||||
type: string
|
||||
exclude:
|
||||
type: BoolString
|
||||
type: object
|
||||
systemPackages:
|
||||
properties:
|
||||
amzn:
|
||||
|
||||
8
examples/collect/host/sysctl.yaml
Normal file
8
examples/collect/host/sysctl.yaml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: HostCollector
|
||||
metadata:
|
||||
name: sysctl
|
||||
spec:
|
||||
collectors:
|
||||
- sysctl:
|
||||
collectorName: sysctl
|
||||
10
examples/preflight/host/sysctl.yaml
Normal file
10
examples/preflight/host/sysctl.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
apiVersion: troubleshoot.sh/v1beta2
|
||||
kind: HostPreflight
|
||||
metadata:
|
||||
name: sysctl
|
||||
spec:
|
||||
collectors:
|
||||
- sysctl:
|
||||
collectorName: sysctl
|
||||
#TODO add analyzer once implemented
|
||||
analyzers: []
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
88
pkg/collect/host_sysctl.go
Normal file
88
pkg/collect/host_sysctl.go
Normal file
@@ -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 <key> = <value> where in Darwin you get <key> : <value>
|
||||
// where <value> 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
|
||||
}
|
||||
173
pkg/collect/host_sysctl_test.go
Normal file
173
pkg/collect/host_sysctl_test.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -15124,6 +15124,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sysctl": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collectorName": {
|
||||
"type": "string"
|
||||
},
|
||||
"exclude": {
|
||||
"oneOf": [{"type": "string"},{"type": "boolean"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemPackages": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -19712,6 +19712,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"sysctl": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"collectorName": {
|
||||
"type": "string"
|
||||
},
|
||||
"exclude": {
|
||||
"oneOf": [{"type": "string"},{"type": "boolean"}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"systemPackages": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user