kitchen-vagrant 2.2.1 → 2.4.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4abf5224b64f63f370ce34d71838b1aa1706a70a2a8f7285219388a977eb2bdf
4
- data.tar.gz: 91b8de7ff382f07694a1df7d7714237e77be14be7b4faa854b509e97dc31fc31
3
+ metadata.gz: eb41469b88a11508fe064bd6b04389da5cdb617ef027e5a633f47a1c88478be0
4
+ data.tar.gz: d4e9d511d64c4bff5a21121f2e2e9bcad8cd453efe7981de9a2c87597bb5f758
5
5
  SHA512:
6
- metadata.gz: 3b42b779d8c9726c89ef893e2ae4289775ae488eb26d2ff52d3a909e3efd97296db770801908a41beb73df703a9e065dae8836b82ce0fab7fc4dc9eb968ba02d
7
- data.tar.gz: 4656b6f370e4c2233ad1c0fb8ab933599829599348fab18461f8ab487df9e180b040267e3fcd01124453b7c223f84792e276b7f8140ecf4e8c03377d62e79eb7
6
+ metadata.gz: 8332c2ee9115ff0c69684420767948b5ad620051b4e99b5ac763e4b08636fe81caa4678ba9ccbbb5e662f6495438499c1fdafd9e7c127738c4219980d61f09f2
7
+ data.tar.gz: 163808d47b023456f260985e2180e7de2f46d0b0201729d828bb328d2f16d89c381b4eb7b43191e42a8d4645c14b2e39d6e630409e0847dd01c0e4497235af73
@@ -15,30 +15,90 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
 
18
+ require "base64" unless defined?(Base64)
18
19
  require "mixlib/shellout" unless defined?(Mixlib::ShellOut)
19
20
  require "fileutils" unless defined?(FileUtils)
20
21
  require "json" unless defined?(JSON)
21
22
 
22
23
  module Kitchen
24
+ # Test Kitchen driver plugins.
23
25
  module Driver
26
+ # Helpers for talking to Hyper-V from the Vagrant driver.
27
+ #
28
+ # Hyper-V has no command line interface of its own, so everything here
29
+ # funnels through PowerShell: a script is wrapped so that it dot-sources
30
+ # the bundled `support/hyperv.ps1`, base64-encoded for `-EncodedCommand`
31
+ # (which sidesteps all shell quoting problems), executed, and its JSON
32
+ # output parsed back into Ruby.
33
+ #
34
+ # The module expects to be mixed into a {Kitchen::Configurable} that also
35
+ # provides {Kitchen::Logging}, which the Vagrant driver does.
36
+ #
37
+ # @author Steven Murawski <smurawski@chef.io>
24
38
  module HypervHelpers
39
+ # Encodes a PowerShell script for `powershell.exe -EncodedCommand`.
40
+ #
41
+ # PowerShell requires UTF-16LE, not UTF-8, and strict (unwrapped) base64.
42
+ #
43
+ # @param script [String] a PowerShell script
44
+ # @return [String] the script as single-line base64
45
+ # @api private
25
46
  def encode_command(script)
26
47
  encoded_script = script.encode("UTF-16LE", "UTF-8")
27
48
  Base64.strict_encode64(encoded_script)
28
49
  end
29
50
 
51
+ # The processor architecture Windows reports for this process.
52
+ #
53
+ # `PROCESSOR_ARCHITEW6432` is only set for a 32-bit process running under
54
+ # WOW64, where it holds the *machine's* architecture while
55
+ # `PROCESSOR_ARCHITECTURE` holds the emulated one -- so it wins when
56
+ # present.
57
+ #
58
+ # @return [String,nil] e.g. `"AMD64"`, or nil off Windows
59
+ # @api private
60
+ def os_architecture
61
+ ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
62
+ end
63
+
64
+ # Whether the running Ruby is a 64-bit build, determined from the size of
65
+ # a packed pointer.
66
+ #
67
+ # @return [true,false] whether this Ruby is 64-bit
68
+ # @api private
69
+ def ruby_64bit?
70
+ ["foo"].pack("p").size != 4
71
+ end
72
+
73
+ # Whether both the OS and the running Ruby are 64-bit.
74
+ #
75
+ # @return [true,false] whether this is a 64-bit Ruby on a 64-bit Windows
76
+ # @api private
30
77
  def is_64bit?
31
- os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
32
- ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
33
- os_arch == "AMD64" && ruby_arch == 64
78
+ os_architecture == "AMD64" && ruby_64bit?
34
79
  end
35
80
 
81
+ # Whether both the OS and the running Ruby are 32-bit.
82
+ #
83
+ # Note this is deliberately *not* the negation of {#is_64bit?}: a 32-bit
84
+ # Ruby on a 64-bit Windows (the WOW64 case) is neither.
85
+ #
86
+ # @return [true,false] whether this is a 32-bit Ruby on a 32-bit Windows
87
+ # @api private
36
88
  def is_32bit?
