Foreman Discovery: Override existing DHCP record on provision without error

Problem:
We have the problem that we can’t provision newly discovered host, because a lease already exists from the PXE boot. It fails silently on the GUI however in the foreman-proxy.log shows that there is a record already existing. However, the code checks whether the record requested is identical to the one existing and if so it just overrides it. (Which should be the case in my scenario.) Unfortunately, the API request is missing the hardware_type option and therefore the requested record and the existing record diverge. Here is the corresponding log from the foreman-proxy.log:

2020-03-16T20:32:54 47ccb30d [W] Request to create a conflicting DHCP record
2020-03-16T20:32:54 47ccb30d [D] request: #<Proxy::DHCP::Reservation:0x0055f8c0de8bb8 @type="reservation", @name="hostname", @subnet=#<Proxy::DHCP::Subnet:0x0055f8bff7e078 @network="10.0.0.0", @netmask="255.255.
248.0", @ipaddr=#<IPAddr: IPv4:10.0.0.0/255.255.248.0>, @options={:routers=>["10.0.0.1"], :domain_name_servers=>["10.
0.0.5", "10.0.0.3"], :ntp_servers=>[["ntp"]], :range=>["10.0.0.20", "10.0.6.255"]}, @m=#<Monitor:0x0055f8c0b5f7c0 @mo
n_owner=nil, @mon_count=0, @mon_mutex=#<Thread::Mutex:0x0055f8c0b5f770>>, @netmask_to_i=4294965248>, @ip="10.0.3.37",
 @mac="f2:f4:60:fd:db:c6", @options={:deleteable=>true, :hostname=>"hostname"}>
2020-03-16T20:32:54 47ccb30d [D] existing: [#<Proxy::DHCP::Reservation:0x0055f8c09fcb58 @type="reservation", @name="hostname", @subnet=#<Proxy::DHCP::Subnet:0x0055f8bff7e078 @network="10.0.0.0", @netmask="255.25
5.248.0", @ipaddr=#<IPAddr: IPv4:10.0.0.0/255.255.248.0>, @options={:routers=>["10.0.0.1"], :domain_name_servers=>["1
0.0.0.5", "10.0.0.3"], :ntp_servers=>[["ntp"]], :range=>["10.0.0.20", "10.0.6.255"]}, @m=#<Monitor:0x0055f8c0b5f7c0 @
mon_owner=nil, @mon_count=0, @mon_mutex=#<Thread::Mutex:0x0055f8c0b5f770>>, @netmask_to_i=4294965248>, @ip="10.0.3.37
", @mac="f2:f4:60:fd:db:c6", @options={:deleteable=>true, :hostname=>"hostname", :hardware_typ
e=>"ethernet"}>]
2020-03-16T20:32:54 47ccb30d [E] Record 10.0.0.0/10.0.3.37 already exists
2020-03-16T20:32:54 47ccb30d [W] Record 10.0.0.0/10.0.3.37 already exists
Proxy::DHCP::Collision: Record 10.0.0.0/10.0.3.37 already exists
/usr/share/foreman-proxy/modules/dhcp_common/server.rb:157:in `add_record'
/usr/share/foreman-proxy/modules/dhcp_common/isc/omapi_provider.rb:29:in `add_record'
/usr/share/foreman-proxy/modules/dhcp/dhcp_api.rb:98:in `block in <class:DhcpApi>'
/usr/lib/ruby/vendor_ruby/sinatra/base.rb:1611:in `call'
...

A diff of the requested and the existing record shows that the :hardware_typ e=>"ethernet" is missing in the requested record.

... foreman-proxy/modules/dhcp_common/server.rb
145      if similar_records.any? {|record| record == to_return}
146         # we already got this record, no need to do anything
147         logger.debug "We already got the same DHCP record - skipping"
148         raise Proxy::DHCP::AlreadyExists
149       end
150
151       unless similar_records.empty?
152         logger.warn "Request to create a conflicting DHCP record"
153         logger.debug "request: #{to_return.inspect}"
154         logger.debug "existing: #{similar_records.inspect}"
155         raise Proxy::DHCP::Collision, "Record #{subnet.network}/#{ip_address} already exists"
156       end
...

Expected outcome:
A error in the GUI with the possibility to override the entry.
Send the hardware_type field in the request to the proxies DHCP endpoint.

Foreman and Proxy versions:
foreman/buster,now 1.24.2-1 amd64 [installed]
foreman-proxy/stretch,now 1.24.2-1 all [installed]

Foreman and Proxy plugin versions:
ruby-foreman-discovery/plugins,now 16.0.1-1 all [installed]

Distribution and version:
SMP Debian 4.9.189-3+deb9u2 - Stretch (Proxy)
SMP Debian 4.19.67-2+deb10u2 - Buster (Master)

Thank you everyone!
Bind regards
Florian

Hello Florian,

well that’s weird. Our configuration file / leases parser correctly parses hardware type and should put that into the options hash. I’ve just created a unit test for that:

diff --git a/test/dhcp/dhcp_isc_subnet_service_initialization_test.rb b/test/dhcp/dhcp_isc_subnet_service_initialization_test.rb
index e66781b..afe1bff 100644
--- a/test/dhcp/dhcp_isc_subnet_service_initialization_test.rb
+++ b/test/dhcp/dhcp_isc_subnet_service_initialization_test.rb
@@ -158,6 +158,14 @@ class DhcpIscSubnetServiceInitializationTest < Test::Unit::TestCase
     assert_not_nil @subnet_service.find_host_by_hostname("undeleted.example.com")
   end
 
+  def test_parsing_host_hardware_type
+    subnet = Proxy::DHCP::Subnet.new("192.168.122.0", "255.255.255.0")
+    @subnet_service.add_subnet(subnet)
+    @initialization.load_leases_file(File.read("./test/fixtures/dhcp/dhcp.leases"))
+    host = @subnet_service.find_host_by_hostname("undeleted.example.com")
+    assert_equal "ethernet", host.options[:hardware_type]
+  end
+
   def test_host_with_duplicate_mac_address_is_removed
     @subnet_service.add_subnet(subnet = Proxy::DHCP::Subnet.new("192.168.0.0", "255.255.255.0"))
     @subnet_service.add_host("192.168.0.0", ::Proxy::DHCP::Reservation.new("test", "192.168.0.10", "00:11:22:33:44:55", subnet))

Can you check your leases file? Does it have the hardware line? I assume it does. Then check with dhcpd_config_check.rb script we ship with proxy to show you how it loaded it up:

# ruby extra/dhcpd_config_check.rb -c /etc/dhcp/dhcpd.conf 
Subnets: 192.168.99.0/255.255.255.0, 192.168.199.0/255.255.255.0, 192.168.100.0/255.255.255.0
Hosts and leases: 
Didn't recognize: 
omapi-port 7911, parents: group: root_group
keyomapi_key, parents: group: root_group
, parents: group: root_group
omapi-key omapi_key, parents: group: root_group
default-lease-time 600, parents: group: root_group
max-lease-time 7200, parents: group: root_group
ddns-update-style none, parents: group: root_group
allow booting, parents: group: root_group
allow bootp, parents: group: root_group
log-facility local7, parents: group: root_group
Successfully parsed configuration file '/etc/dhcp/dhcpd.conf'

(This output is empty.)

Hello Lukáš,

thanks for the answer. I think the problem is not originating from the proxy. In fact I believe it just works fine. I tested the configuration as you suggested. The output looks similar:

Subnets: 10.0.0.0/255.255.248.0, 10.8.0.0/255.255.248.0
Hosts and leases: Host: gilgalad-eth0, Host: gilgalad-eth1, Host: auenland-eth0, Host: grischnakh, Host: grima-eth0, Host: peregrin, Host: fangorn, Host: birchseed, Host: ema, Host: elrond
Didn't recognize:
omapi-port 7911, parents: group: root_group
keyomapi_key, parents: group: root_group
, parents: group: root_group
omapi-key omapi_key, parents: group: root_group
default-lease-time 43200, parents: group: root_group
max-lease-time 86400, parents: group: root_group
ddns-update-style interim, parents: group: root_group
ignore client-updates, parents: group: root_group
authoritative, parents: group: root_group
allow booting, parents: group: root_group
allow bootp, parents: group: root_group
log-facility local7, parents: group: root_group
class"pxeclients", parents: group: root_group
, parents: group: root_group
Successfully parsed configuration file '/etc/dhcp/dhcpd.conf'

However, I think the problem is originating from the Foreman (master/discovery plugin) itself.I tried to dig into the code, but I wasn’t able to find the corresponding line.

The request from the master is incomplete on purpose or not. If you look closely at these two lines:

2020-03-16T20:32:54 47ccb30d [D] request: #<Proxy::DHCP::Reservation:0x0055f8c0de8bb8 @type="reservation", @name="hostname", @subnet=#<Proxy::DHCP::Subnet:0x0055f8bff7e078 @network="10.0.0.0", @netmask="255.255.
248.0", @ipaddr=#<IPAddr: IPv4:10.0.0.0/255.255.248.0>, @options={:routers=>["10.0.0.1"], :domain_name_servers=>["10.
0.0.5", "10.0.0.3"], :ntp_servers=>[["ntp"]], :range=>["10.0.0.20", "10.0.6.255"]}, @m=#<Monitor:0x0055f8c0b5f7c0 @mo
n_owner=nil, @mon_count=0, @mon_mutex=#<Thread::Mutex:0x0055f8c0b5f770>>, @netmask_to_i=4294965248>, @ip="10.0.3.37",
 @mac="f2:f4:60:fd:db:c6", @options={:deleteable=>true, :hostname=>"hostname"}>

2020-03-16T20:32:54 47ccb30d [D] existing: [#<Proxy::DHCP::Reservation:0x0055f8c09fcb58 @type="reservation", @name="hostname", @subnet=#<Proxy::DHCP::Subnet:0x0055f8bff7e078 @network="10.0.0.0", @netmask="255.25
5.248.0", @ipaddr=#<IPAddr: IPv4:10.0.0.0/255.255.248.0>, @options={:routers=>["10.0.0.1"], :domain_name_servers=>["1
0.0.0.5", "10.0.0.3"], :ntp_servers=>[["ntp"]], :range=>["10.0.0.20", "10.0.6.255"]}, @m=#<Monitor:0x0055f8c0b5f7c0 @
mon_owner=nil, @mon_count=0, @mon_mutex=#<Thread::Mutex:0x0055f8c0b5f770>>, @netmask_to_i=4294965248>, @ip="10.0.3.37
", @mac="f2:f4:60:fd:db:c6", @options={:deleteable=>true, :hostname=>"hostname", :hardware_typ
e=>"ethernet"}>]

You can see that the records just differ by the option hardware_type of the the requested lease.
The one requested by the master: @options={:deleteable=>true, :hostname=>"hostname"}
The one found by the proxy: @options={:deleteable=>true, :hostname=>"hostname", :hardware_type=>"ethernet"}>]

This will cause the comparison, of the already existing record, to fail:

https://github.com/theforeman/smart-proxy/blob/02d8443fc6df43ff87b081ba2edf1aca9fe05a4b/modules/dhcp_common/server.rb#L153
153       unless similar_records.empty?

I found out that it uses the options provided directly from the API request sent by the master.

Or am I missing something? :frowning:

Thank you very much for you time and your help!

Best regards
Florian

Good analysis, I am puzzled why we see this now. This hasn’t changed for a long time. Anyway, here is a fix:

@ekohl

1 Like

Thank you very much for your time and your efforts!