katello 4.9.0.rc1 → 4.9.0.rc2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of katello might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/app/lib/actions/katello/content_view/update.rb +24 -0
- data/app/lib/actions/pulp3/orchestration/orphan_cleanup/remove_orphans.rb +1 -1
- data/app/models/katello/concerns/pulp_database_unit.rb +8 -0
- data/app/models/katello/content_view.rb +7 -5
- data/app/models/katello/content_view_erratum_filter_rule.rb +5 -0
- data/app/models/katello/content_view_module_stream_filter_rule.rb +5 -0
- data/app/models/katello/content_view_package_group_filter_rule.rb +5 -0
- data/app/models/katello/repository.rb +2 -1
- data/app/services/katello/content_unit_indexer.rb +27 -2
- data/app/services/katello/pulp3/api/core.rb +1 -1
- data/app/services/katello/pulp3/repository/docker.rb +7 -0
- data/app/services/katello/pulp3/repository.rb +15 -0
- data/app/services/katello/smart_proxy_helper.rb +6 -1
- data/lib/katello/plugin.rb +1 -1
- data/lib/katello/tasks/upgrades/4.9/clean_orphaned_filter_rules.rake +40 -0
- data/lib/katello/version.rb +1 -1
- data/locale/Makefile +4 -3
- metadata +7 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d870b3e8317aae2cc5675bdebddc2541257c470714209df06d79135ebc751643
|
4
|
+
data.tar.gz: e19a405204719de4c723e6b7a0c4612e40983698df33562f74e28e0eb2fb83f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee637b366b689e48fdbde217e2255b683c35095ffcbc3ad1234b9446ba7757be992a5b33a61650dc1fc5ac25ff4474185e2e58772991475c0f6a78627bbd70c4
|
7
|
+
data.tar.gz: 6eac40d9467c4365c19c82ecdce5d53f42418f511c7e35e12f987c5e58b432c3919f0ff0a2c1d581084e14e8f1ca29147f298a505331ec1d6907a6f132b4e209
|
@@ -4,6 +4,30 @@ module Actions
|
|
4
4
|
class Update < Actions::EntryAction
|
5
5
|
def plan(content_view, content_view_params)
|
6
6
|
action_subject content_view
|
7
|
+
content_view_params = content_view_params.with_indifferent_access
|
8
|
+
|
9
|
+
# If we are removing repositories, remove their filter rules
|
10
|
+
if content_view.filters.present? && content_view.repository_ids.present? && content_view_params.key?(:repository_ids)
|
11
|
+
repo_ids_to_remove = content_view.repository_ids - content_view_params[:repository_ids]
|
12
|
+
if repo_ids_to_remove.present?
|
13
|
+
# Only yum-type repositories have by-ID filter rules
|
14
|
+
old_repos = content_view.repositories.yum_type
|
15
|
+
new_repos = ::Katello::Repository.where(id: content_view_params[:repository_ids]).yum_type
|
16
|
+
|
17
|
+
lost_module_stream_ids = (::Katello::ModuleStream.in_repositories(old_repos) -
|
18
|
+
::Katello::ModuleStream.in_repositories(new_repos)).pluck(:id)
|
19
|
+
::Katello::ContentViewModuleStreamFilterRule.in_content_views([content_view.id]).where(module_stream_id: lost_module_stream_ids).delete_all
|
20
|
+
|
21
|
+
lost_errata_ids = (::Katello::Erratum.in_repositories(old_repos) -
|
22
|
+
::Katello::Erratum.in_repositories(new_repos)).pluck(:errata_id)
|
23
|
+
::Katello::ContentViewErratumFilterRule.in_content_views([content_view.id]).where(errata_id: lost_errata_ids).delete_all
|
24
|
+
|
25
|
+
lost_package_group_hrefs = (::Katello::PackageGroup.in_repositories(old_repos) -
|
26
|
+
::Katello::PackageGroup.in_repositories(new_repos)).pluck(:pulp_id)
|
27
|
+
::Katello::ContentViewPackageGroupFilterRule.in_content_views([content_view.id]).where(uuid: lost_package_group_hrefs).delete_all
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
7
31
|
content_view.update!(content_view_params)
|
8
32
|
end
|
9
33
|
end
|
@@ -7,13 +7,13 @@ module Actions
|
|
7
7
|
if proxy.pulp3_enabled?
|
8
8
|
sequence do
|
9
9
|
plan_action(Actions::Pulp3::OrphanCleanup::DeleteOrphanRepositoryVersions, proxy)
|
10
|
-
plan_action(Actions::Pulp3::OrphanCleanup::RemoveOrphans, proxy)
|
11
10
|
if proxy.pulp_mirror?
|
12
11
|
plan_action(Actions::Pulp3::OrphanCleanup::RemoveUnneededRepos, proxy)
|
13
12
|
plan_action(Actions::Pulp3::OrphanCleanup::DeleteOrphanDistributions, proxy)
|
14
13
|
plan_action(Actions::Pulp3::OrphanCleanup::DeleteOrphanAlternateContentSources, proxy)
|
15
14
|
plan_action(Actions::Pulp3::OrphanCleanup::DeleteOrphanRemotes, proxy)
|
16
15
|
end
|
16
|
+
plan_action(Actions::Pulp3::OrphanCleanup::RemoveOrphans, proxy)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
@@ -167,6 +167,14 @@ module Katello
|
|
167
167
|
where("#{self.table_name}.id in (?) or #{self.table_name}.pulp_id in (?)", id_integers, ids)
|
168
168
|
end
|
169
169
|
|
170
|
+
def orphaned
|
171
|
+
if many_repository_associations
|
172
|
+
where.not(:id => repository_association_class.where(:repository_id => ::Katello::Repository.all).select(unit_id_field))
|
173
|
+
else
|
174
|
+
where.not(:repository_id => ::Katello::Repository.all)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
170
178
|
def in_repositories(repos)
|
171
179
|
if many_repository_associations
|
172
180
|
where(:id => repository_association_class.where(:repository_id => repos).select(unit_id_field))
|
@@ -95,11 +95,13 @@ module Katello
|
|
95
95
|
:repository_import,
|
96
96
|
:repository_export_syncable])
|
97
97
|
}
|
98
|
-
scope :ignore_generated, -> {
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
98
|
+
scope :ignore_generated, ->(include_library_generated: false) {
|
99
|
+
ignored_values = [:repository_export,
|
100
|
+
:repository_import,
|
101
|
+
:library_export_syncable,
|
102
|
+
:repository_export_syncable]
|
103
|
+
ignored_values += [:library_export, :library_import] if include_library_generated
|
104
|
+
where.not(generated_for: ignored_values)
|
103
105
|
}
|
104
106
|
scope :generated_for_library, -> { where(:generated_for => [:library_export, :library_import, :library_export_syncable]) }
|
105
107
|
|
@@ -26,6 +26,11 @@ module Katello
|
|
26
26
|
:message => (_("must be one of the following: %s") % DATE_TYPES.join(', '))
|
27
27
|
}
|
28
28
|
|
29
|
+
def self.in_content_views(content_view_ids)
|
30
|
+
joins('INNER JOIN katello_content_view_filters ON katello_content_view_erratum_filter_rules.content_view_filter_id = katello_content_view_filters.id').
|
31
|
+
where("katello_content_view_filters.content_view_id IN (#{content_view_ids.join(',')})")
|
32
|
+
end
|
33
|
+
|
29
34
|
def filter_has_date_or_type_rule?
|
30
35
|
filter.erratum_rules.any? { |rule| rule.start_date || rule.end_date || !rule.types.blank? }
|
31
36
|
end
|
@@ -7,5 +7,10 @@ module Katello
|
|
7
7
|
:foreign_key => :content_view_filter_id
|
8
8
|
belongs_to :module_stream, :class_name => "Katello::ModuleStream", :inverse_of => :rules
|
9
9
|
validates :module_stream_id, :presence => true, :uniqueness => { :scope => :content_view_filter_id }
|
10
|
+
|
11
|
+
def self.in_content_views(content_view_ids)
|
12
|
+
joins('INNER JOIN katello_content_view_filters ON katello_content_view_module_stream_filter_rules.content_view_filter_id = katello_content_view_filters.id').
|
13
|
+
where("katello_content_view_filters.content_view_id IN (#{content_view_ids.join(',')})")
|
14
|
+
end
|
10
15
|
end
|
11
16
|
end
|
@@ -8,5 +8,10 @@ module Katello
|
|
8
8
|
:foreign_key => :content_view_filter_id
|
9
9
|
|
10
10
|
validates :uuid, :presence => true, :uniqueness => { :scope => :content_view_filter_id }
|
11
|
+
|
12
|
+
def self.in_content_views(content_view_ids)
|
13
|
+
joins('INNER JOIN katello_content_view_filters ON katello_content_view_package_group_filter_rules.content_view_filter_id = katello_content_view_filters.id').
|
14
|
+
where("katello_content_view_filters.content_view_id IN (#{content_view_ids.join(',')})")
|
15
|
+
end
|
11
16
|
end
|
12
17
|
end
|
@@ -673,7 +673,7 @@ module Katello
|
|
673
673
|
|
674
674
|
def self.smart_proxy_syncable
|
675
675
|
joins(:content_view_version => :content_view).
|
676
|
-
merge(ContentView.ignore_generated).
|
676
|
+
merge(ContentView.ignore_generated(include_library_generated: true)).
|
677
677
|
where.not(environment_id: nil)
|
678
678
|
end
|
679
679
|
|
@@ -935,6 +935,7 @@ module Katello
|
|
935
935
|
repository_type.content_types_to_index.each do |type|
|
936
936
|
Katello::Logging.time("CONTENT_INDEX", data: {type: type.model_class}) do
|
937
937
|
Katello::ContentUnitIndexer.new(content_type: type, repository: self, optimized: !full_index).import_all
|
938
|
+
type.model_class.orphaned.destroy_all
|
938
939
|
end
|
939
940
|
end
|
940
941
|
repository_type.index_additional_data_proc&.call(self)
|
@@ -142,14 +142,39 @@ module Katello
|
|
142
142
|
def sync_repository_associations(assocication_tracker, additive: false)
|
143
143
|
unless additive
|
144
144
|
ActiveRecord::Base.connection.uncached do
|
145
|
-
@model_class.repository_association_class.where(repository_id: @repository.id).where.
|
146
|
-
not(@model_class.unit_id_field => assocication_tracker.unit_ids)
|
145
|
+
repo_associations_to_destroy = @model_class.repository_association_class.where(repository_id: @repository.id).where.
|
146
|
+
not(@model_class.unit_id_field => assocication_tracker.unit_ids)
|
147
|
+
clean_filter_rules(repo_associations_to_destroy) if repo_associations_to_destroy.present? && [::Katello::ModuleStream, ::Katello::Erratum, ::Katello::PackageGroup].include?(@model_class)
|
148
|
+
repo_associations_to_destroy.destroy_all
|
147
149
|
end
|
148
150
|
end
|
149
151
|
return if assocication_tracker.db_values.empty?
|
150
152
|
@model_class.repository_association_class.upsert_all(assocication_tracker.db_values, :unique_by => association_class_uniqiness_attributes)
|
151
153
|
end
|
152
154
|
|
155
|
+
def clean_filter_rules(repo_associations_to_destroy)
|
156
|
+
affected_content_view_ids = @repository.content_views.non_default.pluck(:id)
|
157
|
+
return false if affected_content_view_ids.empty?
|
158
|
+
case @model_class.to_s
|
159
|
+
when 'Katello::ModuleStream'
|
160
|
+
module_stream_ids = repo_associations_to_destroy.pluck(:module_stream_id)
|
161
|
+
filter_rules = ::Katello::ContentViewModuleStreamFilterRule.
|
162
|
+
in_content_views(affected_content_view_ids).where(module_stream_id: module_stream_ids)
|
163
|
+
filter_rules.delete_all
|
164
|
+
when 'Katello::Erratum'
|
165
|
+
errata_ids = ::Katello::Erratum.where(id: repo_associations_to_destroy.select(:erratum_id)).pluck(:errata_id)
|
166
|
+
filter_rules = ::Katello::ContentViewErratumFilterRule.in_content_views(affected_content_view_ids).where(errata_id: errata_ids)
|
167
|
+
filter_rules.delete_all
|
168
|
+
when 'Katello::PackageGroup'
|
169
|
+
package_group_uuids = ::Katello::PackageGroup.where(id: repo_associations_to_destroy.select(:package_group_id)).pluck(:pulp_id)
|
170
|
+
filter_rules = ::Katello::ContentViewPackageGroupFilterRule.
|
171
|
+
in_content_views(affected_content_view_ids).where(uuid: package_group_uuids)
|
172
|
+
filter_rules.delete_all
|
173
|
+
else
|
174
|
+
return false
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
153
178
|
def association_class_uniqiness_attributes
|
154
179
|
columns = [@model_class.unit_id_field, 'repository_id']
|
155
180
|
found = ActiveRecord::Base.connection.indexes(@model_class.repository_association_class.table_name).find do |index|
|
@@ -168,7 +168,7 @@ module Katello
|
|
168
168
|
end
|
169
169
|
|
170
170
|
def delete_orphans
|
171
|
-
[orphans_api.cleanup(PulpcoreClient::OrphansCleanup.new(orphan_protection_time: Setting[:orphan_protection_time]))]
|
171
|
+
[orphans_api.cleanup(PulpcoreClient::OrphansCleanup.new(orphan_protection_time: (smart_proxy.pulp_mirror? ? 0 : Setting[:orphan_protection_time])))]
|
172
172
|
end
|
173
173
|
|
174
174
|
def delete_remote(remote_href)
|
@@ -67,6 +67,13 @@ module Katello
|
|
67
67
|
def add_content(content_unit_href)
|
68
68
|
content_unit_href = [content_unit_href] unless content_unit_href.is_a?(Array)
|
69
69
|
api.repositories_api.add(repository_reference.repository_href, content_units: content_unit_href)
|
70
|
+
rescue api.client_module::ApiError => e
|
71
|
+
if e.message.include? 'Could not find the following content units'
|
72
|
+
raise ::Katello::Errors::Pulp3Error, "Content units that do not exist in Pulp were requested to be copied."\
|
73
|
+
" Please run a complete sync on the following repository: #{repository_reference.root_repository.name}. Original error: #{e.message}"
|
74
|
+
else
|
75
|
+
raise e
|
76
|
+
end
|
70
77
|
end
|
71
78
|
|
72
79
|
def copy_units_recursively(unit_hrefs, clear_repo = false)
|
@@ -493,11 +493,26 @@ module Katello
|
|
493
493
|
else
|
494
494
|
api.repositories_api.modify(repository_reference.repository_href, add_content_units: content_unit_href)
|
495
495
|
end
|
496
|
+
rescue api.client_module::ApiError => e
|
497
|
+
if e.message.include? 'Could not find the following content units'
|
498
|
+
raise ::Katello::Errors::Pulp3Error, "Content units that do not exist in Pulp were requested to be copied."\
|
499
|
+
" Please run a complete sync on the following repository: #{repository_reference.root_repository.name}. Original error: #{e.message}"
|
500
|
+
else
|
501
|
+
raise e
|
502
|
+
end
|
496
503
|
end
|
497
504
|
|
498
505
|
def add_content_for_repo(repository_href, content_unit_href)
|
499
506
|
content_unit_href = [content_unit_href] unless content_unit_href.is_a?(Array)
|
500
507
|
api.repositories_api.modify(repository_href, add_content_units: content_unit_href)
|
508
|
+
rescue api.client_module::ApiError => e
|
509
|
+
if e.message.include? 'Could not find the following content units'
|
510
|
+
raise ::Katello::Errors::Pulp3Error, "Content units that do not exist in Pulp were requested to be copied."\
|
511
|
+
" Please run a complete sync on the following repository:"\
|
512
|
+
" #{::Katello::Pulp3::RepositoryReference.find_by(repository_href: repository_href).root_repository.name}. Original error: #{e.message}"
|
513
|
+
else
|
514
|
+
raise e
|
515
|
+
end
|
501
516
|
end
|
502
517
|
|
503
518
|
def unit_keys(uploads)
|
@@ -21,6 +21,11 @@ module Katello
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
+
def library_export_repo(repository = nil)
|
25
|
+
cv = repository.content_view if repository
|
26
|
+
cv.library_export?
|
27
|
+
end
|
28
|
+
|
24
29
|
def clear_smart_proxy_sync_histories(repo_list = [])
|
25
30
|
if repo_list.empty?
|
26
31
|
@smart_proxy.smart_proxy_sync_histories.delete_all
|
@@ -32,7 +37,7 @@ module Katello
|
|
32
37
|
|
33
38
|
def combined_repos_available_to_capsule(environment = nil, content_view = nil, repository = nil)
|
34
39
|
lifecycle_environment_check(environment, repository)
|
35
|
-
if repository
|
40
|
+
if repository && !library_export_repo(repository)
|
36
41
|
[repository]
|
37
42
|
else
|
38
43
|
repositories_available_to_capsule(environment, content_view)
|
data/lib/katello/plugin.rb
CHANGED
@@ -670,7 +670,7 @@ Foreman::Plugin.register :katello do
|
|
670
670
|
type: :integer,
|
671
671
|
default: 1440,
|
672
672
|
full_name: N_('Orphaned Content Protection Time'),
|
673
|
-
description: N_('Time in minutes
|
673
|
+
description: N_('Time in minutes before content that is not contained within a repository and has not been accessed is considered orphaned.')
|
674
674
|
|
675
675
|
setting 'remote_execution_prefer_registered_through_proxy',
|
676
676
|
type: :boolean,
|
@@ -0,0 +1,40 @@
|
|
1
|
+
namespace :katello do
|
2
|
+
namespace :upgrades do
|
3
|
+
namespace '4.9' do
|
4
|
+
desc "Clean orphaned filter rules that cause Pulp copy errors during content view publishing"
|
5
|
+
task :clean_orphaned_filter_rules => ['environment'] do
|
6
|
+
module_stream_count = 0
|
7
|
+
erratum_count = 0
|
8
|
+
package_group_count = 0
|
9
|
+
|
10
|
+
::Katello::ContentViewModuleStreamFilterRule.all.each do |rule|
|
11
|
+
# Delete if rule exists in a CV that does not have the matching module stream in its repositories
|
12
|
+
content_view = rule.filter.content_view
|
13
|
+
unless ::Katello::ModuleStream.in_repositories(content_view.repositories)&.pluck(:id)&.include?(rule.module_stream_id)
|
14
|
+
rule.delete
|
15
|
+
module_stream_count += 1
|
16
|
+
end
|
17
|
+
end
|
18
|
+
puts "#{module_stream_count} orphaned content view module stream filter rules were deleted."
|
19
|
+
|
20
|
+
::Katello::ContentViewErratumFilterRule.all.each do |rule|
|
21
|
+
content_view = rule.filter.content_view
|
22
|
+
unless ::Katello::Erratum.in_repositories(content_view.repositories)&.pluck(:errata_id)&.include?(rule.errata_id)
|
23
|
+
rule.delete
|
24
|
+
erratum_count += 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
puts "#{erratum_count} orphaned content view erratum filter rules were deleted."
|
28
|
+
|
29
|
+
::Katello::ContentViewPackageGroupFilterRule.all.each do |rule|
|
30
|
+
content_view = rule.filter.content_view
|
31
|
+
unless ::Katello::PackageGroup.in_repositories(content_view.repositories)&.pluck(:pulp_id)&.include?(rule.uuid)
|
32
|
+
rule.delete
|
33
|
+
package_group_count += 1
|
34
|
+
end
|
35
|
+
end
|
36
|
+
puts "#{package_group_count} orphaned content view package group filter rules were deleted."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/lib/katello/version.rb
CHANGED
data/locale/Makefile
CHANGED
@@ -10,6 +10,7 @@ DOMAIN = katello
|
|
10
10
|
VERSION = $(shell git describe --abbrev=0 --tags)
|
11
11
|
POTFILE = $(DOMAIN).pot
|
12
12
|
MOFILE = $(DOMAIN).mo
|
13
|
+
ACTIONFILE = action_names.rb
|
13
14
|
POFILES = $(shell find . -name '$(DOMAIN).po')
|
14
15
|
MOFILES = $(patsubst %.po,%.mo,$(POFILES))
|
15
16
|
POXFILES = $(patsubst %.po,%.pox,$(POFILES))
|
@@ -50,11 +51,11 @@ tx-pull: $(EDITFILES)
|
|
50
51
|
|
51
52
|
tx-update: tx-pull
|
52
53
|
@echo
|
53
|
-
@echo Run rake plugin:gettext[$(DOMAIN)] from the Foreman installation, then make -C locale
|
54
|
+
@echo Run rake plugin:gettext[$(DOMAIN)] and rake plugin:po_to_json[$(DOMAIN)] from the Foreman installation, then make -C locale po-files to finish
|
54
55
|
@echo
|
55
56
|
|
56
|
-
|
57
|
-
git add $(POFILES) $(POTFILE) ../locale
|
57
|
+
commit-translation-files: $(POFILES)
|
58
|
+
git add $(POFILES) $(POTFILE) $(ACTIONFILE) ../app/assets/javascripts/katello/locale
|
58
59
|
git commit -m "i18n - pulling from tx"
|
59
60
|
@echo
|
60
61
|
@echo Changes commited!
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: katello
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.9.0.
|
4
|
+
version: 4.9.0.rc2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- N/A
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-06-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -4482,6 +4482,7 @@ files:
|
|
4482
4482
|
- lib/katello/tasks/upgrades/4.4/publish_import_cvvs.rake
|
4483
4483
|
- lib/katello/tasks/upgrades/4.8/fix_incorrect_providers.rake
|
4484
4484
|
- lib/katello/tasks/upgrades/4.8/regenerate_imported_repository_metadata.rake
|
4485
|
+
- lib/katello/tasks/upgrades/4.9/clean_orphaned_filter_rules.rake
|
4485
4486
|
- lib/katello/tasks/upgrades/4.9/update_custom_products_enablement.rake
|
4486
4487
|
- lib/katello/tasks/virt_who_report.rake
|
4487
4488
|
- lib/katello/url_constrained_cookie_store.rb
|
@@ -5502,7 +5503,7 @@ homepage: http://www.katello.org
|
|
5502
5503
|
licenses:
|
5503
5504
|
- GPL-2.0
|
5504
5505
|
metadata: {}
|
5505
|
-
post_install_message:
|
5506
|
+
post_install_message:
|
5506
5507
|
rdoc_options: []
|
5507
5508
|
require_paths:
|
5508
5509
|
- lib
|
@@ -5517,8 +5518,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
5517
5518
|
- !ruby/object:Gem::Version
|
5518
5519
|
version: 1.3.1
|
5519
5520
|
requirements: []
|
5520
|
-
rubygems_version: 3.
|
5521
|
-
signing_key:
|
5521
|
+
rubygems_version: 3.3.26
|
5522
|
+
signing_key:
|
5522
5523
|
specification_version: 4
|
5523
5524
|
summary: Content and Subscription Management plugin for Foreman
|
5524
5525
|
test_files: []
|