vagrant-vmware-desktop 0.0.1 → 3.0.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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/lib/vagrant-vmware-desktop.rb +190 -0
  3. data/lib/vagrant-vmware-desktop/action.rb +442 -0
  4. data/lib/vagrant-vmware-desktop/action/base_mac_to_ip.rb +55 -0
  5. data/lib/vagrant-vmware-desktop/action/boot.rb +26 -0
  6. data/lib/vagrant-vmware-desktop/action/check_existing_network.rb +35 -0
  7. data/lib/vagrant-vmware-desktop/action/check_vmware.rb +28 -0
  8. data/lib/vagrant-vmware-desktop/action/checkpoint.rb +86 -0
  9. data/lib/vagrant-vmware-desktop/action/clear_shared_folders.rb +25 -0
  10. data/lib/vagrant-vmware-desktop/action/common.rb +16 -0
  11. data/lib/vagrant-vmware-desktop/action/compatibility.rb +36 -0
  12. data/lib/vagrant-vmware-desktop/action/created.rb +20 -0
  13. data/lib/vagrant-vmware-desktop/action/destroy.rb +32 -0
  14. data/lib/vagrant-vmware-desktop/action/discard_suspended_state.rb +32 -0
  15. data/lib/vagrant-vmware-desktop/action/export.rb +29 -0
  16. data/lib/vagrant-vmware-desktop/action/fix_old_machine_id.rb +29 -0
  17. data/lib/vagrant-vmware-desktop/action/forward_ports.rb +110 -0
  18. data/lib/vagrant-vmware-desktop/action/halt.rb +27 -0
  19. data/lib/vagrant-vmware-desktop/action/import.rb +138 -0
  20. data/lib/vagrant-vmware-desktop/action/machine_lock.rb +26 -0
  21. data/lib/vagrant-vmware-desktop/action/message_already_running.rb +18 -0
  22. data/lib/vagrant-vmware-desktop/action/message_not_created.rb +18 -0
  23. data/lib/vagrant-vmware-desktop/action/message_not_running.rb +18 -0
  24. data/lib/vagrant-vmware-desktop/action/network.rb +339 -0
  25. data/lib/vagrant-vmware-desktop/action/package_vagrantfile.rb +46 -0
  26. data/lib/vagrant-vmware-desktop/action/prepare_forwarded_port_collision_params.rb +28 -0
  27. data/lib/vagrant-vmware-desktop/action/prepare_nfs_settings.rb +43 -0
  28. data/lib/vagrant-vmware-desktop/action/prepare_synced_folder_cleanup.rb +19 -0
  29. data/lib/vagrant-vmware-desktop/action/prune_forwarded_ports.rb +30 -0
  30. data/lib/vagrant-vmware-desktop/action/prune_nfs_exports.rb +22 -0
  31. data/lib/vagrant-vmware-desktop/action/running.rb +20 -0
  32. data/lib/vagrant-vmware-desktop/action/set_display_name.rb +37 -0
  33. data/lib/vagrant-vmware-desktop/action/share_folders.rb +97 -0
  34. data/lib/vagrant-vmware-desktop/action/snapshot_delete.rb +26 -0
  35. data/lib/vagrant-vmware-desktop/action/snapshot_restore.rb +26 -0
  36. data/lib/vagrant-vmware-desktop/action/snapshot_save.rb +26 -0
  37. data/lib/vagrant-vmware-desktop/action/suspend.rb +26 -0
  38. data/lib/vagrant-vmware-desktop/action/suspended.rb +24 -0
  39. data/lib/vagrant-vmware-desktop/action/vmx_modify.rb +39 -0
  40. data/lib/vagrant-vmware-desktop/action/wait_for_address.rb +31 -0
  41. data/lib/vagrant-vmware-desktop/action/wait_for_communicator_compat.rb +32 -0
  42. data/lib/vagrant-vmware-desktop/action/wait_for_vmx_halt.rb +35 -0
  43. data/lib/vagrant-vmware-desktop/cap/disk.rb +287 -0
  44. data/lib/vagrant-vmware-desktop/cap/provider.rb +37 -0
  45. data/lib/vagrant-vmware-desktop/cap/snapshot.rb +41 -0
  46. data/lib/vagrant-vmware-desktop/checkpoint_client.rb +203 -0
  47. data/lib/vagrant-vmware-desktop/config.rb +377 -0
  48. data/lib/vagrant-vmware-desktop/constants.rb +16 -0
  49. data/lib/vagrant-vmware-desktop/driver.rb +15 -0
  50. data/lib/vagrant-vmware-desktop/driver/base.rb +1356 -0
  51. data/lib/vagrant-vmware-desktop/errors.rb +342 -0
  52. data/lib/vagrant-vmware-desktop/guest_cap/linux/mount_vmware_shared_folder.rb +158 -0
  53. data/lib/vagrant-vmware-desktop/guest_cap/linux/verify_vmware_hgfs.rb +27 -0
  54. data/lib/vagrant-vmware-desktop/helper/lock.rb +26 -0
  55. data/lib/vagrant-vmware-desktop/helper/routing_table.rb +182 -0
  56. data/lib/vagrant-vmware-desktop/helper/vagrant_utility.rb +185 -0
  57. data/lib/vagrant-vmware-desktop/plugin.rb +148 -0
  58. data/lib/vagrant-vmware-desktop/provider.rb +96 -0
  59. data/lib/vagrant-vmware-desktop/setup_plugin.rb +24 -0
  60. data/lib/vagrant-vmware-desktop/synced_folder.rb +93 -0
  61. data/locales/en.yml +634 -0
  62. metadata +71 -17
