Skip to content

Conversation

@Odilhao
Copy link
Member

@Odilhao Odilhao commented Nov 24, 2025

What are the changes introduced in this pull request?

Cherrypick f6422b4 and b2abedd

Considerations taken when implementing this change?

What are the testing steps for this pull request?

Summary by Sourcery

Update inventory and insights integration to improve host facet handling and HBI cleanup, while modernizing Dynflow-based job tests and bumping the plugin version to 12.2.13.

New Features:

  • Invoke HBI host deletion when unregistering a host in IoP mode via a new RegistrationManager extension using the host UUID.

Enhancements:

  • Add a ForemanInventoryUpload.host_by_id_url helper and use it when talking to the inventory service.
  • Fix CreateMissingInsightsFacets to count created facets across batches correctly and adjust its logging verbosity.
  • Wire ForemanRhCloud::RegistrationManagerExtensions into Katello::RegistrationManager so unregister flows integrate with HBI cleanup.

Tests:

  • Refactor existing async job specs to use Dynflow::Testing factories/assertions instead of ForemanTasks.sync_task, including explicit handling of polling actions and action outputs.
  • Add comprehensive tests for HostInventoryReportJob, SingleHostReportJob, GenerateReportJob, and GenerateHostReport covering planning, upload behavior, filters, labels, and error paths.
  • Add tests around CreateMissingInsightsFacets behavior, including batch processing, logging, error handling, and correct facet creation/counting.
  • Introduce tests for the new RegistrationManager extensions to validate HBI deletion behavior, error handling, and integration with Katello registration.

jeremylenz and others added 3 commits November 24, 2025 17:02
…1112)

* Add tests for 5 Dynflow actions and fix facet count bug

This commit adds comprehensive tests for the 5 Dynflow actions introduced
in commit 596699a.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>

(cherry picked from commit f6422b4)
* Clear host from HBI on unregister

(cherry picked from commit b2abedd)
@sourcery-ai
Copy link

sourcery-ai bot commented Nov 24, 2025

Reviewer's Guide

Refactors async job tests to use Dynflow testing factories instead of ForemanTasks.sync_task, adds comprehensive test coverage for new inventory report-related async jobs and registration behavior, introduces HBI host deletion on host unregister in IoP mode with robust error handling, fixes Insights facets counting/logging, extends Foreman inventory URL helpers, and bumps the plugin version to 12.2.13.

Sequence diagram for host unregister with HBI deletion in IoP mode

sequenceDiagram
  actor User
  participant Katello_RegistrationManager
  participant ForemanRhCloud_RegistrationManagerExtensions
  participant ForemanInventoryUpload
  participant HBI_Service
  participant Rails_logger

  User->>Katello_RegistrationManager: unregister_host(host, options)
  activate Katello_RegistrationManager
  Note over Katello_RegistrationManager,ForemanRhCloud_RegistrationManagerExtensions: singleton_class.prepend routes call to extension
  Katello_RegistrationManager->>ForemanRhCloud_RegistrationManagerExtensions: unregister_host(host, options)
  activate ForemanRhCloud_RegistrationManagerExtensions

  ForemanRhCloud_RegistrationManagerExtensions->>host: reload

  alt IoP_mode_and_not_organization_destroy_and_host_has_insights_uuid
    ForemanRhCloud_RegistrationManagerExtensions->>ForemanRhCloud_RegistrationManagerExtensions: hbi_host_destroy(host)
    activate ForemanRhCloud_RegistrationManagerExtensions
    ForemanRhCloud_RegistrationManagerExtensions->>Rails_logger: debug Unregistering host uuid from HBI
    ForemanRhCloud_RegistrationManagerExtensions->>ForemanInventoryUpload: host_by_id_url(uuid)
    ForemanInventoryUpload-->>ForemanRhCloud_RegistrationManagerExtensions: hbi_host_url
    ForemanRhCloud_RegistrationManagerExtensions->>HBI_Service: DELETE hbi_host_url

    alt HBI_host_not_found
      HBI_Service-->>ForemanRhCloud_RegistrationManagerExtensions: RestClient::NotFound
      ForemanRhCloud_RegistrationManagerExtensions->>Rails_logger: warn Attempted to destroy HBI host uuid but does not exist
    else HBI_delete_error
      HBI_Service-->>ForemanRhCloud_RegistrationManagerExtensions: StandardError
      ForemanRhCloud_RegistrationManagerExtensions->>Rails_logger: error Failed to destroy HBI host uuid message
    else HBI_delete_success
      HBI_Service-->>ForemanRhCloud_RegistrationManagerExtensions: 204 No Content
    end
    deactivate ForemanRhCloud_RegistrationManagerExtensions
  else Hosted_mode_or_organization_destroy_or_no_insights_uuid
    ForemanRhCloud_RegistrationManagerExtensions-->>ForemanRhCloud_RegistrationManagerExtensions: skip_hbi_host_destroy
  end

  ForemanRhCloud_RegistrationManagerExtensions->>host: insights.destroy!
  ForemanRhCloud_RegistrationManagerExtensions->>Katello_RegistrationManager: super(host, options)
  deactivate ForemanRhCloud_RegistrationManagerExtensions
  Katello_RegistrationManager-->>User: unregister_result
  deactivate Katello_RegistrationManager
