<# ### step 1 gather specs provision virtual machine on hypervisor final result: cpu configured, with hot-add memory configured, with hot-add disks 1, 2, and 3 attached with correct sizing network connected virtual machine powered on update SCTASK Step 1 completed ### step 2 - customize Windows Guest OS wait for SCTASK Step 1 completed message configure time zone, mount points, page file, etc update SCTASK Step 2 completed ### step 3 - administration wait for SCTASK Step 2 completed message domain join nd.gov at a minimum install sccm regardless if domain joined update SCTASK Step 3 completed ### step 4 - validation confirm all agents are installed disks are present and formatted #> <# .SYNOPSIS A short one-line action-based description, e.g. 'Tests if a function is valid' .DESCRIPTION A longer description of the function, its purpose, common use cases, etc. .NOTES Information or caveats about the function e.g. 'This function is not supported in Linux' .LINK Specify a URI to a help page, this will show when Get-Help -Online is used. .EXAMPLE $NewITDWindowsVmVMwareParams = @{ ComputerName = 'itdzmtest300.nd.gov'; CPU = 2; MemoryGB = 8; DiskOsGB = 50; DiskSwapGB = 9; DiskDataGB = 20; Subnet = '10.11.12.0/23'; OS = 'Windows Server 2022 Datacenter'; Environment = "Test"; Datacenter = "Bismarck"; AppName = "ITD-POC-zmeier"; StartupPriority = 4; LicensingRestrictions = "No Licensing Restrictions"; Credential = $PrvCred; } New-ITDWindowsVmVMware @NewITDWindowsVmVMwareParams #> function New-ITDWindowsVmVMwareStep1 { [CmdletBinding()] param ( [string] $FQDN, [int] $CPU, [int] $MemoryGB, [int] $DiskOsGB, [int] $DiskSwapGB, [int] $DiskDataGB, [string] $Subnet, [string] $OS, [string] $VMEnvironment, [string] $Datacenter, [string] $AppName, [int] $StartupPriority, [string] $LicensingRestrictions, [PSCredential] $Credential ) begin { } process { $FQDN = $FQDN.ToLower() $HostName = $FQDN.split('.')[0] If ($VMEnvironment -eq "Development") { $VMEnvironment = "Test" } Write-Verbose -Message "Prepare Credentials and Connections" $RadiusCred = New-Object System.Management.Automation.PSCredential($Credential.username.split('\')[1], ($Credential.Password)) Write-Verbose -Message "Infoblox: Find DNS pre-existing record, or create one" Write-Verbose -Message "FQDN: $FQDN" -Verbose Write-Verbose -Message "Hostname: $Hostname" -Verbose Clear-DnsClientCache $InfobloxVlanMetadata = Get-ITDIbVlan -CIDR $Subnet -Credential $RadiusCred $Cidr = ($InfobloxVlanMetadata.AssignedTo | Out-String).TrimEnd() [Net.IpAddress]$NetworkId = $Cidr.split('/')[0] ####### ####### Remove 10.10.10.10 references when DNS sync is fixed ####### [Net.IPAddress]$IpAddress = (Resolve-DnsName -Name $FQDN -ErrorAction SilentlyContinue -Server 10.10.10.10).IPAddress $SubnetMaskInt = $CIDR.split('/')[1] $Int64 = ([convert]::ToInt64(('1' * $SubnetMaskInt + '0' * (32 - $SubnetMaskInt)), 2)) [Net.IPAddress]$SubnetMask = '{0}.{1}.{2}.{3}' -f ([math]::Truncate($Int64 / 16777216)).ToString(), ([math]::Truncate(($Int64 % 16777216) / 65536)).ToString(), ([math]::Truncate(($Int64 % 65536) / 256)).ToString(), ([math]::Truncate($Int64 % 256)).ToString() $IPSplit = $Subnet.Split('.') [Net.IPAddress]$DefaultGateway = ($IPSplit[0] + '.' + $IPSplit[1] + '.' + $IPSplit[2] + '.' + (($CIDR.split('/')[0].split('.')[-1] -as [int]) + 1) ) If ($null -ne $IpAddress) { If (($IpAddress.Address -band $SubnetMask.Address) -eq ($NetworkId.Address -band $SubnetMask.Address)) { Write-Verbose -Message "DNS record already exists, CIDR Block match" } Else { Write-Error "DNS record already exists, but does not match CIDR Block" Break } } Else { Write-Verbose -Message "Pre-existing IP address not found, creating new DNS record." try { New-ITDIbDNSRecordNextAvailableIP -Hostname $FQDN -CIDR $CIDR -Credential $RadiusCred Start-Sleep -Seconds 5 Write-Verbose -Message ("FQDN is " + $FQDN) } catch { } ####### ####### Review this code after DNS problems resolved - 2024/09/24 zm ####### ####### [Net.IPAddress]$IpAddress = (Resolve-DnsName -Name $FQDN -ErrorAction Stop -Server 10.10.10.10).IPAddress [Net.IPAddress]$IpAddress = (Get-ITDIbDNSRecord -Hostname $FQDN -Credential $RadiusCred).IPv4Address If ((Test-NetConnection -ComputerName $IpAddress.IPAddressToString).PingSucceeded) { Write-Error "IP Address already in use." -ErrorAction Stop } } Write-Verbose -Message "Passwordstate: If local administrator password does not exist in vault, create it." If ($FQDN -like "itdcnd*") { $PasswordStateList = "Peoplesoft Share PW" } Else { $PasswordStateList = "CSRC" } $GuestVMLocalCredential = Get-ITDPassword -Title $FQDN -UserName itdadmin -Credential $Credential If ($null -eq $GuestVMLocalCredential) { Write-Verbose -Message "Passwordstate: Local admin password record does not exist, creating new credentials" $GuestVMLocalCredential = New-ITDPassword -Title $FQDN -UserName itdadmin -Description 'Local Administrator' -PasswordList $PasswordStateList -Credential $Credential } Else { Write-Verbose -Message "Passwordstate: Local admin password record already exists, use those credentials" } $GuestCredentialAB = New-Object System.Management.Automation.PSCredential ('itdadmin', ($GuestVMLocalCredential.GetNetworkCredential().Password | ConvertTo-SecureString -AsPlainText -Force)) $GuestCredentialBB = New-Object System.Management.Automation.PSCredential ('Administrator', ($GuestVMLocalCredential.GetNetworkCredential().Password | ConvertTo-SecureString -AsPlainText -Force)) Write-Verbose -Message "Decide VMware Cluster and Licensing" switch ($LicensingRestrictions) { "No Licensing Restrictions" { $ClusterRoot = "WINDOWS" } "Microsoft SharePoint Server" { $ClusterRoot = "WINDOWS" } "Microsoft SharePoint Server (Academic)" { $ClusterRoot = "WINDOWS" } "Microsoft SQL Developer" { $ClusterRoot = "WINDOWS" } "Microsoft SQL MSDN" { $ClusterRoot = "WINDOWS" } "Microsoft SQL Standard" { $ClusterRoot = "WINDOWS" } "Microsoft SQL Standard (Academic)" { $ClusterRoot = "WINDOWS" } "Microsoft SQL Standard (Vendor Provided)" { $ClusterRoot = "WINDOWS" } "Microsoft SQL Enterprise" { $ClusterRoot = "SQLe" } "Microsoft SQL Enterprise (Academic)" { $ClusterRoot = "SQLa" } "IBM Websphere" { $ClusterRoot = "WAS" } "Powerschool" { $ClusterRoot = "PS" } "Pexip" { $ClusterRoot = "TEL" } } Write-Verbose -Message "Decide Datacenter" switch ($Datacenter) { "Bismarck" { $ClusterInt = 1 } "Mandan" { $ClusterInt = 2 } } $Cluster = $ClusterRoot + $ClusterInt Write-Verbose -Message "VMware Cluster is $Cluster, gathering metadata for $Cluster" switch ($Cluster) { "WINDOWS1" { $ViServer = 'itdvmvc1.nd.gov' $ComputeCluster = Get-Cluster WINDOWS1 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-PDC-Data-Server" If ($LicensingRestrictions -like "*SQL*") { $DatastoreCluster = Get-DatastoreCluster -Name "WINDOWS1_FS92_SQL" $DiskStorageFormat = 'EagerZeroedThick' } Else { $DatastoreCluster = Get-DatastoreCluster -Name "WINDOWS1_FS92_Gen" $DiskStorageFormat = 'Thin' } } "WINDOWS2" { $ViServer = 'itdvmvc2.nd.gov' $ComputeCluster = Get-Cluster WINDOWS2 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-SDC-Data-Server" $DiskStorageFormat = 'Thin' If ($LicensingRestrictions -like "*SQL*") { $DatastoreCluster = Get-DatastoreCluster -Name "WINDOWS2_FS92_SQL" $DiskStorageFormat = 'EagerZeroedThick' } Else { $DatastoreCluster = Get-DatastoreCluster -Name "WINDOWS2_FS92_Gen" $DiskStorageFormat = 'Thin' } } "SQLa1" { $ViServer = 'itdvmvc1.nd.gov' $ComputeCluster = Get-Cluster SQLa1 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-PDC-Data-Server" $DiskStorageFormat = 'EagerZeroedThick' $DatastoreCluster = Get-DatastoreCluster -Name "SQLa1_FS92_Gen" } "SQLa2" { $ViServer = 'itdvmvc2.nd.gov' $ComputeCluster = Get-Cluster SQLa2 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-SDC-Data-Server" $DiskStorageFormat = 'EagerZeroedThick' $DatastoreCluster = Get-DatastoreCluster -Name "SQLa2_FS92_Gen" } "SQLe1" { $ViServer = 'itdvmvc1.nd.gov' $ComputeCluster = Get-Cluster SQLe1 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-PDC-Data-Server" $DiskStorageFormat = 'EagerZeroedThick' $DatastoreCluster = Get-DatastoreCluster -Name "SQLe1_FS92_Gen" } "SQLe2" { $ViServer = 'itdvmvc2.nd.gov' $ComputeCluster = Get-Cluster SQLe2 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-SDC-Data-Server" $DiskStorageFormat = 'EagerZeroedThick' $DatastoreCluster = Get-DatastoreCluster -Name "SQLe2_FS92_Gen" } "WAS1" { $ViServer = 'itdvmvc1.nd.gov' $ComputeCluster = Get-Cluster WAS1 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-PDC-Data-Server" $DiskStorageFormat = 'Thin' $DatastoreCluster = Get-DatastoreCluster -Name "WAS1_FS92_Gen" } "WAS2" { $ViServer = 'itdvmvc2.nd.gov' $ComputeCluster = Get-Cluster WAS2 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-SDC-Data-Server" $DiskStorageFormat = 'Thin' $DatastoreCluster = Get-DatastoreCluster -Name "WAS2_FS92_Gen" } "PS1" { $ViServer = 'itdvmvc1.nd.gov' $ComputeCluster = Get-Cluster PS1 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-PDC-Data-Server" $DiskStorageFormat = 'Thin' $DatastoreCluster = Get-DatastoreCluster -Name "PS1_FS92_Gen" } "PS2" { $ViServer = 'itdvmvc2.nd.gov' $ComputeCluster = Get-Cluster PS2 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-SDC-Data-Server" $DiskStorageFormat = 'Thin' $DatastoreCluster = Get-DatastoreCluster -Name "PS2_FS92_Gen" } "TEL1" { $ViServer = 'itdvmvc1.nd.gov' $ComputeCluster = Get-Cluster TEL1 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-PDC-TEL1-Data" $DiskStorageFormat = 'Thin' $DatastoreCluster = Get-DatastoreCluster -Name "TEL1_FS92_Gen" } "TEL2" { $ViServer = 'itdvmvc2.nd.gov' $ComputeCluster = Get-Cluster TEL2 $VirtualSwitch = Get-VDSwitch -Name "dvSwitch-PDC-TEL2-Data" $DiskStorageFormat = 'Thin' $DatastoreCluster = Get-DatastoreCluster -Name "TEL2_FS92_Gen" } Default { Write-Error "Cluster not found" -ErrorAction Stop } } Write-Verbose -Message "Validate entered disk sizes" Write-Verbose -Message "DiskOsGB is $DiskOsGB. Validating its not a stupid number." -Verbose switch ($DiskOsGB) { { $_ -lt 50 } { Write-Verbose -Message "DiskOsGB is 0 or below. Since an OS Disk is required, defaulting to 50GB" -Verbose $DiskOsGB = 50 } } Write-Verbose -Message "DiskSwapGB is $DiskSwapGB. Validating its not a stupid number." -Verbose switch ($DiskSwapGB) { { $_ -le 0 } { Write-Verbose -Message "DiskSwapGB is zero or below. Since an Swap Disk is required, defaulting to `$Memory + 1GB." -Verbose $DiskSwapGB = ($MemoryGB + 1) } } Write-Verbose -Message "DiskDataGB is $DiskDataGB. Validating its not a stupid number." -Verbose switch ($DiskDataGB) { { $_ -le 0 } { Write-Verbose -Message "DiskDataGB is 0. Since an Data Disk is optional, data disk will not be created." -Verbose $DiskDataGB = 0 } { $_ -gt 500 } { Write-Verbose -Message "DiskDataGB is 500GB or more. DiskDataGB will be set to the maximum of 500GB." -Verbose $DiskDataGB = 500 } } Write-Verbose -Message "Determine Datastore / Datastore Cluster" $DiskTotal = $DiskOsGB + $DiskSwapGB + $DiskDataGB If ($DatastoreCluster) { } Else { $DatastoreCluster = Get-DatastoreCluster | Where-Object Name -Like ("*" + $ComputeCluster + "*") } $ClusterDatastoreWithHighestFreeSpaceGB = ($DatastoreCluster | Get-Datastore | Sort-Object FreeSpaceGB -Descending | Select-Object -First 1) If ($ClusterDatastoreWithHighestFreeSpaceGB.FreeSpaceGB -gt $DiskTotal) { Write-Warning ("VM DiskTotal " + $DiskTotal + "GB, will fit on " + $ClusterDatastoreWithHighestFreeSpaceGB.Name + " (" + [math]::round($ClusterDatastoreWithHighestFreeSpaceGB.FreeSpaceGB, 0) + "GB free)") } else { Write-Warning ("VM DiskTotal " + $DiskTotal + "GB, will not fit on " + $ClusterDatastoreWithHighestFreeSpaceGB.Name + " (" + [math]::round($ClusterDatastoreWithHighestFreeSpaceGB.FreeSpaceGB, 0) + "GB free)") Write-Error ("New VM " + $FQDN + " needs " + $DiskTotal + "GB of free space on a single datastore in the " + $DatastoreCluster.Name + " datastore cluster.") -ErrorAction Stop } $FolderLocation = $ComputeCluster | Get-Datacenter | Get-Folder -Name "_New Builds" Write-Verbose -Message "Determine VMware VM Template for OS: $OS" switch ($OS) { "Windows Server 2012R2 Standard" { $Template = "Windows Server 2012R2 Standard" } "Windows Server 2016 Standard" { $Template = "Windows Server 2016 Standard" } "Windows Server 2019" { $Template = "Windows Server 2019 Standard 1809.19" } "Windows Server 2019 Standard" { $Template = "Windows Server 2019 Standard 1809.19" } "Windows Server 2019 Datacenter" { $Template = "Windows Server 2019 Standard 1809.19" } "Windows Server 2022" { $Template = "Windows Server 2022 Standard 2108.21" } "Windows Server 2022 Datacenter" { $Template = "Windows Server 2022 Standard 2108.21" } "Windows Server 2025" { $Template = "Windows Server 2025 Standard 24H2.6" } Default { Write-Error "Invalid template option: $OS" -ErrorAction Stop } } Write-Verbose -Message "Determine VMware Port Group" $PortGroupsAvailable = Get-VDPortgroup -Server $ViServer -VDSwitch $VirtualSwitch $PortGroup = $PortGroupsAvailable | Where-Object Name -Like ("dvPG_*" + $NetworkId.IPAddressToString + "_" + $SubnetMaskInt) If (!($PortGroup)) { Write-Error "Virtual port group not found" -ErrorAction Stop Stop } If (@($PortGroup).count -gt 1) { Write-Error "Multiple port groups found" -ErrorAction Stop Stop } Write-Verbose -Message "Configure Guest OS Customization Spec" $NewOSSpecName = ("AutoBuild-$Hostname-" + (Get-Date -UFormat "%Y%m%d%H%M%S")) Write-Warning "NewOSSpecName = $NewOSSpecName" Get-OSCustomizationSpec -Name 'Windows (Auto)' -Server $ViServer | New-OSCustomizationSpec -Name $NewOSSpecName -Type Persistent -Server $ViServer Get-OSCustomizationSpec -Name $NewOSSpecName -Server $ViServer | ` Set-OSCustomizationSpec ` -NamingScheme fixed ` -NamingPrefix $Hostname ` -AdminPassword $GuestCredentialBB.GetNetworkCredential().Password Get-OSCustomizationSpec -Name $NewOSSpecName -Server $ViServer | ` Get-OSCustomizationNicMapping | ` Set-OSCustomizationNicMapping ` -IpMode UseStaticIP ` -IpAddress $IpAddress.IPAddressToString ` -SubnetMask $SubnetMask.IPAddressToString ` -DefaultGateway $DefaultGateway.IPAddressToString ` -Dns "10.2.7.40", "10.10.10.10" $OSSpec = Get-OSCustomizationSpec -Name $NewOSSpecName -Server $ViServer Set-Location C:\Temp $NewVMParams = @{ Name = $FQDN; ResourcePool = $ComputeCluster.Name; Datastore = $DatastoreCluster; DiskStorageFormat = $DiskStorageFormat; Location = $FolderLocation; # Removed when using Content Library #Template = $Template; #OSCustomizationSpec = $OSSpec } Get-ContentLibraryItem -Name $Template -Server $ViServer | New-VM @NewVMParams $VM = Get-VM -Name $FQDN $VM | Set-VM -OSCustomizationSpec $OSSpec -Confirm:$false Write-Verbose -Message "Set vCenter Tags on VM" New-TagAssignment -Entity (Get-VM $FQDN -Server $VIServer) -Tag (Get-Tag -Server $ViServer -Category AppName -Name $AppName) -Server $VIServer New-TagAssignment -Entity (Get-VM $FQDN -Server $VIServer) -Tag (Get-Tag -Server $ViServer -Category DTAP -Name $VMEnvironment) -Server $VIServer New-TagAssignment -Entity (Get-VM $FQDN -Server $VIServer) -Tag (Get-Tag -Server $ViServer -Category LicensingRestrictions -Name $LicensingRestrictions) -Server $VIServer New-TagAssignment -Entity (Get-VM $FQDN -Server $VIServer) -Tag (Get-Tag -Server $ViServer -Category StartupPriority -Name $StartupPriority) -Server $VIServer # Ensure CPU/Memory Hot-Add Enabled $vmView = $VM | Get-View $vmConfigSpec = New-Object VMware.Vim.VirtualMachineConfigSpec $vmOptValCPU = New-Object VMware.Vim.OptionValue $vmOptValMem = New-Object VMware.Vim.OptionValue $vmOptValCPU.Key = "vcpu.hotadd" $vmOptValMem.Key = "mem.hotadd" $vmOptValCPU.Value = "true" $vmOptValMem.Value = "true" $vmConfigSpec.ExtraConfig += $vmOptValCPU $vmConfigSpec.ExtraConfig += $vmOptValMem $vmView.ReconfigVM($vmConfigSpec) # Set CPU, Memory, Network $VM | Set-VM -NumCpu $CPU -MemoryGB $MemoryGB -Confirm:$false $VM | Get-NetworkAdapter | Set-NetworkAdapter -Portgroup $PortGroup -Confirm:$false Write-Verbose -Message "Config and Update Disks" $VMDisk = $VM | Get-HardDisk $VMDisk1 = $VMDisk | Where-Object Name -EQ "Hard disk 1" $VMDisk2 = $VMDisk | Where-Object Name -EQ "Hard disk 2" $VMDisk3 = $VMDisk | Where-Object Name -EQ "Hard disk 3" Write-Verbose -Message "Update Disk 1" $VMDisk1 = $VM | Get-HardDisk | Where-Object Name -EQ "Hard disk 1" If ($VMDisk1.CapacityGB -lt $DiskOsGB) { Set-HardDisk -HardDisk $VMDisk1 -CapacityGB $DiskOsGB -Confirm:$false } Write-Verbose -Message "Config Disk 2" If (!$VMDisk2) { $VM | New-HardDisk ` -CapacityGB ($MemoryGB + 1) ` -StorageFormat Thin ` -DiskType Flat ` -Persistence Persistent } $VMDisk2 = $VM | Get-HardDisk | Where-Object Name -EQ "Hard disk 2" If ($VMDisk2.CapacityGB -lt $DiskSwapGB) { Set-HardDisk -HardDisk $VMDisk2 -CapacityGB $DiskSwapGB -Confirm:$false } Write-Verbose -Message "Config Disk 3" If ($DiskDataGB -gt 0) { Write-Verbose -Message "$DiskDataGB greater than zero" If (!$VMDisk3) { $VM | New-HardDisk ` -CapacityGB $DiskDataGB ` -StorageFormat Thin ` -DiskType Flat ` -Persistence Persistent } $VMDisk3 = $VM | Get-HardDisk | Where-Object Name -EQ "Hard disk 3" If ($VMDisk3.CapacityGB -lt $DiskDataGB) { Set-HardDisk -HardDisk $VMDisk3 -CapacityGB $DiskDataGB -Confirm:$false } } Write-Verbose -Message "Set VMware Tools Upgrade Policy to UpgradeAtPowerCycle" $VMView = $VM | Get-View $vmConfigSpec = New-Object VMware.Vim.VirtualMachineConfigSpec $vmConfigSpec.Tools = New-Object VMware.Vim.ToolsConfigInfo $vmConfigSpec.Tools.ToolsUpgradePolicy = "UpgradeAtPowerCycle" $VMView.ReconfigVM($vmConfigSpec) Write-Verbose -Message "Power On VM" $VM | Start-VM Write-Verbose -Message "[$FQDN]:Step 1 End" } end { } }