vagrant-parallels 0.2.1 → 0.2.2.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +21 -13
  3. data/.travis.yml +1 -0
  4. data/README.md +43 -54
  5. data/config/i18n-tasks.yml.erb +1 -1
  6. data/debug.log +941 -0
  7. data/lib/vagrant-parallels/action.rb +0 -7
  8. data/lib/vagrant-parallels/action/check_accessible.rb +1 -1
  9. data/lib/vagrant-parallels/action/check_guest_tools.rb +10 -2
  10. data/lib/vagrant-parallels/action/clear_network_interfaces.rb +1 -1
  11. data/lib/vagrant-parallels/action/customize.rb +6 -4
  12. data/lib/vagrant-parallels/action/export.rb +56 -12
  13. data/lib/vagrant-parallels/action/import.rb +49 -30
  14. data/lib/vagrant-parallels/action/network.rb +137 -48
  15. data/lib/vagrant-parallels/action/package_config_files.rb +0 -12
  16. data/lib/vagrant-parallels/action/prepare_nfs_valid_ids.rb +1 -1
  17. data/lib/vagrant-parallels/action/set_name.rb +2 -2
  18. data/lib/vagrant-parallels/config.rb +11 -2
  19. data/lib/vagrant-parallels/driver/base.rb +281 -0
  20. data/lib/vagrant-parallels/driver/meta.rb +138 -0
  21. data/lib/vagrant-parallels/driver/{prl_ctl.rb → pd_8.rb} +116 -256
  22. data/lib/vagrant-parallels/driver/pd_9.rb +417 -0
  23. data/lib/vagrant-parallels/errors.rb +15 -7
  24. data/lib/vagrant-parallels/plugin.rb +7 -7
  25. data/lib/vagrant-parallels/provider.rb +33 -3
  26. data/lib/vagrant-parallels/version.rb +1 -1
  27. data/locales/en.yml +30 -16
  28. data/test/unit/base.rb +1 -5
  29. data/test/unit/config_test.rb +13 -2
  30. data/test/unit/driver/pd_8_test.rb +196 -0
  31. data/test/unit/driver/pd_9_test.rb +196 -0
  32. data/test/unit/locales/locales_test.rb +1 -1
  33. data/test/unit/support/shared/parallels_context.rb +2 -2
  34. data/test/unit/support/shared/pd_driver_examples.rb +243 -0
  35. data/test/unit/synced_folder_test.rb +37 -0
  36. data/vagrant-parallels.gemspec +5 -5
  37. metadata +39 -32
  38. data/lib/vagrant-parallels/action/match_mac_address.rb +0 -28
  39. data/lib/vagrant-parallels/action/register_template.rb +0 -24
  40. data/lib/vagrant-parallels/action/unregister_template.rb +0 -26
  41. data/test/support/isolated_environment.rb +0 -46
  42. data/test/support/tempdir.rb +0 -43
  43. data/test/unit/driver/prl_ctl_test.rb +0 -148
@@ -1,54 +1,41 @@
1
1
  require 'log4r'
2
- require 'json'
3
2
 
4
- require 'vagrant/util/busy'
5
- require "vagrant/util/network_ip"
6
3
  require 'vagrant/util/platform'
7
- require 'vagrant/util/retryable'
8
- require 'vagrant/util/subprocess'
4
+
5
+ require File.expand_path("../base", __FILE__)
9
6
 
10
7
  module VagrantPlugins
11
8
  module Parallels
12
9
  module Driver
13
- # Base class for all Parallels drivers.
14
- #
15
- # This class provides useful tools for things such as executing
16
- # PrlCtl and handling SIGINTs and so on.
17
- class PrlCtl
18
- # Include this so we can use `Subprocess` more easily.
19
- include Vagrant::Util::Retryable
20
- include Vagrant::Util::NetworkIP
21
-
22
- attr_reader :uuid
23
-
10
+ # Driver for Parallels Desktop 8.
11
+ class PD_8 < Base
24
12
  def initialize(uuid)
