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