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