@cryptotaxi247 / netdata / commits / 22265b5e6

Convert go collectors to use ndexec module for external command invocation (#21067)

Co-authored-by: ilyam8 <ilya@netdata.cloud>

Austin S. Hemmelgarn committed Sep 30, 2025 at 11:36 UTC 22265b5e6e162f82d4be7021bd3ec0142bf20c4d
52 files changed +361 -748
src/go/cmd/godplugin/main.go
+1 -1
@@ -60,7 +60,7 @@ func main() {
60 MinUpdateEvery: opts.UpdateEvery,
61 })
62
63 - a.Debugf("plugin: name=%s, version=%s", a.Name, buildinfo.Version)
63 + a.Debugf("plugin: name=%s, %s", a.Name, buildinfo.Info())
64 if u, err := user.Current(); err == nil {
65 a.Debugf("current user: name=%s, uid=%s", u.Username, u.Uid)
66 }
src/go/pkg/buildinfo/buildinfo.go
+20 -6
@@ -2,19 +2,33 @@
2
3 package buildinfo
4
5 -// The variables in this file are set during the build process using linker flags.
5 +import "fmt"
6
7 -// Version stores the agent's version number.
7 +// The following variables are set at build time using linker flags.
8 +
9 +// Version is the Netdata Agent version.
10 var Version = "v0.0.0"
11
10 -// UserConfigDir stores the path to the user configuration directory.
12 +// UserConfigDir is the path to the user configuration directory.
13 var UserConfigDir = ""
14
13 -// StockConfigDir stores the path to the stock (default) configuration directory.
15 +// StockConfigDir is the path to the stock (default) configuration directory.
16 var StockConfigDir = ""
17
16 -// PluginsDir stores the directory where pulgins were installed at build time.
18 +// PluginsDir is the path to the installed plugins directory.
19 var PluginsDir = "/usr/libexec/netdata/plugins.d"
20
19 -// NetdataBinDir stores the directory where executables were installed at build time.
21 +// NetdataBinDir is the path to the installed executables directory.
22 var NetdataBinDir = "/usr/sbin"
23 +
24 +// Info returns all build information as a single line with snake_case keys.
25 +func Info() string {
26 + return fmt.Sprintf(
27 + "version=%s user_config_dir=%s stock_config_dir=%s plugins_dir=%s netdata_bin_dir=%s",
28 + Version,
29 + UserConfigDir,
30 + StockConfigDir,
31 + PluginsDir,
32 + NetdataBinDir,
33 + )
34 +}
src/go/plugin/go.d/agent/discovery/sd/discoverer/netlistensd/ll.go
+2 -13
@@ -4,13 +4,12 @@ package netlistensd
4
5 import (
6 "context"
7 - "fmt"
7 "os"
9 - "os/exec"
8 "path/filepath"
9 "time"
10
11 "github.com/netdata/netdata/go/plugins/pkg/executable"
12 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
13 )
14
15 type localListeners interface {
@@ -38,9 +37,6 @@ type localListenersExec struct {
37 }
38
39 func (e *localListenersExec) discover(ctx context.Context) ([]byte, error) {
41 - execCtx, cancel := context.WithTimeout(ctx, e.timeout)
42 - defer cancel()
43 -
40 // TCPv4/6 and UPDv4 sockets in LISTEN state
41 // https://github.com/netdata/netdata/blob/master/src/collectors/utils/local_listeners.c
42 args := []string{
@@ -51,12 +47,5 @@ func (e *localListenersExec) discover(ctx context.Context) ([]byte, error) {
47 "no-namespaces",
48 }
49
54 - cmd := exec.CommandContext(execCtx, e.binPath, args...)
55 -
56 - bs, err := cmd.Output()
57 - if err != nil {
58 - return nil, fmt.Errorf("error on executing '%s': %v", cmd, err)
59 - }
60 -
61 - return bs, nil
50 + return ndexec.RunUnprivileged(nil, e.timeout, e.binPath, args...)
51 }
src/go/plugin/go.d/collector/adaptecraid/collector_test.go
+2 -2
@@ -49,8 +49,8 @@ func TestCollector_Init(t *testing.T) {
49 config Config
50 wantFail bool
51 }{
52 - "fails if 'ndsudo' not found": {
53 - wantFail: true,
52 + "success with default config": {
53 + wantFail: false,
54 config: New().Config,
55 },
56 }
src/go/plugin/go.d/collector/adaptecraid/exec.go
+7 -27
@@ -5,12 +5,10 @@
5 package adaptecraid
6
7 import (
8 - "context"
9 - "fmt"
10 - "os/exec"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 )
13
14 type arcconfCli interface {
@@ -18,40 +16,22 @@ type arcconfCli interface {
16 physicalDevicesInfo() ([]byte, error)
17 }
18
21 -func newArcconfCliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *arcconfCliExec {
19 +func newArcconfCliExec(timeout time.Duration, log *logger.Logger) *arcconfCliExec {
20 return &arcconfCliExec{
23 - Logger: log,
24 - ndsudoPath: ndsudoPath,
25 - timeout: timeout,
21 + Logger: log,
22 + timeout: timeout,
23 }
24 }
25
26 type arcconfCliExec struct {
27 *logger.Logger
31 -
32 - ndsudoPath string
33 - timeout time.Duration
28 + timeout time.Duration
29 }
30
31 func (e *arcconfCliExec) logicalDevicesInfo() ([]byte, error) {
37 - return e.execute("arcconf-ld-info")
32 + return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-ld-info")
33 }
34
35 func (e *arcconfCliExec) physicalDevicesInfo() ([]byte, error) {
41 - return e.execute("arcconf-pd-info")
42 -}
43 -
44 -func (e *arcconfCliExec) execute(args ...string) ([]byte, error) {
45 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
46 - defer cancel()
47 -
48 - cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
49 - e.Debugf("executing '%s'", cmd)
50 -
51 - bs, err := cmd.Output()
52 - if err != nil {
53 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
54 - }
55 -
56 - return bs, nil
36 + return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-pd-info")
37 }
src/go/plugin/go.d/collector/adaptecraid/init.go
+1 -16
@@ -4,22 +4,7 @@
4
5 package adaptecraid
6
7 -import (
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
13 -)
14 -
7 func (c *Collector) initArcconfCliExec() (arcconfCli, error) {
16 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
17 -
18 - if _, err := os.Stat(ndsudoPath); err != nil {
19 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
20 - }
21 -
22 - arcconfExec := newArcconfCliExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
23 -
8 + arcconfExec := newArcconfCliExec(c.Timeout.Duration(), c.Logger)
9 return arcconfExec, nil
10 }
src/go/plugin/go.d/collector/ap/exec.go
+3 -27
@@ -5,12 +5,10 @@
5 package ap
6
7 import (
8 - "context"
9 - "fmt"
10 - "os/exec"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 )
13
14 type iwBinary interface {
@@ -33,31 +31,9 @@ type iwCliExec struct {
31 }
32
33 func (e *iwCliExec) devices() ([]byte, error) {
36 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
37 - defer cancel()
38 -
39 - cmd := exec.CommandContext(ctx, e.binPath, "dev")
40 - e.Debugf("executing '%s'", cmd)
41 -
42 - bs, err := cmd.Output()
43 - if err != nil {
44 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
45 - }
46 -
47 - return bs, nil
34 + return ndexec.RunUnprivileged(e.Logger, e.timeout, e.binPath, "dev")
35 }
36
37 func (e *iwCliExec) stationStatistics(ifaceName string) ([]byte, error) {
51 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
52 - defer cancel()
53 -
54 - cmd := exec.CommandContext(ctx, e.binPath, ifaceName, "station", "dump")
55 - e.Debugf("executing '%s'", cmd)
56 -
57 - bs, err := cmd.Output()
58 - if err != nil {
59 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
60 - }
61 -
62 - return bs, nil
38 + return ndexec.RunUnprivileged(e.Logger, e.timeout, e.binPath, ifaceName, "station", "dump")
39 }
src/go/plugin/go.d/collector/chrony/exec.go
+6 -21
@@ -3,44 +3,29 @@
3 package chrony
4
5 import (
6 - "context"
7 - "fmt"
8 - "os/exec"
6 "time"
7
8 "github.com/netdata/netdata/go/plugins/logger"
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
10 )
11
12 type chronyBinary interface {
13 serverStats() ([]byte, error)
14 }
15
18 -func newChronycExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *chronycExec {
16 +func newChronycExec(timeout time.Duration, log *logger.Logger) *chronycExec {
17 return &chronycExec{
20 - Logger: log,
21 - ndsudoPath: ndsudoPath,
22 - timeout: timeout,
18 + Logger: log,
19 + timeout: timeout,
20 }
21 }
22
23 type chronycExec struct {
24 *logger.Logger
25
29 - ndsudoPath string
30 - timeout time.Duration
26 + timeout time.Duration
27 }
28
29 func (e *chronycExec) serverStats() ([]byte, error) {
34 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
35 - defer cancel()
36 -
37 - cmd := exec.CommandContext(ctx, e.ndsudoPath, "chronyc-serverstats")
38 - e.Debugf("executing '%s'", cmd)
39 -
40 - bs, err := cmd.Output()
41 - if err != nil {
42 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
43 - }
44 -
45 - return bs, nil
30 + return ndexec.RunNDSudo(e.Logger, e.timeout, "chronyc-serverstats")
31 }
src/go/plugin/go.d/collector/chrony/init.go
+1 -12
@@ -4,12 +4,7 @@ package chrony
4
5 import (
6 "errors"
7 - "fmt"
7 "net"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
8 )
9
10 func (c *Collector) validateConfig() error {
@@ -30,13 +25,7 @@ func (c *Collector) initChronycBinary() (chronyBinary, error) {
25 return nil, nil
26 }
27
33 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
34 -
35 - if _, err := os.Stat(ndsudoPath); err != nil {
36 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
37 - }
38 -
39 - chronyc := newChronycExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
28 + chronyc := newChronycExec(c.Timeout.Duration(), c.Logger)
29
30 return chronyc, nil
31 }
src/go/plugin/go.d/collector/dmcache/collector_test.go
+2 -2
@@ -40,8 +40,8 @@ func TestCollector_Init(t *testing.T) {
40 config Config
41 wantFail bool
42 }{
43 - "fails if failed to locate ndsudo": {
44 - wantFail: true,
43 + "success with default config": {
44 + wantFail: false,
45 config: New().Config,
46 },
47 }
src/go/plugin/go.d/collector/dmcache/exec.go
+6 -21
@@ -5,44 +5,29 @@
5 package dmcache
6
7 import (
8 - "context"
9 - "fmt"
10 - "os/exec"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 )
13
14 type dmsetupCli interface {
15 cacheStatus() ([]byte, error)
16 }
17
20 -func newDmsetupExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *dmsetupExec {
18 +func newDmsetupExec(timeout time.Duration, log *logger.Logger) *dmsetupExec {
19 return &dmsetupExec{
22 - Logger: log,
23 - ndsudoPath: ndsudoPath,
24 - timeout: timeout,
20 + Logger: log,
21 + timeout: timeout,
22 }
23 }
24
25 type dmsetupExec struct {
26 *logger.Logger
27
31 - ndsudoPath string
32 - timeout time.Duration
28 + timeout time.Duration
29 }
30
31 func (e *dmsetupExec) cacheStatus() ([]byte, error) {
36 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
37 - defer cancel()
38 -
39 - cmd := exec.CommandContext(ctx, e.ndsudoPath, "dmsetup-status-cache")
40 - e.Debugf("executing '%s'", cmd)
41 -
42 - bs, err := cmd.Output()
43 - if err != nil {
44 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
45 - }
46 -
47 - return bs, nil
32 + return ndexec.RunNDSudo(e.Logger, e.timeout, "dmsetup-status-cache")
33 }
src/go/plugin/go.d/collector/dmcache/init.go
+1 -15
@@ -4,22 +4,8 @@
4
5 package dmcache
6
7 -import (
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
13 -)
14 -
7 func (c *Collector) initDmsetupCLI() (dmsetupCli, error) {
16 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
17 - if _, err := os.Stat(ndsudoPath); err != nil {
18 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
19 -
20 - }
21 -
22 - dmsetup := newDmsetupExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
8 + dmsetup := newDmsetupExec(c.Timeout.Duration(), c.Logger)
9
10 return dmsetup, nil
11 }
src/go/plugin/go.d/collector/ethtool/exec.go
+6 -26
@@ -3,49 +3,29 @@
3 package ethtool
4
5 import (
6 - "context"
7 - "fmt"
8 - "os/exec"
6 "time"
7
8 "github.com/netdata/netdata/go/plugins/logger"
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
10 )
11
12 type ethtoolCli interface {
13 moduleEeprom(iface string) ([]byte, error)
14 }
15
18 -func newEthtoolExec(ndsudoPath string, timeout time.Duration, logger *logger.Logger) *ethtoolCLIExec {
16 +func newEthtoolExec(timeout time.Duration, logger *logger.Logger) *ethtoolCLIExec {
17 return &ethtoolCLIExec{
20 - Logger: logger,
21 - ndsudoPath: ndsudoPath,
22 - timeout: timeout,
18 + Logger: logger,
19 + timeout: timeout,
20 }
21 }
22
23 type ethtoolCLIExec struct {
24 *logger.Logger
25
29 - ndsudoPath string
30 - timeout time.Duration
26 + timeout time.Duration
27 }
28
29 func (e *ethtoolCLIExec) moduleEeprom(iface string) ([]byte, error) {
34 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
35 - defer cancel()
36 -
37 - cmd := exec.CommandContext(ctx,
38 - e.ndsudoPath,
39 - "ethtool-module-info",
40 - "--devname",
41 - iface,
42 - )
43 - e.Debugf("executing '%s'", cmd)
44 -
45 - bs, err := cmd.Output()
46 - if err != nil {
47 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
48 - }
49 -
50 - return bs, nil
30 + return ndexec.RunNDSudo(e.Logger, e.timeout, "ethtool-module-info", "--devname", iface)
31 }
src/go/plugin/go.d/collector/ethtool/init.go
+1 -12
@@ -4,11 +4,6 @@ package ethtool
4
5 import (
6 "errors"
7 - "fmt"
8 - "os"
9 - "path/filepath"
10 -
11 - "github.com/netdata/netdata/go/plugins/pkg/executable"
7 )
8
9 func (c *Collector) validateConfig() error {
@@ -19,13 +14,7 @@ func (c *Collector) validateConfig() error {
14 }
15
16 func (c *Collector) initEthtoolCli() (ethtoolCli, error) {
22 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
23 - if _, err := os.Stat(ndsudoPath); err != nil {
24 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
25 -
26 - }
27 -
28 - et := newEthtoolExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
17 + et := newEthtoolExec(c.Timeout.Duration(), c.Logger)
18
19 return et, nil
20 }
src/go/plugin/go.d/collector/exim/collector_test.go
+2 -2
@@ -38,8 +38,8 @@ func TestCollector_Init(t *testing.T) {
38 config Config
39 wantFail bool
40 }{
41 - "fails if failed to locate ndsudo": {
42 - wantFail: true,
41 + "success with default config": {
42 + wantFail: false,
43 config: New().Config,
44 },
45 }
src/go/plugin/go.d/collector/exim/exec.go
+6 -22
@@ -3,45 +3,29 @@
3 package exim
4
5 import (
6 - "context"
7 - "fmt"
8 - "os/exec"
6 "time"
7
8 "github.com/netdata/netdata/go/plugins/logger"
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
10 )
11
12 type eximBinary interface {
13 countMessagesInQueue() ([]byte, error)
14 }
15
18 -func newEximExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *eximExec {
16 +func newEximExec(timeout time.Duration, log *logger.Logger) *eximExec {
17 return &eximExec{
20 - Logger: log,
21 - ndsudoPath: ndsudoPath,
22 - timeout: timeout,
18 + Logger: log,
19 + timeout: timeout,
20 }
21 }
22
23 type eximExec struct {
24 *logger.Logger
25
29 - ndsudoPath string
30 - timeout time.Duration
26 + timeout time.Duration
27 }
28
29 func (e *eximExec) countMessagesInQueue() ([]byte, error) {
34 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
35 - defer cancel()
36 -
37 - cmd := exec.CommandContext(ctx, e.ndsudoPath, "exim-bpc")
38 -
39 - e.Debugf("executing '%s'", cmd)
40 -
41 - bs, err := cmd.Output()
42 - if err != nil {
43 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
44 - }
45 -
46 - return bs, nil
30 + return ndexec.RunNDSudo(e.Logger, e.timeout, "exim-bpc")
31 }
src/go/plugin/go.d/collector/exim/init.go
+1 -15
@@ -2,22 +2,8 @@
2
3 package exim
4
5 -import (
6 - "fmt"
7 - "os"
8 - "path/filepath"
9 -
10 - "github.com/netdata/netdata/go/plugins/pkg/executable"
11 -)
12 -
5 func (c *Collector) initEximExec() (eximBinary, error) {
14 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
15 - if _, err := os.Stat(ndsudoPath); err != nil {
16 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
17 -
18 - }
19 -
20 - exim := newEximExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
6 + exim := newEximExec(c.Timeout.Duration(), c.Logger)
7
8 return exim, nil
9 }
src/go/plugin/go.d/collector/fail2ban/collector_test.go
+2 -2
@@ -46,8 +46,8 @@ func TestCollector_Init(t *testing.T) {
46 config Config
47 wantFail bool
48 }{
49 - "fails if failed to locate ndsudo": {
50 - wantFail: true,
49 + "success with default config": {
50 + wantFail: false,
51 config: New().Config,
52 },
53 }
src/go/plugin/go.d/collector/fail2ban/exec.go
+6 -17
@@ -5,15 +5,13 @@
5 package fail2ban
6
7 import (
8 - "context"
8 "errors"
10 - "fmt"
9 "os"
12 - "os/exec"
10 "strings"
11 "time"
12
13 "github.com/netdata/netdata/go/plugins/logger"
14 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
15 )
16
17 var errJailNotExist = errors.New("jail not exist")
@@ -25,12 +23,11 @@ type fail2banClientCli interface {
23 jailStatus(s string) ([]byte, error)
24 }
25
28 -func newFail2BanClientCliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *fail2banClientCliExec {
26 +func newFail2BanClientCliExec(timeout time.Duration, log *logger.Logger) *fail2banClientCliExec {
27 _, err := os.Stat("/host/var/run")
28
29 return &fail2banClientCliExec{
30 Logger: log,
33 - ndsudoPath: ndsudoPath,
31 timeout: timeout,
32 isInsideDocker: err == nil,
33 }
@@ -39,7 +36,6 @@ func newFail2BanClientCliExec(ndsudoPath string, timeout time.Duration, log *log
36 type fail2banClientCliExec struct {
37 *logger.Logger
38
42 - ndsudoPath string
39 timeout time.Duration
40 isInsideDocker bool
41 }
@@ -65,20 +61,13 @@ func (e *fail2banClientCliExec) jailStatus(jail string) ([]byte, error) {
61 )
62 }
63
68 -func (e *fail2banClientCliExec) execute(args ...string) ([]byte, error) {
69 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
70 - defer cancel()
71 -
72 - cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
73 - e.Debugf("executing '%s'", cmd)
74 -
75 - bs, err := cmd.Output()
64 +func (e *fail2banClientCliExec) execute(cmd string, args ...string) ([]byte, error) {
65 + bs, err := ndexec.RunNDSudo(e.Logger, e.timeout, cmd, args...)
66 if err != nil {
67 if strings.HasPrefix(strings.TrimSpace(string(bs)), "Sorry but the jail") {
68 return nil, errJailNotExist
69 }
80 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
70 + return nil, err
71 }
82 -
83 - return bs, nil
72 + return bs, err
73 }
src/go/plugin/go.d/collector/fail2ban/init.go
+1 -15
@@ -4,22 +4,8 @@
4
5 package fail2ban
6
7 -import (
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
13 -)
14 -
7 func (c *Collector) initFail2banClientCliExec() (fail2banClientCli, error) {
16 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
17 - if _, err := os.Stat(ndsudoPath); err != nil {
18 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
19 -
20 - }
21 -
22 - f2bClientExec := newFail2BanClientCliExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
8 + f2bClientExec := newFail2BanClientCliExec(c.Timeout.Duration(), c.Logger)
9
10 return f2bClientExec, nil
11 }
src/go/plugin/go.d/collector/hpssa/collector_test.go
+2 -2
@@ -43,8 +43,8 @@ func TestCollector_Init(t *testing.T) {
43 config Config
44 wantFail bool
45 }{
46 - "fails if 'ndsudo' not found": {
47 - wantFail: true,
46 + "success with default config": {
47 + wantFail: false,
48 config: New().Config,
49 },
50 }
src/go/plugin/go.d/collector/hpssa/exec.go
+6 -25
@@ -3,48 +3,29 @@
3 package hpssa
4
5 import (
6 - "context"
7 - "fmt"
8 - "os/exec"
6 "time"
7
8 "github.com/netdata/netdata/go/plugins/logger"
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
10 )
11
12 type ssacliBinary interface {
13 controllersInfo() ([]byte, error)
14 }
15
18 -func newSsacliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *ssacliExec {
16 +func newSsacliExec(timeout time.Duration, log *logger.Logger) *ssacliExec {
17 return &ssacliExec{
20 - Logger: log,
21 - ndsudoPath: ndsudoPath,
22 - timeout: timeout,
18 + Logger: log,
19 + timeout: timeout,
20 }
21 }
22
23 type ssacliExec struct {
24 *logger.Logger
25
29 - ndsudoPath string
30 - timeout time.Duration
26 + timeout time.Duration
27 }
28
29 func (e *ssacliExec) controllersInfo() ([]byte, error) {
34 - return e.execute("ssacli-controllers-info")
35 -}
36 -
37 -func (e *ssacliExec) execute(args ...string) ([]byte, error) {
38 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
39 - defer cancel()
40 -
41 - cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
42 - e.Debugf("executing '%s'", cmd)
43 -
44 - bs, err := cmd.Output()
45 - if err != nil {
46 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
47 - }
48 -
49 - return bs, nil
30 + return ndexec.RunNDSudo(e.Logger, e.timeout, "ssacli-controllers-info")
31 }
src/go/plugin/go.d/collector/hpssa/init.go
+1 -15
@@ -2,22 +2,8 @@
2
3 package hpssa
4
5 -import (
6 - "fmt"
7 - "os"
8 - "path/filepath"
9 -
10 - "github.com/netdata/netdata/go/plugins/pkg/executable"
11 -)
12 -
5 func (c *Collector) initSsacliBinary() (ssacliBinary, error) {
14 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
15 -
16 - if _, err := os.Stat(ndsudoPath); err != nil {
17 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
18 - }
19 -
20 - ssacliExec := newSsacliExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
6 + ssacliExec := newSsacliExec(c.Timeout.Duration(), c.Logger)
7
8 return ssacliExec, nil
9 }
src/go/plugin/go.d/collector/lvm/collector_test.go
+2 -2
@@ -46,8 +46,8 @@ func TestCollector_Init(t *testing.T) {
46 config Config
47 wantFail bool
48 }{
49 - "fails if failed to locate ndsudo": {
50 - wantFail: true,
49 + "success with default config": {
50 + wantFail: false,
51 config: New().Config,
52 },
53 }
src/go/plugin/go.d/collector/lvm/exec.go
+8 -22
@@ -5,49 +5,35 @@
5 package lvm
6
7 import (
8 - "context"
9 - "fmt"
10 - "os/exec"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 )
13
14 type lvmCLI interface {
15 lvsReportJson() ([]byte, error)
16 }
17
20 -func newLVMCLIExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *lvmCLIExec {
18 +func newLVMCLIExec(timeout time.Duration, log *logger.Logger) *lvmCLIExec {
19 return &lvmCLIExec{
22 - Logger: log,
23 - ndsudoPath: ndsudoPath,
24 - timeout: timeout,
20 + Logger: log,
21 + timeout: timeout,
22 }
23 }
24
25 type lvmCLIExec struct {
26 *logger.Logger
27
31 - ndsudoPath string
32 - timeout time.Duration
28 + timeout time.Duration
29 }
30
31 func (e *lvmCLIExec) lvsReportJson() ([]byte, error) {
36 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
37 - defer cancel()
38 -
39 - cmd := exec.CommandContext(ctx,
40 - e.ndsudoPath,
32 + return ndexec.RunNDSudo(
33 + e.Logger,
34 + e.timeout,
35 "lvs-report-json",
36 "--options",
37 "vg_name,lv_name,lv_size,data_percent,metadata_percent,lv_attr",
38 )
45 - e.Debugf("executing '%s'", cmd)
46 -
47 - bs, err := cmd.Output()
48 - if err != nil {
49 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
50 - }
51 -
52 - return bs, nil
39 }
src/go/plugin/go.d/collector/lvm/init.go
+1 -15
@@ -4,22 +4,8 @@
4
5 package lvm
6
7 -import (
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
13 -)
14 -
7 func (c *Collector) initLVMCLIExec() (lvmCLI, error) {
16 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
17 - if _, err := os.Stat(ndsudoPath); err != nil {
18 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
19 -
20 - }
21 -
22 - lvmExec := newLVMCLIExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
8 + lvmExec := newLVMCLIExec(c.Timeout.Duration(), c.Logger)
9
10 return lvmExec, nil
11 }
src/go/plugin/go.d/collector/megacli/collector_test.go
+2 -2
@@ -49,8 +49,8 @@ func TestCollector_Init(t *testing.T) {
49 config Config
50 wantFail bool
51 }{
52 - "fails if 'ndsudo' not found": {
53 - wantFail: true,
52 + "success with default config": {
53 + wantFail: false,
54 config: New().Config,
55 },
56 }
src/go/plugin/go.d/collector/megacli/exec.go
+7 -26
@@ -5,12 +5,10 @@
5 package megacli
6
7 import (
8 - "context"
9 - "fmt"
10 - "os/exec"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 )
13
14 type megaCli interface {
@@ -18,40 +16,23 @@ type megaCli interface {
16 bbuInfo() ([]byte, error)
17 }
18
21 -func newMegaCliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *megaCliExec {
19 +func newMegaCliExec(timeout time.Duration, log *logger.Logger) *megaCliExec {
20 return &megaCliExec{
23 - Logger: log,
24 - ndsudoPath: ndsudoPath,
25 - timeout: timeout,
21 + Logger: log,
22 + timeout: timeout,
23 }
24 }
25
26 type megaCliExec struct {
27 *logger.Logger
28
32 - ndsudoPath string
33 - timeout time.Duration
29 + timeout time.Duration
30 }
31
32 func (e *megaCliExec) physDrivesInfo() ([]byte, error) {
37 - return e.execute("megacli-disk-info")
33 + return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-disk-info")
34 }
35
36 func (e *megaCliExec) bbuInfo() ([]byte, error) {
41 - return e.execute("megacli-battery-info")
42 -}
43 -
44 -func (e *megaCliExec) execute(args ...string) ([]byte, error) {
45 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
46 - defer cancel()
47 -
48 - cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
49 - e.Debugf("executing '%s'", cmd)
50 -
51 - bs, err := cmd.Output()
52 - if err != nil {
53 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
54 - }
55 -
56 - return bs, nil
37 + return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-battery-info")
38 }
src/go/plugin/go.d/collector/megacli/init.go
+1 -15
@@ -4,22 +4,8 @@
4
5 package megacli
6
7 -import (
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
13 -)
14 -
7 func (c *Collector) initMegaCliExec() (megaCli, error) {
16 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
17 -
18 - if _, err := os.Stat(ndsudoPath); err != nil {
19 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
20 - }
21 -
22 - megaExec := newMegaCliExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
8 + megaExec := newMegaCliExec(c.Timeout.Duration(), c.Logger)
9
10 return megaExec, nil
11 }
src/go/plugin/go.d/collector/nsd/collector_test.go
+2 -2
@@ -43,8 +43,8 @@ func TestCollector_Init(t *testing.T) {
43 config Config
44 wantFail bool
45 }{
46 - "fails if failed to locate ndsudo": {
47 - wantFail: true,
46 + "success with default config": {
47 + wantFail: false,
48 config: New().Config,
49 },
50 }
src/go/plugin/go.d/collector/nsd/exec.go
+6 -22
@@ -5,45 +5,29 @@
5 package nsd
6
7 import (
8 - "context"
9 - "fmt"
10 - "os/exec"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 )
13
14 type nsdControlBinary interface {
15 stats() ([]byte, error)
16 }
17
20 -func newNsdControlExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *nsdControlExec {
18 +func newNsdControlExec(timeout time.Duration, log *logger.Logger) *nsdControlExec {
19 return &nsdControlExec{
22 - Logger: log,
23 - ndsudoPath: ndsudoPath,
24 - timeout: timeout,
20 + Logger: log,
21 + timeout: timeout,
22 }
23 }
24
25 type nsdControlExec struct {
26 *logger.Logger
27
31 - ndsudoPath string
32 - timeout time.Duration
28 + timeout time.Duration
29 }
30
31 func (e *nsdControlExec) stats() ([]byte, error) {
36 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
37 - defer cancel()
38 -
39 - cmd := exec.CommandContext(ctx, e.ndsudoPath, "nsd-control-stats")
40 -
41 - e.Debugf("executing '%s'", cmd)
42 -
43 - bs, err := cmd.Output()
44 - if err != nil {
45 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
46 - }
47 -
48 - return bs, nil
32 + return ndexec.RunNDSudo(e.Logger, e.timeout, "nsd-control-stats")
33 }
src/go/plugin/go.d/collector/nsd/init.go
+1 -15
@@ -4,22 +4,8 @@
4
5 package nsd
6
7 -import (
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
13 -)
14 -
7 func (c *Collector) initNsdControlExec() (nsdControlBinary, error) {
16 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
17 - if _, err := os.Stat(ndsudoPath); err != nil {
18 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
19 -
20 - }
21 -
22 - nsdControl := newNsdControlExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
8 + nsdControl := newNsdControlExec(c.Timeout.Duration(), c.Logger)
9
10 return nsdControl, nil
11 }
src/go/plugin/go.d/collector/nvidia_smi/exec.go
+6 -15
@@ -5,15 +5,16 @@ package nvidia_smi
5 import (
6 "bufio"
7 "bytes"
8 - "context"
8 "errors"
10 - "fmt"
9 "os/exec"
10 + "path/filepath"
11 "strconv"
12 "sync"
13 "time"
14
15 "github.com/netdata/netdata/go/plugins/logger"
16 + "github.com/netdata/netdata/go/plugins/pkg/buildinfo"
17 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
18 )
19
20 type nvidiaSmiBinary interface {
@@ -52,18 +53,7 @@ type nvidiaSmiExec struct {
53 }
54
55 func (e *nvidiaSmiExec) queryGPUInfo() ([]byte, error) {
55 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
56 - defer cancel()
57 -
58 - cmd := exec.CommandContext(ctx, e.binPath, "-q", "-x")
59 -
60 - e.Debugf("executing '%s'", cmd)
61 - bs, err := cmd.Output()
62 - if err != nil {
63 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
64 - }
65 -
66 - return bs, nil
56 + return ndexec.RunUnprivileged(e.Logger, e.timeout, e.binPath, "-q", "-x")
57 }
58
59 func (e *nvidiaSmiExec) stop() error { return nil }
@@ -102,7 +92,8 @@ func (e *nvidiaSmiLoopExec) run() error {
92 secs = e.updateEvery
93 }
94
105 - cmd := exec.Command(e.binPath, "-q", "-x", "-l", strconv.Itoa(secs))
95 + ndrunPath := filepath.Join(buildinfo.NetdataBinDir, "nd-run")
96 + cmd := exec.Command(ndrunPath, e.binPath, "-q", "-x", "-l", strconv.Itoa(secs))
97
98 e.Debugf("executing '%s'", cmd)
99
src/go/plugin/go.d/collector/nvme/collector_test.go
+2 -2
@@ -58,8 +58,8 @@ func TestCollector_Init(t *testing.T) {
58 config Config
59 wantFail bool
60 }{
61 - "fails if 'ndsudo' not found": {
62 - wantFail: true,
61 + "success with default config": {
62 + wantFail: false,
63 config: New().Config,
64 },
65 }
src/go/plugin/go.d/collector/nvme/exec.go
+5 -13
@@ -6,12 +6,12 @@ package nvme
6
7 import (
8 "bytes"
9 - "context"
9 "encoding/json"
11 - "os/exec"
10 "strconv"
11 "strings"
12 "time"
13 +
14 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
15 )
16
17 type nvmeDeviceList struct {
@@ -144,12 +144,11 @@ type nvmeCli interface {
144 }
145
146 type nvmeCLIExec struct {
147 - ndsudoPath string
148 - timeout time.Duration
147 + timeout time.Duration
148 }
149
150 func (n *nvmeCLIExec) list() (*nvmeDeviceList, error) {
152 - bs, err := n.execute("nvme-list")
151 + bs, err := ndexec.RunNDSudo(nil, n.timeout, "nvme-list")
152 if err != nil {
153 return nil, err
154 }
@@ -163,7 +162,7 @@ func (n *nvmeCLIExec) list() (*nvmeDeviceList, error) {
162 }
163
164 func (n *nvmeCLIExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
166 - bs, err := n.execute("nvme-smart-log", "--device", devicePath)
165 + bs, err := ndexec.RunNDSudo(nil, n.timeout, "nvme-smart-log", "--device", devicePath)
166 if err != nil {
167 return nil, err
168 }
@@ -175,10 +174,3 @@ func (n *nvmeCLIExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
174
175 return &v, nil
176 }
178 -
179 -func (n *nvmeCLIExec) execute(arg ...string) ([]byte, error) {
180 - ctx, cancel := context.WithTimeout(context.Background(), n.timeout)
181 - defer cancel()
182 -
183 - return exec.CommandContext(ctx, n.ndsudoPath, arg...).Output()
184 -}
src/go/plugin/go.d/collector/nvme/init.go
+1 -18
@@ -4,25 +4,8 @@
4
5 package nvme
6
7 -import (
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
13 -)
14 -
7 func (c *Collector) initNVMeCLIExec() (nvmeCli, error) {
16 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
17 -
18 - if _, err := os.Stat(ndsudoPath); err != nil {
19 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
20 - }
21 -
22 - nvmeExec := &nvmeCLIExec{
23 - ndsudoPath: ndsudoPath,
24 - timeout: c.Timeout.Duration(),
25 - }
8 + nvmeExec := &nvmeCLIExec{timeout: c.Timeout.Duration()}
9
10 return nvmeExec, nil
11 }
src/go/plugin/go.d/collector/postfix/exec.go
+2 -15
@@ -3,12 +3,10 @@
3 package postfix
4
5 import (
6 - "context"
7 - "fmt"
8 - "os/exec"
6 "time"
7
8 "github.com/netdata/netdata/go/plugins/logger"
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
10 )
11
12 type postqueueBinary interface {
@@ -30,16 +28,5 @@ type postqueueExec struct {
28 }
29
30 func (p *postqueueExec) list() ([]byte, error) {
33 - ctx, cancel := context.WithTimeout(context.Background(), p.timeout)
34 - defer cancel()
35 -
36 - cmd := exec.CommandContext(ctx, p.binPath, "-p")
37 - p.Debugf("executing '%s'", cmd)
38 -
39 - bs, err := cmd.Output()
40 - if err != nil {
41 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
42 - }
43 -
44 - return bs, nil
31 + return ndexec.RunUnprivileged(p.Logger, p.timeout, p.binPath, "-p")
32 }
src/go/plugin/go.d/collector/samba/collector_test.go
+2 -2
@@ -41,8 +41,8 @@ func TestCollector_Init(t *testing.T) {
41 config Config
42 wantFail bool
43 }{
44 - "fails if failed to locate ndsudo": {
45 - wantFail: true,
44 + "success with default config": {
45 + wantFail: false,
46 config: New().Config,
47 },
48 }
src/go/plugin/go.d/collector/samba/exec.go
+6 -22
@@ -3,45 +3,29 @@
3 package samba
4
5 import (
6 - "context"
7 - "fmt"
8 - "os/exec"
6 "time"
7
8 "github.com/netdata/netdata/go/plugins/logger"
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
10 )
11
12 type smbStatusBinary interface {
13 profile() ([]byte, error)
14 }
15
18 -func newSmbStatusBinary(ndsudoPath string, timeout time.Duration, log *logger.Logger) smbStatusBinary {
16 +func newSmbStatusBinary(timeout time.Duration, log *logger.Logger) smbStatusBinary {
17 return &smbStatusExec{
20 - Logger: log,
21 - ndsudoPath: ndsudoPath,
22 - timeout: timeout,
18 + Logger: log,
19 + timeout: timeout,
20 }
21 }
22
23 type smbStatusExec struct {
24 *logger.Logger
25
29 - ndsudoPath string
30 - timeout time.Duration
26 + timeout time.Duration
27 }
28
29 func (e *smbStatusExec) profile() ([]byte, error) {
34 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
35 - defer cancel()
36 -
37 - cmd := exec.CommandContext(ctx, e.ndsudoPath, "smbstatus-profile")
38 -
39 - e.Debugf("executing '%s'", cmd)
40 -
41 - bs, err := cmd.Output()
42 - if err != nil {
43 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
44 - }
45 -
46 - return bs, nil
30 + return ndexec.RunNDSudo(e.Logger, e.timeout, "smbstatus-profile")
31 }
src/go/plugin/go.d/collector/samba/init.go
+1 -15
@@ -2,22 +2,8 @@
2
3 package samba
4
5 -import (
6 - "fmt"
7 - "os"
8 - "path/filepath"
9 -
10 - "github.com/netdata/netdata/go/plugins/pkg/executable"
11 -)
12 -
5 func (c *Collector) initSmbStatusBinary() (smbStatusBinary, error) {
14 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
15 - if _, err := os.Stat(ndsudoPath); err != nil {
16 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
17 -
18 - }
19 -
20 - smbStatus := newSmbStatusBinary(ndsudoPath, c.Timeout.Duration(), c.Logger)
6 + smbStatus := newSmbStatusBinary(c.Timeout.Duration(), c.Logger)
7
8 return smbStatus, nil
9 }
src/go/plugin/go.d/collector/smartctl/collector_test.go
+3 -2
@@ -70,8 +70,8 @@ func TestCollector_Init(t *testing.T) {
70 return cfg
71 }(),
72 },
73 - "fails if 'ndsudo' not found": {
74 - wantFail: true,
73 + "success with default config": {
74 + wantFail: false,
75 config: New().Config,
76 },
77 }
@@ -79,6 +79,7 @@ func TestCollector_Init(t *testing.T) {
79 for name, test := range tests {
80 t.Run(name, func(t *testing.T) {
81 collr := New()
82 + collr.Config = test.config
83
84 if test.wantFail {
85 assert.Error(t, collr.Init(context.Background()))
src/go/plugin/go.d/collector/smartctl/exec.go
+13 -20
@@ -13,6 +13,7 @@ import (
13 "time"
14
15 "github.com/netdata/netdata/go/plugins/logger"
16 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
17
18 "github.com/tidwall/gjson"
19 )
@@ -26,15 +27,13 @@ type smartctlCli interface {
27 type ndsudoSmartctlCli struct {
28 *logger.Logger
29
29 - ndsudoPath string
30 - timeout time.Duration
30 + timeout time.Duration
31 }
32
33 -func newNdsudoSmartctlCli(ndsudoPath string, timeout time.Duration, log *logger.Logger) *ndsudoSmartctlCli {
33 +func newNdsudoSmartctlCli(timeout time.Duration, log *logger.Logger) *ndsudoSmartctlCli {
34 return &ndsudoSmartctlCli{
35 - Logger: log,
36 - ndsudoPath: ndsudoPath,
37 - timeout: timeout,
35 + Logger: log,
36 + timeout: timeout,
37 }
38 }
39
@@ -53,24 +52,18 @@ func (e *ndsudoSmartctlCli) deviceInfo(deviceName, deviceType, powerMode string)
52 )
53 }
54
56 -func (e *ndsudoSmartctlCli) execute(args ...string) (*gjson.Result, error) {
57 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
58 - defer cancel()
59 -
60 - cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
61 - e.Debugf("executing '%s'", cmd)
62 -
63 - bs, err := cmd.Output()
55 +func (e *ndsudoSmartctlCli) execute(cmd string, args ...string) (*gjson.Result, error) {
56 + bs, cmdStr, err := ndexec.RunNDSudoWithCmd(e.Logger, e.timeout, cmd, args...)
57 if err != nil {
58 if errors.Is(err, context.DeadlineExceeded) || isExecExitCode(err, 1) || len(bs) == 0 {
66 - return nil, fmt.Errorf("'%s' execution failed: %v", cmd, err)
59 + return nil, fmt.Errorf("'%s' execution failed: %v", cmdStr, err)
60 }
61 }
62
70 - return parseOutput(cmd.String(), bs, args, e.Logger)
63 + return parseOutput(cmdStr, bs, e.Logger)
64 }
65
73 -// directSmartctlCli executes smartctl directly (Windows, macOS, etc.)
66 +// directSmartctlCli executes smartctl directly (Windows only)
67 type directSmartctlCli struct {
68 *logger.Logger
69
@@ -119,11 +112,11 @@ func (e *directSmartctlCli) execute(args ...string) (*gjson.Result, error) {
112 }
113 }
114
122 - return parseOutput(cmd.String(), bs, args, e.Logger)
115 + return parseOutput(cmd.String(), bs, e.Logger)
116 }
117
118 // Common output parsing function
126 -func parseOutput(cmdStr string, bs []byte, args []string, log *logger.Logger) (*gjson.Result, error) {
119 +func parseOutput(cmdStr string, bs []byte, log *logger.Logger) (*gjson.Result, error) {
120 if len(bs) == 0 {
121 return nil, fmt.Errorf("'%s' returned no output", cmdStr)
122 }
@@ -131,7 +124,7 @@ func parseOutput(cmdStr string, bs []byte, args []string, log *logger.Logger) (*
124 if logger.Level.Enabled(slog.LevelDebug) {
125 var buf bytes.Buffer
126 if err := json.Compact(&buf, bs); err == nil {
134 - log.Debugf("exec: %v, resp: %s", args, buf.String())
127 + log.Debugf("exec: %v, resp: %s", cmdStr, buf.String())
128 }
129 }
130
src/go/plugin/go.d/collector/smartctl/init.go
+4 -10
@@ -9,7 +9,6 @@ import (
9 "path/filepath"
10 "runtime"
11
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
12 "github.com/netdata/netdata/go/plugins/pkg/matcher"
13 )
14
@@ -43,19 +42,14 @@ func (c *Collector) initDeviceSelector() (matcher.Matcher, error) {
42 }
43
44 func (c *Collector) initSmartctlCli() (smartctlCli, error) {
46 - if runtime.GOOS == "linux" {
47 - return c.initNdsudoSmartctlCli()
45 + if runtime.GOOS == "windows" {
46 + return c.initDirectSmartctlCli()
47 }
49 - return c.initDirectSmartctlCli()
48 + return c.initNdsudoSmartctlCli()
49 }
50
51 func (c *Collector) initNdsudoSmartctlCli() (smartctlCli, error) {
53 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
54 - if _, err := os.Stat(ndsudoPath); err != nil {
55 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
56 - }
57 -
58 - smartctlExec := newNdsudoSmartctlCli(ndsudoPath, c.Timeout.Duration(), c.Logger)
52 + smartctlExec := newNdsudoSmartctlCli(c.Timeout.Duration(), c.Logger)
53 return smartctlExec, nil
54 }
55
src/go/plugin/go.d/collector/storcli/collector_test.go
+2 -2
@@ -47,8 +47,8 @@ func TestCollector_Init(t *testing.T) {
47 config Config
48 wantFail bool
49 }{
50 - "fails if 'ndsudo' not found": {
51 - wantFail: true,
50 + "success with default config": {
51 + wantFail: false,
52 config: New().Config,
53 },
54 }
src/go/plugin/go.d/collector/storcli/exec.go
+7 -26
@@ -5,12 +5,10 @@
5 package storcli
6
7 import (
8 - "context"
9 - "fmt"
10 - "os/exec"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 )
13
14 type storCli interface {
@@ -18,40 +16,23 @@ type storCli interface {
16 drivesInfo() ([]byte, error)
17 }
18
21 -func newStorCliExec(ndsudoPath string, timeout time.Duration, log *logger.Logger) *storCliExec {
19 +func newStorCliExec(timeout time.Duration, log *logger.Logger) *storCliExec {
20 return &storCliExec{
23 - Logger: log,
24 - ndsudoPath: ndsudoPath,
25 - timeout: timeout,
21 + Logger: log,
22 + timeout: timeout,
23 }
24 }
25
26 type storCliExec struct {
27 *logger.Logger
28
32 - ndsudoPath string
33 - timeout time.Duration
29 + timeout time.Duration
30 }
31
32 func (e *storCliExec) controllersInfo() ([]byte, error) {
37 - return e.execute("storcli-controllers-info")
33 + return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-controllers-info")
34 }
35
36 func (e *storCliExec) drivesInfo() ([]byte, error) {
41 - return e.execute("storcli-drives-info")
42 -}
43 -
44 -func (e *storCliExec) execute(args ...string) ([]byte, error) {
45 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
46 - defer cancel()
47 -
48 - cmd := exec.CommandContext(ctx, e.ndsudoPath, args...)
49 - e.Debugf("executing '%s'", cmd)
50 -
51 - bs, err := cmd.Output()
52 - if err != nil {
53 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
54 - }
55 -
56 - return bs, nil
37 + return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-drives-info")
38 }
src/go/plugin/go.d/collector/storcli/init.go
+1 -15
@@ -4,22 +4,8 @@
4
5 package storcli
6
7 -import (
8 - "fmt"
9 - "os"
10 - "path/filepath"
11 -
12 - "github.com/netdata/netdata/go/plugins/pkg/executable"
13 -)
14 -
7 func (c *Collector) initStorCliExec() (storCli, error) {
16 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
17 -
18 - if _, err := os.Stat(ndsudoPath); err != nil {
19 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
20 - }
21 -
22 - storExec := newStorCliExec(ndsudoPath, c.Timeout.Duration(), c.Logger)
8 + storExec := newStorCliExec(c.Timeout.Duration(), c.Logger)
9
10 return storExec, nil
11 }
src/go/plugin/go.d/collector/varnish/collector_test.go
+2 -2
@@ -40,8 +40,8 @@ func TestCollector_Init(t *testing.T) {
40 config Config
41 wantFail bool
42 }{
43 - "fails if failed to locate ndsudo": {
44 - wantFail: true,
43 + "success with default config": {
44 + wantFail: false,
45 config: New().Config,
46 },
47 }
src/go/plugin/go.d/collector/varnish/exec.go
+3 -17
@@ -4,23 +4,21 @@ package varnish
4
5 import (
6 "context"
7 - "fmt"
8 - "os/exec"
7 "strconv"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/dockerhost"
12 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
13 )
14
15 type varnishstatBinary interface {
16 statistics() ([]byte, error)
17 }
18
20 -func newVarnishstatExecBinary(binPath string, cfg Config, log *logger.Logger) varnishstatBinary {
19 +func newVarnishstatExecBinary(cfg Config, log *logger.Logger) varnishstatBinary {
20 return &varnishstatExec{
21 Logger: log,
23 - binPath: binPath,
22 timeout: cfg.Timeout.Duration(),
23 instanceName: cfg.InstanceName,
24 }
@@ -29,24 +27,12 @@ func newVarnishstatExecBinary(binPath string, cfg Config, log *logger.Logger) va
27 type varnishstatExec struct {
28 *logger.Logger
29
32 - binPath string
30 timeout time.Duration
31 instanceName string
32 }
33
34 func (e *varnishstatExec) statistics() ([]byte, error) {
38 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
39 - defer cancel()
40 -
41 - cmd := exec.CommandContext(ctx, e.binPath, "varnishstat-stats", "--instanceName", e.instanceName)
42 - e.Debugf("executing '%s'", cmd)
43 -
44 - bs, err := cmd.Output()
45 - if err != nil {
46 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
47 - }
48 -
49 - return bs, nil
35 + return ndexec.RunNDSudo(e.Logger, e.timeout, "varnishstat-stats", "--instanceName", e.instanceName)
36 }
37
38 func newVarnishstatDockerExecBinary(cfg Config, log *logger.Logger) varnishstatBinary {
src/go/plugin/go.d/collector/varnish/init.go
+1 -16
@@ -2,27 +2,12 @@
2
3 package varnish
4
5 -import (
6 - "fmt"
7 - "os"
8 - "path/filepath"
9 -
10 - "github.com/netdata/netdata/go/plugins/pkg/executable"
11 -)
12 -
5 func (c *Collector) initVarnishstatBinary() (varnishstatBinary, error) {
6 if c.Config.DockerContainer != "" {
7 return newVarnishstatDockerExecBinary(c.Config, c.Logger), nil
8 }
9
18 - ndsudoPath := filepath.Join(executable.Directory, "ndsudo")
19 -
20 - if _, err := os.Stat(ndsudoPath); err != nil {
21 - return nil, fmt.Errorf("ndsudo executable not found: %v", err)
22 -
23 - }
24 -
25 - varnishstat := newVarnishstatExecBinary(ndsudoPath, c.Config, c.Logger)
10 + varnishstat := newVarnishstatExecBinary(c.Config, c.Logger)
11
12 return varnishstat, nil
13 }
src/go/plugin/go.d/collector/zfspool/exec.go
+3 -27
@@ -5,12 +5,10 @@
5 package zfspool
6
7 import (
8 - "context"
9 - "fmt"
10 - "os/exec"
8 "time"
9
10 "github.com/netdata/netdata/go/plugins/logger"
11 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12 )
13
14 type zpoolCli interface {
@@ -33,31 +31,9 @@ type zpoolCLIExec struct {
31 }
32
33 func (e *zpoolCLIExec) list() ([]byte, error) {
36 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
37 - defer cancel()
38 -
39 - cmd := exec.CommandContext(ctx, e.binPath, "list", "-p")
40 - e.Debugf("executing '%s'", cmd)
41 -
42 - bs, err := cmd.Output()
43 - if err != nil {
44 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
45 - }
46 -
47 - return bs, nil
34 + return ndexec.RunUnprivileged(e.Logger, e.timeout, e.binPath, "list", "-p")
35 }
36
37 func (e *zpoolCLIExec) listWithVdev(pool string) ([]byte, error) {
51 - ctx, cancel := context.WithTimeout(context.Background(), e.timeout)
52 - defer cancel()
53 -
54 - cmd := exec.CommandContext(ctx, e.binPath, "list", "-p", "-v", "-L", pool)
55 - e.Debugf("executing '%s'", cmd)
56 -
57 - bs, err := cmd.Output()
58 - if err != nil {
59 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
60 - }
61 -
62 - return bs, nil
38 + return ndexec.RunUnprivileged(e.Logger, e.timeout, e.binPath, "list", "-p", "-v", "-L", pool)
39 }
src/go/plugin/go.d/pkg/ndexec/ndexec.go
+60 -72
@@ -3,104 +3,92 @@
3 package ndexec
4
5 import (
6 + "bytes"
7 "context"
8 "fmt"
9 "os/exec"
10 "path/filepath"
11 + "runtime"
12 + "strings"
13 "time"
14
15 "github.com/netdata/netdata/go/plugins/logger"
16 "github.com/netdata/netdata/go/plugins/pkg/buildinfo"
17 )
18
16 -// CommandUnprivileged runs a command without any extra privileges and
17 -// returns the exec.Cmd instance.
18 -//
19 -// ctx is a context.Context to use to run the command. logger is a Logger
20 -// instance to use to log the command to be executed. timeout indicates
21 -// the timeout for the command. arg is a list of the command arguments,
22 -// with the first string in the slice being the command to run.
23 -//
24 -// This invokes the command and logs a debug message that the command
25 -// is being executed, and then returns the exec.Cmd object for the command.
26 -func CommandUnprivileged(ctx context.Context, logger *logger.Logger, arg ...string) *exec.Cmd {
27 - ndrunPath := filepath.Join(buildinfo.NetdataBinDir, "nd-run")
28 -
29 - cmd := exec.CommandContext(ctx, ndrunPath, arg...)
30 - if logger != nil {
31 - logger.Debugf("executing '%s'", cmd)
32 - }
19 +const stderrLimit = 8 << 10 // 8 KiB
20
34 - return cmd
21 +// Runner holds helper paths for execution.
22 +type runner struct {
23 + ndRunPath string
24 + ndSudoPath string
25 }
26
37 -// RunUnprivileged runs a command without any inherited privileges via
38 -// the nd-run helper.
39 -//
40 -// logger is a Logger instance to use to log the command to be executed.
41 -// timeout indicates the timeout for the command. arg is a list of the
42 -// command arguments, with the first string in the slice being the command
43 -// to run.
44 -//
45 -// This handles constructing the context for execution, logs a debug
46 -// message that the command is being executed, and checks for errors in
47 -// the command invocation, then returns the command output.
48 -func RunUnprivileged(logger *logger.Logger, timeout time.Duration, arg ...string) ([]byte, error) {
49 - ctx, cancel := context.WithTimeout(context.Background(), timeout)
50 - defer cancel()
27 +func newRunnerFromBuildinfo() *runner {
28 + var sfx string
29 + if runtime.GOOS == "windows" {
30 + sfx = ".exe"
31 + }
32 + return &runner{
33 + ndRunPath: filepath.Join(buildinfo.NetdataBinDir, "nd-run"+sfx),
34 + ndSudoPath: filepath.Join(buildinfo.PluginsDir, "ndsudo"+sfx),
35 + }
36 +}
37
52 - cmd := CommandUnprivileged(ctx, logger, arg...)
38 +var defaultRunner = newRunnerFromBuildinfo()
39
54 - bs, err := cmd.Output()
55 - if err != nil {
56 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
57 - }
40 +// RunUnprivileged runs binPath via nd-run with a timeout.
41 +// Returns stdout. On error, wraps the original error and includes a trimmed stderr snippet.
42 +func RunUnprivileged(log *logger.Logger, timeout time.Duration, binPath string, args ...string) ([]byte, error) {
43 + out, _, err := RunUnprivilegedWithCmd(log, timeout, binPath, args...)
44 + return out, err
45 +}
46
59 - return bs, nil
47 +// RunNDSudo runs cmd via ndsudo with a timeout.
48 +// Returns stdout. On error, wraps the original error and includes a trimmed stderr snippet
49 +func RunNDSudo(log *logger.Logger, timeout time.Duration, cmd string, args ...string) ([]byte, error) {
50 + out, _, err := RunNDSudoWithCmd(log, timeout, cmd, args...)
51 + return out, err
52 }
53
62 -// CommandNDSudo runs a command via the ndsudo helper and returns the exec.Cmd instance.
63 -//
64 -// ctx is a context.Context to use to run the command. logger is a Logger
65 -// instance to use to log the command to be executed. timeout indicates
66 -// the timeout for the command. arg is a list of the command arguments,
67 -// with the first string in the slice being the command to run.
68 -//
69 -// This invokes the command and logs a debug message that the command
70 -// is being executed, and then returns the exec.Cmd object for the command.
71 -func CommandNDSudo(ctx context.Context, logger *logger.Logger, arg ...string) *exec.Cmd {
72 - ndsudoPath := filepath.Join(buildinfo.PluginsDir, "ndsudo")
73 -
74 - cmd := exec.CommandContext(ctx, ndsudoPath, arg...)
75 - if logger != nil {
76 - logger.Debugf("executing '%s'", cmd)
77 - }
54 +// RunUnprivilegedWithCmd runs binPath via nd-run and also returns the formatted command string.
55 +func RunUnprivilegedWithCmd(log *logger.Logger, timeout time.Duration, binPath string, args ...string) ([]byte, string, error) {
56 + argv := append([]string{binPath}, args...)
57 + return defaultRunner.run(log, timeout, defaultRunner.ndRunPath, "RunUnprivileged", argv...)
58 +}
59
79 - return cmd
60 +// RunNDSudoWithCmd runs cmd via ndsudo and also returns the formatted command string.
61 +func RunNDSudoWithCmd(log *logger.Logger, timeout time.Duration, cmd string, args ...string) ([]byte, string, error) {
62 + argv := append([]string{cmd}, args...)
63 + return defaultRunner.run(log, timeout, defaultRunner.ndSudoPath, "RunNDSudo", argv...)
64 }
65
82 -// RunNDSudo runs a command via the ndsudo helper.
83 -//
84 -// logger is a Logger instance to use to log the command to be executed.
85 -// timeout indicates the timeout for the command. arg is a list of the
86 -// command arguments, with the first string in the slice being the command
87 -// to run.
88 -//
89 -// This handles constructing the context for execution, logs a debug
90 -// message that the command is being executed, and checks for errors in
91 -// the command invocation, then returns the command output.
92 -//
93 -// The command to be run must also be properly handled by ndsudo.
94 -func RunNDSudo(logger *logger.Logger, timeout time.Duration, arg ...string) ([]byte, error) {
66 +func (r *runner) run(log *logger.Logger, timeout time.Duration, helperPath, label string, argv ...string) ([]byte, string, error) {
67 ctx, cancel := context.WithTimeout(context.Background(), timeout)
68 defer cancel()
69
98 - cmd := CommandNDSudo(ctx, logger, arg...)
70 + ex := exec.CommandContext(ctx, helperPath, argv...) // argv comes from trusted sources; no shell, args passed separately
71 +
72 + log.Debugf("executing: %v", ex)
73 +
74 + var stderr bytes.Buffer
75 + ex.Stderr = &stderr
76 +
77 + cmdStr := ex.String()
78
100 - bs, err := cmd.Output()
79 + out, err := ex.Output()
80 if err != nil {
102 - return nil, fmt.Errorf("error on '%s': %v", cmd, err)
81 + s := stderr.String()
82 + if len(s) > stderrLimit {
83 + s = s[:stderrLimit] + "… (truncated)"
84 + }
85 + // Normalize context-related errors so callers can errors.Is(..., context.DeadlineExceeded)
86 + if ctx.Err() != nil {
87 + err = ctx.Err()
88 + }
89 +
90 + return nil, cmdStr, fmt.Errorf("%s: %v: %w (stderr: %s)", label, ex, err, strings.TrimSpace(s))
91 }
92
105 - return bs, nil
93 + return out, cmdStr, nil
94 }
src/go/plugin/go.d/pkg/ndexec/ndexec_test.go new
+121
@@ -0,0 +1,121 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package ndexec
4 +
5 +import (
6 + "context"
7 + "os"
8 + "path/filepath"
9 + "runtime"
10 + "strings"
11 + "testing"
12 + "time"
13 +
14 + "github.com/stretchr/testify/assert"
15 + "github.com/stretchr/testify/require"
16 +)
17 +
18 +func TestRunner_run(t *testing.T) {
19 + if runtime.GOOS == "windows" {
20 + t.Skip("uses sh scripts")
21 + }
22 +
23 + tmp := t.TempDir()
24 +
25 + writeExe := func(path, body string) {
26 + require.NoError(t, os.WriteFile(path, []byte(body), 0o755), "write %s", path)
27 + }
28 +
29 + // Target scripts the helper will exec into.
30 + echoArgs := filepath.Join(tmp, "echoargs.sh")
31 + writeExe(echoArgs, `#!/bin/sh
32 +printf '%s|' "$@"
33 +echo
34 +`)
35 +
36 + longErr := filepath.Join(tmp, "longerr.sh")
37 + long := strings.Repeat("x", 9000) // > stderrLimit
38 + writeExe(longErr, `#!/bin/sh
39 +printf '`+long+`' 1>&2
40 +exit 17
41 +`)
42 +
43 + sleeper := filepath.Join(tmp, "sleep.sh")
44 + writeExe(sleeper, `#!/bin/sh
45 +sleep "$1"
46 +`)
47 +
48 + // Fake helper (acts like nd-run/ndsudo): replaces itself with the target.
49 + helper := filepath.Join(tmp, "helper.sh")
50 + writeExe(helper, `#!/bin/sh
51 +exec "$@"
52 +`)
53 +
54 + type tc struct {
55 + helperPath string
56 + argv []string
57 + timeout time.Duration
58 + wantOut string
59 + wantErr bool
60 + errContains []string
61 + check func(t *testing.T, out []byte, err error)
62 + }
63 +
64 + tests := map[string]tc{
65 + "success_echo_args": {
66 + helperPath: helper,
67 + argv: []string{echoArgs, `a b`, `c"d`},
68 + timeout: time.Second,
69 + wantOut: "a b|c\"d|\n",
70 + },
71 + "nonzero_with_trimmed_stderr": {
72 + helperPath: helper,
73 + argv: []string{longErr},
74 + timeout: time.Second,
75 + wantErr: true,
76 + errContains: []string{"stderr:", "truncated"},
77 + },
78 + "timeout": {
79 + helperPath: helper,
80 + argv: []string{sleeper, "2"},
81 + timeout: 200 * time.Millisecond,
82 + wantErr: true,
83 + errContains: []string{"deadline"},
84 + check: func(t *testing.T, _ []byte, err error) {
85 + // Either errors.Is(err, context.DeadlineExceeded) or message contains it.
86 + assert.ErrorIs(t, err, context.DeadlineExceeded)
87 + },
88 + },
89 + "helper_missing": {
90 + helperPath: filepath.Join(tmp, "missing", "helper.sh"),
91 + argv: []string{echoArgs},
92 + timeout: time.Second,
93 + wantErr: true,
94 + errContains: []string{"no such file", "helper.sh"},
95 + },
96 + }
97 +
98 + r := &runner{} // we pass helperPath directly to run()
99 +
100 + for name, tt := range tests {
101 + t.Run(name, func(t *testing.T) {
102 + out, _, err := r.run(nil, tt.timeout, tt.helperPath, "RunTest", tt.argv...)
103 +
104 + if tt.wantErr {
105 + require.Error(t, err)
106 + for _, frag := range tt.errContains {
107 + assert.Contains(t, strings.ToLower(err.Error()), strings.ToLower(frag))
108 + }
109 + } else {
110 + require.NoError(t, err)
111 + if tt.wantOut != "" {
112 + assert.Equal(t, tt.wantOut, string(out))
113 + }
114 + }
115 +
116 + if tt.check != nil {
117 + tt.check(t, out, err)
118 + }
119 + })
120 + }
121 +}