vagrant-parallels 1.3.13 → 1.4.0.rc1

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.
@@ -18,7 +18,8 @@ module VagrantPlugins
18
18
  include Vagrant::Util::Retryable
19
19
  include Vagrant::Util::NetworkIP
20
20
 
21
- def initialize
21
+ def initialize(uuid)
22
+ @uuid = uuid
22
23
  @logger = Log4r::Logger.new('vagrant_parallels::driver::base')
23
24
 
24
25
  # This flag is used to keep track of interrupted state (SIGINT)
@@ -38,38 +39,125 @@ module VagrantPlugins
38
39
  @logger.info("prlsrvctl path: #{@prlsrvctl_path}")
39
40
  end
40
41
 
42
+ # Removes all port forwarding rules for the virtual machine.
43
+ def clear_forwarded_ports
44
+ raise NotImplementedError
45
+ end
46
+
41
47
  # Clears the shared folders that have been set on the virtual machine.
42
48
  def clear_shared_folders
49
+ share_ids = read_shared_folders.keys
50
+ share_ids.each do |id|
51
+ execute_prlctl('set', @uuid, '--shf-host-del', id)
52
+ end
53
+ end
54
+
55
+ # Makes a clone of the virtual machine.
56
+ #
57
+ # @param [String] src_name Name or UUID of the source VM or template.
58
+ # @param [String] dst_name Name of the destination VM.
59
+ # @param [<String => String>] options Options to clone virtual machine.
60
+ # @return [String] UUID of the new VM.
61
+ def clone_vm(src_name, dst_name, options={})
62
+ args = ['clone', src_name, '--name', dst_name]
63
+ args << '--template' if options[:template]
64
+ args.concat(['--dst', options[:dst]]) if options[:dst]
65
+
66
+ # Linked clone options
67
+ args << '--linked' if options[:linked]
68
+ args.concat(['--id', options[:snapshot_id]]) if options[:snapshot_id]
69
+
70
+ execute_prlctl(*args) do |_, data|
71
+ lines = data.split('\r')
72
+ # The progress of the clone will be in the last line. Do a greedy
73
+ # regular expression to find what we're looking for.
74
+ if lines.last =~ /Copying hard disk.+?(\d{,3}) ?%/
75
+ yield $1.to_i if block_given?
76
+ end
77
+ end
78
+ read_vms[dst_name]
43
79
  end
44
80
 
45
81
  # Compacts all disk drives of virtual machine
46
- def compact
82
+ def compact(uuid)
83
+ hw_info = read_settings(uuid).fetch('Hardware', {})
84
+ used_drives = hw_info.select do |name, _|
85
+ name.start_with? 'hdd'
86
+ end
87
+ used_drives.each_value do |drive_params|
88
+ execute(@prldisktool_path, 'compact', '--hdd', drive_params['image']) do |_, data|
89
+ lines = data.split('\r')
90
+ # The progress of the compact will be in the last line. Do a greedy
91
+ # regular expression to find what we're looking for.
92
+ if lines.last =~ /.+?(\d{,3}) ?%/
93
+ yield $1.to_i if block_given?
94
+ end
95
+ end
96
+ end
47
97
  end
48
98
 
49
99
  # Creates a host only network with the given options.
50
100
  #
51
- # @param [Hash] options Options to create the host only network.
52
- # @return [Hash] The details of the host only network, including
53
- # keys `:name`, `:ip`, `:netmask` and `:dhcp`
101
+ # @param [<Symbol => String>] options Hostonly network options.
102
+ # @return [<Symbol => String>] The details of the host only network,
103
+ # including keys `:name`, `:ip`, `:netmask` and `:dhcp`
104
+ # @param [<Symbol => String>] options
54
105
  def create_host_only_network(options)
