diff --git a/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_API_RUNTIME_SPEC.md b/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_API_RUNTIME_SPEC.md index 1e37860501..79089bba83 100644 --- a/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_API_RUNTIME_SPEC.md +++ b/docs/release-control/v6/internal/VMWARE_VCENTER_PHASE1_API_RUNTIME_SPEC.md @@ -122,6 +122,17 @@ Phase-1 rule: through the shared browser client so the settings workflow can distinguish version-floor, auth, TLS, permission, and network failures without a VMware-only fetch path +6. once endpoint, TLS, Automation session, base inventory, VI JSON release + negotiation and login, and the performance-counter catalog have succeeded, + optional signal or performance reads must use the same partial-success + classification as runtime polling; `permission`, `not_found`, `unavailable`, + and `endpoint` failures return a successful but explicitly `degraded` test + result with the negotiated `viRelease`, `issueCount`, and preserved `issues`, + while authentication, TLS, network, and required-family failures remain hard + connection-test failures +7. the settings workflow must render that degraded result as a warning with the + upstream diagnostic, never as a fully green connection or as a total + connection failure ## Runtime Health And Poll Summary Contract diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index a32c367dd6..6a147e6b51 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -3573,6 +3573,12 @@ saved-connection preview/test/update payloads must preserve stored enablement unless the caller explicitly changes it, so setup surfaces do not accidentally preview an unchanged active connection as inactive just because JSON omitted a bool field. +The VMware credential editor must also distinguish required-floor failure from +optional enrichment degradation. When the canonical connection-test response +is successful but `degraded`, the editor must keep the exact upstream +diagnostic visible in a limited-data warning and identify the negotiated VI +JSON release when available; it must not show the test as fully green or route +the warning through the hard-failure presentation. That same validation contract must stay coherent across the public `/api/auto-register` route and the direct canonical handler path used by the same runtime surface, so Unified Agent/setup entry points do not inherit divergent diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 0a3fda02f8..c474f698cb 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -5360,6 +5360,17 @@ preserve the canonical top-level `code` plus string-valued `details.error` and `frontend-modern/src/utils/apiClient.ts` plus `frontend-modern/src/api/responseUtils.ts` must carry that metadata through the shared error object without inventing a VMware-only fetch or parsing path. +After the required endpoint, TLS, Automation API session, base inventory, +negotiated VI JSON login, and performance-counter catalog gates succeed, those +same test routes must classify optional signal and performance read failures +the same way as the live poller. Non-fatal enrichment categories +(`permission`, `not_found`, `unavailable`, and `endpoint`) return +`success: true` with `degraded`, the negotiated `viRelease`, `issueCount`, and +preserved `issues`; authentication, TLS, network, unsupported-version, and +required API-family failures remain hard failures. A degraded response is not +a green test: the shared VMware settings client must preserve the diagnostic +and present a limited-data warning instead of converting the optional upstream +failure into either generic success or total connection failure. That same TrueNAS and VMware platform-connections contract now also owns per-surface scope as a first-class field on the connection shape. The TrueNAS connection payload must carry positive `monitorDatasets`, diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 9d40299be9..95af0739ef 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -827,6 +827,13 @@ with no edit overlay must refresh that same summary owner, and recomputing or shadowing it inside handler-local runtime state. Internal sub-second test harness intervals must not leak `intervalSeconds: 0` onto that operator-facing contract. +Manual saved-connection tests that prove the required VMware API floor but +encounter non-fatal optional signal or performance failures must record a +successful poll attempt together with `observed.degraded`, `issueCount`, and +the summarized upstream diagnostics. They must not erase the same +partial-success evidence that a live inventory poll would publish, while +authentication, TLS, network, and required API-family failures must continue +to increment canonical poll failure state. That same monitoring boundary now also owns runtime mock rebind continuity for API-backed supplemental providers. When `/api/system/mock-mode` flips on a running server, the live TrueNAS and VMware provider bindings must swap to the diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 3d6342e0bf..89ff344b47 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -1287,6 +1287,11 @@ recovery scope, or a storage/recovery-owned secret source. aliases. 27. Keep alert-side recovery drill-ins on that same embedded-owner route-state contract. When alert investigation surfaces such as resource-incident panels expose recovery follow-up links for TrueNAS or future API-backed platforms, they must route through an owning platform/runtime destination using canonical recovery query vocabulary instead of freezing alert-local recovery URLs, reviving the retired Recovery aggregate route, or introducing another provider-shaped recovery handoff vocabulary. 28. Keep VMware onboarding runtime and recovery semantics separate on that same adjacent platform-connections contract. When `internal/api/router.go`, `internal/api/router_routes_registration.go`, or `internal/api/vmware_handlers.go` evolve VMware connection CRUD, poller-owned `poll` / `observed` summary payloads, saved-test refresh, or observed datastore/VM snapshot visibility, storage and recovery may consume the resulting shared context but must not treat those onboarding/runtime payloads as canonical recovery artifacts, restore capability, or recovery-local control transport. + A successful VMware connection test with `degraded` optional signal or + performance diagnostics remains connection-readiness context only; storage + and recovery must not reinterpret that warning as datastore protection + damage, snapshot recovery failure, restore readiness, or recovery-point + evidence. The shared route-registration file may also expose the authenticated running-release notes endpoint for deployment/update presentation. That route remains deployment-installability and API-contract owned; storage diff --git a/frontend-modern/src/api/__tests__/vmware.test.ts b/frontend-modern/src/api/__tests__/vmware.test.ts index 47dc404a0a..dd6ea240fb 100644 --- a/frontend-modern/src/api/__tests__/vmware.test.ts +++ b/frontend-modern/src/api/__tests__/vmware.test.ts @@ -188,7 +188,17 @@ describe('VMwareAPI', () => { username: 'administrator@vsphere.local', password: 'secret', }), - ).resolves.toEqual({ success: true }); + ).resolves.toEqual({ + success: true, + hosts: 0, + vms: 0, + datastores: 0, + networks: 0, + viRelease: undefined, + degraded: false, + issueCount: 0, + issues: [], + }); await expect( VMwareAPI.testSavedConnection('conn/1', { host: 'vcsa.lab.local', @@ -196,7 +206,17 @@ describe('VMwareAPI', () => { password: '********', insecureSkipVerify: true, }), - ).resolves.toEqual({ success: true }); + ).resolves.toEqual({ + success: true, + hosts: 0, + vms: 0, + datastores: 0, + networks: 0, + viRelease: undefined, + degraded: false, + issueCount: 0, + issues: [], + }); expect(apiFetchJSON).toHaveBeenNthCalledWith( 1, @@ -260,6 +280,54 @@ describe('VMwareAPI', () => { ); }); + it('preserves degraded connection-test diagnostics', async () => { + vi.mocked(apiFetchJSON).mockResolvedValueOnce({ + success: true, + hosts: 2, + vms: 14, + datastores: 3, + networks: 5, + viRelease: ' 8.0.2.0 ', + degraded: true, + issueCount: 1, + issues: [ + { + stage: ' signals ', + entity_type: ' host ', + entity_id: ' host-101 ', + category: ' endpoint ', + message: ' VMware HostSystem recent events request failed with HTTP 500 ', + }, + ], + }); + + await expect( + VMwareAPI.testSavedConnection('conn-1', { + host: 'vcsa.lab.local', + username: 'administrator@vsphere.local', + password: '********', + }), + ).resolves.toEqual({ + success: true, + hosts: 2, + vms: 14, + datastores: 3, + networks: 5, + viRelease: '8.0.2.0', + degraded: true, + issueCount: 1, + issues: [ + { + stage: 'signals', + entityType: 'host', + entityId: 'host-101', + category: 'endpoint', + message: 'VMware HostSystem recent events request failed with HTTP 500', + }, + ], + }); + }); + it('recognizes the masked-secret placeholder used for update preservation', () => { expect(isRedactedVMwareSecret('********')).toBe(true); expect(isRedactedVMwareSecret(' ******** ')).toBe(true); diff --git a/frontend-modern/src/api/vmware.ts b/frontend-modern/src/api/vmware.ts index f7526ee29e..18e7a06f83 100644 --- a/frontend-modern/src/api/vmware.ts +++ b/frontend-modern/src/api/vmware.ts @@ -86,8 +86,52 @@ export interface VMwareConnectionInput { export interface VMwareConnectionTestResult { success: boolean; + hosts: number; + vms: number; + datastores: number; + networks: number; + viRelease?: string; + degraded: boolean; + issueCount: number; + issues: VMwareConnectionTestIssue[]; } +export interface VMwareConnectionTestIssue { + stage?: string; + entityType?: string; + entityId?: string; + category?: string; + message?: string; +} + +type RawVMwareConnectionTestIssue = Partial & { + entity_type?: unknown; + entity_id?: unknown; +}; + +const normalizeVMwareConnectionTestResult = ( + response: Partial & { + issues?: RawVMwareConnectionTestIssue[]; + }, +): VMwareConnectionTestResult => ({ + success: strictBoolean(response.success), + hosts: finiteNumberOrUndefined(response.hosts) ?? 0, + vms: finiteNumberOrUndefined(response.vms) ?? 0, + datastores: finiteNumberOrUndefined(response.datastores) ?? 0, + networks: finiteNumberOrUndefined(response.networks) ?? 0, + viRelease: optionalTrimmedString(response.viRelease), + degraded: strictBoolean(response.degraded), + issueCount: finiteNumberOrUndefined(response.issueCount) ?? 0, + issues: + arrayOrUndefined(response.issues)?.map((issue) => ({ + stage: optionalTrimmedString(issue.stage), + entityType: optionalTrimmedString(issue.entityType ?? issue.entity_type), + entityId: optionalTrimmedString(issue.entityId ?? issue.entity_id), + category: optionalTrimmedString(issue.category), + message: optionalTrimmedString(issue.message), + })) ?? [], +}); + const normalizeVMwareConnectionPollError = ( error: RawVMwareConnectionPollError | undefined, ): VMwareConnectionPollError | undefined => { @@ -213,9 +257,7 @@ export class VMwareAPI { body: JSON.stringify(serializeVMwareConnectionInput(input)), }, ); - return { - success: strictBoolean(response.success), - }; + return normalizeVMwareConnectionTestResult(response); } static async testSavedConnection( @@ -231,9 +273,7 @@ export class VMwareAPI { : {}), }, ); - return { - success: strictBoolean(response.success), - }; + return normalizeVMwareConnectionTestResult(response); } static async previewConnection( diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx index 9469786430..ee7307a3b7 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx @@ -90,6 +90,23 @@ export const VMwareCredentialSlot: Component = (props )} + + {(warning) => ( + +

{warning().message}

+

{warning().guidance}

+ + } + icon={} + /> + )} +
+