Skip to content

Commit ac37634

Browse files
jeremylenzclaude
andauthored
Fix IoP host UUID data consistency issues (#1131)
* Fix IoP host UUID data consistency issues In IoP mode, hosts previously registered to Red Hat cloud could display stale insights facet UUIDs instead of their current subscription manager UUIDs. This caused data inconsistencies when viewing host details, recommendations, and during unregistration. Changes: - Add insights_uuid method that returns correct UUID based on mode - Add ensure_iop_insights_uuid to sync mismatched UUIDs - Update all call sites to use insights_uuid method - Add comprehensive test coverage (14 new tests) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]> * Address code review comments - Add comment documenting ensure_iop_insights_uuid safety - Add subscription_facet assertions to factory traits - Fix guard in ConnectorPlaybookExecutionReporterTask to use insights_uuid - Add IoP mode test for advisor_engine_controller 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]> * Fix rubocop: add trailing comma 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]> * Add insights facet preloading to avoid N+1 queries Preload the insights facet when fetching hosts in the API to prevent N+1 database queries when rendering host lists with facet data. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]> * Address nofar comments * remove tests no longer needed --------- Co-authored-by: Claude Sonnet 4.5 <[email protected]>
1 parent 943d3df commit ac37634

File tree

11 files changed

+283
-53
lines changed

11 files changed

+283
-53
lines changed

app/controllers/concerns/foreman_rh_cloud/registration_manager_extensions.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ def unregister_host(host, options = {})
1313
host.reload
1414

1515
# Only delete from HBI in IoP mode (hosted mode uses async job for cleanup)
16-
hbi_host_destroy(host) if ForemanRhCloud.with_iop_smart_proxy? && !organization_destroy && host.insights_facet&.uuid&.presence
16+
hbi_host_destroy(host) if ForemanRhCloud.with_iop_smart_proxy? && !organization_destroy && host.insights_uuid.presence
1717
host.insights&.destroy!
1818
super(host, options)
1919
end
2020

2121
def hbi_host_destroy(host)
22-
uuid = host.insights_facet.uuid
22+
uuid = host.insights_uuid
2323
logger.debug "Unregistering host #{uuid} from HBI"
2424
execute_cloud_request(
2525
organization: host.organization,

app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ def generate_host_report
2121
@host.id
2222
)
2323

24+
# Ensure insights UUID matches subscription UUID (only runs in IoP mode per method guard above)
25+
@host.ensure_iop_insights_uuid
26+
2427
# in IoP case, the hosts are identified by the sub-man ID, and we can assume they already
2528
# exist in the local inventory. This will also handle facet creation for new hosts.
2629
return if @host.insights

app/models/concerns/rh_cloud_host.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,19 @@ module RhCloudHost
2626
def insights_facet
2727
insights
2828
end
29+
30+
# In IoP, read directly from the subscription facet to avoid stale data (see comment on ensure_iop_insights_uuid)
31+
def insights_uuid
32+
ForemanRhCloud.with_iop_smart_proxy? ? subscription_facet&.uuid : insights_facet&.uuid
33+
end
34+
35+
# In non-IoP, insights_facet uuids are assigned by Hosted.
36+
# In IoP, insights_facet uuids must match Katello subscription_facet uuids.
37+
# If the host was previously registered to hosted Insights,
38+
# we need to correct its uuid.
39+
def ensure_iop_insights_uuid
40+
return unless insights_facet.present? && subscription_facet.present? && insights_facet.uuid != subscription_facet.uuid
41+
insights_facet.update!(uuid: subscription_facet.uuid)
42+
end
2943
end
3044
end
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
collection @hosts
22

33
attributes :name
4-
node :insights_uuid do |host|
5-
host.insights_facet&.uuid
6-
end
4+
node :insights_uuid, &:insights_uuid
75
node :insights_hit_details do |host|
86
host&.facts('insights::hit_details')&.values&.first
97
end

app/views/api/v2/hosts/insights/base.rabl

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
attributes :uuid
2-
1+
node :uuid do |facet|
2+
facet&.host&.insights_uuid
3+
end
34
node :insights_hit_details do |facet|
45
facet&.host&.facts('insights::hit_details')&.values&.first
56
end

