foreman_rh_cloud 13.2.8 → 13.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/insights_cloud/candlepin_proxies_extensions.rb +23 -0
  3. data/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb +9 -0
  4. data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +13 -10
  5. data/app/controllers/insights_cloud/ui_requests_controller.rb +3 -7
  6. data/app/models/concerns/rh_cloud_host.rb +4 -2
  7. data/app/models/insights_client_report_status.rb +9 -1
  8. data/app/models/inventory_sync/inventory_status.rb +16 -4
  9. data/lib/foreman_inventory_upload/generators/fact_helpers.rb +26 -4
  10. data/lib/foreman_inventory_upload.rb +8 -1
  11. data/lib/foreman_rh_cloud/engine.rb +1 -0
  12. data/lib/foreman_rh_cloud/version.rb +1 -1
  13. data/lib/foreman_rh_cloud.rb +36 -9
  14. data/lib/insights_cloud/async/insights_generate_notifications.rb +10 -1
  15. data/lib/inventory_sync/async/inventory_full_sync.rb +39 -3
  16. data/lib/inventory_sync/async/inventory_self_host_sync.rb +12 -2
  17. data/package.json +1 -1
  18. data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +56 -2
  19. data/test/controllers/insights_cloud/candlepin_proxies_extensions_test.rb +70 -0
  20. data/test/controllers/insights_cloud/ui_requests_controller_test.rb +16 -2
  21. data/test/jobs/insights_client_status_aging_test.rb +40 -0
  22. data/test/jobs/insights_generate_notifications_test.rb +26 -0
  23. data/test/jobs/inventory_full_sync_test.rb +212 -0
  24. data/test/jobs/inventory_self_host_sync_test.rb +9 -0
  25. data/test/models/insights_client_report_status_test.rb +109 -0
  26. data/test/models/inventory_sync/inventory_status_test.rb +85 -0
  27. data/test/unit/foreman_rh_cloud_self_host_test.rb +50 -2
  28. data/test/unit/metadata_generator_test.rb +24 -1
  29. data/test/unit/rh_cloud_host_test.rb +60 -0
  30. data/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js +24 -2
  31. data/webpack/CVEsHostDetailsTab/__tests__/CVEsHostDetailsTab.test.js +73 -10
  32. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/SyncButtonActions.js +8 -2
  33. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/__snapshots__/integrations.test.js.snap +1 -0
  34. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js +1 -0
  35. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/Toast.js +43 -17
  36. data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/__tests__/Toast.test.js +82 -0
  37. data/webpack/ForemanRhCloudHelpers.js +22 -0
  38. data/webpack/InsightsHostDetailsTab/NewHostDetailsTab.js +27 -1
  39. data/webpack/InsightsHostDetailsTab/__tests__/NewHostDetailsTab.test.js +134 -22
  40. metadata +16 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 04c6a1daea1675deb081565cf7d9e83e669a3fd01a577a032c9d7d5ba825cbce
4
- data.tar.gz: 5172a5c764d0dbcb71cf47e19e423f0eae01db27636be594320dece4c6364719
3
+ metadata.gz: 4301f698357e719f6ba5d08677bdeaf3ccefb995835dcf86f3e253cb0c01d599
4
+ data.tar.gz: 77f3bc1ef3e85cc1d8988a53f2cd5fdcd65b99a9fb6d6da1cc5f366bbfebc6d3
5
5
  SHA512:
