Class: VagrantPlugins::Parallels::Driver::Base

Inherits:
Object
  • Object
show all
Includes:
Vagrant::Util::NetworkIP, Vagrant::Util::Retryable
Defined in:
lib/vagrant-parallels/driver/base.rb

Overview

Base class for all Parallels drivers.

This class provides useful tools for things such as executing PrlCtl and handling SIGINTs and so on.

Direct Known Subclasses

Meta, PD_11

Instance Method Summary collapse

Constructor Details

#initialize(uuid) ⇒ Base

Returns a new instance of Base.



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/vagrant-parallels/driver/base.rb', line 23

def initialize(uuid)
  @uuid = uuid
  @logger = Log4r::Logger.new('vagrant_parallels::driver::base')

  # This flag is used to keep track of interrupted state (SIGINT)
  @interrupted = false

  @prlctl_path = util_path('prlctl')
  @prlsrvctl_path = util_path('prlsrvctl')
  @prldisktool_path = util_path('prl_disk_tool')

  unless @prlctl_path
    # This means that Parallels Desktop was not found, so we raise this
    # error here.
    raise VagrantPlugins::Parallels::Errors::ParallelsNotDetected
  end

  @logger.info("prlctl path: #{@prlctl_path}")
  @logger.info("prlsrvctl path: #{@prlsrvctl_path}")
end

Instance Method Details

#clear_forwarded_ports(ports) ⇒ Object

Removes the specified port forwarding rules for the virtual machine.

Each port should be described as a hash with the following keys:

{
  name:      'example',
  protocol:  'tcp',
  guest:     'target-vm-uuid',
  hostport:  '8080',
  guestport: '80'
}

Parameters:

  • ports (Array<Symbol => String>)
    • List of ports.



57
58
59
60
61
62
63
64
# File 'lib/vagrant-parallels/driver/base.rb', line 57

def clear_forwarded_ports(ports)
  args = []
  ports.each do |r|
    args.concat(["--nat-#{r[:protocol]}-del", r[:name]])
  end

  execute_prlsrvctl('net', 'set', read_shared_network_id, *args) unless args.empty?
end

#clear_shared_foldersObject

Clears the shared folders that have been set on the virtual machine.



67
68
69
70
71
72
# File 'lib/vagrant-parallels/driver/base.rb', line 67

def clear_shared_folders
  share_ids = read_shared_folders.keys
  share_ids.each do |id|
    execute_prlctl('set', @uuid, '--shf-host-del', id)
  end
end

#clone_vm(src_name, options = {}) ⇒ String

Makes a clone of the virtual machine.

Parameters:

  • src_name (String)

    Name or UUID of the source VM or template.

  • options (<String => String>) (defaults to: {})

    Options to clone virtual machine.

Returns:

  • (String)

    UUID of the new VM.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/vagrant-parallels/driver/base.rb', line 79

def clone_vm(src_name, options = {})
  dst_name = "vagrant_temp_#{(Time.now.to_f * 1000.0).to_i}_#{rand(100000)}"

  args = ['clone', src_name, '--name', dst_name]
  args.concat(['--dst', options[:dst]]) if options[:dst]

  # Linked clone options
  args << '--linked' if options[:linked]
  args.concat(['--id', options[:snapshot_id]]) if options[:snapshot_id]

  # Regenerate SourceVmUuid of the cloned VM
  args << '--regenerate-src-uuid' if options[:regenerate_src_uuid]

  execute_prlctl(*args) do |_, data|
    lines = data.split('\r')
    # The progress of the clone will be in the last line. Do a greedy
    # regular expression to find what we're looking for.
    if lines.last =~ /Copying hard disk.+?(\d{,3}) ?%/
      yield $1.to_i if block_given?
    end
  end
  read_vms[dst_name]
end

#compact_hdd(hdd_path) ⇒ Object

Compacts the specified virtual disk image

Parameters:

  • hdd_path (<String>)

    Path to the target ‘*.hdd’



106
107
108
109
110
111
112
113
114
115
# File 'lib/vagrant-parallels/driver/base.rb', line 106

def compact_hdd(hdd_path)
  execute(@prldisktool_path, 'compact', '--hdd', hdd_path) do |_, data|
    lines = data.split('\r')
    # The progress of the compact will be in the last line. Do a greedy
    # regular expression to find what we're looking for.
    if lines.last =~ /.+?(\d{,3}) ?%/
      yield $1.to_i if block_given?
    end
  end
