module Beaker module DSL module InstallUtils # # This module contains methods useful for Windows installs # module WindowsUtils # Given a host, returns it's system TEMP path # # @param [Host] host An object implementing {Beaker::Hosts}'s interface. # # @return [String] system temp path def get_system_temp_path(host) host.system_temp_path end alias get_temp_path get_system_temp_path # Generates commands to be inserted into a Windows batch file to launch an MSI install # @param [String] msi_path The path of the MSI - can be a local Windows style file path like # c:\temp\puppet.msi OR a url like https://download.com/puppet.msi or file://c:\temp\puppet.msi # @param [Hash{String=>String}] msi_opts MSI installer options # See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties # @param [String] log_path The path to write the MSI log - must be a local Windows style file path # # @api private def msi_install_script(msi_path, msi_opts, log_path) # msiexec requires backslashes in file paths launched under cmd.exe start /w url_pattern = %r{^(https?|file)://} msi_path = msi_path.gsub(%r{/}, '\\') if msi_path !~ url_pattern msi_params = msi_opts.map { |k, v| "#{k}=#{v}" }.join(' ') # msiexec requires quotes around paths with backslashes - c:\ or file://c:\ # not strictly needed for http:// but it simplifies this code batch_contents = <<~BATCH start /w msiexec.exe /i "#{msi_path}" /qn /L*V #{log_path} #{msi_params} exit /B %errorlevel% BATCH end # Given a host, path to MSI and MSI options, will create a batch file # on the host, returning the path to the randomized batch file and # the randomized log file # # @param [Host] host An object implementing {Beaker::Hosts}'s interface. # @param [String] msi_path The path of the MSI - can be a local Windows # style file path like c:\temp\puppet.msi OR a url like # https://download.com/puppet.msi or file://c:\temp\puppet.msi # @param [Hash{String=>String}] msi_opts MSI installer options # See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties # # @api private # @return [String, String] path to the batch file, patch to the log file def create_install_msi_batch_on(host, msi_path, msi_opts) timestamp = Time.new.strftime('%Y-%m-%d_%H.%M.%S') tmp_path = host.system_temp_path tmp_path.gsub!('/', '\\') batch_name = "install-puppet-msi-#{timestamp}.bat" batch_path = "#{tmp_path}#{host.scp_separator}#{batch_name}" log_path = "#{tmp_path}\\install-puppet-#{timestamp}.log" Tempfile.open(batch_name) do |tmp_file| batch_contents = msi_install_script(msi_path, msi_opts, log_path) File.open(tmp_file.path, 'w') { |file| file.puts(batch_contents) } host.do_scp_to(tmp_file.path, batch_path, {}) end [batch_path, log_path] end # Given hosts construct a PATH that includes puppetbindir, facterbindir and hierabindir # @param [Host, Array<Host>, String, Symbol] hosts One or more hosts to act upon, # or a role (String or Symbol) that identifies one or more hosts. # @param [String] msi_path The path of the MSI - can be a local Windows style file path like # c:\temp\puppet.msi OR a url like https://download.com/puppet.msi or file://c:\temp\puppet.msi # @param [Hash{String=>String}] msi_opts MSI installer options # See https://docs.puppetlabs.com/guides/install_puppet/install_windows.html#msi-properties # @option msi_opts [String] INSTALLIDIR Where Puppet and its dependencies should be installed. # (Defaults vary based on operating system and intaller architecture) # Requires Puppet 2.7.12 / PE 2.5.0 # @option msi_opts [String] PUPPET_MASTER_SERVER The hostname where the puppet master server can be reached. # (Defaults to puppet) # Requires Puppet 2.7.12 / PE 2.5.0 # @option msi_opts [String] PUPPET_CA_SERVER The hostname where the CA puppet master server can be reached, if you are using multiple masters and only one of them is acting as the CA. # (Defaults the value of PUPPET_MASTER_SERVER) # Requires Puppet 2.7.12 / PE 2.5.0 # @option msi_opts [String] PUPPET_AGENT_CERTNAME The node’s certificate name, and the name it uses when requesting catalogs. This will set a value for # (Defaults to the node's fqdn as discovered by facter fqdn) # Requires Puppet 2.7.12 / PE 2.5.0 # @option msi_opts [String] PUPPET_AGENT_ENVIRONMENT The node’s environment. # (Defaults to production) # Requires Puppet 3.3.1 / PE 3.1.0 # @option msi_opts [String] PUPPET_AGENT_STARTUP_MODE Whether the puppet agent service should run (or be allowed to run) # (Defaults to Manual - valid values are Automatic, Manual or Disabled) # Requires Puppet 3.4.0 / PE 3.2.0 # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_USER Whether the puppet agent service should run (or be allowed to run) # (Defaults to LocalSystem) # Requires Puppet 3.4.0 / PE 3.2.0 # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_PASSWORD The password to use for puppet agent’s user account # (No default) # Requires Puppet 3.4.0 / PE 3.2.0 # @option msi_opts [String] PUPPET_AGENT_ACCOUNT_DOMAIN The domain of puppet agent’s user account. # (Defaults to .) # Requires Puppet 3.4.0 / PE 3.2.0 # @option opts [Boolean] :debug output the MSI installation log when set to true # otherwise do not output log (false; default behavior) # # @example # install_msi_on(hosts, 'c:\puppet.msi', {:debug => true}) # # @api private def install_msi_on(hosts, msi_path, msi_opts = {}, opts = {}) block_on hosts do |host| msi_opts['PUPPET_AGENT_STARTUP_MODE'] ||= 'Manual' batch_path, log_file = create_install_msi_batch_on(host, msi_path, msi_opts) # Powershell command looses an escaped slash resulting in cygwin relative path # See https://github.com/puppetlabs/beaker/pull/1626#issuecomment-621341555 log_file_escaped = log_file.gsub('\\', '\\\\\\') # begin / rescue here so that we can reuse existing error msg propagation begin # 1641 = ERROR_SUCCESS_REBOOT_INITIATED # 3010 = ERROR_SUCCESS_REBOOT_REQUIRED on host, Command.new("\"#{batch_path}\"", [], { cmdexe: true }), acceptable_exit_codes: [0, 1641, 3010] rescue StandardError logger.info(file_contents_on(host, log_file_escaped)) raise end logger.info(file_contents_on(host, log_file_escaped)) if opts[:debug] unless host.is_cygwin? # Enable the PATH updates host.close # Some systems require a full reboot to trigger the enabled path host.reboot unless on(host, Command.new('puppet -h', [], { cmdexe: true }), accept_all_exit_codes: true).exit_code == 0 end # verify service status post install # if puppet service exists, then pe-puppet is not queried # if puppet service does not exist, pe-puppet is queried and that exit code is used # therefore, this command will always exit 0 if either service is installed # # We also take advantage of this output to verify the startup # settings are honored as supplied to the MSI on host, Command.new('sc qc puppet || sc qc pe-puppet', [], { cmdexe: true }) do |result| output = result.stdout startup_mode = msi_opts['PUPPET_AGENT_STARTUP_MODE'].upcase search = case startup_mode when 'AUTOMATIC' { code: 2, name: 'AUTO_START' } when 'MANUAL' { code: 3, name: 'DEMAND_START' } when 'DISABLED' { code: 4, name: 'DISABLED' } end if output !~ /^\s+START_TYPE\s+:\s+#{search[:code]}\s+#{search[:name]}/ raise "puppet service startup mode did not match supplied MSI option '#{startup_mode}'" end end # (PA-514) value for PUPPET_AGENT_STARTUP_MODE should be present in # registry and honored after install/upgrade. reg_key = if host.is_x86_64? 'HKLM\\SOFTWARE\\Wow6432Node\\Puppet Labs\\PuppetInstaller' else 'HKLM\\SOFTWARE\\Puppet Labs\\PuppetInstaller' end reg_query_command = %(reg query "#{reg_key}" /v "RememberedPuppetAgentStartupMode" | findstr #{msi_opts['PUPPET_AGENT_STARTUP_MODE']}) on host, Command.new(reg_query_command, [], { cmdexe: true }) # emit the misc/versions.txt file which contains component versions for # puppet, facter, hiera, pxp-agent, packaging and vendored Ruby [ "'%PROGRAMFILES%\\Puppet Labs\\puppet\\misc\\versions.txt'", "'%PROGRAMFILES(X86)%\\Puppet Labs\\puppet\\misc\\versions.txt'", ].each do |path| result = on(host, "cmd /c type #{path}", accept_all_exit_codes: true) if result.exit_code == 0 logger.info(result.stdout) break end end end end # Installs a specified msi path on given hosts # @param [Host, Array<Host>, String, Symbol] hosts One or more hosts to act upon, # or a role (String or Symbol) that identifies one or more hosts. # @param [String] msi_path The path of the MSI - can be a local Windows style file path like # c:\temp\foo.msi OR a url like https://download.com/foo.msi or file://c:\temp\foo.msi # @param [Hash{String=>String}] msi_opts MSI installer options # @option opts [Boolean] :debug output the MSI installation log when set to true # otherwise do not output log (false; default behavior) # # @example # generic_install_msi_on(hosts, 'https://releases.hashicorp.com/vagrant/1.8.4/vagrant_1.8.4.msi', {}, {:debug => true}) # # @api private def generic_install_msi_on(hosts, msi_path, msi_opts = {}, opts = {}) block_on hosts do |host| batch_path, log_file = create_install_msi_batch_on(host, msi_path, msi_opts) # Powershell command looses an escaped slash resulting in cygwin relative path # See https://github.com/puppetlabs/beaker/pull/1626#issuecomment-621341555 log_file_escaped = log_file.gsub('\\', '\\\\\\') # begin / rescue here so that we can reuse existing error msg propagation begin # 1641 = ERROR_SUCCESS_REBOOT_INITIATED # 3010 = ERROR_SUCCESS_REBOOT_REQUIRED on host, Command.new("\"#{batch_path}\"", [], { cmdexe: true }), acceptable_exit_codes: [0, 1641, 3010] rescue StandardError logger.info(file_contents_on(host, log_file_escaped)) raise end logger.info(file_contents_on(host, log_file_escaped)) if opts[:debug] host.close unless host.is_cygwin? end end end end end end