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:
João Antunes
2024-11-07 18:18:11 +00:00
committed by GitHub
parent 06506ed95d
commit 77c9968ff6
13 changed files with 357 additions and 0 deletions

View File

@@ -17393,6 +17393,13 @@ spec:
- CIDRRangeAlloc
- desiredCIDR
type: object
sysctl:
properties:
collectorName:
type: string
exclude:
type: BoolString
type: object
systemPackages:
properties:
amzn:

View File

@@ -1719,6 +1719,13 @@ spec:
- CIDRRangeAlloc
- desiredCIDR
type: object
sysctl:
properties:
collectorName:
type: string
exclude:
type: BoolString
type: object
systemPackages:
properties:
amzn:

View File

@@ -1719,6 +1719,13 @@ spec:
- CIDRRangeAlloc
- desiredCIDR
type: object
sysctl:
properties:
collectorName:
type: string
exclude:
type: BoolString
type: object
systemPackages:
properties:
amzn:

View File

@@ -20366,6 +20366,13 @@ spec:
- CIDRRangeAlloc
- desiredCIDR
type: object
sysctl:
properties:
collectorName:
type: string
exclude:
type: BoolString
type: object
systemPackages:
properties:
amzn:

View File

@@ -0,0 +1,8 @@
apiVersion: troubleshoot.sh/v1beta2
kind: HostCollector
metadata:
name: sysctl
spec:
collectors:
- sysctl:
collectorName: sysctl

View 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: []

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View 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
}

View 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(&parameters)
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)
}

View File

@@ -15124,6 +15124,17 @@
}
}
},
"sysctl": {
"type": "object",
"properties": {
"collectorName": {
"type": "string"
},
"exclude": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
}
}
},
"systemPackages": {
"type": "object",
"properties": {

View File

@@ -19712,6 +19712,17 @@
}
}
},
"sysctl": {
"type": "object",
"properties": {
"collectorName": {
"type": "string"
},
"exclude": {
"oneOf": [{"type": "string"},{"type": "boolean"}]
}
}
},
"systemPackages": {
"type": "object",
"properties": {