6
- metadata.gz: 15b24817679825c5e60bc0e0ca0b33c18eadae7277483155c641f3d3d140bd911c0c7ad7dca15c40e3d63f9a8e354acfcd5789048658fd2888a33fa4b9741d0d
7
- data.tar.gz: 0cfe77f01b645de55c8e3e41ac4411201c5ec4819e5926d3054bac08b551e12dc923dffa6b1503956cb1075c688a5a3d94e5a63a8c4e332414e10f78054d3954
6
+ metadata.gz: 000fad5efc5a8bd42e03045e33bc10795a5d658aa3ef1098e6f95fa2a0476630cc85fe6b7b40929e817a4ca0d166fd88c8073b4faa972b72a84d0a146eb7aa70
7
+ data.tar.gz: 06fe93496e585831f593b8d31223577eb236adf7d1e4d90342cd203ddc9fbf9a80e9b58636b033fa04adefc806885feba8326a4e34dc38844bbaacd083df9c91
@@ -0,0 +1,23 @@
1
+ module InsightsCloud
2
+ module CandlepinProxiesExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # Update insights client status when hosts check in via subscription-manager facts upload
7
+ # rubocop:disable Rails/LexicallyScopedActionFilter
8
+ after_action :update_insights_client_status, only: [:facts]
9
+ # rubocop:enable Rails/LexicallyScopedActionFilter
10
+ end
11
+
12
+ def update_insights_client_status
13
+ # Update InsightsClientReportStatus whenever host checks in via subscription-manager
14
+ # This ensures USER_OMITTED status gets set even when insights-client isn't installed
15
+ # (parameter=false means insights-client won't be installed, so it won't hit MachineTelemetriesController)
16
+ hoststatus = @host.get_status(InsightsClientReportStatus)
17
+ Rails.logger.debug "Current status: #{hoststatus.to_label}"
18
+ @host.get_status(InsightsClientReportStatus).refresh!
19
+ @host.refresh_global_status!
20
+ Rails.logger.debug "New status: #{hoststatus.to_label}"
21
+ end
22
+ end
23
+ end
@@ -6,6 +6,7 @@ module InsightsCloud
6
6
  # This method explicitly listens on Katello actions
7
7
  # rubocop:disable Rails/LexicallyScopedActionFilter
8
8
  after_action :generate_host_report, only: [:upload_package_profile, :upload_profiles]
9
+ after_action :update_insights_client_status, only: [:upload_package_profile, :upload_profiles]
9
10
  # rubocop:enable Rails/LexicallyScopedActionFilter
10
11
  end
11
12
 
@@ -31,5 +32,13 @@ module InsightsCloud
31
32
  insights_facet = @host.build_insights(uuid: @host.subscription_facet.uuid)
32
33
  insights_facet.save
33
34
  end
35
+
36
+ def update_insights_client_status
37
+ # Update InsightsClientReportStatus whenever host checks in via subscription-manager
38
+ # This ensures USER_OMITTED status gets set even when insights-client isn't installed
39
+ # (parameter=false means insights-client won't be installed, so it won't hit MachineTelemetriesController)
40
+ @host.get_status(InsightsClientReportStatus).refresh!
41
+ @host.refresh_global_status!
42
+ end
34
43
  end
35
44
  end
@@ -32,14 +32,10 @@ module InsightsCloud::Api
32
32
  rescue RestClient::ExceptionWithResponse => e
33
33
  response_obj = e.response.presence || e.exception
34
34
  code = response_obj.try(:code) || response_obj.try(:http_code) || 500
35
- message = 'Cloud request failed'
36
-
37
- return render json: {
38
- :message => message,
39
- :error => response_obj.to_s,
40
- :headers => {},
41
- :response => response_obj,
42
- }, status: code
35
+ upstream_content_type = response_obj.try(:headers)&.[](:content_type)
36
+ content_type = upstream_content_type&.match?(/json/) ? upstream_content_type : 'application/json'
37
+
38
+ return render body: response_obj.to_s, status: code, content_type: content_type
43
39
  rescue StandardError => e
44
40
  # Catch any other exceptions here, such as Errno::ECONNREFUSED
45
41
  logger.warn("Cloud request failed with exception: #{e}")
@@ -88,9 +84,16 @@ module InsightsCloud::Api
88
84
  private
89
85
 
90
86
  def ensure_telemetry_enabled_for_consumer
91
- unless (config = telemetry_config(@host))
87
+ config = telemetry_config(@host)
88
+
89
+ # Always update the status based on current parameter value
90
+ # This will set USER_OMITTED if parameter is false, or REPORTING/NO_REPORT if true
91
+ @host.get_status(InsightsClientReportStatus).refresh!
92
+ @host.refresh_global_status!
93
+
94
+ unless config
92
95
  logger.debug("Rejected telemetry forwarding for host #{@host.name}, insights param is set to: #{config}")
93
- render_message 'Telemetry is not enabled for this host', :status => 403
96
+ return render_message('Telemetry is not enabled for this host', :status => 403)
94
97
  end
95
98
  config
96
99
  end
