/** * 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 java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Map; import java.util.Set; import javax.servlet.http.HttpServletResponse; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.StringUtils; import org.candlepin.audit.EventFactory; import org.candlepin.audit.EventSink; import org.candlepin.common.exceptions.BadRequestException; import org.candlepin.common.exceptions.ForbiddenException; import org.candlepin.common.exceptions.IseException; import org.candlepin.common.exceptions.NotFoundException; import org.candlepin.guice.PrincipalProvider; import org.candlepin.model.CdnCurator; import org.candlepin.model.Consumer; import org.candlepin.model.ConsumerCurator; import org.candlepin.model.EntitlementCurator; import org.candlepin.model.ImportRecord; import org.candlepin.pinsetter.tasks.ExportJob; import org.candlepin.pinsetter.tasks.ImportJob; import org.candlepin.model.Owner; import org.candlepin.sync.ConflictOverrides; import org.candlepin.sync.ExportCreationException; import org.candlepin.sync.ExportResult; import org.candlepin.sync.Exporter; import org.candlepin.sync.Importer; import org.candlepin.sync.ImporterException; import org.candlepin.sync.file.ManifestFile; import org.candlepin.sync.file.ManifestFileService; import org.candlepin.sync.file.ManifestFileType; import org.candlepin.sync.file.ManifestFileServiceException; import org.candlepin.util.Util; import org.quartz.JobDetail; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xnap.commons.i18n.I18n; import com.google.inject.Inject; import com.google.inject.persist.Transactional; /** * This class serves as a controller layer for manifest export and import and encapsulates * the {@link Importer} and {@link Exporter} functionality so that it isn't spread out * across multiple classes. * */ public class ManifestManager { private static Logger log = LoggerFactory.getLogger(ManifestManager.class); private ManifestFileService manifestFileService; private Exporter exporter; private Importer importer; private EntitlementCurator entitlementCurator; private PoolManager poolManager; private ConsumerCurator consumerCurator; private CdnCurator cdnCurator; private PrincipalProvider principalProvider; private I18n i18n; private EventSink sink; private EventFactory eventFactory; @Inject public ManifestManager(ManifestFileService manifestFileService, Exporter exporter, Importer importer, ConsumerCurator consumerCurator, EntitlementCurator entitlementCurator, CdnCurator cdnCurator, PoolManager poolManager, PrincipalProvider principalProvider, I18n i18n, EventSink eventSink, EventFactory eventFactory) { this.manifestFileService = manifestFileService; this.exporter = exporter; this.importer = importer; this.consumerCurator = consumerCurator; this.cdnCurator = cdnCurator; this.entitlementCurator = entitlementCurator; this.poolManager = poolManager; this.principalProvider = principalProvider; this.i18n = i18n; this.sink = eventSink; this.eventFactory = eventFactory; } /** * Asynchronously generates a manifest for the target consumer. * * @param consumerUuid the target consumer's UUID. * @param cdnLabel the CDN label to store in the meta file. * @param webUrl the URL pointing to the manifest's originating web application. * @param apiUrl the API URL pointing to the manifest's originating candlepin API. * @param extensionData data to be passed to the {@link ExportExtensionAdapter} when creating * a new export of the target consumer. * @return the details of the async export job. */ public JobDetail generateManifestAsync(String consumerUuid, String cdnLabel, String webUrl, String apiUrl, Map<String, String> extensionData) { log.info("Scheduling Async Export for consumer {}", consumerUuid); Consumer consumer = validateConsumerForExport(consumerUuid, cdnLabel); return ExportJob.scheduleExport(consumer, cdnLabel, webUrl, apiUrl, extensionData); } /** * Generates a manifest for the specified consumer. * * @param consumerUuid the target consumer's UUID. * @param cdnLabel the CDN label to store in the meta file. * @param webUrl the URL pointing to the manifest's originating web application. * @param apiUrl the API URL pointing to the manifest's originating candlepin API. * @param extensionData data to be passed to the {@link ExportExtensionAdapter} when creating * a new export of the target consumer. * @return an archive of the target consumer * @throws ExportCreationException when an export fails. */ public File generateManifest(String consumerUuid, String cdnLabel, String webUrl, String apiUrl, Map<String, String> extensionData) throws ExportCreationException { log.info("Exporting consumer {}", consumerUuid); Consumer consumer = validateConsumerForExport(consumerUuid, cdnLabel); poolManager.regenerateDirtyEntitlements(entitlementCurator.listByConsumer(consumer)); File export = exporter.getFullExport(consumer, cdnLabel, webUrl, apiUrl, extensionData); sink.queueEvent(eventFactory.exportCreated(consumer)); return export; } /** * Stores the specified archive via the {@link ManifestFileService} and triggers an * asynchronous manifest import. * * @param owner the target owner. * @param archive the manifest file archive. * @param uploadedFileName the name of the file as uploaded (archive will contain the cached name). * @param overrides any {@link ConflictOverrides}s to apply during the import process. * @return the {@link JobDetail} that represents the asynchronous import job to start. * @throws ManifestFileServiceException if the archive could not be stored. */ public JobDetail importManifestAsync(Owner owner, File archive, String uploadedFileName, ConflictOverrides overrides) throws ManifestFileServiceException { ManifestFile manifestRecordId = storeImport(archive, owner); return ImportJob.scheduleImport(owner, manifestRecordId.getId(), uploadedFileName, overrides); } /** * Imports the specified manifest archive into the specifed {@link Owner}. * * @param owner the target owner. * @param archive the archive to import * @param uploadedFileName the name of the originally uploaded file. * @param overrides the {@link ConflictOverrides} to apply during the import process. * @return the result of the import. * @throws ImporterException if there is an issue importing the manifest. */ public ImportRecord importManifest(Owner owner, File archive, String uploadedFileName, ConflictOverrides overrides) throws ImporterException { return importer.loadExport(owner, archive, overrides, uploadedFileName); } /** * Records a failed import in the database. * * @param owner the target owner. * @param error the error that caused the failure. * @param filename the uploaded filename. */ public void recordImportFailure(Owner owner, Throwable error, String filename) { importer.recordImportFailure(owner, error, filename); } /** * Imports a stored manifest file into the target {@link Owner}. The stored file is deleted * as soon as the import is complete. * * @param targetOwner the target owner. * @param fileId the manifest file ID. * @param overrides the {@link ConflictOverrides} to apply to the import process. * @param uploadedFileName the originally uploaded file name. * @return the result of the import. * @throws BadRequestException if the file is not found in the {@link ManifestFileService} * @throws ImporterException if there is an issue importing the file. */ @Transactional public ImportRecord importStoredManifest(Owner targetOwner, String fileId, ConflictOverrides overrides, String uploadedFileName) throws BadRequestException, ImporterException { ManifestFile manifest = manifestFileService.get(fileId); if (manifest == null) { throw new BadRequestException(i18n.tr("The requested manifest file was not found: {0}", fileId)); } ImportRecord importResult = importer.loadStoredExport(manifest, targetOwner, overrides, uploadedFileName); deleteStoredManifest(manifest.getId()); return importResult; } /** * Performs a cleanup of the manifest records. It will remove records that * are of the specified age (in minutes) and will clean up all related files. * Because the uploaded/downloaded manifest files are provided by a service * any rogue records are removed as well. * * @param maxAgeInMinutes the maximum age of the file in minutes. A negative value * indicates no expiry. * @return the number of expired exports that were deleted. * @throws ManifestFileServiceException if an error occurs when cleaning up records. */ @Transactional public int cleanup(int maxAgeInMinutes) throws ManifestFileServiceException { if (maxAgeInMinutes < 0) { return 0; } return manifestFileService.deleteExpired(Util.addMinutesToDt(maxAgeInMinutes * -1)); } /** * Write the stored manifest file to the specified response output stream and update * the appropriate response data. * * @param exportId the id of the manifest file to find. * @param exportedConsumerUuid the UUID of the consumer the export was generated for. * @param response the response to write the file to. * @throws ManifestFileServiceException if there was an issue getting the file from the service * @throws NotFoundException if the manifest file is not found * @throws BadRequestException if the manifests target consumer does not match the specified * consumer. * @throws IseException if there was an issue writing the file to the response. */ @Transactional public void writeStoredExportToResponse(String exportId, String exportedConsumerUuid, HttpServletResponse response) throws ManifestFileServiceException, NotFoundException, BadRequestException, IseException { Consumer exportedConsumer = consumerCurator.verifyAndLookupConsumer(exportedConsumerUuid); // In order to stream the results from the DB to the client // we write the file contents directly to the response output stream. // // NOTE: Passing the database input stream to the response builder seems // like it would be a correct approach here, but large object streaming // can only be done inside a single transaction, so we have to stream it // manually. ManifestFile manifest = manifestFileService.get(exportId); if (manifest == null) { throw new NotFoundException( i18n.tr("Unable to find specified manifest by id: {0}", exportId)); } // The specified consumer must match that of the manifest. if (!exportedConsumer.getUuid().equals(manifest.getTargetId())) { throw new BadRequestException( i18n.tr("Could not validate export against specifed consumer: {0}", exportedConsumer.getUuid())); } BufferedOutputStream output = null; InputStream input = null; try { response.setContentType("application/zip"); response.setHeader("Content-Disposition", "attachment; filename=" + manifest.getName()); // NOTE: Input and output streams are expected to be closed by their creators. input = manifest.getInputStream(); output = new BufferedOutputStream(response.getOutputStream()); int data = input.read(); while (data != -1) { output.write(data); data = input.read(); } output.flush(); } catch (Exception e) { // Reset the response data so that a json response can be returned, // by RestEasy. response.setContentType("text/json"); response.setHeader("Content-Disposition", ""); throw new IseException(i18n.tr("Unable to download manifest: {0}", exportId), e); } } private Consumer validateConsumerForExport(String consumerUuid, String cdnLabel) { // FIXME Should this be testing the CdnLabel as well? Consumer consumer = consumerCurator.verifyAndLookupConsumer(consumerUuid); if (consumer.getType() == null || !consumer.isManifestDistributor()) { throw new ForbiddenException( i18n.tr("Unit {0} cannot be exported. A manifest cannot be made for units of type ''{1}''.", consumerUuid, consumer.getType().getLabel())); } if (!StringUtils.isBlank(cdnLabel) && cdnCurator.lookupByLabel(cdnLabel) == null) { throw new ForbiddenException( i18n.tr("A CDN with label {0} does not exist on this system.", cdnLabel)); } return consumer; } /** * Generates a manifest for the specifed consumer and stores the resulting file via the * {@link ManifestFileService}. * * @param consumerUuid the target consumer's UUID. * @param cdnLabel the CDN label to store in the meta file. * @param webUrl the URL pointing to the manifest's originating web application. * @param apiUrl the API URL pointing to the manifest's originating candlepin API. * @param extensionData data to be passed to the {@link ExportExtensionAdapter} when creating * a new export of the target consumer. * @return an {@link ExportResult} containing the details of the stored file. * @throws ExportCreationException if there are any issues generating the manifest. */ public ExportResult generateAndStoreManifest(String consumerUuid, String cdnLabel, String webUrl, String apiUrl, Map<String, String> extensionData) throws ExportCreationException { Consumer consumer = validateConsumerForExport(consumerUuid, cdnLabel); File export = null; try { poolManager.regenerateDirtyEntitlements(entitlementCurator.listByConsumer(consumer)); export = exporter.getFullExport(consumer, cdnLabel, webUrl, apiUrl, extensionData); ManifestFile manifestFile = storeExport(export, consumer); sink.queueEvent(eventFactory.exportCreated(consumer)); return new ExportResult(consumer.getUuid(), manifestFile.getId()); } catch (ManifestFileServiceException e) { throw new ExportCreationException("Unable to create export archive", e); } finally { // We no longer need the export work directory since the archive has been saved in the DB. if (export != null) { File workDir = export.getParentFile(); try { FileUtils.deleteDirectory(workDir); } catch (IOException ioe) { // It'll get cleaned up by the ManifestCleanerJob if it couldn't // be deleted for some reason. } } } } /** * Deletes the manifest file stored by the {@link ManifestFileService}. If there was * an issue deleting the manifest, the exception is just logged. The file will eventually * be deleted by the {@link ManifestCleanerJob}. * * @param manifestFileId the ID of the manifest to be deleted. */ @Transactional public void deleteStoredManifest(String manifestFileId) { try { log.info("Deleting stored manifest file: {}", manifestFileId); manifestFileService.delete(manifestFileId); } catch (Exception e) { // Just log any exception here. This will eventually get cleaned up by // a cleaner job. log.warn("Could not delete import file by id: {}", manifestFileId, e); } } /** * Generates an archive of the specified consumer's entitlements. * * @param consumer the target consumer * @param serials the entitlement serials to export. * @return an archive to the specified consumer's entitlements. * @throws ExportCreationException if the archive could not be created. */ public File generateEntitlementArchive(Consumer consumer, Set<Long> serials) throws ExportCreationException { log.debug("Getting client certificate zip file for consumer: {}", consumer.getUuid()); poolManager.regenerateDirtyEntitlements( entitlementCurator.listByConsumer(consumer)); return exporter.getEntitlementExport(consumer, serials); } /** * Stores the specified manifest import file via the {@link ManifestFileService}. * * @param the manifest import {@link File} to store * @return the id of the stored manifest file. */ @Transactional protected ManifestFile storeImport(File importFile, Owner targetOwner) throws ManifestFileServiceException { // Store the manifest record, and then store the file. return storeFile(importFile, ManifestFileType.IMPORT, targetOwner.getKey()); } /** * Stores the specified manifest export file. * * @param exportFile the manifest export {@link File} to store. * @return the id of the stored manifest file. * @throws ManifestFileServiceException */ @Transactional protected ManifestFile storeExport(File exportFile, Consumer distributor) throws ManifestFileServiceException { // Only allow a single export for a consumer at a time. Delete all others before // storing the new one. int count = manifestFileService.delete(ManifestFileType.EXPORT, distributor.getUuid()); log.debug("Deleted {} existing export files for distributor {}.", count, distributor.getUuid()); return storeFile(exportFile, ManifestFileType.EXPORT, distributor.getUuid()); } private ManifestFile storeFile(File targetFile, ManifestFileType type, String targetId) throws ManifestFileServiceException { return manifestFileService.store(type, targetFile, principalProvider.get().getName(), targetId); } }