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.
- checksums.yaml +4 -4
- data/.gitignore +21 -13
- data/.travis.yml +1 -0
- data/README.md +43 -54
- data/config/i18n-tasks.yml.erb +1 -1
- data/debug.log +941 -0
- data/lib/vagrant-parallels/action.rb +0 -7
- data/lib/vagrant-parallels/action/check_accessible.rb +1 -1
- data/lib/vagrant-parallels/action/check_guest_tools.rb +10 -2
- data/lib/vagrant-parallels/action/clear_network_interfaces.rb +1 -1
- data/lib/vagrant-parallels/action/customize.rb +6 -4
- data/lib/vagrant-parallels/action/export.rb +56 -12
- data/lib/vagrant-parallels/action/import.rb +49 -30
- data/lib/vagrant-parallels/action/network.rb +137 -48
- data/lib/vagrant-parallels/action/package_config_files.rb +0 -12
- data/lib/vagrant-parallels/action/prepare_nfs_valid_ids.rb +1 -1
- data/lib/vagrant-parallels/action/set_name.rb +2 -2
- data/lib/vagrant-parallels/config.rb +11 -2
- data/lib/vagrant-parallels/driver/base.rb +281 -0
- data/lib/vagrant-parallels/driver/meta.rb +138 -0
- data/lib/vagrant-parallels/driver/{prl_ctl.rb → pd_8.rb} +116 -256
- data/lib/vagrant-parallels/driver/pd_9.rb +417 -0
- data/lib/vagrant-parallels/errors.rb +15 -7
- data/lib/vagrant-parallels/plugin.rb +7 -7
- data/lib/vagrant-parallels/provider.rb +33 -3
- data/lib/vagrant-parallels/version.rb +1 -1
- data/locales/en.yml +30 -16
- data/test/unit/base.rb +1 -5
- data/test/unit/config_test.rb +13 -2
- data/test/unit/driver/pd_8_test.rb +196 -0
- data/test/unit/driver/pd_9_test.rb +196 -0
- data/test/unit/locales/locales_test.rb +1 -1
- data/test/unit/support/shared/parallels_context.rb +2 -2
- data/test/unit/support/shared/pd_driver_examples.rb +243 -0
- data/test/unit/synced_folder_test.rb +37 -0
- data/vagrant-parallels.gemspec +5 -5
- metadata +39 -32
- data/lib/vagrant-parallels/action/match_mac_address.rb +0 -28
- data/lib/vagrant-parallels/action/register_template.rb +0 -24
- data/lib/vagrant-parallels/action/unregister_template.rb +0 -26
- data/test/support/isolated_environment.rb +0 -46
- data/test/support/tempdir.rb +0 -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
|
10
|
+
class PrlCtlError < VagrantParallelsError
|
11
11
|
error_key(:prlctl_error)
|
12
12
|
end
|
13
13
|
|
14
|
-
class
|
15
|
-
error_key(:
|
14
|
+
class ParallelsInstallIncomplete < VagrantParallelsError
|
15
|
+
error_key(:parallels_install_incomplete)
|
16
16
|
end
|
17
17
|
|
18
|
-
class
|
19
|
-
error_key(:
|
18
|
+
class ParallelsInvalidVersion < VagrantParallelsError
|
19
|
+
error_key(:parallels_invalid_version)
|
20
20
|
end
|
21
21
|
|
22
|
-
class
|
23
|
-
error_key(:
|
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
|