@@ -36,14 +36,10 @@ module InsightsCloud
36
36
  rescue RestClient::ExceptionWithResponse => e
37
37
  response_obj = e.response.presence || e.exception
38
38
  code = response_obj.try(:code) || response_obj.try(:http_code) || 500
39
- message = 'Cloud request failed'
39
+ upstream_content_type = response_obj.try(:headers)&.[](:content_type)
40
+ content_type = upstream_content_type&.match?(/json/) ? upstream_content_type : 'application/json'
40
41
 
41
- return render json: {
42
- :message => message,
43
- :error => response_obj.to_s,
44
- :headers => {},
45
- :response => response_obj,
46
- }, status: code
42
+ return render body: response_obj.to_s, status: code, content_type: content_type
47
43
  rescue StandardError => e
48
44
  # Catch any other exceptions here, such as Errno::ECONNREFUSED
49
45
  logger.warn("Cloud request failed with exception: #{e}")
@@ -15,12 +15,14 @@ module RhCloudHost
15
15
  has_one :insights_client_report_status_object, :class_name => '::InsightsClientReportStatus', :foreign_key => 'host_id'
16
16
  scoped_search :relation => :insights_client_report_status_object, :on => :status, :rename => :insights_client_report_status,
17
17
  :complete_value => { :reporting => ::InsightsClientReportStatus::REPORTING,
18
- :no_report => ::InsightsClientReportStatus::NO_REPORT }
18
+ :no_report => ::InsightsClientReportStatus::NO_REPORT,
19
+ :user_omitted => ::InsightsClientReportStatus::USER_OMITTED }
19
20
 
20
21
  has_one :inventory_sync_status_object, :class_name => '::InventorySync::InventoryStatus', :foreign_key => 'host_id'
21
22
  scoped_search :relation => :inventory_sync_status_object, :on => :status, :rename => :insights_inventory_sync_status,
22
23
  :complete_value => { :disconnect => ::InventorySync::InventoryStatus::DISCONNECT,
23
- :sync => ::InventorySync::InventoryStatus::SYNC }
24
+ :sync => ::InventorySync::InventoryStatus::SYNC,
25
+ :user_omitted => ::InventorySync::InventoryStatus::USER_OMITTED }
24
26
  scoped_search :on => :id, :rename => :insights_uuid, :only_explicit => true,
25
27
  :ext_method => :search_by_insights_uuid, :complete_value => false
26
28
 
@@ -3,8 +3,9 @@ class InsightsClientReportStatus < HostStatus::Status
3
3
 
4
4
  REPORTING = 0
5
5
  NO_REPORT = 1
6
+ USER_OMITTED = 2
6
7
 
7
- scope :stale, -> { where.not(reported_at: (Time.now - REPORT_INTERVAL)..Time.now) }
8
+ scope :stale, -> { where.not(status: USER_OMITTED).where.not(reported_at: (Time.now - REPORT_INTERVAL)..Time.now) }
8
9
  scope :reporting, -> { where(status: REPORTING) }
9
10
 
10
11
  def self.status_name
@@ -17,6 +18,8 @@ class InsightsClientReportStatus < HostStatus::Status
17
18
  N_('Reporting')
18
19
  when NO_REPORT
19
20
  N_('Not reporting')
21
+ when USER_OMITTED
22
+ N_('Not reporting because host_registration_insights parameter value is false')
20
23
  end
21
24
  end
22
25
 
@@ -26,10 +29,15 @@ class InsightsClientReportStatus < HostStatus::Status
26
29
  ::HostStatus::Global::OK
27
30
  when NO_REPORT
28
31
  ::HostStatus::Global::ERROR
32
+ when USER_OMITTED
33
+ ::HostStatus::Global::OK
29
34
  end
30
35
  end
31
36
 
32
37
  def to_status
38
+ excluded_by_host_param =
39
+ ::Foreman::Cast.to_bool(host.host_param('host_registration_insights')) == false
40
+ return USER_OMITTED if excluded_by_host_param
33
41
  in_interval? ? REPORTING : NO_REPORT
34
42
  end
35
43
 
@@ -2,6 +2,7 @@ module InventorySync
2
2
  class InventoryStatus < HostStatus::Status
3
3
  DISCONNECT = 0
4
4
  SYNC = 1
