Bug #20474
closedMultiple free IPs returned after record deletion
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.
Updated by Lukas Zapletal over 8 years ago
- Related to Bug #20475: Implement Random DB IPAM added
Updated by Lukas Zapletal over 8 years ago
- Related to Bug #20173: Concurrent calls to Subnet#unused_ip may return the same ip address added
Updated by Lukas Zapletal over 8 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.
Updated by Lukas Zapletal about 8 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.
Updated by Lukas Zapletal about 8 years ago
- Priority changed from Normal to High
BZ set, customer facing.
Updated by Anonymous about 8 years ago
- Status changed from New to Assigned
- Assignee set to Anonymous
Updated by The Foreman Bot about 8 years ago
- Status changed from Assigned to Ready For Testing
- Pull request https://github.com/theforeman/smart-proxy/pull/543 added
Updated by Anonymous about 8 years ago
- Status changed from Ready For Testing to Closed
- % Done changed from 0 to 100
Applied in changeset 54f44d807b8540fd3561a0d16ab68c68deb88c0f.