From c0c77fe23be126e4123be646ea20b2ced1c596c9 Mon Sep 17 00:00:00 2001 From: Nadja Heitmann Date: Fri, 24 Apr 2026 14:10:48 +0200 Subject: [PATCH 1/2] fix(settings): prefix all settings with 'ansible_director_' In case we want to remove the plugin, we need clearly defined names for the settings, so we do not accidently remove some setting from another plugin or core. --- lib/foreman_ansible_director/register.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/foreman_ansible_director/register.rb b/lib/foreman_ansible_director/register.rb index 7dca3d7..0cc6d62 100644 --- a/lib/foreman_ansible_director/register.rb +++ b/lib/foreman_ansible_director/register.rb @@ -185,26 +185,26 @@ settings do category :ansible_director, 'Ansible Director' do - setting 'ad_default_galaxy_url', + setting 'ansible_director_default_galaxy_url', type: :string, description: 'Default URL used when importing content from an Ansible Galaxy instance.', default: ::ForemanAnsibleDirector::Constants::DEFAULT_GALAXY_URL, full_name: 'Content - Default Ansible Galaxy URL' - setting 'ad_content_import_override', + setting 'ansible_director_content_import_override', type: :boolean, description: 'When enabled, the content importer will override any existing content that matches the identifier of a content unit scheduled for import, ensuring the new content replaces the existing version rather than being skipped.', default: false, full_name: 'Content - Force override of existing content' - setting 'ad_default_ansible_core_version', + setting 'ansible_director_default_ansible_core_version', type: :string, description: 'Default Ansible-Core version used for Execution Environments. Must match an available release from PyPI (check history: https://pypi.org/project/ansible-core/#history).', default: ::ForemanAnsibleDirector::Constants::DEFAULT_ANSIBLE_VERSION, full_name: 'Execution Environments - Default ansible-core version' - setting 'ad_default_ee_rex', + setting 'ansible_director_default_ee_rex', type: :integer, description: 'Default Execution Environment used for execution of Remote Execution jobs.', default: nil, @@ -218,7 +218,7 @@ ) ] } - setting 'ad_default_ee_internal', + setting 'ansible_director_default_ee_internal', type: :integer, description: 'Default Execution Environment used for execution of default Ansible jobs.', default: nil, @@ -232,14 +232,14 @@ ) ] } - setting 'ad_lce_path_force_incremental', + setting 'ansible_director_lce_path_force_incremental', type: :boolean, description: 'When enabled, lifecycle environment promotions must follow the defined path incrementally (e.g., DEV → TEST → PROD). When disabled, promotions can skip intermediate environments (e.g., DEV → PROD directly, with implicit promotion of DEV → TEST).', default: true, full_name: 'Lifecycle environments - Force incremental promotion of lifecycle environments' - setting 'ad_lce_path_prevent_destruction_if_used', + setting 'ansible_director_lce_path_prevent_destruction_if_used', type: :boolean, description: 'When enabled, lifecycle environment paths can only be deleted if none of its lifecycle environments is referenced anywhere.', From efa13db96b9bbf6ee85de4275129c6451c47e415 Mon Sep 17 00:00:00 2001 From: Nadja Heitmann Date: Tue, 12 May 2026 07:20:21 +0000 Subject: [PATCH 2/2] feat(installation): Add task to remove db migrations and pulp content Preparation for plugin deinstallation Refs similar task in https://github.com/ATIX-AG/foreman_resource_quota/commit/37b7c99c28fd2dc6ae1f96cccec608dec22efa8e Also adds task to clean up the created Pulp content. --- .../proxy/base_client.rb | 34 ++-- .../pulp3/base_client.rb | 2 +- lib/foreman_ansible_director/engine.rb | 4 + lib/tasks/foreman_ansible_director_tasks.rake | 153 ++++++++++++++++++ 4 files changed, 181 insertions(+), 12 deletions(-) diff --git a/app/services/foreman_ansible_director/proxy/base_client.rb b/app/services/foreman_ansible_director/proxy/base_client.rb index 222c439..43651a9 100644 --- a/app/services/foreman_ansible_director/proxy/base_client.rb +++ b/app/services/foreman_ansible_director/proxy/base_client.rb @@ -5,23 +5,35 @@ module ForemanAnsibleDirector module Proxy class BaseClient class << self + def smart_proxy + missing_proxy_message = + 'No Smart Proxy with the Ansible_Director feature is available for Foreman Ansible Director.' + ::SmartProxy.unscoped.with_features('Ansible_Director').first || + raise(Foreman::Exception, missing_proxy_message) + end + def proxy_resource - ssl_config = { + RestClient::Resource.new(smart_proxy_url, ssl_config) + end + + private + + def smart_proxy_url + return smart_proxy.url if Rails.application.config.x.ansible_director.running_in_rake + + # TODO: This should be configurable by the user and use a selector + return 'http://192.168.121.1:8080' if Rails.env.development? + + ::SmartProxy.first.url + end + + def ssl_config + { ssl_client_cert: ::ForemanAnsibleDirector::Cert::Certs.ssl_client_cert, ssl_client_key: ::ForemanAnsibleDirector::Cert::Certs.ssl_client_key, ssl_ca_file: ::ForemanAnsibleDirector::Cert::Certs.ca_cert_file, verify_ssl: OpenSSL::SSL::VERIFY_PEER, } - - # TODO: This should be configurable by the user and use a selector - if Rails.env.development? - return RestClient::Resource.new( - 'http://192.168.121.1:8080' - ) - end - RestClient::Resource.new( - ::SmartProxy.first.url, ssl_config - ) end end end diff --git a/app/services/foreman_ansible_director/pulp3/base_client.rb b/app/services/foreman_ansible_director/pulp3/base_client.rb index 27f0c4d..f03513b 100644 --- a/app/services/foreman_ansible_director/pulp3/base_client.rb +++ b/app/services/foreman_ansible_director/pulp3/base_client.rb @@ -7,7 +7,7 @@ class BaseClient class << self def pulp3_configuration(config_class) config_class.new do |config| - uri = URI.parse(::SmartProxy.first.url) + uri = URI.parse(::SmartProxy.unscoped.first.url) config.host = uri.host config.scheme = uri.scheme pulp3_ssl_configuration(config) diff --git a/lib/foreman_ansible_director/engine.rb b/lib/foreman_ansible_director/engine.rb index 2b44e42..5e6fae0 100644 --- a/lib/foreman_ansible_director/engine.rb +++ b/lib/foreman_ansible_director/engine.rb @@ -9,6 +9,10 @@ class Engine < ::Rails::Engine isolate_namespace ForemanAnsibleDirector engine_name 'foreman_ansible_director' + initializer 'foreman_ansible_director.cleanup_context' do |app| + app.config.x.ansible_director.running_in_rake = false + end + # Add any db migrations initializer 'foreman_ansible_director.load_app_instance_data' do |app| ForemanAnsibleDirector::Engine.paths['db/migrate'].existent.each do |path| diff --git a/lib/tasks/foreman_ansible_director_tasks.rake b/lib/tasks/foreman_ansible_director_tasks.rake index eb3a0a3..c9d548a 100644 --- a/lib/tasks/foreman_ansible_director_tasks.rake +++ b/lib/tasks/foreman_ansible_director_tasks.rake @@ -1,6 +1,159 @@ # frozen_string_literal: true require 'rake/testtask' +require 'set' +require 'pulp_ansible_client' + +module ForemanAnsibleDirector + module TaskHelpers + module_function + + ANSIBLE_DIRECTOR_FEATURE = 'Ansible_Director' + PULP_PAGE_SIZE = 1000 + PULP_CLEANUP_RESOURCES = { + distribution: { + api_class: ::PulpAnsibleClient::DistributionsAnsibleApi, + action: 'ForemanAnsibleDirector::Actions::Pulp3::Ansible::Distribution::Destroy', + input_key: :distribution_href, + }, + repository: { + api_class: ::PulpAnsibleClient::RepositoriesAnsibleApi, + action: 'ForemanAnsibleDirector::Actions::Pulp3::Ansible::Repository::Destroy', + input_key: :repository_href, + }, + git_remote: { + api_class: ::PulpAnsibleClient::RemotesGitApi, + action: 'ForemanAnsibleDirector::Actions::Pulp3::Ansible::Remote::Git::Destroy', + input_key: :git_remote_href, + }, + collection_remote: { + api_class: ::PulpAnsibleClient::RemotesCollectionApi, + action: 'ForemanAnsibleDirector::Actions::Pulp3::Ansible::Remote::Collection::Destroy', + input_key: :collection_remote_href, + }, + role_remote: { + api_class: ::PulpAnsibleClient::RemotesRoleApi, + action: 'ForemanAnsibleDirector::Actions::Pulp3::Ansible::Remote::Role::Destroy', + input_key: :role_remote_href, + }, + }.freeze + + def records_exist?(records) + records.respond_to?(:exists?) ? records.exists? : records.any? + end + + def ansible_director_proxies + ::SmartProxy.unscoped.with_features(ANSIBLE_DIRECTOR_FEATURE) + end + + def ensure_ansible_director_proxy! + proxies = ansible_director_proxies + missing_feature_message = + 'No Smart Proxy with the Ansible_Director feature is available for Foreman Ansible Director Pulp cleanup.' + raise Foreman::Exception, missing_feature_message unless records_exist?(proxies) + + proxy = proxies.first + return if proxy&.url.present? + + missing_url_message = + 'No Smart Proxy with a configured URL is available for Foreman Ansible Director Pulp cleanup.' + raise Foreman::Exception, missing_url_message + end + + def destroy_pulp_object(deleted_hrefs, key, href, action, input_key) + return if href.blank? || deleted_hrefs[key].include?(href) + + ForemanTasks.sync_task(action, input_key => href) + deleted_hrefs[key] << href + end + + def cleanup_pulp_objects(deleted_hrefs) + PULP_CLEANUP_RESOURCES.each do |resource_type, resource_config| + cleanup_resource_hrefs(resource_type, resource_config, deleted_hrefs) + end + end + + def cleanup_resource_hrefs(resource_type, resource_config, deleted_hrefs) + action_class = resource_config[:action].constantize + + fetch_pulp_hrefs(resource_config).each do |href| + destroy_pulp_object( + deleted_hrefs, + resource_type, + href, + action_class, + resource_config[:input_key] + ) + end + end + + def fetch_pulp_hrefs(resource_config) + api_client = resource_config[:api_class].new(::ForemanAnsibleDirector::Pulp3::BaseClient.ansible_api_client) + offset = 0 + hrefs = [] + + loop do + response = api_client.list( + limit: PULP_PAGE_SIZE, + offset: offset, + pulp_label_select: pulp_label_select + ) + + hrefs.concat(response.results.map(&:pulp_href)) + + offset += PULP_PAGE_SIZE + break if response.results.empty? || offset >= response.count + end + + hrefs + end + + def pulp_label_select + ::ForemanAnsibleDirector::Constants::PULP_OBJECT_LABELS.map do |key, value| + "#{key}=#{value}" + end.join(',') + end + + def build_deleted_hrefs + PULP_CLEANUP_RESOURCES.each_key.with_object({}) do |resource_type, deleted_hrefs| + deleted_hrefs[resource_type] = Set.new + end + end + end +end + +# Tasks +namespace :foreman_ansible_director do + desc 'Delete all Pulp objects associated with imported content unit versions' + task cleanup_pulp_objects: ['environment', 'dynflow:client'] do + ::ForemanAnsibleDirector::TaskHelpers.ensure_ansible_director_proxy! + + previous_value = Rails.application.config.x.ansible_director.running_in_rake + Rails.application.config.x.ansible_director.running_in_rake = true + + begin + # Track Pulp hrefs that have already been deleted so each labeled resource + # is destroyed only once, even if Pulp returns duplicates across pages. + deleted_hrefs = ::ForemanAnsibleDirector::TaskHelpers.build_deleted_hrefs + + ::ForemanAnsibleDirector::TaskHelpers.cleanup_pulp_objects(deleted_hrefs) + ensure + Rails.application.config.x.ansible_director.running_in_rake = previous_value + end + end + + desc 'Run all Foreman Ansible Director cleanup tasks' + task cleanup_all: :environment do + Rake::Task['foreman_ansible_director:cleanup_pulp_objects'].invoke + Rake::Task['foreman_ansible_director:revert_db_migrations'].invoke + end + + desc 'Revert all database migrations of this plugin, preparing plugin uninstall' + task revert_db_migrations: :environment do + plugin = Foreman::Plugin.find ForemanAnsibleDirector.name.underscore + ActiveRecord::MigrationContext.new(plugin.migrations_paths, ActiveRecord::SchemaMigration).down + end +end # Tests namespace :test do