25
- @logger = Log4r::Logger.new("vagrant::provider::parallels::prlctl")
13
+ super()
26
14
 
27
- # This flag is used to keep track of interrupted state (SIGINT)
28
- @interrupted = false
29
-
30
- # Store machine id
15
+ @logger = Log4r::Logger.new("vagrant::provider::parallels::pd_8")
31
16
  @uuid = uuid
17
+ end
32
18
 
33
- # Set the path to prlctl
34
- @prlctl_path = "prlctl"
35
- @prlsrvctl_path = "prlsrvctl"
36
19
 
37
- @logger.info("CLI prlctl path: #{@prlctl_path}")
38
- @logger.info("CLI prlsrvctl path: #{@prlsrvctl_path}")
20
+ def compact(uuid)
21
+ used_drives = read_settings.fetch('Hardware', {}).select { |name, _| name.start_with? 'hdd' }
22
+ used_drives.each_value do |drive_params|
23
+ execute(:prl_disk_tool, 'compact', '--hdd', drive_params["image"]) do |type, data|
24
+ lines = data.split("\r")
25
+ # The progress of the compact will be in the last line. Do a greedy
26
+ # regular expression to find what we're looking for.
27
+ if lines.last =~ /.+?(\d{,3}) ?%/
28
+ yield $1.to_i if block_given?
29
+ end
30
+ end
31
+ end
39
32
  end
40
33
 
41
- def compact(uuid=nil)
42
- uuid ||= @uuid
43
- # TODO: VM can have more than one hdd!
44
- path_to_hdd = read_settings(uuid).fetch("Hardware", {}).fetch("hdd0", {}).fetch("image", nil)
45
- raw('prl_disk_tool', 'compact', '--hdd', path_to_hdd) do |type, data|
46
- lines = data.split("\r")
47
- # The progress of the import will be in the last line. Do a greedy
48
- # regular expression to find what we're looking for.
49
- if lines.last =~ /.+?(\d{,3}) ?%/
50
- yield $1.to_i if block_given?
51
- end
34
+ def clear_shared_folders
35
+ shf = read_settings.fetch("Host Shared Folders", {}).keys
36
+ shf.delete("enabled")
37
+ shf.each do |folder|
38
+ execute("set", @uuid, "--shf-host-del", folder)
52
39
  end
53
40
  end
54
41
 
@@ -72,27 +59,19 @@ module VagrantPlugins
72
59
 
73
60
  # Return the details
74
61
  return {
75
- :name => options[:name],
76
- :bound_to => bound_to,
77
- :ip => options[:adapter_ip],
78
- :netmask => options[:netmask],
79
- :dhcp => options[:dhcp]
62
+ :name => options[:name],
63
+ :bound_to => bound_to,
64
+ :ip => options[:adapter_ip],
65
+ :netmask => options[:netmask],
66
+ :dhcp => options[:dhcp]
80
67
  }
81
68
  end
82
69
 
83
- def clear_shared_folders
84
- shf = read_settings.fetch("Host Shared Folders", {}).keys
85
- shf.delete("enabled")
86
- shf.each do |folder|
87
- execute("set", @uuid, "--shf-host-del", folder)
88
- end
89
- end
90
-
91
70
  def delete
92
71
  execute('delete', @uuid)
93
72
  end
94
73
 
95
- def delete_adapters
74
+ def delete_disabled_adapters
96
75
  read_settings.fetch('Hardware', {}).each do |adapter, params|
97
76
  if adapter.start_with?('net') and !params.fetch("enabled", true)
98
77
  execute('set', @uuid, '--device-del', adapter)
@@ -101,19 +80,19 @@ module VagrantPlugins
101
80
  end
102
81
 
103
82
  def delete_unused_host_only_networks
104
- networks = read_virtual_networks()
83
+ networks = read_virtual_networks
105
84
 
