@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,1460 @@
1
+ import type {
2
+ FetchFn,
3
+ IntegrationConfig,
4
+ IntegrationCredentials,
5
+ IntegrationPack,
6
+ IntegrationProbeHandler,
7
+ } from '@sonde/shared';
8
+
9
+ // --- Auth helpers ---
10
+
11
+ /** Build auth headers: Basic (username:password) or X-Ntnx-Api-Key */
12
+ export function buildAuthHeaders(credentials: IntegrationCredentials): Record<string, string> {
13
+ if (credentials.authMethod === 'bearer_token') {
14
+ const key = credentials.credentials.nutanixApiKey ?? '';
15
+ return { 'X-Ntnx-Api-Key': key };
16
+ }
17
+
18
+ // basic auth (api_key method)
19
+ const { username, password } = credentials.credentials;
20
+ if (username && password) {
21
+ return { Authorization: `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}` };
22
+ }
23
+ return {};
24
+ }
25
+
26
+ // --- Nutanix REST helpers ---
27
+
28
+ /** Build a full Nutanix Prism Central v4 URL */
29
+ export function nutanixUrl(
30
+ endpoint: string,
31
+ namespace: string,
32
+ path: string,
33
+ params?: Record<string, string>,
34
+ ): string {
35
+ const base = `${endpoint.replace(/\/$/, '')}/api/${namespace}/v4.0/${path}`;
36
+ const url = new URL(base);
37
+ if (params) {
38
+ for (const [key, value] of Object.entries(params)) {
39
+ url.searchParams.set(key, value);
40
+ }
41
+ }
42
+ return url.toString();
43
+ }
44
+
45
+ /** GET a Nutanix v4 endpoint, unwrap response envelope, return { data, totalCount } */
46
+ export async function nutanixGet(
47
+ namespace: string,
48
+ path: string,
49
+ config: IntegrationConfig,
50
+ credentials: IntegrationCredentials,
51
+ fetchFn: FetchFn,
52
+ params?: Record<string, string>,
53
+ ): Promise<{ data: unknown; totalCount?: number }> {
54
+ const url = nutanixUrl(config.endpoint, namespace, path, params);
55
+ const headers: Record<string, string> = {
56
+ Accept: 'application/json',
57
+ ...buildAuthHeaders(credentials),
58
+ ...config.headers,
59
+ };
60
+
61
+ const res = await fetchFn(url, { headers });
62
+ if (!res.ok) throw new Error(`Nutanix API returned ${res.status}: ${res.statusText}`);
63
+
64
+ const body = (await res.json()) as {
65
+ data?: unknown;
66
+ metadata?: { totalAvailableResults?: number };
67
+ };
68
+
69
+ return {
70
+ data: body.data,
71
+ totalCount: body.metadata?.totalAvailableResults,
72
+ };
73
+ }
74
+
75
+ /** POST to a Nutanix endpoint (used for v3 category query fallback) */
76
+ export async function nutanixPost(
77
+ url: string,
78
+ body: unknown,
79
+ config: IntegrationConfig,
80
+ credentials: IntegrationCredentials,
81
+ fetchFn: FetchFn,
82
+ ): Promise<unknown> {
83
+ const headers: Record<string, string> = {
84
+ Accept: 'application/json',
85
+ 'Content-Type': 'application/json',
86
+ ...buildAuthHeaders(credentials),
87
+ ...config.headers,
88
+ };
89
+
90
+ const res = await fetchFn(url, {
91
+ method: 'POST',
92
+ headers,
93
+ body: JSON.stringify(body),
94
+ });
95
+ if (!res.ok) throw new Error(`Nutanix API returned ${res.status}: ${res.statusText}`);
96
+ return res.json();
97
+ }
98
+
99
+ // --- Unit conversions ---
100
+
101
+ /** Convert parts-per-million to percentage (2 decimal places) */
102
+ export function ppmToPercent(ppm: number): number {
103
+ return Math.round((ppm / 10000) * 100) / 100;
104
+ }
105
+
106
+ /** Convert microseconds to milliseconds (2 decimal places) */
107
+ export function usecsToMs(usecs: number): number {
108
+ return Math.round((usecs / 1000) * 100) / 100;
109
+ }
110
+
111
+ // --- Probe handlers ---
112
+
113
+ const clustersList: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
114
+ const name = params?.name as string | undefined;
115
+ const queryParams: Record<string, string> = {};
116
+ if (name) {
117
+ queryParams.$filter = `name eq '${name}'`;
118
+ }
119
+
120
+ const { data, totalCount } = await nutanixGet(
121
+ 'clustermgmt',
122
+ 'config/clusters',
123
+ config,
124
+ credentials,
125
+ fetchFn,
126
+ queryParams,
127
+ );
128
+
129
+ const items = (data as Array<Record<string, unknown>>) ?? [];
130
+ return {
131
+ clusters: items.map((c) => ({
132
+ name: c.name ?? null,
133
+ extId: c.extId ?? null,
134
+ hypervisorType:
135
+ ((c.config as Record<string, unknown>)?.hypervisorType as string) ??
136
+ (c.hypervisorType as string) ??
137
+ null,
138
+ aosVersion:
139
+ ((c.config as Record<string, unknown>)?.buildInfo as Record<string, unknown>)?.version ??
140
+ (c.aosVersion as string) ??
141
+ null,
142
+ numNodes:
143
+ ((c.nodes as Record<string, unknown>)?.numberOfNodes as number) ??
144
+ (c.numNodes as number) ??
145
+ null,
146
+ redundancyFactor:
147
+ ((c.config as Record<string, unknown>)?.redundancyFactor as number) ??
148
+ (c.redundancyFactor as number) ??
149
+ null,
150
+ operationMode: (c.operationMode as string) ?? null,
151
+ isDegraded:
152
+ (c.operationMode as string) !== undefined && (c.operationMode as string) !== 'NORMAL',
153
+ })),
154
+ totalCount: totalCount ?? items.length,
155
+ };
156
+ };
157
+
158
+ const hostsList: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
159
+ const clusterId = params?.cluster_id as string | undefined;
160
+ const queryParams: Record<string, string> = {};
161
+ if (clusterId) {
162
+ queryParams.$filter = `clusterExtId eq '${clusterId}'`;
163
+ }
164
+
165
+ const { data, totalCount } = await nutanixGet(
166
+ 'clustermgmt',
167
+ 'config/hosts',
168
+ config,
169
+ credentials,
170
+ fetchFn,
171
+ queryParams,
172
+ );
173
+
174
+ const items = (data as Array<Record<string, unknown>>) ?? [];
175
+ return {
176
+ hosts: items.map((h) => ({
177
+ name: (h.hostName as string) ?? (h.name as string) ?? null,
178
+ extId: h.extId ?? null,
179
+ serialNumber: (h.serialNumber as string) ?? null,
180
+ blockModel: (h.blockModel as string) ?? null,
181
+ hypervisorVersion:
182
+ ((h.hypervisor as Record<string, unknown>)?.fullName as string) ??
183
+ (h.hypervisorVersion as string) ??
184
+ null,
185
+ cpuModel: (h.cpuModel as string) ?? null,
186
+ numCpuSockets: (h.numCpuSockets as number) ?? null,
187
+ numCpuCores: (h.numCpuCores as number) ?? null,
188
+ memoryCapacityBytes: (h.memoryCapacityBytes as number) ?? null,
189
+ controllerVmIp:
190
+ ((h.controllerVm as Record<string, unknown>)?.ip as string) ??
191
+ (h.controllerVmIp as string) ??
192
+ null,
193
+ hypervisorIp:
194
+ ((h.hypervisor as Record<string, unknown>)?.ip as string) ??
195
+ (h.hypervisorIp as string) ??
196
+ null,
197
+ ipmiIp: ((h.ipmi as Record<string, unknown>)?.ip as string) ?? (h.ipmiIp as string) ?? null,
198
+ maintenanceMode: (h.maintenanceMode as boolean) ?? false,
199
+ })),
200
+ totalCount: totalCount ?? items.length,
201
+ };
202
+ };
203
+
204
+ const vmsList: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
205
+ const name = params?.name as string | undefined;
206
+ const powerState = params?.power_state as string | undefined;
207
+ const clusterId = params?.cluster_id as string | undefined;
208
+ const limit = (params?.limit as number) || 50;
209
+
210
+ const filters: string[] = [];
211
+ if (name) filters.push(`name eq '${name}'`);
212
+ if (powerState) filters.push(`powerState eq '${powerState}'`);
213
+ if (clusterId) filters.push(`clusterExtId eq '${clusterId}'`);
214
+
215
+ const queryParams: Record<string, string> = { $limit: String(limit) };
216
+ if (filters.length > 0) {
217
+ queryParams.$filter = filters.join(' and ');
218
+ }
219
+
220
+ const { data, totalCount } = await nutanixGet(
221
+ 'vmm',
222
+ 'ahv/config/vms',
223
+ config,
224
+ credentials,
225
+ fetchFn,
226
+ queryParams,
227
+ );
228
+
229
+ const items = (data as Array<Record<string, unknown>>) ?? [];
230
+ return {
231
+ vms: items.map((v) => ({
232
+ name: v.name ?? null,
233
+ extId: v.extId ?? null,
234
+ powerState: v.powerState ?? null,
235
+ numSockets: v.numSockets ?? null,
236
+ numCoresPerSocket: v.numCoresPerSocket ?? null,
237
+ memorySizeMb:
238
+ v.memorySizeBytes != null
239
+ ? Math.round((v.memorySizeBytes as number) / 1048576)
240
+ : ((v.memorySizeMb as number) ?? null),
241
+ clusterExtId: (v.cluster as Record<string, unknown>)?.extId ?? v.clusterExtId ?? null,
242
+ hostExtId: (v.host as Record<string, unknown>)?.extId ?? v.hostExtId ?? null,
243
+ description: v.description ?? null,
244
+ createTime: v.createTime ?? null,
245
+ })),
246
+ totalCount: totalCount ?? items.length,
247
+ };
248
+ };
249
+
250
+ const vmDetail: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
251
+ const vmId = params?.vm_id as string;
252
+ if (!vmId) throw new Error('vm_id parameter is required');
253
+
254
+ const { data } = await nutanixGet('vmm', `ahv/config/vms/${vmId}`, config, credentials, fetchFn);
255
+
256
+ const vm = data as Record<string, unknown>;
257
+
258
+ // Parse disks
259
+ const disks = ((vm.disks as Array<Record<string, unknown>>) ?? []).map((d) => {
260
+ const backing = (d.backingInfo as Record<string, unknown>) ?? {};
261
+ return {
262
+ diskAddress: d.diskAddress ?? null,
263
+ deviceType: (backing.deviceType as string) ?? (d.deviceType as string) ?? null,
264
+ storageContainerId: (backing.storageContainerId as string) ?? null,
265
+ sizeBytes: (backing.diskSizeBytes as number) ?? (backing.vmDiskSize as number) ?? null,
266
+ };
267
+ });
268
+
269
+ // Parse NICs
270
+ const nics = ((vm.nics as Array<Record<string, unknown>>) ?? []).map((n) => {
271
+ const network = (n.networkInfo as Record<string, unknown>) ?? {};
272
+ const subnetRef = (network.subnet as Record<string, unknown>) ?? {};
273
+ return {
274
+ macAddress: (n.macAddress as string) ?? (network.macAddress as string) ?? null,
275
+ subnetExtId: (subnetRef.extId as string) ?? (network.subnetExtId as string) ?? null,
276
+ nicType: (network.nicType as string) ?? (n.nicType as string) ?? null,
277
+ isConnected: (network.isConnected as boolean) ?? (n.isConnected as boolean) ?? null,
278
+ };
279
+ });
280
+
281
+ // Total allocated storage
282
+ const totalStorageBytes = disks.reduce((sum, d) => sum + ((d.sizeBytes as number) ?? 0), 0);
283
+
284
+ return {
285
+ name: vm.name ?? null,
286
+ extId: vm.extId ?? null,
287
+ powerState: vm.powerState ?? null,
288
+ numSockets: vm.numSockets ?? null,
289
+ numCoresPerSocket: vm.numCoresPerSocket ?? null,
290
+ memorySizeMb:
291
+ vm.memorySizeBytes != null
292
+ ? Math.round((vm.memorySizeBytes as number) / 1048576)
293
+ : ((vm.memorySizeMb as number) ?? null),
294
+ description: vm.description ?? null,
295
+ clusterExtId: (vm.cluster as Record<string, unknown>)?.extId ?? vm.clusterExtId ?? null,
296
+ hostExtId: (vm.host as Record<string, unknown>)?.extId ?? vm.hostExtId ?? null,
297
+ disks,
298
+ nics,
299
+ totalStorageBytes,
300
+ bootConfig: vm.bootConfig ?? null,
301
+ categories: vm.categories ?? null,
302
+ guestTools: vm.guestTools ?? null,
303
+ createTime: vm.createTime ?? null,
304
+ };
305
+ };
306
+
307
+ const vmStats: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
308
+ const vmId = params?.vm_id as string;
309
+ if (!vmId) throw new Error('vm_id parameter is required');
310
+
311
+ const { data } = await nutanixGet('vmm', `ahv/stats/vms/${vmId}`, config, credentials, fetchFn);
312
+
313
+ const stats = (data as Array<Record<string, unknown>>) ?? [];
314
+
315
+ const findStat = (metricType: string): number | null => {
316
+ const entry = stats.find(
317
+ (s) => s.metricType === metricType || (s.extId as string)?.includes(metricType.toLowerCase()),
318
+ );
319
+ return entry?.value != null ? (entry.value as number) : null;
320
+ };
321
+
322
+ const cpuPpm = findStat('CPU_USAGE_PPM') ?? findStat('hypervisor_cpu_usage_ppm');
323
+ const memPpm = findStat('MEMORY_USAGE_PPM') ?? findStat('memory_usage_ppm');
324
+ const iops = findStat('IOPS') ?? findStat('controller_num_iops');
325
+ const bwKbps = findStat('IO_BANDWIDTH_KBPS') ?? findStat('controller_io_bandwidth_kBps');
326
+ const latencyUsecs =
327
+ findStat('AVG_IO_LATENCY_USECS') ?? findStat('controller_avg_io_latency_usecs');
328
+ const rxBytes = findStat('NETWORK_RX_BYTES') ?? findStat('hypervisor_num_received_bytes');
329
+ const txBytes = findStat('NETWORK_TX_BYTES') ?? findStat('hypervisor_num_transmitted_bytes');
330
+
331
+ return {
332
+ cpuUsagePct: cpuPpm != null ? ppmToPercent(cpuPpm) : null,
333
+ memoryUsagePct: memPpm != null ? ppmToPercent(memPpm) : null,
334
+ iops: iops ?? null,
335
+ ioBandwidthKbps: bwKbps ?? null,
336
+ avgIoLatencyMs: latencyUsecs != null ? usecsToMs(latencyUsecs) : null,
337
+ networkRxBytes: rxBytes ?? null,
338
+ networkTxBytes: txBytes ?? null,
339
+ };
340
+ };
341
+
342
+ const alertsList: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
343
+ const severity = params?.severity as string | undefined;
344
+ const resolved = params?.resolved as boolean | undefined;
345
+ const hours = params?.hours as number | undefined;
346
+ const entityType = params?.entity_type as string | undefined;
347
+ const limit = (params?.limit as number) || 50;
348
+
349
+ const filters: string[] = [];
350
+ if (severity) filters.push(`severity eq '${severity}'`);
351
+ if (resolved === true) filters.push("resolvedStatus eq 'RESOLVED'");
352
+ if (resolved === false) filters.push("resolvedStatus eq 'UNRESOLVED'");
353
+ if (hours) {
354
+ const since = new Date(Date.now() - hours * 3600000).toISOString();
355
+ filters.push(`creationTime ge '${since}'`);
356
+ }
357
+ if (entityType) filters.push(`sourceEntity/type eq '${entityType}'`);
358
+
359
+ const queryParams: Record<string, string> = { $limit: String(limit) };
360
+ if (filters.length > 0) {
361
+ queryParams.$filter = filters.join(' and ');
362
+ }
363
+
364
+ const { data, totalCount } = await nutanixGet(
365
+ 'monitoring',
366
+ 'alerts',
367
+ config,
368
+ credentials,
369
+ fetchFn,
370
+ queryParams,
371
+ );
372
+
373
+ const items = (data as Array<Record<string, unknown>>) ?? [];
374
+ return {
375
+ alerts: items.map((a) => {
376
+ const source = (a.sourceEntity as Record<string, unknown>) ?? {};
377
+ return {
378
+ title: a.title ?? null,
379
+ severity: a.severity ?? null,
380
+ sourceEntity: {
381
+ type: source.type ?? null,
382
+ name: source.name ?? null,
383
+ extId: source.extId ?? null,
384
+ },
385
+ creationTime: a.creationTime ?? null,
386
+ description: a.description ?? null,
387
+ resolvedStatus: a.resolvedStatus ?? null,
388
+ impactType: a.impactType ?? null,
389
+ possibleCauses: a.possibleCauses ?? null,
390
+ resolutionSteps: a.resolutionSteps ?? null,
391
+ };
392
+ }),
393
+ totalCount: totalCount ?? items.length,
394
+ };
395
+ };
396
+
397
+ const alertsSummary: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
398
+ const queryParams: Record<string, string> = { $limit: '500' };
399
+
400
+ const { data } = await nutanixGet(
401
+ 'monitoring',
402
+ 'alerts',
403
+ config,
404
+ credentials,
405
+ fetchFn,
406
+ queryParams,
407
+ );
408
+
409
+ const items = (data as Array<Record<string, unknown>>) ?? [];
410
+
411
+ const bySeverity: Record<string, number> = { CRITICAL: 0, WARNING: 0, INFO: 0 };
412
+ const byEntityType: Record<string, number> = {};
413
+ const unresolvedCritical: Array<{
414
+ title: unknown;
415
+ sourceEntity: unknown;
416
+ creationTime: unknown;
417
+ }> = [];
418
+
419
+ for (const a of items) {
420
+ const sev = (a.severity as string) ?? 'INFO';
421
+ bySeverity[sev] = (bySeverity[sev] ?? 0) + 1;
422
+
423
+ const source = (a.sourceEntity as Record<string, unknown>) ?? {};
424
+ const entityType = (source.type as string) ?? 'Unknown';
425
+ byEntityType[entityType] = (byEntityType[entityType] ?? 0) + 1;
426
+
427
+ if (sev === 'CRITICAL' && a.resolvedStatus !== 'RESOLVED') {
428
+ unresolvedCritical.push({
429
+ title: a.title,
430
+ sourceEntity: { type: source.type, name: source.name, extId: source.extId },
431
+ creationTime: a.creationTime,
432
+ });
433
+ }
434
+ }
435
+
436
+ return {
437
+ bySeverity,
438
+ byEntityType,
439
+ unresolvedCritical,
440
+ totalCount: items.length,
441
+ };
442
+ };
443
+
444
+ const storageContainers: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
445
+ const clusterId = params?.cluster_id as string | undefined;
446
+ const queryParams: Record<string, string> = {};
447
+ if (clusterId) {
448
+ queryParams.$filter = `clusterExtId eq '${clusterId}'`;
449
+ }
450
+
451
+ const { data, totalCount } = await nutanixGet(
452
+ 'clustermgmt',
453
+ 'config/storage-containers',
454
+ config,
455
+ credentials,
456
+ fetchFn,
457
+ queryParams,
458
+ );
459
+
460
+ const items = (data as Array<Record<string, unknown>>) ?? [];
461
+ return {
462
+ containers: items.map((c) => {
463
+ const max = (c.maxCapacity as number) ?? (c.maxCapacityBytes as number) ?? 0;
464
+ const used = (c.usedCapacity as number) ?? (c.usedBytes as number) ?? 0;
465
+ const reserved = (c.reservedCapacity as number) ?? (c.reservedCapacityBytes as number) ?? 0;
466
+ const usedPct = max > 0 ? Math.round((used / max) * 10000) / 100 : 0;
467
+ const available = max - used;
468
+
469
+ return {
470
+ name: c.name ?? null,
471
+ extId: c.extId ?? null,
472
+ maxCapacityBytes: max,
473
+ usedBytes: used,
474
+ reservedCapacityBytes: reserved,
475
+ replicationFactor: c.replicationFactor ?? null,
476
+ compressionEnabled: c.compressionEnabled ?? false,
477
+ deduplicationEnabled:
478
+ (c.onDiskDedup as string) === 'POST_PROCESS' ||
479
+ (c.onDiskDedup as string) === 'INLINE' ||
480
+ ((c.deduplicationEnabled as boolean) ?? false),
481
+ erasureCodingEnabled: c.erasureCodingEnabled ?? false,
482
+ usedPct,
483
+ availableBytes: available,
484
+ highUsage: usedPct > 85,
485
+ };
486
+ }),
487
+ totalCount: totalCount ?? items.length,
488
+ };
489
+ };
490
+
491
+ const categoriesList: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
492
+ const key = params?.key as string | undefined;
493
+ const queryParams: Record<string, string> = {};
494
+ if (key) {
495
+ queryParams.$filter = `key eq '${key}'`;
496
+ }
497
+
498
+ const { data, totalCount } = await nutanixGet(
499
+ 'prism',
500
+ 'config/categories',
501
+ config,
502
+ credentials,
503
+ fetchFn,
504
+ queryParams,
505
+ );
506
+
507
+ const items = (data as Array<Record<string, unknown>>) ?? [];
508
+ return {
509
+ categories: items.map((c) => ({
510
+ key: c.key ?? null,
511
+ value: c.value ?? null,
512
+ description: c.description ?? null,
513
+ type: c.type ?? null,
514
+ })),
515
+ totalCount: totalCount ?? items.length,
516
+ };
517
+ };
518
+
519
+ const categoriesEntities: IntegrationProbeHandler = async (
520
+ params,
521
+ config,
522
+ credentials,
523
+ fetchFn,
524
+ ) => {
525
+ const key = params?.key as string;
526
+ const value = params?.value as string;
527
+ if (!key) throw new Error('key parameter is required');
528
+ if (!value) throw new Error('value parameter is required');
529
+
530
+ const url = `${config.endpoint.replace(/\/$/, '')}/api/nutanix/v3/category/query`;
531
+ const body = {
532
+ usage_type: 'APPLIED_TO',
533
+ category_filter: {
534
+ type: 'CATEGORIES_MATCH_ANY',
535
+ params: { [key]: [value] },
536
+ },
537
+ };
538
+
539
+ const result = (await nutanixPost(url, body, config, credentials, fetchFn)) as {
540
+ results?: Array<{
541
+ kind?: string;
542
+ kind_reference_list?: Array<{
543
+ kind?: string;
544
+ uuid?: string;
545
+ name?: string;
546
+ }>;
547
+ }>;
548
+ };
549
+
550
+ const entities: Array<{ entityType: string; entityId: string; entityName: string | null }> = [];
551
+ for (const group of result.results ?? []) {
552
+ const entityType = group.kind ?? 'unknown';
553
+ for (const ref of group.kind_reference_list ?? []) {
554
+ entities.push({
555
+ entityType,
556
+ entityId: ref.uuid ?? '',
557
+ entityName: ref.name ?? null,
558
+ });
559
+ }
560
+ }
561
+
562
+ return {
563
+ entities,
564
+ totalCount: entities.length,
565
+ };
566
+ };
567
+
568
+ const networksList: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
569
+ const clusterId = params?.cluster_id as string | undefined;
570
+ const queryParams: Record<string, string> = {};
571
+ if (clusterId) {
572
+ queryParams.$filter = `clusterExtId eq '${clusterId}'`;
573
+ }
574
+
575
+ const { data, totalCount } = await nutanixGet(
576
+ 'networking',
577
+ 'config/subnets',
578
+ config,
579
+ credentials,
580
+ fetchFn,
581
+ queryParams,
582
+ );
583
+
584
+ const items = (data as Array<Record<string, unknown>>) ?? [];
585
+ return {
586
+ subnets: items.map((s) => {
587
+ const ipConfig = (s.ipConfig as Record<string, unknown>) ?? {};
588
+ const pool = ((ipConfig.ipv4Config as Record<string, unknown>) ?? ipConfig) as Record<
589
+ string,
590
+ unknown
591
+ >;
592
+ return {
593
+ name: s.name ?? null,
594
+ type: s.subnetType ?? s.type ?? null,
595
+ vlanId: s.vlanId ?? null,
596
+ networkIp: (pool.networkIp as string) ?? (pool.subnetIp as string) ?? null,
597
+ prefixLength: (pool.prefixLength as number) ?? null,
598
+ dhcpEnabled:
599
+ (pool.dhcpServerAddress as unknown) != null || ((s.dhcpEnabled as boolean) ?? false),
600
+ vpcRef: (s.vpcReference as Record<string, unknown>)?.extId ?? s.vpcRef ?? null,
601
+ clusterExtId:
602
+ (s.clusterReference as string) ??
603
+ (s.cluster as Record<string, unknown>)?.extId ??
604
+ s.clusterExtId ??
605
+ null,
606
+ };
607
+ }),
608
+ totalCount: totalCount ?? items.length,
609
+ };
610
+ };
611
+
612
+ const tasksRecent: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
613
+ const hours = (params?.hours as number) || 24;
614
+ const status = params?.status as string | undefined;
615
+ const limit = (params?.limit as number) || 50;
616
+
617
+ const since = new Date(Date.now() - hours * 3600000).toISOString();
618
+ const filters: string[] = [`startTime ge '${since}'`];
619
+ if (status) filters.push(`status eq '${status}'`);
620
+
621
+ const queryParams: Record<string, string> = {
622
+ $limit: String(limit),
623
+ $filter: filters.join(' and '),
624
+ };
625
+
626
+ const { data, totalCount } = await nutanixGet(
627
+ 'prism',
628
+ 'config/tasks',
629
+ config,
630
+ credentials,
631
+ fetchFn,
632
+ queryParams,
633
+ );
634
+
635
+ const items = (data as Array<Record<string, unknown>>) ?? [];
636
+ const now = Date.now();
637
+ const oneHourMs = 3600000;
638
+
639
+ return {
640
+ tasks: items.map((t) => {
641
+ const startTime = t.startTime as string | null;
642
+ const endTime = (t.completedTime as string) ?? (t.endTime as string) ?? null;
643
+ const isFailed = (t.status as string) === 'FAILED';
644
+ const isLongRunning =
645
+ !endTime && startTime && now - new Date(startTime).getTime() > oneHourMs;
646
+
647
+ return {
648
+ type: (t.operationType as string) ?? (t.type as string) ?? null,
649
+ status: t.status ?? null,
650
+ entityRef: t.entityReference ?? t.entityRef ?? null,
651
+ startTime: startTime ?? null,
652
+ endTime,
653
+ errorMessage:
654
+ (t.errorMessages as Array<Record<string, unknown>>)?.[0]?.message ??
655
+ (t.errorMessage as string) ??
656
+ null,
657
+ progressPct: (t.progressPercentage as number) ?? (t.progressPct as number) ?? null,
658
+ isFailed,
659
+ isLongRunning: !!isLongRunning,
660
+ };
661
+ }),
662
+ totalCount: totalCount ?? items.length,
663
+ };
664
+ };
665
+
666
+ const clusterHealth: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
667
+ const clusterId = params?.cluster_id as string | undefined;
668
+
669
+ // Fetch cluster info
670
+ let clusterInfo: Record<string, unknown> | null = null;
671
+ if (clusterId) {
672
+ const { data } = await nutanixGet(
673
+ 'clustermgmt',
674
+ `config/clusters/${clusterId}`,
675
+ config,
676
+ credentials,
677
+ fetchFn,
678
+ );
679
+ clusterInfo = data as Record<string, unknown>;
680
+ } else {
681
+ const { data } = await nutanixGet(
682
+ 'clustermgmt',
683
+ 'config/clusters',
684
+ config,
685
+ credentials,
686
+ fetchFn,
687
+ { $limit: '1' },
688
+ );
689
+ const clusters = (data as Array<Record<string, unknown>>) ?? [];
690
+ clusterInfo = clusters[0] ?? null;
691
+ }
692
+
693
+ const cExtId = (clusterInfo?.extId as string) ?? clusterId;
694
+
695
+ // Parallel fetches: hosts, critical alerts, storage
696
+ const hostsFilter: Record<string, string> = {};
697
+ if (cExtId) hostsFilter.$filter = `clusterExtId eq '${cExtId}'`;
698
+
699
+ const storageFilter: Record<string, string> = {};
700
+ if (cExtId) storageFilter.$filter = `clusterExtId eq '${cExtId}'`;
701
+
702
+ const [hostsResult, alertsResult, storageResult] = await Promise.all([
703
+ nutanixGet('clustermgmt', 'config/hosts', config, credentials, fetchFn, hostsFilter),
704
+ nutanixGet('monitoring', 'alerts', config, credentials, fetchFn, {
705
+ $filter: "severity eq 'CRITICAL' and resolvedStatus eq 'UNRESOLVED'",
706
+ $limit: '100',
707
+ }),
708
+ nutanixGet(
709
+ 'clustermgmt',
710
+ 'config/storage-containers',
711
+ config,
712
+ credentials,
713
+ fetchFn,
714
+ storageFilter,
715
+ ),
716
+ ]);
717
+
718
+ const hosts = (hostsResult.data as Array<Record<string, unknown>>) ?? [];
719
+ const alerts = (alertsResult.data as Array<Record<string, unknown>>) ?? [];
720
+ const containers = (storageResult.data as Array<Record<string, unknown>>) ?? [];
721
+
722
+ const degradedNodes = hosts.filter(
723
+ (h) => (h.maintenanceMode as boolean) === true || (h.status as string) === 'DEGRADED',
724
+ );
725
+
726
+ const storageContainersInfo = containers.map((c) => {
727
+ const max = (c.maxCapacity as number) ?? (c.maxCapacityBytes as number) ?? 0;
728
+ const used = (c.usedCapacity as number) ?? (c.usedBytes as number) ?? 0;
729
+ return {
730
+ name: c.name,
731
+ usedPct: max > 0 ? Math.round((used / max) * 10000) / 100 : 0,
732
+ };
733
+ });
734
+
735
+ const issues: string[] = [];
736
+ if (degradedNodes.length > 0) {
737
+ issues.push(`${degradedNodes.length} node(s) degraded or in maintenance`);
738
+ }
739
+ if (alerts.length > 0) {
740
+ issues.push(`${alerts.length} unresolved critical alert(s)`);
741
+ }
742
+ for (const sc of storageContainersInfo) {
743
+ if (sc.usedPct > 85) {
744
+ issues.push(`Storage container ${sc.name} at ${sc.usedPct}% capacity`);
745
+ }
746
+ }
747
+
748
+ let healthAssessment: string;
749
+ if (alerts.length > 0 || degradedNodes.length > 0) {
750
+ healthAssessment = 'CRITICAL';
751
+ } else if (storageContainersInfo.some((sc) => sc.usedPct > 85)) {
752
+ healthAssessment = 'WARNING';
753
+ } else {
754
+ healthAssessment = 'HEALTHY';
755
+ }
756
+
757
+ return {
758
+ cluster: clusterInfo
759
+ ? {
760
+ name: clusterInfo.name,
761
+ extId: clusterInfo.extId,
762
+ operationMode: clusterInfo.operationMode,
763
+ }
764
+ : null,
765
+ nodeCount: hosts.length,
766
+ degradedNodes: degradedNodes.map((h) => ({
767
+ name: h.hostName ?? h.name,
768
+ extId: h.extId,
769
+ maintenanceMode: h.maintenanceMode,
770
+ })),
771
+ criticalAlerts: alerts.map((a) => ({
772
+ title: a.title,
773
+ sourceEntity: a.sourceEntity,
774
+ creationTime: a.creationTime,
775
+ })),
776
+ storageContainers: storageContainersInfo,
777
+ healthAssessment,
778
+ issues,
779
+ };
780
+ };
781
+
782
+ const vmSnapshots: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
783
+ const vmId = params?.vm_id as string;
784
+ if (!vmId) throw new Error('vm_id parameter is required');
785
+
786
+ // Try v4 dataprotection API first
787
+ let items: Array<Record<string, unknown>> = [];
788
+ let usedV3 = false;
789
+
790
+ try {
791
+ const { data } = await nutanixGet(
792
+ 'dataprotection',
793
+ 'config/recovery-points',
794
+ config,
795
+ credentials,
796
+ fetchFn,
797
+ { $filter: `vmExtId eq '${vmId}'` },
798
+ );
799
+ items = (data as Array<Record<string, unknown>>) ?? [];
800
+ } catch {
801
+ // Fall back to v3
802
+ const url = `${config.endpoint.replace(/\/$/, '')}/api/nutanix/v3/vm_recovery_points/list`;
803
+ const result = (await nutanixPost(
804
+ url,
805
+ { filter: `vm_uuid==${vmId}`, length: 100 },
806
+ config,
807
+ credentials,
808
+ fetchFn,
809
+ )) as { entities?: Array<Record<string, unknown>> };
810
+ items = result.entities ?? [];
811
+ usedV3 = true;
812
+ }
813
+
814
+ const now = Date.now();
815
+ const sevenDaysMs = 7 * 24 * 3600000;
816
+
817
+ const snapshots = items.map((rp) => {
818
+ const name =
819
+ (rp.name as string) ??
820
+ (rp.status as Record<string, unknown>)?.name ??
821
+ (rp.extId as string) ??
822
+ null;
823
+ const creationTime =
824
+ (rp.creationTime as string) ?? (rp.status as Record<string, unknown>)?.creation_time ?? null;
825
+ const expirationTime =
826
+ (rp.expirationTime as string) ??
827
+ (rp.status as Record<string, unknown>)?.expiration_time ??
828
+ null;
829
+ const consistencyType =
830
+ (rp.recoveryPointType as string) ??
831
+ (rp.status as Record<string, unknown>)?.recovery_point_type ??
832
+ null;
833
+ const sizeBytes = (rp.sizeBytes as number) ?? (rp.diskSizeBytes as number) ?? null;
834
+
835
+ const createdMs = creationTime ? new Date(creationTime as string).getTime() : null;
836
+ const ageMs = createdMs ? now - createdMs : null;
837
+ const isOld = ageMs != null && ageMs > sevenDaysMs;
838
+
839
+ const expirationMs = expirationTime ? new Date(expirationTime as string).getTime() : null;
840
+ const isExpired = expirationMs != null && expirationMs < now;
841
+
842
+ return {
843
+ name,
844
+ extId: rp.extId ?? null,
845
+ creationTime,
846
+ expirationTime,
847
+ consistencyType,
848
+ sizeBytes,
849
+ ageDays: ageMs != null ? Math.round((ageMs / 86400000) * 10) / 10 : null,
850
+ isOld,
851
+ isExpired,
852
+ };
853
+ });
854
+
855
+ const warnings: string[] = [];
856
+ const oldSnapshots = snapshots.filter((s) => s.isOld);
857
+ const expiredSnapshots = snapshots.filter((s) => s.isExpired);
858
+ if (oldSnapshots.length > 0) {
859
+ warnings.push(`${oldSnapshots.length} snapshot(s) older than 7 days`);
860
+ }
861
+ if (expiredSnapshots.length > 0) {
862
+ warnings.push(`${expiredSnapshots.length} expired snapshot(s) not cleaned up`);
863
+ }
864
+
865
+ return { snapshots, totalCount: snapshots.length, usedV3, warnings };
866
+ };
867
+
868
+ const protectionPolicies: IntegrationProbeHandler = async (
869
+ params,
870
+ config,
871
+ credentials,
872
+ fetchFn,
873
+ ) => {
874
+ const vmId = params?.vm_id as string | undefined;
875
+
876
+ const { data, totalCount } = await nutanixGet(
877
+ 'dataprotection',
878
+ 'config/protection-policies',
879
+ config,
880
+ credentials,
881
+ fetchFn,
882
+ );
883
+
884
+ const items = (data as Array<Record<string, unknown>>) ?? [];
885
+
886
+ const policies = items.map((p) => {
887
+ const schedules = (p.schedules as Array<Record<string, unknown>>) ?? [];
888
+ const primarySchedule = schedules[0];
889
+ const rpo = primarySchedule
890
+ ? {
891
+ value: primarySchedule.recoveryPointObjective ?? primarySchedule.rpoInMinutes ?? null,
892
+ unit: (primarySchedule.rpoUnit as string) ?? 'MINUTES',
893
+ }
894
+ : null;
895
+
896
+ const retention = primarySchedule
897
+ ? {
898
+ local: (primarySchedule.localRetentionCount as number) ?? null,
899
+ remote: (primarySchedule.remoteRetentionCount as number) ?? null,
900
+ }
901
+ : null;
902
+
903
+ const protectedEntities =
904
+ (p.protectedEntities as Array<Record<string, unknown>>) ??
905
+ (p.entityReferences as Array<Record<string, unknown>>) ??
906
+ [];
907
+
908
+ return {
909
+ name: p.name ?? null,
910
+ extId: p.extId ?? null,
911
+ description: p.description ?? null,
912
+ rpo,
913
+ retention,
914
+ remoteSite: (p.remoteSiteReference as Record<string, unknown>)?.name ?? p.remoteSite ?? null,
915
+ protectedEntityCount: protectedEntities.length,
916
+ protectedEntityIds: protectedEntities.map(
917
+ (e) => (e.extId as string) ?? (e.entityId as string) ?? '',
918
+ ),
919
+ lastSuccessfulReplication: p.lastSuccessfulReplicationTime ?? null,
920
+ };
921
+ });
922
+
923
+ if (vmId) {
924
+ const covering = policies.filter((p) => p.protectedEntityIds.includes(vmId));
925
+ return {
926
+ policies: covering,
927
+ totalCount: covering.length,
928
+ vmCovered: covering.length > 0,
929
+ allPoliciesCount: policies.length,
930
+ };
931
+ }
932
+
933
+ return { policies, totalCount: totalCount ?? policies.length };
934
+ };
935
+
936
+ const lifecycleStatus: IntegrationProbeHandler = async (_params, config, credentials, fetchFn) => {
937
+ const { data, totalCount } = await nutanixGet(
938
+ 'lifecycle',
939
+ 'resources/entities',
940
+ config,
941
+ credentials,
942
+ fetchFn,
943
+ );
944
+
945
+ const items = (data as Array<Record<string, unknown>>) ?? [];
946
+
947
+ const entities = items.map((e) => {
948
+ const availableVersion =
949
+ (e.availableVersion as Record<string, unknown>)?.version ??
950
+ (e.availableVersion as string) ??
951
+ null;
952
+ const currentVersion =
953
+ (e.installedVersion as Record<string, unknown>)?.version ??
954
+ (e.currentVersion as string) ??
955
+ null;
956
+ return {
957
+ entityType: (e.entityType as string) ?? (e.entityModel as string) ?? null,
958
+ name: e.name ?? null,
959
+ extId: e.extId ?? null,
960
+ currentVersion,
961
+ availableVersion,
962
+ updateStatus: (e.updateStatus as string) ?? null,
963
+ hasUpdate: availableVersion != null && availableVersion !== currentVersion,
964
+ };
965
+ });
966
+
967
+ const warnings: string[] = [];
968
+ const updatable = entities.filter((e) => e.hasUpdate);
969
+ if (updatable.length > 0) {
970
+ warnings.push(`${updatable.length} component(s) have available updates`);
971
+ for (const e of updatable) {
972
+ warnings.push(`${e.entityType ?? e.name}: ${e.currentVersion} → ${e.availableVersion}`);
973
+ }
974
+ }
975
+
976
+ return {
977
+ entities,
978
+ updatableCount: updatable.length,
979
+ totalCount: totalCount ?? entities.length,
980
+ warnings,
981
+ };
982
+ };
983
+
984
+ const hostStats: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
985
+ const hostId = params?.host_id as string;
986
+ if (!hostId) throw new Error('host_id parameter is required');
987
+
988
+ const { data } = await nutanixGet(
989
+ 'clustermgmt',
990
+ `stats/hosts/${hostId}`,
991
+ config,
992
+ credentials,
993
+ fetchFn,
994
+ );
995
+
996
+ const stats = (data as Array<Record<string, unknown>>) ?? [];
997
+
998
+ const findStat = (metricType: string): number | null => {
999
+ const entry = stats.find(
1000
+ (s) => s.metricType === metricType || (s.extId as string)?.includes(metricType.toLowerCase()),
1001
+ );
1002
+ return entry?.value != null ? (entry.value as number) : null;
1003
+ };
1004
+
1005
+ const cpuPpm = findStat('HYPERVISOR_CPU_USAGE_PPM') ?? findStat('hypervisor_cpu_usage_ppm');
1006
+ const memPpm = findStat('HYPERVISOR_MEMORY_USAGE_PPM') ?? findStat('hypervisor_memory_usage_ppm');
1007
+ const iops = findStat('IOPS') ?? findStat('controller_num_iops');
1008
+ const bwKbps = findStat('IO_BANDWIDTH_KBPS') ?? findStat('controller_io_bandwidth_kBps');
1009
+ const networkRx = findStat('NETWORK_RX_BYTES') ?? findStat('hypervisor_num_received_bytes');
1010
+ const networkTx = findStat('NETWORK_TX_BYTES') ?? findStat('hypervisor_num_transmitted_bytes');
1011
+ const uptimeUsecs = findStat('HYPERVISOR_UPTIME_USECS') ?? findStat('hypervisor_uptime_usecs');
1012
+
1013
+ const cpuPct = cpuPpm != null ? ppmToPercent(cpuPpm) : null;
1014
+ const memPct = memPpm != null ? ppmToPercent(memPpm) : null;
1015
+
1016
+ const warnings: string[] = [];
1017
+ if (cpuPct != null && cpuPct > 85) {
1018
+ warnings.push(`Host CPU at ${cpuPct}%`);
1019
+ }
1020
+ if (memPct != null && memPct > 90) {
1021
+ warnings.push(`Host memory at ${memPct}%`);
1022
+ }
1023
+
1024
+ return {
1025
+ cpuUsagePct: cpuPct,
1026
+ memoryUsagePct: memPct,
1027
+ iops: iops ?? null,
1028
+ ioBandwidthKbps: bwKbps ?? null,
1029
+ networkRxBytes: networkRx ?? null,
1030
+ networkTxBytes: networkTx ?? null,
1031
+ hypervisorUptimeUsecs: uptimeUsecs ?? null,
1032
+ warnings,
1033
+ };
1034
+ };
1035
+
1036
+ const clusterStats: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
1037
+ const clusterId = params?.cluster_id as string;
1038
+ if (!clusterId) throw new Error('cluster_id parameter is required');
1039
+
1040
+ const { data } = await nutanixGet(
1041
+ 'clustermgmt',
1042
+ `stats/clusters/${clusterId}`,
1043
+ config,
1044
+ credentials,
1045
+ fetchFn,
1046
+ );
1047
+
1048
+ const stats = (data as Array<Record<string, unknown>>) ?? [];
1049
+
1050
+ const findStat = (metricType: string): number | null => {
1051
+ const entry = stats.find(
1052
+ (s) => s.metricType === metricType || (s.extId as string)?.includes(metricType.toLowerCase()),
1053
+ );
1054
+ return entry?.value != null ? (entry.value as number) : null;
1055
+ };
1056
+
1057
+ const cpuCapacityHz = findStat('CPU_CAPACITY_HZ') ?? findStat('cpu_capacity_hz');
1058
+ const cpuUsedHz = findStat('CPU_USAGE_HZ') ?? findStat('hypervisor_cpu_usage_hz');
1059
+ const memCapacity = findStat('MEMORY_CAPACITY_BYTES') ?? findStat('memory_capacity_bytes');
1060
+ const memUsed = findStat('MEMORY_USAGE_BYTES') ?? findStat('hypervisor_memory_usage_bytes');
1061
+ const storageCapacity = findStat('STORAGE_CAPACITY_BYTES') ?? findStat('storage_capacity_bytes');
1062
+ const storageUsed = findStat('STORAGE_USAGE_BYTES') ?? findStat('storage_usage_bytes');
1063
+ const iops = findStat('IOPS') ?? findStat('controller_num_iops');
1064
+ const latencyUsecs =
1065
+ findStat('AVG_IO_LATENCY_USECS') ?? findStat('controller_avg_io_latency_usecs');
1066
+
1067
+ const cpuPct =
1068
+ cpuCapacityHz && cpuUsedHz && cpuCapacityHz > 0
1069
+ ? Math.round((cpuUsedHz / cpuCapacityHz) * 10000) / 100
1070
+ : null;
1071
+ const memPct =
1072
+ memCapacity && memUsed && memCapacity > 0
1073
+ ? Math.round((memUsed / memCapacity) * 10000) / 100
1074
+ : null;
1075
+ const storagePct =
1076
+ storageCapacity && storageUsed && storageCapacity > 0
1077
+ ? Math.round((storageUsed / storageCapacity) * 10000) / 100
1078
+ : null;
1079
+
1080
+ return {
1081
+ cpuCapacityHz,
1082
+ cpuUsedHz,
1083
+ cpuUsagePct: cpuPct,
1084
+ memoryCapacityBytes: memCapacity,
1085
+ memoryUsedBytes: memUsed,
1086
+ memoryUsagePct: memPct,
1087
+ storageCapacityBytes: storageCapacity,
1088
+ storageUsedBytes: storageUsed,
1089
+ storageUsagePct: storagePct,
1090
+ iops: iops ?? null,
1091
+ avgIoLatencyMs: latencyUsecs != null ? usecsToMs(latencyUsecs) : null,
1092
+ };
1093
+ };
1094
+
1095
+ const imagesList: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
1096
+ const name = params?.name as string | undefined;
1097
+ const queryParams: Record<string, string> = {};
1098
+ if (name) {
1099
+ queryParams.$filter = `name eq '${name}'`;
1100
+ }
1101
+
1102
+ const { data, totalCount } = await nutanixGet(
1103
+ 'vmm',
1104
+ 'content/images',
1105
+ config,
1106
+ credentials,
1107
+ fetchFn,
1108
+ queryParams,
1109
+ );
1110
+
1111
+ const items = (data as Array<Record<string, unknown>>) ?? [];
1112
+ return {
1113
+ images: items.map((img) => ({
1114
+ name: img.name ?? null,
1115
+ extId: img.extId ?? null,
1116
+ type: (img.type as string) ?? (img.imageType as string) ?? null,
1117
+ sizeBytes: (img.sizeBytes as number) ?? (img.size as number) ?? null,
1118
+ sourceClusterId:
1119
+ (img.source as Record<string, unknown>)?.clusterExtId ?? img.sourceClusterId ?? null,
1120
+ description: img.description ?? null,
1121
+ createTime: img.createTime ?? null,
1122
+ })),
1123
+ totalCount: totalCount ?? items.length,
1124
+ };
1125
+ };
1126
+
1127
+ const vmsByHost: IntegrationProbeHandler = async (params, config, credentials, fetchFn) => {
1128
+ const hostId = params?.host_id as string;
1129
+ if (!hostId) throw new Error('host_id parameter is required');
1130
+
1131
+ const { data, totalCount } = await nutanixGet(
1132
+ 'vmm',
1133
+ 'ahv/config/vms',
1134
+ config,
1135
+ credentials,
1136
+ fetchFn,
1137
+ { $filter: `hostExtId eq '${hostId}'` },
1138
+ );
1139
+
1140
+ const items = (data as Array<Record<string, unknown>>) ?? [];
1141
+ return {
1142
+ vms: items.map((v) => ({
1143
+ name: v.name ?? null,
1144
+ extId: v.extId ?? null,
1145
+ powerState: v.powerState ?? null,
1146
+ numSockets: v.numSockets ?? null,
1147
+ numCoresPerSocket: v.numCoresPerSocket ?? null,
1148
+ memorySizeMb:
1149
+ v.memorySizeBytes != null
1150
+ ? Math.round((v.memorySizeBytes as number) / 1048576)
1151
+ : ((v.memorySizeMb as number) ?? null),
1152
+ description: v.description ?? null,
1153
+ })),
1154
+ totalCount: totalCount ?? items.length,
1155
+ hostId,
1156
+ };
1157
+ };
1158
+
1159
+ // --- Pack definition ---
1160
+
1161
+ export const nutanixPack: IntegrationPack = {
1162
+ manifest: {
1163
+ name: 'nutanix',
1164
+ type: 'integration',
1165
+ version: '0.1.0',
1166
+ description:
1167
+ 'Nutanix Prism Central — clusters, VMs, hosts, alerts, storage, categories, networks, and tasks via v4 REST API',
1168
+ requires: { groups: [], files: [], commands: [] },
1169
+ probes: [
1170
+ {
1171
+ name: 'clusters.list',
1172
+ description: 'List Nutanix clusters with health and configuration',
1173
+ capability: 'observe',
1174
+ params: {
1175
+ name: { type: 'string', description: 'Filter by cluster name', required: false },
1176
+ },
1177
+ timeout: 15000,
1178
+ },
1179
+ {
1180
+ name: 'hosts.list',
1181
+ description: 'List hypervisor hosts with hardware and network details',
1182
+ capability: 'observe',
1183
+ params: {
1184
+ cluster_id: {
1185
+ type: 'string',
1186
+ description: 'Filter by cluster extId',
1187
+ required: false,
1188
+ },
1189
+ },
1190
+ timeout: 15000,
1191
+ },
1192
+ {
1193
+ name: 'vms.list',
1194
+ description: 'List AHV virtual machines with filtering',
1195
+ capability: 'observe',
1196
+ params: {
1197
+ name: { type: 'string', description: 'Filter by VM name', required: false },
1198
+ power_state: {
1199
+ type: 'string',
1200
+ description: 'Filter by power state (ON/OFF)',
1201
+ required: false,
1202
+ },
1203
+ cluster_id: {
1204
+ type: 'string',
1205
+ description: 'Filter by cluster extId',
1206
+ required: false,
1207
+ },
1208
+ limit: { type: 'number', description: 'Max results (default: 50)', required: false },
1209
+ },
1210
+ timeout: 15000,
1211
+ },
1212
+ {
1213
+ name: 'vm.detail',
1214
+ description: 'Full VM configuration with disks, NICs, boot config, and categories',
1215
+ capability: 'observe',
1216
+ params: {
1217
+ vm_id: { type: 'string', description: 'VM extId', required: true },
1218
+ },
1219
+ timeout: 15000,
1220
+ },
1221
+ {
1222
+ name: 'vm.stats',
1223
+ description: 'VM performance stats — CPU, memory, IOPS, latency, network I/O',
1224
+ capability: 'observe',
1225
+ params: {
1226
+ vm_id: { type: 'string', description: 'VM extId', required: true },
1227
+ },
1228
+ timeout: 30000,
1229
+ },
1230
+ {
1231
+ name: 'alerts.list',
1232
+ description: 'List alerts with severity, time range, and entity type filters',
1233
+ capability: 'observe',
1234
+ params: {
1235
+ severity: {
1236
+ type: 'string',
1237
+ description: 'Filter by severity (CRITICAL/WARNING/INFO)',
1238
+ required: false,
1239
+ },
1240
+ resolved: {
1241
+ type: 'boolean',
1242
+ description: 'Filter by resolution status',
1243
+ required: false,
1244
+ },
1245
+ hours: {
1246
+ type: 'number',
1247
+ description: 'Only alerts from the last N hours',
1248
+ required: false,
1249
+ },
1250
+ entity_type: {
1251
+ type: 'string',
1252
+ description: 'Filter by source entity type',
1253
+ required: false,
1254
+ },
1255
+ limit: { type: 'number', description: 'Max results (default: 50)', required: false },
1256
+ },
1257
+ timeout: 15000,
1258
+ },
1259
+ {
1260
+ name: 'alerts.summary',
1261
+ description: 'Alert summary grouped by severity and entity type',
1262
+ capability: 'observe',
1263
+ params: {},
1264
+ timeout: 15000,
1265
+ },
1266
+ {
1267
+ name: 'storage.containers',
1268
+ description: 'Storage containers with capacity, usage, and data services status',
1269
+ capability: 'observe',
1270
+ params: {
1271
+ cluster_id: {
1272
+ type: 'string',
1273
+ description: 'Filter by cluster extId',
1274
+ required: false,
1275
+ },
1276
+ },
1277
+ timeout: 15000,
1278
+ },
1279
+ {
1280
+ name: 'categories.list',
1281
+ description: 'List Prism categories',
1282
+ capability: 'observe',
1283
+ params: {
1284
+ key: { type: 'string', description: 'Filter by category key', required: false },
1285
+ },
1286
+ timeout: 15000,
1287
+ },
1288
+ {
1289
+ name: 'categories.entities',
1290
+ description: 'Find entities tagged with a specific category key:value pair (v3 API)',
1291
+ capability: 'observe',
1292
+ params: {
1293
+ key: { type: 'string', description: 'Category key', required: true },
1294
+ value: { type: 'string', description: 'Category value', required: true },
1295
+ },
1296
+ timeout: 15000,
1297
+ },
1298
+ {
1299
+ name: 'networks.list',
1300
+ description: 'List subnets/networks with VLAN, IP config, and cluster assignment',
1301
+ capability: 'observe',
1302
+ params: {
1303
+ cluster_id: {
1304
+ type: 'string',
1305
+ description: 'Filter by cluster extId',
1306
+ required: false,
1307
+ },
1308
+ },
1309
+ timeout: 15000,
1310
+ },
1311
+ {
1312
+ name: 'tasks.recent',
1313
+ description: 'Recent Prism tasks with failure and long-running detection',
1314
+ capability: 'observe',
1315
+ params: {
1316
+ hours: {
1317
+ type: 'number',
1318
+ description: 'Lookback window in hours (default: 24)',
1319
+ required: false,
1320
+ },
1321
+ status: {
1322
+ type: 'string',
1323
+ description: 'Filter by status (e.g. FAILED)',
1324
+ required: false,
1325
+ },
1326
+ limit: { type: 'number', description: 'Max results (default: 50)', required: false },
1327
+ },
1328
+ timeout: 15000,
1329
+ },
1330
+ {
1331
+ name: 'cluster.health',
1332
+ description:
1333
+ 'Composite health check — cluster info, hosts, critical alerts, and storage capacity',
1334
+ capability: 'observe',
1335
+ params: {
1336
+ cluster_id: {
1337
+ type: 'string',
1338
+ description: 'Cluster extId (uses first cluster if omitted)',
1339
+ required: false,
1340
+ },
1341
+ },
1342
+ timeout: 30000,
1343
+ },
1344
+ {
1345
+ name: 'vm.snapshots',
1346
+ description: 'VM recovery points / snapshots with age and expiration warnings',
1347
+ capability: 'observe',
1348
+ params: {
1349
+ vm_id: { type: 'string', description: 'VM extId', required: true },
1350
+ },
1351
+ timeout: 15000,
1352
+ },
1353
+ {
1354
+ name: 'protection.policies',
1355
+ description: 'Data protection policies with RPO, retention, and coverage',
1356
+ capability: 'observe',
1357
+ params: {
1358
+ vm_id: {
1359
+ type: 'string',
1360
+ description: 'VM extId — show only policies covering this VM',
1361
+ required: false,
1362
+ },
1363
+ },
1364
+ timeout: 15000,
1365
+ },
1366
+ {
1367
+ name: 'lifecycle.status',
1368
+ description: 'LCM entity versions and available updates (AOS, hypervisor, firmware, NCC)',
1369
+ capability: 'observe',
1370
+ params: {},
1371
+ timeout: 15000,
1372
+ },
1373
+ {
1374
+ name: 'host.stats',
1375
+ description: 'Host performance stats with CPU/memory threshold warnings',
1376
+ capability: 'observe',
1377
+ params: {
1378
+ host_id: { type: 'string', description: 'Host extId', required: true },
1379
+ },
1380
+ timeout: 15000,
1381
+ },
1382
+ {
1383
+ name: 'cluster.stats',
1384
+ description: 'Aggregate cluster metrics — CPU, memory, storage utilization, IOPS, latency',
1385
+ capability: 'observe',
1386
+ params: {
1387
+ cluster_id: { type: 'string', description: 'Cluster extId', required: true },
1388
+ },
1389
+ timeout: 15000,
1390
+ },
1391
+ {
1392
+ name: 'images.list',
1393
+ description: 'List disk images and ISOs in the image library',
1394
+ capability: 'observe',
1395
+ params: {
1396
+ name: { type: 'string', description: 'Filter by image name', required: false },
1397
+ },
1398
+ timeout: 15000,
1399
+ },
1400
+ {
1401
+ name: 'vms.by_host',
1402
+ description: 'List all VMs on a specific host — useful for maintenance planning',
1403
+ capability: 'observe',
1404
+ params: {
1405
+ host_id: { type: 'string', description: 'Host extId', required: true },
1406
+ },
1407
+ timeout: 15000,
1408
+ },
1409
+ ],
1410
+ runbook: {
1411
+ category: 'hyperconverged',
1412
+ probes: ['clusters.list', 'alerts.summary', 'storage.containers'],
1413
+ parallel: true,
1414
+ },
1415
+ },
1416
+
1417
+ handlers: {
1418
+ 'clusters.list': clustersList,
1419
+ 'hosts.list': hostsList,
1420
+ 'vms.list': vmsList,
1421
+ 'vm.detail': vmDetail,
1422
+ 'vm.stats': vmStats,
1423
+ 'alerts.list': alertsList,
1424
+ 'alerts.summary': alertsSummary,
1425
+ 'storage.containers': storageContainers,
1426
+ 'categories.list': categoriesList,
1427
+ 'categories.entities': categoriesEntities,
1428
+ 'networks.list': networksList,
1429
+ 'tasks.recent': tasksRecent,
1430
+ 'cluster.health': clusterHealth,
1431
+ 'vm.snapshots': vmSnapshots,
1432
+ 'protection.policies': protectionPolicies,
1433
+ 'lifecycle.status': lifecycleStatus,
1434
+ 'host.stats': hostStats,
1435
+ 'cluster.stats': clusterStats,
1436
+ 'images.list': imagesList,
1437
+ 'vms.by_host': vmsByHost,
1438
+ },
1439
+
1440
+ testConnection: async (config, credentials, fetchFn) => {
1441
+ try {
1442
+ const url = nutanixUrl(config.endpoint, 'clustermgmt', 'config/clusters');
1443
+ const fullUrl = new URL(url);
1444
+ fullUrl.searchParams.set('$limit', '1');
1445
+
1446
+ const headers: Record<string, string> = {
1447
+ Accept: 'application/json',
1448
+ ...buildAuthHeaders(credentials),
1449
+ ...config.headers,
1450
+ };
1451
+
1452
+ const res = await fetchFn(fullUrl.toString(), { headers });
1453
+ if (!res.ok) return false;
1454
+ const body = (await res.json()) as { data?: unknown };
1455
+ return body.data != null;
1456
+ } catch {
1457
+ return false;
1458
+ }
1459
+ },
1460
+ };