foreman_rh_cloud 7.0.46 → 8.0.47
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/foreman_inventory_upload/uploads_settings_controller.rb +1 -0
- data/app/models/insights_missing_host.rb +5 -0
- data/db/migrate/20230515140211_add_missing_hosts_table.foreman_rh_cloud.rb +13 -0
- data/lib/foreman_inventory_upload/async/remove_insights_hosts_job.rb +68 -0
- data/lib/foreman_inventory_upload/generators/tags.rb +3 -5
- data/lib/foreman_inventory_upload.rb +6 -1
- data/lib/foreman_rh_cloud/engine.rb +2 -1
- data/lib/foreman_rh_cloud/settings.rb +1 -0
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/lib/inventory_sync/async/host_result.rb +5 -0
- data/lib/inventory_sync/async/inventory_hosts_sync.rb +30 -1
- data/lib/inventory_sync/async/inventory_scheduled_sync.rb +13 -2
- data/lib/tasks/hybrid_cloud.rake +6 -5
- data/package.json +1 -1
- data/test/controllers/insights_cloud/api/machine_telemetries_controller_test.rb +11 -3
- data/test/factories/insights_factories.rb +8 -0
- data/test/factories/inventory_upload_factories.rb +15 -2
- data/test/jobs/inventory_hosts_sync_test.rb +32 -0
- data/test/jobs/inventory_scheduled_sync_test.rb +12 -0
- data/test/jobs/remove_insights_hosts_job_test.rb +90 -0
- data/test/test_plugin_helper.rb +17 -0
- data/test/unit/slice_generator_test.rb +15 -9
- data/test/unit/tags_generator_test.rb +22 -7
- data/webpack/ForemanInventoryUpload/Components/InventorySettings/AdvancedSetting/AdvancedSettingsConstants.js +7 -0
- data/webpack/ForemanInventoryUpload/Components/InventorySettings/InventorySettingsSelectors.js +3 -0
- data/webpack/ForemanInventoryUpload/Components/InventorySettings/__tests__/__snapshots__/InventorySettings.test.js.snap +4 -0
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e4c84356ce25f9683795e8adb7ab42d223cef0a4ba84a4a8d9f5836cc6b80986
|
|
4
|
+
data.tar.gz: 5f86cd9c8240aff463e25620a5e0bb47fbc15ab7318647f40918d98a85513ab8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0a7765f56a4d7e695dd253b17fd87820e8455fbe95a23450e2577125f65f20acc813723a2ff036c81d6b41c4d75d1bcf3b3aea65fb1b4042b916a68a117340a5
|
|
7
|
+
data.tar.gz: d8e8db10a0fc49b9741d38f93e5d59917f85b7ac6fcc8e3007dcb84346ccd15c07bd644a91de1a5ea38942cbb5b27553d5a7c59315e317fe79d1b13b542151bd
|
|
@@ -6,6 +6,7 @@ module ForemanInventoryUpload
|
|
|
6
6
|
hostObfuscationEnabled: Setting[:obfuscate_inventory_hostnames],
|
|
7
7
|
ipsObfuscationEnabled: Setting[:obfuscate_inventory_ips],
|
|
8
8
|
excludePackagesEnabled: Setting[:exclude_installed_packages],
|
|
9
|
+
allowAutoInsightsMismatchDelete: Setting[:allow_auto_insights_mismatch_delete],
|
|
9
10
|
CloudConnectorStatus: ForemanInventoryUpload::UploadsSettingsController.cloud_connector_status,
|
|
10
11
|
lastSyncTask: last_successful_inventory_sync_task,
|
|
11
12
|
}, status: :ok
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddMissingHostsTable < ActiveRecord::Migration[6.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :insights_missing_hosts do |t|
|
|
6
|
+
t.integer :organization_id
|
|
7
|
+
t.string :name
|
|
8
|
+
t.string :insights_id
|
|
9
|
+
t.string :rhsm_id
|
|
10
|
+
t.string :ip_address
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module ForemanInventoryUpload
|
|
2
|
+
module Async
|
|
3
|
+
class RemoveInsightsHostsJob < ::Actions::EntryAction
|
|
4
|
+
include ForemanRhCloud::CertAuth
|
|
5
|
+
|
|
6
|
+
def plan(search_term, organization_id)
|
|
7
|
+
plan_self(search_term: search_term, organization_id: organization_id)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run
|
|
11
|
+
logger.debug("Attempting to remove hosts by search term: #{search_term}")
|
|
12
|
+
|
|
13
|
+
host_uuids = InsightsMissingHost.search_for(search_term).pluck(:insights_id)
|
|
14
|
+
|
|
15
|
+
page_number = 1
|
|
16
|
+
while (current_page = host_uuids.paginate(page: page_number, per_page: page_size)).present?
|
|
17
|
+
logger.debug("Removing #{(page_number - 1) * page_size} - #{page_number * page_size}/#{current_page.total_entries} hosts: #{current_page.join(',')}")
|
|
18
|
+
response = delete_page(current_page, organization)
|
|
19
|
+
# write the response in case we want to track it later
|
|
20
|
+
output["response_page#{page_number}"] = response.body
|
|
21
|
+
|
|
22
|
+
# remove host records that reported success after deletion
|
|
23
|
+
if response.code >= 200 && response.code < 300
|
|
24
|
+
remove_host_records(current_page)
|
|
25
|
+
else
|
|
26
|
+
error! "Cloud responded with code: #{response.code}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
page_number += 1
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def logger
|
|
34
|
+
Foreman::Logging.logger('background')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def search_term
|
|
38
|
+
input[:search_term]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def organization
|
|
42
|
+
@organization ||= Organization.find_by(id: input[:organization_id])
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def delete_page(host_uuids, organization)
|
|
46
|
+
execute_cloud_request(
|
|
47
|
+
organization: organization,
|
|
48
|
+
method: :delete,
|
|
49
|
+
url: ForemanInventoryUpload.hosts_by_ids_url(host_uuids),
|
|
50
|
+
headers: {
|
|
51
|
+
content_type: :json,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
rescue RestClient::ExceptionWithResponse => error_response
|
|
55
|
+
error_response.response
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def remove_host_records(uuids)
|
|
59
|
+
InsightsMissingHost.where(insights_id: uuids).delete_all
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def page_size
|
|
63
|
+
# the_most_conservative_url_size_limit(2083) / uri_size(36) with some spares for the domain name
|
|
64
|
+
40
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -45,11 +45,9 @@ module ForemanInventoryUpload
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def content_data
|
|
48
|
-
[
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
] +
|
|
52
|
-
(@host.activation_keys || []).map { |item| ['activation_key', item.name] }
|
|
48
|
+
(@host.lifecycle_environments.uniq || []).map { |item| ['lifecycle_environment', item.name] } +
|
|
49
|
+
(@host.activation_keys || []).map { |item| ['activation_key', item.name] } +
|
|
50
|
+
(@host.content_views || []).map { |item| ['content_view', item.name] }
|
|
53
51
|
end
|
|
54
52
|
|
|
55
53
|
def satellite_server_data
|
|
@@ -71,7 +71,7 @@ module ForemanInventoryUpload
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def self.inventory_base_url
|
|
74
|
-
ForemanRhCloud.cert_base_url
|
|
74
|
+
"#{ForemanRhCloud.cert_base_url}/api/inventory/v1/hosts"
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
def self.inventory_export_url
|
|
@@ -82,4 +82,9 @@ module ForemanInventoryUpload
|
|
|
82
82
|
def self.inventory_self_url
|
|
83
83
|
inventory_base_url + "?hostname_or_id=#{ForemanRhCloud.foreman_host.fqdn}"
|
|
84
84
|
end
|
|
85
|
+
|
|
86
|
+
def self.hosts_by_ids_url(host_ids)
|
|
87
|
+
host_ids_string = host_ids.join(',')
|
|
88
|
+
"#{inventory_base_url}/#{host_ids_string}"
|
|
89
|
+
end
|
|
85
90
|
end
|
|
@@ -127,7 +127,8 @@ module ForemanRhCloud
|
|
|
127
127
|
|
|
128
128
|
extend_page 'hosts/_list' do |context|
|
|
129
129
|
context.with_profile :cloud, _('RH Cloud'), default: true do
|
|
130
|
-
add_pagelet :hosts_table_column_header, key: :insights_recommendations_count, label: _('Recommendations'), sortable: true, width: '12%', class: 'hidden-xs ellipsis', priority: 100
|
|
130
|
+
add_pagelet :hosts_table_column_header, key: :insights_recommendations_count, label: _('Recommendations'), sortable: true, width: '12%', class: 'hidden-xs ellipsis', priority: 100,
|
|
131
|
+
export_data: CsvExporter::ExportDefinition.new(:insights_recommendations_count, callback: ->(host) { host&.insights_hits&.count })
|
|
131
132
|
add_pagelet :hosts_table_column_content, key: :insights_recommendations_count, callback: ->(host) { hits_counts_cell(host) }, class: 'hidden-xs ellipsis text-center', priority: 100
|
|
132
133
|
end
|
|
133
134
|
end
|
|
@@ -2,6 +2,7 @@ Foreman::SettingManager.define(:foreman) do
|
|
|
2
2
|
category(:rh_cloud, N_('RHCloud')) do
|
|
3
3
|
setting('allow_auto_inventory_upload', type: :boolean, description: N_('Enable automatic upload of your host inventory to the Red Hat cloud'), default: true, full_name: N_('Automatic inventory upload'))
|
|
4
4
|
setting('allow_auto_insights_sync', type: :boolean, description: N_('Enable automatic synchronization of Insights recommendations from the Red Hat cloud'), default: false, full_name: N_('Synchronize recommendations Automatically'))
|
|
5
|
+
setting('allow_auto_insights_mismatch_delete', type: :boolean, description: N_('Enable automatic deletion of mismatched host records from the Red Hat cloud'), default: false, full_name: N_('Automatic mismatch deletion'))
|
|
5
6
|
setting('obfuscate_inventory_hostnames', type: :boolean, description: N_('Obfuscate host names sent to the Red Hat cloud'), default: false, full_name: N_('Obfuscate host names'))
|
|
6
7
|
setting('obfuscate_inventory_ips', type: :boolean, description: N_('Obfuscate ipv4 addresses sent to the Red Hat cloud'), default: false, full_name: N_('Obfuscate host ipv4 addresses'))
|
|
7
8
|
setting('exclude_installed_packages', type: :boolean, description: N_('Exclude installed packages from being uploaded to the Red Hat cloud'), default: false, full_name: N_("Exclude installed Packages"))
|
|
@@ -12,6 +12,7 @@ module InventorySync
|
|
|
12
12
|
@sub_ids = result["results"].map { |host| host['subscription_manager_id'] }
|
|
13
13
|
@uuid_by_sub_id = Hash[result["results"].map { |host| [host['subscription_manager_id'], host['id']] }]
|
|
14
14
|
@uuid_by_fqdn = Hash[result["results"].map { |host| [host['fqdn']&.downcase, host['id']] }]
|
|
15
|
+
@results = result["results"]
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def status_hashes
|
|
@@ -42,6 +43,10 @@ module InventorySync
|
|
|
42
43
|
@host_uuids ||= Hash[@sub_ids.map { |sub_id| [host_id(sub_id), @uuid_by_sub_id[sub_id]] }].except(nil)
|
|
43
44
|
end
|
|
44
45
|
|
|
46
|
+
def missing_hosts
|
|
47
|
+
@results.select { |host| hosts[host['subscription_manager_id']].nil? }
|
|
48
|
+
end
|
|
49
|
+
|
|
45
50
|
def percentage
|
|
46
51
|
ratio = @per_page * @page * 1.0 / @total * 100
|
|
47
52
|
ratio > 100 ? 100 : ratio.truncate(2)
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
module InventorySync
|
|
2
2
|
module Async
|
|
3
3
|
class InventoryHostsSync < QueryInventoryJob
|
|
4
|
+
MAX_IP_STRING_SIZE = 254
|
|
5
|
+
|
|
4
6
|
set_callback :iteration, :around, :setup_facet_transaction
|
|
5
7
|
set_callback :step, :around, :create_facets
|
|
8
|
+
set_callback :step, :around, :create_missing_hosts
|
|
6
9
|
|
|
7
10
|
def plan(organizations)
|
|
8
11
|
# by default the tasks will be executed concurrently
|
|
@@ -11,7 +14,7 @@ module InventorySync
|
|
|
11
14
|
end
|
|
12
15
|
|
|
13
16
|
def setup_facet_transaction
|
|
14
|
-
|
|
17
|
+
ActiveRecord::Base.transaction do
|
|
15
18
|
yield
|
|
16
19
|
end
|
|
17
20
|
end
|
|
@@ -23,6 +26,17 @@ module InventorySync
|
|
|
23
26
|
results
|
|
24
27
|
end
|
|
25
28
|
|
|
29
|
+
def create_missing_hosts
|
|
30
|
+
results = yield
|
|
31
|
+
missing_hosts = results.missing_hosts.map { |host| to_missing_host_record(host, results.organization) }
|
|
32
|
+
# remove records that are no longer in the query results
|
|
33
|
+
InsightsMissingHost.
|
|
34
|
+
where.not(insights_id: missing_hosts.map { |host_hash| host_hash[:insights_id] }).
|
|
35
|
+
where(organization_id: results.organization.id).delete_all
|
|
36
|
+
# readd new hosts that appear in the results, but the subscription_id is missing from the DB.
|
|
37
|
+
InsightsMissingHost.upsert_all(missing_hosts) if missing_hosts.present?
|
|
38
|
+
end
|
|
39
|
+
|
|
26
40
|
def rescue_strategy_for_self
|
|
27
41
|
Dynflow::Action::Rescue::Fail
|
|
28
42
|
end
|
|
@@ -42,6 +56,21 @@ module InventorySync
|
|
|
42
56
|
InsightsFacet.upsert_all(all_facets, unique_by: :host_id) unless all_facets.empty?
|
|
43
57
|
end
|
|
44
58
|
|
|
59
|
+
def to_missing_host_record(host_result, organization)
|
|
60
|
+
{
|
|
61
|
+
name: host_result['fqdn'],
|
|
62
|
+
insights_id: host_result['id'],
|
|
63
|
+
rhsm_id: host_result['subscription_manager_id'],
|
|
64
|
+
ip_address: to_ip_address_string(host_result['ip_addresses']),
|
|
65
|
+
organization_id: organization.id,
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def to_ip_address_string(ip_addresses)
|
|
70
|
+
string_size = 0
|
|
71
|
+
ip_addresses.take_while { |address| (string_size += address.length) <= MAX_IP_STRING_SIZE }
|
|
72
|
+
end
|
|
73
|
+
|
|
45
74
|
def plan_self_host_sync
|
|
46
75
|
plan_action InventorySync::Async::InventorySelfHostSync
|
|
47
76
|
end
|
|
@@ -14,8 +14,14 @@ module InventorySync
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
after_delay do
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# perform a sequence of sync then delete in parallel for all organizations
|
|
18
|
+
concurrence do
|
|
19
|
+
Organization.unscoped.each do |org|
|
|
20
|
+
sequence do
|
|
21
|
+
plan_org_sync(org)
|
|
22
|
+
plan_remove_insights_hosts(org) if Setting[:allow_auto_insights_mismatch_delete]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
19
25
|
end
|
|
20
26
|
end
|
|
21
27
|
end
|
|
@@ -24,6 +30,11 @@ module InventorySync
|
|
|
24
30
|
plan_action InventoryFullSync, org
|
|
25
31
|
end
|
|
26
32
|
|
|
33
|
+
def plan_remove_insights_hosts
|
|
34
|
+
# plan a remove hosts action with search set to empty (all records)
|
|
35
|
+
plan_action(ForemanInventoryUpload::Async::RemoveInsightsHostsJob, '', org.id)
|
|
36
|
+
end
|
|
37
|
+
|
|
27
38
|
def logger
|
|
28
39
|
action_logger
|
|
29
40
|
end
|
data/lib/tasks/hybrid_cloud.rake
CHANGED
|
@@ -22,9 +22,9 @@ namespace :rh_cloud do |args|
|
|
|
22
22
|
exit(1)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
organization = Organization.find_by(id: ENV['org_id'].to_i) # saw this coming in as a string, so making sure it gets passed as an integer.
|
|
26
|
-
|
|
27
|
-
@
|
|
25
|
+
@organization = Organization.find_by(id: ENV['org_id'].to_i) # saw this coming in as a string, so making sure it gets passed as an integer.
|
|
26
|
+
@uid = cp_owner_id(@organization)
|
|
27
|
+
@hostname = ForemanRhCloud.foreman_host_name
|
|
28
28
|
logger.error('Organization provided does not have a manifest imported.') + exit(1) if @uid.nil?
|
|
29
29
|
|
|
30
30
|
puts 'Paste your token, output will be hidden.'
|
|
@@ -39,7 +39,8 @@ namespace :rh_cloud do |args|
|
|
|
39
39
|
|
|
40
40
|
def payload
|
|
41
41
|
{
|
|
42
|
-
"uid": @uid
|
|
42
|
+
"uid": @uid,
|
|
43
|
+
"display_name": "#{@hostname}+#{@organization.label}"
|
|
43
44
|
}
|
|
44
45
|
end
|
|
45
46
|
|
|
@@ -49,7 +50,7 @@ namespace :rh_cloud do |args|
|
|
|
49
50
|
|
|
50
51
|
begin
|
|
51
52
|
response = execute_cloud_request(
|
|
52
|
-
organization: organization,
|
|
53
|
+
organization: @organization,
|
|
53
54
|
method: method,
|
|
54
55
|
url: registrations_url,
|
|
55
56
|
headers: headers,
|
data/package.json
CHANGED
|
@@ -110,7 +110,7 @@ module InsightsCloud::Api
|
|
|
110
110
|
User.current = User.find_by(login: 'secret_admin')
|
|
111
111
|
|
|
112
112
|
@env = FactoryBot.create(:katello_k_t_environment)
|
|
113
|
-
|
|
113
|
+
@env2 = FactoryBot.create(:katello_k_t_environment, organization: @env.organization)
|
|
114
114
|
|
|
115
115
|
@host = FactoryBot.create(
|
|
116
116
|
:host,
|
|
@@ -118,8 +118,16 @@ module InsightsCloud::Api
|
|
|
118
118
|
:with_content,
|
|
119
119
|
:with_hostgroup,
|
|
120
120
|
:with_parameter,
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
content_view_environments: [
|
|
122
|
+
FactoryBot.create(
|
|
123
|
+
:katello_content_view_environment,
|
|
124
|
+
content_view: FactoryBot.create(:katello_content_view, organization: @env.organization),
|
|
125
|
+
lifecycle_environment: @env),
|
|
126
|
+
FactoryBot.create(
|
|
127
|
+
:katello_content_view_environment,
|
|
128
|
+
content_view: FactoryBot.create(:katello_content_view, organization: @env.organization),
|
|
129
|
+
lifecycle_environment: @env2),
|
|
130
|
+
],
|
|
123
131
|
organization: @env.organization
|
|
124
132
|
)
|
|
125
133
|
|
|
@@ -41,6 +41,14 @@ FactoryBot.define do
|
|
|
41
41
|
resolution_risk { 1 }
|
|
42
42
|
resolution_type { 'fix' }
|
|
43
43
|
end
|
|
44
|
+
|
|
45
|
+
factory :insights_missing_host do
|
|
46
|
+
organization { association :organization }
|
|
47
|
+
sequence(:name) { |n| "removed.host#{n}.test" }
|
|
48
|
+
insights_id { Foreman.uuid }
|
|
49
|
+
rhsm_id { Foreman.uuid }
|
|
50
|
+
ip_address { "192.168.1.1,192.168.2.1" }
|
|
51
|
+
end
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
FactoryBot.modify do
|
|
@@ -33,6 +33,13 @@ FactoryBot.define do
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
FactoryBot.define do
|
|
37
|
+
factory :katello_content_view_environment, :class => Katello::ContentViewEnvironment do
|
|
38
|
+
sequence(:name) { |n| "name#{n}" }
|
|
39
|
+
sequence(:label) { |n| "label#{n}" }
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
36
43
|
FactoryBot.define do
|
|
37
44
|
factory :katello_k_t_environment, :aliases => [:katello_environment], :class => Katello::KTEnvironment do
|
|
38
45
|
sequence(:name) { |n| "Environment#{n}" }
|
|
@@ -93,6 +100,7 @@ FactoryBot.modify do
|
|
|
93
100
|
content_view { nil }
|
|
94
101
|
lifecycle_environment { nil }
|
|
95
102
|
content_source { nil }
|
|
103
|
+
content_view_environments { [] }
|
|
96
104
|
end
|
|
97
105
|
|
|
98
106
|
trait :with_content do
|
|
@@ -100,9 +108,14 @@ FactoryBot.modify do
|
|
|
100
108
|
|
|
101
109
|
after(:build) do |host, evaluator|
|
|
102
110
|
if host.content_facet
|
|
103
|
-
|
|
104
|
-
|
|
111
|
+
if evaluator.content_view && evaluator.lifecycle_environment
|
|
112
|
+
host.content_facet.assign_single_environment(
|
|
113
|
+
content_view_id: evaluator.content_view.id,
|
|
114
|
+
lifecycle_environment_id: evaluator.lifecycle_environment.id
|
|
115
|
+
)
|
|
116
|
+
end
|
|
105
117
|
host.content_facet.content_source = evaluator.content_source if evaluator.content_source
|
|
118
|
+
host.content_facet.content_view_environments = evaluator.content_view_environments unless evaluator.content_view_environments.empty?
|
|
106
119
|
end
|
|
107
120
|
end
|
|
108
121
|
end
|
|
@@ -294,4 +294,36 @@ class InventoryHostsSyncTest < ActiveSupport::TestCase
|
|
|
294
294
|
|
|
295
295
|
assert_nil @host2.insights
|
|
296
296
|
end
|
|
297
|
+
|
|
298
|
+
test 'Inventory should create new missing host records' do
|
|
299
|
+
org = FactoryBot.create(:organization)
|
|
300
|
+
|
|
301
|
+
InventorySync::Async::InventoryHostsSync.any_instance.expects(:query_inventory).returns(@inventory)
|
|
302
|
+
InventorySync::Async::InventoryHostsSync.any_instance.expects(:plan_self_host_sync)
|
|
303
|
+
|
|
304
|
+
setup_certs_expectation do
|
|
305
|
+
InventorySync::Async::InventoryHostsSync.any_instance.stubs(:candlepin_id_cert)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
ForemanTasks.sync_task(InventorySync::Async::InventoryHostsSync, [org])
|
|
309
|
+
|
|
310
|
+
assert_equal 2, InsightsMissingHost.count
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
test 'Inventory should remove old missing host records' do
|
|
314
|
+
org = FactoryBot.create(:organization)
|
|
315
|
+
|
|
316
|
+
InventorySync::Async::InventoryHostsSync.any_instance.expects(:query_inventory).returns(@inventory)
|
|
317
|
+
InventorySync::Async::InventoryHostsSync.any_instance.expects(:plan_self_host_sync)
|
|
318
|
+
|
|
319
|
+
FactoryBot.create(:insights_missing_host, organization: org)
|
|
320
|
+
|
|
321
|
+
setup_certs_expectation do
|
|
322
|
+
InventorySync::Async::InventoryHostsSync.any_instance.stubs(:candlepin_id_cert)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
ForemanTasks.sync_task(InventorySync::Async::InventoryHostsSync, [org])
|
|
326
|
+
|
|
327
|
+
assert_equal 2, InsightsMissingHost.count
|
|
328
|
+
end
|
|
297
329
|
end
|
|
@@ -6,8 +6,10 @@ class InventoryScheduledSyncTest < ActiveSupport::TestCase
|
|
|
6
6
|
|
|
7
7
|
test 'Schedules an execution if auto upload is enabled' do
|
|
8
8
|
Setting[:allow_auto_inventory_upload] = true
|
|
9
|
+
Setting[:allow_auto_insights_mismatch_delete] = true
|
|
9
10
|
|
|
10
11
|
InventorySync::Async::InventoryScheduledSync.any_instance.expects(:plan_org_sync).times(Organization.unscoped.count)
|
|
12
|
+
InventorySync::Async::InventoryScheduledSync.any_instance.expects(:plan_remove_insights_hosts).times(Organization.unscoped.count)
|
|
11
13
|
|
|
12
14
|
ForemanTasks.sync_task(InventorySync::Async::InventoryScheduledSync)
|
|
13
15
|
end
|
|
@@ -19,4 +21,14 @@ class InventoryScheduledSyncTest < ActiveSupport::TestCase
|
|
|
19
21
|
|
|
20
22
|
ForemanTasks.sync_task(InventorySync::Async::InventoryScheduledSync)
|
|
21
23
|
end
|
|
24
|
+
|
|
25
|
+
test 'Skips mismatch deletion if the setting is disabled' do
|
|
26
|
+
Setting[:allow_auto_inventory_upload] = true
|
|
27
|
+
Setting[:allow_auto_insights_mismatch_delete] = false
|
|
28
|
+
|
|
29
|
+
InventorySync::Async::InventoryScheduledSync.any_instance.expects(:plan_org_sync).times(Organization.unscoped.count)
|
|
30
|
+
InventorySync::Async::InventoryScheduledSync.any_instance.expects(:plan_remove_insights_hosts).never
|
|
31
|
+
|
|
32
|
+
ForemanTasks.sync_task(InventorySync::Async::InventoryScheduledSync)
|
|
33
|
+
end
|
|
22
34
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require 'test_plugin_helper'
|
|
2
|
+
require 'foreman_tasks/test_helpers'
|
|
3
|
+
|
|
4
|
+
class RemoveInsightsHostJobTest < ActiveSupport::TestCase
|
|
5
|
+
include ForemanTasks::TestHelpers::WithInThreadExecutor
|
|
6
|
+
|
|
7
|
+
setup do
|
|
8
|
+
User.current = User.find_by(login: 'secret_admin')
|
|
9
|
+
|
|
10
|
+
Organization.any_instance.stubs(:manifest_expired?).returns(false)
|
|
11
|
+
@org = FactoryBot.create(:organization)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
test 'Deletes host records on cloud success' do
|
|
15
|
+
ForemanInventoryUpload::Async::RemoveInsightsHostsJob.any_instance.expects(:execute_cloud_request).returns(
|
|
16
|
+
mock_response
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
FactoryBot.create(:insights_missing_host, organization: @org)
|
|
20
|
+
|
|
21
|
+
task = ForemanTasks.sync_task(ForemanInventoryUpload::Async::RemoveInsightsHostsJob, '', @org.id)
|
|
22
|
+
|
|
23
|
+
assert_equal 0, InsightsMissingHost.count
|
|
24
|
+
assert_equal 'success', task.result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
test 'Does not delete hosts on cloud failure' do
|
|
28
|
+
ForemanInventoryUpload::Async::RemoveInsightsHostsJob.any_instance.expects(:execute_cloud_request).raises(
|
|
29
|
+
RestClient::Exceptions::EXCEPTIONS_MAP.fetch(500).new(mock_response(code: 500), 500)
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
FactoryBot.create(:insights_missing_host, organization: @org)
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
ForemanTasks.sync_task(ForemanInventoryUpload::Async::RemoveInsightsHostsJob, '', @org.id)
|
|
36
|
+
rescue ForemanTasks::TaskError => ex
|
|
37
|
+
task = ex.task
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
assert_equal 1, InsightsMissingHost.count
|
|
41
|
+
assert_equal 'error', task.result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
test 'Paginates the hosts list' do
|
|
45
|
+
ForemanInventoryUpload::Async::RemoveInsightsHostsJob.any_instance.stubs(:page_size).returns(1)
|
|
46
|
+
|
|
47
|
+
ForemanInventoryUpload::Async::RemoveInsightsHostsJob.any_instance.expects(:execute_cloud_request).returns(
|
|
48
|
+
mock_response(body: 'response2')
|
|
49
|
+
)
|
|
50
|
+
ForemanInventoryUpload::Async::RemoveInsightsHostsJob.any_instance.expects(:execute_cloud_request).returns(
|
|
51
|
+
mock_response(body: 'response1')
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
FactoryBot.create(:insights_missing_host, organization: @org)
|
|
55
|
+
FactoryBot.create(:insights_missing_host, organization: @org)
|
|
56
|
+
|
|
57
|
+
task = ForemanTasks.sync_task(ForemanInventoryUpload::Async::RemoveInsightsHostsJob, '', @org.id)
|
|
58
|
+
|
|
59
|
+
assert_equal 0, InsightsMissingHost.count
|
|
60
|
+
assert_equal 'success', task.result
|
|
61
|
+
assert_equal 'response1', task.output[:response_page1]
|
|
62
|
+
assert_equal 'response2', task.output[:response_page2]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
test 'Uses scoped_search to select hosts' do
|
|
66
|
+
ForemanInventoryUpload::Async::RemoveInsightsHostsJob.any_instance.stubs(:page_size).returns(1)
|
|
67
|
+
|
|
68
|
+
# Since the request is paginated per 1 host, I would expect only one call to execute_cloud_request
|
|
69
|
+
ForemanInventoryUpload::Async::RemoveInsightsHostsJob.any_instance.expects(:execute_cloud_request).returns(
|
|
70
|
+
mock_response(body: 'response1')
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
FactoryBot.create(:insights_missing_host, name: 'test a', organization: @org)
|
|
74
|
+
FactoryBot.create(:insights_missing_host, name: 'test b', organization: @org)
|
|
75
|
+
|
|
76
|
+
task = ForemanTasks.sync_task(ForemanInventoryUpload::Async::RemoveInsightsHostsJob, 'name ~ b', @org.id)
|
|
77
|
+
|
|
78
|
+
assert_equal 1, InsightsMissingHost.count
|
|
79
|
+
assert_equal 'test a', InsightsMissingHost.first.name
|
|
80
|
+
assert_equal 'success', task.result
|
|
81
|
+
assert_equal 'response1', task.output[:response_page1]
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def mock_response(code: 200, body: '')
|
|
85
|
+
response = mock('response')
|
|
86
|
+
response.stubs(:code).returns(code)
|
|
87
|
+
response.stubs(:body).returns(body)
|
|
88
|
+
response
|
|
89
|
+
end
|
|
90
|
+
end
|
data/test/test_plugin_helper.rb
CHANGED
|
@@ -8,6 +8,13 @@ FactoryBot.definition_file_paths << File.join(File.dirname(__FILE__), 'factories
|
|
|
8
8
|
# FactoryBot.definition_file_paths << "#{Katello::Engine.root}/test/factories"
|
|
9
9
|
FactoryBot.reload
|
|
10
10
|
|
|
11
|
+
begin
|
|
12
|
+
require 'pry-rescue/minitest'
|
|
13
|
+
require 'pry-byebug'
|
|
14
|
+
rescue LoadError
|
|
15
|
+
# if the extension is not loaded - please continue, no harm done.
|
|
16
|
+
end
|
|
17
|
+
|
|
11
18
|
module FolderIsolation
|
|
12
19
|
extend ActiveSupport::Concern
|
|
13
20
|
|
|
@@ -100,3 +107,13 @@ module MockForemanHostname
|
|
|
100
107
|
end
|
|
101
108
|
end
|
|
102
109
|
end
|
|
110
|
+
|
|
111
|
+
module CandlepinIsolation
|
|
112
|
+
extend ActiveSupport::Concern
|
|
113
|
+
|
|
114
|
+
included do
|
|
115
|
+
setup do
|
|
116
|
+
Katello::Host::SubscriptionFacet.any_instance.stubs(:backend_update_needed?).returns(false)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -2,6 +2,7 @@ require 'test_plugin_helper'
|
|
|
2
2
|
|
|
3
3
|
class SliceGeneratorTest < ActiveSupport::TestCase
|
|
4
4
|
include KatelloLocationFix
|
|
5
|
+
include CandlepinIsolation
|
|
5
6
|
|
|
6
7
|
setup do
|
|
7
8
|
User.current = User.find_by(login: 'secret_admin')
|
|
@@ -30,7 +31,7 @@ class SliceGeneratorTest < ActiveSupport::TestCase
|
|
|
30
31
|
subscription: FactoryBot.create(:katello_subscription, organization_id: env.organization.id)
|
|
31
32
|
)
|
|
32
33
|
@host.interfaces.first.identifier = 'test_nic1'
|
|
33
|
-
@host.save!
|
|
34
|
+
@host.interfaces.first.save!
|
|
34
35
|
|
|
35
36
|
ForemanInventoryUpload::Generators::Queries.instance_variable_set(:@fact_names, nil)
|
|
36
37
|
end
|
|
@@ -300,6 +301,11 @@ class SliceGeneratorTest < ActiveSupport::TestCase
|
|
|
300
301
|
|
|
301
302
|
test 'generates a report with satellite facts' do
|
|
302
303
|
hostgroup = FactoryBot.create(:hostgroup, name: 'Special"name')
|
|
304
|
+
# Don't try to update CP in tests
|
|
305
|
+
Katello::Resources::Candlepin::Consumer.stubs(:update)
|
|
306
|
+
# Don't try update facts for the host
|
|
307
|
+
Katello::Host::SubscriptionFacet.stubs(:update_facts)
|
|
308
|
+
|
|
303
309
|
@host.hostgroup = hostgroup
|
|
304
310
|
@host.save!
|
|
305
311
|
|
|
@@ -326,7 +332,7 @@ class SliceGeneratorTest < ActiveSupport::TestCase
|
|
|
326
332
|
actual_host = actual['hosts'].first
|
|
327
333
|
assert_tag('satellite-id', actual_host, 'satellite_instance_id')
|
|
328
334
|
assert_tag(@host.organization_id.to_s, actual_host, 'organization_id')
|
|
329
|
-
assert_tag(@host.
|
|
335
|
+
assert_tag(@host.content_views.first.name, actual_host, 'content_view')
|
|
330
336
|
assert_tag(@host.location.name, actual_host, 'location')
|
|
331
337
|
assert_tag(@host.organization.name, actual_host, 'organization')
|
|
332
338
|
assert_tag(@host.hostgroup.name, actual_host, 'hostgroup')
|
|
@@ -348,13 +354,13 @@ class SliceGeneratorTest < ActiveSupport::TestCase
|
|
|
348
354
|
:host,
|
|
349
355
|
:with_subscription,
|
|
350
356
|
:with_content,
|
|
351
|
-
content_view: @host.
|
|
352
|
-
lifecycle_environment: @host.
|
|
357
|
+
content_view: @host.content_views.first,
|
|
358
|
+
lifecycle_environment: @host.lifecycle_environments.first,
|
|
353
359
|
organization: @host.organization
|
|
354
360
|
)
|
|
355
361
|
|
|
356
362
|
@host.subscription_facet.hypervisor_host = hypervisor_host
|
|
357
|
-
@host.save!
|
|
363
|
+
@host.subscription_facet.save!
|
|
358
364
|
|
|
359
365
|
batch = Host.where(id: @host.id).in_batches.first
|
|
360
366
|
generator = create_generator(batch)
|
|
@@ -497,8 +503,8 @@ class SliceGeneratorTest < ActiveSupport::TestCase
|
|
|
497
503
|
:host,
|
|
498
504
|
:with_subscription,
|
|
499
505
|
:with_content,
|
|
500
|
-
content_view: @host.
|
|
501
|
-
lifecycle_environment: @host.
|
|
506
|
+
content_view: @host.content_views.first,
|
|
507
|
+
lifecycle_environment: @host.lifecycle_environments.first,
|
|
502
508
|
organization: new_org
|
|
503
509
|
)
|
|
504
510
|
|
|
@@ -644,8 +650,8 @@ class SliceGeneratorTest < ActiveSupport::TestCase
|
|
|
644
650
|
:host,
|
|
645
651
|
:with_subscription,
|
|
646
652
|
:with_content,
|
|
647
|
-
content_view: @host.
|
|
648
|
-
lifecycle_environment: @host.
|
|
653
|
+
content_view: @host.content_views.first,
|
|
654
|
+
lifecycle_environment: @host.lifecycle_environments.first,
|
|
649
655
|
organization: @host.organization,
|
|
650
656
|
installed_packages: [installed_package]
|
|
651
657
|
)
|
|
@@ -2,12 +2,13 @@ require 'test_plugin_helper'
|
|
|
2
2
|
|
|
3
3
|
class TagsGeneratorTest < ActiveSupport::TestCase
|
|
4
4
|
include KatelloLocationFix
|
|
5
|
+
include CandlepinIsolation
|
|
5
6
|
|
|
6
7
|
setup do
|
|
7
8
|
User.current = User.find_by(login: 'secret_admin')
|
|
8
9
|
|
|
9
10
|
env = FactoryBot.create(:katello_k_t_environment)
|
|
10
|
-
|
|
11
|
+
env2 = FactoryBot.create(:katello_k_t_environment, organization: env.organization)
|
|
11
12
|
|
|
12
13
|
@location1 = FactoryBot.create(:location)
|
|
13
14
|
@location2 = FactoryBot.create(:location, parent: @location1)
|
|
@@ -20,15 +21,27 @@ class TagsGeneratorTest < ActiveSupport::TestCase
|
|
|
20
21
|
:redhat,
|
|
21
22
|
:with_subscription,
|
|
22
23
|
:with_content,
|
|
23
|
-
content_view: cv.first,
|
|
24
|
-
lifecycle_environment: env,
|
|
25
24
|
organization: env.organization,
|
|
26
25
|
location: @location2,
|
|
27
|
-
hostgroup: @hostgroup2
|
|
26
|
+
hostgroup: @hostgroup2,
|
|
27
|
+
content_view_environments: [
|
|
28
|
+
FactoryBot.create(
|
|
29
|
+
:katello_content_view_environment,
|
|
30
|
+
content_view: FactoryBot.create(:katello_content_view, organization: env.organization),
|
|
31
|
+
lifecycle_environment: env),
|
|
32
|
+
FactoryBot.create(
|
|
33
|
+
:katello_content_view_environment,
|
|
34
|
+
content_view: FactoryBot.create(:katello_content_view, organization: env.organization),
|
|
35
|
+
lifecycle_environment: env2),
|
|
36
|
+
]
|
|
28
37
|
)
|
|
29
38
|
|
|
30
39
|
@host.organization.pools << FactoryBot.create(:katello_pool, account_number: '1234', cp_id: 1)
|
|
31
40
|
@host.interfaces.first.identifier = 'test_nic1'
|
|
41
|
+
# Don't try to update CP in tests
|
|
42
|
+
Katello::Resources::Candlepin::Consumer.stubs(:update)
|
|
43
|
+
# Don't try update facts for the host
|
|
44
|
+
Katello::Host::SubscriptionFacet.stubs(:update_facts)
|
|
32
45
|
@host.save!
|
|
33
46
|
end
|
|
34
47
|
|
|
@@ -46,8 +59,10 @@ class TagsGeneratorTest < ActiveSupport::TestCase
|
|
|
46
59
|
assert_nil actual['host collection']
|
|
47
60
|
|
|
48
61
|
assert_equal @host.organization.name, actual['organization'].first.last
|
|
49
|
-
assert_equal @host.
|
|
50
|
-
assert_equal @host.
|
|
62
|
+
assert_equal @host.lifecycle_environments.pluck(:name).min, actual['lifecycle_environment'].map(&:second).min
|
|
63
|
+
assert_equal @host.lifecycle_environments.pluck(:name).max, actual['lifecycle_environment'].map(&:second).max
|
|
64
|
+
assert_equal @host.content_views.pluck(:name).min, actual['content_view'].map(&:second).min
|
|
65
|
+
assert_equal @host.content_views.pluck(:name).max, actual['content_view'].map(&:second).max
|
|
51
66
|
assert_equal Foreman.instance_id, actual['satellite_instance_id'].first.last
|
|
52
67
|
assert_equal @host.organization_id.to_s, actual['organization_id'].first.last
|
|
53
68
|
end
|
|
@@ -55,7 +70,7 @@ class TagsGeneratorTest < ActiveSupport::TestCase
|
|
|
55
70
|
test 'filters tags with empty values' do
|
|
56
71
|
generator = create_generator
|
|
57
72
|
|
|
58
|
-
@host.stubs(:
|
|
73
|
+
@host.stubs(:content_views).returns([])
|
|
59
74
|
|
|
60
75
|
actual = generator.generate.group_by { |key, value| key }
|
|
61
76
|
|
|
@@ -25,4 +25,11 @@ export const settingsDict = {
|
|
|
25
25
|
'Exclude installed packages from being uploaded to the Red Hat cloud'
|
|
26
26
|
),
|
|
27
27
|
},
|
|
28
|
+
allowAutoInsightsMismatchDelete: {
|
|
29
|
+
name: 'allow_auto_insights_mismatch_delete',
|
|
30
|
+
label: __('Automatic mismatch deletion'),
|
|
31
|
+
tooltip: __(
|
|
32
|
+
'Enable automatic deletion of mismatched host records from the Red Hat cloud'
|
|
33
|
+
),
|
|
34
|
+
},
|
|
28
35
|
};
|
|
@@ -23,5 +23,9 @@ exports[`InventorySettings rendering render without Props 1`] = `
|
|
|
23
23
|
key="excludePackagesEnabled"
|
|
24
24
|
setting="excludePackagesEnabled"
|
|
25
25
|
/>
|
|
26
|
+
<AdvancedSetting
|
|
27
|
+
key="allowAutoInsightsMismatchDelete"
|
|
28
|
+
setting="allowAutoInsightsMismatchDelete"
|
|
29
|
+
/>
|
|
26
30
|
</div>
|
|
27
31
|
`;
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_rh_cloud
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 8.0.47
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Red Hat Cloud team
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-
|
|
11
|
+
date: 2023-06-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: katello
|
|
@@ -141,6 +141,7 @@ files:
|
|
|
141
141
|
- app/models/insights_client_report_status.rb
|
|
142
142
|
- app/models/insights_facet.rb
|
|
143
143
|
- app/models/insights_hit.rb
|
|
144
|
+
- app/models/insights_missing_host.rb
|
|
144
145
|
- app/models/insights_resolution.rb
|
|
145
146
|
- app/models/insights_rule.rb
|
|
146
147
|
- app/models/inventory_sync/inventory_status.rb
|
|
@@ -183,6 +184,7 @@ files:
|
|
|
183
184
|
- db/migrate/20211027000001_create_task_output.foreman_rh_cloud.rb
|
|
184
185
|
- db/migrate/20220321000001_add_unique_to_insights_rules.foreman_rh_cloud.rb
|
|
185
186
|
- db/migrate/20221102110254_fix_rh_cloud_settings_category_to_dsl.rb
|
|
187
|
+
- db/migrate/20230515140211_add_missing_hosts_table.foreman_rh_cloud.rb
|
|
186
188
|
- db/seeds.d/179_ui_notifications.rb
|
|
187
189
|
- db/seeds.d/50_job_templates.rb
|
|
188
190
|
- lib/foreman_inventory_upload.rb
|
|
@@ -192,6 +194,7 @@ files:
|
|
|
192
194
|
- lib/foreman_inventory_upload/async/generate_report_job.rb
|
|
193
195
|
- lib/foreman_inventory_upload/async/progress_output.rb
|
|
194
196
|
- lib/foreman_inventory_upload/async/queue_for_upload_job.rb
|
|
197
|
+
- lib/foreman_inventory_upload/async/remove_insights_hosts_job.rb
|
|
195
198
|
- lib/foreman_inventory_upload/async/shell_process.rb
|
|
196
199
|
- lib/foreman_inventory_upload/async/upload_report_job.rb
|
|
197
200
|
- lib/foreman_inventory_upload/generators/archived_report.rb
|
|
@@ -256,6 +259,7 @@ files:
|
|
|
256
259
|
- test/jobs/inventory_hosts_sync_test.rb
|
|
257
260
|
- test/jobs/inventory_scheduled_sync_test.rb
|
|
258
261
|
- test/jobs/inventory_self_host_sync_test.rb
|
|
262
|
+
- test/jobs/remove_insights_hosts_job_test.rb
|
|
259
263
|
- test/jobs/upload_report_job_test.rb
|
|
260
264
|
- test/models/insights_client_report_status_test.rb
|
|
261
265
|
- test/test_plugin_helper.rb
|
|
@@ -692,6 +696,7 @@ test_files:
|
|
|
692
696
|
- test/jobs/inventory_hosts_sync_test.rb
|
|
693
697
|
- test/jobs/inventory_scheduled_sync_test.rb
|
|
694
698
|
- test/jobs/inventory_self_host_sync_test.rb
|
|
699
|
+
- test/jobs/remove_insights_hosts_job_test.rb
|
|
695
700
|
- test/jobs/upload_report_job_test.rb
|
|
696
701
|
- test/models/insights_client_report_status_test.rb
|
|
697
702
|
- test/test_plugin_helper.rb
|