beaker_puppet_helpers 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +14 -14
- data/.github/workflows/release.yml +1 -1
- data/.github/workflows/test.yml +5 -2
- data/CHANGELOG.md +21 -0
- data/Gemfile +2 -0
- data/beaker_puppet_helpers.gemspec +4 -2
- data/lib/beaker_puppet_helpers/windows_utils.rb +295 -0
- data/lib/beaker_puppet_helpers.rb +2 -0
- data/spec/beaker_puppet_helpers/windows_utils_spec.rb +403 -0
- data/spec/helpers.rb +92 -0
- data/spec/spec_helper.rb +11 -0
- metadata +38 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 43c0fc876e6e5ec072e587d7637ec6c9cfbed07934d2dea55732383cda9e24ee
|
4
|
+
data.tar.gz: 63fed535614ee30e2e1e01ee5dda0786d55f8681b5dd66b531eabebe52498104
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8b78e838186e5cdf9343ee4ad309910845076c3d57cd1ff3fcc6b1b04d3f1d0f8237d2278735ac47e1d33cb4eedfc0ae8318eba30380a87962c0ee8fbee8050b
|
7
|
+
data.tar.gz: f9a3fda821c51b22486d2c8a752051976c8d235ed20e0ab294544fd13f5f8fcc321aaf3945c6da6a0508a7123d5ab3d4bb071559327650a460238fde5e62c310
|
data/.github/dependabot.yml
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
version: 2
|
2
2
|
updates:
|
3
|
-
# raise PRs for gem updates
|
4
|
-
- package-ecosystem: bundler
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
3
|
+
# raise PRs for gem updates
|
4
|
+
- package-ecosystem: bundler
|
5
|
+
directory: "/"
|
6
|
+
schedule:
|
7
|
+
interval: daily
|
8
|
+
time: "13:00"
|
9
|
+
open-pull-requests-limit: 10
|
10
10
|
|
11
|
-
# Maintain dependencies for GitHub Actions
|
12
|
-
- package-ecosystem: github-actions
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
11
|
+
# Maintain dependencies for GitHub Actions
|
12
|
+
- package-ecosystem: github-actions
|
13
|
+
directory: "/"
|
14
|
+
schedule:
|
15
|
+
interval: daily
|
16
|
+
time: "13:00"
|
17
|
+
open-pull-requests-limit: 10
|
data/.github/workflows/test.yml
CHANGED
@@ -7,6 +7,9 @@ on:
|
|
7
7
|
branches:
|
8
8
|
- master
|
9
9
|
|
10
|
+
permissions:
|
11
|
+
contents: read
|
12
|
+
|
10
13
|
env:
|
11
14
|
BEAKER_HYPERVISOR: docker
|
12
15
|
|
@@ -16,7 +19,7 @@ jobs:
|
|
16
19
|
outputs:
|
17
20
|
ruby: ${{ steps.ruby.outputs.versions }}
|
18
21
|
steps:
|
19
|
-
- uses: actions/checkout@
|
22
|
+
- uses: actions/checkout@v5
|
20
23
|
- name: Install Ruby ${{ matrix.ruby }}
|
21
24
|
uses: ruby/setup-ruby@v1
|
22
25
|
with:
|
@@ -36,7 +39,7 @@ jobs:
|
|
36
39
|
ruby: ${{ fromJSON(needs.rubocop_and_matrix.outputs.ruby) }}
|
37
40
|
name: Ruby ${{ matrix.ruby }}
|
38
41
|
steps:
|
39
|
-
- uses: actions/checkout@
|
42
|
+
- uses: actions/checkout@v5
|
40
43
|
- name: Install Ruby ${{ matrix.ruby }}
|
41
44
|
uses: ruby/setup-ruby@v1
|
42
45
|
with:
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,27 @@
|
|
2
2
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
4
4
|
|
5
|
+
## [3.1.1](https://github.com/voxpupuli/beaker_puppet_helpers/tree/3.1.1) (2025-09-22)
|
6
|
+
|
7
|
+
[Full Changelog](https://github.com/voxpupuli/beaker_puppet_helpers/compare/3.1.0...3.1.1)
|
8
|
+
|
9
|
+
**Fixed bugs:**
|
10
|
+
|
11
|
+
- open-uri: Move from development to runtime dependency [\#83](https://github.com/voxpupuli/beaker_puppet_helpers/pull/83) ([bastelfreak](https://github.com/bastelfreak))
|
12
|
+
- nokogiri: Move from dev to runtime dependency [\#82](https://github.com/voxpupuli/beaker_puppet_helpers/pull/82) ([bastelfreak](https://github.com/bastelfreak))
|
13
|
+
|
14
|
+
## [3.1.0](https://github.com/voxpupuli/beaker_puppet_helpers/tree/3.1.0) (2025-09-22)
|
15
|
+
|
16
|
+
[Full Changelog](https://github.com/voxpupuli/beaker_puppet_helpers/compare/3.0.0...3.1.0)
|
17
|
+
|
18
|
+
**Implemented enhancements:**
|
19
|
+
|
20
|
+
- \(\#77\) Add support for installing openvox on windows [\#78](https://github.com/voxpupuli/beaker_puppet_helpers/pull/78) ([michael-riddle](https://github.com/michael-riddle))
|
21
|
+
|
22
|
+
**Fixed bugs:**
|
23
|
+
|
24
|
+
- gemspec: Dont set upper limit for ruby version [\#80](https://github.com/voxpupuli/beaker_puppet_helpers/pull/80) ([bastelfreak](https://github.com/bastelfreak))
|
25
|
+
|
5
26
|
## [3.0.0](https://github.com/voxpupuli/beaker_puppet_helpers/tree/3.0.0) (2025-08-07)
|
6
27
|
|
7
28
|
[Full Changelog](https://github.com/voxpupuli/beaker_puppet_helpers/compare/2.3.0...3.0.0)
|
data/Gemfile
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Gem::Specification.new do |s|
|
4
4
|
s.name = 'beaker_puppet_helpers'
|
5
|
-
s.version = '3.
|
5
|
+
s.version = '3.1.1'
|
6
6
|
s.authors = ['Vox Pupuli']
|
7
7
|
s.email = ['voxpupuli@groups.io']
|
8
8
|
s.homepage = 'https://github.com/voxpupuli/beaker_puppet_helpers'
|
@@ -10,13 +10,15 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.description = 'For use for the Beaker acceptance testing tool'
|
11
11
|
s.license = 'Apache-2.0'
|
12
12
|
|
13
|
-
s.required_ruby_version = '>= 3.2'
|
13
|
+
s.required_ruby_version = '>= 3.2'
|
14
14
|
|
15
15
|
s.files = `git ls-files`.split("\n")
|
16
16
|
s.require_paths = ['lib']
|
17
17
|
|
18
18
|
# Run time dependencies
|
19
19
|
s.add_dependency 'beaker', '>= 5.8.1', '< 8'
|
20
|
+
s.add_dependency 'nokogiri', '~> 1.18', '>= 1.18.10'
|
21
|
+
s.add_dependency 'open-uri', '< 0.6'
|
20
22
|
s.add_dependency 'puppet-modulebuilder', '>= 0.3', '< 3'
|
21
23
|
|
22
24
|
s.add_development_dependency 'voxpupuli-rubocop', '~> 4.1.0'
|
@@ -0,0 +1,295 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'open-uri'
|
6
|
+
|
7
|
+
module BeakerPuppetHelpers
|
8
|
+
#
|
9
|
+
# This module contains methods useful for Windows installs
|
10
|
+
#
|
11
|
+
module WindowsUtils
|
12
|
+
# Given a host, returns it's system TEMP path
|
13
|
+
#
|
14
|
+
# @param [Host] host An object implementing {Beaker::Hosts}'s interface.
|
15
|
+
#
|
16
|
+
# @return [String] system temp path
|
17
|
+
def get_system_temp_path(host)
|
18
|
+
host.system_temp_path
|
19
|
+
end
|
20
|
+
alias get_temp_path get_system_temp_path
|
21
|
+
|
22
|
+
# Given the puppet collection, returns the url of the newest msi available in the appropriate repo
|
23
|
+
#
|
24
|
+
# @param [String]
|
25
|
+
# The collection to install. The default (openvox) is the latest
|
26
|
+
# available version. Can also be openvox8, puppet8 and others.
|
27
|
+
#
|
28
|
+
# @return [String] url of the newest msi available in the package repo
|
29
|
+
def get_agent_package_url(collection = 'openvox')
|
30
|
+
windows_package_base_url =
|
31
|
+
if collection.start_with?('puppet')
|
32
|
+
'https://downloads.puppetlabs.com/windows/'
|
33
|
+
elsif collection.start_with?('openvox')
|
34
|
+
'https://downloads.voxpupuli.org/windows/'
|
35
|
+
else
|
36
|
+
raise "Unsupported collection: #{collection}"
|
37
|
+
end
|
38
|
+
# If the collection ends in a number, we can infer the package url directly
|
39
|
+
if /\d+$/.match?(collection)
|
40
|
+
windows_package_url = "#{windows_package_base_url}#{collection}/"
|
41
|
+
else
|
42
|
+
# Obtain the list of collections from the appropriate base url and pick the latest
|
43
|
+
base_url = URI.parse(windows_package_base_url)
|
44
|
+
base_response = Net::HTTP.get_response(base_url)
|
45
|
+
raise "Failed to fetch URL: #{base_response.code} #{base_response.message}" unless base_response.is_a?(Net::HTTPSuccess)
|
46
|
+
|
47
|
+
base_doc = Nokogiri::HTML(base_response.body)
|
48
|
+
collection_dirs = base_doc.css('a').filter_map { |a| a['href'] }.grep(/^#{collection}\d+/)
|
49
|
+
raise "No collections found at #{base_url} for colleciton #{collection}" if collection_dirs.empty?
|
50
|
+
|
51
|
+
latest_collection = collection_dirs.max_by do |collection_version|
|
52
|
+
# Grab the digits before the slash and convert to integer
|
53
|
+
collection_version[/\d+/].to_i
|
54
|
+
end
|
55
|
+
windows_package_url = "#{windows_package_base_url}#{latest_collection}"
|
56
|
+
end
|
57
|
+
url = URI.parse(windows_package_url)
|
58
|
+
response = Net::HTTP.get_response(url)
|
59
|
+
|
60
|
+
# Fetch and parse the page
|
61
|
+
raise "Failed to fetch URL: #{response.code} #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|
62
|
+
|
63
|
+
doc = Nokogiri::HTML(response.body)
|
64
|
+
|
65
|
+
# Create the regex for the agent package
|
66
|
+
base_collection_name = collection.gsub(/\d+$/, '')
|
67
|
+
agent_regex = /#{base_collection_name}-agent-(\d+\.\d+\.\d+)-.*\.msi$/i
|
68
|
+
# Extract all hrefs that look like the appropriate MSI files
|
69
|
+
files = doc.css('a').filter_map { |a| a['href'] }.grep(agent_regex)
|
70
|
+
|
71
|
+
raise "No MSI files found at #{windows_package_url}" if files.empty?
|
72
|
+
|
73
|
+
latest_msi = files.max_by do |file|
|
74
|
+
version_str = file.match(agent_regex)[1]
|
75
|
+
Gem::Version.new(version_str)
|
76
|
+
end
|
77
|
+
|
78
|
+
# Remove index.html if it exists in the windows_package_url
|
79
|
+
windows_package_repo = windows_package_url.sub(/index\.html$/, '')
|
80
|
+
# Return the full url to the latest msi
|
81
|
+
"#{windows_package_repo}#{latest_msi}"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Generates commands to be inserted into a Windows batch file to launch an MSI install
|
85
|
+
# @param [String] msi_path The path of the MSI - can be a local Windows style file path like
|
86
|
+
# C:\Windows\Temp\puppet-agent.msi OR a url like https://download.com/puppet.msi or file://C:\Windows\Temp\puppet-agent.msi
|
87
|
+
# @param [Hash{String=>String}] msi_opts MSI installer options
|
88
|
+
# @param [String] log_path The path to write the MSI log - must be a local Windows style file path
|
89
|
+
#
|
90
|
+
# @api private
|
91
|
+
def msi_install_script(msi_path, msi_opts, log_path)
|
92
|
+
# msiexec requires backslashes in file paths launched under cmd.exe start /w
|
93
|
+
url_pattern = %r{^(https?|file)://}
|
94
|
+
msi_path = msi_path.tr('/', '\\') unless msi_path&.match?(url_pattern)
|
95
|
+
|
96
|
+
msi_params = msi_opts.map { |k, v| "#{k}=#{v}" }.join(' ')
|
97
|
+
|
98
|
+
# msiexec requires quotes around paths with backslashes - c:\ or file://c:\
|
99
|
+
# not strictly needed for http:// but it simplifies this code
|
100
|
+
<<~BATCH
|
101
|
+
start /w msiexec.exe /i "#{msi_path}" /qn /L*V #{log_path} #{msi_params}
|
102
|
+
exit /B %errorlevel%
|
103
|
+
BATCH
|
104
|
+
end
|
105
|
+
|
106
|
+
# Given a host, path to MSI and MSI options, will create a batch file
|
107
|
+
# on the host, returning the path to the randomized batch file and
|
108
|
+
# the randomized log file
|
109
|
+
#
|
110
|
+
# @param [Host] host An object implementing {Beaker::Hosts}'s interface.
|
111
|
+
# @param [String] msi_path The path of the MSI - can be a local Windows
|
112
|
+
# style file path like c:\temp\puppet.msi OR a url like
|
113
|
+
# https://download.com/puppet.msi or file://c:\temp\puppet.msi
|
114
|
+
# @param [Hash{String=>String}] msi_opts MSI installer options
|
115
|
+
#
|
116
|
+
# @api private
|
117
|
+
# @return [String, String] path to the batch file, patch to the log file
|
118
|
+
def create_install_msi_batch_on(host, msi_path, msi_opts)
|
119
|
+
timestamp = Time.new.strftime('%Y-%m-%d_%H.%M.%S')
|
120
|
+
tmp_path = host.system_temp_path.tr('/', '\\')
|
121
|
+
|
122
|
+
batch_name = "install-puppet-msi-#{timestamp}.bat"
|
123
|
+
batch_path = "#{tmp_path}#{host.scp_separator}#{batch_name}"
|
124
|
+
log_path = "#{tmp_path}\\install-puppet-#{timestamp}.log"
|
125
|
+
|
126
|
+
Tempfile.open(batch_name) do |tmp_file|
|
127
|
+
batch_contents = msi_install_script(msi_path, msi_opts, log_path)
|
128
|
+
|
129
|
+
File.open(tmp_file.path, 'w') { |file| file.puts(batch_contents) }
|
130
|
+
host.do_scp_to(tmp_file.path, batch_path, {})
|
131
|
+
end
|
132
|
+
|
133
|
+
[batch_path, log_path]
|
134
|
+
end
|
135
|
+
|
136
|
+
# Installs a specified MSI package on given hosts.
|
137
|
+
# @param [Host, Array<Host>, String, Symbol] hosts One or more hosts to act upon,
|
138
|
+
# or a role (String or Symbol) that identifies one or more hosts.
|
139
|
+
# @param [String] msi_path
|
140
|
+
# The path of the MSI - can be a local Windows style file path like
|
141
|
+
# c:\temp\puppet.msi OR a url like https://download.com/puppet.msi or file://c:\temp\puppet.msi
|
142
|
+
# can also be a collection like 'puppet', 'puppet8', 'openvox', or 'openvox8'
|
143
|
+
# @param [Hash{String=>String}] msi_opts MSI installer options
|
144
|
+
# @option msi_opts [String] INSTALLDIR Where Puppet and its dependencies should be installed.
|
145
|
+
# (Defaults vary based on operating system and installer architecture)
|
146
|
+
# Requires Puppet 2.7.12 / PE 2.5.0
|
147
|
+
# @option msi_opts [String] PUPPET_MASTER_SERVER The hostname where the puppet master server can be reached.
|
148
|
+
# (Defaults to puppet)
|
149
|
+
# Requires Puppet 2.7.12 / PE 2.5.0
|
150
|
+
# @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.
|
151
|
+
# (Defaults the value of PUPPET_MASTER_SERVER)
|
152
|
+
# Requires Puppet 2.7.12 / PE 2.5.0
|
153
|
+
# @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
|
154
|
+
# (Defaults to the node's fqdn as discovered by facter fqdn)
|
155
|
+
# Requires Puppet 2.7.12 / PE 2.5.0
|
156
|
+
# @option msi_opts [String] PUPPET_AGENT_ENVIRONMENT The node’s environment.
|
157
|
+
# (Defaults to production)
|
158
|
+
# Requires Puppet 3.3.1 / PE 3.1.0
|
159
|
+
# @option msi_opts [String] PUPPET_AGENT_STARTUP_MODE Whether the puppet agent service should run (or be allowed to run)
|
160
|
+
# (Defaults to Manual - valid values are Automatic, Manual or Disabled)
|
161
|
+
# Requires Puppet 3.4.0 / PE 3.2.0
|
162
|
+
# @option msi_opts [String] PUPPET_AGENT_ACCOUNT_USER Whether the puppet agent service should run (or be allowed to run)
|
163
|
+
# (Defaults to LocalSystem)
|
164
|
+
# Requires Puppet 3.4.0 / PE 3.2.0
|
165
|
+
# @option msi_opts [String] PUPPET_AGENT_ACCOUNT_PASSWORD The password to use for puppet agent’s user account
|
166
|
+
# (No default)
|
167
|
+
# Requires Puppet 3.4.0 / PE 3.2.0
|
168
|
+
# @option msi_opts [String] PUPPET_AGENT_ACCOUNT_DOMAIN The domain of puppet agent’s user account.
|
169
|
+
# (Defaults to .)
|
170
|
+
# Requires Puppet 3.4.0 / PE 3.2.0
|
171
|
+
# @param [Hash] opts Options hash to control installation behavior.
|
172
|
+
#
|
173
|
+
# @example
|
174
|
+
# install_msi_on(hosts, 'c:\\puppet.msi', msi_opts: {'PUPPET_AGENT_STARTUP_MODE' => 'Manual'}, opts: {:debug => true})
|
175
|
+
#
|
176
|
+
# @api private
|
177
|
+
def install_msi_on(hosts, msi_path, msi_opts: {}, opts: {})
|
178
|
+
# If the msi patch matches a collection, get the url for the latest msi available for that collection
|
179
|
+
expanded_msi_path = /^(puppet|openvox)\d*$/.match?(msi_path) ? get_agent_package_url(msi_path) : msi_path
|
180
|
+
block_on hosts do |host|
|
181
|
+
msi_opts['PUPPET_AGENT_STARTUP_MODE'] ||= 'Manual'
|
182
|
+
batch_path, log_file = create_install_msi_batch_on(host, expanded_msi_path, msi_opts)
|
183
|
+
# Powershell command looses an escaped slash resulting in cygwin relative path
|
184
|
+
# See https://github.com/puppetlabs/beaker/pull/1626#issuecomment-621341555
|
185
|
+
log_file_escaped = log_file.gsub('\\', '\\\\\\')
|
186
|
+
# begin / rescue here so that we can reuse existing error msg propagation
|
187
|
+
begin
|
188
|
+
# 1641 = ERROR_SUCCESS_REBOOT_INITIATED
|
189
|
+
# 3010 = ERROR_SUCCESS_REBOOT_REQUIRED
|
190
|
+
on host, Beaker::Command.new("\"#{batch_path}\"", [], { cmdexe: true }), acceptable_exit_codes: [0, 1641, 3010]
|
191
|
+
rescue StandardError
|
192
|
+
logger.info(file_contents_on(host, log_file_escaped))
|
193
|
+
raise
|
194
|
+
end
|
195
|
+
|
196
|
+
logger.info(file_contents_on(host, log_file_escaped)) if opts[:debug]
|
197
|
+
|
198
|
+
unless host.is_cygwin?
|
199
|
+
# Enable the PATH updates
|
200
|
+
host.close
|
201
|
+
|
202
|
+
# Some systems require a full reboot to trigger the enabled path
|
203
|
+
host.reboot unless on(host, Beaker::Command.new('puppet -h', [], { cmdexe: true }),
|
204
|
+
accept_all_exit_codes: true).exit_code.zero?
|
205
|
+
end
|
206
|
+
|
207
|
+
# verify service status post install
|
208
|
+
# if puppet service exists, then pe-puppet is not queried
|
209
|
+
# if puppet service does not exist, pe-puppet is queried and that exit code is used
|
210
|
+
# therefore, this command will always exit 0 if either service is installed
|
211
|
+
#
|
212
|
+
# We also take advantage of this output to verify the startup
|
213
|
+
# settings are honored as supplied to the MSI
|
214
|
+
on host, Beaker::Command.new('sc qc puppet || sc qc pe-puppet', [], { cmdexe: true }) do |result|
|
215
|
+
output = result.stdout
|
216
|
+
startup_mode = msi_opts['PUPPET_AGENT_STARTUP_MODE'].upcase
|
217
|
+
|
218
|
+
search = case startup_mode # rubocop:disable Style/HashLikeCase
|
219
|
+
when 'AUTOMATIC'
|
220
|
+
{ code: 2, name: 'AUTO_START' }
|
221
|
+
when 'MANUAL'
|
222
|
+
{ code: 3, name: 'DEMAND_START' }
|
223
|
+
when 'DISABLED'
|
224
|
+
{ code: 4, name: 'DISABLED' }
|
225
|
+
end
|
226
|
+
|
227
|
+
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)
|
228
|
+
end
|
229
|
+
|
230
|
+
# (PA-514) value for PUPPET_AGENT_STARTUP_MODE should be present in
|
231
|
+
# registry and honored after install/upgrade.
|
232
|
+
reg_key = if host.is_x86_64?
|
233
|
+
'HKLM\\SOFTWARE\\Wow6432Node\\Puppet Labs\\PuppetInstaller'
|
234
|
+
else
|
235
|
+
'HKLM\\SOFTWARE\\Puppet Labs\\PuppetInstaller'
|
236
|
+
end
|
237
|
+
reg_query_command = %(reg query "#{reg_key}" /v "RememberedPuppetAgentStartupMode" | findstr #{msi_opts['PUPPET_AGENT_STARTUP_MODE']})
|
238
|
+
on host, Beaker::Command.new(reg_query_command, [], { cmdexe: true })
|
239
|
+
|
240
|
+
# emit the misc/versions.txt file which contains component versions for
|
241
|
+
# puppet, facter, hiera, pxp-agent, packaging and vendored Ruby
|
242
|
+
[
|
243
|
+
"'%PROGRAMFILES%\\Puppet Labs\\puppet\\misc\\versions.txt'",
|
244
|
+
"'%PROGRAMFILES(X86)%\\Puppet Labs\\puppet\\misc\\versions.txt'",
|
245
|
+
].each do |path|
|
246
|
+
result = on(host, "cmd /c type #{path}", accept_all_exit_codes: true)
|
247
|
+
if result.exit_code.zero?
|
248
|
+
logger.info(result.stdout)
|
249
|
+
break
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# Installs a specified msi path on given hosts
|
256
|
+
# @param [Host, Array<Host>, String, Symbol] hosts One or more hosts to act upon,
|
257
|
+
# or a role (String or Symbol) that identifies one or more hosts.
|
258
|
+
# @param [String] msi_path
|
259
|
+
# The path of the MSI - can be a local Windows style file path like
|
260
|
+
# c:\temp\foo.msi OR a url like https://download.com/foo.msi or file://c:\temp\foo.msi
|
261
|
+
# can also be a collection like 'puppet', 'puppet8', 'openvox', or 'openvox8'
|
262
|
+
# @param [Hash{String=>String}] msi_opts MSI installer options
|
263
|
+
# @param [Hash] opts Options hash to control installation behavior.
|
264
|
+
#
|
265
|
+
# @example
|
266
|
+
# generic_install_msi_on(hosts, 'https://releases.hashicorp.com/vagrant/1.8.4/vagrant_1.8.4.msi', msi_opts: {}, opts: {:debug => true})
|
267
|
+
#
|
268
|
+
# @api private
|
269
|
+
def generic_install_msi_on(hosts, msi_path, msi_opts: {}, opts: {})
|
270
|
+
# If the msi patch matches a collection, get the url for the latest msi available for that collection
|
271
|
+
expanded_msi_path = /^(puppet|openvox)\d*$/.match?(msi_path) ? get_agent_package_url(msi_path) : msi_path
|
272
|
+
|
273
|
+
block_on hosts do |host|
|
274
|
+
batch_path, log_file = create_install_msi_batch_on(host, expanded_msi_path, msi_opts)
|
275
|
+
# Powershell command looses an escaped slash resulting in cygwin relative path
|
276
|
+
# See https://github.com/puppetlabs/beaker/pull/1626#issuecomment-621341555
|
277
|
+
log_file_escaped = log_file.gsub('\\', '\\\\\\')
|
278
|
+
# begin / rescue here so that we can reuse existing error msg propagation
|
279
|
+
begin
|
280
|
+
# 1641 = ERROR_SUCCESS_REBOOT_INITIATED
|
281
|
+
# 3010 = ERROR_SUCCESS_REBOOT_REQUIRED
|
282
|
+
on host, Beaker::Command.new("\"#{batch_path}\"", [], { cmdexe: true }), acceptable_exit_codes: [0, 1641, 3010]
|
283
|
+
rescue StandardError
|
284
|
+
logger.info(file_contents_on(host, log_file_escaped))
|
285
|
+
|
286
|
+
raise
|
287
|
+
end
|
288
|
+
|
289
|
+
logger.info(file_contents_on(host, log_file_escaped)) if opts[:debug]
|
290
|
+
|
291
|
+
host.close unless host.is_cygwin?
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|
@@ -5,8 +5,10 @@ module BeakerPuppetHelpers
|
|
5
5
|
autoload :DSL, File.join(__dir__, 'beaker_puppet_helpers', 'dsl.rb')
|
6
6
|
autoload :InstallUtils, File.join(__dir__, 'beaker_puppet_helpers', 'install_utils.rb')
|
7
7
|
autoload :ModuleUtils, File.join(__dir__, 'beaker_puppet_helpers', 'module_utils.rb')
|
8
|
+
autoload :WindowsUtils, File.join(__dir__, 'beaker_puppet_helpers', 'windows_utils.rb')
|
8
9
|
end
|
9
10
|
|
10
11
|
require 'beaker'
|
11
12
|
Beaker::DSL.register(BeakerPuppetHelpers::DSL)
|
12
13
|
Beaker::DSL.register(BeakerPuppetHelpers::ModuleUtils)
|
14
|
+
Beaker::DSL.register(BeakerPuppetHelpers::WindowsUtils)
|
@@ -0,0 +1,403 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable RSpec/MultipleMemoizedHelpers
|
4
|
+
require 'spec_helper'
|
5
|
+
|
6
|
+
class ClassMixedWithDSLInstallUtils
|
7
|
+
include Beaker::DSL::Patterns
|
8
|
+
include BeakerPuppetHelpers::WindowsUtils
|
9
|
+
|
10
|
+
def logger
|
11
|
+
@logger ||= RSpec::Mocks::Double.new('Beaker::Logger').as_null_object
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe BeakerPuppetHelpers::WindowsUtils do
|
16
|
+
subject(:dsl) { ClassMixedWithDSLInstallUtils.new }
|
17
|
+
|
18
|
+
let(:windows_temp) { 'C:\\Windows\\Temp' }
|
19
|
+
let(:batch_path) { '/fake/batch/path' }
|
20
|
+
let(:msi_path) { 'c:\\foo\\puppet.msi' }
|
21
|
+
let(:winhost) do
|
22
|
+
make_host('winhost',
|
23
|
+
{ platform: Beaker::Platform.new('windows-2008r2-64'),
|
24
|
+
pe_ver: '3.0',
|
25
|
+
working_dir: '/tmp',
|
26
|
+
is_cygwin: true, })
|
27
|
+
end
|
28
|
+
let(:winhost_non_cygwin) do
|
29
|
+
make_host('winhost_non_cygwin',
|
30
|
+
{ platform: 'windows',
|
31
|
+
pe_ver: '3.0',
|
32
|
+
working_dir: '/tmp',
|
33
|
+
is_cygwin: 'false', })
|
34
|
+
end
|
35
|
+
let(:hosts) { [winhost, winhost_non_cygwin] }
|
36
|
+
|
37
|
+
def expect_install_called
|
38
|
+
result = Beaker::Result.new(nil, 'temp')
|
39
|
+
result.exit_code = 0
|
40
|
+
|
41
|
+
hosts.each do |host|
|
42
|
+
expectation = expect(subject).to receive(:on).with(host, having_attributes(command: "\"#{batch_path}\""),
|
43
|
+
anything).and_return(result)
|
44
|
+
if block_given?
|
45
|
+
should_break = yield expectation
|
46
|
+
break if should_break
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def expect_status_called(start_type = 'DEMAND_START')
|
52
|
+
result = Beaker::Result.new(nil, 'temp')
|
53
|
+
result.exit_code = 0
|
54
|
+
result.stdout = case start_type
|
55
|
+
when 'DISABLED'
|
56
|
+
' START_TYPE : 4 DISABLED'
|
57
|
+
when 'AUTOMATIC'
|
58
|
+
' START_TYPE : 2 AUTO_START'
|
59
|
+
else # 'DEMAND_START'
|
60
|
+
' START_TYPE : 3 DEMAND_START'
|
61
|
+
end
|
62
|
+
|
63
|
+
hosts.each do |host|
|
64
|
+
expect(subject).to receive(:on).with(host,
|
65
|
+
having_attributes(command: 'sc qc puppet || sc qc pe-puppet')).and_yield(result)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def expect_version_log_called(_times = hosts.length)
|
70
|
+
path = "'%PROGRAMFILES%\\Puppet Labs\\puppet\\misc\\versions.txt'"
|
71
|
+
|
72
|
+
result = Beaker::Result.new(nil, 'temp')
|
73
|
+
result.exit_code = 0
|
74
|
+
|
75
|
+
hosts.each do |host|
|
76
|
+
expect(subject).to receive(:on).with(host, "cmd /c type #{path}", anything).and_return(result)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def expect_reg_query_called(_times = hosts.length)
|
81
|
+
expect(hosts).to all(receive(:is_x86_64?).and_return(true))
|
82
|
+
|
83
|
+
hosts.each do |host|
|
84
|
+
expect(subject).to receive(:on)
|
85
|
+
.with(host, having_attributes(command: /reg query "HKLM\\SOFTWARE\\Wow6432Node\\Puppet Labs\\PuppetInstaller/))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def expect_puppet_path_called
|
90
|
+
hosts.each do |host|
|
91
|
+
next if host.is_cygwin?
|
92
|
+
|
93
|
+
result = Beaker::Result.new(nil, 'temp')
|
94
|
+
result.exit_code = 0
|
95
|
+
|
96
|
+
expect(subject).to receive(:on)
|
97
|
+
.with(host, having_attributes(command: 'puppet -h'), anything)
|
98
|
+
.and_return(result)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe '#get_agent_package_url' do
|
103
|
+
let(:base_package_repo_html) do
|
104
|
+
<<-HTML
|
105
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
|
106
|
+
<html>
|
107
|
+
<head>
|
108
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
109
|
+
<title>Index of /windows/</title>
|
110
|
+
</head>
|
111
|
+
<body>
|
112
|
+
<h1>Index of /windows/</h1>
|
113
|
+
<hr>
|
114
|
+
<pre>
|
115
|
+
<a href="../">../</a>
|
116
|
+
<a href="openvox7/">openvox7/</a>
|
117
|
+
<a href="openvox8/">openvox8/</a>
|
118
|
+
</pre>
|
119
|
+
<hr>
|
120
|
+
</body>
|
121
|
+
</html>
|
122
|
+
HTML
|
123
|
+
end
|
124
|
+
let(:package_repo_html) do
|
125
|
+
<<-HTML
|
126
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" "http://www.w3.org/TR/REC-html40/loose.dtd">
|
127
|
+
<html>
|
128
|
+
<head>
|
129
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
130
|
+
<title>Index of /windows/openvox8/</title>
|
131
|
+
</head>
|
132
|
+
<body>
|
133
|
+
<h1>Index of /windows/openvox8/</h1>
|
134
|
+
<hr>
|
135
|
+
<pre>
|
136
|
+
<a href="../">../</a>
|
137
|
+
<a href="unsigned/">unsigned/</a>
|
138
|
+
<a href="openvox-agent-8.19.2-x64.msi">openvox-agent-8.19.2-x64.msi</a>
|
139
|
+
<a href="openvox-agent-8.21.2-rc1-x64.msi">openvox-agent-8.21.2-rc1-x64.msi</a>
|
140
|
+
<a href="openvox-agent-8.22.0-x64.msi">openvox-agent-8.22.0-x64.msi</a>
|
141
|
+
<a href="openvox-agent-8.22.1-x64.msi">openvox-agent-8.22.1-x64.msi</a>
|
142
|
+
<a href="openvox-agent-8.23.0-x64.msi">openvox-agent-8.23.0-x64.msi</a>
|
143
|
+
<a href="openvox-agent-8.23.1-x64.msi">openvox-agent-8.23.1-x64.msi</a>
|
144
|
+
</pre>
|
145
|
+
<hr>
|
146
|
+
</body>
|
147
|
+
</html>
|
148
|
+
HTML
|
149
|
+
end
|
150
|
+
|
151
|
+
before do
|
152
|
+
# Stub Net::HTTP.get_response for base url and final url
|
153
|
+
allow(Net::HTTP).to receive(:get_response) do |uri|
|
154
|
+
case uri.to_s
|
155
|
+
when 'https://downloads.voxpupuli.org/windows/'
|
156
|
+
instance_double(Net::HTTPOK, is_a?: true, body: base_package_repo_html)
|
157
|
+
when 'https://downloads.voxpupuli.org/windows/openvox8/'
|
158
|
+
instance_double(Net::HTTPOK, is_a?: true, body: package_repo_html)
|
159
|
+
else
|
160
|
+
raise "Unexpected URL: #{uri}"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'returns the latest MSI URL for the openvox collection' do
|
166
|
+
result = dsl.get_agent_package_url('openvox')
|
167
|
+
expect(result).to eq('https://downloads.voxpupuli.org/windows/openvox8/openvox-agent-8.23.1-x64.msi')
|
168
|
+
end
|
169
|
+
|
170
|
+
it 'returns the latest MSI URL for the openvox8 collection' do
|
171
|
+
result = dsl.get_agent_package_url('openvox8')
|
172
|
+
expect(result).to eq('https://downloads.voxpupuli.org/windows/openvox8/openvox-agent-8.23.1-x64.msi')
|
173
|
+
end
|
174
|
+
|
175
|
+
it 'raises for unsupported collection prefix' do
|
176
|
+
expect { dsl.get_agent_package_url('foo') }.to raise_error('Unsupported collection: foo')
|
177
|
+
end
|
178
|
+
|
179
|
+
it 'raises when no MSI files found' do
|
180
|
+
empty_html = '<html><body></body></html>'
|
181
|
+
allow(Net::HTTP).to receive(:get_response).and_return(
|
182
|
+
instance_double(Net::HTTPOK, is_a?: true, body: empty_html),
|
183
|
+
)
|
184
|
+
expect { dsl.get_agent_package_url('openvox8') }.to raise_error('No MSI files found at https://downloads.voxpupuli.org/windows/openvox8/')
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
describe '#install_msi_on' do
|
189
|
+
let(:log_file) { '/fake/log/file.log' }
|
190
|
+
|
191
|
+
before do
|
192
|
+
result = Beaker::Result.new(nil, 'temp')
|
193
|
+
result.exit_code = 0
|
194
|
+
|
195
|
+
hosts.each do |host|
|
196
|
+
allow(dsl).to receive(:on)
|
197
|
+
.with(host, having_attributes(command: "\"#{batch_path}\""))
|
198
|
+
.and_return(result)
|
199
|
+
end
|
200
|
+
|
201
|
+
allow(dsl).to receive(:create_install_msi_batch_on).and_return([batch_path, log_file])
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'specifies a PUPPET_AGENT_STARTUP_MODE of Manual by default' do
|
205
|
+
expect_install_called
|
206
|
+
expect_puppet_path_called
|
207
|
+
expect_status_called
|
208
|
+
expect_reg_query_called
|
209
|
+
expect_version_log_called
|
210
|
+
expect(dsl).to receive(:create_install_msi_batch_on).with(anything, anything, { 'PUPPET_AGENT_STARTUP_MODE' => 'Manual' })
|
211
|
+
dsl.install_msi_on(hosts, msi_path, msi_opts: {})
|
212
|
+
end
|
213
|
+
|
214
|
+
it 'allows configuration of PUPPET_AGENT_STARTUP_MODE to Automatic' do
|
215
|
+
expect_install_called
|
216
|
+
expect_puppet_path_called
|
217
|
+
expect_status_called('AUTOMATIC')
|
218
|
+
expect_reg_query_called
|
219
|
+
expect_version_log_called
|
220
|
+
value = 'Automatic'
|
221
|
+
expect(dsl).to receive(:create_install_msi_batch_on).with(anything, anything, { 'PUPPET_AGENT_STARTUP_MODE' => value })
|
222
|
+
dsl.install_msi_on(hosts, msi_path, msi_opts: { 'PUPPET_AGENT_STARTUP_MODE' => value })
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'allows configuration of PUPPET_AGENT_STARTUP_MODE to Disabled' do
|
226
|
+
expect_install_called
|
227
|
+
expect_puppet_path_called
|
228
|
+
expect_status_called('DISABLED')
|
229
|
+
expect_reg_query_called
|
230
|
+
expect_version_log_called
|
231
|
+
value = 'Disabled'
|
232
|
+
expect(dsl).to receive(:create_install_msi_batch_on).with(anything, anything, { 'PUPPET_AGENT_STARTUP_MODE' => value })
|
233
|
+
dsl.install_msi_on(hosts, msi_path, msi_opts: { 'PUPPET_AGENT_STARTUP_MODE' => value })
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'does not generate a command to emit a log file without the :debug option set' do
|
237
|
+
expect_install_called
|
238
|
+
expect_puppet_path_called
|
239
|
+
expect_status_called
|
240
|
+
expect_reg_query_called
|
241
|
+
expect_version_log_called
|
242
|
+
|
243
|
+
expect(dsl).not_to receive(:file_contents_on).with(anything, log_file)
|
244
|
+
|
245
|
+
dsl.install_msi_on(hosts, msi_path)
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'generates a command to emit a log file when the install script fails' do
|
249
|
+
# NOTE: a single failure aborts executing against remaining hosts
|
250
|
+
expect_install_called do |e|
|
251
|
+
e.and_raise
|
252
|
+
true # break
|
253
|
+
end
|
254
|
+
|
255
|
+
expect(dsl).to receive(:file_contents_on).with(anything, log_file)
|
256
|
+
expect do
|
257
|
+
dsl.install_msi_on(hosts, msi_path)
|
258
|
+
end.to raise_error(RuntimeError)
|
259
|
+
end
|
260
|
+
|
261
|
+
it 'generates a command to emit a log file with the :debug option set' do
|
262
|
+
expect_install_called
|
263
|
+
expect_reg_query_called
|
264
|
+
expect_puppet_path_called
|
265
|
+
expect_status_called
|
266
|
+
expect_version_log_called
|
267
|
+
|
268
|
+
expect(dsl).to receive(:file_contents_on).with(anything, log_file).exactly(hosts.length).times
|
269
|
+
|
270
|
+
dsl.install_msi_on(hosts, msi_path, msi_opts: {}, opts: { debug: true })
|
271
|
+
end
|
272
|
+
|
273
|
+
it 'passes msi_path to #create_install_msi_batch_on as-is' do
|
274
|
+
expect_install_called
|
275
|
+
expect_reg_query_called
|
276
|
+
expect_puppet_path_called
|
277
|
+
expect_status_called
|
278
|
+
expect_version_log_called
|
279
|
+
test_path = 'test/path'
|
280
|
+
expect(dsl).to receive(:create_install_msi_batch_on).with(anything, test_path, anything)
|
281
|
+
dsl.install_msi_on(hosts, test_path)
|
282
|
+
end
|
283
|
+
|
284
|
+
it 'searches in Wow6432Node for the remembered startup setting on 64-bit hosts' do
|
285
|
+
expect_install_called
|
286
|
+
expect_puppet_path_called
|
287
|
+
expect_status_called
|
288
|
+
expect_version_log_called
|
289
|
+
|
290
|
+
hosts.each do |host|
|
291
|
+
expect(host).to receive(:is_x86_64?).and_return(true)
|
292
|
+
|
293
|
+
expect(dsl).to receive(:on).with(host, having_attributes(command: 'reg query "HKLM\\SOFTWARE\\Wow6432Node\\Puppet Labs\\PuppetInstaller" /v "RememberedPuppetAgentStartupMode" | findstr Manual'))
|
294
|
+
end
|
295
|
+
|
296
|
+
dsl.install_msi_on(hosts, msi_path, msi_opts: { 'PUPPET_AGENT_STARTUP_MODE' => 'Manual' })
|
297
|
+
end
|
298
|
+
|
299
|
+
it 'omits Wow6432Node in the registry search for remembered startup setting on 32-bit hosts' do
|
300
|
+
expect_install_called
|
301
|
+
expect_puppet_path_called
|
302
|
+
expect_status_called
|
303
|
+
expect_version_log_called
|
304
|
+
|
305
|
+
hosts.each do |host|
|
306
|
+
expect(host).to receive(:is_x86_64?).and_return(false)
|
307
|
+
|
308
|
+
expect(dsl).to receive(:on).with(host, having_attributes(command: 'reg query "HKLM\\SOFTWARE\\Puppet Labs\\PuppetInstaller" /v "RememberedPuppetAgentStartupMode" | findstr Manual'))
|
309
|
+
end
|
310
|
+
|
311
|
+
dsl.install_msi_on(hosts, msi_path, msi_opts: { 'PUPPET_AGENT_STARTUP_MODE' => 'Manual' })
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
describe '#create_install_msi_batch_on' do
|
316
|
+
let(:tmp) { '/tmp/create_install_msi_batch_on' }
|
317
|
+
let(:tmp_slashes) { tmp.tr('/', '\\') }
|
318
|
+
|
319
|
+
before do
|
320
|
+
FakeFS::FileSystem.add(File.expand_path(tmp))
|
321
|
+
hosts.each do |host|
|
322
|
+
allow(host).to receive(:system_temp_path).and_return(tmp)
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'passes msi_path & msi_opts down to #msi_install_script' do
|
327
|
+
allow(winhost).to receive(:do_scp_to)
|
328
|
+
test_path = '/path/to/test/with/13540'
|
329
|
+
test_opts = { 'key1' => 'val1', 'key2' => 'val2' }
|
330
|
+
expect(dsl).to receive(:msi_install_script).with(
|
331
|
+
test_path, test_opts, anything
|
332
|
+
)
|
333
|
+
dsl.create_install_msi_batch_on(winhost, test_path, test_opts)
|
334
|
+
end
|
335
|
+
|
336
|
+
it 'SCPs to & returns the same batch file path, corrected for slashes' do
|
337
|
+
test_time = Time.now
|
338
|
+
allow(Time).to receive(:new).and_return(test_time)
|
339
|
+
timestamp = test_time.strftime('%Y-%m-%d_%H.%M.%S')
|
340
|
+
|
341
|
+
correct_path = "#{tmp_slashes}\\install-puppet-msi-#{timestamp}.bat"
|
342
|
+
expect(winhost).to receive(:do_scp_to).with(anything, correct_path, {})
|
343
|
+
test_path, = dsl.create_install_msi_batch_on(winhost, msi_path, {})
|
344
|
+
expect(test_path).to eq(correct_path)
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'returns & sends log_path to #msi_install_scripts, corrected for slashes' do
|
348
|
+
allow(winhost).to receive(:do_scp_to)
|
349
|
+
test_time = Time.now
|
350
|
+
allow(Time).to receive(:new).and_return(test_time)
|
351
|
+
timestamp = test_time.strftime('%Y-%m-%d_%H.%M.%S')
|
352
|
+
|
353
|
+
correct_path = "#{tmp_slashes}\\install-puppet-#{timestamp}.log"
|
354
|
+
expect(dsl).to receive(:msi_install_script).with(anything, anything, correct_path)
|
355
|
+
_, log_path = dsl.create_install_msi_batch_on(winhost, msi_path, {})
|
356
|
+
expect(log_path).to eq(correct_path)
|
357
|
+
end
|
358
|
+
end
|
359
|
+
|
360
|
+
describe '#msi_install_script' do
|
361
|
+
let(:log_path) { '/log/msi_install_script' }
|
362
|
+
|
363
|
+
context 'with msi_params parameter' do
|
364
|
+
it 'can take an empty hash' do
|
365
|
+
expected_cmd = %r{^start /w msiexec\.exe /i ".*" /qn /L\*V #{log_path}\ .exit}m
|
366
|
+
expect(dsl.msi_install_script(msi_path, {}, log_path)).to match(expected_cmd)
|
367
|
+
end
|
368
|
+
|
369
|
+
it 'uses a key-value pair correctly' do
|
370
|
+
params = { 'tk1' => 'tv1' }
|
371
|
+
expected_cmd = %r{^start /w msiexec\.exe /i ".*" /qn /L\*V #{log_path}\ tk1=tv1}
|
372
|
+
expect(dsl.msi_install_script(msi_path, params, log_path)).to match(expected_cmd)
|
373
|
+
end
|
374
|
+
|
375
|
+
it 'uses multiple key-value pairs correctly' do
|
376
|
+
params = { 'tk1' => 'tv1', 'tk2' => 'tv2' }
|
377
|
+
expected_cmd = %r{^start /w msiexec\.exe /i ".*" /qn /L\*V #{log_path}\ tk1=tv1\ tk2=tv2}
|
378
|
+
expect(dsl.msi_install_script(msi_path, params, log_path)).to match(expected_cmd)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
context 'with msi_path parameter' do
|
383
|
+
it 'generates an appropriate command with a MSI file path using non-Windows slashes' do
|
384
|
+
msi_path = 'c:/foo/puppet.msi'
|
385
|
+
expected_cmd = %r{^start /w msiexec\.exe /i "c:\\foo\\puppet.msi" /qn /L\*V #{log_path}}
|
386
|
+
expect(dsl.msi_install_script(msi_path, {}, log_path)).to match(expected_cmd)
|
387
|
+
end
|
388
|
+
|
389
|
+
it 'generates an appropriate command with a MSI http(s) url' do
|
390
|
+
msi_url = 'https://downloads.puppetlabs.com/puppet.msi'
|
391
|
+
expected_cmd = %r{^start /w msiexec\.exe /i "https://downloads\.puppetlabs\.com/puppet\.msi" /qn /L\*V #{log_path}}
|
392
|
+
expect(dsl.msi_install_script(msi_url, {}, log_path)).to match(expected_cmd)
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'generates an appropriate command with a MSI file url' do
|
396
|
+
msi_url = 'file://c:\\foo\\puppet.msi'
|
397
|
+
expected_cmd = %r{^start /w msiexec\.exe /i "file://c:\\foo\\puppet\.msi" /qn /L\*V #{log_path}}
|
398
|
+
expect(dsl.msi_install_script(msi_url, {}, log_path)).to match(expected_cmd)
|
399
|
+
end
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
# rubocop:enable RSpec/MultipleMemoizedHelpers
|
data/spec/helpers.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HostHelpers
|
4
|
+
HOST_DEFAULTS = { platform: 'unix',
|
5
|
+
roles: ['agent'],
|
6
|
+
snapshot: 'snap',
|
7
|
+
ip: 'default.ip.address',
|
8
|
+
private_ip: 'private.ip.address',
|
9
|
+
dns_name: 'default.box.tld',
|
10
|
+
box: 'default_box_name',
|
11
|
+
box_url: 'http://default.box.url',
|
12
|
+
image: 'default_image',
|
13
|
+
flavor: 'm1.large',
|
14
|
+
user_data: '#cloud-config\nmanage_etc_hosts: true\nfinal_message: "The host is finally up!"', }.freeze
|
15
|
+
|
16
|
+
HOST_NAME = 'vm%d'
|
17
|
+
HOST_SNAPSHOT = 'snapshot%d'
|
18
|
+
HOST_IP = 'ip.address.for.%s'
|
19
|
+
HOST_BOX = 'vm2%s_of_my_box'
|
20
|
+
HOST_BOX_URL = 'http://address.for.my.box.%s'
|
21
|
+
HOST_DNS_NAME = '%s.box.tld'
|
22
|
+
HOST_TEMPLATE = '%s_has_a_template'
|
23
|
+
HOST_PRIVATE_IP = 'private.ip.for.%s'
|
24
|
+
|
25
|
+
def logger
|
26
|
+
instance_double(Logger).as_null_object
|
27
|
+
end
|
28
|
+
|
29
|
+
def make_opts
|
30
|
+
opts = Beaker::Options::Presets.new
|
31
|
+
opts.presets.merge(opts.env_vars).merge({ logger: logger,
|
32
|
+
host_config: 'sample.config',
|
33
|
+
type: nil,
|
34
|
+
pooling_api: 'http://vcloud.delivery.puppetlabs.net/',
|
35
|
+
datastore: 'instance0',
|
36
|
+
folder: 'Delivery/Quality Assurance/Staging/Dynamic',
|
37
|
+
resourcepool: 'delivery/Quality Assurance/Staging/Dynamic',
|
38
|
+
gce_project: 'beaker-compute',
|
39
|
+
gce_keyfile: '/path/to/keyfile.p12',
|
40
|
+
gce_password: 'notasecret',
|
41
|
+
gce_email: '12345678910@developer.gserviceaccount.com',
|
42
|
+
openstack_api_key: 'P1as$w0rd',
|
43
|
+
openstack_username: 'user',
|
44
|
+
openstack_auth_url: 'http://openstack_hypervisor.labs.net:5000/v2.0/tokens',
|
45
|
+
openstack_tenant: 'testing',
|
46
|
+
openstack_network: 'testing',
|
47
|
+
openstack_keyname: 'nopass',
|
48
|
+
floating_ip_pool: 'my_pool',
|
49
|
+
security_group: %w[my_sg default], })
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate_result(name, opts)
|
53
|
+
stdout = opts.key?(:stdout) ? opts[:stdout] : name
|
54
|
+
stderr = opts.key?(:stderr) ? opts[:stderr] : name
|
55
|
+
exit_code = opts.key?(:exit_code) ? opts[:exit_code] : 0
|
56
|
+
exit_code = [exit_code].flatten
|
57
|
+
result = instance_double(Beaker::Result, stdout: stdout, stderr: stderr)
|
58
|
+
allow(result).to receive_messages(stdout: stdout, stderr: stderr)
|
59
|
+
allow(result).to receive(:exit_code).and_return(*exit_code)
|
60
|
+
result
|
61
|
+
end
|
62
|
+
|
63
|
+
def make_host_opts(name, opts)
|
64
|
+
make_opts.merge({ 'HOSTS' => { name => opts } }).merge(opts)
|
65
|
+
end
|
66
|
+
|
67
|
+
def make_host(name, host_hash)
|
68
|
+
host_hash = Beaker::Options::OptionsHash.new.merge(HOST_DEFAULTS.merge(host_hash))
|
69
|
+
|
70
|
+
host = Beaker::Host.create(name, host_hash, make_opts)
|
71
|
+
|
72
|
+
allow(host).to receive(:exec).and_return(generate_result(name, host_hash))
|
73
|
+
allow(host).to receive(:close)
|
74
|
+
host
|
75
|
+
end
|
76
|
+
|
77
|
+
def make_hosts(preset_opts = {}, amt = 3)
|
78
|
+
hosts = []
|
79
|
+
(1..amt).each do |num|
|
80
|
+
name = HOST_NAME % num
|
81
|
+
opts = { snapshot: HOST_SNAPSHOT % num,
|
82
|
+
ip: HOST_IP % name,
|
83
|
+
private_ip: HOST_PRIVATE_IP % name,
|
84
|
+
dns_name: HOST_DNS_NAME % name,
|
85
|
+
template: HOST_TEMPLATE % name,
|
86
|
+
box: HOST_BOX % name,
|
87
|
+
box_url: HOST_BOX_URL % name, }.merge(preset_opts)
|
88
|
+
hosts << make_host(name, opts)
|
89
|
+
end
|
90
|
+
hosts
|
91
|
+
end
|
92
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'beaker_puppet_helpers'
|
4
|
+
|
5
|
+
# require 'pp' statement needed before fakefs, otherwise they can collide. Ref:
|
6
|
+
# https://github.com/fakefs/fakefs#fakefs-----typeerror-superclass-mismatch-for-class-file
|
7
|
+
require 'pp'
|
8
|
+
require 'fakefs/spec_helpers'
|
9
|
+
require 'helpers'
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.include FakeFS::SpecHelpers
|
13
|
+
config.include HostHelpers
|
14
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: beaker_puppet_helpers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vox Pupuli
|
@@ -29,6 +29,40 @@ dependencies:
|
|
29
29
|
- - "<"
|
30
30
|
- !ruby/object:Gem::Version
|
31
31
|
version: '8'
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: nokogiri
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - "~>"
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '1.18'
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 1.18.10
|
42
|
+
type: :runtime
|
43
|
+
prerelease: false
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - "~>"
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '1.18'
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: 1.18.10
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: open-uri
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - "<"
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0.6'
|
59
|
+
type: :runtime
|
60
|
+
prerelease: false
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
62
|
+
requirements:
|
63
|
+
- - "<"
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: '0.6'
|
32
66
|
- !ruby/object:Gem::Dependency
|
33
67
|
name: puppet-modulebuilder
|
34
68
|
requirement: !ruby/object:Gem::Requirement
|
@@ -91,9 +125,12 @@ files:
|
|
91
125
|
- lib/beaker_puppet_helpers/dsl.rb
|
92
126
|
- lib/beaker_puppet_helpers/install_utils.rb
|
93
127
|
- lib/beaker_puppet_helpers/module_utils.rb
|
128
|
+
- lib/beaker_puppet_helpers/windows_utils.rb
|
94
129
|
- spec/beaker_puppet_helpers/dsl_spec.rb
|
95
130
|
- spec/beaker_puppet_helpers/install_utils_spec.rb
|
96
131
|
- spec/beaker_puppet_helpers/module_utils_spec.rb
|
132
|
+
- spec/beaker_puppet_helpers/windows_utils_spec.rb
|
133
|
+
- spec/helpers.rb
|
97
134
|
- spec/spec_helper.rb
|
98
135
|
homepage: https://github.com/voxpupuli/beaker_puppet_helpers
|
99
136
|
licenses:
|
@@ -107,9 +144,6 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
144
|
- - ">="
|
108
145
|
- !ruby/object:Gem::Version
|
109
146
|
version: '3.2'
|
110
|
-
- - "<"
|
111
|
-
- !ruby/object:Gem::Version
|
112
|
-
version: '4'
|
113
147
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
148
|
requirements:
|
115
149
|
- - ">="
|