5
+ USER_OMITTED = 2
5
6
 
6
7
  def self.status_name
7
8
  N_('Inventory')
@@ -13,6 +14,8 @@ module InventorySync
13
14
  ::HostStatus::Global::WARN
14
15
  when SYNC
15
16
  ::HostStatus::Global::OK
17
+ when USER_OMITTED
18
+ ::HostStatus::Global::OK
16
19
  else
17
20
  ::HostStatus::Global::WARN
18
21
  end
@@ -21,16 +24,25 @@ module InventorySync
21
24
  def to_label
22
25
  case status
23
26
  when DISCONNECT
24
- N_('Host was not uploaded to your RH cloud inventory')
27
+ N_('Host is not present on console.redhat.com Inventory service')
25
28
  when SYNC
26
- N_('Successfully uploaded to your RH cloud inventory')
29
+ N_('Host is uploaded and present on console.redhat.com Inventory service')
30
+ when USER_OMITTED
31
+ N_('Host is excluded from upload to console.redhat.com Inventory service due to host parameter')
27
32
  end
28
33
  end
29
34
 
30
35
  def to_status(options = {})
31
- # this method used to calculate status.
32
- # Since the calculation is done externally we should return the previously calculated status
36
+ # Normally, this method is used to calculate status.
37
+ # In foreman_rh_cloud 'we do things a bit differently around here.'
38
+ # Calculation is done externally in InventorySync::Async::InventoryFullSync, so we simply return the previously calculated status.
33
39
  status
34
40
  end
41
+
42
+ def relevant?(_options = {})
43
+ # Inventory status is not relevant in IoP mode since we use single-host reports
44
+ # that don't sync with the cloud inventory service
45
+ !ForemanRhCloud.with_iop_smart_proxy?
46
+ end
35
47
  end
36
48
  end
@@ -114,6 +114,7 @@ module ForemanInventoryUpload
114
114
 
115
115
  def host_ips(host)
116
116
  # Determines and returns the IP addresses associated with a host, applying obfuscation if enabled.
117
+ return {} if host.nil?
117
118
 
118
119
  # If IP obfuscation is enabled for the host return a representation of obfuscated IP addresses.
119
120
  return obfuscated_ips(host) if obfuscate_ips?(host)
@@ -163,10 +164,29 @@ module ForemanInventoryUpload
163
164
 
164
165
  def hostname_match
165
166
  bash_hostname = `uname -n`.chomp
166
- foreman_hostname = ForemanRhCloud.foreman_host&.name
167
- if bash_hostname == foreman_hostname
168
- fqdn(ForemanRhCloud.foreman_host)
169
- elsif Setting[:obfuscate_inventory_hostnames]
167
+ foreman_host = ForemanRhCloud.foreman_host
168
+
169
+ # If bash hostname matches foreman_host, use fqdn
170
+ if foreman_host && bash_hostname == foreman_host.name
171
+ return fqdn(foreman_host)
172
+ end
173
+
174
+ # If no foreman_host, try foreman_host_name from Setting[:foreman_url]
175
+ unless foreman_host
176
+ foreman_hostname_from_setting = ForemanRhCloud.foreman_host_name
177
+ if foreman_hostname_from_setting
178
+ # Apply obfuscation if enabled
179
+ return obfuscate_fqdn(foreman_hostname_from_setting) if Setting[:obfuscate_inventory_hostnames]
180
+
181
+ return foreman_hostname_from_setting
182
+ end
183
+ # Otherwise fall through to bash_hostname below
184
+ # NOTE: Containerized foremanctl setups must configure Setting[:foreman_url]
185
+ # as bash hostname may not be available or meaningful in containers
186
+ end
187
+
188
+ # Fallback to bash hostname (with obfuscation if enabled)
189
+ if Setting[:obfuscate_inventory_hostnames]
170
190
  obfuscate_fqdn(bash_hostname)
171
191
  else
172
192
  bash_hostname
@@ -174,6 +194,8 @@ module ForemanInventoryUpload
174
194
  end
175
195
 
176
196
  def bios_uuid(host)
197
+ return nil if host.nil?
198
+
177
199
  value = fact_value(host, 'dmi::system::uuid') || ''
178
200
  uuid_value(value)
179
201
  end
