update
This commit is contained in:
+299
@@ -0,0 +1,299 @@
|
||||
<#
|
||||
.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*' }
|
||||
|
||||
#--- 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) {
|
||||
|
||||
$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
|
||||
Reference in New Issue
Block a user