vmware-aiops 2.3.2__tar.gz → 2.4.1__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_aiops-2.3.2 → vmware_aiops-2.4.1}/.claude-plugin/plugin.json +1 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.mcp.json +1 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/PKG-INFO +2 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/README-CN.md +1 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/README.md +1 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/RELEASE_NOTES.md +41 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/openclaw.plugin.json +1 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/package.json +1 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/pyproject.toml +1 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/server.json +2 -2
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/SKILL.md +2 -2
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/setup-guide.md +6 -5
- vmware_aiops-2.4.1/tests/eval/regression/test_browse_ds_path_join.py +79 -0
- vmware_aiops-2.4.1/tests/eval/regression/test_browse_sees_every_file.py +70 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_cli_writes_guarded.py +3 -1
- vmware_aiops-2.4.1/tests/eval/regression/test_deploy_failures_are_failures.py +205 -0
- vmware_aiops-2.4.1/tests/eval/regression/test_error_hints_fit_the_surface.py +85 -0
- vmware_aiops-2.4.1/tests/eval/regression/test_image_registry_per_target.py +64 -0
- vmware_aiops-2.4.1/tests/eval/regression/test_linked_clone_failure_on_the_wire.py +71 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_list_envelope_conformance.py +1 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_snapshot_async.py +6 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_v1_5_25_blockers.py +8 -3
- vmware_aiops-2.4.1/tests/eval/regression/test_write_round_low_findings.py +104 -0
- vmware_aiops-2.4.1/tests/test_composite_tools_obey_policy.py +218 -0
- vmware_aiops-2.4.1/tests/test_plan_steps_obey_policy.py +299 -0
- vmware_aiops-2.4.1/tests/test_policy_roles.py +117 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/uv.lock +8 -8
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/__init__.py +1 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/deploy.py +26 -2
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/vm.py +4 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/config.py +7 -2
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/_shared.py +42 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/datastore.py +13 -3
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/deploy.py +24 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/guest.py +5 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/plan.py +62 -10
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/ttl.py +3 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/vm.py +3 -2
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/cluster_mgmt.py +4 -1
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/datastore_browser.py +89 -18
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/inventory.py +4 -3
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/ova_deploy.py +44 -11
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/plan_gate.py +73 -7
- vmware_aiops-2.4.1/vmware_aiops/ops/step_policy.py +154 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/vm_deploy.py +86 -23
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/vm_gate.py +9 -4
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/vm_lifecycle.py +78 -36
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/policy_environment.py +3 -0
- vmware_aiops-2.4.1/vmware_aiops/policy_roles.py +138 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.clawhubignore +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.env.example +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/dependabot.yml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.gitignore +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/Dockerfile +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/LICENSE +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/SECURITY.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/config.example.yaml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docker-compose.yml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/continue.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/cursor.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/goose.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/localcowork.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/mcp-agent.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/vscode-copilot.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/plans/2026-03-09-plan-apply-design.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/deploy.yaml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/README.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/claude-code.json +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/continue.yaml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/cursor.json +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/goose.json +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/localcowork.json +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/mcp-agent.yaml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/vscode-copilot.json +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/ollama-local-setup.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/evals/evals.json +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/agent-guardrails.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/capabilities.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/cli-reference.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/investigation-protocol.md +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/smithery.yaml +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/conftest.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/_family.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/_scores.json +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/_scoring.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/_skill.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/conftest.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/_diag_fakes.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/_pc_fakes.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_aiops_specific.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_alarm_health_scale_n1.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_cli_destructive_guards.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_cluster_health_summary_delegation.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_code_review_fixes.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_config_path_resolution_is_shared.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_daemon_calls_are_audited.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_daemon_host_log_cycles.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_deploy_correctness.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_doctor_authenticates_every_target.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_documented_gates_match_the_registry.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_enum_params_match_implementation.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_env_secret_obfuscation.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_event_sets_match_real_class_names.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_events_are_read_newest_first.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_guest_download_writes_the_local_disk.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_guest_username_is_explicit.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_host_log_scan_reads_real_logs.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_host_paths_protected.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_instructions_name_targets.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_inventory_scale_issue31.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_list_plans_is_non_destructive.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_login_without_vsphere_role.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_mcp_frame_marks_failures.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_ova_network_mapping.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_release_blockers.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_returned_failure_is_audited.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_session_probe_eviction.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_set_ttl_safety.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_text_io_declares_encoding.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_tool_annotations_match_behaviour.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_unreachable_host_vmks.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_vim_attribute_conformance.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_access_info.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_cluster_mgmt_sanitize.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_cluster_ttl_plan_gate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_concurrent_local_state.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_config.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_drs_rules.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_gate_review_fixes.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_guest_tools_gate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_host_network_mgmt.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_init_and_onboarding.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_network_mgmt.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_no_destructive_ops.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_plan_gate_narrow_fixes.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_plan_rollback_final_fixes.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_reset_alarm_gate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_results_name_their_target.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_session_pool.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_stop_signal_runs_atexit.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_vm_delete_blast_radius.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_vm_tools_gate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/_fsutil.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/access_requirements.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/_common.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/_root.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/access.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/alarm.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/cluster.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/doctor.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/hub.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/investigate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/mcp_config.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/plan.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/scan.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/summary.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/connection.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/doctor.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/init_wizard.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/__main__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/server.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/access.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/alarm.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/cluster.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/network.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/summary.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/notify/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/notify/audit.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/notify/logger.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/notify/webhook.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/_paging.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/alarm_mgmt.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/cluster_gate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/gate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/guest_gate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/guest_ops.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/health.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/host_network_mgmt.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/iscsi_config.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/network_mgmt.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/plan_executor.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/planner.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/ttl.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/vm_delete_gate.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/scanner/__init__.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/scanner/alarm_scanner.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/scanner/log_scanner.py +0 -0
- {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/scanner/scheduler.py +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
|
|
3
3
|
"name": "vmware-aiops",
|
|
4
4
|
"displayName": "VMware AIops",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.4.1",
|
|
6
6
|
"description": "AI-powered VMware vCenter/ESXi VM lifecycle and deployment with 61 MCP tools.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Wei Zhou",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vmware-aiops
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.1
|
|
4
4
|
Summary: VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool
|
|
5
5
|
Project-URL: Homepage, https://github.com/vmware-skills/VMware-AIops
|
|
6
6
|
Project-URL: Repository, https://github.com/vmware-skills/VMware-AIops
|
|
@@ -1204,6 +1204,7 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
|
|
|
1204
1204
|
(default 2) sessions per target; each session is a login on the target, and a call waits at most 300 s for one.
|
|
1205
1205
|
- **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
|
|
1206
1206
|
nothing on the platform. Run one endpoint per privilege level.
|
|
1207
|
+
- **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".
|
|
1207
1208
|
|
|
1208
1209
|
**Offer only read tools, plus the writes you choose:**
|
|
1209
1210
|
|
|
@@ -843,6 +843,7 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
|
|
|
843
843
|
个会话;每个会话都是在该 target 上的一次登录,一个调用最多等待 300 秒拿到会话。
|
|
844
844
|
- **能连到这个端点的人,都以该 server 配置的账号操作。** token 只用来识别调用者,不在平台上授予任何权限。
|
|
845
845
|
每个权限级别单独起一个端点。
|
|
846
|
+
- **一个端点给多人或多种角色用:** 先运行一次 `vmware-audit roles enable`,再给每个 token 指定角色(`vmware-audit token create 名字 --role reader`,或在 `roles.yaml` 里定义可用的工具和 target;需 vmware-policy ≥ 1.24.0)。角色只能限定这个 token 在本端点上能调用什么,不能超出账号本身的权限。详见 vmware-policy README 的「网络调用者的角色」一节。
|
|
846
847
|
|
|
847
848
|
**只提供读工具,加上你选定的写工具:**
|
|
848
849
|
|
|
@@ -1180,6 +1180,7 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
|
|
|
1180
1180
|
(default 2) sessions per target; each session is a login on the target, and a call waits at most 300 s for one.
|
|
1181
1181
|
- **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
|
|
1182
1182
|
nothing on the platform. Run one endpoint per privilege level.
|
|
1183
|
+
- **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".
|
|
1183
1184
|
|
|
1184
1185
|
**Offer only read tools, plus the writes you choose:**
|
|
1185
1186
|
|
|
@@ -1,3 +1,44 @@
|
|
|
1
|
+
## v2.4.1 — fixes from the 2026-10-17 real-hardware read and write rounds
|
|
2
|
+
|
|
3
|
+
* **Tools that run other tools' operations obey `rules.yaml` for those tools**, on the MCP and CLI surfaces.
|
|
4
|
+
`restore_vm_clean_slate`, the batch tools, `provision_vm_guest`, and the deploy tools' `power_on` / snapshot /
|
|
5
|
+
CPU-memory follow-ups are refused, with a `denied` audit row, when the policy denies the tool that does that part
|
|
6
|
+
(a deny on `revert_vm_snapshot` used to leave clean slate working; a deny on `power_on_vm` did not stop
|
|
7
|
+
`batch_clone_vms(power_on=True)`). A follow-up is checked only when the call asks for it. `run_vm_guest_command_with_output` is
|
|
8
|
+
checked as `run_vm_guest_command` (and `download_vm_guest_file`) too.
|
|
9
|
+
* **Behaviour change for custom network roles:** a role check reads no arguments, so a role that allows
|
|
10
|
+
`deploy_vm_from_template`, `deploy_vm_from_ova` or `deploy_linked_clone` must now also allow `power_on_vm`,
|
|
11
|
+
`create_vm_snapshot` and `reconfigure_vm` (and one allowing `run_vm_guest_command_with_output` must allow
|
|
12
|
+
`run_vm_guest_command` and `download_vm_guest_file`), or every call is refused. The built-in `reader` and `admin`
|
|
13
|
+
roles are unaffected.
|
|
14
|
+
* **Plan steps obey `rules.yaml`.** Each step of `apply_vm_plan` / `rollback_vm_plan` is authorized by vmware-policy
|
|
15
|
+
under the tool that does the same thing (a `delete_vm` step as `delete_vm`, `power_off` as `power_off_vm`, the
|
|
16
|
+
rollback that deletes a VM the plan created as `delete_vm`), at that tool's risk level — in the preview and again
|
|
17
|
+
just before the step runs, with a `denied` audit row. A deny on `delete_vm` used to stop the tool but not a plan step.
|
|
18
|
+
* **Failures are failures.** Deploy, clone, linked clone, template conversion, ISO attach, `create_vm`, `migrate_vm`,
|
|
19
|
+
`reconfigure_vm` and `configure_cluster` with nothing to change returned their refusals as status strings, which
|
|
20
|
+
the MCP frame and the audit recorded as success; they now raise. A plan that stops part-way, a rollback that leaves
|
|
21
|
+
a step not undone, and a plan that fails validation now carry the family `error` key.
|
|
22
|
+
* **`deploy_vm_from_template` works on a vCenter with standalone hosts**: the clone gets the template's resource
|
|
23
|
+
pool (it failed with `spec.location.pool`), and the tool now takes `target_host`.
|
|
24
|
+
* **`scan_all_datastore_images` keeps each target's images**: the cache was one list replaced by every scan, so
|
|
25
|
+
scanning `datastore1` on an ESXi host erased what a vCenter scan had found. Entries now carry `target`; a corrupt
|
|
26
|
+
cache file is rebuilt by the next scan and explained by the error instead of a bare JSON error.
|
|
27
|
+
* **`browse_datastore` sees every file** (`.ova`, `.ovf`, `.vmx`, `.log` were filtered out), `ds_path` is well formed
|
|
28
|
+
for ESXi and vCenter folder paths, and a leading `/` is refused with the corrected path, as vmware-storage does.
|
|
29
|
+
* Error text names MCP tools and parameters (CLI form in brackets); doctor is suggested only for connectivity errors;
|
|
30
|
+
a CPU-feature vMotion refusal leads with the remedy; `get_vm_task_status` no longer says an unknown id means the
|
|
31
|
+
task finished; powering off a VM that is already off is a plain `noop`.
|
|
32
|
+
|
|
33
|
+
## v2.4.0 — roles for network callers (HLD §18)
|
|
34
|
+
|
|
35
|
+
* Declares the role metadata vmware-policy ≥ 1.24.0 needs to apply **roles** to this server over HTTPS
|
|
36
|
+
(HLD §18): how a missing target resolves, the guest and host-file tools that escape a role, and what each composite tool can run — `apply_vm_plan` and `rollback_vm_plan` reach every tool a plan step maps to plus `delete_vm` (rollback), `set_vm_ttl` reaches `delete_vm`, the batch tools reach the deploy and clone tools and the follow-up steps their arguments ask for (`reconfigure_vm`, `create_vm_snapshot`, `power_on_vm`, and `attach_iso_to_vm` for a deployment spec); a test derives these from what the batch code calls. A role may allow a composite only if it allows all of them. A test fails when a new plan action is not mapped to a tool. With an older vmware-policy nothing changes. Once an operator runs
|
|
37
|
+
`vmware-audit roles enable`, a token's role decides which tools and targets it may use here; a refused call is
|
|
38
|
+
audited as `denied`. See vmware-policy's README, "Roles for network callers".
|
|
39
|
+
* 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.
|
|
40
|
+
* README and setup guide: one endpoint for several people or roles (run `vmware-audit roles enable` first).
|
|
41
|
+
|
|
1
42
|
## v2.3.2 — tools may not touch this server's own control files
|
|
2
43
|
|
|
3
44
|
* **Security fix (HLD §18 step 0).** guest file download/upload, OVA and deployment-spec paths could name this server's own files, so over HTTPS a token holder could
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/vmware-skills/VMware-AIops",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.4.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-aiops",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.4.1",
|
|
16
16
|
"runtimeHint": "uvx",
|
|
17
17
|
"packageArguments": [
|
|
18
18
|
{
|
|
@@ -63,7 +63,7 @@ Read before connecting an agent. Per-tool inventory: `references/capabilities.md
|
|
|
63
63
|
## Quick Install
|
|
64
64
|
|
|
65
65
|
```bash
|
|
66
|
-
uv tool install vmware-aiops==2.
|
|
66
|
+
uv tool install vmware-aiops==2.4.1
|
|
67
67
|
vmware-aiops doctor
|
|
68
68
|
vmware-aiops hub status # see which family members are installed
|
|
69
69
|
```
|
|
@@ -303,7 +303,7 @@ The account has no vSphere role. Assign one (Read-Only or higher) on the vCenter
|
|
|
303
303
|
## Setup
|
|
304
304
|
|
|
305
305
|
```bash
|
|
306
|
-
uv tool install vmware-aiops==2.
|
|
306
|
+
uv tool install vmware-aiops==2.4.1
|
|
307
307
|
mkdir -p ~/.vmware-aiops
|
|
308
308
|
vmware-aiops init # generates config.yaml and .env templates
|
|
309
309
|
chmod 600 ~/.vmware-aiops/.env
|
|
@@ -6,13 +6,13 @@ All install methods fetch from the same source: [github.com/vmware-skills/VMware
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# Via PyPI (recommended for version pinning)
|
|
9
|
-
uv tool install vmware-aiops==2.
|
|
9
|
+
uv tool install vmware-aiops==2.4.1
|
|
10
10
|
|
|
11
11
|
# Via Skills.sh (fetches from GitHub)
|
|
12
|
-
npx skills add vmware-skills/VMware-AIops#v2.
|
|
12
|
+
npx skills add vmware-skills/VMware-AIops#v2.4.1
|
|
13
13
|
|
|
14
14
|
# Via ClawHub (fetches from ClawHub registry snapshot of GitHub)
|
|
15
|
-
clawhub install @zw008/vmware-aiops --version 2.
|
|
15
|
+
clawhub install @zw008/vmware-aiops --version 2.4.1
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
### Claude Code
|
|
@@ -35,7 +35,7 @@ claude mcp add vmware-aiops -- vmware-aiops mcp
|
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
# 1. Install from PyPI (source: github.com/vmware-skills/VMware-AIops)
|
|
38
|
-
uv tool install vmware-aiops==2.
|
|
38
|
+
uv tool install vmware-aiops==2.4.1
|
|
39
39
|
|
|
40
40
|
# 2. Verify installation source
|
|
41
41
|
vmware-aiops --version # confirms installed version
|
|
@@ -73,7 +73,7 @@ The `vmware-aiops` package installs a Python CLI binary and its dependencies (py
|
|
|
73
73
|
## Development Install
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
git clone --branch v2.
|
|
76
|
+
git clone --branch v2.4.1 https://github.com/vmware-skills/VMware-AIops.git
|
|
77
77
|
cd VMware-AIops
|
|
78
78
|
uv venv && source .venv/bin/activate
|
|
79
79
|
# --no-sources: pyproject's [tool.uv.sources] points vmware-monitor at a sibling
|
|
@@ -245,4 +245,5 @@ vmware-aiops mcp --transport https --host 0.0.0.0 --port 8443 \
|
|
|
245
245
|
- **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
|
|
246
246
|
nothing on the platform. Run one endpoint per privilege level (for example a read-only account behind
|
|
247
247
|
`--expose read`, and a separate endpoint for operators).
|
|
248
|
+
- **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".
|
|
248
249
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""browse_datastore must return a ds_path that deploy/attach tools can use.
|
|
2
|
+
|
|
3
|
+
Regression source: real ESXi 8 host (home-esxi, 2026-10-17). The browse task
|
|
4
|
+
reports each folder's ``folderPath`` with no trailing separator —
|
|
5
|
+
``"[datastore1]"`` for the root, ``"[datastore1] backup"`` for a folder —
|
|
6
|
+
and the old code glued the file name straight on, producing
|
|
7
|
+
``"[datastore1] backupVMware-VCSA.iso"`` and ``"[datastore1]Hermers - TT"``.
|
|
8
|
+
Agents pass that string into deploy_vm_from_ova / attach_iso_to_vm, so a
|
|
9
|
+
malformed path breaks the very flow the browse exists for. The earlier tests
|
|
10
|
+
mocked ``"[ds1] images/"`` (trailing slash), the one shape that hid the bug.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from pyVmomi import vim
|
|
18
|
+
|
|
19
|
+
from vmware_aiops.ops import datastore_browser as dsb
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.unit
|
|
23
|
+
@pytest.mark.parametrize(
|
|
24
|
+
("folder", "name", "expected"),
|
|
25
|
+
[
|
|
26
|
+
# Root, as ESXi reports it: no space after the bracket.
|
|
27
|
+
("[datastore1]", "Hermers - TT", "[datastore1] Hermers - TT"),
|
|
28
|
+
("[datastore1]", "x.iso", "[datastore1] x.iso"),
|
|
29
|
+
# Root, as vCenter reports it: trailing space.
|
|
30
|
+
("[datastore1] ", "x.iso", "[datastore1] x.iso"),
|
|
31
|
+
# Folder without a trailing slash (ESXi).
|
|
32
|
+
("[datastore1] backup", "x.iso", "[datastore1] backup/x.iso"),
|
|
33
|
+
(
|
|
34
|
+
"[datastore1] Hermers - TT",
|
|
35
|
+
"Hermers - TT.vmdk",
|
|
36
|
+
"[datastore1] Hermers - TT/Hermers - TT.vmdk",
|
|
37
|
+
),
|
|
38
|
+
("[datastore1] a/b", "x.iso", "[datastore1] a/b/x.iso"),
|
|
39
|
+
# Folder with a trailing slash (vCenter) — must not double it.
|
|
40
|
+
("[datastore1] backup/", "x.iso", "[datastore1] backup/x.iso"),
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
def test_join_ds_path(folder, name, expected):
|
|
44
|
+
assert dsb._join_ds_path(folder, name) == expected
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.unit
|
|
48
|
+
def test_browse_root_returns_well_formed_paths_for_every_folder(monkeypatch):
|
|
49
|
+
"""Root browse is recursive: subfolder rows come back too, each well formed."""
|
|
50
|
+
def _file(path):
|
|
51
|
+
return SimpleNamespace(path=path, fileSize=1024 * 1024, modification="2026-10-17")
|
|
52
|
+
|
|
53
|
+
results = [
|
|
54
|
+
SimpleNamespace(folderPath="[datastore1]", file=[_file("backup")]),
|
|
55
|
+
SimpleNamespace(folderPath="[datastore1] backup", file=[_file("x.iso")]),
|
|
56
|
+
]
|
|
57
|
+
seen = {}
|
|
58
|
+
|
|
59
|
+
def fake_search(datastorePath, searchSpec): # noqa: N803 — pyVmomi API names
|
|
60
|
+
seen["path"] = datastorePath
|
|
61
|
+
return SimpleNamespace(
|
|
62
|
+
info=SimpleNamespace(state=vim.TaskInfo.State.success, result=results, error=None)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
monkeypatch.setattr(
|
|
66
|
+
dsb,
|
|
67
|
+
"find_datastore_by_name",
|
|
68
|
+
lambda si, name: SimpleNamespace(
|
|
69
|
+
browser=SimpleNamespace(SearchDatastoreSubFolders_Task=fake_search)
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
rows = dsb.browse_datastore(object(), "datastore1")["items"]
|
|
74
|
+
|
|
75
|
+
assert seen["path"] == "[datastore1]"
|
|
76
|
+
assert {r["name"]: r["ds_path"] for r in rows} == {
|
|
77
|
+
"backup": "[datastore1] backup",
|
|
78
|
+
"x.iso": "[datastore1] backup/x.iso",
|
|
79
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""browse_datastore must see every file, and treat a leading '/' one way.
|
|
2
|
+
|
|
3
|
+
Real hardware, 2026-10-17: vmware-aiops' search spec listed only IsoImageQuery,
|
|
4
|
+
VmDiskQuery and FolderQuery under a comment saying "include all file types".
|
|
5
|
+
A DatastoreBrowser query is a filter, not a hint -- with those three, .ova /
|
|
6
|
+
.ovf / .vmx / .log files are never returned. On the lab vCenter it found 0 of
|
|
7
|
+
the 2 OVAs on 'datastore1 (1)' that vmware-storage (which also passes the
|
|
8
|
+
generic Query) found, so scan_all_datastore_images could not do the one job its
|
|
9
|
+
description gives it: finding an OVA for deploy_vm_from_ova.
|
|
10
|
+
|
|
11
|
+
The twin also drifted on a leading slash: vmware-aiops passed '/vcsa8' through,
|
|
12
|
+
vmware-storage refused it as "absolute" (its 1.5.35 hardening). Both now refuse
|
|
13
|
+
it, and the error names the corrected path.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from types import SimpleNamespace
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
from pyVmomi import vim
|
|
21
|
+
|
|
22
|
+
from vmware_aiops.ops import datastore_browser as dsb
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _browse(monkeypatch, path=""):
|
|
26
|
+
seen: dict = {}
|
|
27
|
+
|
|
28
|
+
def fake_search(datastorePath, searchSpec): # noqa: N803 -- pyVmomi API names
|
|
29
|
+
seen["path"] = datastorePath
|
|
30
|
+
seen["spec"] = searchSpec
|
|
31
|
+
return SimpleNamespace(
|
|
32
|
+
info=SimpleNamespace(state=vim.TaskInfo.State.success, result=[], error=None)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
monkeypatch.setattr(
|
|
36
|
+
dsb,
|
|
37
|
+
"find_datastore_by_name",
|
|
38
|
+
lambda si, name: SimpleNamespace(
|
|
39
|
+
browser=SimpleNamespace(SearchDatastoreSubFolders_Task=fake_search)
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
dsb.browse_datastore(object(), "datastore1", path=path, pattern="*.ova")
|
|
43
|
+
return seen
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.unit
|
|
47
|
+
def test_search_spec_includes_the_generic_file_query(monkeypatch) -> None:
|
|
48
|
+
spec = _browse(monkeypatch)["spec"]
|
|
49
|
+
# Exact type: every specific query subclasses Query, so isinstance would pass
|
|
50
|
+
# for the three that hid the OVAs.
|
|
51
|
+
assert any(type(q) is vim.host.DatastoreBrowser.Query for q in spec.query)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.unit
|
|
55
|
+
def test_relative_path_is_joined_under_the_datastore(monkeypatch) -> None:
|
|
56
|
+
assert _browse(monkeypatch, "vcsa8")["path"] == "[datastore1] vcsa8"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.unit
|
|
60
|
+
@pytest.mark.parametrize("path", ["/vcsa8", "//vcsa8"])
|
|
61
|
+
def test_leading_slash_is_refused_with_the_fix(monkeypatch, path) -> None:
|
|
62
|
+
with pytest.raises(ValueError, match="path='vcsa8'"):
|
|
63
|
+
_browse(monkeypatch, path)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.unit
|
|
67
|
+
@pytest.mark.parametrize("path", ["../other", "a/../../b", "x\x00y", "\\\\server\\share", "/"])
|
|
68
|
+
def test_traversal_and_odd_paths_are_refused(monkeypatch, path) -> None:
|
|
69
|
+
with pytest.raises(ValueError, match="datastore-relative"):
|
|
70
|
+
_browse(monkeypatch, path)
|
|
@@ -51,7 +51,9 @@ def _ops_refs(tree: ast.AST) -> tuple[dict[str, str], set[str]]:
|
|
|
51
51
|
for n in ast.walk(tree):
|
|
52
52
|
if isinstance(n, ast.ImportFrom) and n.module:
|
|
53
53
|
parts = n.module.split(".")
|
|
54
|
-
|
|
54
|
+
# step_policy authorizes the operation; it is not the operation, and
|
|
55
|
+
# every composite command calls it (2026-10-17).
|
|
56
|
+
if "ops" in parts and parts[-1] != "step_policy":
|
|
55
57
|
if parts[-1] == "ops":
|
|
56
58
|
mods.update(a.asname or a.name for a in n.names)
|
|
57
59
|
else:
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""A deploy that did not happen must not be reported, or audited, as success.
|
|
2
|
+
|
|
3
|
+
Real hardware, 2026-10-17: ``deploy_vm_from_ova`` with a datastore name that
|
|
4
|
+
does not exist returned the string ``"Datastore 'x' not found."``. The ops layer
|
|
5
|
+
*returned* it instead of raising, so ``@tool_errors`` never saw an exception:
|
|
6
|
+
the MCP frame said success, the audit row said ``status=ok``, and the plan
|
|
7
|
+
executor and batch deploy counted the step as done. The same shape ran through
|
|
8
|
+
every early exit in ``vm_deploy`` / ``ova_deploy``.
|
|
9
|
+
|
|
10
|
+
These tests assert the audit status and the raised type -- the returned text
|
|
11
|
+
was never the problem.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from unittest.mock import MagicMock
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from pyVmomi import vim
|
|
20
|
+
from vmware_policy.budget import reset_budget
|
|
21
|
+
from vmware_policy.policy import reset_policy_engine
|
|
22
|
+
from vmware_policy.undo import reset_undo_store
|
|
23
|
+
|
|
24
|
+
import vmware_aiops.mcp_server.tools.deploy as deploy_tools
|
|
25
|
+
from vmware_aiops.ops import ova_deploy, vm_deploy
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@pytest.fixture(autouse=True)
|
|
29
|
+
def harness(tmp_path, monkeypatch):
|
|
30
|
+
monkeypatch.setenv("OPS_HOME", str(tmp_path))
|
|
31
|
+
reset_policy_engine()
|
|
32
|
+
reset_budget()
|
|
33
|
+
reset_undo_store()
|
|
34
|
+
yield
|
|
35
|
+
reset_policy_engine()
|
|
36
|
+
reset_budget()
|
|
37
|
+
reset_undo_store()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def audit_rows(monkeypatch):
|
|
42
|
+
rows: list[dict] = []
|
|
43
|
+
|
|
44
|
+
class _Recorder:
|
|
45
|
+
def log(self, **kw):
|
|
46
|
+
rows.append(kw)
|
|
47
|
+
|
|
48
|
+
monkeypatch.setattr("vmware_policy.guard.get_engine", lambda: _Recorder())
|
|
49
|
+
return rows
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.unit
|
|
53
|
+
def test_ova_deploy_to_a_missing_datastore_raises(monkeypatch) -> None:
|
|
54
|
+
monkeypatch.setattr(ova_deploy, "find_datastore_by_name", lambda si, n: None)
|
|
55
|
+
with pytest.raises(vm_deploy.DeployError, match="list_all_datastores"):
|
|
56
|
+
vm_deploy.deploy_ova(MagicMock(), "/x.ova", "vm1", datastore_name="nope")
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.unit
|
|
60
|
+
def test_template_deploy_of_a_missing_template_raises(monkeypatch) -> None:
|
|
61
|
+
monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: None)
|
|
62
|
+
with pytest.raises(vm_deploy.DeployError, match="not found"):
|
|
63
|
+
vm_deploy.deploy_from_template(MagicMock(), "tmpl", "vm1")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.unit
|
|
67
|
+
def test_template_deploy_to_a_missing_datastore_raises(monkeypatch) -> None:
|
|
68
|
+
tmpl = MagicMock()
|
|
69
|
+
tmpl.config.template = True
|
|
70
|
+
tmpl.runtime.host.parent.resourcePool = vim.ResourcePool("resgroup-1", None)
|
|
71
|
+
monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: tmpl)
|
|
72
|
+
monkeypatch.setattr(vm_deploy, "find_datastore_by_name", lambda si, n: None)
|
|
73
|
+
with pytest.raises(vm_deploy.DeployError, match="list_all_datastores"):
|
|
74
|
+
vm_deploy.deploy_from_template(MagicMock(), "tmpl", "vm1", datastore_name="nope")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@pytest.mark.unit
|
|
78
|
+
def test_linked_clone_of_a_missing_source_raises(monkeypatch) -> None:
|
|
79
|
+
monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: None)
|
|
80
|
+
with pytest.raises(vm_deploy.DeployError):
|
|
81
|
+
vm_deploy.linked_clone(MagicMock(), "src", "vm1", snapshot_name="s")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.unit
|
|
85
|
+
def test_ova_tool_failure_is_audited_as_error(audit_rows, monkeypatch) -> None:
|
|
86
|
+
monkeypatch.setattr(deploy_tools, "_get_connection", lambda target=None: MagicMock())
|
|
87
|
+
monkeypatch.setattr(ova_deploy, "find_datastore_by_name", lambda si, n: None)
|
|
88
|
+
|
|
89
|
+
returned = deploy_tools.deploy_vm_from_ova("/x.ova", "vm1", "nope", target="t")
|
|
90
|
+
|
|
91
|
+
assert returned.startswith("Error:")
|
|
92
|
+
assert "nope" in returned
|
|
93
|
+
assert audit_rows and audit_rows[0]["status"] == "error"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.mark.unit
|
|
97
|
+
def test_template_tool_failure_is_audited_as_error(audit_rows, monkeypatch) -> None:
|
|
98
|
+
monkeypatch.setattr(deploy_tools, "_get_connection", lambda target=None: MagicMock())
|
|
99
|
+
monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: None)
|
|
100
|
+
|
|
101
|
+
returned = deploy_tools.deploy_vm_from_template("tmpl", "vm1", target="t")
|
|
102
|
+
|
|
103
|
+
assert returned.startswith("Error:")
|
|
104
|
+
assert audit_rows and audit_rows[0]["status"] == "error"
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.unit
|
|
108
|
+
def test_batch_deploy_marks_a_failed_vm_as_error(monkeypatch) -> None:
|
|
109
|
+
"""A failed template deploy used to land in 'messages' with status ok."""
|
|
110
|
+
monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: None)
|
|
111
|
+
spec = {"template": "tmpl", "vms": [{"name": "vm1"}]}
|
|
112
|
+
monkeypatch.setattr(vm_deploy, "load_deploy_spec", lambda p: spec)
|
|
113
|
+
|
|
114
|
+
results = vm_deploy.batch_deploy(MagicMock(), "spec.yaml")
|
|
115
|
+
|
|
116
|
+
assert results[0]["status"] == "error"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@pytest.mark.unit
|
|
120
|
+
def test_create_vm_on_a_missing_datastore_raises(monkeypatch) -> None:
|
|
121
|
+
"""The same shape one module over: create_vm returned this string too."""
|
|
122
|
+
from vmware_aiops.ops import inventory, vm_lifecycle
|
|
123
|
+
|
|
124
|
+
monkeypatch.setattr(vm_lifecycle, "resolve_datacenter", lambda si, *a, **k: MagicMock())
|
|
125
|
+
monkeypatch.setattr(vm_lifecycle, "find_compute_resource", lambda *a, **k: MagicMock())
|
|
126
|
+
monkeypatch.setattr(vm_lifecycle, "find_datastore_by_name", lambda si, n: None)
|
|
127
|
+
with pytest.raises(inventory.InventoryError, match="list_all_datastores"):
|
|
128
|
+
vm_lifecycle.create_vm(MagicMock(), "vm1", datastore_name="nope")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ── 2026-10-17 write tests on home-vcenter (second batch) ────────────────────
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@pytest.mark.unit
|
|
135
|
+
@pytest.mark.parametrize(
|
|
136
|
+
"kwargs, says",
|
|
137
|
+
[
|
|
138
|
+
({"target_host": "esx-gone"}, "list_esxi_hosts"),
|
|
139
|
+
({"target_datastore": "ds-gone"}, "list_all_datastores"),
|
|
140
|
+
],
|
|
141
|
+
)
|
|
142
|
+
def test_clone_to_a_missing_host_or_datastore_raises(monkeypatch, kwargs, says) -> None:
|
|
143
|
+
"""Its hints also named CLI commands that do not exist
|
|
144
|
+
('vmware-aiops cluster list-hosts', 'vmware-aiops datastore list')."""
|
|
145
|
+
from vmware_aiops.ops import inventory, vm_lifecycle
|
|
146
|
+
|
|
147
|
+
monkeypatch.setattr(vm_lifecycle, "_require_vm", lambda si, n: MagicMock())
|
|
148
|
+
monkeypatch.setattr(vm_lifecycle, "find_host_by_name", lambda si, n: None)
|
|
149
|
+
monkeypatch.setattr(vm_lifecycle, "find_datastore_by_name", lambda si, n: None)
|
|
150
|
+
with pytest.raises(inventory.InventoryError, match=says) as info:
|
|
151
|
+
vm_lifecycle.clone_vm(MagicMock(), "src", "new", **kwargs)
|
|
152
|
+
assert "list-hosts" not in str(info.value) and "datastore list" not in str(info.value)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.mark.unit
|
|
156
|
+
def test_reconfigure_with_nothing_to_change_raises(monkeypatch) -> None:
|
|
157
|
+
from vmware_aiops.ops import vm_lifecycle
|
|
158
|
+
|
|
159
|
+
monkeypatch.setattr(vm_lifecycle, "_require_vm", lambda si, n: MagicMock())
|
|
160
|
+
with pytest.raises(ValueError, match="memory_mb"):
|
|
161
|
+
vm_lifecycle.reconfigure_vm(MagicMock(), "vm1")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _template_on_standalone_host(monkeypatch):
|
|
165
|
+
pool = vim.ResourcePool("resgroup-standalone", None)
|
|
166
|
+
tmpl = MagicMock(name="template")
|
|
167
|
+
tmpl.config.template = True
|
|
168
|
+
tmpl.runtime.host.parent.resourcePool = pool
|
|
169
|
+
seen = {}
|
|
170
|
+
|
|
171
|
+
def clone(folder, name, spec):
|
|
172
|
+
seen["spec"] = spec
|
|
173
|
+
return MagicMock()
|
|
174
|
+
|
|
175
|
+
tmpl.Clone = clone
|
|
176
|
+
monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: tmpl)
|
|
177
|
+
monkeypatch.setattr(vm_deploy, "_wait_for_task", lambda *a, **k: None)
|
|
178
|
+
return pool, seen
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@pytest.mark.unit
|
|
182
|
+
def test_template_deploy_without_a_host_names_the_templates_pool(monkeypatch) -> None:
|
|
183
|
+
"""vCenter with standalone hosts: 'A specified parameter was not correct:
|
|
184
|
+
spec.location.pool' on every call, because the pool was only set when
|
|
185
|
+
target_host was passed and the MCP tool did not expose target_host."""
|
|
186
|
+
pool, seen = _template_on_standalone_host(monkeypatch)
|
|
187
|
+
|
|
188
|
+
vm_deploy.deploy_from_template(MagicMock(), "tmpl", "vm1")
|
|
189
|
+
|
|
190
|
+
assert seen["spec"].location.pool is pool
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@pytest.mark.unit
|
|
194
|
+
def test_template_tool_exposes_target_host(monkeypatch) -> None:
|
|
195
|
+
import inspect
|
|
196
|
+
|
|
197
|
+
assert "target_host" in inspect.signature(deploy_tools.deploy_vm_from_template).parameters
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@pytest.mark.unit
|
|
201
|
+
def test_configure_cluster_with_nothing_to_change_raises() -> None:
|
|
202
|
+
from vmware_aiops.ops import cluster_mgmt
|
|
203
|
+
|
|
204
|
+
with pytest.raises(ValueError, match="drs_behavior"):
|
|
205
|
+
cluster_mgmt.configure_cluster(MagicMock(), "c1")
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Error hints must fit the error, and the surface that shows them.
|
|
2
|
+
|
|
3
|
+
Real hardware, 2026-10-17: every MCP error carried "Run 'vmware-aiops doctor'
|
|
4
|
+
to verify connectivity and credentials." -- also for a name that does not
|
|
5
|
+
resolve, where connectivity is fine and the message already says what to change.
|
|
6
|
+
And messages an agent reads said "Pass --target" / "Poll with: vm task-status
|
|
7
|
+
... --target", a CLI flag and a CLI command; the MCP names are the ``target``
|
|
8
|
+
parameter and ``get_vm_task_status``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from vmware_aiops.config import AppConfig
|
|
16
|
+
from vmware_aiops.mcp_server import _shared
|
|
17
|
+
from vmware_aiops.ops.ova_deploy import DeployError
|
|
18
|
+
from vmware_aiops.ops.vm_lifecycle import TaskStillRunning, VMNotFoundError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _unknown_target():
|
|
22
|
+
from vmware_aiops.config import UnknownTargetError
|
|
23
|
+
|
|
24
|
+
return UnknownTargetError("Target 'x' not found.")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.mark.unit
|
|
28
|
+
@pytest.mark.parametrize(
|
|
29
|
+
"exc", [ValueError("bad"), _unknown_target(), VMNotFoundError("x"), DeployError("x")]
|
|
30
|
+
)
|
|
31
|
+
def test_an_authored_request_error_does_not_send_the_agent_to_doctor(exc) -> None:
|
|
32
|
+
assert "doctor" not in _shared._hint_for(exc)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.mark.unit
|
|
36
|
+
@pytest.mark.parametrize("exc", [ConnectionError("x"), TimeoutError("x"), RuntimeError("x")])
|
|
37
|
+
def test_connectivity_and_unplanned_errors_keep_the_doctor_hint(exc) -> None:
|
|
38
|
+
assert _shared._hint_for(exc) == _shared._DOCTOR_HINT
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@pytest.mark.unit
|
|
42
|
+
def test_unknown_target_names_the_mcp_parameter() -> None:
|
|
43
|
+
with pytest.raises(KeyError) as info:
|
|
44
|
+
AppConfig(targets=()).get_target("nope")
|
|
45
|
+
assert "target=" in str(info.value)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@pytest.mark.unit
|
|
49
|
+
def test_still_running_names_the_mcp_poll_tool() -> None:
|
|
50
|
+
assert "get_vm_task_status" in str(TaskStillRunning("task-1", 60))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@pytest.mark.unit
|
|
54
|
+
@pytest.mark.parametrize("make", [
|
|
55
|
+
lambda: __import__("vmware_aiops.ops.vm_lifecycle", fromlist=["x"]).TaskFailedError(
|
|
56
|
+
"Task failed: ... | fault=vim.fault.MemoryHotPlugNotSupported"),
|
|
57
|
+
lambda: __import__("vmware_aiops.ops.guest_ops", fromlist=["x"]).GuestOpsError(
|
|
58
|
+
"VMware Tools is not running in the guest"),
|
|
59
|
+
lambda: __import__("vmware_aiops.ops.gate", fromlist=["x"]).GateRefusedError(
|
|
60
|
+
"refused: VMware Tools is not running"),
|
|
61
|
+
])
|
|
62
|
+
def test_task_guest_and_gate_refusals_do_not_send_the_agent_to_doctor(make) -> None:
|
|
63
|
+
"""Write round 2026-10-17: MemoryHotPlugNotSupported and Tools-not-running
|
|
64
|
+
errors said 'Run doctor to verify connectivity and credentials'."""
|
|
65
|
+
assert "doctor" not in _shared._hint_for(make())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.mark.unit
|
|
70
|
+
@pytest.mark.parametrize("exc", [
|
|
71
|
+
__import__("json").JSONDecodeError("Expecting value", "<html>", 0),
|
|
72
|
+
KeyError("summary.capacity"),
|
|
73
|
+
UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid start byte"),
|
|
74
|
+
])
|
|
75
|
+
def test_parse_errors_and_bare_key_errors_keep_the_doctor_hint(exc) -> None:
|
|
76
|
+
"""Independent review 2026-10-17: a reverse proxy answering HTML raises
|
|
77
|
+
JSONDecodeError (a ValueError), and a programming slip raises a bare KeyError;
|
|
78
|
+
neither message says what to change, so they must not be told it does."""
|
|
79
|
+
assert _shared._hint_for(exc) == _shared._DOCTOR_HINT
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.unit
|
|
83
|
+
def test_an_unknown_target_is_still_a_key_error() -> None:
|
|
84
|
+
"""Callers catch KeyError from get_target; the subclass must not break them."""
|
|
85
|
+
assert issubclass(type(_unknown_target()), KeyError)
|