106
+ # Create the interface
107
+ execute_prlsrvctl('net', 'add', options[:network_id], '--type', 'host-only')
108
+
109
+ # Configure it
110
+ args = ['--ip', "#{options[:adapter_ip]}/#{options[:netmask]}"]
111
+ if options[:dhcp]
112
+ args.concat(['--dhcp-ip', options[:dhcp][:ip],
113
+ '--ip-scope-start', options[:dhcp][:lower],
114
+ '--ip-scope-end', options[:dhcp][:upper]])
115
+ end
116
+
117
+ execute_prlsrvctl('net', 'set', options[:network_id], *args)
118
+
119
+ # Return the details
120
+ {
121
+ name: options[:network_id],
122
+ ip: options[:adapter_ip],
123
+ netmask: options[:netmask],
124
+ dhcp: options[:dhcp]
125
+ }
126
+ end
127
+
128
+ # Creates a snapshot for the specified virtual machine.
129
+ #
130
+ # @param [String] uuid Name or UUID of the target VM.
131
+ # @param [<Symbol => String, Boolean>] options Snapshot options.
132
+ # @return [String] ID of the created snapshot.
133
+ def create_snapshot(uuid, options)
134
+ raise NotImplementedError
55
135
  end
56
136
 
57
137
  # Deletes the virtual machine references by this driver.
58
138
  def delete
139
+ execute_prlctl('delete', @uuid)
59
140
  end
60
141
 
61
142
  # Deletes all disabled network adapters from the VM configuration
62
143
  def delete_disabled_adapters
144
+ read_settings.fetch('Hardware', {}).each do |adapter, params|
145
+ if adapter.start_with?('net') and !params.fetch('enabled', true)
146
+ execute_prlctl('set', @uuid, '--device-del', adapter)
147
+ end
148
+ end
63
149
  end
64
150
 
65
151
  # Deletes any host only networks that aren't being used for anything.
66
152
  def delete_unused_host_only_networks
153
+ raise NotImplementedError
67
154
  end
68
155
 
69
156
  # Disables requiring password on such operations as creating, adding,
70
157
  # removing or cloning the virtual machine.
71
158
  #
72
159
  def disable_password_restrictions
160
+ raise NotImplementedError
73
161
  end
74
162
 
75
163
  # Enables network adapters on the VM.
@@ -85,104 +173,259 @@ module VagrantPlugins
85
173
  #
86
174
  # This must support setting up both host only and bridged networks.
87
175
  #
88
- # @param [Array<Hash>] adapters Array of adapters to enable.
176
+ # @param [Array<Symbol => Symbol, String>] adapters
177
+ # Array of adapters to be enabled.
89
178
  def enable_adapters(adapters)
179
+ raise NotImplementedError
90
180
  end
91
181
 
92
- # Exports the virtual machine to the given path.
182
+ # Create a set of port forwarding rules for a virtual machine.
183
+ #
184
+ # This will not affect any previously set forwarded ports,
185
+ # so be sure to delete those if you need to.
93
186
  #
94
- # @param [String] path Path to the OVF file.
95
- # @yield [progress] Yields the block with the progress of the export.
96
- def export(path)
187
+ # The format of each port hash should be the following:
188
+ #
189
+ # {
190
+ # guestport: 80,
191
+ # hostport: 8500,
192
+ # name: "foo",
193
+ # protocol: "tcp"
194
+ # }
195
+ #
196
+ # Note that "protocol" is optional and will default to "tcp".
197
+ #
198
+ # @param [Array<Hash>] ports An array of ports to set. See documentation
199
+ # for more information on the format.
200
+ def forward_ports(ports)
201
+ raise NotImplementedError
97
202
  end
98
203
 
99
204
  # Halts the virtual machine (pulls the plug).
100
- def halt(force)
205
+ def halt(force=false)
206
+ args = ['stop', @uuid]
207
+ args << '--kill' if force
208
+ execute_prlctl(*args)
101
209
  end
102
210
 
103
- # Imports the VM by cloning from registered template.
211
+ # Returns a list of bridged interfaces.
104
212
  #
