lib/vagrant-parallels/driver/prl_ctl.rb in vagrant-parallels-0.0.4 vs lib/vagrant-parallels/driver/prl_ctl.rb in vagrant-parallels-0.0.5
- old
+ new
@@ -1,9 +1,10 @@
require 'log4r'
require 'json'
require 'vagrant/util/busy'
+require "vagrant/util/network_ip"
require 'vagrant/util/platform'
require 'vagrant/util/retryable'
require 'vagrant/util/subprocess'
module VagrantPlugins
@@ -14,10 +15,11 @@
# This class provides useful tools for things such as executing
# PrlCtl and handling SIGINTs and so on.
class PrlCtl
# Include this so we can use `Subprocess` more easily.
include Vagrant::Util::Retryable
+ include Vagrant::Util::NetworkIP
attr_reader :uuid
def initialize(uuid)
@logger = Log4r::Logger.new("vagrant::provider::parallels::prlctl")
@@ -27,15 +29,118 @@
# Store machine id
@uuid = uuid
# Set the path to prlctl
- @manager_path = "prlctl"
+ @prlctl_path = "prlctl"
+ @prlsrvctl_path = "prlsrvctl"
- @logger.info("Parallels path: #{@manager_path}")
+ @logger.info("CLI prlctl path: #{@prlctl_path}")
+ @logger.info("CLI prlsrvctl path: #{@prlsrvctl_path}")
end
+ def create_host_only_network(options)
+ # Create the interface
+ execute(:prlsrvctl, "net", "add", options[:name], "--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[:name], *args)
+
+ # Determine interface to which it has been bound
+ net_info = json { execute(:prlsrvctl, 'net', 'info', options[:name], '--json', retryable: true) }
+ bound_to = net_info['Bound To']
+
+ # Return the details
+ return {
+ :name => options[:name],
+ :bound_to => bound_to,
+ :ip => options[:adapter_ip],
+ :netmask => options[:netmask],
+ :dhcp => options[:dhcp]
+ }
+ end
+
+ def delete_unused_host_only_networks
+ networks = read_virtual_networks()
+
+ # 'Shared'(vnic0) and 'Host-Only'(vnic1) are default in Parallels Desktop
+ # They should not be deleted anyway.
+ networks.keep_if do |net|
+ net['Type'] == "host-only" && /^vnic(\d+)$/.match(net['Bound To'])[1].to_i >= 2
+ end
+
+ read_all_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['Bound To'] == net_params.fetch('iface', nil)}
+ end
+
+ end
+
+ networks.each do |net|
+ # Delete the actual host only network interface.
+ execute(:prlsrvctl, "net", "del", net["Network ID"])
+ end
+ end
+
+ 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 { |name| name.start_with? 'net' }
+
+ # Disable all previously existing adapters (except shared 'vnet0')
+ existing_adapters.each do |adapter|
+ if adapter != 'vnet0'
+ execute('set', @uuid, '--device-set', adapter, '--disable')
+ end
+ end
+
+ adapters.each do |adapter|
+ args = []
+ if existing_adapters.include? "net#{adapter[:adapter]}"
+ args.concat(["--device-set","net#{adapter[:adapter]}", "--enable"])
+ else
+ args.concat(["--device-add", "net"])
+ end
+
+ if adapter[:hostonly] or adapter[:bridge]
+ # Oddly enough, but there is a 'bridge' anyway.
+ # The only difference is the destination interface:
+ # - in host-only (private) network it will be bridged to the 'vnicX' device
+ # - in real bridge (public) network it will be bridged to the assigned device
+ args.concat(["--type", "bridged", "--iface", adapter[:bound_to]])
+ end
+
+ if adapter[:shared]
+ args.concat(["--type", "shared"])
+ end
+
+ if adapter[:dhcp]
+ args.concat(["--dhcp", "yes"])
+ elsif adapter[:ip]
+ args.concat(["--ipdel", "all", "--ipadd", "#{adapter[:ip]}/#{adapter[:netmask]}"])
+ end
+
+ if adapter[:mac_address]
+ args.concat(["--mac", adapter[:mac_address]])
+ end
+
+ if adapter[:nic_type]
+ args.concat(["--adapter-type", adapter[:nic_type].to_s])
+ end
+
+ execute("set", @uuid, *args)
+ end
+ end
+
# Returns the current state of this VM.
#
# @return [Symbol]
def read_state
read_settings(@uuid).fetch('State', 'inaccessible').to_sym
@@ -67,14 +172,84 @@
end
list
end
+ def read_bridged_interfaces
+ net_list = read_virtual_networks()
+
+ # Skip 'vnicXXX' and 'Default' interfaces
+ net_list.delete_if do |net|
+ net['Type'] != "bridged" or net['Bound To'] =~ /^(vnic(.+?)|Default)$/
+ end
+
+ bridged_ifaces = []
+ net_list.collect do |iface|
+ info = {}
+ ifconfig = raw('ifconfig', iface['Bound To']).stdout
+ # Assign default values
+ info[:name] = iface['Network ID']
+ info[:ip] = "0.0.0.0"
+ info[:netmask] = "0.0.0.0"
+ info[:status] = "Down"
+
+ ifconfig.split("\n").each do |line|
+ if line =~ /(?<=inet\s)(\S*)/
+ info[:ip] = $1.to_s
+ end
+ if line =~ /(?<=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(".")
+ elsif line =~ /\W(UP)\W/
+ info[:status] = "Up"
+ end
+ end
+ bridged_ifaces << info
+ end
+ bridged_ifaces
+ end
+
+ def read_host_only_interfaces
+ net_list = read_virtual_networks()
+ net_list.keep_if { |net| net['Type'] == "host-only" }
+
+ hostonly_ifaces = []
+ net_list.collect do |iface|
+ info = {}
+ net_info = json { execute(:prlsrvctl, 'net', 'info', iface['Network ID'], '--json') }
+ # Really we need to work with bounded virtual interface
+ info[:name] = net_info['Network ID']
+ info[:bound_to] = net_info['Bound To']
+ info[:ip] = net_info['Parallels adapter']['IP address']
+ info[:netmask] = net_info['Parallels adapter']['Subnet mask']
+ # Such interfaces are always in 'Up'
+ info[:status] = "Up"
+
+ # There may be a fake DHCPv4 parameters
+ # We can trust them only if adapter IP and DHCP IP are in the same subnet
+ dhcp_ip = net_info['DHCPv4 server']['Server address']
+ if network_address(info[:ip], info[:netmask]) == network_address(dhcp_ip, info[:netmask])
+ info[:dhcp] = {
+ :ip => dhcp_ip,
+ :lower => net_info['DHCPv4 server']['IP scope start address'],
+ :upper => net_info['DHCPv4 server']['IP scope end address']
+ }
+ end
+ hostonly_ifaces << info
+ end
+ hostonly_ifaces
+ end
+
def read_mac_address
read_settings.fetch('Hardware', {}).fetch('net0', {}).fetch('mac', nil)
end
+ def read_virtual_networks
+ json { execute(:prlsrvctl, 'net', 'list', '--json', retryable: true) }
+ end
+
# Verifies that the driver is ready to accept work.
#
# This should raise a VagrantError if things are not ready.
def verify!
# TODO: Use version method?
@@ -91,30 +266,25 @@
execute("set", @uuid, "--shf-host-del", folder)
end
end
def import(template_uuid, vm_name)
- last = 0
execute("clone", template_uuid, '--name', vm_name) do |type, data|
lines = data.split("\r")
# The progress of the import will be in the last line. Do a greedy
# regular expression to find what we're looking for.
- if lines.last =~ /.+?(\d{,3})%/
- current = $1.to_i
- if current > last
- last = current
- yield current if block_given?
- end
+ if lines.last =~ /.+?(\d{,3}) ?%/
+ yield $1.to_i if block_given?
end
end
@uuid = read_settings(vm_name).fetch('ID', vm_name)
end
def delete_adapters
- read_settings.fetch('Hardware').each do |k, _|
- if k != 'net0' and k.start_with? 'net'
- execute('set', @uuid, '--device-del', k)
+ read_settings.fetch('Hardware', {}).each do |adapter, params|
+ if adapter.start_with?('net') and !params.fetch("enabled", true)
+ execute('set', @uuid, '--device-del', adapter)
end
end
end
def resume
@@ -138,41 +308,31 @@
def delete
execute('delete', @uuid)
end
def export(path, vm_name)
- last = 0
execute("clone", @uuid, "--name", vm_name, "--template", "--dst", path.to_s) do |type, data|
lines = data.split("\r")
# The progress of the import will be in the last line. Do a greedy
# regular expression to find what we're looking for.
- if lines.last =~ /.+?(\d{,3})%/
- current = $1.to_i
- if current > last
- last = current
- yield current if block_given?
- end
+ if lines.last =~ /.+?(\d{,3}) ?%/
+ yield $1.to_i if block_given?
end
end
read_settings(vm_name).fetch('ID', vm_name)
end
def compact(uuid=nil)
uuid ||= @uuid
path_to_hdd = read_settings(uuid).fetch("Hardware", {}).fetch("hdd0", {}).fetch("image", nil)
- last = 0
raw('prl_disk_tool', 'compact', '--hdd', path_to_hdd) do |type, data|
lines = data.split("\r")
# The progress of the import will be in the last line. Do a greedy
# regular expression to find what we're looking for.
if lines.last =~ /.+?(\d{,3}) ?%/
- current = $1.to_i
- if current > last
- last = current
- yield current if block_given?
- end
+ yield $1.to_i if block_given?
end
end
end
def register(pvm_file)
@@ -220,10 +380,15 @@
return ip
end
end
end
+ # apply custom vm setting via set parameter
+ def set_vm_settings(command)
+ raw(@manager_path, *command)
+ end
+
private
# Parse the JSON from *all* VMs and templates. Then return an array of objects (without duplicates)
def read_all_info
vms_arr = json({}) do
@@ -247,57 +412,65 @@
def guest_execute(*command)
execute('exec', @uuid, *command)
end
+ def error_detection(command_response)
+ errored = false
+ # If the command was a failure, then raise an exception that is
+ # nicely handled by Vagrant.
+ if command_response.exit_code != 0
+ if @interrupted
+ @logger.info("Exit code != 0, but interrupted. Ignoring.")
+ elsif command_response.exit_code == 126
+ # This exit code happens if PrlCtl is on the PATH,
+ # but another executable it tries to execute is missing.
+ # This is usually indicative of a corrupted Parallels install.
+ raise VagrantPlugins::Parallels::Errors::ParallelsErrorNotFoundError
+ else
+ errored = true
+ end
+ elsif command_response.stderr =~ /failed to open \/dev\/prlctl/i
+ # This catches an error message that only shows when kernel
+ # drivers aren't properly installed.
+ @logger.error("Error message about unable to open prlctl")
+ raise VagrantPlugins::Parallels::Errors::ParallelsErrorKernelModuleNotLoaded
+ elsif command_response.stderr =~ /Unable to perform/i
+ @logger.info("VM not running for command to work.")
+ errored = true
+ elsif command_response.stderr =~ /Invalid usage/i
+ @logger.info("PrlCtl error text found, assuming error.")
+ errored = true
+ end
+ errored
+ end
+
# Execute the given subcommand for PrlCtl and return the output.
def execute(*command, &block)
+ # Get the utility to execute: 'prlctl' by default and 'prlsrvctl' if it set as a first argument in command
+ if command.first == :prlsrvctl
+ cli = @prlsrvctl_path
+ command.delete_at(0)
+ else
+ cli = @prlctl_path
+ end
+
# Get the options hash if it exists
opts = {}
opts = command.pop if command.last.is_a?(Hash)
- tries = 0
- tries = 3 if opts[:retryable]
+ tries = opts[:retryable] ? 3 : 0
# Variable to store our execution result
r = nil
# If there is an error with PrlCtl, this gets set to true
errored = false
retryable(on: VagrantPlugins::Parallels::Errors::ParallelsError, tries: tries, sleep: 1) do
# Execute the command
- r = raw(@manager_path, *command, &block)
-
- # If the command was a failure, then raise an exception that is
- # nicely handled by Vagrant.
- if r.exit_code != 0
- if @interrupted
- @logger.info("Exit code != 0, but interrupted. Ignoring.")
- elsif r.exit_code == 126
- # This exit code happens if PrlCtl is on the PATH,
- # but another executable it tries to execute is missing.
- # This is usually indicative of a corrupted Parallels install.
- raise VagrantPlugins::Parallels::Errors::ParallelsErrorNotFoundError
- else
- errored = true
- end
- else
- if r.stderr =~ /failed to open \/dev\/prlctl/i
- # This catches an error message that only shows when kernel
- # drivers aren't properly installed.
- @logger.error("Error message about unable to open prlctl")
- raise VagrantPlugins::Parallels::Errors::ParallelsErrorKernelModuleNotLoaded
- end
-
- if r.stderr =~ /Unable to perform/i
- @logger.info("VM not running for command to work.")
- errored = true
- elsif r.stderr =~ /Invalid usage/i
- @logger.info("PrlCtl error text found, assuming error.")
- errored = true
- end
- end
+ r = raw(cli, *command, &block)
+ errored = error_detection(r)
end
# If there was an error running PrlCtl, show the error and the
# output.
if errored