@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.
Files changed (359) hide show
  1. package/.turbo/turbo-build.log +6 -0
  2. package/.turbo/turbo-test.log +814 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/CHANGELOG.md +10 -0
  5. package/dist/index.d.ts +16 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +40 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/integrations/citrix.d.ts +13 -0
  10. package/dist/integrations/citrix.d.ts.map +1 -0
  11. package/dist/integrations/citrix.js +420 -0
  12. package/dist/integrations/citrix.js.map +1 -0
  13. package/dist/integrations/citrix.test.d.ts +2 -0
  14. package/dist/integrations/citrix.test.d.ts.map +1 -0
  15. package/dist/integrations/citrix.test.js +464 -0
  16. package/dist/integrations/citrix.test.js.map +1 -0
  17. package/dist/integrations/graph.d.ts +9 -0
  18. package/dist/integrations/graph.d.ts.map +1 -0
  19. package/dist/integrations/graph.js +290 -0
  20. package/dist/integrations/graph.js.map +1 -0
  21. package/dist/integrations/graph.test.d.ts +2 -0
  22. package/dist/integrations/graph.test.d.ts.map +1 -0
  23. package/dist/integrations/graph.test.js +356 -0
  24. package/dist/integrations/graph.test.js.map +1 -0
  25. package/dist/integrations/httpbin.d.ts +3 -0
  26. package/dist/integrations/httpbin.d.ts.map +1 -0
  27. package/dist/integrations/httpbin.js +70 -0
  28. package/dist/integrations/httpbin.js.map +1 -0
  29. package/dist/integrations/nutanix.d.ts +18 -0
  30. package/dist/integrations/nutanix.d.ts.map +1 -0
  31. package/dist/integrations/nutanix.js +1121 -0
  32. package/dist/integrations/nutanix.js.map +1 -0
  33. package/dist/integrations/nutanix.test.d.ts +2 -0
  34. package/dist/integrations/nutanix.test.d.ts.map +1 -0
  35. package/dist/integrations/nutanix.test.js +978 -0
  36. package/dist/integrations/nutanix.test.js.map +1 -0
  37. package/dist/integrations/proxmox.d.ts +12 -0
  38. package/dist/integrations/proxmox.d.ts.map +1 -0
  39. package/dist/integrations/proxmox.js +733 -0
  40. package/dist/integrations/proxmox.js.map +1 -0
  41. package/dist/integrations/proxmox.test.d.ts +2 -0
  42. package/dist/integrations/proxmox.test.d.ts.map +1 -0
  43. package/dist/integrations/proxmox.test.js +697 -0
  44. package/dist/integrations/proxmox.test.js.map +1 -0
  45. package/dist/integrations/servicenow.d.ts +3 -0
  46. package/dist/integrations/servicenow.d.ts.map +1 -0
  47. package/dist/integrations/servicenow.js +257 -0
  48. package/dist/integrations/servicenow.js.map +1 -0
  49. package/dist/integrations/servicenow.test.d.ts +2 -0
  50. package/dist/integrations/servicenow.test.d.ts.map +1 -0
  51. package/dist/integrations/servicenow.test.js +217 -0
  52. package/dist/integrations/servicenow.test.js.map +1 -0
  53. package/dist/integrations/splunk.d.ts +9 -0
  54. package/dist/integrations/splunk.d.ts.map +1 -0
  55. package/dist/integrations/splunk.js +242 -0
  56. package/dist/integrations/splunk.js.map +1 -0
  57. package/dist/integrations/splunk.test.d.ts +2 -0
  58. package/dist/integrations/splunk.test.d.ts.map +1 -0
  59. package/dist/integrations/splunk.test.js +323 -0
  60. package/dist/integrations/splunk.test.js.map +1 -0
  61. package/dist/mysql/index.d.ts +3 -0
  62. package/dist/mysql/index.d.ts.map +1 -0
  63. package/dist/mysql/index.js +13 -0
  64. package/dist/mysql/index.js.map +1 -0
  65. package/dist/mysql/manifest.d.ts +3 -0
  66. package/dist/mysql/manifest.d.ts.map +1 -0
  67. package/dist/mysql/manifest.js +69 -0
  68. package/dist/mysql/manifest.js.map +1 -0
  69. package/dist/mysql/probes/databases-list.d.ts +13 -0
  70. package/dist/mysql/probes/databases-list.d.ts.map +1 -0
  71. package/dist/mysql/probes/databases-list.js +31 -0
  72. package/dist/mysql/probes/databases-list.js.map +1 -0
  73. package/dist/mysql/probes/databases-list.test.d.ts +2 -0
  74. package/dist/mysql/probes/databases-list.test.d.ts.map +1 -0
  75. package/dist/mysql/probes/databases-list.test.js +54 -0
  76. package/dist/mysql/probes/databases-list.test.js.map +1 -0
  77. package/dist/mysql/probes/processlist.d.ts +18 -0
  78. package/dist/mysql/probes/processlist.d.ts.map +1 -0
  79. package/dist/mysql/probes/processlist.js +36 -0
  80. package/dist/mysql/probes/processlist.js.map +1 -0
  81. package/dist/mysql/probes/processlist.test.d.ts +2 -0
  82. package/dist/mysql/probes/processlist.test.d.ts.map +1 -0
  83. package/dist/mysql/probes/processlist.test.js +41 -0
  84. package/dist/mysql/probes/processlist.test.js.map +1 -0
  85. package/dist/mysql/probes/status.d.ts +14 -0
  86. package/dist/mysql/probes/status.d.ts.map +1 -0
  87. package/dist/mysql/probes/status.js +40 -0
  88. package/dist/mysql/probes/status.js.map +1 -0
  89. package/dist/mysql/probes/status.test.d.ts +2 -0
  90. package/dist/mysql/probes/status.test.d.ts.map +1 -0
  91. package/dist/mysql/probes/status.test.js +43 -0
  92. package/dist/mysql/probes/status.test.js.map +1 -0
  93. package/dist/nginx/index.d.ts +3 -0
  94. package/dist/nginx/index.d.ts.map +1 -0
  95. package/dist/nginx/index.js +13 -0
  96. package/dist/nginx/index.js.map +1 -0
  97. package/dist/nginx/manifest.d.ts +3 -0
  98. package/dist/nginx/manifest.d.ts.map +1 -0
  99. package/dist/nginx/manifest.js +68 -0
  100. package/dist/nginx/manifest.js.map +1 -0
  101. package/dist/nginx/probes/access-log-tail.d.ts +9 -0
  102. package/dist/nginx/probes/access-log-tail.d.ts.map +1 -0
  103. package/dist/nginx/probes/access-log-tail.js +14 -0
  104. package/dist/nginx/probes/access-log-tail.js.map +1 -0
  105. package/dist/nginx/probes/access-log-tail.test.d.ts +2 -0
  106. package/dist/nginx/probes/access-log-tail.test.d.ts.map +1 -0
  107. package/dist/nginx/probes/access-log-tail.test.js +40 -0
  108. package/dist/nginx/probes/access-log-tail.test.js.map +1 -0
  109. package/dist/nginx/probes/config-test.d.ts +8 -0
  110. package/dist/nginx/probes/config-test.d.ts.map +1 -0
  111. package/dist/nginx/probes/config-test.js +18 -0
  112. package/dist/nginx/probes/config-test.js.map +1 -0
  113. package/dist/nginx/probes/config-test.test.d.ts +2 -0
  114. package/dist/nginx/probes/config-test.test.d.ts.map +1 -0
  115. package/dist/nginx/probes/config-test.test.js +35 -0
  116. package/dist/nginx/probes/config-test.test.js.map +1 -0
  117. package/dist/nginx/probes/error-log-tail.d.ts +9 -0
  118. package/dist/nginx/probes/error-log-tail.d.ts.map +1 -0
  119. package/dist/nginx/probes/error-log-tail.js +14 -0
  120. package/dist/nginx/probes/error-log-tail.js.map +1 -0
  121. package/dist/nginx/probes/error-log-tail.test.d.ts +2 -0
  122. package/dist/nginx/probes/error-log-tail.test.d.ts.map +1 -0
  123. package/dist/nginx/probes/error-log-tail.test.js +34 -0
  124. package/dist/nginx/probes/error-log-tail.test.js.map +1 -0
  125. package/dist/postgres/index.d.ts +3 -0
  126. package/dist/postgres/index.d.ts.map +1 -0
  127. package/dist/postgres/index.js +13 -0
  128. package/dist/postgres/index.js.map +1 -0
  129. package/dist/postgres/manifest.d.ts +3 -0
  130. package/dist/postgres/manifest.d.ts.map +1 -0
  131. package/dist/postgres/manifest.js +90 -0
  132. package/dist/postgres/manifest.js.map +1 -0
  133. package/dist/postgres/probes/connections-active.d.ts +17 -0
  134. package/dist/postgres/probes/connections-active.d.ts.map +1 -0
  135. package/dist/postgres/probes/connections-active.js +37 -0
  136. package/dist/postgres/probes/connections-active.js.map +1 -0
  137. package/dist/postgres/probes/connections-active.test.d.ts +2 -0
  138. package/dist/postgres/probes/connections-active.test.d.ts.map +1 -0
  139. package/dist/postgres/probes/connections-active.test.js +36 -0
  140. package/dist/postgres/probes/connections-active.test.js.map +1 -0
  141. package/dist/postgres/probes/databases-list.d.ts +14 -0
  142. package/dist/postgres/probes/databases-list.d.ts.map +1 -0
  143. package/dist/postgres/probes/databases-list.js +34 -0
  144. package/dist/postgres/probes/databases-list.js.map +1 -0
  145. package/dist/postgres/probes/databases-list.test.d.ts +2 -0
  146. package/dist/postgres/probes/databases-list.test.d.ts.map +1 -0
  147. package/dist/postgres/probes/databases-list.test.js +49 -0
  148. package/dist/postgres/probes/databases-list.test.js.map +1 -0
  149. package/dist/postgres/probes/query-slow.d.ts +17 -0
  150. package/dist/postgres/probes/query-slow.d.ts.map +1 -0
  151. package/dist/postgres/probes/query-slow.js +37 -0
  152. package/dist/postgres/probes/query-slow.js.map +1 -0
  153. package/dist/postgres/probes/query-slow.test.d.ts +2 -0
  154. package/dist/postgres/probes/query-slow.test.d.ts.map +1 -0
  155. package/dist/postgres/probes/query-slow.test.js +30 -0
  156. package/dist/postgres/probes/query-slow.test.js.map +1 -0
  157. package/dist/proxmox/index.d.ts +3 -0
  158. package/dist/proxmox/index.d.ts.map +1 -0
  159. package/dist/proxmox/index.js +23 -0
  160. package/dist/proxmox/index.js.map +1 -0
  161. package/dist/proxmox/manifest.d.ts +3 -0
  162. package/dist/proxmox/manifest.d.ts.map +1 -0
  163. package/dist/proxmox/manifest.js +75 -0
  164. package/dist/proxmox/manifest.js.map +1 -0
  165. package/dist/proxmox/probes/ceph-status.d.ts +36 -0
  166. package/dist/proxmox/probes/ceph-status.d.ts.map +1 -0
  167. package/dist/proxmox/probes/ceph-status.js +71 -0
  168. package/dist/proxmox/probes/ceph-status.js.map +1 -0
  169. package/dist/proxmox/probes/ceph-status.test.d.ts +2 -0
  170. package/dist/proxmox/probes/ceph-status.test.d.ts.map +1 -0
  171. package/dist/proxmox/probes/ceph-status.test.js +115 -0
  172. package/dist/proxmox/probes/ceph-status.test.js.map +1 -0
  173. package/dist/proxmox/probes/cluster-config.d.ts +31 -0
  174. package/dist/proxmox/probes/cluster-config.d.ts.map +1 -0
  175. package/dist/proxmox/probes/cluster-config.js +72 -0
  176. package/dist/proxmox/probes/cluster-config.js.map +1 -0
  177. package/dist/proxmox/probes/cluster-config.test.d.ts +2 -0
  178. package/dist/proxmox/probes/cluster-config.test.d.ts.map +1 -0
  179. package/dist/proxmox/probes/cluster-config.test.js +107 -0
  180. package/dist/proxmox/probes/cluster-config.test.js.map +1 -0
  181. package/dist/proxmox/probes/ha-status.d.ts +18 -0
  182. package/dist/proxmox/probes/ha-status.d.ts.map +1 -0
  183. package/dist/proxmox/probes/ha-status.js +38 -0
  184. package/dist/proxmox/probes/ha-status.js.map +1 -0
  185. package/dist/proxmox/probes/ha-status.test.d.ts +2 -0
  186. package/dist/proxmox/probes/ha-status.test.d.ts.map +1 -0
  187. package/dist/proxmox/probes/ha-status.test.js +66 -0
  188. package/dist/proxmox/probes/ha-status.test.js.map +1 -0
  189. package/dist/proxmox/probes/lvm.d.ts +35 -0
  190. package/dist/proxmox/probes/lvm.d.ts.map +1 -0
  191. package/dist/proxmox/probes/lvm.js +75 -0
  192. package/dist/proxmox/probes/lvm.js.map +1 -0
  193. package/dist/proxmox/probes/lvm.test.d.ts +2 -0
  194. package/dist/proxmox/probes/lvm.test.d.ts.map +1 -0
  195. package/dist/proxmox/probes/lvm.test.js +128 -0
  196. package/dist/proxmox/probes/lvm.test.js.map +1 -0
  197. package/dist/proxmox/probes/lxc-config.d.ts +29 -0
  198. package/dist/proxmox/probes/lxc-config.d.ts.map +1 -0
  199. package/dist/proxmox/probes/lxc-config.js +67 -0
  200. package/dist/proxmox/probes/lxc-config.js.map +1 -0
  201. package/dist/proxmox/probes/lxc-config.test.d.ts +2 -0
  202. package/dist/proxmox/probes/lxc-config.test.d.ts.map +1 -0
  203. package/dist/proxmox/probes/lxc-config.test.js +77 -0
  204. package/dist/proxmox/probes/lxc-config.test.js.map +1 -0
  205. package/dist/proxmox/probes/lxc-list.d.ts +20 -0
  206. package/dist/proxmox/probes/lxc-list.d.ts.map +1 -0
  207. package/dist/proxmox/probes/lxc-list.js +49 -0
  208. package/dist/proxmox/probes/lxc-list.js.map +1 -0
  209. package/dist/proxmox/probes/lxc-list.test.d.ts +2 -0
  210. package/dist/proxmox/probes/lxc-list.test.d.ts.map +1 -0
  211. package/dist/proxmox/probes/lxc-list.test.js +51 -0
  212. package/dist/proxmox/probes/lxc-list.test.js.map +1 -0
  213. package/dist/proxmox/probes/vm-config.d.ts +21 -0
  214. package/dist/proxmox/probes/vm-config.d.ts.map +1 -0
  215. package/dist/proxmox/probes/vm-config.js +58 -0
  216. package/dist/proxmox/probes/vm-config.js.map +1 -0
  217. package/dist/proxmox/probes/vm-config.test.d.ts +2 -0
  218. package/dist/proxmox/probes/vm-config.test.d.ts.map +1 -0
  219. package/dist/proxmox/probes/vm-config.test.js +80 -0
  220. package/dist/proxmox/probes/vm-config.test.js.map +1 -0
  221. package/dist/proxmox/probes/vm-locks.d.ts +16 -0
  222. package/dist/proxmox/probes/vm-locks.d.ts.map +1 -0
  223. package/dist/proxmox/probes/vm-locks.js +35 -0
  224. package/dist/proxmox/probes/vm-locks.js.map +1 -0
  225. package/dist/proxmox/probes/vm-locks.test.d.ts +2 -0
  226. package/dist/proxmox/probes/vm-locks.test.d.ts.map +1 -0
  227. package/dist/proxmox/probes/vm-locks.test.js +54 -0
  228. package/dist/proxmox/probes/vm-locks.test.js.map +1 -0
  229. package/dist/redis/index.d.ts +3 -0
  230. package/dist/redis/index.d.ts.map +1 -0
  231. package/dist/redis/index.js +13 -0
  232. package/dist/redis/index.js.map +1 -0
  233. package/dist/redis/manifest.d.ts +3 -0
  234. package/dist/redis/manifest.d.ts.map +1 -0
  235. package/dist/redis/manifest.js +51 -0
  236. package/dist/redis/manifest.js.map +1 -0
  237. package/dist/redis/probes/info.d.ts +15 -0
  238. package/dist/redis/probes/info.d.ts.map +1 -0
  239. package/dist/redis/probes/info.js +32 -0
  240. package/dist/redis/probes/info.js.map +1 -0
  241. package/dist/redis/probes/info.test.d.ts +2 -0
  242. package/dist/redis/probes/info.test.d.ts.map +1 -0
  243. package/dist/redis/probes/info.test.js +64 -0
  244. package/dist/redis/probes/info.test.js.map +1 -0
  245. package/dist/redis/probes/keys-count.d.ts +13 -0
  246. package/dist/redis/probes/keys-count.d.ts.map +1 -0
  247. package/dist/redis/probes/keys-count.js +24 -0
  248. package/dist/redis/probes/keys-count.js.map +1 -0
  249. package/dist/redis/probes/keys-count.test.d.ts +2 -0
  250. package/dist/redis/probes/keys-count.test.d.ts.map +1 -0
  251. package/dist/redis/probes/keys-count.test.js +37 -0
  252. package/dist/redis/probes/keys-count.test.js.map +1 -0
  253. package/dist/redis/probes/memory-usage.d.ts +16 -0
  254. package/dist/redis/probes/memory-usage.d.ts.map +1 -0
  255. package/dist/redis/probes/memory-usage.js +31 -0
  256. package/dist/redis/probes/memory-usage.js.map +1 -0
  257. package/dist/redis/probes/memory-usage.test.d.ts +2 -0
  258. package/dist/redis/probes/memory-usage.test.d.ts.map +1 -0
  259. package/dist/redis/probes/memory-usage.test.js +48 -0
  260. package/dist/redis/probes/memory-usage.test.js.map +1 -0
  261. package/dist/runbooks/nutanix.d.ts +3 -0
  262. package/dist/runbooks/nutanix.d.ts.map +1 -0
  263. package/dist/runbooks/nutanix.js +619 -0
  264. package/dist/runbooks/nutanix.js.map +1 -0
  265. package/dist/runbooks/nutanix.test.d.ts +2 -0
  266. package/dist/runbooks/nutanix.test.d.ts.map +1 -0
  267. package/dist/runbooks/nutanix.test.js +971 -0
  268. package/dist/runbooks/nutanix.test.js.map +1 -0
  269. package/dist/runbooks/proxmox.d.ts +3 -0
  270. package/dist/runbooks/proxmox.d.ts.map +1 -0
  271. package/dist/runbooks/proxmox.js +451 -0
  272. package/dist/runbooks/proxmox.js.map +1 -0
  273. package/dist/runbooks/proxmox.test.d.ts +2 -0
  274. package/dist/runbooks/proxmox.test.d.ts.map +1 -0
  275. package/dist/runbooks/proxmox.test.js +700 -0
  276. package/dist/runbooks/proxmox.test.js.map +1 -0
  277. package/dist/signatures.d.ts +2 -0
  278. package/dist/signatures.d.ts.map +1 -0
  279. package/dist/signatures.js +2 -0
  280. package/dist/signatures.js.map +1 -0
  281. package/dist/types.d.ts +53 -0
  282. package/dist/types.d.ts.map +1 -1
  283. package/dist/validation.d.ts +6 -1
  284. package/dist/validation.d.ts.map +1 -1
  285. package/dist/validation.js +10 -1
  286. package/dist/validation.js.map +1 -1
  287. package/package.json +1 -1
  288. package/src/index.ts +60 -6
  289. package/src/integrations/citrix.test.ts +592 -0
  290. package/src/integrations/citrix.ts +557 -0
  291. package/src/integrations/graph.test.ts +478 -0
  292. package/src/integrations/graph.ts +413 -0
  293. package/src/integrations/httpbin.ts +72 -0
  294. package/src/integrations/nutanix.test.ts +1508 -0
  295. package/src/integrations/nutanix.ts +1460 -0
  296. package/src/integrations/proxmox.test.ts +1020 -0
  297. package/src/integrations/proxmox.ts +989 -0
  298. package/src/integrations/servicenow.test.ts +314 -0
  299. package/src/integrations/servicenow.ts +285 -0
  300. package/src/integrations/splunk.test.ts +440 -0
  301. package/src/integrations/splunk.ts +356 -0
  302. package/src/mysql/index.ts +14 -0
  303. package/src/mysql/manifest.ts +70 -0
  304. package/src/mysql/probes/databases-list.test.ts +62 -0
  305. package/src/mysql/probes/databases-list.ts +45 -0
  306. package/src/mysql/probes/processlist.test.ts +47 -0
  307. package/src/mysql/probes/processlist.ts +55 -0
  308. package/src/mysql/probes/status.test.ts +50 -0
  309. package/src/mysql/probes/status.ts +56 -0
  310. package/src/nginx/index.ts +14 -0
  311. package/src/nginx/manifest.ts +69 -0
  312. package/src/nginx/probes/access-log-tail.test.ts +51 -0
  313. package/src/nginx/probes/access-log-tail.ts +23 -0
  314. package/src/nginx/probes/config-test.test.ts +47 -0
  315. package/src/nginx/probes/config-test.ts +24 -0
  316. package/src/nginx/probes/error-log-tail.test.ts +44 -0
  317. package/src/nginx/probes/error-log-tail.ts +23 -0
  318. package/src/postgres/index.ts +14 -0
  319. package/src/postgres/manifest.ts +91 -0
  320. package/src/postgres/probes/connections-active.test.ts +42 -0
  321. package/src/postgres/probes/connections-active.ts +55 -0
  322. package/src/postgres/probes/databases-list.test.ts +57 -0
  323. package/src/postgres/probes/databases-list.ts +49 -0
  324. package/src/postgres/probes/query-slow.test.ts +37 -0
  325. package/src/postgres/probes/query-slow.ts +55 -0
  326. package/src/proxmox/index.ts +24 -0
  327. package/src/proxmox/manifest.ts +76 -0
  328. package/src/proxmox/probes/ceph-status.test.ts +126 -0
  329. package/src/proxmox/probes/ceph-status.ts +116 -0
  330. package/src/proxmox/probes/cluster-config.test.ts +118 -0
  331. package/src/proxmox/probes/cluster-config.ts +97 -0
  332. package/src/proxmox/probes/ha-status.test.ts +76 -0
  333. package/src/proxmox/probes/ha-status.ts +56 -0
  334. package/src/proxmox/probes/lvm.test.ts +140 -0
  335. package/src/proxmox/probes/lvm.ts +121 -0
  336. package/src/proxmox/probes/lxc-config.test.ts +89 -0
  337. package/src/proxmox/probes/lxc-config.ts +90 -0
  338. package/src/proxmox/probes/lxc-list.test.ts +60 -0
  339. package/src/proxmox/probes/lxc-list.ts +67 -0
  340. package/src/proxmox/probes/vm-config.test.ts +93 -0
  341. package/src/proxmox/probes/vm-config.ts +77 -0
  342. package/src/proxmox/probes/vm-locks.test.ts +63 -0
  343. package/src/proxmox/probes/vm-locks.ts +49 -0
  344. package/src/redis/index.ts +14 -0
  345. package/src/redis/manifest.ts +52 -0
  346. package/src/redis/probes/info.test.ts +73 -0
  347. package/src/redis/probes/info.ts +46 -0
  348. package/src/redis/probes/keys-count.test.ts +44 -0
  349. package/src/redis/probes/keys-count.ts +38 -0
  350. package/src/redis/probes/memory-usage.test.ts +54 -0
  351. package/src/redis/probes/memory-usage.ts +46 -0
  352. package/src/runbooks/nutanix.test.ts +1138 -0
  353. package/src/runbooks/nutanix.ts +941 -0
  354. package/src/runbooks/proxmox.test.ts +838 -0
  355. package/src/runbooks/proxmox.ts +626 -0
  356. package/src/signatures.ts +1 -0
  357. package/src/types.ts +62 -0
  358. package/src/validation.ts +21 -1
  359. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,76 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { HaStatusResult } from './ha-status.js';
