Module: BeakerPuppetHelpers::WindowsUtils

Defined in:
lib/beaker_puppet_helpers/windows_utils.rb

Overview

This module contains methods useful for Windows installs

Instance Method Summary collapse

Instance Method Details

#create_install_msi_batch_on(host, msi_path, msi_opts) ⇒ String

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

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

Parameters:

  • host (Host)

    An object implementing Beaker::Hosts‘s interface.

  • msi_path (String)

    The path of the MSI - can be a local Windows style file path like c:temppuppet.msi OR a url like download.com/puppet.msi or file://c:temppuppet.msi

  • msi_opts (Hash{String=>String})

    MSI installer options

Returns:

  • (String, String)

    path to the batch file, patch to the log file



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/beaker_puppet_helpers/windows_utils.rb', line 118

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.tr('/', '\\')

  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

#generic_install_msi_on(hosts, msi_path, msi_opts: {}, opts: {}) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Installs a specified msi path on given hosts

Examples:

generic_install_msi_on(hosts, 'https://releases.hashicorp.com/vagrant/1.8.4/vagrant_1.8.4.msi', msi_opts: {}, opts: {:debug => true})

Parameters:

  • hosts (Host, Array<Host>, String, Symbol)

    One or more hosts to act upon, or a role (String or Symbol) that identifies one or more hosts.

  • msi_path (String)

    The path of the MSI - can be a local Windows style file path like c:tempfoo.msi OR a url like download.com/foo.msi or file://c:tempfoo.msi can also be a collection like ‘puppet’, ‘puppet8’, ‘openvox’, or ‘openvox8’

  • msi_opts (Hash{String=>String}) (defaults to: {})

    MSI installer options

  • opts (Hash) (defaults to: {})

    Options hash to control installation behavior.



269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
# File 'lib/beaker_puppet_helpers/windows_utils.rb', line 269