end

#connect_network_interface(name) ⇒ Object

Connects the host machine to the specified virtual network interface Could be used for Parallels’ Shared and Host-Only interfaces only.

Parameters:

  • name (<String>)

    Network interface name. Example: ‘Shared’

Raises:

  • (NotImplementedError)


121
122
123
# File 'lib/vagrant-parallels/driver/base.rb', line 121

def connect_network_interface(name)
  raise NotImplementedError
end

#create_host_only_network(options) ⇒ <Symbol => String>

Creates a host only network with the given options.

including keys ‘:name`, `:ip`, `:netmask` and `:dhcp`

Parameters:

  • options (<Symbol => String>)

    Hostonly network options.

  • options (<Symbol => String>)

Returns:

  • (<Symbol => String>)

    The details of the host only network,



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# File 'lib/vagrant-parallels/driver/base.rb', line 131

def create_host_only_network(options)
  # Create the interface
  execute_prlsrvctl('net', 'add', options[:network_id], '--type', 'host-only')

  # Configure it
  args = ['--ip', "#{options[:adapter_ip]}/#{options[:netmask]}"]
  if options[:dhcp]
    args.concat(['--dhcp-ip', options[:dhcp][:ip],
                 '--ip-scope-start', options[:dhcp][:lower],
                 '--ip-scope-end', options[:dhcp][:upper]])
  end

  execute_prlsrvctl('net', 'set', options[:network_id], *args)

  # Return the details
  {
    name: options[:network_id],
    ip: options[:adapter_ip],
    netmask: options[:netmask],
    dhcp: options[:dhcp]
  }
end

#create_snapshot(uuid, snapshot_name) ⇒ String

Creates a snapshot for the specified virtual machine.

Parameters:

  • uuid (String)

    Name or UUID of the target VM

  • snapshot_name (String)

    Snapshot name

Returns:

  • (String)

    ID of the created snapshot.

Raises:



159
160
161
162
163
164
# File 'lib/vagrant-parallels/driver/base.rb', line 159

def create_snapshot(uuid, snapshot_name)
  stdout = execute_prlctl('snapshot', uuid, '--name', snapshot_name)
  return Regexp.last_match(1) if stdout =~ /{([\w-]+)}/

  raise Errors::SnapshotIdNotDetected, stdout: stdout
end

#deleteObject

Deletes the virtual machine references by this driver.



167
168
169
# File 'lib/vagrant-parallels/driver/base.rb', line 167

def delete
  execute_prlctl('delete', @uuid)
end

#delete_disabled_adaptersObject

Deletes all disabled network adapters from the VM configuration



172
173
174
175
176
177
178
# File 'lib/vagrant-parallels/driver/base.rb', line 172

def delete_disabled_adapters
  read_settings.fetch('Hardware', {}).each do |adapter, params|
    if adapter.start_with?('net') and !params.fetch('enabled', true)
      execute_prlctl('set', @uuid, '--device-del', adapter)
    end
  end
end

#delete_snapshot(uuid, snapshot_id) ⇒ Object

Deletes the specified snapshot

Parameters:

  • uuid (String)

    Name or UUID of the target VM

  • snapshot_id (String)

    Snapshot ID



184
185
186
187
188
189
190
# File 'lib/vagrant-parallels/driver/base.rb', line 184

def delete_snapshot(uuid, snapshot_id)
  # Sometimes this command fails with 'Data synchronization is currently
  # in progress'. Just wait and retry.
  retryable(on: VagrantPlugins::Parallels::Errors::ExecutionError, tries: 2, sleep: 2) do
    execute_prlctl('snapshot-delete', uuid, '--id', snapshot_id)
  end
end

#delete_unused_host_only_networksObject

Deletes host-only networks that aren’t being used by any virtual machine.



193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/vagrant-parallels/driver/base.rb', line 193

def delete_unused_host_only_networks
  networks = read_virtual_networks

  # Exclude all host-only network interfaces which were not created by vagrant provider.
  networks.keep_if do |net|
    net['Type'] == 'host-only' && net['Network ID'] =~ /^vagrant-vnet(\d+)$/
  end

  read_vms_info.each do |vm|
    used_nets = vm.fetch('Hardware', {}).select { |name, _| name.start_with? 'net' }
    used_nets.each_value do |net_params|
      networks.delete_if { |net| net['Network ID'] == net_params.fetch('iface', nil) }
    end
  end

  # Delete all unused network interfaces.
  networks.each do |net|
    execute_prlsrvctl('net', 'del', net['Network ID'])
  end
end

#disable_password_restrictions(acts) ⇒ Object

Disables requiring password on such operations as creating, adding, removing or cloning the virtual machine.

‘create-vm’, ‘add-vm’, ‘remove-vm’, ‘clone-vm’

Parameters:

  • acts (Array<String>)

    List of actions. Available values:



219
220
221
222
223
224
# File 'lib/vagrant-parallels/driver/base.rb', line 219

def disable_password_restrictions(acts)
  server_info = json { execute_prlsrvctl('info', '--json') }
  server_info.fetch('Require password to', []).each do |act|
    execute_prlsrvctl('set', '--require-pwd', "#{act}:off") if acts.include? act
  end
end

#enable_adapters(adapters) ⇒ Object

Enables network adapters on the VM.

The format of each adapter specification should be like so:

:type     => :hostonly,
:hostonly => "vagrant-vnet0",
:name     => "vnic2",
:nic_type => "virtio"

This must support setting up both host only and bridged networks.

Array of adapters to be enabled.

Parameters:

  • adapters (Array<Symbol => Symbol, String>)


241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/vagrant-parallels/driver/base.rb', line 241

def enable_adapters(adapters)
  # Get adapters which have already configured for this VM
  # Such adapters will be just overridden
  existing_adapters = read_settings.fetch('Hardware', {}).keys.select do |name|
    name.start_with? 'net'
  end

  # Disable all previously existing adapters (except shared 'vnet0')
  existing_adapters.each do |adapter|
    execute_prlctl('set', @uuid, '--device-set', adapter, '--disable') if adapter != 'vnet0'
  end

  adapters.each do |adapter|
    args = []
    if existing_adapters.include? "net#{adapter[:adapter]}"
      args.concat(['--device-set', "net#{adapter[:adapter]}", '--enable'])
    else
      args.concat(%w[--device-add net])
    end

    case adapter[:type]
    when :hostonly
      args.concat(['--type', 'host', '--iface', adapter[:hostonly]])
    when :bridged
      args.concat(['--type', 'bridged', '--iface', adapter[:bridge]])
    when :shared
      args.concat(%w[--type shared])
    end

    args.concat(['--mac', adapter[:mac_address]]) if adapter[:mac_address]

    args.concat(['--adapter-type', adapter[:nic_type].to_s]) if adapter[:nic_type]

    execute_prlctl('set', @uuid, *args)
  end
end

#execute_prlctl(*command, &block) ⇒ Object

Wraps ‘execute’ and returns the output of given ‘prlctl’ subcommand.



859
860
861
# File 'lib/vagrant-parallels/driver/base.rb', line 859

def execute_prlctl(*command, &block)
  execute(@prlctl_path, *command, &block)
end

#forward_ports(ports) ⇒ Object

Create a set of port forwarding rules for a virtual machine.

This will not affect any previously set forwarded ports, so be sure to delete those if you need to.

The format of each port hash should be the following:

{
  guestport: 80,
  hostport: 8500,
  name: "foo",
  protocol: "tcp"
}

Note that “protocol” is optional and will default to “tcp”.

for more information on the format.

Parameters:

  • ports (Array<Hash>)

    An array of ports to set. See documentation



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/vagrant-parallels/driver/base.rb', line 296

def forward_ports(ports)
  args = []
  ports.each do |options|
    protocol = options[:protocol] || 'tcp'
    pf_builder = [
      options[:name],
      options[:host_port],
      @uuid,
      options[:guest_port]
    ]

    args.concat(["--nat-#{protocol}-add", pf_builder.join(',')])
  end

  execute_prlsrvctl('net', 'set', read_shared_network_id, *args)
end

#halt(force = false) ⇒ Object

Halts the virtual machine (pulls the plug).



343
344
345
346
347
# File 'lib/vagrant-parallels/driver/base.rb', line 343

def halt(force = false)
  args = ['stop', @uuid]
  args << '--kill' if force
  execute_prlctl(*args)
end

#list_snapshots(uuid) ⇒ <String => String>

Lists all snapshots for the specified VM. Returns an empty hash if there are no snapshots.

Parameters:

  • uuid (String)

    Name or UUID of the target VM.

Returns:

  • (<String => String>)

    Name’ => ‘Snapshot UUID’



318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/vagrant-parallels/driver/base.rb', line 318

def list_snapshots(uuid)
  settings = read_settings(uuid)
  snap_config = File.join(settings.fetch('Home'), 'Snapshots.xml')

  # There are no snapshots, exit
  return {} unless File.exist?(snap_config)

  xml = Nokogiri::XML(File.read(snap_config))
  snapshots = {}

  # Loop over all 'SavedStateItem' and fetch 'Name' => 'ID' pairs
  xml.xpath('//SavedStateItem').each do |snap|
    snap_id = snap.attr('guid')

    # The first entry is always empty (the base sate)
    next if snap_id.empty?

    snap_name = snap.at('Name').text
    snapshots[snap_name] = snap_id
  end

  snapshots
end

#read_bridged_interfacesArray<Symbol => String>

Returns a list of bridged interfaces.

Returns:

  • (Array<Symbol => String>)


352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
# File 'lib/vagrant-parallels/driver/base.rb', line 352

def read_bridged_interfaces
  host_hw_info = read_host_info.fetch('Hardware info', {})
  net_list = host_hw_info.select do |name, attrs|
    # Get all network interfaces except 'vnicXXX'
    attrs.fetch('type') == 'net' and name !~ /^(vnic(.+?))$/
  end

  bridged_ifaces = []
  net_list.keys.each do |iface|
    info = {}
    ifconfig = execute('ifconfig', iface)
    # Assign default values
    info[:name] = iface
    info[:ip] = '0.0.0.0'
    info[:netmask] = '0.0.0.0'
    info[:status] = 'Down'

    info[:ip] = $1.to_s if ifconfig =~ /(?<=inet\s)(\S*)/
    if ifconfig =~ /(?<=netmask\s)(\S*)/
      # Netmask will be converted from hex to dec:
      # '0xffffff00' -> '255.255.255.0'
      info[:netmask] = $1.hex.to_s(16).scan(/../).each.map { |octet| octet.hex }.join('.')
    end
    info[:status] = 'Up' if ifconfig =~ /\W(UP)\W/ and ifconfig !~ /(?<=status:\s)inactive$/

    bridged_ifaces << info
  end
  bridged_ifaces
end

#read_forwarded_ports(global = false) ⇒ Array<Symbol => String>

Returns the list of port forwarding rules. Each rule will be represented as a hash with the following keys:

{
  name:      'example',
  protocol:  'tcp',
  guest:     'target-vm-uuid',
  hostport:  '8080',
  guestport: '80'
}

Otherwise only rules related to the context VM will be returned.

Parameters:

  • global (Boolean) (defaults to: false)

    If true, returns all the rules on the host.

Returns:

  • (Array<Symbol => String>)


396
397
398
399
400
401
402
403
404
# File 'lib/vagrant-parallels/driver/base.rb', line 396

def read_forwarded_ports(global = false)
  all_rules = read_shared_interface[:nat]

  if global
    all_rules
  else
    all_rules.select { |r| r[:guest].include?(@uuid) }
  end
end

#read_guest_ip_dhcpString

Returns an IP of the virtual machine fetched from the DHCP lease file. It requires that Shared network adapter is configured for this VM and it obtains an IP via DHCP. Returns an empty string if the IP coudn’t be determined this way.

Returns:

  • (String)

    IP address leased by DHCP server in “Shared” network



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
# File 'lib/vagrant-parallels/driver/base.rb', line 412

def read_guest_ip_dhcp
  mac_addr = read_mac_address.downcase
  leases_file = '/Library/Preferences/Parallels/parallels_dhcp_leases'
  leases = {}
  begin
    File.open(leases_file).grep(/#{mac_addr}/) do |line|
      _, ip, exp, dur, = line.split /([\d.]*)="(\d*),(\d*),(\w*),(\w*)".*/
      leases[ip] = exp.to_i - dur.to_i
    end
  rescue Errno::EACCES
    raise Errors::DhcpLeasesNotAccessible, leases_file: leases_file.to_s
  rescue Errno::ENOENT
    # File does not exist
    # Perhaps, it is the fist start of Parallels Desktop
    return ''
  end

  return '' if leases.empty?

  # Get the most resent lease and return an associated IP
  leases.max_by { |_ip, lease_time| lease_time }.first
end

#read_guest_ip_prlctlString

Returns an IP of the virtual machine fetched from prlctl. Returns an empty string if the IP coudn’t be determined this way.

Returns:

  • (String)

    IP address returned by ‘prlctl list -f` command