37
- os_arch = ENV["PROCESSOR_ARCHITEW6432"] || ENV["PROCESSOR_ARCHITECTURE"]
38
- ruby_arch = ["foo"].pack("p").size == 4 ? 32 : 64
39
- os_arch != "AMD64" && ruby_arch == 32
89
+ os_architecture != "AMD64" && !ruby_64bit?
40
90
  end
41
91
 
92
+ # Path to a PowerShell that matches the machine's architecture.
93
+ #
94
+ # When Ruby and Windows agree on bitness, the real `System32` PowerShell
95
+ # is correct. When they disagree -- a 32-bit Ruby on 64-bit Windows --
96
+ # `System32` would be silently redirected by WOW64 to the 32-bit
97
+ # PowerShell, which cannot see the Hyper-V cmdlets; `Sysnative` is the
98
+ # alias that reaches the 64-bit one.
99
+ #
100
+ # @return [String] absolute path to powershell.exe
101
+ # @api private
42
102
  def powershell_64_bit
43
103
  if is_64bit? || is_32bit?
44
104
  'c:\windows\system32\windowspowershell\v1.0\powershell.exe'
@@ -47,6 +107,12 @@ module Kitchen
47
107
  end
48
108
  end
49
109
 
110
+ # Builds a full `powershell.exe` command line that dot-sources the gem's
111
+ # `support/hyperv.ps1` helper functions and then runs `script`.
112
+ #
113
+ # @param script [String] a PowerShell script
114
+ # @return [String] a command line ready to hand to a shell
115
+ # @api private
50
116
  def wrap_command(script)
51
117
  base_script_path = File.join(File.dirname(__FILE__), "/../../../support/hyperv.ps1")
52
118
  debug("Loading functions from #{base_script_path}")
@@ -60,6 +126,7 @@ module Kitchen
60
126
  #
61
127
  # @param cmd [String] command to run locally
62
128
  # @param options [Hash] options hash
129
+ # @return [Hash,nil] the parsed JSON the script emitted, if any
63
130
  # @see Kitchen::ShellOut.run_command
64
131
  # @api private
65
132
  def run_ps(cmd, options = {})
@@ -70,6 +137,13 @@ module Kitchen
70
137
  execute_command wrapped_command, options
71
138
  end
72
139
 
140
+ # Runs a command locally and parses its output as JSON.
141
+ #
142
+ # @param cmd [String] command to run locally
143
+ # @param options [Hash] options passed through to `Mixlib::ShellOut`
144
+ # @return [Hash,Array,nil] the parsed output, or nil if there was none
145
+ # @raise [RuntimeError] if the command exited non-zero
146
+ # @api private
73
147
  def execute_command(cmd, options = {})
74
148
  debug("#Local Command BEGIN (#{cmd})")
75
149
  sh = Mixlib::ShellOut.new(cmd, options)
@@ -81,10 +155,21 @@ module Kitchen
81
155
  JSON.parse(stdout) if stdout.length > 2
82
156
  end
83
157
 
158
+ # Strips the interactive prompt lines PowerShell interleaves with real
159
+ # output, which would otherwise make the result unparseable as JSON.
160
+ #
161
+ # @param stdout [String] raw standard output
162
+ # @return [String] output with `PS ...>` lines removed
163
+ # @api private
84
164
  def sanitize_stdout(stdout)
85
165
  stdout.split("\n").select { |s| !s.start_with?("PS") }.join("\n")
86
166
  end
87
167
 
168
+ # Asks Hyper-V for the virtual switch new VMs should be attached to.
169
+ #
170
+ # @return [String] the name of the switch
171
+ # @raise [RuntimeError] if no usable switch could be determined
172
+ # @api private
88
173
  def hyperv_switch
89
174
  default_switch_object = run_ps hyperv_default_switch_ps
90
175
  if default_switch_object.nil? ||
@@ -96,6 +181,11 @@ module Kitchen
96
181
  default_switch_object["Name"]
97
182
  end
98
183
 
184
+ # The PowerShell that {#hyperv_switch} runs. Honours
185
+ # `KITCHEN_HYPERV_SWITCH`; without it, `Get-DefaultVMSwitch` picks one.
186
+ #
187
+ # @return [String] a PowerShell script emitting a JSON switch object
188
+ # @api private
99
189
  def hyperv_default_switch_ps
100
190
  <<-VMSWITCH
101
191
  Get-DefaultVMSwitch #{ENV["KITCHEN_HYPERV_SWITCH"]} | ConvertTo-Json
@@ -104,6 +194,11 @@ module Kitchen
104
194
 
