From bda952f85ab09b3072ff10bae5c2df4eb3173cff Mon Sep 17 00:00:00 2001 From: Michael Panzlaff Date: Mon, 6 Jul 2026 14:06:41 +0200 Subject: [PATCH] Add MegwareEurekaCollector --- collectors/collectorManager.go | 61 ++++++----- collectors/megwareEureka.go | 194 +++++++++++++++++++++++++++++++++ collectors/megwareEureka.md | 44 ++++++++ 3 files changed, 269 insertions(+), 30 deletions(-) create mode 100644 collectors/megwareEureka.go create mode 100644 collectors/megwareEureka.md diff --git a/collectors/collectorManager.go b/collectors/collectorManager.go index 88445fa..f352293 100644 --- a/collectors/collectorManager.go +++ b/collectors/collectorManager.go @@ -21,36 +21,37 @@ import ( // Map of all available metric collectors var AvailableCollectors = map[string]MetricCollector{ - "likwid": new(LikwidCollector), - "loadavg": new(LoadavgCollector), - "memstat": new(MemstatCollector), - "netstat": new(NetstatCollector), - "ibstat": new(InfinibandCollector), - "lustrestat": new(LustreCollector), - "cpustat": new(CpustatCollector), - "topprocs": new(TopProcsCollector), - "nvidia": new(NvidiaCollector), - "customcmd": new(CustomCmdCollector), - "iostat": new(IOstatCollector), - "diskstat": new(DiskstatCollector), - "tempstat": new(TempCollector), - "ipmistat": new(IpmiCollector), - "gpfs": new(GpfsCollector), - "cpufreq": new(CPUFreqCollector), - "cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector), - "nfs3stat": new(Nfs3Collector), - "nfs4stat": new(Nfs4Collector), - "numastats": new(NUMAStatsCollector), - "beegfs_meta": new(BeegfsMetaCollector), - "beegfs_storage": new(BeegfsStorageCollector), - "rapl": new(RAPLCollector), - "rocm_smi": new(RocmSmiCollector), - "self": new(SelfCollector), - "schedstat": new(SchedstatCollector), - "nfsiostat": new(NfsIOStatCollector), - "slurm_cgroup": new(SlurmCgroupCollector), - "smartmon": new(SmartMonCollector), - "lenovo_dense_power": new(LenovoDensePowerCollector), + "likwid": new(LikwidCollector), + "loadavg": new(LoadavgCollector), + "memstat": new(MemstatCollector), + "netstat": new(NetstatCollector), + "ibstat": new(InfinibandCollector), + "lustrestat": new(LustreCollector), + "cpustat": new(CpustatCollector), + "topprocs": new(TopProcsCollector), + "nvidia": new(NvidiaCollector), + "customcmd": new(CustomCmdCollector), + "iostat": new(IOstatCollector), + "diskstat": new(DiskstatCollector), + "tempstat": new(TempCollector), + "ipmistat": new(IpmiCollector), + "gpfs": new(GpfsCollector), + "cpufreq": new(CPUFreqCollector), + "cpufreq_cpuinfo": new(CPUFreqCpuInfoCollector), + "nfs3stat": new(Nfs3Collector), + "nfs4stat": new(Nfs4Collector), + "numastats": new(NUMAStatsCollector), + "beegfs_meta": new(BeegfsMetaCollector), + "beegfs_storage": new(BeegfsStorageCollector), + "rapl": new(RAPLCollector), + "rocm_smi": new(RocmSmiCollector), + "self": new(SelfCollector), + "schedstat": new(SchedstatCollector), + "nfsiostat": new(NfsIOStatCollector), + "slurm_cgroup": new(SlurmCgroupCollector), + "smartmon": new(SmartMonCollector), + "lenovo_dense_power": new(LenovoDensePowerCollector), + "megware_eureka": new(MegwareEurekaCollector), } // Metric collector manager data structure diff --git a/collectors/megwareEureka.go b/collectors/megwareEureka.go new file mode 100644 index 0000000..692bff5 --- /dev/null +++ b/collectors/megwareEureka.go @@ -0,0 +1,194 @@ +// Copyright (C) NHR@FAU, University Erlangen-Nuremberg. +// All rights reserved. +// Use of this source code is governed by a MIT-style +// license that can be found in the LICENSE file. +// additional authors: +// Michael Panzlaff (NHR@FAU) + +package collectors + +import ( + "bytes" + "encoding/json" + "fmt" + "os/exec" + "time" + + cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger" + lp "github.com/ClusterCockpit/cc-lib/v2/ccMessage" +) + +// MPS refers to Monolithic Power Systems, from which the MP5922 is used +// to measure telemetry on the Eureka platform. +type mpsData struct { + cnt int64 `json:"Cnt"` // No idea what this is, ignore + energy float64 `json:"Energy"` + vin float64 `json:"Vin"` + iin float64 `json:"Iin"` + pin float64 `json:"Pin"` + pinAvg float64 `json:"PinAvg"` + vout float64 `json:"Vout"` + iout float64 `json:"Iout"` + pout float64 `json:"Pout"` + standbyVout float64 `json:"StandbyVout"` + standbyIout float64 `json:"StandbyIout"` + standbyPout float64 `json:"StandbyPout"` + tempBusbar float64 `json:"TempBusbar"` + tempSsd float64 `json:"TempSsd"` + tempMps float64 `json:"TempMps"` + // No idea what the ones below mean exactl. Ignore them for now. + energyTime int64 `json:"EnergyTime"` + energyAccumulator int64 `json:"EnergyAccumulator"` + energyRolloverCnt int64 `json:"EnergyRolloverCnt"` + energySampleCntU24 int64 `json:"EnergySampleCntU24"` + + timestamp time.Time +} + +type MegwareEurekaCollector struct { + metricCollector + + config struct { + U20Path string `json:"u20_path"` + Sudo bool `json:"use_sudo"` + } + + u20path string + + energyValLast float64 + energyTimeLast time.Time +} + +func (m *MegwareEurekaCollector) Init(config json.RawMessage) error { + // Check if already initialized + if m.init { + return nil + } + + m.name = "MegwareEureka" + if err := m.setup(); err != nil { + return fmt.Errorf("%s Init(): setup() call failed: %w", m.name, err) + } + m.meta = map[string]string{ + "source": m.name, + "group": "U20", + } + + m.config.U20Path = "u20" + + if len(config) > 0 { + d := json.NewDecoder(bytes.NewReader(config)) + d.DisallowUnknownFields() + if err := d.Decode(&m.config); err != nil { + return fmt.Errorf("%s Init(): Error decoding JSON config: %w", m.name, err) + } + } + + m.u20path = m.config.U20Path + + data, err := m.readMpsData() + if err != nil { + return fmt.Errorf("Energy reading test failed: %w", err) + } + + m.energyValLast = data.energy + m.energyTimeLast = time.Now() + m.init = true + + return nil +} + +func (m *MegwareEurekaCollector) readMpsData() (*mpsData, error) { + argv := make([]string, 0) + if m.config.Sudo { + argv = append(argv, "sudo", "-n") + } + + argv = append(argv, m.u20path, "values", "--read", "GET_MPS_POLL_VALUES") + + cmd := exec.Command(argv[0], argv[1:]...) + + var stdout bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + if err != nil { + return nil, fmt.Errorf("Failed to run u20: %w (stdout=%s stderr=%s)", err, stdout.String(), stderr.String()) + } + + var u20output struct { + getMpsPollValues mpsData `json:"GET_MPS_POLL_VALUES"` + } + + err = json.Unmarshal([]byte(stdout.String()), &u20output) + if err != nil { + return nil, fmt.Errorf("Unable to decode u20 JSON output: %w (stdout=%s)", err, stdout.String()) + } + + u20output.getMpsPollValues.timestamp = time.Now() + + return &u20output.getMpsPollValues, nil +} + +func (m *MegwareEurekaCollector) Read(interval time.Duration, output chan lp.CCMessage) { + // Check if already initialized + if !m.init { + return + } + + data, err := m.readMpsData() + if err != nil { + cclog.ComponentErrorf(m.name, "readMpsData failed: %v", err) + return + } + + powerVal := 0.0 + + if data.timestamp.After(m.energyTimeLast) { + // Important, m.energy comes in Wh, so multiply by 3600 to get Ws (aka Joule) + energyValDiff := data.energy - m.energyValLast + energyTimeDiff := data.timestamp.Sub(m.energyTimeLast) + powerVal = energyValDiff * 3600 / energyTimeDiff.Seconds() + + m.energyValLast = data.energy + m.energyTimeLast = data.timestamp + } + + metricNamePrefix := "eureka_" + metricMap := map[string]struct{ + value any + unit string + }{ + "power": { value: powerVal, unit: "Watts" }, + "vin": { value: data.vin, unit: "Volts" }, + "iin": { value: data.iin, unit: "Amperes" }, + "pin": { value: data.pin, unit: "Watts" }, + "pin_avg": { value: data.pinAvg, unit: "Watts" }, + "vout": { value: data.vout, unit: "Volts" }, + "iout": { value: data.iout, unit: "Amperes" }, + "pout": { value: data.pout, unit: "Watts" }, + "standby_vout": { value: data.standbyVout, unit: "Volts" }, + "standby_iout": { value: data.standbyIout, unit: "Amperes" }, + "standby_pout": { value: data.standbyPout, unit: "Watts" }, + "temp_busbar": { value: data.tempBusbar, unit: "degC" }, + "temp_ssd": { value: data.tempSsd, unit: "degC" }, + "temp_mps": { value: data.tempMps, unit: "degC" }, + } + + for metricName, metricData := range metricMap { + metricName = metricNamePrefix + metricName + metric, err := lp.NewMetric(metricName, map[string]string{"type": "node"}, m.meta, metricData.value, data.timestamp) + if err != nil { + cclog.ComponentErrorf(m.name, "lp.NewMetric failed: %v", err) + return + } + metric.AddMeta("unit", metricData.unit) + output <- metric + } +} + +func (m *MegwareEurekaCollector) Close() { + m.init = false +} diff --git a/collectors/megwareEureka.md b/collectors/megwareEureka.md new file mode 100644 index 0000000..d9aad67 --- /dev/null +++ b/collectors/megwareEureka.md @@ -0,0 +1,44 @@ + + +## `lenovo_dense_power` collector + +```json + "megware_eureka": { + "u20_path": "/path/to/ipmitool", + "use_sudo": true + } +``` + +The `megware_eureka` collector reads power and other metrics from Megware Eureka machines via `u20`. +If the u20 tool is available for your machines, it's possible that this collector is compatible. +If you don't have access to u20, please contact your Megware sales person. + +You can test the collector compatibility by running the following command: + +``` +$ u20 values --read GET_MPS_POLL_VALUES +``` + +If this returns a JSON with energy, voltage and current readings, you're fine. + +In addition, `u20` typically requires root to run. +In order to run `cc-metric-collector` without root priviliges, you can enable `use_sudo`. +Add a file like this in `/etc/sudoers.d/` to allow `cc-metric-collector` to run the required commands: + +``` +# Do not log the following sudo commands from monitoring, since this causes a lot of log spam. +# However keep log_denied enabled, to detect failures +Defaults: monitoring !log_allowed, !pam_session + +# Allow to use u20 for Megware power readings +monitoring ALL = (root) NOPASSWD:/usr/bin/u20 values --read GET_MPS_POLL_VALUES +```