NTS.Tools.MSHyperV.psm1
function New-VMVolume { <# .Description Creates a storage pool, virtual disk and volume intended for VMs RAID Level will always be 0, so be carefull .Parameter VMDriveLetter Letter for the volume .Parameter StoragePoolName Name of the StoragePool .Parameter VirtualDiskName Name of the VirtualDisk .Parameter VMVolumeName Name of the Volume .Parameter VDiskRaidLevel RAID Level of the virtual disk, allowed is Simple, Mirror, Parity .Parameter VDiskPhysicalDiskRedundancy how many disks should be redundant .Parameter LogFileName log file name .Example New-VMVolume -VMDriveLetter 'W' .NOTES - There must be at least one other disk in addition to disk 0. - If an NVMe disk is present, only this is taken #> param ( [Parameter(Mandatory = $false)] [char] $VMDriveLetter = 'V', [Parameter(Mandatory = $false)] [string] $StoragePoolName = "Pool01", [Parameter(Mandatory = $false)] [string] $VirtualDiskName = "VDisk01", [Parameter(Mandatory = $false)] [string] $VMVolumeName = "VMs", [Parameter(Mandatory = $false)] [ValidateSet("Simple", "Mirror", "Parity")] [string] $VDiskRaidLevel = "Simple", [Parameter(Mandatory = $false)] [int] $VDiskPhysicalDiskRedundancy = 1, [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } try { if ($null -eq (Get-Volume -DriveLetter $VMDriveLetter -ErrorAction SilentlyContinue)) { if ($null -eq (Get-StoragePool -FriendlyName $StoragePoolName -ErrorAction SilentlyContinue)) { $PhysicalDisks = Get-PhysicalDisk -CanPool $true | Where-Object -FilterScript { $PSitem.Bustype -ne "USB" } $NVMe_Devices = $PhysicalDisks | Where-Object -FilterScript { $PSItem.Bustype -eq "NVMe" -and $PSitem.Size -gt 256GB } $Non_NVMe_Devices = $PhysicalDisks | Where-Object -FilterScript { $PSItem.Bustype -ne "NVMe" } if ($null -ne $NVMe_Devices) { $SelectedDisks = $NVMe_Devices } else { $SelectedDisks = $Non_NVMe_Devices } Write-ToLogOrConsole @LogParam -Severity Info -Message "selected disks $($SelectedDisks.FriendlyName)" if ($null -eq $SelectedDisks) { throw "no disks were found that can be used for the storagepool" } if ($null -ne $NVMe_Devices -and ($SelectedDisks | Measure-Object).Count) { Write-ToLogOrConsole @LogParam -Severity Info -Message "creating volume $($VMVolumeName)" Initialize-Disk -Number $SelectedDisks.DeviceId -PartitionStyle GPT | Out-Null New-Volume -DiskNumber $SelectedDisks.DeviceId -FriendlyName $VMVolumeName -FileSystem ReFS -DriveLetter $VMDriveLetter | Out-Null } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "creating storage pool $($StoragePoolName)" $StorageSubSystemFriendlyName = (Get-StorageSubSystem -FriendlyName "*Windows*").FriendlyName New-StoragePool -StorageSubSystemFriendlyName $StorageSubSystemFriendlyName -FriendlyName $StoragePoolName -PhysicalDisks $SelectedDisks | Out-Null if ($null -eq (Get-VirtualDisk -FriendlyName $VirtualDiskName -ErrorAction SilentlyContinue)) { Write-ToLogOrConsole @LogParam -Severity Info -Message "create virtual disk $($VirtualDiskName) on $($StoragePoolName)" if ($VDiskRaidLevel -ne "Simple") { New-VirtualDisk -StoragePoolFriendlyName $StoragePoolName ` -FriendlyName $VirtualDiskName -UseMaximumSize ` -ProvisioningType Fixed ` -ResiliencySettingName $VDiskRaidLevel ` -PhysicalDiskRedundancy $VDiskPhysicalDiskRedundancy | Out-Null } else { New-VirtualDisk -StoragePoolFriendlyName $StoragePoolName ` -FriendlyName $VirtualDiskName -UseMaximumSize ` -ProvisioningType Fixed ` -ResiliencySettingName $VDiskRaidLevel | Out-Null } Write-ToLogOrConsole @LogParam -Severity Info -Message "creating volume $($VMVolumeName)" Initialize-Disk -FriendlyName $VirtualDiskName -PartitionStyle GPT | Out-Null $VDiskNumber = (Get-Disk -FriendlyName $VirtualDiskName).Number New-Volume -DiskNumber $VDiskNumber -FriendlyName $VMVolumeName -FileSystem ReFS -DriveLetter $VMDriveLetter | Out-Null } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "virtual disk $($VirtualDiskName) already exists - skipping" } } } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "pool $($StoragePoolName) already exists - skipping" } } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "volume $($VMDriveLetter) already exists - skipping" } } catch { $ErrorMessage = "error during creation of vm volume: $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } } function New-VMVSwitch { <# .Description Creates a VM switch based on a network card with the Up state. 10Gbit NICs are preferred .Parameter Name Name of the VM Switch .Parameter LogFileName log file name .Example New-VMVSwitch -Name 'IC' .NOTES there must be at least one nic with status 'up' #> param ( [Parameter(Mandatory = $false)] [string] $Name = "LAN", [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } try { if ($null -eq (Get-VMSwitch -Name $Name -ErrorAction SilentlyContinue)) { $pNICs = Get-NetAdapter -Physical | Where-Object -Property Status -eq "UP" $10G_NICs = $pNICs | Where-Object -Property LinkSpeed -EQ "10 Gbps" $1G_NICs = $pNICs | Where-Object -Property LinkSpeed -EQ "1 Gbps" if ($10G_NICs) { $Selected_NIC = $10G_NICs[0] } elseif ($1G_NICs) { $Selected_NIC = $1G_NICs[0] } else { $Selected_NIC = (Get-NetAdapter -Physical | Where-Object -Property Status -eq "UP")[0] } Write-ToLogOrConsole @LogParam -Severity Info -Message "create vswitch $($Name) with netadapter $($Selected_NIC.Name)" New-VMSwitch -Name $Name -NetAdapterName $Selected_NIC.Name -AllowManagementOS $false | Out-Null Add-VMNetworkAdapter -ManagementOS -SwitchName $Name -Name "vNIC-$($Name)" Rename-NetAdapter -Name $Selected_NIC.Name -NewName "pNIC-$($Name)" } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "virtual vswitch $($Name) already exists - skipping" } } catch { $ErrorMessage = "error during creation of virtual switch: $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } } function Register-VMInConfigMgr { <# .Description Registers the VM in ConfigMgr with its MAC address for required deployments .Parameter Name name of the vm and so the name of device object in configmgr .Parameter MacAddress macaddres of the nic .Parameter CM_CollectionName configmgr collection name with the required deployment .Parameter CM_Siteserver_FQDN full qualified domain name of the site server .Parameter CM_Credentials configmgr credentials .Parameter SecondsToWait seconds to wait after the registration process has finished .Parameter LogFileName log file name .Example Register-VMInConfigMgr -Name $PSItem.Name -MacAddress (Get-VM -Name $PSItem.Name | Get-VMNetworkAdapter).MacAddress -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName $LogFileName .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $MacAddress, [Parameter(Mandatory = $true)] [string] $CM_CollectionName, [Parameter(Mandatory = $true)] [string] $CM_Siteserver_FQDN, [Parameter(Mandatory = $true)] [PSCredential] $CM_Credentials, [Parameter(Mandatory = $false)] [int] $SecondsToWait = (Get-Random -Minimum 70 -Maximum 100), [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } $CM_Collection_All_Systems_ID = "SMS00001" #region functions function Confirm-CMCollectionMembership { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $DeviceName, [Parameter(Mandatory = $true)] [string] $CollectionName, [Parameter(Mandatory = $true)] [System.Management.Automation.Runspaces.PSSession] $CMPS_Session, [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } try { $Device_Exists_In_Collection = $false $Counter = 0 $MaxCounter = 40 do { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($DeviceName) - checking collection memberships in $($CollectionName) ($("{0:d2}" -f $Counter)/$($MaxCounter))" $CurrentCollectionMembers = Invoke-Command -Session $CMPS_Session -ArgumentList $DeviceName, $CollectionName -ScriptBlock { try { return (Get-CMCollectionMember -CollectionName $args[1] -Name $args[0]) } catch { throw $PSItem.Exception.Message } } if ($null -ne $CurrentCollectionMembers) { $Device_Exists_In_Collection = $true } else { Start-Sleep -Seconds 10; $Counter++ } if ($Counter -eq 12 -or $Counter -eq 24 -or $Counter -eq 36) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($DeviceName) - doing cm collection update on $($CollectionName) ($("{0:d2}" -f $Counter)/$($MaxCounter))" Invoke-Command -Session $CMPS_Session -ArgumentList $CollectionName -ScriptBlock { Start-CMCollectionUpdate -CollectionName $args[0] } } } while ($Device_Exists_In_Collection -eq $false -and $Counter -lt $MaxCounter) if ($Counter -ge $MaxCounter) { throw "could not find in the collection $($CollectionName) after a while" } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($DeviceName) - device collection in $($CollectionName)" } } catch { $ErrorMessage = "$($Name) - error during registration of device infos in configmgr: $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } } #endregion try { Confirm-VMPresence -Name $Name # PS Session try { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - Connecting to ConfigMgr $($CM_Siteserver_FQDN)" $CMPS_Session = New-PSSession -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials Invoke-Command -Session $CMPS_Session -ArgumentList $CM_Collection_All_Systems_ID -ScriptBlock { $CMPSDriveName = "CMPS-$(Get-Random)" Import-Module -Name "ConfigurationManager" New-PSDrive -Name $CMPSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null Set-Location -Path "$($CMPSDriveName):\" # define functions function Start-CMCollectionUpdate { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $CollectionName, [Parameter(Mandatory = $false)] [int] $NotBeforeMinutues = 2 ) $Collection = Get-CMCollection -Name $CollectionName $RefreshTime = (Get-Date).AddHours(-1) - $Collection.IncrementalEvaluationLastRefreshTime if ($RefreshTime.TotalMinutes -gt $NotBeforeMinutues) { Invoke-CMCollectionUpdate -CollectionId $Collection.CollectionID Start-Sleep -Seconds 5 } } # configmgr vars $CM_Collection_All_Systems_ID = $args[0] $CM_Collection_All_Systems_Name = (Get-CMCollection -Id $CM_Collection_All_Systems_ID).Name $CM_SiteCode = (Get-CMSite).SiteCode $CM_Collection_All_Systems_Name | Out-Null # just for the script analysis stuff $CM_SiteCode | Out-Null # just for the script analysis stuff } $CM_Collection_All_Systems_Name = Invoke-Command -Session $CMPS_Session -ArgumentList $CM_Collection_All_Systems_ID -ScriptBlock { return $CM_Collection_All_Systems_Name } } catch { $ErrorMessage = "error ps session to $($CM_Siteserver_FQDN): $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } # checking existing devices try { $FoundExistingDevice = Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $MacAddress -ScriptBlock { try { $Temp_CMDevice = Get-CMDevice -Name $args[0] if (($null -ne $Temp_CMDevice) -and ($Temp_CMDevice.MACAddress.ToString().Replace(":", "") -ne $args[1])) { return $true } } catch { throw $PSItem.Exception.Message } } if ($FoundExistingDevice -eq $true) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - removing existing computer info in configmgr - macaddress $($MacAddress), because the mac address is not correct" Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $MacAddress -ScriptBlock { try { $Temp_CMDevice = Get-CMDevice -Name $args[0] if (($null -ne $Temp_CMDevice) -and ($Temp_CMDevice.MACAddress.ToString().Replace(":", "") -ne $args[1])) { Remove-CMDevice -Name $args[0] -Force -Confirm:$false } } catch { throw $PSItem.Exception.Message } } } } catch { $ErrorMessage = "error checking/removing device in configmgr - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } # check destination collection existance try { $CollectionExits = Invoke-Command -Session $CMPS_Session -ArgumentList $CM_CollectionName -ScriptBlock { try { if ($null -eq (Get-CMCollection -Name $args[0])) { return $false } else { return $true } } catch { throw $PSItem.Exception.Message } } if ($CollectionExits -eq $false) { $ErrorMessage = "collection $($CM_CollectionName) does not existing" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } Start-Sleep -Seconds 10 } catch { $PSItem.Exception.Message } # import device try { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - creating computer info in configmgr - macaddress $($MacAddress)" Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $MacAddress, $CM_CollectionName -ScriptBlock { try { $Temp_CMDevice = Get-CMDevice -Name $args[0] if ($null -eq $Temp_CMDevice) { Import-CMComputerInformation -CollectionName $args[2] -ComputerName $args[0] -MacAddress $args[1] } } catch { throw $PSItem.Exception.Message } } } catch { $ErrorMessage = "error importing device in configmgr - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } # add device to target collection try { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - adding computer info to target collection $($CM_CollectionName)" # checking all system refreshinterval Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CM_CollectionName -ScriptBlock { try { Start-CMCollectionUpdate -CollectionName $CM_Collection_All_Systems_Name } catch { throw $PSItem.Exception.Message } } Start-Sleep -Seconds 10 # check collection membership all systems Confirm-CMCollectionMembership -DeviceName $Name -CollectionName $CM_Collection_All_Systems_Name -CMPS_Session $CMPS_Session -LogFileName $LogParam.LogFileName # create membership rule Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CM_CollectionName -ScriptBlock { try { Add-CMDeviceCollectionDirectMembershipRule -CollectionName $args[1] -ResourceID (Get-CMDevice -Name $args[0]).ResourceID } catch { throw $PSItem.Exception.Message } } Start-Sleep -Seconds 5 # check collection membership target Confirm-CMCollectionMembership -DeviceName $Name -CollectionName $CM_CollectionName -CMPS_Session $CMPS_Session -LogFileName $LogParam.LogFileName } catch { $ErrorMessage = "error adding device to target collection - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } # remove collections, so there is only the targeted try { $CollectionMembershipsToRemove = Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CM_CollectionName -ScriptBlock { try { $Temp_CMDevice = Get-CMDevice -Name $args[0] $TargetCollection = Get-CMCollection -Name $args[1] if ($null -ne $Temp_CMDevice) { return Get-CimInstance -Namespace "root/Sms/site_$($CM_SiteCode)" -ClassName "SMS_FullCollectionMembership" -Filter "ResourceID = $($Temp_CMDevice.ResourceID)" | ` Where-Object -Property CollectionID -ne $CM_Collection_All_Systems_ID | ` Where-Object -Property CollectionID -ne $TargetCollection.CollectionID } } catch { throw $PSItem.Exception.Message } } if ($null -ne $CollectionMembershipsToRemove) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name): removing additional collection membership in $($CollectionMembershipsToRemove.CollectionID)" Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CollectionMembershipsToRemove -ScriptBlock { try { $Temp_CMDevice = Get-CMDevice -Name $args[0] if ($null -ne $Temp_CMDevice) { $args[1] | ForEach-Object { $MembershipRule = Get-CMCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID | Where-Object -Property RuleName -EQ $Temp_CMDevice.Name if ($null -ne $MembershipRule) { Remove-CMDeviceCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID -ResourceId $MembershipRule.ResourceId -Confirm:$false -Force } } } } catch { throw $PSItem.Exception.Message } } } } catch { $ErrorMessage = "error removing additional collections - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - finished registration, now waiting $($SecondsToWait) seconds for the configmgr collection updates and give the old man some time" Start-Sleep -Seconds $SecondsToWait } catch { $ErrorMessage = "$($Name) - error during registration of device infos in configmgr: $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } finally { Remove-PSSession -Session $CMPS_Session } } function Confirm-VMDeployment { <# .Description Checks the ConfigMgr database to see if the deployment of VM objects is complete. .Parameter Name Name of the VM .Parameter CM_CollectionName ConfigMgr Collection Name with the required Deployment .Parameter CM_Siteserver_FQDN FQDN of ConfigMgr .Parameter CM_Credentials Credentials of a user that can create/edit/delete CMDevices and add them to a Collection. Should be able to start a collection update .Parameter VM_Credentials admin credentials to connect to the vm .Parameter TimeoutInMinutes time out in minitues, when to stop checking the deploymentstatus .Parameter DisableAutoClearPXE disables the pxe flag clear functionality .Parameter PXEClearAfterInSeconds defines how many seconds to wait until the pxe flag should be cleared .Parameter WaitForCMDatabaseThresholdInSeconds defines the amount of seconds to wait for data in configmgr db .Parameter WaitForAutoPilotDevicesInSeconds defines the amount of seconds to wait for a device after the last autopilot ts step .Example Confirm-VMDeployment -Name "VM-01" -CM_CollectionName $CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials .NOTES To detect Autopilot Task Sequences there need to be as Last Step "Autopilot: Remove unattend.xml from Panther" #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $CM_CollectionName, [Parameter(Mandatory = $true)] [string] $CM_Siteserver_FQDN, [Parameter(Mandatory = $true)] [PSCredential] $CM_Credentials, [Parameter(Mandatory = $true)] [PSCredential] $VM_Credentials, [Parameter(Mandatory = $false)] [int] $TimeoutInMinutes = 120, [Parameter(Mandatory = $false)] [switch] $DisableAutoClearPXE, [Parameter(Mandatory = $false)] [int] $PXEClearAfterInSeconds = 250, [Parameter(Mandatory = $false)] [int] $WaitForCMDatabaseThresholdInSeconds = 700, [Parameter(Mandatory = $false)] [int] $WaitForAutoPilotDevicesInSeconds = 240, [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' $AutoPilot_StepName = "Autopilot: Remove unattend.xml from Panther" if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } # PS Session try { Confirm-VMPresence -Name $Name $CMPS_Session = New-PSSession -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials Invoke-Command -Session $CMPS_Session -ScriptBlock { $CMPSDriveName = "CMPS-$(Get-Random)" Import-Module -Name "ConfigurationManager" New-PSDrive -Name $CMPSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null Set-Location -Path "$($CMPSDriveName):\" } } catch { throw "error ps session to $($CM_Siteserver_FQDN): $($PSItem.Exception.Message)" } # set environment try { $ResourceID = Invoke-Command -Session $CMPS_Session -ArgumentList $Name -ScriptBlock { try { return (Get-CMDevice -Name $args[0] -Fast).ResourceID } catch { throw $PSItem.Exception.Message } } $DeploymentID = Invoke-Command -Session $CMPS_Session -ArgumentList $CM_CollectionName -ScriptBlock { try { return (Get-CMDeployment -CollectionName $args[0]).DeploymentID } catch { throw $PSItem.Exception.Message } } $TaskSequenceName = Invoke-Command -Session $CMPS_Session -ArgumentList $CM_CollectionName -ScriptBlock { try { return (Get-CMDeployment -CollectionName $args[0]).SoftwareName } catch { throw $PSItem.Exception.Message } } $DeploymentProperties = @{ ResourceID = $ResourceID DeploymentID = $DeploymentID TaskSequenceName = $TaskSequenceName AutopilotTaskSequence = $false Started = $false StartDate = $null Duration = $null PXEFlagSet = $false SummarizationTriggered = $false PXEFlagCleared = $false FoundDataInConfigMgrDB = $false Running = $false Finished = $false } # output currently used task sequence for vm Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - the task sequence $($DeploymentProperties.TaskSequenceName) is used for the os deployment" } catch { throw "could not set environment - $($PSItem.Exception.Message)" } # check status try { # check if deployment has started do { Write-Verbose "current DeploymentProperties" $DeploymentProperties.Keys | ForEach-Object { Write-Verbose "$($PSItem)_$($DeploymentProperties."$($PSitem)")" } # get configmgr device pxe flag status # https://katystech.blog/configmgr/delving-into-the-last-pxe-advertisement-flag $PXEAdvertisementStatus = Invoke-Command -Session $CMPS_Session -ArgumentList $Name -ScriptBlock { $Device_PXE_Properties = Get-CimInstance -Namespace "ROOT\SMS\site_$((Get-CMSite).SiteCode)" -Query "SELECT NetbiosName,AdvertisementID,LastPXEAdvertisementTime FROM SMS_LastPXEAdvertisement WHERE NetbiosName = '$($args[0])'" if($Device_PXE_Properties.LastPXEAdvertisementTime -eq "" -or $null -eq $Device_PXE_Properties.LastPXEAdvertisementTime) { # "not booted --> PXE Flag not set" $PXEFlagSet = $false } elseif($Device_PXE_Properties.LastPXEAdvertisementTime -ne "") { # "booted at $($Device_PXE_Properties.LastPXEAdvertisementTime)--> PXE Flag set" $PXEFlagSet = $true } return @{ PXEFlagSet = $PXEFlagSet LastPXEAdvertisementTime = $Device_PXE_Properties.LastPXEAdvertisementTime } } $DeploymentProperties.PXEFlagSet = $PXEAdvertisementStatus.PXEFlagSet $DeploymentProperties.LastPXEAdvertisementTime = $PXEAdvertisementStatus.LastPXEAdvertisementTime Write-Verbose "$($Name): DeploymentProperties.PXEFlagSet_$($DeploymentProperties.PXEFlagSet) DeploymentProperties.LastPXEAdvertisementTime_$($DeploymentProperties.LastPXEAdvertisementTime)" if ($DeploymentProperties.PXEFlagSet -eq $true) { $DeploymentProperties.StartDate = Get-Date Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - pxe flag set, deployment has started" $DeploymentProperties.Started = $true } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - pxe flag not set, deployment not started yet" Start-Sleep -Seconds 15 } } while ($DeploymentProperties.Started -eq $false) # check status of deployment if ($DeploymentProperties.Started -eq $true) { $DeploymentProperties.Running = $true do { $StatusMessages = $null $DeploymentProperties.Duration = (Get-Date) - $DeploymentProperties.StartDate Write-Verbose "$($Name): current DeploymentProperties" $DeploymentProperties.Keys | ForEach-Object { Write-Verbose "$($Name): $($PSItem)_$($DeploymentProperties."$($PSitem)")" } if ($DeploymentProperties.PXEFlagSet -eq $true) { # get current status message deployment from configmgr db $StatusMessages = Invoke-Command -Session $CMPS_Session -ArgumentList $DeploymentProperties.ResourceID, $DeploymentProperties.DeploymentID, $CM_Siteserver_FQDN -ScriptBlock { try { $CM_SiteCode = (Get-CMSite).SiteCode $Query = "Select AdvertisementID,ResourceID,Step,ActionName,LastStatusMessageIDName from v_TaskExecutionStatus where (AdvertisementID = '$($args[1])' AND ResourceID = '$($args[0])')" return (Invoke-Sqlcmd -ServerInstance "$($args[2])\$($CM_SiteCode)" -Database "CM_$($CM_SiteCode)" -Query $Query | Sort-Object -Property Step -Descending) } catch { throw "could not get data from db - $($PSItem.Exception.Message)" } } Write-Verbose "$($Name): current StatusMessages $($StatusMessages)" # output current status try { if ($null -eq $StatusMessages -or $StatusMessages -eq "") { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - waiting on data in configmgr database, $("{0}" -f [math]::round($DeploymentProperties.Duration.TotalSeconds,1)) seconds since start ($($PXEClearAfterInSeconds)/$($WaitForCMDatabaseThresholdInSeconds)), PXE Flag set on $($DeploymentProperties.LastPXEAdvertisementTime)" } else { $DeploymentProperties.FoundDataInConfigMgrDB = $true $StatusObject = @{ "AdvertisementID" = $StatusMessages[0].AdvertisementID "ResourceID" = $StatusMessages[0].ResourceID "Step" = $StatusMessages[0].Step "ActionName" = $StatusMessages[0].ActionName "LastStatusMessageIDName" = $StatusMessages[0].LastStatusMessageIDName } #region set actioname if empty if ($StatusMessages[0].ActionName -ne "") { $StatusObject.ActionName = $StatusMessages[0].ActionName } elseif ($StatusMessages[1].ActionName -ne "") { $StatusObject.ActionName = "no actionname, last was $($StatusMessages[1].ActionName)" } elseif ($StatusMessages[2].ActionName -ne "") { $StatusObject.ActionName = "no actionname, last was $($StatusMessages[2].ActionName)" } elseif ($StatusMessages[3].ActionName -ne "") { $StatusObject.ActionName = "no actionname, last was $($StatusMessages[3].ActionName)" } elseif ($StatusMessages[4].ActionName -ne "") { $StatusObject.ActionName = "no actionname, last was $($StatusMessages[4].ActionName)" } elseif ($StatusMessages[5].ActionName -ne "") { $StatusObject.ActionName = "no actionname, last was $($StatusMessages[5].ActionName)" } elseif ($StatusMessages[6].ActionName -ne "") { $StatusObject.ActionName = "no actionname, last was $($StatusMessages[6].ActionName)" } else { $StatusObject.ActionName = "no actionname, couldnt find it" } #endregion Write-Verbose "$($Name): currentstatusmessage $($StatusObject.LastStatusMessageIDName)" if ($StatusObject.ActionName -like "*$($AutoPilot_StepName)*" ) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - the task sequence contains the step $($AutoPilot_StepName) - continue with the corresponding steps" $DeploymentProperties.AutopilotTaskSequence = $true $DeploymentProperties.Running = $false } elseif ($StatusObject.ActionName -like "*Final Restart*" -or ` $StatusObject.LastStatusMessageIDName -like "*The task sequence manager successfully completed execution of the task sequence*" -or ` $StatusObject.LastStatusMessageIDName -like "*The task execution engine successfully completed a task sequence*" ) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - finished deployment" $DeploymentProperties.Running = $false } else { if ($StatusObject.Step.ToString().Length -eq 1) { $StepNumber = "00$($StatusObject.Step)" } elseif ($StatusObject.Step.ToString().Length -eq 2) { $StepNumber = "0$($StatusObject.Step)" } Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - step $($StepNumber) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } } } catch { throw "could not generate output - $($PSItem.Exception.Message)" } } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - pxe flag not set, deployment not started yet, $("{0}" -f [math]::round($DeploymentProperties.Duration.TotalSeconds,1)) seconds since start ($($PXEClearAfterInSeconds)/$($WaitForCMDatabaseThresholdInSeconds))" } #region trigger cm deployment summarization Write-Verbose "$($Name): DisableAutoClearPXE_$($DisableAutoClearPXE) DeploymentProperties.Duration.TotalSeconds_$($DeploymentProperties.Duration.TotalSeconds) PXEFlagCleared_$($DeploymentProperties.PXEFlagCleared) FoundDataInConfigMgrDB_$($DeploymentProperties.FoundDataInConfigMgrDB)" if ( $DisableAutoClearPXE -eq $false -and ` $DeploymentProperties.Duration.TotalSeconds -ge ($PXEClearAfterInSeconds * 1 / 2 ) -and ` $DeploymentProperties.Duration.TotalSeconds -lt $WaitForCMDatabaseThresholdInSeconds -and ` $DeploymentProperties.SummarizationTriggered -ne $true -and ` $DeploymentProperties.FoundDataInConfigMgrDB -eq $false ) { Write-ToLogOrConsole @LogParam -Severity Warning -Message "$($Name) - triggering cm deployment summarization on collection $($CM_CollectionName)" Invoke-Command -Session $CMPS_Session -ArgumentList $CM_CollectionName -ScriptBlock { try { Invoke-CMDeploymentSummarization -CollectionName $args[0] } catch { throw "could not trigger summarization - $($PSItem.Exception.Message)" } } $DeploymentProperties.SummarizationTriggered = $true Start-Sleep -Seconds 15 } #endregion #region pxe clear after a period of time no boot was detected Write-Verbose "$($Name): DisableAutoClearPXE_$($DisableAutoClearPXE) DeploymentProperties.Duration.TotalSeconds_$($DeploymentProperties.Duration.TotalSeconds) PXEFlagCleared_$($DeploymentProperties.PXEFlagCleared) FoundDataInConfigMgrDB_$($DeploymentProperties.FoundDataInConfigMgrDB)" if ( $DisableAutoClearPXE -eq $false -and ` $DeploymentProperties.Duration.TotalSeconds -ge $PXEClearAfterInSeconds -and ` $DeploymentProperties.Duration.TotalSeconds -lt $WaitForCMDatabaseThresholdInSeconds -and ` $DeploymentProperties.PXEFlagCleared -ne $true -and ` $DeploymentProperties.FoundDataInConfigMgrDB -eq $false ) { Write-ToLogOrConsole @LogParam -Severity Warning -Message "$($Name) - the task sequence did not start, because no data was found in configmgr db, try to reset pxe flag and restart vm once" Get-VM -Name $Name | Stop-VM -TurnOff -Force Invoke-Command -Session $CMPS_Session -ArgumentList $Name -ScriptBlock { try { Get-CMDevice -Name $args[0] -Fast | Clear-CMPxeDeployment } catch { throw "could not clear pxe flag - $($PSItem.Exception.Message)" } } $DeploymentProperties.PXEFlagCleared = $true Start-Sleep -Seconds 7 Start-VM -Name $Name } #endregion # stops after timeout if ($DeploymentProperties.Duration.TotalSeconds -ge $WaitForCMDatabaseThresholdInSeconds -and $DeploymentProperties.FoundDataInConfigMgrDB -eq $false) { throw "waited for $($WaitForCMDatabaseThresholdInSeconds) seconds, but no data for $($($StatusObject.DeviceName)) in cm database. verify osd/pxe started on the vm" } Start-Sleep -Seconds 1 # strange errors, session maybe broke # check if deployment is finished if ($DeploymentProperties.Running -eq $false) { $DeploymentProperties.Finished = $true } else { Start-Sleep -Seconds 15 } } while ($DeploymentProperties.Finished -eq $false -and $DeploymentProperties.Duration.TotalMinutes -le $TimeoutInMinutes) } if ($DeploymentProperties.AutopilotTaskSequence -eq $true) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - the task sequence contains the step $($AutoPilot_StepName) - waiting for $($WaitForAutoPilotDevicesInSeconds)" Start-Sleep -Seconds $WaitForAutoPilotDevicesInSeconds } if ($DeploymentProperties.Duration.TotalMinutes -ge $TimeoutInMinutes) { throw "deployment not finished after $($TimeoutInMinutes) mins, check the logs in configmgr or inside the vms" } } catch { $ErrorMessage = "$($Name) - error checking status - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } finally { Remove-PSSession -Session $CMPS_Session } } function New-VirtualMaschine { <# .Description creates a hyperv vm .Parameter Name name of of the virtual machine .Parameter Path file path of the virtual machine .Parameter vSwitchName virtual switch name .Parameter CPUCount amount of virutal cores .Parameter RAM amount of ram .Parameter DiskSize amount of os disk size .Parameter DynamicRAMEnabled should the vm use dynamic ram .Parameter vTPMEnabled should there be a virtual tpm .Parameter AutoStartEnabled shoudl the vm automatically start .Example New-VirtualMaschine -Name "VM01" -Path "V:\VMs" -vSwitchName "vSwitch01" .Example New-VirtualMaschine -Name "VM01" -Path "V:\VMs" -vSwitchName "vSwitch01" -CPUCount 4 -RAM 16GB -DiskSize 120GB -DynamicRAMEnabled $false -vTPMEnabled $true -AutoStartEnabled $true .NOTES #> param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $Path, [Parameter(Mandatory = $true)] [string] $vSwitchName, [Parameter(Mandatory = $false)] [int] $CPUCount = 2, [Parameter(Mandatory = $false)] [Int64] $RAM = 4gb, [Parameter(Mandatory = $false)] [Int64] $DiskSize = 80GB, [Parameter(Mandatory = $false)] [bool] $DynamicRAMEnabled = $false, [Parameter(Mandatory = $false)] [bool] $vTPMEnabled = $false, [Parameter(Mandatory = $false)] [bool] $AutoStartEnabled = $false, [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } # solve possible input problems if ($Path[-1] -eq "\") { $Path = $Path.Substring(0, $Path.Length - 1) } try { $VMVHDXPath = ($Path + "\" + $Name + "\Virtual Hard Disks\" + $Name + ".vhdx") if (Test-Path -Path $VMVHDXPath) { throw "vhdx for $($Name) already exists, please remove it" } try { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - creating vdhx $($VMVHDXPath)" New-VHD -Path $VMVHDXPath -SizeBytes $DiskSize -Dynamic | Out-Null Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - creating vm" New-VM -Name $Name -MemoryStartupBytes $RAM -Path $Path -Generation 2 -VHDPath $VMVHDXPath -BootDevice NetworkAdapter -SwitchName $vSwitchName | Out-Null } catch { $ErrorMessage = "error during creation of vhdx or vm - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } # configure vm additional settings try { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - set vm cpu count to $($CPUCount)" Set-VMProcessor -VMName $Name -Count $CPUCount if ($DynamicRAMEnabled -eq $false) { if ((Get-VM -Name $Name).DynamicMemoryEnabled) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - set vm static memory" Set-VM -Name $Name -StaticMemory } } if ($vTPMEnabled -eq $true) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - enable vm vTPM" Set-VMKeyProtector -VMName $Name -NewLocalKeyProtector Enable-VMTPM -VMName $Name } if ($AutoStartEnabled -eq $true) { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - enable vm automatic start with 10 seconds delay" Set-VM -AutomaticStartAction Start -VMName $Name -AutomaticStartDelay 10 } else { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - disable vm automatic start" Set-VM -AutomaticStartAction Nothing -VMName $Name } Set-VM -AutomaticStopAction ShutDown -VMName $Name Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - enable vm integration services" Get-VMIntegrationService -VMName $Name | Where-Object -Property Enabled -EQ $false | Enable-VMIntegrationService Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - configuring checkpoint settings" Set-VM -VMName $Name -AutomaticCheckpointsEnabled $false -CheckpointType Production } catch { $ErrorMessage = "error while setting properties - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } # configuring mac address Start-VM -Name $Name Start-Sleep -Seconds 1 Stop-VM -Name $Name -Force -TurnOff Start-Sleep -Seconds 3 $MacAddress = (Get-VM -Name $Name | Get-VMNetworkAdapter).MacAddress Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - set vm static mac address" Set-VMNetworkAdapter -VMName $Name -StaticMacAddress $MacAddress # verify vm was created Confirm-VMPresence -Name $Name Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - vm creation complete" } catch { $ErrorMessage = "$($Name) - error during creation - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } } function Test-VMConnection { <# .Description checks if a powershell direct connection to the vm can be established .Parameter VMId Id of the vm .Parameter LocalAdminCreds local admin credentials of the vm .Example # checks the powershell direct connection to VDC01 Test-VMConnection -VMId (Get-VM -Name VDC01).Id -LocalAdminCreds $VM_Credentials .NOTES #> [cmdletbinding()] param ( # VM object id [Parameter(Mandatory = $true)] [Guid] $VMId, # local admin credentials [Parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()] [PSCredential] $LocalAdminCreds ) $VM = Get-VM -Id $VMId try { Write-Output "------" if ($VM.State -eq "Off") { Write-Output "------" Write-Output "$($VM.Name): not running - starting" $VM | Start-VM -WarningAction SilentlyContinue } # Wait for the VM's heartbeat integration component to come up if it is enabled $HearbeatIC = (Get-VMIntegrationService -VM $VM | Where-Object Id -match "84EAAE65-2F2E-45F5-9BB5-0E857DC8EB47") if ($HearbeatIC -and ($HearbeatIC.Enabled -eq $true)) { $StartTime = Get-Date do { $WaitForMinitues = 5 $TimeElapsed = $(Get-Date) - $StartTime if ($($TimeElapsed).TotalMinutes -ge 5) { throw "$($VM.Name): integration components did not come up after $($WaitForMinitues) minutes" } Start-Sleep -sec 1 } until ($HearbeatIC.PrimaryStatusDescription -eq "OK") Write-Output "$($VM.Name): heartbeat IC connected" } do { $WaitForMinitues = 5 $TimeElapsed = $(Get-Date) - $StartTime Write-Output "$($VM.Name): testing connection" if ($($TimeElapsed).TotalMinutes -ge 5) { throw "$($VM.Name): could not connect to ps direct after $($WaitForMinitues) minutes" } Start-Sleep -sec 3 $PSReady = Invoke-Command -VMId $VMId -Credential $LocalAdminCreds -ErrorAction SilentlyContinue -ScriptBlock { $True } } until ($PSReady) } catch { throw "$($VM.Name): $($PSItem.Exception.Message)" } return $PSReady } function Confirm-VMPresence { <# .Description checks if the vm is registered with the local hypervisor .Parameter Name name of the vm .Example Confirm-VMPresence -Name "VM-01" .NOTES #> param ( [Parameter(Mandatory = $true)] [string] $Name ) try { if ($null -eq (Get-VM -Name $Name)) { throw "$($Name) could not be found" } else { Write-Verbose "found vm $($Name) on $($env:COMPUTERNAME)" } } catch { throw $PSItem.Exception.Message } } function Add-VMDisk { <# .Description add a virtual disk to a vm .Parameter VMName name of the vm .Parameter VHDXPath where should the vhdx file be stored .Parameter VHDXSize size in byte of the disk .Example # this will add a disk to the vm and store ist at the file location from the vm Add-VMDisk -VMName $VM.Name -VHDXPath ($VM.ConfigurationLocation + "\Virtual Hard Disks\" + $VM.Name + "-2.vhdx") -VHDXSize $DMP_ContentLibDisk_Size .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $VMName, [Parameter(Mandatory = $true)] [string] $VHDXPath, [Parameter(Mandatory = $false)] [Int64] $VHDXSize = 80GB ) try { Write-Output "$($VMName): creating disk $($VHDXPath)" New-VHD -Path $VHDXPath -SizeBytes $VHDXSize -Dynamic | Out-Null Add-VMHardDiskDrive -VMName $VMName -Path $VHDXPath } catch { throw "$($PSItem.Exception.Message)" } } function Set-VMIPConfig { <# .Description configures the network interface of a vm .Parameter VMName name of the vm .Parameter VMCredential local admin credentials of the vm .Parameter IPAddress ip address that should be assigned .Parameter NetPrefix subnet prefix, aka 24 or 16 .Parameter DefaultGateway gateway of the subnet .Parameter DNSAddresses dns server ip addresses .Example # this will configure the ip interface Set-VMIPConfig -VMName $DMP_VM.Name -VMCredential $VMCredential-IPAddress "192.168.1.21" -NetPrefix $NetPrefix -DefaultGateway $DefaultGateway -DNSAddresses $DNSAddresses .NOTES always uses the first nic founc on the system #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $VMName, [Parameter(Mandatory = $true)] [pscredential] $VMCredential, [Parameter(Mandatory = $true)] [string] $IPAddress, [Parameter(Mandatory = $true)] [int] $NetPrefix, [Parameter(Mandatory = $true)] [string] $DefaultGateway, [Parameter(Mandatory = $true)] [string[]] $DNSAddresses ) Write-Output "$($VMName): configuring network" Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock { try { $InterfaceObject = (Get-NetAdapter)[0] Write-Output "nic with mac $($InterfaceObject.MacAddress) was selected" If (($InterfaceObject | Get-NetIPConfiguration).IPv4Address.IPAddress) { $InterfaceObject | Remove-NetIPAddress -AddressFamily "IPv4" -Confirm:$false } If (($InterfaceObject | Get-NetIPConfiguration).Ipv4DefaultGateway) { $InterfaceObject | Remove-NetRoute -AddressFamily "IPv4" -Confirm:$false } Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\services\Tcpip\Parameters\Interfaces\$($InterfaceObject.InterfaceGuid)" -Name EnableDHCP -Value 0 Start-Sleep -Seconds 2 Write-Output "nic with mac $($InterfaceObject.MacAddress) will have ip $($using:IPAddress)" $InterfaceObject | New-NetIPAddress -IPAddress $using:IPAddress -AddressFamily "IPv4" -PrefixLength $using:NetPrefix -DefaultGateway $using:DefaultGateway | Out-Null $InterfaceObject | Set-DnsClientServerAddress -ServerAddresses $using:DNSAddresses } catch { throw "error setting ip interface - $($PSItem.Exception.Message)" } } } function Add-VMToDomain { <# .Description takes the vm into a domain .Parameter VMName name of the vm .Parameter VMCredential local admin credentials of the vm .Parameter DomainName name of the domain where the vm should be joined .Parameter DomainCredential domain credentials with the permission to join devices .Parameter OUPath ou path in the domain where the vm should be organized .Parameter NoReboot when used, the vm will not reboot after join .Example # this will join the vm to the specified domain and OU Add-VMToDomain -VMName $SiteServer_VM.Name -VMCredential $VMCredential -DomainName $DomainName -DomainCredential $Domain_Credentials -OUPath "OU=Servers,OU=CM,OU=TIER-1,OU=ESAE,DC=INTUNE-CENTER,DC=DE" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $VMName, [Parameter(Mandatory = $true)] [pscredential] $VMCredential, [Parameter(Mandatory = $true)] [string] $DomainName, [Parameter(Mandatory = $true)] [pscredential] $DomainCredential, [Parameter(Mandatory = $false)] [string] $OUPath, [Parameter(Mandatory = $false)] [switch] $NoReboot ) $ErrorActionPreference = 'Stop' try { Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential Write-Output "joining vm $($VMName) to domain $($DomainName)" Invoke-Command -VMName $VMName -Credential $VMCredential -ArgumentList $DomainName, $OUPath, $DomainCredential, $NoReboot -ScriptBlock { try { Confirm-DomainConnectivity -DomainName $args[0] Start-Sleep -Seconds 1 if ($args[3] -eq $true) { if ($null -ne $args[1]) { Add-Computer -Credential $args[2] -DomainName $args[0] -OUPath $args[1] -WarningAction SilentlyContinue } else { Add-Computer -Credential $args[2] -DomainName $args[0] -WarningAction SilentlyContinue } } else { if ($null -ne $args[1]) { Add-Computer -Credential $args[2] -DomainName $args[0] -OUPath $args[1] -Restart -WarningAction SilentlyContinue } else { Add-Computer -Credential $args[2] -DomainName $args[0] -Restart -WarningAction SilentlyContinue } } } catch { throw $PSItem.Exception.Message } } if ($NoReboot -eq $true) { Write-Output "domainjoin of vm $($VMName) successfull - reboot required" } else { Write-Output "domainjoin of vm $($VMName) successfull - vm will do reboot" } } catch { throw "error joining vm $($VMName) to domain $($DomainName) - $($PSItem.Exception.Message)" } } function Confirm-VMState { <# .Description checks if the vm is running and starts it if necessary then checks the connection to the vm via powershell direct .Parameter VMObject name of the vm .Parameter VMCredential local admin credentials of the vm .Example Confirm-VMState -VMObject $VM -VMCredential $VMCred .NOTES #> param ( # VM Objects [Parameter(Mandatory = $true)] [System.Object] $VMObject, [Parameter(Mandatory = $true)] [PSCredential] $VMCredential ) try { if ($VMObject.State -ne "Running") { Write-Output "starting $($VMObject.Name) because the vm was stopped" Start-VM -VM $VMObject Start-Sleep -Seconds 10 } Write-Output "verify the connection to $($VMObject.Name)" if (Test-VMConnection -VMId $VMObject.Id -LocalAdminCreds $VMCredential) { Write-Output "connected to $($VMObject.Name) successful - continue" } else { throw "error while connecting to $($VMObject.Name) with ps direct - $($PSItem.Exception.Message)" } } catch { throw "$($PSItem.Exception.Message)" } } function Confirm-HyperV { <# .Description this function throws errors, when hyper-v is not installed .Example # this checks if hyper-v is installed Confirm-HyperV .NOTES #> try { $OS_Info = Get-CimInstance -ClassName Win32_OperatingSystem if ($OS_Info.ProductType -eq 3) { if ((Get-WindowsFeature -Name Hyper-V).installed -ne $true) { throw "Hyper-v not installed" } } elseif ($OS_Info.ProductType -eq 1) { if ((Get-WindowsOptionalFeature -FeatureName Microsoft-Hyper-V -Online).State -ne "Enabled") { throw "Hyper-v not installed" } } } catch { throw $PSItem.Exception.Message } } function Set-VMInstallSnapshot { <# .Description this creates a snapshot .Parameter VMName name of vm .Parameter SnapshotName name of snapshot .Parameter VMCredential credentials for vm .Example # this creates a snapshot for the vm Set-VMInstallSnapshot -VMName $PSItem -SnapshotName "$(Get-Date -format "yyyy-MM-dd_HH.mm.ss") - initial configuration" -VMCredential $VM_Credentials .NOTES vm will stop and start during this process #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $VMName, [Parameter(Mandatory = $true)] [string] $SnapshotName, [Parameter(Mandatory = $true)] [pscredential] $VMCredential, [Parameter(Mandatory = $false)] [bool] $AutopilotDevice = $false ) try { if ($AutopilotDevice -eq $false) { Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential Stop-VM -Name $VMName -Force } Checkpoint-VM -Name $VMName -SnapshotName $SnapshotName if ($AutopilotDevice -eq $false) { Start-VM -Name $VMName Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential } } catch { throw $PSItem.Exception.Message } } function Restart-VMIfNeeded { <# .Description this function checks if the vm has to reboot and does it, if needed .Parameter VMName name of vm .Parameter Credentials credentials for vm .Example # this does a restart of the vm if needed Restart-VMIfNeeded -VMName $PSItem -Credential $Domain_Credentials .NOTES vm will stop and start during this process #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $VMName, [Parameter(Mandatory = $true)] [pscredential] $Credentials ) $VM = Get-VM -Name $VMName $RebootPending = Invoke-Command -VMName $VMName -Credential $Credentials -ScriptBlock { Import-Module -Name "NTS.Tools" -DisableNameChecking return (Test-RebootPending) } if ($RebootPending) { Write-Output "doing a reboot of $($VMName)" Restart-VM -VM $VM -Type Reboot -Force -Wait Confirm-VMState -VMObject $VM -VMCredential $Credentials } } function New-VMHost { <# .Description this function checks if the vm has to reboot and does it, if needed .Parameter SwitchName name of the virtual switch that should be created .Parameter TrustedHostsValue value for trustedhosts to add, needed for configmgr things .Parameter VM_Drive_Letter letter for the vm volume, which will be created .Parameter LogFileName log file name .Example New-VMHost -SwitchName $Course_Shortcut -TrustedHostsValue "$($CM_Siteserver_NetBIOS),$($CM_Siteserver_FQDN)" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $SwitchName, [Parameter(Mandatory = $true)] [string] $TrustedHostsValue, [Parameter(Mandatory = $false)] [char] $VM_Drive_Letter = 'V', [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } try { # first step - winrm for configmgr Write-Progress -Activity "New-VMHost" -CurrentOperation "configuring trusted hosts" -Status "running" -PercentComplete 0 Write-ToLogOrConsole @LogParam -Severity Info -Message "configuring winrm to allow connections with configmgr" Start-Service -Name "WinRM" Set-Item -Path "WSMan:\localhost\Client\TrustedHosts" -Value $TrustedHostsValue -Force -Concatenate Start-Sleep -Milliseconds 0.4 # just for the progress Write-ToLogOrConsole @LogParam -Severity Info -Message "setting hyper-v environment" # second step - prepare host for vms Write-Progress -Activity "New-VMHost" -CurrentOperation "creating volume" -Status "running" -PercentComplete 25 New-VMVolume -VMDriveLetter $VM_Drive_Letter -LogFileName $LogFileName Start-Sleep -Milliseconds 0.4 # just for the progress # third step - virtual switch Write-Progress -Activity "New-VMHost" -CurrentOperation "creating switch" -Status "running" -PercentComplete 50 New-VMVSwitch -Name $SwitchName -LogFileName $LogFileName Start-Sleep -Seconds 0.4 # just for the progress # fourth step - hyperv config Write-Progress -Activity "New-VMHost" -CurrentOperation "configuring hyperv" -Status "running" -PercentComplete 75 $VM_DefaultPath = "$($VM_Drive_Letter):\VMs" Set-VMHost -EnableEnhancedSessionMode $True -VirtualHardDiskPath $VM_DefaultPath -VirtualMachinePath $VM_DefaultPath Start-Sleep -Milliseconds 0.4 # just for the progress Write-Progress -Activity "New-VMHost" -CurrentOperation "configuring hyperv" -Status "finished" -PercentComplete 100 -Completed } catch { $ErrorMessage = "host preparations failed - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } } function Install-VMs { <# .Description installs vms on localhost and configures a snapshot .Parameter VM_Config Object that may contains multiple descriptive objects for deployment from a VM. For Example: $VM_Config = @() $VM_01 = [PSCustomObject]@{ Name = "$($Course_Shortcut)-VWIN11-$($Participant_Number)1" Path = $VMPath vSwitchName = $SwitchName CPUCount = $CPUCount RAM = $RAM DiskSize = $DynamicDiskSize DynamicRAMEnabled = $false vTPMEnabled = $true AutoStartEnabled = $true CM_CollectionName = $CM_Collection_W11_Autopilot Credentials = $VM_Credentials AutopilotDevice = $true } $VM_Config = $VM_Config + $VM_01 .Parameter CM_Siteserver_FQDN fqdn of siet server .Parameter CM_Credentials credentials to connect to the config .Parameter VM_Credentials admin credentials to connect to the vm .Parameter SecondsToWaitBeforeCreatingSnapshots Seconds to wait before creating Snapshots .Parameter SnapShotNameSuffix string to add to the snapshot name at the end .Example Install-VMs -VM_Config $VM_Config -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName "Install-VMs_$(Get-Date -Format "dd-MM-yyyy_hh.mm.ss").log" .NOTES VM need to have a static mac address and only one nic The Parameter VM_Config accepts the following Parameters for each object, based on New-VirtualMachine and Register-VMInConfigMgr: Name, Path, vSwitchName, CPUCount, RAM, DiskSize, DynamicRAMEnabled, vTPMEnabled, AutoStartEnabled, Credentials, AutopilotDevice, LogFileName, CM_CollectionName #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [PSCustomObject] $VM_Config, [Parameter(Mandatory = $true)] [string] $CM_Siteserver_FQDN, [Parameter(Mandatory = $true)] [pscredential] $CM_Credentials, [Parameter(Mandatory = $false)] [int] $SecondsToWaitBeforeCreatingSnapshots = 120, [Parameter(Mandatory = $false)] [string] $SnapShotNameSuffix = "$(Get-Date -format "dd-MM-yyyy_hh:mm:ss") - initial deployment", [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } # checks if ($PSVersionTable.PSVersion -lt [version]"7.2.0") { throw "you have to use powershell 7.2.0 or higher" } # checking if the vm exists $VM_Config | ForEach-Object { if ($null -ne (Get-VM | Where-Object -Property Name -like $PSitem.Name)) { $ErrorMessage = "vm $($PSItem.Name) already exists, stopping" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } } # deployment try { # stuff for write progress $origin = @{} $VM_Config | Foreach-Object { $origin.($PSItem.Name) = @{} } $sync = [System.Collections.Hashtable]::Synchronized($origin) # create vm deployment jobs $BaseActivityName = "Install-VMs" Write-Progress -Id 0 -Activity $BaseActivityName -Status "Processing" -PercentComplete 0 $Jobs = $VM_Config | Sort-Object -Property Name | Foreach-Object -ThrottleLimit $VM_Config.count -AsJob -Parallel { try { $ErrorActionPreference = 'Stop' Import-Module -Name "NTS.Tools" $StepCountPerVM = 6 # define vars from parent session $LogParam = $using:LogParam $CM_Siteserver_FQDN = $using:CM_Siteserver_FQDN $CM_Credentials = $using:CM_Credentials $syncCopy = $using:sync $process = $syncCopy.$($PSItem.Name) $process.ParentId = 0 $process.Id = $($using:VM_Config).IndexOf($PSItem) + 1 $process.Activity = "VM $($PSItem.Name)" $process.Status = "starting" $process.PercentComplete = 1 # Fake workload start up that takes x amount of time to complete Start-Sleep -Milliseconds (3..10 | Get-Random | Foreach-Object { $PSItem * 100 }) #region do stuff # 1 step $process.Status = "creating vm" New-VirtualMaschine -Name $PSItem.Name -Path $PSItem.Path -vSwitchName $PSItem.vSwitchName -CPUCount $PSItem.CPUCount -RAM $PSItem.RAM -DiskSize $PSItem.DiskSize -DynamicRAMEnabled $PSItem.DynamicRAMEnabled -vTPMEnabled $PSItem.vTPMEnabled -AutoStartEnabled $PSItem.AutoStartEnabled -LogFileName $LogParam.LogFileName $process.PercentComplete = [Math]::Round(1/$StepCountPerVM,2)*100 # 2 step $process.Status = "registering vm" Register-VMInConfigMgr -Name $PSItem.Name -MacAddress (Get-VM -Name $PSItem.Name | Get-VMNetworkAdapter).MacAddress -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName $LogParam.LogFileName $process.PercentComplete = [Math]::Round(2/$StepCountPerVM,2)*100 # 3 step $process.Status = "starting deployment" try { $DelayStartsInSeconds = (Get-Random -Minimum 3 -Maximum 5) Write-Host "$($PSItem): $($LogParam)" Write-ToLogOrConsole @LogParam -Severity Info -Message "$($PSItem.Name) - starting for pxe deployment, using delay of $($DelayStartsInSeconds)" Start-Sleep -Seconds $DelayStartsInSeconds Start-VM -VMName $PSItem.Name } catch { throw "error while starting - $($PSItem.Exception.Message)" } $process.PercentComplete = [Math]::Round(3/$StepCountPerVM,2)*100 # 4 step $process.Status = "verifying deployment" Confirm-VMDeployment -Name $PSItem.Name -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -VM_Credentials $PSItem.Credentials -LogFileName $LogParam.LogFileName $process.PercentComplete = [Math]::Round(4/$StepCountPerVM,2)*100 Write-ToLogOrConsole @LogParam -Severity Info -Message "$($PSItem.Name) - now waiting for $($using:SecondsToWaitBeforeCreatingSnapshots) seconds" Start-Sleep -Seconds $using:SecondsToWaitBeforeCreatingSnapshots # 5 step $process.Status = "creating snapshot" $SnapShotName = "$($PSItem.Name) - $($using:SnapShotNameSuffix)" Write-ToLogOrConsole @LogParam -Severity Info -Message "$($PSItem.Name) - creating snapshot $($SnapShotName)" Set-VMInstallSnapshot -VMName $PSItem.Name -SnapshotName $SnapShotName -VMCredential $PSItem.Credentials -AutopilotDevice $PSItem.AutopilotDevice $process.PercentComplete = [Math]::Round(5/$StepCountPerVM,2)*100 # 6 step $process.Status = "doing cleanup" Remove-VMConfigMgrDeployment -Name $PSItem.Name -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName $LogParam.LogFileName $process.PercentComplete = [Math]::Round(6/$StepCountPerVM,2)*100 # Mark process as completed $process.Status = "finished" $process.Completed = $true #endregion } catch { $process.Status = "failed" $process.PercentComplete = 100 $process.Completed = $true throw $PSItem.Exception.Message } } # verifiy jobs and create processing output while ($Jobs.State -eq 'Running') { try { # get percentage of completed $PercentageCompleted = $($sync.Values.PercentComplete | Measure-Object -Sum).Sum / $VM_Config.count if ($PercentageCompleted -lt 1) { $PercentageCompleted = 1 } $sync.Keys | Foreach-Object { # If key is not defined, ignore if (![string]::IsNullOrEmpty($sync.$PSItem.keys)) { # Create parameter hashtable to splat $param = $sync.$PSItem if ($param.Status -eq "failed") { throw "job for $($param.Activity) failed" } # Execute Write-Progress Write-Progress -Id 0 -Activity $BaseActivityName -Status "running" -PercentComplete $PercentageCompleted Write-Progress @param } } # Wait to refresh to not overload gui Start-Sleep -Seconds 0.4 } catch { $FailedJobs = $Jobs | Get-Job -IncludeChildJob | Where-Object -Property State -eq "Failed" if ($null -ne $FailedJobs) { Write-Host "jobs with id $($FailedJobs.Id) failed, troubleshoot using the following command and viewing the log file" -ForegroundColor Red Write-Host "(Get-Job -Id $($FailedJobs[0].Id) -IncludeChildJob).JobStateInfo" -ForegroundColor Red } throw $PSItem.Exception.Message } } Write-Progress -Id 0 -Activity $BaseActivityName -Status "Finished" -PercentComplete 100 -Completed } catch { Write-Verbose "stopping running jobs" $Jobs | Stop-Job -Confirm:$false $ErrorMessage = "error deploying vms - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } } function Remove-VMConfigMgrDeployment { <# .Description Removes the ConfigMgr Object related to the vms .Parameter Name Name of the VM .Parameter CM_CollectionName ConfigMgr Collection Name with the required Deployment .Parameter CM_Siteserver_FQDN FQDN of ConfigMgr .Parameter CM_Credentials Credentials of a user that can create/edit/delete CMDevices and add them to a Collection. Should be able to start a collection update .Example Remove-VMConfigMgrDeployment -Name $PSItem.Name -CM_CollectionName $PSItem.CM_CollectionName -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -LogFileName $LogFileName .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $CM_CollectionName, [Parameter(Mandatory = $true)] [string] $CM_Siteserver_FQDN, [Parameter(Mandatory = $true)] [PSCredential] $CM_Credentials, [Parameter(Mandatory = $false)] [string] $LogFileName = "" ) $ErrorActionPreference = 'Stop' if ($LogFileName -eq "") { $LogParam = @{ Console = $true } } elseif ($LogFileName -ne "") { $LogParam = @{ LogFileName = $LogFileName LogFileFolderPath = "$($env:ProgramData)\NTS\LogFiles" } } else { throw "`$LogFileName is not defined properly, was $($LogFileName)" } # PS Session try { Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - Connecting to ConfigMgr $($CM_Siteserver_FQDN)" $CMPS_Session = New-PSSession -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials Invoke-Command -Session $CMPS_Session -ScriptBlock { $CMPSDriveName = "CMPS-$(Get-Random)" Import-Module -Name "ConfigurationManager" New-PSDrive -Name $CMPSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null Set-Location -Path "$($CMPSDriveName):\" } } catch { $ErrorMessage = "error ps session to $($CM_Siteserver_FQDN): $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } try { # remove collections membership rules Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - removing collection membership in $($CM_CollectionName) after the deployment" Invoke-Command -Session $CMPS_Session -ArgumentList $Name, $CM_CollectionName -ScriptBlock { try { $CM_SiteCode = (Get-CMSite).SiteCode $Temp_CMDevice = Get-CMDevice -Name $args[0] $TargetCollection = Get-CMCollection -Name $args[1] if ($null -ne $Temp_CMDevice) { $Collections = Get-CimInstance -Namespace "root/Sms/site_$($CM_SiteCode)" -ClassName "SMS_FullCollectionMembership" -Filter "ResourceID = $($Temp_CMDevice.ResourceID)" | ` Where-Object -Property CollectionID -eq $TargetCollection.CollectionID if ($null -ne $Collections) { $Collections | ForEach-Object { $MembershipRule = Get-CMCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID | Where-Object -Property RuleName -EQ $Temp_CMDevice.Name if ($null -ne $MembershipRule) { Remove-CMDeviceCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID -ResourceId $MembershipRule.ResourceId -Confirm:$false -Force } } } } } catch { throw "error removing collection membership rules - $($PSItem.Exception.Message)" } } # removing device object after the deployment Write-ToLogOrConsole @LogParam -Severity Info -Message "$($Name) - removing computer info in configmgr after the deployment" Invoke-Command -Session $CMPS_Session -ArgumentList $Name -ScriptBlock { try { $Temp_CMDevice = Get-CMDevice -Name $args[0] if ($null -ne $Temp_CMDevice) { Remove-CMDevice -Name $args[0] -Force -Confirm:$false } } catch { throw "error removing device - $($PSItem.Exception.Message)" } } } catch { $ErrorMessage = "$($Name) - $($PSItem.Exception.Message)" Write-ToLogOrConsole @LogParam -Severity Error -Message $ErrorMessage throw $ErrorMessage } finally { Remove-PSSession -Session $CMPS_Session } } function New-VMDiskFormated { <# .Description this function adds a disk to the vm and formats .Parameter VMName name of vm .Parameter VolumeDriveLetter letter for volume .Parameter VolumeFriendlyName volume label .Parameter VolumeSize size in bytes .Example # adds a disk with the size of $ContentLibDisk_Size and formats it with letter L New-VMDiskFormated -VMName $PSItem -VolumeDriveLetter "L" -VolumeFriendlyName "ContentLib" -VolumeSize $ContentLibDisk_Size .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $VMName, [Parameter(Mandatory = $true)] [char] $VolumeDriveLetter, [Parameter(Mandatory = $true)] [string] $VolumeFriendlyName, [Parameter(Mandatory = $true)] [string] $VolumeSize, [Parameter(Mandatory = $true)] [pscredential] $VMCredential, [Parameter(Mandatory = $false)] [string] $VHDXPathXPath, [Parameter(Mandatory = $false)] [ValidateSet("NTFS", "REFS")] [string] $FileSystem = "NTFS" ) try { $VM = Get-VM -Name $VMName $CurrentVMDiskCount = (Get-VHD -VMId $VM.Id).count if ($null -eq $VHDXPathXPath -or $VHDXPathXPath -eq "") { $VHDXPathXPath = "$($VM.ConfigurationLocation)\Virtual Hard Disks\$($VM.Name)-$($CurrentVMDiskCount).vhdx" } } catch { throw "could not find out the vm or the number of disks present - $($PSItem.Exception.Message)" } try { $VolumeExists = Invoke-Command -VMName $VM.Name -Credential $VMCredential -ScriptBlock { if ($null -eq (Get-Volume -DriveLetter $using:VolumeDriveLetter -ErrorAction SilentlyContinue)) { return $false } else { $true } } if ($VolumeExists -eq $false) { Write-Output "adding disk to $($VM.Name)" Add-VMDisk -VMName $VM.Name -VHDXPath $VHDXPathXPath -VHDXSize $VolumeSize Invoke-Command -VMName $VM.Name -Credential $VMCredential -ScriptBlock { try { Write-Output "formating disk" $PhysicalDisk = Get-PhysicalDisk | Where-Object -Property Size -eq $using:VolumeSize | Sort-Object -Property DeviceId if ($PhysicalDisk.Count -gt 1) { $Disk = ($PhysicalDisk | ForEach-Object { Get-Disk -Number $PSItem.DeviceId | Where-Object -Property PartitionStyle -eq "RAW" })[0] New-Volume -DiskNumber $Disk.Number -FriendlyName $using:VolumeFriendlyName -FileSystem $using:FileSystem -DriveLetter $using:VolumeDriveLetter | Out-Null } else { New-Volume -DiskNumber $PhysicalDisk.DeviceId -FriendlyName $using:VolumeFriendlyName -FileSystem $using:FileSystem -DriveLetter $using:VolumeDriveLetter | Out-Null } } catch { throw $PSItem.Exception.Message } } } else { Write-Output "$($VM.Name): volume already exits" } } catch { throw "could not format volume - $($PSItem.Exception.Message)" } } function Connect-CourseVMToNetwork { <# .SYNOPSIS connects the vms with a course room .DESCRIPTION configures the vm network adapter for a specific course room, based on a hyper-v switch and vlan settings .PARAMETER VMFolderName Name of the folder under $($env:SystemDrive)\ClusterStorage\VMs where the vms are located .PARAMETER CourseRoom Course room name .PARAMETER ShowOnly only display changes, but dont make the changes .EXAMPLE Connect-CourseVMToNetwork -VMFolderName PKI -CourseRoom '4OG(Oben)' -ShowOnly shows only the possible change of the vmnetwork types configuration for the vms in PKI .EXAMPLE Connect-CourseVMToNetwork -VMFolderName PKI -CourseRoom '4OG(Oben)' connects all VMs in Folder "PKI" to the specified course room .NOTES - the vm files must be in the course folder under the $($env:SystemDrive)\ClusterStorage\VMs - all vlans available can be found at "https://netbox.ntsystems.de/ipam/vlans/" - which vlans are configure at "https://wlan.ntsystems.de/manage/default/devices" - vlan 10,190,403 will be skipped and therefore not changed #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet("Local", "Cluster")] [string] $HostType, [Parameter(Mandatory = $true, ParameterSetName = "Default", Position = 2)] [ValidateSet( "4OG(Oben)", "EG2(GROSS)", "EG3(KLEIN)", "VSCHULUNG1", "VSCHULUNG2", "NTSInstall" )] [string] $CourseRoom, [Parameter(Mandatory = $false, ParameterSetName = "Default", Position = 3)] [switch] $ShowOnly ) dynamicParam { # Set the dynamic parameters' name $ParameterName = 'VMFolderName' # Create and set the parameters' attributes $ParameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute $ParameterAttribute.Mandatory = $true $ParameterAttribute.Position = 0 $ParameterAttribute.ParameterSetName = "Default" # Create the collection of attributes $AttributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) # Generate and set the ValidateSet $arrSet = (Get-ChildItem -Path "$($env:SystemDrive)\ClusterStorage\VMs" | Where-Object -Property Name -NotLike "_*").Name $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) # Add the ValidateSet to the attributes collection $AttributeCollection.Add($ValidateSetAttribute) # Create and return the dynamic parameter $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) # Create the dictionary $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) return $RuntimeParameterDictionary } begin { function Show-CurrentVNICConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.Array] $VMs ) try { $VMs | ForEach-Object { Get-VMNetworkAdapter -VM $PSItem | Select-Object -Property VMName, Name, SwitchName, MacAddress, @{label = "VLANOperationMode"; expression = { $PSItem.VlanSetting.OperationMode } }, @{label = "AccessVlanId"; expression = { $PSItem.VlanSetting.AccessVlanId } } } | Format-Table -AutoSize } catch { throw $PSItem.Exception.Message } } # static variabels $VMFolderName = $PsBoundParameters[$ParameterName] $ErrorActionPreference = 'Stop' $ConsoleHighlightColor = "Green" # vmswith and vlans $VMSwitch = "SCHULUNG" switch ($CourseRoom) { "4OG(Oben)" { $VLANID = "191" } "EG2(GROSS)" { $VLANID = "192" } "EG3(KLEIN)" { $VLANID = "193" } "VSCHULUNG1" { $VLANID = "194" } "VSCHULUNG2" { $VLANID = "195" } "NTSInstall" { $VLANID = "Untagged" } Default { throw "$($CourseRoom) was not found" } } $VLANsThatShouldNotBeChanged = @( "10", # EXTERN "190", # SCHULUNG-DMZ "403" # NTSRouting ) # get Cluster info if ($HostType -eq "Cluster") { $ComputerNames = (Get-ClusterNode).Name | ForEach-Object { $PSItem + "." + $env:USERDNSDOMAIN } } elseif ($HostType -eq "Local") { $ComputerNames = $env:COMPUTERNAME } # collect VM info try { $VMs = Get-VM -ComputerName $ComputerNames | Where-Object -Property Path -like "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)\*" } catch { throw "could not fetch vms - $($PSItem.Exception.Message)" } } process { # get settings Write-Host "`ncurrent vm netadapter configuration" -ForegroundColor $ConsoleHighlightColor Show-CurrentVNICConfig -VMs $VMs # set settings if ($ShowOnly) { Write-Host "`nshowing change that would apply" -ForegroundColor $ConsoleHighlightColor } else { Confirm-Question -Question "do you want to proceed?" } foreach ($HostName in $ComputerNames) { $CurrentHostVMs = $VMs | Where-Object -Property Computername -eq $HostName.Split(".")[0] $CurrentHostVMs | ForEach-Object { $VMNetadapters = Get-VMNetworkAdapter -VM $PSItem if ($null -eq $VMNetadapters) { throw "did not find any vm netadapters for $($PSitem.Name)" } else { foreach ($Adapter in $VMNetadapters) { if ($VLANsThatShouldNotBeChanged -contains $Adapter.VlanSetting.AccessVlanId) { Write-Verbose "$($Adapter.VMName): skipping adapter $($Adapter.Name) because its currently connected to VLAN $($Adapter.VlanSetting.AccessVlanId)" } elseif ($ShowOnly) { Write-Output "$($Adapter.VMName): adapter $($Adapter.Name) will be connected to $($VMSwitch) using VLAN $($VLANID)" } else { try { Connect-VMNetworkAdapter -VMNetworkAdapter $Adapter -SwitchName $VMSwitch Write-Verbose "$($Adapter.VMName): adapter $($Adapter.Name) now connected to $($VMSwitch)" } catch { throw "could not connect vnic to vswitch - $($PSItem.Exception.Message)" } try { if ($VLANID -eq "Untagged") { Set-VMNetworkAdapterVlan -VMNetworkAdapter $Adapter -Untagged } else { Set-VMNetworkAdapterVlan -VMNetworkAdapter $Adapter -Access -VlanId $VLANID } Write-Verbose "$($Adapter.VMName): adapter $($Adapter.Name) is using vlan $($VLANID) now" } catch { throw "could not set vlan to vnic - $($PSItem.Exception.Message)" } } } } } } # get settings if (!($ShowOnly)) { Write-Host "`nconfiguration after change" -ForegroundColor $ConsoleHighlightColor Show-CurrentVNICConfig -VMs $VMs } } } function Restore-CourseVMToLastSnapshot { <# .SYNOPSIS restores latest snapshots of course vms .DESCRIPTION restores latest snapshots of course vms .PARAMETER VMFolderName Name of the folder under $($env:SystemDrive)\ClusterStorage\VMs where the vms are located .PARAMETER ShowOnly only display changes, but dont make the changes .EXAMPLE Restore-CourseVMToLastSnapshot -VMFolderName PKI -ShowOnly shows only the possible snapshot which should be applied to vms in folder pki .EXAMPLE Restore-CourseVMToLastSnapshot -VMFolderName PKI applies the latest snapshot to vms of folder pki .NOTES - the vm files must be in the course folder under the $($env:SystemDrive)\ClusterStorage\VMs #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, ParameterSetName = "Default", Position = 2)] [switch] $ShowOnly ) dynamicParam { # Set the dynamic parameters' name $ParameterName = 'VMFolderName' # Create and set the parameters' attributes $ParameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute $ParameterAttribute.Mandatory = $true $ParameterAttribute.Position = 1 $ParameterAttribute.ParameterSetName = "Default" # Create the collection of attributes $AttributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) # Generate and set the ValidateSet $arrSet = (Get-ChildItem -Path "$($env:SystemDrive)\ClusterStorage\VMs" | Where-Object -Property Name -NotLike "_*").Name $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) # Add the ValidateSet to the attributes collection $AttributeCollection.Add($ValidateSetAttribute) # Create and return the dynamic parameter $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) # Create the dictionary $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) return $RuntimeParameterDictionary } begin { # static variabels $VMFolderName = $PsBoundParameters[$ParameterName] $ErrorActionPreference = 'Stop' $ConsoleHighlightColor = "Green" # collect VM info try { $VMs = Get-VM | Where-Object -Property Path -like "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)\*" } catch { throw "could not fetch vms - $($PSItem.Exception.Message)" } } process { # get settings try { $AllVMSnapshots = Get-VMSnapshot -VM $VMs $SelectedSnapshots = foreach ($VM in $VMs) { $CurrentVMSnapshots = $AllVMSnapshots | Where-Object -Property VMName -eq $VM.Name $SelectedCurrentVMSnapshots = $CurrentVMSnapshots | Sort-Object -Property CreationTime -Descending if ($null -ne $SelectedCurrentVMSnapshots) { $SelectedCurrentVMSnapshots[0] } else { Write-Verbose "no snapshot found for $($VM.Name)" } } } catch { throw "could not get vm snapshots - $($PSItem.Exception.Message)" } # verify snapshot counts foreach ($VM in $VMs) { $CurrentVMSnapshots = $AllVMSnapshots | Where-Object -Property VMName -eq $VM.Name if ($CurrentVMSnapshots.count -ge 2) { Write-Verbose "found more than 1 snapshot for $($VM.Name)" } if ($CurrentVMSnapshots.count - 0) { Write-Verbose "found no snapshot for $($VM.Name)" } } Write-Host "`nfound the following snapshots" -ForegroundColor $ConsoleHighlightColor $AllVMSnapshots | Sort-Object -Property VMName, CreationTime | Format-Table -AutoSize Write-Host "`nthe following snapshots will be used to restore the vm state, if no snapshot was found then the vm will keep its state" -ForegroundColor $ConsoleHighlightColor $SelectedSnapshots | Sort-Object -Property VMName, CreationTime | Format-Table -AutoSize if (-not $ShowOnly) { Confirm-Question -Question "do you want to proceed?" $SelectedSnapshots | ForEach-Object { try { Write-Verbose "apply snapshot $($PSItem.Name) to vm $($PSItem.VMName)" Restore-VMCheckpoint -VMSnapshot $PSItem -Confirm:$False } catch { throw "could not apply snapshot $($PSItem.Name) to vm $($PSItem.VMName) - $($PSItem.Exception.Message)" } } } } } function Reset-VMDeployment { <# .Description this function can be used to reset course vm deployment .Parameter Type type of reset .Parameter SnapShotNamePattern pattern for the snapshot name .Parameter VMFilesPath path to vm files .Parameter Force switch for not asking to do stuff .Example # removes all vms and the files Reset-VMDeployment -Type Remove -Force .Example # sets vms to snapshot with name like "initial" Reset-VMDeployment -Type ToSnapshot -SnapShotNamePattern "initial" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet("Remove", "ToSnapshot")] [string] $Type, [Parameter(Mandatory = $true, ParameterSetName = "ToSnapshot")] [string] $SnapShotNamePattern, [Parameter(Mandatory = $false, ParameterSetName = "Remove")] [string] $VMFilesPath = "V:\VMs", [Parameter(Mandatory = $false)] [switch] $Force ) try { if ($Force -eq $false) { if ($Type -eq "Remove") { Confirm-Question -Question "are you sure to remove all vms on this host?" } elseif ($Type -eq "ToSnapshot") { Confirm-Question -Question "are you sure to reset all vms on this host to the specified snapshot?" } } $VMsToRemove = (Get-VM).Name | Where-Object -FilterScript { (Get-ChildItem -Path $VMFilesPath -ErrorAction SilentlyContinue).Name -contains $PSItem } $VMsToRemove | ForEach-Object { Write-Verbose "found vm $($PSItem.Name)" } if ($Type -eq "Remove") { try { Write-Host "Turn of VMs" $VMsToRemove | Stop-VM -TurnOff -Force -WarningAction SilentlyContinue } catch { throw "could not turn off - $($PSItem.Exception.Message)" } try { Write-Host "Removing VMs" $VMsToRemove | Remove-VM -Force } catch { throw "could not remove - $($PSItem.Exception.Message)" } try { Write-Host "Removing VM Files" Remove-Item -Path $VMFilesPath -Force -Recurse } catch { throw "could not remove files - $($PSItem.Exception.Message)" } } elseif ($Type -eq "ToSnapshot") { $VMsToRemove | ForEach-Object { try { $Snapshot = Get-VMSnapshot -VM $PSItem -Name "*$($SnapShotNamePattern)*" -ErrorAction SilentlyContinue if ($Snapshot.count -gt 1) { throw "found more than 1 snapshot for $($PSItem.Name)" } elseif ($Snapshot.count -eq 0) { Write-Warning "found no snapshot for $($PSItem.Name)" } else { Write-Host "apply snapshot '$($Snapshot.Name)' to vm $($PSItem.Name)" Restore-VMCheckpoint -VMSnapshot $Snapshot -Confirm:$False } } catch { throw "could not apply snapshot - $($PSItem.Exception.Message)" } } } } catch { throw "could not reset vm deployment - $($PSItem.Exception.Message)" } } function Import-CourseVM { <# .SYNOPSIS imports vms from specified folder .DESCRIPTION imports vms from specified folder .PARAMETER VMFolderName Name of the folder under $($env:SystemDrive)\ClusterStorage\VMs where the vms are located .PARAMETER ShowOnly only display changes, but dont make the changes .EXAMPLE Import-CourseVM -VMFolderName PKI -ShowOnly shows only the possible vms to import from folder pki .EXAMPLE Import-CourseVM -VMFolderName PKI import vms of folder pki .NOTES - the vm files must be in the course folder under the $($env:SystemDrive)\ClusterStorage\VMs #> [CmdletBinding()] param ( [Parameter(Mandatory = $false, ParameterSetName = "Default", Position = 2)] [switch] $ShowOnly, [Parameter(Mandatory = $false, ParameterSetName = "Default", Position = 3)] [switch] $AddToClusterAsResource ) dynamicParam { # Set the dynamic parameters' name $ParameterName = 'VMFolderName' # Create and set the parameters' attributes $ParameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute $ParameterAttribute.Mandatory = $true $ParameterAttribute.Position = 1 $ParameterAttribute.ParameterSetName = "Default" # Create the collection of attributes $AttributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) # Generate and set the ValidateSet $arrSet = (Get-ChildItem -Path "$($env:SystemDrive)\ClusterStorage\VMs" | Where-Object -Property Name -NotLike "_*").Name $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) # Add the ValidateSet to the attributes collection $AttributeCollection.Add($ValidateSetAttribute) # Create and return the dynamic parameter $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) # Create the dictionary $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) return $RuntimeParameterDictionary } begin { # static variabels $VMFolderName = $PsBoundParameters[$ParameterName] $ErrorActionPreference = 'Stop' $ConsoleHighlightColor = "Green" $VMFolderPath = "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)" } process { try { $VMConfigs = Get-ChildItem -Path $VMFolderPath -Recurse -Filter "Virtual Machines" $VMNames = $VMConfigs.FullName.Replace("Virtual Machines", "").Replace($VMFolderPath, "").Replace("\", "") | Sort-Object $MappingObject = $VMNames | ForEach-Object { return [PSCustomObject]@{ Name = $PSItem VMConfigPath = $VMConfigs.FullName -match $PSItem } } } catch { throw "could not fetch vm config files - $($PSItem.Exception.Message)" } Write-Host "found the following vm config files" -ForegroundColor $ConsoleHighlightColor $MappingObject | Format-Table -AutoSize if (-not $ShowOnly) { Confirm-Question -Question "do you want to start the import for those vms?" foreach ($Object in $MappingObject) { try { $VMConfilePath = Get-ChildItem -Path $Object.VMConfigPath -Recurse -Filter "*.vmcx" Write-Host "importing vm $($Object.Name)" Write-Verbose "importing vm $($Object.Name), config file at $($VMConfilePath.FullName)" Import-VM -Path $VMConfilePath.FullName } catch { throw "could not import vm $($Object.Name), config file at $($VMConfilePath.FullName)" } if ($AddToClusterAsResource) { try { Write-Host "adding vm $($Object.Name) to cluster as role" Add-ClusterVirtualMachineRole -VMName $Object.Name } catch { throw "could not add vm $($Object.Name) to cluster - $($VMConfilePath.FullName)" } } } } } } function New-CourseVM { <# .SYNOPSIS creates a new vm .DESCRIPTION creates a new vm in its course folder .PARAMETER VMFolderName Name of the folder under $($env:SystemDrive)\ClusterStorage\VMs where the vms are located .PARAMETER HostType localhost or cluster, which are all nodes in the cluster .PARAMETER VMName Name of vm .PARAMETER CPUCount count of vcores .PARAMETER RAM amount of ram as 4gb for example .PARAMETER OSDiskSize disk size for os as 120gb for example .PARAMETER VMSwitchName name of virtual switch .PARAMETER AutoStartEnabled should the autostart functionality be enabled .PARAMETER vTPMEnabled should a vtpm be added to the vm .EXAMPLE New-CourseVM -VMFolderName IC -VMName "IC-VWIN11-01" -HostType Cluster Creates a new VM in Folder IC and add it to the cluster as resource .NOTES - the folder must be in the course folder under the $($env:SystemDrive)\ClusterStorage\VMs #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet("Local", "Cluster")] [string] $HostType, [Parameter(Mandatory = $true)] [string] $VMName, [Parameter(Mandatory = $false)] [int] $CPUCount = 4, [Parameter(Mandatory = $false)] [int64] $RAM = 4gb, [Parameter(Mandatory = $false)] [int64] $OSDiskSize = 120GB, [Parameter(Mandatory = $false)] [string] $VMSwitchName = "SCHULUNG", [Parameter(Mandatory = $false)] [bool] $AutoStartEnabled = $false, [Parameter(Mandatory = $false)] [bool] $vTPMEnabled = $false ) dynamicParam { # Set the dynamic parameters' name $ParameterName = 'VMFolderName' # Create and set the parameters' attributes $ParameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute $ParameterAttribute.Mandatory = $true $ParameterAttribute.Position = 0 $ParameterAttribute.ParameterSetName = "Default" # Create the collection of attributes $AttributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) # Generate and set the ValidateSet $arrSet = (Get-ChildItem -Path "$($env:SystemDrive)\ClusterStorage\VMs" | Where-Object -Property Name -NotLike "_*").Name $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) # Add the ValidateSet to the attributes collection $AttributeCollection.Add($ValidateSetAttribute) # Create and return the dynamic parameter $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) # Create the dictionary $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) return $RuntimeParameterDictionary } begin { # static variabels $VMFolderName = $PsBoundParameters[$ParameterName] $ErrorActionPreference = 'Stop' # get Cluster info if ($HostType -eq "Cluster") { $ComputerNames = (Get-ClusterNode).Name | ForEach-Object { $PSItem + "." + $env:USERDNSDOMAIN } } elseif ($HostType -eq "Local") { $ComputerNames = $env:COMPUTERNAME } # collect VM info try { $VMs = Get-VM -ComputerName $ComputerNames | Where-Object -Property Path -like "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)\*" } catch { throw "could not fetch vms - $($PSItem.Exception.Message)" } $ComputerNames | ForEach-Object { Write-Verbose "using physical server $($PSItem)" } $VMPath = "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)\" } process { if ($VMs.name -contains $VMName) { throw "vm $($VMName) already exists" } else { $VMVHDXPath = ($VMPath + "\" + $VMName + "\Virtual Hard Disks\" + $VMName + ".vhdx") Write-Output "creating vm $($VMName) on $($env:COMPUTERNAME)" try { if (Test-Path -Path $VMVHDXPath) { throw "vhdx for $($VMName) already exists, please remove it" } New-VHD -Path $VMVHDXPath -SizeBytes $OSDiskSize -Dynamic | Out-Null New-VM -Name $VMName -MemoryStartupBytes $RAM -Path $VMPath -Generation 2 -VHDPath $VMVHDXPath -BootDevice NetworkAdapter -SwitchName $VMSwitchName | Out-Null } catch { throw "error during creation of vhdx or vm $($VMName) - $($PSItem.Exception.Message)" } try { $CreatedVM = Get-VM -Name $VMName Set-VMProcessor -VMName $VMName -Count $CPUCount if ($CreatedVM.DynamicMemoryEnabled) { Set-VM -Name $VMName -StaticMemory } if ($vTPMEnabled) { Set-VMKeyProtector -VMName $VMName -NewLocalKeyProtector Enable-VMTPM -VMName $VMName } if ($AutoStartEnabled) { Set-VM -AutomaticStartAction Start -VMName $VMName -AutomaticStartDelay 10 } else { Set-VM -AutomaticStartAction Nothing -VMName $VMName } Set-VM -AutomaticStopAction ShutDown -VMName $VMName Get-VMIntegrationService -VMName $VMName | Where-Object -Property Enabled -EQ $false | Enable-VMIntegrationService } catch { throw "error while setting properties $($VMName) - $($PSItem.Exception.Message)" } if ($HostType -eq "Cluster") { Write-Output "adding vm $($VMName) to cluster as role" Add-ClusterVirtualMachineRole -VMName $VMName | Out-Null } } } } function Update-CourseVM { <# .SYNOPSIS starts windows update process .DESCRIPTION starts windows update process .PARAMETER VMName Name of vm .PARAMETER VMCredential credentials for the vm .PARAMETER AutoReboot should the vm reboot after applying the updates .EXAMPLE Update-CourseVM -VMName "IC-VWIN11-01" -VMCredential $VMCredential -AutoReboot $true Updates the OS from the VM "IC-VWIN11-01" and restarts it afterwards .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $VMName, [Parameter(Mandatory = $true)] [pscredential] $VMCredential, [Parameter(Mandatory = $false)] [bool] $AutoReboot = $false ) begin { # load functions from host into vars $funcDef_InitializePowerShellEnviroment = ${function:Initialize-PowerShellEnviroment}.ToString() $funcDef_UpdateWindowsSystem = ${function:Update-WindowsSystem}.ToString() # static variabels $ErrorActionPreference = 'Stop' # collect VM info try { Write-Verbose "checking if vm $($VMName) exists" $VM = Get-VM -Name $VMName -ErrorAction SilentlyContinue if ($null -eq $VM) { throw "could not find vm" } $VMOS = Get-VMOperatingSystem -VMName $VMName if ($null -eq $VMOS -or $VMOS -eq "") { Write-Warning "could not determine vm os, skipping this vm $($VMName)" } elseif ($VMOS -notlike "*Windows*") { Write-Warning "vm $($VMName) has not windows, skipping" } elseif ($VMOS -like "*Windows*") { Write-Verbose "vm $($VMName) has windows" } } catch { throw "could not fetch vm info - $($PSItem.Exception.Message)" } } process { try { Write-Verbose "connecting to vm $($VMName) via ps direct" Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock { Write-Verbose "connected to vm $($using:VMName)" # load funtions from vars into vm session Write-Verbose "start loading functions" ${function:Initialize-PowerShellEnviroment} = $using:funcDef_InitializePowerShellEnviroment ${function:Update-WindowsSystem} = $using:funcDef_UpdateWindowsSystem # start update process try { Initialize-PowerShellEnviroment Update-WindowsSystem -AutoReboot $using:AutoReboot } catch { throw "error updating vm - $($PSItem.Exception.Message)" } } } catch { throw "could not update vm $($VMName)" } } } function Update-CourseEnvironment { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [pscredential] $VMCredential, [Parameter(Mandatory = $false)] [bool] $AutoReboot = $false, [Parameter(Mandatory = $false)] [ValidateScript({ $PSItem -gt 0 -and $PSItem -le 10 })] [int] $ThrottleLimit = 3, [Parameter(Mandatory = $false)] [switch] $ShowOnly ) dynamicParam { # Set the dynamic parameters' name $ParameterName = 'VMFolderName' # Create and set the parameters' attributes $ParameterAttribute = New-Object -Type System.Management.Automation.ParameterAttribute $ParameterAttribute.Mandatory = $true $ParameterAttribute.Position = 0 $ParameterAttribute.ParameterSetName = "Default" # Create the collection of attributes $AttributeCollection = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] # Add the attributes to the attributes collection $AttributeCollection.Add($ParameterAttribute) # Generate and set the ValidateSet $arrSet = (Get-ChildItem -Path "$($env:SystemDrive)\ClusterStorage\VMs" | Where-Object -Property Name -NotLike "_*").Name $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($arrSet) # Add the ValidateSet to the attributes collection $AttributeCollection.Add($ValidateSetAttribute) # Create and return the dynamic parameter $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection) # Create the dictionary $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter) return $RuntimeParameterDictionary } begin { Write-Warning "this function is still work in progress, use with care" # static variabels $VMFolderName = $PsBoundParameters[$ParameterName] $funcDef_InitializePowerShellEnviroment = ${function:Initialize-PowerShellEnviroment}.ToString() $funcDef_UpdateWindowsSystem = ${function:Update-WindowsSystem}.ToString() $funcDef_UpdateCourseVM = ${function:Update-CourseVM}.ToString() $ErrorActionPreference = 'Stop' $ConsoleHighlightColor = "Green" if ($Host.Version.Major -lt 7) { throw "this function is only supported in powershell 7, use Update-CourseVM if you want to update individual vms." } # collect VM info try { $VMs = Get-VM | Where-Object -Property Path -like "$($env:SystemDrive)\ClusterStorage\VMs\$($VMFolderName)\*" if ($null -eq $VMs) { throw "no vms were found" } $SelectedVMs = $VMs | ForEach-Object { $VMOS = Get-VMOperatingSystem -VMName $PSItem.Name if ($null -eq $VMOS -or $VMOS -eq "") { Write-Warning "could not determine vm os, skipping this vm $($PSItem.Name)" } elseif ($VMOS -notlike "*Windows*") { Write-Warning "vm $($PSItem.Name) has not windows, skipping" } elseif ($VMOS -like "*Windows*") { Write-Verbose "vm $($PSItem.Name) has windows" return $PSItem } } } catch { throw "could not fetch vms - $($PSItem.Exception.Message)" } } process { if ($ShowOnly) { Write-Host "this would trigger updates on those vms" -ForegroundColor $ConsoleHighlightColor $VMs.Name } else { if ($SelectedVMs.Count -eq 1) { Update-CourseVM -VMName $SelectedVMs.Name -VMCredential $VMCredential -AutoReboot $AutoReboot } elseif ($null -ne $SelectedVMs) { # https://learn.microsoft.com/en-us/powershell/scripting/learn/deep-dives/write-progress-across-multiple-threads?view=powershell-7.3#full-example # Create a hashtable for process. # Keys should be ID's of the processes $origin = @{} $SelectedVMs | Foreach-Object { $origin.($PSItem.Id) = @{} } # Create synced hashtable $sync = [System.Collections.Hashtable]::Synchronized($origin) Write-Progress -Id 0 -Activity "updating vms" -Status "starting" Start-Sleep -Seconds 1 $Jobs = $SelectedVMs | Foreach-Object -ThrottleLimit 5 -AsJob -Parallel { $syncCopy = $using:sync $process = $syncCopy.$($PSItem.Id) $process.ParentId = 0 $process.Id = $($using:SelectedVMs).IndexOf($PSItem) + 1 $process.Activity = "VMName $($PSItem.Name)" $process.Status = "start update process" # Fake workload start up that takes x amount of time to complete Start-Sleep -Milliseconds ($PSItem.wait * 5) #region do stuff $process.Status = "start update process, connecting" Start-Sleep -Seconds 1 # Define the function inside this thread from parent thread ${function:Initialize-PowerShellEnviroment} = $using:funcDef_InitializePowerShellEnviroment ${function:Update-WindowsSystem} = $using:funcDef_UpdateWindowsSystem ${function:Update-CourseVM} = $using:funcDef_UpdateCourseVM try { Update-CourseVM -VMName $PSItem.Name -VMCredential $using:VMCredential -AutoReboot $using:AutoReboot -Verbose } catch { $PSItem.Exception.Message } $process.Status = "finished update process, $(if ($using:AutoReboot -eq $true){ "rebooting vm" })" Start-Sleep -Seconds 3 # Start-Sleep -Seconds 10 #endregion # Mark process as completed $process.Completed = $true } while ($Jobs.State -eq 'Running') { # get percentage of completed $PercentageCompleted = [Math]::Round([Math]::Ceiling(($sync.Values | Where-Object -Property Completed -eq $true).Count / $VMs.count * 100), 2) $sync.Keys | Foreach-Object { # If key is not defined, ignore if (![string]::IsNullOrEmpty($sync.$PSItem.keys)) { # Create parameter hashtable to splat $param = $sync.$PSItem # Execute Write-Progress Write-Progress -Id 0 -Activity "updating vms" -Status "running" -PercentComplete $PercentageCompleted Write-Progress @param } } # Wait to refresh to not overload gui Start-Sleep -Seconds 0.3 } Write-Progress -Id 0 -Activity "updating vms" -Status "Finished" -Completed } } } } function Get-VMOperatingSystem { # https://stackoverflow.com/questions/38096777/is-there-a-way-to-get-vms-operating-system-name-from-hyper-v-using-powershell [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $VMName ) try { # Check if VM exists and is running. This script doesn't work if the VM is stopped. # Capture error output, source: https://stackoverflow.com/a/66861283/3498768 $vm_not_found = $($vm_state = (Get-VM $VMName).state) 2>&1 if ($null -ne $vm_not_found) { throw "$VMName VM was not found." } if ($vm_state -eq "Off") { Write-Warning "Cannot retrieve information of $VMName. The VM is stopped. Only running VM information can be retrieved." } else { # Get the virtual machine object $vm = Get-CimInstance -namespace "root\virtualization\v2" -query "Select * From Msvm_ComputerSystem Where ElementName='$($VMName)'" # Get associated information $vm_info = Get-CimAssociatedInstance -InputObject $vm # Select only required information $OS = ($vm_info | Where-Object GuestOperatingSystem).GuestOperatingSystem if ($null -ne $OS) { return $OS } else { throw "os info not found" } } } catch { throw "could not find data - $($PSItem.Exception.Message)" } } |