/** * 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.controller; import org.candlepin.audit.Event; import org.candlepin.audit.EventFactory; import org.candlepin.audit.EventSink; import org.candlepin.common.config.Configuration; import org.candlepin.common.exceptions.BadRequestException; import org.candlepin.common.exceptions.ForbiddenException; import org.candlepin.config.ConfigProperties; import org.candlepin.model.CandlepinQuery; import org.candlepin.model.Consumer; import org.candlepin.model.ConsumerCurator; import org.candlepin.model.ConsumerInstalledProduct; import org.candlepin.model.Content; import org.candlepin.model.Entitlement; import org.candlepin.model.EntitlementCurator; import org.candlepin.model.Owner; import org.candlepin.model.OwnerProductCurator; import org.candlepin.model.Pool; import org.candlepin.model.PoolCurator; import org.candlepin.model.PoolQuantity; import org.candlepin.model.Product; import org.candlepin.model.ProductCurator; import org.candlepin.model.dto.ContentData; import org.candlepin.model.dto.ProductContentData; import org.candlepin.model.dto.ProductData; import org.candlepin.policy.EntitlementRefusedException; import org.candlepin.policy.ValidationError; import org.candlepin.policy.ValidationResult; import org.candlepin.policy.js.entitlement.EntitlementRulesTranslator; import org.candlepin.resource.dto.AutobindData; import org.candlepin.service.ProductServiceAdapter; import com.google.inject.Inject; import com.google.inject.persist.Transactional; import org.apache.commons.lang.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xnap.commons.i18n.I18n; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; /** * entitler */ public class Entitler { private static Logger log = LoggerFactory.getLogger(Entitler.class); private Configuration config; private ConsumerCurator consumerCurator; private ContentManager contentManager; private EventFactory evtFactory; private EventSink sink; private EntitlementRulesTranslator messageTranslator; private EntitlementCurator entitlementCurator; private I18n i18n; private OwnerProductCurator ownerProductCurator; private PoolCurator poolCurator; private PoolManager poolManager; private ProductCurator productCurator; private ProductManager productManager; private ProductServiceAdapter productAdapter; private int maxDevLifeDays = 90; final String DEFAULT_DEV_SLA = "Self-Service"; @Inject public Entitler(PoolManager pm, ConsumerCurator cc, I18n i18n, EventFactory evtFactory, EventSink sink, EntitlementRulesTranslator messageTranslator, EntitlementCurator entitlementCurator, Configuration config, OwnerProductCurator ownerProductCurator, PoolCurator poolCurator, ProductCurator productCurator, ProductManager productManager, ProductServiceAdapter productAdapter, ContentManager contentManager) { this.poolManager = pm; this.i18n = i18n; this.evtFactory = evtFactory; this.sink = sink; this.consumerCurator = cc; this.messageTranslator = messageTranslator; this.entitlementCurator = entitlementCurator; this.config = config; this.ownerProductCurator = ownerProductCurator; this.poolCurator = poolCurator; this.productCurator = productCurator; this.productManager = productManager; this.productAdapter = productAdapter; this.contentManager = contentManager; } public List<Entitlement> bindByPoolQuantities(String consumeruuid, Map<String, Integer> poolIdAndQuantities) throws EntitlementRefusedException { Consumer c = consumerCurator.findByUuid(consumeruuid); return bindByPoolQuantities(c, poolIdAndQuantities); } public List<Entitlement> bindByPoolQuantity(Consumer consumer, String poolId, Integer quantity) { Map<String, Integer> poolMap = new HashMap<String, Integer>(); poolMap.put(poolId, quantity); try { return bindByPoolQuantities(consumer, poolMap); } catch (EntitlementRefusedException e) { // TODO: Could be multiple errors, but we'll just report the first // one for now Pool pool = poolCurator.find(poolId); throw new ForbiddenException(messageTranslator.poolErrorToMessage( pool, e.getResults().get(poolId).getErrors().get(0) )); } } public List<Entitlement> bindByPoolQuantities(Consumer consumer, Map<String, Integer> poolIdAndQuantities) throws EntitlementRefusedException { // Attempt to create entitlements: try { List<Entitlement> entitlementList = poolManager.entitleByPools(consumer, poolIdAndQuantities); log.debug("Created {} entitlements.", entitlementList.size()); return entitlementList; } catch (IllegalArgumentException e) { throw new BadRequestException(e.getMessage(), e); } } public void adjustEntitlementQuantity(Consumer consumer, Entitlement ent, Integer quantity) { // Attempt to adjust an entitlement: try { poolManager.adjustEntitlementQuantity(consumer, ent, quantity); } catch (EntitlementRefusedException e) { // TODO: Could be multiple errors, but we'll just report the first one for now: throw new ForbiddenException(messageTranslator.entitlementErrorToMessage( ent, e.getResults().values().iterator().next().getErrors().get(0)) ); } } public List<Entitlement> bindByProducts(String[] productIds, String consumeruuid, Date entitleDate, Collection<String> fromPools) throws AutobindDisabledForOwnerException { Consumer c = consumerCurator.findByUuid(consumeruuid); AutobindData data = AutobindData.create(c).on(entitleDate) .forProducts(productIds).withPools(fromPools); return bindByProducts(data); } /** * Entitles the given Consumer to the given Product. Will seek out pools * which provide access to this product, either directly or as a child, and * select the best one based on a call to the rules engine. * * @param data AutobindData encapsulating data required for an autobind request * @return List of Entitlements * @throws AutobindDisabledForOwnerException when an autobind attempt is made and the owner * has it disabled. */ public List<Entitlement> bindByProducts(AutobindData data) throws AutobindDisabledForOwnerException { return bindByProducts(data, false); } /** * * Force option is used to heal entire org * * @param data AutobindData encapsulating data required for an autobind request * @param force heal host even if it has autoheal disabled * @return List of Entitlements * @throws AutobindDisabledForOwnerException when an autobind attempt is made and the owner * has it disabled. */ @Transactional public List<Entitlement> bindByProducts(AutobindData data, boolean force) throws AutobindDisabledForOwnerException { Consumer consumer = data.getConsumer(); Owner owner = consumer.getOwner(); if (!consumer.isDev() && owner.autobindDisabled()) { log.info("Skipping auto-attach for consumer '{}'. Auto-attach is disabled for owner {}.", consumer, owner.getKey()); throw new AutobindDisabledForOwnerException(i18n.tr("Auto-attach is disabled for owner '{0}'.", owner.getKey())); } // If the consumer is a guest, and has a host, try to heal the host first // Dev consumers should not need to worry about the host or unmapped guest // entitlements based on the planned design of the subscriptions if (consumer.hasFact("virt.uuid") && !consumer.isDev()) { String guestUuid = consumer.getFact("virt.uuid"); // Remove any expired unmapped guest entitlements revokeUnmappedGuestEntitlements(consumer); Consumer host = consumerCurator.getHost(guestUuid, consumer.getOwner()); if (host != null && (force || host.isAutoheal())) { log.info("Attempting to heal host machine with UUID \"{}\" for guest with UUID \"{}\"", host.getUuid(), consumer.getUuid()); if (!StringUtils.equals(host.getServiceLevel(), consumer.getServiceLevel())) { log.warn("Host with UUID \"{}\" has a service level \"{}\" that does not match" + " that of the guest with UUID \"{}\" and service level \"{}\"", host.getUuid(), host.getServiceLevel(), consumer.getUuid(), consumer.getServiceLevel()); } try { List<Entitlement> hostEntitlements = poolManager.entitleByProductsForHost( consumer, host, data.getOnDate(), data.getPossiblePools()); log.debug("Granted host {} entitlements", hostEntitlements.size()); sendEvents(hostEntitlements); } catch (Exception e) { //log and continue, this should NEVER block log.debug("Healing failed for host UUID {} with message: {}", host.getUuid(), e.getMessage()); } /* Consumer is stale at this point. Note that we use find() instead of * findByUuid() or getConsumer() since the latter two methods are secured * to a specific host principal and bindByProducts can get called when * a guest is switching hosts */ consumer = consumerCurator.find(consumer.getId()); data.setConsumer(consumer); } } if (consumer.isDev()) { if (config.getBoolean(ConfigProperties.STANDALONE) || !poolCurator.hasActiveEntitlementPools(consumer.getOwner(), null)) { throw new ForbiddenException(i18n.tr( "Development units may only be used on hosted servers" + " and with orgs that have active subscriptions." )); } // Look up the dev pool for this consumer, and if not found // create one. If a dev pool already exists, remove it and // create a new one. String sku = consumer.getFact("dev_sku"); Pool devPool = poolCurator.findDevPool(consumer); if (devPool != null) { poolManager.deletePool(devPool); } devPool = poolManager.createPool(assembleDevPool(consumer, sku)); data.setPossiblePools(Arrays.asList(devPool.getId())); data.setProductIds(new String[]{sku}); } // Attempt to create entitlements: try { // the pools are only used to bind the guest List<Entitlement> entitlements = poolManager.entitleByProducts(data); log.debug("Created entitlements: {}", entitlements); return entitlements; } catch (EntitlementRefusedException e) { // TODO: Could be multiple errors, but we'll just report the first one for now String productId = "Unknown Product"; if (data.getProductIds().length > 0) { productId = data.getProductIds()[0]; } throw new ForbiddenException(messageTranslator.productErrorToMessage( productId, e.getResults().values().iterator().next().getErrors().get(0) )); } } private Date getEndDate(Product prod, Date startTime) { int interval = maxDevLifeDays; String prodExp = prod.getAttributeValue(Product.Attributes.TTL); if (prodExp != null && Integer.parseInt(prodExp) < maxDevLifeDays) { interval = Integer.parseInt(prodExp); } Calendar cal = Calendar.getInstance(); cal.setTime(startTime); cal.add(Calendar.DAY_OF_YEAR, interval); return cal.getTime(); } /** * Create a development pool for the specified consumer that starts when * the consumer was registered and expires after the duration specified * by the SKU. The pool will be bound to the consumer via the * requires_consumer attribute, meaning only the consumer can bind to * entitlements from it. * * @param consumer the consumer the associate the pool with. * @param sku the product id of the developer SKU. * @return the newly created developer pool (note: not yet persisted) */ protected Pool assembleDevPool(Consumer consumer, String sku) { DeveloperProducts devProducts = getDeveloperPoolProducts(consumer, sku); Product skuProduct = devProducts.getSku(); Date startDate = consumer.getCreated(); Date endDate = getEndDate(skuProduct, startDate); Pool pool = new Pool(consumer.getOwner(), skuProduct, devProducts.getProvided(), 1L, startDate, endDate, "", "", ""); log.info("Created development pool with SKU {}", skuProduct.getId()); pool.setAttribute(Pool.Attributes.DEVELOPMENT_POOL, "true"); pool.setAttribute(Pool.Attributes.REQUIRES_CONSUMER, consumer.getUuid()); return pool; } private DeveloperProducts getDeveloperPoolProducts(Consumer consumer, String sku) { DeveloperProducts devProducts = getDevProductMap(consumer, sku); verifyDevProducts(consumer, sku, devProducts); return devProducts; } /** * Looks up all Products matching the specified SKU and the consumer's * installed products. * * @param consumer the consumer to pull the installed product id list from. * @param sku the product id of the SKU. * @return a {@link DeveloperProducts} object that contains the Product objects * from the adapter. */ private DeveloperProducts getDevProductMap(Consumer consumer, String sku) { List<String> devProductIds = new ArrayList<String>(); devProductIds.add(sku); for (ConsumerInstalledProduct ip : consumer.getInstalledProducts()) { devProductIds.add(ip.getProductId()); } Owner owner = consumer.getOwner(); Map<String, ProductData> productMap = new HashMap<String, ProductData>(); Map<String, ContentData> contentMap = new HashMap<String, ContentData>(); log.debug("Importing products for dev pool resolution..."); for (ProductData product : this.productAdapter.getProductsByIds(owner, devProductIds)) { if (product == null) { continue; } if (sku.equals(product.getId()) && StringUtils.isEmpty(product.getAttributeValue(Product.Attributes.SUPPORT_LEVEL))) { // if there is no SLA, apply the default product.setAttribute(Product.Attributes.SUPPORT_LEVEL, this.DEFAULT_DEV_SLA); } // Product is coming from an upstream source; lock it so only upstream can make // further changes to it. product.setLocked(true); ProductData existingProduct = productMap.get(product.getId()); if (existingProduct != null && !existingProduct.equals(product)) { log.warn("Multiple versions of the same product received during dev pool resolution; " + "discarding duplicate: {} => {}, {}", product.getId(), existingProduct, product ); } else { productMap.put(product.getId(), product); Collection<ProductContentData> pcdCollection = product.getProductContent(); if (pcdCollection != null) { for (ProductContentData pcd : pcdCollection) { // Impl note: // We aren't checking for duplicate mappings to the same content, since our // current implementation of ProductData prevents such a thing. However, if it // is reasonably possible that we could end up with ProductData instances which // do not prevent duplicate content mappings, we should add checks here to // check for, and throw out, such mappings if (pcd == null) { log.error("product contains a null product-content mapping: {}", product); throw new IllegalStateException( "product contains a null product-content mapping: " + product); } ContentData content = pcd.getContent(); // Do some simple mapping validation. Our import method will handle minimal // population validation for us. if (content == null || content.getId() == null) { log.error("product contains a null or incomplete product-content mapping: {}", product); throw new IllegalStateException("product contains a null or incomplete " + "product-content mapping: " + product); } // We need to lock the incoming content here, but doing so will affect // the equality comparison for products. We'll correct them later. ContentData existingContent = contentMap.get(content.getId()); if (existingContent != null && !existingContent.equals(content)) { log.warn("Multiple versions of the same content received during dev pool " + "resolution; discarding duplicate: {} => {}, {}", content.getId(), existingContent, content ); } else { contentMap.put(content.getId(), content); } } } } } log.debug("Importing {} content...", contentMap.size()); for (ContentData cdata : contentMap.values()) { cdata.setLocked(true); } Map<String, Content> importedContent = this.contentManager .importContent(owner, contentMap, productMap.keySet()) .getImportedEntities(); log.debug("Importing {} product(s)...", productMap.size()); Map<String, Product> importedProducts = this.productManager .importProducts(owner, productMap, importedContent) .getImportedEntities(); log.debug("Resolved {} dev product(s) for sku: {}", productMap.size(), sku); return new DeveloperProducts(sku, importedProducts); } /** * Verifies that the expected developer SKU product was found and logs any * consumer installed products that were not found by the adapter. * * @param consumer the consumer who's installed products are to be checked. * @param expectedSku the product id of the developer sku that must be found * in order to build the development pool. * @param devProducts all products retrieved from the adapter that are validated. * @throws ForbiddenException thrown if the sku was not found by the adapter. */ protected void verifyDevProducts(Consumer consumer, String expectedSku, DeveloperProducts devProducts) throws ForbiddenException { if (!devProducts.foundSku()) { throw new ForbiddenException(i18n.tr("SKU product not available to this " + "development unit: ''{0}''", expectedSku)); } for (ConsumerInstalledProduct ip : consumer.getInstalledProducts()) { if (!devProducts.containsProduct(ip.getProductId())) { log.warn(i18n.tr("Installed product not available to this " + "development unit: ''{0}''", ip.getProductId())); } } } /** * Entitles the given Consumer to the given Product. Will seek out pools * which provide access to this product, either directly or as a child, and * select the best one based on a call to the rules engine. * * @param consumer The consumer being entitled. * @return List of Entitlements */ public List<PoolQuantity> getDryRun(Consumer consumer, String serviceLevelOverride) { List<PoolQuantity> result = new ArrayList<PoolQuantity>(); try { Owner owner = consumer.getOwner(); if (consumer.isDev()) { if (config.getBoolean(ConfigProperties.STANDALONE) || !poolCurator.hasActiveEntitlementPools(consumer.getOwner(), null)) { throw new ForbiddenException(i18n.tr( "Development units may only be used on hosted servers" + " and with orgs that have active subscriptions." )); } // Look up the dev pool for this consumer, and if not found // create one. If a dev pool already exists, remove it and // create a new one. String sku = consumer.getFact("dev_sku"); Pool devPool = poolCurator.findDevPool(consumer); if (devPool != null) { poolManager.deletePool(devPool); } devPool = poolManager.createPool(assembleDevPool(consumer, sku)); result.add(new PoolQuantity(devPool, 1)); } else { result = poolManager.getBestPools( consumer, null, null, owner, serviceLevelOverride, null); } log.debug("Created Pool Quantity list: {}", result); } catch (EntitlementRefusedException e) { // If we catch an exception we will just return an empty list // The dry run just reports that an autobind will have no pools // We will debug log the message, but returning does not seem to add // to the process if (log.isDebugEnabled()) { log.debug("consumer {} dry-run errors:", consumer.getUuid()); for (Entry<String, ValidationResult> entry : e.getResults().entrySet()) { log.debug("errors for pool id: {}", entry.getKey()); for (ValidationError error : entry.getValue().getErrors()) { log.debug(error.getResourceKey()); } } } } return result; } public int revokeUnmappedGuestEntitlements(Consumer consumer) { int total = 0; CandlepinQuery<Entitlement> unmappedGuestEntitlements; if (consumer == null) { unmappedGuestEntitlements = entitlementCurator.findByPoolAttribute( "unmapped_guests_only", "true"); } else { unmappedGuestEntitlements = entitlementCurator.findByPoolAttribute( consumer, "unmapped_guests_only", "true"); } // TODO: // Make sure this doesn't choke on MySQL, since we're doing queries with the cursor open. for (Entitlement e : unmappedGuestEntitlements) { if (!e.isValid()) { poolManager.revokeEntitlement(e); total++; } } return total; } public int revokeUnmappedGuestEntitlements() { return revokeUnmappedGuestEntitlements(null); } public void sendEvents(List<Entitlement> entitlements) { if (entitlements != null) { for (Entitlement e : entitlements) { Event event = evtFactory.entitlementCreated(e); sink.queueEvent(event); } } } /** * A private sub class that encapsulates the products obtained from the * product adapter that are used to create the development pool. Its * general purpose is to distinguish between the sku and the provided * products without having to iterate a map to identify the sku. */ private class DeveloperProducts { private Product sku; private Map<String, Product> provided; public DeveloperProducts(String expectedSku, Map<String, Product> products) { this.sku = products.remove(expectedSku); this.provided = products; } public Product getSku() { return sku; } public Collection<Product> getProvided() { return provided.values(); } public boolean foundSku() { return sku != null; } public boolean containsProduct(String productId) { return (foundSku() && productId.equals(sku.getId())) || provided.containsKey(productId); } } }