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 ðtoolCLIExec{
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
+}