439
440
441
442
443
# File 'lib/vagrant-parallels/driver/base.rb', line 439

def read_guest_ip_prlctl
  vm_info = json { execute_prlctl('list', @uuid, '--full', '--json') }
  ip = vm_info.first.fetch('ip_configured', '')
  ip == '-' ? '' : ip
end

#read_guest_tools_iso_path(guest_os, arch = nil) ⇒ String

Returns path to the Parallels Tools ISO file.

Parameters:

  • guest_os (String)

    Guest os type: “linux”, “darwin” or “windows”

Returns:

  • (String)

    Path to the ISO.



449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
# File 'lib/vagrant-parallels/driver/base.rb', line 449

def read_guest_tools_iso_path(guest_os, arch=nil)
  guest_os = (guest_os + (['arm', 'arm64', 'aarch64'].include?(arch.to_s.strip) ? '_arm' : '')).to_sym
  iso_name = {
    linux: 'prl-tools-lin.iso',
    linux_arm: 'prl-tools-lin-arm.iso',
    darwin: 'prl-tools-mac.iso',
    darwin_arm: 'prl-tools-mac-arm.iso',
    windows: 'prl-tools-win.iso',
    windows_arm: 'prl-tools-win-arm.iso',
  }
  return nil unless iso_name[guest_os]

  bundle_id = 'com.parallels.desktop.console'
  bundle_path = execute('mdfind', "kMDItemCFBundleIdentifier == #{bundle_id}")
  iso_path = File.expand_path("./Contents/Resources/Tools/#{iso_name[guest_os]}",
                              bundle_path.split("\n")[0])

  unless File.exist?(iso_path)
    if guest_os == :windows
      # Fallback to exe tools package for older versions of Paralles
      iso_path = File.expand_path("./Contents/Resources/Tools/PTIAgent.exe", bundle_path.split("\n")[0])
    end

    raise Errors::ParallelsToolsIsoNotFound, iso_path: iso_path unless File.exist?(iso_path)
  end

  iso_path
