@sonde/packs 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +53 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +19 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/citrix.d.ts +13 -0
- package/dist/integrations/citrix.d.ts.map +1 -0
- package/dist/integrations/citrix.js +415 -0
- package/dist/integrations/citrix.js.map +1 -0
- package/dist/integrations/citrix.test.d.ts +2 -0
- package/dist/integrations/citrix.test.d.ts.map +1 -0
- package/dist/integrations/citrix.test.js +464 -0
- package/dist/integrations/citrix.test.js.map +1 -0
- package/dist/integrations/graph.d.ts +9 -0
- package/dist/integrations/graph.d.ts.map +1 -0
- package/dist/integrations/graph.js +285 -0
- package/dist/integrations/graph.js.map +1 -0
- package/dist/integrations/graph.test.d.ts +2 -0
- package/dist/integrations/graph.test.d.ts.map +1 -0
- package/dist/integrations/graph.test.js +356 -0
- package/dist/integrations/graph.test.js.map +1 -0
- package/dist/integrations/httpbin.d.ts +3 -0
- package/dist/integrations/httpbin.d.ts.map +1 -0
- package/dist/integrations/httpbin.js +65 -0
- package/dist/integrations/httpbin.js.map +1 -0
- package/dist/integrations/nutanix.d.ts +18 -0
- package/dist/integrations/nutanix.d.ts.map +1 -0
- package/dist/integrations/nutanix.js +1116 -0
- package/dist/integrations/nutanix.js.map +1 -0
- package/dist/integrations/nutanix.test.d.ts +2 -0
- package/dist/integrations/nutanix.test.d.ts.map +1 -0
- package/dist/integrations/nutanix.test.js +978 -0
- package/dist/integrations/nutanix.test.js.map +1 -0
- package/dist/integrations/proxmox.d.ts +12 -0
- package/dist/integrations/proxmox.d.ts.map +1 -0
- package/dist/integrations/proxmox.js +728 -0
- package/dist/integrations/proxmox.js.map +1 -0
- package/dist/integrations/proxmox.test.d.ts +2 -0
- package/dist/integrations/proxmox.test.d.ts.map +1 -0
- package/dist/integrations/proxmox.test.js +697 -0
- package/dist/integrations/proxmox.test.js.map +1 -0
- package/dist/integrations/servicenow.d.ts +3 -0
- package/dist/integrations/servicenow.d.ts.map +1 -0
- package/dist/integrations/servicenow.js +251 -0
- package/dist/integrations/servicenow.js.map +1 -0
- package/dist/integrations/servicenow.test.d.ts +2 -0
- package/dist/integrations/servicenow.test.d.ts.map +1 -0
- package/dist/integrations/servicenow.test.js +217 -0
- package/dist/integrations/servicenow.test.js.map +1 -0
- package/dist/integrations/splunk.d.ts +9 -0
- package/dist/integrations/splunk.d.ts.map +1 -0
- package/dist/integrations/splunk.js +237 -0
- package/dist/integrations/splunk.js.map +1 -0
- package/dist/integrations/splunk.test.d.ts +2 -0
- package/dist/integrations/splunk.test.d.ts.map +1 -0
- package/dist/integrations/splunk.test.js +323 -0
- package/dist/integrations/splunk.test.js.map +1 -0
- package/dist/mysql/index.d.ts +3 -0
- package/dist/mysql/index.d.ts.map +1 -0
- package/dist/mysql/index.js +13 -0
- package/dist/mysql/index.js.map +1 -0
- package/dist/mysql/manifest.d.ts +3 -0
- package/dist/mysql/manifest.d.ts.map +1 -0
- package/dist/mysql/manifest.js +69 -0
- package/dist/mysql/manifest.js.map +1 -0
- package/dist/mysql/probes/databases-list.d.ts +13 -0
- package/dist/mysql/probes/databases-list.d.ts.map +1 -0
- package/dist/mysql/probes/databases-list.js +31 -0
- package/dist/mysql/probes/databases-list.js.map +1 -0
- package/dist/mysql/probes/databases-list.test.d.ts +2 -0
- package/dist/mysql/probes/databases-list.test.d.ts.map +1 -0
- package/dist/mysql/probes/databases-list.test.js +54 -0
- package/dist/mysql/probes/databases-list.test.js.map +1 -0
- package/dist/mysql/probes/processlist.d.ts +18 -0
- package/dist/mysql/probes/processlist.d.ts.map +1 -0
- package/dist/mysql/probes/processlist.js +36 -0
- package/dist/mysql/probes/processlist.js.map +1 -0
- package/dist/mysql/probes/processlist.test.d.ts +2 -0
- package/dist/mysql/probes/processlist.test.d.ts.map +1 -0
- package/dist/mysql/probes/processlist.test.js +41 -0
- package/dist/mysql/probes/processlist.test.js.map +1 -0
- package/dist/mysql/probes/status.d.ts +14 -0
- package/dist/mysql/probes/status.d.ts.map +1 -0
- package/dist/mysql/probes/status.js +40 -0
- package/dist/mysql/probes/status.js.map +1 -0
- package/dist/mysql/probes/status.test.d.ts +2 -0
- package/dist/mysql/probes/status.test.d.ts.map +1 -0
- package/dist/mysql/probes/status.test.js +43 -0
- package/dist/mysql/probes/status.test.js.map +1 -0
- package/dist/nginx/index.d.ts +3 -0
- package/dist/nginx/index.d.ts.map +1 -0
- package/dist/nginx/index.js +13 -0
- package/dist/nginx/index.js.map +1 -0
- package/dist/nginx/manifest.d.ts +3 -0
- package/dist/nginx/manifest.d.ts.map +1 -0
- package/dist/nginx/manifest.js +68 -0
- package/dist/nginx/manifest.js.map +1 -0
- package/dist/nginx/probes/access-log-tail.d.ts +9 -0
- package/dist/nginx/probes/access-log-tail.d.ts.map +1 -0
- package/dist/nginx/probes/access-log-tail.js +14 -0
- package/dist/nginx/probes/access-log-tail.js.map +1 -0
- package/dist/nginx/probes/access-log-tail.test.d.ts +2 -0
- package/dist/nginx/probes/access-log-tail.test.d.ts.map +1 -0
- package/dist/nginx/probes/access-log-tail.test.js +40 -0
- package/dist/nginx/probes/access-log-tail.test.js.map +1 -0
- package/dist/nginx/probes/config-test.d.ts +8 -0
- package/dist/nginx/probes/config-test.d.ts.map +1 -0
- package/dist/nginx/probes/config-test.js +18 -0
- package/dist/nginx/probes/config-test.js.map +1 -0
- package/dist/nginx/probes/config-test.test.d.ts +2 -0
- package/dist/nginx/probes/config-test.test.d.ts.map +1 -0
- package/dist/nginx/probes/config-test.test.js +35 -0
- package/dist/nginx/probes/config-test.test.js.map +1 -0
- package/dist/nginx/probes/error-log-tail.d.ts +9 -0
- package/dist/nginx/probes/error-log-tail.d.ts.map +1 -0
- package/dist/nginx/probes/error-log-tail.js +14 -0
- package/dist/nginx/probes/error-log-tail.js.map +1 -0
- package/dist/nginx/probes/error-log-tail.test.d.ts +2 -0
- package/dist/nginx/probes/error-log-tail.test.d.ts.map +1 -0
- package/dist/nginx/probes/error-log-tail.test.js +34 -0
- package/dist/nginx/probes/error-log-tail.test.js.map +1 -0
- package/dist/postgres/index.d.ts +3 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +13 -0
- package/dist/postgres/index.js.map +1 -0
- package/dist/postgres/manifest.d.ts +3 -0
- package/dist/postgres/manifest.d.ts.map +1 -0
- package/dist/postgres/manifest.js +90 -0
- package/dist/postgres/manifest.js.map +1 -0
- package/dist/postgres/probes/connections-active.d.ts +17 -0
- package/dist/postgres/probes/connections-active.d.ts.map +1 -0
- package/dist/postgres/probes/connections-active.js +37 -0
- package/dist/postgres/probes/connections-active.js.map +1 -0
- package/dist/postgres/probes/connections-active.test.d.ts +2 -0
- package/dist/postgres/probes/connections-active.test.d.ts.map +1 -0
- package/dist/postgres/probes/connections-active.test.js +36 -0
- package/dist/postgres/probes/connections-active.test.js.map +1 -0
- package/dist/postgres/probes/databases-list.d.ts +14 -0
- package/dist/postgres/probes/databases-list.d.ts.map +1 -0
- package/dist/postgres/probes/databases-list.js +34 -0
- package/dist/postgres/probes/databases-list.js.map +1 -0
- package/dist/postgres/probes/databases-list.test.d.ts +2 -0
- package/dist/postgres/probes/databases-list.test.d.ts.map +1 -0
- package/dist/postgres/probes/databases-list.test.js +49 -0
- package/dist/postgres/probes/databases-list.test.js.map +1 -0
- package/dist/postgres/probes/query-slow.d.ts +17 -0
- package/dist/postgres/probes/query-slow.d.ts.map +1 -0
- package/dist/postgres/probes/query-slow.js +37 -0
- package/dist/postgres/probes/query-slow.js.map +1 -0
- package/dist/postgres/probes/query-slow.test.d.ts +2 -0
- package/dist/postgres/probes/query-slow.test.d.ts.map +1 -0
- package/dist/postgres/probes/query-slow.test.js +30 -0
- package/dist/postgres/probes/query-slow.test.js.map +1 -0
- package/dist/proxmox/index.d.ts +3 -0
- package/dist/proxmox/index.d.ts.map +1 -0
- package/dist/proxmox/index.js +23 -0
- package/dist/proxmox/index.js.map +1 -0
- package/dist/proxmox/manifest.d.ts +3 -0
- package/dist/proxmox/manifest.d.ts.map +1 -0
- package/dist/proxmox/manifest.js +75 -0
- package/dist/proxmox/manifest.js.map +1 -0
- package/dist/proxmox/probes/ceph-status.d.ts +36 -0
- package/dist/proxmox/probes/ceph-status.d.ts.map +1 -0
- package/dist/proxmox/probes/ceph-status.js +71 -0
- package/dist/proxmox/probes/ceph-status.js.map +1 -0
- package/dist/proxmox/probes/ceph-status.test.d.ts +2 -0
- package/dist/proxmox/probes/ceph-status.test.d.ts.map +1 -0
- package/dist/proxmox/probes/ceph-status.test.js +115 -0
- package/dist/proxmox/probes/ceph-status.test.js.map +1 -0
- package/dist/proxmox/probes/cluster-config.d.ts +31 -0
- package/dist/proxmox/probes/cluster-config.d.ts.map +1 -0
- package/dist/proxmox/probes/cluster-config.js +72 -0
- package/dist/proxmox/probes/cluster-config.js.map +1 -0
- package/dist/proxmox/probes/cluster-config.test.d.ts +2 -0
- package/dist/proxmox/probes/cluster-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/cluster-config.test.js +107 -0
- package/dist/proxmox/probes/cluster-config.test.js.map +1 -0
- package/dist/proxmox/probes/ha-status.d.ts +18 -0
- package/dist/proxmox/probes/ha-status.d.ts.map +1 -0
- package/dist/proxmox/probes/ha-status.js +38 -0
- package/dist/proxmox/probes/ha-status.js.map +1 -0
- package/dist/proxmox/probes/ha-status.test.d.ts +2 -0
- package/dist/proxmox/probes/ha-status.test.d.ts.map +1 -0
- package/dist/proxmox/probes/ha-status.test.js +66 -0
- package/dist/proxmox/probes/ha-status.test.js.map +1 -0
- package/dist/proxmox/probes/lvm.d.ts +35 -0
- package/dist/proxmox/probes/lvm.d.ts.map +1 -0
- package/dist/proxmox/probes/lvm.js +75 -0
- package/dist/proxmox/probes/lvm.js.map +1 -0
- package/dist/proxmox/probes/lvm.test.d.ts +2 -0
- package/dist/proxmox/probes/lvm.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lvm.test.js +128 -0
- package/dist/proxmox/probes/lvm.test.js.map +1 -0
- package/dist/proxmox/probes/lxc-config.d.ts +29 -0
- package/dist/proxmox/probes/lxc-config.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-config.js +67 -0
- package/dist/proxmox/probes/lxc-config.js.map +1 -0
- package/dist/proxmox/probes/lxc-config.test.d.ts +2 -0
- package/dist/proxmox/probes/lxc-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-config.test.js +77 -0
- package/dist/proxmox/probes/lxc-config.test.js.map +1 -0
- package/dist/proxmox/probes/lxc-list.d.ts +20 -0
- package/dist/proxmox/probes/lxc-list.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-list.js +49 -0
- package/dist/proxmox/probes/lxc-list.js.map +1 -0
- package/dist/proxmox/probes/lxc-list.test.d.ts +2 -0
- package/dist/proxmox/probes/lxc-list.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-list.test.js +51 -0
- package/dist/proxmox/probes/lxc-list.test.js.map +1 -0
- package/dist/proxmox/probes/vm-config.d.ts +21 -0
- package/dist/proxmox/probes/vm-config.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-config.js +58 -0
- package/dist/proxmox/probes/vm-config.js.map +1 -0
- package/dist/proxmox/probes/vm-config.test.d.ts +2 -0
- package/dist/proxmox/probes/vm-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-config.test.js +80 -0
- package/dist/proxmox/probes/vm-config.test.js.map +1 -0
- package/dist/proxmox/probes/vm-locks.d.ts +16 -0
- package/dist/proxmox/probes/vm-locks.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-locks.js +35 -0
- package/dist/proxmox/probes/vm-locks.js.map +1 -0
- package/dist/proxmox/probes/vm-locks.test.d.ts +2 -0
- package/dist/proxmox/probes/vm-locks.test.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-locks.test.js +54 -0
- package/dist/proxmox/probes/vm-locks.test.js.map +1 -0
- package/dist/redis/index.d.ts +3 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +13 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/manifest.d.ts +3 -0
- package/dist/redis/manifest.d.ts.map +1 -0
- package/dist/redis/manifest.js +51 -0
- package/dist/redis/manifest.js.map +1 -0
- package/dist/redis/probes/info.d.ts +15 -0
- package/dist/redis/probes/info.d.ts.map +1 -0
- package/dist/redis/probes/info.js +32 -0
- package/dist/redis/probes/info.js.map +1 -0
- package/dist/redis/probes/info.test.d.ts +2 -0
- package/dist/redis/probes/info.test.d.ts.map +1 -0
- package/dist/redis/probes/info.test.js +64 -0
- package/dist/redis/probes/info.test.js.map +1 -0
- package/dist/redis/probes/keys-count.d.ts +13 -0
- package/dist/redis/probes/keys-count.d.ts.map +1 -0
- package/dist/redis/probes/keys-count.js +24 -0
- package/dist/redis/probes/keys-count.js.map +1 -0
- package/dist/redis/probes/keys-count.test.d.ts +2 -0
- package/dist/redis/probes/keys-count.test.d.ts.map +1 -0
- package/dist/redis/probes/keys-count.test.js +37 -0
- package/dist/redis/probes/keys-count.test.js.map +1 -0
- package/dist/redis/probes/memory-usage.d.ts +16 -0
- package/dist/redis/probes/memory-usage.d.ts.map +1 -0
- package/dist/redis/probes/memory-usage.js +31 -0
- package/dist/redis/probes/memory-usage.js.map +1 -0
- package/dist/redis/probes/memory-usage.test.d.ts +2 -0
- package/dist/redis/probes/memory-usage.test.d.ts.map +1 -0
- package/dist/redis/probes/memory-usage.test.js +48 -0
- package/dist/redis/probes/memory-usage.test.js.map +1 -0
- package/dist/runbooks/nutanix.d.ts +3 -0
- package/dist/runbooks/nutanix.d.ts.map +1 -0
- package/dist/runbooks/nutanix.js +619 -0
- package/dist/runbooks/nutanix.js.map +1 -0
- package/dist/runbooks/nutanix.test.d.ts +2 -0
- package/dist/runbooks/nutanix.test.d.ts.map +1 -0
- package/dist/runbooks/nutanix.test.js +971 -0
- package/dist/runbooks/nutanix.test.js.map +1 -0
- package/dist/runbooks/proxmox.d.ts +3 -0
- package/dist/runbooks/proxmox.d.ts.map +1 -0
- package/dist/runbooks/proxmox.js +451 -0
- package/dist/runbooks/proxmox.js.map +1 -0
- package/dist/runbooks/proxmox.test.d.ts +2 -0
- package/dist/runbooks/proxmox.test.d.ts.map +1 -0
- package/dist/runbooks/proxmox.test.js +700 -0
- package/dist/runbooks/proxmox.test.js.map +1 -0
- package/dist/signatures.d.ts +2 -0
- package/dist/signatures.d.ts.map +1 -0
- package/dist/signatures.js +2 -0
- package/dist/signatures.js.map +1 -0
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +2 -0
- package/dist/system/index.js.map +1 -1
- package/dist/system/manifest.d.ts.map +1 -1
- package/dist/system/manifest.js +19 -1
- package/dist/system/manifest.js.map +1 -1
- package/dist/system/probes/ping.d.ts +20 -0
- package/dist/system/probes/ping.d.ts.map +1 -0
- package/dist/system/probes/ping.js +54 -0
- package/dist/system/probes/ping.js.map +1 -0
- package/dist/system/probes/ping.test.d.ts +2 -0
- package/dist/system/probes/ping.test.d.ts.map +1 -0
- package/dist/system/probes/ping.test.js +127 -0
- package/dist/system/probes/ping.test.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +6 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +10 -1
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +60 -6
- package/src/integrations/citrix.test.ts +592 -0
- package/src/integrations/citrix.ts +553 -0
- package/src/integrations/graph.test.ts +478 -0
- package/src/integrations/graph.ts +409 -0
- package/src/integrations/httpbin.ts +68 -0
- package/src/integrations/nutanix.test.ts +1508 -0
- package/src/integrations/nutanix.ts +1456 -0
- package/src/integrations/proxmox.test.ts +1020 -0
- package/src/integrations/proxmox.ts +985 -0
- package/src/integrations/servicenow.test.ts +314 -0
- package/src/integrations/servicenow.ts +280 -0
- package/src/integrations/splunk.test.ts +440 -0
- package/src/integrations/splunk.ts +352 -0
- package/src/mysql/index.ts +14 -0
- package/src/mysql/manifest.ts +70 -0
- package/src/mysql/probes/databases-list.test.ts +62 -0
- package/src/mysql/probes/databases-list.ts +45 -0
- package/src/mysql/probes/processlist.test.ts +47 -0
- package/src/mysql/probes/processlist.ts +55 -0
- package/src/mysql/probes/status.test.ts +50 -0
- package/src/mysql/probes/status.ts +56 -0
- package/src/nginx/index.ts +14 -0
- package/src/nginx/manifest.ts +69 -0
- package/src/nginx/probes/access-log-tail.test.ts +51 -0
- package/src/nginx/probes/access-log-tail.ts +23 -0
- package/src/nginx/probes/config-test.test.ts +47 -0
- package/src/nginx/probes/config-test.ts +24 -0
- package/src/nginx/probes/error-log-tail.test.ts +44 -0
- package/src/nginx/probes/error-log-tail.ts +23 -0
- package/src/postgres/index.ts +14 -0
- package/src/postgres/manifest.ts +91 -0
- package/src/postgres/probes/connections-active.test.ts +42 -0
- package/src/postgres/probes/connections-active.ts +55 -0
- package/src/postgres/probes/databases-list.test.ts +57 -0
- package/src/postgres/probes/databases-list.ts +49 -0
- package/src/postgres/probes/query-slow.test.ts +37 -0
- package/src/postgres/probes/query-slow.ts +55 -0
- package/src/proxmox/index.ts +24 -0
- package/src/proxmox/manifest.ts +76 -0
- package/src/proxmox/probes/ceph-status.test.ts +126 -0
- package/src/proxmox/probes/ceph-status.ts +116 -0
- package/src/proxmox/probes/cluster-config.test.ts +118 -0
- package/src/proxmox/probes/cluster-config.ts +97 -0
- package/src/proxmox/probes/ha-status.test.ts +76 -0
- package/src/proxmox/probes/ha-status.ts +56 -0
- package/src/proxmox/probes/lvm.test.ts +140 -0
- package/src/proxmox/probes/lvm.ts +121 -0
- package/src/proxmox/probes/lxc-config.test.ts +89 -0
- package/src/proxmox/probes/lxc-config.ts +90 -0
- package/src/proxmox/probes/lxc-list.test.ts +60 -0
- package/src/proxmox/probes/lxc-list.ts +67 -0
- package/src/proxmox/probes/vm-config.test.ts +93 -0
- package/src/proxmox/probes/vm-config.ts +77 -0
- package/src/proxmox/probes/vm-locks.test.ts +63 -0
- package/src/proxmox/probes/vm-locks.ts +49 -0
- package/src/redis/index.ts +14 -0
- package/src/redis/manifest.ts +52 -0
- package/src/redis/probes/info.test.ts +73 -0
- package/src/redis/probes/info.ts +46 -0
- package/src/redis/probes/keys-count.test.ts +44 -0
- package/src/redis/probes/keys-count.ts +38 -0
- package/src/redis/probes/memory-usage.test.ts +54 -0
- package/src/redis/probes/memory-usage.ts +46 -0
- package/src/runbooks/nutanix.test.ts +1138 -0
- package/src/runbooks/nutanix.ts +941 -0
- package/src/runbooks/proxmox.test.ts +838 -0
- package/src/runbooks/proxmox.ts +626 -0
- package/src/signatures.ts +1 -0
- package/src/system/index.ts +2 -0
- package/src/system/manifest.ts +21 -1
- package/src/system/probes/ping.test.ts +163 -0
- package/src/system/probes/ping.ts +89 -0
- package/src/types.ts +62 -0
- package/src/validation.ts +21 -1
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,978 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { buildAuthHeaders, nutanixPack, nutanixUrl, ppmToPercent, usecsToMs } from './nutanix.js';
|
|
3
|
+
const ntnxConfig = {
|
|
4
|
+
endpoint: 'https://prism.local:9440',
|
|
5
|
+
};
|
|
6
|
+
const basicCreds = {
|
|
7
|
+
packName: 'nutanix',
|
|
8
|
+
authMethod: 'api_key',
|
|
9
|
+
credentials: { username: 'admin', password: 'secret123' },
|
|
10
|
+
};
|
|
11
|
+
const apiKeyCreds = {
|
|
12
|
+
packName: 'nutanix',
|
|
13
|
+
authMethod: 'bearer_token',
|
|
14
|
+
credentials: { nutanixApiKey: 'ntnx-api-key-abc123' },
|
|
15
|
+
};
|
|
16
|
+
const handler = (name) => {
|
|
17
|
+
const h = nutanixPack.handlers[name];
|
|
18
|
+
if (!h)
|
|
19
|
+
throw new Error(`Handler ${name} not found`);
|
|
20
|
+
return h;
|
|
21
|
+
};
|
|
22
|
+
function callArgs(fn, index) {
|
|
23
|
+
const args = fn.mock.calls[index];
|
|
24
|
+
if (!args)
|
|
25
|
+
throw new Error(`No call at index ${index}`);
|
|
26
|
+
return args;
|
|
27
|
+
}
|
|
28
|
+
function mockNtnxResponse(body, status = 200) {
|
|
29
|
+
return vi.fn().mockResolvedValue(new Response(JSON.stringify(body), {
|
|
30
|
+
status,
|
|
31
|
+
headers: { 'Content-Type': 'application/json' },
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
function mockFetchError(status) {
|
|
35
|
+
return vi.fn().mockResolvedValue(new Response('Error', { status, statusText: 'Error' }));
|
|
36
|
+
}
|
|
37
|
+
/** Decode URL fully (handles %24 → $ and + → space) */
|
|
38
|
+
function decodeUrl(url) {
|
|
39
|
+
return decodeURIComponent(url).replace(/\+/g, ' ');
|
|
40
|
+
}
|
|
41
|
+
/** Get a specific query param from a URL */
|
|
42
|
+
function getParam(url, key) {
|
|
43
|
+
return new URL(url).searchParams.get(key);
|
|
44
|
+
}
|
|
45
|
+
/** Wrap data in Nutanix v4 response envelope */
|
|
46
|
+
function v4Envelope(data, totalAvailableResults) {
|
|
47
|
+
return {
|
|
48
|
+
data,
|
|
49
|
+
$reserved: {},
|
|
50
|
+
$objectType: 'base.v1.r0.a3.Response',
|
|
51
|
+
metadata: totalAvailableResults != null ? { totalAvailableResults } : undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
describe('nutanix pack', () => {
|
|
55
|
+
describe('auth helpers', () => {
|
|
56
|
+
it('builds Basic auth header for api_key method', () => {
|
|
57
|
+
const headers = buildAuthHeaders(basicCreds);
|
|
58
|
+
const expected = `Basic ${Buffer.from('admin:secret123').toString('base64')}`;
|
|
59
|
+
expect(headers.Authorization).toBe(expected);
|
|
60
|
+
});
|
|
61
|
+
it('builds X-Ntnx-Api-Key header for bearer_token method', () => {
|
|
62
|
+
const headers = buildAuthHeaders(apiKeyCreds);
|
|
63
|
+
expect(headers['X-Ntnx-Api-Key']).toBe('ntnx-api-key-abc123');
|
|
64
|
+
expect(headers.Authorization).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
it('returns empty headers for missing credentials', () => {
|
|
67
|
+
const emptyCreds = {
|
|
68
|
+
packName: 'nutanix',
|
|
69
|
+
authMethod: 'api_key',
|
|
70
|
+
credentials: {},
|
|
71
|
+
};
|
|
72
|
+
const headers = buildAuthHeaders(emptyCreds);
|
|
73
|
+
expect(Object.keys(headers)).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe('nutanixUrl', () => {
|
|
77
|
+
it('builds correct namespaced v4 URL', () => {
|
|
78
|
+
const url = nutanixUrl('https://prism.local:9440', 'clustermgmt', 'config/clusters');
|
|
79
|
+
expect(url).toBe('https://prism.local:9440/api/clustermgmt/v4.0/config/clusters');
|
|
80
|
+
});
|
|
81
|
+
it('includes query params', () => {
|
|
82
|
+
const url = nutanixUrl('https://prism.local:9440', 'vmm', 'ahv/config/vms', {
|
|
83
|
+
$limit: '50',
|
|
84
|
+
$filter: "name eq 'test'",
|
|
85
|
+
});
|
|
86
|
+
expect(getParam(url, '$limit')).toBe('50');
|
|
87
|
+
expect(getParam(url, '$filter')).toBe("name eq 'test'");
|
|
88
|
+
});
|
|
89
|
+
it('strips trailing slash from endpoint', () => {
|
|
90
|
+
const url = nutanixUrl('https://prism.local:9440/', 'prism', 'config/tasks');
|
|
91
|
+
expect(url).toBe('https://prism.local:9440/api/prism/v4.0/config/tasks');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe('unit conversions', () => {
|
|
95
|
+
it('ppmToPercent converts correctly', () => {
|
|
96
|
+
expect(ppmToPercent(250000)).toBe(25);
|
|
97
|
+
expect(ppmToPercent(999999)).toBe(100);
|
|
98
|
+
expect(ppmToPercent(0)).toBe(0);
|
|
99
|
+
expect(ppmToPercent(123456)).toBe(12.35);
|
|
100
|
+
});
|
|
101
|
+
it('usecsToMs converts correctly', () => {
|
|
102
|
+
expect(usecsToMs(1000)).toBe(1);
|
|
103
|
+
expect(usecsToMs(1500)).toBe(1.5);
|
|
104
|
+
expect(usecsToMs(0)).toBe(0);
|
|
105
|
+
expect(usecsToMs(12345)).toBe(12.35);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('envelope unwrap', () => {
|
|
109
|
+
it('extracts data from v4 response wrapper', async () => {
|
|
110
|
+
const fetchFn = mockNtnxResponse(v4Envelope([{ name: 'cluster-1' }], 42));
|
|
111
|
+
const result = (await handler('clusters.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
112
|
+
expect(result.clusters).toHaveLength(1);
|
|
113
|
+
expect(result.totalCount).toBe(42);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe('clusters.list', () => {
|
|
117
|
+
it('returns cluster list', async () => {
|
|
118
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
119
|
+
{
|
|
120
|
+
name: 'prod-cluster',
|
|
121
|
+
extId: 'abc-123',
|
|
122
|
+
hypervisorType: 'AHV',
|
|
123
|
+
aosVersion: '6.5.1',
|
|
124
|
+
numNodes: 4,
|
|
125
|
+
redundancyFactor: 2,
|
|
126
|
+
operationMode: 'NORMAL',
|
|
127
|
+
},
|
|
128
|
+
], 1));
|
|
129
|
+
const result = (await handler('clusters.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
130
|
+
expect(result.clusters).toHaveLength(1);
|
|
131
|
+
expect(result.clusters[0]?.name).toBe('prod-cluster');
|
|
132
|
+
expect(result.clusters[0]?.isDegraded).toBe(false);
|
|
133
|
+
expect(result.totalCount).toBe(1);
|
|
134
|
+
});
|
|
135
|
+
it('applies name filter via OData', async () => {
|
|
136
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
137
|
+
await handler('clusters.list')({ name: 'prod' }, ntnxConfig, basicCreds, fetchFn);
|
|
138
|
+
const [url] = callArgs(fetchFn, 0);
|
|
139
|
+
expect(getParam(url, '$filter')).toBe("name eq 'prod'");
|
|
140
|
+
});
|
|
141
|
+
it('flags degraded clusters', async () => {
|
|
142
|
+
const fetchFn = mockNtnxResponse(v4Envelope([{ name: 'degraded-cluster', operationMode: 'STANDBY' }]));
|
|
143
|
+
const result = (await handler('clusters.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
144
|
+
expect(result.clusters[0]?.isDegraded).toBe(true);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe('hosts.list', () => {
|
|
148
|
+
it('returns hosts', async () => {
|
|
149
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
150
|
+
{
|
|
151
|
+
hostName: 'node-01',
|
|
152
|
+
extId: 'h-123',
|
|
153
|
+
serialNumber: 'SN001',
|
|
154
|
+
blockModel: 'NX-1065',
|
|
155
|
+
numCpuSockets: 2,
|
|
156
|
+
numCpuCores: 16,
|
|
157
|
+
memoryCapacityBytes: 137438953472,
|
|
158
|
+
maintenanceMode: false,
|
|
159
|
+
},
|
|
160
|
+
], 1));
|
|
161
|
+
const result = (await handler('hosts.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
162
|
+
expect(result.hosts).toHaveLength(1);
|
|
163
|
+
expect(result.hosts[0]?.name).toBe('node-01');
|
|
164
|
+
expect(result.hosts[0]?.maintenanceMode).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
it('applies cluster_id filter', async () => {
|
|
167
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
168
|
+
await handler('hosts.list')({ cluster_id: 'c-123' }, ntnxConfig, basicCreds, fetchFn);
|
|
169
|
+
const [url] = callArgs(fetchFn, 0);
|
|
170
|
+
expect(getParam(url, '$filter')).toBe("clusterExtId eq 'c-123'");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe('vms.list', () => {
|
|
174
|
+
it('returns VMs', async () => {
|
|
175
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
176
|
+
{
|
|
177
|
+
name: 'web-01',
|
|
178
|
+
extId: 'vm-123',
|
|
179
|
+
powerState: 'ON',
|
|
180
|
+
numSockets: 2,
|
|
181
|
+
numCoresPerSocket: 4,
|
|
182
|
+
memorySizeBytes: 4294967296,
|
|
183
|
+
},
|
|
184
|
+
], 1));
|
|
185
|
+
const result = (await handler('vms.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
186
|
+
expect(result.vms).toHaveLength(1);
|
|
187
|
+
expect(result.vms[0]?.name).toBe('web-01');
|
|
188
|
+
expect(result.vms[0]?.memorySizeMb).toBe(4096);
|
|
189
|
+
});
|
|
190
|
+
it('combines multiple OData filters', async () => {
|
|
191
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
192
|
+
await handler('vms.list')({ name: 'web', power_state: 'ON', cluster_id: 'c-1' }, ntnxConfig, basicCreds, fetchFn);
|
|
193
|
+
const [url] = callArgs(fetchFn, 0);
|
|
194
|
+
const filter = getParam(url, '$filter') ?? '';
|
|
195
|
+
expect(filter).toContain("name eq 'web'");
|
|
196
|
+
expect(filter).toContain("powerState eq 'ON'");
|
|
197
|
+
expect(filter).toContain("clusterExtId eq 'c-1'");
|
|
198
|
+
expect(filter).toContain(' and ');
|
|
199
|
+
});
|
|
200
|
+
it('uses default limit of 50', async () => {
|
|
201
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
202
|
+
await handler('vms.list')({}, ntnxConfig, basicCreds, fetchFn);
|
|
203
|
+
const [url] = callArgs(fetchFn, 0);
|
|
204
|
+
expect(getParam(url, '$limit')).toBe('50');
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('vm.detail', () => {
|
|
208
|
+
it('parses full VM config with disks and NICs', async () => {
|
|
209
|
+
const fetchFn = mockNtnxResponse(v4Envelope({
|
|
210
|
+
name: 'db-01',
|
|
211
|
+
extId: 'vm-456',
|
|
212
|
+
powerState: 'ON',
|
|
213
|
+
numSockets: 4,
|
|
214
|
+
numCoresPerSocket: 2,
|
|
215
|
+
memorySizeBytes: 8589934592,
|
|
216
|
+
disks: [
|
|
217
|
+
{
|
|
218
|
+
backingInfo: {
|
|
219
|
+
deviceType: 'DISK',
|
|
220
|
+
storageContainerId: 'sc-1',
|
|
221
|
+
diskSizeBytes: 107374182400,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
nics: [
|
|
226
|
+
{
|
|
227
|
+
networkInfo: {
|
|
228
|
+
macAddress: 'AA:BB:CC:DD:EE:FF',
|
|
229
|
+
subnet: { extId: 'subnet-1' },
|
|
230
|
+
nicType: 'NORMAL_NIC',
|
|
231
|
+
isConnected: true,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
235
|
+
bootConfig: { bootType: 'UEFI' },
|
|
236
|
+
categories: [{ key: 'Environment', value: 'Production' }],
|
|
237
|
+
guestTools: { isInstalled: true },
|
|
238
|
+
createTime: '2024-01-15T10:00:00Z',
|
|
239
|
+
}));
|
|
240
|
+
const result = (await handler('vm.detail')({ vm_id: 'vm-456' }, ntnxConfig, basicCreds, fetchFn));
|
|
241
|
+
expect(result.name).toBe('db-01');
|
|
242
|
+
expect(result.disks).toHaveLength(1);
|
|
243
|
+
expect(result.disks[0]?.deviceType).toBe('DISK');
|
|
244
|
+
expect(result.disks[0]?.sizeBytes).toBe(107374182400);
|
|
245
|
+
expect(result.nics).toHaveLength(1);
|
|
246
|
+
expect(result.nics[0]?.macAddress).toBe('AA:BB:CC:DD:EE:FF');
|
|
247
|
+
expect(result.nics[0]?.subnetExtId).toBe('subnet-1');
|
|
248
|
+
expect(result.totalStorageBytes).toBe(107374182400);
|
|
249
|
+
expect(result.bootConfig).toEqual({ bootType: 'UEFI' });
|
|
250
|
+
});
|
|
251
|
+
it('requires vm_id parameter', async () => {
|
|
252
|
+
const fetchFn = mockNtnxResponse({});
|
|
253
|
+
await expect(handler('vm.detail')({}, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('vm_id parameter is required');
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
describe('vm.stats', () => {
|
|
257
|
+
it('converts ppm to percent and usecs to ms', async () => {
|
|
258
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
259
|
+
{ metricType: 'CPU_USAGE_PPM', value: 250000 },
|
|
260
|
+
{ metricType: 'MEMORY_USAGE_PPM', value: 750000 },
|
|
261
|
+
{ metricType: 'IOPS', value: 500 },
|
|
262
|
+
{ metricType: 'IO_BANDWIDTH_KBPS', value: 102400 },
|
|
263
|
+
{ metricType: 'AVG_IO_LATENCY_USECS', value: 5000 },
|
|
264
|
+
{ metricType: 'NETWORK_RX_BYTES', value: 1048576 },
|
|
265
|
+
{ metricType: 'NETWORK_TX_BYTES', value: 524288 },
|
|
266
|
+
]));
|
|
267
|
+
const result = (await handler('vm.stats')({ vm_id: 'vm-123' }, ntnxConfig, basicCreds, fetchFn));
|
|
268
|
+
expect(result.cpuUsagePct).toBe(25);
|
|
269
|
+
expect(result.memoryUsagePct).toBe(75);
|
|
270
|
+
expect(result.iops).toBe(500);
|
|
271
|
+
expect(result.ioBandwidthKbps).toBe(102400);
|
|
272
|
+
expect(result.avgIoLatencyMs).toBe(5);
|
|
273
|
+
expect(result.networkRxBytes).toBe(1048576);
|
|
274
|
+
expect(result.networkTxBytes).toBe(524288);
|
|
275
|
+
});
|
|
276
|
+
it('requires vm_id parameter', async () => {
|
|
277
|
+
const fetchFn = mockNtnxResponse({});
|
|
278
|
+
await expect(handler('vm.stats')({}, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('vm_id parameter is required');
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
describe('alerts.list', () => {
|
|
282
|
+
it('returns alerts', async () => {
|
|
283
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
284
|
+
{
|
|
285
|
+
title: 'Disk failure',
|
|
286
|
+
severity: 'CRITICAL',
|
|
287
|
+
sourceEntity: { type: 'disk', name: 'sda', extId: 'd-1' },
|
|
288
|
+
creationTime: '2024-01-15T10:00:00Z',
|
|
289
|
+
resolvedStatus: 'UNRESOLVED',
|
|
290
|
+
},
|
|
291
|
+
], 1));
|
|
292
|
+
const result = (await handler('alerts.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
293
|
+
expect(result.alerts).toHaveLength(1);
|
|
294
|
+
expect(result.alerts[0]?.title).toBe('Disk failure');
|
|
295
|
+
expect(result.alerts[0]?.severity).toBe('CRITICAL');
|
|
296
|
+
});
|
|
297
|
+
it('applies severity filter', async () => {
|
|
298
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
299
|
+
await handler('alerts.list')({ severity: 'CRITICAL' }, ntnxConfig, basicCreds, fetchFn);
|
|
300
|
+
const [url] = callArgs(fetchFn, 0);
|
|
301
|
+
const filter = getParam(url, '$filter') ?? '';
|
|
302
|
+
expect(filter).toContain("severity eq 'CRITICAL'");
|
|
303
|
+
});
|
|
304
|
+
it('applies time range filter', async () => {
|
|
305
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
306
|
+
await handler('alerts.list')({ hours: 4 }, ntnxConfig, basicCreds, fetchFn);
|
|
307
|
+
const [url] = callArgs(fetchFn, 0);
|
|
308
|
+
const filter = getParam(url, '$filter') ?? '';
|
|
309
|
+
expect(filter).toContain('creationTime ge');
|
|
310
|
+
});
|
|
311
|
+
it('applies resolved filter', async () => {
|
|
312
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
313
|
+
await handler('alerts.list')({ resolved: false }, ntnxConfig, basicCreds, fetchFn);
|
|
314
|
+
const [url] = callArgs(fetchFn, 0);
|
|
315
|
+
const filter = getParam(url, '$filter') ?? '';
|
|
316
|
+
expect(filter).toContain("resolvedStatus eq 'UNRESOLVED'");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
describe('alerts.summary', () => {
|
|
320
|
+
it('aggregates alerts by severity and entity type', async () => {
|
|
321
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
322
|
+
{
|
|
323
|
+
severity: 'CRITICAL',
|
|
324
|
+
resolvedStatus: 'UNRESOLVED',
|
|
325
|
+
title: 'Disk fail',
|
|
326
|
+
sourceEntity: { type: 'disk', name: 'sda' },
|
|
327
|
+
creationTime: '2024-01-15T10:00:00Z',
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
severity: 'WARNING',
|
|
331
|
+
resolvedStatus: 'RESOLVED',
|
|
332
|
+
title: 'High CPU',
|
|
333
|
+
sourceEntity: { type: 'vm' },
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
severity: 'CRITICAL',
|
|
337
|
+
resolvedStatus: 'RESOLVED',
|
|
338
|
+
title: 'Memory',
|
|
339
|
+
sourceEntity: { type: 'host' },
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
severity: 'INFO',
|
|
343
|
+
resolvedStatus: 'UNRESOLVED',
|
|
344
|
+
title: 'Info alert',
|
|
345
|
+
sourceEntity: { type: 'vm' },
|
|
346
|
+
},
|
|
347
|
+
]));
|
|
348
|
+
const result = (await handler('alerts.summary')({}, ntnxConfig, basicCreds, fetchFn));
|
|
349
|
+
expect(result.bySeverity.CRITICAL).toBe(2);
|
|
350
|
+
expect(result.bySeverity.WARNING).toBe(1);
|
|
351
|
+
expect(result.bySeverity.INFO).toBe(1);
|
|
352
|
+
expect(result.byEntityType.disk).toBe(1);
|
|
353
|
+
expect(result.byEntityType.vm).toBe(2);
|
|
354
|
+
expect(result.byEntityType.host).toBe(1);
|
|
355
|
+
expect(result.unresolvedCritical).toHaveLength(1);
|
|
356
|
+
expect(result.totalCount).toBe(4);
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
describe('storage.containers', () => {
|
|
360
|
+
it('returns containers with usage', async () => {
|
|
361
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
362
|
+
{
|
|
363
|
+
name: 'default-container',
|
|
364
|
+
extId: 'sc-1',
|
|
365
|
+
maxCapacityBytes: 1099511627776,
|
|
366
|
+
usedBytes: 549755813888,
|
|
367
|
+
replicationFactor: 2,
|
|
368
|
+
compressionEnabled: true,
|
|
369
|
+
},
|
|
370
|
+
], 1));
|
|
371
|
+
const result = (await handler('storage.containers')({}, ntnxConfig, basicCreds, fetchFn));
|
|
372
|
+
expect(result.containers).toHaveLength(1);
|
|
373
|
+
expect(result.containers[0]?.name).toBe('default-container');
|
|
374
|
+
expect(result.containers[0]?.usedPct).toBe(50);
|
|
375
|
+
expect(result.containers[0]?.highUsage).toBe(false);
|
|
376
|
+
});
|
|
377
|
+
it('flags containers >85% used', async () => {
|
|
378
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
379
|
+
{
|
|
380
|
+
name: 'full-container',
|
|
381
|
+
maxCapacityBytes: 1000,
|
|
382
|
+
usedBytes: 900,
|
|
383
|
+
},
|
|
384
|
+
]));
|
|
385
|
+
const result = (await handler('storage.containers')({}, ntnxConfig, basicCreds, fetchFn));
|
|
386
|
+
expect(result.containers[0]?.highUsage).toBe(true);
|
|
387
|
+
expect(result.containers[0]?.usedPct).toBe(90);
|
|
388
|
+
});
|
|
389
|
+
it('applies cluster_id filter', async () => {
|
|
390
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
391
|
+
await handler('storage.containers')({ cluster_id: 'c-1' }, ntnxConfig, basicCreds, fetchFn);
|
|
392
|
+
const [url] = callArgs(fetchFn, 0);
|
|
393
|
+
expect(getParam(url, '$filter')).toBe("clusterExtId eq 'c-1'");
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
describe('categories.list', () => {
|
|
397
|
+
it('returns categories', async () => {
|
|
398
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
399
|
+
{
|
|
400
|
+
key: 'Environment',
|
|
401
|
+
value: 'Production',
|
|
402
|
+
type: 'USER',
|
|
403
|
+
description: 'Environment tag',
|
|
404
|
+
},
|
|
405
|
+
{ key: 'Environment', value: 'Staging', type: 'USER' },
|
|
406
|
+
], 2));
|
|
407
|
+
const result = (await handler('categories.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
408
|
+
expect(result.categories).toHaveLength(2);
|
|
409
|
+
expect(result.totalCount).toBe(2);
|
|
410
|
+
});
|
|
411
|
+
it('applies key filter', async () => {
|
|
412
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
413
|
+
await handler('categories.list')({ key: 'Environment' }, ntnxConfig, basicCreds, fetchFn);
|
|
414
|
+
const [url] = callArgs(fetchFn, 0);
|
|
415
|
+
expect(getParam(url, '$filter')).toBe("key eq 'Environment'");
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
describe('categories.entities', () => {
|
|
419
|
+
it('posts v3 category query and returns entities', async () => {
|
|
420
|
+
const fetchFn = mockNtnxResponse({
|
|
421
|
+
results: [
|
|
422
|
+
{
|
|
423
|
+
kind: 'vm',
|
|
424
|
+
kind_reference_list: [
|
|
425
|
+
{ kind: 'vm', uuid: 'vm-1', name: 'web-01' },
|
|
426
|
+
{ kind: 'vm', uuid: 'vm-2', name: 'web-02' },
|
|
427
|
+
],
|
|
428
|
+
},
|
|
429
|
+
],
|
|
430
|
+
});
|
|
431
|
+
const result = (await handler('categories.entities')({ key: 'Environment', value: 'Production' }, ntnxConfig, basicCreds, fetchFn));
|
|
432
|
+
expect(result.entities).toHaveLength(2);
|
|
433
|
+
expect(result.entities[0]?.entityType).toBe('vm');
|
|
434
|
+
expect(result.entities[0]?.entityId).toBe('vm-1');
|
|
435
|
+
expect(result.totalCount).toBe(2);
|
|
436
|
+
// Verify it POSTed to v3 URL
|
|
437
|
+
const [url, opts] = callArgs(fetchFn, 0);
|
|
438
|
+
expect(url).toContain('/api/nutanix/v3/category/query');
|
|
439
|
+
expect(opts.method).toBe('POST');
|
|
440
|
+
const body = JSON.parse(opts.body);
|
|
441
|
+
expect(body.category_filter.params.Environment).toEqual(['Production']);
|
|
442
|
+
});
|
|
443
|
+
it('requires key and value parameters', async () => {
|
|
444
|
+
const fetchFn = mockNtnxResponse({});
|
|
445
|
+
await expect(handler('categories.entities')({}, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('key parameter is required');
|
|
446
|
+
await expect(handler('categories.entities')({ key: 'Env' }, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('value parameter is required');
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
describe('networks.list', () => {
|
|
450
|
+
it('returns subnets', async () => {
|
|
451
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
452
|
+
{
|
|
453
|
+
name: 'vlan-100',
|
|
454
|
+
subnetType: 'VLAN',
|
|
455
|
+
vlanId: 100,
|
|
456
|
+
clusterExtId: 'c-1',
|
|
457
|
+
},
|
|
458
|
+
], 1));
|
|
459
|
+
const result = (await handler('networks.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
460
|
+
expect(result.subnets).toHaveLength(1);
|
|
461
|
+
expect(result.subnets[0]?.name).toBe('vlan-100');
|
|
462
|
+
expect(result.subnets[0]?.type).toBe('VLAN');
|
|
463
|
+
expect(result.subnets[0]?.vlanId).toBe(100);
|
|
464
|
+
});
|
|
465
|
+
it('applies cluster_id filter', async () => {
|
|
466
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
467
|
+
await handler('networks.list')({ cluster_id: 'c-1' }, ntnxConfig, basicCreds, fetchFn);
|
|
468
|
+
const [url] = callArgs(fetchFn, 0);
|
|
469
|
+
expect(getParam(url, '$filter')).toBe("clusterExtId eq 'c-1'");
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
describe('tasks.recent', () => {
|
|
473
|
+
it('returns tasks with defaults', async () => {
|
|
474
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
475
|
+
{
|
|
476
|
+
operationType: 'kVmCreate',
|
|
477
|
+
status: 'SUCCEEDED',
|
|
478
|
+
startTime: '2024-01-15T10:00:00Z',
|
|
479
|
+
completedTime: '2024-01-15T10:01:00Z',
|
|
480
|
+
progressPercentage: 100,
|
|
481
|
+
},
|
|
482
|
+
], 1));
|
|
483
|
+
const result = (await handler('tasks.recent')({}, ntnxConfig, basicCreds, fetchFn));
|
|
484
|
+
expect(result.tasks).toHaveLength(1);
|
|
485
|
+
expect(result.tasks[0]?.type).toBe('kVmCreate');
|
|
486
|
+
expect(result.tasks[0]?.isFailed).toBe(false);
|
|
487
|
+
expect(result.tasks[0]?.isLongRunning).toBe(false);
|
|
488
|
+
});
|
|
489
|
+
it('detects failed tasks', async () => {
|
|
490
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
491
|
+
{
|
|
492
|
+
operationType: 'kVmUpdate',
|
|
493
|
+
status: 'FAILED',
|
|
494
|
+
startTime: '2024-01-15T10:00:00Z',
|
|
495
|
+
errorMessages: [{ message: 'Out of memory' }],
|
|
496
|
+
},
|
|
497
|
+
]));
|
|
498
|
+
const result = (await handler('tasks.recent')({}, ntnxConfig, basicCreds, fetchFn));
|
|
499
|
+
expect(result.tasks[0]?.isFailed).toBe(true);
|
|
500
|
+
expect(result.tasks[0]?.errorMessage).toBe('Out of memory');
|
|
501
|
+
});
|
|
502
|
+
it('detects long-running tasks (>1hr without end time)', async () => {
|
|
503
|
+
const twoHoursAgo = new Date(Date.now() - 7200000).toISOString();
|
|
504
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
505
|
+
{
|
|
506
|
+
operationType: 'kMigrate',
|
|
507
|
+
status: 'RUNNING',
|
|
508
|
+
startTime: twoHoursAgo,
|
|
509
|
+
},
|
|
510
|
+
]));
|
|
511
|
+
const result = (await handler('tasks.recent')({}, ntnxConfig, basicCreds, fetchFn));
|
|
512
|
+
expect(result.tasks[0]?.isLongRunning).toBe(true);
|
|
513
|
+
});
|
|
514
|
+
it('applies status filter', async () => {
|
|
515
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
516
|
+
await handler('tasks.recent')({ status: 'FAILED' }, ntnxConfig, basicCreds, fetchFn);
|
|
517
|
+
const [url] = callArgs(fetchFn, 0);
|
|
518
|
+
const filter = getParam(url, '$filter') ?? '';
|
|
519
|
+
expect(filter).toContain("status eq 'FAILED'");
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
describe('cluster.health', () => {
|
|
523
|
+
it('returns composite health assessment', async () => {
|
|
524
|
+
let callCount = 0;
|
|
525
|
+
const fetchFn = vi.fn().mockImplementation(() => {
|
|
526
|
+
callCount++;
|
|
527
|
+
if (callCount === 1) {
|
|
528
|
+
// clusters list
|
|
529
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([{ name: 'prod', extId: 'c-1', operationMode: 'NORMAL' }])), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
530
|
+
}
|
|
531
|
+
if (callCount === 2) {
|
|
532
|
+
// hosts
|
|
533
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([
|
|
534
|
+
{ hostName: 'n1', extId: 'h-1', maintenanceMode: false },
|
|
535
|
+
{ hostName: 'n2', extId: 'h-2', maintenanceMode: false },
|
|
536
|
+
])), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
537
|
+
}
|
|
538
|
+
if (callCount === 3) {
|
|
539
|
+
// critical alerts
|
|
540
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([])), {
|
|
541
|
+
status: 200,
|
|
542
|
+
headers: { 'Content-Type': 'application/json' },
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
// storage
|
|
546
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([{ name: 'default', maxCapacityBytes: 1000, usedBytes: 400 }])), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
547
|
+
});
|
|
548
|
+
const result = (await handler('cluster.health')({}, ntnxConfig, basicCreds, fetchFn));
|
|
549
|
+
expect(result.cluster.name).toBe('prod');
|
|
550
|
+
expect(result.nodeCount).toBe(2);
|
|
551
|
+
expect(result.degradedNodes).toHaveLength(0);
|
|
552
|
+
expect(result.criticalAlerts).toHaveLength(0);
|
|
553
|
+
expect(result.healthAssessment).toBe('HEALTHY');
|
|
554
|
+
expect(result.issues).toHaveLength(0);
|
|
555
|
+
});
|
|
556
|
+
it('returns CRITICAL when alerts or degraded nodes exist', async () => {
|
|
557
|
+
let callCount = 0;
|
|
558
|
+
const fetchFn = vi.fn().mockImplementation(() => {
|
|
559
|
+
callCount++;
|
|
560
|
+
if (callCount === 1) {
|
|
561
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([{ name: 'prod', extId: 'c-1' }])), {
|
|
562
|
+
status: 200,
|
|
563
|
+
headers: { 'Content-Type': 'application/json' },
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
if (callCount === 2) {
|
|
567
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([{ hostName: 'n1', extId: 'h-1', maintenanceMode: true }])), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
568
|
+
}
|
|
569
|
+
if (callCount === 3) {
|
|
570
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([{ title: 'Critical!', severity: 'CRITICAL' }])), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
571
|
+
}
|
|
572
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([])), {
|
|
573
|
+
status: 200,
|
|
574
|
+
headers: { 'Content-Type': 'application/json' },
|
|
575
|
+
}));
|
|
576
|
+
});
|
|
577
|
+
const result = (await handler('cluster.health')({}, ntnxConfig, basicCreds, fetchFn));
|
|
578
|
+
expect(result.healthAssessment).toBe('CRITICAL');
|
|
579
|
+
expect(result.issues.length).toBeGreaterThan(0);
|
|
580
|
+
});
|
|
581
|
+
it('returns WARNING when storage >85%', async () => {
|
|
582
|
+
let callCount = 0;
|
|
583
|
+
const fetchFn = vi.fn().mockImplementation(() => {
|
|
584
|
+
callCount++;
|
|
585
|
+
if (callCount === 1) {
|
|
586
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([{ name: 'prod', extId: 'c-1' }])), {
|
|
587
|
+
status: 200,
|
|
588
|
+
headers: { 'Content-Type': 'application/json' },
|
|
589
|
+
}));
|
|
590
|
+
}
|
|
591
|
+
if (callCount === 2) {
|
|
592
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([{ hostName: 'n1', maintenanceMode: false }])), {
|
|
593
|
+
status: 200,
|
|
594
|
+
headers: { 'Content-Type': 'application/json' },
|
|
595
|
+
}));
|
|
596
|
+
}
|
|
597
|
+
if (callCount === 3) {
|
|
598
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([])), {
|
|
599
|
+
status: 200,
|
|
600
|
+
headers: { 'Content-Type': 'application/json' },
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
return Promise.resolve(new Response(JSON.stringify(v4Envelope([{ name: 'full-sc', maxCapacityBytes: 1000, usedBytes: 900 }])), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
604
|
+
});
|
|
605
|
+
const result = (await handler('cluster.health')({}, ntnxConfig, basicCreds, fetchFn));
|
|
606
|
+
expect(result.healthAssessment).toBe('WARNING');
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
describe('testConnection', () => {
|
|
610
|
+
it('returns true on successful cluster query', async () => {
|
|
611
|
+
const fetchFn = mockNtnxResponse(v4Envelope([{ name: 'cluster-1' }]));
|
|
612
|
+
const result = await nutanixPack.testConnection(ntnxConfig, basicCreds, fetchFn);
|
|
613
|
+
expect(result).toBe(true);
|
|
614
|
+
const [url] = callArgs(fetchFn, 0);
|
|
615
|
+
expect(url).toContain('/api/clustermgmt/v4.0/config/clusters');
|
|
616
|
+
expect(getParam(url, '$limit')).toBe('1');
|
|
617
|
+
});
|
|
618
|
+
it('returns false on 401', async () => {
|
|
619
|
+
const fetchFn = mockFetchError(401);
|
|
620
|
+
const result = await nutanixPack.testConnection(ntnxConfig, basicCreds, fetchFn);
|
|
621
|
+
expect(result).toBe(false);
|
|
622
|
+
});
|
|
623
|
+
it('throws on network error', async () => {
|
|
624
|
+
const fetchFn = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
625
|
+
await expect(nutanixPack.testConnection(ntnxConfig, basicCreds, fetchFn))
|
|
626
|
+
.rejects.toThrow('ECONNREFUSED');
|
|
627
|
+
});
|
|
628
|
+
it('returns false when data is null', async () => {
|
|
629
|
+
const fetchFn = mockNtnxResponse({ data: null });
|
|
630
|
+
const result = await nutanixPack.testConnection(ntnxConfig, basicCreds, fetchFn);
|
|
631
|
+
expect(result).toBe(false);
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
describe('manifest', () => {
|
|
635
|
+
it('has correct name and 20 probes', () => {
|
|
636
|
+
expect(nutanixPack.manifest.name).toBe('nutanix');
|
|
637
|
+
expect(nutanixPack.manifest.probes).toHaveLength(20);
|
|
638
|
+
});
|
|
639
|
+
it('all handlers match manifest probes', () => {
|
|
640
|
+
const probeNames = nutanixPack.manifest.probes.map((p) => p.name);
|
|
641
|
+
const handlerNames = Object.keys(nutanixPack.handlers);
|
|
642
|
+
expect(handlerNames.sort()).toEqual(probeNames.sort());
|
|
643
|
+
});
|
|
644
|
+
it('all probes have observe capability', () => {
|
|
645
|
+
for (const probe of nutanixPack.manifest.probes) {
|
|
646
|
+
expect(probe.capability).toBe('observe');
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
it('has correct timeouts (30s for vm.stats and cluster.health, 15s for others)', () => {
|
|
650
|
+
const probeMap = new Map(nutanixPack.manifest.probes.map((p) => [p.name, p.timeout]));
|
|
651
|
+
expect(probeMap.get('vm.stats')).toBe(30000);
|
|
652
|
+
expect(probeMap.get('cluster.health')).toBe(30000);
|
|
653
|
+
for (const [name, timeout] of probeMap) {
|
|
654
|
+
if (name !== 'vm.stats' && name !== 'cluster.health') {
|
|
655
|
+
expect(timeout).toBe(15000);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
it('has hyperconverged runbook', () => {
|
|
660
|
+
expect(nutanixPack.manifest.runbook).toEqual({
|
|
661
|
+
category: 'hyperconverged',
|
|
662
|
+
probes: ['clusters.list', 'alerts.summary', 'storage.containers'],
|
|
663
|
+
parallel: true,
|
|
664
|
+
});
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
describe('vm.snapshots', () => {
|
|
668
|
+
it('returns snapshots via v4 API', async () => {
|
|
669
|
+
const now = new Date().toISOString();
|
|
670
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
671
|
+
{
|
|
672
|
+
name: 'daily-snap',
|
|
673
|
+
extId: 'rp-1',
|
|
674
|
+
creationTime: now,
|
|
675
|
+
expirationTime: new Date(Date.now() + 86400000).toISOString(),
|
|
676
|
+
recoveryPointType: 'CRASH_CONSISTENT',
|
|
677
|
+
sizeBytes: 1073741824,
|
|
678
|
+
},
|
|
679
|
+
]));
|
|
680
|
+
const result = (await handler('vm.snapshots')({ vm_id: 'vm-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
681
|
+
expect(result.snapshots).toHaveLength(1);
|
|
682
|
+
expect(result.snapshots[0]?.name).toBe('daily-snap');
|
|
683
|
+
expect(result.snapshots[0]?.isOld).toBe(false);
|
|
684
|
+
expect(result.snapshots[0]?.isExpired).toBe(false);
|
|
685
|
+
expect(result.snapshots[0]?.consistencyType).toBe('CRASH_CONSISTENT');
|
|
686
|
+
expect(result.usedV3).toBe(false);
|
|
687
|
+
expect(result.warnings).toHaveLength(0);
|
|
688
|
+
});
|
|
689
|
+
it('flags old snapshots (>7 days)', async () => {
|
|
690
|
+
const eightDaysAgo = new Date(Date.now() - 8 * 86400000).toISOString();
|
|
691
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
692
|
+
{ name: 'old-snap', creationTime: eightDaysAgo, recoveryPointType: 'APP_CONSISTENT' },
|
|
693
|
+
]));
|
|
694
|
+
const result = (await handler('vm.snapshots')({ vm_id: 'vm-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
695
|
+
expect(result.snapshots[0]?.isOld).toBe(true);
|
|
696
|
+
expect(result.snapshots[0]?.ageDays).toBeGreaterThan(7);
|
|
697
|
+
expect(result.warnings.some((w) => w.includes('older than 7 days'))).toBe(true);
|
|
698
|
+
});
|
|
699
|
+
it('flags expired snapshots not cleaned up', async () => {
|
|
700
|
+
const pastExpiration = new Date(Date.now() - 86400000).toISOString();
|
|
701
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
702
|
+
{
|
|
703
|
+
name: 'expired-snap',
|
|
704
|
+
creationTime: new Date(Date.now() - 2 * 86400000).toISOString(),
|
|
705
|
+
expirationTime: pastExpiration,
|
|
706
|
+
},
|
|
707
|
+
]));
|
|
708
|
+
const result = (await handler('vm.snapshots')({ vm_id: 'vm-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
709
|
+
expect(result.snapshots[0]?.isExpired).toBe(true);
|
|
710
|
+
expect(result.warnings.some((w) => w.includes('expired'))).toBe(true);
|
|
711
|
+
});
|
|
712
|
+
it('falls back to v3 API when v4 fails', async () => {
|
|
713
|
+
let callCount = 0;
|
|
714
|
+
const fetchFn = vi.fn().mockImplementation((url) => {
|
|
715
|
+
callCount++;
|
|
716
|
+
if (callCount === 1) {
|
|
717
|
+
// v4 fails
|
|
718
|
+
return Promise.resolve(new Response('Not Found', { status: 404, statusText: 'Not Found' }));
|
|
719
|
+
}
|
|
720
|
+
// v3 succeeds
|
|
721
|
+
return Promise.resolve(new Response(JSON.stringify({
|
|
722
|
+
entities: [
|
|
723
|
+
{
|
|
724
|
+
status: {
|
|
725
|
+
name: 'v3-snap',
|
|
726
|
+
creation_time: new Date().toISOString(),
|
|
727
|
+
recovery_point_type: 'CRASH_CONSISTENT',
|
|
728
|
+
},
|
|
729
|
+
extId: 'rp-v3',
|
|
730
|
+
},
|
|
731
|
+
],
|
|
732
|
+
}), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
733
|
+
});
|
|
734
|
+
const result = (await handler('vm.snapshots')({ vm_id: 'vm-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
735
|
+
expect(result.usedV3).toBe(true);
|
|
736
|
+
expect(result.snapshots).toHaveLength(1);
|
|
737
|
+
});
|
|
738
|
+
it('requires vm_id parameter', async () => {
|
|
739
|
+
const fetchFn = mockNtnxResponse({});
|
|
740
|
+
await expect(handler('vm.snapshots')({}, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('vm_id parameter is required');
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
describe('protection.policies', () => {
|
|
744
|
+
it('returns all policies', async () => {
|
|
745
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
746
|
+
{
|
|
747
|
+
name: 'daily-backup',
|
|
748
|
+
extId: 'pp-1',
|
|
749
|
+
schedules: [
|
|
750
|
+
{ recoveryPointObjective: 60, rpoUnit: 'MINUTES', localRetentionCount: 7 },
|
|
751
|
+
],
|
|
752
|
+
protectedEntities: [{ extId: 'vm-1' }, { extId: 'vm-2' }],
|
|
753
|
+
lastSuccessfulReplicationTime: '2024-01-15T10:00:00Z',
|
|
754
|
+
},
|
|
755
|
+
], 1));
|
|
756
|
+
const result = (await handler('protection.policies')({}, ntnxConfig, basicCreds, fetchFn));
|
|
757
|
+
expect(result.policies).toHaveLength(1);
|
|
758
|
+
expect(result.policies[0]?.name).toBe('daily-backup');
|
|
759
|
+
expect(result.policies[0]?.protectedEntityCount).toBe(2);
|
|
760
|
+
expect(result.policies[0]?.rpo?.value).toBe(60);
|
|
761
|
+
});
|
|
762
|
+
it('filters by vm_id and reports coverage', async () => {
|
|
763
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
764
|
+
{
|
|
765
|
+
name: 'covers-vm1',
|
|
766
|
+
extId: 'pp-1',
|
|
767
|
+
protectedEntities: [{ extId: 'vm-1' }],
|
|
768
|
+
schedules: [],
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
name: 'other-policy',
|
|
772
|
+
extId: 'pp-2',
|
|
773
|
+
protectedEntities: [{ extId: 'vm-99' }],
|
|
774
|
+
schedules: [],
|
|
775
|
+
},
|
|
776
|
+
]));
|
|
777
|
+
const result = (await handler('protection.policies')({ vm_id: 'vm-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
778
|
+
expect(result.policies).toHaveLength(1);
|
|
779
|
+
expect(result.policies[0]?.name).toBe('covers-vm1');
|
|
780
|
+
expect(result.vmCovered).toBe(true);
|
|
781
|
+
expect(result.allPoliciesCount).toBe(2);
|
|
782
|
+
});
|
|
783
|
+
it('reports uncovered VM', async () => {
|
|
784
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
785
|
+
{
|
|
786
|
+
name: 'policy-1',
|
|
787
|
+
extId: 'pp-1',
|
|
788
|
+
protectedEntities: [{ extId: 'vm-99' }],
|
|
789
|
+
schedules: [],
|
|
790
|
+
},
|
|
791
|
+
]));
|
|
792
|
+
const result = (await handler('protection.policies')({ vm_id: 'vm-orphan' }, ntnxConfig, basicCreds, fetchFn));
|
|
793
|
+
expect(result.vmCovered).toBe(false);
|
|
794
|
+
expect(result.totalCount).toBe(0);
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
describe('lifecycle.status', () => {
|
|
798
|
+
it('returns LCM entities with update detection', async () => {
|
|
799
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
800
|
+
{
|
|
801
|
+
entityType: 'AOS',
|
|
802
|
+
name: 'AOS',
|
|
803
|
+
extId: 'lcm-1',
|
|
804
|
+
installedVersion: { version: '6.5.1' },
|
|
805
|
+
availableVersion: { version: '6.5.2' },
|
|
806
|
+
},
|
|
807
|
+
{
|
|
808
|
+
entityType: 'NCC',
|
|
809
|
+
name: 'NCC',
|
|
810
|
+
extId: 'lcm-2',
|
|
811
|
+
installedVersion: { version: '4.6.0' },
|
|
812
|
+
availableVersion: { version: '4.6.0' },
|
|
813
|
+
},
|
|
814
|
+
]));
|
|
815
|
+
const result = (await handler('lifecycle.status')({}, ntnxConfig, basicCreds, fetchFn));
|
|
816
|
+
expect(result.entities).toHaveLength(2);
|
|
817
|
+
expect(result.entities[0]?.hasUpdate).toBe(true);
|
|
818
|
+
expect(result.entities[0]?.currentVersion).toBe('6.5.1');
|
|
819
|
+
expect(result.entities[0]?.availableVersion).toBe('6.5.2');
|
|
820
|
+
expect(result.entities[1]?.hasUpdate).toBe(false);
|
|
821
|
+
expect(result.updatableCount).toBe(1);
|
|
822
|
+
expect(result.warnings.some((w) => w.includes('1 component(s)'))).toBe(true);
|
|
823
|
+
expect(result.warnings.some((w) => w.includes('6.5.1'))).toBe(true);
|
|
824
|
+
});
|
|
825
|
+
it('returns no warnings when everything is current', async () => {
|
|
826
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
827
|
+
{
|
|
828
|
+
entityType: 'AOS',
|
|
829
|
+
installedVersion: { version: '6.5.2' },
|
|
830
|
+
availableVersion: { version: '6.5.2' },
|
|
831
|
+
},
|
|
832
|
+
]));
|
|
833
|
+
const result = (await handler('lifecycle.status')({}, ntnxConfig, basicCreds, fetchFn));
|
|
834
|
+
expect(result.updatableCount).toBe(0);
|
|
835
|
+
expect(result.warnings).toHaveLength(0);
|
|
836
|
+
});
|
|
837
|
+
});
|
|
838
|
+
describe('host.stats', () => {
|
|
839
|
+
it('returns host metrics with ppm conversion', async () => {
|
|
840
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
841
|
+
{ metricType: 'HYPERVISOR_CPU_USAGE_PPM', value: 500000 },
|
|
842
|
+
{ metricType: 'HYPERVISOR_MEMORY_USAGE_PPM', value: 700000 },
|
|
843
|
+
{ metricType: 'IOPS', value: 1200 },
|
|
844
|
+
{ metricType: 'IO_BANDWIDTH_KBPS', value: 204800 },
|
|
845
|
+
{ metricType: 'NETWORK_RX_BYTES', value: 2097152 },
|
|
846
|
+
{ metricType: 'NETWORK_TX_BYTES', value: 1048576 },
|
|
847
|
+
{ metricType: 'HYPERVISOR_UPTIME_USECS', value: 86400000000 },
|
|
848
|
+
]));
|
|
849
|
+
const result = (await handler('host.stats')({ host_id: 'h-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
850
|
+
expect(result.cpuUsagePct).toBe(50);
|
|
851
|
+
expect(result.memoryUsagePct).toBe(70);
|
|
852
|
+
expect(result.iops).toBe(1200);
|
|
853
|
+
expect(result.warnings).toHaveLength(0);
|
|
854
|
+
});
|
|
855
|
+
it('flags high CPU (>85%)', async () => {
|
|
856
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
857
|
+
{ metricType: 'HYPERVISOR_CPU_USAGE_PPM', value: 900000 },
|
|
858
|
+
{ metricType: 'HYPERVISOR_MEMORY_USAGE_PPM', value: 500000 },
|
|
859
|
+
]));
|
|
860
|
+
const result = (await handler('host.stats')({ host_id: 'h-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
861
|
+
expect(result.cpuUsagePct).toBe(90);
|
|
862
|
+
expect(result.warnings.some((w) => w.includes('CPU at 90%'))).toBe(true);
|
|
863
|
+
});
|
|
864
|
+
it('flags high memory (>90%)', async () => {
|
|
865
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
866
|
+
{ metricType: 'HYPERVISOR_CPU_USAGE_PPM', value: 200000 },
|
|
867
|
+
{ metricType: 'HYPERVISOR_MEMORY_USAGE_PPM', value: 950000 },
|
|
868
|
+
]));
|
|
869
|
+
const result = (await handler('host.stats')({ host_id: 'h-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
870
|
+
expect(result.memoryUsagePct).toBe(95);
|
|
871
|
+
expect(result.warnings.some((w) => w.includes('memory at 95%'))).toBe(true);
|
|
872
|
+
});
|
|
873
|
+
it('requires host_id parameter', async () => {
|
|
874
|
+
const fetchFn = mockNtnxResponse({});
|
|
875
|
+
await expect(handler('host.stats')({}, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('host_id parameter is required');
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
describe('cluster.stats', () => {
|
|
879
|
+
it('returns aggregate cluster metrics with utilization percentages', async () => {
|
|
880
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
881
|
+
{ metricType: 'CPU_CAPACITY_HZ', value: 100000000000 },
|
|
882
|
+
{ metricType: 'CPU_USAGE_HZ', value: 40000000000 },
|
|
883
|
+
{ metricType: 'MEMORY_CAPACITY_BYTES', value: 274877906944 },
|
|
884
|
+
{ metricType: 'MEMORY_USAGE_BYTES', value: 137438953472 },
|
|
885
|
+
{ metricType: 'STORAGE_CAPACITY_BYTES', value: 10995116277760 },
|
|
886
|
+
{ metricType: 'STORAGE_USAGE_BYTES', value: 5497558138880 },
|
|
887
|
+
{ metricType: 'IOPS', value: 5000 },
|
|
888
|
+
{ metricType: 'AVG_IO_LATENCY_USECS', value: 2000 },
|
|
889
|
+
]));
|
|
890
|
+
const result = (await handler('cluster.stats')({ cluster_id: 'c-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
891
|
+
expect(result.cpuUsagePct).toBe(40);
|
|
892
|
+
expect(result.memoryUsagePct).toBe(50);
|
|
893
|
+
expect(result.storageUsagePct).toBe(50);
|
|
894
|
+
expect(result.iops).toBe(5000);
|
|
895
|
+
expect(result.avgIoLatencyMs).toBe(2);
|
|
896
|
+
});
|
|
897
|
+
it('requires cluster_id parameter', async () => {
|
|
898
|
+
const fetchFn = mockNtnxResponse({});
|
|
899
|
+
await expect(handler('cluster.stats')({}, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('cluster_id parameter is required');
|
|
900
|
+
});
|
|
901
|
+
});
|
|
902
|
+
describe('images.list', () => {
|
|
903
|
+
it('returns images', async () => {
|
|
904
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
905
|
+
{
|
|
906
|
+
name: 'ubuntu-22.04',
|
|
907
|
+
extId: 'img-1',
|
|
908
|
+
type: 'DISK_IMAGE',
|
|
909
|
+
sizeBytes: 2147483648,
|
|
910
|
+
description: 'Ubuntu 22.04 LTS',
|
|
911
|
+
createTime: '2024-01-10T08:00:00Z',
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
name: 'windows-2022.iso',
|
|
915
|
+
extId: 'img-2',
|
|
916
|
+
type: 'ISO',
|
|
917
|
+
sizeBytes: 5368709120,
|
|
918
|
+
},
|
|
919
|
+
], 2));
|
|
920
|
+
const result = (await handler('images.list')({}, ntnxConfig, basicCreds, fetchFn));
|
|
921
|
+
expect(result.images).toHaveLength(2);
|
|
922
|
+
expect(result.images[0]?.name).toBe('ubuntu-22.04');
|
|
923
|
+
expect(result.images[0]?.type).toBe('DISK_IMAGE');
|
|
924
|
+
expect(result.images[1]?.type).toBe('ISO');
|
|
925
|
+
expect(result.totalCount).toBe(2);
|
|
926
|
+
});
|
|
927
|
+
it('applies name filter', async () => {
|
|
928
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
929
|
+
await handler('images.list')({ name: 'ubuntu' }, ntnxConfig, basicCreds, fetchFn);
|
|
930
|
+
const [url] = callArgs(fetchFn, 0);
|
|
931
|
+
expect(getParam(url, '$filter')).toBe("name eq 'ubuntu'");
|
|
932
|
+
});
|
|
933
|
+
});
|
|
934
|
+
describe('vms.by_host', () => {
|
|
935
|
+
it('returns VMs on a specific host', async () => {
|
|
936
|
+
const fetchFn = mockNtnxResponse(v4Envelope([
|
|
937
|
+
{
|
|
938
|
+
name: 'web-01',
|
|
939
|
+
extId: 'vm-1',
|
|
940
|
+
powerState: 'ON',
|
|
941
|
+
numSockets: 2,
|
|
942
|
+
memorySizeBytes: 4294967296,
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
name: 'db-01',
|
|
946
|
+
extId: 'vm-2',
|
|
947
|
+
powerState: 'ON',
|
|
948
|
+
numSockets: 4,
|
|
949
|
+
memorySizeBytes: 8589934592,
|
|
950
|
+
},
|
|
951
|
+
], 2));
|
|
952
|
+
const result = (await handler('vms.by_host')({ host_id: 'h-1' }, ntnxConfig, basicCreds, fetchFn));
|
|
953
|
+
expect(result.vms).toHaveLength(2);
|
|
954
|
+
expect(result.vms[0]?.name).toBe('web-01');
|
|
955
|
+
expect(result.vms[0]?.memorySizeMb).toBe(4096);
|
|
956
|
+
expect(result.vms[1]?.memorySizeMb).toBe(8192);
|
|
957
|
+
expect(result.hostId).toBe('h-1');
|
|
958
|
+
expect(result.totalCount).toBe(2);
|
|
959
|
+
});
|
|
960
|
+
it('applies hostExtId filter in URL', async () => {
|
|
961
|
+
const fetchFn = mockNtnxResponse(v4Envelope([]));
|
|
962
|
+
await handler('vms.by_host')({ host_id: 'h-42' }, ntnxConfig, basicCreds, fetchFn);
|
|
963
|
+
const [url] = callArgs(fetchFn, 0);
|
|
964
|
+
expect(getParam(url, '$filter')).toBe("hostExtId eq 'h-42'");
|
|
965
|
+
});
|
|
966
|
+
it('requires host_id parameter', async () => {
|
|
967
|
+
const fetchFn = mockNtnxResponse({});
|
|
968
|
+
await expect(handler('vms.by_host')({}, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('host_id parameter is required');
|
|
969
|
+
});
|
|
970
|
+
});
|
|
971
|
+
describe('error handling', () => {
|
|
972
|
+
it('throws on non-200 API response for probes', async () => {
|
|
973
|
+
const fetchFn = mockFetchError(403);
|
|
974
|
+
await expect(handler('clusters.list')({}, ntnxConfig, basicCreds, fetchFn)).rejects.toThrow('Nutanix API returned 403');
|
|
975
|
+
});
|
|
976
|
+
});
|
|
977
|
+
});
|
|
978
|
+
//# sourceMappingURL=nutanix.test.js.map
|