Skip to content

Commit

Permalink
Fixes #36716 - Add check for missing content
Browse files Browse the repository at this point in the history
  • Loading branch information
m-bucher committed Sep 4, 2023
1 parent c8bdc1c commit e6fb497
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 0 deletions.
36 changes: 36 additions & 0 deletions definitions/checks/candlepin/product_content_association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module Checks
module Candlepin
class ProductContentAssociation < ForemanMaintain::Check
metadata do
description 'Make sure Product to Repository association in Candlepin DB is complete'
label :candlepin_prod_repo_assoc
tags :post_upgrade
for_feature :candlepin_database
end

def missing_cp_associations
feature(:candlepin_database).query(<<~SQL)
SELECT c.content_id, c.uuid, c.name
FROM cp2_content c
JOIN cp2_owner_content oc ON c.uuid=oc.content_uuid
LEFT OUTER JOIN (
SELECT pc.content_uuid
FROM cp2_products p
JOIN cp2_owner_products op ON p.uuid=op.product_uuid
JOIN cp2_product_content pc ON p.uuid=pc.product_uuid
) x ON c.uuid = x.content_uuid
WHERE x.content_uuid IS NULL
SQL
end

def run
missing = missing_cp_associations

assert(missing.empty?,
"Candlepin DB is missing some Product to Content associations!\n" \
"Found #{missing.length} content entries with missing product association.",
:next_steps => [Procedures::Candlepin::ProductContentAssociation.new])
end
end
end
end
124 changes: 124 additions & 0 deletions definitions/procedures/candlepin/product_content_association.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
require 'set'

module Procedures::Candlepin
class ProductContentAssociation < ForemanMaintain::Procedure
metadata do
for_feature :candlepin_database
description 'Reassociate Content to Product in CandlepinDB'
end

# returns a Hash of candlepin product cp_id keys with a Hash of queried values
# consisting of the product's name and the number of associated content
# e.g. { '<cp_id>' => { 'name' => '...', count => X, ..}, ... }
def foreman_content_num_by_product
feature(:foreman_database).query(<<~SQL).map { |e| [e['cp_id'], e] }.to_h
SELECT p.cp_id as cp_id, p.name as name, COUNT(c.id) as count
FROM katello_products p
JOIN katello_product_contents pc ON p.id = pc.product_id
JOIN katello_contents c ON pc.content_id = c.id
GROUP BY p.cp_id, p.name
SQL
end

# return Hash of query-result Hashes with the respective candlepin product_id as key
# similar to foreman_content_num_by_product()
def cp_content_count_by_product
feature(:candlepin_database).query(<<~SQL).map { |e| [e['product_id'], e] }.to_h
SELECT product.product_id, product.uuid, product.name, COUNT(content.content_id)
FROM cp_pool pool
JOIN cp2_products product ON pool.product_uuid = product.uuid
LEFT JOIN cp2_product_content pc ON product.uuid = pc.product_uuid
LEFT JOIN cp2_content content ON content.uuid = pc.content_uuid
GROUP BY product.uuid
SQL
end

# returns a set of cp2_content ids for given product_id
def cp_product_content_ids(product_id)
feature(:candlepin_database).query(<<~SQL).map { |e| e['content_id'] }.to_set
SELECT content.content_id
FROM cp_pool pool
JOIN cp2_products product ON pool.product_uuid = product.uuid
JOIN cp2_product_content pc ON product.uuid = pc.product_uuid
JOIN cp2_content content ON content.uuid = pc.content_uuid
WHERE product.product_id = '#{product_id}'
SQL
end

# return Set of candlepin content ids from katello_content table
# for candlepin product with cp_id
def katello_content_ids(cp_id)
feature(:foreman_database).query(<<~SQL).map { |e| e['cp_content_id'] }.to_set
SELECT c.cp_content_id
FROM katello_products p
JOIN katello_product_contents pc ON p.id = pc.product_id
JOIN katello_contents c ON pc.content_id = c.id
WHERE p.cp_id = '#{cp_id}'
SQL
end

def assemble_restore_commands(look_closer_products)
commands = []
look_closer_products.each do |cp_id, product|
puts "Process Product #{product['name'].inspect}"
# get content_ids from candlepin and katello
missing_ids = katello_content_ids(cp_id) - cp_product_content_ids(cp_id)