105
195
  private
106
196
 
197
+ # Renders a Ruby Array as a PowerShell array literal.
198
+ #
199
+ # @param list [Array,nil] the values to render
200
+ # @return [String] e.g. `@("a", "b")`, or `@()` when empty
201
+ # @api private
107
202
  def ruby_array_to_ps_array(list)
108
203
  return "@()" if list.nil? || list.empty?
109
204
 
@@ -18,6 +18,13 @@
18
18
  require "erb" unless defined?(Erb)
19
19
  require "fileutils" unless defined?(FileUtils)
20
20
  require "rubygems/version"
21
+ # :nocov:
22
+ # Chefstyle's Chef/Ruby/UnlessDefinedRequire wants this guard, but the guard's
23
+ # other branch is unreachable: kitchen has already loaded "time" by here. The
24
+ # repo enforces a branch-coverage floor, so exclude the line rather than let
25
+ # the two rules fight.
26
+ require "time" unless defined?(Time.now.iso8601)
27
+ # :nocov:
21
28
 
22
29
  require "kitchen"
23
30
  require_relative "vagrant_version"
@@ -31,6 +38,10 @@ module Kitchen
31
38
  #
32
39
  # @author Fletcher Nichol <fnichol@nichol.ca>
33
40
  class Vagrant < Kitchen::Driver::Base
41
+ # Machine states Vagrant reports for a box that is up and reachable.
42
+ #
43
+ # @return [Array<String>]
44
+ LIVE_STATES = %w{running}.freeze
34
45
 
35
46
  include ShellOut
36
47
  include Kitchen::Driver::HypervHelpers
@@ -127,6 +138,11 @@ module Kitchen
127
138
  info("Vagrant instance #{instance.to_str} created.")
128
139
  end
129
140
 
141
+ # The box this Instance should use when the user has not named one.
142
+ #
143
+ # Platforms the Bento project builds are mapped onto their `bento/`
144
+ # box; anything else is assumed to name a box directly.
145
+ #
130
146
  # @return [String,nil] the Vagrant box for this Instance
131
147
  def default_box
132
148
  if bento_box?(instance.platform.name)
@@ -136,6 +152,11 @@ module Kitchen
136
152
  end
137
153
  end
138
154
 
155
+ # The box URL this Instance should use when the user has not named one.
156
+ #
157
+ # Always nil: modern Vagrant resolves boxes through Vagrant Cloud, so an
158
+ # explicit URL is only needed for privately hosted boxes.
159
+ #
139
160
  # @return [String,nil] the Vagrant box URL for this Instance
140
161
  def default_box_url
141
162
  nil
@@ -157,6 +178,12 @@ module Kitchen
157
178
  state.delete(:hostname)
158
179
  end
159
180
 
181
+ # Packages a created instance into a redistributable `.box` file in the
182
+ # current working directory, then destroys the instance.
183
+ #
184
+ # @param state [Hash] mutable instance state
185
+ # @raise [UserError] if the instance has not been created
186
+ # @raise [ActionFailed] if the action could not be completed
160
187
  def package(state)
161
188
  if state[:hostname].nil?
162
189
  raise UserError, "Vagrant instance not created!"
@@ -172,6 +199,27 @@ module Kitchen
172
199
  destroy(state)
173
200
  end
174
201
 
202
+ # Reports what Vagrant currently thinks of the machine.
203
+ #
204
+ # @param state [Hash] instance state naming the machine
205
+ # @return [Hash] a Test Kitchen status hash, or the base implementation's
206
+ # answer when there is nothing to ask Vagrant about
207
+ def status(state)
208
+ return super unless state[:hostname]
209
+
210
+ machine_state = vagrant_machine_state
211
+ return super unless machine_state
212
+
213
+ {
214
+ live: LIVE_STATES.include?(machine_state),
215
+ state: machine_state,
216
+ source: "driver",
217
+ resource_id: instance.name,
218
+ message: "Vagrant reports the machine as #{machine_state}",
219
+ checked_at: Time.now.utc.iso8601,
220
+ }
221
+ end
222
+
175
223
  # A lifecycle method that should be invoked when the object is about
176
224
  # ready to be used. A reference to an Instance is required as
177
225
  # configuration dependant data may be access through an Instance. This
@@ -209,6 +257,23 @@ module Kitchen
209
257
  end
210
258
  end
211
259
 