105
- # @param [String] template_uuid Registered template UUID.
106
- # @return [String] UUID of the imported VM.
107
- def import(template_uuid)
213
+ # @return [Array<Symbol => String>]
214
+ def read_bridged_interfaces
215
+ host_hw_info = read_host_info.fetch('Hardware info', {})
216
+ net_list = host_hw_info.select do |name, attrs|
217
+ # Get all network interfaces except 'vnicXXX'
218
+ attrs.fetch('type') == 'net' and name !~ /^(vnic(.+?))$/
219
+ end
220
+
221
+ bridged_ifaces = []
222
+ net_list.keys.each do |iface|
223
+ info = {}
224
+ ifconfig = execute('ifconfig', iface)
225
+ # Assign default values
226
+ info[:name] = iface
227
+ info[:ip] = '0.0.0.0'
228
+ info[:netmask] = '0.0.0.0'
229
+ info[:status] = 'Down'
230
+
231
+ if ifconfig =~ /(?<=inet\s)(\S*)/
232
+ info[:ip] = $1.to_s
233
+ end
234
+ if ifconfig =~ /(?<=netmask\s)(\S*)/
235
+ # Netmask will be converted from hex to dec:
236
+ # '0xffffff00' -> '255.255.255.0'
237
+ info[:netmask] = $1.hex.to_s(16).scan(/../).each.map{|octet| octet.hex}.join('.')
238
+ end
239
+ if ifconfig =~ /\W(UP)\W/ and ifconfig !~ /(?<=status:\s)inactive$/
240
+ info[:status] = 'Up'
241
+ end
242
+
243
+ bridged_ifaces << info
244
+ end
245
+ bridged_ifaces
108
246
  end
109
247
 
110
- # Parses given block (JSON string) to object
111
- def json(default=nil)
112
- data = yield
113
- JSON.parse(data) rescue default
248
+ # Returns current snapshot ID for the specified VM. Returns nil if
249
+ # the VM doesn't have any snapshot.
250
+ #
251
+ # @param [String] uuid Name or UUID of the target VM.
252
+ # @return [String]
253
+ def read_current_snapshot(uuid)
254
+ raise NotImplementedError
114
255
  end
115
256
 
116
- # Returns the maximum number of network adapters.
117
- def max_network_adapters
118
- 16
257
+ def read_forwarded_ports(global=false)
258
+ raise NotImplementedError
119
259
  end
120
260
 
121
- # Returns a list of bridged interfaces.
261
+ # Returns an IP of the virtual machine. It requires that Shared network
262
+ # adapter is configured for this VM and it obtains an IP via DHCP.
122
263
  #
