vagrant-parallels 0.2.1 → 0.2.2.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.
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
@@ -0,0 +1,417 @@
1
+ require 'log4r'
2
+
3
+ require 'vagrant/util/platform'
4
+
5
+ require File.expand_path("../base", __FILE__)
6
+
7
+ module VagrantPlugins
8
+ module Parallels
9
+ module Driver
10
+ # Driver for Parallels Desktop 9.
11
+ class PD_9 < Base
12
+ def initialize(uuid)
13
+ super()
14
+
15
+ @logger = Log4r::Logger.new("vagrant::provider::parallels::pd_9")
16
+ @uuid = uuid
17
+ end
18
+
19
+
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
32
+ end
33
+
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)
39
+ end
40
+ end
41
+
42
+ def create_host_only_network(options)
43
+ # Create the interface
44
+ execute(:prlsrvctl, "net", "add", options[:name], "--type", "host-only")
45
+
46
+ # Configure it
47
+ args = ["--ip", "#{options[:adapter_ip]}/#{options[:netmask]}"]
48
+ if options[:dhcp]
49
+ args.concat(["--dhcp-ip", options[:dhcp][:ip],
50
+ "--ip-scope-start", options[:dhcp][:lower],
51
+ "--ip-scope-end", options[:dhcp][:upper]])
52
+ end
53
+
54
+ execute(:prlsrvctl, "net", "set", options[:name], *args)
55
+
56
+ # Determine interface to which it has been bound
57
+ net_info = json { execute(:prlsrvctl, 'net', 'info', options[:name], '--json', retryable: true) }
58
+ bound_to = net_info['Bound To']
59
+
60
+ # Return the details
61
+ return {
62
+ :name => options[:name],
63
+ :bound_to => bound_to,
64
+ :ip => options[:adapter_ip],
65
+ :netmask => options[:netmask],
66
+ :dhcp => options[:dhcp]
67
+ }
68
+ end
69
+
70
+ def delete
71
+ execute('delete', @uuid)
72
+ end
73
+
74
+ def delete_disabled_adapters
75
+ read_settings.fetch('Hardware', {}).each do |adapter, params|
76
+ if adapter.start_with?('net') and !params.fetch("enabled", true)
77
+ execute('set', @uuid, '--device-del', adapter)
78
+ end
79
+ end
80
+ end
81
+
82
+ def delete_unused_host_only_networks
83
+ networks = read_virtual_networks
84
+
85
+ # 'Shared'(vnic0) and 'Host-Only'(vnic1) are default in Parallels Desktop
86
+ # They should not be deleted anyway.
87
+ networks.keep_if do |net|
88
+ net['Type'] == "host-only" &&
89
+ net['Bound To'].match(/^(?>vnic|Parallels Host-Only #)(\d+)$/)[1].to_i >= 2
90
+ end
91
+
92
+ read_vms_info.each do |vm|
93
+ used_nets = vm.fetch('Hardware', {}).select { |name, _| name.start_with? 'net' }
94
+ used_nets.each_value do |net_params|
95
+ networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil) }
96
+ end
97
+
98
+ end
99
+
100
+ networks.each do |net|
101
+ # Delete the actual host only network interface.
102
+ execute(:prlsrvctl, "net", "del", net["Network ID"])
103
+ end
104
+ end
105
+
106
+ def enable_adapters(adapters)
107
+ # Get adapters which have already configured for this VM
108
+ # Such adapters will be just overridden
109
+ existing_adapters = read_settings.fetch('Hardware', {}).keys.select { |name| name.start_with? 'net' }
110
+
111
+ # Disable all previously existing adapters (except shared 'vnet0')
112
+ existing_adapters.each do |adapter|
113
+ if adapter != 'vnet0'
114
+ execute('set', @uuid, '--device-set', adapter, '--disable')
115
+ end
116
+ end
117
+
118
+ adapters.each do |adapter|
119
+ args = []
120
+ if existing_adapters.include? "net#{adapter[:adapter]}"
121
+ args.concat(["--device-set","net#{adapter[:adapter]}", "--enable"])
122
+ else
123
+ args.concat(["--device-add", "net"])
124
+ end
125
+
126
+ if adapter[:hostonly] or adapter[:bridge]
127
+ # Oddly enough, but there is a 'bridge' anyway.
128
+ # The only difference is the destination interface:
129
+ # - in host-only (private) network it will be bridged to the 'vnicX' device
130
+ # - in real bridge (public) network it will be bridged to the assigned device
131
+ args.concat(["--type", "bridged", "--iface", adapter[:bound_to]])
132
+ end
133
+
134
+ if adapter[:type] == :shared
135
+ args.concat(["--type", "shared"])
136
+ end
137
+
138
+ if adapter[:mac_address]
139
+ args.concat(["--mac", adapter[:mac_address]])
140
+ end
141
+
142
+ if adapter[:nic_type]
143
+ args.concat(["--adapter-type", adapter[:nic_type].to_s])
144
+ end
145
+
146
+ execute("set", @uuid, *args)
147
+ end
148
+ end
149
+
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|
156
+ lines = data.split("\r")
157
+ # The progress of the export will be in the last line. Do a greedy
158
+ # regular expression to find what we're looking for.
159
+ if lines.last =~ /.+?(\d{,3}) ?%/
160
+ yield $1.to_i if block_given?
161
+ end
162
+ end
163
+ read_vms[tpl_name]
164
+ end
165
+
166
+ def halt(force=false)
167
+ args = ['stop', @uuid]
168
+ args << '--kill' if force
169
+ execute(*args)
170
+ end
171
+
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
+
176
+ execute("clone", template_uuid, '--name', vm_name) do |type, data|
177
+ lines = data.split("\r")
178
+ # The progress of the import will be in the last line. Do a greedy
179
+ # regular expression to find what we're looking for.
180
+ if lines.last =~ /.+?(\d{,3}) ?%/
181
+ yield $1.to_i if block_given?
182
+ end
183
+ end
184
+ read_vms[vm_name]
185
+ end
186
+
187
+ def read_bridged_interfaces
188
+ net_list = read_virtual_networks
189
+
190
+ # Skip 'vnicXXX' and 'Default' interfaces
191
+ net_list.delete_if do |net|
192
+ net['Type'] != "bridged" or
193
+ net['Bound To'] =~ /^(vnic(.+?))$/ or
194
+ net['Network ID'] == "Default"
195
+ end
196
+
197
+ bridged_ifaces = []
198
+ net_list.collect do |iface|
199
+ info = {}
200
+ ifconfig = execute(:ifconfig, iface['Bound To'])
201
+ # Assign default values
202
+ info[:name] = iface['Network ID'].gsub(/\s\(.*?\)$/, '')
203
+ info[:bound_to] = iface['Bound To']
204
+ info[:ip] = "0.0.0.0"
205
+ info[:netmask] = "0.0.0.0"
206
+ info[:status] = "Down"
207
+
208
+ if ifconfig =~ /(?<=inet\s)(\S*)/
209
+ info[:ip] = $1.to_s
210
+ end
211
+ if ifconfig =~ /(?<=netmask\s)(\S*)/
212
+ # Netmask will be converted from hex to dec:
213
+ # '0xffffff00' -> '255.255.255.0'
214
+ info[:netmask] = $1.hex.to_s(16).scan(/../).each.map{|octet| octet.hex}.join(".")
215
+ end
216
+ if ifconfig =~ /\W(UP)\W/ and ifconfig !~ /(?<=status:\s)inactive$/
217
+ info[:status] = "Up"
218
+ end
219
+
220
+ bridged_ifaces << info
221
+ end
222
+ bridged_ifaces
223
+ end
224
+
225
+ def read_guest_tools_version
226
+ read_settings.fetch('GuestTools', {}).fetch('version', nil)
227
+ end
228
+
229
+ def read_host_only_interfaces
230
+ net_list = read_virtual_networks
231
+ net_list.keep_if { |net| net['Type'] == "host-only" }
232
+
233
+ hostonly_ifaces = []
234
+ net_list.collect do |iface|
235
+ info = {}
236
+ net_info = json { execute(:prlsrvctl, 'net', 'info', iface['Network ID'], '--json') }
237
+ # Really we need to work with bounded virtual interface
238
+ info[:name] = net_info['Network ID']
239
+ info[:bound_to] = net_info['Bound To']
240
+ info[:ip] = net_info['Parallels adapter']['IP address']
241
+ info[:netmask] = net_info['Parallels adapter']['Subnet mask']
242
+ # Such interfaces are always in 'Up'
243
+ info[:status] = "Up"
244
+
245
+ # There may be a fake DHCPv4 parameters
246
+ # We can trust them only if adapter IP and DHCP IP are in the same subnet
247
+ dhcp_ip = net_info['DHCPv4 server']['Server address']
248
+ if network_address(info[:ip], info[:netmask]) == network_address(dhcp_ip, info[:netmask])
249
+ info[:dhcp] = {
250
+ :ip => dhcp_ip,
251
+ :lower => net_info['DHCPv4 server']['IP scope start address'],
252
+ :upper => net_info['DHCPv4 server']['IP scope end address']
253
+ }
254
+ end
255
+ hostonly_ifaces << info
256
+ end
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
268
+ end
269
+
270
+ def read_mac_address
271
+ read_settings.fetch('Hardware', {}).fetch('net0', {}).fetch('mac', nil)
272
+ end
273
+
274
+ def read_network_interfaces
275
+ nics = {}
276
+
277
+ # Get enabled VM's network interfaces
278
+ ifaces = read_settings.fetch('Hardware', {}).keep_if do |dev, params|
279
+ dev.start_with?('net') and params.fetch("enabled", true)
280
+ end
281
+ ifaces.each do |name, params|
282
+ adapter = name.match(/^net(\d+)$/)[1].to_i
283
+ nics[adapter] ||= {}
284
+
285
+ if params['type'] == "shared"
286
+ nics[adapter][:type] = :shared
287
+ elsif params['type'] == "host"
288
+ # It is PD internal host-only network and it is bounded to 'vnic1'
289
+ nics[adapter][:type] = :hostonly
290
+ nics[adapter][:hostonly] = "vnic1"
291
+ elsif params['type'] == "bridged" and params.fetch('iface','').start_with?('vnic')
292
+ # Bridged to the 'vnicXX'? Then it is a host-only, actually.
293
+ nics[adapter][:type] = :hostonly
294
+ nics[adapter][:hostonly] = params.fetch('iface','')
295
+ elsif params['type'] == "bridged"
296
+ nics[adapter][:type] = :bridged
297
+ nics[adapter][:bridge] = params.fetch('iface','')
298
+ end
299
+ end
300
+ nics
301
+ end
302
+
303
+ def read_settings
304
+ vm = json { execute('list', @uuid, '--info', '--json', retryable: true) }
305
+ vm.last
306
+ end
307
+
308
+ def read_state
309
+ vm = json { execute('list', @uuid, '--json', retryable: true) }
310
+ return nil if !vm.last
311
+ vm.last.fetch('status').to_sym
312
+ end
313
+
314
+ def read_virtual_networks
315
+ json { execute(:prlsrvctl, 'net', 'list', '--json', retryable: true) }
316
+ end
317
+
318
+ def read_vms
319
+ results = {}
320
+ vms_arr = json([]) do
321
+ execute('list', '--all', '--json', retryable: true)
322
+ end
323
+ templates_arr = json([]) do
324
+ execute('list', '--all', '--json', '--template', retryable: true)
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)
338
+ end
339
+ templates_arr = json([]) do
340
+ execute('list', '--all','--info', '--json', '--template', retryable: true)
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
354
+ end
355
+
356
+ def register(pvm_file)
357
+ execute("register", pvm_file)
358
+ end
359
+
360
+ def registered?(uuid)
361
+ read_vms.has_value?(uuid)
362
+ end
363
+
364
+ def resume
365
+ execute('resume', @uuid)
366
+ end
367
+
368
+ def set_mac_address(mac)
369
+ execute('set', @uuid, '--device-set', 'net0', '--type', 'shared', '--mac', mac)
370
+ end
371
+
372
+ def set_name(name)
373
+ execute('set', @uuid, '--name', name, :retryable => true)
374
+ end
375
+
376
+ def share_folders(folders)
377
+ folders.each do |folder|
378
+ # Add the shared folder
379
+ execute('set', @uuid, '--shf-host-add', folder[:name], '--path', folder[:hostpath])
380
+ end
381
+ end
382
+
383
+ def ssh_port(expected_port)
384
+ expected_port
385
+ end
386
+
387
+ def start
388
+ execute('start', @uuid)
389
+ end
390
+
391
+ def suspend
392
+ execute('suspend', @uuid)
393
+ end
394
+
395
+ def unregister(uuid)
396
+ execute("unregister", uuid)
397
+ end
398
+
399
+ def verify!
400
+ version
401
+ end
402
+
403
+ def version
404
+ if execute('--version', retryable: true) =~ /prlctl version ([\d\.]+)/
405
+ $1.downcase
406
+ else
407
+ raise VagrantPlugins::Parallels::Errors::ParallelsInstallIncomplete
408
+ end
409
+ end
410
+
411
+ def vm_exists?(uuid)
412
+ raw("list", uuid).exit_code == 0
413
+ end
414
+ end
415
+ end
416
+ end
417
+ end
@@ -7,25 +7,33 @@ module VagrantPlugins
7
7
  error_namespace("vagrant_parallels.errors")
