303 lines
12 KiB
PowerShell
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
|