Project

General

Profile

Actions

Bug #20474

closed

Multiple free IPs returned after record deletion

Added by Lukas Zapletal over 6 years ago. Updated over 6 years ago.

Status:
Closed
Priority:
High
Assignee:
-
Category:
DHCP
Target version:
-
Difficulty:
Triaged:
Fixed in Releases:
Found in Releases:

Description

In #20173 we fixed possible race condition, but there is one more:

  • create reservation
  • call unused_ip and X is returned
  • delete the above reservation
  • call unused_ip and Y is returned

Now, the contract is expected to satisfy X != Y but it's not the case right now thanks to the design of our unused_ip method (rotating list shifts to the left on delete returning already returned IP). I think we need to rethink how this is done.

We should also not use /tmp as the directory, it's getting deleted via systemd timer in RHEL7 or other distros as well.


Related issues 2 (0 open2 closed)

Related to Foreman - Bug #20475: Implement Random DB IPAMClosedLukas Zapletal08/01/2017Actions
Related to Smart Proxy - Bug #20173: Concurrent calls to Subnet#unused_ip may return the same ip addressClosed06/30/2017Actions
Actions #1

Updated by Lukas Zapletal over 6 years ago

  • Description updated (diff)
Actions #2

Updated by Lukas Zapletal over 6 years ago

  • Related to Bug #20475: Implement Random DB IPAM added
Actions #3

Updated by Lukas Zapletal over 6 years ago

  • Related to Bug #20173: Concurrent calls to Subnet#unused_ip may return the same ip address added
Actions #4

Updated by Lukas Zapletal over 6 years ago

  • Bugzilla link set to 1459644

Randomizing will look like the best solution. We can seed random generator from MAC address as we did in #20475.

Actions #5

Updated by Lukas Zapletal over 6 years ago

Foreman core now contains randomized IPAM DB implementation, we need similar for Smart Proxy. Ideally this would be an implementation that Ivan this is customer-facing, can you put this on your scrum?

module IPAM
  # Internal DB IPAM returning all IPs in random order to minimize race conditions
  class RandomDb < Base
    def generator
       @generator ||= Random.new(mac ? mac.gsub(':', '').to_i(16) : Random.new_seed)
    end

    def random_ip
      IPAddr.new(generator.rand(subnet_range.first.to_i..subnet_range.last.to_i), subnet.family)
    end

    # Safety check not to spend much CPU time when there are no many free IPs left. This gives up
    # in about a second on Ryzen 1700 running with Ruby 2.4.
    MAX_ITERATIONS = 100_000
    def suggest_ip
      iterations = 0
      loop do
        # next random IP from the sequence generated by MAC seed
        candidate = random_ip
        iterations += 1
        break if iterations >= MAX_ITERATIONS
        # try to match it
        ip = candidate.to_s
        if !excluded_ips.include?(ip) && !subnet.known_ips.include?(ip)
          logger.debug("Found #{ip} in #{iterations} iterations")
          return ip
        end
      end
      logger.debug("Not suggesting IP Address for #{subnet} as no free IP found in reasonable time (#{iterations} iterations)")
      errors.add(:subnet, _('no random free IP could be found in our DB, enlarge subnet range'))
      nil
    end
  end
end

The following unit tests simulates the race condition:

  def test_unused_ip_with_concurrent_record_add
    @subnet.stubs(:icmp_pingable?)
    @subnet.stubs(:tcp_pingable?)
    records = []
    records << Proxy::DHCP::Reservation.new('test', "192.168.0.1", "aa:bb:cc:dd:ee:01", @subnet, :hostname =>'test_01')
    assert_equal "192.168.0.2", @subnet.unused_ip(records)
    assert_equal "192.168.0.3", @subnet.unused_ip(records)
    records << Proxy::DHCP::Reservation.new('test', "192.168.0.2", "aa:bb:cc:dd:ee:02", @subnet, :hostname =>'test_02')
    assert_equal "192.168.0.4", @subnet.unused_ip(records) # this fails - returns 192.168.0.5
  ensure
    File.delete('test/tmp/foreman-proxy_192.168.0.0_24.tmp')
  end

  def test_unused_ip_with_concurrent_record_delete
    @subnet.stubs(:icmp_pingable?)
    @subnet.stubs(:tcp_pingable?)
    records = []
    records << Proxy::DHCP::Reservation.new('test', "192.168.0.1", "aa:bb:cc:dd:ee:01", @subnet, :hostname =>'test_01')
    records << Proxy::DHCP::Reservation.new('test', "192.168.0.2", "aa:bb:cc:dd:ee:02", @subnet, :hostname =>'test_02')
    assert_equal "192.168.0.3", @subnet.unused_ip(records)
    records.pop
    assert_equal "192.168.0.4", @subnet.unused_ip(records)  # this fails - returns 192.168.0.3
  ensure
    File.delete('test/tmp/foreman-proxy_192.168.0.0_24.tmp')
  end

And the following unit test never returns (CPU spikes at 100%) because Ruby is busy with creating 16 million records array in memory. The randomized can solve that:

  def test_unused_aclass_ip_beware_this_never_finishes
    @network = "10.0.0.0" 
    @netmask = "255.0.0.0" 
    @subnet = Proxy::DHCP::Subnet.new @network, @netmask
    @subnet.stubs(:icmp_pingable?)
    @subnet.stubs(:tcp_pingable?)
    r = Proxy::DHCP::Reservation.new('test', "10.0.0.1", "aa:bb:cc:dd:ee:ff", @subnet, :hostname =>'test1')
    assert_equal "10.0.0.2", @subnet.unused_ip([r])
  ensure
    File.delete('test/tmp/foreman-proxy_10.0.0.0_8.tmp')
  end

Ideally the code is refactored so unused_ip has it's own providers injected into subnet/server and we provide two implementations: the current one (perhaps fixed so it uses less memory) and random which I believe is very easy to implement (see the code bit from core) and workarounds the issue nicely.

Actions #6

Updated by Lukas Zapletal over 6 years ago

  • Priority changed from Normal to High

BZ set, customer facing.

Actions #7

Updated by Anonymous over 6 years ago

  • Status changed from New to Assigned
  • Assignee set to Anonymous
Actions #8

Updated by The Foreman Bot over 6 years ago

  • Status changed from Assigned to Ready For Testing
  • Pull request https://github.com/theforeman/smart-proxy/pull/543 added
Actions #9

Updated by Anonymous over 6 years ago

  • Status changed from Ready For Testing to Closed
  • % Done changed from 0 to 100
Actions

Also available in: Atom PDF