foreman_rh_cloud 12.1.3 → 12.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6bc60ead37485cf4bb034dd6ccbd416ca9ad957c37123553c1cf529a2a948d48
4
- data.tar.gz: 7490a0bf69893145965f99d692735dca1ad3786439efb306e99423f555c83ad0
3
+ metadata.gz: 7ed52afd2a65d3cc87c3ae660716ca1044af88b359f84f108cbe005c01a37a42
4
+ data.tar.gz: b00def287ae973805693edaf6b9eb13e72846992bcb7e5432073ff722458d565
5
5
  SHA512:
6
- metadata.gz: 68a8fcbff3c147991ee6c05699a9a0be941552dd97bcd94fb8230635120c40302a06b1ed4d2428a16ad71b353da2a56696e045eca29a3beda0b3df2877bd2206
7
- data.tar.gz: 00ae0a4cafbad304e4da6ff962c8214c67aecc0e7af37a8c2dbd1be1f889dbca095cf93f4c6222441cef08298d333758ff8c2f942358736aca8473cc813fee34
6
+ metadata.gz: 3ba0a99ac74b2b311f3569cc6dee79a5043be05abdfe41ca25e36f9c4a6e1d903732e8e38ae580c01634842ec493e8ab350f0b3d04b680e032aba9e730bd1935
7
+ data.tar.gz: 147d7da2c6352b9333bf1a5c8537c1468d2246d7bd656846ff515170b7c3841c55c73ae4e7fef690c39f56d893721efa994e271b4214d154f9542e03da43c808
data/README.md CHANGED
@@ -1,18 +1,16 @@
1
1
  [![Ruby tests](https://github.com/theforeman/foreman_rh_cloud/actions/workflows/ruby_tests.yml/badge.svg)](https://github.com/theforeman/foreman_rh_cloud/actions/workflows/ruby_tests.yml)
2
2
  [![JS](https://github.com/theforeman/foreman_rh_cloud/actions/workflows/js_tests.yml/badge.svg)](https://github.com/theforeman/foreman_rh_cloud/actions/workflows/js_tests.yml)
3
+ [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/theforeman/foreman_rh_cloud)
3
4
 
4
5
  # ForemanRhCloud
5
6
 
6
- *Introduction here*
7
-
8
7
  ## Installation
9
8
 
10
9
  See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
11
10
  for how to install Foreman plugins
12
11
 
13
- ## Usage
14
-
15
- *Usage here*
12
+ ## Project overview
13
+ See our [wiki](https://deepwiki.com/theforeman/foreman_rh_cloud)
16
14
 
17
15
  ### In Satellite
18
16
 
@@ -0,0 +1,33 @@
1
+ module InsightsCloud
2
+ module PackageProfileUploadExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ # This method explicitly listens on Katello actions
7
+ # rubocop:disable Rails/LexicallyScopedActionFilter
8
+ after_action :generate_host_report, only: [:upload_package_profile, :upload_profiles]
9
+ # rubocop:enable Rails/LexicallyScopedActionFilter
10
+ end
11
+
12
+ def generate_host_report
13
+ return unless ForemanRhCloud.with_local_advisor_engine?
14
+
15
+ logger.debug("Generating host-specific report for host #{@host.name}")
16
+
17
+ ForemanTasks.async_task(
18
+ ForemanInventoryUpload::Async::GenerateReportJob,
19
+ ForemanInventoryUpload.generated_reports_folder,
20
+ @host.organization_id,
21
+ false,
22
+ "id=#{@host.id}"
23
+ )
24
+
25
+ # in IoP case, the hosts are identified by the sub-man ID, and we can assume they already
26
+ # exist in the local inventory. This will also handle facet creation for new hosts.
27
+ return if @host.insights
28
+
29
+ insights_facet = @host.build_insights(uuid: @host.subscription_facet.uuid)
30
+ insights_facet.save
31
+ end
32
+ end
33
+ end
@@ -17,7 +17,7 @@ module InsightsCloud::Api
17
17
  def forward_request
18
18
  certs = candlepin_id_cert @organization
19
19
  begin
20
- @cloud_response = ::ForemanRhCloud::CloudRequestForwarder.new.forward_request(request, controller_name, @branch_id, certs)
20
+ @cloud_response = ::ForemanRhCloud::CloudRequestForwarder.new.forward_request(request, controller_name, @branch_id, certs, @host)
21
21
  rescue RestClient::Exceptions::Timeout => e
22
22
  response_obj = e.response.presence || e.exception
23
23
  return render json: { message: response_obj.to_s, error: response_obj.to_s }, status: :gateway_timeout
@@ -10,7 +10,8 @@ module ForemanRhCloud
10
10
  end
11
11
 
12
12
  def execute_cloud_request(params)
13
- certs = candlepin_id_cert(params.delete(:organization))
13
+ organization = params.delete(:organization)
14
+ certs = ForemanRhCloud.with_local_advisor_engine? ? foreman_certificate : candlepin_id_cert(organization)
14
15
  final_params = {
15
16
  ssl_client_cert: OpenSSL::X509::Certificate.new(certs[:cert]),
16
17
  ssl_client_key: OpenSSL::PKey.read(certs[:key]),
@@ -18,5 +19,12 @@ module ForemanRhCloud
18
19
 
19
20
  super(final_params)
20
21
  end
22
+
23
+ def foreman_certificate
24
+ @foreman_certificate ||= {
25
+ cert: File.read(Setting[:ssl_certificate]),
26
+ key: File.read(Setting[:ssl_priv_key]),
27
+ }
28
+ end
21
29
  end
22
30
  end
@@ -4,7 +4,7 @@ module ForemanRhCloud
4
4
  class CloudRequestForwarder
5
5
  include ForemanRhCloud::CloudRequest
6
6
 
7
- def forward_request(original_request, controller_name, branch_id, certs)
7
+ def forward_request(original_request, controller_name, branch_id, certs, host)
8
8
  forward_params = prepare_forward_params(original_request, branch_id)
9
9
  logger.debug("Request parameters for telemetry request: #{forward_params}")
10
10
 
@@ -12,14 +12,14 @@ module ForemanRhCloud
12
12
 
13
13
  logger.debug("User agent for telemetry is: #{http_user_agent original_request}")
14
14
 
15
- request_opts = prepare_request_opts(original_request, forward_payload, forward_params, certs)
15
+ request_opts = prepare_request_opts(original_request, forward_payload, forward_params, certs, host)
16
16
 
17
17
  logger.debug("Sending request to: #{request_opts[:url]}")
18
18
 
19
19
  execute_cloud_request(request_opts)
20
20
  end
21
21
 
22
- def prepare_request_opts(original_request, forward_payload, forward_params, certs)
22
+ def prepare_request_opts(original_request, forward_payload, forward_params, certs, host)
23
23
  base_params = {
24
24
  method: original_request.method,
25
25
  payload: forward_payload,
@@ -28,6 +28,7 @@ module ForemanRhCloud
28
28
  params: forward_params,
29
29
  user_agent: http_user_agent(original_request),
30
30
  content_type: original_request.media_type.presence || original_request.format.to_s,
31
+ Forwarded: prepare_forwarded_header(host),
31
32
  }
32
33
  ),
33
34
  }
@@ -105,6 +106,10 @@ module ForemanRhCloud
105
106
  headers
106
107
  end
107
108
 
109
+ def prepare_forwarded_header(host)
110
+ "for=\"_#{host.subscription_facet.uuid}\""
111
+ end
112
+
108
113
  def lightspeed?
109
114
  ->(request_path) { request_path.include? '/lightspeed' }
110
115
  end
@@ -3,3 +3,9 @@ attributes :uuid
3
3
  node :insights_hit_details do |facet|
4
4
  facet&.host&.facts('insights::hit_details')&.values&.first
5
5
  end
6
+ node :insights_hits_count do |facet|
7
+ facet.hits&.count
8
+ end
9
+ node :use_local_advisor_engine do |_facet|
10
+ ForemanRhCloud.with_local_advisor_engine?
11
+ end
@@ -5,18 +5,19 @@ module ForemanInventoryUpload
5
5
  "report_for_#{label}"
6
6
  end
7
7
 
8
- def plan(base_folder, organization_id, disconnected)
8
+ def plan(base_folder, organization_id, disconnected, hosts_filter = nil)
9
9
  sequence do
10
10
  super(
11
- GenerateReportJob.output_label(organization_id),
11
+ GenerateReportJob.output_label("#{organization_id}#{hosts_filter.empty? ? nil : "[#{hosts_filter.to_s.parameterize}]"}"),
12
12
  organization_id: organization_id,
13
- base_folder: base_folder
13
+ base_folder: base_folder,
14
+ hosts_filter: hosts_filter
14
15
  )
15
16
 
16
17
  plan_action(
17
18
  QueueForUploadJob,
18
19
  base_folder,
19
- ForemanInventoryUpload.facts_archive_name(organization_id),
20
+ ForemanInventoryUpload.facts_archive_name(organization_id, hosts_filter),
20
21
  organization_id,
21
22
  disconnected
22
23
  )
@@ -34,7 +35,8 @@ module ForemanInventoryUpload
34
35
  def env
35
36
  super.merge(
36
37
  'target' => base_folder,
37
- 'organization_id' => organization_id
38
+ 'organization_id' => organization_id,
39
+ 'hosts_filter' => hosts_filter
38
40
  )
39
41
  end
40
42
 
@@ -45,6 +47,10 @@ module ForemanInventoryUpload
45
47
  def organization_id
46
48
  input[:organization_id]
47
49
  end
50
+
51
+ def hosts_filter
52
+ input[:hosts_filter]
53
+ end
48
54
  end
49
55
  end
50
56
  end
@@ -33,8 +33,8 @@ module ForemanInventoryUpload
33
33
  end
34
34
 
35
35
  Tempfile.create([organization.name, '.pem']) do |cer_file|
36
- cer_file.write(rh_credentials[:cert])
37
- cer_file.write(rh_credentials[:key])
36
+ cer_file.write(certificate[:cert])
37
+ cer_file.write(certificate[:key])
38
38
  cer_file.flush
39
39
  @cer_path = cer_file.path
40
40
  super
@@ -59,8 +59,12 @@ module ForemanInventoryUpload
59
59
  env_vars
60
60
  end
61
61
 
62
- def rh_credentials
63
- @rh_credentials ||= begin
62
+ def certificate
63
+ ForemanRhCloud.with_local_advisor_engine? ? foreman_certificate : manifest_certificate
64
+ end
65
+
66
+ def manifest_certificate
67
+ @manifest_certificate ||= begin
64
68
  candlepin_id_certificate = organization.owner_details['upstreamConsumer']['idCert']
65
69
  {
66
70
  cert: candlepin_id_certificate['cert'],
@@ -69,6 +73,13 @@ module ForemanInventoryUpload
69
73
  end
70
74
  end
71
75
 
76
+ def foreman_certificate
77
+ @foreman_certificate ||= {
78
+ cert: File.read(Setting[:ssl_certificate]),
79
+ key: File.read(Setting[:ssl_priv_key]),
80
+ }
81
+ end
82
+
72
83
  def filename
73
84
  input[:filename]
74
85
  end
@@ -6,10 +6,10 @@ module ForemanInventoryUpload
6
6
  @logger = logger
7
7
  end
8
8
 
9
- def render(organization:)
9
+ def render(organization:, filter: nil)
10
10
  Dir.mktmpdir do |tmpdir|
11
11
  @logger.info "Started generating hosts report in #{tmpdir}"
12
- host_batches = ForemanInventoryUpload::Generators::Queries.for_org(organization)
12
+ host_batches = ForemanInventoryUpload::Generators::Queries.for_org(organization, hosts_query: filter || '')
13
13
  File.open(File.join(tmpdir, 'metadata.json'), 'w') do |metadata_out|
14
14
  metadata_generator = ForemanInventoryUpload::Generators::Metadata.new(metadata_out)
15
15
  metadata_generator.render do |inner_generator|
@@ -57,17 +57,39 @@ module ForemanInventoryUpload
57
57
  end
58
58
 
59
59
  def obfuscate_hostname?(host)
60
+ # Returns true if hostname obfuscation should be applied for a given host, based on hierarchy:
61
+ # 1. Global setting for hostname obfuscation.
62
+ return true if Setting[:obfuscate_inventory_hostnames]
63
+
60
64
  insights_client_setting = fact_value(host, 'insights_client::obfuscate_hostname_enabled')
61
65
  insights_client_setting = ActiveModel::Type::Boolean.new.cast(insights_client_setting)
62
- return insights_client_setting unless insights_client_setting.nil?
63
66
 
64
- Setting[:obfuscate_inventory_hostnames]
67
+ # 2. host fact reported by insights_client
68
+ # 3. if neither of the above, don't obfuscate.
69
+ insights_client_setting.nil? ? false : insights_client_setting
65
70
  end
66
71
 
67
72
  def fqdn(host)
68
- return host.fqdn unless obfuscate_hostname?(host)
69
-
70
- fact_value(host, 'insights_client::hostname') || obfuscate_fqdn(host.fqdn)
73
+ if obfuscate_hostname?(host)
74
+ # If obfuscation is enabled, attempt to retrieve an already obfuscated hostname
75
+ # from the 'insights_client::obfuscated_hostname' fact.
76
+ # Example format of `parsed_insights_array`:
77
+ # [{"original"=>"host.example.com", "obfuscated"=>"0dd449d0a027.example.com"},
78
+ # {"original"=>"satellite.example.com", "obfuscated"=>"host2.example.com"}]
79
+ begin
80
+ parsed_insights_array = JSON.parse(fact_value(host, 'insights_client::obfuscated_hostname') || '[]')
81
+ rescue JSON::ParserError
82
+ parsed_insights_array = []
83
+ end
84
+ # Obfuscate using the following hierarchy:
85
+ # 1. the obfuscated_hostname fact sent by insights_client
86
+ parsed_insights_item = parsed_insights_array.find { |item| item['original'] == host.fqdn }
87
+ # 2. our own helper method
88
+ parsed_insights_item&.[]('obfuscated') || obfuscate_fqdn(host.fqdn)
89
+ else
90
+ # If hostname obfuscation is not enabled for this host, return the host's original FQDN.
91
+ host.fqdn
92
+ end
71
93
  end
72
94
 
73
95
  def obfuscate_fqdn(fqdn)
@@ -75,35 +97,65 @@ module ForemanInventoryUpload
75
97
  end
76
98
 
77
99
  def obfuscate_ips?(host)
78
- insights_client_setting = fact_value(host, 'insights_client::obfuscate_ip_enabled')
79
- insights_client_setting = ActiveModel::Type::Boolean.new.cast(insights_client_setting)
80
- return insights_client_setting unless insights_client_setting.nil?
100
+ # Returns true if IP obfuscation should be applied for a given host, based on hierarchy:
101
+ # 1. Global setting for IP obfuscation.
102
+ return true if Setting[:obfuscate_inventory_ips]
81
103
 
82
- Setting[:obfuscate_inventory_ips]
104
+ insights_client_ipv4_setting = fact_value(host, 'insights_client::obfuscate_ipv4_enabled')
105
+ insights_client_ipv6_setting = fact_value(host, 'insights_client::obfuscate_ipv6_enabled')
106
+
107
+ cast_ipv4_setting = ActiveModel::Type::Boolean.new.cast(insights_client_ipv4_setting)
108
+ cast_ipv6_setting = ActiveModel::Type::Boolean.new.cast(insights_client_ipv6_setting)
109
+
110
+ # 2. The host's IPv4 or IPv6 obfuscation fact value is true
111
+ # 3. If neither of the above, don't obfuscate.
112
+ cast_ipv4_setting || cast_ipv6_setting || false
83
113
  end
84
114
 
85
115
  def host_ips(host)
116
+ # Determines and returns the IP addresses associated with a host, applying obfuscation if enabled.
117
+
118
+ # If IP obfuscation is enabled for the host return a representation of obfuscated IP addresses.
86
119
  return obfuscated_ips(host) if obfuscate_ips?(host)
87
120
 
88
- # return a pass through proxy hash in case no obfuscation needed
121
+ # If IP obfuscation is NOT needed, return a special kind of Hash.
122
+ # where when you try to access a key in it
123
+ # if the key doesn't exist, it simply returns the key itself.
124
+ # This is useful because it means if you try to get an IP from this hash,
125
+ # you'll just get the original IP back. It allows the calling code to
126
+ # use the same interface whether obfuscation is applied or not.
89
127
  Hash.new { |h, k| k }
90
128
  end
91
129
 
92
130
  def obfuscated_ips(host)
93
- insights_client_ips = JSON.parse(fact_value(host, 'insights_client::ips') || '[]')
131
+ # Example format of `parsed_insights_array`:
132
+ # [{"original": "192.168.1.10", "obfuscated": "10.230.230.1"},
133
+ # {"original": "192.168.1.11", "obfuscated": "10.230.230.2"}]
134
+ begin
135
+ parsed_insights_array = JSON.parse(fact_value(host, 'insights_client::obfuscated_ipv4') || '[]')
136
+ rescue JSON::ParserError
137
+ parsed_insights_array = []
138
+ end
94
139
 
140
+ # Create a new Hash to store the mapping from original IP addresses to their obfuscated versions.
141
+ # where the 'original' IP is the key and the 'obfuscated' IP is the value.
95
142
  obfuscated_ips = Hash[
96
- insights_client_ips.map { |ip_record| [ip_record['original'], ip_record['obfuscated']] }
143
+ parsed_insights_array.map { |ip_record| [ip_record['original'], ip_record['obfuscated']] }
97
144
  ]
98
145
 
146
+ # Sets a default proc for the obfuscated_ips hash.
147
+ # When a key is accessed that does not exist in the hash, this proc is called.
148
+ # It assigns the result of obfuscate_ip(key, hash) to the missing key in the hash.
149
+ # This ensures that any missing IP address key will be obfuscated and stored automatically.
99
150
  obfuscated_ips.default_proc = proc do |hash, key|
100
151
  hash[key] = obfuscate_ip(key, hash)
101
152
  end
102
-
103
153
  obfuscated_ips
104
154
  end
105
155
 
106
156
  def obfuscate_ip(ip, ips_dict)
157
+ # Produce a new, unique obfuscated IP that is
158
+ # numerically one greater than the highest existing obfuscated IP
107
159
  max_obfuscated = ips_dict.values.map { |v| IPAddr.new(v).to_i }.max || IPAddr.new('10.230.230.0').to_i
108
160
 
109
161
  IPAddr.new(max_obfuscated + 1, Socket::AF_INET).to_s
@@ -26,9 +26,11 @@ module ForemanInventoryUpload
26
26
  'dmi::system::product_name',
27
27
  'dmi::chassis::asset_tag',
28
28
  'insights_client::obfuscate_hostname_enabled',
29
- 'insights_client::obfuscate_ip_enabled',
30
- 'insights_client::hostname',
31
- 'insights_client::ips',
29
+ 'insights_client::obfuscate_ipv4_enabled',
30
+ 'insights_client::obfuscate_ipv6_enabled',
31
+ 'insights_client::obfuscated_ipv4',
32
+ 'insights_client::obfuscated_ipv6',
33
+ 'insights_client::obfuscated_hostname',
32
34
  'insights_id',
33
35
  'conversions::activity',
34
36
  'conversions::packages::0::nevra',
@@ -58,8 +60,8 @@ module ForemanInventoryUpload
58
60
  )
59
61
  end
60
62
 
61
- def self.for_org(organization_id, use_batches: true)
62
- base_query = for_slice(Host.unscoped.where(organization_id: organization_id))
63
+ def self.for_org(organization_id, use_batches: true, hosts_query: '')
64
+ base_query = for_slice(Host.unscoped.where(organization_id: organization_id).search_for(hosts_query))
63
65
  use_batches ? base_query.in_batches(of: ForemanInventoryUpload.slice_size) : base_query
64
66
  end
65
67
  end
@@ -190,7 +190,6 @@ module ForemanInventoryUpload
190
190
  ) { |v| os_release_value(*v) }
191
191
  @stream.simple_field('os_kernel_version', fact_value(host, 'uname::release'))
192
192
  @stream.simple_field('arch', host.architecture&.name)
193
- @stream.simple_field('katello_agent_running', false)
194
193
  @stream.simple_field(
195
194
  'infrastructure_type',
196
195
  ActiveModel::Type::Boolean.new.cast(fact_value(host, 'virt::is_guest')) ? 'virtual' : 'physical'
@@ -52,8 +52,8 @@ module ForemanInventoryUpload
52
52
  'uploader.sh'
53
53
  end
54
54
 
55
- def self.facts_archive_name(organization)
56
- "report_for_#{organization}.tar.xz"
55
+ def self.facts_archive_name(organization, filter = nil)
56
+ "report_for_#{organization}#{filter.empty? ? nil : "[#{filter.to_s.parameterize}]"}.tar.xz"
57
57
  end
58
58
 
59
59
  def self.upload_url
@@ -114,8 +114,7 @@ module ForemanRhCloud
114
114
  caption: N_('Inventory Upload'),
115
115
  url: '/foreman_rh_cloud/inventory_upload',
116
116
  url_hash: { controller: :react, action: :index },
117
- parent: :insights_menu,
118
- if: -> { !ForemanRhCloud.with_local_advisor_engine? }
117
+ parent: :insights_menu
119
118
  menu :top_menu, :insights_hits, caption: N_('Recommendations'), url: '/foreman_rh_cloud/insights_cloud', url_hash: { controller: :react, action: :index }, parent: :insights_menu
120
119
  menu :top_menu,
121
120
  :insights_vulnerability,
@@ -165,6 +164,8 @@ module ForemanRhCloud
165
164
  ::Katello::UINotifications::Subscriptions::ManifestImportSuccess.include ForemanInventoryUpload::Notifications::ManifestImportSuccessNotificationOverride if defined?(Katello)
166
165
 
167
166
  ::Host::Managed.include RhCloudHost
167
+
168
+ ::Katello::Api::Rhsm::CandlepinDynflowProxyController.include InsightsCloud::PackageProfileUploadExtensions
168
169
  end
169
170
  end
170
171
 
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '12.1.3'.freeze
2
+ VERSION = '12.1.4'.freeze
3
3
  end
@@ -8,6 +8,8 @@ module InventorySync
8
8
  set_callback :step, :around, :create_missing_hosts
9
9
 
10
10
  def plan(organizations)
11
+ # Do not run for local advisor, since we use sub-man id to identify hosts.
12
+ return if ForemanRhCloud.with_local_advisor_engine?
11
13
  # by default the tasks will be executed concurrently
12
14
  super(organizations)
13
15
  plan_self_host_sync
@@ -26,6 +26,7 @@ namespace :rh_cloud_inventory do
26
26
  task generate: :environment do
27
27
  organizations = [ENV['organization_id']]
28
28
  base_folder = ENV['target'] || Dir.pwd
29
+ filter = ENV['hosts_filter']
29
30
 
30
31
  unless File.writable?(base_folder)
31
32
  puts "#{base_folder} is not writable by the current process"
@@ -40,9 +41,9 @@ namespace :rh_cloud_inventory do
40
41
 
41
42
  User.as_anonymous_admin do
42
43
  organizations.each do |organization|
43
- target = File.join(base_folder, ForemanInventoryUpload.facts_archive_name(organization))
44
+ target = File.join(base_folder, ForemanInventoryUpload.facts_archive_name(organization, filter))
44
45
  archived_report_generator = ForemanInventoryUpload::Generators::ArchivedReport.new(target, Logger.new(STDOUT))
45
- archived_report_generator.render(organization: organization)
46
+ archived_report_generator.render(organization: organization, filter: filter)
46
47
  puts "Successfully generated #{target} for organization id #{organization}"
47
48
  end
48
49
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "12.1.3",
3
+ "version": "12.1.4",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -50,7 +50,7 @@ class ArchivedReportGeneratorTest < ActiveSupport::TestCase
50
50
  batches = Host.where(id: @host.id).in_batches
51
51
  test_org = FactoryBot.create(:organization)
52
52
 
53
- ForemanInventoryUpload::Generators::Queries.expects(:for_org).with(test_org.id).returns(batches)
53
+ ForemanInventoryUpload::Generators::Queries.expects(:for_org).with(test_org.id, hosts_query: '').returns(batches)
54
54
  ForemanInventoryUpload::Generators::Slice.any_instance.stubs(:golden_ticket?).returns(false)
55
55
  Dir.mktmpdir do |tmpdir|
56
56
  target = File.join(tmpdir, 'test.tar.gz')
@@ -1,4 +1,5 @@
1
1
  require 'test_plugin_helper'
2
+ require 'digest'
2
3
 
3
4
  class FactHelpersTest < ActiveSupport::TestCase
4
5
  class FactsHelpersTestStub
@@ -29,7 +30,7 @@ class FactHelpersTest < ActiveSupport::TestCase
29
30
 
30
31
  test 'obfuscates ips with insights-client data' do
31
32
  host = mock('host')
32
- @instance.expects(:fact_value).with(host, 'insights_client::ips').returns(
33
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns(
33
34
  '[{"obfuscated": "10.230.230.1", "original": "224.0.0.1"}, {"obfuscated": "10.230.230.255", "original": "224.0.0.251"}]'
34
35
  )
35
36
 
@@ -41,11 +42,275 @@ class FactHelpersTest < ActiveSupport::TestCase
41
42
 
42
43
  test 'obfuscates ips without insights-client data' do
43
44
  host = mock('host')
44
- @instance.expects(:fact_value).with(host, 'insights_client::ips').returns(nil)
45
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns(nil)
45
46
 
46
47
  actual = @instance.obfuscated_ips(host)
47
48
 
48
49
  assert_equal '10.230.230.1', actual['224.0.0.1']
49
50
  assert_equal '10.230.230.2', actual['224.0.0.2']
50
51
  end
52
+
53
+ describe 'obfuscate_hostname?' do
54
+ test 'returns true when global setting is enabled' do
55
+ Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(true)
56
+ host = mock('host')
57
+
58
+ result = @instance.obfuscate_hostname?(host)
59
+
60
+ assert result
61
+ end
62
+
63
+ test 'returns false when global setting is disabled and no host-specific setting' do
64
+ Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(false)
65
+ host = mock('host')
66
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_hostname_enabled').returns(nil)
67
+
68
+ result = @instance.obfuscate_hostname?(host)
69
+
70
+ refute result
71
+ end
72
+
73
+ test 'returns true when host-specific setting is enabled' do
74
+ Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(false)
75
+ host = mock('host')
76
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_hostname_enabled').returns('true')
77
+
78
+ result = @instance.obfuscate_hostname?(host)
79
+
80
+ assert result
81
+ end
82
+
83
+ test 'returns false when host-specific setting is disabled' do
84
+ Setting.expects(:[]).with(:obfuscate_inventory_hostnames).returns(false)
85
+ host = mock('host')
86
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_hostname_enabled').returns('false')
87
+
88
+ result = @instance.obfuscate_hostname?(host)
89
+
90
+ refute result
91
+ end
92
+ end
93
+
94
+ describe 'fqdn' do
95
+ test 'returns original fqdn when obfuscation is disabled' do
96
+ host = mock('host')
97
+ host.expects(:fqdn).returns('test.example.com')
98
+ @instance.expects(:obfuscate_hostname?).with(host).returns(false)
99
+
100
+ result = @instance.fqdn(host)
101
+
102
+ assert_equal 'test.example.com', result
103
+ end
104
+
105
+ test 'returns obfuscated hostname from insights_client fact when available' do
106
+ host = mock('host')
107
+ host.expects(:fqdn).returns('test.example.com').once
108
+ @instance.expects(:obfuscate_hostname?).with(host).returns(true)
109
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns(
110
+ '[{"original": "test.example.com", "obfuscated": "abc123.example.com"}]'
111
+ )
112
+
113
+ result = @instance.fqdn(host)
114
+
115
+ assert_equal 'abc123.example.com', result
116
+ end
117
+
118
+ test 'returns dynamically obfuscated hostname when insights_client fact is not available' do
119
+ host = mock('host')
120
+ host.stubs(:fqdn).returns('test.example.com')
121
+ @instance.expects(:obfuscate_hostname?).with(host).returns(true)
122
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns(nil)
123
+
124
+ result = @instance.fqdn(host)
125
+
126
+ expected = "#{Digest::SHA1.hexdigest('test.example.com')}.example.com"
127
+ assert_equal expected, result
128
+ end
129
+
130
+ test 'returns dynamically obfuscated hostname when insights_client fact does not contain matching host' do
131
+ host = mock('host')
132
+ host.expects(:fqdn).returns('test.example.com').twice
133
+ @instance.expects(:obfuscate_hostname?).with(host).returns(true)
134
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns(
135
+ '[{"original": "other.example.com", "obfuscated": "abc123.example.com"}]'
136
+ )
137
+ @instance.expects(:obfuscate_fqdn).with('test.example.com').returns('dynamically_obfuscated.example.com')
138
+
139
+ result = @instance.fqdn(host)
140
+
141
+ assert_equal 'dynamically_obfuscated.example.com', result
142
+ end
143
+
144
+ test 'handles invalid JSON in insights_client fact gracefully' do
145
+ host = mock('host')
146
+ host.stubs(:fqdn).returns('test.example.com')
147
+ @instance.expects(:obfuscate_hostname?).with(host).returns(true)
148
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns('invalid json')
149
+
150
+ result = @instance.fqdn(host)
151
+
152
+ expected = "#{Digest::SHA1.hexdigest('test.example.com')}.example.com"
153
+ assert_equal expected, result
154
+ end
155
+
156
+ test 'handles empty insights_client fact' do
157
+ host = mock('host')
158
+ host.stubs(:fqdn).returns('test.example.com')
159
+ @instance.expects(:obfuscate_hostname?).with(host).returns(true)
160
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_hostname').returns('[]')
161
+
162
+ result = @instance.fqdn(host)
163
+
164
+ expected = "#{Digest::SHA1.hexdigest('test.example.com')}.example.com"
165
+ assert_equal expected, result
166
+ end
167
+ end
168
+
169
+ describe 'obfuscate_ips?' do
170
+ test 'returns true when global setting is enabled' do
171
+ Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(true)
172
+ host = mock('host')
173
+
174
+ result = @instance.obfuscate_ips?(host)
175
+
176
+ assert result
177
+ end
178
+
179
+ test 'returns false when global setting is disabled and no host-specific settings' do
180
+ Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false)
181
+ host = mock('host')
182
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns(nil)
183
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns(nil)
184
+
185
+ result = @instance.obfuscate_ips?(host)
186
+
187
+ refute result
188
+ end
189
+
190
+ test 'returns true when host-specific IPv4 setting is enabled' do
191
+ Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false)
192
+ host = mock('host')
193
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns('true')
194
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns(nil)
195
+
196
+ result = @instance.obfuscate_ips?(host)
197
+
198
+ assert result
199
+ end
200
+
201
+ test 'returns true when host-specific IPv6 setting is enabled' do
202
+ Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false)
203
+ host = mock('host')
204
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns(nil)
205
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns('true')
206
+
207
+ result = @instance.obfuscate_ips?(host)
208
+
209
+ assert result
210
+ end
211
+
212
+ test 'returns true when both IPv4 and IPv6 settings are enabled' do
213
+ Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false)
214
+ host = mock('host')
215
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns('true')
216
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns('true')
217
+
218
+ result = @instance.obfuscate_ips?(host)
219
+
220
+ assert result
221
+ end
222
+
223
+ test 'returns false when both IPv4 and IPv6 settings are disabled' do
224
+ Setting.expects(:[]).with(:obfuscate_inventory_ips).returns(false)
225
+ host = mock('host')
226
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv4_enabled').returns('false')
227
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscate_ipv6_enabled').returns('false')
228
+
229
+ result = @instance.obfuscate_ips?(host)
230
+
231
+ refute result
232
+ end
233
+ end
234
+
235
+ describe 'obfuscate_ip' do
236
+ test 'generates first IP when no existing obfuscated IPs' do
237
+ ips_dict = {}
238
+
239
+ result = @instance.obfuscate_ip('192.168.1.1', ips_dict)
240
+
241
+ assert_equal '10.230.230.1', result
242
+ end
243
+
244
+ test 'generates next sequential IP when existing obfuscated IPs present' do
245
+ ips_dict = { '192.168.1.1' => '10.230.230.5', '192.168.1.2' => '10.230.230.10' }
246
+
247
+ result = @instance.obfuscate_ip('192.168.1.3', ips_dict)
248
+
249
+ assert_equal '10.230.230.11', result
250
+ end
251
+
252
+ test 'handles mixed IP ranges correctly' do
253
+ ips_dict = { '192.168.1.1' => '10.230.230.255', '192.168.1.2' => '10.230.230.1' }
254
+
255
+ result = @instance.obfuscate_ip('192.168.1.3', ips_dict)
256
+
257
+ assert_equal '10.230.231.0', result
258
+ end
259
+
260
+ test 'generates valid IP addresses' do
261
+ ips_dict = {}
262
+
263
+ result = @instance.obfuscate_ip('any.ip.address', ips_dict)
264
+
265
+ assert_match(/\A\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/, result)
266
+ assert_nothing_raised { IPAddr.new(result) }
267
+ end
268
+ end
269
+
270
+ describe 'obfuscated_ips' do
271
+ test 'handles invalid JSON in insights_client fact gracefully' do
272
+ host = mock('host')
273
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns('invalid json')
274
+
275
+ result = @instance.obfuscated_ips(host)
276
+
277
+ assert_equal '10.230.230.1', result['192.168.1.1']
278
+ end
279
+
280
+ test 'handles empty insights_client fact' do
281
+ host = mock('host')
282
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns('[]')
283
+
284
+ result = @instance.obfuscated_ips(host)
285
+
286
+ assert_equal '10.230.230.1', result['192.168.1.1']
287
+ end
288
+
289
+ test 'preserves existing obfuscated IPs and generates new ones' do
290
+ host = mock('host')
291
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns(
292
+ '[{"original": "192.168.1.1", "obfuscated": "10.230.230.5"}]'
293
+ )
294
+
295
+ result = @instance.obfuscated_ips(host)
296
+
297
+ assert_equal '10.230.230.5', result['192.168.1.1']
298
+ assert_equal '10.230.230.6', result['192.168.1.2']
299
+ end
300
+
301
+ test 'default_proc generates unique sequential IPs' do
302
+ host = mock('host')
303
+ @instance.expects(:fact_value).with(host, 'insights_client::obfuscated_ipv4').returns(nil)
304
+
305
+ result = @instance.obfuscated_ips(host)
306
+
307
+ ip1 = result['192.168.1.1']
308
+ ip2 = result['192.168.1.2']
309
+ ip3 = result['192.168.1.3']
310
+
311
+ assert_equal '10.230.230.1', ip1
312
+ assert_equal '10.230.230.2', ip2
313
+ assert_equal '10.230.230.3', ip3
314
+ end
315
+ end
51
316
  end
@@ -3,6 +3,7 @@ require 'puma/null_io'
3
3
 
4
4
  class CloudRequestForwarderTest < ActiveSupport::TestCase
5
5
  include MockCerts
6
+ include KatelloCVEHelper
6
7
 
7
8
  setup do
8
9
  @forwarder = ::ForemanRhCloud::CloudRequestForwarder.new
@@ -10,6 +11,22 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase
10
11
  ForemanRhCloud.stubs(:base_url).returns('https://cloud.example.com')
11
12
  ForemanRhCloud.stubs(:cert_base_url).returns('https://cert.cloud.example.com')
12
13
  ForemanRhCloud.stubs(:legacy_insights_url).returns('https://cert-api.access.example.com')
14
+
15
+ UpstreamOnlySettingsTestHelper.set_if_available('allow_multiple_content_views')
16
+ env = FactoryBot.create(:katello_k_t_environment)
17
+ env2 = FactoryBot.create(:katello_k_t_environment, organization: env.organization)
18
+
19
+ @host = FactoryBot.create(
20
+ :host,
21
+ :with_subscription,
22
+ :with_content,
23
+ :with_hostgroup,
24
+ :with_parameter,
25
+ content_view_environments: [make_cve(lifecycle_environment: env), make_cve(lifecycle_environment: env2)],
26
+ organization: env.organization
27
+ )
28
+
29
+ @host.subscription_facet.pools << FactoryBot.create(:katello_pool, account_number: '5678', cp_id: 1)
13
30
  end
14
31
 
15
32
  test 'should prepare correct cloud url' do
@@ -150,7 +167,7 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase
150
167
  'action_dispatch.request.query_parameters' => params
151
168
  )
152
169
 
153
- actual = @forwarder.prepare_request_opts(req, 'TEST PAYLOAD', params, generate_certs_hash)
170
+ actual = @forwarder.prepare_request_opts(req, 'TEST PAYLOAD', params, generate_certs_hash, @host)
154
171
 
155
172
  assert_match /foo/, actual[:headers][:user_agent]
156
173
  assert_match /bar/, actual[:headers][:user_agent]
@@ -175,7 +192,7 @@ class CloudRequestForwarderTest < ActiveSupport::TestCase
175
192
  'action_dispatch.request.query_parameters' => params
176
193
  )
177
194
 
178
- actual = @forwarder.prepare_request_opts(req, 'TEST PAYLOAD', params, generate_certs_hash)
195
+ actual = @forwarder.prepare_request_opts(req, 'TEST PAYLOAD', params, generate_certs_hash, @host)
179
196
 
180
197
  assert_match /text\/html/, actual[:headers][:content_type]
181
198
  end
@@ -64,9 +64,10 @@ class SliceGeneratorTest < ActiveSupport::TestCase
64
64
  'dmi::system::product_name',
65
65
  'dmi::chassis::asset_tag',
66
66
  'insights_client::obfuscate_hostname_enabled',
67
+ 'insights_client::obfuscated_hostname',
68
+ 'insights_client::obfuscate_ipv4_enabled',
69
+ 'insights_client::obfuscated_ipv4',
67
70
  'insights_client::hostname',
68
- 'insights_client::obfuscate_ip_enabled',
69
- 'insights_client::ips',
70
71
  'insights_id',
71
72
  ]
72
73
  end
@@ -418,14 +419,38 @@ class SliceGeneratorTest < ActiveSupport::TestCase
418
419
  assert_equal 1, generator.hosts_count
419
420
  end
420
421
 
421
- test 'generates obfuscated ip_address fields with inisghts-client' do
422
+ test 'does not obfuscate fqdn when insights_client obfuscate_hostname_enabled fact is missing and obfuscate_inventory_hostnames setting is false' do
423
+ # Create a host and obfuscated_hostname fact, but do NOT create the obfuscate_hostname_enabled fact
424
+ obfuscated_hostname_data = [
425
+ { 'original' => @host.fqdn, 'obfuscated' => '0dd449d0a027.example.com' },
426
+ ]
427
+ obfuscated_hostname_value = JSON.generate(obfuscated_hostname_data)
428
+ FactoryBot.create(:fact_value,
429
+ fact_name: fact_names['insights_client::obfuscated_hostname'],
430
+ value: obfuscated_hostname_value,
431
+ host: @host)
432
+ # Do NOT create the 'insights_client::obfuscate_hostname_enabled' fact
433
+
434
+ batch = Host.where(id: @host.id).in_batches.first
435
+ generator = create_generator(batch)
436
+
437
+ json_str = generator.render
438
+ actual = JSON.parse(json_str.join("\n"))
439
+
440
+ assert_not_nil(actual_host = actual['hosts'].first)
441
+ assert_equal @host.fqdn, actual_host['fqdn'], "FQDN should not be obfuscated when obfuscate_hostname_enabled is missing and setting is false"
442
+ assert_not_nil(actual_facts = actual_host['facts'].first['facts'])
443
+ assert_not_equal true, actual_facts['is_hostname_obfuscated']
444
+ end
445
+
446
+ test 'generates obfuscated ip_address fields when insights-client facts are present' do
422
447
  nic = FactoryBot.build(:nic_managed)
423
448
  @host.interfaces << nic
424
449
 
425
- FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscate_ip_enabled'], value: 'true', host: @host)
450
+ FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscate_ipv4_enabled'], value: 'true', host: @host)
426
451
  FactoryBot.create(
427
452
  :fact_value,
428
- fact_name: fact_names['insights_client::ips'],
453
+ fact_name: fact_names['insights_client::obfuscated_ipv4'],
429
454
  value: "[{\"obfuscated\": \"10.230.230.100\", \"original\": \"#{nic.ip}\"}]",
430
455
  host: @host
431
456
  )
@@ -448,9 +473,17 @@ class SliceGeneratorTest < ActiveSupport::TestCase
448
473
  assert_equal 1, generator.hosts_count
449
474
  end
450
475
 
451
- test 'obfuscates fqdn when instructed by insights-client' do
476
+ test 'obfuscates fqdn when insights-client facts are present' do
477
+ obfuscated_hostname_data = [
478
+ { 'original' => @host.fqdn, 'obfuscated' => '0dd449d0a027.example.com' },
479
+ { 'original' => 'satellite.theforeman.org', 'obfuscated' => 'host2.example.com' },
480
+ ]
481
+ obfuscated_hostname_value = JSON.generate(obfuscated_hostname_data)
482
+ FactoryBot.create(:fact_value,
483
+ fact_name: fact_names['insights_client::obfuscated_hostname'],
484
+ value: obfuscated_hostname_value,
485
+ host: @host)
452
486
  FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscate_hostname_enabled'], value: 'true', host: @host)
453
- FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::hostname'], value: 'obfuscated_name', host: @host)
454
487
 
455
488
  batch = Host.where(id: @host.id).in_batches.first
456
489
  generator = create_generator(batch)
@@ -460,7 +493,7 @@ class SliceGeneratorTest < ActiveSupport::TestCase
460
493
 
461
494
  assert_equal '00000000-0000-0000-0000-000000000000', actual['report_slice_id']
462
495
  assert_not_nil(actual_host = actual['hosts'].first)
463
- assert_equal 'obfuscated_name', actual_host['fqdn']
496
+ assert_equal obfuscated_hostname_data.first['obfuscated'], actual_host['fqdn']
464
497
  assert_equal '1234', actual_host['account']
465
498
  assert_not_nil(actual_facts = actual_host['facts'].first['facts'])
466
499
  assert_equal true, actual_facts['is_hostname_obfuscated']
@@ -487,9 +520,35 @@ class SliceGeneratorTest < ActiveSupport::TestCase
487
520
  assert_equal 1, generator.hosts_count
488
521
  end
489
522
 
490
- test 'does not obfuscate fqdn when insights-client sets to false' do
523
+ test 'obfuscates host fqdn with insights-client when setting set' do
524
+ Setting[:obfuscate_inventory_hostnames] = true
525
+ FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::hostname'], value: @host.fqdn, host: @host)
526
+
527
+ batch = Host.where(id: @host.id).in_batches.first
528
+ generator = create_generator(batch)
529
+
530
+ json_str = generator.render
531
+ actual = JSON.parse(json_str.join("\n"))
532
+
533
+ obfuscated_fqdn = Digest::SHA1.hexdigest(@host.fqdn) + '.example.com'
534
+
535
+ assert_equal '00000000-0000-0000-0000-000000000000', actual['report_slice_id']
536
+ assert_not_nil(actual_host = actual['hosts'].first)
537
+ assert_equal obfuscated_fqdn, actual_host['fqdn']
538
+ assert_equal '1234', actual_host['account']
539
+ assert_not_nil(actual_facts = actual_host['facts'].first['facts'])
540
+ assert_equal true, actual_facts['is_hostname_obfuscated']
541
+ assert_equal 1, generator.hosts_count
542
+ end
543
+
544
+ test 'does not obfuscate fqdn when host fact from insights-client has a value of false' do
545
+ obfuscated_hostname_data = [
546
+ { 'original' => @host.fqdn, 'obfuscated' => '0dd449d0a027.example.com' },
547
+ { 'original' => 'satellite.theforeman.org', 'obfuscated' => 'host2.example.com' },
548
+ ]
549
+ obfuscated_hostname_value = JSON.generate(obfuscated_hostname_data)
491
550
  FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscate_hostname_enabled'], value: 'false', host: @host)
492
- FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::hostname'], value: 'obfuscated_name', host: @host)
551
+ FactoryBot.create(:fact_value, fact_name: fact_names['insights_client::obfuscated_hostname'], value: obfuscated_hostname_value, host: @host)
493
552
 
494
553
  batch = Host.where(id: @host.id).in_batches.first
495
554
  generator = create_generator(batch)
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { translate as __ } from 'foremanReact/common/I18n';
3
+ import { propsToCamelCase } from 'foremanReact/common/helpers';
4
+
5
+ const RecommendationsCell = hostDetails => {
6
+ const insightsAttributes = propsToCamelCase(
7
+ // eslint-disable-next-line camelcase
8
+ hostDetails?.insights_attributes ?? {}
9
+ );
10
+ // Local insights advisor
11
+ if (insightsAttributes.useLocalAdvisorEngine) {
12
+ // TODO: Replace this placeholder with the actual local advisor integration
13
+ return <span>Local advisor placeholder</span>;
14
+ }
15
+
16
+ // Hosted insights advisor
17
+ const { insightsHitsCount: hitsCount } = insightsAttributes;
18
+ if (hitsCount === undefined || hitsCount === null) return '—';
19
+ const hostname = hostDetails?.name;
20
+ const encodedHostname = encodeURIComponent(hostname);
21
+ const hitsUrl = `/foreman_rh_cloud/insights_cloud?search=hostname+%3D+${encodedHostname}`;
22
+ return <a href={hitsUrl}>{hitsCount}</a>;
23
+ };
24
+
25
+ const hostsIndexColumnExtensions = [
26
+ {
27
+ columnName: 'insights_recommendations_count',
28
+ title: __('Recommendations'),
29
+ wrapper: RecommendationsCell,
30
+ weight: 1500,
31
+ isSorted: true,
32
+ },
33
+ ];
34
+
35
+ hostsIndexColumnExtensions.forEach(column => {
36
+ column.tableName = 'hosts';
37
+ column.categoryName = 'Insights';
38
+ column.categoryKey = 'insights';
39
+ });
40
+
41
+ export default hostsIndexColumnExtensions;
@@ -1,7 +1,10 @@
1
+ import { registerColumns } from 'foremanReact/components/HostsIndex/Columns/core';
1
2
  import { registerReducers } from './ForemanRhCloudReducers';
2
3
  import { registerFills } from './ForemanRhCloudFills';
3
4
  import { registerRoutes } from './ForemanRhCloudPages';
5
+ import hostsIndexColumnExtensions from './ForemanColumnExtensions/index';
4
6
 
5
7
  registerReducers();
6
8
  registerFills();
7
9
  registerRoutes();
10
+ registerColumns(hostsIndexColumnExtensions);
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: 12.1.3
4
+ version: 12.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Red Hat Cloud team
@@ -100,6 +100,7 @@ files:
100
100
  - app/controllers/api/v2/rh_cloud/inventory_controller.rb
101
101
  - app/controllers/concerns/insights_cloud/candlepin_cache.rb
102
102
  - app/controllers/concerns/insights_cloud/client_authentication.rb
103
+ - app/controllers/concerns/insights_cloud/package_profile_upload_extensions.rb
103
104
  - app/controllers/concerns/inventory_upload/report_actions.rb
104
105
  - app/controllers/concerns/inventory_upload/task_actions.rb
105
106
  - app/controllers/foreman_inventory_upload/accounts_controller.rb
@@ -284,6 +285,7 @@ files:
284
285
  - webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js
285
286
  - webpack/CVEsHostDetailsTab/__tests__/CVEsHostDetailsTab.test.js
286
287
  - webpack/CVEsHostDetailsTab/index.js
288
+ - webpack/ForemanColumnExtensions/index.js
287
289
  - webpack/ForemanInventoryUpload/Components/AccountList/AccountList.fixtures.js
288
290
  - webpack/ForemanInventoryUpload/Components/AccountList/AccountList.js
289
291
  - webpack/ForemanInventoryUpload/Components/AccountList/AccountList.stories.js