vagrant-parallels 0.0.4 → 0.0.5

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.
@@ -27,8 +27,8 @@ module VagrantPlugins
27
27
  mount_shared_folders
28
28
  end
29
29
 
30
- # This method returns an actual list of VirtualBox shared
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
- @manager_path = "prlctl"
34
+ @prlctl_path = "prlctl"
35
+ @prlsrvctl_path = "prlsrvctl"
33
36
 
34
- @logger.info("Parallels path: #{@manager_path}")
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
- current = $1.to_i
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 |k, _|
114
- if k != 'net0' and k.start_with? 'net'
115
- execute('set', @uuid, '--device-del', k)
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
- current = $1.to_i
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
- current = $1.to_i
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(@manager_path, *command, &block)
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