From 6aba3a6231d7275c92fcb1cb0003710b0b9e0911 Mon Sep 17 00:00:00 2001 From: "Sean P. McDonald" Date: Wed, 2 Jan 2019 16:58:59 -0800 Subject: [PATCH] (MODULES-8431) Update windows installation to use powershell This commit updates the installation method on windows to use a powershell script rather than a batch file --- .../functions/windows_msi_installargs.rb | 19 ++ manifests/windows/install.pp | 35 +-- .../puppet_agent_windows_install_spec.rb | 76 ++--- templates/install_puppet.bat.erb | 83 ------ templates/install_puppet.ps1.erb | 260 ++++++++++++++++++ 5 files changed, 326 insertions(+), 147 deletions(-) create mode 100644 lib/puppet/parser/functions/windows_msi_installargs.rb delete mode 100644 templates/install_puppet.bat.erb create mode 100644 templates/install_puppet.ps1.erb diff --git a/lib/puppet/parser/functions/windows_msi_installargs.rb b/lib/puppet/parser/functions/windows_msi_installargs.rb new file mode 100644 index 000000000..97becdd22 --- /dev/null +++ b/lib/puppet/parser/functions/windows_msi_installargs.rb @@ -0,0 +1,19 @@ +module Puppet::Parser::Functions + newfunction(:windows_msi_installargs, :arity => 1, :type => :rvalue, :doc => <<-EOS + Return the $install_options parameter as a string usable in an msiexec command + EOS + ) do |args| + + install_args = args[0] + + arg_string = install_args.map do |option| + if option.class == Hash + key_value = option.shift + "#{key_value[0]}=\"#{key_value[1]}\"" + else + option + end + end + return arg_string.join(' ') + end +end diff --git a/manifests/windows/install.pp b/manifests/windows/install.pp index 53a6905ba..634d25c7f 100644 --- a/manifests/windows/install.pp +++ b/manifests/windows/install.pp @@ -27,33 +27,27 @@ $_https_source = "https://downloads.puppetlabs.com/windows/${dir}${package_file_name}" } - $_install_options = $install_options ? { - [] => ['REINSTALLMODE="amus"'], - default => $install_options - } + $_installps1 = windows_native_path("${::env_temp_variable}/install_puppet.ps1") $_source = $source ? { undef => $_https_source, /^[a-zA-Z]:/ => windows_native_path($source), default => $source, } - $_msi_location = $_source ? { /^puppet:/ => windows_native_path("${::env_temp_variable}/puppet-agent.msi"), default => $_source, } - - $_installbat = windows_native_path("${::env_temp_variable}/install_puppet.bat") if $_source =~ /^puppet:/ { file{ $_msi_location: source => $_source, - before => File["${_installbat}"], + before => File["${_installps1}"], } } - $_cmd_location = $::rubyplatform ? { - /i386/ => 'C:\\Windows\\system32\\cmd.exe', - default => "${::system32}\\cmd.exe" + $_install_options = $install_options ? { + [] => windows_msi_installargs(['REINSTALLMODE="amus"']), + default => windows_msi_installargs($install_options) } if (member($::puppet_agent::service_names, 'puppet')) { @@ -62,17 +56,26 @@ $_agent_startup_mode = undef } + if $msi_move_locked_files { + $_move_dll_workaround = '$true' + } else { + $_move_dll_workaround = '$false' + } + $_timestamp = strftime('%Y_%m_%d-%H_%M') $_logfile = windows_native_path("${::env_temp_variable}/puppet-${_timestamp}-installer.log") $_puppet_master = $::puppet_master_server + $_install_pid_file_loc = windows_native_path("${::env_temp_variable}/puppet_agent_install.pid") + notice ("Puppet upgrade log file at ${_logfile}") debug ("Installing puppet from ${_msi_location}") - file { "${_installbat}": + + file { "${_installps1}": ensure => file, - content => template('puppet_agent/install_puppet.bat.erb') + content => template('puppet_agent/install_puppet.ps1.erb') } - -> exec { 'install_puppet.bat': - command => "${::system32}\\cmd.exe /c start /b ${_cmd_location} /c \"${_installbat}\" ${::puppet_agent_pid}", + -> exec { 'install_puppet.ps1': + command => "${::system32}\\cmd.exe /c start /b ${::system32}\\WindowsPowerShell\\v1.0\\powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -NonInteractive -File ${_installps1} ${::puppet_agent_pid}", path => $::path, } @@ -80,6 +83,6 @@ exec { 'fix inheritable SYSTEM perms': command => "${::system32}\\icacls.exe \"${::puppet_client_datadir}\" /grant \"SYSTEM:(OI)(CI)(F)\"", unless => "${::system32}\\icacls.exe \"${::puppet_client_datadir}\" | findstr \"SYSTEM:(OI)(CI)(F)\"", - require => Exec['install_puppet.bat'], + require => Exec['install_puppet.ps1'], } } diff --git a/spec/classes/puppet_agent_windows_install_spec.rb b/spec/classes/puppet_agent_windows_install_spec.rb index 777e6800a..6c6a9bb0c 100644 --- a/spec/classes/puppet_agent_windows_install_spec.rb +++ b/spec/classes/puppet_agent_windows_install_spec.rb @@ -3,7 +3,7 @@ RSpec.describe 'puppet_agent', tag: 'win' do package_version = '1.10.100.1' global_params = { - :package_version => package_version + :package_version => package_version, } {'5.1' => {:expect_arch => 'x86', :appdata => 'C:\Documents and Settings\All Users\Application Data'}, @@ -70,8 +70,8 @@ })} it { is_expected.to contain_class('puppet_agent::windows::install') } - it { is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - %r[#{Regexp.escape("msiexec.exe /qn /norestart /i \"#{values[:appdata]}\\Puppetlabs\\packages\\puppet-agent-#{values[:expect_arch]}.msi\"")}]) + it { is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content( + %r[#{Regexp.escape("\$Source=\'#{values[:appdata]}\\Puppetlabs\\packages\\puppet-agent-#{values[:expect_arch]}.msi\'")}]) } it { is_expected.to contain_exec('fix inheritable SYSTEM perms') } end @@ -83,10 +83,10 @@ {:install_options => ['OPTION1=value1','OPTION2=value2'],}) } it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/msiexec.exe .+ OPTION1=value1 OPTION2=value2/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$InstallArgs='OPTION1=value1 OPTION2=value2'/) } it { - is_expected.not_to contain_file('C:\tmp\install_puppet.bat').with_content(/msiexec.exe .+ REINSTALLMODE="amus"/) + is_expected.not_to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$InstallArgs='REINSTALLMODE="amus"'/) } end end @@ -94,7 +94,7 @@ context 'Default INSTALLMODE Option' do describe 'REINSTALLMODE=amus' do it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/msiexec.exe .+ REINSTALLMODE="amus"/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$InstallArgs='REINSTALLMODE="amus"'/) } end end @@ -105,9 +105,8 @@ {:source => 'https://alternate.com/puppet-agent.msi',}) } it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - /msiexec.exe \/qn \/norestart \/i "https:\/\/alternate.com\/puppet-agent.msi"/) - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/\/l\*vx "C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log"/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Source='https:\/\/alternate.com\/puppet-agent.msi'/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Logfile='C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log'/) } it { is_expected.to contain_exec('fix inheritable SYSTEM perms') } end @@ -117,9 +116,8 @@ {:source => 'C:/tmp/puppet-agent-x64.msi',}) } it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - /msiexec.exe \/qn \/norestart \/i "C:\\tmp\\puppet-agent-x64\.msi"/) - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/\/l\*vx "C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log"/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Source='C:\\tmp\\puppet-agent-x64\.msi'/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Logfile='C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log'/) } it { is_expected.to contain_exec('fix inheritable SYSTEM perms') } end @@ -129,21 +127,8 @@ {:source => 'C:\Temp/ Folder\puppet-agent-x64.msi',}) } it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - /msiexec.exe \/qn \/norestart \/i "C:\\Temp Folder\\puppet-agent-x64\.msi"/) - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/\/l\*vx "C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log"/) - } - it { is_expected.to contain_exec('fix inheritable SYSTEM perms') } - end - - describe 'C:/Temp/ Folder/puppet-agent-x64.msi' do - let(:params) { global_params.merge( - {:source => 'C:/Temp/ Folder/puppet-agent-x64.msi',}) - } - it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - /msiexec.exe \/qn \/norestart \/i "C:\\Temp Folder\\puppet-agent-x64\.msi"/) - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/\/l\*vx "C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log"/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Source='C:\\Temp Folder\\puppet-agent-x64\.msi'/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Logfile='C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log'/) } it { is_expected.to contain_exec('fix inheritable SYSTEM perms') } end @@ -153,22 +138,20 @@ {:source => "\\\\garded\\c$\\puppet-agent-x64.msi",}) } it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - /msiexec.exe \/qn \/norestart \/i "\\\\garded\\c\$\\puppet-agent-x64\.msi"/) - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/\/l\*vx "C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log"/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Source='\\\\garded\\c\$\\puppet-agent-x64\.msi'/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Logfile='C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log'/) } it { is_expected.to contain_exec('fix inheritable SYSTEM perms') } end describe 'default source' do it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - /msiexec.exe \/qn \/norestart \/i "https:\/\/downloads.puppetlabs.com\/windows\/puppet-agent-#{package_version}-#{values[:expect_arch]}\.msi"/) - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/\/l\*vx "C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer\.log"/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Source='https:\/\/downloads.puppetlabs.com\/windows\/puppet-agent-#{package_version}-#{values[:expect_arch]}\.msi'/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Logfile='C:\\tmp\\puppet-\d+_\d+_\d+-\d+_\d+-installer.log'/) } it { - should contain_exec('install_puppet.bat').with { { - 'command' => 'C:\windows\sysnative\cmd.exe /c start /b "C:\tmp\install_puppet.bat" 42', + should contain_exec('install_puppet.ps1').with { { + 'command' => 'C:\windows\sysnative\cmd.exe /c start /b C:\windows\sysnative\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass -NoProfile -NoLogo -NonInteractive -Command C:\tmp\install_puppet.ps1 42', } } } it { @@ -182,10 +165,8 @@ {:source => 'puppet:///puppet_agent/puppet-agent-1.1.0-x86.msi'}) } it { - is_expected.to contain_file('C:\tmp\puppet-agent.msi').with_before('File[C:\tmp\install_puppet.bat]') - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - /msiexec.exe \/qn \/norestart \/i "C:\\tmp\\puppet-agent.msi"/ - ) + is_expected.to contain_file('C:\tmp\puppet-agent.msi').with_before('File[C:\tmp\install_puppet.ps1]') + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Source='C:\\tmp\\puppet-agent.msi'/) } it { is_expected.to contain_exec('fix inheritable SYSTEM perms') } end @@ -197,8 +178,7 @@ {:arch => 'x86'}) } it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content( - /msiexec.exe \/qn \/norestart \/i "https:\/\/downloads.puppetlabs.com\/windows\/puppet-agent-#{package_version}-x86.msi"/ + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$Source='https:\/\/downloads.puppetlabs.com\/windows\/puppet-agent-#{package_version}-x86.msi'/ ) } end @@ -227,7 +207,7 @@ context 'msi_move_locked_files =>' do describe 'default' do it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').without_content(/Move puppetres\.dll/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$UseLockedFilesWorkaround=\$false/) } end @@ -237,7 +217,7 @@ } it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').without_content(/Move puppetres\.dll/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$UseLockedFilesWorkaround=\$false/) } end @@ -247,7 +227,7 @@ } it { - is_expected.to contain_file('C:\tmp\install_puppet.bat').with_content(/Move puppetres\.dll/) + is_expected.to contain_file('C:\tmp\install_puppet.ps1').with_content(/\$UseLockedFilesWorkaround=\$true/) } end end @@ -272,8 +252,8 @@ let(:params) { global_params } it { - is_expected.to contain_exec('install_puppet.bat').with { { - 'command' => 'C:\windows\sysnative\cmd.exe /c start /b C:\windows\system32\cmd.exe /c "C:\tmp\install_puppet.bat" 42', + is_expected.to contain_exec('install_puppet.ps1').with { { + 'command' => 'C:\windows\sysnative\cmd.exe /c start /b C:\windows\system32\cmd.exe /c "C:\tmp\install_puppet.ps1" 42', } } } @@ -285,8 +265,8 @@ let(:params) { global_params } it { - is_expected.to contain_exec('install_puppet.bat').with { { - 'command' => 'C:\windows\sysnative\cmd.exe /c start /b C:\windows\sysnative\cmd.exe /c "C:\tmp\install_puppet.bat" 42', + is_expected.to contain_exec('install_puppet.ps1').with { { + 'command' => 'C:\windows\sysnative\cmd.exe /c start /b C:\windows\sysnative\cmd.exe /c "C:\tmp\install_puppet.ps1" 42', } } } diff --git a/templates/install_puppet.bat.erb b/templates/install_puppet.bat.erb deleted file mode 100644 index f043a7879..000000000 --- a/templates/install_puppet.bat.erb +++ /dev/null @@ -1,83 +0,0 @@ -SET - -set AGENT_PID=%1 -set windowTitle=Puppet Agent Upgrade -title %windowTitle% - -set pid= -for /f "tokens=2" %%a in ('tasklist /v ^| findstr /c:"%windowTitle%"') do set pid=%%a -set pid_path=%~dp0puppet_agent_upgrade.pid - -set environment= -for /f "delims=" %%i in ('puppet config print --section agent environment') do set environment=%%i - -if exist %pid_path% del %pid_path% -@echo %pid%> %pid_path% - -SET /A pid_checks_performed=0 - -:wait_for_pid -REM Wait 5 seconds -ping 127.0.0.1 -n 6 > NUL -wmic path Win32_Process where handle=%AGENT_PID% get handle /format:textvaluelist | findstr /I "Handle=%AGENT_PID%" - -SET /A pid_checks_performed+=1 -REM Wait for a max of 120 seconds (or 24 iterations) before aborting the upgrade -IF %errorlevel% == 0 IF %pid_checks_performed% == 24 ( - REM Adding CustomSource allows EventCreate to work properly - reg add HKLM\SYSTEM\CurrentControlSet\services\eventlog\Application\Puppet /v CustomSource /t REG_DWORD /d 1 /f - EVENTCREATE /T ERROR /L APPLICATION /SO Puppet /ID 101 /D "Puppet agent upgrade failed while waiting for Puppet process [ID: %AGENT_PID%] to exit." - GOTO End -) - -IF %errorlevel% == 0 ( GOTO wait_for_pid ) - -REM This *must* occur after Puppet Agent has finished applying its -REM prior catalog which manages the pxp-agent service state. If not, -REM the catalog includes the PE module which starts the service and -REM sets its startup type, which prevents installs from proceeding. -REM This may fail on agents without pxp-agent, but since this is not -REM run interactively and the next command sets ERRORLEVEL, it's OK. -net stop pxp-agent -REM Same for the Marionette Collective Service and Puppet Agent service -net stop mcollective -net stop puppet - -REM Log the current user token privileges for debugging -whoami /all - -<% if @msi_move_locked_files %> -REM Move puppetres.dll to avoid file locks and service restarts (MODULES-4207) - -REM Puppet-Agent, less than 1.6.0, location -SET PUPPETRES=<%= @env_windows_installdir %>\misc\puppetres.dll -SET RANDFILE=%TEMP%\puppetres-%RANDOM%-1.DLL -IF EXIST "%PUPPETRES%" ( - MOVE /Y "%PUPPETRES%" "%RANDFILE%" - DEL /Q "%RANDFILE%" - - IF EXIST "%PUPPETRES%" ( - ECHO *** WARNING - Unable to move %PUPPETRES% - ) -) - -REM Puppet-Agent, 1.6.0 and above, location -SET "PUPPETRES=<%= @env_windows_installdir %>\puppet\bin\puppetres.dll" -SET RANDFILE=%TEMP%\puppetres-%RANDOM%-2.DLL -IF EXIST "%PUPPETRES%" ( - MOVE /Y "%PUPPETRES%" "%RANDFILE%" - DEL /Q "%RANDFILE%" - - IF EXIST "%PUPPETRES%" ( - ECHO *** WARNING - Unable to move %PUPPETRES% - ) -) -<% end %> - -start /wait msiexec.exe /qn /norestart /i "<%= @_msi_location %>" /l*vx "<%= @_logfile %>" PUPPET_MASTER_SERVER="<%= @_puppet_master %>" PUPPET_AGENT_ENVIRONMENT="%environment%" <% unless @install_dir.to_s.empty? -%>INSTALLDIR="<%= @install_dir %>"<% end -%> <% unless @_agent_startup_mode.to_s.empty? -%>PUPPET_AGENT_STARTUP_MODE="<%= @_agent_startup_mode %>"<% end -%><% unless @_install_options.to_s.empty? -%><% @_install_options.each do |option| -%> <%= option%><% end -%><% end -%> - -:End - -if exist %pid_path% del %pid_path% - -ENDLOCAL diff --git a/templates/install_puppet.ps1.erb b/templates/install_puppet.ps1.erb new file mode 100644 index 000000000..d02013fbb --- /dev/null +++ b/templates/install_puppet.ps1.erb @@ -0,0 +1,260 @@ +# install_puppet.ps1 +<# +.Synopsis + Install or upgrade puppet-agent +.Description + This script will install or upgrade puppet-agent on a windows machine from the MSI file at $Source. + If the script is supplied with $PuppetPID it will wait on the $PuppetPID before attempting to perform + an upgrade. +.Parameter PuppetPID + The process ID of puppet to wait on before executing an upgrade. Note that all puppet processes must shut + down before an installation can occur +.Parameter Source + The location of the new puppet-agent MSI installer +.Parameter Logfile + File location where the installation will log output +.Parameter InstallDir + Optionally change the default location where puppet-agent will be installed +.Parameter PuppetMaster + The location of the puppet master +.Parameter PuppetStartType + Optionally change the default start type of puppet +.Parameter InstallArgs + Provide any extra argmuments to the MSI installation +.Parameter UseLockedFilesWorkaround + Set to $true to enable execution of the puppetres.dll move workaround. See https://tickets.puppetlabs.com/browse/MODULES-4207 +#> +[CmdletBinding()] +param( + # PuppetPID _must_ come first!, this script needs PuppetPID to be a positional parameter to execute + # correctly from the module. + [parameter(Position=0)] + [String] $PuppetPID, + [String] $Source='<%= @_msi_location %>', + [String] $Logfile='<%= @_logfile %>', + [String] $InstallDir='<%= @install_dir %>', + [String] $PuppetMaster='<%= @_puppet_master %>', + [String] $PuppetStartType='<%= @_agent_startup_mode %>', + [String] $InstallArgs='<%= @_install_options %>', + [String] $InstallScriptPIDFile='<%= @_install_pid_file_loc %>', + [Bool] $UseLockedFilesWorkaround=<%= @_move_dll_workaround %> +) +# Find-InstallDir, Move-PuppetresDLL and Reset-PuppetresDLL serve as a workaround for older +# installations of puppet: we used to need to move puppetres.dll out of the way during puppet +# upgrades because the file would lock and cause network stack restarts. +# See https://tickets.puppetlabs.com/browse/MODULES-4207 +<# +.Synopsis + Fetch the location of the puppet installation from the registry +#> +function Script:Find-InstallDir { + begin { + if (Get-ItemProperty -Path "HKLM:\SOFTWARE\Puppet Labs\Puppet" -Name RememberedInstallDir64 -ErrorAction SilentlyContinue) { + return (Get-ItemProperty -Path "HKLM:\SOFTWARE\Puppet Labs\Puppet").RememberedInstallDir64 + } elseif (Get-ItemProperty -Path "HKLM:\SOFTWARE\Puppet Labs\Puppet" -Name RememberedInstallDir -ErrorAction SilentlyContinue) { + return (Get-ItemProperty -Path "HKLM:\SOFTWARE\Puppet Labs\Puppet").RememberedInstallDir + } else { + return $null + } + } +} + +<# +.Synopsis + Move/rename puppetres.dll to a temporary location +#> +function Script:Move-PuppetresDLL { + begin { + $rand_string = [String[]](Get-Random) + $temp_puppetres = "$env:temp\puppetres$rand_string.dll" + # _Never_ use the $InstallDir top-level parameter to try and find the InstallDir + # for the puppetres workaround. The workaround should _always_ fetch the InstallDir + # from the registry. This is so a user can specify a different InstallDir than the + # directory where the current package is installed and the workaround will still work + # for the already installed package + $InstallDir = Find-InstallDir + if (Test-Path "$InstallDir\puppet\bin\puppetres.dll") { + Write-Log "Moving puppetres.dll to $temp_puppetres" + Move-Item -Path "$InstallDir\puppet\bin\puppetres.dll" -Destination $temp_puppetres + } + return $temp_puppetres + } +} + +<# +.Synopsis + Restore puppetres.dll to the original location. This should only be used when an installation fails +.Parameter temp_puppetres + Location of the temporary puppetres.dll file. +#> +function Script:Reset-PuppetresDLL { + param( + [Parameter(Mandatory=$true)] + [String] $temp_puppetres + ) + begin { + # _Never_ use the $InstallDir top-level parameter to try and find the InstallDir + # for the puppetres workaround. The workaround should _always_ fetch the InstallDir + # from the registry. This is so a user can specify a different InstallDir than the + # directory where the current package is installed and the workaround will still work + # for the already installed package + $InstallDir = Find-InstallDir + if (Test-Path $temp_puppetres -and -not (Test-Path "$InstallDir\puppet\bin\puppetres.dll")) { + Write-Log "Restoring puppetres.dll" + Move-Item -Path $temp_puppetres -Destination "$InstallDir\puppet\bin\puppetres.dll" + } + } +} + +<# +.Synopsis + Write message to location of $Logfile +.Parameter message + String containing the message to write +#> +function Script:Write-Log { + param( + [Parameter(Mandatory=$true)] + [String] $message + ) + begin { + $message | Out-File -FilePath $Logfile -Append + } +} + +<# +.Synopsis + Take control of the installation lockfile, fail if the lock + already exists +.Parameter install_pid_lock + Location of the installation pid lock file +#> +function Script:Lock-Installation { + param( + [Parameter(Mandatory=$true)] + [String] $install_pid_lock + ) + begin { + Write-Log "Locking installation" + if (Test-Path $install_pid_lock) { + Write-Log "Another process has control of $install_pid_lock! Cannot lock, exiting..." + throw + } else { + $PID | Out-File -NoClobber -FilePath $install_pid_lock + } + Write-Log "Locked" + } +} + +<# +.Synopsis + Release control of the installation lockfile +.Parameter install_pid_lock + Location of the installation pid lock file +#> +function Script:Unlock-Installation { + param( + [Parameter(Mandatory=$true)] + [String] $install_pid_lock + ) + begin { + Write-Log "Unlocking installation" + if (Test-Path $install_pid_lock) { + if ((Get-Content $install_pid_lock) -ne $PID) { + Write-Log "Another process has control of $install_pid_lock! Cannot unlock, exiting..." + } else { + try { + Remove-Item -Force $install_pid_lock + Write-Log "Unlocked" + } catch { + Write-Log $_ + } + } + } + } +} + +# ************** Execution start ************** +$ErrorActionPreference = "Stop" +$service_names=@( + "puppet", + "pxp-agent", + "mcollective" +) +try { + Write-Log "Installation PID:$PID" + $install_pid_lock = Join-Path -Path (Split-Path -Parent (puppet.bat config print agent_catalog_run_lockfile)) -ChildPath 'puppet_install.pid' + Lock-Installation $install_pid_lock + # Wait for the puppet run to finish + # + # We *must* wait for Puppet Agent to finished applying its prior catalog + # because puppet manages the pxp-agent service state. If puppet restarts + # the pxp-agent service it will prevent installs from proceeding. + Write-Log "Waiting for puppet to stop, PID:$PuppetPID" + try { + Get-Process -ID $PuppetPID -ErrorAction SilentlyContinue | Wait-Process -Timeout 120 + # We have to catch all exceptions here since the Error action preference of 'stop' will cause wait-process + # to actually throw [System.Management.Automation.ActionPreferenceStopException] rather than the actual + # exceptions. Once inside the catch block, the value of $_ is correctly set as the actual exception + } catch { + if ($_.Exception.GetType() -eq [System.TimeoutException]) { + Write-Log "ERROR: timed out waiting for puppet run to finish!" + throw $_ + } else { + Write-Log "ERROR: failed while trying to wait for puppet process with $($_.Exception.GetType())" + throw $_ + } + } + # We *must* shutdown all puppet-agent services for the MSI installation to correctly + # work without requiring a restart. + foreach($service in $service_names) { + $serv_exists = Get-Service -Name $service -ErrorAction SilentlyContinue + if ($serv_exists) { + Write-Log "Stopping $($service) before upgrade" + Stop-Service $service + } + } + if ($UseLockedFilesWorkaround) { + $temp_puppetres = Move-PuppetresDLL + } + $msi_arguments = "/qn /norestart /i `"$Source`" /l*vx+ `"$Logfile`"" + $msi_arguments += " PUPPET_AGENT_ENVIRONMENT=`"$((puppet.bat config print --section agent environment) -replace '\s','')`"" + if ($InstallDir) { + $msi_arguments += " INSTALLDIR=`"$InstallDir`"" + } + if ($PuppetMaster) { + $msi_arguments += " PUPPET_MASTER_SERVER=`"$PuppetMaster`"" + } + if ($PuppetStartType) { + $msi_arguments += " PUPPET_AGENT_STARTUP_MODE=`"$PuppetStartType`"" + } + $msi_arguments += " $InstallArgs" + Write-Log "Beginning MSI installation with Arguments: $msi_arguments" + Write-Log "****************************** Begin msiexec.exe output ******************************" + # $msiexec_proc = Start-Process -FilePath msiexec.exe -PassThru -ArgumentList $msi_arguments + # $msiexec_proc.WaitForExit() + $startInfo = New-Object System.Diagnostics.ProcessStartInfo('msiexec.exe', $msi_arguments) + $startInfo.UseShellExecute = $false + $startInfo.CreateNoWindow = $true + $invocationId = [Guid]::NewGuid().ToString() + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $startInfo + $process.EnableRaisingEvents = $true + $exitedEvent = Register-ObjectEvent -InputObject $process -EventName 'Exited' -SourceIdentifier $invocationId + $process.Start() | Out-Null + # park current thread until the PS event is signaled upon process exit + # OR the timeout has elapsed + $waitResult = Wait-Event -SourceIdentifier $invocationId + Write-Log "****************************** End msiexec.exe output ******************************" + if ($process.ExitCode -ne 0){ + Write-Log "ERROR: msiexec.exe installation failed!!! Return code $($msi_exec.ExitCode)" + throw + } +} catch { + Write-Log "ERROR: $_" + if ($UseLockedFilesWorkaround) { + Reset-PuppetresDLL $temp_puppetres + } +} finally { + Unlock-Installation $install_pid_lock +}