end

#read_guest_tools_stateSymbol

Returns the state of guest tools that is installed on this VM. Can be any of:

  • :installed

  • :not_installed

  • :possibly_installed

  • :outdated

Returns:

  • (Symbol)


486
487
488
489
490
# File 'lib/vagrant-parallels/driver/base.rb', line 486

def read_guest_tools_state
  state = read_settings.fetch('GuestTools', {}).fetch('state', nil)
  state ||= 'not_installed'
  state.to_sym
end

#read_host_info<Symbol => String>

Returns Parallels Desktop properties and common information about the host machine.

Returns:

  • (<Symbol => String>)


496
497
498
# File 'lib/vagrant-parallels/driver/base.rb', line 496

def read_host_info
  json { execute_prlctl('server', 'info', '--json') }
end

#read_host_only_interfacesArray<Symbol => String>

Returns a list of available host only interfaces. Each interface is represented as a Hash with the following details:

name:     'Host-Only',     # Parallels Network ID
ip:       '10.37.129.2',   # IP address of the interface
netmask:  '255.255.255.0', # netmask associated with the interface
status:   'Up'             # status of the interface

Returns:

  • (Array<Symbol => String>)


511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# File 'lib/vagrant-parallels/driver/base.rb', line 511

def read_host_only_interfaces
  net_list = read_virtual_networks
  net_list.keep_if { |net| net['Type'] == 'host-only' }

  hostonly_ifaces = []
  net_list.each do |iface|
    net_info = json do
      execute_prlsrvctl('net', 'info', iface['Network ID'], '--json')
    end

    iface = {
      name: net_info['Network ID'],
      status: 'Down'
    }

    adapter = net_info['Parallels adapter']
    if adapter
      iface[:ip] = adapter['IPv4 address']
      iface[:netmask] = adapter['IPv4 subnet mask']
      iface[:status] = 'Up'

      if adapter['IPv6 address'] && adapter['IPv6 subnet mask']
        iface[:ipv6] = adapter['IPv6 address']
        iface[:ipv6_prefix] = adapter['IPv6 subnet mask']
      end
    end

    hostonly_ifaces << iface
  end
  hostonly_ifaces