123
- # @return [Hash]
124
- def read_bridged_interfaces
264
+ #
265
+ # @return [String] IP address leased by DHCP server in "Shared" network
266
+ def read_guest_ip
267
+ mac_addr = read_mac_address.downcase
268
+ leases_file = '/Library/Preferences/Parallels/parallels_dhcp_leases'
269
+ begin
270
+ File.open(leases_file).grep(/#{mac_addr}/) do |line|
271
+ return line[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/]
272
+ end
273
+ rescue Errno::EACCES
274
+ raise Errors::DhcpLeasesNotAccessible, :leases_file => leases_file.to_s
275
+ rescue Errno::ENOENT
276
+ # File does not exist
277
+ # Perhaps, it is the fist start of Parallels Desktop
278
+ return nil
279
+ end
280
+
281
+ nil
282
+ end
283
+
284
+ # Returns path to the Parallels Tools ISO file.
285
+ #
286
+ # @param [String] guest_os Guest os type: "linux", "darwin" or "windows"
287
+ # @return [String] Path to the ISO.
288
+ def read_guest_tools_iso_path(guest_os)
289
+ guest_os = guest_os.to_sym
290
+ iso_name ={
291
+ linux: 'prl-tools-lin.iso',
292
+ darwin: 'prl-tools-mac.iso',
293
+ windows: 'prl-tools-win.iso'
294
+ }
295
+ return nil if !iso_name[guest_os]
296
+
297
+ bundle_id = 'com.parallels.desktop.console'
298
+ bundle_path = execute('mdfind', "kMDItemCFBundleIdentifier == #{bundle_id}")
299
+ iso_path = File.expand_path("./Contents/Resources/Tools/#{iso_name[guest_os]}",
300
+ bundle_path.split("\n")[0])
301
+
302
+ if !File.exist?(iso_path)
303
+ raise Errors::ParallelsToolsIsoNotFound, :iso_path => iso_path
304
+ end
305
+
306
+ iso_path
125
307
  end
126
308
 
127
309
  # Returns the state of guest tools that is installed on this VM.
128
310
  # Can be any of:
129
- # * "installed"
130
- # * "not_installed"
131
- # * "possibly_installed"
132
- # * "outdated"
311
+ # * :installed
312
+ # * :not_installed
313
+ # * :possibly_installed
314
+ # * :outdated
133
315
  #
134
- # @return [String]
316
+ # @return [Symbol]
135
317
  def read_guest_tools_state
318
+ state = read_settings.fetch('GuestTools', {}).fetch('state', nil)
319
+ state = 'not_installed' if !state
320
+ state.to_sym
136
321
  end
137
322
 
138
- # Returns path to the Parallels Tools ISO file.
323
+ # Returns Parallels Desktop properties and common information about
324
+ # the host machine.
139
325
  #
140
- # @param [String] guest_os Guest os type: "linux", "darwin" or "windows"
141
- # @return [String] Path to the ISO.
142
- def read_guest_tools_iso_path(guest_os)
326
+ # @return [<Symbol => String>]
327
+ def read_host_info
328
+ json { execute_prlctl('server', 'info', '--json') }
143
329
  end
144
330
 
145
331
  # Returns a list of available host only interfaces.
146
332
  #
147
- # @return [Hash]
333
+ # @return [Array<Symbol => String>]
148
334
  def read_host_only_interfaces
335
+ raise NotImplementedError
149
336
  end
150
337
 
151
338
  # Returns the MAC address of the first Shared network interface.
152
339
  #
153
340
  # @return [String]
154
341
  def read_mac_address
342
+ hw_info = read_settings.fetch('Hardware', {})
343
+ shared_ifaces = hw_info.select do |name, params|
344
+ name.start_with?('net') && params['type'] == 'shared'
345
+ end
346
+
347
+ if shared_ifaces.empty?
348
+ raise Errors::SharedAdapterNotFound
349
+ end
350
+
351
+ shared_ifaces.values.first.fetch('mac', nil)
155
352
  end
156
353
 
157
354
  # Returns the array of network interface card MAC addresses
158
355
  #
159
356
  # @return [Array<String>]
160
357
  def read_mac_addresses
358
+ read_vm_option('mac').strip.gsub(':', '').split(' ')
161
359
  end
162
360
 
163
361
  # Returns a list of network interfaces of the VM.
164
362
  #
165
- # @return [Hash]
363
+ # @return [<Integer => Hash>]
166
364
  def read_network_interfaces
365
+ raise NotImplementedError
366
+ end
367
+
368
+ # Returns virtual machine settings
369
+ #
370
+ # @return [<String => String, Hash>]
371
+ def read_settings(uuid=@uuid)
372
+ vm = json { execute_prlctl('list', uuid, '--info', '--no-header', '--json') }
373
+ vm.last
374
+ end
375
+
376
+ # Returns the unique name (e.q. "ID") on the first Shared network in
377
+ # Parallels Desktop configuration.
378
+ # By default there is only one called "Shared".
379
+ #
380
+ # @return [String] Shared network ID
381
+ def read_shared_network_id
382
+ # There should be only one Shared interface
383
+ shared_net = read_virtual_networks.detect do |net|
384
+ net['Type'] == 'shared'
385
+ end
386
+ shared_net.fetch('Network ID')
167
387
  end
168
388
 
169
389
  # Returns info about shared network interface.
170
390
  #
171
- # @return [Hash]
391
+ # @return [<Symbol => String, Hash>]
172
392
  def read_shared_interface
393
+ raise NotImplementedError
173
394
  end
174
395
 
175
396
  # Returns a list of shared folders in format:
176
397
  # { id => hostpath, ... }
177
398
  #
178
- # @return [Hash]
399
+ # @return [<String => String>]
179
400
  def read_shared_folders
401
+ shf_info = read_settings.fetch('Host Shared Folders', {})
402
+ list = {}
403
+ shf_info.delete_if { |k,v| k == 'enabled' }.each do |id, data|
404
+ list[id] = data.fetch('path')
405
+ end
406
+
407
+ list
180
408
  end
181
409
 
182
410
  # Returns the current state of this VM.
183
411
  #
184
- # @return [Symbol]
412
+ # @return [Symbol] Virtual machine state
185
413
  def read_state
414
+ read_vm_option('status').strip.to_sym
415
+ end
416
+
417
+ # Returns a list of all forwarded ports in use by active
418
+ # virtual machines.
419
+ #
420
+ # @return [Array]
421
+ def read_used_ports
422
+ end
423
+
424
+ # Returns the configuration of all virtual networks in Parallels Desktop.
425
+ #
426
+ # @return [Array<String => String>]
427
+ def read_virtual_networks
428
+ json { execute_prlsrvctl('net', 'list', '--json') }
186
429
  end
187
430
 
188
431
  # Returns a value of specified VM option. Raises an exception if value
@@ -192,41 +435,99 @@ module VagrantPlugins
192
435
  # @param [String] uuid Virtual machine UUID
193
436
  # @return [String]
194
437
  def read_vm_option(option, uuid=@uuid)
438
+ out = execute_prlctl('list', uuid,'--no-header', '-o', option).strip
439
+ if out.empty?
440
+ raise Errors::ParallelsVMOptionNotFound, vm_option: option
441
+ end
442
+
443
+ out
195
444
  end
196
445
 
197
- # Returns a list of all registered
198
- # virtual machines and templates.
446
+ # Returns names and ids of all virtual machines and templates.
199
447
  #
200
- # @return [Hash]
448
+ # @return [<String => String>]
201
449
  def read_vms
450
+ args = %w(list --all --no-header --json -o name,uuid)
451
+ vms_arr = json([]) { execute_prlctl(*args) }
452
+ templates_arr = json([]) { execute_prlctl(*args, '--template') }
453
+
454
+ vms = vms_arr | templates_arr
455
+ Hash[vms.map { |i| [i.fetch('name'), i.fetch('uuid')] }]
456
+ end
457
+
458
+ # Returns the configuration of all VMs and templates.
459
+ #
460
+ # @return [Array <String => String>]
461
+ def read_vms_info
462
+ args = %w(list --all --info --no-header --json)
463
+ vms_arr = json([]) { execute_prlctl(*args) }
464
+ templates_arr = json([]) { execute_prlctl(*args, '--template') }
465
+
466
+ vms_arr | templates_arr
202
467
  end
203
468
 
204
469
  # Regenerates 'SourceVmUuid' to avoid SMBIOS UUID collision [GH-113]
205
470
  #
206
471
  def regenerate_src_uuid
472
+ settings = read_settings
473
+ vm_config = File.join(settings.fetch('Home'), 'config.pvs')
474
+
475
+ # Generate and put new SourceVmUuid
476
+ xml = Nokogiri::XML(File.open(vm_config))
477
+ p = '//ParallelsVirtualMachine/Identification/SourceVmUuid'
478
+ xml.xpath(p).first.content = "{#{SecureRandom.uuid}}"
479
+
480
+ File.open(vm_config, 'w') do |f|
481
+ f.write xml.to_xml
482
+ end
207
483
  end
208
484
 
209
485
  # Registers the virtual machine
210
486
  #
211
487
  # @param [String] pvm_file Path to the machine image (*.pvm)
212
488
  def register(pvm_file)
489
+ args = [@prlctl_path, 'register', pvm_file]
490
+
491
+ 3.times do
492
+ result = raw(*args)
493
+ # Exit if everything is OK
494
+ return if result.exit_code == 0
495
+
496
+ # It may occur in the race condition with other Vagrant processes.
497
+ # It is OK, just exit.
498
+ return if result.stderr.include?('is already registered.')
499
+
500
+ # Sleep a bit though to give Parallels Desktop time to fix itself
501
+ sleep 2
502
+ end
503
+
504
+ # If we reach this point, it means that we consistently got the
505
+ # failure, do a standard execute now. This will raise an
506
+ # exception if it fails again.
507
+ execute(*args)
213
508
  end
214
509
 
215
- # Resumes the virtual machine.
510
+ # Checks that specified virtual machine is registered
216
511
  #
217
- def resume
512
+ # @return [Boolean]
513
+ def registered?(uuid)
514
+ args = %w(list --all --info --no-header -o uuid)
515
+
516
+ execute_prlctl(*args).include?(uuid) ||
517
+ execute_prlctl(*args, '--template').include?(uuid)
218
518
  end
219
519
 
220
- # Sets the MAC address of the first network adapter.
520
+ # Resumes the virtual machine.
221
521
  #
222
- # @param [String] mac MAC address without any spaces/hyphens.
223
- def set_mac_address(mac)
522
+ def resume
523
+ execute_prlctl('resume', @uuid)
224
524
  end
225
525
 
226
526
  # Sets the name of the virtual machine.
227
527
  #
228
528
  # @param [String] name New VM name.
229
529
  def set_name(name)
530
+ execute_prlctl('set', @uuid, '--name', name)
230
531
  end
231
532
 
232
533
  # Sets Power Consumption method.
@@ -234,39 +535,104 @@ module VagrantPlugins
234
535
  # @param [Boolean] optimized Use "Longer Battery Life"
235
536
  # instead "Better Performance"
236
537
  def set_power_consumption_mode(optimized)
538
+ raise NotImplementedError
237
539
  end
238
540
 
239
541
  # Share a set of folders on this VM.
240
542
  #
241
- # @param [Array<Hash>] folders
543
+ # @param [Array<Symbol => String>] folders
242
544
  def share_folders(folders)
545
+ folders.each do |folder|
546
+ # Add the shared folder
547
+ execute_prlctl('set', @uuid,
548
+ '--shf-host-add', folder[:name],
549
+ '--path', folder[:hostpath])
550
+ end
551
+ end
552
+
553
+ # Reads the SSH IP of this VM.
554
+ #
555
+ # @return [String] IP address to use for SSH connection to the VM.
556
+ def ssh_ip
557
+ read_guest_ip
243
558
  end
244
559
 
245
560
  # Reads the SSH port of this VM.
246
561
  #
247
562
  # @param [Integer] expected Expected guest port of SSH.
563
+ # @return [Integer] Port number to use for SSH connection to the VM.
248
564
  def ssh_port(expected)
565
+ expected
249
566
  end
250
567
 
251
568
  # Starts the virtual machine.
252
569
  #
253
570
  def start
571
+ execute_prlctl('start', @uuid)
254
572
  end
255
573
 
256
- # Suspend the virtual machine.
574
+ # Suspends the virtual machine.
257
575
  def suspend
576
+ execute_prlctl('suspend', @uuid)
577
+ end
578
+
579
+ # Performs un-registeration of the specified VM in Parallels Desktop.
580
+ # Virtual machine will be removed from the VM list, but its image will
581
+ # not be deleted from the disk. So, it can be registered again.
582
+ def unregister(uuid)
583
+ args = [@prlctl_path, 'unregister', uuid]
584
+ 3.times do
585
+ result = raw(*args)
586
+ # Exit if everything is OK
587
+ return if result.exit_code == 0
588
+
589
+ # It may occur in the race condition with other Vagrant processes.
590
+ # Both are OK, just exit.
591
+ return if result.stderr.include?('is not registered')
592
+ return if result.stderr.include?('is being cloned')
593
+
594
+ # Sleep a bit though to give Parallels Desktop time to fix itself
595
+ sleep 2
596
+ end
597
+
598
+ # If we reach this point, it means that we consistently got the
599
+ # failure, do a standard execute now. This will raise an
600
+ # exception if it fails again.
601
+ execute(*args)
258
602
  end
259
603
 
260
604
  # Unshare folders.
261
605
  def unshare_folders(names)
606
+ names.each do |name|
607
+ execute_prlctl('set', @uuid, '--shf-host-del', name)
608
+ end
262
609
  end
263
610
 
264
611
  # Checks if a VM with the given UUID exists.
265
612
  #
266
613
  # @return [Boolean]
267
614
  def vm_exists?(uuid)
615
+ 5.times do
616
+ result = raw(@prlctl_path, 'list', uuid)
617
+ return true if result.exit_code == 0
618
+
619
+ # Sometimes this happens. In this case, retry.
620
+ # If we don't see this text, the VM really doesn't exist.
621
+ return false if !result.stderr.include?('Login failed:')
622
+
623
+ # Sleep a bit though to give Parallels Desktop time to fix itself
624
+ sleep 2
625
+ end
626
+
627
+ # If we reach this point, it means that we consistently got the
628
+ # failure, do a standard prlctl now. This will raise an
629
+ # exception if it fails again.
630
+ execute_prlctl('list', uuid)
631
+ true
268
632
  end
269
633
 
634
+ private
635
+
270
636
  # Wraps 'execute' and returns the output of given 'prlctl' subcommand.
271
637
  def execute_prlctl(*command, &block)
272
638
  execute(@prlctl_path, *command, &block)
@@ -285,7 +651,7 @@ module VagrantPlugins
285
651
  # nicely handled by Vagrant.
286
652
  if r.exit_code != 0
287
653
  if @interrupted
288
- @logger.info("Exit code != 0, but interrupted. Ignoring.")
654
+ @logger.info('Exit code != 0, but interrupted. Ignoring.')
289
655
  else
290
656
  # If there was an error running command, show the error and the
291
657
  # output.
@@ -297,6 +663,12 @@ module VagrantPlugins
297
663
  r.stdout
298
664
  end
299
665
 
666
+ # Parses given block (JSON string) to object
667
+ def json(default=nil)
668
+ data = yield
669
+ JSON.parse(data) rescue default
670
+ end
671
+
300
672
  # Executes a command and returns the raw result object.
301
673
  def raw(*command, &block)
302
674
  int_callback = lambda do
@@ -304,7 +676,7 @@ module VagrantPlugins
304
676
 
305
677
  # We have to execute this in a thread due to trap contexts
306
678
  # and locks.
307
- Thread.new { @logger.info("Interrupted.") }
679
+ Thread.new { @logger.info('Interrupted.') }
308
680
  end
309
681
 
310
682
  # Append in the options for subprocess
@@ -315,8 +687,6 @@ module VagrantPlugins
315
687
  end
316
688
  end
317
689
 
318
- private
319
-
320
690
  def util_path(bin)
321
691
  path = Vagrant::Util::Which.which(bin)
322
692
  return path if path
@@ -325,6 +695,7 @@ module VagrantPlugins
325
695
  path = File.join(folder, bin)
326
696
  return path if File.file?(path)
327
697
  end
698
+ nil
328
699
  end
329
700
 
330
701
  end