106
85
  # 'Shared'(vnic0) and 'Host-Only'(vnic1) are default in Parallels Desktop
107
86
  # They should not be deleted anyway.
108
87
  networks.keep_if do |net|
109
88
  net['Type'] == "host-only" &&
110
- net['Bound To'].match(/^(?>vnic|Parallels Host-Only #)(\d+)$/)[1].to_i >= 2
89
+ net['Bound To'].match(/^(?>vnic|Parallels Host-Only #)(\d+)$/)[1].to_i >= 2
111
90
  end
112
91
 
113
- read_all_info.each do |vm|
92
+ read_vms_info.each do |vm|
114
93
  used_nets = vm.fetch('Hardware', {}).select { |name, _| name.start_with? 'net' }
115
94
  used_nets.each_value do |net_params|
116
- networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil)}
95
+ networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil) }
117
96
  end
118
97
 
119
98
  end
@@ -152,18 +131,10 @@ module VagrantPlugins
152
131
  args.concat(["--type", "bridged", "--iface", adapter[:bound_to]])
153
132
  end
154
133
 
155
- if adapter[:shared]
134
+ if adapter[:type] == :shared
156
135
  args.concat(["--type", "shared"])
157
136
  end
158
137
 
159
- if adapter[:dhcp]
160
- args.concat(["--dhcp", "yes"])
161
- elsif adapter[:ip]
162
- args.concat(["--ipdel", "all", "--ipadd", "#{adapter[:ip]}/#{adapter[:netmask]}"])
163
- else
164
- args.concat(["--dhcp", "no"])
165
- end
166
-
167
138
  if adapter[:mac_address]
168
139
  args.concat(["--mac", adapter[:mac_address]])
169
140
  end
@@ -176,17 +147,20 @@ module VagrantPlugins
176
147
  end
177
148
  end
178
149
 
179
- def export(path, vm_name)
180
- execute("clone", @uuid, "--name", vm_name, "--template", "--dst", path.to_s) do |type, data|
150
+ def execute_command(command)
151
+ execute(*command)
152
+ end
153
+
154
+ def export(path, tpl_name)
155
+ execute("clone", @uuid, "--name", tpl_name, "--template", "--dst", path.to_s) do |type, data|
181
156
  lines = data.split("\r")
182
- # The progress of the import will be in the last line. Do a greedy
157
+ # The progress of the export will be in the last line. Do a greedy
183
158
  # regular expression to find what we're looking for.
184
159
  if lines.last =~ /.+?(\d{,3}) ?%/
185
160
  yield $1.to_i if block_given?
186
161
  end
187
162
  end
188
-
189
- read_settings(vm_name).fetch('ID', vm_name)
163
+ read_vms[tpl_name]
190
164
  end
191
165
 
192
166
  def halt(force=false)
@@ -195,7 +169,10 @@ module VagrantPlugins
195
169
  execute(*args)
196
170
  end
197
171
 
198
- def import(template_uuid, vm_name)
172
+ def import(template_uuid)
173
+ template_name = read_vms.key(template_uuid)
174
+ vm_name = "#{template_name}_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}"
175
+
199
176
  execute("clone", template_uuid, '--name', vm_name) do |type, data|
200
177
  lines = data.split("\r")
201
178
  # The progress of the import will be in the last line. Do a greedy
@@ -204,71 +181,23 @@ module VagrantPlugins
204
181
  yield $1.to_i if block_given?
205
182
  end
206
183
  end