end

#read_mac_addressString

Returns the MAC address of the first Shared network interface.

Returns:

  • (String)

Raises:



546
547
548
549
550
551
552
553
554
555
# File 'lib/vagrant-parallels/driver/base.rb', line 546

def read_mac_address
  hw_info = read_settings.fetch('Hardware', {})
  shared_ifaces = hw_info.select do |name, params|
    name.start_with?('net') && params['type'] == 'shared'
  end

  raise Errors::SharedInterfaceNotFound if shared_ifaces.empty?

  shared_ifaces.values.first.fetch('mac', nil)
end

#read_mac_addressesArray<String>

Returns the array of network interface card MAC addresses

Returns:

  • (Array<String>)


560
561
562
# File 'lib/vagrant-parallels/driver/base.rb', line 560

def read_mac_addresses
  read_vm_option('mac').strip.gsub(':', '').split(' ')
end

#read_network_interfaces<Integer => Hash>

Returns a list of network interfaces of the VM.

Returns:

  • (<Integer => Hash>)


567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/vagrant-parallels/driver/base.rb', line 567

def read_network_interfaces
  nics = {}

  # Get enabled VM's network interfaces
  ifaces = read_settings.fetch('Hardware', {}).keep_if do |dev, params|
    dev.start_with?('net') and params.fetch('enabled', true)
  end
  ifaces.each do |name, params|
    adapter = name.match(/^net(\d+)$/)[1].to_i
    nics[adapter] ||= {}

    case params['type']
    when 'shared'
      nics[adapter][:type] = :shared
    when 'host'
      nics[adapter][:type] = :hostonly
      nics[adapter][:hostonly] = params.fetch('iface', '')
    when 'bridged'
      nics[adapter][:type] = :bridged
      nics[adapter][:bridge] = params.fetch('iface', '')
    end
  end
  nics
