diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0bc126dd2..17f8ca078 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -174,15 +174,20 @@ and configured to be able to access the Foreman server. 4. The rhcd instance needs to be registered in the `Sources` cloud registry as a valid listener. -This configuration process is initiated by the "Configure -cloud connector" button in the UI, and is implemented as a -REX job and a subscriber task. The REX job is initiated -with a specific feature, and executes a playbook from -`foreman-operations` collection. One of the steps in the -task is a request to Foreman server to persist the rhcd id. -The subscriber task waits for this task to finish and then -using the rhcd id from the REX job, it calls the Sources -API to properly subscribe the new rhcd instance. +This configuration process is handled by foremanctl +(`foremanctl deploy --add-feature cloud-connector`), which +installs the packages, templates the worker config, starts +the rhcd service, and sets the `rhc_instance_id` setting +via the Foreman API. After setup, foremanctl calls +`POST /api/v2/rh_cloud/announce_to_sources` to register +the rhcd instance in the Sources cloud registry. + +A daily recurring task (`CloudConnectorAnnounceTask`) also +checks Sources registration for self-healing — if the +registration is ever lost, it re-registers automatically. +The task skips organizations that have had recent +cloud-initiated remediation jobs (proving connectivity) +or that lack a valid manifest. Once the configuration phase is finished the remediation request will be able to flow from the cloud, through the diff --git a/CLAUDE.md b/CLAUDE.md index 06868e366..97955daeb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -159,6 +159,7 @@ Each namespace follows Rails directory structure (`app/{models,controllers,views - `InventorySync::Async::InventoryScheduledSync` - Daily inventory sync - `InsightsCloud::Async::InsightsScheduledSync` - Daily recommendations sync - `InsightsCloud::Async::InsightsClientStatusAging` - Daily status cleanup + - `InsightsCloud::Async::CloudConnectorAnnounceTask` - Daily Sources registration check (self-healing) **Key models:** - `InsightsRule` - Problem definitions from Red Hat Insights @@ -250,10 +251,13 @@ return ( ### Cloud Connector (Cloud-Initiated Remediations) -Requires setup on Foreman server: +Setup via foremanctl (`foremanctl deploy --add-feature cloud-connector`): - `rhcd` service (listener) - `yggdrasil-worker-forwarder` (worker) -- Registration in Red Hat Sources registry +- `rhc_instance_id` setting set via Foreman API +- Registration in Red Hat Sources registry via `POST /api/v2/rh_cloud/announce_to_sources` + +Sources registration is also checked daily by `CloudConnectorAnnounceTask` for self-healing. Flow: Cloud UI → rhcd → worker → Foreman API → REX job → playbook execution diff --git a/app/controllers/api/v2/rh_cloud/inventory_controller.rb b/app/controllers/api/v2/rh_cloud/inventory_controller.rb index c38eaf06b..5b6689daa 100644 --- a/app/controllers/api/v2/rh_cloud/inventory_controller.rb +++ b/app/controllers/api/v2/rh_cloud/inventory_controller.rb @@ -7,7 +7,7 @@ class InventoryController < ::Api::V2::BaseController include InventoryUpload::TaskActions include ForemanRhCloud::IopSmartProxyAccess - before_action :require_non_iop_smart_proxy, only: [:enable_cloud_connector] + before_action :require_non_iop_smart_proxy, only: [:announce_to_sources] api :GET, "/organizations/:organization_id/rh_cloud/report", N_("Download latest report") param :organization_id, Integer, required: true, desc: N_("Set the current organization context for the request") @@ -70,10 +70,13 @@ def sync_inventory_status render json: { message: error.message }, status: :bad_request end - api :POST, "/rh_cloud/enable_connector", N_("Enable cloud connector") - def enable_cloud_connector - cloud_connector = ForemanRhCloud::CloudConnector.new - render json: cloud_connector.install.to_json + api :POST, "/rh_cloud/announce_to_sources", N_("Register in Red Hat Sources for cloud connector") + def announce_to_sources + task = ForemanTasks.async_task(InsightsCloud::Async::CloudConnectorAnnounceTask, true) + + render json: { + task: task, + }, status: :ok end end end diff --git a/app/controllers/foreman_inventory_upload/accounts_controller.rb b/app/controllers/foreman_inventory_upload/accounts_controller.rb index c1adb0ac0..ebe000847 100644 --- a/app/controllers/foreman_inventory_upload/accounts_controller.rb +++ b/app/controllers/foreman_inventory_upload/accounts_controller.rb @@ -29,7 +29,6 @@ def index render json: { accounts: accounts, - CloudConnectorStatus: ForemanInventoryUpload::UploadsSettingsController.cloud_connector_status, }, status: :ok end diff --git a/app/controllers/foreman_inventory_upload/uploads_controller.rb b/app/controllers/foreman_inventory_upload/uploads_controller.rb index e1324ee45..d7dd04a04 100644 --- a/app/controllers/foreman_inventory_upload/uploads_controller.rb +++ b/app/controllers/foreman_inventory_upload/uploads_controller.rb @@ -1,22 +1,11 @@ module ForemanInventoryUpload class UploadsController < ::ApplicationController include InventoryUpload::ReportActions - include ForemanRhCloud::IopSmartProxyAccess - - before_action :require_non_iop_smart_proxy, only: [:enable_cloud_connector] def download_file filename, file = report_file(params[:organization_id]) send_file file, disposition: 'attachment', filename: filename end - - def enable_cloud_connector - # Set the autoupload to true, since it's required by the feature. - Setting[:allow_auto_inventory_upload] = true - - cloud_connector = ForemanRhCloud::CloudConnector.new - render json: cloud_connector.install.to_json - end end end diff --git a/app/controllers/foreman_inventory_upload/uploads_settings_controller.rb b/app/controllers/foreman_inventory_upload/uploads_settings_controller.rb index a6bd3636f..5848418c8 100644 --- a/app/controllers/foreman_inventory_upload/uploads_settings_controller.rb +++ b/app/controllers/foreman_inventory_upload/uploads_settings_controller.rb @@ -9,7 +9,6 @@ def index ipsObfuscationEnabled: Setting[:obfuscate_inventory_ips], excludePackagesEnabled: Setting[:exclude_installed_packages], allowAutoInsightsMismatchDelete: Setting[:allow_auto_insights_mismatch_delete], - CloudConnectorStatus: ForemanInventoryUpload::UploadsSettingsController.cloud_connector_status, lastSyncTask: last_successful_inventory_sync_task, }, status: :ok end @@ -19,13 +18,6 @@ def set_advanced_setting index end - def self.cloud_connector_status - cloud_connector = ForemanRhCloud::CloudConnector.new - job = cloud_connector&.latest_job - return nil unless job - { id: job.id, task: ForemanTasks::Task.where(:id => job.task_id).first } - end - def last_successful_inventory_sync_task task = ForemanTasks::Task.where(label: 'InventorySync::Async::InventoryFullSync', result: 'success') .reorder('ended_at desc').first diff --git a/app/services/foreman_rh_cloud/cloud_connector.rb b/app/services/foreman_rh_cloud/cloud_connector.rb deleted file mode 100644 index 5f8f2efae..000000000 --- a/app/services/foreman_rh_cloud/cloud_connector.rb +++ /dev/null @@ -1,74 +0,0 @@ -require 'securerandom' - -module ForemanRhCloud - class CloudConnector - CLOUD_CONNECTOR_USER = 'cloud_connector_user'.freeze - CLOUD_CONNECTOR_TOKEN_NAME = 'Cloud connector access token'.freeze - CLOUD_CONNECTOR_FEATURE = 'ansible_configure_cloud_connector'.freeze - - def install - user = service_user - token_value = personal_access_token(user) - target_host = foreman_host - composer = nil - - input = { - :satellite_cloud_connector_user => service_user.login, - :satellite_cloud_connector_password => token_value, - } - - if (http_proxy = ForemanRhCloud.proxy_setting) - input[:satellite_cloud_connector_http_proxy] = http_proxy - end - - Taxonomy.as_taxonomy(target_host.organization, target_host.location) do - composer = ::JobInvocationComposer.for_feature( - CLOUD_CONNECTOR_FEATURE, - [target_host.id], - input - ) - composer.trigger! - end - - composer.job_invocation - end - - def latest_job - feature_id = RemoteExecutionFeature.find_by_label(CLOUD_CONNECTOR_FEATURE)&.id - return nil unless feature_id - JobInvocation.where(:remote_execution_feature_id => feature_id).includes(:task).reorder('foreman_tasks_tasks.started_at DESC').first - end - - def personal_access_token(user) - access_token = PersonalAccessToken.find_by_name(CLOUD_CONNECTOR_TOKEN_NAME) - - access_token&.destroy! # destroy the old token if exists - - personal_access_token = PersonalAccessToken.new(:name => CLOUD_CONNECTOR_TOKEN_NAME, :user => user) - token_value = personal_access_token.generate_token - personal_access_token.save! - - token_value - end - - def service_user - user = User.find_by_login(CLOUD_CONNECTOR_USER) - - if user.nil? - user = User.create!( - :login => CLOUD_CONNECTOR_USER, - :password => SecureRandom.base64(255), - :description => "This is a service user used by cloud connector to talk to the Satellite API", - :auth_source => AuthSourceInternal.first, - :admin => true - ) - end - - user - end - - def foreman_host - ForemanRhCloud.foreman_host - end - end -end diff --git a/app/services/foreman_rh_cloud/cloud_presence.rb b/app/services/foreman_rh_cloud/cloud_presence.rb index 399008bbc..a23786064 100644 --- a/app/services/foreman_rh_cloud/cloud_presence.rb +++ b/app/services/foreman_rh_cloud/cloud_presence.rb @@ -74,6 +74,9 @@ def create_satellite_instance_source def register_rhc_instance raise Foreman::Exception.new('rhc_instance_id is empty, cannot register RHC to the cloud') if Setting[:rhc_instance_id].empty? + + return :already_registered if rhc_connection_exists? + source_id = satellite_instance_source || create_satellite_instance_source create_response = JSON.parse( @@ -95,6 +98,22 @@ def register_rhc_instance @satellite_instance_source = create_response['id'] end + def rhc_connection_exists? + response = JSON.parse( + execute_cloud_request( + method: :get, + url: rhc_connection_url, + headers: { + content_type: :json, + }, + ssl_client_cert: OpenSSL::X509::Certificate.new(certs[:cert]), + ssl_client_key: OpenSSL::PKey.read(certs[:key]) + ) + ) + + response['data']&.any? + end + private def sources_url(path) @@ -113,6 +132,10 @@ def create_satellite_instance_source_url sources_url('/sources') end + def rhc_connection_url + sources_url("/rhc_connections?filter[rhc_id]=#{Setting[:rhc_instance_id]}") + end + def create_rhc_connections_url sources_url('/rhc_connections') end diff --git a/config/routes.rb b/config/routes.rb index 93e60ae82..75ff617e3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -7,8 +7,6 @@ get 'settings', to: 'uploads_settings#index' post 'setting', to: 'uploads_settings#set_advanced_setting' - post 'cloud_connector', to: 'uploads#enable_cloud_connector' - resources :tasks, only: [:create, :show] get 'status', to: 'cloud_status#index' @@ -71,7 +69,7 @@ end namespace 'rh_cloud' do - post 'enable_connector', to: 'inventory#enable_cloud_connector' + post 'announce_to_sources', to: 'inventory#announce_to_sources' post 'cloud_request', to: 'cloud_request#update' get 'advisor_engine_config', to: 'advisor_engine_config#show' diff --git a/lib/foreman_rh_cloud/engine.rb b/lib/foreman_rh_cloud/engine.rb index f109ec58f..1d085f6ae 100644 --- a/lib/foreman_rh_cloud/engine.rb +++ b/lib/foreman_rh_cloud/engine.rb @@ -61,6 +61,7 @@ def self.register_scheduled_task(task_class, cronline) ForemanRhCloud::Engine.register_scheduled_task(InventorySync::Async::InventoryScheduledSync, '0 0 * * *') ForemanRhCloud::Engine.register_scheduled_task(InsightsCloud::Async::InsightsScheduledSync, '0 0 * * *') ForemanRhCloud::Engine.register_scheduled_task(InsightsCloud::Async::InsightsClientStatusAging, '0 0 * * *') + ForemanRhCloud::Engine.register_scheduled_task(InsightsCloud::Async::CloudConnectorAnnounceTask, '0 0 * * *') end end rescue ActiveRecord::NoDatabaseError @@ -80,12 +81,6 @@ def self.register_scheduled_task(task_class, cronline) host_action_button: false, provided_inputs: ['playbook_url', 'report_url', 'correlation_id', 'report_interval'] ) - RemoteExecutionFeature.register( - :ansible_configure_cloud_connector, - N_('Configure Cloud Connector on given hosts'), - :description => N_('Configure Cloud Connector on given hosts'), - :proxy_selector_override => ::RemoteExecutionProxySelector::INTERNAL_PROXY - ) end # Ideally this code belongs to an initializer. The problem is that Katello controllers are not initialized completely until after the end of the to_prepare blocks diff --git a/lib/foreman_rh_cloud/plugin.rb b/lib/foreman_rh_cloud/plugin.rb index 09505b0e2..51b73511c 100644 --- a/lib/foreman_rh_cloud/plugin.rb +++ b/lib/foreman_rh_cloud/plugin.rb @@ -31,8 +31,7 @@ def self.register :generate_foreman_rh_cloud, 'foreman_inventory_upload/reports': [:generate], 'foreman_inventory_upload/tasks': [:create], - 'api/v2/rh_cloud/inventory': [:get_hosts, :remove_hosts, :sync_inventory_status, :download_file, :generate_report, :enable_cloud_connector], - 'foreman_inventory_upload/uploads': [:enable_cloud_connector], + 'api/v2/rh_cloud/inventory': [:get_hosts, :remove_hosts, :sync_inventory_status, :download_file, :generate_report, :announce_to_sources], 'foreman_inventory_upload/uploads_settings': [:set_advanced_setting], 'foreman_inventory_upload/missing_hosts': [:remove_hosts], 'insights_cloud/settings': [:update], diff --git a/lib/insights_cloud/async/cloud_connector_announce_task.rb b/lib/insights_cloud/async/cloud_connector_announce_task.rb index b84e664e9..368630579 100644 --- a/lib/insights_cloud/async/cloud_connector_announce_task.rb +++ b/lib/insights_cloud/async/cloud_connector_announce_task.rb @@ -1,39 +1,96 @@ module InsightsCloud module Async class CloudConnectorAnnounceTask < ::Actions::EntryAction - def self.subscribe - Actions::RemoteExecution::RunHostsJob - end + include ::Actions::RecurringAction + include ::ForemanRhCloud::CertAuth + include ForemanInventoryUpload::Async::DelayedStart - def self.connector_feature_id - @connector_feature_id ||= RemoteExecutionFeature.feature!(ForemanRhCloud::CloudConnector::CLOUD_CONNECTOR_FEATURE).id - end + def plan(immediate = false) + if ForemanRhCloud.with_iop_smart_proxy? + logger.debug('Sources announcement skipped: running in IoP mode') + return + end - def plan(job_invocation) - return unless connector_playbook_job?(job_invocation) + if Setting[:rhc_instance_id].blank? + logger.debug('Sources announcement skipped: rhc_instance_id is not set') + return + end - plan_self + unless Setting[:allow_auto_inventory_upload] + logger.debug( + 'Cloud connector is configured (rhc_instance_id is set) but automatic inventory upload is disabled. ' \ + 'Enable the "Automatic inventory upload" setting for full cloud connector functionality.' + ) + end + + if immediate + plan_self + else + after_delay do + plan_self + end + end end - def finalize + def run + registered = [] + confirmed_by_remediation = [] + already_registered = [] + skipped = [] + failed = {} + Organization.unscoped.each do |org| + unless cert_auth_available?(org) + skipped << org.name + next + end + + if recent_cloud_remediation?(org) + confirmed_by_remediation << org.name + next + end + presence = ForemanRhCloud::CloudPresence.new(org, logger) - presence.announce_to_sources + result = presence.announce_to_sources + if result == :already_registered + already_registered << org.name + else + registered << org.name + end rescue StandardError => ex - logger.warn(ex) + logger.warn("Failed to announce to Sources for organization #{org.name}: #{ex}") + logger.debug { ex.backtrace.join("\n") } + failed[org.name] = ex.message end - end - def rescue_strategy_for_self - Dynflow::Action::Rescue::Skip + parts = [] + parts << "Registered: #{registered.join(', ')}" if registered.any? + parts << "Already registered: #{already_registered.join(', ')}" if already_registered.any? + parts << "Already registered (recent cloud remediation): #{confirmed_by_remediation.join(', ')}" if confirmed_by_remediation.any? + parts << "Skipped (no manifest): #{skipped.join(', ')}" if skipped.any? + if failed.any? + failed_details = failed.map { |name, msg| "#{name}: #{msg}" }.join('; ') + parts << "Failed: #{failed_details}" + end + output[:status] = parts.join('. ') + + error!("Sources announcement failed for: #{failed.keys.join(', ')}") if failed.any? end - def connector_playbook_job?(job_invocation) - job_invocation&.remote_execution_feature_id == connector_feature_id + def recent_cloud_remediation?(org) + feature = RemoteExecutionFeature.find_by(label: 'rh_cloud_connector_run_playbook') + return false unless feature + + JobInvocation.where(remote_execution_feature_id: feature.id) + .joins(:task) + .joins(targeting: :hosts) + .where(hosts: { organization_id: org.id }) + .where('foreman_tasks_tasks.started_at > ?', 24.hours.ago) + .exists? end - def connector_feature_id - self.class.connector_feature_id + def rescue_strategy_for_self + Dynflow::Action::Rescue::Skip end def logger diff --git a/test/controllers/inventory_upload/api/inventory_controller_test.rb b/test/controllers/inventory_upload/api/inventory_controller_test.rb index 12c855d7b..32c090979 100644 --- a/test/controllers/inventory_upload/api/inventory_controller_test.rb +++ b/test/controllers/inventory_upload/api/inventory_controller_test.rb @@ -35,20 +35,20 @@ class InventoryControllerTest < ActionController::TestCase assert_equal test_task.id.to_s, actual_task['id'] end - test 'Starts cloud connector configuration job' do - test_job = FactoryBot.create(:job_invocation) + test 'Triggers Sources announcement task' do + test_task = FactoryBot.create(:some_task) - ForemanRhCloud::CloudConnector.any_instance - .expects(:install) - .returns(test_job) + ForemanTasks.expects(:async_task) + .with(InsightsCloud::Async::CloudConnectorAnnounceTask, true) + .returns(test_task) - post :enable_cloud_connector + post :announce_to_sources assert_response :success - actual_job = @response.parsed_body - - assert_equal test_job.id, actual_job['id'] + actual_task = @response.parsed_body['task'] + assert_not_nil actual_task + assert_equal test_task.id.to_s, actual_task['id'] end end end diff --git a/test/jobs/cloud_connector_announce_task_test.rb b/test/jobs/cloud_connector_announce_task_test.rb index 15058c849..630e30f93 100644 --- a/test/jobs/cloud_connector_announce_task_test.rb +++ b/test/jobs/cloud_connector_announce_task_test.rb @@ -1,125 +1,135 @@ -require 'json' require 'test_plugin_helper' require 'foreman_tasks/test_helpers' -require "#{ForemanTasks::Engine.root}/test/support/dummy_dynflow_action" class CloudConnectorAnnounceTaskTest < ActiveSupport::TestCase include Dynflow::Testing::Factories setup do - RemoteExecutionFeature.register( - :ansible_configure_cloud_connector, - N_('Configure Cloud Connector on given hosts'), - :description => N_('Configure Cloud Connector on given hosts'), - :proxy_selector_override => ::RemoteExecutionProxySelector::INTERNAL_PROXY - ) + InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance + .stubs(:recent_cloud_remediation?).returns(false) + end + + teardown do + ForemanRhCloud.unstub(:with_iop_smart_proxy?) + end + + test 'announces to sources for all organizations when rhc_instance_id is set' do + Setting[:rhc_instance_id] = 'test-rhc-id' + + InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance + .stubs(:cert_auth_available?).returns(true) - @job_invocation = generate_job_invocation(:ansible_configure_cloud_connector) + ForemanRhCloud::CloudPresence.any_instance + .expects(:announce_to_sources) + .times(Organization.unscoped.count) - # reset connector feature ID cache - InsightsCloud::Async::CloudConnectorAnnounceTask.instance_variable_set(:@connector_feature_id, nil) + action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask) + action = run_action(action) + + status = action.output[:status].to_s + assert_match(/Registered:|Already registered:/, status) + refute_match(/Skipped/, status) + refute_match(/Failed/, status) end - test 'It executes cloud presence announcer' do - ForemanRhCloud::CloudPresence.any_instance.expects(:announce_to_sources).times(Organization.unscoped.count) + test 'skips when rhc_instance_id is not set' do + Setting[:rhc_instance_id] = nil + + action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask) - action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask, @job_invocation) - finalize_action(action) + assert_empty action.execution_plan.planned_run_steps end - private - - def generate_job_invocation(feature_name) - job_template = FactoryBot.build( - :job_template, - :template => 'BLEH' - ) - feature = RemoteExecutionFeature.feature!(feature_name).id - - job_invocation = FactoryBot.create( - :job_invocation, - remote_execution_feature_id: feature, - task_id: FactoryBot.create(:dynflow_task).id - ) - - job_template.template_inputs << playbook_url_input = FactoryBot.build(:template_input, - :name => 'playbook_url', - :input_type => 'user', - :required => true) - job_template.template_inputs << report_url_input = FactoryBot.build(:template_input, - :name => 'report_url', - :input_type => 'user', - :required => true) - job_template.template_inputs << correlation_id_input = FactoryBot.build(:template_input, - :name => 'correlation_id', - :input_type => 'user', - :required => true) - job_template.template_inputs << report_interval_input = FactoryBot.build(:template_input, - :name => 'report_interval', - :input_type => 'user', - :required => true) - - template_invocation = FactoryBot.build(:template_invocation, - :template => job_template, - :job_invocation => job_invocation) - - template_invocation.input_values << FactoryBot.create( - :template_invocation_input_value, - :template_invocation => template_invocation, - :template_input => playbook_url_input, - :value => 'http://example.com/TEST_PLAYBOOK' - ) - template_invocation.input_values << FactoryBot.create( - :template_invocation_input_value, - :template_invocation => template_invocation, - :template_input => report_url_input, - :value => 'http://example.com/TEST_REPORT' - ) - template_invocation.input_values << FactoryBot.create( - :template_invocation_input_value, - :template_invocation => template_invocation, - :template_input => correlation_id_input, - :value => 'TEST_CORRELATION' - ) - template_invocation.input_values << FactoryBot.create( - :template_invocation_input_value, - :template_invocation => template_invocation, - :template_input => report_interval_input, - :value => '1' - ) - - @host1 = FactoryBot.create(:host, :with_insights_hits, name: 'host1') - @host1.insights.uuid = 'TEST_UUID1' - @host1.insights.save! - @host2 = FactoryBot.create(:host, :with_insights_hits, name: 'host2') - @host2.insights.uuid = 'TEST_UUID2' - @host2.insights.save! - - targeting = FactoryBot.create(:targeting, hosts: [@host1, @host2]) - job_invocation.targeting = targeting - job_invocation.save! - - job_invocation.template_invocations << FactoryBot.create( - :template_invocation, - run_host_job_task: FactoryBot.create(:dynflow_task), - host_id: @host1.id - ) - job_invocation.template_invocations << FactoryBot.create( - :template_invocation, - run_host_job_task: FactoryBot.create(:dynflow_task), - host_id: @host2.id - ) - - fake_output = (1..5).map do |i| - { 'timestamp' => (Time.now - (5 - i)).to_f, 'output' => "#{i}\n" } - end - Support::DummyDynflowAction.any_instance.stubs(:live_output).returns(fake_output) - Support::DummyDynflowAction.any_instance.stubs(:exit_status).returns(0) + test 'skips when rhc_instance_id is empty string' do + Setting[:rhc_instance_id] = '' + + action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask) + + assert_empty action.execution_plan.planned_run_steps + end + + test 'skips when in IoP mode' do + Setting[:rhc_instance_id] = 'test-rhc-id' + ForemanRhCloud.stubs(:with_iop_smart_proxy?).returns(true) + + action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask) + + assert_empty action.execution_plan.planned_run_steps + end + + test 'skips organizations without a manifest' do + Setting[:rhc_instance_id] = 'test-rhc-id' + + InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance + .stubs(:cert_auth_available?).returns(false) + + ForemanRhCloud::CloudPresence.any_instance + .expects(:announce_to_sources) + .never - job_invocation + action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask) + action = run_action(action) + + status = action.output[:status].to_s + assert_match(/Skipped \(no manifest\):/, status) + refute_match(/Registered/, status) + refute_match(/Failed/, status) + end + + test 'still runs when allow_auto_inventory_upload is disabled' do + Setting[:rhc_instance_id] = 'test-rhc-id' + Setting[:allow_auto_inventory_upload] = false + + InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance + .stubs(:cert_auth_available?).returns(true) + + ForemanRhCloud::CloudPresence.any_instance + .expects(:announce_to_sources) + .times(Organization.unscoped.count) + + action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask) + action = run_action(action) + + assert_match(/Registered:|Already registered:/, action.output[:status].to_s) + end + + test 'skips orgs with recent cloud remediation jobs' do + Setting[:rhc_instance_id] = 'test-rhc-id' + + InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance + .stubs(:cert_auth_available?).returns(true) + InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance + .stubs(:recent_cloud_remediation?).returns(true) + + ForemanRhCloud::CloudPresence.any_instance + .expects(:announce_to_sources) + .never + + action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask) + action = run_action(action) + + status = action.output[:status].to_s + assert_match(/Already registered \(recent cloud remediation\):/, status) + refute_match(/\bRegistered:/, status) end - def read_jsonl(jsonl) - jsonl.lines.map { |l| JSON.parse(l) } + test 'continues processing other orgs when one fails and task errors' do + Setting[:rhc_instance_id] = 'test-rhc-id' + + InsightsCloud::Async::CloudConnectorAnnounceTask.any_instance + .stubs(:cert_auth_available?).returns(true) + + call_count = 0 + ForemanRhCloud::CloudPresence.any_instance.stubs(:announce_to_sources).with do + call_count += 1 + raise(StandardError.new('API error')) if call_count == 1 + true + end + + action = create_and_plan_action(InsightsCloud::Async::CloudConnectorAnnounceTask) + + error = assert_raises(StandardError) { run_action(action) } + assert_match(/Sources announcement failed for:/, error.message) + assert_equal Organization.unscoped.count, call_count end end