foreman_rh_cloud 13.2.8 → 13.2.9
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.
- checksums.yaml +4 -4
- data/app/controllers/concerns/insights_cloud/candlepin_proxies_extensions.rb +23 -0
- data/app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb +9 -0
- data/app/controllers/insights_cloud/api/machine_telemetries_controller.rb +9 -2
- data/app/models/concerns/rh_cloud_host.rb +4 -2
- data/app/models/insights_client_report_status.rb +9 -1
- data/app/models/inventory_sync/inventory_status.rb +16 -4
- data/lib/foreman_rh_cloud/engine.rb +1 -0
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/lib/inventory_sync/async/inventory_full_sync.rb +39 -3
- data/package.json +1 -1
- data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +40 -0
- data/test/controllers/insights_cloud/candlepin_proxies_extensions_test.rb +70 -0
- data/test/jobs/insights_client_status_aging_test.rb +40 -0
- data/test/jobs/inventory_full_sync_test.rb +212 -0
- data/test/models/insights_client_report_status_test.rb +109 -0
- data/test/models/inventory_sync/inventory_status_test.rb +85 -0
- data/test/unit/rh_cloud_host_test.rb +60 -0
- data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/SyncButtonActions.js +8 -2
- data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/__snapshots__/integrations.test.js.snap +1 -0
- data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js +1 -0
- data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/Toast.js +43 -17
- data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/__tests__/Toast.test.js +82 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8ea9a50897e8e89678229d4357182f5cf0f487e1c7775048c7e6e363d9eb0b68
|
|
4
|
+
data.tar.gz: 574992cb545615924d46aec3cb5f1856abaa4c287bd0ddfde4af1b23fc7c8ca0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 92bf788426b36beda79e9444141f93cbe1ac00b5c51cb0c28717d23dec6c220548c0cf4d9bdebe30e8cfb3a5f27922aad3b447f5344c1226a44de1f2482e4279
|
|
7
|
+
data.tar.gz: ed6b64ed049ea2ed4948d22a7160f5953d64cd2230353636e0075ead94331bae1d6aa9b805d64bff10b2ce5a27339bc78b3564ea3d535db1d0ed61cb3fd0e420
|
|
@@ -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
|
|
@@ -88,9 +88,16 @@ module InsightsCloud::Api
|
|
|
88
88
|
private
|
|
89
89
|
|
|
90
90
|
def ensure_telemetry_enabled_for_consumer
|
|
91
|
-
|
|
91
|
+
config = telemetry_config(@host)
|
|
92
|
+
|
|
93
|
+
# Always update the status based on current parameter value
|
|
94
|
+
# This will set USER_OMITTED if parameter is false, or REPORTING/NO_REPORT if true
|
|
95
|
+
@host.get_status(InsightsClientReportStatus).refresh!
|
|
96
|
+
@host.refresh_global_status!
|
|
97
|
+
|
|
98
|
+
unless config
|
|
92
99
|
logger.debug("Rejected telemetry forwarding for host #{@host.name}, insights param is set to: #{config}")
|
|
93
|
-
render_message
|
|
100
|
+
return render_message('Telemetry is not enabled for this host', :status => 403)
|
|
94
101
|
end
|
|
95
102
|
config
|
|
96
103
|
end
|
|
@@ -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
|
|
27
|
+
N_('Host is not present on console.redhat.com Inventory service')
|
|
25
28
|
when SYNC
|
|
26
|
-
N_('
|
|
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
|
-
#
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
27
|
-
logger.debug("Disconnected hosts
|
|
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
|
data/package.json
CHANGED
|
@@ -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)
|
|
@@ -170,6 +171,45 @@ module InsightsCloud::Api
|
|
|
170
171
|
|
|
171
172
|
assert_not_nil InsightsFacet.find_by(host_id: @host.id)
|
|
172
173
|
end
|
|
174
|
+
|
|
175
|
+
test "should update InsightsClientReportStatus when parameter is false and block request" do
|
|
176
|
+
# Remove the common parameter from setup and set it to false
|
|
177
|
+
CommonParameter.where(name: InsightsCloud.enable_client_param).delete_all
|
|
178
|
+
FactoryBot.create(:common_parameter, name: InsightsCloud.enable_client_param, key_type: 'boolean', value: false)
|
|
179
|
+
|
|
180
|
+
# Stub telemetry_config to return false (disabled)
|
|
181
|
+
InsightsCloud::Api::MachineTelemetriesController.any_instance.stubs(:telemetry_config).returns(false)
|
|
182
|
+
|
|
183
|
+
get :forward_request, params: { "path" => "platform/ingress/v1/upload" }
|
|
184
|
+
|
|
185
|
+
# Request should be blocked
|
|
186
|
+
assert_response 403
|
|
187
|
+
assert_equal 'Telemetry is not enabled for this host', JSON.parse(@response.body)['message']
|
|
188
|
+
|
|
189
|
+
# But status should be updated to USER_OMITTED
|
|
190
|
+
@host.reload
|
|
191
|
+
insights_status = @host.get_status(InsightsClientReportStatus)
|
|
192
|
+
assert insights_status.persisted?, 'InsightsClientReportStatus should be created'
|
|
193
|
+
assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
test "should update InsightsClientReportStatus on successful upload" do
|
|
197
|
+
# Stub telemetry_config to return true (enabled) - common param from setup works for this
|
|
198
|
+
InsightsCloud::Api::MachineTelemetriesController.any_instance.stubs(:telemetry_config).returns(true)
|
|
199
|
+
|
|
200
|
+
net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
|
|
201
|
+
res = RestClient::Response.create('response body', net_http_resp, @http_req)
|
|
202
|
+
::ForemanRhCloud::CloudRequestForwarder.any_instance.stubs(:forward_request).returns(res)
|
|
203
|
+
|
|
204
|
+
get :forward_request, params: { "path" => "/redhat_access/r/insights/uploads/" }
|
|
205
|
+
|
|
206
|
+
assert_response :success
|
|
207
|
+
|
|
208
|
+
# Status should have been refreshed during the request
|
|
209
|
+
@host.reload
|
|
210
|
+
insights_status = @host.get_status(InsightsClientReportStatus)
|
|
211
|
+
assert insights_status.persisted?, 'InsightsClientReportStatus should be created and persisted'
|
|
212
|
+
end
|
|
173
213
|
end
|
|
174
214
|
|
|
175
215
|
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
|
|
@@ -31,4 +31,44 @@ class InsightsClientStatusAgingTest < ActiveSupport::TestCase
|
|
|
31
31
|
assert_equal InsightsClientReportStatus::NO_REPORT, @host3.get_status(InsightsClientReportStatus).status
|
|
32
32
|
assert_equal InsightsClientReportStatus::NO_REPORT, @host4.get_status(InsightsClientReportStatus).status
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
test 'aging job does not affect USER_OMITTED hosts' do
|
|
36
|
+
# Host 1: USER_OMITTED with old reported_at (should stay USER_OMITTED)
|
|
37
|
+
InsightsClientReportStatus.find_or_initialize_by(host_id: @host1.id).update(
|
|
38
|
+
status: InsightsClientReportStatus::USER_OMITTED,
|
|
39
|
+
reported_at: Time.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Host 2: REPORTING with old reported_at (should change to NO_REPORT)
|
|
43
|
+
InsightsClientReportStatus.find_or_initialize_by(host_id: @host2.id).update(
|
|
44
|
+
status: InsightsClientReportStatus::REPORTING,
|
|
45
|
+
reported_at: Time.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Host 3: USER_OMITTED with recent reported_at (should stay USER_OMITTED)
|
|
49
|
+
InsightsClientReportStatus.find_or_initialize_by(host_id: @host3.id).update(
|
|
50
|
+
status: InsightsClientReportStatus::USER_OMITTED,
|
|
51
|
+
reported_at: Time.now - 1.day
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Host 4: REPORTING with recent reported_at (should stay REPORTING)
|
|
55
|
+
InsightsClientReportStatus.find_or_initialize_by(host_id: @host4.id).update(
|
|
56
|
+
status: InsightsClientReportStatus::REPORTING,
|
|
57
|
+
reported_at: Time.now - 1.day
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
action = create_and_plan_action(InsightsCloud::Async::InsightsClientStatusAging)
|
|
61
|
+
run_action(action)
|
|
62
|
+
|
|
63
|
+
@hosts.each(&:reload)
|
|
64
|
+
|
|
65
|
+
assert_equal InsightsClientReportStatus::USER_OMITTED, @host1.get_status(InsightsClientReportStatus).status,
|
|
66
|
+
'USER_OMITTED host with old report should stay USER_OMITTED'
|
|
67
|
+
assert_equal InsightsClientReportStatus::NO_REPORT, @host2.get_status(InsightsClientReportStatus).status,
|
|
68
|
+
'REPORTING host with old report should change to NO_REPORT'
|
|
69
|
+
assert_equal InsightsClientReportStatus::USER_OMITTED, @host3.get_status(InsightsClientReportStatus).status,
|
|
70
|
+
'USER_OMITTED host with recent report should stay USER_OMITTED'
|
|
71
|
+
assert_equal InsightsClientReportStatus::REPORTING, @host4.get_status(InsightsClientReportStatus).status,
|
|
72
|
+
'REPORTING host with recent report should stay REPORTING'
|
|
73
|
+
end
|
|
34
74
|
end
|
|
@@ -5,6 +5,7 @@ class InventoryFullSyncTest < ActiveSupport::TestCase
|
|
|
5
5
|
include Dynflow::Testing::Factories
|
|
6
6
|
include MockCerts
|
|
7
7
|
include KatelloCVEHelper
|
|
8
|
+
include CandlepinIsolation
|
|
8
9
|
|
|
9
10
|
setup do
|
|
10
11
|
User.current = User.find_by(login: 'secret_admin')
|
|
@@ -313,4 +314,215 @@ class InventoryFullSyncTest < ActiveSupport::TestCase
|
|
|
313
314
|
|
|
314
315
|
assert_nil InventorySync::InventoryStatus.where(host_id: @host3.id).first
|
|
315
316
|
end
|
|
317
|
+
|
|
318
|
+
test 'user-omitted hosts get USER_OMITTED status' do
|
|
319
|
+
# Add parameter to exclude host1
|
|
320
|
+
@host1.host_parameters << HostParameter.create(
|
|
321
|
+
name: 'host_registration_insights_inventory',
|
|
322
|
+
value: 'false',
|
|
323
|
+
parameter_type: 'boolean'
|
|
324
|
+
)
|
|
325
|
+
@host1.save!
|
|
326
|
+
|
|
327
|
+
setup_certs_expectation do
|
|
328
|
+
InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
|
|
329
|
+
end
|
|
330
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(@inventory)
|
|
331
|
+
FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
|
|
332
|
+
|
|
333
|
+
action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
|
|
334
|
+
run_action(action)
|
|
335
|
+
|
|
336
|
+
@host1.reload
|
|
337
|
+
@host2.reload
|
|
338
|
+
|
|
339
|
+
# Host1 should be USER_OMITTED
|
|
340
|
+
assert_equal InventorySync::InventoryStatus::USER_OMITTED,
|
|
341
|
+
InventorySync::InventoryStatus.where(host_id: @host1.id).first.status,
|
|
342
|
+
'Host with host_registration_insights_inventory=false should have USER_OMITTED status'
|
|
343
|
+
|
|
344
|
+
# Host2 should be SYNC
|
|
345
|
+
assert_equal InventorySync::InventoryStatus::SYNC,
|
|
346
|
+
InventorySync::InventoryStatus.where(host_id: @host2.id).first.status,
|
|
347
|
+
'Normal host should have SYNC status'
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
test 'user-omitted hosts are not marked as disconnected' do
|
|
351
|
+
# Add parameter to exclude host1
|
|
352
|
+
@host1.host_parameters << HostParameter.create(
|
|
353
|
+
name: 'host_registration_insights_inventory',
|
|
354
|
+
value: 'false',
|
|
355
|
+
parameter_type: 'boolean'
|
|
356
|
+
)
|
|
357
|
+
@host1.save!
|
|
358
|
+
|
|
359
|
+
# Host1 is not in the cloud inventory response
|
|
360
|
+
setup_certs_expectation do
|
|
361
|
+
InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
|
|
362
|
+
end
|
|
363
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(@inventory)
|
|
364
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:affected_host_ids).returns([@host1.id, @host2.id])
|
|
365
|
+
FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
|
|
366
|
+
|
|
367
|
+
action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
|
|
368
|
+
run_action(action)
|
|
369
|
+
|
|
370
|
+
@host1.reload
|
|
371
|
+
|
|
372
|
+
# Host1 should be USER_OMITTED, not DISCONNECT
|
|
373
|
+
assert_equal InventorySync::InventoryStatus::USER_OMITTED,
|
|
374
|
+
InventorySync::InventoryStatus.where(host_id: @host1.id).first.status,
|
|
375
|
+
'User-omitted host should have USER_OMITTED status even if not in cloud inventory'
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
test 'host_statuses output includes all three counts' do
|
|
379
|
+
# Create a mix of hosts with different statuses
|
|
380
|
+
# Host1: will be user-omitted
|
|
381
|
+
@host1.host_parameters << HostParameter.create(
|
|
382
|
+
name: 'host_registration_insights_inventory',
|
|
383
|
+
value: 'false',
|
|
384
|
+
parameter_type: 'boolean'
|
|
385
|
+
)
|
|
386
|
+
@host1.save!
|
|
387
|
+
|
|
388
|
+
# Host2: will be synced (in cloud inventory)
|
|
389
|
+
# Host3: will be disconnected (not in cloud inventory)
|
|
390
|
+
|
|
391
|
+
# Create inventory response with only host2 (exclude host1 and host3)
|
|
392
|
+
inventory_with_host2_only = @inventory.dup
|
|
393
|
+
inventory_with_host2_only['results'] = [@inventory['results'][0]] # Only host2
|
|
394
|
+
inventory_with_host2_only['total'] = 1
|
|
395
|
+
inventory_with_host2_only['count'] = 1
|
|
396
|
+
|
|
397
|
+
setup_certs_expectation do
|
|
398
|
+
InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
|
|
399
|
+
end
|
|
400
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(inventory_with_host2_only)
|
|
401
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:affected_host_ids).returns([@host1.id, @host2.id, @host3.id])
|
|
402
|
+
FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
|
|
403
|
+
|
|
404
|
+
action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
|
|
405
|
+
run_action(action)
|
|
406
|
+
|
|
407
|
+
# Verify the statuses were actually set correctly
|
|
408
|
+
@host1.reload
|
|
409
|
+
@host2.reload
|
|
410
|
+
@host3.reload
|
|
411
|
+
|
|
412
|
+
assert_equal InventorySync::InventoryStatus::USER_OMITTED,
|
|
413
|
+
InventorySync::InventoryStatus.where(host_id: @host1.id).first.status,
|
|
414
|
+
'Host with host_registration_insights_inventory=false should have USER_OMITTED status'
|
|
415
|
+
assert_equal InventorySync::InventoryStatus::SYNC,
|
|
416
|
+
InventorySync::InventoryStatus.where(host_id: @host2.id).first.status,
|
|
417
|
+
'Host in cloud inventory should have SYNC status'
|
|
418
|
+
assert_equal InventorySync::InventoryStatus::DISCONNECT,
|
|
419
|
+
InventorySync::InventoryStatus.where(host_id: @host3.id).first.status,
|
|
420
|
+
'Host not in cloud inventory and not user-omitted should have DISCONNECT status'
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
test 'user-omitted status respects parameter inheritance from hostgroup' do
|
|
424
|
+
# Create a hostgroup and assign it to host1
|
|
425
|
+
hostgroup = FactoryBot.create(:hostgroup)
|
|
426
|
+
hostgroup.organizations << @host1.organization
|
|
427
|
+
@host1.hostgroup = hostgroup
|
|
428
|
+
@host1.save!
|
|
429
|
+
|
|
430
|
+
# Set parameter at hostgroup level, not directly on host
|
|
431
|
+
# This verifies the fix that uses search_for instead of querying HostParameter directly
|
|
432
|
+
hostgroup.group_parameters << GroupParameter.create(
|
|
433
|
+
name: 'host_registration_insights_inventory',
|
|
434
|
+
value: 'false',
|
|
435
|
+
key_type: 'boolean'
|
|
436
|
+
)
|
|
437
|
+
hostgroup.save!
|
|
438
|
+
|
|
439
|
+
# Verify parameter is inherited (not set directly on host)
|
|
440
|
+
assert_nil @host1.parameters.find_by(name: 'host_registration_insights_inventory'),
|
|
441
|
+
'Test setup: parameter should not be set directly on host'
|
|
442
|
+
refute ::Foreman::Cast.to_bool(@host1.host_param('host_registration_insights_inventory')),
|
|
443
|
+
'Test setup: parameter should be inherited from hostgroup'
|
|
444
|
+
|
|
445
|
+
# Host2 remains normal (no parameter)
|
|
446
|
+
setup_certs_expectation do
|
|
447
|
+
InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
|
|
448
|
+
end
|
|
449
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(@inventory)
|
|
450
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:affected_host_ids).returns([@host1.id, @host2.id])
|
|
451
|
+
FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
|
|
452
|
+
|
|
453
|
+
action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
|
|
454
|
+
run_action(action)
|
|
455
|
+
|
|
456
|
+
@host1.reload
|
|
457
|
+
@host2.reload
|
|
458
|
+
|
|
459
|
+
# Host1 should be USER_OMITTED (inherited parameter from hostgroup)
|
|
460
|
+
host1_status = InventorySync::InventoryStatus.where(host_id: @host1.id).first
|
|
461
|
+
assert_not_nil host1_status, 'Host1 should have an inventory status'
|
|
462
|
+
assert_equal InventorySync::InventoryStatus::USER_OMITTED, host1_status.status,
|
|
463
|
+
'Host with inherited host_registration_insights_inventory=false should have USER_OMITTED status'
|
|
464
|
+
|
|
465
|
+
# Host2 should be SYNC
|
|
466
|
+
assert_equal InventorySync::InventoryStatus::SYNC,
|
|
467
|
+
InventorySync::InventoryStatus.where(host_id: @host2.id).first.status,
|
|
468
|
+
'Normal host should have SYNC status'
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
test 'user-omitted statuses are cleared before re-creating them to avoid silent create failures' do
|
|
472
|
+
# First sync: host1 is user-omitted
|
|
473
|
+
@host1.host_parameters << HostParameter.create(
|
|
474
|
+
name: 'host_registration_insights_inventory',
|
|
475
|
+
value: 'false',
|
|
476
|
+
parameter_type: 'boolean'
|
|
477
|
+
)
|
|
478
|
+
@host1.save!
|
|
479
|
+
|
|
480
|
+
setup_certs_expectation do
|
|
481
|
+
InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
|
|
482
|
+
end
|
|
483
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:query_inventory).returns(@inventory).twice
|
|
484
|
+
InventorySync::Async::InventoryFullSync.any_instance.expects(:affected_host_ids).returns([@host1.id, @host2.id]).twice
|
|
485
|
+
FactoryBot.create(:fact_value, fact_name: fact_names['virt::uuid'], value: '1234', host: @host2)
|
|
486
|
+
|
|
487
|
+
# Run first sync
|
|
488
|
+
action = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
|
|
489
|
+
run_action(action)
|
|
490
|
+
|
|
491
|
+
@host1.reload
|
|
492
|
+
initial_status = InventorySync::InventoryStatus.where(host_id: @host1.id).first
|
|
493
|
+
assert_equal InventorySync::InventoryStatus::USER_OMITTED, initial_status.status,
|
|
494
|
+
'Initial sync: host should be USER_OMITTED'
|
|
495
|
+
initial_status_id = initial_status.id
|
|
496
|
+
|
|
497
|
+
# Verify there's only one status record for host1
|
|
498
|
+
assert_equal 1, InventorySync::InventoryStatus.where(host_id: @host1.id).count,
|
|
499
|
+
'Should have exactly one status record after first sync'
|
|
500
|
+
|
|
501
|
+
# Run second sync (parameter still false)
|
|
502
|
+
# Without clearing old user_omitted statuses, the .create would silently fail
|
|
503
|
+
# with 'Host has already been taken' due to uniqueness constraint
|
|
504
|
+
setup_certs_expectation do
|
|
505
|
+
InventorySync::Async::InventoryFullSync.any_instance.stubs(:candlepin_id_cert)
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
action2 = create_and_plan_action(InventorySync::Async::InventoryFullSync, @host1.organization)
|
|
509
|
+
run_action(action2)
|
|
510
|
+
|
|
511
|
+
@host1.reload
|
|
512
|
+
final_status = InventorySync::InventoryStatus.where(host_id: @host1.id).first
|
|
513
|
+
|
|
514
|
+
# Verify status is still USER_OMITTED (not stale from first sync)
|
|
515
|
+
assert_equal InventorySync::InventoryStatus::USER_OMITTED, final_status.status,
|
|
516
|
+
'Status should be USER_OMITTED after second sync'
|
|
517
|
+
|
|
518
|
+
# Verify there's still only one status record (old one was deleted before creating new one)
|
|
519
|
+
assert_equal 1, InventorySync::InventoryStatus.where(host_id: @host1.id).count,
|
|
520
|
+
'Should have exactly one status record (old cleared before new created)'
|
|
521
|
+
|
|
522
|
+
# Verify the old status record was actually deleted and replaced with a new one
|
|
523
|
+
refute InventorySync::InventoryStatus.exists?(initial_status_id),
|
|
524
|
+
'Old status record should have been deleted before creating new one'
|
|
525
|
+
assert_not_equal initial_status_id, final_status.id,
|
|
526
|
+
'New status record should have different ID, proving old was deleted'
|
|
527
|
+
end
|
|
316
528
|
end
|
|
@@ -72,4 +72,113 @@ class InsightsClientReportStatusTest < ActiveSupport::TestCase
|
|
|
72
72
|
|
|
73
73
|
assert_equal HostStatus::Global::ERROR, @host.global_status
|
|
74
74
|
end
|
|
75
|
+
|
|
76
|
+
test 'host with host_registration_insights parameter set to false gets USER_OMITTED status' do
|
|
77
|
+
@host.host_parameters << HostParameter.create(
|
|
78
|
+
name: 'host_registration_insights',
|
|
79
|
+
value: 'false',
|
|
80
|
+
parameter_type: 'boolean'
|
|
81
|
+
)
|
|
82
|
+
@host.save!
|
|
83
|
+
|
|
84
|
+
insights_status = @host.get_status(InsightsClientReportStatus)
|
|
85
|
+
insights_status.refresh!
|
|
86
|
+
|
|
87
|
+
assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status
|
|
88
|
+
assert_equal HostStatus::Global::OK, insights_status.to_global
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
test 'USER_OMITTED status has correct label' do
|
|
92
|
+
insights_status = @host.get_status(InsightsClientReportStatus)
|
|
93
|
+
insights_status.status = InsightsClientReportStatus::USER_OMITTED
|
|
94
|
+
insights_status.save!
|
|
95
|
+
|
|
96
|
+
label = insights_status.to_label
|
|
97
|
+
assert_match(/host_registration_insights/, label)
|
|
98
|
+
assert_match(/false/, label)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
test 'stale scope excludes USER_OMITTED hosts' do
|
|
102
|
+
host1 = FactoryBot.create(:host, :managed)
|
|
103
|
+
host2 = FactoryBot.create(:host, :managed)
|
|
104
|
+
host3 = FactoryBot.create(:host, :managed)
|
|
105
|
+
|
|
106
|
+
# Host 1: USER_OMITTED with old reported_at (should NOT be in stale scope)
|
|
107
|
+
status1 = host1.get_status(InsightsClientReportStatus)
|
|
108
|
+
status1.status = InsightsClientReportStatus::USER_OMITTED
|
|
109
|
+
status1.reported_at = Time.zone.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
|
|
110
|
+
status1.save!
|
|
111
|
+
|
|
112
|
+
# Host 2: REPORTING with old reported_at (should be in stale scope)
|
|
113
|
+
status2 = host2.get_status(InsightsClientReportStatus)
|
|
114
|
+
status2.status = InsightsClientReportStatus::REPORTING
|
|
115
|
+
status2.reported_at = Time.zone.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
|
|
116
|
+
status2.save!
|
|
117
|
+
|
|
118
|
+
# Host 3: NO_REPORT with old reported_at (should be in stale scope)
|
|
119
|
+
status3 = host3.get_status(InsightsClientReportStatus)
|
|
120
|
+
status3.status = InsightsClientReportStatus::NO_REPORT
|
|
121
|
+
status3.reported_at = Time.zone.now - InsightsClientReportStatus::REPORT_INTERVAL - 1.day
|
|
122
|
+
status3.save!
|
|
123
|
+
|
|
124
|
+
stale_statuses = InsightsClientReportStatus.stale
|
|
125
|
+
stale_host_ids = stale_statuses.pluck(:host_id)
|
|
126
|
+
|
|
127
|
+
assert_not_includes stale_host_ids, host1.id, 'USER_OMITTED host should not be in stale scope'
|
|
128
|
+
assert_includes stale_host_ids, host2.id, 'REPORTING host with old report should be in stale scope'
|
|
129
|
+
assert_includes stale_host_ids, host3.id, 'NO_REPORT host with old report should be in stale scope'
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
test 'USER_OMITTED status respects parameter inheritance from hostgroup' do
|
|
133
|
+
# Create a hostgroup with parameter = false
|
|
134
|
+
hostgroup = FactoryBot.create(:hostgroup)
|
|
135
|
+
hostgroup.group_parameters << GroupParameter.create(
|
|
136
|
+
name: 'host_registration_insights',
|
|
137
|
+
value: 'false',
|
|
138
|
+
key_type: 'boolean'
|
|
139
|
+
)
|
|
140
|
+
hostgroup.save!
|
|
141
|
+
|
|
142
|
+
@host.hostgroup = hostgroup
|
|
143
|
+
@host.save!
|
|
144
|
+
|
|
145
|
+
# Verify parameter is inherited (not set directly on host)
|
|
146
|
+
assert_nil @host.parameters.find_by(name: 'host_registration_insights'),
|
|
147
|
+
'Test setup: parameter should not be set directly on host'
|
|
148
|
+
|
|
149
|
+
insights_status = @host.get_status(InsightsClientReportStatus)
|
|
150
|
+
insights_status.refresh!
|
|
151
|
+
|
|
152
|
+
assert_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status,
|
|
153
|
+
'Status should be USER_OMITTED when host_registration_insights=false is inherited from hostgroup'
|
|
154
|
+
assert_equal HostStatus::Global::OK, insights_status.to_global,
|
|
155
|
+
'USER_OMITTED status should not affect global status'
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
test 'host parameter overrides inherited parameter from hostgroup' do
|
|
159
|
+
# Create a hostgroup with parameter = false
|
|
160
|
+
hostgroup = FactoryBot.create(:hostgroup)
|
|
161
|
+
hostgroup.group_parameters << GroupParameter.create(
|
|
162
|
+
name: 'host_registration_insights',
|
|
163
|
+
value: 'false',
|
|
164
|
+
key_type: 'boolean'
|
|
165
|
+
)
|
|
166
|
+
hostgroup.save!
|
|
167
|
+
|
|
168
|
+
@host.hostgroup = hostgroup
|
|
169
|
+
|
|
170
|
+
# Override with host parameter = true
|
|
171
|
+
@host.host_parameters << HostParameter.create(
|
|
172
|
+
name: 'host_registration_insights',
|
|
173
|
+
value: 'true',
|
|
174
|
+
parameter_type: 'boolean'
|
|
175
|
+
)
|
|
176
|
+
@host.save!
|
|
177
|
+
|
|
178
|
+
insights_status = @host.get_status(InsightsClientReportStatus)
|
|
179
|
+
insights_status.refresh!
|
|
180
|
+
|
|
181
|
+
assert_not_equal InsightsClientReportStatus::USER_OMITTED, insights_status.status,
|
|
182
|
+
'Status should not be USER_OMITTED when host parameter overrides hostgroup parameter with true'
|
|
183
|
+
end
|
|
75
184
|
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
require 'test_plugin_helper'
|
|
2
|
+
|
|
3
|
+
module InventorySync
|
|
4
|
+
class InventoryStatusTest < ActiveSupport::TestCase
|
|
5
|
+
setup do
|
|
6
|
+
@host = FactoryBot.create(:host, :managed)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
test 'status constants are defined correctly' do
|
|
10
|
+
assert_equal 0, InventorySync::InventoryStatus::DISCONNECT
|
|
11
|
+
assert_equal 1, InventorySync::InventoryStatus::SYNC
|
|
12
|
+
assert_equal 2, InventorySync::InventoryStatus::USER_OMITTED
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
test 'to_global returns OK for USER_OMITTED status' do
|
|
16
|
+
status = @host.get_status(InventorySync::InventoryStatus)
|
|
17
|
+
status.status = InventorySync::InventoryStatus::USER_OMITTED
|
|
18
|
+
status.save!
|
|
19
|
+
|
|
20
|
+
assert_equal HostStatus::Global::OK, status.to_global
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test 'to_global returns WARN for DISCONNECT status' do
|
|
24
|
+
status = @host.get_status(InventorySync::InventoryStatus)
|
|
25
|
+
status.status = InventorySync::InventoryStatus::DISCONNECT
|
|
26
|
+
status.save!
|
|
27
|
+
|
|
28
|
+
assert_equal HostStatus::Global::WARN, status.to_global
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
test 'to_global returns OK for SYNC status' do
|
|
32
|
+
status = @host.get_status(InventorySync::InventoryStatus)
|
|
33
|
+
status.status = InventorySync::InventoryStatus::SYNC
|
|
34
|
+
status.save!
|
|
35
|
+
|
|
36
|
+
assert_equal HostStatus::Global::OK, status.to_global
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
test 'to_label returns appropriate messages for each status' do
|
|
40
|
+
status = @host.get_status(InventorySync::InventoryStatus)
|
|
41
|
+
|
|
42
|
+
# Test DISCONNECT label
|
|
43
|
+
status.status = InventorySync::InventoryStatus::DISCONNECT
|
|
44
|
+
status.save!
|
|
45
|
+
label = status.to_label
|
|
46
|
+
assert_match(/not present/, label.downcase)
|
|
47
|
+
assert_match(/console\.redhat\.com/, label)
|
|
48
|
+
|
|
49
|
+
# Test SYNC label
|
|
50
|
+
status.status = InventorySync::InventoryStatus::SYNC
|
|
51
|
+
status.save!
|
|
52
|
+
label = status.to_label
|
|
53
|
+
assert_match(/uploaded.*present/, label.downcase)
|
|
54
|
+
assert_match(/console\.redhat\.com/, label)
|
|
55
|
+
|
|
56
|
+
# Test USER_OMITTED label
|
|
57
|
+
status.status = InventorySync::InventoryStatus::USER_OMITTED
|
|
58
|
+
status.save!
|
|
59
|
+
label = status.to_label
|
|
60
|
+
assert_match(/excluded/, label.downcase)
|
|
61
|
+
assert_match(/host parameter/, label.downcase)
|
|
62
|
+
assert_match(/console\.redhat\.com/, label)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
test 'relevant? returns true in regular (non-IoP) mode' do
|
|
66
|
+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(false)
|
|
67
|
+
|
|
68
|
+
status = @host.get_status(InventorySync::InventoryStatus)
|
|
69
|
+
status.status = InventorySync::InventoryStatus::SYNC
|
|
70
|
+
status.save!
|
|
71
|
+
|
|
72
|
+
assert status.relevant?, 'Inventory status should be relevant in regular mode'
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
test 'relevant? returns false in IoP mode' do
|
|
76
|
+
ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true)
|
|
77
|
+
|
|
78
|
+
status = @host.get_status(InventorySync::InventoryStatus)
|
|
79
|
+
status.status = InventorySync::InventoryStatus::SYNC
|
|
80
|
+
status.save!
|
|
81
|
+
|
|
82
|
+
refute status.relevant?, 'Inventory status should NOT be relevant in IoP mode'
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -342,4 +342,64 @@ class RhCloudHostTest < ActiveSupport::TestCase
|
|
|
342
342
|
assert_not_includes results, host1
|
|
343
343
|
end
|
|
344
344
|
end
|
|
345
|
+
|
|
346
|
+
test 'scoped search for user_omitted inventory status works' do
|
|
347
|
+
host1 = FactoryBot.create(:host, :managed)
|
|
348
|
+
host2 = FactoryBot.create(:host, :managed)
|
|
349
|
+
host3 = FactoryBot.create(:host, :managed)
|
|
350
|
+
|
|
351
|
+
# Create different inventory statuses
|
|
352
|
+
InventorySync::InventoryStatus.create!(
|
|
353
|
+
host_id: host1.id,
|
|
354
|
+
status: InventorySync::InventoryStatus::SYNC,
|
|
355
|
+
reported_at: Time.zone.now
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
InventorySync::InventoryStatus.create!(
|
|
359
|
+
host_id: host2.id,
|
|
360
|
+
status: InventorySync::InventoryStatus::DISCONNECT,
|
|
361
|
+
reported_at: Time.zone.now
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
InventorySync::InventoryStatus.create!(
|
|
365
|
+
host_id: host3.id,
|
|
366
|
+
status: InventorySync::InventoryStatus::USER_OMITTED,
|
|
367
|
+
reported_at: Time.zone.now
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Search for user_omitted status
|
|
371
|
+
results = Host.search_for('insights_inventory_sync_status = user_omitted')
|
|
372
|
+
result_ids = results.pluck(:id)
|
|
373
|
+
|
|
374
|
+
assert_includes result_ids, host3.id, 'Host with USER_OMITTED status should be in search results'
|
|
375
|
+
assert_not_includes result_ids, host1.id, 'Host with SYNC status should not be in search results'
|
|
376
|
+
assert_not_includes result_ids, host2.id, 'Host with DISCONNECT status should not be in search results'
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
test 'scoped search for user_omitted insights client report status works' do
|
|
380
|
+
host1 = FactoryBot.create(:host, :managed)
|
|
381
|
+
host2 = FactoryBot.create(:host, :managed)
|
|
382
|
+
host3 = FactoryBot.create(:host, :managed)
|
|
383
|
+
|
|
384
|
+
# Create different insights client report statuses
|
|
385
|
+
status1 = host1.get_status(InsightsClientReportStatus)
|
|
386
|
+
status1.status = InsightsClientReportStatus::REPORTING
|
|
387
|
+
status1.save!
|
|
388
|
+
|
|
389
|
+
status2 = host2.get_status(InsightsClientReportStatus)
|
|
390
|
+
status2.status = InsightsClientReportStatus::NO_REPORT
|
|
391
|
+
status2.save!
|
|
392
|
+
|
|
393
|
+
status3 = host3.get_status(InsightsClientReportStatus)
|
|
394
|
+
status3.status = InsightsClientReportStatus::USER_OMITTED
|
|
395
|
+
status3.save!
|
|
396
|
+
|
|
397
|
+
# Search for user_omitted status
|
|
398
|
+
results = Host.search_for('insights_client_report_status = user_omitted')
|
|
399
|
+
result_ids = results.pluck(:id)
|
|
400
|
+
|
|
401
|
+
assert_includes result_ids, host3.id, 'Host with USER_OMITTED status should be in search results'
|
|
402
|
+
assert_not_includes result_ids, host1.id, 'Host with REPORTING status should not be in search results'
|
|
403
|
+
assert_not_includes result_ids, host2.id, 'Host with NO_REPORT status should not be in search results'
|
|
404
|
+
end
|
|
345
405
|
end
|
data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/SyncButtonActions.js
CHANGED
|
@@ -39,14 +39,20 @@ export const setupInventorySyncTaskPolling = (id, dispatch) =>
|
|
|
39
39
|
key: INVENTORY_SYNC_TASK_UPDATE,
|
|
40
40
|
onTaskSuccess: ({
|
|
41
41
|
output: {
|
|
42
|
-
host_statuses: { sync, disconnect },
|
|
42
|
+
host_statuses: { sync, disconnect, user_omitted: userOmitted },
|
|
43
43
|
},
|
|
44
44
|
}) =>
|
|
45
45
|
dispatch(
|
|
46
46
|
addToast({
|
|
47
47
|
sticky: true,
|
|
48
48
|
type: 'success',
|
|
49
|
-
message:
|
|
49
|
+
message: (
|
|
50
|
+
<Toast
|
|
51
|
+
syncHosts={sync}
|
|
52
|
+
disconnectHosts={disconnect}
|
|
53
|
+
userOmittedHosts={userOmitted}
|
|
54
|
+
/>
|
|
55
|
+
),
|
|
50
56
|
})
|
|
51
57
|
),
|
|
52
58
|
dispatch,
|
data/webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/Toast.js
CHANGED
|
@@ -1,33 +1,55 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
2
3
|
import PropTypes from 'prop-types';
|
|
3
4
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
4
|
-
import { foremanUrl } from '../../../../../../ForemanRhCloudHelpers';
|
|
5
5
|
|
|
6
|
-
const
|
|
7
|
-
|
|
6
|
+
const statusSearchParams = statusName =>
|
|
7
|
+
`/new/hosts?search=insights_inventory_sync_status+%3D+${statusName}&page=1`;
|
|
8
|
+
const DISCONNECT = 'disconnect';
|
|
9
|
+
const SYNC = 'sync';
|
|
10
|
+
const USER_OMITTED = 'user_omitted';
|
|
11
|
+
const HostsWithStatusLink = ({ statusName, children }) => (
|
|
12
|
+
<Link to={statusSearchParams(statusName)}>{children}</Link>
|
|
13
|
+
);
|
|
14
|
+
HostsWithStatusLink.propTypes = {
|
|
15
|
+
statusName: PropTypes.string.isRequired,
|
|
16
|
+
children: PropTypes.node.isRequired,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const Toast = ({ syncHosts, disconnectHosts, userOmittedHosts }) => {
|
|
20
|
+
const totalHosts = syncHosts + disconnectHosts + userOmittedHosts;
|
|
8
21
|
return (
|
|
9
22
|
<span>
|
|
10
23
|
<p>
|
|
11
|
-
{__('
|
|
12
|
-
<
|
|
24
|
+
{__('Registered hosts in organization: ')}
|
|
25
|
+
<Link to="/new/hosts?search=set%3F+subscription_uuid&page=1">
|
|
26
|
+
{totalHosts}
|
|
27
|
+
</Link>
|
|
13
28
|
</p>
|
|
14
29
|
<p>
|
|
15
|
-
{__('
|
|
16
|
-
<
|
|
30
|
+
{__('Uploaded and present on console.redhat.com Inventory service: ')}
|
|
31
|
+
<HostsWithStatusLink statusName={SYNC}>{syncHosts}</HostsWithStatusLink>
|
|
17
32
|
</p>
|
|
18
33
|
<p>
|
|
19
|
-
{__('
|
|
20
|
-
<
|
|
34
|
+
{__('Not present on console.redhat.com Inventory service: ')}
|
|
35
|
+
<HostsWithStatusLink statusName={DISCONNECT}>
|
|
36
|
+
{disconnectHosts}
|
|
37
|
+
</HostsWithStatusLink>
|
|
21
38
|
</p>
|
|
39
|
+
{!!userOmittedHosts && (
|
|
40
|
+
<p>
|
|
41
|
+
{__(
|
|
42
|
+
'Excluded from upload to console.redhat.com Inventory service because host_registration_insights_inventory parameter value is false: '
|
|
43
|
+
)}
|
|
44
|
+
<HostsWithStatusLink statusName={USER_OMITTED}>
|
|
45
|
+
{userOmittedHosts}
|
|
46
|
+
</HostsWithStatusLink>
|
|
47
|
+
</p>
|
|
48
|
+
)}
|
|
22
49
|
<p>
|
|
23
|
-
{__(
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
target="_blank"
|
|
27
|
-
rel="noopener noreferrer"
|
|
28
|
-
>
|
|
29
|
-
{__('hosts page')}
|
|
30
|
-
</a>
|
|
50
|
+
{__(
|
|
51
|
+
'You can review this information later by looking at the Inventory status of each host.'
|
|
52
|
+
)}
|
|
31
53
|
</p>
|
|
32
54
|
</span>
|
|
33
55
|
);
|
|
@@ -36,6 +58,10 @@ const Toast = ({ syncHosts, disconnectHosts }) => {
|
|
|
36
58
|
Toast.propTypes = {
|
|
37
59
|
syncHosts: PropTypes.number.isRequired,
|
|
38
60
|
disconnectHosts: PropTypes.number.isRequired,
|
|
61
|
+
userOmittedHosts: PropTypes.number,
|
|
62
|
+
};
|
|
63
|
+
Toast.defaultProps = {
|
|
64
|
+
userOmittedHosts: 0,
|
|
39
65
|
};
|
|
40
66
|
|
|
41
67
|
export default Toast;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { shallow } from '@theforeman/test';
|
|
3
|
+
import Toast from '../Toast';
|
|
4
|
+
|
|
5
|
+
describe('Toast', () => {
|
|
6
|
+
it('renders with all three status counts including user_omitted', () => {
|
|
7
|
+
const wrapper = shallow(
|
|
8
|
+
<Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={2} />
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const links = wrapper.find('HostsWithStatusLink');
|
|
12
|
+
expect(links).toHaveLength(3);
|
|
13
|
+
|
|
14
|
+
// Check the children (numbers) of each link
|
|
15
|
+
expect(
|
|
16
|
+
links
|
|
17
|
+
.at(0)
|
|
18
|
+
.children()
|
|
19
|
+
.text()
|
|
20
|
+
).toBe('5');
|
|
21
|
+
expect(
|
|
22
|
+
links
|
|
23
|
+
.at(1)
|
|
24
|
+
.children()
|
|
25
|
+
.text()
|
|
26
|
+
).toBe('3');
|
|
27
|
+
expect(
|
|
28
|
+
links
|
|
29
|
+
.at(2)
|
|
30
|
+
.children()
|
|
31
|
+
.text()
|
|
32
|
+
).toBe('2');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('does not render user_omitted section when count is 0', () => {
|
|
36
|
+
const wrapper = shallow(
|
|
37
|
+
<Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={0} />
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// Should have only 2 HostsWithStatusLink components (sync and disconnect)
|
|
41
|
+
const links = wrapper.find('HostsWithStatusLink');
|
|
42
|
+
expect(links).toHaveLength(2);
|
|
43
|
+
|
|
44
|
+
// Should not contain the user_omitted explanation text
|
|
45
|
+
expect(wrapper.text()).not.toContain(
|
|
46
|
+
'host_registration_insights_inventory parameter value is false'
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('renders without crashing when userOmittedHosts is not provided (default)', () => {
|
|
51
|
+
const wrapper = shallow(<Toast syncHosts={5} disconnectHosts={3} />);
|
|
52
|
+
|
|
53
|
+
// Should use default value of 0, so only 2 links
|
|
54
|
+
const links = wrapper.find('HostsWithStatusLink');
|
|
55
|
+
expect(links).toHaveLength(2);
|
|
56
|
+
|
|
57
|
+
// Verify the count values
|
|
58
|
+
expect(
|
|
59
|
+
links
|
|
60
|
+
.at(0)
|
|
61
|
+
.children()
|
|
62
|
+
.text()
|
|
63
|
+
).toBe('5');
|
|
64
|
+
expect(
|
|
65
|
+
links
|
|
66
|
+
.at(1)
|
|
67
|
+
.children()
|
|
68
|
+
.text()
|
|
69
|
+
).toBe('3');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('renders correct status links for each category', () => {
|
|
73
|
+
const wrapper = shallow(
|
|
74
|
+
<Toast syncHosts={5} disconnectHosts={3} userOmittedHosts={2} />
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const links = wrapper.find('HostsWithStatusLink');
|
|
78
|
+
expect(links.at(0).prop('statusName')).toBe('sync');
|
|
79
|
+
expect(links.at(1).prop('statusName')).toBe('disconnect');
|
|
80
|
+
expect(links.at(2).prop('statusName')).toBe('user_omitted');
|
|
81
|
+
});
|
|
82
|
+
});
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_rh_cloud
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 13.2.
|
|
4
|
+
version: 13.2.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Red Hat Cloud team
|
|
@@ -101,6 +101,7 @@ files:
|
|
|
101
101
|
- app/controllers/concerns/foreman_rh_cloud/iop_smart_proxy_access.rb
|
|
102
102
|
- app/controllers/concerns/foreman_rh_cloud/registration_manager_extensions.rb
|
|
103
103
|
- app/controllers/concerns/insights_cloud/candlepin_cache.rb
|
|
104
|
+
- app/controllers/concerns/insights_cloud/candlepin_proxies_extensions.rb
|
|
104
105
|
- app/controllers/concerns/insights_cloud/client_authentication.rb
|
|
105
106
|
- app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb
|
|
106
107
|
- app/controllers/concerns/inventory_upload/report_actions.rb
|
|
@@ -248,6 +249,7 @@ files:
|
|
|
248
249
|
- test/controllers/insights_cloud/api/advisor_engine_controller_test.rb
|
|
249
250
|
- test/controllers/insights_cloud/api/cloud_request_controller_test.rb
|
|
250
251
|
- test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb
|
|
252
|
+
- test/controllers/insights_cloud/candlepin_proxies_extensions_test.rb
|
|
251
253
|
- test/controllers/insights_cloud/ui_requests_controller_test.rb
|
|
252
254
|
- test/controllers/insights_sync/settings_controller_test.rb
|
|
253
255
|
- test/controllers/inventory_upload/api/inventory_controller_test.rb
|
|
@@ -274,6 +276,7 @@ files:
|
|
|
274
276
|
- test/jobs/single_host_report_job_test.rb
|
|
275
277
|
- test/jobs/upload_report_direct_job_test.rb
|
|
276
278
|
- test/models/insights_client_report_status_test.rb
|
|
279
|
+
- test/models/inventory_sync/inventory_status_test.rb
|
|
277
280
|
- test/test_plugin_helper.rb
|
|
278
281
|
- test/unit/archived_report_generator_test.rb
|
|
279
282
|
- test/unit/fact_helpers_test.rb
|
|
@@ -448,6 +451,7 @@ files:
|
|
|
448
451
|
- webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/__snapshots__/integrations.test.js.snap
|
|
449
452
|
- webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/__tests__/integrations.test.js
|
|
450
453
|
- webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/Toast.js
|
|
454
|
+
- webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/components/__tests__/Toast.test.js
|
|
451
455
|
- webpack/ForemanInventoryUpload/Components/PageHeader/components/SyncButton/index.js
|
|
452
456
|
- webpack/ForemanInventoryUpload/Components/PageHeader/components/ToolbarButtons/ToolbarButtons.js
|
|
453
457
|
- webpack/ForemanInventoryUpload/Components/PageHeader/components/ToolbarButtons/__tests__/ToolbarButtons.test.js
|
|
@@ -677,6 +681,7 @@ test_files:
|
|
|
677
681
|
- test/controllers/insights_cloud/api/advisor_engine_controller_test.rb
|
|
678
682
|
- test/controllers/insights_cloud/api/cloud_request_controller_test.rb
|
|
679
683
|
- test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb
|
|
684
|
+
- test/controllers/insights_cloud/candlepin_proxies_extensions_test.rb
|
|
680
685
|
- test/controllers/insights_cloud/ui_requests_controller_test.rb
|
|
681
686
|
- test/controllers/insights_sync/settings_controller_test.rb
|
|
682
687
|
- test/controllers/inventory_upload/api/inventory_controller_test.rb
|
|
@@ -703,6 +708,7 @@ test_files:
|
|
|
703
708
|
- test/jobs/single_host_report_job_test.rb
|
|
704
709
|
- test/jobs/upload_report_direct_job_test.rb
|
|
705
710
|
- test/models/insights_client_report_status_test.rb
|
|
711
|
+
- test/models/inventory_sync/inventory_status_test.rb
|
|
706
712
|
- test/test_plugin_helper.rb
|
|
707
713
|
- test/unit/archived_report_generator_test.rb
|
|
708
714
|
- test/unit/fact_helpers_test.rb
|