Files
Backup/_NDGOV_WindowsTeam/ITD.Infra-VMware.Administration/Scripts/Sync-ITDVMwareVMMetadataToSql.ps1
T
Zack Meier 90667af3ab update
2026-04-21 09:37:26 -05:00

303 lines
12 KiB
PowerShell

<#
.SYNOPSIS
Daily VM metadata report for PowerBI trending and hardware capacity planning.
.DESCRIPTION
Collects VM metadata from vCenter including compute, storage, OS, and VMware Tools
information using only data available within vCenter (no direct guest connections).
Exports a timestamped CSV each run -- append daily runs to build a historical dataset
suitable for PowerBI trend analysis and physical hardware purchasing decisions.
.PARAMETER vCenterServers
One or more vCenter server hostnames. Defaults to itdvmvc1.nd.gov and itdvmvc2.nd.gov.
.PARAMETER DatacenterFilter
Wildcard filter applied to datacenter names. Defaults to 'Primary*'.
.PARAMETER OutputPath
Directory where CSV and log files are written.
Defaults to C:\ITDSCRIPT\Reports\VMMetadata.
.PARAMETER CredentialPath
Path to a saved PSCredential XML file for unattended/scheduled runs.
Create one interactively with:
Get-Credential | Export-Clixml -Path C:\ITDSCRIPT\Creds\vCenter.xml
When omitted the script prompts for credentials.
.EXAMPLE
# Interactive run
.\VMware-VMDailyMetadataReport.ps1
.EXAMPLE
# Scheduled / unattended run
.\VMware-VMDailyMetadataReport.ps1 -CredentialPath 'C:\ITDSCRIPT\Creds\vCenter.xml'
.NOTES
Run the following DDL once to create the destination table:
DROP TABLE IF EXISTS [dbo].[VMware_Trends_VM]
CREATE TABLE [dbo].[VMware_Trends_VM] (
[ReportDate] DATETIME2 NOT NULL,
[VMName] NVARCHAR(255) NOT NULL,
[Datacenter] NVARCHAR(100) NULL,
[Cluster] NVARCHAR(100) NULL,
[PowerState] NVARCHAR(50) NULL,
[IsSRMPlaceholder] BIT NOT NULL,
[StoragePlatform] NVARCHAR(100) NULL,
[GuestOS] NVARCHAR(255) NULL,
[vCPUs] INT NULL,
[MemoryGB] DECIMAL(10,2) NULL,
[ProvisionedSpaceGB] DECIMAL(12,2) NULL,
[UsedSpaceGB] DECIMAL(12,2) NULL,
[GuestDiskCapacityGB] DECIMAL(12,2) NULL,
[GuestDiskUsedGB] DECIMAL(12,2) NULL,
[ToolsRunningStatus] NVARCHAR(100) NULL,
[ToolsVersionStatus] NVARCHAR(100) NULL,
[ToolsVersion] NVARCHAR(50) NULL,
[Tag_DRProtection] NVARCHAR(100) NULL,
[Tag_AppName] NVARCHAR(255) NULL,
[Tag_VRDatastores] NVARCHAR(255) NULL,
[Tag_VRRPO] NVARCHAR(100) NULL,
[Tag_DTAP] NVARCHAR(50) NULL,
[Tag_StartupPriority] NVARCHAR(100) NULL,
[Tag_SRMRecoveryType] NVARCHAR(100) NULL,
[Tag_LicensingRestrictions] NVARCHAR(255) NULL
)
Guest OS disk capacity / used columns are NULL for powered-off VMs and SRM placeholders.
#>
[CmdletBinding()]
param(
)
#region --- Setup ---------------------------------------------------------------
[string] $OutputPath = 'C:\temp\VM_Trends\'
[string] $ServerInstance = 'itdintsql22p1.nd.gov\INTSQL22P1'
[string] $Database = 'ITD-Systems-Automation'
[string] $Table = 'VMware_Trends_VM'
[System.Management.Automation.PSCredential] $SqlCredential = $SqlCred #$Secret:sql_itdpsu1
[System.Management.Automation.PSCredential] $vCenterCredential = $PrvCred
$RunDate = Get-Date
$DateStamp = $RunDate.ToString('yyyyMMdd')
$Timestamp = $RunDate.ToString('yyyy-MM-dd')
if (-not (Test-Path -Path $OutputPath)) {
New-Item -ItemType Directory -Path $OutputPath | Out-Null
}
Start-Transcript -Path (Join-Path $OutputPath "VMMetadataReport_$DateStamp.log") -Append
#endregion
#region --- Build VMHost -> Cluster/Datacenter Lookups (avoids per-VM API calls) -
Write-Verbose 'Building host-to-cluster and host-to-datacenter maps...'
$HostClusterMap = @{}
$HostDatacenterMap = @{}
Get-Datacenter | ForEach-Object {
$DatacenterName = $_.Name
Get-VMHost -Location $_ | ForEach-Object {
$HostDatacenterMap[$_.Name] = $DatacenterName
}
}
Get-Cluster | ForEach-Object {
$ClusterName = $_.Name
Get-VMHost -Location $_ | ForEach-Object {
$HostClusterMap[$_.Name] = $ClusterName
}
}
#endregion
#region --- Collect VM Data -----------------------------------------------------
Write-Verbose "Gathering VMs"
# Include ALL VMs (SRM placeholders flagged via column, not excluded).
# vCLS agent VMs are excluded -- they are vSphere internal and not customer workloads.
$AllVMs = Get-VM | Where-Object { $_.Name -notlike 'vCLS*' } | Sort-Object -Property Name
#--- Pre-fetch all tag assignments in one API call (avoids per-VM Get-TagAssignment)
Write-Verbose 'Pre-fetching VM tag assignments...'
$TagLookup = @{}
Get-TagAssignment -Entity $AllVMs | ForEach-Object {
$VMId = $_.Entity.Id
$Cat = $_.Tag.Category.Name
$TagName = $_.Tag.Name
if (-not $TagLookup.ContainsKey($VMId)) { $TagLookup[$VMId] = @{} }
if ($TagLookup[$VMId].ContainsKey($Cat)) {
$TagLookup[$VMId][$Cat] += "; $TagName"
} else {
$TagLookup[$VMId][$Cat] = $TagName
}
}
Write-Verbose "Processing $($AllVMs.Count) VMs..."
$Results = foreach ($VM in $AllVMs) {
Write-Verbose -Message ("Start " + $VM.Name) -Verbose
$StoragePlatforms = $null
$StoragePlatform = $null
$Ext = $VM.ExtensionData # single API object -- reuse for all fields
#--- SRM placeholder detection
$IsSRMPlaceholder = $Ext.Summary.Config.ManagedBy.Type -eq 'placeholderVm'
#--- Cluster / Datacenter (null-safe: standalone hosts have no cluster entry)
$ClusterName = $HostClusterMap[$VM.VMHost.Name]
$DatacenterName = $HostDatacenterMap[$VM.VMHost.Name]
#--- Tag assignments (pre-fetched; null when category not assigned to this VM)
$VMTags = if ($TagLookup.ContainsKey($VM.Id)) { $TagLookup[$VM.Id] } else { @{} }
#--- Storage platform parsed from datastore name convention: VMCLUSTER_LUN_PLATFORM_Desc
# Segment 2 = storage platform identifier (e.g. FS92, A9K).
# Cluster grouping uses the compute Cluster column -- no need to re-derive it here.
$StoragePlatforms = foreach ($DSName in $Ext.Config.DatastoreUrl.Name) {
$Segments = $DSName -split '_'
if ($Segments.Count -ge 3) { $Segments[2] }
}
$StoragePlatform = ($StoragePlatforms | Sort-Object -Unique) -join '; '
#--- VMware Tools guest disk info
# Populated only when Tools is running; null otherwise.
$GuestDiskCapacityGB = $null
$GuestDiskUsedGB = $null
if ($Ext.Guest.Disk) {
$TotalCapBytes = ($Ext.Guest.Disk | Measure-Object -Property Capacity -Sum).Sum
$TotalFreeBytes = ($Ext.Guest.Disk | Measure-Object -Property FreeSpace -Sum).Sum
$GuestDiskCapacityGB = [Math]::Round($TotalCapBytes / 1GB, 2)
$GuestDiskUsedGB = [Math]::Round(($TotalCapBytes - $TotalFreeBytes) / 1GB, 2)
}
[PSCustomObject]@{
# --- Identity & grouping
ReportDate = $Timestamp # for PowerBI time-series/trend axis
VMName = $VM.Name
Datacenter = $DatacenterName
Cluster = $ClusterName
PowerState = $VM.PowerState
IsSRMPlaceholder = $IsSRMPlaceholder
StoragePlatform = $StoragePlatform
GuestOS = $Ext.Guest.GuestFullName
# --- Compute
vCPUs = $VM.NumCpu
MemoryGB = $VM.MemoryGB
# --- Datastore-level storage
# ProvisionedSpaceGB : maximum the VM could consume (thin disks counted at max size)
# UsedSpaceGB : bytes actually committed on datastores right now
ProvisionedSpaceGB = [Math]::Round($VM.ProvisionedSpaceGB, 2)
UsedSpaceGB = [Math]::Round($VM.UsedSpaceGB, 2)
# --- Guest OS-level storage (from VMware Tools; null when Tools not running)
# GuestDiskCapacityGB : sum of all volume capacities seen inside the guest
# GuestDiskUsedGB : sum of space consumed across those volumes
GuestDiskCapacityGB = $GuestDiskCapacityGB
GuestDiskUsedGB = $GuestDiskUsedGB
# --- VMware Tools
# ToolsRunningStatus : guestToolsRunning | guestToolsNotRunning | guestToolsExecutingScripts
# ToolsVersionStatus : guestToolsCurrent | guestToolsNeedUpgrade | guestToolsUnmanaged | guestToolsTooNew
# ToolsVersion : numeric build version string reported by vCenter
ToolsRunningStatus = $Ext.Guest.ToolsRunningStatus
ToolsVersionStatus = $Ext.Guest.ToolsVersionStatus
ToolsVersion = $Ext.Guest.ToolsVersion
# --- vCenter Tags
Tag_DRProtection = $VMTags['DR Protection']
Tag_AppName = $VMTags['AppName']
Tag_VRDatastores = $VMTags['VR Datastores']
Tag_VRRPO = $VMTags['VR RPO']
Tag_DTAP = $VMTags['DTAP']
Tag_StartupPriority = $VMTags['StartupPriority']
Tag_SRMRecoveryType = $VMTags['SRM Recovery Type']
Tag_LicensingRestrictions = $VMTags['LicensingRestrictions']
}
}
#endregion
<#region --- Export CSV ---------------------------------------------------------
$OutputFile = Join-Path $OutputPath "VMMetadata_$DateStamp.csv"
$Results | Export-Csv -Path $OutputFile -NoTypeInformation
Write-Verbose "Exported $($Results.Count) VM records to: $OutputFile"
#endregion
#>
#region --- SQL Insert ---------------------------------------------------------
# Build a typed DataTable so Write-SqlTableData knows each column's type
# even when nullable columns contain null values. Type inference from raw
# PSCustomObjects fails when the first row has a null in a numeric column.
$DataTable = [System.Data.DataTable]::new()
$ColDefs = [ordered]@{
ReportDate = [datetime]
VMName = [string]
Datacenter = [string]
Cluster = [string]
PowerState = [string]
IsSRMPlaceholder = [bool]
StoragePlatform = [string]
GuestOS = [string]
vCPUs = [int]
MemoryGB = [decimal]
ProvisionedSpaceGB = [decimal]
UsedSpaceGB = [decimal]
GuestDiskCapacityGB = [decimal]
GuestDiskUsedGB = [decimal]
ToolsRunningStatus = [string]
ToolsVersionStatus = [string]
ToolsVersion = [string]
Tag_DRProtection = [string]
Tag_AppName = [string]
Tag_VRDatastores = [string]
Tag_VRRPO = [string]
Tag_DTAP = [string]
Tag_StartupPriority = [string]
Tag_SRMRecoveryType = [string]
Tag_LicensingRestrictions = [string]
}
foreach ($Col in $ColDefs.GetEnumerator()) {
$Column = [System.Data.DataColumn]::new($Col.Key, $Col.Value)
$Column.AllowDBNull = $true
[void]$DataTable.Columns.Add($Column)
}
foreach ($Row in $Results) {
$DataRow = $DataTable.NewRow()
foreach ($Col in $ColDefs.Keys) {
$Val = $Row.$Col
$DataRow[$Col] = if ($null -ne $Val) { $Val } else { [DBNull]::Value }
}
[void]$DataTable.Rows.Add($DataRow)
}
$SqlParams = @{
ServerInstance = $ServerInstance
DatabaseName = $Database
SchemaName = 'dbo'
TableName = $Table
Credential = $SqlCredential
InputData = $DataTable
}
Write-SqlTableData @SqlParams
Write-Verbose "Inserted $($DataTable.Rows.Count) VM records into [$Database].[dbo].[$Table]"
#endregion
#region --- Cleanup -------------------------------------------------------------
Stop-Transcript
#endregion