vagrant-parallels 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.md +63 -8
- data/lib/vagrant-parallels/action.rb +8 -1
- data/lib/vagrant-parallels/action/check_guest_tools.rb +1 -3
- data/lib/vagrant-parallels/action/clear_network_interfaces.rb +1 -1
- data/lib/vagrant-parallels/action/customize.rb +43 -0
- data/lib/vagrant-parallels/action/destroy_unused_network_interfaces.rb +22 -0
- data/lib/vagrant-parallels/action/network.rb +272 -0
- data/lib/vagrant-parallels/action/share_folders.rb +2 -2
- data/lib/vagrant-parallels/config.rb +59 -0
- data/lib/vagrant-parallels/driver/prl_ctl.rb +231 -58
- data/lib/vagrant-parallels/errors.rb +6 -2
- data/lib/vagrant-parallels/plugin.rb +5 -5
- data/lib/vagrant-parallels/version.rb +1 -1
- data/locales/en.yml +8 -4
- data/sandi_meter/assets/chart.js +12 -0
- data/sandi_meter/assets/forkme.png +0 -0
- data/sandi_meter/assets/jquery.js +6 -0
- data/sandi_meter/assets/morris.css +2 -0
- data/sandi_meter/assets/normalize.css +406 -0
- data/sandi_meter/assets/script.js +83 -0
- data/sandi_meter/assets/style.css +128 -0
- data/sandi_meter/index.html +392 -0
- data/sandi_meter/sandi_meter.log +1 -0
- data/vagrant-parallels.gemspec +2 -2
- metadata +30 -8
- checksums.yaml +0 -15
@@ -27,8 +27,8 @@ module VagrantPlugins
|
|
27
27
|
mount_shared_folders
|
28
28
|
end
|
29
29
|
|
30
|
-
# This method returns an actual list of
|
31
|
-
# folders to create and their proper path.
|
30
|
+
# This method returns an actual list of Parallels Desktop
|
31
|
+
# shared folders to create and their proper path.
|
32
32
|
def shared_folders
|
33
33
|
{}.tap do |result|
|
34
34
|
@env[:machine].config.vm.synced_folders.each do |id, data|
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module VagrantPlugins
|
2
|
+
module Parallels
|
3
|
+
class Config < Vagrant.plugin("2", :config)
|
4
|
+
attr_reader :customizations
|
5
|
+
attr_accessor :destroy_unused_network_interfaces
|
6
|
+
attr_reader :network_adapters
|
7
|
+
attr_accessor :name
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@customizations = []
|
11
|
+
@destroy_unused_network_interfaces = UNSET_VALUE
|
12
|
+
@network_adapters = {}
|
13
|
+
@name = UNSET_VALUE
|
14
|
+
|
15
|
+
network_adapter(0, :shared)
|
16
|
+
end
|
17
|
+
|
18
|
+
def customize(*command)
|
19
|
+
event = command.first.is_a?(String) ? command.shift : "pre-boot"
|
20
|
+
command = command[0]
|
21
|
+
@customizations << [event, command]
|
22
|
+
end
|
23
|
+
|
24
|
+
def network_adapter(slot, type, *args)
|
25
|
+
@network_adapters[slot] = [type, args]
|
26
|
+
end
|
27
|
+
|
28
|
+
def finalize!
|
29
|
+
if @destroy_unused_network_interfaces == UNSET_VALUE
|
30
|
+
@destroy_unused_network_interfaces = true
|
31
|
+
end
|
32
|
+
|
33
|
+
@name = nil if @name == UNSET_VALUE
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate(machine)
|
37
|
+
errors = []
|
38
|
+
valid_events = ["pre-import", "pre-boot", "post-boot"]
|
39
|
+
@customizations.each do |event, _|
|
40
|
+
if !valid_events.include?(event)
|
41
|
+
errors << I18n.t(
|
42
|
+
"vagrant_parallels.config.invalid_event",
|
43
|
+
event: event.to_s,
|
44
|
+
valid_events: valid_events.join(", "))
|
45
|
+
end
|
46
|
+
end
|
47
|
+
@customizations.each do |event, command|
|
48
|
+
if event == "pre-import" && command.index(:id)
|
49
|
+
errors << I18n.t("vagrant_parallels.config.id_in_pre_import")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
{ "Parallels Provider" => errors }
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -2,6 +2,7 @@ require 'log4r'
|
|
2
2
|
require 'json'
|
3
3
|
|
4
4
|
require 'vagrant/util/busy'
|
5
|
+
require "vagrant/util/network_ip"
|
5
6
|
require 'vagrant/util/platform'
|
6
7
|
require 'vagrant/util/retryable'
|
7
8
|
require 'vagrant/util/subprocess'
|
@@ -16,6 +17,7 @@ module VagrantPlugins
|
|
16
17
|
class PrlCtl
|
17
18
|
# Include this so we can use `Subprocess` more easily.
|
18
19
|
include Vagrant::Util::Retryable
|
20
|
+
include Vagrant::Util::NetworkIP
|
19
21
|
|
20
22
|
attr_reader :uuid
|
21
23
|
|
@@ -29,9 +31,112 @@ module VagrantPlugins
|
|
29
31
|
@uuid = uuid
|
30
32
|
|
31
33
|
# Set the path to prlctl
|
32
|
-
@
|
34
|
+
@prlctl_path = "prlctl"
|
35
|
+
@prlsrvctl_path = "prlsrvctl"
|
33
36
|
|
34
|
-
@logger.info("
|
37
|
+
@logger.info("CLI prlctl path: #{@prlctl_path}")
|
38
|
+
@logger.info("CLI prlsrvctl path: #{@prlsrvctl_path}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_host_only_network(options)
|
42
|
+
# Create the interface
|
43
|
+
execute(:prlsrvctl, "net", "add", options[:name], "--type", "host-only")
|
44
|
+
|
45
|
+
# Configure it
|
46
|
+
args = ["--ip", "#{options[:adapter_ip]}/#{options[:netmask]}"]
|
47
|
+
if options[:dhcp]
|
48
|
+
args.concat(["--dhcp-ip", options[:dhcp][:ip],
|
49
|
+
"--ip-scope-start", options[:dhcp][:lower],
|
50
|
+
"--ip-scope-end", options[:dhcp][:upper]])
|
51
|
+
end
|
52
|
+
|
53
|
+
execute(:prlsrvctl, "net", "set", options[:name], *args)
|
54
|
+
|
55
|
+
# Determine interface to which it has been bound
|
56
|
+
net_info = json { execute(:prlsrvctl, 'net', 'info', options[:name], '--json', retryable: true) }
|
57
|
+
bound_to = net_info['Bound To']
|
58
|
+
|
59
|
+
# Return the details
|
60
|
+
return {
|
61
|
+
:name => options[:name],
|
62
|
+
:bound_to => bound_to,
|
63
|
+
:ip => options[:adapter_ip],
|
64
|
+
:netmask => options[:netmask],
|
65
|
+
:dhcp => options[:dhcp]
|
66
|
+
}
|
67
|
+
end
|
68
|
+
|
69
|
+
def delete_unused_host_only_networks
|
70
|
+
networks = read_virtual_networks()
|
71
|
+
|
72
|
+
# 'Shared'(vnic0) and 'Host-Only'(vnic1) are default in Parallels Desktop
|
73
|
+
# They should not be deleted anyway.
|
74
|
+
networks.keep_if do |net|
|
75
|
+
net['Type'] == "host-only" && /^vnic(\d+)$/.match(net['Bound To'])[1].to_i >= 2
|
76
|
+
end
|
77
|
+
|
78
|
+
read_all_info.each do |vm|
|
79
|
+
used_nets = vm.fetch('Hardware', {}).select { |name, _| name.start_with? 'net' }
|
80
|
+
used_nets.each_value do |net_params|
|
81
|
+
networks.delete_if { |net| net['Bound To'] == net_params.fetch('iface', nil)}
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
networks.each do |net|
|
87
|
+
# Delete the actual host only network interface.
|
88
|
+
execute(:prlsrvctl, "net", "del", net["Network ID"])
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def enable_adapters(adapters)
|
93
|
+
# Get adapters which have already configured for this VM
|
94
|
+
# Such adapters will be just overridden
|
95
|
+
existing_adapters = read_settings.fetch('Hardware', {}).keys.select { |name| name.start_with? 'net' }
|
96
|
+
|
97
|
+
# Disable all previously existing adapters (except shared 'vnet0')
|
98
|
+
existing_adapters.each do |adapter|
|
99
|
+
if adapter != 'vnet0'
|
100
|
+
execute('set', @uuid, '--device-set', adapter, '--disable')
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
adapters.each do |adapter|
|
105
|
+
args = []
|
106
|
+
if existing_adapters.include? "net#{adapter[:adapter]}"
|
107
|
+
args.concat(["--device-set","net#{adapter[:adapter]}", "--enable"])
|
108
|
+
else
|
109
|
+
args.concat(["--device-add", "net"])
|
110
|
+
end
|
111
|
+
|
112
|
+
if adapter[:hostonly] or adapter[:bridge]
|
113
|
+
# Oddly enough, but there is a 'bridge' anyway.
|
114
|
+
# The only difference is the destination interface:
|
115
|
+
# - in host-only (private) network it will be bridged to the 'vnicX' device
|
116
|
+
# - in real bridge (public) network it will be bridged to the assigned device
|
117
|
+
args.concat(["--type", "bridged", "--iface", adapter[:bound_to]])
|
118
|
+
end
|
119
|
+
|
120
|
+
if adapter[:shared]
|
121
|
+
args.concat(["--type", "shared"])
|
122
|
+
end
|
123
|
+
|
124
|
+
if adapter[:dhcp]
|
125
|
+
args.concat(["--dhcp", "yes"])
|
126
|
+
elsif adapter[:ip]
|
127
|
+
args.concat(["--ipdel", "all", "--ipadd", "#{adapter[:ip]}/#{adapter[:netmask]}"])
|
128
|
+
end
|
129
|
+
|
130
|
+
if adapter[:mac_address]
|
131
|
+
args.concat(["--mac", adapter[:mac_address]])
|
132
|
+
end
|
133
|
+
|
134
|
+
if adapter[:nic_type]
|
135
|
+
args.concat(["--adapter-type", adapter[:nic_type].to_s])
|
136
|
+
end
|
137
|
+
|
138
|
+
execute("set", @uuid, *args)
|
139
|
+
end
|
35
140
|
end
|
36
141
|
|
37
142
|
# Returns the current state of this VM.
|
@@ -69,10 +174,80 @@ module VagrantPlugins
|
|
69
174
|
list
|
70
175
|
end
|
71
176
|
|
177
|
+
def read_bridged_interfaces
|
178
|
+
net_list = read_virtual_networks()
|
179
|
+
|
180
|
+
# Skip 'vnicXXX' and 'Default' interfaces
|
181
|
+
net_list.delete_if do |net|
|
182
|
+
net['Type'] != "bridged" or net['Bound To'] =~ /^(vnic(.+?)|Default)$/
|
183
|
+
end
|
184
|
+
|
185
|
+
bridged_ifaces = []
|
186
|
+
net_list.collect do |iface|
|
187
|
+
info = {}
|
188
|
+
ifconfig = raw('ifconfig', iface['Bound To']).stdout
|
189
|
+
# Assign default values
|
190
|
+
info[:name] = iface['Network ID']
|
191
|
+
info[:ip] = "0.0.0.0"
|
192
|
+
info[:netmask] = "0.0.0.0"
|
193
|
+
info[:status] = "Down"
|
194
|
+
|
195
|
+
ifconfig.split("\n").each do |line|
|
196
|
+
if line =~ /(?<=inet\s)(\S*)/
|
197
|
+
info[:ip] = $1.to_s
|
198
|
+
end
|
199
|
+
if line =~ /(?<=netmask\s)(\S*)/
|
200
|
+
# Netmask will be converted from hex to dec:
|
201
|
+
# '0xffffff00' -> '255.255.255.0'
|
202
|
+
info[:netmask] = $1.hex.to_s(16).scan(/../).each.map{|octet| octet.hex}.join(".")
|
203
|
+
elsif line =~ /\W(UP)\W/
|
204
|
+
info[:status] = "Up"
|
205
|
+
end
|
206
|
+
end
|
207
|
+
bridged_ifaces << info
|
208
|
+
end
|
209
|
+
bridged_ifaces
|
210
|
+
end
|
211
|
+
|
212
|
+
def read_host_only_interfaces
|
213
|
+
net_list = read_virtual_networks()
|
214
|
+
net_list.keep_if { |net| net['Type'] == "host-only" }
|
215
|
+
|
216
|
+
hostonly_ifaces = []
|
217
|
+
net_list.collect do |iface|
|
218
|
+
info = {}
|
219
|
+
net_info = json { execute(:prlsrvctl, 'net', 'info', iface['Network ID'], '--json') }
|
220
|
+
# Really we need to work with bounded virtual interface
|
221
|
+
info[:name] = net_info['Network ID']
|
222
|
+
info[:bound_to] = net_info['Bound To']
|
223
|
+
info[:ip] = net_info['Parallels adapter']['IP address']
|
224
|
+
info[:netmask] = net_info['Parallels adapter']['Subnet mask']
|
225
|
+
# Such interfaces are always in 'Up'
|
226
|
+
info[:status] = "Up"
|
227
|
+
|
228
|
+
# There may be a fake DHCPv4 parameters
|
229
|
+
# We can trust them only if adapter IP and DHCP IP are in the same subnet
|
230
|
+
dhcp_ip = net_info['DHCPv4 server']['Server address']
|
231
|
+
if network_address(info[:ip], info[:netmask]) == network_address(dhcp_ip, info[:netmask])
|
232
|
+
info[:dhcp] = {
|
233
|
+
:ip => dhcp_ip,
|
234
|
+
:lower => net_info['DHCPv4 server']['IP scope start address'],
|
235
|
+
:upper => net_info['DHCPv4 server']['IP scope end address']
|
236
|
+
}
|
237
|
+
end
|
238
|
+
hostonly_ifaces << info
|
239
|
+
end
|
240
|
+
hostonly_ifaces
|
241
|
+
end
|
242
|
+
|
72
243
|
def read_mac_address
|
73
244
|
read_settings.fetch('Hardware', {}).fetch('net0', {}).fetch('mac', nil)
|
74
245
|
end
|
75
246
|
|
247
|
+
def read_virtual_networks
|
248
|
+
json { execute(:prlsrvctl, 'net', 'list', '--json', retryable: true) }
|
249
|
+
end
|
250
|
+
|
76
251
|
# Verifies that the driver is ready to accept work.
|
77
252
|
#
|
78
253
|
# This should raise a VagrantError if things are not ready.
|
@@ -93,26 +268,21 @@ module VagrantPlugins
|
|
93
268
|
end
|
94
269
|
|
95
270
|
def import(template_uuid, vm_name)
|
96
|
-
last = 0
|
97
271
|
execute("clone", template_uuid, '--name', vm_name) do |type, data|
|
98
272
|
lines = data.split("\r")
|
99
273
|
# The progress of the import will be in the last line. Do a greedy
|
100
274
|
# regular expression to find what we're looking for.
|
101
|
-
if lines.last =~ /.+?(\d{,3})
|
102
|
-
|
103
|
-
if current > last
|
104
|
-
last = current
|
105
|
-
yield current if block_given?
|
106
|
-
end
|
275
|
+
if lines.last =~ /.+?(\d{,3}) ?%/
|
276
|
+
yield $1.to_i if block_given?
|
107
277
|
end
|
108
278
|
end
|
109
279
|
@uuid = read_settings(vm_name).fetch('ID', vm_name)
|
110
280
|
end
|
111
281
|
|
112
282
|
def delete_adapters
|
113
|
-
read_settings.fetch('Hardware').each do |
|
114
|
-
if
|
115
|
-
execute('set', @uuid, '--device-del',
|
283
|
+
read_settings.fetch('Hardware', {}).each do |adapter, params|
|
284
|
+
if adapter.start_with?('net') and !params.fetch("enabled", true)
|
285
|
+
execute('set', @uuid, '--device-del', adapter)
|
116
286
|
end
|
117
287
|
end
|
118
288
|
end
|
@@ -140,17 +310,12 @@ module VagrantPlugins
|
|
140
310
|
end
|
141
311
|
|
142
312
|
def export(path, vm_name)
|
143
|
-
last = 0
|
144
313
|
execute("clone", @uuid, "--name", vm_name, "--template", "--dst", path.to_s) do |type, data|
|
145
314
|
lines = data.split("\r")
|
146
315
|
# The progress of the import will be in the last line. Do a greedy
|
147
316
|
# regular expression to find what we're looking for.
|
148
|
-
if lines.last =~ /.+?(\d{,3})
|
149
|
-
|
150
|
-
if current > last
|
151
|
-
last = current
|
152
|
-
yield current if block_given?
|
153
|
-
end
|
317
|
+
if lines.last =~ /.+?(\d{,3}) ?%/
|
318
|
+
yield $1.to_i if block_given?
|
154
319
|
end
|
155
320
|
end
|
156
321
|
|
@@ -160,17 +325,12 @@ module VagrantPlugins
|
|
160
325
|
def compact(uuid=nil)
|
161
326
|
uuid ||= @uuid
|
162
327
|
path_to_hdd = read_settings(uuid).fetch("Hardware", {}).fetch("hdd0", {}).fetch("image", nil)
|
163
|
-
last = 0
|
164
328
|
raw('prl_disk_tool', 'compact', '--hdd', path_to_hdd) do |type, data|
|
165
329
|
lines = data.split("\r")
|
166
330
|
# The progress of the import will be in the last line. Do a greedy
|
167
331
|
# regular expression to find what we're looking for.
|
168
332
|
if lines.last =~ /.+?(\d{,3}) ?%/
|
169
|
-
|
170
|
-
if current > last
|
171
|
-
last = current
|
172
|
-
yield current if block_given?
|
173
|
-
end
|
333
|
+
yield $1.to_i if block_given?
|
174
334
|
end
|
175
335
|
end
|
176
336
|
end
|
@@ -222,6 +382,11 @@ module VagrantPlugins
|
|
222
382
|
end
|
223
383
|
end
|
224
384
|
|
385
|
+
# apply custom vm setting via set parameter
|
386
|
+
def set_vm_settings(command)
|
387
|
+
raw(@manager_path, *command)
|
388
|
+
end
|
389
|
+
|
225
390
|
private
|
226
391
|
|
227
392
|
# Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates)
|
@@ -249,14 +414,51 @@ module VagrantPlugins
|
|
249
414
|
execute('exec', @uuid, *command)
|
250
415
|
end
|
251
416
|
|
417
|
+
def error_detection(command_response)
|
418
|
+
errored = false
|
419
|
+
# If the command was a failure, then raise an exception that is
|
420
|
+
# nicely handled by Vagrant.
|
421
|
+
if command_response.exit_code != 0
|
422
|
+
if @interrupted
|
423
|
+
@logger.info("Exit code != 0, but interrupted. Ignoring.")
|
424
|
+
elsif command_response.exit_code == 126
|
425
|
+
# This exit code happens if PrlCtl is on the PATH,
|
426
|
+
# but another executable it tries to execute is missing.
|
427
|
+
# This is usually indicative of a corrupted Parallels install.
|
428
|
+
raise VagrantPlugins::Parallels::Errors::ParallelsErrorNotFoundError
|
429
|
+
else
|
430
|
+
errored = true
|
431
|
+
end
|
432
|
+
elsif command_response.stderr =~ /failed to open \/dev\/prlctl/i
|
433
|
+
# This catches an error message that only shows when kernel
|
434
|
+
# drivers aren't properly installed.
|
435
|
+
@logger.error("Error message about unable to open prlctl")
|
436
|
+
raise VagrantPlugins::Parallels::Errors::ParallelsErrorKernelModuleNotLoaded
|
437
|
+
elsif command_response.stderr =~ /Unable to perform/i
|
438
|
+
@logger.info("VM not running for command to work.")
|
439
|
+
errored = true
|
440
|
+
elsif command_response.stderr =~ /Invalid usage/i
|
441
|
+
@logger.info("PrlCtl error text found, assuming error.")
|
442
|
+
errored = true
|
443
|
+
end
|
444
|
+
errored
|
445
|
+
end
|
446
|
+
|
252
447
|
# Execute the given subcommand for PrlCtl and return the output.
|
253
448
|
def execute(*command, &block)
|
449
|
+
# Get the utility to execute: 'prlctl' by default and 'prlsrvctl' if it set as a first argument in command
|
450
|
+
if command.first == :prlsrvctl
|
451
|
+
cli = @prlsrvctl_path
|
452
|
+
command.delete_at(0)
|
453
|
+
else
|
454
|
+
cli = @prlctl_path
|
455
|
+
end
|
456
|
+
|
254
457
|
# Get the options hash if it exists
|
255
458
|
opts = {}
|
256
459
|
opts = command.pop if command.last.is_a?(Hash)
|
257
460
|
|
258
|
-
tries = 0
|
259
|
-
tries = 3 if opts[:retryable]
|
461
|
+
tries = opts[:retryable] ? 3 : 0
|
260
462
|
|
261
463
|
# Variable to store our execution result
|
262
464
|
r = nil
|
@@ -266,37 +468,8 @@ module VagrantPlugins
|
|
266
468
|
|
267
469
|
retryable(on: VagrantPlugins::Parallels::Errors::ParallelsError, tries: tries, sleep: 1) do
|
268
470
|
# Execute the command
|
269
|
-
r = raw(
|
270
|
-
|
271
|
-
# If the command was a failure, then raise an exception that is
|
272
|
-
# nicely handled by Vagrant.
|
273
|
-
if r.exit_code != 0
|
274
|
-
if @interrupted
|
275
|
-
@logger.info("Exit code != 0, but interrupted. Ignoring.")
|
276
|
-
elsif r.exit_code == 126
|
277
|
-
# This exit code happens if PrlCtl is on the PATH,
|
278
|
-
# but another executable it tries to execute is missing.
|
279
|
-
# This is usually indicative of a corrupted Parallels install.
|
280
|
-
raise VagrantPlugins::Parallels::Errors::ParallelsErrorNotFoundError
|
281
|
-
else
|
282
|
-
errored = true
|
283
|
-
end
|
284
|
-
else
|
285
|
-
if r.stderr =~ /failed to open \/dev\/prlctl/i
|
286
|
-
# This catches an error message that only shows when kernel
|
287
|
-
# drivers aren't properly installed.
|
288
|
-
@logger.error("Error message about unable to open prlctl")
|
289
|
-
raise VagrantPlugins::Parallels::Errors::ParallelsErrorKernelModuleNotLoaded
|
290
|
-
end
|
291
|
-
|
292
|
-
if r.stderr =~ /Unable to perform/i
|
293
|
-
@logger.info("VM not running for command to work.")
|
294
|
-
errored = true
|
295
|
-
elsif r.stderr =~ /Invalid usage/i
|
296
|
-
@logger.info("PrlCtl error text found, assuming error.")
|
297
|
-
errored = true
|
298
|
-
end
|
299
|
-
end
|
471
|
+
r = raw(cli, *command, &block)
|
472
|
+
errored = error_detection(r)
|
300
473
|
end
|
301
474
|
|
302
475
|
# If there was an error running PrlCtl, show the error and the
|