end

#read_settings(uuid = @uuid) ⇒ <String => String, Hash>

Returns virtual machine settings

Returns:

  • (<String => String, Hash>)


595
596
597
598
# File 'lib/vagrant-parallels/driver/base.rb', line 595

def read_settings(uuid = @uuid)
  vm = json { execute_prlctl('list', uuid, '--info', '--no-header', '--json') }
  vm.last
end

#read_shared_folders<String => String>

Returns a list of shared folders in format: { id => hostpath, … }

Returns:

  • (<String => String>)


656
657
658
659
660
661
662
663
664
# File 'lib/vagrant-parallels/driver/base.rb', line 656

def read_shared_folders
  shf_info = read_settings.fetch('Host Shared Folders', {})
  list = {}
  shf_info.delete_if { |k, _v| k == 'enabled' }.each do |id, data|
    list[id] = data.fetch('path')
  end

  list
end

#read_shared_interface<Symbol => String, Hash>

Returns info about shared network interface.

Returns:

  • (<Symbol => String, Hash>)


612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
# File 'lib/vagrant-parallels/driver/base.rb', line 612

def read_shared_interface
  net_info = json do
    execute_prlsrvctl('net', 'info', read_shared_network_id, '--json')
  end

  iface = {
    nat: [],
    status: 'Down'
  }
  adapter = net_info['Parallels adapter']

  if adapter
    iface[:ip] = adapter['IPv4 address']
    iface[:netmask] = adapter['IPv4 subnet mask']
    iface[:status] = 'Up'
  end

  if net_info.key?('DHCPv4 server')
    iface[:dhcp] = {
      ip: net_info['DHCPv4 server']['Server address'],
      lower: net_info['DHCPv4 server']['IP scope start address'],
      upper: net_info['DHCPv4 server']['IP scope end address']
    }
  end

  net_info['NAT server'].each do |group, rules|
    rules.each do |name, params|
      iface[:nat] << {
        name: name,
        protocol: group == 'TCP rules' ? 'tcp' : 'udp',
        guest: params['destination IP/VM id'],
        hostport: params['source port'],
        guestport: params['destination port']
      }
    end
  end

  iface