207
- @uuid = read_settings(vm_name).fetch('ID', vm_name)
208
- end
209
-
210
- def ip
211
- mac_addr = read_mac_address.downcase
212
- File.foreach("/Library/Preferences/Parallels/parallels_dhcp_leases") do |line|
213
- if line.include? mac_addr
214
- ip = line[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/]
215
- return ip
216
- end
217
- end
218
- end
219
-
220
-
221
- def mac_in_use?(mac)
222
- all_macs_in_use = []
223
- read_all_info.each do |vm|
224
- all_macs_in_use << vm.fetch('Hardware', {}).fetch('net0',{}).fetch('mac', '')
225
- end
226
-
227
- valid_mac = mac.upcase.tr('^A-F0-9', '')
228
-
229
- all_macs_in_use.include?(valid_mac)
230
- end
231
-
232
- # Returns a hash of all UUIDs assigned to VMs and templates currently
233
- # known by Parallels. Keys are 'name' values
234
- #
235
- # @return [Hash]
236
- def read_all_names
237
- list = {}
238
- read_all_info.each do |item|
239
- list[item.fetch('Name')] = item.fetch('ID')
240
- end
241
-
242
- list
243
- end
244
-
245
- # Returns a hash of all UUIDs assigned to VMs and templates currently
246
- # known by Parallels. Keys are 'Home' directories
247
- #
248
- # @return [Hash]
249
- def read_all_paths
250
- list = {}
251
- read_all_info.each do |item|
252
- if Dir.exists? item.fetch('Home')
253
- list[File.realpath item.fetch('Home')] = item.fetch('ID')
254
- end
255
- end
256
-
257
- list
184
+ read_vms[vm_name]
258
185
  end
259
186
 
260
187
  def read_bridged_interfaces
261
- net_list = read_virtual_networks()
188
+ net_list = read_virtual_networks
262
189
 
263
190
  # Skip 'vnicXXX' and 'Default' interfaces
264
191
  net_list.delete_if do |net|
265
- net['Type'] != "bridged" or net['Bound To'] =~ /^(vnic(.+?)|Default)$/
192
+ net['Type'] != "bridged" or
193
+ net['Bound To'] =~ /^(vnic(.+?))$/ or
194
+ net['Network ID'] == "Default"
266
195
  end
267
196
 
268
197
  bridged_ifaces = []
269
198
  net_list.collect do |iface|
270
199
  info = {}
271
- ifconfig = raw('ifconfig', iface['Bound To']).stdout
200
+ ifconfig = execute(:ifconfig, iface['Bound To'])
272
201
  # Assign default values
273
202
  info[:name] = iface['Network ID'].gsub(/\s\(.*?\)$/, '')
274
203
  info[:bound_to] = iface['Bound To']
@@ -298,7 +227,7 @@ module VagrantPlugins
298
227
  end
299
228
 
300
229
  def read_host_only_interfaces
301
- net_list = read_virtual_networks()
230
+ net_list = read_virtual_networks
302
231
  net_list.keep_if { |net| net['Type'] == "host-only" }
303
232
 
304
233
  hostonly_ifaces = []
@@ -325,7 +254,17 @@ module VagrantPlugins
325
254
  end
326
255
  hostonly_ifaces << info
327
256
  end
328
- hostonly_ifaces
257
+ hostonly_ifaces
258
+ end
259
+
260
+ def read_ip_dhcp
261
+ mac_addr = read_mac_address.downcase
262
+ File.foreach("/Library/Preferences/Parallels/parallels_dhcp_leases") do |line|
263
+ if line.include? mac_addr
264
+ ip = line[/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/]
265
+ return ip
266
+ end
267
+ end
329
268
  end
330
269
 
331
270
  def read_mac_address
@@ -361,46 +300,77 @@ module VagrantPlugins
361
300
  nics
362
301
  end
363
302
 
364
- # Returns the current state of this VM.
365
- #
366
- # @return [Symbol]
303
+ def read_settings
304
+ vm = json { execute('list', @uuid, '--info', '--json', retryable: true).gsub(/^INFO/, '') }
305
+ vm.last
306
+ end
307
+
367
308
  def read_state
