vmware-storage 2.3.0__tar.gz → 2.4.0__tar.gz
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.
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.claude-plugin/plugin.json +1 -1
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.mcp.json +1 -1
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/PKG-INFO +10 -2
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/README-CN.md +7 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/README.md +8 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/RELEASE_NOTES.md +24 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/openclaw.plugin.json +1 -1
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/package.json +1 -1
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/pyproject.toml +2 -2
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/server.json +2 -2
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/SKILL.md +6 -2
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/setup-guide.md +11 -3
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +12 -6
- vmware_storage-2.4.0/tests/eval/regression/test_login_without_a_role.py +215 -0
- vmware_storage-2.4.0/tests/test_policy_roles.py +55 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/uv.lock +5 -5
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/__init__.py +1 -1
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/cli.py +6 -2
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/connection.py +29 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/mcp_server/server.py +40 -12
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/policy_environment.py +4 -0
- vmware_storage-2.4.0/vmware_storage/policy_roles.py +52 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.clawhubignore +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.env.example +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.gitignore +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/Dockerfile +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/LICENSE +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/SECURITY.md +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/config.example.yaml +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/docker-compose.yml +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/README.md +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/claude-code.json +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/continue.yaml +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/cursor.json +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/goose.json +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/localcowork.json +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/mcp-agent.yaml +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/vscode-copilot.json +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/patterns/README.md +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/patterns/iscsi-target-stale-rescan.yaml +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/evals/evals.json +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/agent-guardrails.md +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/capabilities.md +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/cli-reference.md +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/smithery.yaml +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/__init__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/conftest.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/__init__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/__init__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/_family.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/_scores.json +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/_scoring.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/_skill.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/conftest.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/__init__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_audit_isolation.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_cached_loader_agrees_with_the_resolver.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_cli_writes_guarded.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_credential_env_override.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_datastore_list_renders.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_doctor_authenticates_every_target.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_doctor_uses_the_config_the_tools_use.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_env_secret_obfuscation.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_fc_multipath.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_instructions_name_targets.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_inventory_scale_n1.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_iscsi_unreachable_host.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_release_blockers.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_remedy_survives_a_long_home.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_returned_failure_is_audited.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_session_probe_eviction.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_storage_hardening_fixes.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_storage_ops_behavior.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_tool_names_2_0.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_vim_attribute_conformance.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_vsan_efficiency.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_vsan_health_is_actually_queried.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_vsan_reports_what_it_could_not_read.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/spec/__init__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/spec/vsan_efficiency_endpoints.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/spec/vsan_health_endpoints.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_access_info.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_init_and_onboarding.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_iscsi_gate.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_no_destructive_ops.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_no_destructive_vm_code.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_session_pool.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_stop_signal_runs_atexit.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/access_requirements.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/config.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/doctor.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/init_wizard.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/mcp_server/__init__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/mcp_server/__main__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/notify/__init__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/notify/audit.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/__init__.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/datastore_browser.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/inventory.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/iscsi_config.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/iscsi_gate.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/multipath.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/storage_paths.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/vsan.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/vsan_efficiency.py +0 -0
- {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/vsan_sdk.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "vmware-storage",
|
|
4
4
|
"displayName": "VMware Storage",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.4.0",
|
|
6
6
|
"description": "VMware vSphere storage with 15 MCP tools: datastores, iSCSI, vSAN, FC multipath.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Wei Zhou",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vmware-storage
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: VMware vSphere storage management: datastores, iSCSI, vSAN. Domain-focused MCP skill.
|
|
5
5
|
Project-URL: Homepage, https://github.com/vmware-skills/VMware-Storage
|
|
6
6
|
Project-URL: Repository, https://github.com/vmware-skills/VMware-Storage
|
|
@@ -16,7 +16,7 @@ Requires-Dist: python-dotenv<2.0,>=1.0
|
|
|
16
16
|
Requires-Dist: pyvmomi<10.0,>=8.0.3.0
|
|
17
17
|
Requires-Dist: rich<15.0,>=13.0
|
|
18
18
|
Requires-Dist: typer<1.0,>=0.12
|
|
19
|
-
Requires-Dist: vmware-policy<2.0,>=1.
|
|
19
|
+
Requires-Dist: vmware-policy<2.0,>=1.23.0
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
21
|
|
|
22
22
|
<!-- mcp-name: io.github.vmware-skills/vmware-storage -->
|
|
@@ -276,6 +276,7 @@ vmware-storage-mcp
|
|
|
276
276
|
| `get_vsan_health` returns `overall_health: null` | `null` = not asked (see `health_not_queried_reason`), never a measurement; a string is always vSAN's own answer, including its own `"unknown"`. Usual cause: connected to standalone ESXi, but the health service runs in **vCenter**. |
|
|
277
277
|
| Rescan doesn't discover new LUNs | Wait 15-30 seconds after adding targets, then rescan again. Verify target IP is reachable from ESXi. |
|
|
278
278
|
| "Password not found" error | Variable names follow `VMWARE_<TARGET_UPPER>_PASSWORD` (hyphens → underscores). Check `~/.vmware-storage/.env`. |
|
|
279
|
+
| `doctor` fails with "no vSphere role", or every command fails with "session is not authenticated" | The login works but the account has no vSphere role. Assign one (Read-Only or higher) on the vCenter root folder (Administration > Access Control > Global Permissions). Mid-run, the same fault can be an expired session: run the command again. |
|
|
279
280
|
| Connection timeout to vCenter | Use `vmware-storage doctor --skip-auth` to bypass auth checks on high-latency networks. |
|
|
280
281
|
| "Datastore browse did not finish within Ns" | The datastore is very large or busy. Narrow the search with a sub-path and a specific pattern (e.g. `datastore browse ds01 --path templates --pattern "*.ova"`) instead of browsing the root — do not just retry the same broad browse. |
|
|
281
282
|
|
|
@@ -319,9 +320,16 @@ claude mcp add --transport http vmware-storage https://mcp.example.com:8443/mcp
|
|
|
319
320
|
before it binds that unauthenticated requests get 401. Use a certificate your clients trust for those names.
|
|
320
321
|
- Behind a TLS-terminating reverse proxy on the same machine use `--transport http --host 127.0.0.1
|
|
321
322
|
--public-name mcp.example.com` and pass the `Authorization` header through.
|
|
323
|
+
- Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
|
|
324
|
+
fine), list in `--public-name` the host name the balancer sends as `Host`, and have it forward `Authorization`.
|
|
325
|
+
Its health probe can use `GET /healthz`, which needs no token and returns only `{"status": "ok"}`
|
|
326
|
+
(vmware-policy ≥ 1.23.0).
|
|
327
|
+
- Send `traceparent` or `X-Request-ID` with each request to have it stored on the audit row;
|
|
328
|
+
`vmware-audit log --correlation-id ID` then finds the calls behind one platform request.
|
|
322
329
|
- The audit log records each caller as `token:<name>`. Calls run side by side, also on the same target: the server keeps up to `VMWARE_MCP_POOL_SIZE` (default 2) sessions per target, each one a login on that target, and a call waits at most 300 s for a free session (vmware-policy ≥ 1.22.0).
|
|
323
330
|
- **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
|
|
324
331
|
nothing on the platform. Run one endpoint per privilege level. `vmware-storage access-info` shows which account that is and what it may do.
|
|
332
|
+
- **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
|
|
325
333
|
|
|
326
334
|
**Offer only read tools, plus the writes you choose:**
|
|
327
335
|
|
|
@@ -290,6 +290,7 @@ vmware-storage-mcp
|
|
|
290
290
|
| `get_vsan_health` 返回 `overall_health: null` | `null` 表示**没有查询**(原因见 `health_not_queried_reason`),不是测量结果;只要是字符串就一定是 vSAN 自己给的答案,包括它自己的 `"unknown"`。常见原因:连的是独立 ESXi,而健康服务跑在 **vCenter** 上。 |
|
|
291
291
|
| 重扫后未发现新 LUN | 添加目标后等待 15-30 秒再重扫。确认 ESXi 能访问目标 IP。 |
|
|
292
292
|
| "Password not found" 错误 | 变量名规则:`VMWARE_<目标名大写>_PASSWORD`(连字符→下划线)。检查 `~/.vmware-storage/.env`。 |
|
|
293
|
+
| `doctor` 报 "no vSphere role",或每个命令都报 "session is not authenticated" | 能登录,但账号没有 vSphere 角色。在 vCenter 根文件夹上给它分配一个角色(Read-Only 或更高):Administration > Access Control > Global Permissions。运行中出现同一错误也可能是会话过期:重跑命令即可。 |
|
|
293
294
|
| 连接 vCenter 超时 | 使用 `vmware-storage doctor --skip-auth` 跳过高延迟网络的认证检查。 |
|
|
294
295
|
|
|
295
296
|
<!-- usage: remote-and-exposure -->
|
|
@@ -332,9 +333,15 @@ claude mcp add --transport http vmware-storage https://mcp.example.com:8443/mcp
|
|
|
332
333
|
证书要用客户端信任、且覆盖这些名字的证书。
|
|
333
334
|
- 在同一台机器上由反向代理终结 TLS 时,用 `--transport http --host 127.0.0.1 --public-name mcp.example.com`,
|
|
334
335
|
并让代理透传 `Authorization` 头。
|
|
336
|
+
- 负载均衡或网关在另一台机器上时,保持 `--transport https`(自签证书也可以),在 `--public-name` 里写上负载均衡
|
|
337
|
+
转发时带的 `Host` 名,并让它透传 `Authorization`。健康检查可以用 `GET /healthz`:不需要 token,只返回
|
|
338
|
+
`{"status": "ok"}`(需 vmware-policy ≥ 1.23.0)。
|
|
339
|
+
- 每个请求带上 `traceparent` 或 `X-Request-ID`,它会记进审计记录;用 `vmware-audit log --correlation-id ID`
|
|
340
|
+
就能查到平台上某一次请求对应的调用。
|
|
335
341
|
- 审计日志把每个调用者记为 `token:<名字>`。调用并行执行,同一 target 上也一样:server 为每个 target 最多保持 `VMWARE_MCP_POOL_SIZE`(默认 2)个会话,每个会话都是在该 target 上的一次登录;一个调用最多等 300 秒拿到空闲会话(vmware-policy ≥ 1.22.0)。
|
|
336
342
|
- **能连到这个端点的人,都以该 server 配置的账号操作。** token 只用来识别调用者,不在平台上授予任何权限。
|
|
337
343
|
每个权限级别单独起一个端点。`vmware-storage access-info` 可查看是哪个账号、它能做什么。
|
|
344
|
+
- **一个端点给多人或多种角色用:** 先运行一次 `vmware-audit roles enable`,再给每个 token 指定角色(`vmware-audit token create 名字 --role reader`,或在 `roles.yaml` 里定义可用的工具和 target;需 vmware-policy ≥ 1.24.0)。角色只能限定这个 token 在本端点上能调用什么,不能超出账号本身的权限。详见 vmware-policy README 的「网络调用者的角色」一节。
|
|
338
345
|
|
|
339
346
|
**只提供读工具,加上你选定的写工具:**
|
|
340
347
|
|
|
@@ -255,6 +255,7 @@ vmware-storage-mcp
|
|
|
255
255
|
| `get_vsan_health` returns `overall_health: null` | `null` = not asked (see `health_not_queried_reason`), never a measurement; a string is always vSAN's own answer, including its own `"unknown"`. Usual cause: connected to standalone ESXi, but the health service runs in **vCenter**. |
|
|
256
256
|
| Rescan doesn't discover new LUNs | Wait 15-30 seconds after adding targets, then rescan again. Verify target IP is reachable from ESXi. |
|
|
257
257
|
| "Password not found" error | Variable names follow `VMWARE_<TARGET_UPPER>_PASSWORD` (hyphens → underscores). Check `~/.vmware-storage/.env`. |
|
|
258
|
+
| `doctor` fails with "no vSphere role", or every command fails with "session is not authenticated" | The login works but the account has no vSphere role. Assign one (Read-Only or higher) on the vCenter root folder (Administration > Access Control > Global Permissions). Mid-run, the same fault can be an expired session: run the command again. |
|
|
258
259
|
| Connection timeout to vCenter | Use `vmware-storage doctor --skip-auth` to bypass auth checks on high-latency networks. |
|
|
259
260
|
| "Datastore browse did not finish within Ns" | The datastore is very large or busy. Narrow the search with a sub-path and a specific pattern (e.g. `datastore browse ds01 --path templates --pattern "*.ova"`) instead of browsing the root — do not just retry the same broad browse. |
|
|
260
261
|
|
|
@@ -298,9 +299,16 @@ claude mcp add --transport http vmware-storage https://mcp.example.com:8443/mcp
|
|
|
298
299
|
before it binds that unauthenticated requests get 401. Use a certificate your clients trust for those names.
|
|
299
300
|
- Behind a TLS-terminating reverse proxy on the same machine use `--transport http --host 127.0.0.1
|
|
300
301
|
--public-name mcp.example.com` and pass the `Authorization` header through.
|
|
302
|
+
- Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
|
|
303
|
+
fine), list in `--public-name` the host name the balancer sends as `Host`, and have it forward `Authorization`.
|
|
304
|
+
Its health probe can use `GET /healthz`, which needs no token and returns only `{"status": "ok"}`
|
|
305
|
+
(vmware-policy ≥ 1.23.0).
|
|
306
|
+
- Send `traceparent` or `X-Request-ID` with each request to have it stored on the audit row;
|
|
307
|
+
`vmware-audit log --correlation-id ID` then finds the calls behind one platform request.
|
|
301
308
|
- The audit log records each caller as `token:<name>`. Calls run side by side, also on the same target: the server keeps up to `VMWARE_MCP_POOL_SIZE` (default 2) sessions per target, each one a login on that target, and a call waits at most 300 s for a free session (vmware-policy ≥ 1.22.0).
|
|
302
309
|
- **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
|
|
303
310
|
nothing on the platform. Run one endpoint per privilege level. `vmware-storage access-info` shows which account that is and what it may do.
|
|
311
|
+
- **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
|
|
304
312
|
|
|
305
313
|
**Offer only read tools, plus the writes you choose:**
|
|
306
314
|
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
## v2.4.0 — roles for network callers (HLD §18)
|
|
2
|
+
|
|
3
|
+
* Declares the role metadata vmware-policy ≥ 1.24.0 needs to apply **roles** to this server over HTTPS
|
|
4
|
+
(HLD §18): how a missing target resolves. With an older vmware-policy nothing changes. Once an operator runs
|
|
5
|
+
`vmware-audit roles enable`, a token's role decides which tools and targets it may use here; a refused call is
|
|
6
|
+
audited as `denied`. See vmware-policy's README, "Roles for network callers".
|
|
7
|
+
* A target-limited role is checked against the same config the connection uses: the copy the server's connection manager loaded on its first connection. Edit `config.yaml` and restart for both to see the change.
|
|
8
|
+
* README and setup guide: one endpoint for several people or roles.
|
|
9
|
+
|
|
10
|
+
## v2.3.1 — a login with no vSphere role is named as such (AIops#47)
|
|
11
|
+
|
|
12
|
+
* An account that logs in but has no vSphere role (vCenter shows no session for it) is now refused at connect time
|
|
13
|
+
with a message naming the missing role (assign Read-Only or higher), on the `doctor` auth row, the CLI and every
|
|
14
|
+
MCP tool. The session is logged out and never cached. Before, `doctor` passed and every call then failed with
|
|
15
|
+
"the session is not authenticated".
|
|
16
|
+
* Refused only when two signals agree: no session, **and** vCenter refuses a view of the root folder
|
|
17
|
+
(`NotAuthenticated` / `NoPermission`). An account scoped to one folder can also show no session, and still
|
|
18
|
+
connects; anything the check cannot read lets the login through.
|
|
19
|
+
* The MCP hint beside these two errors (tool errors and `get_access_info`) points at `doctor`'s role check
|
|
20
|
+
instead of "verify connectivity".
|
|
21
|
+
* `NotAuthenticated` raised later (session expired, or a role removed while running) now says both causes, on the
|
|
22
|
+
CLI (it was a raw traceback) and over MCP (it was `NotAuthenticated: operation failed.`).
|
|
23
|
+
* Requires vmware-policy 1.23.0.
|
|
24
|
+
|
|
1
25
|
## v2.3.0 — calls run side by side on one target; `get_access_info`
|
|
2
26
|
|
|
3
27
|
**Concurrent calls (HLD §16 D-13(2), issue #18).** On a shared HTTPS server, calls no longer wait for each other:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-storage"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.4.0"
|
|
8
8
|
description = "VMware vSphere storage management: datastores, iSCSI, vSAN. Domain-focused MCP skill."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
"rich>=13.0,<15.0",
|
|
18
18
|
"python-dotenv>=1.0,<2.0",
|
|
19
19
|
"mcp[cli]>=1.28,<2.0",
|
|
20
|
-
"vmware-policy>=1.
|
|
20
|
+
"vmware-policy>=1.23.0,<2.0",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/vmware-skills/VMware-Storage",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.4.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-storage",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.4.0",
|
|
16
16
|
"runtimeHint": "uvx",
|
|
17
17
|
"packageArguments": [
|
|
18
18
|
{
|
|
@@ -40,7 +40,7 @@ VMware vSphere storage management — 15 MCP tools for datastores, iSCSI, vSAN,
|
|
|
40
40
|
## Quick Install
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
|
-
uv tool install vmware-storage==2.
|
|
43
|
+
uv tool install vmware-storage==2.4.0
|
|
44
44
|
vmware-storage init # guided setup: writes config + .env (chmod 600, password grep-safe), then verifies
|
|
45
45
|
vmware-storage doctor
|
|
46
46
|
```
|
|
@@ -234,6 +234,10 @@ After adding iSCSI targets, the storage subsystem may need 10-30 seconds to enum
|
|
|
234
234
|
|
|
235
235
|
The password environment variable is missing. Variable names follow the pattern `VMWARE_<TARGET_NAME_UPPER>_PASSWORD` where hyphens become underscores. Example: target `my-vcenter` needs `VMWARE_MY_VCENTER_PASSWORD`. Check your `~/.vmware-storage/.env` file.
|
|
236
236
|
|
|
237
|
+
### "Session is not authenticated" or "no vSphere role" right after login
|
|
238
|
+
|
|
239
|
+
The password is right; the account has no vSphere role, so vCenter shows no session for it and every call fails. `doctor` now reports this on the auth row. Assign the account a role (Read-Only or higher) on the vCenter root folder under Administration > Access Control > Global Permissions. Seen mid-run, the same fault can also be an expired session: run the command again.
|
|
240
|
+
|
|
237
241
|
### Doctor reports ".env permissions too open"
|
|
238
242
|
|
|
239
243
|
The `.env` file contains passwords and must have owner-only permissions:
|
|
@@ -267,7 +271,7 @@ Corporate TLS proxies inject certificates that uv's bundled CA store doesn't tru
|
|
|
267
271
|
## Setup
|
|
268
272
|
|
|
269
273
|
```bash
|
|
270
|
-
uv tool install vmware-storage==2.
|
|
274
|
+
uv tool install vmware-storage==2.4.0
|
|
271
275
|
mkdir -p ~/.vmware-storage
|
|
272
276
|
cp config.example.yaml ~/.vmware-storage/config.yaml
|
|
273
277
|
# Edit config.yaml with your vCenter/ESXi targets
|
{vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/setup-guide.md
RENAMED
|
@@ -13,19 +13,19 @@ Complete setup and security guide for `vmware-storage`.
|
|
|
13
13
|
### Via uv (recommended)
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
uv tool install vmware-storage==2.
|
|
16
|
+
uv tool install vmware-storage==2.4.0
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
### Via pip
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
pip install vmware-storage==2.
|
|
22
|
+
pip install vmware-storage==2.4.0
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
### From source
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
git clone --branch v2.
|
|
28
|
+
git clone --branch v2.4.0 https://github.com/vmware-skills/VMware-Storage.git
|
|
29
29
|
cd VMware-Storage
|
|
30
30
|
pip install -e .
|
|
31
31
|
```
|
|
@@ -235,6 +235,13 @@ vmware-storage mcp --transport https --host 0.0.0.0 --port 8443 \
|
|
|
235
235
|
- Behind a reverse proxy that terminates TLS (Nginx, HAProxy) on the same machine, use
|
|
236
236
|
`--transport http --host 127.0.0.1 --public-name mcp.example.com` (the names the proxy forwards in `Host`), and
|
|
237
237
|
have the proxy pass the `Authorization` header through.
|
|
238
|
+
- Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
|
|
239
|
+
fine: the balancer is the client that has to trust it), list in `--public-name` the host name the balancer sends
|
|
240
|
+
as `Host`, and have it forward `Authorization`. Point its health probe at `GET /healthz`: it needs no token, returns
|
|
241
|
+
only `{"status": "ok"}` and accepts any `Host` (vmware-policy ≥ 1.23.0). Every other path still needs the token.
|
|
242
|
+
- Correlation: each audit row stores the caller's `traceparent` trace-id, else its `X-Request-ID` /
|
|
243
|
+
`X-Correlation-ID`. Have the agent platform send one, and `vmware-audit log --correlation-id ID` lists the calls
|
|
244
|
+
behind one platform request.
|
|
238
245
|
- `vmware-audit token list` / `token revoke NAME` manage callers; a revoked token is refused from its next request.
|
|
239
246
|
Replace a leaked token under a new name.
|
|
240
247
|
- The server will not start without at least one token, and checks before it binds that unauthenticated requests
|
|
@@ -243,6 +250,7 @@ vmware-storage mcp --transport https --host 0.0.0.0 --port 8443 \
|
|
|
243
250
|
- **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
|
|
244
251
|
nothing on the platform. Run one endpoint per privilege level (for example a read-only account behind
|
|
245
252
|
`--expose read`, and a separate endpoint for operators).
|
|
253
|
+
- **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
|
|
246
254
|
|
|
247
255
|
## Security Details
|
|
248
256
|
|
|
@@ -43,9 +43,15 @@ def _target(name: str = "vcenter-prod") -> TargetConfig:
|
|
|
43
43
|
return TargetConfig(name=name, host="vc.example.com", config_username="svc")
|
|
44
44
|
|
|
45
45
|
|
|
46
|
-
def
|
|
47
|
-
"""An SI
|
|
46
|
+
def _live_si(*_a, **_kw):
|
|
47
|
+
"""An SI with a session; a login without one is refused (AIops#47)."""
|
|
48
48
|
si = MagicMock()
|
|
49
|
+
si.content.sessionManager.currentSession = MagicMock()
|
|
50
|
+
return si
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _expire(si: MagicMock) -> MagicMock:
|
|
54
|
+
"""The session expires after login, so the next connect() evicts it."""
|
|
49
55
|
si.content.sessionManager.currentSession = None
|
|
50
56
|
return si
|
|
51
57
|
|
|
@@ -53,7 +59,7 @@ def _dead_si(*_a, **_kw):
|
|
|
53
59
|
@pytest.mark.unit
|
|
54
60
|
def test_a_released_connection_is_collectable() -> None:
|
|
55
61
|
"""The mechanism: what connect() registers, _release_si takes back."""
|
|
56
|
-
with patch("pyVim.connect.SmartConnect", side_effect=
|
|
62
|
+
with patch("pyVim.connect.SmartConnect", side_effect=_live_si):
|
|
57
63
|
si = ConnectionManager._create_connection(_target())
|
|
58
64
|
ref = weakref.ref(si)
|
|
59
65
|
|
|
@@ -68,7 +74,7 @@ def test_a_connection_not_released_is_pinned() -> None:
|
|
|
68
74
|
"""The negative half. Without this, the test above could pass because
|
|
69
75
|
nothing ever held the object -- and would then keep passing with the whole
|
|
70
76
|
registration deleted."""
|
|
71
|
-
with patch("pyVim.connect.SmartConnect", side_effect=
|
|
77
|
+
with patch("pyVim.connect.SmartConnect", side_effect=_live_si):
|
|
72
78
|
si = ConnectionManager._create_connection(_target())
|
|
73
79
|
ref = weakref.ref(si)
|
|
74
80
|
|
|
@@ -87,10 +93,10 @@ def test_repeated_eviction_does_not_pin_every_session() -> None:
|
|
|
87
93
|
mgr = ConnectionManager(cfg)
|
|
88
94
|
|
|
89
95
|
seen: list[weakref.ref] = []
|
|
90
|
-
with patch("pyVim.connect.SmartConnect", side_effect=
|
|
96
|
+
with patch("pyVim.connect.SmartConnect", side_effect=_live_si), \
|
|
91
97
|
patch("pyVim.connect.Disconnect"):
|
|
92
98
|
for _ in range(20):
|
|
93
|
-
seen.append(weakref.ref(mgr.connect("vcenter-prod")))
|
|
99
|
+
seen.append(weakref.ref(_expire(mgr.connect("vcenter-prod"))))
|
|
94
100
|
# Drop the last, still-cached session the way production does.
|
|
95
101
|
mgr.disconnect_all()
|
|
96
102
|
gc.collect()
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
"""A login that works but has no vSphere role (AIops#47).
|
|
2
|
+
|
|
3
|
+
Reported on vCenter 8.0.3: an account with no vSphere role logs in, vCenter
|
|
4
|
+
shows no session for it (``currentSession`` is ``None``), and every inventory
|
|
5
|
+
call then fails with ``vim.fault.NotAuthenticated``. ``doctor`` passed, and the
|
|
6
|
+
error told the user to check credentials, which were fine.
|
|
7
|
+
|
|
8
|
+
Driven through the real ConnectionManager, doctor, CLI handler and MCP tool;
|
|
9
|
+
only ``SmartConnect`` / ``Disconnect`` are fakes, so nothing touches a network.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from unittest.mock import MagicMock, patch
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
import typer
|
|
18
|
+
from pyVmomi import vim
|
|
19
|
+
|
|
20
|
+
from vmware_storage import doctor
|
|
21
|
+
from vmware_storage.config import AppConfig, TargetConfig
|
|
22
|
+
from vmware_storage.connection import _SI_ATEXIT, ConnectionManager, NoVsphereRoleError
|
|
23
|
+
from vmware_storage.mcp_server import server
|
|
24
|
+
|
|
25
|
+
ROLE_WORDS = ("no vSphere role", "Read-Only")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _config() -> AppConfig:
|
|
29
|
+
return AppConfig(
|
|
30
|
+
targets=(
|
|
31
|
+
TargetConfig(name="lab", host="vc.example.com", config_username="u@vsphere.local"),
|
|
32
|
+
)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _si(has_session: bool, *, root_readable: bool = False) -> MagicMock:
|
|
37
|
+
"""A login as vCenter answers it.
|
|
38
|
+
|
|
39
|
+
No role (AIops#47) takes both signals: ``currentSession`` reads ``None`` and
|
|
40
|
+
a container view on the root folder is refused with NotAuthenticated.
|
|
41
|
+
``root_readable`` is the scoped account (one folder, or Supervisor namespace
|
|
42
|
+
permissions only): no session either, but the root view opens.
|
|
43
|
+
"""
|
|
44
|
+
si = MagicMock(name="si")
|
|
45
|
+
si.content.sessionManager.currentSession = MagicMock() if has_session else None
|
|
46
|
+
if not has_session and not root_readable:
|
|
47
|
+
si.content.viewManager.CreateContainerView.side_effect = vim.fault.NotAuthenticated()
|
|
48
|
+
return si
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def fake_login(monkeypatch):
|
|
53
|
+
monkeypatch.setenv("VMWARE_LAB_PASSWORD", "pw")
|
|
54
|
+
|
|
55
|
+
def install(has_session: bool, root_readable: bool = False):
|
|
56
|
+
si = _si(has_session, root_readable=root_readable)
|
|
57
|
+
return si, patch("pyVim.connect.SmartConnect", return_value=si), patch(
|
|
58
|
+
"pyVim.connect.Disconnect"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
return install
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@pytest.mark.unit
|
|
65
|
+
def test_role_less_login_is_refused_closed_and_not_cached(fake_login):
|
|
66
|
+
si, connect, disconnect = fake_login(has_session=False)
|
|
67
|
+
mgr = ConnectionManager(_config())
|
|
68
|
+
with connect as smart, disconnect as dis:
|
|
69
|
+
with pytest.raises(NoVsphereRoleError) as excinfo:
|
|
70
|
+
mgr.connect("lab")
|
|
71
|
+
msg = str(excinfo.value)
|
|
72
|
+
for word in ROLE_WORDS:
|
|
73
|
+
assert word in msg
|
|
74
|
+
assert "'lab'" in msg
|
|
75
|
+
dis.assert_called_once_with(si) # the session is logged out
|
|
76
|
+
assert id(si) not in _SI_ATEXIT # and its exit handler unpinned
|
|
77
|
+
assert mgr.list_connected() == [] # nothing cached
|
|
78
|
+
with pytest.raises(NoVsphereRoleError):
|
|
79
|
+
mgr.connect("lab")
|
|
80
|
+
assert smart.call_count == 2 # the second call logged in again
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@pytest.mark.unit
|
|
84
|
+
def test_normal_login_is_unaffected(fake_login):
|
|
85
|
+
si, connect, disconnect = fake_login(has_session=True)
|
|
86
|
+
mgr = ConnectionManager(_config())
|
|
87
|
+
with connect, disconnect as dis:
|
|
88
|
+
assert mgr.connect("lab") is si
|
|
89
|
+
assert mgr.list_connected() == ["lab"]
|
|
90
|
+
dis.assert_not_called()
|
|
91
|
+
mgr.disconnect_all()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.mark.unit
|
|
95
|
+
def test_scoped_account_without_a_session_still_connects(fake_login):
|
|
96
|
+
"""``currentSession`` alone is not a verdict: a readable root view lets it through."""
|
|
97
|
+
si, connect, disconnect = fake_login(has_session=False, root_readable=True)
|
|
98
|
+
mgr = ConnectionManager(_config())
|
|
99
|
+
with connect, disconnect as dis:
|
|
100
|
+
assert mgr.connect("lab") is si # no NoVsphereRoleError
|
|
101
|
+
assert mgr.list_connected() == ["lab"] # cached like any session
|
|
102
|
+
dis.assert_not_called()
|
|
103
|
+
view = si.content.viewManager.CreateContainerView.return_value
|
|
104
|
+
view.Destroy.assert_called_once_with() # the probe leaves no view behind
|
|
105
|
+
mgr.disconnect_all()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@pytest.mark.unit
|
|
109
|
+
def test_doctor_auth_row_names_the_missing_role(fake_login, monkeypatch):
|
|
110
|
+
_, connect, disconnect = fake_login(has_session=False)
|
|
111
|
+
monkeypatch.setattr(type(doctor.resolve_config_path()), "exists", lambda self: True)
|
|
112
|
+
monkeypatch.setattr("vmware_storage.config.load_config", _config)
|
|
113
|
+
with connect, disconnect:
|
|
114
|
+
ok, row = doctor._check_auth()
|
|
115
|
+
assert ok is False
|
|
116
|
+
for word in ROLE_WORDS:
|
|
117
|
+
assert word in row
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.unit
|
|
121
|
+
def test_mcp_tool_returns_the_role_message_whole(fake_login, monkeypatch):
|
|
122
|
+
_, connect, disconnect = fake_login(has_session=False)
|
|
123
|
+
mgr = ConnectionManager(_config())
|
|
124
|
+
monkeypatch.setattr(server, "_get_conn_mgr", lambda: mgr)
|
|
125
|
+
with connect, disconnect:
|
|
126
|
+
out = server.list_all_datastores(target="lab")
|
|
127
|
+
for word in ROLE_WORDS:
|
|
128
|
+
assert word in out["error"]
|
|
129
|
+
# The remedy sits at the end of the message; the 300-character cap cut it.
|
|
130
|
+
assert "vmware-storage doctor" in out["error"]
|
|
131
|
+
# The hint points at the role check, not at connectivity, which is fine.
|
|
132
|
+
assert "session" in out["hint"] and "verify connectivity" not in out["hint"]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.mark.unit
|
|
136
|
+
def test_cli_handler_prints_the_role_message(fake_login, monkeypatch, capsys):
|
|
137
|
+
from vmware_storage import cli
|
|
138
|
+
|
|
139
|
+
_, connect, disconnect = fake_login(has_session=False)
|
|
140
|
+
monkeypatch.setattr(cli, "load_config", lambda path=None: _config())
|
|
141
|
+
|
|
142
|
+
@cli.handle_cli_errors
|
|
143
|
+
def run():
|
|
144
|
+
cli._get_connection("lab")
|
|
145
|
+
|
|
146
|
+
with connect, disconnect, pytest.raises(typer.Exit):
|
|
147
|
+
run()
|
|
148
|
+
out = " ".join(capsys.readouterr().out.split())
|
|
149
|
+
for word in ROLE_WORDS:
|
|
150
|
+
assert word in out
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── NotAuthenticated raised later: expired session, or a role removed ─────────
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _says_both(text: str) -> None:
|
|
157
|
+
text = " ".join(text.split())
|
|
158
|
+
assert "expired" in text and "run the command again" in text
|
|
159
|
+
assert "no vSphere role" in text and "Read-Only" in text
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@pytest.mark.unit
|
|
163
|
+
def test_cli_not_authenticated_says_both_causes(capsys):
|
|
164
|
+
from vmware_storage.cli import handle_cli_errors
|
|
165
|
+
|
|
166
|
+
@handle_cli_errors
|
|
167
|
+
def boom():
|
|
168
|
+
raise vim.fault.NotAuthenticated()
|
|
169
|
+
|
|
170
|
+
with pytest.raises(typer.Exit):
|
|
171
|
+
boom()
|
|
172
|
+
_says_both(capsys.readouterr().out)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@pytest.mark.unit
|
|
176
|
+
def test_mcp_not_authenticated_says_both_causes(monkeypatch):
|
|
177
|
+
def _boom(target=None):
|
|
178
|
+
raise vim.fault.NotAuthenticated()
|
|
179
|
+
|
|
180
|
+
monkeypatch.setattr(server, "_get_connection", _boom)
|
|
181
|
+
out = server.list_all_datastores(target="lab")
|
|
182
|
+
_says_both(out["error"])
|
|
183
|
+
assert "operation failed" not in out["error"]
|
|
184
|
+
assert "session" in out["hint"] and "verify connectivity" not in out["hint"]
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@pytest.mark.unit
|
|
188
|
+
def test_get_access_info_hint_names_the_role_check(fake_login, monkeypatch):
|
|
189
|
+
_, connect, disconnect = fake_login(has_session=False)
|
|
190
|
+
mgr = ConnectionManager(_config())
|
|
191
|
+
monkeypatch.setattr(server, "_get_conn_mgr", lambda: mgr)
|
|
192
|
+
with connect, disconnect:
|
|
193
|
+
out = server.get_access_info(target="lab")
|
|
194
|
+
for word in ROLE_WORDS:
|
|
195
|
+
assert word in out["error"]
|
|
196
|
+
assert "session" in out["hint"] and "verify connectivity" not in out["hint"]
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
@pytest.mark.unit
|
|
200
|
+
def test_write_tool_error_reply_hint_names_the_role_check():
|
|
201
|
+
out = server._error_reply(vim.fault.NotAuthenticated(), "storage")
|
|
202
|
+
assert "session" in out["hint"] and "verify connectivity" not in out["hint"]
|
|
203
|
+
other = server._error_reply(TimeoutError("slow"), "storage")
|
|
204
|
+
assert "verify connectivity" in other["hint"]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
@pytest.mark.unit
|
|
208
|
+
def test_doctor_row_is_not_a_class_name(fake_login, monkeypatch):
|
|
209
|
+
"""The row must carry the text, not 'NoVsphereRoleError'."""
|
|
210
|
+
_, connect, disconnect = fake_login(has_session=False)
|
|
211
|
+
monkeypatch.setattr(type(doctor.resolve_config_path()), "exists", lambda self: True)
|
|
212
|
+
monkeypatch.setattr("vmware_storage.config.load_config", _config)
|
|
213
|
+
with connect, disconnect:
|
|
214
|
+
_, row = doctor._check_auth()
|
|
215
|
+
assert "NoVsphereRoleError" not in row
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""HLD §18: the role metadata this skill declares matches its real tools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from vmware_storage import policy_roles
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _registry() -> set[str]:
|
|
13
|
+
from vmware_storage.mcp_server.server import mcp
|
|
14
|
+
|
|
15
|
+
return {t.name for t in asyncio.run(mcp.list_tools())}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.unit
|
|
19
|
+
def test_every_declared_name_is_a_real_tool():
|
|
20
|
+
named = set(policy_roles.ESCAPES) | set(getattr(policy_roles, "AKO_TOOLS", ()))
|
|
21
|
+
assert named - _registry() == set(), sorted(named - _registry())
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.mark.unit
|
|
25
|
+
def test_metadata_is_declared_when_policy_has_roles():
|
|
26
|
+
roles = pytest.importorskip("vmware_policy.roles")
|
|
27
|
+
import vmware_storage.mcp_server.server # noqa: F401 (registers the metadata on import)
|
|
28
|
+
|
|
29
|
+
meta = roles.metadata("storage")
|
|
30
|
+
assert meta is not None and meta.version == 1
|
|
31
|
+
assert meta.escapes == policy_roles.ESCAPES
|
|
32
|
+
assert (meta.resolver is not None) is True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.unit
|
|
36
|
+
def test_the_resolver_uses_the_config_the_connection_manager_holds(monkeypatch):
|
|
37
|
+
"""Review H1/H-1: once the server's connection manager exists, a config edit must
|
|
38
|
+
not move the role check off the config it connects with; before, read the file."""
|
|
39
|
+
import importlib
|
|
40
|
+
import types
|
|
41
|
+
|
|
42
|
+
from vmware_storage import policy_roles as pr
|
|
43
|
+
|
|
44
|
+
server = importlib.import_module("vmware_storage.mcp_server.server")
|
|
45
|
+
assert hasattr(server, "_conn_mgr"), "policy_roles looks up a global the server does not have"
|
|
46
|
+
edited, loads = object(), []
|
|
47
|
+
monkeypatch.setattr(pr, "load_config", lambda: loads.append(1) or edited)
|
|
48
|
+
|
|
49
|
+
monkeypatch.setattr(server, "_conn_mgr", None)
|
|
50
|
+
assert pr._connection_config() is edited and pr._connection_config() is edited
|
|
51
|
+
assert len(loads) == 2 # no manager yet: the file as it is now, every time
|
|
52
|
+
|
|
53
|
+
held = object()
|
|
54
|
+
monkeypatch.setattr(server, "_conn_mgr", types.SimpleNamespace(_config=held))
|
|
55
|
+
assert pr._connection_config() is held # the manager's copy, not the edited file
|
|
@@ -1162,21 +1162,21 @@ wheels = [
|
|
|
1162
1162
|
|
|
1163
1163
|
[[package]]
|
|
1164
1164
|
name = "vmware-policy"
|
|
1165
|
-
version = "1.
|
|
1165
|
+
version = "1.24.0"
|
|
1166
1166
|
source = { registry = "https://pypi.org/simple" }
|
|
1167
1167
|
dependencies = [
|
|
1168
1168
|
{ name = "pyyaml" },
|
|
1169
1169
|
{ name = "rich" },
|
|
1170
1170
|
{ name = "typer" },
|
|
1171
1171
|
]
|
|
1172
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
1172
|
+
sdist = { url = "https://files.pythonhosted.org/packages/4a/62/3a3cb83af77e3dd3c8fc92cb3b7968bfda04a2f71bf0d6f9b72ee3198441/vmware_policy-1.24.0.tar.gz", hash = "sha256:fba12732ccbd4371232c15d9fd014f9f5d697497210aedff2515d1d205ed3577", size = 361302, upload-time = "2026-10-16T07:18:28.317Z" }
|
|
1173
1173
|
wheels = [
|
|
1174
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1174
|
+
{ url = "https://files.pythonhosted.org/packages/3d/f2/fe3d9d384712023916e2cacddc385f14b095509c5e00aea417d1a9550e98/vmware_policy-1.24.0-py3-none-any.whl", hash = "sha256:cf615d0256134f01fb84c39b083649281a66d2c064c2eaa28786a0fa95ec6e58", size = 131460, upload-time = "2026-10-16T07:18:26.907Z" },
|
|
1175
1175
|
]
|
|
1176
1176
|
|
|
1177
1177
|
[[package]]
|
|
1178
1178
|
name = "vmware-storage"
|
|
1179
|
-
version = "2.
|
|
1179
|
+
version = "2.4.0"
|
|
1180
1180
|
source = { editable = "." }
|
|
1181
1181
|
dependencies = [
|
|
1182
1182
|
{ name = "mcp", extra = ["cli"] },
|
|
@@ -1203,7 +1203,7 @@ requires-dist = [
|
|
|
1203
1203
|
{ name = "pyvmomi", specifier = ">=8.0.3.0,<10.0" },
|
|
1204
1204
|
{ name = "rich", specifier = ">=13.0,<15.0" },
|
|
1205
1205
|
{ name = "typer", specifier = ">=0.12,<1.0" },
|
|
1206
|
-
{ name = "vmware-policy", specifier = ">=1.
|
|
1206
|
+
{ name = "vmware-policy", specifier = ">=1.23.0,<2.0" },
|
|
1207
1207
|
]
|
|
1208
1208
|
|
|
1209
1209
|
[package.metadata.requires-dev]
|