end

#read_shared_network_idString

Returns the unique name (e.q. “ID”) on the first Shared network in Parallels Desktop configuration. By default there is only one called “Shared”.

Returns:

  • (String)

    Shared network ID



605
606
607
# File 'lib/vagrant-parallels/driver/base.rb', line 605

def read_shared_network_id
  'Shared'
end

#read_stateSymbol

Returns the current state of this VM.

Returns:

  • (Symbol)

    Virtual machine state



669
670
671
# File 'lib/vagrant-parallels/driver/base.rb', line 669

def read_state
  read_vm_option('status').strip.to_sym
end

#read_used_portsArray

Returns a list of all forwarded ports in use by active virtual machines.

Returns:

  • (Array)


677
678
679
680
# File 'lib/vagrant-parallels/driver/base.rb', line 677

def read_used_ports
  # Ignore our own used ports
  read_forwarded_ports(true).reject { |r| r[:guest].include?(@uuid) }
end

#read_virtual_networksArray<String => String>

Returns the configuration of all virtual networks in Parallels Desktop.

Returns:

  • (Array<String => String>)


685
686
687
# File 'lib/vagrant-parallels/driver/base.rb', line 685

def read_virtual_networks
  json { execute_prlsrvctl('net', 'list', '--json') }
end

#read_vm_option(option, uuid = @uuid) ⇒ String

Returns a value of specified VM option. Raises an exception if value is not available

