Skip to content

Commit 718e497

Browse files
committed
1 parent c8bdc1c commit 718e497

4 files changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
module Checks
2+
module Candlepin
3+
class ProductContentAssociation < ForemanMaintain::Check
4+
metadata do
5+
description 'Make sure Product to Repository association in Candlepin DB is complete'
6+
label :candlepin_prod_repo_assoc
7+
tags :post_upgrade
8+
for_feature :candlepin_database
9+
end
10+
11+
def missing_cp_associations
12+
feature(:candlepin_database).query(<<~SQL)
13+
SELECT c.content_id, c.uuid, c.name
14+
FROM cp2_content c
15+
JOIN cp2_owner_content oc ON c.uuid=oc.content_uuid
16+
LEFT OUTER JOIN (
17+
SELECT pc.content_uuid
18+
FROM cp2_products p
19+
JOIN cp2_owner_products op ON p.uuid=op.product_uuid
20+
JOIN cp2_product_content pc ON p.uuid=pc.product_uuid
21+
) x ON c.uuid = x.content_uuid
22+
WHERE x.content_uuid IS NULL
23+
SQL
24+
end
25+
26+
def run
27+
missing = missing_cp_associations
28+
29+
assert(missing.empty?,
30+
"Candlepin DB is missing some Product to Content associations!\n" \
31+
"Found #{missing.length} content entries with missing product association.",
32+
:next_steps => [Procedures::Candlepin::ProductContentAssociation.new])
33+
end
34+
end
35+
end
36+
end
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
module Procedures::Candlepin
2+
class ProductContentAssociation < ForemanMaintain::Procedure
3+
metadata do
4+
for_feature :candlepin_database
5+
description 'Reassociate Content to Product in CandlepinDB'
6+
end
7+
8+
# returns a Hash of candlepin product cp_id keys with a Hash of queried values
9+
# consisting of the product's name and the number of associated content
10+
# e.g. { '<cp_id>' => { 'name' => '...', count => X, ..}, ... }
11+
def foreman_content_num_by_product
12+
feature(:foreman_database).query(<<~SQL).map { |e| [e['cp_id'], e] }.to_h
13+
SELECT p.cp_id as cp_id, p.name as name, COUNT(c.id) as count
14+
FROM katello_products p
15+
JOIN katello_product_contents pc ON p.id = pc.product_id
16+
JOIN katello_contents c ON pc.content_id = c.id
17+
GROUP BY p.cp_id, p.name
18+
SQL
19+
end
20+
21+
# return Hash of query-result Hashes with the respective candlepin product_id as key
22+
# similar to foreman_content_num_by_product()
23+
def cp_content_count_by_product
24+
feature(:candlepin_database).query(<<~SQL).map { |e| [e['product_id'], e] }.to_h
25+
SELECT product.product_id, product.uuid, product.name, COUNT(content.content_id)
26+
FROM cp_pool pool
27+
JOIN cp2_products product ON pool.product_uuid = product.uuid
28+
LEFT JOIN cp2_product_content pc ON product.uuid = pc.product_uuid
29+
LEFT JOIN cp2_content content ON content.uuid = pc.content_uuid
30+
GROUP BY product.uuid
31+
SQL
32+
end
33+
34+
# returns a set of cp2_content ids for given product_id
35+
def cp_product_content_ids(product_id)
36+
feature(:candlepin_database).query(<<~SQL).map { |e| e['content_id'] }.to_set
37+
SELECT content.content_id
38+
FROM cp_pool pool
39+
JOIN cp2_products product ON pool.product_uuid = product.uuid
40+
JOIN cp2_product_content pc ON product.uuid = pc.product_uuid
41+
JOIN cp2_content content ON content.uuid = pc.content_uuid
42+
WHERE product.product_id = '#{product_id}'
43+
SQL
44+
end
45+
46+
# return Set of candlepin content ids from katello_content table
47+
# for candlepin product with cp_id
48+
def katello_content_ids(cp_id)
49+
feature(:foreman_database).query(<<~SQL).map { |e| e['cp_content_id'] }.to_set
50+
SELECT c.cp_content_id
51+
FROM katello_products p
52+
JOIN katello_product_contents pc ON p.id = pc.product_id
53+
JOIN katello_contents c ON pc.content_id = c.id
54+
WHERE p.cp_id = '#{cp_id}'
55+
SQL
56+
end
57+
58+
def assemble_restore_commands(look_closer_products)
59+
commands = []
60+
look_closer_products.each do |cp_id, product|
61+
puts "Process Product #{product['name'].inspect}"
62+
# get content_ids from candlepin and katello
63+
missing_ids = katello_content_ids(cp_id) - cp_product_content_ids(cp_id)
64+
65+
missing_ids.each do |content_id|
66+
commands << create_new_association_sql_inserts(product['uuid'], content_id)
67+
68+
# clear entity version of affected product to avoid versioning and convergence issues
69+
commands << 'UPDATE cp2_products SET entity_version = NULL ' \
70+
"WHERE uuid = '#{product['uuid']}'"
71+
end
72+
end
73+
commands
74+
end
75+
76+
# returns SQL-INSERT String to recreate missing associations
77+
def create_new_association_sql_inserts(product_uuid, content_id)
78+
missing = feature(:candlepin_database).query(
79+
"SELECT name, uuid FROM cp2_content WHERE content_id = '#{content_id}'"
80+
)
81+
insert_sql = []
82+
missing.each do |content|
83+
puts " - repair missing: #{content['name'].inspect}"
84+
insert_sql << "(REPLACE(uuid_in((md5((random())::text))::cstring)::text, '-', '' )," \
85+
' true,' \
86+
" '#{product_uuid}'," \
87+
" '#{content['uuid']}'," \
88+
' NOW(), NOW())'
89+
end
90+
91+
<<~SQL
92+
INSERT INTO cp2_product_content
93+
(id, enabled, product_uuid, content_uuid, created, updated)
94+
VALUES #{insert_sql.join(', ')}
95+
SQL
96+
end
97+
98+
def run
99+
candlepin_content_num_by_product = cp_content_count_by_product
100+
look_closer_products = {}
101+
102+
foreman_content_num_by_product.each do |product_id, foreman_product|
103+
next unless candlepin_content_num_by_product.key?(product_id)
104+
105+
candlepin_product = candlepin_content_num_by_product[product_id]
106+
next unless foreman_product['count'] != candlepin_product['count']
107+
108+
look_closer_products[product_id] = candlepin_product
109+
end
110+
111+
res = feature(:candlepin_database).psql(<<~SQL)
112+
BEGIN;
113+
#{assemble_restore_commands(look_closer_products).join(";\n")};
114+
COMMIT;
115+
SQL
116+
117+
if res.include? 'ERROR'
118+
warn! "Repairing Product-Content association in CandlepinDB failed. Please check the logs."
119+
end
120+
end
121+
end
122+
end
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require 'test_helper'
2+
3+
describe Checks::Candlepin::ProductContentAssociation do
4+
include DefinitionsTestHelper
5+
6+
subject do
7+
Checks::Candlepin::ProductContentAssociation.new
8+
end
9+
10+
it 'passes when nothing found' do
11+
assume_feature_present(:candlepin_database) do |db|
12+
db.any_instance.expects(:query).returns([])
13+
end
14+
result = run_check(subject)
15+
assert result.success?, 'Check expected to succeed'
16+
end
17+
18+
it 'fails when missing associations' do
19+
assume_feature_present(:candlepin_database) do |db|
20+
db.any_instance.expects(:query).returns([{
21+
'content_id' => '123',
22+
'uuid' => 'feed',
23+
'name' => 'foo',
24+
}])
25+
end
26+
result = run_check(subject)
27+
assert result.fail?, 'Check expected to fail'
28+
msg = "Candlepin DB is missing some Product to Content associations!\n"
29+
msg += 'Found 1 content entries with missing product association.'
30+
assert_match msg, result.output
31+
assert_equal [Procedures::Candlepin::ProductContentAssociation], subject.next_steps.map(&:class)
32+
end
33+
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
require 'test_helper'
2+
3+
describe Procedures::Candlepin::ProductContentAssociation do
4+
include DefinitionsTestHelper
5+
6+
subject do
7+
Procedures::Candlepin::ProductContentAssociation.new
8+
end
9+
10+
it 'fixes missing association' do
11+
product_id = '12345'
12+
product_name = 'dummy'
13+
content_id = '67890'
14+
content_name = 'Missing Repo'
15+
content_uuid = 'dead'
16+
assume_feature_present(:candlepin_database) do |db|
17+
db.any_instance.expects(:query).with(
18+
"SELECT name, uuid FROM cp2_content WHERE content_id = '#{content_id}'"
19+
).once.returns([{
20+
'name' => content_name,
21+
'uuid' => content_uuid,
22+
}])
23+
db.any_instance.expects(:psql).once.returns("BEGIN
24+
INSERT 0 2
25+
UPDATE 1
26+
COMMIT
27+
")
28+
end
29+
30+
subject.expects(:foreman_content_num_by_product).once.returns({ product_id => {
31+
'cp_id' => product_id, 'name' => product_name, 'count' => 1
32+
} })
33+
subject.expects(:cp_content_count_by_product).once.returns({ product_id => {
34+
'product_id' => product_id, 'uuid' => 'feed', 'name' => product_name, 'count' => 0
35+
} })
36+
subject.expects(:cp_product_content_ids).once.with(product_id).returns([].to_set)
37+
subject.expects(:katello_content_ids).once.with(product_id).returns([content_id].to_set)
38+
39+
result = run_procedure(subject)
40+
assert result.success?, 'the procedure was expected to succeed'
41+
msg = "Process Product #{product_name.inspect}\n"
42+
msg += " - repair missing: #{content_name.inspect}\n"
43+
assert_stdout msg
44+
end
45+
end

0 commit comments

Comments
 (0)