/**
* Copyright (c) 2009 - 2012 Red Hat, Inc.
*
* This software is licensed to you under the GNU General Public License,
* version 2 (GPLv2). There is NO WARRANTY for this software, express or
* implied, including the implied warranties of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. You should have received a copy of GPLv2
* along with this software; if not, see
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.txt.
*
* Red Hat trademarks are not licensed under GPLv2. No permission is
* granted to use or replicate Red Hat trademarks that are incorporated
* in this software or its documentation.
*/
package org.candlepin.policy.js.pool;
import org.candlepin.common.config.Configuration;
import org.candlepin.config.ConfigProperties;
import org.candlepin.controller.PoolManager;
import org.candlepin.model.Branding;
import org.candlepin.model.Consumer;
import org.candlepin.model.Entitlement;
import org.candlepin.model.EntitlementCurator;
import org.candlepin.model.OwnerProductCurator;
import org.candlepin.model.Pool;
import org.candlepin.model.Product;
import org.candlepin.model.ProductCurator;
import org.candlepin.model.dto.Subscription;
import com.google.inject.Inject;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.BooleanUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Rules for creation and updating of pools during a refresh pools operation.
*/
public class PoolRules {
private static Logger log = LoggerFactory.getLogger(PoolRules.class);
private PoolManager poolManager;
private Configuration config;
private EntitlementCurator entCurator;
private OwnerProductCurator ownerProductCurator;
private ProductCurator productCurator;
@Inject
public PoolRules(PoolManager poolManager, Configuration config, EntitlementCurator entCurator,
OwnerProductCurator ownerProductCurator, ProductCurator productCurator) {
this.poolManager = poolManager;
this.config = config;
this.entCurator = entCurator;
this.ownerProductCurator = ownerProductCurator;
this.productCurator = productCurator;
}
private long calculateQuantity(long quantity, Product product, String upstreamPoolId) {
long result = quantity * product.getMultiplier();
// In hosted, we increase the quantity on the subscription. However in standalone,
// we assume this already has happened in hosted and the accurate quantity was
// exported:
String multiplier = product.getAttributeValue(Product.Attributes.INSTANCE_MULTIPLIER);
if (multiplier != null && upstreamPoolId == null) {
int instanceMultiplier = Integer.parseInt(multiplier);
log.debug("Increasing pool quantity for instance multiplier: {}", instanceMultiplier);
result = result * instanceMultiplier;
}
return result;
}
public List<Pool> createAndEnrichPools(Subscription sub) {
return createAndEnrichPools(sub, new LinkedList<Pool>());
}
public List<Pool> createAndEnrichPools(Subscription sub, List<Pool> existingPools) {
Pool pool = this.poolManager.convertToMasterPool(sub);
return createAndEnrichPools(pool, existingPools);
}
/**
* Create any pools that need to be created for the given pool.
*
* In some scenarios, due to attribute changes, pools may need to be created even though
* pools already exist for the subscription. A list of pre-existing pools for the given
* sub are provided to help this method determine if something needs to be done or not.
*
* For a genuine new pool, the existing pools list will be empty.
*
* @param masterPool
* @param existingPools
* @return a list of pools created for the given pool
*/
public List<Pool> createAndEnrichPools(Pool masterPool, List<Pool> existingPools) {
List<Pool> pools = new LinkedList<Pool>();
masterPool.setQuantity(calculateQuantity(masterPool.getQuantity(),
masterPool.getProduct(), masterPool.getUpstreamPoolId()));
String virtOnly = masterPool.getProductAttributeValue(Product.Attributes.VIRT_ONLY);
// The following will make virt_only a pool attribute. That makes the
// pool explicitly virt_only to subscription manager and any other
// downstream consumer.
if (virtOnly != null && !virtOnly.isEmpty()) {
masterPool.setAttribute(Pool.Attributes.VIRT_ONLY, virtOnly);
}
log.info("Checking if pools need to be created for: {}", masterPool);
if (!hasMasterPool(existingPools)) {
if (masterPool.getSourceSubscription() != null &&
masterPool.getSourceSubscription().getSubscriptionSubKey().contentEquals("derived")) {
// while we can create bonus pool from master pool, the reverse
// is not possible without the subscription itself
throw new IllegalStateException("Cannot create master pool from bonus pool");
}
pools.add(masterPool);
log.info("Creating new master pool: {}", masterPool);
}
Pool bonusPool = createBonusPool(masterPool, existingPools);
if (bonusPool != null) {
pools.add(bonusPool);
}
return pools;
}
/*
* If this subscription carries a virt_limit, we need to either create a
* bonus pool for any guest (legacy behavior, only in hosted), or a pool for
* temporary use of unmapped guests. (current behavior for any pool with
* virt_limit)
*/
private Pool createBonusPool(Pool masterPool, List<Pool> existingPools) {
Map<String, String> attributes = masterPool.getProductAttributes();
String virtQuantity = getVirtQuantity(
attributes.get(Product.Attributes.VIRT_LIMIT), masterPool.getQuantity()
);
log.info("Checking if bonus pools need to be created for pool: {}", masterPool);
if (attributes.containsKey(Product.Attributes.VIRT_LIMIT) && !hasBonusPool(existingPools) &&
virtQuantity != null) {
boolean hostLimited = "true".equals(attributes.get(Product.Attributes.HOST_LIMITED));
HashMap<String, String> virtAttributes = new HashMap<String, String>();
virtAttributes.put(Pool.Attributes.VIRT_ONLY, "true");
virtAttributes.put(Pool.Attributes.DERIVED_POOL, "true");
virtAttributes.put(Pool.Attributes.PHYSICAL_ONLY, "false");
if (hostLimited || config.getBoolean(ConfigProperties.STANDALONE)) {
virtAttributes.put(Pool.Attributes.UNMAPPED_GUESTS_ONLY, "true");
}
// Make sure the virt pool does not have a virt_limit,
// otherwise this will recurse infinitely
virtAttributes.put(Product.Attributes.VIRT_LIMIT, "0");
// Favor derived products if they are available
Product sku = masterPool.getDerivedProduct() != null ? masterPool.getDerivedProduct() :
masterPool.getProduct();
// Using derived here because only one derived pool is created for
// this subscription
Pool bonusPool = PoolHelper.clonePool(masterPool, sku, virtQuantity, virtAttributes, "derived",
ownerProductCurator, null, productCurator);
log.info("Creating new derived pool: {}", bonusPool);
return bonusPool;
}
return null;
}
private boolean hasMasterPool(List<Pool> pools) {
if (pools != null) {
for (Pool p : pools) {
if (p.getSourceSubscription().getSubscriptionSubKey().equals("master")) {
return true;
}
}
}
return false;
}
private boolean hasBonusPool(List<Pool> pools) {
if (pools != null) {
for (Pool p : pools) {
if (p.getSourceSubscription().getSubscriptionSubKey().equals("derived")) {
return true;
}
}
}
return false;
}
/*
* Returns null if invalid
*/
private String getVirtQuantity(String virtLimit, long quantity) {
if ("unlimited".equals(virtLimit)) {
return virtLimit;
}
try {
int virtLimitInt = Integer.parseInt(virtLimit);
if (virtLimitInt > 0) {
return String.valueOf(virtLimitInt * quantity);
}
}
catch (NumberFormatException nfe) {
// Nothing to update if we get here.
log.warn("Invalid virt_limit attribute specified.");
}
return null;
}
/**
* Refresh pools which have no subscription tied (directly) to them.
*
* @param floatingPools pools with no subscription ID
* @return pool updates
*/
public List<PoolUpdate> updatePools(List<Pool> floatingPools, Map<String, Product> changedProducts) {
List<PoolUpdate> updates = new LinkedList<PoolUpdate>();
for (Pool p : floatingPools) {
if (p.getSubscriptionId() != null) {
// Should be filtered out before calling, but just in case we skip it:
continue;
}
if (p.isDevelopmentPool()) {
continue;
}
if (p.getSourceStack() != null) {
Consumer c = p.getSourceStack().getSourceConsumer();
if (c == null) {
log.error("Stack derived pool has no source consumer: " + p.getId());
}
else {
PoolUpdate update = updatePoolFromStack(p, changedProducts);
if (update.changed()) {
updates.add(update);
}
}
}
}
return updates;
}
public List<PoolUpdate> updatePools(Pool masterPool, List<Pool> existingPools, Long originalQuantity,
Map<String, Product> changedProducts) {
//local.setCertificate(subscription.getCertificate());
log.debug("Refreshing pools for existing master pool: {}", masterPool);
log.debug(" existing pools: {}", existingPools.size());
List<PoolUpdate> poolsUpdated = new LinkedList<PoolUpdate>();
Map<String, String> attributes = masterPool.getProductAttributes();
for (Pool existingPool : existingPools) {
log.debug("Checking pool: {}", existingPool);
// Ensure subscription details are maintained on the master pool
if ("master".equalsIgnoreCase(existingPool.getSubscriptionSubKey())) {
existingPool.setUpstreamPoolId(masterPool.getUpstreamPoolId());
existingPool.setUpstreamEntitlementId(masterPool.getUpstreamEntitlementId());
existingPool.setUpstreamConsumerId(masterPool.getUpstreamConsumerId());
existingPool.setCdn(masterPool.getCdn());
existingPool.setCertificate(masterPool.getCertificate());
}
// Used to track if anything has changed:
PoolUpdate update = new PoolUpdate(existingPool);
update.setDatesChanged(checkForDateChange(masterPool.getStartDate(), masterPool.getEndDate(),
existingPool));
update.setQuantityChanged(checkForQuantityChange(masterPool, existingPool, originalQuantity,
existingPools, attributes));
if (!existingPool.isMarkedForDelete()) {
boolean useDerived = masterPool.getDerivedProduct() != null &&
BooleanUtils.toBoolean(existingPool.getAttributeValue(Pool.Attributes.DERIVED_POOL));
update.setProductsChanged(checkForChangedProducts(
useDerived ? masterPool.getDerivedProduct() : masterPool.getProduct(),
getExpectedProvidedProducts(masterPool, useDerived),
existingPool,
changedProducts)
);
if (!useDerived) {
update.setDerivedProductsChanged(
checkForChangedDerivedProducts(masterPool, existingPool, changedProducts));
}
update.setOrderChanged(checkForOrderDataChanges(masterPool, existingPool));
update.setBrandingChanged(checkForBrandingChanges(masterPool, existingPool));
}
// All done, see if we found any changes and return an update object if so:
if (update.changed()) {
poolsUpdated.add(update);
}
else {
log.debug(" No updates required.");
}
}
return poolsUpdated;
}
/**
* Updates the pool based on the entitlements in the specified stack.
*
* @param pool
* @param changedProducts
* @return pool update specifics
*/
public PoolUpdate updatePoolFromStack(Pool pool, Map<String, Product> changedProducts) {
List<Entitlement> stackedEnts = this.entCurator
.findByStackId(pool.getSourceStack().getSourceConsumer(), pool.getSourceStackId())
.list();
return this.updatePoolFromStackedEntitlements(pool, stackedEnts, changedProducts);
}
/**
* Updates the pool based on the entitlements in the specified stack.
*
* @param pools
* @param consumer
* @return updates
*/
public List<PoolUpdate> updatePoolsFromStack(Consumer consumer, List<Pool> pools,
boolean deleteIfNoStackedEnts) {
return updatePoolsFromStack(consumer, pools, null, deleteIfNoStackedEnts);
}
/**
* Updates the pool based on the entitlements in the specified stack.
*
* @param pools
* @param consumer
* @return updates
*/
public List<PoolUpdate> updatePoolsFromStack(Consumer consumer, List<Pool> pools,
Set<String> alreadyDeletedPools, boolean deleteIfNoStackedEnts) {
Map<String, List<Entitlement>> entitlementMap = new HashMap<String, List<Entitlement>>();
Set<String> sourceStackIds = new HashSet<String>();
List<PoolUpdate> result = new ArrayList<PoolUpdate>();
for (Pool pool : pools) {
sourceStackIds.add(pool.getSourceStackId());
}
for (Entitlement entitlement : this.entCurator.findByStackIds(consumer, sourceStackIds)) {
List<Entitlement> ents = entitlementMap.get(entitlement.getPool().getStackId());
if (ents == null) {
ents = new ArrayList<Entitlement>();
entitlementMap.put(entitlement.getPool().getStackId(), ents);
}
ents.add(entitlement);
}
List<Pool> poolsToDelete = new ArrayList<Pool>();
for (Pool pool : pools) {
List<Entitlement> entitlements = entitlementMap.get(pool.getSourceStackId());
if (CollectionUtils.isNotEmpty(entitlements)) {
result.add(this.updatePoolFromStackedEntitlements(pool, entitlements,
Collections.<String, Product>emptyMap()));
}
else if (deleteIfNoStackedEnts) {
poolsToDelete.add(pool);
}
}
if (!poolsToDelete.isEmpty()) {
this.poolManager.deletePools(poolsToDelete, alreadyDeletedPools);
}
return result;
}
public PoolUpdate updatePoolFromStackedEntitlements(Pool pool, List<Entitlement> stackedEnts,
Map<String, Product> changedProducts) {
PoolUpdate update = new PoolUpdate(pool);
// Nothing to do if there were no entitlements found.
if (CollectionUtils.isEmpty(stackedEnts)) {
return update;
}
pool.setSourceEntitlement(null);
pool.setSourceSubscription(null);
StackedSubPoolValueAccumulator acc = new StackedSubPoolValueAccumulator(pool, stackedEnts,
productCurator);
// Check if the quantity should be changed. If there was no
// virt limiting entitlement, then we leave the quantity alone,
// else, we set the quantity to that of the eldest virt limiting
// entitlement pool.
Entitlement eldestWithVirtLimit = acc.getEldestWithVirtLimit();
if (eldestWithVirtLimit != null) {
// Quantity may have changed, lets see.
String virtLimit =
eldestWithVirtLimit.getPool().getProductAttributeValue(Product.Attributes.VIRT_LIMIT);
Long quantity = Pool.parseQuantity(virtLimit);
if (!quantity.equals(pool.getQuantity())) {
pool.setQuantity(quantity);
update.setQuantityChanged(true);
}
}
update.setDatesChanged(checkForDateChange(acc.getStartDate(), acc.getEndDate(), pool));
// We use the "oldest" entitlement as the master for determining values that
// could have come from the various subscriptions.
Entitlement eldest = acc.getEldest();
Pool eldestEntPool = eldest.getPool();
boolean useDerived = eldestEntPool.getDerivedProduct() != null;
Product product = useDerived ? eldestEntPool.getDerivedProduct() : eldestEntPool.getProduct();
update.setProductAttributesChanged(
!pool.getProductAttributes().equals(product.getAttributes())
);
// Check if product ID, name, or provided products have changed.
update.setProductsChanged(checkForChangedProducts(
product, acc.getExpectedProvidedProds(), pool, changedProducts
));
if (!StringUtils.equals(eldestEntPool.getContractNumber(), pool.getContractNumber()) ||
!StringUtils.equals(eldestEntPool.getOrderNumber(), pool.getOrderNumber()) ||
!StringUtils.equals(eldestEntPool.getAccountNumber(), pool.getAccountNumber())) {
pool.setContractNumber(eldestEntPool.getContractNumber());
pool.setAccountNumber(eldestEntPool.getAccountNumber());
pool.setOrderNumber(eldestEntPool.getOrderNumber());
update.setOrderChanged(true);
}
// If there are any changes made, then mark all the entitlements as dirty
// so that they get regenerated on next checkin.
if (update.changed()) {
for (Entitlement ent : pool.getEntitlements()) {
ent.setDirty(true);
}
}
return update;
}
private boolean checkForOrderDataChanges(Pool pool, Pool existingPool) {
boolean orderDataChanged = PoolHelper.checkForOrderChanges(existingPool, pool);
if (orderDataChanged) {
existingPool.setAccountNumber(pool.getAccountNumber());
existingPool.setOrderNumber(pool.getOrderNumber());
existingPool.setContractNumber(pool.getContractNumber());
}
return orderDataChanged;
}
private boolean checkForBrandingChanges(Pool pool, Pool existingPool) {
boolean brandingChanged = false;
if (pool.getBranding().size() != existingPool.getBranding().size()) {
brandingChanged = true;
}
else {
for (Branding b : pool.getBranding()) {
if (!existingPool.getBranding().contains(b)) {
syncBranding(pool, existingPool);
brandingChanged = true;
break;
}
}
}
if (brandingChanged) {
syncBranding(pool, existingPool);
}
return brandingChanged;
}
/*
* Something has changed, sync the branding.
*/
private void syncBranding(Pool pool, Pool existingPool) {
existingPool.getBranding().clear();
for (Branding b : pool.getBranding()) {
existingPool.getBranding().add(new Branding(b.getProductId(), b.getType(),
b.getName()));
}
}
private Set<Product> getExpectedProvidedProducts(Pool pool, boolean useDerived) {
Set<Product> incomingProvided = new HashSet<Product>();
/**
* It is necessary to use getters for provided products here, because the pool
* is fabricated from subscrfiption (using CandlepinPoolManager.convertToMasterPool
* It is not an actual pool that would be stored in the DB.
*/
Set<Product> source = useDerived ? pool.getDerivedProvidedProducts() : pool.getProvidedProducts();
if (source != null && !source.isEmpty()) {
incomingProvided.addAll(source);
}
return incomingProvided;
}
private boolean checkForChangedProducts(Product incomingProduct, Set<Product> incomingProvided,
Pool existingPool, Map<String, Product> changedProducts) {
Product existingProduct = existingPool.getProduct();
Set<Product> currentProvided = productCurator.getPoolProvidedProductsCached(existingPool);
String pid = existingProduct.getId();
// TODO: ideally we would differentiate between these different product changes
// a little, but in the end it probably doesn't matter:
boolean productsChanged =
(pid != null && !pid.equals(incomingProduct.getId())) ||
!currentProvided.equals(incomingProvided);
// Check if the existing product is in the set of changed products
if (!productsChanged && changedProducts != null && pid != null) {
productsChanged = (changedProducts.get(pid) != null);
}
if (productsChanged) {
existingPool.setProduct(incomingProduct);
existingPool.setProvidedProducts(incomingProvided);
}
return productsChanged;
}
private boolean checkForChangedDerivedProducts(Pool pool, Pool existingPool,
Map<String, Product> changedProducts) {
boolean productsChanged = false;
if (pool.getDerivedProduct() != null) {
productsChanged = !pool.getDerivedProduct().getId()
.equals(existingPool.getDerivedProduct().getId());
productsChanged = productsChanged ||
(changedProducts != null && changedProducts.containsKey(pool.getDerivedProduct().getId()));
}
// Build expected set of ProvidedProducts and compare:
Set<Product> currentProvided = productCurator.getPoolDerivedProvidedProductsCached(existingPool);
Set<Product> incomingProvided = new HashSet<Product>();
/**
* Incoming pool is not in the database yet. It has the
* derived products on the instance itself.
*/
if (pool.getDerivedProvidedProducts() != null) {
for (Product p : pool.getDerivedProvidedProducts()) {
incomingProvided.add(p);
}
}
productsChanged = productsChanged || !currentProvided.equals(incomingProvided);
if (productsChanged) {
// 998317: NPE during refresh causes refresh to abort.
// Above we check getDerivedProduct for null, but here
// we ignore the fact that it may be null. So we will
// now check for null to avoid blowing up.
if (pool.getDerivedProduct() != null) {
existingPool.setDerivedProduct(pool.getDerivedProduct());
}
else {
// subscription no longer has a derived product
existingPool.setDerivedProduct(null);
}
existingPool.getDerivedProvidedProducts().clear();
if (incomingProvided != null && !incomingProvided.isEmpty()) {
existingPool.getDerivedProvidedProducts().addAll(incomingProvided);
}
}
return productsChanged;
}
private boolean checkForDateChange(Date start, Date end, Pool existingPool) {
boolean datesChanged = (!start.equals(existingPool.getStartDate())) ||
(!end.equals(existingPool.getEndDate()));
if (datesChanged) {
existingPool.setStartDate(start);
existingPool.setEndDate(end);
}
return datesChanged;
}
private boolean checkForQuantityChange(Pool pool, Pool existingPool, Long originalQuantity,
List<Pool> existingPools, Map<String, String> attributes) {
// Expected quantity is normally the main pool's quantity, but for
// virt only pools we expect it to be main pool quantity * virt_limit:
long expectedQuantity = calculateQuantity(originalQuantity, pool.getProduct(),
pool.getUpstreamPoolId());
expectedQuantity = processVirtLimitPools(existingPools,
attributes, existingPool, expectedQuantity);
boolean quantityChanged = !(expectedQuantity == existingPool.getQuantity());
if (quantityChanged) {
existingPool.setQuantity(expectedQuantity);
}
return quantityChanged;
}
private long processVirtLimitPools(List<Pool> existingPools,
Map<String, String> attributes, Pool existingPool, long expectedQuantity) {
/*
* WARNING: when updating pools, we have the added complication of having to
* watch out for pools that candlepin creates internally. (i.e. virt bonus
* pools in hosted (created when sub is first detected), and host restricted
* virt pools when on-site. (created when a host binds)
*/
/* Check the product attribute off the subscription too because
* derived products on the subscription are graduated to be the pool products and
* derived products aren't going to have a virt_limit attribute
*/
if (existingPool.hasAttribute(Pool.Attributes.DERIVED_POOL) &&
"true".equalsIgnoreCase(existingPool.getAttributeValue(Pool.Attributes.VIRT_ONLY)) &&
(attributes.containsKey(Product.Attributes.VIRT_LIMIT) ||
existingPool.getProduct().hasAttribute(Product.Attributes.VIRT_LIMIT))) {
if (!attributes.containsKey(Product.Attributes.VIRT_LIMIT)) {
log.warn("virt_limit attribute has been removed from subscription, " +
"flagging pool for deletion if supported: {}", existingPool.getId());
// virt_limit has been removed! We need to clean up this pool. Set
// attribute to notify the server of this:
existingPool.setMarkedForDelete(true);
// Older candlepin's won't look at the delete indicator, so we will
// set the expected quantity to 0 to effectively disable the pool
// on those servers as well.
expectedQuantity = 0;
}
else {
String virtLimitStr = attributes.get(Product.Attributes.VIRT_LIMIT);
if ("unlimited".equals(virtLimitStr)) {
// 0 will only happen if the rules set it to be 0 -- don't modify
// -1 for pretty much all the rest
expectedQuantity = existingPool.getQuantity() == 0 ?
0 : -1;
}
else {
try {
int virtLimit = Integer.parseInt(virtLimitStr);
if (config.getBoolean(ConfigProperties.STANDALONE) && !"true".equals(
existingPool.getAttributeValue(Pool.Attributes.UNMAPPED_GUESTS_ONLY))) {
// this is how we determined the quantity
expectedQuantity = virtLimit;
}
else {
// we need to see if a parent pool exists and has been
// exported. Adjust is number exported from a parent pool.
// If no parent pool, adjust = 0 [a scenario of virtual pool
// only]
//
// WARNING: we're assuming there is only one base
// (non-derived) pool. This may change in the future
// requiring a more complex
// adjustment for exported quantities if there are multiple
// pools in play.
long adjust = 0L;
for (Pool derivedPool : existingPools) {
String isDerived =
derivedPool.getAttributeValue(Pool.Attributes.DERIVED_POOL);
if (isDerived == null) {
adjust = derivedPool.getExported();
}
}
expectedQuantity = (expectedQuantity - adjust) * virtLimit;
}
}
catch (NumberFormatException nfe) {
// Nothing to update if we get here.
// continue;
}
}
}
}
return expectedQuantity;
}
}