Association of Subnet is not found for its subclasses

Hello Foreman community,

I’m developing a plugin for Foreman that ought to add fields for keeping BGP configuration settings per Subnet in the database. I dislike the idea of changing the subnets table, so I want to keep the new attributes in a separate table.

module SubnetBGPConfig < ActiveRecord::Base
  belongs_to :subnet, inverse_of: :subnet_bgp_config
end

module SubnetExtensions
  extend ActiveSupport::Concern

  included do
    has_one :subnet_bgp_config, inverse_of :subnet
    accepts_nested_attributes_for :subnet_bgp_config
  end
end

Now, whenever the Subnet form is rendered, an error occurs:

Association named ‘subnet_bgp_config’ was not found on Subnet::Ipv4; perhaps you misspelled it?

As far as I understand Rails’ single Table Inheritance (STI) this should work just fine, since…

This means that all behavior added to Vehicle is available for Car too, as associations, public methods, etc.

Can you tell what my problem is and/or how to solve it with Foreman (v3.5.1)?

I have tried to attr_exportable :subnet_bgp_config as well as the nested attributes, both failed.

Another option I thought of was…

module SubnetIPv4Extensions
  extend ActiveSupport::Concern
  included
    has_one :subnet_bgp_config, through: :subnet
  end
end

But that failed, because the IPv4 and IPv6 subclasses don’t have an association to Subnets, which makes sense on face value.

Including SubnetExtensions into the subclasses so far isn’t working out either.

I’d be grateful for any hints or tips you can spare. Thank you for your time,
Xavier.

Just as an experiment, I’ve edited the exact same code from above SubnetExtensions class into Foreman’s Subnet model source (app/models/subnet.rb) and the issue goes away:

# foreman-rake console (Rails 6.1.7)
irb(main):001:0> Subnet.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> true
irb(main):002:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> true
irb(main):003:0> Subnet::Ipv6.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> true

Therefor, this issue somehow is caused by using ActiveSupport::Concerns?!

Correction, the issue is not caused by ActiveSupport::Concern, but by underlying concept of mixins. I can tell, because avoiding Concern has no bearing on the missing association in the Subnet subclasses:

module SubnetExtensions
  def self.included (base)
    base.class_eval do
      has_one :subnet_bgp_config
    end
  end
end
# foreman-rake console (Rails 6.1.7)
irb(main):001:0> Subnet.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> true
irb(main):002:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> false

Funny thing is, the ForemanRemoteExecution plugin (v7.2.2) does almost the same: a SubnetExtensions module that extends ActiveSupport::Concern and adds two more associations to the Subnet base class. Both associations are visible to Subnet::Ipv4:

# foreman-rake console (Rails 6.1.7)
irb(main):001:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :target_remote_execution_proxies
=> true
irb(main):002:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :remote_execution_proxies
=> true

The only difference I see is, that in order to add many :remote_execution_proxies, it goes through the polymorphic association :target set up by TargetRemoteExecutionProxy, which in turn is a subclass to ApplicationRecord (instead of ActiveRecord::Base, like my SubnetBGPConfig).

I had tried a polymorphic variant, too, and failed. True, that wasn’t exactly like ForemanRemoteExecution. Or I made some other mistake. Or polymorphic associations don’t work with has_one (since the Rails documentation mentions only has_many). Anyway, I’ll give it a shot once more.

Maybe the crucial difference between my plugin and ForemanRemoteExecution isn’t how the associations are set up, but how the view is rendered:

  • I tried f.fields_for :subnet_bgp_config do ...
  • ForemanRemoteExecution does fields_for :subnet do ...

I so far have not understood why subnet_bgp_config does not work for me. Likewise, I don’t understand why :subnet works for ForemanRemoteExecution! After all, Subnet::Ipv4 has no :subnet association or (instance) method:

# foreman-rake console (Rails 6.1.7)
irb(main):001:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :subnet
=> false
irb(main):002:0> Subnet::Ipv4.instance_methods.include? :subnet
=> false
irb(main):003:0> Subnet::Ipv4.methods.include? :subnet
=> false

Sadly, neither the polymorphic association akin to ForemanRemoteExecution, nor the alternative rendering changed the fact, that Subnet::Ipv4 simply doesn’t know about the associations of Subnet.

If the thing you’re working on is going to be open source, could you please share a link to a repository with it? It would be easier to help you if we could see the whole thing without having to piece it together from the excerpts you posted here.

Thank you for offering some help @aruzicka! The plugin is not meant to be open source, but I very much understand that it would facilitate debugging the issue greatly if you had access to it. Therefor I’ve uploaded the current state of it to GitHub: foreman_subnets_with_bgp_config.

Even including SubnetExtensions in the console does not work (nor does it raise an error):

irb(main):001:0> Subnet::Ipv4.include ForemanSubnetsWithBGPConfig::SubnetExtensions
=> Subnet::Ipv4(id: integer, network: string, mask: string, priority: integer, name: text, vlanid: integer, created_at: datetime, updated_at: datetime, dhcp_id: integer, tftp_id: integer, gateway: string, dns_primary: string, dns_secondary: string, from: string, to: string, dns_id: integer, boot_mode: string, ipam: string, discovery_id: integer, type: string, description: text, mtu: integer, template_id: integer, httpboot_id: integer, netdb_id: integer, nic_delay: integer, externalipam_id: integer, externalipam_group: text, bmc_id: integer)
irb(main):002:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config
=> false

I made a couple more experiments and experienced as many surprises. That culminated into several changes since last time:

  • Avoid the all capitals acronym “BGP”, because Rails natively doesn’t retain it ("subnet_bgp_configs".classify => "SubnetBgpConfig")
  • Stop isolating the namespace for the plugin
  • The :subnet_bgp_config association is now added to all Subnet classes
  • Renamed the SubnetExtensions module to ExtendSubnetWithBgpConfig
    • Even though there were no errors about any naming conflict, I try to avoid the risk entirely by renaming my module
    • Sadly, that is unsuccessful and now the remote_execution_proxies association went missing for Subnet::Ipv4 and Subnet::Ipv6

Current state for Foreman: still broken, just a different error, which probably goes back again to broken inheritance.

irb(main):001:0> [Subnet, Subnet::Ipv4, Subnet::Ipv6].map { |s| s.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config }.all?
=> true
irb(main):002:0> ForemanSubnetsWithBgpConfig::SubnetBgpConfig.first
=> #<ForemanSubnetsWithBgpConfig::SubnetBgpConfig id: 1, subnet_id: 21, as_local: nil, as_remote: nil, ip_remote: nil, created_at: "2024-11-13 08:55:34.158310000 +0000", updated_at: "2024-11-13 08:55:34.158310000 +0000">
irb(main):003:0> ForemanSubnetsWithBgpConfig::SubnetBgpConfig.first.subnet
Traceback (most recent call last):
        2: from lib/tasks/console.rake:5:in 'block in <top (required)>'
        1: from (irb):4
NameError (uninitialized constant Subnet::Ipv4::SubnetBgpConfig)
irb(main):004:0> Subnet.find(21).subnet_bgp_config
Traceback (most recent call last):
        3: from lib/tasks/console.rake:5:in 'block in <top (required)>'
        2: from (irb):4
        1: from (irb):5:in 'rescue in irb_binding'
NameError (uninitialized constant Subnet::Ipv4::SubnetBgpConfig)
irb(main):005:0> Subnet::Ipv4.find(21).subnet_bgp_config
Traceback (most recent call last):
        2: from lib/tasks/console.rake:5:in 'block in <top (required)>'
        1: from (irb):10
NameError (uninitialized constant Subnet::Ipv4::SubnetBgpConfig)
irb(main):006:0> Subnet.reflect_on_all_associations.map(&:name).include? :remote_execution_proxies
=> true
irb(main):007:0> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :remote_execution_proxies
=> false

Here’s a branch with some fixes Commits · adamruzicka/foreman_subnets_with_bgp_config · GitHub . It doesn’t fix everything, but it should be enough to unstuck you.

You need to tell rails what class is on the other end of the association, see Fix undefined constant error · adamruzicka/foreman_subnets_with_bgp_config@26ede44 · GitHub

Including the module to all Subnet, Subnet::Ipv4 and Subnet::Ipv6 shouldn’t be necessary, adding it to just Subnet seems to work for me.

With these changes I was able to create a bgp config in the console, and then I was able to see its fields in the edit form of the subnet.

Iirc f.fields_for $thing and fields_for $thing don’t do the same thing. f.fields_for $thing roughly translates to “take the object for which we are rendering the form, find its $thing association and render the fields in the block for it”. Another thing to bear in mind is that if the association (or the object on the other end of the association) doesn’t exist, it doesn’t render the fields. For this reason, the bgp fields currently don’t show up when creating a subnet, but can show up if you create the subnet and then create its associated bgp config from rails console. If you hook into the subnet controller and build a blank bgp config there, it should work.

fields_for doesn’t really deal with associations, it just takes an arbitrary name.

Not sure what’s up with that, seems to work for me just fine on latest foreman

> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :remote_execution_proxies
=> true

Thank you very much for your help, @aruzicka! I’ll take a look at your fixes post-haste.

I thought I did that

And I was fully expecting it to work for me, too. :slightly_smiling_face: But maybe your proposed changes contain something that I have overlooked.

Yes, I anticipated that problem already, but didn’t get there yet. I imagined it needed to be handled in the view. Something like…

# app/overrides/subnets/_fields/add_bgp_config_fields.html.erb.deface
<% bgp_config = @subnet.subnet_bgp_config || @subnet.build_subnet_bgp_config -%>
<%= f.fields_for bgp_config do |b| -%>
  <%= render 'foreman_subnets_with_bgp_config/subnets/bgp_config', f: b %>
<% end -%>

Sadly, adopting all your fixes, @aruzicka, does not unstuck me. The problem remains just the same:

irb(main):001:0> [Subnet, Subnet::Ipv4, Subnet::Ipv6].map { |s| s.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config }
=> [true, false, false]

Did you have the remove execution plugin included in your test, too?

That seems to work for me just fine

[1] pry(main)> [Subnet, Subnet::Ipv4, Subnet::Ipv6].map { |s| s.reflect_on_all_associations.map(&:name).include? :subnet_bgp_config }
=> [true, true, true]
[2] pry(main)> Subnet::Ipv4.reflect_on_all_associations.map(&:name).include? :remote_execution_proxies
=> true

I did have the remote execution plugin enabled.

I used my dev env with foreman at tfm/foreman@e83f674, without packing it into a gem, installing that gem and then trying to use it as the script in bin suggests.

There have been changes to how we load and register things because of the zeitwerk changes, so if you’re testing this on an older foreman (engine.rb states minimal version as 3.3) what you have there may not work.

Indeed, I’m currently testing on Foreman 3.5.1 (shipped with RedHat Satellite 6.13), while the plugin likely has to be compatible with 3.3.0 (Satellite 6.12).

I’ll try running Foreman without the remote execution plugin. If the situation is still the same, then there simply is no conflict with it and our Foreman version does something that prevents proper loading of the plugin.

Removing the remote execution plugin is not an option for Satellite (specifically, the katello plugin for Foreman).

Guess that’s then the end of the line. I’ll need to discuss with my colleagues whether we postpone this plugin to a Foreman version where this issue is resolved, or whether the settings are put directly into the Subnets table. I’ve done some first experiments in that direction, too, and that seems at least possible.

Thanks again for your assistance, @aruzicka!

Hi !

To make your plugin code compatible with Red Hat Satellite 6.13, which is based on Foreman 2.5 (using older Rails versions), we’ll need to adjust your approach to ensure it works properly.

Updated Code:

Model File (subnet_bgp_config.rb):

class SubnetBGPConfig < ActiveRecord::Base
  self.table_name = 'subnet_bgp_configs' # Ensure the correct table name
  belongs_to :subnet, inverse_of: :subnet_bgp_config
end

Extension Module (subnet_extensions.rb):

module SubnetExtensions
  extend ActiveSupport::Concern

  included do
    has_one :subnet_bgp_config, class_name: 'SubnetBGPConfig', inverse_of: :subnet, dependent: :destroy
    accepts_nested_attributes_for :subnet_bgp_config
  end
end

Including Extensions for Subnet Subclasses:

# Explicitly include the module for both IPv4 and IPv6
Subnet::Ipv4.include(SubnetExtensions) if defined?(Subnet::Ipv4)
Subnet::Ipv6.include(SubnetExtensions) if defined?(Subnet::Ipv6)

Explanation of Changes:

  1. Compatibility with Older Rails:
  • Ensured compatibility with older Rails versions used in Satellite 6.13 by explicitly specifying class_name.
  • Included the module directly into the Subnet::Ipv4 and Subnet::Ipv6 subclasses since inheritance handling can differ.
  1. Testing:
  • Ensure that your changes are tested on a Satellite 6.13 dev environment to confirm they work as expected.
  • Restart the Foreman/Satellite services after deploying the plugin.

Note : Maybe it can help but not sure. Not tested.

Good luck !

Re,
It could help if the other not ok…

For Red Hat Satellite 6.13 compatibility, here’s the corrected code that works with the older Rails version:

# app/models/subnet_bgp_config.rb 
class SubnetBgpConfig < ActiveRecord::Base
  belongs_to :subnet, :class_name => 'Subnet'
end

# lib/engine.rb
module YourPlugin
  class Engine < ::Rails::Engine
    config.autoload_paths += Dir["#{config.root}/app/models/concerns"]
    
    initializer 'your_plugin.register_gettext', :after => :load_config_initializers do |app|
      add_to_autoload "#{YourPlugin::Engine.root}/app/models"
    end

    config.to_prepare do
      begin
        Subnet.send(:include, SubnetExtensions)
        ::Subnet::Ipv4.send(:include, SubnetExtensions)
        ::Subnet::Ipv6.send(:include, SubnetExtensions)
      rescue => e
        Rails.logger.warn "YourPlugin: skipping engine hook (#{e})"
      end
    end
  end
end

# app/models/concerns/subnet_extensions.rb
module SubnetExtensions
  extend ActiveSupport::Concern

  included do
    has_one :subnet_bgp_config, :dependent => :destroy
    accepts_nested_attributes_for :subnet_bgp_config
  end
end

This uses older Ruby/Rails syntax and proper engine initialization for Satellite 6.13’s environment.

Note : not tested

Regards,

Thank you @SGuser1,

on first glance, I don’t see that many things I have not tried before. But, since I had given up my approach last week and consequently deleted the branch for it, I can start over with just your suggested items. That will show whether maybe something else of my own code was responsible for the issues.

I’ll report back on my findings asap.