-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathVcfOps.ps1
More file actions
322 lines (295 loc) · 14.2 KB
/
Copy pathVcfOps.ps1
File metadata and controls
322 lines (295 loc) · 14.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
# VcfOps.ps1 — read-only client helpers for VCF Operations (/suite-api), dot-sourced by
# vCheck.ps1 exactly the way Charts.ps1 is. No module infrastructure the repo doesn't already have.
#
# The whole track is OPT-IN and STRICTLY ADDITIVE: with no Operations server configured nothing
# here is ever called, the VMware.Sdk.Vcf.Ops module is never imported (a ~5s cost on Windows),
# and the report is byte-for-byte what it is today. See OPERATIONS.md.
#
# READ-ONLY BY CONSTRUCTION. The SDK exposes 505 Invoke-* cmdlets including
# Invoke-VcfOpsDeleteResource and adapter start/stop. Every call made here goes through
# Invoke-VcfOpsRead, which refuses any cmdlet not on the allowlist below. A health report must
# never mutate the thing it is reporting on, and no plugin should call an SDK cmdlet directly.
#
# Lab-verified 2026-08-31 against VCF Operations 9.1.0.0. Three SDK
# quirks are worked around here rather than in the plugins:
# - Invoke-VcfOpsQueryAlert returns its payload on _Alerts, NOT Alerts.
# - Invoke-VcfOpsGetReclaimData mangles its -Id guid; reclamation needs raw REST (not used yet).
# - Disconnect-VcfOpsServer has no -Confirm parameter.
#region ---------------------------------------------------------------- read-only guard
# The ONLY SDK cmdlets this file may call. Adding a mutating verb here defeats the design rule.
$script:VcfOpsReadOnlyCmdlets = @(
'Invoke-VcfOpsGetServicesInfo'
'Invoke-VcfOpsGetMatchingResources'
'Invoke-VcfOpsQueryAlert'
'Invoke-VcfOpsQueryFindings'
'Invoke-VcfOpsQueryLatestStatsOfResources'
'Invoke-VcfOpsGetStatKeysOfResources'
)
<# Invoke one allowlisted read cmdlet, with a single retry on a transient failure. Returns
$null (and warns) when the call fails twice — callers must treat $null as "no data",
never as an error to propagate into the report. #>
function Invoke-VcfOpsRead {
param(
[Parameter(Mandatory = $true)][string]$Name,
[hashtable]$Arguments = @{ }
)
if ($script:VcfOpsReadOnlyCmdlets -notcontains $Name) {
Write-Warning ("VcfOps: refusing to call '{0}' — not on the read-only allowlist." -f $Name)
return $null
}
foreach ($attempt in 1, 2) {
try {
return (& $Name @Arguments -ErrorAction Stop)
}
catch {
if ($attempt -eq 2) {
Write-Warning ("VcfOps: {0} failed: {1}" -f $Name, $_.Exception.Message)
return $null
}
}
}
}
#endregion
#region ---------------------------------------------------------------- session
<# Import the SDK and connect. Returns the connection object, or $null when Operations is
unreachable/misconfigured — an absent Operations must never break the run. #>
function Connect-VcfOpsSession {
param(
[Parameter(Mandatory = $true)][string]$Server,
[Parameter(Mandatory = $true)][pscredential]$Credential,
[string]$AuthSource,
[bool]$AllowInvalidCertificates = $true
)
if (-not (Get-Module -ListAvailable -Name 'VMware.Sdk.Vcf.Ops')) {
Write-Warning "VcfOps: VMware.Sdk.Vcf.Ops is not installed — skipping the VCF Operations checks. It ships with VCF.PowerCLI 9.1."
return $null
}
try {
# ~5s on Windows in a fresh process, and it is 63% of this track's total cost — which is
# why it lives here, behind the configured/enabled gate, and not at engine start-up.
if (-not (Get-Module -Name 'VMware.Sdk.Vcf.Ops')) { Import-Module 'VMware.Sdk.Vcf.Ops' -ErrorAction Stop }
}
catch {
Write-Warning ("VcfOps: could not import VMware.Sdk.Vcf.Ops: {0}" -f $_.Exception.Message)
return $null
}
$connectArgs = @{
Server = $Server
Credential = $Credential
ErrorAction = 'Stop'
}
if ($AuthSource) { $connectArgs['AuthSource'] = $AuthSource }
if ($AllowInvalidCertificates) { $connectArgs['IgnoreInvalidCertificate'] = $true }
try {
$connection = Connect-VcfOpsServer @connectArgs
}
catch {
Write-Warning ("VcfOps: could not connect to {0}: {1}" -f $Server, $_.Exception.Message)
return $null
}
# A few endpoints have no usable cmdlet (reclamation: the SDK mangles its -Id guid), so keep
# a suite-api token alongside the SDK session for the raw-REST GET helper below. Acquire is
# ~0.3s. The token lives in memory for the run only, exactly like the SDK's own session.
$script:VcfOpsRest = $null
try {
$body = @{ username = $Credential.UserName; password = $Credential.GetNetworkCredential().Password } | ConvertTo-Json
if ($AuthSource) { $body = @{ username = $Credential.UserName; password = $Credential.GetNetworkCredential().Password; authSource = $AuthSource } | ConvertTo-Json }
$token = (Invoke-RestMethod -Method Post -Uri ("https://{0}/suite-api/api/auth/token/acquire" -f $Server) `
-Body $body -ContentType 'application/json' -Headers @{ Accept = 'application/json' } `
-SkipCertificateCheck:$AllowInvalidCertificates -ErrorAction Stop).token
$script:VcfOpsRest = @{
Server = $Server
# 9.1 accepts both "OpsToken" and the legacy 8.x "vRealizeOpsToken" prefix.
Headers = @{ Authorization = "OpsToken $token"; Accept = 'application/json' }
SkipCertificateCheck = [bool]$AllowInvalidCertificates
}
}
catch {
Write-Warning ("VcfOps: suite-api token acquire failed on {0}: {1} (cmdlet-backed checks still work)" -f $Server, $_.Exception.Message)
}
return $connection
}
<# GET one suite-api path. Deliberately GET-only: that is the read-only guarantee for the
endpoints with no cmdlet, in the same spirit as the allowlist above. Returns $null on
failure so callers degrade to "no data". #>
function Invoke-VcfOpsRestGet {
param([Parameter(Mandatory = $true)][string]$Path)
if (-not $script:VcfOpsRest) { return $null }
$uri = "https://{0}/suite-api/{1}" -f $script:VcfOpsRest.Server, $Path.TrimStart('/')
try {
return (Invoke-RestMethod -Method Get -Uri $uri -Headers $script:VcfOpsRest.Headers `
-SkipCertificateCheck:$script:VcfOpsRest.SkipCertificateCheck -ErrorAction Stop)
}
catch {
Write-Warning ("VcfOps: GET {0} failed: {1}" -f $Path, $_.Exception.Message)
return $null
}
}
function Disconnect-VcfOpsSession {
param([string]$Server)
# NOTE: Disconnect-VcfOpsServer has no -Confirm parameter; -Confirm:$false throws.
try { Disconnect-VcfOpsServer -Server $Server -ErrorAction SilentlyContinue | Out-Null } catch { }
}
#endregion
#region ---------------------------------------------------------------- resources and the join
<# Read one resourceIdentifier off an Operations resource by name (VMEntityObjectID,
VMEntityVCID, VMEntityInstanceUUID, VMEntityName, ...). #>
function Get-VcfOpsIdentifier {
param($Resource, [Parameter(Mandatory = $true)][string]$Name)
$ids = $Resource.ResourceKey.ResourceIdentifiers
if (-not $ids) { return $null }
return ($ids | Where-Object { $_.IdentifierType.Name -eq $Name } | Select-Object -First 1).Value
}
<# All Operations resources of one kind, paged, with a hard cap so a fleet-sized instance can
never turn a health report into an unbounded crawl. #>
function Get-VcfOpsResourceList {
param(
[Parameter(Mandatory = $true)][string]$ResourceKind,
[int]$PageSize = 1000,
[int]$MaxObjects = 5000
)
$all = New-Object System.Collections.Generic.List[object]
$page = 0
do {
$query = Initialize-VcfOpsresourcequery -ResourceKind @($ResourceKind)
$result = Invoke-VcfOpsRead 'Invoke-VcfOpsGetMatchingResources' @{
ResourceQuery = $query; Page = $page; PageSize = $PageSize
}
if (-not $result) { break }
foreach ($r in $result.ResourceList) { $all.Add($r) }
$total = [int]$result.PageInfo.TotalCount
$page++
} while ($all.Count -lt $total -and $all.Count -lt $MaxObjects -and $result.ResourceList.Count -gt 0)
if ($all.Count -ge $MaxObjects) {
Write-Warning ("VcfOps: resource kind '{0}' hit the {1}-object cap — results are partial." -f $ResourceKind, $MaxObjects)
}
return $all
}
<# THE JOIN (OPERATIONS.md). Build a two-way index of Operations resources:
.ById Operations resource GUID -> row (alerts carry only this)
.ByMoRef vCenter MoRef -> row (vm-123 / host-28 / domain-c9 / datastore-15)
Rows are scoped to ONE vCenter by VMEntityVCID, which is byte-identical to that vCenter's
own InstanceUuid — this is what keeps a fleet-wide Operations instance inside vCheck's
one-run-per-vCenter policy. Objects belonging to ANOTHER vCenter are still indexed ById but
flagged Foreign, so a caller can tell "belongs to a vCenter this run is not about" apart from
"not indexed at all"; they are never exposed ByMoRef. Resources with no VCID at all
(Operations-internal objects) are indexed ById and are not foreign. #>
function New-VcfOpsResourceIndex {
param(
[string[]]$Kinds = @('VirtualMachine', 'HostSystem', 'ClusterComputeResource', 'Datastore', 'Datacenter'),
[Parameter(Mandatory = $true)][string]$VCenterInstanceUuid,
[int]$MaxObjects = 5000
)
$byId = @{ }
$byMoRef = @{ }
foreach ($kind in $Kinds) {
foreach ($res in (Get-VcfOpsResourceList -ResourceKind $kind -MaxObjects $MaxObjects)) {
$vcid = Get-VcfOpsIdentifier $res 'VMEntityVCID'
$foreign = [bool]($vcid -and $vcid -ne $VCenterInstanceUuid)
$moref = Get-VcfOpsIdentifier $res 'VMEntityObjectID'
$row = [pscustomobject]@{
Id = [string]$res.Identifier
Name = [string]$res.ResourceKey.Name
Kind = [string]$res.ResourceKey.ResourceKindKey
MoRef = [string]$moref
VCID = [string]$vcid
Foreign = $foreign
Health = [string]$res.ResourceHealth
Status = [string](@($res.ResourceStatusStates.ResourceStatus) | Select-Object -First 1)
}
$byId[$row.Id] = $row
# MoRefs are only unique within a vCenter, so another vCenter's objects must never
# enter the MoRef index — vm-123 there is a different VM to vm-123 here.
if ($moref -and -not $foreign) { $byMoRef[$row.MoRef] = $row }
}
}
$localCount = @($byId.Values | Where-Object { -not $_.Foreign }).Count
return [pscustomobject]@{
ById = $byId
ByMoRef = $byMoRef
Count = $localCount # objects belonging to THIS vCenter
ForeignCount = $byId.Count - $localCount # objects belonging to other vCenters in the fleet
}
}
#endregion
#region ---------------------------------------------------------------- verdict feeds
<# Active alerts, paged and capped. Reads the payload off _Alerts (SDK quirk: $a.Alerts is
silently empty while $a.PageInfo.TotalCount is not). #>
function Get-VcfOpsActiveAlerts {
param(
[int]$PageSize = 500,
[int]$MaxAlerts = 2000
)
$all = New-Object System.Collections.Generic.List[object]
$page = 0
do {
$query = Initialize-VcfOpsalertquery -ActiveOnly $true
$result = Invoke-VcfOpsRead 'Invoke-VcfOpsQueryAlert' @{
AlertQuery = $query; Page = $page; PageSize = $PageSize
}
if (-not $result) { break }
$batch = @($result._Alerts)
foreach ($a in $batch) { $all.Add($a) }
$total = [int]$result.PageInfo.TotalCount
$page++
} while ($all.Count -lt $total -and $all.Count -lt $MaxAlerts -and $batch.Count -gt 0)
return $all
}
<# Per-service health of the Operations cluster itself (CASA/LOCATOR/COLLECTOR/ADMINUI/API/
UI/ANALYTICS). health is one of UNKNOWN, INVALID, OK, WARNING, ERROR. #>
function Get-VcfOpsServiceHealth {
$info = Invoke-VcfOpsRead 'Invoke-VcfOpsGetServicesInfo'
if (-not $info) { return @() }
return @($info.Service)
}
<# Latest value of one or more stat keys for one or more resources, flattened to
@{ <resourceId> = @{ <statKey> = <double> } }
Stat keys are RUNTIME data and differ per resource kind — discover them with
Get-VcfOpsStatKey rather than hardcoding, and expect a key to be absent. #>
function Get-VcfOpsLatestStats {
param(
[Parameter(Mandatory = $true)][string[]]$ResourceIds,
[Parameter(Mandatory = $true)][string[]]$StatKeys
)
$out = @{ }
if (-not $ResourceIds -or -not $StatKeys) { return $out }
$query = Initialize-VcfOpslateststatquery -ResourceId ([guid[]]@($ResourceIds)) -StatKey @($StatKeys) -CurrentOnly $true
$result = Invoke-VcfOpsRead 'Invoke-VcfOpsQueryLatestStatsOfResources' @{ LatestStatQuery = $query }
if (-not $result) { return $out }
foreach ($entry in @($result.Values)) {
$id = [string]$entry.ResourceId
if (-not $out.ContainsKey($id)) { $out[$id] = @{ } }
foreach ($stat in @($entry.StatList.Stat)) {
# .Data is an array of samples; CurrentOnly leaves the latest one.
$value = @($stat.Data) | Select-Object -Last 1
if ($null -ne $value) { $out[$id][[string]$stat.StatKey.Key] = [double]$value }
}
}
return $out
}
<# The stat keys a given resource actually exposes. The response property is 'stat-key'
(NOT 'resource-type-attributes'), and the key set differs per resource kind. #>
function Get-VcfOpsStatKey {
param([Parameter(Mandatory = $true)][string]$ResourceId, [string]$Match)
$raw = Invoke-VcfOpsRestGet ("api/resources/{0}/statkeys" -f $ResourceId)
if (-not $raw) { return @() }
$keys = @($raw.'stat-key'.key)
if ($Match) { $keys = @($keys | Where-Object { $_ -match $Match }) }
return $keys
}
<# Reclaimable capacity for one datacenter resource, by reason. Raw REST on purpose:
Invoke-VcfOpsGetReclaimData serialises its -Id guid into the literal string
"Variant,10,Version,4" and the server rejects it with a 400.
Valid reasons (the server enumerates them in its 400 body): POWERED_OFF, IDLE, SNAPSHOT,
ORPHANED_DISK — NOT the IDLE_VMS/SNAPSHOTS spellings the spec suggests. #>
function Get-VcfOpsReclaimData {
param(
[Parameter(Mandatory = $true)][string]$DatacenterResourceId,
[ValidateSet('POWERED_OFF', 'IDLE', 'SNAPSHOT', 'ORPHANED_DISK')][string]$Reason,
[int]$PageSize = 200
)
$raw = Invoke-VcfOpsRestGet ("api/optimization/datacenters/{0}/reclaim/resources?reason={1}&pageSize={2}" -f $DatacenterResourceId, $Reason, $PageSize)
if (-not $raw) { return @() }
return @($raw.reclaimRightsizeResources)
}
#endregion