4
+ import { haStatus, parseHaStatus } from './ha-status.js';
5
+
6
+ const SAMPLE_OUTPUT = `quorum OK
7
+ master pve01 (192.168.1.1, 9dab1c9a)
8
+ vm:100 started pve01 none
9
+ vm:101 started pve02 none
10
+ ct:200 started pve01 none`;
11
+
12
+ describe('parseHaStatus', () => {
13
+ it('parses HA resource entries', () => {
14
+ const result = parseHaStatus(SAMPLE_OUTPUT);
15
+ expect(result.resources).toHaveLength(3);
16
+ expect(result.resources[0]).toEqual({
17
+ sid: 'vm:100',
18
+ state: 'started',
19
+ node: 'pve01',
20
+ request: 'none',
21
+ });
22
+ expect(result.resources[2]).toEqual({
23
+ sid: 'ct:200',
24
+ state: 'started',
25
+ node: 'pve01',
26
+ request: 'none',
27
+ });
28
+ });
29
+
30
+ it('flags error state', () => {
31
+ const output = `quorum OK
32
+ vm:100 error pve01 none`;
33
+ const result = parseHaStatus(output);
34
+ expect(result.warnings).toContain('HA resource vm:100 in error state on pve01');
35
+ });
36
+
37
+ it('flags fence state', () => {
38
+ const output = `quorum OK
39
+ vm:101 fence pve02 none`;
40
+ const result = parseHaStatus(output);
41
+ expect(result.warnings).toContain('HA resource vm:101 in fence state on pve02');
42
+ });
43
+
44
+ it('flags stopped resources', () => {
45
+ const output = `quorum OK
46
+ vm:100 stopped pve01 none`;
47
+ const result = parseHaStatus(output);
48
+ expect(result.warnings).toContain('HA resource vm:100 is stopped');
49
+ });
50
+
51
+ it('returns empty for no resources', () => {
52
+ const result = parseHaStatus('quorum OK');
53
+ expect(result.resources).toHaveLength(0);
54
+ expect(result.warnings).toHaveLength(0);
55
+ });
56
+
57
+ it('skips master/quorum lines', () => {
58
+ const result = parseHaStatus(SAMPLE_OUTPUT);
59
+ const sids = result.resources.map((r) => r.sid);
60
+ expect(sids).not.toContain('quorum');
61
+ expect(sids).not.toContain('master');
62
+ });
63
+ });
64
+
65
+ describe('haStatus handler', () => {
66
+ it('calls ha-manager status and returns parsed result', async () => {
67
+ const mockExec: ExecFn = async (cmd, args) => {
68
+ expect(cmd).toBe('ha-manager');
69
+ expect(args).toEqual(['status']);
70
+ return SAMPLE_OUTPUT;
71
+ };
72
+
73
+ const result = (await haStatus(undefined, mockExec)) as HaStatusResult;
74
+ expect(result.resources).toHaveLength(3);
75
+ });
76
+ });
@@ -0,0 +1,56 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface HaResource {
4
+ sid: string;
5
+ state: string;
6
+ node: string;
7
+ request: string;
8
+ }
9
+
10
+ export interface HaStatusResult {
11
+ resources: HaResource[];
12
+ warnings: string[];
13
+ }
14
+
15
+ /**
16
+ * Runs `ha-manager status` and parses the output.
17
+ * Format: "quorum OK\nsid state node request\n..."
18
+ */
19
+ export const haStatus: ProbeHandler = async (_params, exec) => {
20
+ const stdout = await exec('ha-manager', ['status']);
21
+ return parseHaStatus(stdout);
22
+ };
23
+
24
+ export function parseHaStatus(stdout: string): HaStatusResult {
25
+ const lines = stdout.trim().split('\n').filter(Boolean);
26
+ const resources: HaResource[] = [];
27
+ const warnings: string[] = [];
28
+
29
+ for (const line of lines) {
30
+ // Skip header/quorum lines
31
+ if (line.startsWith('quorum') || line.startsWith('master')) continue;
32
+
33
+ // Resource lines: "vm:100 started pve01 none"
34
+ const parts = line.trim().split(/\s+/);
35
+ if (parts.length < 2) continue;
36
+
37
+ const sid = parts[0] ?? '';
38
+ // Skip if it doesn't look like a resource SID (e.g. vm:100, ct:200)
39
+ if (!sid.includes(':')) continue;
40
+
41
+ const state = parts[1] ?? '';
42
+ const node = parts[2] ?? '';
43
+ const request = parts[3] ?? 'none';
44
+
45
+ resources.push({ sid, state, node, request });
46
+
47
+ if (state === 'error' || state === 'fence') {
48
+ warnings.push(`HA resource ${sid} in ${state} state on ${node}`);
49
+ }
50
+ if (state === 'stopped') {
51
+ warnings.push(`HA resource ${sid} is stopped`);
52
+ }
53
+ }
54
+
55
+ return { resources, warnings };
56
+ }
@@ -0,0 +1,140 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { LvmResult } from './lvm.js';
4
+ import { lvm, parseLvm } from './lvm.js';
5
+
6
+ const LVS_OUTPUT = JSON.stringify({
7
+ report: [
8
+ {
9
+ lv: [
10
+ {
11
+ lv_name: 'data',
12
+ vg_name: 'pve',
13
+ lv_size: '200000000000B',
14
+ lv_attr: '-wi-a-----',
15
+ pool_lv: '',
16
+ data_percent: '',
17
+ },
18
+ {
19
+ lv_name: 'root',
20
+ vg_name: 'pve',
21
+ lv_size: '50000000000B',
22
+ lv_attr: '-wi-a-----',
23
+ pool_lv: '',
24
+ data_percent: '',
25
+ },
26
+ {
27
+ lv_name: 'thinpool',
28
+ vg_name: 'pve',
29
+ lv_size: '400000000000B',
30
+ lv_attr: 'twi-aotz--',
31
+ pool_lv: '',
32
+ data_percent: '92.50',
33
+ },
34
+ ],
35
+ },
36
+ ],
37
+ });
38
+
39
+ const VGS_OUTPUT = JSON.stringify({
40
+ report: [
41
+ {
42
+ vg: [
43
+ { vg_name: 'pve', vg_size: '500000000000B', vg_free: '0B', pv_count: '1', lv_count: '3' },
44
+ ],
45
+ },
46
+ ],
47
+ });
48
+
49
+ const PVS_OUTPUT = JSON.stringify({
50
+ report: [
51
+ {
52
+ pv: [
53
+ {
54
+ pv_name: '/dev/sda3',
55
+ vg_name: 'pve',
56
+ pv_size: '500000000000B',
57
+ pv_free: '0B',
58
+ pv_fmt: 'lvm2',
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ });
64
+
65
+ describe('parseLvm', () => {
66
+ it('parses LVM topology from JSON reports', () => {
67
+ const result = parseLvm(LVS_OUTPUT, VGS_OUTPUT, PVS_OUTPUT);
68
+
69
+ expect(result.logicalVolumes).toHaveLength(3);
70
+ expect(result.logicalVolumes[0]?.name).toBe('data');
71
+ expect(result.logicalVolumes[0]?.vgName).toBe('pve');
72
+
73
+ expect(result.volumeGroups).toHaveLength(1);
74
+ expect(result.volumeGroups[0]?.name).toBe('pve');
75
+
76
+ expect(result.physicalVolumes).toHaveLength(1);
77
+ expect(result.physicalVolumes[0]?.name).toBe('/dev/sda3');
78
+ expect(result.physicalVolumes[0]?.format).toBe('lvm2');
79
+ });
80
+
81
+ it('flags thin pool with high usage', () => {
82
+ const result = parseLvm(LVS_OUTPUT, VGS_OUTPUT, PVS_OUTPUT);
83
+ expect(result.warnings.some((w) => w.includes('thinpool') && w.includes('92.50'))).toBe(true);
84
+ });
85
+
86
+ it('flags VGs with no free space', () => {
87
+ const result = parseLvm(LVS_OUTPUT, VGS_OUTPUT, PVS_OUTPUT);
88
+ expect(result.warnings).toContain('Volume group pve has no free space');
89
+ });
90
+
91
+ it('does not flag healthy thin pools', () => {
92
+ const healthyLvs = JSON.stringify({
93
+ report: [
94
+ {
95
+ lv: [
96
+ {
97
+ lv_name: 'thinpool',
98
+ vg_name: 'pve',
99
+ lv_size: '400B',
100
+ lv_attr: 'twi-aotz--',
101
+ pool_lv: '',
102
+ data_percent: '50.00',
103
+ },
104
+ ],
105
+ },
106
+ ],
107
+ });
108
+ const healthyVgs = JSON.stringify({
109
+ report: [
110
+ {
111
+ vg: [{ vg_name: 'pve', vg_size: '500B', vg_free: '100B', pv_count: '1', lv_count: '1' }],
112
+ },
113
+ ],
114
+ });
115
+ const result = parseLvm(healthyLvs, healthyVgs, PVS_OUTPUT);
116
+ expect(result.warnings).toHaveLength(0);
117
+ });
118
+ });
119
+
120
+ describe('lvm handler', () => {
121
+ it('calls lvs, vgs, pvs with correct args', async () => {
122
+ const calls: Array<{ cmd: string; args: string[] }> = [];
123
+ const mockExec: ExecFn = async (cmd, args) => {
124
+ calls.push({ cmd, args });
125
+ if (cmd === 'lvs') return LVS_OUTPUT;
126
+ if (cmd === 'vgs') return VGS_OUTPUT;
127
+ return PVS_OUTPUT;
128
+ };
129
+
130
+ const result = (await lvm(undefined, mockExec)) as LvmResult;
131
+ expect(result.logicalVolumes).toHaveLength(3);
132
+
133
+ expect(calls).toHaveLength(3);
134
+ expect(calls[0]?.cmd).toBe('lvs');
135
+ expect(calls[0]?.args).toContain('--reportformat');
136
+ expect(calls[0]?.args).toContain('json');
137
+ expect(calls[1]?.cmd).toBe('vgs');
138
+ expect(calls[2]?.cmd).toBe('pvs');
139
+ });
140
+ });
@@ -0,0 +1,121 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface LogicalVolume {
4
+ name: string;
5
+ vgName: string;
6
+ size: string;
7
+ attrs: string;
8
+ pool: string;
9
+ dataPercent: string;
10
+ }
11
+
12
+ export interface VolumeGroup {
13
+ name: string;
14
+ size: string;
15
+ free: string;
16
+ pvCount: number;
17
+ lvCount: number;
18
+ }
19
+
20
+ export interface PhysicalVolume {
21
+ name: string;
22
+ vgName: string;
23
+ size: string;
24
+ free: string;
25
+ format: string;
26
+ }
27
+
28
+ export interface LvmResult {
29
+ logicalVolumes: LogicalVolume[];
30
+ volumeGroups: VolumeGroup[];
31
+ physicalVolumes: PhysicalVolume[];
32
+ warnings: string[];
33
+ }
34
+
35
+ /**
36
+ * Runs lvs, vgs, pvs with --reportformat json and returns structured LVM topology.
37
+ */
38
+ export const lvm: ProbeHandler = async (_params, exec) => {
39
+ const [lvsOut, vgsOut, pvsOut] = await Promise.all([
40
+ exec('lvs', [
41
+ '--reportformat',
42
+ 'json',
43
+ '--units',
44
+ 'b',
45
+ '-o',
46
+ 'lv_name,vg_name,lv_size,lv_attr,pool_lv,data_percent',
47
+ ]),
48
+ exec('vgs', [
49
+ '--reportformat',
50
+ 'json',
51
+ '--units',
52
+ 'b',
53
+ '-o',
54
+ 'vg_name,vg_size,vg_free,pv_count,lv_count',
55
+ ]),
56
+ exec('pvs', [
57
+ '--reportformat',
58
+ 'json',
59
+ '--units',
60
+ 'b',
61
+ '-o',
62
+ 'pv_name,vg_name,pv_size,pv_free,pv_fmt',
63
+ ]),
64
+ ]);
65
+ return parseLvm(lvsOut, vgsOut, pvsOut);
66
+ };
67
+
68
+ export function parseLvm(lvsOut: string, vgsOut: string, pvsOut: string): LvmResult {
69
+ const warnings: string[] = [];
70
+
71
+ const lvsData = JSON.parse(lvsOut);
72
+ const logicalVolumes: LogicalVolume[] = (lvsData.report?.[0]?.lv ?? []).map(
73
+ (lv: Record<string, string>) => ({
74
+ name: lv.lv_name ?? '',
75
+ vgName: lv.vg_name ?? '',
76
+ size: lv.lv_size ?? '',
77
+ attrs: lv.lv_attr ?? '',
78
+ pool: lv.pool_lv ?? '',
79
+ dataPercent: lv.data_percent ?? '',
80
+ }),
81
+ );
82
+
83
+ const vgsData = JSON.parse(vgsOut);
84
+ const volumeGroups: VolumeGroup[] = (vgsData.report?.[0]?.vg ?? []).map(
85
+ (vg: Record<string, string>) => ({
86
+ name: vg.vg_name ?? '',
87
+ size: vg.vg_size ?? '',
88
+ free: vg.vg_free ?? '',
89
+ pvCount: Number(vg.pv_count ?? 0),
90
+ lvCount: Number(vg.lv_count ?? 0),
91
+ }),
92
+ );
93
+
94
+ const pvsData = JSON.parse(pvsOut);
95
+ const physicalVolumes: PhysicalVolume[] = (pvsData.report?.[0]?.pv ?? []).map(
96
+ (pv: Record<string, string>) => ({
97
+ name: pv.pv_name ?? '',
98
+ vgName: pv.vg_name ?? '',
99
+ size: pv.pv_size ?? '',
100
+ free: pv.pv_free ?? '',
101
+ format: pv.pv_fmt ?? '',
102
+ }),
103
+ );
104
+
105
+ // Flag thin pools with high usage
106
+ for (const lv of logicalVolumes) {
107
+ if (lv.dataPercent && Number.parseFloat(lv.dataPercent) > 85) {
108
+ warnings.push(`Thin pool ${lv.name} is ${lv.dataPercent}% used`);
109
+ }
110
+ }
111
+
112
+ // Flag VGs with no free space
113
+ for (const vg of volumeGroups) {
114
+ const freeBytes = Number.parseInt(vg.free, 10);
115
+ if (!Number.isNaN(freeBytes) && freeBytes === 0) {
116
+ warnings.push(`Volume group ${vg.name} has no free space`);
117
+ }
118
+ }
119
+
120
+ return { logicalVolumes, volumeGroups, physicalVolumes, warnings };
121
+ }
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { LxcConfigResult } from './lxc-config.js';
4
+ import { lxcConfig, parseLxcConfig } from './lxc-config.js';
5
+
6
+ const SAMPLE_OUTPUT = `arch: amd64
7
+ cores: 2
8
+ hostname: ct-nginx
9
+ memory: 512
10
+ mp0: local-lvm:subvol-200-disk-1,mp=/data,size=20G
11
+ net0: name=eth0,bridge=vmbr0,firewall=1,hwaddr=AA:BB:CC:DD:EE:FF,ip=dhcp,type=veth
12
+ ostype: debian
13
+ rootfs: local-lvm:subvol-200-disk-0,size=8G
14
+ swap: 256
15
+ unprivileged: 1`;
16
+
17
+ describe('parseLxcConfig', () => {
18
+ it('parses key: value pairs into config map', () => {
19
+ const result = parseLxcConfig(SAMPLE_OUTPUT, 200);
20
+ expect(result.vmid).toBe(200);
21
+ expect(result.config.hostname).toBe('ct-nginx');
22
+ expect(result.config.memory).toBe('512');
23
+ expect(result.config.cores).toBe('2');
24
+ });
25
+
26
+ it('parses rootfs storage and size', () => {
27
+ const result = parseLxcConfig(SAMPLE_OUTPUT, 200);
28
+ expect(result.rootfs).toEqual({ storage: 'local-lvm', size: '8G' });
29
+ });
30
+
31
+ it('parses mountpoints', () => {
32
+ const result = parseLxcConfig(SAMPLE_OUTPUT, 200);
33
+ expect(result.mountpoints).toHaveLength(1);
34
+ expect(result.mountpoints[0]).toEqual({
35
+ key: 'mp0',
36
+ storage: 'local-lvm',
37
+ volume: 'subvol-200-disk-1',
38
+ mountpoint: '/data',
39
+ size: '20G',
40
+ });
41
+ });
42
+
43
+ it('parses network interfaces', () => {
44
+ const result = parseLxcConfig(SAMPLE_OUTPUT, 200);
45
+ expect(result.network).toHaveLength(1);
46
+ expect(result.network[0]?.key).toBe('net0');
47
+ expect(result.network[0]?.raw).toContain('bridge=vmbr0');
48
+ });
49
+
50
+ it('handles no mountpoints', () => {
51
+ const output = `hostname: ct-simple
52
+ rootfs: local-lvm:subvol-300-disk-0,size=4G`;
53
+ const result = parseLxcConfig(output, 300);
54
+ expect(result.mountpoints).toHaveLength(0);
55
+ expect(result.rootfs).toEqual({ storage: 'local-lvm', size: '4G' });
56
+ });
57
+
58
+ it('handles missing rootfs', () => {
59
+ const output = 'hostname: ct-minimal';
60
+ const result = parseLxcConfig(output, 400);
61
+ expect(result.rootfs).toBeNull();
62
+ });
63
+
64
+ it('handles empty output', () => {
65
+ const result = parseLxcConfig('', 200);
66
+ expect(result.config).toEqual({});
67
+ expect(result.rootfs).toBeNull();
68
+ expect(result.mountpoints).toHaveLength(0);
69
+ });
70
+ });
71
+
72
+ describe('lxcConfig handler', () => {
73
+ it('calls pct config with vmid and returns parsed result', async () => {
74
+ const mockExec: ExecFn = async (cmd, args) => {
75
+ expect(cmd).toBe('pct');
76
+ expect(args).toEqual(['config', '200']);
77
+ return SAMPLE_OUTPUT;
78
+ };
79
+
80
+ const result = (await lxcConfig({ vmid: 200 }, mockExec)) as LxcConfigResult;
81
+ expect(result.vmid).toBe(200);
82
+ expect(result.rootfs?.storage).toBe('local-lvm');
83
+ });
84
+
85
+ it('throws when vmid is missing', async () => {
86
+ const mockExec: ExecFn = async () => '';
87
+ await expect(lxcConfig({}, mockExec)).rejects.toThrow('vmid parameter is required');
88
+ });
89
+ });
@@ -0,0 +1,90 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface LxcMountpoint {
4
+ key: string;
5
+ storage: string;
6
+ volume: string;
7
+ mountpoint: string;
8
+ size: string;
9
+ }
10
+
11
+ export interface LxcConfigResult {
12
+ vmid: number;
13
+ config: Record<string, string>;
14
+ rootfs: { storage: string; size: string } | null;
15
+ mountpoints: LxcMountpoint[];
16
+ network: Array<{ key: string; raw: string }>;
17
+ warnings: string[];
18
+ }
19
+
20
+ /**
21
+ * Runs `pct config {vmid}` and parses key: value output.
22
+ * Identifies rootfs storage, mountpoints, network config.
23
+ */
24
+ export const lxcConfig: ProbeHandler = async (params, exec) => {
25
+ const vmid = params?.vmid as number;
26
+ if (vmid == null) throw new Error('vmid parameter is required');
27
+
28
+ const stdout = await exec('pct', ['config', String(vmid)]);
29
+ return parseLxcConfig(stdout, vmid);
30
+ };
31
+
32
+ export function parseLxcConfig(stdout: string, vmid: number): LxcConfigResult {
33
+ const config: Record<string, string> = {};
34
+ const lines = stdout.trim().split('\n').filter(Boolean);
35
+
36
+ for (const line of lines) {
37
+ const colonIdx = line.indexOf(':');
38
+ if (colonIdx === -1) continue;
39
+ const key = line.slice(0, colonIdx).trim();
40
+ const value = line.slice(colonIdx + 1).trim();
41
+ config[key] = value;
42
+ }
43
+
44
+ // Parse rootfs: "local-lvm:subvol-200-disk-0,size=8G"
45
+ let rootfs: { storage: string; size: string } | null = null;
46
+ if (config.rootfs) {
47
+ const colonIdx = config.rootfs.indexOf(':');
48
+ if (colonIdx > -1) {
49
+ const storage = config.rootfs.slice(0, colonIdx);
50
+ const rest = config.rootfs.slice(colonIdx + 1);
51
+ const sizeMatch = rest.match(/size=(\S+)/);
52
+ rootfs = { storage, size: sizeMatch?.[1] ?? '' };
53
+ }
54
+ }
55
+
56
+ // Parse mp0–mp255 mountpoints
57
+ const mountpoints: LxcMountpoint[] = [];
58
+ for (const [key, value] of Object.entries(config)) {
59
+ if (!/^mp\d+$/.test(key)) continue;
60
+ const colonIdx = value.indexOf(':');
61
+ if (colonIdx === -1) continue;
62
+
63
+ const storage = value.slice(0, colonIdx);
64
+ const rest = value.slice(colonIdx + 1);
65
+ const commaIdx = rest.indexOf(',');
66
+ const volume = commaIdx > -1 ? rest.slice(0, commaIdx) : rest;
67
+ const optsPart = commaIdx > -1 ? rest.slice(commaIdx + 1) : '';
68
+
69
+ const mpMatch = optsPart.match(/mp=([^,]+)/);
70
+ const sizeMatch = optsPart.match(/size=(\S+)/);
71
+ mountpoints.push({
72
+ key,
73
+ storage,
74
+ volume,
75
+ mountpoint: mpMatch?.[1] ?? '',
76
+ size: sizeMatch?.[1] ?? '',
77
+ });
78
+ }
79
+
80
+ // Parse network interfaces
81
+ const network: Array<{ key: string; raw: string }> = [];
82
+ for (const [key, value] of Object.entries(config)) {
83
+ if (/^net\d+$/.test(key)) {
84
+ network.push({ key, raw: value });
85
+ }
86
+ }
87
+
88
+ const warnings: string[] = [];
89
+ return { vmid, config, rootfs, mountpoints, network, warnings };
90
+ }
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { ExecFn } from '../../types.js';
3
+ import type { LxcListResult } from './lxc-list.js';
4
+ import { lxcList, parseLxcList } from './lxc-list.js';
5
+
6
+ const SAMPLE_OUTPUT = `VMID Status Lock Name
7
+ 200 running ct-nginx
8
+ 201 stopped backup ct-db
9
+ 202 running ct-redis`;
10
+
11
+ describe('parseLxcList', () => {
12
+ it('parses container list output', () => {
13
+ const result = parseLxcList(SAMPLE_OUTPUT);
14
+ expect(result.containers).toHaveLength(3);
15
+ });
16
+
17
+ it('extracts VMID, status, and name', () => {
18
+ const result = parseLxcList(SAMPLE_OUTPUT);
19
+ expect(result.containers[0]).toEqual({
20
+ vmid: 200,
21
+ status: 'running',
22
+ lock: '',
23
+ name: 'ct-nginx',
24
+ });
25
+ });
26
+
27
+ it('extracts lock status when present', () => {
28
+ const result = parseLxcList(SAMPLE_OUTPUT);
29
+ expect(result.containers[1]).toEqual({
30
+ vmid: 201,
31
+ status: 'stopped',
32
+ lock: 'backup',
33
+ name: 'ct-db',
34
+ });
35
+ });
36
+
37
+ it('handles empty output', () => {
38
+ const result = parseLxcList('');
39
+ expect(result.containers).toHaveLength(0);
40
+ });
41
+
42
+ it('handles header-only output', () => {
43
+ const result = parseLxcList('VMID Status Lock Name');
44
+ expect(result.containers).toHaveLength(0);
45
+ });
46
+ });
47
+
48
+ describe('lxcList handler', () => {
49
+ it('calls pct list and returns parsed result', async () => {
50
+ const mockExec: ExecFn = async (cmd, args) => {
51
+ expect(cmd).toBe('pct');
52
+ expect(args).toEqual(['list']);
53
+ return SAMPLE_OUTPUT;
54
+ };
55
+
56
+ const result = (await lxcList(undefined, mockExec)) as LxcListResult;
57
+ expect(result.containers).toHaveLength(3);
58
+ expect(result.containers[0]?.vmid).toBe(200);
59
+ });
60
+ });
@@ -0,0 +1,67 @@
1
+ import type { ProbeHandler } from '../../types.js';
2
+
3
+ export interface LxcContainer {
4
+ vmid: number;
5
+ status: string;
6
+ lock: string;
7
+ name: string;
8
+ }
9
+
10
+ export interface LxcListResult {
11
+ containers: LxcContainer[];
12
+ }
13
+
14
+ /**
15
+ * Runs `pct list` and parses the tabular output.
16
+ * Format:
17
+ * VMID Status Lock Name
18
+ * 200 running ct-nginx
19
+ * 201 stopped backup ct-db
20
+ */
21
+ export const lxcList: ProbeHandler = async (_params, exec) => {
22
+ const stdout = await exec('pct', ['list']);
23
+ return parseLxcList(stdout);
24
+ };
25
+
26
+ export function parseLxcList(stdout: string): LxcListResult {
27
+ const lines = stdout.trim().split('\n').filter(Boolean);
28
+ const containers: LxcContainer[] = [];
29
+
30
+ // Skip header line
31
+ for (let i = 1; i < lines.length; i++) {
32
+ const line = lines[i];
33
+ if (!line) continue;
34
+ const parts = line.trim().split(/\s+/);
35
+
36
+ // "VMID Status Lock Name" — but Lock may be empty
37
+ // With lock: "200 running backup ct-nginx" (4 parts)
38
+ // Without lock: "200 running ct-nginx" (3 parts, lock column is blank)
39
+
40
+ // pct list uses fixed-width columns. Parse by position.
41
+ // VMID is first number, Status is second word.
42
+ // If there are 4+ parts and part[2] looks like a lock (not a hostname), it's a lock.
43
+ const vmid = Number.parseInt(parts[0] ?? '', 10);
44
+ if (Number.isNaN(vmid)) continue;
45
+
46
+ const status = parts[1] ?? '';
47
+
48
+ // Determine if part[2] is a lock or the name.
49
+ // Lock values are: backup, snapshot, suspended, migrate, etc.
50
+ // Names are typically longer/different. But the safest approach:
51
+ // If there are 4+ parts, the 3rd is lock and 4th+ is name.
52
+ // If there are 3 parts, lock is empty and 3rd is name.
53
+ let lock = '';
54
+ let name = '';
55
+
56
+ if (parts.length >= 4) {
57
+ lock = parts[2] ?? '';
58
+ name = parts.slice(3).join(' ');
59
+ } else if (parts.length === 3) {
60
+ name = parts[2] ?? '';
61
+ }
62
+
63
+ containers.push({ vmid, status, lock, name });
64
+ }
65
+
66
+ return { containers };
67
+ }