missing_ids.each do |content_id|
commands << create_new_association_sql_inserts(product['uuid'], content_id)

# clear entity version of affected product to avoid versioning and convergence issues
commands << 'UPDATE cp2_products SET entity_version = NULL ' \
"WHERE uuid = '#{product['uuid']}'"
end
end
commands
end

# returns SQL-INSERT String to recreate missing associations
def create_new_association_sql_inserts(product_uuid, content_id)
missing = feature(:candlepin_database).query(
"SELECT name, uuid FROM cp2_content WHERE content_id = '#{content_id}'"
)
insert_sql = []
missing.each do |content|
puts " - repair missing: #{content['name'].inspect}"
insert_sql << "(REPLACE(uuid_in((md5((random())::text))::cstring)::text, '-', '' )," \
' true,' \
" '#{product_uuid}'," \
" '#{content['uuid']}'," \
' NOW(), NOW())'
end

<<~SQL
INSERT INTO cp2_product_content
(id, enabled, product_uuid, content_uuid, created, updated)
VALUES #{insert_sql.join(', ')}
SQL
end

def run
candlepin_content_num_by_product = cp_content_count_by_product
look_closer_products = {}

foreman_content_num_by_product.each do |product_id, foreman_product|
next unless candlepin_content_num_by_product.key?(product_id)

candlepin_product = candlepin_content_num_by_product[product_id]
next unless foreman_product['count'] != candlepin_product['count']

look_closer_products[product_id] = candlepin_product
end

res = feature(:candlepin_database).psql(<<~SQL)
BEGIN;
#{assemble_restore_commands(look_closer_products).join(";\n")};
COMMIT;
SQL

if res.include? 'ERROR'
warn! "Repairing Product-Content association in CandlepinDB failed. Please check the logs."
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
require 'test_helper'

describe Checks::Candlepin::ProductContentAssociation do
include DefinitionsTestHelper

subject do
Checks::Candlepin::ProductContentAssociation.new
end

it 'passes when nothing found' do
assume_feature_present(:candlepin_database) do |db|
db.any_instance.expects(:query).returns([])
end
result = run_check(subject)
assert result.success?, 'Check expected to succeed'
end

it 'fails when missing associations' do
assume_feature_present(:candlepin_database) do |db|
db.any_instance.expects(:query).returns([{
'content_id' => '123',
'uuid' => 'feed',
'name' => 'foo',
}])
end
result = run_check(subject)
assert result.fail?, 'Check expected to fail'
msg = "Candlepin DB is missing some Product to Content associations!\n"
msg += 'Found 1 content entries with missing product association.'
assert_match msg, result.output
assert_equal [Procedures::Candlepin::ProductContentAssociation], subject.next_steps.map(&:class)
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
require 'test_helper'

describe Procedures::Candlepin::ProductContentAssociation do
include DefinitionsTestHelper

subject do
Procedures::Candlepin::ProductContentAssociation.new
end

it 'fixes missing association' do
product_id = '12345'
product_name = 'dummy'
content_id = '67890'
content_name = 'Missing Repo'
content_uuid = 'dead'
assume_feature_present(:candlepin_database) do |db|
db.any_instance.expects(:query).with(
"SELECT name, uuid FROM cp2_content WHERE content_id = '#{content_id}'"
).once.returns([{
'name' => content_name,
'uuid' => content_uuid,
}])
db.any_instance.expects(:psql).once.returns("BEGIN
INSERT 0 2
UPDATE 1
COMMIT
")
end

subject.expects(:foreman_content_num_by_product).once.returns({ product_id => {
'cp_id' => product_id, 'name' => product_name, 'count' => 1
} })
subject.expects(:cp_content_count_by_product).once.returns({ product_id => {
'product_id' => product_id, 'uuid' => 'feed', 'name' => product_name, 'count' => 0
} })
subject.expects(:cp_product_content_ids).once.with(product_id).returns([].to_set)
subject.expects(:katello_content_ids).once.with(product_id).returns([content_id].to_set)

result = run_procedure(subject)
assert result.success?, 'the procedure was expected to succeed'
msg = "Process Product #{product_name.inspect}\n"
msg += " - repair missing: #{content_name.inspect}\n"
assert_stdout msg
end
end

0 comments on commit e6fb497

Please sign in to comment.