8
8
  end
9
9
 
10
- class ParallelsError < VagrantParallelsError
10
+ class PrlCtlError < VagrantParallelsError
11
11
  error_key(:prlctl_error)
12
12
  end
13
13
 
14
- class ParallelsErrorNotFoundError < VagrantParallelsError
15
- error_key(:prlctl_not_found_error)
14
+ class ParallelsInstallIncomplete < VagrantParallelsError
15
+ error_key(:parallels_install_incomplete)
16
16
  end
17
17
 
18
- class ParallelsErrorKernelModuleNotLoaded < VagrantParallelsError
19
- error_key(:parallels_kernel_module_not_loaded)
18
+ class ParallelsInvalidVersion < VagrantParallelsError
19
+ error_key(:parallels_invalid_version)
20
20
  end
21
21
 
22
- class ParallelsInstallIncomplete < VagrantParallelsError
23
- error_key(:parallels_install_incomplete)
22
+ class ParallelsNotDetected < VagrantParallelsError
23
+ error_key(:parallels_not_detected)
24
24
  end
25
25
 
26
26
  class ParallelsNoRoomForHighLevelNetwork < VagrantParallelsError
27
27
  error_key(:parallels_no_room_for_high_level_network)
28
28
  end
29
+
30
+ class VMInaccessible < VagrantParallelsError
31
+ error_key(:vm_inaccessible)
32
+ end
33
+
34
+ class MacOSXRequired < VagrantParallelsError
35
+ error_key(:mac_os_x_required)
36
+ end
29
37
  end
30
38
  end
31
39
  end