Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"advisor",
"ansible",
"camelcase",
"cdn",
"checkbox",
"csrf",
"dropdown",
Expand All @@ -22,6 +23,7 @@
"href",
"ips",
"ipv4",
"dropdown",
"jed",
"katello",
"knowledgebase",
Expand All @@ -30,15 +32,15 @@
"nowrap",
"pid",
"redhat",
"redux",
"remediate",
"remediations",
"repo",
"rhc",
"scalprum",
"theforeman",
"tooltip",
"unmount",
"redux",
"dropdown"
"unmount"
],
"minLength": 3
}
Expand Down
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
[![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)
[![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)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/theforeman/foreman_rh_cloud)

# ForemanRhCloud

*Introduction here*

## Installation

See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin)
for how to install Foreman plugins

## Usage

*Usage here*
## Project overview
See our [wiki](https://deepwiki.com/theforeman/foreman_rh_cloud)

### In Satellite

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
module InsightsCloud
module PackageProfileUploadExtensions
extend ActiveSupport::Concern

included do
# This method explicitly listens on Katello actions
# rubocop:disable Rails/LexicallyScopedActionFilter
after_action :generate_host_report, only: [:upload_package_profile, :upload_profiles]
# rubocop:enable Rails/LexicallyScopedActionFilter
end

def generate_host_report
return unless ForemanRhCloud.with_local_advisor_engine?

logger.debug("Generating host-specific report for host #{@host.name}")

ForemanTasks.async_task(
ForemanInventoryUpload::Async::GenerateReportJob,
ForemanInventoryUpload.generated_reports_folder,
@host.organization_id,
false,
"id=#{@host.id}"
)

# in IoP case, the hosts are identified by the sub-man ID, and we can assume they already
# exist in the local inventory. This will also handle facet creation for new hosts.
return if @host.insights

insights_facet = @host.build_insights(uuid: @host.subscription_facet.uuid)
insights_facet.save
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class MachineTelemetriesController < ::Api::V2::BaseController
def forward_request
certs = candlepin_id_cert @organization
begin
@cloud_response = ::ForemanRhCloud::CloudRequestForwarder.new.forward_request(request, controller_name, @branch_id, certs)
@cloud_response = ::ForemanRhCloud::CloudRequestForwarder.new.forward_request(request, controller_name, @branch_id, certs, @host)
rescue RestClient::Exceptions::Timeout => e
response_obj = e.response.presence || e.exception
return render json: { message: response_obj.to_s, error: response_obj.to_s }, status: :gateway_timeout
Expand Down
10 changes: 9 additions & 1 deletion app/services/foreman_rh_cloud/cert_auth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ def cert_auth_available?(organization)
end

def execute_cloud_request(params)
certs = candlepin_id_cert(params.delete(:organization))
organization = params.delete(:organization)
certs = ForemanRhCloud.with_local_advisor_engine? ? foreman_certificate : candlepin_id_cert(organization)
final_params = {
ssl_client_cert: OpenSSL::X509::Certificate.new(certs[:cert]),
ssl_client_key: OpenSSL::PKey.read(certs[:key]),
}.deep_merge(params)

super(final_params)
end

def foreman_certificate
@foreman_certificate ||= {
cert: File.read(Setting[:ssl_certificate]),
key: File.read(Setting[:ssl_priv_key]),
}
end
end
end
11 changes: 8 additions & 3 deletions app/services/foreman_rh_cloud/cloud_request_forwarder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ module ForemanRhCloud
class CloudRequestForwarder
include ForemanRhCloud::CloudRequest

def forward_request(original_request, controller_name, branch_id, certs)
def forward_request(original_request, controller_name, branch_id, certs, host)
forward_params = prepare_forward_params(original_request, branch_id)
logger.debug("Request parameters for telemetry request: #{forward_params}")

forward_payload = prepare_forward_payload(original_request, controller_name)

logger.debug("User agent for telemetry is: #{http_user_agent original_request}")

request_opts = prepare_request_opts(original_request, forward_payload, forward_params, certs)
request_opts = prepare_request_opts(original_request, forward_payload, forward_params, certs, host)

logger.debug("Sending request to: #{request_opts[:url]}")

execute_cloud_request(request_opts)
end

def prepare_request_opts(original_request, forward_payload, forward_params, certs)
def prepare_request_opts(original_request, forward_payload, forward_params, certs, host)
base_params = {
method: original_request.method,
payload: forward_payload,
Expand All @@ -28,6 +28,7 @@ def prepare_request_opts(original_request, forward_payload, forward_params, cert
params: forward_params,
user_agent: http_user_agent(original_request),
content_type: original_request.media_type.presence || original_request.format.to_s,
Forwarded: prepare_forwarded_header(host),
}
),
}
Expand Down Expand Up @@ -105,6 +106,10 @@ def original_headers(original_request)
headers
end