@@ -96,7 +96,14 @@ module ForemanInventoryUpload
96
96
  end
97
97
 
98
98
  def self.inventory_self_url
99
- inventory_base_url + "?hostname_or_id=#{ForemanRhCloud.foreman_host.fqdn}"
99
+ host = ForemanRhCloud.foreman_host
100
+ hostname = host ? host.fqdn : ForemanRhCloud.foreman_host_name
101
+ if hostname.nil?
102
+ Rails.logger.warn("Cannot determine Foreman hostname for inventory sync. " \
103
+ "Please configure Setting[:foreman_url]. " \
104
+ "Containerized setups must explicitly set this.")
105
+ end
106
+ inventory_base_url + "?hostname_or_id=#{hostname}"
100
107
  end
101
108
 
102
109
  def self.host_by_id_url(host_uuid)
@@ -40,6 +40,7 @@ module ForemanRhCloud
40
40
  ::Host::Managed.include RhCloudHost
41
41
 
42
42
  ::Katello::Api::Rhsm::CandlepinDynflowProxyController.include InsightsCloud::PackageProfileUploadExtensions
43
+ ::Katello::Api::Rhsm::CandlepinProxiesController.include InsightsCloud::CandlepinProxiesExtensions
43
44
  ::Katello::RegistrationManager.singleton_class.prepend ::ForemanRhCloud::RegistrationManagerExtensions
44
45
  end
45
46
  end
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '13.2.8'.freeze
2
+ VERSION = '13.2.10'.freeze
3
3
  end
@@ -84,23 +84,50 @@ module ForemanRhCloud
84
84
 
85
85
  # For testing purposes we can override the default hostname with an environment variable SATELLITE_RH_CLOUD_FOREMAN_HOST
86
86
  def self.foreman_host
87
- @foreman_host ||= begin
88
- fullname = foreman_host_name
89
- ::Host.unscoped.friendly.find(fullname)
90
- rescue ActiveRecord::RecordNotFound
91
- # fullname didn't work. Let's try shortname
87
+ return @foreman_host if defined?(@foreman_host)
88
+
89
+ fullname = foreman_host_name
90
+ return @foreman_host = nil unless fullname
91
+
92
+ # Try fullname first
93
+ host = ::Host.unscoped.friendly.where(name: fullname).first
94
+
95
+ # If not found, try shortname
96
+ if host.nil?
92
97
  shortname = /(?<shortname>[^\.]*)\.?.*/.match(fullname)[:shortname]
93
- ::Host.unscoped.friendly.find(shortname)
98
+ host = ::Host.unscoped.friendly.where(name: shortname).first
94
99
  end
100
+
101
+ @foreman_host = host
95
102
  end
96
103
 
97
104
  def self.foreman_host_name
98
- ENV['SATELLITE_RH_CLOUD_FOREMAN_HOST'] || marked_foreman_host&.name || ::SmartProxy.default_capsule.name
105
+ ENV['SATELLITE_RH_CLOUD_FOREMAN_HOST'] || marked_foreman_host&.name || foreman_url_hostname
106
+ end
107
+
108
+ def self.foreman_url_hostname
109
+ return nil unless Setting[:foreman_url]
110
+
111
+ begin
112
+ # Ensure setting is a string to avoid TypeError from URI.parse
113
+ url = Setting[:foreman_url].to_s
114
+ URI.parse(url).host
115
+ rescue URI::InvalidURIError, ArgumentError, TypeError => e
116
+ Rails.logger.warn("Invalid foreman_url setting: #{e.message}")
117
+ nil
118
+ end
99
119
  end
100
120
 
101
121
  def self.marked_foreman_host
102
- ::Host.unscoped.search_for('infrastructure_facet.foreman = true').first
103
- rescue ScopedSearch::QueryNotSupported
122
+ # Find host with infrastructure_facet.foreman_instance = true
123
+ # Facets use a special mechanism in Foreman, so we query the facet table directly
124
+ return nil unless defined?(HostFacets::InfrastructureFacet)
125
+
126
+ facet = HostFacets::InfrastructureFacet.find_by(foreman_instance: true)
127
+ facet&.host
128
+ rescue ActiveRecord::StatementInvalid => e
129
+ # Table might not exist yet during migrations
130
+ Rails.logger.debug("Could not query marked foreman host: #{e.message}")
104
131
  nil