Parameters:

  • option (String)

    Name of option (See all: ‘prlctl list -L`)

  • uuid (String) (defaults to: @uuid)

    Virtual machine UUID

Returns:

  • (String)

Raises:



695
696
697
698
699
700
# File 'lib/vagrant-parallels/driver/base.rb', line 695

def read_vm_option(option, uuid = @uuid)
  out = execute_prlctl('list', uuid, '--no-header', '-o', option).strip
  raise Errors::ParallelsVMOptionNotFound, vm_option: option if out.empty?

  out
end

#read_vms<String => String>

Returns names and ids of all virtual machines and templates.

Returns:

  • (<String => String>)


705
706
707
708
709
710
711
712
# File 'lib/vagrant-parallels/driver/base.rb', line 705

def read_vms
  args = %w[list --all --no-header --json -o name,uuid]
  vms_arr = json { execute_prlctl(*args) }
  templates_arr = json { execute_prlctl(*args, '--template') }

  vms = vms_arr | templates_arr
  Hash[vms.map { |i| [i.fetch('name'), i.fetch('uuid')] }]
end

#read_vms_infoArray <String => String>

Returns the configuration of all VMs and templates.

Returns:

  • (Array <String => String>)


717
718
719
720
721
722
723
# File 'lib/vagrant-parallels/driver/base.rb', line 717

def read_vms_info
  args = %w[list --all --info --no-header --json]
  vms_arr = json { execute_prlctl(*args) }
  templates_arr = json { execute_prlctl(*args, '--template') }

  vms_arr | templates_arr
end

#register(pvm_file, opts = []) ⇒ Object

Registers the virtual machine

Parameters:

  • pvm_file (String)

    Path to the machine image (*.pvm)

  • opts (Array<String>) (defaults to: [])

    List of options for “prlctl register”



729
730
731
# File 'lib/vagrant-parallels/driver/base.rb', line 729

def register(pvm_file, opts = [])
  execute_prlctl('register', pvm_file, *opts)
end

#restore_snapshot(uuid, snapshot_id) ⇒ Object

Switches the VM state to the specified snapshot

Parameters:

  • uuid (String)

    Name or UUID of the target VM

  • snapshot_id (String)

    Snapshot ID



737
738
739
740
741
742
743
# File 'lib/vagrant-parallels/driver/base.rb', line 737

def restore_snapshot(uuid, snapshot_id)
  # Sometimes this command fails with 'Data synchronization is currently
  # in progress'. Just wait and retry.
  retryable(on: VagrantPlugins::Parallels::Errors::ExecutionError, tries: 2, sleep: 2) do
    execute_prlctl('snapshot-switch', uuid, '-i', snapshot_id)
  end
end

#resumeObject

Resumes the virtual machine.



747
748
749
# File 'lib/vagrant-parallels/driver/base.rb', line 747

def resume
  execute_prlctl('resume', @uuid)
end

#set_name(uuid, new_name) ⇒ Object

Sets the name of the virtual machine.

Parameters:

  • uuid (String)

    VM name or UUID

  • new_name (String)

    New VM name



755
756
757
# File 'lib/vagrant-parallels/driver/base.rb', line 755

def set_name(uuid, new_name)
  execute_prlctl('set', uuid, '--name', new_name)
end

#set_power_consumption_mode(optimized) ⇒ Object

Sets Power Consumption method.

instead “Better Performance”

Parameters:

  • optimized (Boolean)

    Use “Longer Battery Life”



763
764
765
766
# File 'lib/vagrant-parallels/driver/base.rb', line 763

def set_power_consumption_mode(optimized)
  state = optimized ? 'on' : 'off'
  execute_prlctl('set', @uuid, '--longer-battery-life', state)
end

#share_folders(folders) ⇒ Object

Share a set of folders on this VM.

Parameters:

  • folders (Array<Symbol => String>)


771
772
773
774
775
776
777
778
# File 'lib/vagrant-parallels/driver/base.rb', line 771

def share_folders(folders)
  folders.each do |folder|
    # Add the shared folder
    execute_prlctl('set', @uuid,
                   '--shf-host-add', folder[:name],
                   '--path', folder[:hostpath])
  end
end

#ssh_ipString

Reads the SSH IP of this VM from DHCP lease file or from ‘prlctl list` command - whatever returns a non-empty result first. The method with DHCP does not work for *.macvm VMs on Apple M-series Macs, so we try both sources here.

Returns:

  • (String)

    IP address to use for SSH connection to the VM.



786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
# File 'lib/vagrant-parallels/driver/base.rb', line 786

def ssh_ip
  5.times do
    ip = read_guest_ip_dhcp
    return ip unless ip.empty?

    ip = read_guest_ip_prlctl
    return ip unless ip.empty?

    sleep 2
  end

  # We didn't manage to determine IP - return nil and
  # expect SSH client to do a retry
  return nil
end

#ssh_port(expected) ⇒ Integer

Reads the SSH port of this VM.

Parameters:

  • expected (Integer)

    Expected guest port of SSH.

Returns:

  • (Integer)

    Port number to use for SSH connection to the VM.



806
807
808
# File 'lib/vagrant-parallels/driver/base.rb', line 806

def ssh_port(expected)
  expected
end

#startObject

Starts the virtual machine.



812
813
814
# File 'lib/vagrant-parallels/driver/base.rb', line 812

def start
  execute_prlctl('start', @uuid)
end

#suspendObject

Suspends the virtual machine.



817
818
819
# File 'lib/vagrant-parallels/driver/base.rb', line 817

def suspend
  execute_prlctl('suspend', @uuid)
end

#unregister(uuid) ⇒ Object

Performs un-registeration of the specified VM in Parallels Desktop. Virtual machine will be removed from the VM list, but its image will not be deleted from the disk. So, it can be registered again.



824
825
826
# File 'lib/vagrant-parallels/driver/base.rb', line 824

def unregister(uuid)
  execute_prlctl('unregister', uuid)
end

#unshare_folders(names) ⇒ Object

Unshare folders.



829
830
831
832
833
# File 'lib/vagrant-parallels/driver/base.rb', line 829

def unshare_folders(names)
  names.each do |name|
    execute_prlctl('set', @uuid, '--shf-host-del', name)
  end
end

#vm_exists?(uuid) ⇒ Boolean

Checks if a VM with the given UUID exists.

Returns:

  • (Boolean)


838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
# File 'lib/vagrant-parallels/driver/base.rb', line 838

def vm_exists?(uuid)
  5.times do
    result = raw(@prlctl_path, 'list', uuid)
    return true if result.exit_code == 0

    # Sometimes this happens. In this case, retry.
    # If we don't see this text, the VM really doesn't exist.
    return false unless result.stderr.include?('Login failed:')

    # Sleep a bit though to give Parallels Desktop time to fix itself
    sleep 2
  end

  # If we reach this point, it means that we consistently got the
  # failure, do a standard prlctl now. This will raise an
  # exception if it fails again.
  execute_prlctl('list', uuid)
  true
end