mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(xo-server-ipmi-sensors) : Migrated the old ipmi feature to a new plugin and added user defined sensors (#9724)
Move ipmi sensor discovery to it's own plugin and removed old logic, ships with a default configuration with the same behaviour as the old system. The plugin can be configured via the UI, accepts an exact match for the vendor and regexes for the ipmi matching. Tested via unit tests. Tested with R620-L1 host on xo-lab by adding and removing Dell from configured vendors
This commit is contained in:
@@ -491,7 +491,9 @@ export interface XenApiDrTask {
|
||||
uuid: string
|
||||
}
|
||||
|
||||
type XenApiHostCallMethods = TagCallMethods & {}
|
||||
type XenApiHostCallMethods = TagCallMethods & {
|
||||
<T>(method: 'call_plugin',plugin:string, fn: string, args: Record<string, string>): Promise<T>
|
||||
}
|
||||
export interface XenApiHost {
|
||||
$ref: Branded<'host'>
|
||||
address: string
|
||||
|
||||
@@ -56,7 +56,6 @@ export type XapiConnection = Xapi & {
|
||||
sessionId: string
|
||||
_url?: { protocol: string; hostname: string; port?: string }
|
||||
}
|
||||
|
||||
type XapiRecordByXapiXoRecord = {
|
||||
gpuGroup: XenApiGpuGroupWrapped
|
||||
host: XenApiHostWrapped
|
||||
@@ -170,6 +169,15 @@ export type XoApp = {
|
||||
addAclV2GroupRole(groupId: XoGroup['id'], roleId: XoAclRole['id']): Promise<XoGroupRole>
|
||||
addAclV2UserRole(userId: XoUser['id'], roleId: XoAclRole['id']): Promise<XoUserRole>
|
||||
addUserToGroup: (userId: XoUser['id'], groupId: XoGroup['id']) => Promise<void>
|
||||
addApiMethod: <A extends unknown[], R>(
|
||||
name: string,
|
||||
method: (...args: A) => Promise<R>,
|
||||
//we use any since it is a legacy call, might be better typed in another pr
|
||||
info: {
|
||||
resolve?: any
|
||||
params?: any
|
||||
}
|
||||
) => () => void // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
authenticateUser: (
|
||||
credentials: { token?: string; username?: string; password?: string },
|
||||
userData?: { ip?: string },
|
||||
|
||||
@@ -5,13 +5,6 @@ import { defer } from 'golike-defer'
|
||||
import { incorrectState, operationFailed } from 'xo-common/api-errors.js'
|
||||
|
||||
import { getCurrentVmUuid } from './_XenStore.mjs'
|
||||
import {
|
||||
addCustomIpmiSensors,
|
||||
addIpmiSensorDataType,
|
||||
containsDigit,
|
||||
IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME,
|
||||
isRelevantIpmiSensor,
|
||||
} from './host/_ipmi.mjs'
|
||||
|
||||
const waitAgentRestart = (xapi, hostRef, prevAgentStartTime) =>
|
||||
new Promise(resolve => {
|
||||
@@ -146,64 +139,6 @@ class Host {
|
||||
await waitAgentRestart(this, ref, agentStartTime)
|
||||
}
|
||||
|
||||
async getIpmiSensors(ref, { cache } = {}) {
|
||||
const biosStrings = await this.call(cache, 'host.get_bios_strings', ref)
|
||||
let productName = biosStrings['system-product-name']?.toLowerCase()
|
||||
|
||||
// Olivier.L request: consider all DELL servers in the same way
|
||||
if (biosStrings['system-manufacturer']?.toLowerCase().includes('dell')) {
|
||||
productName = 'dell'
|
||||
}
|
||||
|
||||
// Olivier.L request: consider all Lenovo servers in the same way
|
||||
if (biosStrings['system-manufacturer']?.toLowerCase().includes('lenovo')) {
|
||||
productName = 'lenovo'
|
||||
}
|
||||
|
||||
const callIpmiPlugin = fn => this.call(cache, 'host.call_plugin', ref, 'ipmitool.py', fn, {})
|
||||
|
||||
if (
|
||||
IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME[productName] === undefined ||
|
||||
(await callIpmiPlugin('is_ipmi_device_available')) === 'false'
|
||||
) {
|
||||
return {}
|
||||
}
|
||||
|
||||
const [stringifiedIpmiSensors, stringifiedIpmiLan] = await Promise.all([
|
||||
callIpmiPlugin('get_all_sensors'),
|
||||
callIpmiPlugin('get_ipmi_lan'),
|
||||
])
|
||||
const ipmiSensors = JSON.parse(stringifiedIpmiSensors)
|
||||
const ipmiLan = JSON.parse(stringifiedIpmiLan)
|
||||
|
||||
const ipmiSensorsByDataType = {}
|
||||
for (const ipmiSensor of [...ipmiSensors, ...ipmiLan]) {
|
||||
if (!isRelevantIpmiSensor(ipmiSensor, productName)) {
|
||||
continue
|
||||
}
|
||||
|
||||
addIpmiSensorDataType(ipmiSensor, productName)
|
||||
const dataType = ipmiSensor.dataType
|
||||
|
||||
const ipmiSensors = ipmiSensorsByDataType[dataType]
|
||||
if (ipmiSensors === undefined) {
|
||||
ipmiSensorsByDataType[dataType] = containsDigit(ipmiSensor.name) ? [] : ipmiSensor
|
||||
} else if (!Array.isArray(ipmiSensors)) {
|
||||
// it can happen various sensors have the same name (e.g. temp for dell cpu temp)
|
||||
// in such case, consider it as an array instead of single value
|
||||
ipmiSensorsByDataType[dataType] = [ipmiSensors]
|
||||
}
|
||||
|
||||
if (Array.isArray(ipmiSensorsByDataType[dataType])) {
|
||||
ipmiSensorsByDataType[dataType].push(ipmiSensor)
|
||||
}
|
||||
}
|
||||
|
||||
addCustomIpmiSensors(ipmiSensorsByDataType, productName)
|
||||
|
||||
return ipmiSensorsByDataType
|
||||
}
|
||||
|
||||
async getMdadmHealth(ref) {
|
||||
try {
|
||||
const result = await this.callAsync('host.call_plugin', ref, 'raid.py', 'check_raid_pool', {})
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
export const IPMI_SENSOR_DATA_TYPE = {
|
||||
totalPower: 'totalPower',
|
||||
outletTemp: 'outletTemp',
|
||||
bmcStatus: 'bmcStatus',
|
||||
inletTemp: 'inletTemp',
|
||||
cpuTemp: 'cpuTemp',
|
||||
fanStatus: 'fanStatus',
|
||||
fanSpeed: 'fanSpeed',
|
||||
psuStatus: 'psuStatus',
|
||||
psuPower: 'psuPower',
|
||||
ip: 'ip',
|
||||
unknown: 'unknown',
|
||||
}
|
||||
|
||||
export const IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME = {
|
||||
'mona_1.44gg': {
|
||||
[IPMI_SENSOR_DATA_TYPE.totalPower]: /^total_power$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.outletTemp]: /^outlet_temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.bmcStatus]: /^bmc_status$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.inletTemp]: /^psu_inlet_temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.cpuTemp]: /^cpu[0-9]+_temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.fanStatus]: /^fan[0-9]+_status$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.fanSpeed]: /^fan[0-9]+_r_speed$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuStatus]: /^psu[0-9]+_status$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuPower]: /^psu[0-9]+_pin$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.ip]: /^ip address$/i,
|
||||
},
|
||||
dell: {
|
||||
[IPMI_SENSOR_DATA_TYPE.fanSpeed]: /^fan[0-9]+(a|b)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuPower]: /^voltage [0-9]+$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuStatus]: /^ps[0-9]+ pg fail$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.cpuTemp]: /^temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.totalPower]: /^pwr consumption$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.inletTemp]: /^inlet temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.ip]: /^ip address$/i,
|
||||
},
|
||||
lenovo: {
|
||||
[IPMI_SENSOR_DATA_TYPE.fanSpeed]: /^fan\s*\d+(?:\s+(?:front|rear))?\s+tach$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.outletTemp]: /^exhaust temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuPower]: /^psu\s*\d+\s+(ac in pwr|dc out pwr)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuStatus]:
|
||||
/^(psu\s*\d+(?:\s+(?:failure|pf failure|in failure|i2c dc fai))?|power supply\s*\d+)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.cpuTemp]: /^cpu\s*\d+\s*temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.totalPower]: /^(sys power|system power|power consumption)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.inletTemp]: /^(ambient temp|inlet temp)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.ip]: /^ip address$/i,
|
||||
},
|
||||
}
|
||||
export const IPMI_SENSOR_CUSTOM_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME = {
|
||||
dell: {
|
||||
[IPMI_SENSOR_DATA_TYPE.bmcStatus]: ipmiSensorsByDataType => ({
|
||||
event: Object.keys(ipmiSensorsByDataType).length > 0 ? 'ok' : 'ko',
|
||||
name: 'bmc status',
|
||||
value: '0x00',
|
||||
}),
|
||||
[IPMI_SENSOR_DATA_TYPE.fanStatus]: ipmiSensorsByDataType => ipmiSensorsByDataType[IPMI_SENSOR_DATA_TYPE.fanSpeed],
|
||||
},
|
||||
lenovo: {
|
||||
[IPMI_SENSOR_DATA_TYPE.bmcStatus]: d => ({
|
||||
event: Object.keys(d).length > 0 ? 'ok' : 'ko',
|
||||
name: 'bmc status',
|
||||
value: '0x00',
|
||||
}),
|
||||
[IPMI_SENSOR_DATA_TYPE.fanStatus]: d =>
|
||||
(d[IPMI_SENSOR_DATA_TYPE.fanSpeed] ?? [])
|
||||
.filter(s => s.event === 'ok' && /\d+\s*rpm/i.test(s.value))
|
||||
.map(s => ({ ...s, rpm: parseInt(s.value, 10) })),
|
||||
},
|
||||
}
|
||||
|
||||
const IPMI_SENSOR_REGEX_BY_PRODUCT_NAME = Object.keys(IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME).reduce(
|
||||
(acc, productName) => {
|
||||
const regexes = Object.values(IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME[productName])
|
||||
const combinedRegex = new RegExp(regexes.map(regex => regex.source).join('|'), 'i')
|
||||
acc[productName] = combinedRegex
|
||||
return acc
|
||||
},
|
||||
{}
|
||||
)
|
||||
|
||||
export const isRelevantIpmiSensor = (data, productName) =>
|
||||
IPMI_SENSOR_REGEX_BY_PRODUCT_NAME[productName].test(data.name)
|
||||
|
||||
export const containsDigit = str => /\d/.test(str)
|
||||
|
||||
export const addIpmiSensorDataType = (data, productName) => {
|
||||
const name = data.name
|
||||
const ipmiRegexByDataType = IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME[productName]
|
||||
|
||||
for (const dataType in ipmiRegexByDataType) {
|
||||
const regex = ipmiRegexByDataType[dataType]
|
||||
if (regex.test(name)) {
|
||||
data.dataType = dataType
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
data.dataType = IPMI_SENSOR_DATA_TYPE.unknown
|
||||
}
|
||||
|
||||
export const addCustomIpmiSensors = (ipmiSensorsByDataType, productName) => {
|
||||
const customSensorsByDataType = IPMI_SENSOR_CUSTOM_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME[productName]
|
||||
for (const dataType in customSensorsByDataType) {
|
||||
const fn = customSensorsByDataType[dataType]
|
||||
const sensors = fn(ipmiSensorsByDataType)
|
||||
ipmiSensorsByDataType[dataType] = Array.isArray(sensors)
|
||||
? sensors.map(sensor => ({ ...sensor, dataType }))
|
||||
: { ...sensors, dataType }
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@
|
||||
- [Export config] Hide passphrase by default (PR [#9824](https://github.com/vatesfr/xen-orchestra/pull/9824))
|
||||
- [Backups] Refactor clean phase for incremental and full backups ([#9765](https://github.com/vatesfr/xen-orchestra/pull/9765))
|
||||
- [REST API] Expose `DELETE /rest/v0/srs/:id` (PR [#9464](https://github.com/vatesfr/xen-orchestra/pull/9464))
|
||||
- [xo-server-ipmi-sensors] Migrated the old ipmi feature to a new plugin and added user defined sensors (PR [#9724](https://github.com/vatesfr/xen-orchestra/pull/9724))
|
||||
- [MCP] Read `HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY` so an internal XOA stays reachable when the AI assistant goes through a corporate proxy (PR [#9820](https://github.com/vatesfr/xen-orchestra/pull/9820))
|
||||
|
||||
### Bug fixes
|
||||
@@ -64,6 +65,7 @@
|
||||
- @xen-orchestra/web-core minor
|
||||
- @xen-orchestra/xapi minor
|
||||
- xo-server minor
|
||||
- xo-server-ipmi-sensors major
|
||||
- xo-web minor
|
||||
|
||||
<!--packages-end-->
|
||||
|
||||
33
packages/xo-server-ipmi-sensors/.eslintrc.cjs
Normal file
33
packages/xo-server-ipmi-sensors/.eslintrc.cjs
Normal file
@@ -0,0 +1,33 @@
|
||||
'use strict'
|
||||
|
||||
const { resolve } = require('path')
|
||||
|
||||
module.exports = {
|
||||
ignorePatterns: ['src/*.d.ts'],
|
||||
overrides: [
|
||||
{
|
||||
files: ['src/**/*.mts'],
|
||||
|
||||
parser: '@typescript-eslint/parser',
|
||||
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
project: './packages/xo-server-ipmi-sensors/tsconfig.json',
|
||||
tsconfigRootDir: resolve(__dirname, '../..'),
|
||||
},
|
||||
|
||||
plugins: ['@typescript-eslint'],
|
||||
|
||||
rules: {
|
||||
// Native module requires file extensions
|
||||
'n/no-missing-import': 'off',
|
||||
|
||||
'n/no-unsupported-features/es-syntax': 'off',
|
||||
|
||||
// Allow TypeScript-specific patterns
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
1
packages/xo-server-ipmi-sensors/.npmignore
Symbolic link
1
packages/xo-server-ipmi-sensors/.npmignore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../scripts/npmignore
|
||||
58
packages/xo-server-ipmi-sensors/README.md
Normal file
58
packages/xo-server-ipmi-sensors/README.md
Normal file
@@ -0,0 +1,58 @@
|
||||
<!-- DO NOT EDIT MANUALLY, THIS FILE HAS BEEN GENERATED -->
|
||||
|
||||
# xo-server-ipmi-sensors
|
||||
|
||||
> XO Server plugin exposing and configuring IPMI sensors
|
||||
|
||||
## Usage
|
||||
|
||||
Provides access to IPMI (Intelligent Platform Management Interface) sensor data from XenServer/XCP-ng hosts.
|
||||
|
||||
Like all other xo-server plugins, it can be configured directly via
|
||||
the web interface.
|
||||
|
||||
This plugin provides an API method (`ipmi-sensors.get_ipmi_sensors`) that retrieves and categorizes IPMI sensor data from supported hosts. Sensor data is automatically categorized by type (temperature, fan speed, power consumption, etc.) using configurable regex patterns for different server vendors.
|
||||
|
||||
**Features:**
|
||||
|
||||
- Automatic detection of supported server vendors (Dell, Lenovo, etc.)
|
||||
- Configurable regex patterns for sensor categorization
|
||||
- Support for multiple sensor data types: temperature, fan speed, power consumption, voltage, status indicators
|
||||
- Custom computed sensors based on raw IPMI data
|
||||
- Integration with XO's host management system
|
||||
- Filtering of relevant sensors based on vendor-specific rules
|
||||
|
||||
**Supported sensor data types:**
|
||||
|
||||
- Total power consumption
|
||||
- Inlet/outlet temperatures
|
||||
- CPU temperatures
|
||||
- Fan status and speed
|
||||
- PSU (Power Supply Unit) status and power
|
||||
- BMC (Baseboard Management Controller) status
|
||||
- IP address information
|
||||
|
||||
## Configuration
|
||||
|
||||
The plugin supports configuration of vendor-specific regex patterns for sensor matching. Default configurations are provided for common server vendors including Dell, Lenovo, and others.
|
||||
|
||||
Configuration can be customized through the XO web interface to:
|
||||
|
||||
- Add support for new server vendors
|
||||
- Modify sensor categorization rules
|
||||
- Adjust regex patterns for specific hardware models
|
||||
|
||||
## Contributions
|
||||
|
||||
Contributions are _very_ welcomed, either on the documentation or on
|
||||
the code.
|
||||
|
||||
You may:
|
||||
|
||||
- report any [issue](https://github.com/vatesfr/xen-orchestra/issues)
|
||||
you've encountered;
|
||||
- fork and create a pull request.
|
||||
|
||||
## License
|
||||
|
||||
[AGPL-3.0-or-later](https://spdx.org/licenses/AGPL-3.0-or-later) © [Vates SAS](https://vates.fr)
|
||||
48
packages/xo-server-ipmi-sensors/package.json
Normal file
48
packages/xo-server-ipmi-sensors/package.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"name": "xo-server-ipmi-sensors",
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"description": "XO Server plugin exposing and configuring IPMI sensors",
|
||||
"type": "module",
|
||||
"keywords": [
|
||||
"xen-orchestra",
|
||||
"xo-server",
|
||||
"plugin",
|
||||
"ipmi",
|
||||
"monitoring"
|
||||
],
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/packages/xo-server-ipmi-sensors",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "packages/xo-server-ipmi-sensors",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"main": "dist/index.mjs",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"dependencies": {
|
||||
"@isaacs/ttlcache": "^1.4.1",
|
||||
"@vates/types": "^1.22.0",
|
||||
"@xen-orchestra/log": "^0.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.0.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rimraf dist/",
|
||||
"dev": "yarn run clean && tsc --watch",
|
||||
"prebuild": "yarn run clean",
|
||||
"prepublishOnly": "yarn run build",
|
||||
"test": "cd dist && node --test"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
61
packages/xo-server-ipmi-sensors/src/default-rules.mts
Normal file
61
packages/xo-server-ipmi-sensors/src/default-rules.mts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { SensorRegexByProduct, IPMI_SENSOR_DATA_TYPE } from './types.mjs'
|
||||
|
||||
export const DEFAULT_IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME: SensorRegexByProduct[] = [
|
||||
{
|
||||
vendor: 'mona_1.44gg',
|
||||
sensorRegexps: {
|
||||
[IPMI_SENSOR_DATA_TYPE.totalPower]: /^total_power$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.outletTemp]: /^outlet_temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.bmcStatus]: /^bmc_status$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.inletTemp]: /^psu_inlet_temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.cpuTemp]: /^cpu[0-9]+_temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.fanStatus]: /^fan[0-9]+_status$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.fanSpeed]: /^fan[0-9]+_r_speed$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuStatus]: /^psu[0-9]+_status$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuPower]: /^psu[0-9]+_pin$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.ip]: /^ip address$/i,
|
||||
},
|
||||
},
|
||||
{
|
||||
vendor: 'dell',
|
||||
sensorRegexps: {
|
||||
[IPMI_SENSOR_DATA_TYPE.fanSpeed]: /^fan[0-9]+(a|b)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuPower]: /^voltage [0-9]+$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuStatus]: /^ps[0-9]+ pg fail$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.cpuTemp]: /^temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.totalPower]: /^pwr consumption$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.inletTemp]: /^inlet temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.ip]: /^ip address$/i,
|
||||
},
|
||||
},
|
||||
{
|
||||
vendor: 'lenovo',
|
||||
sensorRegexps: {
|
||||
[IPMI_SENSOR_DATA_TYPE.fanSpeed]: /^fan\s*\d+(?:\s+(?:front|rear))?\s+tach$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.outletTemp]: /^exhaust temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuPower]: /^psu\s*\d+\s+(ac in pwr|dc out pwr)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.psuStatus]:
|
||||
/^(psu\s*\d+(?:\s+(?:failure|pf failure|in failure|i2c dc fai))?|power supply\s*\d+)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.cpuTemp]: /^cpu\s*\d+\s*temp$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.totalPower]: /^(sys power|system power|power consumption)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.inletTemp]: /^(ambient temp|inlet temp)$/i,
|
||||
[IPMI_SENSOR_DATA_TYPE.ip]: /^ip address$/i,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
function stringifyRegexConfigFull(config: SensorRegexByProduct[]) {
|
||||
return config.map(product => ({
|
||||
vendor: product.vendor,
|
||||
sensorRegexps: Object.fromEntries(
|
||||
Object.entries(product.sensorRegexps).map(([key, value]) => [
|
||||
key,
|
||||
value instanceof RegExp ? value.toString() : value,
|
||||
])
|
||||
),
|
||||
}))
|
||||
}
|
||||
|
||||
export const DEFAULT_IPMI_SENSOR_REGEX_CONFIG_STRINGIFIED = stringifyRegexConfigFull(
|
||||
DEFAULT_IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME
|
||||
)
|
||||
208
packages/xo-server-ipmi-sensors/src/index.mts
Normal file
208
packages/xo-server-ipmi-sensors/src/index.mts
Normal file
@@ -0,0 +1,208 @@
|
||||
/**
|
||||
* XO Server IPMI Sensors Plugin
|
||||
*
|
||||
* This plugin exposes IPMI sensor data from XenServer/XCP-ng hosts.
|
||||
* It collects vendor-specific sensor readings and categorizes them
|
||||
* using configurable regex rules for supported products.
|
||||
*/
|
||||
|
||||
import type { XoApp, XoHost } from '@vates/types'
|
||||
import { createLogger } from '@xen-orchestra/log'
|
||||
import { DEFAULT_IPMI_SENSOR_REGEX_CONFIG_STRINGIFIED } from './default-rules.mjs'
|
||||
import { addIpmiSensorDataType, containsDigit, isRelevantIpmiSensor, parseRegexConfig } from './ipmi-rules.mjs'
|
||||
import { addCustomIpmiSensors } from './legacy-computed-rules.mjs'
|
||||
import {
|
||||
IPMI_SENSOR_DATA_TYPE,
|
||||
SensorRegexByProduct,
|
||||
ReturnedSensorData,
|
||||
FinalSensorData,
|
||||
PluginConfiguration,
|
||||
} from './types.mjs'
|
||||
import TTLCache from '@isaacs/ttlcache'
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
const IPMI_CACHE_TTL = 6e4
|
||||
const logger = createLogger('xo:xo-server-ipmi-sensors')
|
||||
|
||||
// ============================================================================
|
||||
// Configuration Schema (exported for xo-server)
|
||||
// ============================================================================
|
||||
|
||||
export const sensorRegexProperties = Object.fromEntries(
|
||||
Object.values(IPMI_SENSOR_DATA_TYPE).map(key => [key, { type: 'string', default: '' }])
|
||||
)
|
||||
|
||||
export const configurationSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
vendors: {
|
||||
type: 'array',
|
||||
title: 'Supported vendors',
|
||||
description: 'List of supported product names for IPMI sensor regex matching',
|
||||
default: DEFAULT_IPMI_SENSOR_REGEX_CONFIG_STRINGIFIED,
|
||||
|
||||
items: {
|
||||
type: 'object',
|
||||
title: 'Sensor regex by product',
|
||||
description: 'Map of product name > sensor data type > regex pattern (string)',
|
||||
properties: {
|
||||
vendor: {
|
||||
type: 'string',
|
||||
default: 'generic',
|
||||
},
|
||||
sensorRegexps: {
|
||||
type: 'object',
|
||||
description: 'Map of sensor data type > regex pattern with format /pattern/flags (e.g. /^fan[0-9]+$/i)',
|
||||
properties: sensorRegexProperties,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
} as const
|
||||
|
||||
export const configurationPresets = {
|
||||
default: {
|
||||
vendors: DEFAULT_IPMI_SENSOR_REGEX_CONFIG_STRINGIFIED,
|
||||
},
|
||||
} as const
|
||||
|
||||
// ============================================================================
|
||||
// Plugin Class
|
||||
// ============================================================================
|
||||
class IpmiSensorsPlugin {
|
||||
#configuredRulesByProduct: SensorRegexByProduct[]
|
||||
readonly #xo: XoApp
|
||||
// cache is type any because it handles itself
|
||||
#cache: TTLCache<any, any> = new TTLCache({
|
||||
ttl: IPMI_CACHE_TTL,
|
||||
max: 1000,
|
||||
})
|
||||
|
||||
#unloadApiMethods: (() => void)[] = []
|
||||
constructor(xo: XoApp) {
|
||||
this.#xo = xo
|
||||
this.#configuredRulesByProduct = []
|
||||
logger.info('Plugin initialized')
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the plugin with the provided configuration.
|
||||
*/
|
||||
async configure(configuration: PluginConfiguration): Promise<void> {
|
||||
this.#configuredRulesByProduct = parseRegexConfig(configuration.vendors)
|
||||
logger.debug('Plugin configured')
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and start the plugin.
|
||||
*/
|
||||
async load(): Promise<void> {
|
||||
this.#unloadApiMethods.push(
|
||||
this.#xo.addApiMethod<[{ host: XoHost }], FinalSensorData>(
|
||||
'ipmi-sensors.get_ipmi_sensors',
|
||||
this.getIpmiSensors.bind(this),
|
||||
{
|
||||
resolve: { host: ['id', 'host', 'administrate'] },
|
||||
params: { id: { type: 'string' } },
|
||||
}
|
||||
),
|
||||
// replacement for the method in packages/xo-server/src/api/host.mjs
|
||||
this.#xo.addApiMethod<[{ host: XoHost }], FinalSensorData>(
|
||||
'host.getIpmiSensors',
|
||||
this.getIpmiSensors.bind(this),
|
||||
{
|
||||
resolve: { host: ['id', 'host', 'administrate'] },
|
||||
params: { id: { type: 'string' } },
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
async getIpmiSensors({ host }: { host: XoHost }): Promise<FinalSensorData> {
|
||||
const xApiHost = this.#xo.getXapiObject<XoHost>(host, 'host')
|
||||
const biosStrings = xApiHost.bios_strings
|
||||
let productName = biosStrings['system-product-name']?.toLowerCase() || ''
|
||||
const systemManufacturer = biosStrings['system-manufacturer']?.toLowerCase() || ''
|
||||
// Olivier.L request: consider all DELL and Lenovo servers in the same way
|
||||
if (systemManufacturer.includes('dell')) productName = 'dell'
|
||||
if (systemManufacturer.includes('lenovo')) productName = 'lenovo'
|
||||
|
||||
const data = this.#configuredRulesByProduct
|
||||
const callIpmiPlugin = async <T,>(fn: string): Promise<T> => {
|
||||
return await xApiHost.$xapi.call<T>(this.#cache, 'host.call_plugin', xApiHost.$ref, 'ipmitool.py', fn, {})
|
||||
}
|
||||
|
||||
const ipmiDeviceAvailable = await callIpmiPlugin<string>('is_ipmi_device_available')
|
||||
|
||||
if (!data.some(s => s.vendor.toLowerCase() === productName) || ipmiDeviceAvailable === 'false') {
|
||||
logger.info('No IPMI device available or no rules configured for this product, skipping IPMI sensor fetching', {
|
||||
productName,
|
||||
ipmiDeviceAvailable,
|
||||
})
|
||||
return {}
|
||||
}
|
||||
|
||||
const [stringifiedIpmiSensors, stringifiedIpmiLan] = await Promise.all([
|
||||
callIpmiPlugin<string>('get_all_sensors'),
|
||||
callIpmiPlugin<string>('get_ipmi_lan'),
|
||||
])
|
||||
const ipmiSensors = JSON.parse(stringifiedIpmiSensors) as ReturnedSensorData[]
|
||||
const ipmiLan = JSON.parse(stringifiedIpmiLan) as ReturnedSensorData[]
|
||||
const ipmiSensorsByDataType: FinalSensorData = {}
|
||||
for (const ipmiSensor of [...ipmiSensors, ...ipmiLan]) {
|
||||
if (!isRelevantIpmiSensor(ipmiSensor, productName, this.#configuredRulesByProduct)) {
|
||||
continue
|
||||
}
|
||||
|
||||
addIpmiSensorDataType(ipmiSensor, productName, this.#configuredRulesByProduct)
|
||||
const dataType = ipmiSensor.dataType
|
||||
|
||||
const sensorsByDataType = ipmiSensorsByDataType[dataType]
|
||||
if (sensorsByDataType === undefined) {
|
||||
ipmiSensorsByDataType[dataType] = containsDigit(ipmiSensor.name) ? [] : ipmiSensor
|
||||
} else if (!Array.isArray(sensorsByDataType)) {
|
||||
// it can happen various sensors have the same name (e.g. temp for dell cpu temp)
|
||||
// in such case, consider it as an array instead of single value
|
||||
ipmiSensorsByDataType[dataType] = [sensorsByDataType]
|
||||
}
|
||||
const bucket = ipmiSensorsByDataType[dataType]!
|
||||
if (Array.isArray(bucket)) {
|
||||
bucket.push(ipmiSensor)
|
||||
}
|
||||
}
|
||||
|
||||
addCustomIpmiSensors(ipmiSensorsByDataType, productName)
|
||||
|
||||
return ipmiSensorsByDataType
|
||||
}
|
||||
/**
|
||||
* Unload and stop the plugin.
|
||||
*/
|
||||
async unload(): Promise<void> {
|
||||
logger.info('Stopping IPMI sensors server')
|
||||
for (const unloadApiMethod of this.#unloadApiMethods) {
|
||||
unloadApiMethod()
|
||||
}
|
||||
this.#unloadApiMethods = []
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Plugin Factory (exported for xo-server)
|
||||
// ============================================================================
|
||||
|
||||
interface PluginOptions {
|
||||
xo: XoApp
|
||||
}
|
||||
|
||||
function pluginFactory({ xo }: PluginOptions): IpmiSensorsPlugin {
|
||||
return new IpmiSensorsPlugin(xo)
|
||||
}
|
||||
pluginFactory.configurationSchema = configurationSchema
|
||||
pluginFactory.configurationPresets = configurationPresets
|
||||
|
||||
export default pluginFactory
|
||||
62
packages/xo-server-ipmi-sensors/src/ipmi-rules.mts
Normal file
62
packages/xo-server-ipmi-sensors/src/ipmi-rules.mts
Normal file
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
SensorRegexByProduct,
|
||||
SensorRegexByDataType,
|
||||
ReturnedSensorData,
|
||||
IPMI_SENSOR_DATA_TYPE_STRINGS,
|
||||
IPMI_SENSOR_DATA_TYPE,
|
||||
SensorRegexByProductRaw,
|
||||
} from './types.mjs'
|
||||
|
||||
function getRulesForProduct(productName: string, rulesByProduct: SensorRegexByProduct[]): SensorRegexByDataType {
|
||||
const productRules = rulesByProduct.find(rules => rules.vendor.toLowerCase() === productName.toLowerCase())
|
||||
return productRules ? productRules.sensorRegexps : {}
|
||||
}
|
||||
|
||||
export function isRelevantIpmiSensor(
|
||||
data: ReturnedSensorData,
|
||||
productName: string,
|
||||
configuredRules: SensorRegexByProduct[]
|
||||
): boolean {
|
||||
const rules = getRulesForProduct(productName, configuredRules)
|
||||
return Object.values(rules).some(regex => regex && regex.test(data.name))
|
||||
}
|
||||
|
||||
export function addIpmiSensorDataType(
|
||||
data: ReturnedSensorData,
|
||||
productName: string,
|
||||
configuredRules: SensorRegexByProduct[]
|
||||
): asserts data is ReturnedSensorData & { dataType: IPMI_SENSOR_DATA_TYPE_STRINGS } {
|
||||
const rules = getRulesForProduct(productName, configuredRules)
|
||||
for (const [name, regex] of Object.entries(rules)) {
|
||||
if (regex && regex.test(data.name)) {
|
||||
data.dataType = name as IPMI_SENSOR_DATA_TYPE_STRINGS
|
||||
return
|
||||
}
|
||||
}
|
||||
data.dataType = IPMI_SENSOR_DATA_TYPE.unknown as IPMI_SENSOR_DATA_TYPE_STRINGS
|
||||
}
|
||||
|
||||
export function containsDigit(str: string): boolean {
|
||||
return /\d/.test(str)
|
||||
}
|
||||
|
||||
function parseRegex(str: string): RegExp | undefined {
|
||||
if (str === '') return undefined
|
||||
const match = str.match(/^\/(.+)\/([a-z]*)$/i)
|
||||
if (!match) throw new Error(`Invalid regex string: ${str}`)
|
||||
const [, pattern, flags] = match
|
||||
if (pattern === undefined) throw new Error(`Invalid regex string: ${str}`)
|
||||
return new RegExp(pattern, flags)
|
||||
}
|
||||
|
||||
export function parseRegexConfig(config: SensorRegexByProductRaw[]): SensorRegexByProduct[] {
|
||||
return config.map(product => ({
|
||||
vendor: product.vendor,
|
||||
sensorRegexps: Object.fromEntries(
|
||||
Object.entries(product.sensorRegexps).map(([key, value]) => [
|
||||
key,
|
||||
typeof value === 'string' ? parseRegex(value) : undefined,
|
||||
])
|
||||
) as SensorRegexByDataType,
|
||||
}))
|
||||
}
|
||||
106
packages/xo-server-ipmi-sensors/src/ipmi-rules.test.mts
Normal file
106
packages/xo-server-ipmi-sensors/src/ipmi-rules.test.mts
Normal file
@@ -0,0 +1,106 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it } from 'node:test'
|
||||
|
||||
import { addIpmiSensorDataType, isRelevantIpmiSensor, parseRegexConfig } from './ipmi-rules.mjs'
|
||||
import { DEFAULT_IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME } from './default-rules.mjs'
|
||||
import { SensorRegexByProductRaw, ReturnedSensorData, IPMI_SENSOR_DATA_TYPE } from './types.mjs'
|
||||
|
||||
describe('ipmi-rules', () => {
|
||||
describe('parseRegexConfig', () => {
|
||||
it('should parse raw config into RegExp config', () => {
|
||||
const rawConfig: SensorRegexByProductRaw[] = [
|
||||
{
|
||||
vendor: 'test',
|
||||
sensorRegexps: {
|
||||
fanSpeed: '/^fan[0-9]+$/i',
|
||||
cpuTemp: '/^cpu_temp$/',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const parsed = parseRegexConfig(rawConfig)
|
||||
assert.strictEqual(parsed.length, 1)
|
||||
const firstProduct = parsed[0]!
|
||||
assert.strictEqual(firstProduct.vendor, 'test')
|
||||
assert(firstProduct.sensorRegexps.fanSpeed instanceof RegExp)
|
||||
assert.strictEqual(firstProduct.sensorRegexps.fanSpeed?.source, '^fan[0-9]+$')
|
||||
assert.strictEqual(firstProduct.sensorRegexps.fanSpeed?.flags, 'i')
|
||||
assert(firstProduct.sensorRegexps.cpuTemp instanceof RegExp)
|
||||
assert.strictEqual(firstProduct.sensorRegexps.cpuTemp?.source, '^cpu_temp$')
|
||||
})
|
||||
|
||||
it('should handle undefined regex values', () => {
|
||||
const rawConfig: SensorRegexByProductRaw[] = [
|
||||
{
|
||||
vendor: 'test',
|
||||
sensorRegexps: {
|
||||
fanSpeed: undefined,
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const parsed = parseRegexConfig(rawConfig)
|
||||
const firstProduct = parsed[0]!
|
||||
assert.strictEqual(firstProduct.sensorRegexps.fanSpeed, undefined)
|
||||
})
|
||||
})
|
||||
|
||||
describe('isRelevantIpmiSensor', () => {
|
||||
const mockRules = DEFAULT_IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME
|
||||
|
||||
it('should return true for relevant sensors', () => {
|
||||
const sensor: ReturnedSensorData = { name: 'fan1_r_speed', value: '1000', event: 'ok' }
|
||||
assert.strictEqual(isRelevantIpmiSensor(sensor, 'mona_1.44gg', mockRules), true)
|
||||
})
|
||||
|
||||
it('should return false for irrelevant sensors', () => {
|
||||
const sensor: ReturnedSensorData = { name: 'unknown_sensor', value: '0', event: 'ok' }
|
||||
assert.strictEqual(isRelevantIpmiSensor(sensor, 'mona_1.44gg', mockRules), false)
|
||||
})
|
||||
|
||||
it('should return false for unknown product', () => {
|
||||
const sensor: ReturnedSensorData = { name: 'fan1_r_speed', value: '1000', event: 'ok' }
|
||||
assert.strictEqual(isRelevantIpmiSensor(sensor, 'unknown', mockRules), false)
|
||||
})
|
||||
|
||||
it('should be case insensitive for product name', () => {
|
||||
const sensor: ReturnedSensorData = { name: 'fan1_r_speed', value: '1000', event: 'ok' }
|
||||
assert.strictEqual(isRelevantIpmiSensor(sensor, 'MONA_1.44GG', mockRules), true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('addIpmiSensorDataType', () => {
|
||||
const mockRules = DEFAULT_IPMI_SENSOR_REGEX_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME
|
||||
|
||||
it('should add correct dataType for matching sensors', () => {
|
||||
const sensor: ReturnedSensorData = { name: 'fan1_r_speed', value: '1000', event: 'ok' }
|
||||
addIpmiSensorDataType(sensor, 'mona_1.44gg', mockRules)
|
||||
assert.strictEqual(sensor.dataType, IPMI_SENSOR_DATA_TYPE.fanSpeed)
|
||||
})
|
||||
|
||||
it('should add unknown dataType for non-matching sensors', () => {
|
||||
const sensor: ReturnedSensorData = { name: 'unknown_sensor', value: '0', event: 'ok' }
|
||||
addIpmiSensorDataType(sensor, 'mona_1.44gg', mockRules)
|
||||
assert.strictEqual(sensor.dataType, IPMI_SENSOR_DATA_TYPE.unknown)
|
||||
})
|
||||
|
||||
it('should add unknown dataType for unknown product', () => {
|
||||
const sensor: ReturnedSensorData = { name: 'fan1_r_speed', value: '1000', event: 'ok' }
|
||||
addIpmiSensorDataType(sensor, 'unknown', mockRules)
|
||||
assert.strictEqual(sensor.dataType, IPMI_SENSOR_DATA_TYPE.unknown)
|
||||
})
|
||||
|
||||
it('should match first regex in order', () => {
|
||||
// For dell, both fanSpeed and psuPower could potentially match, but fanSpeed comes first
|
||||
const sensor: ReturnedSensorData = { name: 'fan1a', value: '1000', event: 'ok' }
|
||||
addIpmiSensorDataType(sensor, 'dell', mockRules)
|
||||
assert.strictEqual(sensor.dataType, IPMI_SENSOR_DATA_TYPE.fanSpeed)
|
||||
})
|
||||
|
||||
it('should handle case insensitive matching', () => {
|
||||
const sensor: ReturnedSensorData = { name: 'FAN1_R_SPEED', value: '1000', event: 'ok' }
|
||||
addIpmiSensorDataType(sensor, 'mona_1.44gg', mockRules)
|
||||
assert.strictEqual(sensor.dataType, IPMI_SENSOR_DATA_TYPE.fanSpeed)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { FinalSensorData, IPMI_SENSOR_DATA_TYPE_STRINGS, IPMI_SENSOR_DATA_TYPE, ReturnedSensorData } from './types.mjs'
|
||||
|
||||
type CUSTOM_SENSOR_BY_DATA_TYPE_FN = (
|
||||
ipmiSensorsByDataType: FinalSensorData
|
||||
) => Partial<ReturnedSensorData | ReturnedSensorData[]>
|
||||
type CUSTOM_SENSOR_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME = Record<
|
||||
string,
|
||||
Partial<Record<IPMI_SENSOR_DATA_TYPE_STRINGS, CUSTOM_SENSOR_BY_DATA_TYPE_FN>>
|
||||
>
|
||||
|
||||
export const IPMI_SENSOR_CUSTOM_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME: CUSTOM_SENSOR_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME =
|
||||
{
|
||||
dell: {
|
||||
[IPMI_SENSOR_DATA_TYPE.bmcStatus]: (ipmiSensorsByDataType: FinalSensorData) => ({
|
||||
event: Object.keys(ipmiSensorsByDataType).length > 0 ? 'ok' : 'ko',
|
||||
name: 'bmc status',
|
||||
value: '0x00',
|
||||
}),
|
||||
[IPMI_SENSOR_DATA_TYPE.fanStatus]: (ipmiSensorsByDataType: FinalSensorData) =>
|
||||
ipmiSensorsByDataType[IPMI_SENSOR_DATA_TYPE.fanSpeed as IPMI_SENSOR_DATA_TYPE_STRINGS],
|
||||
},
|
||||
lenovo: {
|
||||
[IPMI_SENSOR_DATA_TYPE.bmcStatus]: (ipmiSensorsByDataType: FinalSensorData) => ({
|
||||
event: Object.keys(ipmiSensorsByDataType).length > 0 ? 'ok' : 'ko',
|
||||
name: 'bmc status',
|
||||
value: '0x00',
|
||||
}),
|
||||
[IPMI_SENSOR_DATA_TYPE.fanStatus]: (ipmiSensorsByDataType: FinalSensorData) => {
|
||||
let fanStatuses = ipmiSensorsByDataType[IPMI_SENSOR_DATA_TYPE.fanSpeed as IPMI_SENSOR_DATA_TYPE_STRINGS] ?? []
|
||||
if (!Array.isArray(fanStatuses)) fanStatuses = [fanStatuses]
|
||||
return fanStatuses
|
||||
.filter((s: { event: string; value: string }) => s.event === 'ok' && /\d+\s*rpm/i.test(s.value))
|
||||
.map((s: { event: string; value: string }) => ({ ...s, rpm: parseInt(s.value, 10) }))
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const addCustomIpmiSensors = (ipmiSensorsByDataType: FinalSensorData, productName: string) => {
|
||||
const customSensorsByDataType = IPMI_SENSOR_CUSTOM_BY_DATA_TYPE_BY_SUPPORTED_PRODUCT_NAME[productName]
|
||||
for (const dataType in customSensorsByDataType) {
|
||||
const dataTypeKey = dataType as IPMI_SENSOR_DATA_TYPE_STRINGS
|
||||
const fn = customSensorsByDataType[dataTypeKey]! // we know this exists since we're iterating over the keys of customSensorsByDataType
|
||||
const sensors = fn(ipmiSensorsByDataType)
|
||||
const parsedSensor = Array.isArray(sensors)
|
||||
? sensors.map(sensor => ({ ...sensor, dataType }))
|
||||
: { ...sensors, dataType }
|
||||
ipmiSensorsByDataType[dataTypeKey] = parsedSensor as FinalSensorData[IPMI_SENSOR_DATA_TYPE_STRINGS]
|
||||
}
|
||||
}
|
||||
46
packages/xo-server-ipmi-sensors/src/types.mts
Normal file
46
packages/xo-server-ipmi-sensors/src/types.mts
Normal file
@@ -0,0 +1,46 @@
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
export const IPMI_SENSOR_DATA_TYPE = {
|
||||
totalPower: 'totalPower',
|
||||
outletTemp: 'outletTemp',
|
||||
bmcStatus: 'bmcStatus',
|
||||
inletTemp: 'inletTemp',
|
||||
cpuTemp: 'cpuTemp',
|
||||
fanStatus: 'fanStatus',
|
||||
fanSpeed: 'fanSpeed',
|
||||
psuStatus: 'psuStatus',
|
||||
psuPower: 'psuPower',
|
||||
ip: 'ip',
|
||||
unknown: 'unknown',
|
||||
}
|
||||
export type IPMI_SENSOR_DATA_TYPE_STRINGS = keyof typeof IPMI_SENSOR_DATA_TYPE
|
||||
|
||||
export type SensorRegexByDataType = {
|
||||
[key in keyof typeof IPMI_SENSOR_DATA_TYPE]?: RegExp
|
||||
}
|
||||
export type SensorRegexByDataTypeRaw = {
|
||||
[key in keyof typeof IPMI_SENSOR_DATA_TYPE]?: string
|
||||
}
|
||||
export type SensorRegexByProductRaw = {
|
||||
vendor: string
|
||||
sensorRegexps: SensorRegexByDataTypeRaw
|
||||
}
|
||||
|
||||
export interface PluginConfiguration {
|
||||
vendors: SensorRegexByProductRaw[]
|
||||
}
|
||||
|
||||
export type SensorRegexByProduct = {
|
||||
vendor: string
|
||||
sensorRegexps: SensorRegexByDataType
|
||||
}
|
||||
|
||||
export type ReturnedSensorData = {
|
||||
name: string
|
||||
value: string
|
||||
event: string
|
||||
dataType?: IPMI_SENSOR_DATA_TYPE_STRINGS
|
||||
}
|
||||
|
||||
export type FinalSensorData = Partial<Record<IPMI_SENSOR_DATA_TYPE_STRINGS, ReturnedSensorData | ReturnedSensorData[]>>
|
||||
17
packages/xo-server-ipmi-sensors/tsconfig.json
Normal file
17
packages/xo-server-ipmi-sensors/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"types": ["node"],
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/*.mts", "src/*.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -17,11 +17,6 @@ import { debounceWithKey } from '../_pDebounceWithKey.mjs'
|
||||
/** @typedef {import('@vates/types').XoPif} XoPif */
|
||||
|
||||
const CERT_PUBKEY_MIN_SIZE = 2048
|
||||
const IPMI_CACHE_TTL = 6e4
|
||||
const IPMI_CACHE = new TTLCache({
|
||||
ttl: IPMI_CACHE_TTL,
|
||||
max: 1000,
|
||||
})
|
||||
|
||||
const CACHE_2CRSI = new TTLCache({
|
||||
ttl: 6e4,
|
||||
@@ -667,16 +662,7 @@ getBlockdevices.params = {
|
||||
getBlockdevices.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
|
||||
export function getIpmiSensors({ host }) {
|
||||
return this.getXapi(host).host_getIpmiSensors(host._xapiRef, { cache: IPMI_CACHE })
|
||||
}
|
||||
getIpmiSensors.params = {
|
||||
id: { type: 'string' },
|
||||
}
|
||||
getIpmiSensors.resolve = {
|
||||
host: ['id', 'host', 'administrate'],
|
||||
}
|
||||
// host.getIpmiSensors moved to packages/xo-server-ipmi-sensors/src/index.mts
|
||||
|
||||
export function getBiosInfo({ host }) {
|
||||
return this.getXapi(host).getHostBiosInfo(host._xapiRef, { cache: CACHE_2CRSI })
|
||||
|
||||
@@ -695,7 +695,7 @@ export const subscribeIpmiSensors = host => {
|
||||
subscribeHostsIpmiSensors[hostId] = createSubscription(
|
||||
async () =>
|
||||
_isAdmin
|
||||
? await _call('host.getIpmiSensors', {
|
||||
? await _call('ipmi-sensors.get_ipmi_sensors', {
|
||||
id: hostId,
|
||||
})
|
||||
: undefined,
|
||||
|
||||
@@ -55,10 +55,33 @@ class Plugin extends Component {
|
||||
|
||||
_getUiSchema = createSelector(() => this.props.configurationSchema, generateUiSchema)
|
||||
|
||||
getSchemaDefaults(schema) {
|
||||
if (schema?.type !== 'object' || !schema.properties) {
|
||||
return undefined
|
||||
}
|
||||
const result = {}
|
||||
let hasDefaults = false
|
||||
for (const [key, propSchema] of Object.entries(schema.properties)) {
|
||||
if (propSchema.default !== undefined) {
|
||||
result[key] = propSchema.default
|
||||
hasDefaults = true
|
||||
} else if (propSchema.type === 'object') {
|
||||
const nested = this.getSchemaDefaults(propSchema)
|
||||
if (nested !== undefined) {
|
||||
result[key] = nested
|
||||
hasDefaults = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return hasDefaults ? result : undefined
|
||||
}
|
||||
_updateExpanded = () => {
|
||||
this.setState({
|
||||
expanded: !this.state.expanded,
|
||||
})
|
||||
const newExpanded = !this.state.expanded
|
||||
const editedConfig =
|
||||
newExpanded && !this.props.configuration && this.state.editedConfig === undefined
|
||||
? this.getSchemaDefaults(this.props.configurationSchema)
|
||||
: this.state.editedConfig
|
||||
this.setState({ expanded: newExpanded, editedConfig })
|
||||
}
|
||||
|
||||
_setAutoload = event => {
|
||||
|
||||
Reference in New Issue
Block a user