144 lines
3.8 KiB
JavaScript
144 lines
3.8 KiB
JavaScript
/**
|
|
* IPMI sensor collector via ipmitool.
|
|
*
|
|
* Enable: PEARDATA_IPMI=1
|
|
*
|
|
* Charts: sensors.ipmi.temp.*, sensors.ipmi.fan.*
|
|
*/
|
|
import { execFile } from 'child_process'
|
|
import { promisify } from 'util'
|
|
import { CollectorPlugin } from './plugin.js'
|
|
import { registerChart } from '../../../shared/metrics.js'
|
|
import logger from '../../utils/logger.js'
|
|
|
|
const log = logger.child('ipmi')
|
|
const execFileAsync = promisify(execFile)
|
|
|
|
export function isIpmiEnabled() {
|
|
const v = process.env.PEARDATA_IPMI
|
|
return v === '1' || v === 'on' || v === 'true'
|
|
}
|
|
|
|
/**
|
|
* @param {string} name
|
|
*/
|
|
function sanitizeSensor(name) {
|
|
return String(name)
|
|
.replace(/[^a-zA-Z0-9_.-]/g, '_')
|
|
.replace(/^_+|_+$/g, '')
|
|
.slice(0, 64)
|
|
}
|
|
|
|
/**
|
|
* @param {'temp'|'fan'} kind
|
|
* @param {string} name
|
|
*/
|
|
function makeIpmiChart(kind, name) {
|
|
const safe = sanitizeSensor(name)
|
|
const id = kind === 'temp' ? `sensors.ipmi.temp.${safe}` : `sensors.ipmi.fan.${safe}`
|
|
return {
|
|
id,
|
|
name: id,
|
|
context: kind === 'temp' ? 'sensors.ipmi.temperature' : 'sensors.ipmi.fan',
|
|
title: kind === 'temp' ? `IPMI temperature ${name}` : `IPMI fan ${name}`,
|
|
units: kind === 'temp' ? 'Celsius' : 'RPM',
|
|
family: 'ipmi',
|
|
chartType: 'line',
|
|
priority: kind === 'temp' ? 7020 : 7030,
|
|
plugin: 'ipmi',
|
|
dimensions: [{ id: 'value', name: 'value', algorithm: 'absolute' }],
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse `ipmitool sensor` output lines.
|
|
* @param {string} stdout
|
|
* @returns {Array<{ name: string, kind: 'temp'|'fan', value: number }>}
|
|
*/
|
|
export function parseIpmitoolSensor(stdout) {
|
|
/** @type {Array<{ name: string, kind: 'temp'|'fan', value: number }>} */
|
|
const out = []
|
|
for (const line of String(stdout || '').split('\n')) {
|
|
const t = line.trim()
|
|
if (!t) continue
|
|
const parts = t.split('|').map((s) => s.trim())
|
|
if (parts.length < 4) continue
|
|
const name = parts[0]
|
|
const reading = parts[1]
|
|
const type = parts[2]?.toLowerCase() || ''
|
|
const lowerName = name.toLowerCase()
|
|
const isTemp =
|
|
type.includes('degrees') ||
|
|
type.includes('temperature') ||
|
|
/temp|degrees c/i.test(lowerName)
|
|
const isFan = type.includes('rpm') || /fan|rpm/i.test(lowerName)
|
|
if (!isTemp && !isFan) continue
|
|
const val = Number(String(reading).replace(/[^\d.-]/g, ''))
|
|
if (!Number.isFinite(val)) continue
|
|
out.push({ name, kind: isFan ? 'fan' : 'temp', value: val })
|
|
}
|
|
return out
|
|
}
|
|
|
|
export async function readIpmitoolSensors(timeoutMs = 5000) {
|
|
const { stdout } = await execFileAsync('ipmitool', ['sensor'], {
|
|
timeout: timeoutMs,
|
|
encoding: 'utf8',
|
|
})
|
|
return parseIpmitoolSensor(stdout)
|
|
}
|
|
|
|
export class IpmiCollector extends CollectorPlugin {
|
|
constructor(opts = {}) {
|
|
super({ name: 'ipmi', intervalMs: opts.intervalMs })
|
|
/** @type {boolean|null} */
|
|
this._available = null
|
|
}
|
|
|
|
isEnabled() {
|
|
return isIpmiEnabled()
|
|
}
|
|
|
|
start() {
|
|
if (!this.isEnabled()) return
|
|
log.info('IPMI collector started')
|
|
super.start()
|
|
}
|
|
|
|
async collect() {
|
|
let sensors
|
|
try {
|
|
sensors = await readIpmitoolSensors()
|
|
this._available = true
|
|
} catch (err) {
|
|
if (this._available !== false) {
|
|
log.info('ipmitool unavailable — collector idle', { error: err.message })
|
|
this._available = false
|
|
}
|
|
return []
|
|
}
|
|
const ts = Date.now()
|
|
/** @type {Array<{ chart: string, context: string, ts: number, values: Record<string, number|null> }>} */
|
|
const batch = []
|
|
for (const s of sensors) {
|
|
const def = makeIpmiChart(s.kind, s.name)
|
|
registerChart(def)
|
|
batch.push({
|
|
chart: def.id,
|
|
context: def.context,
|
|
ts,
|
|
values: { value: s.value },
|
|
})
|
|
}
|
|
return batch
|
|
}
|
|
}
|
|
|
|
/** @type {IpmiCollector|null} */
|
|
let singleton = null
|
|
|
|
export function getIpmiCollector() {
|
|
if (!singleton) singleton = new IpmiCollector()
|
|
return singleton
|
|
}
|