105
132
  end
106
133
 
@@ -13,7 +13,16 @@ module InsightsCloud
13
13
  end
14
14
 
15
15
  def add_satellite_notifications
16
- hits_count = InsightsHit.where(host_id: foreman_host.id).count
16
+ host = foreman_host
17
+
18
+ # Skip if no Foreman host record exists
19
+ unless host
20
+ logger.debug("Skipping Insights notifications: no Foreman host record found")
21
+ blueprint&.notifications&.destroy_all
22
+ return
23
+ end
24
+
25
+ hits_count = InsightsHit.where(host_id: host.id).count
17
26
 
18
27
  # Remove stale notifications
19
28
  blueprint.notifications.destroy_all
@@ -15,16 +15,26 @@ module InventorySync
15
15
 
16
16
  def setup_statuses
17
17
  @subscribed_hosts_ids = Set.new(affected_host_ids)
18
+ @omitted_ids = Set.new(user_omitted_host_ids)
19
+
20
+ # Remove user-omitted hosts from subscribed set. In normal operation, affected_host_ids
21
+ # already excludes user-omitted hosts via for_slice, but this handles edge cases like
22
+ # a host transitioning from uploaded to user-omitted between syncs.
23
+ @subscribed_hosts_ids.subtract(@omitted_ids)
18
24
 
19
25
  InventorySync::InventoryStatus.transaction do
20
26
  InventorySync::InventoryStatus.where(host_id: @subscribed_hosts_ids).delete_all
27
+ InventorySync::InventoryStatus.where(host_id: @omitted_ids).delete_all
21
28
  yield
22
- add_missing_hosts_statuses(@subscribed_hosts_ids)
29
+ add_missing_hosts_statuses(@subscribed_hosts_ids) # any remaining hosts after yield are disconnected
30
+ add_user_omitted_host_statuses(@omitted_ids)
23
31
  host_statuses[:disconnect] += @subscribed_hosts_ids.size
32
+ host_statuses[:user_omitted] += @omitted_ids.size
24
33
  end
25
34
 
26
- logger.debug("Synced hosts amount: #{host_statuses[:sync]}")
27
- logger.debug("Disconnected hosts amount: #{host_statuses[:disconnect]}")
35
+ logger.debug("Synced hosts count: #{host_statuses[:sync]}")
36
+ logger.debug("Disconnected hosts count: #{host_statuses[:disconnect]}")
37
+ logger.debug("User-omitted hosts count: #{host_statuses[:user_omitted]}")
28
38
  output[:host_statuses] = host_statuses
29
39
  end
30
40
 
@@ -44,6 +54,7 @@ module InventorySync
44
54
  private
45
55
 
46
56
  def update_hosts_status(status_hashes)
57
+ # create Inventory statuses
47
58
  InventorySync::InventoryStatus.create(status_hashes)
48
59
  updated_ids = status_hashes.map { |hash| hash[:host_id] }
49
60
  @subscribed_hosts_ids.subtract(updated_ids)
@@ -61,10 +72,23 @@ module InventorySync
61
72
  )
62
73
  end
63
74
 
75
+ def add_user_omitted_host_statuses(host_ids)
76
+ InventorySync::InventoryStatus.create(
77
+ host_ids.map do |host_id|
78
+ {
79
+ host_id: host_id,
80
+ status: InventorySync::InventoryStatus::USER_OMITTED,
81
+ reported_at: DateTime.current,
82
+ }
83
+ end
84
+ )
85
+ end
86
+
64
87
  def host_statuses
65
88
  @host_statuses ||= {
66
89
  sync: 0,
67
90
  disconnect: 0,
91
+ user_omitted: 0,
68
92
  }
69
93
  end
70
94
 
@@ -73,6 +97,18 @@ module InventorySync
73
97
  Host.unscoped.where(organization: organizations)
74
98
  ).pluck(:id)
75
99
  end
100
+
101
+ def user_omitted_host_ids
102
+ param_name = InsightsCloud.enable_client_param_inventory
103
+
104
+ # Use search_for to respect parameter inheritance (global, org, hostgroup, host)
105
+ # This matches the same logic used by for_slice, ensuring consistency
106
+ Host.unscoped
107
+ .where(organization: organizations)
108
+ .joins(:subscription_facet)
109
+ .search_for("params.#{param_name} = f")
110
+ .pluck(:id)
111
+ end
76
112
  end