Loading

Updated class diagram for registration manager extensions and inventory URL helpers

classDiagram
  class Katello_RegistrationManager {
    +unregister_host(host, options)
  }

  class ForemanRhCloud_RegistrationManagerExtensions {
    +logger()
    +unregister_host(host, options)
    +hbi_host_destroy(host)
  }

  class ForemanRhCloud_CertAuth {
  }

  class ForemanInventoryUpload {
    +inventory_base_url()
    +inventory_self_url()
    +host_by_id_url(host_uuid)
    +hosts_by_ids_url(host_uuids)
  }

  class ForemanRhCloud_Engine {
    +register_scheduled_task(task_class, cronline)
  }

  Katello_RegistrationManager <|.. ForemanRhCloud_RegistrationManagerExtensions
  ForemanRhCloud_RegistrationManagerExtensions ..> ForemanRhCloud_CertAuth : includes
  ForemanRhCloud_RegistrationManagerExtensions ..> ForemanInventoryUpload : uses
  ForemanRhCloud_Engine ..> Katello_RegistrationManager : extends_registration
  ForemanRhCloud_Engine ..> ForemanRhCloud_RegistrationManagerExtensions : prepends_singleton_class
  ForemanInventoryUpload ..> ForemanRhCloud_Engine : loaded_by
Loading

File-Level Changes

Change Details Files
Refactor existing async job tests from ForemanTasks.sync_task to Dynflow::Testing::Factories helpers and clarify expectations on action outputs and polling behavior.
  • Replace inclusion of ForemanTasks::TestHelpers::WithInThreadExecutor with Dynflow::Testing::Factories in multiple job-related test cases.
  • Update tests to use create_and_plan_action, run_action, and finalize_action instead of ForemanTasks.sync_task, adapting assertions to work on action.output and saved_reports collections.
  • Explicitly simulate polling behavior in tests that rely on Dynflow polling by repeatedly running the Poll event until the action is done.
test/jobs/connector_playbook_execution_reporter_task_test.rb
test/jobs/remove_insights_hosts_job_test.rb
test/jobs/insights_full_sync_test.rb
test/jobs/exponential_backoff_test.rb
test/jobs/inventory_hosts_sync_test.rb
test/jobs/inventory_scheduled_sync_test.rb
test/jobs/queue_for_upload_job_test.rb
test/jobs/insights_resolutions_sync_test.rb
test/jobs/inventory_full_sync_test.rb
test/jobs/upload_report_job_test.rb
test/jobs/insights_rules_sync_test.rb
test/jobs/cloud_connector_announce_task_test.rb
test/jobs/insights_client_status_aging_test.rb
test/jobs/inventory_self_host_sync_test.rb
Introduce and test new async jobs for generating host inventory reports, including single-host variants, and for creating missing Insights facets.
  • Add tests for HostInventoryReportJob planning behavior, including interactions with GenerateHostReport, QueueForUploadJob, and CreateMissingInsightsFacets depending on IoP and upload flags.
  • Add tests for SingleHostReportJob to verify host-specific filtering, humanized naming, archive naming, and facet creation behavior inheritance from HostInventoryReportJob.
  • Add tests for CreateMissingInsightsFacets async action to ensure it creates Insights facets for hosts missing them, correctly counts across batches, logs results, and handles upsert_all failures.
  • Add tests for GenerateReportJob (report generation and optional upload) and GenerateHostReport (archive creation and generator invocation) including filter handling and output labeling.
test/jobs/host_inventory_report_job_test.rb
test/jobs/single_host_report_job_test.rb
test/jobs/create_missing_insights_facets_test.rb
test/jobs/generate_report_job_test.rb
test/jobs/generate_host_report_test.rb
Add RegistrationManager extensions to delete HBI hosts on unregister in IoP mode and cover the behavior with unit tests.
  • Add RegistrationManagerExtensions concern that prepends Katello::RegistrationManager, overriding unregister_host to optionally delete the host from HBI when in IoP mode and cleaning up the Insights facet.
  • Implement hbi_host_destroy helper using execute_cloud_request and ForemanInventoryUpload.host_by_id_url with robust error handling and logging for not-found and other errors.
  • Wire the extension into the engine by prepending it to Katello::RegistrationManager at engine initialization time.
  • Add tests to verify HBI delete is called only in IoP mode with valid Insights UUIDs, that facets are always destroyed, that various error cases are logged but non-fatal, and that URL generation behaves as expected.