260
+ # Checks the host-side things a Vagrant run needs, reporting rather than
261
+ # raising so `kitchen doctor` can list every problem at once.
262
+ #
263
+ # {#verify_dependencies} already raises on an old or absent Vagrant, but
264
+ # it only runs on the actions that need it. This repeats the version rule
265
+ # as a report and adds the file and folder checks that nothing validates
266
+ # today.
267
+ #
268
+ # @param state [Hash] mutable instance and driver state
269
+ # @return [Boolean] true when a problem was reported
270
+ def doctor(state) # rubocop:disable Lint/UnusedMethodArgument
271
+ problems = vagrant_problems + template_problems + synced_folder_problems
272
+
273
+ problems.each { |problem| warn(problem) }
274
+ !problems.empty?
275
+ end
276
+
212
277
  # @return [TrueClass,FalseClass] whether or not the transport's name
213
278
  # implies a WinRM-based transport
214
279
  # @api private
@@ -216,9 +281,11 @@ module Kitchen
216
281
  instance.transport.name.downcase =~ /win_?rm/
217
282
  end
218
283
 
219
- # Setting up the `cache_directory` to store omnibus packages in cache
220
- # and share a local folder to that directory so that we don't pull them
221
- # down every single time
284
+ # The guest-side directory that the host's omnibus package cache should
285
+ # be shared into, so repeated converges do not re-download packages.
286
+ #
287
+ # @return [String,false] the guest path, or false if caching does not
288
+ # apply to this box and provider combination
222
289
  def cache_directory
223
290
  if enable_cache?
224
291
  config[:cache_directory]
@@ -229,7 +296,10 @@ module Kitchen
229
296
 
230
297
  protected
231
298
 
299
+ # Where users are pointed when Vagrant is missing or too old.
232
300
  WEBSITE = "https://developer.hashicorp.com/vagrant/install".freeze
301
+
302
+ # The oldest Vagrant this driver supports.
233
303
  MIN_VER = "2.4.0".freeze
234
304
 
235
305
  class << self
@@ -238,10 +308,11 @@ module Kitchen
238
308
  attr_accessor :vagrant_version
239
309
  end
240
310
 
241
- # Retuns whether or not a platform name could have a correcponding Bento
311
+ # Returns whether or not a platform name could have a corresponding Bento
242
312
  # box produced by the Bento project.
243
313
  # (https://github.com/chef/bento).
244
314
  #
315
+ # @param name [String] a Test Kitchen platform name
245
316
  # @return [TrueClass,FalseClass] whether or not the name could be a Bento
246
317
  # box
247
318
  # @api private
@@ -250,8 +321,13 @@ module Kitchen
250
321
  end
251
322
 
252
323
  # Returns whether or not the we expect the box to work with shared folders
253
- # by matching against a whitelist of bento boxes
254
- # @return [TrueClass,FalseClass] whether or not the box shoud work with
324
+ # by matching against a whitelist of bento boxes.
325
+ #
326
+ # Providers without usable shared folder support are excluded outright,
327
+ # whatever the box.
328
+ #
329
+ # @param box [String] the Vagrant box name
330
+ # @return [TrueClass,FalseClass] whether or not the box should work with
255
331
  # shared folders
256
332
  # @api private
257
333
  def safe_share?(box)
@@ -261,7 +337,11 @@ module Kitchen
261
337
  end
262
338
 
263
339
  # Return true if we found the criteria to enable the cache_directory
264
- # functionality
340
+ # functionality.
341
+ #
342
+ # @return [TrueClass,FalseClass] whether the package cache should be
343
+ # shared into the guest
344
+ # @api private
265
345
  def enable_cache?
266
346
  return false unless config[:cache_directory]
267
347
  return true if safe_share?(config[:box])
@@ -271,8 +351,10 @@ module Kitchen
271
351
  false
272
352
  end
273
353
 
274
- # Renders and writes out a Vagrantfile dedicated to this instance.
354
+ # Renders and writes out a Vagrantfile dedicated to this instance. A
355
+ # no-op if this action has already written one.
275
356
  #
357
+ # @return [void]
276
358
  # @api private
277
359
  def create_vagrantfile
278
360
  return if @vagrantfile_created
@@ -288,6 +370,7 @@ module Kitchen
288
370
  # Logs the Vagrantfile's contents to the debug log level.
289
371
  #
290
372
  # @param vagrantfile [String] path to the Vagrantfile
373
+ # @return [void]
291
374
  # @api private
292
375
  def debug_vagrantfile(vagrantfile)
293
376
  return unless logger.debug?
@@ -297,8 +380,10 @@ module Kitchen
297
380
  debug("------------")
298
381
  end
299
382
 
300
- # Setup path for CA cert
383
+ # Expands `:box_download_ca_cert` relative to the kitchen root, so a
384
+ # `.kitchen.yml` can refer to a cert alongside itself.
301
385
  #
386
+ # @return [void]
302
387
  # @api private
303
388
  def finalize_ca_cert!
304
389
  unless config[:box_download_ca_cert].nil?
@@ -308,9 +393,14 @@ module Kitchen
308
393
  end
309
394
  end
310
395
 
311
- # Create vagrant command to update box to the latest version
396
+ # Replaces a truthy `:box_auto_update` with the `vagrant box update`
397
+ # command line that {#run_box_auto_update} should run. A falsey value is
398
+ # left alone so that `box_auto_update: false` stays disabled.
399
+ #
400
+ # @return [void]
401
+ # @api private
312
402
  def finalize_box_auto_update!
313
- return if config[:box_auto_update].nil?
403
+ return unless config[:box_auto_update]
314
404
 
315
405
  cmd = "#{config[:vagrant_binary]} box update --box #{config[:box]}"
316
406
  cmd += " --architecture #{config[:box_arch]}" if config[:box_arch]
@@ -319,9 +409,14 @@ module Kitchen
319
409
  config[:box_auto_update] = cmd
320
410
  end
321
411
 
322
- # Create vagrant command to remove older versions of the box
412
+ # Replaces a truthy `:box_auto_prune` with the `vagrant box prune`
413
+ # command line that {#run_box_auto_prune} should run. A falsey value is
414
+ # left alone so that `box_auto_prune: false` stays disabled.
415
+ #
416
+ # @return [void]
417
+ # @api private
323
418
  def finalize_box_auto_prune!
324
- return if config[:box_auto_prune].nil?
419
+ return unless config[:box_auto_prune]
325
420
 
326
421
  cmd = "#{config[:vagrant_binary]} box prune --force --keep-active-boxes --name #{config[:box]}"
327
422
  cmd += " --provider #{config[:provider]}" if config[:provider]
@@ -357,8 +452,26 @@ module Kitchen
357
452
  end
358
453
  end
359
454
 
360
- # Replaces an `%{instance_name}` tokens in the synced folder items.
455
+ # Formats network options for use in the Vagrantfile.
456
+ #
457
+ # Accepts either a Hash (rendered as Ruby keyword syntax) or a String
458
+ # (passed through as-is, which is what {#finalize_network!} produces).
459
+ #
460
+ # @param options [Hash,String,#to_s] network options
461
+ # @return [String] formatted options string for Vagrantfile
462
+ # @api private
463
+ def format_network_options(options)
464
+ return options if options.is_a?(String)
465
+ return options.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") if options.is_a?(Hash)
466
+
467
+ options.to_s
468
+ end
469
+
470
+ # Normalises every `:synced_folders` entry: expands the host path
471
+ # against the kitchen root, substitutes `%{instance_name}` tokens, and
472
+ # formats the options for the template.
361
473
  #
474
+ # @return [void]
362
475
  # @api private
363
476
  def finalize_synced_folders!
364
477
  config[:synced_folders] = config[:synced_folders]
@@ -375,9 +488,11 @@ module Kitchen
375
488
  add_extra_synced_folders!
376
489
  end
377
490
 
378
- # We would like to sync a local folder to the instance so we can
379
- # take advantage of the packages that we might have in cache,
380
- # therefore we wont download a package we already have
491
+ # Shares the host's omnibus package cache into the guest so a converge
492
+ # does not re-download packages it already has.
493
+ #
494
+ # @return [void]
495
+ # @api private
381
496
  def add_extra_synced_folders!
382
497
  if cache_directory
383
498
  FileUtils.mkdir_p(local_kitchen_cache)
@@ -389,9 +504,12 @@ module Kitchen
389
504
  end
390
505
  end
391
506
 
392
- # Truncates the length of `:vm_hostname` to 12 characters for
393
- # Windows-based operating systems.
507
+ # Truncates an over-long `:vm_hostname` on Windows guests, where the
508
+ # NetBIOS name is capped at 15 characters. The final character of the
509
+ # original is kept as a suffix so that names which differ only in their
510
+ # tail do not collapse onto each other.
394
511
  #
512
+ # @return [void]
395
513
  # @api private
396
514
  def finalize_vm_hostname!
397
515
  string = config[:vm_hostname]
@@ -401,17 +519,20 @@ module Kitchen
401
519
  end
402
520
  end
403
521
 
404
- # If Hyper-V and no network configuration
405
- # check KITCHEN_HYPERV_SWITCH and fallback to helper method
406
- # to select the best switch
522
+ # Gives a Hyper-V instance a default `public_network` bridged onto the
523
+ # switch named by `KITCHEN_HYPERV_SWITCH`, or whichever switch
524
+ # {HypervHelpers#hyperv_switch} considers best. Instances that already
525
+ # configure a network, and every other provider, are left alone.
526
+ #
527
+ # @return [void]
407
528
  # @api private
408
529
  def finalize_network!
409
- if config[:provider] == "hyperv" && config[:network].empty?
410
- config[:network].push([
411
- "public_network",
412
- "bridge: \"#{hyperv_switch}\"",
413
- ])
414
- end
530
+ return unless config[:provider] == "hyperv" && config[:network].empty?
531
+
532
+ # Deliberately a new Array rather than a push: `default_config` hands
533
+ # every instance the *same* Array object, so mutating it in place would
534
+ # leak this network into every other instance in the process.
535
+ config[:network] = [["public_network", %{bridge: "#{hyperv_switch}"}]]
415
536
  end
416
537
 
417
538
  # Renders the Vagrantfile ERb template.
@@ -431,6 +552,90 @@ module Kitchen
431
552
  end
432
553
  end
433
554
 
555
+ # Asks Vagrant for the machine state.
556
+ #
557
+ # `--machine-readable` is parsed rather than the human output because the
558
+ # human wording is localised and reworded between Vagrant releases, while
559
+ # the machine format is a stable comma-separated
560
+ # `timestamp,target,type,data` and the `state` row carries the raw value
561
+ # (`running`, `poweroff`, `not_created`, ...).
562
+ #
563
+ # @return [String, nil] the machine state, or nil when there is no
564
+ # Vagrantfile to ask about, the command fails, or no state row is found
565
+ # @api private
566
+ def vagrant_machine_state
567
+ return nil unless File.exist?(File.join(vagrant_root, "Vagrantfile"))
568
+
569
+ output = run("#{config[:vagrant_binary]} status --machine-readable")
570
+ parse_machine_state(output)
571
+ rescue ::StandardError => e
572
+ debug("Could not read the Vagrant machine state: #{e.message}")
573
+ nil
574
+ end
575
+
576
+ # Pulls the `state` row out of Vagrant's machine-readable output.
577
+ #
578
+ # @param output [String] the raw `--machine-readable` output
579
+ # @return [String, nil] the state value, or nil when no state row is present
580
+ # @api private
581
+ def parse_machine_state(output)
582
+ output.to_s.each_line do |line|
583
+ fields = line.strip.split(",")
584
+ return fields[3] if fields[2] == "state" && fields[3]
585
+ end
586
+ nil
587
+ end
588
+
589
+ # Vagrant itself: present, and new enough.
590
+ #
591
+ # @return [Array<String>] a problem, or an empty array
592
+ def vagrant_problems
593
+ found = vagrant_version
594
+ if Gem::Version.new(found) < Gem::Version.new(MIN_VER.dup)
595
+ return ["Vagrant #{found} is older than the #{MIN_VER} this driver " \
596
+ "needs. Upgrade from #{WEBSITE}."]
597
+ end
598
+
599
+ info("vagrant #{found} found at #{config[:vagrant_binary]}")
600
+ []
601
+ rescue UserError => e
602
+ [e.message]
603
+ rescue ::StandardError => e
604
+ ["Could not run #{config[:vagrant_binary]} --version: #{e.message}"]
605
+ end
606
+
607
+ # The Vagrantfile template, and any extra Vagrantfiles spliced into it.
608
+ # Both are `expand_path_for` settings, so a wrong relative path becomes
609
+ # an absolute path that simply is not there.
610
+ #
611
+ # @return [Array<String>] one problem per missing file
612
+ def template_problems
613
+ problems = []
614
+
615
+ unless File.exist?(config[:vagrantfile_erb].to_s)
616
+ problems << "vagrantfile_erb #{config[:vagrantfile_erb]} does not exist."
617
+ end
618
+
619
+ Array(config[:vagrantfiles]).each do |path|
620
+ problems << "vagrantfiles entry #{path} does not exist." unless File.exist?(path.to_s)
621
+ end
622
+
623
+ problems
624
+ end
625
+
626
+ # Host paths in `synced_folders`. Vagrant creates a missing host path
627
+ # silently for some providers and fails for others, so an absent source
628
+ # is worth naming either way.
629
+ #
630
+ # @return [Array<String>] one problem per missing host path
631
+ def synced_folder_problems
632
+ Array(config[:synced_folders]).filter_map do |source, destination, _options|
633
+ next if source.nil? || File.exist?(source.to_s)
634
+
635
+ "synced_folders source #{source} (mounted at #{destination}) does not exist."
636
+ end
637
+ end
638
+
434
639
  # Convenience method to run a command locally.
435
640
  #
436
641
  # @param cmd [String] command to run locally
@@ -454,6 +659,9 @@ module Kitchen
454
659
  # any bundler environment should we detect one. Otherwise, subcommands
455
660
  # will inherit our bundled environment.
456
661
  # @see https://github.com/test-kitchen/kitchen-vagrant/issues/190
662
+ # @param cmd [String] command to run locally
663
+ # @param options [Hash] options hash
664
+ # @return [String] the standard output of the command
457
665
  # @see Kitchen::ShellOut#run_command
458
666
  # rubocop:disable Metrics/CyclomaticComplexity
459
667
  def run_command(cmd, options = {})
@@ -479,10 +687,10 @@ module Kitchen
479
687
  # is passed to a windows process with a PATH, Vagrant's batch installer
480
688
  # (https://github.com/mitchellh/vagrant-installers/blob/master/substrate
481
689
  # /modules/vagrant_installer/templates/windows_vagrant.bat.erb)
482
- # does not efectively prepend the vagrant ruby path in a persistent
690
+ # does not effectively prepend the vagrant ruby path in a persistent
483
691
  # manner which causes vagrant to use the same ruby as test-kitchen and
484
692
  # then the environment is essentially corrupted leading to many errors
485
- # and dispair
693
+ # and despair
486
694
  unless windows_host?
487
695
  gem_home = ENV["GEM_HOME"]
488
696
  if gem_home && (env["PATH"] || ENV["PATH"])
@@ -496,8 +704,11 @@ module Kitchen
496
704
  end
497
705
  # rubocop:enable Metrics/CyclomaticComplexity
498
706
 
499
- # Check if a newer version of the vagrant box is available and warn the user
707
+ # Check if a newer version of the vagrant box is available and warn the
708
+ # user. Skipped when `:box_auto_update` is on, since the update happens
709
+ # regardless.
500
710
  #
711
+ # @return [void]
501
712
  # @api private
502
713
  def check_box_outdated
503
714
  # Skip if box_auto_update is enabled (they'll get the update anyway)
@@ -519,6 +730,7 @@ module Kitchen
519
730
  # Parse vagrant box outdated output and warn if a new version is available
520
731
  #
521
732
  # @param output [String] output from vagrant box outdated command
733
+ # @return [void]
522
734
  # @api private
523
735
  def warn_if_outdated(output)
524
736
  return unless box_is_outdated?(output)
@@ -549,15 +761,20 @@ module Kitchen
549
761
  output_downcase.include?("newer version of the box")
550
762
  end
551
763
 
764
+ # Characters that may legitimately appear inside a box version. Vagrant
765
+ # quotes versions in prose ("version '202401.31.0'.") and separates them
766
+ # with punctuation in tabular output ("Current: 1.2.3, Latest: 4.5.6"),
767
+ # so the version has to end on an alphanumeric to avoid swallowing the
768
+ # trailing quote, comma or full stop.
769
+ VERSION_PATTERN = /v?(\w[\w.+-]*\w|\w)/
770
+
552
771
  # Extract current version from vagrant box outdated output
553
772
  #
554
773
  # @param output [String] output from vagrant box outdated command
555
774
  # @return [String, nil] current version or nil if not found
556
775
  # @api private
557
776
  def extract_current_version(output)
558
- match = output.match(/Current:\s+v?(\S+)/i) ||
559
- output.match(/currently have version\s+'?v?([^'.\s]+)/i)
560
- match ? match[1] : nil
777
+ extract_version(output, /Current:\s+/i, /currently have version\s+'?/i)
561
778
  end
562
779
 
563
780
  # Extract latest version from vagrant box outdated output
@@ -566,12 +783,32 @@ module Kitchen
566
783
  # @return [String, nil] latest version or nil if not found
567
784
  # @api private
568
785
  def extract_latest_version(output)
569
- match = output.match(/Latest:\s+v?(\S+)/i) ||
570
- output.match(/latest is version\s+'?v?([^'.\s]+)/i)
571
- match ? match[1] : nil
786
+ extract_version(output, /Latest:\s+/i, /latest is version\s+'?/i)
787
+ end
788
+
789
+ # Finds the first version number that follows any of the given prefixes.
790
+ #
791
+ # @param output [String] output from vagrant box outdated command
792
+ # @param prefixes [Array<Regexp>] prefixes to look for, in priority order
793
+ # @return [String, nil] the version, or nil if no prefix matched
794
+ # @api private
795
+ def extract_version(output, *prefixes)
796
+ prefixes.each do |prefix|
797
+ match = output.match(/#{prefix.source}#{VERSION_PATTERN.source}/i)
798
+ return match[1] if match
799
+ end
800
+ nil
572
801
  end
573
802
 
574
- # Tell vagrant to update vagrant box to latest version
803
+ # Runs the `vagrant box update` command built by
804
+ # {#finalize_box_auto_update!}, if any.
805
+ #
806
+ # A box that has never been downloaded cannot be updated; that specific
807
+ # failure is expected on a first run and is swallowed.
808
+ #
809
+ # @return [void]
810
+ # @raise [Kitchen::ShellOut::ShellCommandFailed] for any other failure
811
+ # @api private
575
812
  def run_box_auto_update
576
813
  if config[:box_auto_update]
577
814
  begin
@@ -584,15 +821,21 @@ module Kitchen
584
821
  end
585
822
  end
586
823
 
587
- # Tell vagrant to remove older vagrant boxes
824
+ # Runs the `vagrant box prune` command built by
825
+ # {#finalize_box_auto_prune!}, if any.
826
+ #
827
+ # @return [void]
828
+ # @api private
588
829
  def run_box_auto_prune
589
830
  if config[:box_auto_prune]
590
831
  run(config[:box_auto_prune])
591
832
  end
592
833
  end
593
834
 
594
- # Runs a local command before `vagrant up` has been called.
835
+ # Runs `:pre_create_command`, if set, from the kitchen root -- before
836
+ # `vagrant up`, so it can prepare anything the Vagrantfile depends on.
595
837
  #
838
+ # @return [void]
596
839
  # @api private
597
840
  def run_pre_create_command
598
841
  if config[:pre_create_command]
@@ -603,6 +846,8 @@ module Kitchen
603
846
  # Runs a local command without streaming the stdout to the logger.
604
847
  #
605
848
  # @param cmd [String] command to run locally
849
+ # @param options [Hash] options hash
850
+ # @return [String] the standard output of the command
606
851
  # @api private
607
852
  def run_silently(cmd, options = {})
608
853
  merged = {
@@ -613,6 +858,7 @@ module Kitchen
613
858
 
614
859
  # Runs the `vagrant up` command locally.
615
860
  #
861
+ # @return [void]
616
862
  # @api private
617
863
  def run_vagrant_up
618
864
  cmd = "#{config[:vagrant_binary]} up"
@@ -621,9 +867,11 @@ module Kitchen
621
867
  run(cmd)
622
868
  end
623
869
 
624
- # Updates any state after creation.
870
+ # Records the connection details Vagrant reports for the new machine
871
+ # into the instance state, so the transport can reach it.
625
872
  #
626
873
  # @param state [Hash] mutable instance state
874
+ # @return [void]
627
875
  # @api private
628
876
  def update_state(state)
629
877
  hash = winrm_transport? ? vagrant_config(:winrm) : vagrant_config(:ssh)
@@ -17,9 +17,10 @@
17
17
 
18
18
  module Kitchen
19
19
 
20
+ # Test Kitchen driver plugins.
20
21
  module Driver
21
22
 
22
23
  # Version string for Vagrant Kitchen driver
23
- VAGRANT_VERSION = "2.2.1".freeze
24
+ VAGRANT_VERSION = "2.4.0".freeze
24
25
  end
25
26
  end
@@ -85,7 +85,7 @@ Vagrant.configure("2") do |c|
85
85
  <% end %>
86
86
 
87
87
  <% Array(config[:network]).each do |opts| %>
88
- c.vm.network(:<%= opts[0] %>, <%= opts[1].map { |k, v| "#{k}: #{v.inspect}" }.join(', ') %>)
88
+ c.vm.network(:<%= opts[0] %>, <%= format_network_options(opts[1]) %>)
89
89
  <% end %>
90
90
 
91
91
  c.vm.synced_folder ".", "/vagrant", disabled: true
@@ -97,6 +97,8 @@ Vagrant.configure("2") do |c|
97
97
  <% case config[:provider]
98
98
  when "virtualbox" %>
99
99
  p.name = "kitchen-<%= File.basename(config[:kitchen_root]) %>-<%= instance.name %>-<%= SecureRandom.uuid %>"
100
+ <% when "tart" %>
101
+ p.name = "kitchen-<%= File.basename(config[:kitchen_root]) %>-<%= instance.name %>"
100
102
  <% end %>
101
103
 
102
104
  <% case config[:provider]
metadata CHANGED
@@ -1,22 +1,36 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kitchen-vagrant
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.1
4
+ version: 2.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fletcher Nichol
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-22 00:00:00.000000000 Z
11
+ date: 2026-08-24 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: base64
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.2'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: test-kitchen
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
31
  - - ">="
18
32
  - !ruby/object:Gem::Version
19
- version: '1.4'
33
+ version: '3.0'
20
34
  - - "<"
21
35
  - !ruby/object:Gem::Version
22
36
  version: '5'
@@ -26,7 +40,7 @@ dependencies:
26
40
  requirements:
27
41
  - - ">="
28
42
  - !ruby/object:Gem::Version
29
- version: '1.4'
43
+ version: '3.0'
30
44
  - - "<"
31
45
  - !ruby/object:Gem::Version
32
46
  version: '5'
@@ -55,7 +69,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
55
69
  requirements:
56
70
  - - ">="
57
71
  - !ruby/object:Gem::Version
58
- version: '3.0'
72
+ version: '3.1'
59
73
  required_rubygems_version: !ruby/object:Gem::Requirement
60
74
  requirements:
61
75
  - - ">="