77
113
  end
78
114
  end
@@ -4,7 +4,14 @@ module InventorySync
4
4
  set_callback :step, :around, :create_facets
5
5
 
6
6
  def plan
7
- super(ForemanRhCloud.foreman_host.organization)
7
+ host = ForemanRhCloud.foreman_host
8
+
9
+ if host.nil?
10
+ logger.warn("Skipping self-host inventory sync: no Foreman host record found.")
11
+ return
12
+ end
13
+
14
+ super(host.organization)
8
15
  end
9
16
 
10
17
  def create_facets
@@ -22,7 +29,10 @@ module InventorySync
22
29
  private
23
30
 
24
31
  def add_missing_insights_facet(uuids_hash)
25
- facet = InsightsFacet.find_or_create_by(host_id: ForemanRhCloud.foreman_host.id) do |facet|
32
+ host = ForemanRhCloud.foreman_host
33
+ return unless host # Guard against nil
34
+
35
+ facet = InsightsFacet.find_or_create_by(host_id: host.id) do |facet|
26
36
  facet.uuid = uuids_hash.values.first
27
37
  end
28
38
 
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "13.2.7",
3
+ "version": "13.2.10",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -4,6 +4,7 @@ require 'rest-client'
4
4
  module InsightsCloud::Api
5
5
  class MachineTelemetriesControllerTest < ActionController::TestCase
6
6
  include KatelloCVEHelper
7
+ include CandlepinIsolation
7
8
 
8
9
  setup do
9
10
  FactoryBot.create(:common_parameter, name: InsightsCloud.enable_client_param, key_type: 'boolean', value: true)
@@ -153,8 +154,22 @@ module InsightsCloud::Api
153
154
 
154
155
  get :forward_request, params: { "path" => "platform/module-update-router/v1/channel" }
155
156
  assert_equal 500, @response.status
156
- assert_equal 'Cloud request failed', JSON.parse(@response.body)['message']
157
- assert_match /#{@body}/, JSON.parse(@response.body)['response']
157
+ assert_equal @body, @response.body
158
+ end
159
+
160
+ test "should forward JSON error responses without double-escaping" do
161
+ json_error = { errors: [{ detail: 'inventory_id must exist', status: '404' }] }.to_json
162
+ net_http_resp = Net::HTTPResponse.new(1.0, 404, "Not Found")
163
+ net_http_resp['content-type'] = 'application/json'
164
+ res = RestClient::Response.create(json_error, net_http_resp, @http_req)
165
+ ::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:execute_cloud_request).raises(RestClient::NotFound.new(res))
166
+
167
+ get :forward_request, params: { "path" => "api/vulnerability/v1/systems/00000000-0000-0000-0000-000000000000" }
168
+ assert_equal 404, @response.status
169
+ assert_includes @response.content_type, 'application/json'
170
+ assert_equal json_error, @response.body
171
+ parsed = JSON.parse(@response.body)
172
+ assert_equal 'inventory_id must exist', parsed['errors'][0]['detail']
158
173
  end
159
174
 
160
175
  test "should create insights facet" do
@@ -170,6 +185,45 @@ module InsightsCloud::Api
170
185
 
171
186
  assert_not_nil InsightsFacet.find_by(host_id: @host.id)
172
187
  end
