@sonde/packs 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +6 -0
- package/.turbo/turbo-test.log +814 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +10 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/citrix.d.ts +13 -0
- package/dist/integrations/citrix.d.ts.map +1 -0
- package/dist/integrations/citrix.js +420 -0
- package/dist/integrations/citrix.js.map +1 -0
- package/dist/integrations/citrix.test.d.ts +2 -0
- package/dist/integrations/citrix.test.d.ts.map +1 -0
- package/dist/integrations/citrix.test.js +464 -0
- package/dist/integrations/citrix.test.js.map +1 -0
- package/dist/integrations/graph.d.ts +9 -0
- package/dist/integrations/graph.d.ts.map +1 -0
- package/dist/integrations/graph.js +290 -0
- package/dist/integrations/graph.js.map +1 -0
- package/dist/integrations/graph.test.d.ts +2 -0
- package/dist/integrations/graph.test.d.ts.map +1 -0
- package/dist/integrations/graph.test.js +356 -0
- package/dist/integrations/graph.test.js.map +1 -0
- package/dist/integrations/httpbin.d.ts +3 -0
- package/dist/integrations/httpbin.d.ts.map +1 -0
- package/dist/integrations/httpbin.js +70 -0
- package/dist/integrations/httpbin.js.map +1 -0
- package/dist/integrations/nutanix.d.ts +18 -0
- package/dist/integrations/nutanix.d.ts.map +1 -0
- package/dist/integrations/nutanix.js +1121 -0
- package/dist/integrations/nutanix.js.map +1 -0
- package/dist/integrations/nutanix.test.d.ts +2 -0
- package/dist/integrations/nutanix.test.d.ts.map +1 -0
- package/dist/integrations/nutanix.test.js +978 -0
- package/dist/integrations/nutanix.test.js.map +1 -0
- package/dist/integrations/proxmox.d.ts +12 -0
- package/dist/integrations/proxmox.d.ts.map +1 -0
- package/dist/integrations/proxmox.js +733 -0
- package/dist/integrations/proxmox.js.map +1 -0
- package/dist/integrations/proxmox.test.d.ts +2 -0
- package/dist/integrations/proxmox.test.d.ts.map +1 -0
- package/dist/integrations/proxmox.test.js +697 -0
- package/dist/integrations/proxmox.test.js.map +1 -0
- package/dist/integrations/servicenow.d.ts +3 -0
- package/dist/integrations/servicenow.d.ts.map +1 -0
- package/dist/integrations/servicenow.js +257 -0
- package/dist/integrations/servicenow.js.map +1 -0
- package/dist/integrations/servicenow.test.d.ts +2 -0
- package/dist/integrations/servicenow.test.d.ts.map +1 -0
- package/dist/integrations/servicenow.test.js +217 -0
- package/dist/integrations/servicenow.test.js.map +1 -0
- package/dist/integrations/splunk.d.ts +9 -0
- package/dist/integrations/splunk.d.ts.map +1 -0
- package/dist/integrations/splunk.js +242 -0
- package/dist/integrations/splunk.js.map +1 -0
- package/dist/integrations/splunk.test.d.ts +2 -0
- package/dist/integrations/splunk.test.d.ts.map +1 -0
- package/dist/integrations/splunk.test.js +323 -0
- package/dist/integrations/splunk.test.js.map +1 -0
- package/dist/mysql/index.d.ts +3 -0
- package/dist/mysql/index.d.ts.map +1 -0
- package/dist/mysql/index.js +13 -0
- package/dist/mysql/index.js.map +1 -0
- package/dist/mysql/manifest.d.ts +3 -0
- package/dist/mysql/manifest.d.ts.map +1 -0
- package/dist/mysql/manifest.js +69 -0
- package/dist/mysql/manifest.js.map +1 -0
- package/dist/mysql/probes/databases-list.d.ts +13 -0
- package/dist/mysql/probes/databases-list.d.ts.map +1 -0
- package/dist/mysql/probes/databases-list.js +31 -0
- package/dist/mysql/probes/databases-list.js.map +1 -0
- package/dist/mysql/probes/databases-list.test.d.ts +2 -0
- package/dist/mysql/probes/databases-list.test.d.ts.map +1 -0
- package/dist/mysql/probes/databases-list.test.js +54 -0
- package/dist/mysql/probes/databases-list.test.js.map +1 -0
- package/dist/mysql/probes/processlist.d.ts +18 -0
- package/dist/mysql/probes/processlist.d.ts.map +1 -0
- package/dist/mysql/probes/processlist.js +36 -0
- package/dist/mysql/probes/processlist.js.map +1 -0
- package/dist/mysql/probes/processlist.test.d.ts +2 -0
- package/dist/mysql/probes/processlist.test.d.ts.map +1 -0
- package/dist/mysql/probes/processlist.test.js +41 -0
- package/dist/mysql/probes/processlist.test.js.map +1 -0
- package/dist/mysql/probes/status.d.ts +14 -0
- package/dist/mysql/probes/status.d.ts.map +1 -0
- package/dist/mysql/probes/status.js +40 -0
- package/dist/mysql/probes/status.js.map +1 -0
- package/dist/mysql/probes/status.test.d.ts +2 -0
- package/dist/mysql/probes/status.test.d.ts.map +1 -0
- package/dist/mysql/probes/status.test.js +43 -0
- package/dist/mysql/probes/status.test.js.map +1 -0
- package/dist/nginx/index.d.ts +3 -0
- package/dist/nginx/index.d.ts.map +1 -0
- package/dist/nginx/index.js +13 -0
- package/dist/nginx/index.js.map +1 -0
- package/dist/nginx/manifest.d.ts +3 -0
- package/dist/nginx/manifest.d.ts.map +1 -0
- package/dist/nginx/manifest.js +68 -0
- package/dist/nginx/manifest.js.map +1 -0
- package/dist/nginx/probes/access-log-tail.d.ts +9 -0
- package/dist/nginx/probes/access-log-tail.d.ts.map +1 -0
- package/dist/nginx/probes/access-log-tail.js +14 -0
- package/dist/nginx/probes/access-log-tail.js.map +1 -0
- package/dist/nginx/probes/access-log-tail.test.d.ts +2 -0
- package/dist/nginx/probes/access-log-tail.test.d.ts.map +1 -0
- package/dist/nginx/probes/access-log-tail.test.js +40 -0
- package/dist/nginx/probes/access-log-tail.test.js.map +1 -0
- package/dist/nginx/probes/config-test.d.ts +8 -0
- package/dist/nginx/probes/config-test.d.ts.map +1 -0
- package/dist/nginx/probes/config-test.js +18 -0
- package/dist/nginx/probes/config-test.js.map +1 -0
- package/dist/nginx/probes/config-test.test.d.ts +2 -0
- package/dist/nginx/probes/config-test.test.d.ts.map +1 -0
- package/dist/nginx/probes/config-test.test.js +35 -0
- package/dist/nginx/probes/config-test.test.js.map +1 -0
- package/dist/nginx/probes/error-log-tail.d.ts +9 -0
- package/dist/nginx/probes/error-log-tail.d.ts.map +1 -0
- package/dist/nginx/probes/error-log-tail.js +14 -0
- package/dist/nginx/probes/error-log-tail.js.map +1 -0
- package/dist/nginx/probes/error-log-tail.test.d.ts +2 -0
- package/dist/nginx/probes/error-log-tail.test.d.ts.map +1 -0
- package/dist/nginx/probes/error-log-tail.test.js +34 -0
- package/dist/nginx/probes/error-log-tail.test.js.map +1 -0
- package/dist/postgres/index.d.ts +3 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +13 -0
- package/dist/postgres/index.js.map +1 -0
- package/dist/postgres/manifest.d.ts +3 -0
- package/dist/postgres/manifest.d.ts.map +1 -0
- package/dist/postgres/manifest.js +90 -0
- package/dist/postgres/manifest.js.map +1 -0
- package/dist/postgres/probes/connections-active.d.ts +17 -0
- package/dist/postgres/probes/connections-active.d.ts.map +1 -0
- package/dist/postgres/probes/connections-active.js +37 -0
- package/dist/postgres/probes/connections-active.js.map +1 -0
- package/dist/postgres/probes/connections-active.test.d.ts +2 -0
- package/dist/postgres/probes/connections-active.test.d.ts.map +1 -0
- package/dist/postgres/probes/connections-active.test.js +36 -0
- package/dist/postgres/probes/connections-active.test.js.map +1 -0
- package/dist/postgres/probes/databases-list.d.ts +14 -0
- package/dist/postgres/probes/databases-list.d.ts.map +1 -0
- package/dist/postgres/probes/databases-list.js +34 -0
- package/dist/postgres/probes/databases-list.js.map +1 -0
- package/dist/postgres/probes/databases-list.test.d.ts +2 -0
- package/dist/postgres/probes/databases-list.test.d.ts.map +1 -0
- package/dist/postgres/probes/databases-list.test.js +49 -0
- package/dist/postgres/probes/databases-list.test.js.map +1 -0
- package/dist/postgres/probes/query-slow.d.ts +17 -0
- package/dist/postgres/probes/query-slow.d.ts.map +1 -0
- package/dist/postgres/probes/query-slow.js +37 -0
- package/dist/postgres/probes/query-slow.js.map +1 -0
- package/dist/postgres/probes/query-slow.test.d.ts +2 -0
- package/dist/postgres/probes/query-slow.test.d.ts.map +1 -0
- package/dist/postgres/probes/query-slow.test.js +30 -0
- package/dist/postgres/probes/query-slow.test.js.map +1 -0
- package/dist/proxmox/index.d.ts +3 -0
- package/dist/proxmox/index.d.ts.map +1 -0
- package/dist/proxmox/index.js +23 -0
- package/dist/proxmox/index.js.map +1 -0
- package/dist/proxmox/manifest.d.ts +3 -0
- package/dist/proxmox/manifest.d.ts.map +1 -0
- package/dist/proxmox/manifest.js +75 -0
- package/dist/proxmox/manifest.js.map +1 -0
- package/dist/proxmox/probes/ceph-status.d.ts +36 -0
- package/dist/proxmox/probes/ceph-status.d.ts.map +1 -0
- package/dist/proxmox/probes/ceph-status.js +71 -0
- package/dist/proxmox/probes/ceph-status.js.map +1 -0
- package/dist/proxmox/probes/ceph-status.test.d.ts +2 -0
- package/dist/proxmox/probes/ceph-status.test.d.ts.map +1 -0
- package/dist/proxmox/probes/ceph-status.test.js +115 -0
- package/dist/proxmox/probes/ceph-status.test.js.map +1 -0
- package/dist/proxmox/probes/cluster-config.d.ts +31 -0
- package/dist/proxmox/probes/cluster-config.d.ts.map +1 -0
- package/dist/proxmox/probes/cluster-config.js +72 -0
- package/dist/proxmox/probes/cluster-config.js.map +1 -0
- package/dist/proxmox/probes/cluster-config.test.d.ts +2 -0
- package/dist/proxmox/probes/cluster-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/cluster-config.test.js +107 -0
- package/dist/proxmox/probes/cluster-config.test.js.map +1 -0
- package/dist/proxmox/probes/ha-status.d.ts +18 -0
- package/dist/proxmox/probes/ha-status.d.ts.map +1 -0
- package/dist/proxmox/probes/ha-status.js +38 -0
- package/dist/proxmox/probes/ha-status.js.map +1 -0
- package/dist/proxmox/probes/ha-status.test.d.ts +2 -0
- package/dist/proxmox/probes/ha-status.test.d.ts.map +1 -0
- package/dist/proxmox/probes/ha-status.test.js +66 -0
- package/dist/proxmox/probes/ha-status.test.js.map +1 -0
- package/dist/proxmox/probes/lvm.d.ts +35 -0
- package/dist/proxmox/probes/lvm.d.ts.map +1 -0
- package/dist/proxmox/probes/lvm.js +75 -0
- package/dist/proxmox/probes/lvm.js.map +1 -0
- package/dist/proxmox/probes/lvm.test.d.ts +2 -0
- package/dist/proxmox/probes/lvm.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lvm.test.js +128 -0
- package/dist/proxmox/probes/lvm.test.js.map +1 -0
- package/dist/proxmox/probes/lxc-config.d.ts +29 -0
- package/dist/proxmox/probes/lxc-config.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-config.js +67 -0
- package/dist/proxmox/probes/lxc-config.js.map +1 -0
- package/dist/proxmox/probes/lxc-config.test.d.ts +2 -0
- package/dist/proxmox/probes/lxc-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-config.test.js +77 -0
- package/dist/proxmox/probes/lxc-config.test.js.map +1 -0
- package/dist/proxmox/probes/lxc-list.d.ts +20 -0
- package/dist/proxmox/probes/lxc-list.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-list.js +49 -0
- package/dist/proxmox/probes/lxc-list.js.map +1 -0
- package/dist/proxmox/probes/lxc-list.test.d.ts +2 -0
- package/dist/proxmox/probes/lxc-list.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-list.test.js +51 -0
- package/dist/proxmox/probes/lxc-list.test.js.map +1 -0
- package/dist/proxmox/probes/vm-config.d.ts +21 -0
- package/dist/proxmox/probes/vm-config.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-config.js +58 -0
- package/dist/proxmox/probes/vm-config.js.map +1 -0
- package/dist/proxmox/probes/vm-config.test.d.ts +2 -0
- package/dist/proxmox/probes/vm-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-config.test.js +80 -0
- package/dist/proxmox/probes/vm-config.test.js.map +1 -0
- package/dist/proxmox/probes/vm-locks.d.ts +16 -0
- package/dist/proxmox/probes/vm-locks.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-locks.js +35 -0
- package/dist/proxmox/probes/vm-locks.js.map +1 -0
- package/dist/proxmox/probes/vm-locks.test.d.ts +2 -0
- package/dist/proxmox/probes/vm-locks.test.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-locks.test.js +54 -0
- package/dist/proxmox/probes/vm-locks.test.js.map +1 -0
- package/dist/redis/index.d.ts +3 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +13 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/manifest.d.ts +3 -0
- package/dist/redis/manifest.d.ts.map +1 -0
- package/dist/redis/manifest.js +51 -0
- package/dist/redis/manifest.js.map +1 -0
- package/dist/redis/probes/info.d.ts +15 -0
- package/dist/redis/probes/info.d.ts.map +1 -0
- package/dist/redis/probes/info.js +32 -0
- package/dist/redis/probes/info.js.map +1 -0
- package/dist/redis/probes/info.test.d.ts +2 -0
- package/dist/redis/probes/info.test.d.ts.map +1 -0
- package/dist/redis/probes/info.test.js +64 -0
- package/dist/redis/probes/info.test.js.map +1 -0
- package/dist/redis/probes/keys-count.d.ts +13 -0
- package/dist/redis/probes/keys-count.d.ts.map +1 -0
- package/dist/redis/probes/keys-count.js +24 -0
- package/dist/redis/probes/keys-count.js.map +1 -0
- package/dist/redis/probes/keys-count.test.d.ts +2 -0
- package/dist/redis/probes/keys-count.test.d.ts.map +1 -0
- package/dist/redis/probes/keys-count.test.js +37 -0
- package/dist/redis/probes/keys-count.test.js.map +1 -0
- package/dist/redis/probes/memory-usage.d.ts +16 -0
- package/dist/redis/probes/memory-usage.d.ts.map +1 -0
- package/dist/redis/probes/memory-usage.js +31 -0
- package/dist/redis/probes/memory-usage.js.map +1 -0
- package/dist/redis/probes/memory-usage.test.d.ts +2 -0
- package/dist/redis/probes/memory-usage.test.d.ts.map +1 -0
- package/dist/redis/probes/memory-usage.test.js +48 -0
- package/dist/redis/probes/memory-usage.test.js.map +1 -0
- package/dist/runbooks/nutanix.d.ts +3 -0
- package/dist/runbooks/nutanix.d.ts.map +1 -0
- package/dist/runbooks/nutanix.js +619 -0
- package/dist/runbooks/nutanix.js.map +1 -0
- package/dist/runbooks/nutanix.test.d.ts +2 -0
- package/dist/runbooks/nutanix.test.d.ts.map +1 -0
- package/dist/runbooks/nutanix.test.js +971 -0
- package/dist/runbooks/nutanix.test.js.map +1 -0
- package/dist/runbooks/proxmox.d.ts +3 -0
- package/dist/runbooks/proxmox.d.ts.map +1 -0
- package/dist/runbooks/proxmox.js +451 -0
- package/dist/runbooks/proxmox.js.map +1 -0
- package/dist/runbooks/proxmox.test.d.ts +2 -0
- package/dist/runbooks/proxmox.test.d.ts.map +1 -0
- package/dist/runbooks/proxmox.test.js +700 -0
- package/dist/runbooks/proxmox.test.js.map +1 -0
- package/dist/signatures.d.ts +2 -0
- package/dist/signatures.d.ts.map +1 -0
- package/dist/signatures.js +2 -0
- package/dist/signatures.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +6 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +10 -1
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +60 -6
- package/src/integrations/citrix.test.ts +592 -0
- package/src/integrations/citrix.ts +557 -0
- package/src/integrations/graph.test.ts +478 -0
- package/src/integrations/graph.ts +413 -0
- package/src/integrations/httpbin.ts +72 -0
- package/src/integrations/nutanix.test.ts +1508 -0
- package/src/integrations/nutanix.ts +1460 -0
- package/src/integrations/proxmox.test.ts +1020 -0
- package/src/integrations/proxmox.ts +989 -0
- package/src/integrations/servicenow.test.ts +314 -0
- package/src/integrations/servicenow.ts +285 -0
- package/src/integrations/splunk.test.ts +440 -0
- package/src/integrations/splunk.ts +356 -0
- package/src/mysql/index.ts +14 -0
- package/src/mysql/manifest.ts +70 -0
- package/src/mysql/probes/databases-list.test.ts +62 -0
- package/src/mysql/probes/databases-list.ts +45 -0
- package/src/mysql/probes/processlist.test.ts +47 -0
- package/src/mysql/probes/processlist.ts +55 -0
- package/src/mysql/probes/status.test.ts +50 -0
- package/src/mysql/probes/status.ts +56 -0
- package/src/nginx/index.ts +14 -0
- package/src/nginx/manifest.ts +69 -0
- package/src/nginx/probes/access-log-tail.test.ts +51 -0
- package/src/nginx/probes/access-log-tail.ts +23 -0
- package/src/nginx/probes/config-test.test.ts +47 -0
- package/src/nginx/probes/config-test.ts +24 -0
- package/src/nginx/probes/error-log-tail.test.ts +44 -0
- package/src/nginx/probes/error-log-tail.ts +23 -0
- package/src/postgres/index.ts +14 -0
- package/src/postgres/manifest.ts +91 -0
- package/src/postgres/probes/connections-active.test.ts +42 -0
- package/src/postgres/probes/connections-active.ts +55 -0
- package/src/postgres/probes/databases-list.test.ts +57 -0
- package/src/postgres/probes/databases-list.ts +49 -0
- package/src/postgres/probes/query-slow.test.ts +37 -0
- package/src/postgres/probes/query-slow.ts +55 -0
- package/src/proxmox/index.ts +24 -0
- package/src/proxmox/manifest.ts +76 -0
- package/src/proxmox/probes/ceph-status.test.ts +126 -0
- package/src/proxmox/probes/ceph-status.ts +116 -0
- package/src/proxmox/probes/cluster-config.test.ts +118 -0
- package/src/proxmox/probes/cluster-config.ts +97 -0
- package/src/proxmox/probes/ha-status.test.ts +76 -0
- package/src/proxmox/probes/ha-status.ts +56 -0
- package/src/proxmox/probes/lvm.test.ts +140 -0
- package/src/proxmox/probes/lvm.ts +121 -0
- package/src/proxmox/probes/lxc-config.test.ts +89 -0
- package/src/proxmox/probes/lxc-config.ts +90 -0
- package/src/proxmox/probes/lxc-list.test.ts +60 -0
- package/src/proxmox/probes/lxc-list.ts +67 -0
- package/src/proxmox/probes/vm-config.test.ts +93 -0
- package/src/proxmox/probes/vm-config.ts +77 -0
- package/src/proxmox/probes/vm-locks.test.ts +63 -0
- package/src/proxmox/probes/vm-locks.ts +49 -0
- package/src/redis/index.ts +14 -0
- package/src/redis/manifest.ts +52 -0
- package/src/redis/probes/info.test.ts +73 -0
- package/src/redis/probes/info.ts +46 -0
- package/src/redis/probes/keys-count.test.ts +44 -0
- package/src/redis/probes/keys-count.ts +38 -0
- package/src/redis/probes/memory-usage.test.ts +54 -0
- package/src/redis/probes/memory-usage.ts +46 -0
- package/src/runbooks/nutanix.test.ts +1138 -0
- package/src/runbooks/nutanix.ts +941 -0
- package/src/runbooks/proxmox.test.ts +838 -0
- package/src/runbooks/proxmox.ts +626 -0
- package/src/signatures.ts +1 -0
- package/src/types.ts +62 -0
- package/src/validation.ts +21 -1
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ExecFn } from '../../types.js';
|
|
3
|
+
import type { QuerySlowResult } from './query-slow.js';
|
|
4
|
+
import { parseQuerySlow, querySlow } from './query-slow.js';
|
|
5
|
+
|
|
6
|
+
const SAMPLE_OUTPUT =
|
|
7
|
+
'9876\tmyapp\tappuser\t5432\tactive\tSELECT * FROM large_table WHERE expensive_join';
|
|
8
|
+
|
|
9
|
+
describe('parseQuerySlow', () => {
|
|
10
|
+
it('parses slow queries', () => {
|
|
11
|
+
const result = parseQuerySlow(SAMPLE_OUTPUT, 1000);
|
|
12
|
+
expect(result.count).toBe(1);
|
|
13
|
+
expect(result.thresholdMs).toBe(1000);
|
|
14
|
+
expect(result.queries[0]?.pid).toBe(9876);
|
|
15
|
+
expect(result.queries[0]?.query).toContain('expensive_join');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('handles no slow queries', () => {
|
|
19
|
+
const result = parseQuerySlow('', 1000);
|
|
20
|
+
expect(result.count).toBe(0);
|
|
21
|
+
expect(result.queries).toEqual([]);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('querySlow handler', () => {
|
|
26
|
+
it('calls psql with threshold param', async () => {
|
|
27
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
28
|
+
expect(cmd).toBe('psql');
|
|
29
|
+
const query = args[args.length - 1];
|
|
30
|
+
expect(query).toContain('2000');
|
|
31
|
+
return SAMPLE_OUTPUT;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const result = (await querySlow({ thresholdMs: 2000 }, mockExec)) as QuerySlowResult;
|
|
35
|
+
expect(result.thresholdMs).toBe(2000);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ProbeHandler } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
export interface SlowQueryInfo {
|
|
4
|
+
pid: number;
|
|
5
|
+
database: string;
|
|
6
|
+
user: string;
|
|
7
|
+
durationMs: number;
|
|
8
|
+
state: string;
|
|
9
|
+
query: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface QuerySlowResult {
|
|
13
|
+
queries: SlowQueryInfo[];
|
|
14
|
+
count: number;
|
|
15
|
+
thresholdMs: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const querySlow: ProbeHandler = async (params, exec) => {
|
|
19
|
+
const host = (params?.host as string) ?? 'localhost';
|
|
20
|
+
const port = String((params?.port as number) ?? 5432);
|
|
21
|
+
const user = (params?.user as string) ?? 'postgres';
|
|
22
|
+
const thresholdMs = (params?.thresholdMs as number) ?? 1000;
|
|
23
|
+
|
|
24
|
+
const stdout = await exec('psql', [
|
|
25
|
+
'-h',
|
|
26
|
+
host,
|
|
27
|
+
'-p',
|
|
28
|
+
port,
|
|
29
|
+
'-U',
|
|
30
|
+
user,
|
|
31
|
+
'-t',
|
|
32
|
+
'-A',
|
|
33
|
+
'-F',
|
|
34
|
+
'\t',
|
|
35
|
+
'-c',
|
|
36
|
+
`SELECT pid, datname, usename, EXTRACT(EPOCH FROM (now() - query_start))::int * 1000, state, LEFT(query, 300) FROM pg_stat_activity WHERE state = 'active' AND pid <> pg_backend_pid() AND EXTRACT(EPOCH FROM (now() - query_start)) * 1000 > ${thresholdMs} ORDER BY query_start ASC`,
|
|
37
|
+
]);
|
|
38
|
+
return parseQuerySlow(stdout, thresholdMs);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export function parseQuerySlow(stdout: string, thresholdMs: number): QuerySlowResult {
|
|
42
|
+
const lines = stdout.trim().split('\n').filter(Boolean);
|
|
43
|
+
const queries: SlowQueryInfo[] = lines.map((line) => {
|
|
44
|
+
const parts = line.split('\t');
|
|
45
|
+
return {
|
|
46
|
+
pid: Number(parts[0]) || 0,
|
|
47
|
+
database: parts[1] ?? '',
|
|
48
|
+
user: parts[2] ?? '',
|
|
49
|
+
durationMs: Number(parts[3]) || 0,
|
|
50
|
+
state: parts[4] ?? '',
|
|
51
|
+
query: parts[5] ?? '',
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
return { queries, count: queries.length, thresholdMs };
|
|
55
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Pack } from '../types.js';
|
|
2
|
+
import { proxmoxAgentManifest } from './manifest.js';
|
|
3
|
+
import { cephStatus } from './probes/ceph-status.js';
|
|
4
|
+
import { clusterConfig } from './probes/cluster-config.js';
|
|
5
|
+
import { haStatus } from './probes/ha-status.js';
|
|
6
|
+
import { lvm } from './probes/lvm.js';
|
|
7
|
+
import { lxcConfig } from './probes/lxc-config.js';
|
|
8
|
+
import { lxcList } from './probes/lxc-list.js';
|
|
9
|
+
import { vmConfig } from './probes/vm-config.js';
|
|
10
|
+
import { vmLocks } from './probes/vm-locks.js';
|
|
11
|
+
|
|
12
|
+
export const proxmoxAgentPack: Pack = {
|
|
13
|
+
manifest: proxmoxAgentManifest,
|
|
14
|
+
handlers: {
|
|
15
|
+
'proxmox-node.local.vm.config': vmConfig,
|
|
16
|
+
'proxmox-node.local.ha.status': haStatus,
|
|
17
|
+
'proxmox-node.local.lvm': lvm,
|
|
18
|
+
'proxmox-node.local.ceph.status': cephStatus,
|
|
19
|
+
'proxmox-node.local.lxc.config': lxcConfig,
|
|
20
|
+
'proxmox-node.local.lxc.list': lxcList,
|
|
21
|
+
'proxmox-node.local.cluster.config': clusterConfig,
|
|
22
|
+
'proxmox-node.local.vm.locks': vmLocks,
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { PackManifest } from '@sonde/shared';
|
|
2
|
+
|
|
3
|
+
export const proxmoxAgentManifest: PackManifest = {
|
|
4
|
+
name: 'proxmox-node',
|
|
5
|
+
version: '0.1.0',
|
|
6
|
+
description: 'Proxmox VE node-local probes — VM/LXC config, HA status, LVM, Ceph, cluster',
|
|
7
|
+
requires: {
|
|
8
|
+
groups: [],
|
|
9
|
+
files: ['/etc/pve/'],
|
|
10
|
+
commands: ['qm', 'pct', 'ha-manager', 'pvesh', 'lvs', 'vgs', 'pvs', 'pvecm'],
|
|
11
|
+
},
|
|
12
|
+
probes: [
|
|
13
|
+
{
|
|
14
|
+
name: 'local.vm.config',
|
|
15
|
+
description: 'QEMU VM configuration via qm config',
|
|
16
|
+
capability: 'observe',
|
|
17
|
+
params: {
|
|
18
|
+
vmid: { type: 'number', description: 'VM ID', required: true },
|
|
19
|
+
},
|
|
20
|
+
timeout: 10_000,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'local.ha.status',
|
|
24
|
+
description: 'HA manager resource states',
|
|
25
|
+
capability: 'observe',
|
|
26
|
+
timeout: 10_000,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'local.lvm',
|
|
30
|
+
description: 'LVM topology — logical volumes, volume groups, physical volumes',
|
|
31
|
+
capability: 'observe',
|
|
32
|
+
timeout: 15_000,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'local.ceph.status',
|
|
36
|
+
description: 'Ceph cluster health, OSD status, and PG states',
|
|
37
|
+
capability: 'observe',
|
|
38
|
+
timeout: 15_000,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'local.lxc.config',
|
|
42
|
+
description: 'LXC container configuration via pct config',
|
|
43
|
+
capability: 'observe',
|
|
44
|
+
params: {
|
|
45
|
+
vmid: { type: 'number', description: 'Container VMID', required: true },
|
|
46
|
+
},
|
|
47
|
+
timeout: 10_000,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'local.lxc.list',
|
|
51
|
+
description: 'List all LXC containers on this node',
|
|
52
|
+
capability: 'observe',
|
|
53
|
+
timeout: 10_000,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'local.cluster.config',
|
|
57
|
+
description: 'Cluster membership, quorum, and vote status via pvecm',
|
|
58
|
+
capability: 'observe',
|
|
59
|
+
timeout: 10_000,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'local.vm.locks',
|
|
63
|
+
description: 'Check for locked QEMU VMs via /run/lock/qemu-server/',
|
|
64
|
+
capability: 'observe',
|
|
65
|
+
timeout: 10_000,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
runbook: {
|
|
69
|
+
category: 'proxmox',
|
|
70
|
+
probes: ['local.ha.status', 'local.lvm', 'local.cluster.config'],
|
|
71
|
+
parallel: true,
|
|
72
|
+
},
|
|
73
|
+
detect: {
|
|
74
|
+
commands: ['qm', 'pct'],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ExecFn } from '../../types.js';
|
|
3
|
+
import type { CephStatusResult } from './ceph-status.js';
|
|
4
|
+
import { cephStatus, parseOsdTree } from './ceph-status.js';
|
|
5
|
+
|
|
6
|
+
const CEPH_STATUS_JSON = JSON.stringify({
|
|
7
|
+
health: { status: 'HEALTH_OK' },
|
|
8
|
+
osdmap: { num_osds: 6, num_up_osds: 6, num_in_osds: 6 },
|
|
9
|
+
pgmap: {
|
|
10
|
+
pgs_by_state: [{ state_name: 'active+clean', count: 256 }],
|
|
11
|
+
bytes_total: 6000000000000,
|
|
12
|
+
bytes_used: 2000000000000,
|
|
13
|
+
bytes_avail: 4000000000000,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const OSD_TREE_JSON = JSON.stringify({
|
|
18
|
+
nodes: [
|
|
19
|
+
{ id: -1, name: 'default', type: 'root' },
|
|
20
|
+
{ id: -2, name: 'pve01', type: 'host' },
|
|
21
|
+
{
|
|
22
|
+
id: 0,
|
|
23
|
+
name: 'osd.0',
|
|
24
|
+
type: 'osd',
|
|
25
|
+
status: 'up',
|
|
26
|
+
crush_weight: 1.0,
|
|
27
|
+
reweight: 1.0,
|
|
28
|
+
host: 'pve01',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 1,
|
|
32
|
+
name: 'osd.1',
|
|
33
|
+
type: 'osd',
|
|
34
|
+
status: 'up',
|
|
35
|
+
crush_weight: 1.0,
|
|
36
|
+
reweight: 1.0,
|
|
37
|
+
host: 'pve01',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 2,
|
|
41
|
+
name: 'osd.2',
|
|
42
|
+
type: 'osd',
|
|
43
|
+
status: 'down',
|
|
44
|
+
crush_weight: 1.0,
|
|
45
|
+
reweight: 0,
|
|
46
|
+
host: 'pve02',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('cephStatus handler', () => {
|
|
52
|
+
it('returns health, OSD counts, and PG states', async () => {
|
|
53
|
+
let callCount = 0;
|
|
54
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
55
|
+
callCount++;
|
|
56
|
+
expect(cmd).toBe('ceph');
|
|
57
|
+
if (callCount === 1) {
|
|
58
|
+
expect(args).toEqual(['status', '--format', 'json']);
|
|
59
|
+
return CEPH_STATUS_JSON;
|
|
60
|
+
}
|
|
61
|
+
expect(args).toEqual(['osd', 'tree', '--format', 'json']);
|
|
62
|
+
return OSD_TREE_JSON;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const result = (await cephStatus(undefined, mockExec)) as CephStatusResult;
|
|
66
|
+
expect(result.available).toBe(true);
|
|
67
|
+
expect(result.health).toBe('HEALTH_OK');
|
|
68
|
+
expect(result.osdCount).toBe(6);
|
|
69
|
+
expect(result.osdUp).toBe(6);
|
|
70
|
+
expect(result.osdIn).toBe(6);
|
|
71
|
+
expect(result.pgStates).toHaveLength(1);
|
|
72
|
+
expect(result.pgStates[0]).toEqual({ state: 'active+clean', count: 256 });
|
|
73
|
+
expect(result.usage.total).toBe(6000000000000);
|
|
74
|
+
expect(result.osds).toHaveLength(3);
|
|
75
|
+
expect(result.warnings).toHaveLength(0);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('flags degraded health', async () => {
|
|
79
|
+
const degraded = JSON.stringify({
|
|
80
|
+
health: { status: 'HEALTH_WARN' },
|
|
81
|
+
osdmap: { num_osds: 6, num_up_osds: 4, num_in_osds: 6 },
|
|
82
|
+
pgmap: { bytes_total: 0, bytes_used: 0, bytes_avail: 0 },
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
86
|
+
if (args[0] === 'status') return degraded;
|
|
87
|
+
return '{"nodes":[]}';
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const result = (await cephStatus(undefined, mockExec)) as CephStatusResult;
|
|
91
|
+
expect(result.warnings.some((w) => w.includes('HEALTH_WARN'))).toBe(true);
|
|
92
|
+
expect(result.warnings.some((w) => w.includes('2 OSD(s) down'))).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('handles missing ceph binary gracefully', async () => {
|
|
96
|
+
const mockExec: ExecFn = async () => {
|
|
97
|
+
throw new Error('command not found: ceph');
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const result = (await cephStatus(undefined, mockExec)) as { available: boolean };
|
|
101
|
+
expect(result.available).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('parseOsdTree', () => {
|
|
106
|
+
it('extracts only OSD nodes from the tree', () => {
|
|
107
|
+
const osds = parseOsdTree(OSD_TREE_JSON);
|
|
108
|
+
expect(osds).toHaveLength(3);
|
|
109
|
+
expect(osds[0]).toEqual({
|
|
110
|
+
id: 0,
|
|
111
|
+
name: 'osd.0',
|
|
112
|
+
type: 'osd',
|
|
113
|
+
status: 'up',
|
|
114
|
+
crush_weight: 1.0,
|
|
115
|
+
reweight: 1.0,
|
|
116
|
+
host: 'pve01',
|
|
117
|
+
});
|
|
118
|
+
expect(osds[2]?.status).toBe('down');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('filters out non-OSD nodes', () => {
|
|
122
|
+
const osds = parseOsdTree(OSD_TREE_JSON);
|
|
123
|
+
const types = osds.map((o) => o.type);
|
|
124
|
+
expect(types.every((t) => t === 'osd')).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { ProbeHandler } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
export interface OsdNode {
|
|
4
|
+
id: number;
|
|
5
|
+
name: string;
|
|
6
|
+
type: string;
|
|
7
|
+
status: string;
|
|
8
|
+
crush_weight: number;
|
|
9
|
+
reweight: number;
|
|
10
|
+
host: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CephStatusResult {
|
|
14
|
+
available: boolean;
|
|
15
|
+
health: string;
|
|
16
|
+
osdCount: number;
|
|
17
|
+
osdUp: number;
|
|
18
|
+
osdIn: number;
|
|
19
|
+
pgStates: Array<{ state: string; count: number }>;
|
|
20
|
+
usage: { total: number; used: number; avail: number };
|
|
21
|
+
osds: OsdNode[];
|
|
22
|
+
warnings: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Runs `ceph status --format json` and `ceph osd tree --format json`.
|
|
27
|
+
* Gracefully handles missing ceph binary.
|
|
28
|
+
*/
|
|
29
|
+
export const cephStatus: ProbeHandler = async (_params, exec) => {
|
|
30
|
+
let statusOut: string;
|
|
31
|
+
try {
|
|
32
|
+
statusOut = await exec('ceph', ['status', '--format', 'json']);
|
|
33
|
+
} catch {
|
|
34
|
+
return {
|
|
35
|
+
available: false,
|
|
36
|
+
health: null,
|
|
37
|
+
warnings: ['Ceph is not installed or not accessible on this node'],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return parseCephStatus(statusOut, exec);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export async function parseCephStatus(
|
|
45
|
+
statusOut: string,
|
|
46
|
+
exec?: (cmd: string, args: string[]) => Promise<string>,
|
|
47
|
+
): Promise<CephStatusResult> {
|
|
48
|
+
const data = JSON.parse(statusOut);
|
|
49
|
+
const warnings: string[] = [];
|
|
50
|
+
|
|
51
|
+
const health = data.health?.status ?? 'unknown';
|
|
52
|
+
const osdmap = data.osdmap ?? {};
|
|
53
|
+
const osdCount = osdmap.num_osds ?? 0;
|
|
54
|
+
const osdUp = osdmap.num_up_osds ?? 0;
|
|
55
|
+
const osdIn = osdmap.num_in_osds ?? 0;
|
|
56
|
+
|
|
57
|
+
if (health !== 'HEALTH_OK') {
|
|
58
|
+
warnings.push(`Ceph health: ${health}`);
|
|
59
|
+
}
|
|
60
|
+
if (osdCount > 0 && osdUp < osdCount) {
|
|
61
|
+
warnings.push(`${osdCount - osdUp} OSD(s) down`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const pgmap = data.pgmap ?? {};
|
|
65
|
+
const pgStates: Array<{ state: string; count: number }> = (pgmap.pgs_by_state ?? []).map(
|
|
66
|
+
(p: { state_name?: string; count?: number }) => ({
|
|
67
|
+
state: p.state_name ?? '',
|
|
68
|
+
count: p.count ?? 0,
|
|
69
|
+
}),
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const usage = {
|
|
73
|
+
total: pgmap.bytes_total ?? 0,
|
|
74
|
+
used: pgmap.bytes_used ?? 0,
|
|
75
|
+
avail: pgmap.bytes_avail ?? 0,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// OSD tree for placement info
|
|
79
|
+
let osds: OsdNode[] = [];
|
|
80
|
+
if (exec) {
|
|
81
|
+
try {
|
|
82
|
+
const treeOut = await exec('ceph', ['osd', 'tree', '--format', 'json']);
|
|
83
|
+
osds = parseOsdTree(treeOut);
|
|
84
|
+
} catch {
|
|
85
|
+
// OSD tree is best-effort
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { available: true, health, osdCount, osdUp, osdIn, pgStates, usage, osds, warnings };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function parseOsdTree(stdout: string): OsdNode[] {
|
|
93
|
+
const data = JSON.parse(stdout);
|
|
94
|
+
const nodes = data.nodes ?? [];
|
|
95
|
+
return nodes
|
|
96
|
+
.filter((n: { type?: string }) => n.type === 'osd')
|
|
97
|
+
.map(
|
|
98
|
+
(n: {
|
|
99
|
+
id?: number;
|
|
100
|
+
name?: string;
|
|
101
|
+
type?: string;
|
|
102
|
+
status?: string;
|
|
103
|
+
crush_weight?: number;
|
|
104
|
+
reweight?: number;
|
|
105
|
+
host?: string;
|
|
106
|
+
}) => ({
|
|
107
|
+
id: n.id ?? 0,
|
|
108
|
+
name: n.name ?? '',
|
|
109
|
+
type: n.type ?? 'osd',
|
|
110
|
+
status: n.status ?? 'unknown',
|
|
111
|
+
crush_weight: n.crush_weight ?? 0,
|
|
112
|
+
reweight: n.reweight ?? 0,
|
|
113
|
+
host: n.host ?? '',
|
|
114
|
+
}),
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { ExecFn } from '../../types.js';
|
|
3
|
+
import type { ClusterConfigResult } from './cluster-config.js';
|
|
4
|
+
import { clusterConfig, parseClusterConfig } from './cluster-config.js';
|
|
5
|
+
|
|
6
|
+
const SAMPLE_OUTPUT = `Cluster information
|
|
7
|
+
~~~~~~~~~~~~~~~~~~
|
|
8
|
+
Name: mycluster
|
|
9
|
+
Config Version: 3
|
|
10
|
+
Transport: knet
|
|
11
|
+
Secure auth: on
|
|
12
|
+
|
|
13
|
+
Quorum information
|
|
14
|
+
~~~~~~~~~~~~~~~~~~
|
|
15
|
+
Date: Mon Feb 17 10:00:00 2026
|
|
16
|
+
Quorum provider: corosync_votequorum
|
|
17
|
+
Nodes: 3
|
|
18
|
+
Node ID: 0x00000001
|
|
19
|
+
Ring ID: 1.123
|
|
20
|
+
Quorate: Yes
|
|
21
|
+
|
|
22
|
+
Votequorum information
|
|
23
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
24
|
+
Expected votes: 3
|
|
25
|
+
Highest expected: 3
|
|
26
|
+
Total votes: 3
|
|
27
|
+
Quorum: 2
|
|
28
|
+
Flags: Quorate
|
|
29
|
+
|
|
30
|
+
Membership information
|
|
31
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
32
|
+
Nodeid Votes Name
|
|
33
|
+
1 1 pve01 (local)
|
|
34
|
+
2 1 pve02
|
|
35
|
+
3 1 pve03`;
|
|
36
|
+
|
|
37
|
+
describe('parseClusterConfig', () => {
|
|
38
|
+
it('extracts cluster name', () => {
|
|
39
|
+
const result = parseClusterConfig(SAMPLE_OUTPUT);
|
|
40
|
+
expect(result.clusterName).toBe('mycluster');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('detects quorate status', () => {
|
|
44
|
+
const result = parseClusterConfig(SAMPLE_OUTPUT);
|
|
45
|
+
expect(result.quorate).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('extracts vote counts', () => {
|
|
49
|
+
const result = parseClusterConfig(SAMPLE_OUTPUT);
|
|
50
|
+
expect(result.totalVotes).toBe(3);
|
|
51
|
+
expect(result.expectedVotes).toBe(3);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('parses membership nodes', () => {
|
|
55
|
+
const result = parseClusterConfig(SAMPLE_OUTPUT);
|
|
56
|
+
expect(result.nodes).toHaveLength(3);
|
|
57
|
+
expect(result.nodes[0]).toEqual({
|
|
58
|
+
nodeId: '1',
|
|
59
|
+
name: 'pve01',
|
|
60
|
+
votes: 1,
|
|
61
|
+
local: true,
|
|
62
|
+
});
|
|
63
|
+
expect(result.nodes[1]).toEqual({
|
|
64
|
+
nodeId: '2',
|
|
65
|
+
name: 'pve02',
|
|
66
|
+
votes: 1,
|
|
67
|
+
local: false,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('no warnings when quorate', () => {
|
|
72
|
+
const result = parseClusterConfig(SAMPLE_OUTPUT);
|
|
73
|
+
expect(result.warnings).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('warns when not quorate', () => {
|
|
77
|
+
const notQuorate = SAMPLE_OUTPUT.replace('Quorate: Yes', 'Quorate: No');
|
|
78
|
+
const result = parseClusterConfig(notQuorate);
|
|
79
|
+
expect(result.warnings).toContain('Cluster is not quorate');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('handles single-node output', () => {
|
|
83
|
+
const singleNode = `Cluster information
|
|
84
|
+
~~~~~~~~~~~~~~~~~~
|
|
85
|
+
Name: standalone
|
|
86
|
+
|
|
87
|
+
Quorum information
|
|
88
|
+
~~~~~~~~~~~~~~~~~~
|
|
89
|
+
Quorate: Yes
|
|
90
|
+
|
|
91
|
+
Votequorum information
|
|
92
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
93
|
+
Expected votes: 1
|
|
94
|
+
Total votes: 1
|
|
95
|
+
|
|
96
|
+
Membership information
|
|
97
|
+
~~~~~~~~~~~~~~~~~~~~~~
|
|
98
|
+
Nodeid Votes Name
|
|
99
|
+
1 1 pve01 (local)`;
|
|
100
|
+
const result = parseClusterConfig(singleNode);
|
|
101
|
+
expect(result.nodes).toHaveLength(1);
|
|
102
|
+
expect(result.nodes[0]?.local).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('clusterConfig handler', () => {
|
|
107
|
+
it('calls pvecm status and returns parsed result', async () => {
|
|
108
|
+
const mockExec: ExecFn = async (cmd, args) => {
|
|
109
|
+
expect(cmd).toBe('pvecm');
|
|
110
|
+
expect(args).toEqual(['status']);
|
|
111
|
+
return SAMPLE_OUTPUT;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = (await clusterConfig(undefined, mockExec)) as ClusterConfigResult;
|
|
115
|
+
expect(result.clusterName).toBe('mycluster');
|
|
116
|
+
expect(result.nodes).toHaveLength(3);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ProbeHandler } from '../../types.js';
|
|
2
|
+
|
|
3
|
+
export interface ClusterNode {
|
|
4
|
+
nodeId: string;
|
|
5
|
+
name: string;
|
|
6
|
+
votes: number;
|
|
7
|
+
local: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ClusterConfigResult {
|
|
11
|
+
clusterName: string;
|
|
12
|
+
quorate: boolean;
|
|
13
|
+
totalVotes: number;
|
|
14
|
+
expectedVotes: number;
|
|
15
|
+
nodes: ClusterNode[];
|
|
16
|
+
warnings: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs `pvecm status` and parses cluster membership.
|
|
21
|
+
* Output contains sections like:
|
|
22
|
+
* Cluster information
|
|
23
|
+
* ~~~~~~~~~~~~~~~~~~
|
|
24
|
+
* Name: mycluster
|
|
25
|
+
* ...
|
|
26
|
+
* Membership information
|
|
27
|
+
* ~~~~~~~~~~~~~~~~~~~~~~
|
|
28
|
+
* Nodeid Votes Name
|
|
29
|
+
* 1 1 pve01 (local)
|
|
30
|
+
* 2 1 pve02
|
|
31
|
+
*/
|
|
32
|
+
export const clusterConfig: ProbeHandler = async (_params, exec) => {
|
|
33
|
+
const stdout = await exec('pvecm', ['status']);
|
|
34
|
+
return parseClusterConfig(stdout);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function parseClusterConfig(stdout: string): ClusterConfigResult {
|
|
38
|
+
const lines = stdout.trim().split('\n');
|
|
39
|
+
const warnings: string[] = [];
|
|
40
|
+
|
|
41
|
+
let clusterName = '';
|
|
42
|
+
let quorate = false;
|
|
43
|
+
let totalVotes = 0;
|
|
44
|
+
let expectedVotes = 0;
|
|
45
|
+
const nodes: ClusterNode[] = [];
|
|
46
|
+
|
|
47
|
+
let inMembership = false;
|
|
48
|
+
let membershipHeaderSeen = false;
|
|
49
|
+
|
|
50
|
+
for (const line of lines) {
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
|
|
53
|
+
// Key: Value pairs in cluster info section
|
|
54
|
+
if (trimmed.startsWith('Name:')) {
|
|
55
|
+
clusterName = trimmed.slice(5).trim();
|
|
56
|
+
}
|
|
57
|
+
if (trimmed.startsWith('Quorate:')) {
|
|
58
|
+
quorate = trimmed.toLowerCase().includes('yes');
|
|
59
|
+
}
|
|
60
|
+
if (trimmed.startsWith('Total votes:')) {
|
|
61
|
+
totalVotes = Number.parseInt(trimmed.slice('Total votes:'.length).trim(), 10) || 0;
|
|
62
|
+
}
|
|
63
|
+
if (trimmed.startsWith('Expected votes:')) {
|
|
64
|
+
expectedVotes = Number.parseInt(trimmed.slice('Expected votes:'.length).trim(), 10) || 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Membership section detection
|
|
68
|
+
if (trimmed.startsWith('Membership information') || trimmed.startsWith('Nodeid')) {
|
|
69
|
+
inMembership = true;
|
|
70
|
+
if (trimmed.startsWith('Nodeid')) membershipHeaderSeen = true;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (trimmed.startsWith('~~~')) continue;
|
|
74
|
+
|
|
75
|
+
// Parse node lines: "1 1 pve01 (local)"
|
|
76
|
+
if (inMembership && membershipHeaderSeen && trimmed) {
|
|
77
|
+
const parts = trimmed.split(/\s+/);
|
|
78
|
+
if (parts.length >= 3) {
|
|
79
|
+
const nodeId = parts[0] ?? '';
|
|
80
|
+
const votes = Number.parseInt(parts[1] ?? '', 10);
|
|
81
|
+
if (Number.isNaN(votes)) continue;
|
|
82
|
+
|
|
83
|
+
const nameAndFlags = parts.slice(2).join(' ');
|
|
84
|
+
const local = nameAndFlags.includes('(local)');
|
|
85
|
+
const name = nameAndFlags.replace('(local)', '').trim();
|
|
86
|
+
|
|
87
|
+
nodes.push({ nodeId, name, votes, local });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!quorate) {
|
|
93
|
+
warnings.push('Cluster is not quorate');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { clusterName, quorate, totalVotes, expectedVotes, nodes, warnings };
|
|
97
|
+
}
|