vmware-aiops 2.3.0__tar.gz → 2.3.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.0 → vmware_aiops-2.3.1}/.claude-plugin/plugin.json +1 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.mcp.json +1 -1
- vmware_aiops-2.3.0/README.md → vmware_aiops-2.3.1/PKG-INFO +33 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/README-CN.md +8 -0
- vmware_aiops-2.3.0/PKG-INFO → vmware_aiops-2.3.1/README.md +9 -24
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/RELEASE_NOTES.md +28 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/openclaw.plugin.json +1 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/package.json +1 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/pyproject.toml +3 -3
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/server.json +2 -2
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/skills/vmware-aiops/SKILL.md +9 -7
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/skills/vmware-aiops/references/setup-guide.md +12 -5
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +8 -3
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_deploy_correctness.py +5 -0
- vmware_aiops-2.3.1/tests/eval/regression/test_login_without_vsphere_role.py +225 -0
- vmware_aiops-2.3.1/tests/eval/regression/test_ova_network_mapping.py +107 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/uv.lock +7 -7
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/__init__.py +1 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/_common.py +19 -2
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/deploy.py +4 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/connection.py +32 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/_shared.py +25 -4
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/deploy.py +3 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/summary.py +3 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/ova_deploy.py +51 -2
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.clawhubignore +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.env.example +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.github/dependabot.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/.gitignore +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/Dockerfile +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/LICENSE +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/SECURITY.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/config.example.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/docker-compose.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/docs/integrations/continue.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/docs/integrations/cursor.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/docs/integrations/goose.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/docs/integrations/localcowork.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/docs/integrations/mcp-agent.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/docs/integrations/vscode-copilot.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/docs/plans/2026-03-09-plan-apply-design.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/deploy.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/mcp-configs/README.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/mcp-configs/claude-code.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/mcp-configs/continue.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/mcp-configs/cursor.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/mcp-configs/goose.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/mcp-configs/localcowork.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/mcp-configs/mcp-agent.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/mcp-configs/vscode-copilot.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/examples/ollama-local-setup.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/skills/vmware-aiops/evals/evals.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/skills/vmware-aiops/references/agent-guardrails.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/skills/vmware-aiops/references/capabilities.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/skills/vmware-aiops/references/cli-reference.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/skills/vmware-aiops/references/investigation-protocol.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/smithery.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/conftest.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/_family.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/_scores.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/_scoring.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/_skill.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/conftest.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/_diag_fakes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/_pc_fakes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_aiops_specific.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_alarm_health_scale_n1.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_cli_destructive_guards.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_cli_writes_guarded.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_cluster_health_summary_delegation.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_code_review_fixes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_config_path_resolution_is_shared.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_daemon_calls_are_audited.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_daemon_host_log_cycles.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_doctor_authenticates_every_target.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_documented_gates_match_the_registry.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_enum_params_match_implementation.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_env_secret_obfuscation.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_event_sets_match_real_class_names.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_events_are_read_newest_first.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_guest_download_writes_the_local_disk.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_guest_username_is_explicit.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_host_log_scan_reads_real_logs.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_instructions_name_targets.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_inventory_scale_issue31.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_list_envelope_conformance.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_list_plans_is_non_destructive.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_mcp_frame_marks_failures.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_release_blockers.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_returned_failure_is_audited.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_session_probe_eviction.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_set_ttl_safety.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_snapshot_async.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_text_io_declares_encoding.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_tool_annotations_match_behaviour.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_unreachable_host_vmks.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_v1_5_25_blockers.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/eval/regression/test_vim_attribute_conformance.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_access_info.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_cluster_mgmt_sanitize.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_cluster_ttl_plan_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_concurrent_local_state.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_config.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_drs_rules.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_gate_review_fixes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_guest_tools_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_host_network_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_init_and_onboarding.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_network_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_no_destructive_ops.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_plan_gate_narrow_fixes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_plan_rollback_final_fixes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_reset_alarm_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_results_name_their_target.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_session_pool.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_stop_signal_runs_atexit.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_vm_delete_blast_radius.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/tests/test_vm_tools_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/_fsutil.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/access_requirements.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/_root.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/access.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/alarm.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/cluster.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/doctor.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/hub.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/investigate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/mcp_config.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/plan.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/scan.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/summary.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/cli/vm.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/config.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/doctor.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/init_wizard.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/__main__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/server.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/access.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/alarm.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/cluster.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/datastore.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/guest.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/network.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/plan.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/ttl.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/mcp_server/tools/vm.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/notify/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/notify/audit.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/notify/logger.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/notify/webhook.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/_paging.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/alarm_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/cluster_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/cluster_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/datastore_browser.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/guest_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/guest_ops.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/health.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/host_network_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/inventory.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/iscsi_config.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/network_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/plan_executor.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/plan_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/planner.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/ttl.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/vm_delete_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/vm_deploy.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/vm_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/ops/vm_lifecycle.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/policy_environment.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/scanner/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/scanner/alarm_scanner.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.1}/vmware_aiops/scanner/log_scanner.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.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.3.
|
|
5
|
+
"version": "2.3.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,3 +1,27 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vmware-aiops
|
|
3
|
+
Version: 2.3.1
|
|
4
|
+
Summary: VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool
|
|
5
|
+
Project-URL: Homepage, https://github.com/vmware-skills/VMware-AIops
|
|
6
|
+
Project-URL: Repository, https://github.com/vmware-skills/VMware-AIops
|
|
7
|
+
Project-URL: Issues, https://github.com/vmware-skills/VMware-AIops/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/vmware-skills/VMware-AIops/blob/main/RELEASE_NOTES.md
|
|
9
|
+
Author-email: Wei Zhou <wei-wz.zhou@broadcom.com>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Requires-Dist: apscheduler<4.0,>=3.10
|
|
14
|
+
Requires-Dist: httpx<1.0,>=0.27
|
|
15
|
+
Requires-Dist: mcp[cli]<2.0,>=1.28
|
|
16
|
+
Requires-Dist: pyaml<27.0,>=24.0
|
|
17
|
+
Requires-Dist: python-dotenv<2.0,>=1.0
|
|
18
|
+
Requires-Dist: pyvmomi<10.0,>=8.0.3.0
|
|
19
|
+
Requires-Dist: rich<16.0,>=13.0
|
|
20
|
+
Requires-Dist: typer<1.0,>=0.12
|
|
21
|
+
Requires-Dist: vmware-monitor<3.0,>=2.3.1
|
|
22
|
+
Requires-Dist: vmware-policy<2.0,>=1.23.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
1
25
|
<!-- mcp-name: io.github.vmware-skills/vmware-aiops -->
|
|
2
26
|
# VMware AIops
|
|
3
27
|
|
|
@@ -436,6 +460,9 @@ Run `vmware-aiops plan list` to see failed plan status. Ask user if they want to
|
|
|
436
460
|
1. Verify target is reachable: `vmware-aiops doctor`
|
|
437
461
|
2. For self-signed certs: set `verify_ssl: false` in config.yaml (lab environments only)
|
|
438
462
|
|
|
463
|
+
### `doctor` reports "no vSphere role", or every command fails with "session is not authenticated"
|
|
464
|
+
The account logs in but has no vSphere role, so vCenter gives it no session (AIops#47). Assign it a role (Read-Only or higher) on the vCenter root folder: Administration > Access Control > Global Permissions. Changing the password will not help. `vmware-aiops doctor` reports this case by name, and `attention` counts such a target as unread (overall at least WARN).
|
|
465
|
+
|
|
439
466
|
---
|
|
440
467
|
|
|
441
468
|
## Supported AI Platforms
|
|
@@ -1164,6 +1191,12 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
|
|
|
1164
1191
|
before it binds that unauthenticated requests get 401. Use a certificate your clients trust for those names.
|
|
1165
1192
|
- Behind a TLS-terminating reverse proxy on the same machine use `--transport http --host 127.0.0.1
|
|
1166
1193
|
--public-name mcp.example.com` and pass the `Authorization` header through.
|
|
1194
|
+
- Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
|
|
1195
|
+
fine), list in `--public-name` the host name the balancer sends as `Host`, and have it forward `Authorization`.
|
|
1196
|
+
Its health probe can use `GET /healthz`, which needs no token and returns only `{"status": "ok"}`
|
|
1197
|
+
(vmware-policy ≥ 1.23.0).
|
|
1198
|
+
- Send `traceparent` or `X-Request-ID` with each request to have it stored on the audit row;
|
|
1199
|
+
`vmware-audit log --correlation-id ID` then finds the calls behind one platform request.
|
|
1167
1200
|
- The audit log records each caller as `token:<name>`. Calls run side by side, with up to `VMWARE_MCP_POOL_SIZE`
|
|
1168
1201
|
(default 2) sessions per target; each session is a login on the target, and a call waits at most 300 s for one.
|
|
1169
1202
|
- **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
|
|
@@ -379,6 +379,9 @@ vSphere 中 VM 名称区分大小写。请使用 `vmware-monitor inventory vms`
|
|
|
379
379
|
1. 验证目标可达:`vmware-aiops doctor`
|
|
380
380
|
2. 自签名证书:在 config.yaml 中设置 `verify_ssl: false`(仅限实验环境)
|
|
381
381
|
|
|
382
|
+
### `doctor` 报 "no vSphere role",或所有命令都报 "session is not authenticated"
|
|
383
|
+
账号能登录,但没有任何 vSphere 角色,所以 vCenter 不给它会话(AIops#47)。在 vCenter 根文件夹上给它分配角色(Read-Only 或更高):Administration > Access Control > Global Permissions。改密码没有用。`vmware-aiops doctor` 会直接报出这种情况,`attention` 也会把这样的目标算作未读取(总体状态至少为 WARN)。
|
|
384
|
+
|
|
382
385
|
---
|
|
383
386
|
|
|
384
387
|
## 支持的 AI 平台
|
|
@@ -829,6 +832,11 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
|
|
|
829
832
|
证书要用客户端信任、且覆盖这些名字的证书。
|
|
830
833
|
- 在同一台机器上由反向代理终结 TLS 时,用 `--transport http --host 127.0.0.1 --public-name mcp.example.com`,
|
|
831
834
|
并让代理透传 `Authorization` 头。
|
|
835
|
+
- 负载均衡或网关在另一台机器上时,保持 `--transport https`(自签证书也可以),在 `--public-name` 里写上负载均衡
|
|
836
|
+
转发时带的 `Host` 名,并让它透传 `Authorization`。健康检查可以用 `GET /healthz`:不需要 token,只返回
|
|
837
|
+
`{"status": "ok"}`(需 vmware-policy ≥ 1.23.0)。
|
|
838
|
+
- 每个请求带上 `traceparent` 或 `X-Request-ID`,它会记进审计记录;用 `vmware-audit log --correlation-id ID`
|
|
839
|
+
就能查到平台上某一次请求对应的调用。
|
|
832
840
|
- 审计日志把每个调用者记为 `token:<名字>`。调用并行执行,每个 target 最多 `VMWARE_MCP_POOL_SIZE`(默认 2)
|
|
833
841
|
个会话;每个会话都是在该 target 上的一次登录,一个调用最多等待 300 秒拿到会话。
|
|
834
842
|
- **能连到这个端点的人,都以该 server 配置的账号操作。** token 只用来识别调用者,不在平台上授予任何权限。
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: vmware-aiops
|
|
3
|
-
Version: 2.3.0
|
|
4
|
-
Summary: VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool
|
|
5
|
-
Project-URL: Homepage, https://github.com/vmware-skills/VMware-AIops
|
|
6
|
-
Project-URL: Repository, https://github.com/vmware-skills/VMware-AIops
|
|
7
|
-
Project-URL: Issues, https://github.com/vmware-skills/VMware-AIops/issues
|
|
8
|
-
Project-URL: Changelog, https://github.com/vmware-skills/VMware-AIops/blob/main/RELEASE_NOTES.md
|
|
9
|
-
Author-email: Wei Zhou <wei-wz.zhou@broadcom.com>
|
|
10
|
-
License-Expression: MIT
|
|
11
|
-
License-File: LICENSE
|
|
12
|
-
Requires-Python: >=3.10
|
|
13
|
-
Requires-Dist: apscheduler<4.0,>=3.10
|
|
14
|
-
Requires-Dist: httpx<1.0,>=0.27
|
|
15
|
-
Requires-Dist: mcp[cli]<2.0,>=1.28
|
|
16
|
-
Requires-Dist: pyaml<27.0,>=24.0
|
|
17
|
-
Requires-Dist: python-dotenv<2.0,>=1.0
|
|
18
|
-
Requires-Dist: pyvmomi<10.0,>=8.0.3.0
|
|
19
|
-
Requires-Dist: rich<16.0,>=13.0
|
|
20
|
-
Requires-Dist: typer<1.0,>=0.12
|
|
21
|
-
Requires-Dist: vmware-monitor<3.0,>=2.0.0
|
|
22
|
-
Requires-Dist: vmware-policy<2.0,>=1.22.0
|
|
23
|
-
Description-Content-Type: text/markdown
|
|
24
|
-
|
|
25
1
|
<!-- mcp-name: io.github.vmware-skills/vmware-aiops -->
|
|
26
2
|
# VMware AIops
|
|
27
3
|
|
|
@@ -460,6 +436,9 @@ Run `vmware-aiops plan list` to see failed plan status. Ask user if they want to
|
|
|
460
436
|
1. Verify target is reachable: `vmware-aiops doctor`
|
|
461
437
|
2. For self-signed certs: set `verify_ssl: false` in config.yaml (lab environments only)
|
|
462
438
|
|
|
439
|
+
### `doctor` reports "no vSphere role", or every command fails with "session is not authenticated"
|
|
440
|
+
The account logs in but has no vSphere role, so vCenter gives it no session (AIops#47). Assign it a role (Read-Only or higher) on the vCenter root folder: Administration > Access Control > Global Permissions. Changing the password will not help. `vmware-aiops doctor` reports this case by name, and `attention` counts such a target as unread (overall at least WARN).
|
|
441
|
+
|
|
463
442
|
---
|
|
464
443
|
|
|
465
444
|
## Supported AI Platforms
|
|
@@ -1188,6 +1167,12 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
|
|
|
1188
1167
|
before it binds that unauthenticated requests get 401. Use a certificate your clients trust for those names.
|
|
1189
1168
|
- Behind a TLS-terminating reverse proxy on the same machine use `--transport http --host 127.0.0.1
|
|
1190
1169
|
--public-name mcp.example.com` and pass the `Authorization` header through.
|
|
1170
|
+
- Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
|
|
1171
|
+
fine), list in `--public-name` the host name the balancer sends as `Host`, and have it forward `Authorization`.
|
|
1172
|
+
Its health probe can use `GET /healthz`, which needs no token and returns only `{"status": "ok"}`
|
|
1173
|
+
(vmware-policy ≥ 1.23.0).
|
|
1174
|
+
- Send `traceparent` or `X-Request-ID` with each request to have it stored on the audit row;
|
|
1175
|
+
`vmware-audit log --correlation-id ID` then finds the calls behind one platform request.
|
|
1191
1176
|
- The audit log records each caller as `token:<name>`. Calls run side by side, with up to `VMWARE_MCP_POOL_SIZE`
|
|
1192
1177
|
(default 2) sessions per target; each session is a login on the target, and a call waits at most 300 s for one.
|
|
1193
1178
|
- **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
|
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
## v2.3.1 — an account with no vSphere role is named, not passed (AIops#47)
|
|
2
|
+
|
|
3
|
+
Needs vmware-policy 1.23.0, and the vmware-monitor release with the same fix for the `attention` part.
|
|
4
|
+
|
|
5
|
+
* **A login with no vSphere role fails at connect time with the reason.** vCenter accepts the login but shows no
|
|
6
|
+
session for it, and every call then fails with `NotAuthenticated`. The connection is now closed at once and
|
|
7
|
+
`doctor`, the CLI and MCP tools say the account most likely has no vSphere role and to assign one (Read-Only or
|
|
8
|
+
higher). Reported on vCenter 8.0.3; not yet reproduced on our own hardware.
|
|
9
|
+
* Refused only when two signals agree: no session, **and** vCenter refuses a view of the root folder
|
|
10
|
+
(`NotAuthenticated` / `NoPermission`). An account scoped to one folder can also show no session, and still
|
|
11
|
+
connects; anything the check cannot read lets the login through.
|
|
12
|
+
* **`NotAuthenticated` later on names both causes**: the session expired (retry), or the account's role was
|
|
13
|
+
removed. It no longer says "verify connectivity and credentials".
|
|
14
|
+
* **Every CLI command prints one line for a vSphere fault.** `summary` (and any other command) printed a raw
|
|
15
|
+
traceback for faults other than a failed login; they now print the fault's message and what to check.
|
|
16
|
+
* **`attention` never reports OK for targets it could not read** (computed by vmware-monitor): any unread target
|
|
17
|
+
makes the overall status at least WARN, none read makes it CRITICAL, and the console and HTML say how many were
|
|
18
|
+
not read. A role-less target is listed as unreachable with reason `NoVSphereRoleError`.
|
|
19
|
+
|
|
20
|
+
## Unreleased — OVA deploy honours `network_name`
|
|
21
|
+
|
|
22
|
+
* **`deploy_vm_from_ova` / `deploy ova --network` now attach the NICs to the network you name.** The import spec
|
|
23
|
+
carried no network mapping, so the argument had no effect and each NIC landed on whatever network the OVF
|
|
24
|
+
descriptor named. Every network the OVF declares is now mapped to the named port group (standard or distributed).
|
|
25
|
+
A name that matches no network, or more than one (a standard and a distributed port group can share a name), is
|
|
26
|
+
refused before anything is imported, with the available names in the error. An OVF descriptor the server cannot
|
|
27
|
+
parse is refused the same way. Found in the 2026-10-16 pre-release review.
|
|
28
|
+
|
|
1
29
|
## v2.3.0 — calls on one target run side by side; `get_access_info`
|
|
2
30
|
|
|
3
31
|
Needs vmware-policy 1.22.0.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-aiops"
|
|
7
|
-
version = "2.3.
|
|
7
|
+
version = "2.3.1"
|
|
8
8
|
description = "VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -19,11 +19,11 @@ dependencies = [
|
|
|
19
19
|
"httpx>=0.27,<1.0",
|
|
20
20
|
"python-dotenv>=1.0,<2.0",
|
|
21
21
|
"mcp[cli]>=1.28,<2.0",
|
|
22
|
-
"vmware-policy>=1.
|
|
22
|
+
"vmware-policy>=1.23.0,<2.0",
|
|
23
23
|
# Read-only cluster-health triage (summary + HTML snapshot) is owned by
|
|
24
24
|
# vmware-monitor; AIops delegates to it so a conversation with the AIops
|
|
25
25
|
# entry point can produce the same report without duplicating the code.
|
|
26
|
-
"vmware-monitor>=2.
|
|
26
|
+
"vmware-monitor>=2.3.1,<3.0", # attention status floor + role-less login (AIops#47)
|
|
27
27
|
]
|
|
28
28
|
|
|
29
29
|
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/vmware-skills/VMware-AIops",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.3.
|
|
10
|
+
"version": "2.3.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-aiops",
|
|
15
|
-
"version": "2.3.
|
|
15
|
+
"version": "2.3.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.3.
|
|
66
|
+
uv tool install vmware-aiops==2.3.1
|
|
67
67
|
vmware-aiops doctor
|
|
68
68
|
vmware-aiops hub status # see which family members are installed
|
|
69
69
|
```
|
|
@@ -285,11 +285,10 @@ Use `run_vm_guest_command_with_output` instead of `run_vm_guest_command` — it
|
|
|
285
285
|
Large OVA files (>10GB) may exceed the default 120s timeout. The upload happens via HTTP NFC lease — ensure network between the machine running vmware-aiops and ESXi is stable.
|
|
286
286
|
|
|
287
287
|
### Snapshot delete is slow / "still running after Ns"
|
|
288
|
-
Deleting
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`vm
|
|
292
|
-
prefer `vm snapshot-delete <name> --name <snap> --no-wait` to get the task id immediately and poll.
|
|
288
|
+
Deleting a large snapshot consolidates its delta disk, often for minutes. `vm snapshot-delete` waits
|
|
289
|
+
up to 30 min; a "still running, NOT failed" message with a task id means the delete did **not** fail —
|
|
290
|
+
poll `vm task-status <task-id>`; do not re-issue it. For large ones, use
|
|
291
|
+
`vm snapshot-delete <name> --name <snap> --no-wait` and poll.
|
|
293
292
|
|
|
294
293
|
### Plan apply fails mid-way
|
|
295
294
|
Run `vmware-aiops plan list` to see failed plan status. Ask user if they want to rollback with `rollback_vm_plan`. Irreversible steps (delete_vm) are skipped during rollback.
|
|
@@ -298,10 +297,13 @@ Run `vmware-aiops plan list` to see failed plan status. Ask user if they want to
|
|
|
298
297
|
1. Verify target is reachable: `vmware-aiops doctor`
|
|
299
298
|
2. For self-signed certs: set `verify_ssl: false` in config.yaml (lab environments only)
|
|
300
299
|
|
|
300
|
+
### Every command fails with "session is not authenticated"
|
|
301
|
+
The account has no vSphere role. Assign one (Read-Only or higher) on the vCenter root folder; `vmware-aiops doctor` reports this case by name.
|
|
302
|
+
|
|
301
303
|
## Setup
|
|
302
304
|
|
|
303
305
|
```bash
|
|
304
|
-
uv tool install vmware-aiops==2.3.
|
|
306
|
+
uv tool install vmware-aiops==2.3.1
|
|
305
307
|
mkdir -p ~/.vmware-aiops
|
|
306
308
|
vmware-aiops init # generates config.yaml and .env templates
|
|
307
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.3.
|
|
9
|
+
uv tool install vmware-aiops==2.3.1
|
|
10
10
|
|
|
11
11
|
# Via Skills.sh (fetches from GitHub)
|
|
12
|
-
npx skills add vmware-skills/VMware-AIops#v2.3.
|
|
12
|
+
npx skills add vmware-skills/VMware-AIops#v2.3.1
|
|
13
13
|
|
|
14
14
|
# Via ClawHub (fetches from ClawHub registry snapshot of GitHub)
|
|
15
|
-
clawhub install @zw008/vmware-aiops --version 2.3.
|
|
15
|
+
clawhub install @zw008/vmware-aiops --version 2.3.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.3.
|
|
38
|
+
uv tool install vmware-aiops==2.3.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.3.
|
|
76
|
+
git clone --branch v2.3.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
|
|
@@ -226,6 +226,13 @@ vmware-aiops mcp --transport https --host 0.0.0.0 --port 8443 \
|
|
|
226
226
|
- Behind a reverse proxy that terminates TLS (Nginx, HAProxy) on the same machine, use
|
|
227
227
|
`--transport http --host 127.0.0.1 --public-name mcp.example.com` (the names the proxy forwards in `Host`), and
|
|
228
228
|
have the proxy pass the `Authorization` header through.
|
|
229
|
+
- Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
|
|
230
|
+
fine: the balancer is the client that has to trust it), list in `--public-name` the host name the balancer sends
|
|
231
|
+
as `Host`, and have it forward `Authorization`. Point its health probe at `GET /healthz`: it needs no token, returns
|
|
232
|
+
only `{"status": "ok"}` and accepts any `Host` (vmware-policy ≥ 1.23.0). Every other path still needs the token.
|
|
233
|
+
- Correlation: each audit row stores the caller's `traceparent` trace-id, else its `X-Request-ID` /
|
|
234
|
+
`X-Correlation-ID`. Have the agent platform send one, and `vmware-audit log --correlation-id ID` lists the calls
|
|
235
|
+
behind one platform request.
|
|
229
236
|
- `vmware-audit token list` / `token revoke NAME` manage callers; a revoked token is refused from its next request.
|
|
230
237
|
Replace a leaked token under a new name.
|
|
231
238
|
- The server will not start without at least one token, and checks before it binds that unauthenticated requests
|
|
@@ -20,8 +20,9 @@ that registered its own handler would pass with the production one deleted.
|
|
|
20
20
|
from __future__ import annotations
|
|
21
21
|
|
|
22
22
|
import gc
|
|
23
|
+
import itertools
|
|
23
24
|
import weakref
|
|
24
|
-
from unittest.mock import MagicMock, patch
|
|
25
|
+
from unittest.mock import MagicMock, PropertyMock, patch
|
|
25
26
|
|
|
26
27
|
import pytest
|
|
27
28
|
|
|
@@ -44,9 +45,13 @@ def _target(name: str = "vcenter-prod") -> TargetConfig:
|
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def _dead_si(*_a, **_kw):
|
|
47
|
-
"""An SI
|
|
48
|
+
"""An SI that is alive at login and dead from then on, so the next connect()
|
|
49
|
+
evicts it. Dead from the start would be refused at login as a role-less
|
|
50
|
+
account (AIops#47), which is a different path."""
|
|
48
51
|
si = MagicMock()
|
|
49
|
-
si.content.sessionManager.currentSession =
|
|
52
|
+
type(si.content.sessionManager).currentSession = PropertyMock(
|
|
53
|
+
side_effect=itertools.chain([object()], itertools.repeat(None))
|
|
54
|
+
)
|
|
50
55
|
return si
|
|
51
56
|
|
|
52
57
|
|
|
@@ -235,6 +235,11 @@ def test_deploy_ova_maps_device_urls_by_importkey_out_of_order() -> None:
|
|
|
235
235
|
|
|
236
236
|
content = MagicMock()
|
|
237
237
|
content.ovfManager.CreateImportSpec.return_value = import_spec_result
|
|
238
|
+
# A descriptor that parses cleanly and declares no networks (mapping is
|
|
239
|
+
# covered in test_ova_network_mapping.py).
|
|
240
|
+
content.ovfManager.ParseDescriptor.return_value = SimpleNamespace(
|
|
241
|
+
error=None, warning=None, network=[]
|
|
242
|
+
)
|
|
238
243
|
si = MagicMock()
|
|
239
244
|
si.RetrieveContent.return_value = content
|
|
240
245
|
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""A login that works but has no vSphere role must say so (AIops#47).
|
|
2
|
+
|
|
3
|
+
Reported against vCenter 8.0.3, not reproduced here (the lab is down): an SSO
|
|
4
|
+
account with no vSphere role logs in, ``sessionManager.currentSession`` reads
|
|
5
|
+
``None``, and every inventory call then fails with ``vim.fault.NotAuthenticated``.
|
|
6
|
+
``doctor`` passed, the CLI said "verify connectivity and credentials", and
|
|
7
|
+
``vmware-aiops summary`` printed a raw traceback for the fault.
|
|
8
|
+
|
|
9
|
+
Driven through the real ``ConnectionManager`` with only ``SmartConnect`` and
|
|
10
|
+
``Disconnect`` replaced, so the production factory, pool and cleanup all run.
|
|
11
|
+
The attention roll-up is vmware-monitor's code and is tested there.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import io
|
|
17
|
+
from unittest.mock import MagicMock, PropertyMock, patch
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
import typer
|
|
21
|
+
from pyVmomi import vim
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
|
|
24
|
+
from vmware_aiops import connection as conn_mod
|
|
25
|
+
from vmware_aiops.config import AppConfig, TargetConfig
|
|
26
|
+
from vmware_aiops.connection import ConnectionManager, NoVSphereRoleError
|
|
27
|
+
|
|
28
|
+
pytestmark = pytest.mark.unit
|
|
29
|
+
|
|
30
|
+
ROLE = "no vSphere role"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@pytest.fixture(autouse=True)
|
|
34
|
+
def _password(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
35
|
+
for name in ("VC1", "VC2"):
|
|
36
|
+
monkeypatch.setenv(f"VMWARE_{name}_PASSWORD", "test-only")
|
|
37
|
+
monkeypatch.setenv(f"VMWARE_AIOPS_{name}_PASSWORD", "test-only")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _config(*names: str) -> AppConfig:
|
|
41
|
+
return AppConfig(
|
|
42
|
+
targets=tuple(
|
|
43
|
+
TargetConfig(name=n, host=f"{n}.example.com", config_username="svc") for n in names
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _si(has_session: bool, *, root_readable: bool = False) -> MagicMock:
|
|
49
|
+
"""A login as vCenter answers it.
|
|
50
|
+
|
|
51
|
+
No role (AIops#47) takes both signals: ``currentSession`` reads ``None`` and
|
|
52
|
+
a container view on the root folder is refused with NotAuthenticated.
|
|
53
|
+
``root_readable`` is the scoped account (one folder, or Supervisor namespace
|
|
54
|
+
permissions only): no session either, but the root view opens.
|
|
55
|
+
"""
|
|
56
|
+
si = MagicMock()
|
|
57
|
+
si.content.sessionManager.currentSession = object() if has_session else None
|
|
58
|
+
if not has_session and not root_readable:
|
|
59
|
+
si.content.viewManager.CreateContainerView.side_effect = vim.fault.NotAuthenticated()
|
|
60
|
+
return si
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_role_less_login_is_refused_closed_and_not_cached() -> None:
|
|
64
|
+
mgr = ConnectionManager(_config("vc1"))
|
|
65
|
+
si = _si(False)
|
|
66
|
+
with (
|
|
67
|
+
patch("pyVim.connect.SmartConnect", return_value=si),
|
|
68
|
+
patch("pyVim.connect.Disconnect") as disconnect,
|
|
69
|
+
pytest.raises(NoVSphereRoleError) as err,
|
|
70
|
+
):
|
|
71
|
+
mgr.connect("vc1")
|
|
72
|
+
|
|
73
|
+
msg = str(err.value)
|
|
74
|
+
assert ROLE in msg and "Read-Only" in msg and "'vc1'" in msg
|
|
75
|
+
assert "vmware-aiops doctor" in msg
|
|
76
|
+
disconnect.assert_called_once_with(si) # the login is logged out
|
|
77
|
+
assert id(si) not in conn_mod._SI_ATEXIT # its atexit cleanup taken back
|
|
78
|
+
assert id(si) not in conn_mod._SI_VERIFY_SSL # its side store cleared
|
|
79
|
+
assert mgr.list_connected() == [] # and the pool kept nothing
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_a_normal_login_is_unaffected() -> None:
|
|
83
|
+
mgr = ConnectionManager(_config("vc1"))
|
|
84
|
+
si = _si(True)
|
|
85
|
+
with (
|
|
86
|
+
patch("pyVim.connect.SmartConnect", return_value=si),
|
|
87
|
+
patch("pyVim.connect.Disconnect") as disconnect,
|
|
88
|
+
):
|
|
89
|
+
assert mgr.connect("vc1") is si
|
|
90
|
+
disconnect.assert_not_called()
|
|
91
|
+
assert mgr.list_connected() == ["vc1"]
|
|
92
|
+
mgr.disconnect_all()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_a_scoped_account_without_a_session_still_connects() -> None:
|
|
96
|
+
"""``currentSession`` alone is not a verdict: a readable root view lets it through."""
|
|
97
|
+
mgr = ConnectionManager(_config("vc1"))
|
|
98
|
+
si = _si(False, root_readable=True)
|
|
99
|
+
with (
|
|
100
|
+
patch("pyVim.connect.SmartConnect", return_value=si),
|
|
101
|
+
patch("pyVim.connect.Disconnect") as disconnect,
|
|
102
|
+
):
|
|
103
|
+
assert mgr.connect("vc1") is si # no NoVSphereRoleError
|
|
104
|
+
disconnect.assert_not_called()
|
|
105
|
+
assert mgr.list_connected() == ["vc1"] # cached like any session
|
|
106
|
+
view = si.content.viewManager.CreateContainerView.return_value
|
|
107
|
+
view.Destroy.assert_called_once_with() # the probe leaves no view behind
|
|
108
|
+
mgr.disconnect_all()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_an_unreadable_session_is_not_a_verdict() -> None:
|
|
112
|
+
"""Unknown is not "no role": a probe that raises lets the login through."""
|
|
113
|
+
mgr = ConnectionManager(_config("vc1"))
|
|
114
|
+
si = MagicMock()
|
|
115
|
+
type(si.content.sessionManager).currentSession = PropertyMock(side_effect=RuntimeError)
|
|
116
|
+
with patch("pyVim.connect.SmartConnect", return_value=si), patch("pyVim.connect.Disconnect"):
|
|
117
|
+
assert ConnectionManager._create_connection(mgr._config.get_target("vc1")) is si
|
|
118
|
+
conn_mod._close_session(si)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_connect_all_names_the_role_case() -> None:
|
|
122
|
+
mgr = ConnectionManager(_config("vc1", "vc2"))
|
|
123
|
+
sis = {"vc1.example.com": _si(True), "vc2.example.com": _si(False)}
|
|
124
|
+
with (
|
|
125
|
+
patch("pyVim.connect.SmartConnect", side_effect=lambda host, **_: sis[host]),
|
|
126
|
+
patch("pyVim.connect.Disconnect"),
|
|
127
|
+
):
|
|
128
|
+
sessions, unreachable = mgr.connect_all()
|
|
129
|
+
mgr.disconnect_all()
|
|
130
|
+
assert [n for n, _ in sessions] == ["vc1"]
|
|
131
|
+
assert unreachable == [("vc2", "NoVSphereRoleError")]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_doctor_auth_row_fails_with_the_role_message(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
135
|
+
from vmware_aiops import doctor as doc
|
|
136
|
+
|
|
137
|
+
monkeypatch.setattr(type(doc.resolve_config_path()), "exists", lambda self: True)
|
|
138
|
+
monkeypatch.setattr("vmware_aiops.config.load_config", lambda: _config("vc1"))
|
|
139
|
+
with (
|
|
140
|
+
patch("pyVim.connect.SmartConnect", return_value=_si(False)),
|
|
141
|
+
patch("pyVim.connect.Disconnect"),
|
|
142
|
+
):
|
|
143
|
+
ok, row = doc._check_auth()
|
|
144
|
+
assert ok is False
|
|
145
|
+
assert ROLE in row and "vc1 ✗" in row
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _cli_output(monkeypatch: pytest.MonkeyPatch, exc: Exception) -> str:
|
|
149
|
+
from vmware_aiops.cli import _common
|
|
150
|
+
|
|
151
|
+
buf = io.StringIO()
|
|
152
|
+
monkeypatch.setattr(_common, "console", Console(file=buf, width=2000))
|
|
153
|
+
|
|
154
|
+
@_common.cli_errors
|
|
155
|
+
def cmd() -> None:
|
|
156
|
+
raise exc
|
|
157
|
+
|
|
158
|
+
with pytest.raises(typer.Exit):
|
|
159
|
+
cmd()
|
|
160
|
+
return buf.getvalue()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_cli_prints_the_role_message_intact(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
164
|
+
msg = conn_mod.no_role_message("vc1", "vmware-aiops")
|
|
165
|
+
out = _cli_output(monkeypatch, NoVSphereRoleError(msg))
|
|
166
|
+
assert msg in out
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_cli_not_authenticated_names_both_causes(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
170
|
+
fault = vim.fault.NotAuthenticated(msg="The session is not authenticated.")
|
|
171
|
+
out = _cli_output(monkeypatch, fault)
|
|
172
|
+
assert "expired" in out and "vSphere role" in out
|
|
173
|
+
assert "verify connectivity and credentials" not in out
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_cli_any_other_fault_is_one_line_not_a_traceback(
|
|
177
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
178
|
+
) -> None:
|
|
179
|
+
"""Issue item 5: `summary` printed a traceback for a vSphere fault."""
|
|
180
|
+
out = _cli_output(monkeypatch, vim.fault.NoPermission(msg="Permission to perform this"))
|
|
181
|
+
assert "vSphere API fault: Permission to perform this" in out
|
|
182
|
+
assert "vSphere role" in out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def test_summary_goes_through_cli_errors(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
186
|
+
"""The command the issue named, end to end through its real decorators."""
|
|
187
|
+
from vmware_aiops.cli import _common, summary
|
|
188
|
+
|
|
189
|
+
buf = io.StringIO()
|
|
190
|
+
monkeypatch.setattr(_common, "console", Console(file=buf, width=2000))
|
|
191
|
+
|
|
192
|
+
def _raise(*_a, **_kw):
|
|
193
|
+
raise vim.fault.NotAuthenticated(msg="The session is not authenticated.")
|
|
194
|
+
|
|
195
|
+
monkeypatch.setattr(summary, "_get_connection", _raise)
|
|
196
|
+
with pytest.raises(typer.Exit):
|
|
197
|
+
summary.cluster_summary_cmd(
|
|
198
|
+
cluster=None, no_vms=False, top=10, html=False, html_path=None, target=None, config=None
|
|
199
|
+
)
|
|
200
|
+
assert "vSphere role" in buf.getvalue()
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _mcp_payload(exc: Exception, shape: str = "dict"):
|
|
204
|
+
from vmware_aiops.mcp_server._shared import tool_errors
|
|
205
|
+
|
|
206
|
+
@tool_errors(shape)
|
|
207
|
+
def tool():
|
|
208
|
+
raise exc
|
|
209
|
+
|
|
210
|
+
return tool()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_mcp_passes_the_whole_role_message() -> None:
|
|
214
|
+
msg = conn_mod.no_role_message("a-rather-long-target-name-in-config", "vmware-aiops")
|
|
215
|
+
payload = _mcp_payload(NoVSphereRoleError(msg))
|
|
216
|
+
assert payload["error"] == msg # not cut, not reduced to a class name
|
|
217
|
+
assert "credentials" not in payload["hint"]
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_mcp_not_authenticated_names_both_causes() -> None:
|
|
221
|
+
payload = _mcp_payload(vim.fault.NotAuthenticated(msg="The session is not authenticated."))
|
|
222
|
+
assert "expired" in payload["error"] and "vSphere role" in payload["error"]
|
|
223
|
+
assert "credentials" not in payload["hint"]
|
|
224
|
+
text = _mcp_payload(vim.fault.NotAuthenticated(), shape="str")
|
|
225
|
+
assert "vSphere role" in text and "verify connectivity and credentials" not in text
|