NTS-ConfigMgrTools.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 .Example # Creates a volume with the letter W 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 ) $ErrorActionPreference = 'Stop' try { 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 } $StorageSubSystemFriendlyName = (Get-StorageSubSystem -FriendlyName "*Windows*").FriendlyName Write-Output "$($env:COMPUTERNAME): create storage pool $($StoragePoolName)" if ($null -eq $SelectedDisks) { throw "no disks were found that can be used for the storagepool" } New-StoragePool -StorageSubSystemFriendlyName $StorageSubSystemFriendlyName -FriendlyName $StoragePoolName -PhysicalDisks $SelectedDisks | Out-Null if ($null -eq (Get-VirtualDisk -FriendlyName $VirtualDiskName -ErrorAction SilentlyContinue)) { Write-Output "$($env:COMPUTERNAME): create vdisk $($VirtualDiskName)" 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 } Initialize-Disk -FriendlyName $VirtualDiskName -PartitionStyle GPT | Out-Null $VDiskNumber = (Get-Disk -FriendlyName $VirtualDiskName).Number Write-Output "$($env:COMPUTERNAME): create volume $($VMVolumeName)" New-Volume -DiskNumber $VDiskNumber -FriendlyName $VMVolumeName -FileSystem ReFS -DriveLetter $VMDriveLetter | Out-Null } else { Write-Output "$($env:COMPUTERNAME): virtual disk $($VirtualDiskName) already exists - skipping" } } else { Write-Output "$($env:COMPUTERNAME): pool $($StoragePoolName) already exists - skipping" } } catch { throw "$($env:COMPUTERNAME): error during creation of vm volume: $($PSItem.Exception.Message)" } } function New-VMVSwitch { <# .Description Creates a VM switch based on a network card with the Up state. 10Gbit NICs are preferred .Parameter Course_Shortcut Name of the VM Switch .Example # Creates a new VM Switch with the name IC New-VMVSwitch -Course_Shortcut 'IC' .NOTES there must be at least one nic with status 'up' #> param ( # Course Shortcut [Parameter(Mandatory = $false)] [string] $Course_Shortcut = "LAN" ) try { if ($null -eq (Get-VMSwitch -Name $Course_Shortcut -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-Output "$($env:COMPUTERNAME): create vswitch $($Course_Shortcut)" New-VMSwitch -Name $Course_Shortcut -NetAdapterName $Selected_NIC.Name -AllowManagementOS $false | Out-Null Add-VMNetworkAdapter -ManagementOS -SwitchName $Course_Shortcut -Name "vNIC-$($Course_Shortcut)" Rename-NetAdapter -Name $Selected_NIC.Name -NewName "pNIC-$($Course_Shortcut)" } else { Write-Output "$($env:COMPUTERNAME): virtual vswitch $($Course_Shortcut) already exists - skipping" } } catch { throw "$($env:COMPUTERNAME): error during creation of virtual switch: $($PSItem.Exception.Message)" } } function Register-VM_in_CM { <# .Description Registers the VM Objects in ConfigMgr with its MAC address for Required Deployments .Parameter VM_Config_Obj Object that contains multiple descriptive objects for deployment from a VM. .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 # Registers the VMs from $VM_Config with $CM_Siteserver_FQDN Register-VM_in_CM -VM_Config_Obj $VM_Config -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials .NOTES the VM_Config_Obj object, should be like ` `$VM_Config_Obj = @{} `$VM_01 = @{ Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1' RAM = `$RAM CPU = `$CPUCount CM_Collection_Name = `$CM_Collection_W11_Autopilot Credentials = `$VM_Credentials DiskSize = `$DynamicDiskSize MAC = "" } #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [hashtable] $VM_Config_Obj, [Parameter(Mandatory = $true)] [string] $CM_Siteserver_FQDN, [Parameter(Mandatory = $true)] [PSCredential] $CM_Credentials ) Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj try { Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock { 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) { Write-Output "$($env:COMPUTERNAME): doing cm collection update on $($Collection.Name)" Invoke-CMCollectionUpdate -CollectionId $Collection.CollectionID Start-Sleep -Seconds 5 } } function Confirm-CMCollectionMembership { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $DeviceName, [Parameter(Mandatory = $true)] [string] $CollectionName ) $Device_Exists_In_Collection = $false $Counter = 0 Write-Output "$($DeviceName) - $("{0:d2}" -f $Counter): checking collection memberships in $($CollectionName)" while ($Device_Exists_In_Collection -eq $false -and $Counter -lt 40) { $CurrentCollectionMembers = Get-CMCollectionMember -CollectionName $CollectionName -Name $DeviceName if ($null -ne $CurrentCollectionMembers) { $Device_Exists_In_Collection = $true } else { Write-Output "$($DeviceName) - $("{0:d2}" -f $Counter): device not found in collection $($CollectionName)" Start-Sleep -Seconds 10 $Counter++ } if ($Counter -eq 12 -or $Counter -eq 24 -or $Counter -eq 36) { Start-CMCollectionUpdate -CollectionName $CollectionName } } if ($Counter -ge 40) { throw "$($DeviceName) - $("{0:d2}" -f $Counter): could not find in the collection $($CollectionName)" } else { Write-Output "$($DeviceName) - $("{0:d2}" -f $Counter): $($CollectionName) - found, continuing" } } $VM_Config_Obj = $using:VM_Config_Obj $PSDriveName = "CM-Temp-Drive" $CM_Collection_All_Systems_ID = "SMS00001" # load configmgr ps module Import-Module -Name ConfigurationManager New-PSDrive -Name $PSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null Set-Location -Path "$($PSDriveName):\" $CM_Collection_All_Systems_Name = (Get-CMCollection -Id $CM_Collection_All_Systems_ID).Name $CM_SiteCode = (Get-CMSite).SiteCode $CM_Devices_Names = $VM_Config_Obj.Keys | Sort-Object # check destination collection existance $CM_Devices_Names | ForEach-Object { if ($null -eq (Get-CMCollection -Name $VM_Config_Obj.$PSItem.CM_Collection_Name)) { throw "collection $($VM_Config_Obj.$PSItem.CM_Collection_Name) does not existing" } } # checking existing devices $CM_Devices_Names | ForEach-Object { $Temp_CMDevice = Get-CMDevice -Name $PSItem if (($null -ne $Temp_CMDevice) -and ($Temp_CMDevice.MACAddress.ToString().Replace(":", "") -ne $VM_Config_Obj.$PSItem.MAC)) { Write-Output "$($PSItem): removing existing computer info in configmgr - macaddress $($VM_Config_Obj.$PSItem.MAC), because the mac address is not correct" Remove-CMDevice -Name $PSItem -Force -Confirm:$false } } Start-Sleep -Seconds 5 # import device $CM_Devices_Names | ForEach-Object { $Temp_CMDevice = Get-CMDevice -Name $PSItem if ($null -eq $Temp_CMDevice) { Write-Output "$($PSItem): creating computer info in configmgr - macaddress $($VM_Config_Obj.$PSItem.MAC)" Import-CMComputerInformation -CollectionName $CM_Collection_All_Systems_Name -ComputerName $PSItem -MacAddress $VM_Config_Obj.$PSItem.MAC } } # checking all system refreshinterval Start-CMCollectionUpdate -CollectionName $CM_Collection_All_Systems_Name Start-Sleep -Seconds 10 # check collection membership all systems $CM_Devices_Names | ForEach-Object { Confirm-CMCollectionMembership -DeviceName $PSItem -CollectionName $CM_Collection_All_Systems_Name } # create membership rule $CM_Devices_Names | ForEach-Object { Add-CMDeviceCollectionDirectMembershipRule -CollectionName $VM_Config_Obj.$PSItem.CM_Collection_Name -ResourceID (Get-CMDevice -Name $PSItem).ResourceID } Start-Sleep -Seconds 5 # check collection membership target $CM_Devices_Names | ForEach-Object { Confirm-CMCollectionMembership -DeviceName $PSItem -CollectionName $VM_Config_Obj.$PSItem.CM_Collection_Name } # remove collections, so there is only the targeted $CM_Devices_Names | ForEach-Object { $Temp_CMDevice = Get-CMDevice -Name $PSItem $TargetCollection = Get-CMCollection -Name $($VM_Config_Obj.$PSItem.CM_Collection_Name) 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 -ne $CM_Collection_All_Systems_ID | ` Where-Object -Property CollectionID -ne $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) { Write-Output "$($Temp_CMDevice.Name): removing collection membership in $((Get-CMCollection -Id $PSItem.CollectionID).Name)" Remove-CMDeviceCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID -ResourceId $MembershipRule.ResourceId -Confirm:$false -Force } } } } } Set-Location -Path $env:SystemDrive Remove-PSDrive -Name $PSDriveName } $SecondsToWait = 120 Write-Output "$($env:COMPUTERNAME): finished registration, now waiting $($SecondsToWait) seconds for the configmgr database updates and stabilization" Start-Sleep -Seconds $SecondsToWait } catch { throw "error during registration of device infos in configmgr: $($PSItem.Exception.Message)" } } function Start-VM_Deployment { <# .Description Starts VMs .Parameter VM_Config_Obj Object that contains multiple descriptive objects for deployment from a VM. .Example # Starts VMs, based on objects in $VM_Config Start-VM_Deployment -VM_Config_Obj $VM_Config .NOTES the VM_Config_Obj object, should be like ` `$VM_Config_Obj = @{} `$VM_01 = @{ Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1' RAM = `$RAM CPU = `$CPUCount CM_Collection_Name = `$CM_Collection_W11_Autopilot Credentials = `$VM_Credentials DiskSize = `$DynamicDiskSize MAC = "" } #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [hashtable] $VM_Config_Obj ) if ($null -eq $VM_Config_Obj) { throw "no VM_Config_Obj was supplied, please specify" } try { foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) { Write-Output "$($VM): starting deployment" Start-Sleep -Seconds 2 Start-VM -VMName $VM } } catch { throw "error while starting vms: $($PSItem.Exception.Message)" } } function Confirm-VM_Deployment { <# .Description Checks the ConfigMgr database to see if the deployment of VM objects is complete. .Parameter VM_Config_Obj Object that contains multiple descriptive objects for deployment from a VM. .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 # Checks the ConfigMgr database to see if the deployment of VM objects in $VM_Config is complete. Confirm-VM_Deployment -VM_Config_Obj $VM_Config -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials .NOTES the VM_Config_Obj object, should be like ` `$VM_Config_Obj = @{} `$VM_01 = @{ Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1' RAM = `$RAM CPU = `$CPUCount CM_Collection_Name = `$CM_Collection_W11_Autopilot Credentials = `$VM_Credentials DiskSize = `$DynamicDiskSize MAC = "" } #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [hashtable] $VM_Config_Obj, [Parameter(Mandatory = $true)] [string] $CM_Siteserver_FQDN, [Parameter(Mandatory = $true)] [PSCredential] $CM_Credentials ) if ($null -eq $VM_Config_Obj) { throw "no VM_Config_Obj was supplied, please specify" } if ($null -eq $CM_Siteserver_FQDN) { throw "no CM_Siteserver_FQDN was supplied, please specify" } if ($null -eq $CM_Credentials) { throw "no CM_Credentials was supplied, please specify" } try { Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock { try { $PSDriveName = "CM-PB2" Import-Module -Name ConfigurationManager New-PSDrive -Name $PSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null Set-Location -Path "$($PSDriveName):\" $CM_SiteCode = (Get-CMSite).SiteCode $Deployment_Finished = $false $Deployment_Check_Count = 0 $CM_Deployment_Running = @{} foreach ($VM in $using:VM_Config_Obj.keys | Sort-Object) { $CM_Deployment_Running.Add($VM, $true) } do { $Deployment_Check_Count++ $using:VM_Config_Obj.keys | ForEach-Object { $StatusMessages = $null $TempDevice = Get-CMDevice -Name $PSItem -Fast $DeploymentID = (Get-CMDeployment | Where-Object -Property CollectionName -eq $($using:VM_Config_Obj).$PSItem.CM_Collection_Name).DeploymentID $Query = "Select AdvertisementID,ResourceID,Step,ActionName,LastStatusMessageIDName from v_TaskExecutionStatus where (AdvertisementID = '$($DeploymentID)' AND ResourceID = '$($TempDevice.ResourceID)')" try { $StatusMessages = Invoke-Sqlcmd -ServerInstance "$($using:CM_Siteserver_FQDN)\$($CM_SiteCode)" -Database "CM_$($CM_SiteCode)" -Query $Query | Sort-Object -Property Step -Descending } catch { throw "could not get data from db - $($PSItem.Exception.Message)" } if ($null -eq $StatusMessages -or $StatusMessages -eq "") { Write-Output "$($PSItem): waiting on data in configmgr database" } else { $StatusObject = @{ "DeviceName" = $TempDevice.Name "AdvertisementID" = $StatusMessages[0].AdvertisementID "ResourceID" = $StatusMessages[0].ResourceID "Step" = $StatusMessages[0].Step "ActionName" = $StatusMessages[0].ActionName "LastStatusMessageIDName" = $StatusMessages[0].LastStatusMessageIDName } if ($StatusObject.LastStatusMessageIDName -like "*The task sequence manager successfully completed execution of the task sequence*") { Write-Output "$($StatusObject.DeviceName): finished" $CM_Deployment_Running.$($PSItem) = $false } else { if ($StatusMessages[0].ActionName -ne ""){ Write-Output "$($StatusObject.DeviceName): - $($StatusObject.Step) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } elseif ($StatusMessages[1].ActionName -ne ""){ $StatusObject.ActionName = $StatusMessages[1].ActionName Write-Output "$($StatusObject.DeviceName): - $($StatusObject.Step) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } elseif ($StatusMessages[2].ActionName -ne ""){ $StatusObject.ActionName = $StatusMessages[2].ActionName Write-Output "$($StatusObject.DeviceName): - $($StatusObject.Step) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } elseif ($StatusMessages[3].ActionName -ne ""){ $StatusObject.ActionName = $StatusMessages[3].ActionName Write-Output "$($StatusObject.DeviceName): - $($StatusObject.Step) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } elseif ($StatusMessages[4].ActionName -ne ""){ $StatusObject.ActionName = $StatusMessages[4].ActionName Write-Output "$($StatusObject.DeviceName): - $($StatusObject.Step) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } elseif ($StatusMessages[5].ActionName -ne ""){ $StatusObject.ActionName = $StatusMessages[5].ActionName Write-Output "$($StatusObject.DeviceName): - $($StatusObject.Step) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } elseif ($StatusMessages[6].ActionName -ne ""){ $StatusObject.ActionName = $StatusMessages[6].ActionName Write-Output "$($StatusObject.DeviceName): - $($StatusObject.Step) - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } else { Write-Output "$($StatusObject.DeviceName) - no actionname - $($StatusObject.ActionName): $($StatusObject.LastStatusMessageIDName)" } } } } Write-Output "---" if ($CM_Deployment_Running.Values -notcontains $true) { $Deployment_Finished = $true } else { Start-Sleep -Seconds 15 } } while (($Deployment_Finished -eq $false) -and $Deployment_Check_Count -lt 180 ) if ($Deployment_Check_Count -ge 200) { throw "deployment not finished after 100 mins, check the logs in configmgr or inside the vms" } Set-Location -Path $env:SystemDrive Remove-PSDrive -Name $PSDriveName } catch { throw $PSItem.Exception.Message } } } catch { throw "error while checking deployment status: $($PSItem.Exception.Message)" } } function New-VMs_Objectbased { <# .Description Creates vms based on objects .Parameter VM_Config_Obj Object that contains multiple descriptive objects for deployment from a VM. .Parameter Course_Shortcut Name of the VM Switch .Parameter VMDriveLetter Letter for the volume where the vms should be stored .Example # Creates vms based on $VM_Config and connects them to the vm switch $Course_Shortcut New-VMs_Objectbased -VM_Config $VM_Config -Course_Shortcut $Course_Shortcut .NOTES edits $VM_Config by updating the macaddress attribute of each vm object the VM_Config_Obj object, should be like ` `$VM_Config_Obj = @{} `$VM_01 = @{ Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1' RAM = `$RAM CPU = `$CPUCount CM_Collection_Name = `$CM_Collection_W11_Autopilot Credentials = `$VM_Credentials DiskSize = `$DynamicDiskSize MAC = "" } #> param ( [Parameter(Mandatory = $false)] [hashtable] $VM_Config_Obj, [Parameter(Mandatory = $false)] [string] $Course_Shortcut = 'LAN', [Parameter(Mandatory = $false)] [char] $VMDriveLetter = 'V' ) if ($null -eq $VM_Config_Obj) { throw "no VM_Config_Obj was supplied, please specify" } try { $VM_Base_Path = $VMDriveLetter + ":\VMs" foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) { $VMVHDXPath = ($VM_Base_Path + "\" + $VM_Config_Obj.$VM.Name + "\" + $VM_Config_Obj.$VM.Name + ".vhdx") Write-Output "$($VM_Config_Obj.$VM.Name): creating vm" try { New-VHD -Path $VMVHDXPath -SizeBytes $VM_Config_Obj.$VM.DiskSize -Dynamic | Out-Null New-VM -Name $VM_Config_Obj.$VM.Name -MemoryStartupBytes $VM_Config_Obj.$VM.RAM -Path $VM_Base_Path -Generation 2 -VHDPath $VMVHDXPath -BootDevice NetworkAdapter -SwitchName $Course_Shortcut | Out-Null } catch { throw "$($VM_Config_Obj.$VM.Name): error during creation of vhdx or vm - $($PSItem.Exception.Message)" } try { Set-VMProcessor -VMName $VM_Config_Obj.$VM.Name -Count $VM_Config_Obj.$VM.CPU if ((Get-VM -Name $VM_Config_Obj.$VM.Name).DynamicMemoryEnabled) { Set-VM -Name $VM_Config_Obj.$VM.Name -StaticMemory } Set-VMKeyProtector -VMName $VM_Config_Obj.$VM.Name -NewLocalKeyProtector Enable-VMTPM -VMName $VM_Config_Obj.$VM.Name Set-VM -AutomaticStartAction Start -VMName $VM_Config_Obj.$VM.Name -AutomaticStartDelay 10 Set-VM -AutomaticStopAction ShutDown -VMName $VM_Config_Obj.$VM.Name Get-VMIntegrationService -VMName $VM_Config_Obj.$VM.Name | Where-Object -Property Enabled -EQ $false | Enable-VMIntegrationService } catch { throw "$($VM_Config_Obj.$VM.Name): error while setting properties - $($PSItem.Exception.Message)" } Start-VM -Name $VM_Config_Obj.$VM.Name Start-Sleep -Seconds 2 Stop-VM -Name $VM_Config_Obj.$VM.Name -Force -TurnOff Start-Sleep -Seconds 1 } foreach ($VM in $VM_Config_Obj.Keys | Sort-Object) { $VM_Config_Obj.$VM.MAC = (Get-VM -Name $VM_Config_Obj.$VM.Name | Get-VMNetworkAdapter).MacAddress Set-VMNetworkAdapter -VMName $VM_Config_Obj.$VM.Name -StaticMacAddress $VM_Config_Obj.$VM.MAC } Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj } catch { throw "error during creation of vms: $($PSItem.Exception.Message)" } } 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 vms are registered with the local hypervisor .Parameter VM_Config_Obj Object that contains multiple descriptive objects for deployment from a VM. .Example # checks if the vms in $VM_Config_Obj are registerd to the local hypervisor Confirm-VMPresence -VM_Config_Obj $VM_Config_Obj .NOTES the VM_Config_Obj object, should be like ` `$VM_Config_Obj = @{} `$VM_01 = @{ Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1' RAM = `$RAM CPU = `$CPUCount CM_Collection_Name = `$CM_Collection_W11_Autopilot Credentials = `$VM_Credentials DiskSize = `$DynamicDiskSize MAC = "" } #> param ( [Parameter(Mandatory = $false)] [hashtable] $VM_Config_Obj ) try { $VM_Config_Obj.Keys | ForEach-Object { if ($null -eq (Get-VM -Name $VM_Config_Obj.$PSItem.Name)) { throw "$($VM_Config_Obj.$PSItem.Name): could not be found" } } } catch { throw "$($PSItem.Exception.Message)" } } function Get-SQLISO { <# .Description downloads sql iso from microsoft .Parameter Version version of the iso .Parameter Outpath path where the iso is stored .Example # stores the iso to $Outpath Get-SQLISO -Outpath $Outpath .NOTES downloads the sql enterprise eval setup and with it the iso #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet("2019", "2022")] [string] $Version, [Parameter(Mandatory = $true)] [string] $Outpath ) switch ($Version) { "2019" { $DownloadURL = "https://download.microsoft.com/download/4/8/6/486005eb-7aa8-4128-aac0-6569782b37b0/SQL2019-SSEI-Eval.exe" } "2022" { $DownloadURL = "https://go.microsoft.com/fwlink/?linkid=2215202&clcid=0x409&culture=en-us&country=us" } Default { throw "no version was selected" } } if ((Test-Path -Path $Outpath) -eq $false) { New-Item -Path $Outpath -ItemType Directory -Force | Out-Null } $Outpath = (Get-Item -Path $Outpath).FullName $SQLEvalSetupPath = "$($Version)-SSEI-Eval.exe" $SQLEvalSetupFullPath = "$($Outpath)\$($SQLEvalSetupPath)" # downloading eval setup try { Start-FileDownload -DownloadURL $DownloadURL -FileOutPath $SQLEvalSetupFullPath } catch { throw "error downloading eval setup: $($PSItem.Exception.Message)" } # downloading iso try { $ISOPath = (Get-ChildItem -Path $Outpath | Where-Object -FilterScript { $PSItem.Name -like "SQL*.iso" }).FullName if ($null -ne $ISOPath -and (Test-Path -Path $ISOPath) -eq $true) { if ((Get-Item $ISOPath).LastWriteTime -gt (Get-Date).AddHours(-2)) { # juenger als zwei stunden # do nothing Write-Output "$($env:COMPUTERNAME): found sql iso at $($ISOPath), will use it" } else { Write-Output "$($env:COMPUTERNAME): found sql iso at $($ISOPath), removing the files because too old" Remove-Item -Path $ISOPath -Recurse -Force | Out-Null } } Write-Output "$($env:COMPUTERNAME): downloading sql iso with eval setup" $Arguments = "/ACTION=Download /MEDIAPATH=$($Outpath)\ /MEDIATYPE=ISO /LANGUAGE=en-US /QUIET" Start-Process $SQLEvalSetupFullPath -ArgumentList $Arguments -Wait -NoNewWindow Remove-Item -Path $SQLEvalSetupFullPath -Force } catch { throw "error downloading iso: $($PSItem.Exception.Message)" } Write-Output "$($env:COMPUTERNAME): finished download - check folder $($Outpath)" } function Initialize-SQLSetup { <# .Description prepares the sql installation files for i .Parameter Outpath path where the data should be cached .Example # stores the files to $TempFolderForSQL Initialize-SQLSetup -Outpath $TempFolderForSQL .NOTES downloads the sql install files and stores them extracted to the outpath #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet("2019", "2022")] [string] $Version = "2019", [Parameter(Mandatory = $true)] [string] $Outpath ) if ((Test-Path -Path $Outpath) -eq $false) { New-Item -Path $Outpath -ItemType Directory -Force | Out-Null } $Outpath = (Get-Item -Path $Outpath).FullName # checks $SQLFileFolder = "$($Outpath)\SQLFiles\" if ((Test-Path -Path $SQLFileFolder) -eq $true ) { throw "$($env:COMPUTERNAME): folder $($SQLFileFolder) already exits, please cleanup or skip" } # downloading iso try { Get-SQLISO -Version $Version -Outpath $Outpath } catch { throw "error during download - $($PSItem.Exception.Message)" } # copying files try { if ((Test-Path -Path $SQLFileFolder) -eq $true) { if ((Get-Item $SQLFileFolder).LastWriteTime -gt (Get-Date).AddHours(-2)) { # juenger als zwei stunden # do nothing Write-Output "$($env:COMPUTERNAME): found setup files at $($SQLFileFolder) will use it" } else { Write-Output "$($env:COMPUTERNAME): found setup files at $($SQLFileFolder), removing the files because too old" Remove-Item -Path $SQLFileFolder -Recurse -Force | Out-Null } } Write-Output "$($env:COMPUTERNAME): mounting iso" $ISO = Get-ChildItem -Path $Outpath | Where-Object -FilterScript { $PSItem.Name -like "SQL*.iso" } $MountedISOs = Mount-DiskImage -PassThru -ImagePath $ISO.FullName $Volume = Get-Volume -DiskImage $MountedISOs Write-Output "$($env:COMPUTERNAME): copy files from iso to $($SQLFileFolder)" Copy-Item -Path "$($Volume.DriveLetter):\" -Destination $SQLFileFolder -Recurse -Force Write-Output "$($env:COMPUTERNAME): finished copy job" Dismount-DiskImage $MountedISOs.ImagePath | Out-Null Write-Output "$($env:COMPUTERNAME): iso was dismounted" } catch { throw "error during mount or copy - $($PSItem.Exception.Message)" } } function Install-SQLADServiceAccount { <# .Description adds the local server to a group with permission to the serviec account and then installs it .Parameter SQLServiceAccount name of the group managed service account .Parameter GroupWithPermissions name of the group with permissions to retrieve the password of the group managed service account .Example # this installs a gMSA Install-SQLADServiceAccount -SQLServiceAccount $SQLEngine.Name -GroupWithPermissions $SQLEngine.GroupWithPermissions .NOTES - the server needs to be member of a domain - the group managed service account must exit - the user which runs this command must have permissions to add the local device to the group #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $SQLServiceAccount, [Parameter(Mandatory = $true)] [string] $GroupWithPermissions ) if ((Get-CimInstance -ClassName Win32_ComputerSystem).PartofDomain -eq $false) { throw "$($env:COMPUTERNAME): not a member of a domain" } Install-WindowsFeature -Name RSAT-AD-PowerShell | Out-Null if ($null -eq (Get-ADServiceAccount -Identity $SQLServiceAccount -ErrorAction SilentlyContinue)) { throw "service account $($SQLServiceAccount)$ could not be found" } # adding device to group try { Write-Output "$($env:COMPUTERNAME): adding to group $($GroupWithPermissions)" $Self = Get-ADComputer -Identity $env:COMPUTERNAME Add-ADGroupMember -Identity $GroupWithPermissions -Members $Self } catch { throw "not able to add to group $($GroupWithPermissions): $($PSItem.Exception.Message)" } # installing and testing service account try { Write-Output "$($env:COMPUTERNAME): installing $($SQLServiceAccount)" Start-Process -FilePath klist -ArgumentList "purge -lh 0 -li 0x3e7" -NoNewWindow -Wait Start-Sleep -Seconds 5 Start-Process -FilePath klist -ArgumentList "purge -lh 0 -li 0x3e7" -NoNewWindow -Wait Start-Sleep -Seconds 10 Install-ADServiceAccount -Identity ($SQLServiceAccount + "$") if ((Test-ADServiceAccount -Identity ($SQLServiceAccount + "$") -WarningAction SilentlyContinue) -eq $false) { throw "service account $($SQLServiceAccount)$ is not installed, please verify" } } catch { throw "could not install $($SQLServiceAccount) - $($PSItem.Exception.Message)" } } function Install-SQLInstance { <# .Description adds the local server to a group with permission to the serviec account and then installs it .Parameter Name name of group managed service account .Parameter UseLocalAccount specify if local accounts are used .Parameter UseGmSA specify if group managed service accounts are used .Parameter EngineAccountName service account name for sql engine .Parameter AgentAccountName service account name for sql agent .Parameter UseMixedAuth when used, the instance will use sql and windows authentication .Parameter SAPWD sa pwd .Parameter INSTALLSQLDATADIR specifies the data directory for SQL Server data files .Parameter INSTANCEDIR specifies nondefault installation directory for instance-specific components .Parameter SQLBACKUPDIR backup file path for the sql database .Parameter SQLUSERDBDIR user file path for the sql database .Parameter SQLUSERDBLOGDIR user log file path for the sql database .Parameter SQLTEMPDBDIR temp file path for the sql database .Parameter SQLTEMPDBLOGDIR temp log file path for the sql database .Parameter Features features that should be installed, like 'SQLENGINE,FULLTEXT,CONN' .Parameter SQLSYSADMINACCOUNTS accounts that should be admin on that instance .Parameter SQLMinRAM minimum ram for instance .Parameter SQLMaxRAM maximum ram for instance .Example # this installs an sql instance with the name PB1 which uses gmsa with customized file paths Install-SQLInstance -Name "PB1" ` -Features "SQLENGINE" ` -SQLSYSADMINACCOUNTS ('"' + "IC\T1-CMAdmin" + '" "' + "IC\T1-CMAdmins" + '"') ` -SQLBACKUPDIR "S:\SQL\PB1\BACKUP\DATA" ` -SQLUSERDBDIR "S:\SQL\PB1\USERDATA\DATA" ` -SQLUSERDBLOGDIR "S:\SQL\PB1\USERLOG\LOG" ` -SQLTEMPDBDIR "S:\SQL\PB1\TEMPDATA\DATA" ` -SQLTEMPDBLOGDIR "S:\SQL\PB1\TEMPLOG\LOG" ` -UseGmSA ` -EngineAccountName $SQLEngine ` -AgentAccountName $SQLAgent .Example # this installs an sql instance with the name INSTANCE and local accounts to default file paths Install-SQLInstance -Name "INSTANCE" -UseLocalAccount .NOTES #> [CmdletBinding(DefaultParameterSetName = 'local')] param ( [Parameter(ParameterSetName = 'local', Mandatory = $false)] [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)] [string] $SetupPath = ".\SQLFiles\setup.exe", [Parameter(ParameterSetName = 'local', Mandatory = $true)] [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)] [string] $Name, [Parameter(ParameterSetName = 'local')] [switch] $UseLocalAccount, [Parameter(ParameterSetName = 'gmsa')] [switch] $UseGmSA, [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)] [string] $EngineAccountName, [Parameter(ParameterSetName = 'gmsa', Mandatory = $true)] [string] $AgentAccountName, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [switch] $UseMixedAuth, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SAPWD, [Parameter(ParameterSetName = 'local', Mandatory = $false)] [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)] [string] $INSTALLSQLDATADIR, [Parameter(ParameterSetName = 'local', Mandatory = $false)] [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)] [string] $INSTANCEDIR, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SQLBACKUPDIR, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SQLUSERDBDIR, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SQLUSERDBLOGDIR, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SQLTEMPDBDIR, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SQLTEMPDBLOGDIR, [Parameter(ParameterSetName = 'local', Mandatory = $false)] [Parameter(ParameterSetName = 'gmsa', Mandatory = $false)] [string] $Features, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SQLSYSADMINACCOUNTS, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SQLMinRAM, [Parameter(ParameterSetName = 'local')] [Parameter(ParameterSetName = 'gmsa')] [string] $SQLMaxRAM ) # define arguments if ($UseLocalAccount -eq $false -and $UseGmSA -eq $false) { throw "you have to specify 'UseLocalAccount' or 'UseGmSA'" } if ($UseMixedAuth -eq $false -and $null -eq $SAPWD) { throw "you have to specify 'SAPWD' when 'UseMixedAuth' is used" } if ($uselocalaccount) { $AGTSVCACCOUNT = 'NT Service\SQLAgent$' + $Name $SQLSVCACCOUNT = 'NT Service\MSSQL$' + $Name } elseif ($UseGmSA) { $AGTSVCACCOUNT = $env:USERDOMAIN + "\" + (Get-ADServiceAccount -Identity $AgentAccountName).SamAccountName $SQLSVCACCOUNT = $env:USERDOMAIN + "\" + (Get-ADServiceAccount -Identity $EngineAccountName).SamAccountName } $SQLTELSVCACCT = 'NT Service\SQLTELEMETRY$' + $Name if ($Features -eq "") { $Features = "SQLENGINE" } if ($null -eq $SQLBACKUPDIR) { $SQLBACKUPDIR = $INSTALLSQLDATADIR + "\BACKUP\DATA" } if ($null -eq $SQLUSERDBDIR) { $SQLUSERDBDIR = $INSTALLSQLDATADIR + "\USER\DATA" } if ($null -eq $SQLUSERDBLOGDIR) { $SQLUSERDBLOGDIR = $INSTALLSQLDATADIR + "\USERLOG\LOG" } if ($null -eq $SQLTEMPDBDIR) { $SQLTEMPDBDIR = $INSTALLSQLDATADIR + "\TEMP\DATA" } if ($null -eq $SQLTEMPDBLOGDIR) { $SQLTEMPDBLOGDIR = $INSTALLSQLDATADIR + "\TEMPLOG\LOG" } if ($SQLSYSADMINACCOUNTS -eq "") { $SQLSYSADMINACCOUNTS = "`"$($env:COMPUTERNAME)\Administrator`"" } # check dependencies try { if ($UseGmSA) { if ($env:USERDOMAIN -eq $env:COMPUTERNAME) { throw "you are logged in with a local user, domain user required" } if ((Test-ADServiceAccount -Identity "$($EngineAccountName)$") -eq $false) { throw "gmsa $($EngineAccountName) not installed" } if ((Test-ADServiceAccount -Identity "$($AgentAccountName)$") -eq $false) { throw "gmsa $($AgentAccountName) not installed" } } $InstalledInstances = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server' -ErrorAction SilentlyContinue).InstalledInstances if ($InstalledInstances -contains $Name) { throw "SQL instance is already installed $($Name)" } } catch { throw "error during prerequesits check : $($Psitem.Exception.Message)" } # start installation try { Write-Output "$($env:COMPUTERNAME): starting install of instance $($Name)" $Arguments = @( '/IACCEPTSQLSERVERLICENSETERMS="True"' '/IACCEPTPYTHONLICENSETERMS="False"' '/ACTION="Install"' '/ENU="True"' '/IACCEPTROPENLICENSETERMS="False"' '/SUPPRESSPRIVACYSTATEMENTNOTICE="False"' '/QUIET="True"' '/QUIETSIMPLE="False"' '/UpdateEnabled="True"' '/USEMICROSOFTUPDATE="False"' '/SUPPRESSPAIDEDITIONNOTICE="False"' '/UpdateSource="MU"' ('/FEATURES=' + $Features) ('/INSTANCENAME="' + $Name + '"') ('/INSTALLSHAREDDIR="' + $env:ProgramFiles + '\Microsoft SQL Server"') ('/INSTALLSHAREDWOWDIR="' + ${env:ProgramFiles(x86)} + '\Microsoft SQL Server"') ('/INSTANCEID="' + $Name + '"') ('/SQLTELSVCACCT="' + $SQLTELSVCACCT + '"') '/SQLTELSVCSTARTUPTYPE="Automatic"' ('/AGTSVCACCOUNT="' + $AGTSVCACCOUNT + '"') '/AGTSVCSTARTUPTYPE="Automatic"' '/SQLSVCSTARTUPTYPE="Automatic"' '/SQLCOLLATION="SQL_Latin1_General_CP1_CI_AS"' ('/SQLSVCACCOUNT="' + $SQLSVCACCOUNT + '"') ('/SQLSYSADMINACCOUNTS=' + $SQLSYSADMINACCOUNTS) ('/SQLBACKUPDIR="' + $SQLBACKUPDIR + '"') ('/SQLUSERDBDIR="' + $SQLUSERDBDIR + '"') ('/SQLUSERDBLOGDIR="' + $SQLUSERDBLOGDIR + '"') ('/SQLTEMPDBDIR="' + $SQLTEMPDBDIR + '"') ('/SQLTEMPDBLOGDIR="' + $SQLTEMPDBLOGDIR + '"') '/TCPENABLED="1"' '/NPENABLED="0"' '/BROWSERSVCSTARTUPTYPE="Automatic"' ) if ($UseMixedAuth -eq $true) { $SAArguments = @( '/SECURITYMODE="SQL"' '/SAPWD="' + $SAPWD + '"' ) $Arguments = $Arguments + $SAArguments } if ($INSTALLSQLDATADIR -ne "") { $INSTALLSQLDATADIR_Arguments = @( ('/INSTALLSQLDATADIR="' + $INSTALLSQLDATADIR + '"') ) $Arguments = $Arguments + $INSTALLSQLDATADIR_Arguments } if ($INSTANCEDIR -ne "") { $INSTANCEDIR_Arguments = @( ('/INSTANCEDIR="' + $INSTANCEDIR + '"') ) $Arguments = $Arguments + $INSTANCEDIR_Arguments } if ($SQLMinRAM -ne "") { $MinRAMArguments = @( ('/SQLMINMEMORY="' + $SQLMinRAM + '"') ) $Arguments = $Arguments + $MinRAMArguments } if ($SQLMaxRAM -ne "") { $MaxRAMArguments = @( ('/SQLMAXMEMORY="' + $SQLMaxRAM + '"') ) $Arguments = $Arguments + $MaxRAMArguments } $Process = Start-Process $SetupPath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru if ($Process.ExitCode -ne 0) { $Message = Search-SQLSummaryLog -SearchString "Exit message" throw "check logs of sql setup - $($env:ProgramFiles)\Microsoft SQL Server\*\Setup Bootstrap\Log `nmessage found: $($Message)" } } catch { throw "error during installation of sql instance $($Name): $($Psitem.EXception.Message)" } # service customization try { if ($UseGmSA) { Write-Output "$($env:COMPUTERNAME): configuring SQLAgent`$$($Name) service to autostart delayed" $Agent = (Get-Service -Name "SQLAgent`$$($Name)").Name Start-Process "sc" -ArgumentList ('config "' + $($Agent) + '" start=delayed-auto') -NoNewWindow Write-Output "$($env:COMPUTERNAME): configuring MSSQL`$$($Name) service to autostart delayed" $Engine = (Get-Service -Name "MSSQL`$$($Name)").Name Start-Process "sc" -ArgumentList ('config "' + $($Engine) + '" start=delayed-auto') -NoNewWindow } } catch { throw "error during service customization : $($Psitem.EXception.Message)" } Write-Output "$($env:COMPUTERNAME): finished install of instance $($Name)" } function Set-SQLInstanceStaticPort { <# .Description configures the sql instance to use a static tcp port for all ips .Parameter InstanceName sql instance name .Parameter StaticPort port which should be used .Example # this installs an sql instance with the name PB1 which uses gmsa with customized file paths .Example # this installs an sql instance with the name INSTANCE and local accounts to default file paths Set-SQLInstanceStaticPort -InstanceName "INSTANCE" -StaticPort "1503" .NOTES Support is only available for SQL 2019 Standard / Enterprise #> param ( [Parameter(Mandatory = $true)] [string] $InstanceName, [Parameter(Mandatory = $true)] [string] $StaticPort ) $RegPath = (Resolve-Path -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\MSSQL*$($InstanceName)\MSSQLServer\SuperSocketNetLib\Tcp\IPAll\").Path if (Test-Path -Path $RegPath) { try { $SQLSettings = Get-ItemProperty -Path $RegPath if ($SQLSettings.TcpDynamicPorts -ne $StaticPort) { Write-Output "$($env:COMPUTERNAME): set tcp port of $($InstanceName) to $($StaticPort)" Set-ItemProperty -Path $RegPath -Name TcpPort -Value $StaticPort Set-ItemProperty -Path $RegPath -Name TcpDynamicPorts -Value "" } Write-Output "$($env:COMPUTERNAME): restarting $($InstanceName) to apply change" Get-Service "*$($InstanceName)*" | Restart-Service -Force -WarningAction SilentlyContinue } catch { throw "could not configure the static port on $($InstanceName) - $($PSItem.Exception.Message)" } } else { throw "could not find the registry path for the instance" } } function Test-SQLDatabaseConnection { [CmdletBinding()] param ( [Parameter(Position = 0, Mandatory = $True, ValueFromPipeline = $True)] [string] $Server, [Parameter(Position = 1, Mandatory = $false)] [string] $Database = "master", [Parameter(Position = 2, Mandatory = $True, ParameterSetName = "SQLAuth")] [pscredential] $SACredential, [Parameter(Position = 2, Mandatory = $True, ParameterSetName = "WindowsAuth")] [switch] $UseWindowsAuthentication ) if ($Server -notlike "*\*") { $Server = "$($env:COMPUTERNAME)\$($Server)" } # connect to the database, then immediatly close the connection. If an exception occurrs it indicates the conneciton was not successful. $dbConnection = New-Object System.Data.SqlClient.SqlConnection if (!$UseWindowsAuthentication) { $dbConnection.ConnectionString = "Data Source=$($Server); uid=$($SACredential.UserName); pwd=$($SACredential.GetNetworkCredential().Password); Database=$($Database);Integrated Security=False" } else { $dbConnection.ConnectionString = "Data Source=$($Server); Database=$($Database);Integrated Security=True;" } try { Measure-Command { $dbConnection.Open() } | Out-Null $Success = $true } # exceptions will be raised if the database connection failed. catch { $Success = $false } Finally { # close the database connection $dbConnection.Close() } return $Success } function Get-SQLCU { <# .Description downloads sql cu from microsoft .Parameter Version version of the cu .Parameter Outpath path where the cu is stored .Example # stores the cu to $Outpath Get-SQLISO -Version 2019_latest -Outpath $Outpath .NOTES downloads the sql enterprise eval setup and with it the iso #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet("2019_latest")] [string] $Version, [Parameter(Mandatory = $true)] [string] $Outpath ) $ErrorActionPreference = 'Stop' if ($Outpath[-1] -eq "\") { $Outpath = $Outpath.Substring(0, $Outpath.Length - 1) } $SQLCUFilePath = "$($Outpath)\SQL-$($Version)-cu.exe" switch ($Version) { "2019_latest" { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=100809" } Default { throw "no version was selected or not supported" } } try { $Content = Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL $UpdateLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href Start-FileDownload -DownloadURL $UpdateLink -FileOutPath $SQLCUFilePath } catch { throw "$($env:COMPUTERNAME): error getting sql cu files - $($PSItem.Exception.Message)" } Write-Output "$($env:COMPUTERNAME): finished download - check folder $($Outpath)" } function Install-SQLCU { <# .Description installs sql 2019 latest cu to all local instances .Parameter Version version string of sql cu, like 2019_latest .Parameter TempFolder folder, where files will be stored temporally .Example # this installes sql 2019 latest cu to all instances Install-SQLCU -Version "2019_latest" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet("2019_latest")] [string] $Version, [Parameter(Mandatory = $false)] [string] $TempFolder = "$($env:ProgramData)\NTS\SQL\CU" ) $ErrorActionPreference = 'Stop' $SQLCUFilePath = "$($TempFolder)\SQL-$($Version)-cu.exe" $SQLCUUnpackedFilePath = "$($TempFolder)\$($Version)-cu\" try { Get-SQLCU -Version $Version -Outpath $TempFolder # unpacking sql cu to file system if ((Test-Path -Path $SQLCUFilePath) -eq $false) { throw "cannot find cu package file" } Write-Output "$($env:COMPUTERNAME): unpacking sql cu" $Arguments = "/X:$($SQLCUUnpackedFilePath)" $Process = Start-Process $SQLCUFilePath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru if ($Process.ExitCode -ne 0) { throw "there was an error unpacking cu files" } } catch { throw "$($env:COMPUTERNAME): error getting sql cu files - $($PSItem.Exception.Message)" } try { # installation Write-Output "$($env:COMPUTERNAME): installing sql cu" $Arguments = "/ACTION=Patch /ALLINSTANCES /QUIET /IACCEPTSQLSERVERLICENSETERMS /ENU" $Process = Start-Process ($SQLCUUnpackedFilePath + "setup.exe") -ArgumentList $Arguments -Wait -NoNewWindow -PassThru if ($Process.ExitCode -ne 0) { $FinalResult = Search-SQLSummaryLog -SearchString "Final result" $ExitMessage = Search-SQLSummaryLog -SearchString "Exit message" if ($ExitMessage -like "*No features were updated during the setup execution. The requested features may not be installed or features are already at a higher patch level*") { Write-Output "$($env:COMPUTERNAME): no updates were applied, because already at a higher or equal patch level" } elseif ($FinalResult -like "*Passed but reboot required, see logs for details*") { Write-Output "$($env:COMPUTERNAME): sql cu install but reboot required" } else { throw "there was an error installing sql cu - $($ExitMessage)" } } # cleanup Start-FolderCleanUp -FolderToRemove $TempFolder } catch { throw "$($env:COMPUTERNAME): error sql cu installation - $($PSItem.Exception.Message) - see logs at C:\Program Files\Microsoft SQL Server\*\Setup Bootstrap\Log\" } } function Install-SQLSSMS { <# .Description this installs the latest version of sql sql management studio .Example # this installs the latest version of sql ssms Install-SQLSSMS .NOTES always installs the latest version from https://aka.ms/ssmsfullsetup #> $LatestSetupURL = "https://aka.ms/ssmsfullsetup" $TempFolderPath = "$($env:ProgramData)\NTS\SQL\SSMS\" $SetupFilePath = $TempFolderPath + "SSMS-Latest.exe" # download try { if ((Test-Path -Path $TempFolderPath) -eq $false) { New-Item -Path $TempFolderPath -ItemType Directory -Force | Out-Null } Write-Output "$($env:COMPUTERNAME): downloading SSMS" Start-FileDownload -DownloadURL $LatestSetupURL -FileOutPath $SetupFilePath } catch { throw "error downloading the setup - $($PSItem.Exception.Message)" } # install try { Write-Output "$($env:COMPUTERNAME): installing SSMS" Start-Process -FilePath $SetupFilePath -ArgumentList "/quiet /norestart" -Wait -NoNewWindow Write-Output "$($env:COMPUTERNAME): finished installing SSMS" Write-Output "$($env:COMPUTERNAME): doing cleanup" Remove-Item -Path $TempFolderPath -Recurse -Force } catch { throw "error installing - $($PSItem.Exception.Message)" } } function Get-SQLSSRSSetup { <# .Description this downloads the sql reporting services setup .Parameter Version version of sql reporting services setup .Parameter Outpath where should the setup file be stored .Example # this will download the setup to $SSRSTempFolder Get-SQLSSRSSetup -TempFolder $SSRSTempFolder .NOTES setup file name will be "$($TempFolder)\SQL$($Version)_ReportingServices-latest.exe" #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet("2019")] [string] $Version = "2019", [Parameter(Mandatory = $false)] [string] $Outpath = "$($env:ProgramData)\NTS\SQL\ReportingServices" ) try { $SQL_RPServices_FilePath = "$($Outpath)\SQL$($Version)_ReportingServices-latest.exe" if ((Test-Path -Path $Outpath) -eq $false) { New-Item -Path $Outpath -ItemType Directory -Force | Out-Null } if ($Version -eq "2019") { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=100122" } $Content = Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL $SetupLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href Write-Output "$($env:COMPUTERNAME): downloading sql reporting services" Start-FileDownload -DownloadURL $SetupLink -FileOutPath $SQL_RPServices_FilePath } catch { throw "$($env:COMPUTERNAME): - $($PSItem.Exception.Message)" } } function Install-SQLSSRS { <# .Description this will install sql reporting services .Parameter Version version of sql reporting services setup .Parameter SetupFilePath path to setup file .Example # this will install ssrs to the local server Install-SQLSSRS -Version 2019 .NOTES setup file name must be at "$($TempFolder)\SQL$($Version)_ReportingServices-latest.exe" #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet("2019")] [string] $Version = "2019", [Parameter(Mandatory = $false)] [string] $SetupFilePath = "$($env:ProgramData)\NTS\SQL\ReportingServices\SQL$($Version)_ReportingServices-latest.exe", [Parameter(Mandatory = $false)] [string] $SQL_RPServices_LogPath = "$($env:ProgramData)\NTS\SQL\ReportingServices\install.log" ) try { if (Test-Path -Path $SetupFilePath) { Write-Output "$($env:COMPUTERNAME): installing sql reporting services" $Arguments = "/quiet /IAcceptLicenseTerms /Edition=Eval /norestart /log $($SQL_RPServices_LogPath)" $Process = Start-Process $SetupFilePath -ArgumentList $Arguments -Wait -NoNewWindow -PassThru if ($Process.ExitCode -ne 0) { throw "error installing ssrs - check logs at $($SQL_RPServices_LogPath)" } } else { throw "setup file not found at $($SetupFilePath)" } } catch { throw "$($env:COMPUTERNAME): - $($PSItem.Exception.Message)" } } function Initialize-SQLSSRS { <# .Description this will configure the sql reporting services .Parameter Version version of sql reporting services setup .Parameter SQL_Instance mssql instance .Parameter SSRS_ServiceAccountCredentials credentials for the service account of ssrs, must be an domain user .Parameter SQL_DB_Name name of ssrs db .Example # this will configure the ssrs to use a domain account as service Initialize-SQLSSRS -SQL_Instance "$($env:COMPUTERNAME)\$($using:CM_SQL_RPT_InstanceName)" -SSRS_ServiceAccountCredentials $using:CM_SQL_SSRS_ServiceAccountCredentials -TempFolder $SSRSTempFolder .NOTES https://blog.aelterman.com/2018/01/03/complete-automated-configuration-of-sql-server-2017-reporting-services/ https://github.com/mrsquish/AutomationScripts/blob/main/ConfigureSSRS.ps1 https://gist.github.com/SvenAelterman/f2fd058bf3a8aa6f37ac69e5d5dd2511 #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet("2019", "2017")] [string] $Version = "2019", [Parameter(Mandatory = $true)] [string] $SQL_Instance, [Parameter(Mandatory = $true)] [pscredential] $SSRS_ServiceAccountCredentials, [Parameter(Mandatory = $false)] [string] $SQL_DB_Name = "ReportServer" ) switch ($Version) { "2019" { $WMI_Namespace = "root\Microsoft\SqlServer\ReportServer\RS_SSRS\v15\Admin" } "2017" { $WMI_Namespace = "root\Microsoft\SqlServer\ReportServer\RS_SSRS\v14\Admin" } Default { throw "no version was selected or the version is unsupported" } } if ($SQL_Instance -notlike "*\*") { $SQL_Instance = "$($env:COMPUTERNAME)\$($SQL_Instance)" } # test sql instance connection if ((Test-SQLDatabaseConnection -Server $SQL_Instance -UseWindowsAuthentication) -eq $false) { throw "could not connect to sql instance $($SQL_Instance) using integrated security" } try { $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace If ($RSConfig.IsInitialized -eq $true) { throw "ssrs is already initialized, stopping" } Write-Output "$($env:COMPUTERNAME): configuring sql reporting services" # set service account $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace $useBuiltInServiceAccount = $false Write-Output "$($env:COMPUTERNAME): configuring $($SSRS_ServiceAccountCredentials.UserName) as service account for ssrs" $RSConfig.SetWindowsServiceIdentity($useBuiltInServiceAccount, $($SSRS_ServiceAccountCredentials.UserName), $($SSRS_ServiceAccountCredentials.GetNetworkCredential().Password)) | out-null # need to reset the URLs for domain service account to work Write-Output "$($env:COMPUTERNAME): configuring http urls" $HTTPport = 80 $RSConfig.RemoveURL("ReportServerWebService", "http://+:$($HTTPport)", 1033) | out-null $RSConfig.RemoveURL("ReportServerWebApp", "http://+:$($HTTPport)", 1033) | out-null $RSConfig.SetVirtualDirectory("ReportServerWebService", "ReportServer", 1033) | out-null $RSConfig.SetVirtualDirectory("ReportServerWebApp", "Reports", 1033) | out-null $RSConfig.ReserveURL("ReportServerWebService", "http://+:$($HTTPport)", 1033) | out-null $RSConfig.ReserveURL("ReportServerWebApp", "http://+:$($HTTPport)", 1033) | out-null # restart SSRS service for changes to take effect # Restart-Service -Name $RSConfig.ServiceName -Force # retrieve the current configuration $RSConfig = Get-WmiObject -Class "MSReportServer_ConfigurationSetting" -Namespace $WMI_Namespace # get the ReportServer and ReportServerTempDB creation script [string]$dbscript = $RSConfig.GenerateDatabaseCreationScript($SQL_DB_Name, 1033, $false).Script # import the SQL Server PowerShell module Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force -Scope Process Import-Module sqlps -DisableNameChecking | Out-Null # establish a connection to the database server # $ErrorActionPreference = 'SilentlyContinue' # $RunCount = 0 # do { Write-Output "$($env:COMPUTERNAME): connecting to instance $($SQL_Instance)" $conn = New-Object Microsoft.SqlServer.Management.Common.ServerConnection -ArgumentList $SQL_Instance $conn.ApplicationName = "SSRS Configuration Script" $conn.StatementTimeout = 0 $conn.Connect() # Start-Sleep -Seconds 5 # $RunCount++ # } # while ($conn.IsOpen -eq $false -and $RunCount -lt 6) # $ErrorActionPreference = 'Continue' if ($conn.IsOpen -ne $true) { throw "could not connect to $($SQL_Instance)" } $smo = New-Object Microsoft.SqlServer.Management.Smo.Server -ArgumentList $conn # create the ReportServer and ReportServerTempDB databases Write-Output "$($env:COMPUTERNAME): generating databases in instance $($SQL_Instance)" $db = $smo.Databases["master"] $db.ExecuteNonQuery($dbscript) # set permissions for the databases $dbscript = $RSConfig.GenerateDatabaseRightsScript($RSConfig.WindowsServiceIdentityConfigured, $SQL_DB_Name, $false, $true).Script $db.ExecuteNonQuery($dbscript) # set the database connection info # check hresult auf wert 0 Write-Output "$($env:COMPUTERNAME): configuring db connection for ssrs to instance $($SQL_Instance) with db $($SQL_DB_Name)" $RSConfig.SetDatabaseConnection($SQL_Instance, $SQL_DB_Name, 2, "", "") | Out-Null $RSConfig.InitializeReportServer($RSConfig.InstallationID) | Out-Null # restart services # check hresult auf wert 0 $RSConfig.SetServiceState($false, $false, $false) | Out-Null Restart-Service $RSConfig.ServiceName $RSConfig.SetServiceState($true, $true, $true) | Out-Null } catch { throw "$($env:COMPUTERNAME): $($PSItem.Exception.Message)" } } function Search-SQLSummaryLog { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $SearchString ) try { $SummaryLogFilePath = (Get-Item -Path "$($env:ProgramFiles)\Microsoft SQL Server\*\Setup Bootstrap\Log\Summary.txt" | Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1).FullName $Content = Get-Content -Path $SummaryLogFilePath if ($null -eq $Content) { throw "file could not be found" } else { $FilteredContent = $Content | Select-String -Pattern $SearchString if ($null -eq $FilteredContent) { return "no message found with selected search pattern" } else { $Message = $FilteredContent[0].Tostring().Replace($SearchString, "").Trim() if ($Message -like "*:*") { $Message = $Message.Replace(":", "").Trim() } return $Message } } } catch { throw $PSItem.Exception.Message } } function Start-FolderCleanUp { <# .Description this function can be used to remove folders and its items .Parameter FolderToRemove version of sql reporting services setup .Example # this will remove the folder $SSRSTempFolder and its items Start-CleanUp -FolderToRemove $SSRSTempFolder .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $FolderToRemove ) Write-Output "$($env:COMPUTERNAME): removing temp files from $($FolderToRemove)" try { Remove-Item -Path $FolderToRemove -Recurse -Force } catch { throw "error while cleanup - $($PSItem.Exception.Message)" } Write-Output "$($env:COMPUTERNAME): cleanup finished" } 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 + "\" + $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 ) try { Write-Output "$($VMName): configuring network" Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock { $InterfaceObject = (Get-NetAdapter)[0] Write-Output "$($env:COMPUTERNAME): 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 "$($env:COMPUTERNAME): 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 "$($VMName): 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 ) try { Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential Write-Output "$($env:COMPUTERNAME): joining vm $($VMName) to domain $($DomainName)" Invoke-Command -VMName $VMName -Credential $VMCredential -ScriptBlock { Start-Sleep -Seconds 1 if ($NoReboot -eq $true) { if ($null -ne $OUPath) { Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -OUPath $using:OUPath -WarningAction SilentlyContinue } else { Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -WarningAction SilentlyContinue } } else { if ($null -ne $OUPath) { Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -OUPath $using:OUPath -Restart -WarningAction SilentlyContinue } else { Add-Computer -Credential $using:DomainCredential -DomainName $using:DomainName -Restart -WarningAction SilentlyContinue } } } if ($NoReboot -eq $true) { Write-Output "$($env:COMPUTERNAME): domainjoin of vm $($VMName) successfull - reboot required" } else { Write-Output "$($env:COMPUTERNAME): domainjoin of vm $($VMName) successfull - vm will do reboot" } } catch { throw "$($env:COMPUTERNAME): 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 # this will check the specified vm Confirm-VMState -VMObject $VM -VMCredential $VMCred .NOTES VMObject should be like this: $VM_01 = @{ Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1' RAM = `$RAM CPU = `$CPUCount CM_Collection_Name = `$CM_Collection_W11_Autopilot Credentials = `$VM_Credentials DiskSize = `$DynamicDiskSize MAC = "" #> param ( # VM Objects [Parameter(Mandatory = $true)] [System.Object] $VMObject, [Parameter(Mandatory = $true)] [PSCredential] $VMCredential ) try { if ($VMObject.State -ne "Running") { Write-Output "$($env:COMPUTERNAME): starting $($VMObject.Name) because the vm was stopped" Start-VM -VM $VMObject Start-Sleep -Seconds 10 } Write-Output "$($env:COMPUTERNAME): verify the connection to $($VMObject.Name)" if (Test-VMConnection -VMId $VMObject.Id -LocalAdminCreds $VMCredential) { Write-Output "$($env:COMPUTERNAME): connected to $($VMObject.Name) successful - continue" } else { throw "$($env:COMPUTERNAME): error while connecting to $($VMObject.Name) with ps direct - $($PSItem.Exception.Message)" } } catch { throw "$($PSItem.Exception.Message)" } } function Install-NLASvcFixForDCs { <# .Description checks if dns is specified as dependy for service nlasvc and if its not the case it will add it. .Example # this will service dns as dependency for the service nlasvc Install-NLASvcFixForDCs .NOTES #> try { $RegKey = "HKLM:\SYSTEM\CurrentControlSet\Services\NlaSvc" $RegKeyValue = "DependOnService" $ServiceName = "DNS" Write-Output "$($env:COMPUTERNAME): adding service $($ServiceName) as dependency for service NlaSvc" $NLASvcDependencies = (Get-ItemProperty -Path $RegKey -Name $RegKeyValue).$RegKeyValue if ($NLASvcDependencies -notcontains $ServiceName) { $FixedNLASvcDependencies = $NLASvcDependencies + $ServiceName Set-ItemProperty -Path $RegKey -Name $RegKeyValue -Value $FixedNLASvcDependencies Write-Output "$($env:COMPUTERNAME): NlaSvc fix applied" } else { Write-Output "$($env:COMPUTERNAME): NlaSvc fix already applied" } } catch { throw $PSItem.Exception.Message } } function New-EASEOUStructure { <# .Description this will create the esae structure with OUs, User, Groups and Group Managed Service Accounts .Example # creates the ease structure on the local domain controller New-EASEOUStructure .NOTES must be executed on a domain controller #> if ((Get-CimInstance -ClassName Win32_OperatingSystem).ProductType -ne 2) { throw "$($env:COMPUTERNAME): this is not a domain controller, can only be used on domain controllers" } Import-Module Activedirectory $Domain = Get-ADDomain Write-Output "$($env:COMPUTERNAME): apply esae structure on $($Domain.Forest)" #region ous try { Write-Output "$($env:COMPUTERNAME): creating ous" $TopLevelOU = 'ESAE' New-ADOrganizationalUnit -Name $TopLevelOU -Path $Domain.DistinguishedName # ESAE $ESAEPath = "OU=$TopLevelOU,$($Domain.DistinguishedName)" $ESAEOUs = 'Groups', 'PAW', 'Tier 0', 'Tier 1', 'Tier 2' $ESAEOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $ESAEPath } # ESAE\PAW $PAWPath = "OU=PAW,$ESAEPath" $PAWOUs = 'Accounts', 'Devices', 'Groups' $PAWOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $PAWPath } # ESAE\Tier0 $Tier0Path = "OU=Tier 0,$ESAEPath" $Tier0OUs = 'AD', 'PKI', 'SQL', 'ADFS', 'ADSync' $Tier0OUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier0Path } # ESAE\Tier1 $Tier1Path = "OU=Tier 1,$ESAEPath" $Tier1OUs = 'Exchange', 'ConfigMgr', 'SQL' $Tier1OUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier1Path } # ESAE\Tier2 $Tier2Path = "OU=Tier 2,$ESAEPath" $Tier2OUs = 'Accounts', 'Devices', 'Groups' $Tier2OUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier2Path } # ESAE\Tier0\AD $Tier0ADPath = "OU=AD,$Tier0Path" $Tier0ADOUs = 'Accounts', 'Groups' $Tier0ADOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier0ADPath } # ESAE\Tier0\PKI $Tier0PKIPath = "OU=PKI,$Tier0Path" $Tier0PKIOUs = 'Accounts', 'Groups', 'Servers' $Tier0PKIOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier0PKIPath } # ESAE\Tier0\SQL $Tier0SQLPath = "OU=SQL,$Tier0Path" $Tier0SQLOUs = 'Accounts', 'Groups', 'Servers' $Tier0SQLOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier0SQLPath } # ESAE\Tier0\ADFS $Tier0ADFSPath = "OU=ADFS,$Tier0Path" $Tier0ADFSOUs = 'Accounts', 'Groups', 'Servers' $Tier0ADFSOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier0ADFSPath } # ESAE\Tier0\ADSync $Tier0ADSyncPath = "OU=ADSync,$Tier0Path" $Tier0ADSyncOUs = 'Accounts', 'Groups', 'Servers' $Tier0ADSyncOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier0ADSyncPath } # ESAE\Tier1\Exchange $Tier1ExchangePath = "OU=Exchange,$Tier1Path" $Tier1ExchangeOUs = 'Accounts', 'Groups', 'Servers' $Tier1ExchangeOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier1ExchangePath } # ESAE\Tier1\ConfigMgr $Tier1ConfigMgrPath = "OU=ConfigMgr,$Tier1Path" $Tier1ConfigMgrOUs = 'Accounts', 'Groups', 'Servers' $Tier1ConfigMgrOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier1ConfigMgrPath } # ESAE\Tier1\SQL $Tier1SQLPath = "OU=SQL,$Tier1Path" $Tier1SQLOUs = 'Accounts', 'Groups', 'Servers' $Tier1SQLOUs | ForEach-Object { New-ADOrganizationalUnit -Name $PSItem -Path $Tier1SQLPath } } catch { throw "error creating ous: $($PSItem.Exception.Message)" } #endregion #region groups try { Write-Output "$($env:COMPUTERNAME): creating groups" # PAW $GroupNames = 'T0-Allowed', 'T1-Allowed', 'T2-Allowed', 'T0-Denied', 'T1-Denied', 'T2-Denied' $GroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$ESAEPath" } $PAWGroupNames = 'PAW-Devices', 'PAW-Maintenance', 'PAW-Users' $PAWGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$PAWPath" } # T0 $Tier0ADGroupNames = 'T0-CloudAdmins', 'T0-DomAdmins', 'T0-EntAdmins', 'T0-RODCAdmins', 'T0-SchAdmins' $Tier0ADGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0ADPath" } $Tier0PKIGroupNames = 'T0-PKIAdmins', 'T0-PKISCAgents' $Tier0PKIGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0PKIPath" } $Tier0SQLGroupNames = 'T0-SQLAdmins', 'T0-SQLServers' $Tier0SQLGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0SQLPath" } $Tier0ADFSGroupNames = 'T0-ADFSAdmins', 'T0-ADFSServers', 'T0-ADFSSQLServers' $Tier0ADFSGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0ADFSPath" } $Tier0ADSyncGroupNames = 'T0-ADSyncAdmins', 'T0-ADSyncServers', 'T0-ADSyncSQLServers' $Tier0ADSyncGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier0ADSyncPath" } # T1 $Tier1ExchangeGroupNames = 'T1-ExAdmins', 'T1-ExAllowed', 'T1-ExMaintenance', 'T1-ExMBXAdmins', 'T1-ExOrgAdmins' $Tier1ExchangeGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier1ExchangePath" } $Tier1CMGroupNames = 'T1-CMAdmins', 'T1-CMServers', 'T1-CMSQLServers' $Tier1CMGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier1ConfigMgrPath" } $Tier1SQLGroupNames = 'T1-SQLAdmins', 'T1-SQLServers' $Tier1SQLGroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier1SQLPath" } # T2 $Tier2GroupNames = 'T2-HelpDesk' $Tier2GroupNames | ForEach-Object { New-ADGroup -Name $PSItem -SamAccountName $PSItem -GroupCategory Security -GroupScope Universal -DisplayName $PSItem -Path "OU=Groups,$Tier2Path" } } catch { throw "error creating groups: $($PSItem.Exception.Message)" } #endregion #region users try { Write-Output "$($env:COMPUTERNAME): creating users" $Password = ConvertTo-SecureString -String 'C0mplex' -AsPlainText -Force # PAW $PAWUserNames = 'PAWMan1' $PAWUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$PAWPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } # T0 $Tier0ADUserNames = 'T0-DomAdmin1', 'T0-EntAdmin1' $Tier0ADUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0ADPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } $Tier0PKIUserNames = 'T0-PKIAdmin1', 'T0-PKISCA1' $Tier0PKIUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0PKIPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } $Tier0SQLUserNames = 'T0-SQLAdmin1', 'T0-SQLAdmin2' $Tier0SQLUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0SQLPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } $Tier0ADFSUserNames = 'T0-ADFSAdmin1', 'T0-ADFSAdmin2' $Tier0ADFSUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0ADFSPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } $Tier0ADSyncUserNames = 'T0-ADSyncAdmin1', 'T0-ADSyncAdmin2' $Tier0ADSyncUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier0ADSyncPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } # T1 $Tier1ExchangeUserNames = 'T1-ExAdmin1', 'T1-ExAdmin2' $Tier1ExchangeUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier1ExchangePath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } $Tier1CMUserNames = 'T1-CMAdmin1', 'T1-CMAdmin2', 'T1-CMSQLRPT' $Tier1CMUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier1ConfigMgrPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } $Tier1SQLUserNames = 'T1-SQLAdmin1', 'T1-SQLAdmin2' $Tier1SQLUserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier1SQLPath" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } # T2 $Tier2UserNames = 'T2-HelpDesk1', 'T2-HelpDesk2' $Tier2UserNames | ForEach-Object { New-ADUser -Name $PSItem -Path "OU=Accounts,$Tier2Path" -SamAccountName $PSItem -UserPrincipalName "$PSItem@$($Domain.Forest)" -GivenName $PSItem -DisplayName $PSItem -AccountPassword $Password -Enabled $true -PasswordNeverExpires $true } } catch { throw "error creating users: $($PSItem.Exception.Message)" } #endregion #region gmsa try { Write-Output "$($env:COMPUTERNAME): creating gmsa" Add-KdsRootKey -EffectiveTime (Get-Date).AddHours(-10) | Out-Null # T0 New-ADServiceAccount -Name "T0-SQLSvc" -DNSHostName "T0-SQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-SQLServers" New-ADServiceAccount -Name "T0-SQLAgt" -DNSHostName "T0-SQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-SQLServers" # T0\ADFS New-ADServiceAccount -Name "T0-ADFSSvc" -DNSHostName "T0-ADFSSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADFSServers" New-ADServiceAccount -Name "T0-ADFSSQLSvc" -DNSHostName "T0-ADFSSQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADFSSQLServers" New-ADServiceAccount -Name "T0-ADFSSQLAgt" -DNSHostName "T0-ADFSSQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADFSSQLServers" # T0\ADSync New-ADServiceAccount -Name "T0-ADSyncSvc" -DNSHostName "T0-ADSyncSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADSyncServers" New-ADServiceAccount -Name "T0-ADSyncSQLSvc" -DNSHostName "T0-ADSyncSQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADSyncSQLServers" New-ADServiceAccount -Name "T0-ADSyncSQLAgt" -DNSHostName "T0-ADSyncSQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T0-ADSyncSQLServers" # T1 New-ADServiceAccount -Name "T1-SQLSvc" -DNSHostName "T1-SQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T1-SQLServers" New-ADServiceAccount -Name "T1-SQLAgt" -DNSHostName "T1-SQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T1-SQLServers" # T1\ConfigMgr New-ADServiceAccount -Name "T1-CMSQLSvc" -DNSHostName "T1-CMSQLSvc.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T1-CMSQLServers" New-ADServiceAccount -Name "T1-CMSQLAgt" -DNSHostName "T1-CMSQLAgt.$($Domain.Forest)" -PrincipalsAllowedToRetrieveManagedPassword "T1-CMSQLServers" } catch { throw "error creating gmsa: $($PSItem.Exception.Message)" } #endregion #region group membership try { Write-Output "$($env:COMPUTERNAME): creating group membership" # Tier Groups $Tier0AllowedGroups = 'T0-CloudAdmins', 'T0-DomAdmins', 'T0-EntAdmins', 'T0-RODCAdmins', 'T0-SchAdmins', 'T0-PKIAdmins', 'T0-PKISCAgents', 'T0-SQLAdmins', 'T0-ADFSAdmins', 'T0-ADSyncAdmins' $Tier1AllowedGroups = 'T1-CMAdmins', 'T1-ExAdmins', 'T1-ExAllowed', 'T1-ExMaintenance', 'T1-ExMBXAdmins', 'T1-ExOrgAdmins', 'T1-SQLAdmins' $Tier2AllowedGroups = 'T2-HelpDesk' Add-ADGroupMember -Identity 'T0-Allowed' -Members $Tier0AllowedGroups Add-ADGroupMember -Identity 'T0-Denied' -Members ($Tier1AllowedGroups + $Tier2AllowedGroups) Add-ADGroupMember -Identity 'T1-Allowed' -Members $Tier1AllowedGroups Add-ADGroupMember -Identity 'T1-Denied' -Members ($Tier0AllowedGroups + $Tier2AllowedGroups) Add-ADGroupMember -Identity 'T2-Allowed' -Members $Tier2AllowedGroups Add-ADGroupMember -Identity 'T2-Denied' -Members ($Tier0AllowedGroups + $Tier1AllowedGroups) # PAW Add-ADGroupMember -Identity 'PAW-Maintenance' -Members 'PAWMan1' # T0 Add-ADGroupMember -Identity 'Domain Admins' -Members 'T0-DomAdmin1' Add-ADGroupMember -Identity 'Enterprise Admins' -Members 'T0-EntAdmin1' Add-ADGroupMember -Identity 'T0-PKIAdmins' -Members 'T0-PKIAdmin1' Add-ADGroupMember -Identity 'T0-PKISCAgents' -Members 'T0-PKISCA1' Add-ADGroupMember -Identity 'T0-SQLAdmins' -Members 'T0-SQLAdmin1', 'T0-SQLAdmin2' # T1 Add-ADGroupMember -Identity 'T1-ExAdmins' -Members 'T1-ExAdmin1', 'T1-ExAdmin2' Add-ADGroupMember -Identity 'T1-CMAdmins' -Members 'T1-CMAdmin1', 'T1-CMAdmin2' Add-ADGroupMember -Identity 'T1-SQLAdmins' -Members 'T1-SQLAdmin1', 'T1-SQLAdmin2' # T2 Add-ADGroupMember -Identity 'T2-HelpDesk' -Members 'T2-HelpDesk1', 'T2-HelpDesk2' } catch { throw "error creating group memberships: $($PSItem.Exception.Message)" } #endregion } function Repair-DFSRReplication { <# .Description this will forcefully repair dfsr replication on the domain, use with care .Example # this will forcefully repair dfsr replication on the domain Repair-DFSRReplication .NOTES must be executed on a domain controller #> $Domain = (Get-ADDomain -Current LoggedOnUser).DistinguishedName $PDC = (Get-ADDomain).PDCEmulator $DCs = Get-ADObject -Filter { (objectclass -eq "computer") } -SearchBase "OU=Domain Controllers,$Domain" -Properties Name | Sort-Object Name $DCs | ForEach-Object { Invoke-Command -ComputerName $PSItem.Name -ScriptBlock { $Domain = (Get-ADDomain -Current LoggedOnUser).DistinguishedName $PDC = (Get-ADDomain).PDCEmulator $sysvolADObject = Get-ADObject -Filter { (objectclass -eq "msDFSR-Subscription") } ` -SearchBase "CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,CN=$($env:COMPUTERNAME),OU=Domain Controllers,$Domain"` -Properties "msDFSR-Options", "msDFSR-Enabled" Write-Output "$($env:COMPUTERNAME): Restarting NIC to correct Network Profile" $NetAdapter = Get-NetAdapter $NetAdapter[0] | Restart-NetAdapter -Confirm:$false Write-Output "$($env:COMPUTERNAME): Installing DFSR Management Tools" Install-WindowsFeature -Name RSAT-DFS-Mgmt-Con | Out-Null if ($($PDC).Contains($env:COMPUTERNAME)) { Write-Output "$($env:COMPUTERNAME): Setting msDFSR-Options to 1" Set-ADObject -Identity $sysvolADObject.DistinguishedName -Replace @{"msDFSR-Options" = 1 } } Write-Output "$($env:COMPUTERNAME): Setting msDFSR-Enabled to false" Set-ADObject -Identity $sysvolADObject.DistinguishedName -Replace @{"msDFSR-Enabled" = $false } } } Write-Output "$($env:COMPUTERNAME): Forcing AD Replication" repadmin /syncall /P /e Start-Sleep(10) Invoke-Command -ComputerName $PDC -ScriptBlock { $Domain = (Get-ADDomain -Current LoggedOnUser).DistinguishedName $sysvolADObject = Get-ADObject -Filter { (objectclass -eq "msDFSR-Subscription") } ` -SearchBase "CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,CN=$($env:COMPUTERNAME),OU=Domain Controllers,$Domain"` -Properties "msDFSR-Options", "msDFSR-Enabled" Write-Output "$($env:COMPUTERNAME): Restarting DFSR Service" Restart-Service DFSR Start-Sleep(20) Write-Output "$($env:COMPUTERNAME): Setting msDFSR-Enabled to true" Set-ADObject -Identity $sysvolADObject.DistinguishedName -Replace @{"msDFSR-Enabled" = $true } Write-Output "$($env:COMPUTERNAME): Forcing AD Replication" repadmin /syncall /P /e Start-Sleep(10) Write-Output "$($env:COMPUTERNAME): Executing DFSRDIAG" dfsrdiag pollad } $DCs | ForEach-Object { Invoke-Command -ComputerName $PSItem.Name -ScriptBlock { $Domain = (Get-ADDomain -Current LoggedOnUser).DistinguishedName $PDC = (Get-ADDomain).PDCEmulator $sysvolADObject = Get-ADObject -Filter { (objectclass -eq "msDFSR-Subscription") } ` -SearchBase "CN=SYSVOL Subscription,CN=Domain System Volume,CN=DFSR-LocalSettings,CN=$($env:COMPUTERNAME),OU=Domain Controllers,$Domain"` -Properties "msDFSR-Options", "msDFSR-Enabled" if (!$($PDC).Contains($($env:COMPUTERNAME))) { Write-Output "$($env:COMPUTERNAME): Restarting DFSR Service" Restart-Service DFSR Start-Sleep(20) Write-Output "$($env:COMPUTERNAME): Setting msDFSR-Enabled to true" Set-ADObject -Identity $sysvolADObject.DistinguishedName -Replace @{"msDFSR-Enabled" = $true } Write-Output "$($env:COMPUTERNAME): Executing DFSRDIAG" dfsrdiag pollad } } } } function Set-Interface { <# .Description configures the network interface, ip, dns, gateway .Parameter InterfaceObject nic objects .Parameter IPAddress ipaddress .Parameter NetPrefix net prefix, e.g. 24 .Parameter DefaultGateway default gateway in the subnet .Parameter DNSAddresses dns server addresses .Parameter NewName new name of the network adapter .Example # configures the specified network card Set-Interface -InterfaceObject $SFP10G_NICs[0] -IPAddress $CLU1_IPAddress -NetPrefix $NetPrefix -DefaultGateway $CLU_DefaultGateway -DNSAddresses $CLU_DNSAddresses -NewName "Datacenter-1" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] $InterfaceObject, [Parameter(Mandatory = $true)] $IPAddress, [Parameter(Mandatory = $true)] $NetPrefix, [Parameter(Mandatory = $false)] $DefaultGateway, [Parameter(Mandatory = $false)] $DNSAddresses, [Parameter(Mandatory = $false)] $NewName ) try { Write-Output "$($env:COMPUTERNAME): configuring nic with macaddress $($InterfaceObject.MacAddress)" 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 if ($null -ne $DefaultGateway) { $InterfaceObject | New-NetIPAddress -IPAddress $IPAddress -AddressFamily "IPv4" -PrefixLength $NetPrefix -DefaultGateway $DefaultGateway | Out-Null Write-Output "$($env:COMPUTERNAME): interface $($InterfaceObject.InterfaceDescription) has the static ip $($IPAddress) now" } else { $InterfaceObject | New-NetIPAddress -IPAddress $IPAddress -AddressFamily "IPv4" -PrefixLength $NetPrefix | Out-Null Write-Output "$($env:COMPUTERNAME): interface $($InterfaceObject.InterfaceDescription) has the static ip $($IPAddress) now" } if ($null -ne $DNSAddresses) { $InterfaceObject | Set-DnsClientServerAddress -ServerAddresses $DNSAddresses } if ($null -ne $NewName) { $InterfaceObject | Rename-NetAdapter -NewName $NewName Write-Output "$($env:COMPUTERNAME): interface $($InterfaceObject.InterfaceDescription) renamed to $($NewName)" } } catch { throw "error setting $($InterfaceObject.Name): $($PSItem.Exception.Message)" } } function Install-WADK { <# .Description this function can be used to install Windows ADK and Windows ADK PE .Parameter Latest use if you want the latest version .Parameter Features a list of Windows ADK to be installed .Parameter IncludeWinPE use if you want windows adk pe installed .Parameter Outpath path where the install and log files are saved .Example # installs windows adk and windows adk pe for configmgr site server Install-WADK -Latest -IncludeWinPE -Features OptionId.DeploymentTools, OptionId.UserStateMigrationTool .NOTES requires internet connection #> [CmdletBinding()] param ( [Parameter(ParameterSetName = 'Latest')] [switch] $Latest, [Parameter(ParameterSetName = 'Latest', Mandatory = $true)] [ValidateSet( "OptionId.ApplicationCompatibilityToolkit", "OptionId.DeploymentTools", "OptionId.ImagingAndConfigurationDesigner", "OptionId.ICDConfigurationDesigner", "OptionId.UserStateMigrationTool", "OptionId.VolumeActivationManagementTool", "OptionId.WindowsPerformanceToolkit", "OptionId.UEVTools", "OptionId.AppmanSequencer", "OptionId.AppmanAutoSequencer", "OptionId.MediaeXperienceAnalyzer", "OptionId.MediaeXperienceAnalyzer", "OptionId.WindowsAssessmentToolkit" )] [string[]] $Features, [Parameter(ParameterSetName = 'Latest')] [switch] $IncludeWinPE, [Parameter(ParameterSetName = 'Latest')] [string] $Outpath = "C:\Programdata\NTS\Windows_ADK" ) if ($Latest -eq $true) { $WADK_Download_URL_Latest = "https://go.microsoft.com/fwlink/?linkid=2196127" $WADK_PE_Download_URL_Latest = "https://go.microsoft.com/fwlink/?linkid=2196224" } else { throw "other version than latest are currently not supported" } try { if ((Test-Path -Path $Outpath) -eq $false) { New-Item -Path $Outpath -ItemType Directory -Force | Out-Null } $Outpath = (Get-Item -Path $Outpath).FullName if ($Features.count -gt 1) { $Features | ForEach-Object { [string]$Features_Selected = $Features_Selected + " " + $PSItem } } $WADK_LatestPath = "$($Outpath)\adksetup-latest.exe" $WADK_LogPath = "$($Outpath)\install-adksetup-latest.log" $WADK_PE_LatestPath = "$($Outpath)\adkwinpesetup-latest.exe" $WADK_PE_LogPath = "$($Outpath)\install-adkwinpesetup-latest.log" # download Write-Output "$($env:COMPUTERNAME): downloading adk setup files" Start-FileDownload -DownloadURL $WADK_Download_URL_Latest -FileOutPath $WADK_LatestPath # install Write-Output "$($env:COMPUTERNAME): installing adk with the features $($Features_Selected)" $Process = Start-Process -FilePath $WADK_LatestPath -ArgumentList "/quiet /norestart /features $($Features_Selected) /l $($WADK_LogPath)" -NoNewWindow -Wait -PassThru if ($Process.ExitCode -ne 0) { throw "check log at $($WADK_LogPath)" } if ($IncludeWinPE -eq $true) { # download Write-Output "$($env:COMPUTERNAME): downloading adk pe setup files" Start-FileDownload -DownloadURL $WADK_PE_Download_URL_Latest -FileOutPath $WADK_PE_LatestPath # install Write-Output "$($env:COMPUTERNAME): installing adk pe" $Process = Start-Process -FilePath $WADK_PE_LatestPath -ArgumentList "/quiet /norestart /features OptionId.WindowsPreinstallationEnvironment /l $($WADK_PE_LogPath)" -NoNewWindow -Wait -PassThru if ($Process.ExitCode -ne 0) { throw "check log at $($WADK_PE_LogPath)" } } Start-FolderCleanUp -FolderToRemove $Outpath } catch { throw "something went wrong $($PSItem.Exception.Message)" } } function Initialize-CM_MP_Prereq { <# .Description use this function to install configmgr management point prerequesits .Example # install configmgr management point prerequesits Initialize-CM_MP_Prereq .NOTES #> $Features = @( "NET-Framework-Core" "FileAndStorage-Services" "Storage-Services" "Web-Server" "Web-WebServer" "Web-Common-Http" "Web-Default-Doc" "Web-Dir-Browsing" "Web-Http-Errors" "Web-Static-Content" "Web-Http-Redirect" "Web-DAV-Publishing" "Web-Health" "Web-Http-Logging" "Web-Custom-Logging" "Web-Log-Libraries" "Web-ODBC-Logging" "Web-Request-Monitor" "Web-Http-Tracing" "Web-Performance" "Web-Stat-Compression" "Web-Dyn-Compression" "Web-Security" "Web-Filtering" "Web-Basic-Auth" "Web-CertProvider" "Web-Client-Auth" "Web-Digest-Auth" "Web-Cert-Auth" "Web-IP-Security" "Web-Url-Auth" "Web-Windows-Auth" "Web-App-Dev" "Web-Net-Ext" "Web-Net-Ext45" "Web-AppInit" "Web-ASP" "Web-Asp-Net" "Web-Asp-Net45" "Web-CGI" "Web-ISAPI-Ext" "Web-ISAPI-Filter" "Web-Includes" "Web-WebSockets" "Web-Ftp-Server" "Web-Ftp-Service" "Web-Ftp-Ext" "Web-Mgmt-Tools" "Web-Mgmt-Console" "Web-Mgmt-Compat" "Web-Metabase" "Web-Lgcy-Mgmt-Console" "Web-Lgcy-Scripting" "Web-WMI" "Web-Scripting-Tools" "Web-Mgmt-Service" "NET-Framework-Features" "NET-Framework-Core" "NET-Framework-45-Features" "NET-Framework-45-Core" "NET-Framework-45-ASPNET" "NET-WCF-Services45" "NET-WCF-HTTP-Activation45" "NET-WCF-MSMQ-Activation45" "NET-WCF-Pipe-Activation45" "NET-WCF-TCP-Activation45" "NET-WCF-TCP-PortSharing45" "BITS" "BITS-IIS-Ext" "BITS-Compact-Server" "MSMQ" "MSMQ-Services" "MSMQ-Server" "Windows-Defender" "RDC" "RSAT" "RSAT-Feature-Tools" "RSAT-Bits-Server" "System-DataArchiver" "PowerShellRoot" "PowerShell" "PowerShell-V2" "WAS" "WAS-Process-Model" "WAS-Config-APIs" "WoW64-Support" "XPS-Viewer" ) try { Write-Output "$($env:COMPUTERNAME): installing required features for configmgr management point" Install-WindowsFeature -Name $Features | Out-Null } catch { throw $PSItem.Exception.Message } } function Initialize-CM_DP_Prereq { <# .Description use this function to install configmgr distribution point prerequesits .Example # install configmgr distribution point prerequesits Initialize-CM_DP_Prereq .NOTES #> $Features = @( "FileAndStorage-Services" "File-Services" "FS-FileServer" "Storage-Services" "Web-Server" "Web-WebServer" "Web-Common-Http" "Web-Default-Doc" "Web-Dir-Browsing" "Web-Http-Errors" "Web-Static-Content" "Web-Http-Redirect" "Web-Health" "Web-Http-Logging" "Web-Performance" "Web-Stat-Compression" "Web-Security" "Web-Filtering" "Web-Windows-Auth" "Web-App-Dev" "Web-ISAPI-Ext" "Web-Mgmt-Tools" "Web-Mgmt-Console" "Web-Mgmt-Compat" "Web-Metabase" "Web-WMI" "Web-Scripting-Tools" "NET-Framework-45-Features" "NET-Framework-45-Core" "NET-WCF-Services45" "NET-WCF-TCP-PortSharing45" "Windows-Defender" "RDC" "System-DataArchiver" "PowerShellRoot" "PowerShell" "WoW64-Support" "XPS-Viewer" ) try { Write-Output "$($env:COMPUTERNAME): creating NO_SMS_ON_DRIVE.SMS on boot volume" if ((Test-Path -Path "$($env:SystemDrive)\NO_SMS_ON_DRIVE.SMS") -eq $false) { New-Item -Path "$($env:SystemDrive)\NO_SMS_ON_DRIVE.SMS" -ItemType File | Out-Null } Write-Output "$($env:COMPUTERNAME): installing required features for configmgr distribution point" Install-WindowsFeature -Name $Features | Out-Null } catch { throw $PSItem.Exception.Message } } function Initialize-CM_SiteServer_Prereq { <# .Description use this function to install configmgr site server prerequesits .Example # install configmgr site server prerequesits Initialize-CM_SiteServer_Prereq .NOTES #> $Features = @( "RDC" "UpdateServices-RSAT" "NET-Framework-Features" ) try { Write-Output "$($env:COMPUTERNAME): creating NO_SMS_ON_DRIVE.SMS on boot volume" if ((Test-Path -Path "$($env:SystemDrive)\NO_SMS_ON_DRIVE.SMS") -eq $false) { New-Item -Path "$($env:SystemDrive)\NO_SMS_ON_DRIVE.SMS" -ItemType File | Out-Null } Write-Output "$($env:COMPUTERNAME): installing required features for configmgr site server" Install-WindowsFeature -Name $Features -IncludeAllSubFeature | Out-Null } catch { throw $PSItem.Exception.Message } } function Initialize-CM_SUP_Prereq { <# .Description use this function to install configmgr software update point prerequesits .Example # install configmgr software update point prerequesits Initialize-CM_SUP_Prereq .NOTES #> try { Write-Output "$($env:COMPUTERNAME): installing required features for configmgr software update point" Install-WindowsFeature -Name RDC, UpdateServices-RSAT -IncludeAllSubFeature | Out-Null } catch { throw $PSItem.Exception.Message } } function Add-CM_ADContainer { <# .Description use this function to create the system management container in ad and add permissions to the local server .Example # checks if the container exits and adds permissions Add-CM_ADContainer .NOTES #> try { Import-Module -Name "ActiveDirectory" $AD_DistinguishedName = (Get-ADDomain).DistinguishedName $CM_ContainerName = "SYSTEM MANAGEMENT" Write-Output "$($env:COMPUTERNAME): adding container 'SYSTEM MANAGEMENT'" if ($null -eq (Get-ADObject -Filter 'ObjectClass -eq "container"' -SearchBase "CN=System,$($AD_DistinguishedName)" | Where-Object -Property Name -eq $CM_ContainerName)) { New-ADObject -Name $CM_ContainerName -Path "CN=System,$($AD_DistinguishedName)" -Type Container } Write-Output "$($env:COMPUTERNAME): adding permissions for the ad container" $path = "AD:\CN=$($CM_ContainerName),CN=System,$($AD_DistinguishedName)" $ADCompObject = Get-ADComputer -Identity $env:COMPUTERNAME $adRights = [DirectoryServices.ActiveDirectoryRights]::GenericAll $accessType = [Security.AccessControl.AccessControlType]::Allow $inheritance = [DirectoryServices.ActiveDirectorySecurityInheritance]::All $fullAccessACE = New-Object -TypeName DirectoryServices.ActiveDirectoryAccessRule -ArgumentList @($ADCompObject.SID, $adRights, $accessType, $inheritance) $acl = Get-Acl -Path $path $acl.AddAccessRule($fullAccessACE) Set-Acl -Path $path -AclObject $acl } catch { throw $PSItem.Exception.Message } } function Install-WSUS { <# .Description this will install the required functions for wsus and do the post install tasks .Parameter UseWID wsus with windows internal database .Parameter UseSQL wsus with mssql database .Parameter WSUSFilePath where should the file be stored .Parameter SQLInstance sql instance for wsus .Example # configures the specified network card Set-Interface -InterfaceObject $SFP10G_NICs[0] -IPAddress $CLU1_IPAddress -NetPrefix $NetPrefix -DefaultGateway $CLU_DefaultGateway -DNSAddresses $CLU_DNSAddresses -NewName "Datacenter-1" .NOTES https://smsagent.blog/2014/02/07/installing-and-configuring-wsus-with-powershell/ #> [CmdletBinding()] param ( [Parameter(ParameterSetName = 'WID')] [switch] $UseWID, [Parameter(ParameterSetName = 'SQL')] [switch] $UseSQL, [Parameter(ParameterSetName = 'WID', Mandatory = $true)] [Parameter(ParameterSetName = 'SQL', Mandatory = $true)] [string] $WSUSFilePath, [Parameter(ParameterSetName = 'SQL', Mandatory = $true)] [string] $SQLInstance # "MyServer\MyInstance" ) if ((Test-Path -Path $WSUSFilePath) -eq $false) { New-Item -Path $WSUSFilePath -ItemType Directory -Force | Out-Null } try { if ($UseWID -eq $true) { Write-Output "$($env:COMPUTERNAME): installing required features for wsus" Install-WindowsFeature UpdateServices -IncludeManagementTools -WarningVariable SilentlyContinue | Out-Null Write-Output "$($env:COMPUTERNAME): doing postinstall with wid" Start-Process -FilePath "$($env:ProgramFiles)\Update Services\Tools\wsusutil.exe" -ArgumentList "postinstall CONTENT_DIR=$($WSUSFilePath)" -NoNewWindow -Wait } elseif ($UseSQL -eq $true) { Write-Output "$($env:COMPUTERNAME): installing required features for wsus" Install-WindowsFeature -Name UpdateServices-Services, UpdateServices-DB -IncludeManagementTools -WarningVariable SilentlyContinue | Out-Null Write-Output "$($env:COMPUTERNAME): doing postinstall with sql instance $($SQLInstance)" Start-Process -FilePath "$($env:ProgramFiles)\Update Services\Tools\wsusutil.exe" -ArgumentList "postinstall SQL_INSTANCE_NAME=$($SQLInstance) CONTENT_DIR=$($WSUSFilePath)" -NoNewWindow -Wait } } catch { throw $PSItem.Exception.Message } } function Add-SQLDBRole { <# .Description this function can be used to add a role to db for an sql login .Parameter InstanceName name of the instance .Parameter SQLogin sql login name .Parameter DBName database name .Parameter DBRole role that should be granted to the sql login .Example # adds db_owner to local system on susdb Add-SQLDBRole -InstanceName "$($env:COMPUTERNAME)\$using:CM_SQL_WSUS_InstanceName" -SQLogin "NT AUTHORITY\SYSTEM" -DBName "SUSDB" -DBRole 'db_owner' .NOTES https://www.sqlservercentral.com/blogs/use-powershell-to-add-a-login-to-a-database-role-in-all-databases #> param ( [Parameter(Mandatory = $true)] [string] $InstanceName, [Parameter(Mandatory = $true)] [string] $SQLogin, [Parameter(Mandatory = $true)] [string] $DBName, [Parameter(Mandatory = $true)] [string] $DBRole ) try { # Load the SMO assembly [System.Reflection.Assembly]::LoadWithPartialName( 'Microsoft.SqlServer.SMO') | Out-Null # Connect to the instance using SMO $SQLServer = new-object ('Microsoft.SqlServer.Management.Smo.Server') $InstanceName # Get the defined login - if it doesn't exist it's an error $SQLoginObject = $SQLServer.Logins[$SQLogin] if ($null -eq $SQLoginObject) { throw "$($SQLogin) is not a valid SQL Server Login on this instance." } $SQLLoginName = $SQLoginObject.Name $SQLDatabaseObject = $SQLServer.Databases[$DBName] # Check to see if the login is a user in this database $SQLUserObject = $SQLDatabaseObject.Users[$SQLLoginName] if ($null -eq $SQLUserObject) { # Not present, so add it $SQLUserObject = New-Object ('Microsoft.SqlServer.Management.Smo.User') ($SQLDatabaseObject, $SQLLoginName) $SQLUserObject.Login = $SQLLoginName $SQLUserObject.Create() } # Check to see if the user is a member of the db_owner role if ($SQLUserObject.IsMember($DBRole) -ne $True) { # Not a member, so add that role $SQLConnection = new-object system.data.SqlClient.SqlConnection("Data Source=$($InstanceName);Integrated Security=SSPI;Initial Catalog=$($DBName)"); $SQLConnection.Open() $SQLQuery = "EXEC sp_addrolemember @rolename = N'$($DBRole)', @membername = N'$($SQLLoginName)'" $SQLCommand = new-object "System.Data.SqlClient.SqlCommand" ($SQLQuery, $SQLConnection) Write-Output "$($env:COMPUTERNAME): adding db role $($DBRole) to $($SQLogin) on instance $($InstanceName) for db $($DBName)" $SQLCommand.ExecuteNonQuery() | out-null $SQLConnection.Close() } } catch { throw "could not grant sql db role - $($PSItem.Exception.Message)" } } function Confirm-CM_Prerequisites { <# .Description this function will search for the configmgr install volume and run the prerequisite checks for a site server .Parameter PrereqchkFilePath path to the prereqchk.exe .Parameter CM_SiteServerFQDN fqdn of the site server .Parameter CM_SQL_Site_Instance database server with instance name, eg. <fqdn of the site server>\<instancename> .Example # this will run the checks and throw an error if something is not passed Confirm-CM_Prerequisites -CM_SiteServerFQDN $CM_SiteServerFQDN -CM_SQL_Site_Instance ($CM_SiteServerFQDN + "\" + $using:CM_SQL_Site_InstanceName) .NOTES https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/prerequisite-checker #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string] $PrereqchkFilePath, [Parameter(Mandatory = $true)] [string] $CM_SiteServerFQDN, [Parameter(Mandatory = $true)] [string] $CM_SQL_Site_Instance ) $CM_PrereqchkLogFilePath = "$($env:SystemDrive)\ConfigMgrPrereq.log" if ($PrereqchkFilePath -eq "") { $CM_SetupVolumes = Get-CM_Setup_Volume if ($CM_SetupVolumes.DriveLetter.count -eq 1) { $CM_Prereqchk_Filepath = "$(($CM_SetupVolumes).DriveLetter):\SMSSETUP\BIN\X64\prereqchk.exe" } else { throw "there are more than one or less than one installation media for configmgr" } } else { if (Test-Path -Path $PrereqchkFilePath) { $CM_Prereqchk_Filepath = $PrereqchkFilePath } else { throw "cannot find prereqchk.exe at $($PrereqchkFilePath)" } } if (Test-Path -Path $CM_PrereqchkLogFilePath) { Remove-Item -Path $CM_PrereqchkLogFilePath -Force | Out-Null } try { Write-Output "$($env:COMPUTERNAME): checking prerequisites for the site server role & admin console" Start-Process -FilePath $CM_Prereqchk_Filepath -ArgumentList "/NOUI /PRI /SDK $($CM_SiteServerFQDN) /SQL $CM_SQL_Site_Instance /SCP" -Wait -NoNewWindow Start-Process -FilePath $CM_Prereqchk_Filepath -ArgumentList "/NOUI /ADMINUI" -Wait -NoNewWindow } catch { throw "failed to run $($CM_Prereqchk_Filepath) - $($PSItem.Exception.Message)" } $Content = Get-Content -Path $CM_PrereqchkLogFilePath $SuccessMessage = $Content -like "*Prerequisite checking is completed.*" $FailureMessage = $Content -like "*ERROR:*" if ($null -eq $SuccessMessage[0] -and $null -ne $FailureMessage[0]) { if ($FailureMessage -like "*ERROR: Failed to connect to SQL Server 'master' db.*" -and $FailureMessage.Count -gt 2) { throw "found errors in log $($CM_PrereqchkLogFilePath):`n$($FailureMessage)" } } Write-Output "$($env:COMPUTERNAME): all prerequisites are met for configmgr installation" } function Test-FileLock { <# .Description this function test if a file is in use and returns true if so .Parameter Path file path to the file .Example # this checks if the file is in use Test-FileLock -Path C:\WINDOWS\CCM\Logs\PolicyAgentProvider.log .NOTES https://stackoverflow.com/questions/24992681/powershell-check-if-a-file-is-locked #> [CmdletBinding()] param ( [parameter(Mandatory = $true)] [string] $Path ) $oFile = New-Object System.IO.FileInfo $Path if ((Test-Path -Path $Path) -eq $false) { return $false } try { $oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None) if ($oStream) { $oStream.Close() } $false } catch { # file is locked by a process. return $true } } function Uninstall-ConfigMgrAgent { <# .Description this function uninstalls the configmgr agent .Example # this function uninstalls the configmgr agent Uninstall-ConfigMgrAgent .NOTES https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/prerequisite-checker #> $SkipCleanup = $false try { $CCMExecServiceName = "CcmExec" $CCMSetupFilePath = "$($env:windir)\ccmsetup\ccmsetup.exe" if ($null -ne (Get-Service -Name $CCMExecServiceName -ErrorAction SilentlyContinue) -or (Test-Path -Path $CCMSetupFilePath)) { Write-Output "$($env:COMPUTERNAME): starting configmgr Agent uninstall" Start-Process -FilePath $CCMSetupFilePath -ArgumentList "/uninstall" -Wait -NoNewWindow $LogFileContent = Get-Content -Path "$($env:windir)\ccmsetup\logs\CCMSetup.log" $SuccesMessage = $LogFileContent -like "*[LOG[Uninstall succeeded.]LOG]*" } else { Write-Output "$($env:COMPUTERNAME): Service $($CCMExecServiceName) not found and no ccmsetup.exe, skipping" $SkipCleanup = $true } } catch { throw "$($env:COMPUTERNAME): error while uninstalling the agent - $($PSItem.Exception.Message)" } try { if ($SkipCleanup -eq $false) { if ($SuccesMessage.Count -gt 0) { Write-Output "$($env:COMPUTERNAME): finished configmgr Agent uninstall" Write-Output "$($env:COMPUTERNAME): doing cleanup" if (Test-Path -Path "$($env:windir)\CCM") { $Items = Get-ChildItem -Path "$($env:windir)\CCM" $Items | ForEach-Object { if ((Test-FileLock -Path $PSItem.FullName) -ne $true) { Remove-Item -Path $PSItem.FullName -Force -Recurse | Out-Null } } } if (Test-Path -Path "$($env:windir)\ccmsetup") { Remove-Item -Path "$($env:windir)\ccmsetup" -Force -Recurse | Out-Null } Write-Output "$($env:COMPUTERNAME): finished doing cleanup" } else { throw "uninstall was not successful $($PSItem.Exception.Message)" } } } catch { throw "$($env:COMPUTERNAME): error doing cleanup - $($PSItem.Exception.Message)" } } function Get-CM_Setupfiles { <# .Description downloads the eval setup of configmgr current branch .Parameter Version version of the iso .Parameter Outpath path where the setup file is stored .Example # stores the setup file to $Outpath Get-CM_Setupfiles -Version 2203 -Outpath $Outpath .NOTES downloads the configmgr current branch eval setup #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [ValidateSet("2203")] [string] $Version = "2203", [Parameter(Mandatory = $true)] [string] $Outpath ) switch ($Version) { "2203" { $DownloadURL = "https://go.microsoft.com/fwlink/p/?LinkID=2195628&clcid=0x409&culture=en-us&country=US" } Default { throw "no version was selected" } } if ((Test-Path -Path $Outpath) -eq $false) { New-Item -Path $Outpath -ItemType Directory -Force | Out-Null } $Outpath = (Get-Item -Path $Outpath).FullName $SetupPath = "ConfigMgr-$($Version)-CB-Eval.exe" $SetupFullPath = "$($Outpath)\$($SetupPath)" try { Start-FileDownload -DownloadURL $DownloadURL -FileOutPath $SetupFullPath Write-Output "$($env:COMPUTERNAME): finished download, starting extraction to $($Outpath)" $Arguments = "/auto `"$($Outpath)`"" Start-Process -FilePath $SetupFullPath -ArgumentList $Arguments -NoNewWindow -Wait Write-Output "$($env:COMPUTERNAME): finished, setup files can be found at $($Outpath)" } catch { throw "error downloading eval setup: $($PSItem.Exception.Message)" } } function Get-CM_PrerequisiteFiles { <# .Description this function calls SMSSETUP\BIN\X64\Setupdl.exe from configmgr iso .Parameter SetupdlFilePath path to the Setupdl.exe .Parameter Outpath save path of the downloaded files .Example # downloads the files to $($env:SystemDrive)\Temp\ConfigMgr\SetupFiles Get-CM_SetupFiles -Outpath "$($env:SystemDrive)\Temp\ConfigMgr\SetupFiles" .NOTES https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/setup-downloader #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string] $SetupdlFilePath, [Parameter(Mandatory = $true)] [string] $Outpath ) try { if ((Test-Path -Path $Outpath) -eq $false) { New-Item -Path $Outpath -ItemType Directory -Force | Out-Null } $Outpath = Resolve-Path $Outpath $LogFilePath = "$($env:SystemDrive)\ConfigMgrSetup.log" if ($SetupdlFilePath -eq "") { $CM_SetupVolumes = Get-CM_Setup_Volume if ($CM_SetupVolumes.DriveLetter.count -eq 1) { $CM_SetupFileDownloaderPath = "$(($CM_SetupVolumes).DriveLetter):\SMSSETUP\BIN\X64\Setupdl.exe" } else { throw "there are more than one or less than one installation media for configmgr" } } else { if (Test-Path -Path $SetupdlFilePath) { $CM_SetupFileDownloaderPath = $SetupdlFilePath } else { throw "cannot find Setupdl.exe at $($SetupdlFilePath)" } } if (Test-Path -Path $LogFilePath) { Remove-Item -Path $LogFilePath -Force | Out-Null } try { Write-Output "$($env:COMPUTERNAME): starting download of configmgr setup prerequisite files" Start-Process -FilePath $CM_SetupFileDownloaderPath -ArgumentList "/NoUI $($Outpath)" -Wait -NoNewWindow } catch { throw "failed to run $($LogFilePath) - $($PSItem.Exception.Message)" } $Content = Get-Content -Path $LogFilePath $SuccessMessage = $Content -like "*INFO: Setup downloader * FINISHED*" if ($null -eq $SuccessMessage[0]) { throw "no success message, check log $($LogFilePath)" } Write-Output "$($env:COMPUTERNAME): finished download of configmgr setup files" } catch { throw "error downloading configmgr setup files - $($PSItem.Exception.Message)" } } function Get-CM_Setup_Volume { <# .Description this function searches all volumes for the setup.exe from the configmgr iso .Example # this will return the volume where confimgr setup is Uninstall-ConfigMgrAgent .NOTES #> try { $Volumes = Get-Volume | Where-Object -FilterScript { $PSItem.DriveLetter -NE "C" -and $null -ne $PSItem.DriveLetter } $Volumes | ForEach-Object { if (Test-Path -Path "$($PSItem.DriveLetter):\SMSSETUP\BIN\X64\setup.exe") { return $PSItem } } } catch { throw $PSItem.Exception.Message } } function Initialize-CM_Schema_To_AD { <# .Description this function extends the schema using extadsch.exe for the configmgr .Parameter ExtadschFilePath path to the extadsch.exe .Example # ad schema will be prepared for configmgr Initialize-CM_Schema_To_AD .NOTES should be run on the siteserver with domain admin privileges temporarily the current user is added to the schema admin group #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string] $ExtadschFilePath ) try { if (((Get-ADGroupMember -Identity 'Schema Admins').Name -eq $env:USERNAME) -ne $true) { Write-Output "$($env:COMPUTERNAME): adding current user to schema admins" Add-ADGroupMember -Identity 'Schema Admins' -Members $env:USERNAME } if ($ExtadschFilePath -eq "") { $CM_SetupVolumes = Get-CM_Setup_Volume if ($CM_SetupVolumes.DriveLetter.count -eq 1) { $CM_Extadsch_Filepath = "$(($CM_SetupVolumes).DriveLetter):\SMSSETUP\BIN\X64\extadsch.exe" } else { throw "there are more than one or less than one installation media for configmgr" } } else { if (Test-Path -Path $ExtadschFilePath) { $CM_Extadsch_Filepath = $ExtadschFilePath } else { throw "cannot find extadsch.exe at $($ExtadschFilePath)" } } Write-Output "$($env:COMPUTERNAME): extending schema" Start-Process -FilePath $CM_Extadsch_Filepath -Wait -NoNewWindow $LogFilePath = "$($env:SystemDrive)\ExtADSch.log" $LogFileContent = Get-Content -Path $LogFilePath $SuccessMessage = $LogFileContent -like "*Successfully extended the Active Directory schema.*" $FailedMessages = $LogFileContent -like "*Failed to create*" if ($null -ne $SuccessMessage[0]) { Write-Output "$($env:COMPUTERNAME): finished extending schema" } else { throw "something went wrong, check the log at $($LogFilePath):`n$($FailedMessages[0])" } if (((Get-ADGroupMember -Identity 'Schema Admins').Name -eq $env:USERNAME) -ne $true) { Write-Output "$($env:COMPUTERNAME): removing current user from schema admins" Remove-ADGroupMember -Identity 'Schema Admins' -Members $env:USERNAME -Confirm:$false } } catch { throw "error extending schema - $($PSItem.Exception.Message)" } } function Install-CM_SiteServer { <# .Description this function extends the schema using extadsch.exe for the configmgr .Parameter SetupPath path to the Setup.exe .Parameter SiteName FriendlyName of the Site .Parameter SiteCode sitecode .Parameter PrerequisitePath path to prerequisite files .Parameter SQLServer fqdn of the sql server, can be the local server .Parameter SQLInstanceName name of the instance .Example # this will start the installation of a site server Install-CM_SiteServer -SiteName $CM_SiteName ` -SiteCode $CM_SiteCode ` -PrerequisitePath $PrerequisitePath ` -SQLServer $CM_Site_SQLServer ` -SQLInstanceName $CM_SQL_Site_InstanceName .NOTES https://learn.microsoft.com/en-us/mem/configmgr/core/servers/deploy/install/command-line-options-for-setup #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [string] $SetupPath, [Parameter(Mandatory = $true)] [string] $SiteName, [Parameter(Mandatory = $true)] [string] $SiteCode, [Parameter(Mandatory = $true)] [string] $PrerequisitePath, [Parameter(Mandatory = $true)] [string] $SQLServer, [Parameter(Mandatory = $true)] [string] $SQLInstanceName ) try { $SiteServer = $($env:COMPUTERNAME + "." + $env:USERDNSDOMAIN) $SetupIniPath = "$($env:ProgramData)\NTS\ConfigMgr\SetupConfig.ini" $LogFilePath = "$($env:SystemDrive)\ConfigMgrSetup.log" $ConfigurationIni = "[Identification] Action=InstallPrimarySite CDLatest=0 [Options] ProductID=Eval SiteCode=$($SiteCode) SiteName=$($SiteName) SMSInstallDir=$($env:SystemDrive)\Program Files\Microsoft Configuration Manager SDKServer=$($env:COMPUTERNAME + "." + $env:USERDNSDOMAIN) PrerequisiteComp=1 PrerequisitePath=$($PrerequisitePath) AdminConsole=1 JoinCEIP=0 MobileDeviceLanguage=0 RoleCommunicationProtocol=HTTPorHTTPS ClientsUsePKICertificate=0 [SQLConfigOptions] SQLServerName=$($SQLServer + "\" + $SQLInstanceName) DatabaseName=$("CM_" + $SiteCode) [CloudConnectorOptions] CloudConnector=1 CloudConnectorServer=$($SiteServer) UseProxy=0 [SABranchOptions] SAActive=0 CurrentBranch=1 " New-Item -Path $SetupIniPath -ItemType File -Force | Out-Null Set-Content -Path $SetupIniPath -Value $ConfigurationIni Write-Output "$($env:COMPUTERNAME): starting configmgr site server installtion" Write-Output "$($env:COMPUTERNAME): to see the progress please view this log $($env:SystemDrive)\ConfigMgrSetup.log on the site server" Write-Output "$($env:COMPUTERNAME): this can take a while" if ($SetupPath -eq "") { $CM_SetupVolumes = Get-CM_Setup_Volume if ($CM_SetupVolumes.DriveLetter.count -eq 1) { Start-Process -FilePath "$(($CM_SetupVolumes).DriveLetter):\SMSSETUP\BIN\X64\setup.exe" -ArgumentList "/SCRIPT $($SetupIniPath)" -Wait -NoNewWindow } else { throw "there are more than one or less than one installation media for configmgr" } } else { if (Test-Path -Path $SetupPath) { Start-Process -FilePath $SetupPath -ArgumentList "/SCRIPT $($SetupIniPath)" -Wait -NoNewWindow } else { throw "cannot find the setup file at $($SetupPath)" } } $Content = Get-Content -Path $LogFilePath $SuccessMessage = $Content -like "*~===================== Completed Configuration Manager Server Setup =====================*" if ($null -eq $SuccessMessage[0]) { throw "no success message, check log $($LogFilePath)" } Write-Output "$($env:COMPUTERNAME): finished the configmgr site server installtion" } 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 #> $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" } } } function Add-SQLMountPoint { <# .Description this function creates a mountpoint .Parameter DiskNumber number of disk which should be configured .Parameter SQLBasePath where should be mountpoint be created .Parameter DiskLabel label for the volume and the underlaying folders .Example # this checks if hyper-v is installed Confirm-HyperV .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $DiskNumber, [Parameter(Mandatory = $false)] [string] $SQLBasePath = "C:\SQL\", [Parameter(Mandatory = $true)] [string] $DiskLabel ) $MountPath = $SQLBasePath + $DiskLabel Write-Output "$($env:COMPUTERNAME): formating disk for $($DiskLabel)" Set-Disk -Number $DiskNumber -IsOffline $false Set-Disk -Number $DiskNumber -IsReadOnly $false Get-Disk -Number $DiskNumber | Initialize-Disk -PartitionStyle GPT -PassThru | New-Partition -UseMaximumSize | Format-Volume -NewFileSystemLabel $DiskLabel -FileSystem ReFS -AllocationUnitSize 65536 -SetIntegrityStreams $false | Out-Null New-Item -ItemType Directory -Path $MountPath | Out-Null Get-Partition -DiskNumber $DiskNumber | Add-PartitionAccessPath -AccessPath $MountPath -ErrorAction SilentlyContinue if ($DiskLabel -like "*LOG*") { New-Item -ItemType Directory -Path "$($SQLBasePath)\$($DiskLabel)\LOG" -ErrorAction SilentlyContinue | Out-Null } else { New-Item -ItemType Directory -Path "$($SQLBasePath)\$($DiskLabel)\DATA" -ErrorAction SilentlyContinue | Out-Null } Start-Sleep -Seconds 2 } 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 ) Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential Stop-VM -Name $VMName -Force Write-Output "$($env:COMPUTERNAME): creating snapshot $($VMName) - $($SnapshotName)" Checkpoint-VM -Name $VMName -SnapshotName $SnapshotName Start-VM -Name $VMName Confirm-VMState -VMObject (Get-VM -Name $VMName) -VMCredential $VMCredential } function Test-RebootPending { ############################################################################### # Check-PendingReboot.ps1 # Andres Bohren / www.icewolf.ch / blog.icewolf.ch / a.bohren@icewolf.ch # Version 1.0 / 03.06.2020 - Initial Version - Andres Bohren # Version 1.1 / 27.04.2022 - Updated Script - Andres Bohren ############################################################################### <# .SYNOPSIS This Script checks diffrent Registry Keys and Values do determine if a Reboot is pending. .DESCRIPTION I found this Table on the Internet and decided to Write a Powershell Script to check if a Reboot is pending. Not all Keys are checked. But feel free to extend the Script. https://adamtheautomator.com/pending-reboot-registry-windows/ KEY VALUE CONDITION HKLM:\SOFTWARE\Microsoft\Updates UpdateExeVolatile Value is anything other than 0 HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager PendingFileRenameOperations value exists HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager PendingFileRenameOperations2 value exists HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired NA key exists HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Services\Pending NA Any GUID subkeys exist HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting NA key exists HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce DVDRebootSignal value exists HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending NA key exists HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootInProgress NA key exists HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\PackagesPending NA key exists HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts NA key exists HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon JoinDomain value exists HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon AvoidSpnSet value exists HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName ComputerName Value ComputerName in HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName is different .EXAMPLE ./Check-PendingReboot.ps1 #> function Test-RegistryValue { param ( [parameter(Mandatory = $true)][ValidateNotNullOrEmpty()]$Path, [parameter(Mandatory = $true)] [ValidateNotNullOrEmpty()]$Value ) try { Get-ItemProperty -Path $Path -Name $Value -EA Stop return $true } catch { return $false } } [bool]$PendingReboot = $false #Check for Keys If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") -eq $true) { # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" $PendingReboot = $true } If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting") -eq $true) { # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\PostRebootReporting" $PendingReboot = $true } If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") -eq $true) { # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" $PendingReboot = $true } If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") -eq $true) { # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" $PendingReboot = $true } If ((Test-Path -Path "HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts") -eq $true) { # Write-Host "HKLM:\SOFTWARE\Microsoft\ServerManager\CurrentRebootAttempts" $PendingReboot = $true } #Check for Values If ((Test-RegistryValue -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "RebootInProgress") -eq $true) { # Write-Host "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing > RebootInProgress" $PendingReboot = $true } If ((Test-RegistryValue -Path "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing" -Value "PackagesPending") -eq $true) { # Write-Host "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing > PackagesPending" $PendingReboot = $true } If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Value "PendingFileRenameOperations") -eq $true) { # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager > PendingFileRenameOperations" $PendingReboot = $true } If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Value "PendingFileRenameOperations2") -eq $true) { # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager > PendingFileRenameOperations2" $PendingReboot = $true } If ((Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce" -Value "DVDRebootSignal") -eq $true) { # Write-Host "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce > DVDRebootSignal" $PendingReboot = $true } If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon" -Value "JoinDomain") -eq $true) { # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon > JoinDomain" $PendingReboot = $true } If ((Test-RegistryValue -Path "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon" -Value "AvoidSpnSet") -eq $true) { # Write-Host "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon > AvoidSpnSet" $PendingReboot = $true } return $PendingReboot } 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-ConfigMgrTools" return (Test-RebootPending) } if ($RebootPending) { Write-Output "$($env:COMPUTERNAME): doing an reboot of $($VMName)" Restart-VM -VM $VM -Type Reboot -Force -Wait Confirm-VMState -VMObject $VM -VMCredential $Credentials } } function Build-VMHost { <# .Description this function checks if the vm has to reboot and does it, if needed .Parameter Course_Shortcut shortcut of the participant, used for the switchname .Parameter TrustedHostsValue value for trustedhosts to add, needed for configmgr things .Parameter VM_Drive_Letter letter for the vm volume, which will be created .Example # this prepared the host and creates vm switch, vm volume Build-VMHost -Course_Shortcut $Course_Shortcut -TrustedHostsValue "$($CM_Siteserver_NetBIOS),$($CM_Siteserver_FQDN)" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Course_Shortcut, [Parameter(Mandatory = $true)] [string] $TrustedHostsValue, [Parameter(Mandatory = $false)] [char] $VM_Drive_Letter = 'V' ) $Host_Preparations_Start = Get-Date try { Write-Output "`n$($env:COMPUTERNAME): prepare host" # configmgr Start-Service -Name "WinRM" Set-Item -Path "WSMan:\localhost\Client\TrustedHosts" -Value $TrustedHostsValue -Force -Concatenate # prepare host for vms New-VMVolume -VMDriveLetter $VM_Drive_Letter New-VMVSwitch -Course_Shortcut $Course_Shortcut # hyperv $VM_DefaultPath = "$($VM_Drive_Letter):\VMs" Set-VMHost -EnableEnhancedSessionMode $True -VirtualHardDiskPath $VM_DefaultPath -VirtualMachinePath $VM_DefaultPath } catch { throw "$($env:COMPUTERNAME): host preparations failed: $($PSItem.Exception.Message)" } $Host_Preparations_Duration = (Get-Date) - $Host_Preparations_Start Write-Output "$($env:COMPUTERNAME): host preparation took $($Host_Preparations_Duration.Hours)h $($Host_Preparations_Duration.Minutes)m $($Host_Preparations_Duration.Seconds)s" } function Deploy-VMs { <# .Description this function checks if the vm has to reboot and does it, if needed .Parameter VM_Config_Obj Object that contains multiple descriptive objects for deployment from a VM. .Parameter Course_Shortcut shortcut of the participant .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 .Example # this does a restart of the vm if needed Deploy-VMs -VM_Config_Obj $VM_Config -Course_Shortcut $Course_Shortcut -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials -VM_Credentials $VM_Credentials .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [hashtable] $VM_Config_Obj, [Parameter(Mandatory = $true)] [string] $Course_Shortcut, [Parameter(Mandatory = $true)] [string] $CM_Siteserver_FQDN, [Parameter(Mandatory = $true)] [pscredential] $CM_Credentials, [Parameter(Mandatory = $true)] [pscredential] $VM_Credentials ) $VM_Deployment_Start = Get-Date try { Write-Output "`n$($env:COMPUTERNAME): starting vm deployments" New-VMs_Objectbased -VM_Config $VM_Config_Obj -Course_Shortcut $Course_Shortcut Write-Output "`n$($env:COMPUTERNAME): starting vm registration in configmgr" Register-VM_in_CM -VM_Config_Obj $VM_Config_Obj -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials Write-Output "`n$($env:COMPUTERNAME): starting vms" Start-VM_Deployment -VM_Config_Obj $VM_Config_Obj Write-Output "`n$($env:COMPUTERNAME): validating vm deployments" Confirm-VM_Deployment -VM_Config_Obj $VM_Config_Obj -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials Write-Output "finished vm deployments - doing cleanup in configmgr" Remove-VMConfigMgrDeployment -VM_Config_Obj $VM_Config_Obj -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials } catch { throw "$($env:COMPUTERNAME): error deploying vms: $($PSItem.Exception.Message)" } $VM_Config_Obj.Keys | Sort-Object | ForEach-Object { Set-VMInstallSnapshot -VMName $PSItem -SnapshotName "$(Get-Date -format "yyyy-MM-dd_HH.mm.ss") - initial deployment" -VMCredential $VM_Credentials } $VM_Deployment_Duration = (Get-Date) - $VM_Deployment_Start Write-Output "$($env:COMPUTERNAME): vm deployment took $($VM_Deployment_Duration.Hours)h $($VM_Deployment_Duration.Minutes)m $($VM_Deployment_Duration.Seconds)s" } function Remove-VMConfigMgrDeployment { <# .Description Removes the ConfigMgr Object related to the vms .Parameter VM_Config_Obj Object that contains multiple descriptive objects for deployment from a VM. .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 # Removes all vm objects in configmgr Remove-VMConfigMgrDeployment -VM_Config_Obj $VM_Config -CM_Siteserver_FQDN $CM_Siteserver_FQDN -CM_Credentials $CM_Credentials .NOTES the VM_Config_Obj object, should be like ` `$VM_Config_Obj = @{} `$VM_01 = @{ Name = '`$Course_Shortcut)-VWIN11-`$Participant_Number)1' RAM = `$RAM CPU = `$CPUCount CM_Collection_Name = `$CM_Collection_W11_Autopilot Credentials = `$VM_Credentials DiskSize = `$DynamicDiskSize MAC = "" } #> [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [hashtable] $VM_Config_Obj, [Parameter(Mandatory = $true)] [string] $CM_Siteserver_FQDN, [Parameter(Mandatory = $true)] [PSCredential] $CM_Credentials ) Invoke-Command -ComputerName $CM_Siteserver_FQDN -Credential $CM_Credentials -ScriptBlock { $VM_Config_Obj = $using:VM_Config_Obj $PSDriveName = "CM-Temp-Drive" # load configmgr ps module Import-Module -Name ConfigurationManager New-PSDrive -Name $PSDriveName -PSProvider "CMSite" -Root $using:CM_Siteserver_FQDN -Description "Primary site" | Out-Null Set-Location -Path "$($PSDriveName):\" # remove collections membership rules $CM_SiteCode = (Get-CMSite).SiteCode $VM_Config_Obj.Keys | ForEach-Object { $Temp_CMDevice = Get-CMDevice -Name $PSItem $TargetCollection = Get-CMCollection -Name $($VM_Config_Obj.$PSItem.CM_Collection_Name) 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) { Write-Output "$($Temp_CMDevice.Name): removing collection membership in $((Get-CMCollection -Id $PSItem.CollectionID).Name) after the deployment" Remove-CMDeviceCollectionDirectMembershipRule -CollectionId $PSItem.CollectionID -ResourceId $MembershipRule.ResourceId -Confirm:$false -Force } } } } } # removing device object after the deployment $VM_Config_Obj.Keys | Sort-Object | ForEach-Object { $Temp_CMDevice = Get-CMDevice -Name $PSItem if ($null -ne $Temp_CMDevice) { Write-Output "$($PSItem): removing computer info in configmgr after the deployment" Remove-CMDevice -Name $PSItem -Force -Confirm:$false } } } } 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 ) $VM = Get-VM -Name $VMName $CurrentVMDiskCount = (Get-VHD -VMId $VM.Id).count $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) { Add-VMDisk -VMName $VM.Name -VHDXPath "$($VM.ConfigurationLocation)\$($VM.Name)-$($CurrentVMDiskCount).vhdx" -VHDXSize $VolumeSize Invoke-Command -VMName $VM.Name -Credential $VMCredential -ScriptBlock { Write-Output "$($env:COMPUTERNAME): formating disks" $PhysicalDisk = Get-PhysicalDisk | Where-Object -Property Size -eq $using:VolumeSize New-Volume -DiskNumber $PhysicalDisk.DeviceId -FriendlyName $using:VolumeFriendlyName -FileSystem NTFS -DriveLetter $using:VolumeDriveLetter | Out-Null } } } function Get-ExchangeCU { <# .Description downloads exchange cu from microsoft .Parameter Version version of the cu .Parameter Outpath path where the cu is stored .Example # stores the cu to $Outpath Get-ExchangeCU -Version 2019_CU12 -Outpath $Outpath .NOTES https://learn.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019 #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidateSet("2019_CU12", "2016_CU23")] [string] $Version, [Parameter(Mandatory = $true)] [string] $Outpath ) $ErrorActionPreference = 'Stop' if ($Outpath[-1] -eq "\") { $Outpath = $Outpath.Substring(0, $Outpath.Length - 1) } if ((Test-Path -Path $Outpath) -eq $false) { New-Item -Path $Outpath -Force -ItemType Directory | Out-Null } $ExchangeCUFilePath = "$($Outpath)\Exchange-$($Version).iso" switch ($Version) { "2019_CU12" { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=104131" } "2016_CU23" { $DownloadURL = "https://www.microsoft.com/en-us/download/confirmation.aspx?id=104132" } Default { throw "no version was selected or not supported" } } try { $Content = Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL $UpdateLink = ($Content.Links | Where-Object -FilterScript { $PSItem.href -like "*download.microsoft.com*" -and $PSItem.outerHTML -like "*download manually*" }).href Start-FileDownload -DownloadURL $UpdateLink -FileOutPath $ExchangeCUFilePath } catch { throw "$($env:COMPUTERNAME): error getting exchange cu files - $($PSItem.Exception.Message)" } Write-Output "$($env:COMPUTERNAME): finished download - check folder $($Outpath)" } function Install-ExchangePrerequisites { <# .Description use this function to install exchange windows feature prerequesits .Example # install exchange windows feature prerequesits Install-ExchangePrerequisites .NOTES https://learn.microsoft.com/en-us/exchange/plan-and-deploy/prerequisites?view=exchserver-2019 #> $Features = @( "Server-Media-Foundation" "NET-Framework-45-Features" "RPC-over-HTTP-proxy" "RSAT-Clustering" "RSAT-Clustering-CmdInterface" "RSAT-Clustering-Mgmt" "RSAT-Clustering-PowerShell" "WAS-Process-Model" "Web-Asp-Net45" "Web-Basic-Auth" "Web-Client-Auth" "Web-Digest-Auth" "Web-Dir-Browsing" "Web-Dyn-Compression" "Web-Http-Errors" "Web-Http-Logging" "Web-Http-Redirect" "Web-Http-Tracing" "Web-ISAPI-Ext" "Web-ISAPI-Filter" "Web-Lgcy-Mgmt-Console" "Web-Metabase" "Web-Mgmt-Console" "Web-Mgmt-Service" "Web-Net-Ext45" "Web-Request-Monitor" "Web-Server" "Web-Stat-Compression" "Web-Static-Content" "Web-Windows-Auth" "Web-WMI" "Windows-Identity-Foundation" "RSAT-ADDS" ) try { Write-Output "$($env:COMPUTERNAME): installing required features for exchange" Install-WindowsFeature -Name $Features | Out-Null } catch { throw $PSItem.Exception.Message } } function Initialize-ADForExchange { <# .Description used the exchange setup.exe to prepare ad for exchange installation or upgrade .Parameter SetupPath path to the exchange setup.exe .Parameter OrganizationName name of the exchange organization .Example # prepares active directory for the organization mw corp org Initialize-ADForExchange -SetupPath "$($Volume.DriveLetter):\Setup.exe" -OrganizationName "mw corp org" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $SetupPath, [Parameter(Mandatory = $true)] [string] $OrganizationName ) if (Test-Path -Path $SetupPath) { Write-Output "$($env:COMPUTERNAME): preparing ad - check logs at $($env:SystemDrive)\ExchangeSetupLogs for more details" if ($OrganizationName -eq "") { $Process = Start-Process -FilePath $SetupPath -ArgumentList "/PrepareAD /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF" -Wait -NoNewWindow -PassThru } else { $Process = Start-Process -FilePath $SetupPath -ArgumentList "/PrepareAD /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /OrganizationName:`"$($OrganizationName)`"" -Wait -NoNewWindow -PassThru } if ($Process.ExitCode -ne 0) { $Content = Get-Content -Path "$($env:SystemDrive)\ExchangeSetupLogs\ExchangeSetup.log" $Message = ($Content | Select-String -Pattern "ERROR")[-1].ToString() throw "error preparing ad - $($Message)" } Write-Output "$($env:COMPUTERNAME): finished preparing ad" } else { throw "cannot find $($SetupPath)" } } function Initialize-SchemaForExchange { <# .Description used the exchange setup.exe to prepare schema of ad for exchange installation or upgrade .Parameter SetupPath path to the exchange setup.exe .Example # prepares active directory schema for exchange install Initialize-SchemaForExchange -SetupPath "$($Volume.DriveLetter):\Setup.exe" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $SetupPath ) if (Test-Path -Path $SetupPath) { Write-Output "$($env:COMPUTERNAME): preparing schema - check logs at $($env:SystemDrive)\ExchangeSetupLogs for more details" $Process = Start-Process -FilePath $SetupPath -ArgumentList "/PrepareSchema /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF" -Wait -NoNewWindow -PassThru if ($Process.ExitCode -ne 0) { $Content = Get-Content -Path "$($env:SystemDrive)\ExchangeSetupLogs\ExchangeSetup.log" $Message = ($Content | Select-String -Pattern "ERROR")[-1].ToString() throw "error preparing schema - $($Message)" } Write-Output "$($env:COMPUTERNAME): finished preparing schema" } else { throw "cannot find $($SetupPath)" } } function Install-Exchange { <# .Description used the exchange setup.exe install exchange on the local server .Parameter SetupPath path to the exchange setup.exe .Example # installs exchange on the local server Install-Exchange -SetupPath "$($Volume.DriveLetter):\Setup.exe" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $SetupPath ) if (Test-Path -Path $SetupPath) { Write-Output "$($env:COMPUTERNAME): installing exchange - check logs at $($env:SystemDrive)\ExchangeSetupLogs for more details" $Process = Start-Process -FilePath $SetupPath -ArgumentList "/m:install /roles:mb /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF /InstallWindowsComponents" -Wait -NoNewWindow -PassThru if ($Process.ExitCode -ne 0) { $Content = Get-Content -Path "$($env:SystemDrive)\ExchangeSetupLogs\ExchangeSetup.log" $Message = ($Content | Select-String -Pattern "ERROR")[-1].ToString() throw "error installing exchange - $($Message)" } Write-Output "$($env:COMPUTERNAME): finished installing exchange" } else { throw "cannot find $($SetupPath)" } } function Start-FileDownload { <# .Description this function can be used to download files, but also checks if the destination has already the file .Parameter DownloadURL url of the source .Parameter FileOutPath path where the file should be saved, with extension .Parameter MaxAgeOfFile maximum file modification date .Example # downloads the file Start-FileDownload -DownloadURL "https://www.microsoft.com/en-us/download/confirmation.aspx?id=104131" -FileOutPath "$($Outpath)\Exchange-$($Version).iso" .NOTES #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $DownloadURL, [Parameter(Mandatory = $true)] [string] $FileOutPath, [Parameter(Mandatory = $false)] [datetime] $MaxAgeOfFile = (Get-Date).AddHours(-2) ) # verify folder try { $FileName = $FileOutPath.Split("\")[-1] $FolderName = $FileOutPath.replace($FileName, "") if ((Test-Path -Path $FolderName) -eq $false) { New-Item -Path $FolderName -ItemType Directory -Force | Out-Null } } catch { throw "error creating dest folder - $($PSItem.Exception.Message)" } # download try { if ((Test-Path -Path $FileOutPath) -eq $true) { if ((Get-Item $FileOutPath).LastWriteTime -gt $MaxAgeOfFile) { Write-Output "$($env:COMPUTERNAME): found $($FileOutPath), will use it" } else { Write-Output "$($env:COMPUTERNAME): found $($SetupName) at $($FileOutPath), removing the files because too old" Remove-Item -Path $FileOutPath -Recurse -Force | Out-Null } } Write-Output "$($env:COMPUTERNAME): downloading from $($DownloadURL) to $($FileOutPath)" $ProgressPreference = "SilentlyContinue" Invoke-WebRequest -UseBasicParsing -Uri $DownloadURL -OutFile $FileOutPath $ProgressPreference = "Continue" Write-Output "$($env:COMPUTERNAME): download finished" } catch { throw "error downloading - $($PSItem.Exception.Message)" } } |