VDI-GoldenImage.ps1
<#PSScriptInfo
.VERSION 2412.0 .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 Horizon Golden Image MasterPC Tools VMware Omnissa DynamicEnvironmentManager AppVolumes Microsoft Teams FSLogix OneDrive GoogleDrive OSOT automation maintenance finalize .PROJECTURI .RELEASENOTES [v2412.0 - 20241216] Removed task to install Microsoft Teams Classic (only New Microsoft Teams) Default startup type of 'optimize drives' (defragsvc) services set to 'manual' (because of FSlogix 'Disk Compaction' fce) Modification according to the current situation of VMware and Omnissa Bug Fixes and revision [v2404.0 - 20240430] New option to import configuration from 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 [v2312.0 - 20231220] Added optional param DisableSpoolerRestart to App Volumes [v2311.1 - 20231130] Bug Fixes and revision [v2311.0 - 20231107] Added task to install/update Microsoft FSLogix & OneDrive with downloading of latest version from web [v2310.0 - 20231027] Added task to install/update Horizon Agent, Dynamic Environment Manager and App Volumes [v2309.1 - 20230926] Added task to install/update Microsoft Teams for VDI with downloading of latest version from web [v2309.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 Omnissa (VMware) Horizon .DESCRIPTION Script with basic sets of maintenance task for VDI Golden Image running on Omnissa (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: 2412 Author: Fara Jan Creation Date: 2023-09-06 Last Update: 2024-12-16 .PARAMETER Action Specifies the action/task of the script. If not specified a menu with a list of actions is displayed. .PARAMETER ConfigFile Specifies configuration file for load/import to redefine script variables (useful for easy updating of this script) If not specified the script tries to find and import a file named as script + '.config' e.g. 'VDI-GoldenImage.ps1.config' .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 - install latest Microsoft Teams from online or offline MSIX 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", "OneDrive", "GoogleDrive", "CfgInfo", "Exit")] [string] $Action, [Parameter(Mandatory=$false)] [string] $ConfigFile ) #----------------[ Settings ]---------------- $VAR = @{ # Script Name ScriptName = "VDI Golden Image Maintenance [v2412]" # 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 (Windows OS Optimalization Tool) --- # OSOT info web: https://techzone.omnissa.com/resource/windows-os-optimization-tool-horizon-guide # Path to the OSOT executable file (automatic getting of latest OSOT version) OsotPath = "C:\Program Files\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.omnissa.com/bundle/Desktops-and-Applications-in-HorizonV2406/page/MicrosoftWindowsInstallerCommandLineOptions.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 # --- DEM (Dynamic Environment Manager) Config share Path (Empty if no DEM) --- DemConfigPath = "\\domain.int\VDI$\DEMConfig\general" # --- 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 Integrated Printing: https://docs.omnissa.com/bundle/AppVolumesAdminGuideV2410/page/DeactivateRestartingtheSpoolerServiceWhenUsingIntegratedPrinting.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 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 # --- Other Settings --- # Show details of install params ShowInstallParams = $false # --- LOG settings --- LogDir = "Logs" # relative path to $_.ScriptPath LogFileName = "VDI-GI-Maintenance-{0:yyyyMMdd_HHmmss}.txt" LogArchiveFiles = 14 } # --- 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-*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 = "teamsbootstrapper.exe"; LogFile = "Log_MsTeams_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 Update if($VAR.MsTeamsDisableAutoUpdate){ $regPath = "HKLM:\SOFTWARE\Microsoft\Teams" $regArr += [pscustomobject] @{Path = $regPath; Name = "disableAutoUpdate"; PropertyType = "Dword"; ValueOn = 0; ValueOff = 1 } } # --- 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 = Join-Path $VAR.ScriptPath $VAR.InstallSrcDir $LogDir = Join-Path $VAR.ScriptPath $VAR.LogDir $LogFile = (Join-Path $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 "Windows OS Optimalization Tool" 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 'Windows OS Optimalization Tool' 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" # --- Load/Import script configuration file for redefine script variables defined in $VAR (useful for easy updating of this script) --- $cfgInfo = "" # string with details of configuraction details (listable in the Script Menu) # Configuration file for import (defined by param $ConfigDile or default file is same as script + '.config' e.g. 'VDI-GoldenImage.ps1.config' $CfgFile2Import = (Split-Path -Path $PSCommandPath -Leaf) -replace ".ps1", ".ps1.config" if($ConfigFile -notlike ""){ $CfgFile2Import = Split-Path -Path $ConfigFile -Leaf } $CfgFile2Import = Join-Path $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){ if($val -is [string]){ $val = $val.Trim("'""") # trim ' or " from start/end } $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*" } } elseif($type.Name -ilike "Object*"){ # Object/Array if($val -is [string]){ $vall = $val.Trim("@()") } $val = @() if($vall -notlike ""){ $val += $vall -split "," | ForEach-Object { $_.Trim().Trim("'""") } if($val.GetType().Name -inotlike "Object*"){ $cfgInfo += MsgFce "Line $($lineNumF): Variable '$($key)' value '$($val)' output is not 'object/array' type ($($val.GetType()))" -Output "return" $val = "*ValErr*" } } } if(($val -notlike "*ValErr*") -or ($val.GetType().Name -like "Object*")){ $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 --- $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 Horizon Agent" if($VAR.DemConfigPath -notlike ""){ $MENU += "DEM: Install/Update Dynamic Environment Agent" } if($VAR.AppVolManager.Count){ $MENU += "AppVolumes: Install/Update AppVolumes Agent" } $MENU += "FSLogix: Install/Update Microsoft FSLogix Agent" + $menuStrDownload $MENU += "MsTeams: Install/Update Microsoft Teams for VDI (MSIX)" + $menuStrDownload $MENU += "OneDrive: Install/Update Microsoft OneDrive" + $menuStrDownload $MENU += "GoogleDrive: Install/Update Google Drive" + $menuStrDownload $MENU += "CfgInfo: Show script configuration details" $MENU += "Exit" # Display menu if no Action specified if($Action -like ""){ $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: --- 'Windows OS Optimalization Tool' 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: --- 'Windows OS Optimalization Tool' 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 = "Manual"; 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" MsgFce "Current version: $($sw.VersionBuild)" MsgFce "New version: $($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 = (Join-Path $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 if($VAR.ShowInstallParams){ MsgFce "Install params: $($params)" } 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" MsgFce "Current version: $($sw.VersionBuild)" MsgFce "Install source: $($swSrc.VersionBuild)" } } else{ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } } # --- Horizon Agent --- elseif($Action -like "Horizon"){ MsgFce "INFO: --- Install/Update VMware/Omnissa 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])}} #build version corection (missin in some install sources) $buildSrc = if($swSrc.VersionBuild -notlike ""){ $swSrc.VersionBuild } else { $swSrc.Version } $buildSw = if($swSrc.VersionBuild -notlike ""){ $sw.VersionBuild } else { $sw.Version } if(!$sw -or ($buildSw -lt $buildSrc)){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path." if($sw){ MsgFce "Current version: $($sw.Version) [build: $($sw.VersionBuild)]" } else { MsgFce "Current version: -" } MsgFce "New version: $($swSrc.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 = (Join-Path $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 if($VAR.ShowInstallParams){ MsgFce "Install params: $($params)" } 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" MsgFce "Current version: $($sw.Version) [build: $($sw.VersionBuild)]" MsgFce "Install source: $($swSrc.Version) [build: $($swSrc.VersionBuild)]" } # 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 } } # --- Dynamic Environment Manager Agent --- elseif($Action -like "DEM"){ MsgFce "INFO: --- Install/Update VMware/Omnissa 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 -or ($sw.VersionBuild -lt $msiData.ProductVersion)){ MsgFce "INFO: A newer version of '$($SwSet.$Action.Name)' was found in the installation source path." if($sw){ MsgFce "Current version: $($sw.VersionBuild)" } else { MsgFce "Current version: -" } MsgFce "New version: $($msiData.ProductVersion)" $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 = (Join-Path $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 if($VAR.ShowInstallParams){ MsgFce "Install params: $($params)" } 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" MsgFce "Current version: $($sw.VersionBuild)" MsgFce "Install source: $($msiData.ProductVersion)" } } else{ MsgFce "No installation package for '$($SwSet.$Action.Name)' (filter mask: $($SwSet.$Action.SrcFile)) found in install source path ($($InstallSrcDir))" -Output warn } } # --- AppVolumes Agent --- elseif($Action -like "AppVolumes"){ MsgFce "INFO: --- Install/Update VMware/Omnissa 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." if($sw){ MsgFce "Current version: $($sw.VersionBuild)" } else { MsgFce "Current version: -" } MsgFce "New version: $($msiData.ProductVersion)" $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 = (Join-Path $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 if($VAR.ShowInstallParams){ MsgFce "Install params: $($params)" } 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" MsgFce "Current version: $($sw.VersionBuild)" MsgFce "Install source: $($msiData.ProductVersion)" } } 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 = Join-Path $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." if($sw){ MsgFce "Current version: $($sw.VersionBuild)" } else { MsgFce "Current version: -" } MsgFce "New version: $($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 = (Join-Path $LogDir $SwSet.$Action.LogFile) -f (Get-Date) $params = "/install /passive /norestart /log {0}" -f $log if($VAR.ShowInstallParams){ MsgFce "Install params: $($params)" } 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" MsgFce "Current version: $($sw.VersionBuild)" MsgFce "Install source: $($swSrc.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 (MSIX) --- elseif($Action -like "MsTeams"){ MsgFce "INFO: --- Install/Update Microsoft Teams for VDI (MSIX) ---" $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 = Join-Path $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)'..." if($VAR.ShowInstallParams){ MsgFce "Install params: $($params)" } $log = (Join-Path $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 = Join-Path $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." if($sw){ MsgFce "Current version: $($sw.VersionBuild)" } else { MsgFce "Current version: -" } MsgFce "New version: $($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)'..." $params = "/allusers" if($VAR.ShowInstallParams){ MsgFce "Install params: $($params)" } 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" MsgFce "Current version: $($sw.VersionBuild)" MsgFce "Install source: $($swSrc.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 = Join-Path $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." if($sw){ MsgFce "Current version: $($sw.VersionBuild)" } else { MsgFce "Current version: -" } MsgFce "New version: $($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)'..." $params = "--silent --skip_launch_new" if($VAR.GoogleDriveDesktopShortcuts){ $params += " --desktop_shortcut" } if(!$VAR.GoogleDriveGSuiteShortcuts){ $params += " --gsuite_shortcuts=false" } if($VAR.ShowInstallParams){ MsgFce "Install params: $($params)" } 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" MsgFce "Current version: $($sw.VersionBuild)" MsgFce "Install source: $($swSrc.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 } |