vagrant-vmware-desktop 0.0.1 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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