feat(collectors): add Windows support for hardware monitoring collectors (#21635)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: ilyam8 <ilya@netdata.cloud>
Costa Tsaousis committed
Feb 13, 2026 at 04:21 UTC
686fdb86f912b5cedf2ad3780b6363b6cee67c5c
37 files changed
+555
-113
src/go/plugin/go.d/collector/adaptecraid/charts.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package adaptecraid
4
5
import (
src/go/plugin/go.d/collector/adaptecraid/collect.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package adaptecraid
4
5
import (
src/go/plugin/go.d/collector/adaptecraid/collect_ld.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package adaptecraid
4
5
import (
src/go/plugin/go.d/collector/adaptecraid/collect_pd.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package adaptecraid
4
5
import (
src/go/plugin/go.d/collector/adaptecraid/collector.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package adaptecraid
4
5
import (
src/go/plugin/go.d/collector/adaptecraid/collector_test.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package adaptecraid
4
5
import (
src/go/plugin/go.d/collector/adaptecraid/exec.go
+33
-10
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package adaptecraid
4
5
import (
@@ -16,22 +14,47 @@ type arcconfCli interface {
14
physicalDevicesInfo() ([]byte, error)
15
}
16
19
-func newArcconfCliExec(timeout time.Duration, log *logger.Logger) *arcconfCliExec {
20
- return &arcconfCliExec{
17
+// ndsudoArcconfCliExec executes arcconf via ndsudo (Linux/BSD)
18
+type ndsudoArcconfCliExec struct {
19
+ *logger.Logger
20
+ timeout time.Duration
21
+}
22
+
23
+func newNdsudoArcconfCliExec(timeout time.Duration, log *logger.Logger) *ndsudoArcconfCliExec {
24
+ return &ndsudoArcconfCliExec{
25
Logger: log,
26
timeout: timeout,
27
}
28
}
29
26
-type arcconfCliExec struct {
30
+func (e *ndsudoArcconfCliExec) logicalDevicesInfo() ([]byte, error) {
31
+ return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-ld-info")
32
+}
33
+
34
+func (e *ndsudoArcconfCliExec) physicalDevicesInfo() ([]byte, error) {
35
+ return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-pd-info")
36
+}
37
+
38
+// directArcconfCliExec executes arcconf directly (Windows)
39
+type directArcconfCliExec struct {
40
*logger.Logger
28
- timeout time.Duration
41
+
42
+ arcconfPath string
43
+ timeout time.Duration
44
}
45
31
-func (e *arcconfCliExec) logicalDevicesInfo() ([]byte, error) {
32
- return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-ld-info")
46
+func newDirectArcconfCliExec(arcconfPath string, timeout time.Duration, log *logger.Logger) *directArcconfCliExec {
47
+ return &directArcconfCliExec{
48
+ Logger: log,
49
+ arcconfPath: arcconfPath,
50
+ timeout: timeout,
51
+ }
52
}
53
35
-func (e *arcconfCliExec) physicalDevicesInfo() ([]byte, error) {
36
- return ndexec.RunNDSudo(e.Logger, e.timeout, "arcconf-pd-info")
54
+func (e *directArcconfCliExec) logicalDevicesInfo() ([]byte, error) {
55
+ return ndexec.RunDirect(e.Logger, e.timeout, e.arcconfPath, "GETCONFIG", "1", "LD")
56
+}
57
+
58
+func (e *directArcconfCliExec) physicalDevicesInfo() ([]byte, error) {
59
+ return ndexec.RunDirect(e.Logger, e.timeout, e.arcconfPath, "GETCONFIG", "1", "PD")
60
}
src/go/plugin/go.d/collector/adaptecraid/init.go
+35
-3
@@ -1,10 +1,42 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package adaptecraid
4
5
+import (
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "runtime"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12
+)
13
+
14
func (c *Collector) initArcconfCliExec() (arcconfCli, error) {
8
- arcconfExec := newArcconfCliExec(c.Timeout.Duration(), c.Logger)
15
+ if runtime.GOOS == "windows" {
16
+ return c.initDirectArcconfCliExec()
17
+ }
18
+ return c.initNdsudoArcconfCliExec()
19
+}
20
+
21
+func (c *Collector) initNdsudoArcconfCliExec() (arcconfCli, error) {
22
+ arcconfExec := newNdsudoArcconfCliExec(c.Timeout.Duration(), c.Logger)
23
return arcconfExec, nil
24
}
25
+
26
+func (c *Collector) initDirectArcconfCliExec() (arcconfCli, error) {
27
+ path, err := ndexec.FindBinary(
28
+ []string{"arcconf", "ARCCONF"},
29
+ []string{
30
+ filepath.Join(os.Getenv("ProgramFiles"), "Adaptec", "ARCCONF", "arcconf.exe"),
31
+ filepath.Join(os.Getenv("ProgramFiles"), "Microsemi", "ARCCONF", "arcconf.exe"),
32
+ filepath.Join(os.Getenv("ProgramFiles(x86)"), "Adaptec", "ARCCONF", "arcconf.exe"),
33
+ },
34
+ )
35
+ if err != nil {
36
+ return nil, fmt.Errorf("arcconf: %w", err)
37
+ }
38
+
39
+ c.Debugf("found arcconf at: %s", path)
40
+
41
+ return newDirectArcconfCliExec(path, c.Timeout.Duration(), c.Logger), nil
42
+}
src/go/plugin/go.d/collector/hpssa/exec.go
+27
-6
@@ -13,19 +13,40 @@ type ssacliBinary interface {
13
controllersInfo() ([]byte, error)
14
}
15
16
-func newSsacliExec(timeout time.Duration, log *logger.Logger) *ssacliExec {
17
- return &ssacliExec{
16
+// ndsudoSsacliExec executes ssacli via ndsudo (Linux/BSD)
17
+type ndsudoSsacliExec struct {
18
+ *logger.Logger
19
+
20
+ timeout time.Duration
21
+}
22
+
23
+func newNdsudoSsacliExec(timeout time.Duration, log *logger.Logger) *ndsudoSsacliExec {
24
+ return &ndsudoSsacliExec{
25
Logger: log,
26
timeout: timeout,
27
}
28
}
29
23
-type ssacliExec struct {
30
+func (e *ndsudoSsacliExec) controllersInfo() ([]byte, error) {
31
+ return ndexec.RunNDSudo(e.Logger, e.timeout, "ssacli-controllers-info")
32
+}
33
+
34
+// directSsacliExec executes ssacli directly (Windows)
35
+type directSsacliExec struct {
36
*logger.Logger
37
26
- timeout time.Duration
38
+ ssacliPath string
39
+ timeout time.Duration
40
}
41
29
-func (e *ssacliExec) controllersInfo() ([]byte, error) {
30
- return ndexec.RunNDSudo(e.Logger, e.timeout, "ssacli-controllers-info")
42
+func newDirectSsacliExec(ssacliPath string, timeout time.Duration, log *logger.Logger) *directSsacliExec {
43
+ return &directSsacliExec{
44
+ Logger: log,
45
+ ssacliPath: ssacliPath,
46
+ timeout: timeout,
47
+ }
48
+}
49
+
50
+func (e *directSsacliExec) controllersInfo() ([]byte, error) {
51
+ return ndexec.RunDirect(e.Logger, e.timeout, e.ssacliPath, "ctrl", "all", "show", "config", "detail")
52
}
src/go/plugin/go.d/collector/hpssa/init.go
+35
-1
@@ -2,8 +2,42 @@
2
3
package hpssa
4
5
+import (
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "runtime"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12
+)
13
+
14
func (c *Collector) initSsacliBinary() (ssacliBinary, error) {
6
- ssacliExec := newSsacliExec(c.Timeout.Duration(), c.Logger)
15
+ if runtime.GOOS == "windows" {
16
+ return c.initDirectSsacliExec()
17
+ }
18
+ return c.initNdsudoSsacliExec()
19
+}
20
21
+func (c *Collector) initNdsudoSsacliExec() (ssacliBinary, error) {
22
+ ssacliExec := newNdsudoSsacliExec(c.Timeout.Duration(), c.Logger)
23
return ssacliExec, nil
24
}
25
+
26
+func (c *Collector) initDirectSsacliExec() (ssacliBinary, error) {
27
+ path, err := ndexec.FindBinary(
28
+ []string{"ssacli", "SSACLI", "hpssacli"},
29
+ []string{
30
+ filepath.Join(os.Getenv("ProgramFiles"), "Smart Storage Administrator", "ssacli", "bin", "ssacli.exe"),
31
+ filepath.Join(os.Getenv("ProgramFiles"), "HP", "hpssacli", "bin", "hpssacli.exe"),
32
+ filepath.Join(os.Getenv("ProgramFiles"), "Compaq", "Hpacucli", "Bin", "hpacucli.exe"),
33
+ filepath.Join(os.Getenv("ProgramFiles(x86)"), "Smart Storage Administrator", "ssacli", "bin", "ssacli.exe"),
34
+ },
35
+ )
36
+ if err != nil {
37
+ return nil, fmt.Errorf("ssacli: %w", err)
38
+ }
39
+
40
+ c.Debugf("found ssacli at: %s", path)
41
+
42
+ return newDirectSsacliExec(path, c.Timeout.Duration(), c.Logger), nil
43
+}
src/go/plugin/go.d/collector/megacli/charts.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package megacli
4
5
import (
src/go/plugin/go.d/collector/megacli/collect.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package megacli
4
5
import (
src/go/plugin/go.d/collector/megacli/collect_bbu.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package megacli
4
5
import (
src/go/plugin/go.d/collector/megacli/collect_phys_drives.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package megacli
4
5
import (
src/go/plugin/go.d/collector/megacli/collector.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package megacli
4
5
import (
src/go/plugin/go.d/collector/megacli/collector_test.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package megacli
4
5
import (
src/go/plugin/go.d/collector/megacli/exec.go
+33
-10
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package megacli
4
5
import (
@@ -16,23 +14,48 @@ type megaCli interface {
14
bbuInfo() ([]byte, error)
15
}
16
19
-func newMegaCliExec(timeout time.Duration, log *logger.Logger) *megaCliExec {
20
- return &megaCliExec{
17
+// ndsudoMegaCliExec executes megacli via ndsudo (Linux/BSD)
18
+type ndsudoMegaCliExec struct {
19
+ *logger.Logger
20
+
21
+ timeout time.Duration
22
+}
23
+
24
+func newNdsudoMegaCliExec(timeout time.Duration, log *logger.Logger) *ndsudoMegaCliExec {
25
+ return &ndsudoMegaCliExec{
26
Logger: log,
27
timeout: timeout,
28
}
29
}
30
26
-type megaCliExec struct {
31
+func (e *ndsudoMegaCliExec) physDrivesInfo() ([]byte, error) {
32
+ return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-disk-info")
33
+}
34
+
35
+func (e *ndsudoMegaCliExec) bbuInfo() ([]byte, error) {
36
+ return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-battery-info")
37
+}
38
+
39
+// directMegaCliExec executes megacli directly (Windows)
40
+type directMegaCliExec struct {
41
*logger.Logger
42
29
- timeout time.Duration
43
+ megacliPath string
44
+ timeout time.Duration
45
}
46
32
-func (e *megaCliExec) physDrivesInfo() ([]byte, error) {
33
- return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-disk-info")
47
+func newDirectMegaCliExec(megacliPath string, timeout time.Duration, log *logger.Logger) *directMegaCliExec {
48
+ return &directMegaCliExec{
49
+ Logger: log,
50
+ megacliPath: megacliPath,
51
+ timeout: timeout,
52
+ }
53
}
54
36
-func (e *megaCliExec) bbuInfo() ([]byte, error) {
37
- return ndexec.RunNDSudo(e.Logger, e.timeout, "megacli-battery-info")
55
+func (e *directMegaCliExec) physDrivesInfo() ([]byte, error) {
56
+ return ndexec.RunDirect(e.Logger, e.timeout, e.megacliPath, "-LDPDInfo", "-aAll", "-NoLog")
57
+}
58
+
59
+func (e *directMegaCliExec) bbuInfo() ([]byte, error) {
60
+ return ndexec.RunDirect(e.Logger, e.timeout, e.megacliPath, "-AdpBbuCmd", "-aAll", "-NoLog")
61
}
src/go/plugin/go.d/collector/megacli/init.go
+34
-3
@@ -1,11 +1,42 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package megacli
4
5
+import (
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "runtime"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12
+)
13
+
14
func (c *Collector) initMegaCliExec() (megaCli, error) {
8
- megaExec := newMegaCliExec(c.Timeout.Duration(), c.Logger)
15
+ if runtime.GOOS == "windows" {
16
+ return c.initDirectMegaCliExec()
17
+ }
18
+ return c.initNdsudoMegaCliExec()
19
+}
20
21
+func (c *Collector) initNdsudoMegaCliExec() (megaCli, error) {
22
+ megaExec := newNdsudoMegaCliExec(c.Timeout.Duration(), c.Logger)
23
return megaExec, nil
24
}
25
+
26
+func (c *Collector) initDirectMegaCliExec() (megaCli, error) {
27
+ path, err := ndexec.FindBinary(
28
+ []string{"megacli", "MegaCli", "MegaCli64", "megacli64"},
29
+ []string{
30
+ filepath.Join(os.Getenv("ProgramFiles"), "LSI", "MegaCLI", "MegaCli64.exe"),
31
+ filepath.Join(os.Getenv("ProgramFiles"), "Broadcom", "MegaCLI", "MegaCli64.exe"),
32
+ filepath.Join(os.Getenv("ProgramFiles(x86)"), "MegaCLI", "MegaCli64.exe"),
33
+ },
34
+ )
35
+ if err != nil {
36
+ return nil, fmt.Errorf("megacli: %w", err)
37
+ }
38
+
39
+ c.Debugf("found megacli at: %s", path)
40
+
41
+ return newDirectMegaCliExec(path, c.Timeout.Duration(), c.Logger), nil
42
+}
src/go/plugin/go.d/collector/nvidia_smi/exec.go
+23
@@ -8,6 +8,7 @@ import (
8
"errors"
9
"os/exec"
10
"path/filepath"
11
+ "runtime"
12
"strconv"
13
"sync"
14
"time"
@@ -24,6 +25,13 @@ type nvidiaSmiBinary interface {
25
26
func newNvidiaSmiBinary(path string, cfg Config, log *logger.Logger) (nvidiaSmiBinary, error) {
27
if !cfg.LoopMode {
28
+ if runtime.GOOS == "windows" {
29
+ return &nvidiaSmiDirectExec{
30
+ Logger: log,
31
+ binPath: path,
32
+ timeout: cfg.Timeout.Duration(),
33
+ }, nil
34
+ }
35
return &nvidiaSmiExec{
36
Logger: log,
37
binPath: path,
@@ -45,6 +53,7 @@ func newNvidiaSmiBinary(path string, cfg Config, log *logger.Logger) (nvidiaSmiB
53
return smi, nil
54
}
55
56
+// nvidiaSmiExec executes nvidia-smi via nd-run (Linux/BSD)
57
type nvidiaSmiExec struct {
58
*logger.Logger
59
@@ -58,6 +67,20 @@ func (e *nvidiaSmiExec) queryGPUInfo() ([]byte, error) {
67
68
func (e *nvidiaSmiExec) stop() error { return nil }
69
70
+// nvidiaSmiDirectExec executes nvidia-smi directly (Windows)
71
+type nvidiaSmiDirectExec struct {
72
+ *logger.Logger
73
+
74
+ binPath string
75
+ timeout time.Duration
76
+}
77
+
78
+func (e *nvidiaSmiDirectExec) queryGPUInfo() ([]byte, error) {
79
+ return ndexec.RunDirect(e.Logger, e.timeout, e.binPath, "-q", "-x")
80
+}
81
+
82
+func (e *nvidiaSmiDirectExec) stop() error { return nil }
83
+
84
type nvidiaSmiLoopExec struct {
85
*logger.Logger
86
src/go/plugin/go.d/collector/nvidia_smi/init.go
+21
-2
@@ -6,16 +6,30 @@ import (
6
"fmt"
7
"os"
8
"os/exec"
9
+ "path/filepath"
10
+ "runtime"
11
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
13
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/pathvalidate"
14
)
15
16
func (c *Collector) initNvidiaSmiExec() (nvidiaSmiBinary, error) {
17
binPath := c.BinaryPath
15
- if _, err := os.Stat(binPath); os.IsNotExist(err) {
18
+ if binPath == "" || !fileExists(binPath) {
19
path, err := exec.LookPath(c.binName)
20
if err != nil {
18
- return nil, fmt.Errorf("error on lookup '%s': %v", c.binName, err)
21
+ if runtime.GOOS == "windows" {
22
+ path, err = ndexec.FindBinary(
23
+ nil,
24
+ []string{
25
+ filepath.Join(os.Getenv("ProgramFiles"), "NVIDIA Corporation", "NVSMI", "nvidia-smi.exe"),
26
+ filepath.Join(os.Getenv("SystemRoot"), "System32", "nvidia-smi.exe"),
27
+ },
28
+ )
29
+ }
30
+ if err != nil {
31
+ return nil, fmt.Errorf("error on lookup '%s': %v", c.binName, err)
32
+ }
33
}
34
binPath = path
35
}
@@ -27,3 +41,8 @@ func (c *Collector) initNvidiaSmiExec() (nvidiaSmiBinary, error) {
41
42
return newNvidiaSmiBinary(validatedPath, c.Config, c.Logger)
43
}
44
+
45
+func fileExists(path string) bool {
46
+ _, err := os.Stat(path)
47
+ return !os.IsNotExist(err)
48
+}
src/go/plugin/go.d/collector/nvme/charts.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package nvme
4
5
import (
src/go/plugin/go.d/collector/nvme/collect.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package nvme
4
5
import (
src/go/plugin/go.d/collector/nvme/collector.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package nvme
4
5
import (
src/go/plugin/go.d/collector/nvme/collector_test.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package nvme
4
5
import (
src/go/plugin/go.d/collector/nvme/exec.go
+41
-5
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package nvme
4
5
import (
@@ -11,6 +9,7 @@ import (
9
"strings"
10
"time"
11
12
+ "github.com/netdata/netdata/go/plugins/logger"
13
"github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
14
)
15
@@ -143,11 +142,12 @@ type nvmeCli interface {
142
smartLog(devicePath string) (*nvmeDeviceSmartLog, error)
143
}
144
146
-type nvmeCLIExec struct {
145
+// ndsudoNvmeCliExec executes nvme via ndsudo (Linux/BSD)
146
+type ndsudoNvmeCliExec struct {
147
timeout time.Duration
148
}
149
150
-func (n *nvmeCLIExec) list() (*nvmeDeviceList, error) {
150
+func (n *ndsudoNvmeCliExec) list() (*nvmeDeviceList, error) {
151
bs, err := ndexec.RunNDSudo(nil, n.timeout, "nvme-list")
152
if err != nil {
153
return nil, err
@@ -161,7 +161,7 @@ func (n *nvmeCLIExec) list() (*nvmeDeviceList, error) {
161
return &v, nil
162
}
163
164
-func (n *nvmeCLIExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
164
+func (n *ndsudoNvmeCliExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
165
bs, err := ndexec.RunNDSudo(nil, n.timeout, "nvme-smart-log", "--device", devicePath)
166
if err != nil {
167
return nil, err
@@ -174,3 +174,39 @@ func (n *nvmeCLIExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
174
175
return &v, nil
176
}
177
+
178
+// directNvmeCliExec executes nvme directly (Windows)
179
+type directNvmeCliExec struct {
180
+ *logger.Logger
181
+
182
+ nvmePath string
183
+ timeout time.Duration
184
+}
185
+
186
+func (n *directNvmeCliExec) list() (*nvmeDeviceList, error) {
187
+ bs, err := ndexec.RunDirect(n.Logger, n.timeout, n.nvmePath, "list", "--output-format=json")
188
+ if err != nil {
189
+ return nil, err
190
+ }
191
+
192
+ var v nvmeDeviceList
193
+ if err := json.Unmarshal(bs, &v); err != nil {
194
+ return nil, err
195
+ }
196
+
197
+ return &v, nil
198
+}
199
+
200
+func (n *directNvmeCliExec) smartLog(devicePath string) (*nvmeDeviceSmartLog, error) {
201
+ bs, err := ndexec.RunDirect(n.Logger, n.timeout, n.nvmePath, "smart-log", devicePath, "--output-format=json")
202
+ if err != nil {
203
+ return nil, err
204
+ }
205
+
206
+ var v nvmeDeviceSmartLog
207
+ if err := json.Unmarshal(bs, &v); err != nil {
208
+ return nil, err
209
+ }
210
+
211
+ return &v, nil
212
+}
src/go/plugin/go.d/collector/nvme/init.go
+37
-3
@@ -1,11 +1,45 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package nvme
4
5
+import (
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "runtime"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12
+)
13
+
14
func (c *Collector) initNVMeCLIExec() (nvmeCli, error) {
8
- nvmeExec := &nvmeCLIExec{timeout: c.Timeout.Duration()}
15
+ if runtime.GOOS == "windows" {
16
+ return c.initDirectNvmeCliExec()
17
+ }
18
+ return c.initNdsudoNvmeCliExec()
19
+}
20
21
+func (c *Collector) initNdsudoNvmeCliExec() (nvmeCli, error) {
22
+ nvmeExec := &ndsudoNvmeCliExec{timeout: c.Timeout.Duration()}
23
return nvmeExec, nil
24
}
25
+
26
+func (c *Collector) initDirectNvmeCliExec() (nvmeCli, error) {
27
+ path, err := ndexec.FindBinary(
28
+ []string{"nvme", "nvme-cli"},
29
+ []string{
30
+ filepath.Join(os.Getenv("ProgramFiles"), "nvme-cli", "nvme.exe"),
31
+ filepath.Join(os.Getenv("ProgramFiles(x86)"), "nvme-cli", "nvme.exe"),
32
+ },
33
+ )
34
+ if err != nil {
35
+ return nil, fmt.Errorf("nvme: %w", err)
36
+ }
37
+
38
+ c.Debugf("found nvme at: %s", path)
39
+
40
+ return &directNvmeCliExec{
41
+ Logger: c.Logger,
42
+ nvmePath: path,
43
+ timeout: c.Timeout.Duration(),
44
+ }, nil
45
+}
src/go/plugin/go.d/collector/smartctl/init.go
+11
-13
@@ -5,11 +5,11 @@ package smartctl
5
import (
6
"fmt"
7
"os"
8
- "os/exec"
8
"path/filepath"
9
"runtime"
10
11
"github.com/netdata/netdata/go/plugins/pkg/matcher"
12
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
13
)
14
15
func (c *Collector) validateConfig() error {
@@ -54,19 +54,17 @@ func (c *Collector) initNdsudoSmartctlCli() (smartctlCli, error) {
54
}
55
56
func (c *Collector) initDirectSmartctlCli() (smartctlCli, error) {
57
- smartctlPath, err := exec.LookPath("smartctl")
57
+ path, err := ndexec.FindBinary(
58
+ []string{"smartctl"},
59
+ []string{
60
+ filepath.Join(os.Getenv("ProgramFiles"), "smartmontools", "bin", "smartctl.exe"),
61
+ },
62
+ )
63
if err != nil {
59
- if runtime.GOOS != "windows" {
60
- return nil, fmt.Errorf("smartctl executable not found in PATH: %v", err)
61
- }
62
- defaultWinPath := filepath.Join("C:\\Program Files\\smartmontools\\bin", "smartctl.exe")
63
- if _, err := os.Stat(defaultWinPath); err != nil {
64
- return nil, fmt.Errorf("smartctl executable not found in PATH or default location: %v", err)
65
- }
66
- smartctlPath = defaultWinPath
64
+ return nil, fmt.Errorf("smartctl: %w", err)
65
}
66
69
- c.Debugf("found smartctl at: %s", smartctlPath)
70
- smartctlExec := newDirectSmartctlCli(smartctlPath, c.Timeout.Duration(), c.Logger)
71
- return smartctlExec, nil
67
+ c.Debugf("found smartctl at: %s", path)
68
+
69
+ return newDirectSmartctlCli(path, c.Timeout.Duration(), c.Logger), nil
70
}
src/go/plugin/go.d/collector/storcli/charts.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package storcli
4
5
import (
src/go/plugin/go.d/collector/storcli/collect.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package storcli
4
5
import "fmt"
src/go/plugin/go.d/collector/storcli/collect_controllers.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package storcli
4
5
import (
src/go/plugin/go.d/collector/storcli/collect_drives.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package storcli
4
5
import (
src/go/plugin/go.d/collector/storcli/collector.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package storcli
4
5
import (
src/go/plugin/go.d/collector/storcli/collector_test.go
-2
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package storcli
4
5
import (
src/go/plugin/go.d/collector/storcli/exec.go
+33
-10
@@ -1,7 +1,5 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package storcli
4
5
import (
@@ -16,23 +14,48 @@ type storCli interface {
14
drivesInfo() ([]byte, error)
15
}
16
19
-func newStorCliExec(timeout time.Duration, log *logger.Logger) *storCliExec {
20
- return &storCliExec{
17
+// ndsudoStorCliExec executes storcli via ndsudo (Linux/BSD)
18
+type ndsudoStorCliExec struct {
19
+ *logger.Logger
20
+
21
+ timeout time.Duration
22
+}
23
+
24
+func newNdsudoStorCliExec(timeout time.Duration, log *logger.Logger) *ndsudoStorCliExec {
25
+ return &ndsudoStorCliExec{
26
Logger: log,
27
timeout: timeout,
28
}
29
}
30
26
-type storCliExec struct {
31
+func (e *ndsudoStorCliExec) controllersInfo() ([]byte, error) {
32
+ return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-controllers-info")
33
+}
34
+
35
+func (e *ndsudoStorCliExec) drivesInfo() ([]byte, error) {
36
+ return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-drives-info")
37
+}
38
+
39
+// directStorCliExec executes storcli directly (Windows)
40
+type directStorCliExec struct {
41
*logger.Logger
42
29
- timeout time.Duration
43
+ storcliPath string
44
+ timeout time.Duration
45
}
46
32
-func (e *storCliExec) controllersInfo() ([]byte, error) {
33
- return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-controllers-info")
47
+func newDirectStorCliExec(storcliPath string, timeout time.Duration, log *logger.Logger) *directStorCliExec {
48
+ return &directStorCliExec{
49
+ Logger: log,
50
+ storcliPath: storcliPath,
51
+ timeout: timeout,
52
+ }
53
}
54
36
-func (e *storCliExec) drivesInfo() ([]byte, error) {
37
- return ndexec.RunNDSudo(e.Logger, e.timeout, "storcli-drives-info")
55
+func (e *directStorCliExec) controllersInfo() ([]byte, error) {
56
+ return ndexec.RunDirect(e.Logger, e.timeout, e.storcliPath, "/cALL", "show", "all", "J", "nolog")
57
+}
58
+
59
+func (e *directStorCliExec) drivesInfo() ([]byte, error) {
60
+ return ndexec.RunDirect(e.Logger, e.timeout, e.storcliPath, "/cALL/eALL/sALL", "show", "all", "J", "nolog")
61
}
src/go/plugin/go.d/collector/storcli/init.go
+34
-3
@@ -1,11 +1,42 @@
1
// SPDX-License-Identifier: GPL-3.0-or-later
2
3
-//go:build linux || freebsd || openbsd || netbsd || dragonfly
4
-
3
package storcli
4
5
+import (
6
+ "fmt"
7
+ "os"
8
+ "path/filepath"
9
+ "runtime"
10
+
11
+ "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/ndexec"
12
+)
13
+
14
func (c *Collector) initStorCliExec() (storCli, error) {
8
- storExec := newStorCliExec(c.Timeout.Duration(), c.Logger)
15
+ if runtime.GOOS == "windows" {
16
+ return c.initDirectStorCliExec()
17
+ }
18
+ return c.initNdsudoStorCliExec()
19
+}
20
21
+func (c *Collector) initNdsudoStorCliExec() (storCli, error) {
22
+ storExec := newNdsudoStorCliExec(c.Timeout.Duration(), c.Logger)
23
return storExec, nil
24
}
25
+
26
+func (c *Collector) initDirectStorCliExec() (storCli, error) {
27
+ path, err := ndexec.FindBinary(
28
+ []string{"storcli", "storcli64", "StorCLI", "StorCLI64"},
29
+ []string{
30
+ filepath.Join(os.Getenv("ProgramFiles"), "Broadcom", "StorCLI", "storcli64.exe"),
31
+ filepath.Join(os.Getenv("ProgramFiles"), "LSI", "StorCLI", "storcli64.exe"),
32
+ filepath.Join(os.Getenv("ProgramFiles(x86)"), "StorCLI", "storcli64.exe"),
33
+ },
34
+ )
35
+ if err != nil {
36
+ return nil, fmt.Errorf("storcli: %w", err)
37
+ }
38
+
39
+ c.Debugf("found storcli at: %s", path)
40
+
41
+ return newDirectStorCliExec(path, c.Timeout.Duration(), c.Logger), nil
42
+}
src/go/plugin/go.d/pkg/ndexec/ndexec.go
+50
@@ -6,6 +6,7 @@ import (
6
"bytes"
7
"context"
8
"fmt"
9
+ "os"
10
"os/exec"
11
"path/filepath"
12
"runtime"
@@ -114,6 +115,55 @@ func SetRunnerPathsForTests(ndRunPath, ndSudoPath string) {
115
}
116
}
117
118
+// RunDirect runs binPath directly with a timeout, without any wrapper (nd-run/ndsudo).
119
+// Returns stdout. On error, includes the command string and a trimmed stderr snippet.
120
+func RunDirect(log *logger.Logger, timeout time.Duration, binPath string, args ...string) ([]byte, error) {
121
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
122
+ defer cancel()
123
+
124
+ cmd := exec.CommandContext(ctx, binPath, args...)
125
+
126
+ if log != nil {
127
+ log.Debugf("executing '%s'", cmd)
128
+ }
129
+
130
+ var stderr bytes.Buffer
131
+ cmd.Stderr = &stderr
132
+
133
+ bs, err := cmd.Output()
134
+ if err != nil {
135
+ s := stderr.String()
136
+ if len(s) > stderrLimit {
137
+ s = s[:stderrLimit] + "… (truncated)"
138
+ }
139
+ return nil, fmt.Errorf("'%s' execution failed: %w (stderr: %s)", cmd, err, strings.TrimSpace(s))
140
+ }
141
+
142
+ return bs, nil
143
+}
144
+
145
+// FindBinary searches for a binary by trying names in PATH first,
146
+// then checking defaultPaths on the filesystem.
147
+// Returns the first found path, or an error if not found.
148
+func FindBinary(names []string, defaultPaths []string) (string, error) {
149
+ for _, name := range names {
150
+ if path, err := exec.LookPath(name); err == nil {
151
+ return path, nil
152
+ }
153
+ }
154
+
155
+ for _, path := range defaultPaths {
156
+ if fi, err := os.Stat(path); err == nil && !fi.IsDir() {
157
+ return path, nil
158
+ }
159
+ }
160
+
161
+ if len(names) == 0 {
162
+ return "", fmt.Errorf("executable not found in default locations")
163
+ }
164
+ return "", fmt.Errorf("executable not found in PATH (%s) or default locations", strings.Join(names, ", "))
165
+}
166
+
167
func (r *runner) run(log *logger.Logger, timeout time.Duration, dir string, helperPath, label string, env []string, argv ...string) ([]byte, string, ResourceUsage, error) {
168
ctx, cancel := context.WithTimeout(context.Background(), timeout)
169
defer cancel()
src/go/plugin/go.d/pkg/ndexec/ndexec_test.go
+108
@@ -120,6 +120,114 @@ exec "$@"
120
}
121
}
122
123
+func TestRunDirect(t *testing.T) {
124
+ if runtime.GOOS == "windows" {
125
+ t.Skip("uses sh scripts")
126
+ }
127
+
128
+ tmp := t.TempDir()
129
+
130
+ writeExe := func(path, body string) {
131
+ require.NoError(t, os.WriteFile(path, []byte(body), 0o755))
132
+ }
133
+
134
+ echoArgs := filepath.Join(tmp, "echoargs.sh")
135
+ writeExe(echoArgs, "#!/bin/sh\nprintf '%s|' \"$@\"\necho\n")
136
+
137
+ stderrScript := filepath.Join(tmp, "stderr.sh")
138
+ writeExe(stderrScript, "#!/bin/sh\necho 'some error' 1>&2\nexit 1\n")
139
+
140
+ sleeper := filepath.Join(tmp, "sleep.sh")
141
+ writeExe(sleeper, "#!/bin/sh\nsleep 2\n")
142
+
143
+ t.Run("success", func(t *testing.T) {
144
+ out, err := RunDirect(nil, time.Second, echoArgs, "hello", "world")
145
+ require.NoError(t, err)
146
+ assert.Equal(t, "hello|world|\n", string(out))
147
+ })
148
+
149
+ t.Run("non-zero exit with stderr", func(t *testing.T) {
150
+ _, err := RunDirect(nil, time.Second, stderrScript)
151
+ require.Error(t, err)
152
+ assert.Contains(t, err.Error(), "execution failed")
153
+ assert.Contains(t, err.Error(), "some error")
154
+ })
155
+
156
+ t.Run("timeout", func(t *testing.T) {
157
+ _, err := RunDirect(nil, 200*time.Millisecond, sleeper)
158
+ require.Error(t, err)
159
+ assert.Contains(t, err.Error(), "execution failed")
160
+ })
161
+
162
+ t.Run("binary not found", func(t *testing.T) {
163
+ _, err := RunDirect(nil, time.Second, filepath.Join(tmp, "nonexistent"))
164
+ require.Error(t, err)
165
+ assert.Contains(t, err.Error(), "execution failed")
166
+ })
167
+
168
+ t.Run("long stderr truncated", func(t *testing.T) {
169
+ longStderr := filepath.Join(tmp, "longstderr.sh")
170
+ writeExe(longStderr, "#!/bin/sh\nprintf '"+strings.Repeat("x", 9000)+"' 1>&2\nexit 1\n")
171
+
172
+ _, err := RunDirect(nil, 5*time.Second, longStderr)
173
+ require.Error(t, err)
174
+ assert.Contains(t, err.Error(), "truncated")
175
+ })
176
+}
177
+
178
+func TestFindBinary(t *testing.T) {
179
+ tmp := t.TempDir()
180
+
181
+ binPath := filepath.Join(tmp, "testbin")
182
+ require.NoError(t, os.WriteFile(binPath, []byte("#!/bin/sh\n"), 0o755))
183
+
184
+ t.Run("found in default paths", func(t *testing.T) {
185
+ path, err := FindBinary(
186
+ []string{"nonexistent-binary-12345"},
187
+ []string{filepath.Join(tmp, "missing"), binPath},
188
+ )
189
+ require.NoError(t, err)
190
+ assert.Equal(t, binPath, path)
191
+ })
192
+
193
+ t.Run("not found anywhere", func(t *testing.T) {
194
+ _, err := FindBinary(
195
+ []string{"nonexistent-binary-12345"},
196
+ []string{filepath.Join(tmp, "also-missing")},
197
+ )
198
+ require.Error(t, err)
199
+ assert.Contains(t, err.Error(), "executable not found")
200
+ })
201
+
202
+ t.Run("skips directories", func(t *testing.T) {
203
+ dirPath := filepath.Join(tmp, "adir")
204
+ require.NoError(t, os.Mkdir(dirPath, 0o755))
205
+
206
+ _, err := FindBinary(
207
+ []string{"nonexistent-binary-12345"},
208
+ []string{dirPath},
209
+ )
210
+ require.Error(t, err)
211
+ })
212
+
213
+ t.Run("nil names searches only default paths", func(t *testing.T) {
214
+ path, err := FindBinary(nil, []string{binPath})
215
+ require.NoError(t, err)
216
+ assert.Equal(t, binPath, path)
217
+ })
218
+
219
+ t.Run("nil names not found", func(t *testing.T) {
220
+ _, err := FindBinary(nil, []string{filepath.Join(tmp, "missing")})
221
+ require.Error(t, err)
222
+ assert.Contains(t, err.Error(), "executable not found")
223
+ })
224
+
225
+ t.Run("both nil", func(t *testing.T) {
226
+ _, err := FindBinary(nil, nil)
227
+ require.Error(t, err)
228
+ })
229
+}
230
+
231
func TestRunUnprivilegedWithOptionsCmdWorkingDir(t *testing.T) {
232
if runtime.GOOS == "windows" {
233
t.Skip("uses sh scripts")