lib/foreman_rh_cloud/plugin.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ def self.register
118118
end
119119
end
120120

121+
# Preload insights facet to avoid N+1 queries when rendering host list with facets
122+
add_controller_action_scope('Api::V2::HostsController', :index) do |base_scope|
123+
base_scope.preload(:insights)
124+
end
125+
121126
register_global_js_file 'global'
122127

123128
register_custom_status InventorySync::InventoryStatus

lib/insights_cloud/async/connector_playbook_execution_reporter_task.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,10 @@ def report_url
126126

127127
def invocation_status
128128
hosts_state = Hash[job_invocation.targeting.hosts.map do |host|
129-
next unless host.insights&.uuid
129+
next unless host.insights_uuid
130130
[
131-
host.insights.uuid,
132-
task_status(job_invocation.sub_task_for_host(host), host.insights.uuid),
131+
host.insights_uuid,
132+
task_status(job_invocation.sub_task_for_host(host), host.insights_uuid),
133133
]
134134
end.compact]
135135

test/controllers/insights_cloud/api/advisor_engine_controller_test.rb

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,41 @@ class AdvisorEngineControllerTest < ActionController::TestCase
1313
end
1414

1515
test 'shows hosts with uuids' do
16-
uuids = [@host1.insights.uuid, @host2.insights.uuid]
16+
uuids = [@host1.insights_uuid, @host2.insights_uuid]
1717
get :host_details, params: { organization_id: @test_org.id, host_uuids: uuids }
1818
assert_response :success
1919
assert_template 'api/v2/advisor_engine/host_details'
2020
assert_equal @test_org.hosts.joins(:insights).where(:insights => { :uuid => uuids }).count, assigns(:hosts).count
2121
refute_equal @test_org.hosts.count, assigns(:hosts).count
2222
end
2323

24+
test 'in IoP mode host_details uses subscription uuid when insights uuid is stale' do
25+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
26+
27+
stale_insights_uuid = 'stale-insights-uuid-123'
28+
subscription_uuid = 'subscription-uuid-456'
29+
30+
# Create host with diverging facet UUIDs
31+
host = FactoryBot.create(:host, :with_subscription, organization: @test_org)
32+
host.subscription_facet.update!(uuid: subscription_uuid)
33+
host.insights = FactoryBot.create(:insights_facet, host_id: host.id, uuid: stale_insights_uuid)
34+
host.save!
35+
36+
# Query using the stale insights UUID
37+
get :host_details, params: {
38+
organization_id: @test_org.id,
39+
host_uuids: [stale_insights_uuid],
40+
}
41+
42+
assert_response :success
43+
body = JSON.parse(response.body)
44+
45+
# Should return the subscription UUID, not the stale insights UUID
46+
insights_uuids = body.map { |h| h['insights_uuid'] }
47+
assert_includes insights_uuids, subscription_uuid, "Should use subscription UUID in IoP mode"
48+
refute_includes insights_uuids, stale_insights_uuid, "Should not use stale insights UUID"
49+
end
50+
2451
test 'shows error when no hosts found' do
2552
get :host_details, params: { organization_id: @test_org.id, host_uuids: ['nonexistentuuid'] }
2653
assert_response :not_found

test/factories/insights_factories.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,5 +58,34 @@
5858
host.insights = FactoryBot.create(:insights_facet, :with_hits, host_id: host.id)
5959
end
6060
end
61+
62+
trait :with_mismatched_insights_uuid do
63+
# Simulates host previously registered to cloud with stale UUID
64+
# Requires :with_subscription trait to be used together
65+
after(:create) do |host, _evaluator|
66+
raise "subscription_facet required for :with_mismatched_insights_uuid trait" unless host.subscription_facet&.uuid
67+
68+
host.insights = FactoryBot.create(
69+
:insights_facet,
70+
host_id: host.id,
71+
uuid: 'stale-cloud-uuid' # Different from subscription_facet
72+
)
73+
end
74+
end
75+
76+
trait :with_synced_insights_uuid do
77+
# Ideal state: insights_facet UUID matches subscription_facet UUID
78+
# Requires :with_subscription trait to be used together
79+
after(:create) do |host, _evaluator|
80+
raise "subscription_facet required for :with_synced_insights_uuid trait" unless host.subscription_facet&.uuid
81+
82+
subscription_uuid = host.subscription_facet.uuid
83+
host.insights = FactoryBot.create(
84+
:insights_facet,
85+
host_id: host.id,
86+
uuid: subscription_uuid # Same as subscription_facet
87+
)
88+
end
89+
end
6190
end
6291
end

