lib/vagrant-vcloud/action/handle_nat_port_collisions.rb
require 'set'
module VagrantPlugins
module VCloud
module Action
# This middleware class will detect and handle collisions with
# forwarded ports, whether that means raising an error or repairing
# them automatically.
#
# Parameters it takes from the environment hash:
#
# * `:port_collision_repair` - If true, it will attempt to repair
# port collisions. If false, it will raise an exception when
# there is a collision.
#
# * `:port_collision_extra_in_use` - An array of ports that are
# considered in use.
#
# * `:port_collision_remap` - A hash remapping certain host ports
# to other host ports.
#
class HandleNATPortCollisions
def initialize(app, env)
@app = app
@logger = Log4r::Logger.new(
'vagrant_vcloud::action::handle_port_collisions'
)
end
def call(env)
@logger.info('Detecting any forwarded port collisions...')
# Determine a list of usable ports for repair
usable_ports = Set.new(env[:machine].config.vm.usable_port_range)
# Pass one, remove all defined host ports from usable ports
with_forwarded_ports(env) do |options|
usable_ports.delete(options[:host])
end
cfg = env[:machine].provider_config
cnx = cfg.vcloud_cnx.driver
vapp_id = env[:machine].get_vapp_id
@logger.debug('Getting VM info...')
vm_name = cfg.name ? cfg.name.to_sym : env[:machine].name
vm = cnx.get_vapp(vapp_id)
vm_info = vm[:vms_hash][vm_name.to_sym]
network_name = get_edge_network_name(env)
vapp_edge_ip = cnx.get_vapp_edge_public_ip(vapp_id, network_name)
@logger.debug('Getting edge gateway port forwarding rules...')
edge_gateway_rules = cnx.get_edge_gateway_rules(cfg.vdc_edge_gateway,
cfg.vdc_id)
edge_dnat_rules = edge_gateway_rules.select {|r| (r[:rule_type] == 'DNAT' && r[:translated_ip] != vapp_edge_ip)}
edge_ports_in_use = edge_dnat_rules.map{|r| r[:original_port].to_i}.to_set
@logger.debug('Getting port forwarding rules...')
vapp_nat_rules = cnx.get_vapp_port_forwarding_rules(vapp_id, network_name)
ports_in_use = vapp_nat_rules.map{|r| r[:nat_external_port].to_i}.to_set
# merge the vapp ports and the edge gateway ports together, all are in use
ports_in_use = ports_in_use | edge_ports_in_use
# Pass two, detect/handle any collisions
with_forwarded_ports(env) do |options|
guest_port = options[:guest]
host_port = options[:host]
# Find if there already is a NAT rule to guest_port of this VM
if r = vapp_nat_rules.find { |rule| (rule[:vapp_scoped_local_id] == vm_info[:vapp_scoped_local_id] &&
rule[:nat_internal_port] == guest_port.to_s) }
host_port = r[:nat_external_port].to_i
@logger.info(
"Found existing port forwarding rule #{host_port} to #{guest_port}"
)
options[:host] = host_port
options[:already_exists] = true
else
# If the port is open (listening for TCP connections)
if ports_in_use.include?(host_port)
if !options[:auto_correct]
raise Errors::ForwardPortCollision,
:guest_port => guest_port.to_s,
:host_port => host_port.to_s
end
@logger.info("Attempting to repair FP collision: #{host_port}")
repaired_port = nil
while !usable_ports.empty?
# Attempt to repair the forwarded port
repaired_port = usable_ports.to_a.sort[0]
usable_ports.delete(repaired_port)
# If the port is in use, then we can't use this either...
if ports_in_use.include?(repaired_port)
@logger.info(
"Repaired port also in use: #{repaired_port}." +
'Trying another...'
)
next
end
# We have a port so break out
break
end
# If we have no usable ports then we can't repair
if !repaired_port && usable_ports.empty?
raise Errors::ForwardPortAutolistEmpty,
:vm_name => env[:machine].name,
:guest_port => guest_port.to_s,
:host_port => host_port.to_s
end
# Modify the args in place
options[:host] = repaired_port
@logger.info(
"Repaired FP collision: #{host_port} to #{repaired_port}"
)
# Notify the user
env[:ui].info(
I18n.t(
'vagrant.actions.vm.forward_ports.fixed_collision',
:host_port => host_port.to_s,
:guest_port => guest_port.to_s,
:new_port => repaired_port.to_s
)
)
end
end
end
@app.call(env)
end
protected
def with_forwarded_ports(env)
env[:machine].config.vm.networks.each do |type, options|
# Ignore anything but forwarded ports
next if type != :forwarded_port
next if !options[:disabled].nil? && options[:disabled] == true
yield options
end
# advanced networking check
if !env[:machine].provider_config.nics.nil?
env[:machine].provider_config.nics.each do |nic|
next if nic[:forwarded_port].nil?
nic[:forwarded_port].each do |fp|
next if !fp[:disabled].nil? && fp[:disabled] == true
yield fp
end
end
end
end
def get_edge_network_name(env)
# advanced networking check
if !env[:machine].provider_config.networks.nil?
env[:machine].provider_config.networks[:vapp].each do |net|
# Ignore anything but forwarded ports
next if net[:vdc_network_name].nil?
return net[:vdc_network_name]
end
end
return env[:machine].provider_config.vdc_network_name
end
end
end
end
end