def generic_install_msi_on(hosts, msi_path, msi_opts: {}, opts: {})
  # If the msi patch matches a collection, get the url for the latest msi available for that collection
  expanded_msi_path = /^(puppet|openvox)\d*$/.match?(msi_path) ? get_agent_package_url(msi_path) : msi_path

  block_on hosts do |host|
    batch_path, log_file = create_install_msi_batch_on(host, expanded_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, Beaker::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

#get_agent_package_url(collection = 'openvox') ⇒ String

Given the puppet collection, returns the url of the newest msi available in the appropriate repo

Parameters:

  • The (String)

    collection to install. The default (openvox) is the latest available version. Can also be openvox8, puppet8 and others.

Returns:

  • (String)

    url of the newest msi available in the package repo



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# File 'lib/beaker_puppet_helpers/windows_utils.rb', line 29

def get_agent_package_url(collection = 'openvox')
  windows_package_base_url =
    if collection.start_with?('puppet')
      'https://downloads.puppetlabs.com/windows/'
    elsif collection.start_with?('openvox')
      'https://downloads.voxpupuli.org/windows/'
    else
      raise "Unsupported collection: #{collection}"
    end
  # If the collection ends in a number, we can infer the package url directly
  if /\d+$/.match?(collection)
    windows_package_url = "#{windows_package_base_url}#{collection}/"
  else
    # Obtain the list of collections from the appropriate base url and pick the latest
    base_url = URI.parse(windows_package_base_url)
    base_response = Net::HTTP.get_response(base_url)
    raise "Failed to fetch URL: #{base_response.code} #{base_response.message}" unless base_response.is_a?(Net::HTTPSuccess)

    base_doc = Nokogiri::HTML(base_response.body)
    collection_dirs = base_doc.css('a').filter_map { |a| a['href'] }.grep(/^#{collection}\d+/)
    raise "No collections found at #{base_url} for colleciton #{collection}" if collection_dirs.empty?

    latest_collection = collection_dirs.max_by do |collection_version|
      # Grab the digits before the slash and convert to integer
      collection_version[/\d+/].to_i
    end
    windows_package_url = "#{windows_package_base_url}#{latest_collection}"
  end
  url = URI.parse(windows_package_url)
  response = Net::HTTP.get_response(url)

  # Fetch and parse the page
  raise "Failed to fetch URL: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)

  doc = Nokogiri::HTML(response.body)

  # Create the regex for the agent package
  base_collection_name = collection.gsub(/\d+$/, '')
  agent_regex = /#{base_collection_name}-agent-(\d+\.\d+\.\d+)-.*\.msi$/i
  # Extract all hrefs that look like the appropriate MSI files
  files = doc.css('a').filter_map { |a| a['href'] }.grep(agent_regex)

  raise "No MSI files found at #{windows_package_url}" if files.empty?

  latest_msi = files.max_by do |file|
    version_str = file.match(agent_regex)[1]
    Gem::Version.new(version_str)
  end

  # Remove index.html if it exists in the windows_package_url
  windows_package_repo = windows_package_url.sub(/index\.html$/, '')
  # Return the full url to the latest msi
  "#{windows_package_repo}#{latest_msi}"
end

#get_system_temp_path(host) ⇒ String Also known as: get_temp_path

Given a host, returns it’s system TEMP path

Parameters:

  • host (Host)

    An object implementing Beaker::Hosts‘s interface.

Returns:

  • (String)

    system temp path



17
18
19
# File 'lib/beaker_puppet_helpers/windows_utils.rb', line 17

def get_system_temp_path(host)
  host.system_temp_path
end

#install_msi_on(hosts, msi_path, msi_opts: {}, opts: {}) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Installs a specified MSI package on given hosts.

Examples:

install_msi_on(hosts, 'c:\\puppet.msi', msi_opts: {'PUPPET_AGENT_STARTUP_MODE' => 'Manual'}, opts: {:debug => true})

Parameters:

  • hosts (Host, Array<Host>, String, Symbol)

    One or more hosts to act upon, or a role (String or Symbol) that identifies one or more hosts.

  • msi_path (String)

    The path of the MSI - can be a local Windows style file path like c:temppuppet.msi OR a url like download.com/puppet.msi or file://c:temppuppet.msi can also be a collection like ‘puppet’, ‘puppet8’, ‘openvox’, or ‘openvox8’

  • msi_opts (Hash{String=>String}) (defaults to: {})

    MSI installer options

  • opts (Hash) (defaults to: {})

    Options hash to control installation behavior.

Options Hash (msi_opts:):

  • INSTALLDIR (String)

    Where Puppet and its dependencies should be installed. (Defaults vary based on operating system and installer architecture) Requires Puppet 2.7.12 / PE 2.5.0

  • PUPPET_MASTER_SERVER (String)

    The hostname where the puppet master server can be reached. (Defaults to puppet) Requires Puppet 2.7.12 / PE 2.5.0

  • PUPPET_CA_SERVER (String)

    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

  • PUPPET_AGENT_CERTNAME (String)

    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

  • PUPPET_AGENT_ENVIRONMENT (String)

    The node’s environment. (Defaults to production) Requires Puppet 3.3.1 / PE 3.1.0

  • PUPPET_AGENT_STARTUP_MODE (String)

    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

  • PUPPET_AGENT_ACCOUNT_USER (String)

    Whether the puppet agent service should run (or be allowed to run) (Defaults to LocalSystem) Requires Puppet 3.4.0 / PE 3.2.0

  • PUPPET_AGENT_ACCOUNT_PASSWORD (String)

    The password to use for puppet agent’s user account (No default) Requires Puppet 3.4.0 / PE 3.2.0

  • PUPPET_AGENT_ACCOUNT_DOMAIN (String)

    The domain of puppet agent’s user account. (Defaults to .) Requires Puppet 3.4.0 / PE 3.2.0



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/beaker_puppet_helpers/windows_utils.rb', line 177

def install_msi_on(hosts, msi_path, msi_opts: {}, opts: {})
  # If the msi patch matches a collection, get the url for the latest msi available for that collection
  expanded_msi_path = /^(puppet|openvox)\d*$/.match?(msi_path) ? get_agent_package_url(msi_path) : msi_path
  block_on hosts do |host|
    msi_opts['PUPPET_AGENT_STARTUP_MODE'] ||= 'Manual'
    batch_path, log_file = create_install_msi_batch_on(host, expanded_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, Beaker::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, Beaker::Command.new('puppet -h', [], { cmdexe: true }),
                            accept_all_exit_codes: true).exit_code.zero?
    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, Beaker::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 # rubocop:disable Style/HashLikeCase
               when 'AUTOMATIC'
                 { code: 2, name: 'AUTO_START' }
               when 'MANUAL'
                 { code: 3, name: 'DEMAND_START' }
               when 'DISABLED'
                 { code: 4, name: 'DISABLED' }
               end

      raise "puppet service startup mode did not match supplied MSI option '#{startup_mode}'" unless /^\s+START_TYPE\s+:\s+#{search[:code]}\s+#{search[:name]}/.match?(output)
    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, Beaker::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.zero?
        logger.info(result.stdout)
        break
      end
    end
  end
end

#msi_install_script(msi_path, msi_opts, log_path) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Generates commands to be inserted into a Windows batch file to launch an MSI install

Parameters:

  • msi_path (String)

    The path of the MSI - can be a local Windows style file path like C:WindowsTemppuppet-agent.msi OR a url like download.com/puppet.msi or file://C:WindowsTemppuppet-agent.msi

  • msi_opts (Hash{String=>String})

    MSI installer options

  • log_path (String)

    The path to write the MSI log - must be a local Windows style file path



91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/beaker_puppet_helpers/windows_utils.rb', line 91

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.tr('/', '\\') unless msi_path&.match?(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
  "    start /w msiexec.exe /i \"\#{msi_path}\" /qn /L*V \#{log_path} \#{msi_params}\n    exit /B %errorlevel%\n  BATCH\nend\n"