Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #36716 - Add check for missing product-content #757

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
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
Loading