188
+
189
+ test "should update InsightsClientReportStatus when parameter is false and block request" do
190
+ # Remove the common parameter from setup and set it to false
191
+ CommonParameter.where(name: InsightsCloud.enable_client_param).delete_all
192
+ FactoryBot.create(:common_parameter, name: InsightsCloud.enable_client_param, key_type: 'boolean', value: false)
193
+
194
+ # Stub telemetry_config to return false (disabled)
195
+ InsightsCloud::Api::MachineTelemetriesController.any_instance.stubs(:telemetry_config).returns(false)
196
+
197
+ get :forward_request, params: { "path" => "platform/ingress/v1/upload" }
198
+
199
+ # Request should be blocked
200
+ assert_response 403
201
+ assert_equal 'Telemetry is not enabled for this host', JSON.parse(@response.body)['message']
202
+
203
+ # But status should be updated to USER_OMITTED
204
+ @host.reload
205
+ insights_status = @host.get_status(InsightsClientReportStatus)
206
+ assert insights_status.persisted?, 'InsightsClientReportStatus should be created'
207
+ assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status
208
+ end
209
+
210
+ test "should update InsightsClientReportStatus on successful upload" do
211
+ # Stub telemetry_config to return true (enabled) - common param from setup works for this
212
+ InsightsCloud::Api::MachineTelemetriesController.any_instance.stubs(:telemetry_config).returns(true)
213
+
214
+ net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
215
+ res = RestClient::Response.create('response body', net_http_resp, @http_req)
216
+ ::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:forward_request).returns(res)
217
+
218
+ get :forward_request, params: { "path" => "/redhat_access/r/insights/uploads/" }
219
+
220
+ assert_response :success
221
+
222
+ # Status should have been refreshed during the request
223
+ @host.reload
224
+ insights_status = @host.get_status(InsightsClientReportStatus)
225
+ assert insights_status.persisted?, 'InsightsClientReportStatus should be created and persisted'
226
+ end
173
227
  end
174
228
 
175
229
  context '#branch_info' do
@@ -0,0 +1,70 @@
1
+ require 'test_plugin_helper'
2
+
3
+ module InsightsCloud
4
+ class CandlepinProxiesExtensionsTest < ActiveSupport::TestCase
5
+ setup do
6
+ @organization = FactoryBot.create(:organization)
7
+ @host = FactoryBot.create(:host, :with_subscription, :managed, organization: @organization)
8
+ end
9
+
10
+ test 'updates InsightsClientReportStatus to USER_OMITTED when parameter is false' do
11
+ # Set parameter to false so status should be USER_OMITTED
12
+ FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: false)
13
+
14
+ # Simulate what the callback does
15
+ @host.get_status(InsightsClientReportStatus).refresh!
16
+ @host.refresh_global_status!
17
+
18
+ # Verify the status was updated
19
+ @host.reload
20
+ insights_status = @host.get_status(InsightsClientReportStatus)
21
+ assert insights_status.persisted?, 'InsightsClientReportStatus should be persisted'
22
+ assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status,
23
+ 'Status should be USER_OMITTED when host_registration_insights=false'
24
+ end
25
+
26
+ test 'sets USER_OMITTED status when parameter is inherited from hostgroup' do
27
+ # Create hostgroup with parameter
28
+ hostgroup = FactoryBot.create(:hostgroup)
29
+ hostgroup.group_parameters << GroupParameter.create(
30
+ name: 'host_registration_insights',
31
+ value: 'false',
32
+ key_type: 'boolean'
33
+ )
34
+ hostgroup.save!
35
+
36
+ @host.hostgroup = hostgroup
37
+ @host.save!
38
+
39
+ # Simulate what the callback does
40
+ @host.get_status(InsightsClientReportStatus).refresh!
41
+ @host.refresh_global_status!
42
+
43
+ # Verify status respects inherited parameter
44
+ @host.reload
45
+ insights_status = @host.get_status(InsightsClientReportStatus)
46
+ assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status,
47
+ 'Status should be USER_OMITTED when host_registration_insights=false is inherited from hostgroup'
48
+ end
49
+
50
+ test 'refreshes global status' do
51
+ FactoryBot.create(:common_parameter, name: 'host_registration_insights', key_type: 'boolean', value: true)
52
+
53
+ # Simulate what the callback does
54
+ @host.get_status(InsightsClientReportStatus).refresh!
55
+ @host.refresh_global_status!
56
+
57
+ # Verify global status was updated
58
+ @host.reload
59
+ assert_not_nil @host.global_status, 'Global status should be set'
60
+ end
61
+
62
+ test 'CandlepinProxiesController includes the update_insights_client_status method' do
63
+ # Verify the concern is included and method is available
64
+ controller = Katello::Api::Rhsm::CandlepinProxiesController.new
65
+
66
+ assert_respond_to controller, :update_insights_client_status,
67
+ 'CandlepinProxiesController should have update_insights_client_status method from concern'
68
+ end
69
+ end
70
+ end