Class: VagrantPlugins::VCloud::Action::HandleNATPortCollisions

Inherits:
Object
  • Object
show all
Defined in:
lib/vagrant-vcloud/action/handle_nat_port_collisions.rb

Overview

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.

Instance Method Summary collapse

Constructor Details

#initialize(app, env) ⇒ HandleNATPortCollisions

Returns a new instance of HandleNATPortCollisions.


23
24
25
26
27
28
# File 'lib/vagrant-vcloud/action/handle_nat_port_collisions.rb', line 23

def initialize(app, env)
  @app    = app
  @logger = Log4r::Logger.new(
    'vagrant_vcloud::action::handle_port_collisions'
  )
end

Instance Method Details

#call(env) ⇒ Object


30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/vagrant-vcloud/action/handle_nat_port_collisions.rb', line 30

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