VDI-GoldenImage.ps1
<#PSScriptInfo
.VERSION 1.6.1 .GUID d25f8ad6-47fc-4b0c-a239-74ef3be16d2c .AUTHOR FaraJan .COMPANYNAME Data Protection Delivery Center s.r.o. .COPYRIGHT 2024 DPDC CZ. All rights reserved. .LICENSEURI https://mit-license.org .TAGS VDI VMware Horizon Golden Image MasterPC Tools DynamicEnvironmentManager AppVolumes Microsoft Teams FSLogix OneDrive GoogleDrive OSOT automation maintenance finalize .PROJECTURI .RELEASENOTES [v1.6 - 20240430] New option to save configuration to a standalone file 'VDI-GoldenImage.ps1.config' or specify file by 'ConfigFile' parameter Added task to install/update New Microsoft Teams with downloading of latest version from web or offline MSIX Added task to install/update Google Drive with downloading of latest version from web [v1.5 - 20231220] Added optional param DisableSpoolerRestart to App Volumes [v1.4 - 20231130] Revisions and Bug Fixes [v1.3 - 20231107] Added task to install/update Microsoft FSLogix & OneDrive with downloading of latest version from web [v1.2 - 20231027] Added task to install/update Horizon Agent, VMware Dynamic Environment Manager and App Volumes [v1.1 - 20230926] Added task to install/update Microsoft Teams for VDI with downloading of latest version from web [v1.0 - 20230906] First version with management of OS/Office updates, OSOT finalize action and VM Tools install/update task #> <# .SYNOPSIS Maintenance script for easier programmable management of VDI Golden Image (Master PC) running VMware Horizon .DESCRIPTION Script with basic sets of maintenance task for VDI Golden Image running on VMware Horizon. Typical tasks include updating OS, Office and other software as well as finalizing and cleaning up the image before final snapshot and deployment to virtual desktops. Other tasks are installing/updating agents for running VDI (Horizon, Dynamic Environment manager, App Volumes, FSlogix, MS Teams for VDI, OneDrive and Google Drive). .NOTES Version: 1.6 Author: Fara Jan Creation Date: 2023-09-06 Last Update: 2024-04-30 .PARAMETER Action Specifies the action/task of the script. If not specified a menu with a list of actions is displayed. .INPUTS System.String or Empty .OUTPUTS System.String Console/Log .EXAMPLE # Run script and display menu list of all script actions powershell C:\ProgramData\VDI\VDI-GoldenImage.ps1 .EXAMPLE # Enable and runs OS/Office/SW updates in Golden Image PS> VDI-GoldenImage.ps1 -Action Update .EXAMPLE # Disable OS/Office/SW updates, runs system clean-up tasks and prepare Golden Image for Horizon PS> VDI-GoldenImage.ps1 -Action Finalize .EXAMPLE # Install/update VM Tools in Golden Image - comparing current version and version in InstallSrcDir PS> VDI-GoldenImage.ps1 -Action VmTools .EXAMPLE # Install/update Microsoft Teams for VDI - comparing current version and version in InstallSrcDir or latest online version PS> VDI-GoldenImage.ps1 -Action MsTeams .NOTES Recommended location for script: C:\ProgramData\VDI #> #----------------[ Script param ]---------------- param ( [Parameter(Mandatory=$false)] [ValidateSet("Update", "Finalize", "FinalizeFast", "Defrag", "VmTools", "Horizon", "DEM", "AppVolumes", "FSLogix", "MsTeams", "MsTeamsNEW", "OneDrive", "GoogleDrive", "CfgInfo", "Exit")] [string] $Action, [Parameter(Mandatory=$false)] [string] $ConfigFile ) #----------------[ Settings ]---------------- $VAR = @{ # Script Name ScriptName = "VDI Golden Image Maintenance" # Script Path ($PSScriptRoot for CurrentPath) ScriptPath = $PSScriptRoot # Script Menu Title ScriptMenuTitle = "Please select a script action" # --- VDI Image - Updates Settings --- # Windows/Office (365/2019) updates managed/controlled by this script - disabled by default & run only during Golden Image maintenance/update ManageWindowsUpdates = $true ManageOfficeUpdates = $true # Other updates Settings (Web browser and other SW updates) ManageMsEdgeUpdate = $true ManageGoogleUpdates = $true ManageAdobeUpdates = $true ManageOneDriveUpdates = $true # --- Finalize Settings (VMware OSOT) --- # OSOT info web: https://techzone.vmware.com/resource/windows-os-optimization-tool-vmware-horizon-guide # Path to the OSOT executable file (automatic getting of latest OSOT version) OsotPath = "C:\Program Files\VMware OSOT" # Finalize Argument Settings OsotFinalizeArg = "-v -f 0 1 2 3 4 5 9 10 11" #All excepts: '(6) Clears Default user profile', '(7) Zero empty disk space', '(8) Creates local group policies' #OsotFinalizeArg = "-v -f 3 4" #demo # Finalize Argument Settings for Fast cleanup OsotFinalizeArgFast = "-v -f 3 4 9 10 11" # Disk Cleanup, EventLog Cleanup, Clears KMS Settings, Flush DNS cache, Releases IP Address # Shutdown VM after Finalize OsotShutdownAfterFinalize = $true OsotShutdownAfterFinalizeFast = $true # SCCM: Clear Configuration during finalize (eliminate duplicates) SccmClearConfig = $false # --- VM Tools Settings --- # Enable Carbon Black Helper VmToolsCarbonBlack = $false # Enable NSX Guest Instrospection Drivers VmToolsNsxIntrospection = $false # --- Horizon Agent Install options --- # Install Options DOC: https://docs.vmware.com/en/VMware-Horizon/2309/virtual-desktops/GUID-3096DA8B-034B-435B-877E-5D2B18672A95.html # Horizon Agent Features, if HorizonAgentAddLocal = ALL then HorizonAgentRemove is also used HorizonAgentAddLocal = "ALL" #"Core,PCoIP,USB,NGVC,RTAV,ClientDriveRedirection,GEOREDIR,V4V,VmwVaudio,VmwVidd,TSMMR,BlastUDP,PerfTracker,HelpDesk,PrintRedir,PSG" HorizonAgentRemove = "SerialPortRedirection,ScannerRedirection,SmartCard,SdoSensor" # Delete Horizon Peformance Tracker Icon from all users desktop DelPerfTrackerDesktopIcon = $true # Increasing a default timeout limit for Post-Sync/ClonePrep Customization Scripts (default 20000 ms = 20 s) HorizonAgentExecScriptTimeout = 90000 # --- VMware DEM Config share Path (Empty if no VMware DEM) --- DemConfigPath = "\\domain.int\VDI$\DEMConfig\general" # --- VMware App Volumes Agent Configuration --- # AppVol Manager(s) (DNS hostname/IP) - multiple array items for HA configuration; Empty Array if No App Volumes AppVolManager = @("vdi-avm01.domail.local", "vdi-avm02.domail.local") # Deactivate Restarting the Spooler Service When Using VMware Integrated Printing: https://docs.vmware.com/en/VMware-App-Volumes/2309/app-volumes-admin-guide/GUID-B7CABC0E-9313-4BA8-A718-569E5665D4E9.html AppVolDisableSpoolerRestart = $true # The maximum wait for a response from the AppVol Manager, in seconds. If set to 0, the wait for response is forever (default 300 sec = 5 min) AppVolMaxDelayTimeOutS = 30 # --- Microsoft Teams Settings --- # Install New MS Teams from offline MSIX (stored in InstallSrcDir) MsTeamsOfflineMSIX = $false MsTeamsDisableAutoUpdate = $true MsTeamsDisableAutoStart = $false # --- Google Drive Settings --- GoogleDriveDesktopShortcuts = $false GoogleDriveGSuiteShortcuts = $true # --- VDI Agents Install Source Directory (VM Tools, Horizon/DEM/AppVol Agent, MS Teams, ...)--- InstallSrcDir = "Install" # relative path to $_.ScriptPath # --- LOG settings --- LogDir = "Logs" # relative path to $_.ScriptPath LogFileName = "VDI-GI-Maintenance-{0:yyyyMMdd_HHmmss}.txt" LogArchiveFiles = 14 } # --- Script Main Menu --- $MENU = @() $menuStr1 = $menuStr2 = "" $menuStrDownload = " (with downloading the installation from the Internet)" if($VAR.ManageWindowsUpdates -or $VAR.ManageOfficeUpdates){ $menuStr1 = "Update: Enable & runs updates for " $menuStr1 += if($VAR.ManageWindowsUpdates -and $VAR.ManageOfficeUpdates){ "Windows and Office" } elseif($VAR.ManageWindowsUpdates){ "Windows" } else{ "Office" } $menuStr2 = "Disable " $menuStr2 += if($VAR.ManageWindowsUpdates -and $VAR.ManageOfficeUpdates){ "Windows and Office" } elseif($VAR.ManageWindowsUpdates){ "Windows" } else{ "Office" } $menuStr2 += " updates & " } if($menuStr1 -notlike ""){ $MENU += $menuStr1 } $MENU += "Finalize: $($menuStr2)Runs system clean-up tasks and prepare Golden Image for Horizon" $MENU += "FinalizeFast: Runs clean-up tasks without NGEN optimization, DISM cleanup and without CompactOS (faster)" $MENU += "Defrag: Disk defragmentation of the Golden Image" $MENU += "VmTools: Install/Update VMware VM Tools" $MENU += "Horizon: Install/Update VMware Horizon Agent" if($VAR.DemConfigPath -notlike ""){ $MENU += "DEM: Install/Update VMware Dynamic Environment Agent" } if($VAR.AppVolManager.Count){ $MENU += "AppVolumes: Install/Update VMware AppVolumes Agent" } $MENU += "FSLogix: Install/Update Microsoft FSLogix Agent" + $menuStrDownload $MENU += "MsTeams: Install/Update Microsoft Teams for VDI" + $menuStrDownload $MENU += "MsTeamsNEW: Install/Update Microsoft Teams for VDI (NEW)" + $menuStrDownload $MENU += "OneDrive: Install/Update Microsoft OneDrive" + $menuStrDownload $MENU += "GoogleDrive: Install/Update Google Drive" + $menuStrDownload $MENU += "CfgInfo: Show script configuration details" $MENU += "Exit" # --- Settings for SW Install/Update (SW Name, SW Source Install File Name, Installation Log File Name, Ask for install of newer version) --- $SwSet = @{ VmTools = @{Name = "VMware Tools"; SrcFile = "VMware-tools-*-x86_64.exe"; LogFile = "Log_VmTools_install_{0:yyyyMMdd_HHmmss}.txt"; AskIfNewer = $true} Horizon = @{Name = "VMware Horizon Agent"; SrcFile = "VMware-Horizon-Agent-x86_64-*.exe"; LogFile = "Log_HorizonAgent_install_{0:yyyyMMdd_HHmmss}.txt"; AskIfNewer = $true} DEM = @{Name = "VMware Dynamic Environment Manager"; SrcFile = "VMware Dynamic Environment Manager*x64.msi"; LogFile = "Log_DEMAgent_install_{0:yyyyMMdd_HHmmss}.txt"; AskIfNewer = $true} AppVolumes = @{Name = "App Volumes Agent"; SrcFile = "App Volumes Agent*.msi"; LogFile = "Log_AppVolAgent_install_{0:yyyyMMdd_HHmmss}.txt"; AskIfNewer = $true} FSLogix = @{Name = "Microsoft FSLogix Apps"; SrcFile = "FSLogix_Apps_*.zip"; LogFile = "Log_MsFSLogix_install_{0:yyyyMMdd_HHmmss}.txt"; AskIfNewer = $true} MsTeams = @{Name = "Microsoft Teams for VDI"; SrcFile = "Teams_windows_x64.msi"; LogFile = "Log_MsTeams_{1}install_{0:yyyyMMdd_HHmmss}.txt"; AskIfNewer = $true} MsTeamsNEW = @{Name = "Microsoft Teams for VDI NEW"; SrcFile = "teamsbootstrapper.exe"; LogFile = "Log_MsTeamsNew_install_{0:yyyyMMdd_HHmmss}.txt"; AskIfNewer = $true} OneDrive = @{Name = "Microsoft OneDrive"; SrcFile = "OneDriveSetup.exe"; LogFile = ""; AskIfNewer = $true} GoogleDrive = @{Name = "Google Drive"; SrcFile = "GoogleDriveSetup.exe"; LogFile = ""; AskIfNewer = $true} } # --- Windows/Office Update Registry Settings Parameters --- $regArr = @() $regDelTag = "***DELETE***" # Windows Update if($VAR.ManageWindowsUpdates){ $regPath = "HKLM:\\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" $regArr += [pscustomobject] @{Path = $regPath; Name = "BranchReadinessLevel"; PropertyType = "Dword"; ValueOn = 16; ValueOff = "32" } $regArr += [pscustomobject] @{Path = $regPath; Name = "DeferQualityUpdatesPeriodInDays"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 30 } $regArr += [pscustomobject] @{Path = $regPath; Name = "DisableWindowsUpdateAccess"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 } $regArr += [pscustomobject] @{Path = $regPath; Name = "DoNotConnectToWindowsUpdateInternetLocations"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 } $regArr += [pscustomobject] @{Path = $regPath; Name = "PauseFeatureUpdatesStartTime"; PropertyType = "String"; ValueOn = $("{0:yyyy-MM-dd}" -f (Get-Date)); ValueOff = "2021-10-15" } $regArr += [pscustomobject] @{Path = $regPath; Name = "PauseQualityUpdatesStartTime"; PropertyType = "String"; ValueOn = "2015-12-31"; ValueOff = "2021-10-31" } $regArr += [pscustomobject] @{Path = $regPath; Name = "SetDisableUXWUAccess"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 } $regPath += "\AU" $regArr += [pscustomobject] @{Path = $regPath; Name = "AllowMUUpdateService"; PropertyType = "Dword"; ValueOn = 1; ValueOff = $regDelTag } $regArr += [pscustomobject] @{Path = $regPath; Name = "AUOptions"; PropertyType = "Dword"; ValueOn = 4; ValueOff = $regDelTag } $regArr += [pscustomobject] @{Path = $regPath; Name = "NoAutoUpdate"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 } $regArr += [pscustomobject] @{Path = $regPath; Name = "ScheduledInstallDay"; PropertyType = "Dword"; ValueOn = 0; ValueOff = $regDelTag } $regArr += [pscustomobject] @{Path = $regPath; Name = "ScheduledInstallEveryWeek"; PropertyType = "Dword"; ValueOn = 1; ValueOff = $regDelTag } $regArr += [pscustomobject] @{Path = $regPath; Name = "ScheduledInstallTime"; PropertyType = "Dword"; ValueOn = 3; ValueOff = $regDelTag } } # Office Update if($VAR.ManageOfficeUpdates){ $regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Office\16.0\Common\OfficeUpdate" $regArr += [pscustomobject] @{Path = $regPath; Name = "EnableAutomaticUpdates"; PropertyType = "Dword"; ValueOn = 1; ValueOff = 0 } $regArr += [pscustomobject] @{Path = $regPath; Name = "HideEnableDisableUpdates"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 } } # MsTeams NEW Update if($VAR.MsTeamsDisableAutoUpdate){ $regPath = "HKLM:\SOFTWARE\Microsoft\Teams" $regArr += [pscustomobject] @{Path = $regPath; Name = "disableAutoUpdate"; PropertyType = "Dword"; ValueOn = 1; ValueOff = 0 } } # --- Windows Service Settings --- $svcArr = @() if($VAR.ManageWindowsUpdates){ $svcArr += [pscustomobject] @{Name = "wuauserv"; StartupTypeOn = "Manual"; SvcActionOn = "Start"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Windows Update $svcArr += [pscustomobject] @{Name = "UsoSvc"; StartupTypeOn = "Manual"; SvcActionOn = "Start"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Update Orchestrator Service $svcArr += [pscustomobject] @{Name = "StorSvc"; StartupTypeOn = "Automatic"; SvcActionOn = "Start"; StartupTypeOff = "Manual"; SvcActionOff = "Stop" } # Storage Service if($VAR.AppVolManager.Count){ $svcArr += [pscustomobject] @{Name = "svservice"; StartupTypeOn = "Disabled"; SvcActionOn = "Stop"; StartupTypeOff = "Automatic"; SvcActionOff = "Start" } # AppVolumes $svcArr += [pscustomobject] @{Name = "svdriver"; StartupTypeOn = "Disabled"; SvcActionOn = "Stop"; StartupTypeOff = "Automatic"; SvcActionOff = "Start" } # AppVolumes } } if($VAR.ManageGoogleUpdates){ $svcArr += [pscustomobject] @{Name = "gupdate"; StartupTypeOn = "Manual"; SvcActionOn = "None"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Google update service $svcArr += [pscustomobject] @{Name = "gupdatem"; StartupTypeOn = "Manual"; SvcActionOn = "None"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Google update service } if($VAR.ManageAdobeUpdates){ $svcArr += [pscustomobject] @{Name = "AdobeARMservice"; StartupTypeOn = "Manual"; SvcActionOn = "Start"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" } # Adobe Reader update service } # --- Array of Scheduled tasks to disable during finalize --- $schtaskArr = @() if($VAR.ManageMsEdgeUpdate){ $schtaskArr += "MicrosoftEdgeUpdateTaskMachineCore" $schtaskArr += "MicrosoftEdgeUpdateTaskMachineUA" } if($VAR.ManageGoogleUpdates){ $schtaskArr += "GoogleUpdateTaskMachineCore*" $schtaskArr += "GoogleUpdateTaskMachineUA*" } if($VAR.ManageAdobeUpdates){ $schtaskArr += "Adobe Acrobat Update Task" } if($VAR.ManageOneDriveUpdates){ $schtaskArr += "OneDrive Per-Machine Standalone Update Task" } # -------- Clear-Host # -------- #----------------[ Declarations ]---------------- $InstallSrcDir = $VAR.ScriptPath + "\" + $VAR.InstallSrcDir $LogDir = $VAR.ScriptPath + "\" + $VAR.LogDir $LogFile = $LogDir + "\" + $VAR.LogFileName -f (Get-Date) #----------------[ Initilization ]---------------- # Install source & Log folder if(!(Test-Path $InstallSrcDir -PathType Container)){ New-Item -Path $InstallSrcDir -ItemType Directory | out-null } if(!(Test-Path $LogDir -PathType Container)){ New-Item -Path $LogDir -ItemType Directory | out-null } #----------------[ Logging ]---------------- # Transcript Log - Start if($Host.Name -match "ConsoleHost"){ Start-Transcript -Path $LogFile -append } #----------------[ Functions ]------------------ # Write Empty Lines Function EmptyLines(){ Param ( [int] $lines = 1) for($i=1; $i -le $lines; $i++){ Write-Host "`r`n" } } # Script Messages FCE # Write the message to the console output and also add it to the body of the email (global VAR) # Param $StripHtml to clear HTML formating to console output # Param $NoAddToEmailBody to skip concat message with global variable $VAR.EmailBody Function MsgFce(){ param ( [Parameter(Position=0,Mandatory=$True)] [string] $Msg, [ValidateSet("host","warn","verbose","return")] [string] $Output="host", [switch] $StripHtml, [switch] $NoAddToEmailBody ) $Msg0 = if($StripHtml){ $Msg -replace "<[^>]*?>|<[^>]*>" } else { $Msg } if(($Script:VAR.EmailBody -is [array]) -and !$NoAddToEmailBody){ $Script:VAR.EmailBody += $Msg } if($Output -ilike "host"){ Write-Host $Msg0 } elseif($Output -ilike "warn"){ Write-Warning -Message $Msg0 -Verbose } elseif($Output -ilike "verbose"){ Write-Verbose -Message $Msg0 -Verbose } elseif($Output -ilike "return"){ $Msg0 = "`r`n" + $Msg0 return $Msg0 } } # Simple choice menu FCE # Writes an output of array items to select # Example of use: $MenuItems = @("Yes", "No"); $Title = "Contine?" function MenuSimple(){ Param( [Parameter(Position=0, Mandatory=$True)] [string[]] $MenuItems, [string] $Title, [boolean] $Cls ) $header = $null if(![string]::IsNullOrWhiteSpace($Title)) { $len = [math]::Max(($MenuItems | Measure-Object -Maximum -Property Length).Maximum, $Title.Length) $header = "{0}{1}{2}" -f $Title, [Environment]::NewLine, ("-" * $len) } # menu items and space align if more than 9 items $len = if($MenuItems.Count -gt 9){ 2 } else { 1 } $items = ($MenuItems | ForEach-Object{ "[{0}]{1}{2}" -f ++$i, $(if($i -lt 10){" " * $len} else{" "}), $_ }) -join [Environment]::NewLine # display the menu and return the chosen option while($true){ if($Cls){ Clear-Host } else { Write-Host } if($header){ Write-Host $header -ForegroundColor Yellow } Write-Host $items Write-Host $index = (Read-Host -Prompt 'Please make your choice') $index = $index -as [int] if((1..$MenuItems.Count) -contains $index){ return $MenuItems[$index-1] } else { Write-Warning "Invalid choice.. Please try again." Start-Sleep -Seconds 2 } } } # Helper function get action name from script menu function GeMenuAction(){ Param( [Parameter(Position=0, Mandatory=$True)] [string] $MenuSel ) return $($MenuSel.Split(":")[0]) } # Registry settings FCE for enable/disable updates (enable = ValueOn / disable = ValueOff) # Example of param: [pscustomobject] @{Path = ""; Name = ""; PropertyType = "Dword/String/..."; ValueOn = ; ValueOff = } Function SetRegistry(){ param ( [Parameter(Mandatory=$true)] [array] $RegArr, [Parameter(Mandatory=$true)] [ValidateSet("On","Off")] [string] $UpdateAction ) $regValueKey = "Value$($UpdateAction)" foreach($reg in $RegArr){ if(!(Test-Path $reg.Path)){ New-Item -Path $reg.Path -Force | Out-Null } # $regDelTag = variable with specific tag value to remove registry value if($reg.$regValueKey -notlike $Script:regDelTag){ MsgFce "Set registry '$($reg.Name)' = '$($reg.$regValueKey)' ($($reg.PropertyType)) at '$($reg.Path)'" New-ItemProperty -Path $reg.Path -Name $reg.Name -PropertyType $reg.PropertyType -Value $reg.$regValueKey -Force | Out-Null } else{ MsgFce "Removed registry '$($reg.Name)' ($($reg.PropertyType)) at '$($reg.Path)'" Remove-ItemProperty -Path $reg.Path -Name $reg.Name -ErrorAction SilentlyContinue | Out-Null } } } # Windows service settings FCE for enable/disable updates (enable = StartupTypeOn | SvcActionOn / disable = StartupTypeOff | SvcActionOff) # Example of param: [pscustomobject] @{Name = ""; StartupTypeOn = "Manual/Automatic/Disabled"; SvcActionOn = "Start/Stop"; StartupTypeOff = "Manual/Automatic/Disabled"; SvcActionOff = "Start/Stop" } Function SetService(){ param ( [Parameter(Mandatory=$true)] [array] $SvcArr, [Parameter(Mandatory=$true)] [ValidateSet("On","Off")] [string] $UpdateAction ) $svcStartupTypeKey = "StartupType$($UpdateAction)" $svcActionKey = "SvcAction$($UpdateAction)" foreach($svc in $SvcArr){ $service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue if($service.Length){ MsgFce "Set service '$($svc.Name)' (Startup Type '$($svc.$svcStartupTypeKey)')" $service | Set-Service -StartupType $svc.$svcStartupTypeKey MsgFce "Service '$($svc.Name)' Action '$($svc.$svcActionKey)'" if($svc.$SvcActionKey -ilike "start"){ $service | Start-Service } elseif($svc.$SvcActionKey -ilike "stop"){ $service | Stop-Service } elseif($svc.$SvcActionKey -ilike "none"){ # skip } else{ MsgFce "...unknown service action..." -Output warn } } else{ MsgFce "Skipping service '$($svc.Name)' (doesn't exist)" -Output warn } } } # Windows Task Scheduler - Enabling/Disabling schtask ($Schtask = Scheduled Task Name mask) Function SetSchtaskState(){ param ( [Parameter(Mandatory=$true)] [string] $Schtask, [Parameter(Mandatory=$true)] [ValidateSet("Enable","Disable")] [string] $State ) $task = Get-ScheduledTask -TaskName $Schtask if(($task | Measure-Object).Count -eq 1){ $msg = "Scheduled task '$($task.TaskName)' - state set to " if($State -like "Enable"){ $msg += "'Enabled'" $task | Enable-ScheduledTask | Out-Null } if($State -like "Disable"){ $msg += "'Disabled'" $task | Disable-ScheduledTask | Out-Null } MsgFce $msg } else{ MsgFce "Fce 'Get-ScheduledTask' for task '$($Schtask)' returned more than one item => cannot set state" -Output warn } } # Getting latest version of VMware OSOT and call the OSOT exe with specified arguments Function OsotCmd{ param ( [Parameter(Mandatory=$true)] [string] $OsotArg, [Parameter(Mandatory=$false)] [boolean] $OsotShutdown = $false ) # Getting of latest version VMware OSOT Executable $osotFileFilter = "VMwareHorizonOSOptimizationTool-x86_64-*" $osotExe = Get-ChildItem -Path $Script:VAR.OsotPath -Filter $osotFileFilter -ErrorAction SilentlyContinue | Select-Object -Property Name,FullName, @{N='Version';E={$($_.BaseName.Split("-")[2])}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object Version | Select-Object -Last 1 if($osotExe){ # add Shutdown param if($OsotShutdown){ $OsotArg += " -shutdown" } # split arguments to array $params = $OsotArg.Split(" ") # call Osot & $($OsotExe.FullName) $params } else{ MsgFce "No VMware OSOT executable ((filter mask: $($OsotFileFilter))) found in OSOT path ($($VAR.OsotPath)) => cannot proceed finalize" -Output warn } } # Getting MSI file properties Function Get-MsiInformation(){ param ( [Parameter(Position=0, Mandatory=$true)] [ValidateNotNullOrEmpty()] [System.IO.FileInfo[]] $MSI ) if($MSI.Count -ne 1){ Write-Warning "ERROR: Only ONE MSI file for fce Get-MsiInformation!" break } try{ $file = Get-ChildItem $MSI -ErrorAction Stop } catch{ Write-Warning "Unable to get file $($MSI) $($_.Exception.Message)" return } $dataMSI = [ordered] @{ FileName = $file.Name FileFullName = $file.FullName "FileLength(MB)" = $file.Length / 1MB } # Read property from MSI database $winInstaller = New-Object -ComObject WindowsInstaller.Installer # open MSI DB read only $msiDbOpenReadOnly = 0 try{ $msiDb = $winInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $winInstaller, @($file.FullName, $msiDbOpenReadOnly)) } catch{ Write-Debug $_.Exception.Message } if($msiDb){ $properties = @("ProductName", "ProductVersion", "Manufacturer", "ProductCode", "UpgradeCode") foreach($property in $properties){ $query = "SELECT Value FROM Property WHERE Property = '$($property)'" $view = $msiDb.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $msiDb, ($query)) $view.GetType().InvokeMember("Execute", "InvokeMethod", $null, $view, $null) $record = $view.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $view, $null) try{ $value = $record.GetType().InvokeMember("StringData", "GetProperty", $null, $record, 1) } catch { Write-Debug "Unable to get '$property' $($_.Exception.Message)" $value = "" } $dataMSI.$property = $value } # Other MSI Details # https://docs.microsoft.com/en-us/windows/win32/msi/summary-information-stream-property-set $msiInfo = $msiDb.GetType().InvokeMember("SummaryInformation", "GetProperty",$null , $msiDb, $null) $propertyIndex = @{"Title"=2; "Subject"=3; "Author"=4; "Comment"=6; "CreationDate"=12; "RevisionNumber"=9;"ApplicationName"=18} foreach($key in $propertyIndex.Keys){ $dataMSI.$key = $msiInfo.Property($propertyIndex.$key) } } # Close MSI Run garbage collection and release ComObject $null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($winInstaller) [System.GC]::Collect() return [pscustomobject] $dataMSI } # Function of clearing SCCM Configuration Function SccmClearConfig{ # Reset SCCM client on computer before taking snapshot for cloning $svc = "ccmexec" $service = Get-Service -Name $svc -ErrorAction SilentlyContinue if($service.Length){ $service | Stop-Service } else{ MsgFce "Service '$($svc)' (doesn't exist)" -Output warn } # Remove SCCM agent ini config file $sccmCfg = "$($Env:WinDir)\SMSCFG.ini" if(Test-Path $sccmCfg -PathType Leaf){ Remove-Item -Path $sccmCfg -Force } else{ MsgFce "SCCM config file ('$($sccmCfg)') not found => skipped" -Output warn } # Delete certificate by subject/serialnumber/issuer/whatever Get-ChildItem Cert:\LocalMachine\SMS -ErrorAction SilentlyContinue | Where-Object { $_.Subject -match "SMS" } | Remove-Item # Clear SCCM cached files Remove-Item "$($Env:WinDir)\ccmcache\*" -recurse -ErrorAction SilentlyContinue } #----------------[ Main Execution ]--------------- $msg = "$($VAR.ScriptName) --- The beginning of the script --- [{0:yyyy-MM-dd HH:mm:ss}]" -f (Get-Date) MsgFce $msg # UAC required if(!([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")){ MsgFce "ERROR: Administrator rights are required to run this script!" -Output warn break } # Settings for faster Invoke-WebRequest downloads (without progress) $ProgressPreference = "SilentlyContinue" # Import script configuration for redefine script variables $cfgInfo = "" # Configuration file for import $CfgFile2Import = (Split-Path -Path $PSCommandPath -Leaf) -replace ".ps1", ".ps1.config" if($ConfigFile -notlike ""){ $CfgFile2Import = Split-Path -Path $ConfigFile -Leaf } $CfgFile2Import = $VAR.ScriptPath + "\" + $CfgFile2Import # Load configuration file and import variables if(Test-Path $CfgFile2Import -PathType Leaf){ $cfgInfo += MsgFce "INFO: Configuration file '$($CfgFile2Import)' exist and is used to redefine script variables in '`$VAR' with the specified values!" -Output return $lineNum = 1 Get-Content $CfgFile2Import | ForEach-Object { $line = $_.Trim() $lineNumF = "{0:D3}" -f $lineNum if($line -ne "" -and !$line.StartsWith("#")){ $key,$val = $line.Split("=",2).Trim() if($key -in $VAR.Keys){ $type = $VAR.$key.GetType() if($type.Name -like "Boolean"){ if($val -iin @("true","false")){ $val = [System.Convert]::ToBoolean($val) } elseif($val -iin @("`$true","`$false")){ $val = [System.Convert]::ToBoolean($val.Substring(1)) } else{ $cfgInfo += MsgFce "Line $($lineNumF): Variable '$($key)' value '$($val)' is not 'boolean' type" -Output "return" $val = "*ValErr*" } } elseif($type.Name -like "Int*"){ if([int]::TryParse($val,[ref] "123")){ $val = [int] $val } else{ $cfgInfo += MsgFce "Line $($lineNumF): Variable '$($key)' value '$($val)' is not 'int' type" -Output "return" $val = "*ValErr*" } } if($val -notlike "*ValErr*"){ $cfgInfo += MsgFce "Line $($lineNumF): Variable '$($key)' set to '$($val)'" -Output return $VAR.$key = $val } else{ $cfgInfo += MsgFce "Line $($lineNumF): ERROR Variable '$($key)' cannot be set due tu wront type or format" -Output return } } else{ $cfgInfo += MsgFce "Line $($lineNumF): Unknown variable or content '$($line)'" -Output return } $lineNum++ } } } else{ $cfgInfo += MsgFce "INFO: Configuration file '$($CfgFile2Import)' doesn't exist => script values in '`$VAR' is used!" -Output return } # --- Script Main Menu --- if($Action -like ""){ # Script Menu $menuSel = MenuSimple -MenuItems $MENU -Title $VAR.ScriptMenuTitle $Action = GeMenuAction $menuSel } # --- Show configuration details and again show Script Menu --- While($Action -like "CfgInfo"){ EmptyLines MsgFce "INFO: --- Script Configuration Details ---" $cfgInfo EmptyLines MsgFce "List of all variables:" $VAR.GetEnumerator() | Sort-Object Name | Format-Table # Script Menu $menuSel = MenuSimple -MenuItems $MENU -Title $VAR.ScriptMenuTitle $Action = GeMenuAction $menuSel } # Param/Action info EmptyLines MsgFce "Script 'Action': $($Action)" EmptyLines # if switch by Action param # --- Enables & runs Windows/Office updates --- if($Action -like "Update"){ # Set Update Registry if($regArr.Count){ $str += if($VAR.ManageWindowsUpdates -and $VAR.ManageOfficeUpdates){ "Windows and Office"} elseif($VAR.ManageWindowsUpdates){ "Windows" } else{ "Office" } MsgFce "INFO: --- Enable $($str) Update registry & run ---" SetRegistry -RegArr $regArr -UpdateAction On } # Set Update Services if($svcArr.Count){ MsgFce "INFO: --- Enable updating services ---" SetService -SvcArr $svcArr -UpdateAction On } # Run GPupdate if($regArr.Count -or $svcArr.Count){ & gpupdate } # Start Windows OS Update GUI if($VAR.ManageWindowsUpdates){ MsgFce "INFO: Starting Windows Update process..." Start-Process "ms-settings:windowsupdate" } # Start Office Update GUI if($VAR.ManageOfficeUpdates){ $msoClientExe = "C:\Program Files\Common Files\microsoft shared\ClickToRun\OfficeC2RClient.exe" $msoClientArg = "/update user" if(Test-Path $msoClientExe -PathType Leaf){ MsgFce "INFO: Starting Microsoft Office update process..." Start-Process $msoClientExe $msoClientArg } else{ MsgFce "Microsoft Office update client not found ('$($msoClientExe)') => skipped" -Output warn } } } # --- OSOT Finalize --- elseif($Action -like "Finalize"){ # Disable Update Services & Registry if($svcArr.Count){ MsgFce "INFO: --- Disable updating services ---" SetService -SvcArr $svcArr -UpdateAction Off } # Set Update Registry if($regArr.Count){ $str += if($VAR.ManageWindowsUpdates -and $VAR.ManageOfficeUpdates){ "Windows and Office"} elseif($VAR.ManageWindowsUpdates){ "Windows" } else{ "Office" } MsgFce "INFO: --- Disable $($str) update registry ---" SetRegistry -RegArr $regArr -UpdateAction Off } # Run GPupdate if($regArr.Count -or $svcArr.Count){ & gpupdate } # Other maintenance - clear old windows updates files, log files etc. $dirArr = @() $dirArr += [pscustomobject] @{Path = "C:\Windows\SoftwareDistribution\Download\"; FileMask = "*.*"; OlderThanXDays = 0 } $dirArr += [pscustomobject] @{Path = "C:\ProgramData\VMware\VDM\Logs\"; FileMask = "*.*"; OlderThanXDays = 0 } $dirArr += [pscustomobject] @{Path = "C:\Program Files (x86)\CloudVolumes\Agent\Logs"; FileMask = "*.log"; OlderThanXDays = 0 } if($dirArr.Count){ MsgFce "INFO: --- Cleaning up some directories ---" foreach($dir in $dirArr){ # select items to delete $items = Get-ChildItem -Path $dir.Path -Recurse -force -ErrorAction SilentlyContinue | Where-Object {-not $_.PsIsContainer -and ($_.Name -ilike $dir.FileMask)} if($dir.OlderThanXDays){ $items = $items | Where-Object {($_.LastwriteTime -lt (Get-Date).AddDays(-$dir.OlderThanXDays))} } if($items.Count){ $items | Remove-Item -Force -Recurse -ErrorAction SilentlyContinue } # info msg $str = if($dir.OlderThanXDays){ ", older than $($dir.OlderThanXDays) day(s)"} else{ "" } $str += " => Total number of deleted files: " + $items.Count MsgFce "INFO: Directory: '$($dir.Path)', File Mask: '$($dir.FileMask)'$($str)" } } # Set/Disable Scheduled tasks if($schtaskArr.Count){ MsgFce "INFO: --- Disabling some scheduled tasks ---" foreach($schtask in $schtaskArr){ SetSchtaskState -Schtask $schtask -State Disable } } # SCCM Configuration Cleanup if($VAR.SccmClearConfig){ MsgFce "INFO: --- Clearing SCCM Configuration ---" SccmClearConfig } # OSOT Finalize Cmd MsgFce "INFO: --- VMware OSOT Finalize ---" OsotCmd -OsotArg $VAR.OsotFinalizeArg -OsotShutdown $VAR.OsotShutdownAfterFinalize } # --- OSOT Finalize fast --- elseif($Action -like "FinalizeFast"){ # SCCM Configuration Cleanup if($VAR.SccmClearConfig){ MsgFce "INFO: --- Clearing SCCM Configuration ---" SccmClearConfig } # OSOT Finalize Cmd MsgFce "INFO: --- VMware OSOT Finalize FAST ---" OsotCmd -OsotArg $VAR.OsotFinalizeArgFast -OsotShutdown $VAR.OsotShutdownAfterFinalizeFast } # --- Disk defragmentation --- elseif($Action -like "Defrag"){ MsgFce "INFO: --- Disk defragmentation ---" $defragsvc = @([pscustomobject] @{Name = "defragsvc"; StartupTypeOn = "Manual"; SvcActionOn = "Start"; StartupTypeOff = "Disabled"; SvcActionOff = "Stop" }) #$defragsvc | Out-GridView SetService -SvcArr $defragsvc -UpdateAction On MsgFce "... running defragmentation ..." $params = "C: /U /V" Start-Process defrag -Wait -ArgumentList $params.Split(" ") SetService -SvcArr $defragsvc -UpdateAction Off } # --- VMware Tools --- elseif($Action -like "VmTools"){ MsgFce "INFO: --- Install/Update VMware VM Tools ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile | Select-Object -Property Name, FullName, @{N='Version';E={$($_.BaseName.Split("-")[2])}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object VersionBuild | Select-Object -Last 1 if($swSrc){ $sw = Get-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}} if($sw.VersionBuild -lt $swSrc.VersionBuild){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))" $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "Yes" } if($continue -like "Yes"){ # Installation MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date) # remove some VM tools components (not necessary for VDI) $remove = "VmwTimeProvider,ServiceDiscovery,VSS" if(!$VAR.VmToolsCarbonBlack){ $remove += ",CBHelper" } if(!$VAR.VmToolsNsxIntrospection){ $remove += ",NetworkIntrospection,FileIntrospection" } $params = '/S /v "/qb /l* ""{0}"" REBOOT=R ADDLOCAL=ALL REMOVE={1}"' -f $log, $remove Start-Process -FilePath $swSrc.FullName -Wait -ArgumentList $params.Split(" ") } else{ MsgFce "INFO: The installation has been cancelled" } } else{ MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))" } } else{ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } } # --- VMware Horizon Agent --- elseif($Action -like "Horizon"){ MsgFce "INFO: --- Install/Update VMware Horizon Agent ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.ProductVersion}}, @{N='VersionBuild';E={[int] $($_.BaseName.Split("-")[6])}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1 if($swSrc){ $sw = Get-ItemProperty "HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='Version';E={$($_.DisplayVersion -replace ".*\(","" -replace "\).*","") }}, @{N='VersionBuild';E={[int] $($($_.DisplayVersion.Split(".")[3]).Split(" ")[0])}} if($sw.VersionBuild -lt $swSrc.VersionBuild){ MsgFce "INFO: A newer version of $($SwSet.$Action.Name) was found in the installation source path (Install source | current version ::: $($swSrc.Version) [build: $($swSrc.VersionBuild)] | $($sw.Version) [build: $($swSrc.VersionBuild)])" $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "Yes" } if($continue -like "Yes"){ # Installation MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date) $options = if(($VAR.HorizonAgentAddLocal -like "ALL") -and ($VAR.HorizonAgentRemove -notlike "")){ " REMOVE=$($VAR.HorizonAgentRemove)" } else { "" } $params = '/S /v "/qb /l* ""{0}"" VDM_VC_MANAGED_AGENT=1 SUPPRESS_RUNONCE_CHECK=1 ADDLOCAL={1}{2} REBOOT=ReallySuppress"' -f $log, $VAR.HorizonAgentAddLocal, $options Start-Process -FilePath $swSrc.FullName -Wait -ArgumentList $params.Split(" ") } else{ MsgFce "INFO: The installation has been cancelled" } } else{ MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.Version) | $($sw.Version))" } # Remove PerfTracker icon from public desktop (if exist) if($VAR.DelPerfTrackerDesktopIcon){ $ico = "$($env:PUBLIC)\Desktop\VMware Horizon Performance Tracker.lnk" if(Test-Path $ico -PathType Leaf){ MsgFce "INFO: Public desktop icon of Horizon Performance Tracker removed" Remove-Item -Path $ico -Force | out-null } } # Configure Horizon Agent ExecScriptTimeout registry if([int] $VAR.HorizonAgentExecScriptTimeout -gt 2000){ MsgFce "INFO: Setting a new Timeout Limit for ClonePrep Customization Scripts (default limit: 2000 ms => new limit: $($VAR.HorizonAgentExecScriptTimeout) ms)" New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\vmware-viewcomposer-ga" -Name "ExecScriptTimeout" -Value $VAR.HorizonAgentExecScriptTimeout -PropertyType Dword -Force | Out-Null } } else{ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } } # --- VMware DEM Agent --- elseif($Action -like "DEM"){ MsgFce "INFO: --- Install/Update VMware DEM Agent ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile | Select-Object -Property Name, FullName, @{N='Version';E={[int] $($_.BaseName.Split(" ")[5])}}, @{N='VersionBuild';E={[System.Version] $($_.BaseName.Split(" ")[6])}}, LastWriteTime | Sort-Object VersionBuild,LastWriteTime | Select-Object -Last 1 if($swSrc){ $msiData = Get-MsiInformation $swSrc.FullName $sw = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $msiData.ProductName } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString if($sw.VersionBuild -lt $msiData.ProductVersion){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))" $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "Yes" } if($continue -like "Yes"){ # Installation MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date) $options = if($VAR.DemConfigPath){ ' COMPENVCONFIGFILEPATH="'+$VAR.DemConfigPath+'"' } else {''} $params = '/i "{0}" /qb /l* "{1}"{2}' -f $msiData.FileFullName, $log, $options Start-Process "msiexec.exe" -Wait -ArgumentList $params.Split(" ") } else{ MsgFce "INFO: The installation has been cancelled" } } else{ MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))" } } else{ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } } # --- VMware AppVolumes Agent --- elseif($Action -like "AppVolumes"){ MsgFce "INFO: --- Install/Update VMware AppVolumes Agent ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile | Select-Object -Property Name, FullName, @{N='Version';E={$($_.BaseName.Split("-")[2])}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}}, LastWriteTime | Sort-Object VersionBuild,LastWriteTime | Select-Object -Last 1 if($swSrc){ $msiData = Get-MsiInformation $swSrc.FullName $sw = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $msiData.ProductName } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString if($sw.VersionBuild -lt $msiData.ProductVersion){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))" $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "Yes" } if($continue -like "Yes"){ # Check install param if($VAR.AppVolManager.Count -and ($VAR.AppVolManager[0] -notlike "")){ # Installation MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date) # primary AppVol Manager $options = " MANAGER_ADDR="+$VAR.AppVolManager[0] # default port 443 and without validation of SSL Cert (for Golden Image not joined AD) $options += " MANAGER_PORT=443 EnforceSSLCertificateValidation=0" $params = '/i "{0}" /qb /l* "{1}"{2} REBOOT=ReallySuppress' -f $msiData.FileFullName, $log, $options Start-Process "msiexec.exe" -Wait -ArgumentList $params.Split(" ") # Configuration of other(s) AppVol Manager(s) if($VAR.AppVolManager.Count -gt 1){ for($i=1; $i -lt $VAR.AppVolManager.Count; $i++){ $paramName = "Manager$($i+1)" $paramVal = $VAR.AppVolManager[$i]+":443" MsgFce "INFO: AppVolumes Manager '$($paramVal)' was set to registry as '$($paramName)'" New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\svservice\Parameters" -Name $paramName -Value $paramVal -PropertyType String -Force | Out-Null } } # Next AppVol registry configuration if($VAR.AppVolDisableSpoolerRestart){ $paramName = "DisableSpoolerRestart" $paramVal = 1 MsgFce "INFO: AppVolumes registry settings for deactivating restarting of the spooler service" New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\svservice\Parameters" -Name $paramName -Value $paramVal -PropertyType Dword -Force | Out-Null } if($VAR.AppVolMaxDelayTimeOutS -ne 300){ $paramName = "MaxDelayTimeOutS" $paramVal = $VAR.AppVolMaxDelayTimeOutS MsgFce "INFO: AppVolumes registry settings for deactivating restarting of the spooler service" New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Services\svservice\Parameters" -Name $paramName -Value $paramVal -PropertyType Dword -Force | Out-Null } } else{ MsgFce "Primary AppVolumes Manager not configured!" } } else{ MsgFce "INFO: The installation has been cancelled" } } else{ MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))" } } else{ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } } # --- Microsoft FSLogix Agent --- elseif($Action -like "FSLogix"){ MsgFce "INFO: --- Install/Update Microsoft FSLogix ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$($_.BaseName.Split("_")[2])}}, @{N='VersionBuild';E={[System.Version] $($_.BaseName.Split("_")[2])}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1 if(!$swSrc){ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } else{ MsgFce "Installation package for '$($SwSet.$Action.Name)' exists in install source path ($($InstallSrcDir)). FileName: $($swSrc.Name); LastWriteTime: $($swSrc.LastWriteTime)" -Output verbose } # Download Installer? $sel = MenuSimple -MenuItems "Yes","No" -Title "Download the latest version of the '$($SwSet.$Action.Name)' installer from the web?" EmptyLines if($sel -like "Yes"){ $swDownloadUrl = "https://aka.ms/fslogix_download" MsgFce "...downloading the '$($SwSet.$Action.Name)' installation package (link: $($swDownloadUrl))" -Output verbose # download header for getting fileName $downHead = Invoke-WebRequest -UseBasicParsing -Method Head $swDownloadUrl $destFile = $InstallSrcDir +"\"+ $downHead.BaseResponse.ResponseUri.Segments[-1] Invoke-WebRequest $swDownloadUrl -OutFile $destFile $swSrc = @{"FullName" = $destFile} } $FSLogixAppsSetup = "FSLogixAppsSetup.exe" $zipPath = "*x64/Release/$($FSLogixAppsSetup)" # Unzip FSLogix Agent Installer if(Test-Path $swSrc.FullName -PathType Leaf){ Add-Type -Assembly System.IO.Compression.FileSystem $zip = [IO.Compression.ZipFile]::OpenRead($swSrc.FullName) $zip.Entries | Where-Object { $_.FullName -like $zipPath } | ForEach-Object {[System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, "$($InstallSrcDir)\$($_.Name)", $true)} $zip.Dispose() } # ReNew $swSrc by install file from ZIP $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $FSLogixAppsSetup -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.FileVersion}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object VersionBuild | Select-Object -Last 1 if($swSrc){ $sw = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}} if($sw.VersionBuild -lt $swSrc.VersionBuild){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))" $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "Yes" } if($continue -like "Yes"){ # Installation MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date) $params = "/install /passive /norestart /log {0}" -f $log Start-Process -FilePath $swSrc.FullName -Wait -ArgumentList $params.Split(" ") } else{ MsgFce "INFO: The installation has been cancelled" } } else{ MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))" } } else{ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } } # --- Microsoft Teams for VDI --- elseif($Action -like "MsTeams"){ MsgFce "INFO: --- Install/Update Microsoft Teams for VDI ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$($_.BaseName.Split("-")[2])}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1 if(!$swSrc){ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } else{ MsgFce "Installation package for '$($SwSet.$Action.Name)' exists in install source path ($($InstallSrcDir)). FileName: $($swSrc.Name); LastWriteTime: $($swSrc.LastWriteTime)" -Output verbose } # Download Installer? $sel = MenuSimple -MenuItems "Yes","No" -Title "Download the latest version of the '$($SwSet.$Action.Name)' installer from the web?" EmptyLines if($sel -like "Yes"){ $swDownloadUrl = "https://teams.microsoft.com/downloads/desktopurl?env=production&plat=windows&arch=x64&managedInstaller=true&download=true" MsgFce "...downloading the '$($SwSet.$Action.Name)' installation package (link: $($swDownloadUrl))" -Output verbose $destFile = $InstallSrcDir +"\"+ $SwSet.$Action.SrcFile Invoke-WebRequest $swDownloadUrl -OutFile $destFile $swSrc = @{"FullName" = $destFile} } if($swSrc){ $msiData = Get-MsiInformation $swSrc.FullName $sw = Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $msiData.ProductName } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString if($sw.VersionBuild -lt $msiData.ProductVersion){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))" $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "Yes" } if($continue -like "Yes"){ # Uninstall if($sw.UninstallString -notlike ""){ # UnInstallation MsgFce "INFO: Uninstalling the old version of '$($SwSet.$Action.Name)'..." $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date), "un" $msiProductCode = (($sw.UninstallString -split " ")[1] -replace "/I","") $params = '/X "{0}" /l* "{1}" /qb /norestart' -f $msiProductCode, $log Start-Process "msiexec.exe" -Wait -ArgumentList $params.Split(" ") } # Installation MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date), "" $options = if($VAR.MsTeamsDisableAutoStart){ ' OPTIONS="noAutoStart=true"' } else {''} $params = '/i "{0}" /l* "{1}" ALLUSER=1 ALLUSERS=1{2}' -f $msiData.FileFullName, $log, $options Start-Process "msiexec.exe" -Wait -ArgumentList $params.Split(" ") } else{ MsgFce "INFO: The installation has been cancelled" } } else{ MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($msiData.ProductVersion) | $($sw.VersionBuild))" } } } # --- Microsoft Teams for VDI NEW --- elseif($Action -like "MsTeamsNEW"){ MsgFce "INFO: --- Install/Update Microsoft Teams for VDI NEW ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1 if(!$swSrc){ MsgFce "No installer for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in source path ($($InstallSrcDir))" -Output warn } else{ MsgFce "Installer for '$($SwSet.$Action.Name)' exists in source path ($($InstallSrcDir)). FileName: $($swSrc.Name); Version: $($swSrc.VersionBuild); LastWriteTime: $($swSrc.LastWriteTime)" -Output verbose } # Download Installer? $sel = MenuSimple -MenuItems "Yes","No" -Title "Download the latest version of the installer '$($SwSet.$Action.Name)' from the web?" EmptyLines if($sel -like "Yes"){ $swDownloadUrl = "https://go.microsoft.com/fwlink/?linkid=2243204&clcid=0x409" MsgFce "...downloading the '$($swSrc.Name)' installer (link: $($swDownloadUrl))" -Output verbose $destFile = $InstallSrcDir +"\"+ $SwSet.$Action.SrcFile Invoke-WebRequest $swDownloadUrl -OutFile $destFile # Renew swSrc from downloaded file $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1 } if($swSrc){ $params = "-p" if($VAR.MsTeamsOfflineMSIX){ $filter = "MSTeams-x64.msix" $msixSrc = Get-ChildItem -Path $InstallSrcDir -Filter $filter -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, LastWriteTime if($msixSrc.Name -notlike ""){ MsgFce "Offline MSIX package for '$($SwSet.$Action.Name)' exists in install source path ($($InstallSrcDir)). FileName: $($msixSrc.Name); LastWriteTime: $($msixSrc.LastWriteTime)" -Output verbose $params += " -o ""$($msixSrc.FullName)""" } else{ $sel = MenuSimple -MenuItems "Yes","No" -Title "Offline MSIX package for '$($SwSet.$Action.Name)' doesn't exists in install source path ($($InstallSrcDir)). Use latest MSIX from the web?" if($sel -like "No"){ $params = "" } } } if($params -notlike ""){ MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $log = $LogDir+"\"+$SwSet.$Action.LogFile -f (Get-Date), "" Start-Process -FilePath $swSrc.FullName -Wait -ArgumentList $params.Split(" ") -RedirectStandardOutput $log } else{ MsgFce "INFO: No install param => cannot continue with installation" } # Get details about MS Teams AppX version $appx = Get-AppxPackage | Where-Object { $_.Name -like "MSTeams" } | Select-Object Name, Version if($appx.Name -notlike ""){ MsgFce "Installed version of MS Teams: $($appx.Version)" } else{ MsgFce "Not detected any version of MS Teams AppX" -Output warn } } } # --- Microsoft OneDrive (All Users) --- elseif($Action -like "OneDrive"){ MsgFce "INFO: --- Install/Update Microsoft OneDrive (All Users) ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.FileVersion}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1 if(!$swSrc){ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } else{ MsgFce "Installation package for '$($SwSet.$Action.Name)' exists in install source path ($($InstallSrcDir)). FileName: $($swSrc.Name); LastWriteTime: $($swSrc.LastWriteTime)" -Output verbose } # Download Installer? $sel = MenuSimple -MenuItems "Yes","No" -Title "Download the latest version of the '$($SwSet.$Action.Name)' installer from the web?" EmptyLines if($sel -like "Yes"){ $swDownloadUrl = "https://go.microsoft.com/fwlink/?linkid=844652" MsgFce "...downloading the '$($SwSet.$Action.Name)' installation package (link: $($swDownloadUrl))" -Output verbose $destFile = $InstallSrcDir +"\"+ $SwSet.$Action.SrcFile Invoke-WebRequest $swDownloadUrl -OutFile $destFile # Renew swSrc from downloaded file $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.FileVersion}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object VersionBuild | Select-Object -Last 1 } if($swSrc){ $sw = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString if($sw.VersionBuild -lt $swSrc.VersionBuild){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))" $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "Yes" } if($continue -like "Yes"){ # Installation MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $params = "/allusers" Start-Process -FilePath $swSrc.FullName -PassThru -ArgumentList $params.Split(" ") | Out-Null # Disable OneDrive Per-Machine Standalone Update Task Do{ Start-Sleep 5 } While (Get-Process | Where-Object Path -eq $swSrc.FullName) $schtaskName = "OneDrive Per-Machine Standalone Update Task" MsgFce "INFO: Disabling scheduled task '$($schtaskName)'" Start-Sleep 10 schtasks /change /tn $schtaskName /disable | Out-Null } else{ MsgFce "INFO: The installation has been cancelled" } } else{ MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))" } } } # --- Google Drive --- elseif($Action -like "GoogleDrive"){ MsgFce "INFO: --- Install/Update Google Drive ---" $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.FileVersion}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}}, LastWriteTime | Sort-Object VersionBuild | Select-Object -Last 1 if(!$swSrc){ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } else{ MsgFce "Installation package for '$($SwSet.$Action.Name)' exists in install source path ($($InstallSrcDir)). FileName: $($swSrc.Name); LastWriteTime: $($swSrc.LastWriteTime)" -Output verbose } # Download Installer? $sel = MenuSimple -MenuItems "Yes","No" -Title "Download the latest version of the '$($SwSet.$Action.Name)' installer from the web?" EmptyLines if($sel -like "Yes"){ $swDownloadUrl = "https://dl.google.com/drive-file-stream/GoogleDriveSetup.exe" MsgFce "...downloading the '$($SwSet.$Action.Name)' installation package (link: $($swDownloadUrl))" -Output verbose $destFile = $InstallSrcDir +"\"+ $SwSet.$Action.SrcFile Invoke-WebRequest $swDownloadUrl -OutFile $destFile # Renew swSrc from downloaded file $swSrc = Get-ChildItem -Path $InstallSrcDir -Filter $SwSet.$Action.SrcFile -ErrorAction SilentlyContinue | Select-Object -Property Name, FullName, @{N='Version';E={$_.VersionInfo.FileVersion}}, @{N='VersionBuild';E={[System.Version] $_.VersionInfo.FileVersion}} | Sort-Object VersionBuild | Select-Object -Last 1 } if($swSrc){ $sw = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" | Where-Object { $_.DisplayName -like $SwSet.$Action.Name } | Select-Object -Property DisplayName,DisplayVersion, @{N='VersionBuild';E={[System.Version] $_.DisplayVersion}},UninstallString if($sw.VersionBuild -lt $swSrc.VersionBuild){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))" $continue = if($SwSet.$Action.AskIfNewer){ MenuSimple -MenuItems "Yes","No" -Title "Continue the installation?" } else { "Yes" } if($continue -like "Yes"){ # Installation MsgFce "INFO: Installing the new version of '$($SwSet.$Action.Name)'..." $params = "--silent --skip_launch_new" if($VAR.GoogleDriveDesktopShortcuts){ $params += " --desktop_shortcut" } if(!$VAR.GoogleDriveGSuiteShortcuts){ $params += " --gsuite_shortcuts=false" } Start-Process -FilePath $swSrc.FullName -Wait -PassThru -ArgumentList $params.Split(" ") | Out-Null } else{ MsgFce "INFO: The installation has been cancelled" } } else{ MsgFce "INFO: Installed version of $($SwSet.$Action.Name) doesn't need to be installed or updated (Install source | current version ::: $($swSrc.VersionBuild) | $($sw.VersionBuild))" } } } # --- finish --- elseif($Action -like "Exit"){ MsgFce "No action" -Output verbose } else{ MsgFce "ERROR => No action parameter was specified" -Output warn } #----------------[ Logging maintenance ]---------------- $LogFiles = Get-ChildItem $LogDir | Where-Object {-not $_.PSIsContainer} if($LogFiles.Count -gt $VAR.LogArchiveFiles){ EmptyLines MsgFce "There is currently $($LogFiles.Count) files In log archive folder '$($LogDir)' - it's more than set archive limit $($VAR.LogArchiveFiles). The oldest files will be deleted." $LogFiles | Sort-Object LastWriteTime | Select-Object -First ($LogFiles.Count-$VAR.LogArchiveFiles) | Remove-Item } # --- End of the script --- EmptyLines $msg = "$($VAR.ScriptName) --- End of the script --- [{0:yyyy-MM-dd HH:mm:ss}]" -f (Get-Date) MsgFce $msg # Transcript Log - Stop if($Host.Name -match "ConsoleHost"){ Stop-Transcript | out-null } |