def prepare_forwarded_header(host)
"for=\"_#{host.subscription_facet.uuid}\""
end

def lightspeed?
->(request_path) { request_path.include? '/lightspeed' }
end
Expand Down
4 changes: 2 additions & 2 deletions lib/foreman_inventory_upload.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ def self.upload_script_file
'uploader.sh'
end

def self.facts_archive_name(organization)
"report_for_#{organization}.tar.xz"
def self.facts_archive_name(organization, filter = nil)
"report_for_#{organization}#{filter.empty? ? nil : "[#{filter.to_s.parameterize}]"}.tar.xz"
end

def self.upload_url
Expand Down
16 changes: 11 additions & 5 deletions lib/foreman_inventory_upload/async/generate_report_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,19 @@ def self.output_label(label)
"report_for_#{label}"
end

def plan(base_folder, organization_id, disconnected)
def plan(base_folder, organization_id, disconnected, hosts_filter = nil)
sequence do
super(
GenerateReportJob.output_label(organization_id),
GenerateReportJob.output_label("#{organization_id}#{hosts_filter.empty? ? nil : "[#{hosts_filter.to_s.parameterize}]"}"),
organization_id: organization_id,
base_folder: base_folder
base_folder: base_folder,
hosts_filter: hosts_filter
)

plan_action(
QueueForUploadJob,
base_folder,
ForemanInventoryUpload.facts_archive_name(organization_id),
ForemanInventoryUpload.facts_archive_name(organization_id, hosts_filter),
organization_id,
disconnected
)
Expand All @@ -34,7 +35,8 @@ def command
def env
super.merge(
'target' => base_folder,
'organization_id' => organization_id
'organization_id' => organization_id,
'hosts_filter' => hosts_filter
)
end

Expand All @@ -45,6 +47,10 @@ def base_folder
def organization_id
input[:organization_id]
end

def hosts_filter
input[:hosts_filter]
end
end
end
end
19 changes: 15 additions & 4 deletions lib/foreman_inventory_upload/async/upload_report_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ def try_execute
end

Tempfile.create([organization.name, '.pem']) do |cer_file|
cer_file.write(rh_credentials[:cert])
cer_file.write(rh_credentials[:key])
cer_file.write(certificate[:cert])
cer_file.write(certificate[:key])
cer_file.flush
@cer_path = cer_file.path
super
Expand All @@ -59,8 +59,12 @@ def env
env_vars
end

def rh_credentials
@rh_credentials ||= begin
def certificate
ForemanRhCloud.with_local_advisor_engine? ? foreman_certificate : manifest_certificate
end

def manifest_certificate
@manifest_certificate ||= begin
candlepin_id_certificate = organization.owner_details['upstreamConsumer']['idCert']
{
cert: candlepin_id_certificate['cert'],
Expand All @@ -69,6 +73,13 @@ def rh_credentials
end
end

def foreman_certificate
@foreman_certificate ||= {
cert: File.read(Setting[:ssl_certificate]),
key: File.read(Setting[:ssl_priv_key]),
}
end

def filename
input[:filename]
end
Expand Down
4 changes: 2 additions & 2 deletions lib/foreman_inventory_upload/generators/archived_report.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ def initialize(target, logger = Rails.logger)
@logger = logger
end

def render(organization:)
def render(organization:, filter: nil)
Dir.mktmpdir do |tmpdir|
@logger.info "Started generating hosts report in #{tmpdir}"
host_batches = ForemanInventoryUpload::Generators::Queries.for_org(organization)
host_batches = ForemanInventoryUpload::Generators::Queries.for_org(organization, hosts_query: filter || '')
File.open(File.join(tmpdir, 'metadata.json'), 'w') do |metadata_out|
metadata_generator = ForemanInventoryUpload::Generators::Metadata.new(metadata_out)
metadata_generator.render do |inner_generator|
Expand Down
78 changes: 65 additions & 13 deletions lib/foreman_inventory_upload/generators/fact_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,53 +57,105 @@ def cloud_provider(host)
end

def obfuscate_hostname?(host)
# Returns true if hostname obfuscation should be applied for a given host, based on hierarchy:
# 1. Global setting for hostname obfuscation.
return true if Setting[:obfuscate_inventory_hostnames]

insights_client_setting = fact_value(host, 'insights_client::obfuscate_hostname_enabled')
insights_client_setting = ActiveModel::Type::Boolean.new.cast(insights_client_setting)
return insights_client_setting unless insights_client_setting.nil?

Setting[:obfuscate_inventory_hostnames]
# 2. host fact reported by insights_client
# 3. if neither of the above, don't obfuscate.
insights_client_setting.nil? ? false : insights_client_setting
end

def fqdn(host)
return host.fqdn unless obfuscate_hostname?(host)

fact_value(host, 'insights_client::hostname') || obfuscate_fqdn(host.fqdn)
if obfuscate_hostname?(host)
# If obfuscation is enabled, attempt to retrieve an already obfuscated hostname
# from the 'insights_client::obfuscated_hostname' fact.
# Example format of `parsed_insights_array`:
# [{"original"=>"host.example.com", "obfuscated"=>"0dd449d0a027.example.com"},
# {"original"=>"satellite.example.com", "obfuscated"=>"host2.example.com"}]
begin
parsed_insights_array = JSON.parse(fact_value(host, 'insights_client::obfuscated_hostname') || '[]')
rescue JSON::ParserError
parsed_insights_array = []
end
# Obfuscate using the following hierarchy:
# 1. the obfuscated_hostname fact sent by insights_client
parsed_insights_item = parsed_insights_array.find { |item| item['original'] == host.fqdn }
# 2. our own helper method
parsed_insights_item&.[]('obfuscated') || obfuscate_fqdn(host.fqdn)
else
# If hostname obfuscation is not enabled for this host, return the host's original FQDN.
host.fqdn
end
end

def obfuscate_fqdn(fqdn)
"#{Digest::SHA1.hexdigest(fqdn)}.example.com"
end

def obfuscate_ips?(host)
insights_client_setting = fact_value(host, 'insights_client::obfuscate_ip_enabled')
insights_client_setting = ActiveModel::Type::Boolean.new.cast(insights_client_setting)
return insights_client_setting unless insights_client_setting.nil?
# Returns true if IP obfuscation should be applied for a given host, based on hierarchy:
# 1. Global setting for IP obfuscation.
return true if Setting[:obfuscate_inventory_ips]

Setting[:obfuscate_inventory_ips]
insights_client_ipv4_setting = fact_value(host, 'insights_client::obfuscate_ipv4_enabled')
insights_client_ipv6_setting = fact_value(host, 'insights_client::obfuscate_ipv6_enabled')

cast_ipv4_setting = ActiveModel::Type::Boolean.new.cast(insights_client_ipv4_setting)
cast_ipv6_setting = ActiveModel::Type::Boolean.new.cast(insights_client_ipv6_setting)

# 2. The host's IPv4 or IPv6 obfuscation fact value is true
# 3. If neither of the above, don't obfuscate.
cast_ipv4_setting || cast_ipv6_setting || false
end

def host_ips(host)
# Determines and returns the IP addresses associated with a host, applying obfuscation if enabled.

# If IP obfuscation is enabled for the host return a representation of obfuscated IP addresses.
return obfuscated_ips(host) if obfuscate_ips?(host)

# return a pass through proxy hash in case no obfuscation needed
# If IP obfuscation is NOT needed, return a special kind of Hash.
# where when you try to access a key in it
# if the key doesn't exist, it simply returns the key itself.
# This is useful because it means if you try to get an IP from this hash,
# you'll just get the original IP back. It allows the calling code to
# use the same interface whether obfuscation is applied or not.
Hash.new { |h, k| k }
end

def obfuscated_ips(host)
insights_client_ips = JSON.parse(fact_value(host, 'insights_client::ips') || '[]')
# Example format of `parsed_insights_array`:
# [{"original": "192.168.1.10", "obfuscated": "10.230.230.1"},
# {"original": "192.168.1.11", "obfuscated": "10.230.230.2"}]
begin
parsed_insights_array = JSON.parse(fact_value(host, 'insights_client::obfuscated_ipv4') || '[]')
rescue JSON::ParserError
parsed_insights_array = []
end

# Create a new Hash to store the mapping from original IP addresses to their obfuscated versions.
# where the 'original' IP is the key and the 'obfuscated' IP is the value.
obfuscated_ips = Hash[
insights_client_ips.map { |ip_record| [ip_record['original'], ip_record['obfuscated']] }
parsed_insights_array.map { |ip_record| [ip_record['original'], ip_record['obfuscated']] }
]

# Sets a default proc for the obfuscated_ips hash.
# When a key is accessed that does not exist in the hash, this proc is called.
# It assigns the result of obfuscate_ip(key, hash) to the missing key in the hash.
# This ensures that any missing IP address key will be obfuscated and stored automatically.
obfuscated_ips.default_proc = proc do |hash, key|
hash[key] = obfuscate_ip(key, hash)
end

obfuscated_ips
end

def obfuscate_ip(ip, ips_dict)
# Produce a new, unique obfuscated IP that is
# numerically one greater than the highest existing obfuscated IP
max_obfuscated = ips_dict.values.map { |v| IPAddr.new(v).to_i }.max || IPAddr.new('10.230.230.0').to_i

IPAddr.new(max_obfuscated + 1, Socket::AF_INET).to_s
Expand Down
Loading