app/controllers/concerns/foreman_rh_cloud/registration_manager_extensions.rb
lib/foreman_rh_cloud/engine.rb
test/unit/lib/foreman_rh_cloud/registration_manager_extensions_test.rb
Refine Insights facets creation logic and logging behavior.
  • Change CreateMissingInsightsFacets run implementation to count created facets across batches by accumulating facet_count rather than using the relation count directly.
  • Adjust logging level to debug for the summary result message to avoid noisy logs while preserving output[:result] for tests and callers.
lib/foreman_inventory_upload/async/create_missing_insights_facets.rb
Extend ForemanInventoryUpload inventory URL helpers to support single-host HBI URLs and clarify parameter naming.
  • Add host_by_id_url helper that returns inventory_base_url with a single host UUID path segment.
  • Update hosts_by_ids_url to take host_uuids, join them into a comma-separated string, and build the corresponding inventory URL.
  • Use host_by_id_url from RegistrationManagerExtensions for HBI host deletion and cover it in tests.
lib/foreman_inventory_upload.rb
test/unit/lib/foreman_rh_cloud/registration_manager_extensions_test.rb
Bump plugin/package version to 12.2.13 for this release.
  • Update ForemanRhCloud::VERSION constant to 12.2.13.
  • Update package.json version field to 12.2.13.
lib/foreman_rh_cloud/version.rb
package.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey there - I've reviewed your changes - here's some feedback:

  • In ExponentialBackoffTest, the last test no longer asserts on the failure outcome (it just runs the action without checking the error or state), which weakens the test compared to the previous behavior that verified the aggregated message; consider restoring explicit assertions about the failure condition.
  • In ForemanInventoryUpload, hosts_by_ids_url and the new host_by_id_url now have overlapping responsibilities; consider implementing hosts_by_ids_url in terms of host_by_id_url (or vice versa) to avoid duplicating URL construction logic.
  • In ForemanRhCloud::RegistrationManagerExtensions, logging sometimes uses the local logger method and sometimes Rails.logger directly; consider using a single logging entry point for consistency (e.g., always use the logger method).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In ExponentialBackoffTest, the last test no longer asserts on the failure outcome (it just runs the action without checking the error or state), which weakens the test compared to the previous behavior that verified the aggregated message; consider restoring explicit assertions about the failure condition.
- In ForemanInventoryUpload, hosts_by_ids_url and the new host_by_id_url now have overlapping responsibilities; consider implementing hosts_by_ids_url in terms of host_by_id_url (or vice versa) to avoid duplicating URL construction logic.
- In ForemanRhCloud::RegistrationManagerExtensions, logging sometimes uses the local logger method and sometimes Rails.logger directly; consider using a single logging entry point for consistency (e.g., always use the logger method).

## Individual Comments

### Comment 1
<location> `app/controllers/concerns/foreman_rh_cloud/registration_manager_extensions.rb:9-18` </location>
<code_context>
+      Rails.logger
+    end
+
+    def unregister_host(host, options = {})
+      organization_destroy = options.fetch(:organization_destroy, false)
+
+      # Reload to ensure we have fresh association data
+      host.reload
+
+      # Only delete from HBI in IoP mode (hosted mode uses async job for cleanup)
+      hbi_host_destroy(host) if ForemanRhCloud.with_iop_smart_proxy? && !organization_destroy && host.insights_facet&.uuid&.presence
+      host.insights&.destroy!
+      super(host, options)
+    end
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Reloading the host without handling RecordNotFound can cause unregister to fail unexpectedly.

If the host has been deleted or become stale, `host.reload` will raise `ActiveRecord::RecordNotFound` and prevent the rest of `unregister_host` (including `super`) from running. If unregister can be called when the host might already be removed, consider either rescuing `RecordNotFound` around `reload` and continuing with best-effort cleanup, or only reloading when you know the host still exists.
</issue_to_address>

