/* * RHQ Management Platform * Copyright (C) 2005-2014 Red Hat, Inc. * All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation version 2 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package org.rhq.enterprise.server.drift; import static javax.ejb.TransactionAttributeType.NOT_SUPPORTED; import static javax.ejb.TransactionAttributeType.REQUIRES_NEW; import static org.rhq.core.domain.drift.DriftChangeSetCategory.COVERAGE; import static org.rhq.core.domain.drift.DriftFileStatus.LOADED; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.Charset; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import javax.ejb.EJB; import javax.ejb.Stateless; import javax.ejb.TransactionAttribute; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import org.apache.commons.io.IOUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.Session; import org.rhq.common.drift.ChangeSetReader; import org.rhq.common.drift.ChangeSetReaderImpl; import org.rhq.common.drift.FileEntry; import org.rhq.common.drift.Headers; import org.rhq.core.clientapi.agent.drift.DriftAgentService; import org.rhq.core.domain.auth.Subject; import org.rhq.core.domain.criteria.DriftChangeSetCriteria; import org.rhq.core.domain.criteria.DriftCriteria; import org.rhq.core.domain.criteria.JPADriftChangeSetCriteria; import org.rhq.core.domain.criteria.JPADriftCriteria; import org.rhq.core.domain.drift.Drift; import org.rhq.core.domain.drift.DriftChangeSet; import org.rhq.core.domain.drift.DriftChangeSetCategory; import org.rhq.core.domain.drift.DriftComposite; import org.rhq.core.domain.drift.DriftConfigurationDefinition; import org.rhq.core.domain.drift.DriftDefinition; import org.rhq.core.domain.drift.DriftFile; import org.rhq.core.domain.drift.DriftFileStatus; import org.rhq.core.domain.drift.JPADrift; import org.rhq.core.domain.drift.JPADriftChangeSet; import org.rhq.core.domain.drift.JPADriftFile; import org.rhq.core.domain.drift.JPADriftFileBits; import org.rhq.core.domain.drift.JPADriftSet; import org.rhq.core.domain.resource.Resource; import org.rhq.core.domain.util.PageList; import org.rhq.core.util.StopWatch; import org.rhq.core.util.ZipUtil; import org.rhq.core.util.file.FileUtil; import org.rhq.core.util.stream.StreamUtil; import org.rhq.enterprise.server.RHQConstants; import org.rhq.enterprise.server.agentclient.AgentClient; import org.rhq.enterprise.server.auth.SubjectManagerLocal; import org.rhq.enterprise.server.core.AgentManagerLocal; import org.rhq.enterprise.server.plugin.pc.drift.DriftChangeSetSummary; import org.rhq.enterprise.server.util.CriteriaQueryGenerator; import org.rhq.enterprise.server.util.CriteriaQueryRunner; /** * The SLSB method implementation needed to support the JPA (RHQ Default) Drift Server Plugin. * * @author Jay Shaughnessy * @author John Sanda */ @Stateless public class JPADriftServerBean implements JPADriftServerLocal { private static final Log LOG = LogFactory.getLog(JPADriftServerBean.class); @EJB AgentManagerLocal agentManager; @EJB JPADriftServerLocal JPADriftServer; @EJB SubjectManagerLocal subjectManager; @PersistenceContext(unitName = RHQConstants.PERSISTENCE_UNIT_NAME) private EntityManager entityManager; @Override @TransactionAttribute(REQUIRES_NEW) public void purgeByDriftDefinitionName(Subject subject, int resourceId, String driftDefName) throws Exception { int driftsDeleted; int changeSetsDeleted; StopWatch timer = new StopWatch(); // purge all drift entities first Query q = entityManager.createNamedQuery(JPADrift.QUERY_DELETE_BY_DRIFTDEF_RESOURCE); q.setParameter("resourceId", resourceId); q.setParameter("driftDefinitionName", driftDefName); driftsDeleted = q.executeUpdate(); // delete the drift set // JPADriftChangeSet changeSet = entityManager.createQuery( // "select c from JPADriftChangeSet c where c.version = 0 and c.driftDefinition") // now purge all changesets q = entityManager.createNamedQuery(JPADriftChangeSet.QUERY_DELETE_BY_DRIFTDEF_RESOURCE); q.setParameter("resourceId", resourceId); q.setParameter("driftDefinitionName", driftDefName); changeSetsDeleted = q.executeUpdate(); LOG.info("Purged [" + driftsDeleted + "] drift items and [" + changeSetsDeleted + "] changesets associated with drift def [" + driftDefName + "] from resource [" + resourceId + "]. Elapsed time=[" + timer.getElapsed() + "]ms"); } @Override public PageList<JPADriftChangeSet> findDriftChangeSetsByCriteria(Subject subject, DriftChangeSetCriteria criteria) { JPADriftChangeSetCriteria jpaCriteria = (criteria instanceof JPADriftChangeSetCriteria) ? (JPADriftChangeSetCriteria) criteria : new JPADriftChangeSetCriteria(criteria); // If looking for the initial change set make sure version is to to 0 if (criteria.getFilterCategory() != null && criteria.getFilterCategory() == COVERAGE) { // If fetching Drifts then make sure we go through the DriftSet. Note that there is no guarantee // that the fetched Drift will refer to the ChangeSets found, because it may refer to a pinned // template's changeset and be shared amongst changesets. if (jpaCriteria.isFetchDrifts()) { jpaCriteria.fetchInitialDriftSet(true); jpaCriteria.fetchDrifts(false); } jpaCriteria.addFilterVersion("0"); } else { jpaCriteria.fetchInitialDriftSet(false); } CriteriaQueryGenerator generator = new CriteriaQueryGenerator(subject, jpaCriteria); CriteriaQueryRunner<JPADriftChangeSet> queryRunner = new CriteriaQueryRunner<JPADriftChangeSet>(jpaCriteria, generator, entityManager); PageList<JPADriftChangeSet> result = queryRunner.execute(); return result; } @Override public PageList<DriftComposite> findDriftCompositesByCriteria(Subject subject, DriftCriteria criteria) { JPADriftCriteria jpaCriteria = (criteria instanceof JPADriftCriteria) ? (JPADriftCriteria) criteria : new JPADriftCriteria(criteria); jpaCriteria.fetchChangeSet(true); PageList<JPADrift> drifts = findDriftsByCriteria(subject, jpaCriteria); PageList<DriftComposite> result = new PageList<DriftComposite>(); for (JPADrift drift : drifts) { JPADriftChangeSet changeSet = drift.getChangeSet(); DriftDefinition driftDef = changeSet.getDriftDefinition(); result.add(new DriftComposite(drift, changeSet.getResource(), driftDef.getName())); } return result; } @Override public PageList<JPADrift> findDriftsByCriteria(Subject subject, DriftCriteria criteria) { JPADriftCriteria jpaCriteria = (criteria instanceof JPADriftCriteria) ? (JPADriftCriteria) criteria : new JPADriftCriteria(criteria); CriteriaQueryGenerator generator = new CriteriaQueryGenerator(subject, jpaCriteria); CriteriaQueryRunner<JPADrift> queryRunner = new CriteriaQueryRunner<JPADrift>(jpaCriteria, generator, entityManager); PageList<JPADrift> result = queryRunner.execute(); return result; } @Override public String persistChangeSet(Subject subject, DriftChangeSet<?> changeSet) { JPADriftChangeSet jpaChangeSet; if (isTemplateChangeSet(changeSet)) { jpaChangeSet = new JPADriftChangeSet(null, changeSet.getVersion(), changeSet.getCategory(), null); jpaChangeSet.setDriftHandlingMode(changeSet.getDriftHandlingMode()); } else { Resource resource = getResource(changeSet.getResourceId()); DriftDefinition driftDef = null; for (DriftDefinition def : resource.getDriftDefinitions()) { if (def.getId() == changeSet.getDriftDefinitionId()) { driftDef = def; break; } } jpaChangeSet = new JPADriftChangeSet(resource, changeSet.getVersion(), changeSet.getCategory(), driftDef); } JPADriftSet driftSet = new JPADriftSet(); for (Drift<?, ?> drift : changeSet.getDrifts()) { JPADrift jpaDrift = new JPADrift(jpaChangeSet, drift.getPath(), drift.getCategory(), toJPADriftFile(drift .getOldDriftFile()), toJPADriftFile(drift.getNewDriftFile())); driftSet.addDrift(jpaDrift); } entityManager.persist(jpaChangeSet); entityManager.persist(driftSet); jpaChangeSet.setInitialDriftSet(driftSet); return jpaChangeSet.getId(); } private boolean isTemplateChangeSet(DriftChangeSet<?> changeSet) { return changeSet.getResourceId() == 0 && changeSet.getDriftDefinitionId() == 0; } @Override public String copyChangeSet(Subject subject, String changeSetId, int driftDefId, int resourceId) { Resource resource = entityManager.find(Resource.class, resourceId); DriftDefinition driftDef = entityManager.find(DriftDefinition.class, driftDefId); JPADriftChangeSet srcChangeSet = entityManager.find(JPADriftChangeSet.class, Integer.parseInt(changeSetId)); JPADriftChangeSet destChangeSet = new JPADriftChangeSet(resource, 0, COVERAGE, driftDef); destChangeSet.setDriftHandlingMode(DriftConfigurationDefinition.DriftHandlingMode.normal); destChangeSet.setInitialDriftSet(srcChangeSet.getInitialDriftSet()); entityManager.persist(destChangeSet); return destChangeSet.getId(); } private JPADriftFile toJPADriftFile(DriftFile driftFile) { if (driftFile == null) { return null; } JPADriftFile jpaFile = new JPADriftFile(driftFile.getHashId()); jpaFile.setDataSize(driftFile.getDataSize()); jpaFile.setStatus(driftFile.getStatus()); return jpaFile; } @Override public JPADriftFile getDriftFile(Subject subject, String sha256) { JPADriftFile result = entityManager.find(JPADriftFile.class, sha256); return result; } @Override public JPADriftFile persistDriftFile(JPADriftFile driftFile) { entityManager.persist(driftFile); return driftFile; } @Override @TransactionAttribute(REQUIRES_NEW) public void persistDriftFileData(JPADriftFile driftFile, InputStream data, long numBytes) throws Exception { JPADriftFileBits df = entityManager.find(JPADriftFileBits.class, driftFile.getHashId()); if (null == df) { throw new IllegalArgumentException("JPADriftFile not found [" + driftFile.getHashId() + "]"); } Session session = (Session)entityManager.getDelegate(); df.setDataSize(numBytes); df.setData(session.getLobHelper().createBlob(new BufferedInputStream(data), numBytes)); df.setStatus(LOADED); } // This facade does not start, or participate in, a transaction so that it can execute its work // in two new transactions. The first transaction ensures all new entities are committed to the // database. The second transaction can then safely acknowledge that the changeset is persisted // and request drift file content, if necessary. @Override @TransactionAttribute(NOT_SUPPORTED) public DriftChangeSetSummary storeChangeSet(Subject subject, final int resourceId, final File changeSetZip) throws Exception { // a List to be populated by storeChangeSetInNewTransaction for use in ackChangeSetInNewTransaction List<JPADriftFile> driftFilesToRequest = new ArrayList<JPADriftFile>(); // a 1 element array so storeChangeSetInNewTransaction can return the Headers for use in ackChangeSetInNewTransaction Headers[] headers = new Headers[1]; DriftChangeSetSummary result = JPADriftServer.storeChangeSetInNewTransaction(subject, resourceId, changeSetZip, driftFilesToRequest, headers); if (null == result) { return null; } JPADriftServer.ackChangeSetInNewTransaction(subject, resourceId, headers[0], driftFilesToRequest); return result; } @Override @TransactionAttribute(REQUIRES_NEW) public DriftChangeSetSummary storeChangeSetInNewTransaction(Subject subject, final int resourceId, final File changeSetZip, final List<JPADriftFile> driftFilesToRequest, final Headers[] headers) throws Exception { final Resource resource = getResource(resourceId); final DriftChangeSetSummary summary = new DriftChangeSetSummary(); final boolean storeBinaryContent = isBinaryContentStorageEnabled(); try { ZipUtil.walkZipFile(changeSetZip, new ChangeSetFileVisitor() { @Override public boolean visit(ZipEntry zipEntry, ZipInputStream stream) throws Exception { JPADriftChangeSet driftChangeSet; ChangeSetReader reader = new ChangeSetReaderImpl(new BufferedReader(new InputStreamReader(stream)), false); // store the new change set info (not the actual blob) DriftDefinition driftDef = findDriftDefinition(resource, reader.getHeaders()); if (driftDef == null) { LOG.error("Unable to locate DriftDefinition for Resource [" + resource + "]. Change set cannot be saved."); return false; } // TODO: Commenting out the following line for now. We want to set the // version to the value specified in the headers, but we may want to also // validate it against the latest version we have in the database so that // we can make sure that the agent is in sync with the server. // //int version = getChangeSetVersion(resource, config); int version = reader.getHeaders().getVersion(); DriftChangeSetCategory category = reader.getHeaders().getType(); driftChangeSet = new JPADriftChangeSet(resource, version, category, driftDef); entityManager.persist(driftChangeSet); summary.setCategory(category); summary.setResourceId(resourceId); summary.setDriftDefinitionName(reader.getHeaders().getDriftDefinitionName()); summary.setDriftHandlingMode(driftDef.getDriftHandlingMode()); summary.setCreatedTime(driftChangeSet.getCtime()); if (version > 0) { for (FileEntry entry : reader) { boolean addToList = storeBinaryContent || !DriftUtil.isBinaryFile(entry.getFile()); JPADriftFile oldDriftFile = getDriftFile(entry.getOldSHA(), driftFilesToRequest, addToList); JPADriftFile newDriftFile = getDriftFile(entry.getNewSHA(), driftFilesToRequest, addToList); // TODO Figure out an efficient way to save coverage change sets. // The initial/coverage change set could contain hundreds or even thousands // of entries. We probably want to consider doing some kind of batch insert // // jsanda // use a path with only forward slashing to ensure consistent paths across reports String path = FileUtil.useForwardSlash(entry.getFile()); JPADrift drift = new JPADrift(driftChangeSet, path, entry.getType(), oldDriftFile, newDriftFile); entityManager.persist(drift); // we are taking advantage of the fact that we know the summary is only used by the server // if the change set is a DRIFT report. If its a coverage report, it is not used (we do // not alert on coverage reports) - so don't waste memory by collecting all the paths // when we know they aren't going to be used anyway. if (category == DriftChangeSetCategory.DRIFT) { summary.addDriftPathname(path); } } } else { summary.setInitialChangeSet(true); JPADriftSet driftSet = new JPADriftSet(); for (FileEntry entry : reader) { boolean addToList = storeBinaryContent || !DriftUtil.isBinaryFile(entry.getFile()); JPADriftFile newDriftFile = getDriftFile(entry.getNewSHA(), driftFilesToRequest, addToList); String path = FileUtil.useForwardSlash(entry.getFile()); // A Drift always has a changeSet. Note that in this code section the changeset is // always going to be set to a DriftDefinition's changeSet. But that is not always the // case, it could also be set to a DriftDefinitionTemplate's changeSet. driftSet.addDrift(new JPADrift(driftChangeSet, path, entry.getType(), null, newDriftFile)); } entityManager.persist(driftSet); driftChangeSet.setInitialDriftSet(driftSet); entityManager.merge(driftChangeSet); } headers[0] = reader.getHeaders(); return true; } }); return summary; } catch (Exception e) { String msg = "Failed to store drift changeset for "; if (null != resource) { msg += resource; } else { msg += ("resourceId [" + resourceId + "]"); } LOG.error(msg, e); return null; } finally { // delete the changeSetFile? } } @Override @TransactionAttribute(REQUIRES_NEW) public void ackChangeSetInNewTransaction(Subject subject, final int resourceId, final Headers headers, final List<JPADriftFile> driftFilesToRequest) throws Exception { try { AgentClient agentClient = agentManager.getAgentClient(subjectManager.getOverlord(), resourceId); DriftAgentService service = agentClient.getDriftAgentService(); service.ackChangeSet(resourceId, headers.getDriftDefinitionName()); // send a message to the agent requesting the necessary JPADriftFile content. Note that the // driftFile status has been set to REQUESTED outside of this call. if (!driftFilesToRequest.isEmpty()) { try { service.requestDriftFiles(resourceId, headers, driftFilesToRequest); } catch (Exception e) { LOG.warn("Unable to inform agent of drift file request [" + driftFilesToRequest + "]", e); } } } catch (Exception e) { LOG.warn("Unable to acknowledge changeSet storage with agent for " + headers, e); } } private boolean isBinaryContentStorageEnabled() { String binaryContent = System.getProperty("rhq.server.drift.store-binary-content", "false"); return binaryContent.equals("true"); } private JPADriftFile getDriftFile(String sha256, List<JPADriftFile> emptyDriftFiles, boolean addToList) { if (null == sha256 || "0".equals(sha256)) { return null; } JPADriftFile result = entityManager.find(JPADriftFile.class, sha256); // if the JPADriftFile is not yet in the db then persist it, and mark it requested if content is to be fetched // note - by immediately setting the initial status to REQUESTED we avoid a future update and a // potential deadlock scenario where the REQUESTED and LOADED status updates can happen simultaneously if (null == result) { JPADriftFile driftFile = new JPADriftFile(sha256); if (addToList) { driftFile.setStatus(DriftFileStatus.REQUESTED); } result = persistDriftFile(driftFile); if (addToList) { emptyDriftFiles.add(result); } } return result; } private DriftDefinition findDriftDefinition(Resource resource, Headers headers) { for (DriftDefinition driftDef : resource.getDriftDefinitions()) { if (driftDef.getName().equals(headers.getDriftDefinitionName())) { return driftDef; } } return null; } private abstract class ChangeSetFileVisitor implements ZipUtil.ZipEntryVisitor { } @Override public void storeFiles(Subject subject, File filesZip) throws Exception { // No longer using ZipUtil.walkZipFile because an IOException was getting thrown // after reading the first entry, resulting in subsequent entries being skipped. // DriftFileVisitor passed the ZipInputStream to Hibernate.createBlob, and either // Hibernate, the JDBC driver, or something else is closing the stream which in // turn causes the exception. // // jsanda String zipFileName = filesZip.getName(); File tmpDir = new File(System.getProperty("java.io.tmpdir")); File dir = FileUtil.createTempDirectory(zipFileName.substring(0, zipFileName.indexOf(".")),null,tmpDir); dir.mkdir(); ZipUtil.unzipFile(filesZip, dir); for (File file : dir.listFiles()) { JPADriftFile driftFile = new JPADriftFile(file.getName()); try { JPADriftServer.persistDriftFileData(driftFile, new FileInputStream(file), file.length()); } catch (Exception e) { LogFactory.getLog(getClass()).info("Skipping bad drift file", e); } } for (File file : dir.listFiles()) { file.delete(); } boolean deleted = dir.delete(); if (!deleted) { LogFactory.getLog(getClass()).info( "Unable to delete " + dir.getAbsolutePath() + ". This directory and " + "its contents are no longer needed. It can be deleted."); } } @Override public String getDriftFileBits(String hash) { // TODO add security try { JPADriftFileBits content = (JPADriftFileBits) entityManager.createNamedQuery( JPADriftFileBits.QUERY_FIND_BY_ID).setParameter("hashId", hash).getSingleResult(); if (content.getDataSize() == null || content.getDataSize() < 1) { return null; } return IOUtils.toString(content.getBlob().getBinaryStream(), Charset.defaultCharset().name()); } catch (Exception e) { e.printStackTrace(); return null; } } @Override public byte[] getDriftFileAsByteArray(String hash) { try { JPADriftFileBits content = (JPADriftFileBits) entityManager.createNamedQuery( JPADriftFileBits.QUERY_FIND_BY_ID).setParameter("hashId", hash).getSingleResult(); if (content.getDataSize() == null || content.getDataSize() < 1) { return new byte[] {}; } return StreamUtil.slurp(content.getBlob().getBinaryStream()); } catch (SQLException e) { e.printStackTrace(); return null; } } private Resource getResource(int resourceId) { Resource resource = entityManager.find(Resource.class, resourceId); if (null == resource) { throw new IllegalArgumentException("Resource not found [" + resourceId + "]"); } return resource; } }