368
- read_settings(@uuid).fetch('State', 'inaccessible').to_sym
309
+ vm = json { execute('list', @uuid, '--json', retryable: true).gsub(/^INFO/, '') }
310
+ return nil if !vm.last
311
+ vm.last.fetch('status').to_sym
369
312
  end
370
313
 
371
314
  def read_virtual_networks
372
315
  json { execute(:prlsrvctl, 'net', 'list', '--json', retryable: true) }
373
316
  end
374
317
 
375
- def ready?
376
- !!guest_execute('uname') rescue false
318
+ def read_vms
319
+ results = {}
320
+ vms_arr = json([]) do
321
+ execute('list', '--all', '--json', retryable: true).gsub(/^INFO/, '')
322
+ end
323
+ templates_arr = json([]) do
324
+ execute('list', '--all', '--json', '--template', retryable: true).gsub(/^INFO/, '')
325
+ end
326
+ vms = vms_arr | templates_arr
327
+ vms.each do |item|
328
+ results[item.fetch('name')] = item.fetch('uuid')
329
+ end
330
+
331
+ results
332
+ end
333
+
334
+ # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates)
335
+ def read_vms_info
336
+ vms_arr = json([]) do
337
+ execute('list', '--all','--info', '--json', retryable: true).gsub(/^INFO/, '')
338
+ end
339
+ templates_arr = json([]) do
340
+ execute('list', '--all','--info', '--json', '--template', retryable: true).gsub(/^INFO/, '')
341
+ end
342
+ vms_arr | templates_arr
343
+ end
344
+
345
+ def read_vms_paths
346
+ list = {}
347
+ read_vms_info.each do |item|
348
+ if Dir.exists? item.fetch('Home')
349
+ list[File.realpath item.fetch('Home')] = item.fetch('ID')
350
+ end
351
+ end
352
+
353
+ list
377
354
  end
378
355
 
379
356
  def register(pvm_file)
380
357
  execute("register", pvm_file)
381
358
  end
382
359
 
383
- def registered?(path)
384
- # TODO: Make this take UUID and have callers pass that instead
385
- # Need a way to get the UUID from unregistered templates though (config.pvs XML parsing/regex?)
386
- read_all_paths.has_key?(path)
360
+ def registered?(uuid)
361
+ read_vms.has_value?(uuid)
387
362
  end
388
363
 
389
364
  def resume
390
365
  execute('resume', @uuid)
391
366
  end
392
367
 
393
- def set_name(name)
394
- execute('set', @uuid, '--name', name, :retryable => true)
395
- end
396
-
397
368
  def set_mac_address(mac)
398
369
  execute('set', @uuid, '--device-set', 'net0', '--type', 'shared', '--mac', mac)
399
370
  end
400
371
 
401
- # apply custom vm setting via set parameter
402
- def set_vm_settings(command)
403
- raw(@prlctl_path, *command)
372
+ def set_name(name)
373
+ execute('set', @uuid, '--name', name, :retryable => true)
404
374
  end
405
375
 
406
376
  def share_folders(folders)
@@ -411,7 +381,7 @@ module VagrantPlugins
411
381
  end
412
382
 
413
383
  def ssh_port(expected_port)
414
- 22
384
+ expected_port
415
385
  end
416
386
 
417
387
  def start
@@ -426,9 +396,6 @@ module VagrantPlugins
426
396
  execute("unregister", uuid)
427
397
  end
428
398
 
429
- # Verifies that the driver is ready to accept work.
430
- #
431
- # This should raise a VagrantError if things are not ready.
432
399
  def verify!
433
400
  version
434
401
  end
@@ -441,115 +408,8 @@ module VagrantPlugins
441
408
  end
442
409
  end
443
410
 
444
- private
445
-
446
- def guest_execute(*command)
447
- execute('exec', @uuid, *command)
448
- end
449
-
450
- def json(default=nil)
451
- data = yield
452
- JSON.parse(data) rescue default
453
- end
454
-
455
- # Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates)
456
- def read_all_info
457
- vms_arr = json({}) do
458
- execute('list', '--info', '--json', retryable: true).gsub(/^(INFO)?/, '')
459
- end
460
- templates_arr = json({}) do
461
- execute('list', '--info', '--json', '--template', retryable: true).gsub(/^(INFO)?/, '')
462
- end
463
- vms_arr | templates_arr
464
- end
465
-
466
- def read_settings(uuid=nil)
467
- uuid ||= @uuid
468
- json({}) { execute('list', uuid, '--info', '--json', retryable: true).gsub(/^(INFO)?\[/, '').gsub(/\]$/, '') }
469
- end
470
-
471
- def error_detection(command_response)
472
- errored = false
473
- # If the command was a failure, then raise an exception that is
474
- # nicely handled by Vagrant.
475
- if command_response.exit_code != 0
476
- if @interrupted
477
- @logger.info("Exit code != 0, but interrupted. Ignoring.")
478
- elsif command_response.exit_code == 126
479
- # This exit code happens if PrlCtl is on the PATH,
480
- # but another executable it tries to execute is missing.
481
- # This is usually indicative of a corrupted Parallels install.
482
- raise VagrantPlugins::Parallels::Errors::ParallelsErrorNotFoundError
483
- else
484
- errored = true
485
- end
486
- elsif command_response.stderr =~ /failed to open \/dev\/prlctl/i
487
- # This catches an error message that only shows when kernel
488
- # drivers aren't properly installed.
489
- @logger.error("Error message about unable to open prlctl")
490
- raise VagrantPlugins::Parallels::Errors::ParallelsErrorKernelModuleNotLoaded
491
- elsif command_response.stderr =~ /Unable to perform/i
492
- @logger.info("VM not running for command to work.")
493
- errored = true
494
- elsif command_response.stderr =~ /Invalid usage/i
495
- @logger.info("PrlCtl error text found, assuming error.")
496
- errored = true
497
- end
498
- errored
499
- end
500
-
501
- # Execute the given subcommand for PrlCtl and return the output.
502
- def execute(*command, &block)
503
- # Get the utility to execute: 'prlctl' by default and 'prlsrvctl' if it set as a first argument in command
504
- if command.first == :prlsrvctl
505
- cli = @prlsrvctl_path
506
- command.delete_at(0)
507
- else
508
- cli = @prlctl_path
509
- end
510
-
511
- # Get the options hash if it exists
512
- opts = {}
513
- opts = command.pop if command.last.is_a?(Hash)
514
-
515
- tries = opts[:retryable] ? 3 : 0
516
-
517
- # Variable to store our execution result
518
- r = nil
519
-
520
- # If there is an error with PrlCtl, this gets set to true
521
- errored = false
522
-
523
- retryable(on: VagrantPlugins::Parallels::Errors::ParallelsError, tries: tries, sleep: 1) do
524
- # Execute the command
525
- r = raw(cli, *command, &block)
526
- errored = error_detection(r)
527
- end
528
-
529
- # If there was an error running PrlCtl, show the error and the
530
- # output.
531
- if errored
532
- raise VagrantPlugins::Parallels::Errors::ParallelsError,
533
- command: command.inspect,
534
- stderr: r.stderr
535
- end
536
-
537
- r.stdout
538
- end
539
-
540
- # Executes a command and returns the raw result object.
541
- def raw(cli, *command, &block)
542
- int_callback = lambda do
543
- @interrupted = true
544
- @logger.info("Interrupted.")
545
- end
546
-
547
- # Append in the options for subprocess
548
- command << { notify: [:stdout, :stderr] }
549
-
550
- Vagrant::Util::Busy.busy(int_callback) do
551
- Vagrant::Util::Subprocess.execute(cli, *command, &block)
552
- end
411
+ def vm_exists?(uuid)
412
+ raw("list", uuid).exit_code == 0
553
413
  end
554
414
  end
555
415
  end