### Comment 2
<location> `app/controllers/concerns/foreman_rh_cloud/registration_manager_extensions.rb:32-37` </location>
<code_context>
+      super(host, options)
+    end
+
+    def hbi_host_destroy(host)
+      uuid = host.insights_facet.uuid
+      logger.debug "Unregistering host #{uuid} from HBI"
+      execute_cloud_request(
+        organization: host.organization,
+        method: :delete,
+        url: ForemanInventoryUpload.host_by_id_url(uuid),
+        headers: {
+          content_type: :json,
+        }
+      )
+    rescue RestClient::NotFound
+      Rails.logger.warn(_("Attempted to destroy HBI host %s, but host does not exist in HBI") % uuid)
+    rescue StandardError => e
+      # TODO: Improve error handling - don't break registration if HBI delete fails
+      Rails.logger.error(format(_("Failed to destroy HBI host %s: %s"), uuid, e.message))
+    end
+  end
</code_context>

<issue_to_address>
**suggestion:** Use the injected logger consistently instead of hardcoding Rails.logger.

In this module, the `logger` method already wraps `Rails.logger` to allow future overrides. In `hbi_host_destroy`, please use `logger.warn` and `logger.error` instead of calling `Rails.logger` directly to keep logging consistent and maintain that indirection.

```suggestion
    rescue RestClient::NotFound
      logger.warn(_("Attempted to destroy HBI host %s, but host does not exist in HBI") % uuid)
    rescue StandardError => e
      # TODO: Improve error handling - don't break registration if HBI delete fails
      logger.error(format(_("Failed to destroy HBI host %s: %s"), uuid, e.message))
    end
```
</issue_to_address>

### Comment 3
<location> `test/jobs/exponential_backoff_test.rb:42` </location>
<code_context>
     TestAction.any_instance.stubs(:poll_intervals).returns([0, 0, 0])

-    TestAction.any_instance.expects(:action_callback).raises(::Foreman::Exception.new('Foo')).times(3)
+    TestAction.any_instance.expects(:action_callback).raises(StandardError.new('Foo')).at_least_once

-    ForemanTasks.sync_task(TestAction)
</code_context>

<issue_to_address>
**issue (testing):** The exponential backoff failure test no longer asserts that the action ultimately fails with the expected error/message.

Previously this test verified that the task failed after three attempts and that the aggregated message included "Foo" by rescuing ForemanTasks::TaskError and asserting on ex.aggregated_message. After the refactor to use create_and_plan_action/run_action, it just calls run_action(action) without asserting the failure type or message. Please add explicit checks again, e.g.:
- wrap run_action(action) in assert_raises with the expected error, and
- assert that the error message (or relevant action state/retry count) reflects the intended backoff behavior.
This keeps the test validating both the retry logic and the final failure mode.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +9 to +18
def unregister_host(host, options = {})
organization_destroy = options.fetch(:organization_destroy, false)

# Reload to ensure we have fresh association data
host.reload

# Only delete from HBI in IoP mode (hosted mode uses async job for cleanup)
hbi_host_destroy(host) if ForemanRhCloud.with_iop_smart_proxy? && !organization_destroy && host.insights_facet&.uuid&.presence
host.insights&.destroy!
super(host, options)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Reloading the host without handling RecordNotFound can cause unregister to fail unexpectedly.

If the host has been deleted or become stale, host.reload will raise ActiveRecord::RecordNotFound and prevent the rest of unregister_host (including super) from running. If unregister can be called when the host might already be removed, consider either rescuing RecordNotFound around reload and continuing with best-effort cleanup, or only reloading when you know the host still exists.

Comment on lines +32 to +37
rescue RestClient::NotFound
Rails.logger.warn(_("Attempted to destroy HBI host %s, but host does not exist in HBI") % uuid)
rescue StandardError => e
# TODO: Improve error handling - don't break registration if HBI delete fails
Rails.logger.error(format(_("Failed to destroy HBI host %s: %s"), uuid, e.message))
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Use the injected logger consistently instead of hardcoding Rails.logger.

In this module, the logger method already wraps Rails.logger to allow future overrides. In hbi_host_destroy, please use logger.warn and logger.error instead of calling Rails.logger directly to keep logging consistent and maintain that indirection.

Suggested change
rescue RestClient::NotFound
Rails.logger.warn(_("Attempted to destroy HBI host %s, but host does not exist in HBI") % uuid)
rescue StandardError => e
# TODO: Improve error handling - don't break registration if HBI delete fails
Rails.logger.error(format(_("Failed to destroy HBI host %s: %s"), uuid, e.message))
end
rescue RestClient::NotFound
logger.warn(_("Attempted to destroy HBI host %s, but host does not exist in HBI") % uuid)
rescue StandardError => e
# TODO: Improve error handling - don't break registration if HBI delete fails
logger.error(format(_("Failed to destroy HBI host %s: %s"), uuid, e.message))
end

@chris1984 chris1984 merged commit 07c12db into theforeman:foreman_3_16 Nov 24, 2025
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants