Skip to content
Open
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
34 changes: 23 additions & 11 deletions app/services/foreman_ansible_director/proxy/base_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion app/services/foreman_ansible_director/pulp3/base_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't really like this. But it does not seem to work otherwise, even if I add a Rails flag. Maybe because the task is created in rake env but then scheduled from the Rails application. Not sure what exactly happens here. However, something similar seems to exist in Katello:
https://github.com/Katello/katello/blob/master/app/models/katello/concerns/smart_proxy_extensions.rb#L88

config.host = uri.host
config.scheme = uri.scheme
pulp3_ssl_configuration(config)
Expand Down
4 changes: 4 additions & 0 deletions lib/foreman_ansible_director/engine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand Down
14 changes: 7 additions & 7 deletions lib/foreman_ansible_director/register.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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.',
Expand Down
153 changes: 153 additions & 0 deletions lib/tasks/foreman_ansible_director_tasks.rake
Original file line number Diff line number Diff line change
@@ -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
Comment on lines +49 to +61
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I know there is something similar in the proxy base client but I thought it might be a good idea to check on the proxy before even starting a task not when the task is running or whenever this is evaluated.


def destroy_pulp_object(deleted_hrefs, key, href, action, input_key)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is it posible to re-use the introduced labels 1be49d4#diff-bc0ac4dcf2708e3d510050a532b88b8bde8d3e97af27d3b9bde58b62ea777005R13 to find and remove the pulp content / images?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You mean we could have a call to Pulp which simply removes everything with one specific label?

Copy link
Copy Markdown
Member

@sbernhard sbernhard May 8, 2026

Choose a reason for hiding this comment

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

yes. this should be possible - for each remote / distribution / publication / etc.

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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I don't think this will cut it.
Quite a bit more needs to happen for proper cleanup, as Pulp objects also have to be removed.
I see two options:

  • Query all Pulp Ansible Repositories, CollectionRemotes, GitRemotes, and Distributions where the label creator is foreman_ansible_director
  • Iterate over ContentUnitVersions and destroy the associated Pulp objects.

I intend to add a consistency check in general, which will do a mix of both, so this problem may already be solved in the future.


# Tests
namespace :test do
Expand Down
Loading