@@ -0,0 +1,16 @@
1
+ module HashiCorp
2
+ module VagrantVMwareDesktop
3
+ version_text_path = File.expand_path(File.join(__dir__, "../../versions/desktop.txt"))
4
+ if File.exist?(version_text_path)
5
+ VERSION = File.read(version_text_path)
6
+ else
7
+ VERSION = "STUB"
8
+ end
9
+
10
+ # This is the name of the gem.
11
+ #
12
+ # @return [String]
13
+ PRODUCT_NAME = RbConfig::CONFIG["host_os"].include?("darwin") ? "fusion" : "workstation"
14
+ PLUGIN_NAME = "vagrant-vmware-desktop"
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ require "vagrant/util/platform"
2
+
3
+ require "vagrant-vmware-desktop/driver/base"
4
+
5
+ module HashiCorp
6
+ module VagrantVMwareDesktop
7
+ module Driver
8
+ # This returns a new driver for the given VM directory, using the
9
+ # proper underlying platform driver.
10
+ def self.create(vm_dir, config)
11
+ Base.new(vm_dir, config)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,1356 @@
1
+ require "fileutils"
2
+ require "ipaddr"
3
+ require "pathname"
4
+
5
+ require "log4r"
6
+
7
+ require "vagrant/util/busy"
8
+ require 'vagrant/util/platform'
9
+ require 'vagrant/util/retryable'
10
+ require "vagrant/util/subprocess"
11
+
12
+ require "vagrant-vmware-desktop/helper/vagrant_utility"
13
+
14
+ module HashiCorp
15
+ module VagrantVMwareDesktop
16
+ module Driver
17
+ # This is the base driver for VMware products, which contains
18
+ # some shared common helpers.
19
+ class Base
20
+
21
+ # Default NAT device when detection is unavailable
22
+ DEFAULT_NAT_DEVICE = "vmnet8".freeze
23
+
24
+ SECTOR_TO_BYTES = 512.freeze
25
+
26
+ # Vagrant utility version requirement which must be satisfied to properly
27
+ # work with this version of the plugin. This should be used when new API
28
+ # end points are added to the utility to ensure expected functionality.
29
+ VAGRANT_UTILITY_VERSION_REQUIREMENT = "~> 1.0.14".freeze
30
+
31
+ # Enforce VMX ethernet allowlisting. Truthy or falsey values enable/disable. :quiet will
32
+ # enforce the allowlist without printing warning to user
33
+ VMX_ETHERNET_ALLOWLIST_ENFORCE = false
34
+
35
+ # allowlisted networking settings that should not be removed from
36
+ # the VMX settings when configuring devices
37
+ VMX_ETHERNET_ALLOWLIST = ["pcislotnumber"].map(&:freeze).freeze
38
+
39
+ # Warning template printed to user when a allowlisted setting is
40
+ # detected in the VMX configuration _before_ allowlisting is enabled
41
+ VMX_ETHERNET_ALLOWLIST_DETECTION_PREWARNING = <<-EOW.gsub(/^ {10}/, "").freeze
42
+ The VMX file for this box contains a setting that is automatically overwritten by Vagrant
43
+ when started. Vagrant will stop overwriting this setting in an upcoming release which may
44
+ prevent proper networking setup. Below is the detected VMX setting:
45
+
46
+ %VMX_KEY% = "%VMX_VALUE%"
47
+
48
+ If networking fails to properly configure, it may require this VMX setting. It can be manually
49
+ applied via the Vagrantfile:
50
+
51
+ Vagrant.configure(2) do |config|
52
+ config.vm.provider :vmware_desktop do |vmware|
53
+ vmware.vmx["%VMX_KEY%"] = "%VMX_VALUE%"
54
+ end
55
+ end
56
+
57
+ For more information: https://www.vagrantup.com/docs/vmware/boxes.html#vmx-allowlisting
58
+ EOW
59
+
60
+ # Customized setting messages to display after white list is enforced to fix
61
+ # boxes that may be broken by allowlisting
62
+ VMX_ETHERNET_ALLOWLIST_POSTFIX = {
63
+ "pcislotnumber" => <<-EOP.gsub(/^ {12}/, "").freeze
64
+ If networking fails to properly configure, it may require adding the following setting to
65
+ the Vagrantfile:
66
+
67
+ Vagrant.configure(2) do |config|
68
+ config.vm.provider :vmware_desktop do |vmware|
69
+ vmware.vmx["%VMX_KEY%"] = "32"
70
+ end
71
+ end
72
+
73
+ For more information: https://www.vagrantup.com/docs/vmware/boxes.html#vmx-allowlisting
74
+ EOP
75
+ }
76
+
77
+ # Warning template printed to user when a allowlisted setting is
78
+ # detected in the VMX configuration _after_ allowlisting is enabled
79
+ VMX_ETHERNET_ALLOWLIST_DETECTION_WARNING = <<-EOW.gsub(/^ {10}/, "").freeze
80
+ The VMX file for this box contains a setting that is no longer overwritten by Vagrant. This
81
+ may cause networking setup for the VM to fail. Below is the detected VMX setting that is
82
+ no longer overwritten by Vagrant:
83
+
84
+ %VMX_KEY% = "%VMX_VALUE%"
85
+
86
+ For more information: https://www.vagrantup.com/docs/vmware/boxes.html#vmx-allowlisting
87
+ EOW
88
+
89
+ # This is the path to the VM folder. If it is nil then the VM
90
+ # hasn't yet been created.
91
+ #
92
+ # @return [Pathname]
93
+ attr_reader :vm_dir
94
+
95
+ # This is the path to the VMX file.
96
+ #
97
+ # @return [Pathname]
98
+ attr_reader :vmx_path
99
+
100
+ # Helper utility for interacting with the Vagrant VMware Utility REST API
101
+ #
102
+ # @return [Helper::VagrantUtility]
103
+ attr_reader :vagrant_utility
104
+
105
+ # Provider config
106
+ #
107
+ # @return [Vagrant::Config]
108
+ attr_reader :config
109
+
110
+ # Vagrant utility version
111
+ #
112
+ # @return [Gem::Version]
113
+ attr_reader :utility_version
114
+
115
+ # Include this so we can retry some subprocess stuff
116
+ include Vagrant::Util::Retryable
117
+
118
+ def initialize(vmx_path, config)
119
+ @config = config
120
+
121
+ @logger = Log4r::Logger.new("hashicorp::provider::vmware_driver")
122
+ @vmx_path = vmx_path
123
+ @vmx_path = Pathname.new(@vmx_path) if @vmx_path
124
+
125
+ if @vmx_path && @vmx_path.directory?
126
+ # The VMX path is a directory, not a VMX. This is probably due to
127
+ # legacy ID of a VM. Early versions of the VMware provider used the
128
+ # directory as the ID rather than the VMX itself.
129
+ @logger.info("VMX path is a directory. Finding VMX file...")
130
+
131
+ # Set the VM dir
132
+ @vm_dir = @vmx_path
133
+
134
+ # Find the VMX file
135
+ @vmx_path = nil
136
+ @vm_dir.children(true).each do |child|
137
+ if child.basename.to_s =~ /^(.+?)\.vmx$/
138
+ @vmx_path = child
139
+ break
140
+ end
141
+ end
142
+
143
+ raise Errors::DriverMissingVMX, :vm_dir => @vm_dir.to_s if !@vmx_path
144
+ end
145
+
146
+ # Make sure the vm_dir is properly set always to the directory
147
+ # containing the VMX
148
+ @vm_dir = nil
149
+ @vm_dir = @vmx_path.parent if @vmx_path
150
+
151
+ @logger.info("VMX file: #{@vmx_path}")
152
+ @vagrant_utility = Helper::VagrantUtility.new(
153
+ config.utility_host, config.utility_port,
154
+ certificate_path: config.utility_certificate_path
155
+ )
156
+
157
+ if config.force_vmware_license
158
+ @logger.warn("overriding VMware license detection with value: #{config.force_vmware_license}")
159
+ @license = config.force_vmware_license
160
+ end
161
+
162
+ set_vmware_info
163
+ if config.nat_device.nil?
164
+ if professional?
165
+ detect_nat_device!
166
+ else
167
+ @logger.warn("standard license is in use - forcing default NAT device (#{DEFAULT_NAT_DEVICE})")
168
+ config.nat_device = DEFAULT_NAT_DEVICE
169
+ end
170
+ end
171
+ end
172
+
173
+ # @return [Boolean] using standard license
174
+ def standard?
175
+ !professional?
176
+ end
177
+
178
+ # @return [Boolean] using professional license
179
+ def professional?
180
+ @pro_license
181
+ end
182
+
183
+ # Pull list of vmware devices and detect any NAT types. Use
184
+ # first device discovered
185
+ def detect_nat_device!
186
+ vmnets = read_vmnet_devices
187
+ # First check the default device and if it has NAT enabled, use that
188
+ device = vmnets.detect do |dev|
189
+ dev[:name] == DEFAULT_NAT_DEVICE &&
190
+ dev[:type].to_s.downcase == "nat" &&
191
+ dev[:dhcp] && dev[:hostonly_subnet]
192
+ end
193
+ # If the default device isn't properly configured, now we detect
194
+ if !device
195
+ device = vmnets.detect do |dev|
196
+ @logger.debug("checking device for NAT usage #{dev}")
197
+ dev[:type].to_s.downcase == "nat" &&
198
+ dev[:dhcp] && dev[:hostonly_subnet]
199
+ end
200
+ end
201
+ # If we aren't able to properly detect the device, just use the default
202
+ if device.nil?
203
+ @logger.warn("failed to locate configured NAT device, using default - #{DEFAULT_NAT_DEVICE}")
204
+ config.nat_device = DEFAULT_NAT_DEVICE
205
+ return
206
+ end
207
+ @logger.debug("discovered vmware NAT device: #{device[:name]}")
208
+ config.nat_device = device[:name]
209
+ end
210
+
211
+ # Returns an array of all forwarded ports from the VMware NAT
212
+ # configuration files.
213
+ #
214
+ # @return [Array<Integer>]
215
+ def all_forwarded_ports(full_info = false)
216
+ all_fwds = vagrant_utility.get("/vmnet/#{config.nat_device}/portforward")
217
+ if all_fwds.success?
218
+ fwds = all_fwds.get(:content, :port_forwards) || []
219
+ @logger.debug("existing port forwards: #{fwds}")
220
+ full_info ? fwds : fwds.map{|fwd| fwd[:port]}
221
+ else
222
+ raise Errors::DriverAPIPortForwardListError,
223
+ message: all_fwds[:content][:message]
224
+ end
225
+ end
226
+
227
+ # Returns port forwards grouped by IP
228
+ #
229
+ # @param [String] ip guest IP address (optional)
230
+ # @return [Hash]
231
+ def forwarded_ports_by_ip(ip = nil)
232
+ all_fwds = vagrant_utility.get("/vmnet/#{config.nat_device}/portforward")
233
+ result = {}
234
+ Array(all_fwds.get(:content, :port_forwards)).each do |fwd|
235
+ g_ip = fwd[:guest][:ip]
236
+ g_port = fwd[:guest][:port]
237
+ h_port = fwd[:port]
238
+ f_proto = fwd[:protocol].downcase.to_sym
239
+ result[g_ip] ||= {:tcp => {}, :udp => {}}
240
+ result[g_ip][f_proto][g_port] = h_port
241
+ end
242
+ if ip
243
+ @logger.debug("port forwards for IP #{ip}: #{result[ip]}")
244
+ result[ip]
245
+ else
246
+ @logger.debug("port forwards by IP: #{result}")
247
+ result
248
+ end
249
+ end
250
+
251
+ # Returns host port mapping for the given guest port
252
+ #
253
+ # @param [String] ip guest IP address
254
+ # @param [String, Symbol] proto protocol type (tcp/udp)
255
+ # @param [Integer] guest_port guest port
256
+ # @return [Integer, nil] host port
257
+ def host_port_forward(ip, proto, guest_port)
258
+ proto = proto.to_sym
259
+ if ![:udp, :tcp].include?(proto)
260
+ raise ArgumentError.new("Unsupported protocol provided!")
261
+ end
262
+ fwd_ports = forwarded_ports_by_ip(ip)
263
+ if fwd_ports && fwd_ports[proto]
264
+ fwd_ports[proto][guest_port]
265
+ end
266
+ end
267
+
268
+ # This removes all the shared folders from the VM.
269
+ def clear_shared_folders
270
+ @logger.info("Clearing shared folders...")
271
+
272
+ vmx_modify do |vmx|
273
+ vmx.keys.each do |key|
274
+ # Delete all specific shared folder configs
275
+ if key =~ /^sharedfolder\d+\./i
276
+ vmx.delete(key)
277
+ end
278
+ end
279
+
280
+ # Tell VMware that we have no shared folders
281
+ vmx["sharedfolder.maxnum"] = "0"
282
+ end
283
+ end
284
+
285
+ # This clones a VMware VM from one folder to another.
286
+ #
287
+ # @param [Pathname] source_vmx The path to the VMX file of the source.
288
+ # @param [Pathname] destination The path to the directory where the
289
+ # VM will be placed.
290
+ # @param [Boolean] use linked clone
291
+ # @return [Pathname] The path to the new VMX file.
292
+ def clone(source_vmx, destination, linked=false)
293
+ # If we're prior to Vagrant 1.8, then we don't do any linked
294
+ # clones since we don't have some of the core things in to make
295
+ # this a smoother experience.
296
+ if Gem::Version.new(Vagrant::VERSION) < Gem::Version.new("1.8.0")
297
+ linked = false
298
+ end
299
+
300
+ # We can't just check if the user has a standard license since
301
+ # workstation with a standard license (player) will not allow
302
+ # linked cloning, but a standard license on fusion will allow it
303
+ if @license.to_s.downcase == "player"
304
+ @logger.warn("disabling linked clone due to insufficient access based on VMware license")
305
+ linked = false
306
+ end
307
+
308
+ if linked
309
+ @logger.info("Cloning machine using VMware linked clones.")
310
+ # The filename of the resulting VMX
311
+ destination_vmx = destination.join(source_vmx.basename)
312
+
313
+ begin
314
+ # Do a linked clone!
315
+ vmrun("clone", host_path(source_vmx), host_path(destination_vmx), "linked")
316
+ # Common cleanup
317
+ rescue Errors::VMRunError => e
318
+ # Check if this version of VMware doesn't support linked clones
319
+ # and just super it up.
320
+ stdout = e.extra_data[:stdout] || ""
321
+ if stdout.include?("parameters was invalid")
322
+ @logger.warn("VMware version doesn't support linked clones, falling back")
323
+ destination_vmx = false
324
+ else
325
+ raise
326
+ end
327
+ end
328
+ end
329
+
330
+ if !destination_vmx
331
+ @logger.info("Cloning machine using direct copy.")
332
+ # Sanity test
333
+ if !destination.directory?
334
+ raise Errors::CloneFolderNotFolder, path: destination.to_s
335
+ end
336
+
337
+ # Just copy over the files within the folder of the source
338
+ @logger.info("Cloning VM to #{destination}")
339
+ source_vmx.parent.children(true).each do |child|
340
+ @logger.debug("Copying: #{child.basename}")
341
+ begin
342
+ FileUtils.cp_r child.to_s, destination.to_s
343
+ rescue Errno::EACCES
344
+ raise Errors::DriverClonePermissionFailure,
345
+ destination: destination.to_s
346
+ end
347
+
348
+ # If we suddenly didn't become a directory, something is
349
+ # really messed up. We should see in the stack trace its
350
+ # from this case.
351
+ if !destination.directory?
352
+ raise Errors::CloneFolderNotFolder, path: destination.to_s
353
+ end
354
+ end
355
+
356
+ # Calculate the VMX file of the destination
357
+ destination_vmx = destination.join(source_vmx.basename)
358
+ end
359
+
360
+ # Perform the cleanup
361
+ clone_cleanup(destination_vmx)
362
+
363
+ # Return the final name
364
+ destination_vmx
365
+ end
366
+
367
+ def export(destination_vmx)
368
+ destination_vmx = Pathname.new(destination_vmx)
369
+ @logger.debug("Starting full clone export to: #{destination_vmx}")
370
+ vmrun("clone", host_vmx_path, host_path(destination_vmx), "full")
371
+ clone_cleanup(destination_vmx)
372
+ @logger.debug("Full clone export complete #{vmx_path} -> #{destination_vmx}")
373
+ end
374
+
375
+ # This creates a new vmnet device and returns information about that
376
+ # device.
377
+ #
378
+ # @param [Hash] config Configuration for the new vmnet device
379
+ def create_vmnet_device(config)
380
+ result = vagrant_utility.post("/vmnet",
381
+ subnet: config[:subnet_ip],
382
+ mask: config[:netmask]
383
+ )
384
+ if !result.success?
385
+ raise Errors::DriverAPIDeviceCreateError,
386
+ message: result[:content][:message]
387
+ end
388
+ result_device = result.get(:content)
389
+ new_device = {
390
+ name: result_device[:name],
391
+ nummber: result_device[:name].sub('vmnet', '').to_i,
392
+ dhcp: result_device[:dhcp],
393
+ hostonly_netmask: result_device[:mask],
394
+ hostonly_subnet: result_device[:subnet]
395
+ }
396
+ end
397
+
398
+ # This deletes the VM.
399
+ def delete
400
+ @logger.info("Deleting VM: #{@vm_dir}")
401
+ @vm_dir.rmtree
402
+ end
403
+
404
+ # This discards the suspended state of the machine.
405
+ def discard_suspended_state
406
+ Dir.glob("#{@vm_dir}/*.vmss").each do |file|
407
+ @logger.info("Deleting VM state file: #{file}")
408
+ File.delete(file)
409
+ end
410
+
411
+ vmx_modify do |vmx|
412
+ # Remove the checkpoint keys
413
+ vmx.delete("checkpoint.vmState")
414
+ vmx.delete("checkpoint.vmState.readOnly")
415
+ vmx.delete("vmotion.checkpointFBSize")
416
+ end
417
+ end
418
+
419
+ # This enables shared folders on the VM.
420
+ def enable_shared_folders
421
+ @logger.info("Enabling shared folders...")
422
+ vmrun("enableSharedFolders", host_vmx_path, :retryable => true)
423
+ end
424
+
425
+ # This configures a set of forwarded port definitions on the
426
+ # machine.
427
+ #
428
+ # @param [<Array<Hash>] definitions The ports to forward.
429
+ def forward_ports(definitions)
430
+ if Vagrant::Util::Platform.windows?
431
+ vmxpath = @vmx_path.to_s.gsub("/", 92.chr)
432
+ elsif VagrantVMwareDesktop.wsl?
433
+ vmxpath = host_vmx_path
434
+ else
435
+ vmxpath = @vmx_path.to_s
436
+ end
437
+ @logger.debug("requesting ports to be forwarded: #{definitions}")
438
+ # Starting with utility version 1.0.7 we can send all port forward
439
+ # requests at once to be processed. We include backwards compatible
440
+ # support to allow earlier utility versions to remain functional.
441
+ if utility_version > Gem::Version.new("1.0.6")
442
+ @logger.debug("forwarding ports via collection method")
443
+ definitions.group_by{|f| f[:device]}.each_pair do |device, pfwds|
444
+ fwds = pfwds.map do |fwd|
445
+ {
446
+ :port => fwd[:host_port],
447
+ :protocol => fwd.fetch(:protocol, "tcp").to_s.downcase,
448
+ :description => "vagrant: #{vmxpath}",
449
+ :guest => {
450
+ :ip => fwd[:guest_ip],
451
+ :port => fwd[:guest_port]
452
+ }
453
+ }
454
+ end
455
+ result = vagrant_utility.put("/vmnet/#{device}/portforward", fwds)
456
+ if !result.success?
457
+ raise Errors::DriverAPIPortForwardError,
458
+ message: result[:content][:message]
459
+ end
460
+ end
461
+ else
462
+ @logger.debug("forwarding ports via individual method")
463
+ definitions.each do |fwd|
464
+ result = vagrant_utility.put("/vmnet/#{fwd[:device]}/portforward",
465
+ :port => fwd[:host_port],
466
+ :protocol => fwd.fetch(:protocol, "tcp").to_s.downcase,
467
+ :description => "vagrant: #{vmxpath}",
468
+ :guest => {
469
+ :ip => fwd[:guest_ip],
470
+ :port => fwd[:guest_port]
471
+ }
472
+ )
473
+ if !result.success?
474
+ raise Errors::DriverAPIPortForwardError,
475
+ message: result.get(:content, :message)
476
+ end
477
+ end
478
+ end
479
+ end
480
+
481
+ # This is called to prune the forwarded ports from NAT configurations.
482
+ def prune_forwarded_ports
483
+ @logger.debug("requesting prune of unused port forwards")
484
+ result = vagrant_utility.delete("/portforwards")
485
+ if !result.success?
486
+ raise Errors::DriverAPIPortForwardPruneError,
487
+ message: result.get(:content, :message)
488
+ end
489
+ end
490
+
491
+ # This is used to remove all forwarded ports, including those not
492
+ # registered with the plugin
493
+ def scrub_forwarded_ports
494
+ fwds = all_forwarded_ports(true)
495
+ return if fwds.empty?
496
+ if utility_version > Gem::Version.new("1.0.7")
497
+ result = vagrant_utility.delete("/vmnet/#{config.nat_device}/portforward", fwds)
498
+ if !result.success?
499
+ raise Errors::DriverAPIPortForwardPruneError,
500
+ message: result[:content][:message]
501
+ end
502
+ else
503
+ fwds.each do |fwd|
504
+ result = vagrant_utility.delete("/vmnet/#{config.nat_device}/portforward", fwd)
505
+ if !result.success?
506
+ raise Errors::DriverAPIPortForwardPruneError,
507
+ message: result.get(:content, :message)
508
+ end
509
+ end
510
+ end
511
+ end
512
+
513
+ # This returns an IP that can be used to access the machine.
514
+ #
515
+ # @return [String]
516
+ def read_ip(enable_vmrun_ip_lookup=true)
517
+ @logger.info("Reading an accessible IP for machine...")
518
+
519
+ # NOTE: Read from DHCP leases first so we can attempt to fetch the address
520
+ # for the vmnet8 device first. If multiple networks are defined on the guest
521
+ # it will return the address of the last device, which will fail when doing
522
+ # port forward lookups
523
+
524
+ # Read the VMX data so that we can look up the network interfaces
525
+ # and find a properly accessible IP.
526
+ vmx = read_vmx_data
527
+
528
+ 0.upto(8).each do |slot|
529
+ # We don't care if this adapter isn't present
530
+ next if vmx["ethernet#{slot}.present"] != "TRUE"
531
+
532
+ # Get the type of this adapter. Bail if there is no type.
533
+ type = vmx["ethernet#{slot}.connectiontype"]
534
+ next if !type
535
+
536
+ if type != "nat" && type != "custom"
537
+ @logger.warn("Non-NAT interface on slot #{slot}. We can only read IPs of NATs for now.")
538
+ next
539
+ end
540
+
541
+ # Get the MAC address
542
+ @logger.debug("Trying to get MAC address for ethernet#{slot}")
543
+ mac = vmx["ethernet#{slot}.address"]
544
+ if !mac || mac == ""
545
+ @logger.debug("No explicitly set MAC, looking or auto-generated one...")
546
+ mac = vmx["ethernet#{slot}.generatedaddress"]
547
+
548
+ if !mac
549
+ @logger.warn("Couldn't find MAC, can't determine IP.")
550
+ next
551
+ end
552
+ end
553
+
554
+ @logger.debug(" -- MAC: #{mac}")
555
+
556
+ # Look up the IP!
557
+ dhcp_ip = read_dhcp_lease(config.nat_device, mac)
558
+ return dhcp_ip if dhcp_ip
559
+ end
560
+
561
+ if enable_vmrun_ip_lookup
562
+ # Try to read the IP using vmrun getGuestIPAddress. This
563
+ # won't work if the guest doesn't have guest tools installed or
564
+ # is using an old version of VMware.
565
+ begin
566
+ @logger.info("Trying vmrun getGuestIPAddress...")
567
+ result = vmrun("getGuestIPAddress", host_vmx_path)
568
+ result = result.stdout.chomp
569
+
570
+ # If returned address ends with a ".1" do not accept address
571
+ # and allow lookup via VMX.
572
+ # see: https://github.com/vmware/open-vm-tools/issues/93
573
+ if result.end_with?(".1")
574
+ @logger.warn("vmrun getGuestIPAddress returned: #{result}. Result resembles address retrieval from wrong " \
575
+ "interface. Discarding value and proceeding with VMX based lookup.")
576
+ result = nil
577
+ else
578
+ # Try to parse the IP Address. This will raise an exception
579
+ # if it fails, which will halt our attempt to use it.
580
+ IPAddr.new(result)
581
+ @logger.info("vmrun getGuestIPAddress success: #{result}")
582
+ return result
583
+ end
584
+ rescue Errors::VMRunError
585
+ @logger.info("vmrun getGuestIPAddress failed: VMRunError")
586
+ # Ignore, try the MAC address way.
587
+ rescue IPAddr::InvalidAddressError
588
+ @logger.info("vmrun getGuestIPAddress failed: InvalidAddressError for #{result.inspect}")
589
+ # Ignore, try the MAC address way.
590
+ end
591
+ else
592
+ @logger.info("Skipping vmrun getGuestIPAddress as requested by config.")
593
+ end
594
+ nil
595
+ end
596
+
597
+ # This reads all the network adapters that are on the machine and
598
+ # enabled.
599
+ #
600
+ # @return [Array<Hash>]
601
+ def read_network_adapters
602
+ vmx = read_vmx_data
603
+
604
+ adapters = []
605
+ vmx.keys.each do |key|
606
+ # We only care about finding present ethernet adapters
607
+ match = /^ethernet(\d+)\.present$/i.match(key)
608
+ next if !match
609
+ next if vmx[key] != "TRUE"
610
+
611
+ # We found one, so store it away
612
+ slot = match[1]
613
+ adapter = {
614
+ :slot => slot,
615
+ :type => vmx["ethernet#{slot}.connectiontype"]
616
+ }
617
+
618
+ adapters << adapter
619
+ end
620
+
621
+ return adapters
622
+ end
623
+
624
+ # This returns an array of paths to all the running VMX files.
625
+ #
626
+ # @return [Array<String>]
627
+ def read_running_vms
628
+ vmrun("list").stdout.split("\n").find_all do |path|
629
+ path !~ /running VMs:/
630
+ end
631
+ end
632
+
633
+ # This reads the state of this VM and returns a symbol representing
634
+ # it.
635
+ #
636
+ # @return [Symbol]
637
+ def read_state
638
+ # The VM is not created if we don't have a directory
639
+ return :not_created if !@vm_dir
640
+
641
+ # Check to see if the VM is running, which requires shelling out
642
+ vmx_path = nil
643
+ begin
644
+ vmx_path = @vmx_path.realpath.to_s
645
+ rescue Errno::ENOENT
646
+ @logger.info("VMX path doesn't exist, not created: #{@vmx_path}")
647
+ return :not_created
648
+ end
649
+
650
+ vmx_path = host_vmx_path
651
+
652
+ # Downcase the lines in certain case-insensitive cases
653
+ downcase = VagrantVMwareDesktop.case_insensitive_fs?(vmx_path)
654
+
655
+ # OS X is case insensitive so just lowercase everything
656
+ vmx_path = vmx_path.downcase if downcase
657
+
658
+ if Vagrant::Util::Platform.windows?
659
+ # Replace any slashes to be unix-style.
660
+ vmx_path.gsub!(92.chr, "/")
661
+ end
662
+
663
+ vmrun("list").stdout.split("\n").each do |line|
664
+ if Vagrant::Util::Platform.windows?
665
+ # On Windows, we normalize the paths to unix-style.
666
+ line.gsub!("\\", "/")
667
+
668
+ # We also change any drive letter to be lowercased
669
+ line[0] = line[0].downcase
670
+ end
671
+
672
+ # Case insensitive filesystems, so we downcase.
673
+ line = line.downcase if downcase
674
+ return :running if line == vmx_path
675
+ end
676
+
677
+ # Check to see if the VM is suspended based on whether a file
678
+ # exists in the VM directory
679
+ return :suspended if Dir.glob("#{@vm_dir}/*.vmss").length >= 1
680
+
681
+ # I guess it is not running.
682
+ return :not_running
683
+ end
684
+
685
+ # This reads all the network adapters and returns
686
+ # their assigned MAC addresses
687
+ def read_mac_addresses
688
+ vmx = read_vmx_data
689
+ macs = {}
690
+ vmx.keys.each do |key|
691
+ # We only care about finding present ethernet adapters
692
+ match = /^ethernet(\d+)\.present$/i.match(key)
693
+ next if !match
694
+ next if vmx[key] != "TRUE"
695
+
696
+ slot = match[1].to_i
697
+
698
+ # Vagrant's calling code assumes the MAC is all caps no colons
699
+ mac = vmx["ethernet#{slot}.generatedaddress"].to_s
700
+ mac = mac.upcase.gsub(/:/, "")
701
+
702
+ # Vagrant's calling code assumes these will be 1-indexed
703
+ slot += 1
704
+
705
+ macs[slot] = mac
706
+ end
707
+ macs
708
+ end
709
+
710
+ # Reserve an address on the DHCP sever for the given
711
+ # MAC address. Defaults to the NAT device at vmnet8
712
+ #
713
+ # @param [String] ip IP address to reserve
714
+ # @param [String] mac MAC address to associate
715
+ # @return [true]
716
+ def reserve_dhcp_address(ip, mac, vmnet="vmnet8")
717
+ result = vagrant_utility.put("/vmnet/#{vmnet}/dhcpreserve/#{mac}/#{ip}")
718
+ if !result.success?
719
+ raise Errors::DriverAPIAddressReservationError,
720
+ device: vmnet,
721
+ address: ip,
722
+ mac: mac,
723
+ message: result[:content][:message]
724
+ end
725
+ true
726
+ end
727
+
728
+ # This reads the vmnet devices and various information about them.
729
+ #
730
+ # @return [Array<Hash>]
731
+ def read_vmnet_devices
732
+ result = vagrant_utility.get("/vmnet")
733
+ if !result.success?
734
+ raise Errors::DriverAPIDeviceListError,
735
+ message: result[:content][:message]
736
+ end
737
+ Array(result.get(:content, :vmnets)).map do |vmnet|
738
+ {
739
+ name: vmnet[:name],
740
+ type: vmnet[:type],
741
+ number: vmnet[:name].sub('vmnet', '').to_i,
742
+ dhcp: vmnet[:dhcp],
743
+ hostonly_netmask: vmnet[:mask],
744
+ hostonly_subnet: vmnet[:subnet],
745
+ virtual_adapter: "yes"
746
+ }
747
+ end
748
+ end
749
+
750
+ # This modifies the metadata of the virtual machine to add the
751
+ # given network adapters.
752
+ #
753
+ # @param [Array] adapters
754
+ def setup_adapters(adapters, allowlist_verified=false)
755
+ vmx_modify do |vmx|
756
+ # Remove all previous adapters
757
+ vmx.keys.each do |key|
758
+ check_key = key.downcase
759
+ ethernet_key = key.match(/^ethernet\d\.(?<setting_name>.+)$/)
760
+ if !ethernet_key.nil? && !VMX_ETHERNET_ALLOWLIST.include?(ethernet_key["setting_name"])
761
+ @logger.warn("setup_adapter: Removing VMX key: #{ethernet_key}")
762
+ vmx.delete(key)
763
+ elsif ethernet_key
764
+ if !allowlist_verified
765
+ display_ethernet_allowlist_warning(key, vmx[key])
766
+ elsif allowlist_verified == :disable_warning
767
+ @logger.warn("VMX allowlisting warning message has been disabled via configuration. `#{key}`")
768
+ else
769
+ @logger.info("VMX allowlisting has been verified via configuration. `#{key}`")
770
+ end
771
+ if !VMX_ETHERNET_ALLOWLIST_ENFORCE && allowlist_verified != true
772
+ @logger.warn("setup_adapter: Removing allowlisted VMX key: #{ethernet_key}")
773
+ vmx.delete(key)
774
+ end
775
+ end
776
+ end
777
+
778
+ # Go through the adapters to enable and set them up properly
779
+ adapters.each do |adapter|
780
+ key = "ethernet#{adapter[:slot]}"
781
+
782
+ vmx["#{key}.present"] = "TRUE"
783
+ vmx["#{key}.connectiontype"] = adapter[:type].to_s
784
+ vmx["#{key}.virtualdev"] = "e1000"
785
+
786
+ if adapter[:mac_address]
787
+ vmx["#{key}.addresstype"] = "static"
788
+ vmx["#{key}.address"] = adapter[:mac_address]
789
+ else
790
+ # Dynamically generated MAC address
791
+ vmx["#{key}.addresstype"] = "generated"
792
+ end
793
+
794
+ if adapter[:vnet]
795
+ # Some adapters define custom vmnet devices to connect to
796
+ vmx["#{key}.vnet"] = adapter[:vnet]
797
+ vmx["#{key}.connectiontype"] = "custom" if adapter[:type] == :nat
798
+ end
799
+ end
800
+ end
801
+ end
802
+
803
+ # This creates a shared folder within the VM.
804
+ def share_folder(id, hostpath)
805
+ @logger.info("Adding a shared folder '#{id}': #{hostpath}")
806
+
807
+ vmrun("addSharedFolder", host_vmx_path, id, host_path(hostpath))
808
+ vmrun("setSharedFolderState", host_vmx_path, id, host_path(hostpath), "writable")
809
+ end
810
+
811
+ # All the methods below deal with snapshots: taking them, restoring
812
+ # them, deleting them, etc.
813
+ def snapshot_take(name)
814
+ vmrun("snapshot", host_vmx_path, name)
815
+ end
816
+
817
+ def snapshot_delete(name)
818
+ vmrun("deleteSnapshot", host_vmx_path, name)
819
+ end
820
+
821
+ def snapshot_revert(name)
822
+ vmrun("revertToSnapshot", host_vmx_path, name)
823
+ end
824
+
825
+ def snapshot_list
826
+ snapshots = []
827
+ vmrun("listSnapshots", host_vmx_path).stdout.split("\n").each do |line|
828
+ if !line.include?("Total snapshot")
829
+ snapshots << line
830
+ end
831
+ end
832
+
833
+ snapshots
834
+ end
835
+
836
+ def snapshot_tree
837
+ snapshots = []
838
+ vmrun("listSnapshots", host_vmx_path, "showTree").stdout.split("\n").each do |line|
839
+ if !line.include?("Total snapshot")
840
+ # if the line starts with white space then it is a child
841
+ # of the previous line
842
+ if line.start_with?(/\s/)
843
+ name = "#{snapshots.last}/#{line.gsub(/\s/, "")}"
844
+ snapshots << name
845
+ else
846
+ snapshots << line
847
+ end
848
+ end
849
+ end
850
+
851
+ snapshots
852
+ end
853
+
854
+ # This will start the VMware machine.
855
+ def start(gui=false)
856
+ gui_arg = gui ? "gui" : "nogui"
857
+ vmrun("start", host_vmx_path, gui_arg, retryable: true, timeout: 45)
858
+ rescue Vagrant::Util::Subprocess::TimeoutExceeded
859
+ # Sometimes vmrun just hangs. We give it a generous timeout
860
+ # of 45 seconds, and then throw this.
861
+ raise Errors::StartTimeout
862
+ end
863
+
864
+ # This will stop the VMware machine.
865
+ def stop(stop_mode="soft")
866
+ begin
867
+ vmrun("stop", host_vmx_path, stop_mode, :retryable => true, timeout: 15)
868
+ rescue Errors::VMRunError, Vagrant::Util::Subprocess::TimeoutExceeded
869
+ begin
870
+ vmrun("stop", host_vmx_path, "hard", :retryable => true)
871
+ rescue Errors::VMRunError
872
+ # There is a chance that the "soft" stop may have timed out, yet
873
+ # still succeeded which would result in the "hard" stop failing
874
+ # due to the guest not running. Because of this we do a quick
875
+ # state check and only allow the error if the guest is still
876
+ # running
877
+ raise if read_state == :running
878
+ end
879
+ end
880
+ end
881
+
882
+ # This will suspend the VMware machine.
883
+ def suspend
884
+ vmrun("suspend", host_vmx_path, :retryable => true)
885
+ end
886
+
887
+ # This is called to do any message suppression if we need to.
888
+ def suppress_messages
889
+ if PRODUCT_NAME == "fusion"
890
+ contents = <<-DATA
891
+ <?xml version="1.0" encoding="UTF-8"?>
892
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
893
+ <plist version="1.0">
894
+ <dict>
895
+ <key>disallowUpgrade</key>
896
+ <true/>
897
+ </dict>
898
+ </plist>
899
+ DATA
900
+
901
+ if @vmx_path
902
+ filename = @vmx_path.basename.to_s.gsub(".vmx", ".plist")
903
+ @vmx_path.dirname.join(filename).open("w+") do |f|
904
+ f.puts(contents.strip)
905
+ end
906
+ end
907
+ end
908
+ end
909
+
910
+ # This method is called to verify that the installation looks good,
911
+ # and raises an exception if it doesn't.
912
+ def verify!
913
+ utility_requirement = Gem::Requirement.new(VAGRANT_UTILITY_VERSION_REQUIREMENT.split(","))
914
+ if !utility_requirement.satisfied_by?(utility_version)
915
+ raise Errors::UtilityUpgradeRequired,
916
+ utility_version: utility_version.to_s,
917
+ utility_requirement: utility_requirement.to_s
918
+ end
919
+ end
920
+
921
+ # This method is called to verify whether the vmnet devices are healthy.
922
+ # If not, an exception should be raised.
923
+ def verify_vmnet!
924
+ result = vagrant_utility.post("/vmnet/verify")
925
+ if !result.success?
926
+ # Check if the result was a 404. This indicates a utility service
927
+ # running before the vmnet verification endpoint was introduced
928
+ # and is not really an error
929
+ if result[:code] == 404
930
+ return
931
+ end
932
+ raise Errors::VMNetDevicesWontStart
933
+ end
934
+ end
935
+
936
+ # This method returns whether or not the VMX process is still alive.
937
+ def vmx_alive?
938
+ # If we haven't cleanly shut down, then it is alive.
939
+ read_vmx_data["cleanshutdown"] != "TRUE"
940
+
941
+ # Note at some point it would be nice to actually track the VMX
942
+ # process itself. But at this point this isn't very feasible.
943
+ end
944
+
945
+ # This allows modifications of the VMX file by handling it as
946
+ # a simple hash. By adding/modifying/deleting keys in the hash,
947
+ # the VMX is modified.
948
+ def vmx_modify
949
+ # Read the data once
950
+ vmx_data = read_vmx_data
951
+
952
+ # Yield it so that it can be modified
953
+ yield vmx_data
954
+
955
+ # And write it back!
956
+ @logger.info("Modifying VMX data...")
957
+ @vmx_path.open("w") do |f|
958
+ vmx_data.keys.sort.each do |key|
959
+ value = vmx_data[key]
960
+
961
+ # We skip nil values and remove them from the output
962
+ if value.nil?
963
+ @logger.debug(" - DELETE #{key}")
964
+ next
965
+ end
966
+
967
+ # Write the value
968
+ @logger.debug(" - SET #{key} = \"#{value}\"")
969
+ f.puts("#{key} = \"#{value}\"")
970
+ end
971
+
972
+ f.fsync
973
+ end
974
+ end
975
+
976
+ # Gets the version of the vagrant-vmware-utility currently
977
+ # in use
978
+ #
979
+ # @return [String, nil]
980
+ def vmware_utility_version
981
+ @logger.debug("Getting version from vagrant-vmware-utility")
982
+ result = vagrant_utility.get("/version")
983
+ if result.success?
984
+ result.get(:content, :version)
985
+ end
986
+ end
987
+
988
+ # Gets the currently attached disks
989
+ #
990
+ # @param [List<String>] List of disk types to search for
991
+ # @return [Hash] Hash of disks
992
+ def get_disks(types)
993
+ vmx = read_vmx_data
994
+ disks = {}
995
+ vmx.each do |k, v|
996
+ if k.match(/(#{types.map{|t| Regexp.escape(t)}.join("|")})\d+:\d+/)
997
+ key, attribute = k.split(".", 2)
998
+ if disks[key].nil?
999
+ disks[key] = {attribute => v}
1000
+ else
1001
+ disks[key].merge!({attribute => v})
1002
+ end
1003
+ end
1004
+ end
1005
+ disks
1006
+ end
1007
+
1008
+ # Create a vmdk disk
1009
+ #
1010
+ # @params [String] Disk filename
1011
+ # @params [Integer] Size of disk in bytes
1012
+ # @params [Integer] Disk type (given by vmware-vdiskmanager)
1013
+ # @params [String] Disk adapter
1014
+ def create_disk(disk_filename, disk_size, disk_type, disk_adapter)
1015
+ disk_path = File.join(File.dirname(@vmx_path), disk_filename)
1016
+ disk_size = "#{Vagrant::Util::Numeric.bytes_to_megabytes(disk_size).to_s}MB"
1017
+ vdiskmanager("-c", "-s", disk_size, "-t", disk_type.to_s, "-a", disk_adapter, disk_path)
1018
+ disk_path
1019
+ end
1020
+
1021
+ # Make a disk larger
1022
+ #
1023
+ # @params [String] Path to disk
1024
+ # @params [Integer] Size of disk in bytes
1025
+ def grow_disk(disk_path, disk_size)
1026
+ disk_size = "#{Vagrant::Util::Numeric.bytes_to_megabytes(disk_size).to_s}MB"
1027
+ vdiskmanager("-x", disk_size, disk_path)
1028
+ end
1029
+
1030
+ # Adds a disk to the vm's vmx file
1031
+ #
1032
+ # @params [String] filename to insert
1033
+ # @params [String] slot to add the disk to
1034
+ # @params [Map] (deafults to {}) map of extra options to specify in the vmx file of the form {opt => value}
1035
+ def add_disk_to_vmx(filename, slot, extra_opts={})
1036
+ root_key, _ = slot.split(":", 2)
1037
+ vmx_modify do |vmx|
1038
+ vmx["#{root_key}.present"] = "TRUE"
1039
+ vmx["#{slot}.filename"] = filename
1040
+ vmx["#{slot}.present"] = "TRUE"
1041
+ extra_opts.each do |key, value|
1042
+ vmx["#{slot}.#{key}"] = value
1043
+ end
1044
+ end
1045
+ end
1046
+
1047
+ # Removes a disk to the vm's vmx file
1048
+ #
1049
+ # @params [String] filename to remove
1050
+ # @params [List] (defaults to []) list of extra options remove from the vmx file
1051
+ def remove_disk_from_vmx(filename, extra_opts=[])
1052
+ vmx = read_vmx_data
1053
+ vmx_disk_entry = vmx.select { |_, v| v == filename }
1054
+ if !vmx_disk_entry.empty?
1055
+ slot = vmx_disk_entry.keys.first.split(".").first
1056
+ vmx_modify do |vmx|
1057
+ vmx.delete("#{slot}.filename")
1058
+ vmx.delete("#{slot}.present")
1059
+ extra_opts.each do |opt|
1060
+ vmx.delete("#{slot}.#{opt}")
1061
+ end
1062
+ end
1063
+ end
1064
+ end
1065
+
1066
+ # @params [String] disk base filename eg. disk.vmx
1067
+ def remove_disk(disk_filename)
1068
+ disk_path = File.join(File.dirname(@vmx_path), disk_filename)
1069
+ vdiskmanager("-U", disk_path) if File.exist?(disk_path)
1070
+ remove_disk_from_vmx(disk_filename)
1071
+ end
1072
+
1073
+ # Gets the size of a .vmdk disk
1074
+ # Spec for vmdk: https://github.com/libyal/libvmdk/blob/master/documentation/VMWare%20Virtual%20Disk%20Format%20(VMDK).asciidoc
1075
+ #
1076
+ # @params [String] full path to .vmdk file
1077
+ # @return [int, nil] size of disk in bytes, nil if path does not exist
1078
+ def get_disk_size(disk_path)
1079
+ return nil if !File.exist?(disk_path)
1080
+
1081
+ disk_size = 0
1082
+ at_extent_description = false
1083
+ File.foreach(disk_path) do |line|
1084
+ next if !line.valid_encoding?
1085
+ # Look for `# Extent description` header
1086
+ if line.include?("# Extent description")
1087
+ at_extent_description = true
1088
+ next
1089
+ end
1090
+ if at_extent_description
1091
+ # Exit once the "Extent description" section is done
1092
+ # signified by the new line
1093
+ if line == "\n"
1094
+ break
1095
+ else
1096
+ # Get the 2nd entry on each line - number of sectors
1097
+ sectors = line.split(" ")[1].to_i
1098
+ # Convert sectors to bytes
1099
+ disk_size += (sectors * SECTOR_TO_BYTES)
1100
+ end
1101
+ end
1102
+ end
1103
+ disk_size
1104
+ end
1105
+
1106
+ protected
1107
+
1108
+ # This reads the latest DHCP lease for a MAC address on the
1109
+ # given vmnet device.
1110
+ #
1111
+ # @param [String] vmnet The name of the vmnet device like "vmnet8"
1112
+ # @param [String] mac The MAC address
1113
+ # @return [String] The IP, or nil.
1114
+ def read_dhcp_lease(vmnet, mac)
1115
+ @logger.info("Reading DHCP lease for '#{mac}' on '#{vmnet}'")
1116
+ result = vagrant_utility.get("/vmnet/#{vmnet}/dhcplease/#{mac}")
1117
+ if result.success?
1118
+ result.get(:content, :ip)
1119
+ end
1120
+ end
1121
+
1122
+ # This returns the VMX data just as a hash.
1123
+ #
1124
+ # @return [Hash]
1125
+ def read_vmx_data
1126
+ @logger.info("Reading VMX data...")
1127
+
1128
+ # Read the VMX contents into memory
1129
+ contents = @vmx_path.read
1130
+
1131
+ # Convert newlines to unix-style
1132
+ contents.gsub!(/\r\n?/, "\n")
1133
+
1134
+ # Parse it out into a hash
1135
+ vmx_data = {}
1136
+ contents.split("\n").each do |line|
1137
+ # If it is a comment then ignore it
1138
+ next if line =~ /^#/
1139
+
1140
+ # Parse out the key/value
1141
+ match = /^(.+?)\s*=\s*(.*?)\s*$/.match(line)
1142
+ if !match
1143
+ @logger.warn("Weird value in VMX: '#{line}'")
1144
+ next
1145
+ end
1146
+
1147
+ # Set the data
1148
+ key = match[1].strip.downcase
1149
+ value = match[2]
1150
+ value = value[1, value.length-2] if value =~ /^".*?"$/
1151
+ @logger.debug(" - #{key} = #{value}")
1152
+ vmx_data[key] = value
1153
+ end
1154
+
1155
+ # Return it
1156
+ vmx_data
1157
+ end
1158
+
1159
+ # Executes a given executable with retries
1160
+ def vmexec(executable, *command)
1161
+ # Get the options hash if it exists
1162
+ opts = {}
1163
+ opts = command.pop if command.last.is_a?(Hash)
1164
+
1165
+ tries = 0
1166
+ tries = 3 if opts[:retryable]
1167
+
1168
+ interrupted = false
1169
+ sigint_callback = lambda do
1170
+ interrupted = true
1171
+ end
1172
+
1173
+ command_opts = { :notify => [:stdout, :stderr] }
1174
+ command_opts[:timeout] = opts[:timeout] if opts[:timeout]
1175
+
1176
+ command = command.dup
1177
+ command << command_opts
1178
+
1179
+
1180
+ Vagrant::Util::Busy.busy(sigint_callback) do
1181
+ retryable(:on => Errors::VMExecError, :tries => tries, :sleep => 2) do
1182
+ r_path = executable.to_s
1183
+ if VagrantVMwareDesktop.wsl?
1184
+ r_path = VagrantVMwareDesktop.windows_to_wsl_path(r_path)
1185
+ end
1186
+ result = Vagrant::Util::Subprocess.execute(r_path, *command)
1187
+ if result.exit_code != 0
1188
+ raise Errors::VMExecError,
1189
+ :executable => executable.to_s,
1190
+ :command => command.inspect,
1191
+ :stdout => result.stdout.chomp,
1192
+ :stderr => result.stderr.chomp
1193
+ end
1194
+
1195
+ # Make sure we only have unix-style line endings
1196
+ result.stderr.gsub!(/\r\n?/, "\n")
1197
+ result.stdout.gsub!(/\r\n?/, "\n")
1198
+
1199
+ return result
1200
+ end
1201
+ end
1202
+ end
1203
+
1204
+ # This executes the "vmrun" command with the given arguments.
1205
+ def vmrun(*command)
1206
+ begin
1207
+ vmexec(@vmrun_path, *command)
1208
+ rescue Errors::VMExecError => e
1209
+ raise Errors::VMRunError,
1210
+ :command => e.extra_data[:command],
1211
+ :stdout => e.extra_data[:stdout],
1212
+ :stderr => e.extra_data[:stderr]
1213
+ end
1214
+ end
1215
+
1216
+ # This executes the "vmware-vdiskmanager" command with the given arguments.
1217
+ def vdiskmanager(*command)
1218
+ vmexec(@vmware_vdiskmanager_path, *command)
1219
+ end
1220
+
1221
+ # Set VMware information
1222
+ def set_vmware_info
1223
+ result = vagrant_utility.get("/vmware/info")
1224
+ if !result.success?
1225
+ raise Errors::DriverAPIVMwareVersionDetectionError,
1226
+ message: result[:content][:message]
1227
+ end
1228
+ if @license.to_s.empty?
1229
+ @license = result.get(:content, :license).to_s.downcase
1230
+ end
1231
+ if (@license.include?("workstation") || @license.include?("pro")) && !@license.include?("vl")
1232
+ @pro_license = true
1233
+ else
1234
+ @pro_license = false
1235
+ end
1236
+ @version = result.get(:content, :version)
1237
+ @product_name = result.get(:content, :product)
1238
+ result = vagrant_utility.get("/vmware/paths")
1239
+ if !result.success?
1240
+ raise Errors::DriverAPIVmwarePathsDetectionError,
1241
+ message: result[:content][:message]
1242
+ end
1243
+ @vmrun_path = result.get(:content, :vmrun)
1244
+ @vmware_vmx_path = result.get(:content, :vmx)
1245
+ @vmware_vdiskmanager_path = result.get(:content, :vdiskmanager)
1246
+ result = vagrant_utility.get("/version")
1247
+ @utility_version = Gem::Version.new(result[:content][:version])
1248
+ @logger.debug("vagrant vmware utility version detected: #{@utility_version}")
1249
+ @logger.debug("vmware product detected: #{@product_name}")
1250
+ @logger.debug("vmware license in use: #{@license}")
1251
+ if !@pro_license
1252
+ @logger.warn("standard VMware license currently in use which may result in degraded networking experience")
1253
+ end
1254
+ if VagrantVMwareDesktop.wsl?
1255
+ @logger.debug("Detected WSL environment, converting paths...")
1256
+ rpath = @vmrun_path
1257
+ xpath = @vmware_vmx_path
1258
+ dpath = @vmware_vdiskmanager_path
1259
+ @vmrun_path = VagrantVMwareDesktop.windows_to_wsl_path(@vmrun_path)
1260
+ @vmware_vmx_path = VagrantVMwareDesktop.windows_to_wsl_path(@vmware_vmx_path)
1261
+ @vmware_vdiskmanager_path = VagrantVMwareDesktop.windows_to_wsl_path(@vmware_vdiskmanager_path)
1262
+ @logger.debug("Converted `#{rpath}` -> #{@vmrun_path}")
1263
+ @logger.debug("Converted `#{xpath}` -> #{@vmware_vmx_path}")
1264
+ @logger.debug("Converted `#{dpath}` -> #{@vmware_vdiskmanager_path}")
1265
+ end
1266
+ end
1267
+
1268
+
1269
+ # This performs common cleanup tasks on a cloned machine.
1270
+ def clone_cleanup(destination_vmx)
1271
+ destination = destination_vmx.parent
1272
+
1273
+ # Delete any lock files
1274
+ destination.children(true).each do |child|
1275
+ if child.extname == ".lck"
1276
+ @logger.debug("Deleting lock file: #{child}")
1277
+ child.rmtree
1278
+ end
1279
+ end
1280
+
1281
+ # Next we make some VMX modifications
1282
+ self.class.new(destination_vmx, config).vmx_modify do |vmx|
1283
+ # This forces VMware to generate a new UUID which avoids the
1284
+ # "It appears you have moved this VM" error.
1285
+ vmx["uuid.action"] = "create"
1286
+
1287
+ # Ask VMware to auto-answer any dialogs since we'll be running
1288
+ # headless, in general.
1289
+ vmx["msg.autoanswer"] = "true"
1290
+ end
1291
+
1292
+ # Return the destination VMX file
1293
+ destination_vmx
1294
+ end
1295
+
1296
+ # Display warning message about allowlisted VMX ethernet settings
1297
+ def display_ethernet_allowlist_warning(vmx_key, vmx_val)
1298
+ if VMX_ETHERNET_ALLOWLIST_ENFORCE != :quiet
1299
+ if create_notification_file(vmx_key)
1300
+ if VMX_ETHERNET_ALLOWLIST_ENFORCE
1301
+ warning_msg = VMX_ETHERNET_ALLOWLIST_DETECTION_WARNING
1302
+ setting_name = vmx_key.slice(vmx_key.rindex(".") + 1, vmx_key.size).downcase
1303
+ if VMX_ETHERNET_ALLOWLIST_POSTFIX[setting_name]
1304
+ warning_msg += "\n"
1305
+ warning_msg += VMX_ETHERNET_ALLOWLIST_POSTFIX[setting_name]
1306
+ end
1307
+ else
1308
+ warning_msg = VMX_ETHERNET_ALLOWLIST_DETECTION_PREWARNING
1309
+ end
1310
+ warning_msg = warning_msg.gsub("%VMX_KEY%", vmx_key).gsub("%VMX_VALUE%", vmx_val)
1311
+ warning_msg.split("\n").each do |line|
1312
+ $stderr.puts "WARNING: #{line}"
1313
+ end
1314
+ end
1315
+ end
1316
+ end
1317
+
1318
+ # Creates a file within the vm directory to flag if the warning has
1319
+ # already been provided to the user. This helps prevent warnings from being
1320
+ # re-displayed after the initial `up`.
1321
+ def create_notification_file(key)
1322
+ path = File.join(vm_dir, "vagrant-vmx-warn-#{key}.flg")
1323
+ if !File.exist?(path)
1324
+ FileUtils.touch(path)
1325
+ true
1326
+ else
1327
+ false
1328
+ end
1329
+ end
1330
+
1331
+ # This converts the VMX file path to the true
1332
+ # host path. At this point it only applies
1333
+ # modifications if running within the WSL on
1334
+ # Windows. For all other cases, it just forces
1335
+ # a String type.
1336
+ #
1337
+ # @return [String] path to VMX file
1338
+ def host_vmx_path
1339
+ host_path(@vmx_path)
1340
+ end
1341
+
1342
+ # This converts the file path to the true
1343
+ # host path. At this point it only applies
1344
+ # modifications if running within the WSL on
1345
+ # Windows. For all other cases, it just forces
1346
+ # a String type.
1347
+ #
1348
+ # @param [String, Pathname] path
1349
+ # @return [String] path to VMX file
1350
+ def host_path(path)
1351
+ VagrantVMwareDesktop.wsl_to_windows_path(path)
1352
+ end
1353
+ end
1354
+ end
1355
+ end
1356
+ end