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
@@ -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