test/unit/lib/foreman_rh_cloud/registration_manager_extensions_test.rb

Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ module ForemanRhCloud
44
class RegistrationManagerExtensionsTest < ActiveSupport::TestCase
55
setup do
66
@org = FactoryBot.create(:organization)
7-
@host = FactoryBot.create(:host, :managed, organization: @org)
8-
@insights_facet = ::InsightsFacet.create!(host: @host, uuid: 'test-uuid-123')
7+
@host = FactoryBot.create(:host, :managed, :with_subscription, organization: @org)
8+
@insights_facet = ::InsightsFacet.create!(host: @host, uuid: @host.subscription_facet.uuid)
99

1010
# Stub Candlepin interaction (from Katello)
1111
::Katello::Resources::Candlepin::Consumer.stubs(:destroy)
@@ -15,9 +15,9 @@ class RegistrationManagerExtensionsTest < ActiveSupport::TestCase
1515
end
1616

1717
context 'unregister_host' do
18-
test 'should call HBI delete in IoP mode when host has insights facet with UUID' do
18+
test 'should call HBI delete in IoP mode when host has insights UUID' do
1919
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
20-
expected_url = ForemanInventoryUpload.host_by_id_url('test-uuid-123')
20+
expected_url = ForemanInventoryUpload.host_by_id_url(@host.subscription_facet.uuid)
2121

2222
# Expect the cloud request to be made
2323
Katello::RegistrationManager.expects(:execute_cloud_request).with do |params|
@@ -45,44 +45,6 @@ class RegistrationManagerExtensionsTest < ActiveSupport::TestCase
4545
assert_nil InsightsFacet.find_by(id: @insights_facet.id)
4646
end
4747

48-
test 'should NOT call HBI delete when host has no insights_facet' do
49-
host_without_facet = FactoryBot.create(:host, :managed, organization: @org)
50-
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
51-
52-
Katello::RegistrationManager.expects(:execute_cloud_request).never
53-
54-
assert_nothing_raised do
55-
Katello::RegistrationManager.unregister_host(host_without_facet, unregistering: true)
56-
end
57-
end
58-
59-
test 'should NOT call HBI delete when host has insights_facet with empty UUID' do
60-
host_with_empty_uuid_facet = FactoryBot.create(:host, :managed, organization: @org)
61-
empty_uuid_facet = InsightsFacet.create!(host: host_with_empty_uuid_facet, uuid: '')
62-
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
63-
64-
Katello::RegistrationManager.expects(:execute_cloud_request).never
65-
66-
assert_nothing_raised do
67-
Katello::RegistrationManager.unregister_host(host_with_empty_uuid_facet, unregistering: true)
68-
end
69-
70-
assert_nil InsightsFacet.find_by(id: empty_uuid_facet.id)
71-
end
72-
73-
test 'should NOT call HBI delete when insights_facet has no UUID' do
74-
facet_id = @insights_facet.id
75-
@insights_facet.update(uuid: nil)
76-
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
77-
78-
Katello::RegistrationManager.expects(:execute_cloud_request).never
79-
80-
Katello::RegistrationManager.unregister_host(@host, unregistering: true)
81-
82-
# Verify facet was still destroyed
83-
assert_nil InsightsFacet.find_by(id: facet_id)
84-
end
85-
8648
test 'should always destroy insights_facet regardless of IoP mode' do
8749
facet_id = @insights_facet.id
8850
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)

0 commit comments

Comments
 (0)