/** * Copyright 2008 The University of North Carolina at Chapel Hill * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package edu.unc.lib.dl.cdr.services.fixity; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.codehaus.jackson.JsonGenerator; import org.codehaus.jackson.map.ObjectMapper; import org.irods.jargon.core.connection.IRODSAccount; import org.irods.jargon.core.connection.IRODSServerProperties; import org.irods.jargon.core.exception.DataNotFoundException; import org.irods.jargon.core.exception.FileIntegrityException; import org.irods.jargon.core.exception.JargonException; import org.irods.jargon.core.protovalues.ErrorEnum; import org.irods.jargon.core.pub.EnvironmentalInfoAO; import org.irods.jargon.core.pub.IRODSAccessObjectFactory; import org.irods.jargon.core.pub.IRODSFileSystem; import org.irods.jargon.core.pub.IRODSGenQueryExecutor; import org.irods.jargon.core.pub.RemoteExecutionOfCommandsAO; import org.irods.jargon.core.pub.RuleProcessingAO; import org.irods.jargon.core.query.GenQueryBuilderException; import org.irods.jargon.core.query.GenQueryOrderByField; import org.irods.jargon.core.query.IRODSGenQueryBuilder; import org.irods.jargon.core.query.IRODSGenQueryFromBuilder; import org.irods.jargon.core.query.IRODSQueryResultRow; import org.irods.jargon.core.query.IRODSQueryResultSet; import org.irods.jargon.core.query.JargonQueryException; import org.irods.jargon.core.query.QueryConditionOperators; import org.irods.jargon.core.query.RodsGenQueryEnum; import org.irods.jargon.core.rule.IRODSRuleExecResult; import org.irods.jargon.core.rule.IRODSRuleExecResultOutputParameter; import org.irods.jargon.core.rule.IRODSRuleParameter; import org.jdom2.Element; import edu.unc.lib.dl.fedora.FedoraException; import edu.unc.lib.dl.fedora.ManagementClient; import edu.unc.lib.dl.fedora.PID; import edu.unc.lib.dl.util.PremisEventLogger; public class FixityLogTask implements Runnable { private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'kk:mm:ss"); private static final Log LOG = LogFactory.getLog(FixityLogTask.class); private IRODSAccount irodsAccount = null; private IRODSFileSystem irodsFileSystem = null; private ManagementClient managementClient = null; private List<String> resourceNames = null; private int staleIntervalSeconds = 0; private int objectLimit = 0; private String fixityLogPath = null; public IRODSAccount getIrodsAccount() { return irodsAccount; } public void setIrodsAccount(IRODSAccount irodsAccount) { this.irodsAccount = irodsAccount; } public IRODSFileSystem getIrodsFileSystem() { return irodsFileSystem; } public void setIrodsFileSystem(IRODSFileSystem irodsFileSystem) { this.irodsFileSystem = irodsFileSystem; } public ManagementClient getManagementClient() { return managementClient; } public void setManagementClient(ManagementClient managementClient) { this.managementClient = managementClient; } public List<String> getResourceNames() { return resourceNames; } public void setResourceNames(List<String> resourceNames) { this.resourceNames = resourceNames; } public int getStaleIntervalSeconds() { return staleIntervalSeconds; } public void setStaleIntervalSeconds(int staleIntervalSeconds) { this.staleIntervalSeconds = staleIntervalSeconds; } public int getObjectLimit() { return objectLimit; } public void setObjectLimit(int objectLimit) { this.objectLimit = objectLimit; } public String getFixityLogPath() { return fixityLogPath; } public void setFixityLogPath(String fixityLogPath) { this.fixityLogPath = fixityLogPath; } public void run() { try(FileOutputStream fixityLogOutput = new FileOutputStream(fixityLogPath, true)) { // Check that all resources are available List<String> unavailableResourceNames = new ArrayList<String>(); for (String name : resourceNames) { if (!isResourceAvailable(name)) { unavailableResourceNames.add(name); } } if (unavailableResourceNames.size() > 0) { LOG.error("One or more resources are unavailable: " + unavailableResourceNames); return; } // Verify stale objects, write output, and touch timestamp List<String> objectPaths = getStaleObjectPaths(getCurrentIcatTime() - staleIntervalSeconds, objectLimit); for (String path : objectPaths) { if (!shouldVerifyPath(path)) continue; for (String name : resourceNames) { FixityVerificationResult fixityVerificationResult = verifyFixityForObjectOnResource(path, name); appendFixityLogEntry(fixityLogOutput, fixityVerificationResult); appendPremisEvent(fixityVerificationResult); } touchObjectFixityTimestamp(path); } } catch (IOException e) { LOG.error(e); throw new Error(e); } finally { irodsFileSystem.closeAndEatExceptions(); } } private void appendFixityLogEntry(OutputStream output, FixityVerificationResult fixityVerificationResult) throws IOException { Map<String, Object> entry = new LinkedHashMap<String, Object>(); // The members "time", "result", "objectPath", "resourceName", "irodsReleaseVersion", and "jargonVersion" are always present. entry.put("time", dateFormat.format(fixityVerificationResult.getTime())); entry.put("result", fixityVerificationResult.getResult()); entry.put("objectPath", fixityVerificationResult.getObjectPath()); entry.put("resourceName", fixityVerificationResult.getResourceName()); entry.put("irodsReleaseVersion", fixityVerificationResult.getIrodsReleaseVersion()); entry.put("jargonVersion", fixityVerificationResult.getJargonVersion()); // The members "expectedChecksum", "elapsed", "filePath", "irodsErrorCode", and "jargonException" are present only if non-null. if (fixityVerificationResult.getExpectedChecksum() != null) entry.put("expectedChecksum", fixityVerificationResult.getExpectedChecksum()); if (fixityVerificationResult.getElapsed() != null) entry.put("elapsed", fixityVerificationResult.getElapsed()); if (fixityVerificationResult.getFilePath() != null) entry.put("filePath", fixityVerificationResult.getFilePath()); if (fixityVerificationResult.getIrodsErrorCode() != null) entry.put("irodsErrorCode", fixityVerificationResult.getIrodsErrorCode()); // The "jargonException" member is serialized as an object with "className" and "message" members. if (fixityVerificationResult.getJargonException() != null) { JargonException exception = fixityVerificationResult.getJargonException(); Map<String, Object> map = new LinkedHashMap<String, Object>(); map.put("className", exception.getClass().getName()); map.put("message", exception.getMessage()); entry.put("jargonException", map); } // Write the log entry to the output, followed by "\r\n". ObjectMapper mapper = new ObjectMapper(); mapper.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); mapper.writeValue(output, entry); output.write(new byte[] { 0x0D, 0x0A }); // \r\n output.flush(); } private void appendPremisEvent(FixityVerificationResult fixityVerificationResult) { FixityVerificationResult.Result result = fixityVerificationResult.getResult(); if (result == FixityVerificationResult.Result.OK) { return; } PID pid = fixityVerificationResult.getPID(); if (pid != null) { PremisEventLogger premisEventLogger = new PremisEventLogger(null); Element event = premisEventLogger.logEvent(pid); // eventType if (fixityVerificationResult.getResult() == FixityVerificationResult.Result.MISSING) PremisEventLogger.addType(event, PremisEventLogger.Type.REPLICATION); else PremisEventLogger.addType(event, PremisEventLogger.Type.FIXITY_CHECK); // eventDateTime PremisEventLogger.addDateTime(event, fixityVerificationResult.getTime()); // eventDetailedOutcome String detailNote = null; { String resourceName = fixityVerificationResult.getResourceName(); String expectedChecksum = fixityVerificationResult.getExpectedChecksum(); if (result == FixityVerificationResult.Result.ERROR) detailNote = "An error occurred while attempting to verify the object's checksum on the resource " + resourceName + "."; else if (result == FixityVerificationResult.Result.FAILED) detailNote = "The checksum of the object on the resource " + resourceName + " didn't match the expected value of " + expectedChecksum + "."; else if (result == FixityVerificationResult.Result.MISSING) detailNote = "The object was not found on the resource " + resourceName + "."; } String errorNote = null; { ErrorEnum error = fixityVerificationResult.getError(); Integer code = fixityVerificationResult.getIrodsErrorCode(); JargonException exception = fixityVerificationResult.getJargonException(); if (error != null && code != null) errorNote = "iRODS error: " + error + " " + code; else if (code != null) errorNote = "iRODS error: " + code; else if (exception != null) errorNote = "Jargon exception: " + exception.getMessage() + " (" + exception.getClass().getName() + ")"; } // Combine detailNote and errorNote if we have both. // If we have only an errorNote, use that for the detailNote. if (detailNote != null && errorNote != null) detailNote = detailNote + " " + errorNote; else if (detailNote == null && errorNote != null) detailNote = errorNote; PremisEventLogger.addDetailedOutcome(event, fixityVerificationResult.getResult().toString(), detailNote, null); // linkingAgentIdentifier PremisEventLogger.addLinkingAgentIdentifier(event, "Class", this.getClass().getName(), "Software"); PremisEventLogger.addLinkingAgentIdentifier(event, "iRODS release version", fixityVerificationResult.getIrodsReleaseVersion(), "Software"); PremisEventLogger.addLinkingAgentIdentifier(event, "Jargon version", fixityVerificationResult.getJargonVersion(), "Software"); // linkingObjectIdentifier (iRODS object path) PremisEventLogger.addLinkingObjectIdentifier(event, "iRODS object path", fixityVerificationResult.getObjectPath(), null); // linkingObjectIdentifier (datastream) { String datastream = fixityVerificationResult.getDatastream(); if (datastream != null) PremisEventLogger.addLinkingObjectIdentifier(event, "PID", pid + "/" + datastream, null); } try { this.managementClient.writePremisEventsToFedoraObject(premisEventLogger, pid); } catch (FedoraException e) { LOG.error("Error writing PREMIS event for pid: " + pid, e); } } else { LOG.info("No PREMIS event added for fixity verification result without pid: " + fixityVerificationResult); } } private FixityVerificationResult verifyFixityForObjectOnResource(String objectPath, String resourceName) { // Assemble the basics of the verification result. FixityVerificationResult verificationResult = new FixityVerificationResult(); verificationResult.setTime(new Date()); verificationResult.setObjectPath(objectPath); verificationResult.setResourceName(resourceName); // Get server properties and copy to the verification result. IRODSServerProperties serverProperties = this.getServerProperties(); verificationResult.setJargonVersion(IRODSServerProperties.getJargonVersion()); verificationResult.setIrodsReleaseVersion(serverProperties.getRelVersion()); // Query for info about this object. Map<String, String> info = getInfoForObjectOnResource(objectPath, resourceName); // If info is null, the iCAT contains no record of this object on this resource, so the result is MISSING. // Otherwise, we will try to verify the checksum. if (info == null) { verificationResult.setResult(FixityVerificationResult.Result.MISSING); } else { // Assign the checksum and filesystem path. verificationResult.setExpectedChecksum(info.get("DATA_CHECKSUM")); verificationResult.setFilePath(info.get("DATA_PATH")); // Verify the replica's checksum, recording elapsed time. double start, elapsed; Object result = null; Integer code = null; start = (double) System.currentTimeMillis(); result = verifyChecksumForObjectAndReplica(objectPath, info.get("DATA_REPL_NUM")); elapsed = ((double) System.currentTimeMillis() - start) / 1000.0; // If the result is an integer, record it if it is non-zero (an error code). // If the result is an exception, record it. if (result instanceof Integer) { code = (Integer) result; if (code.intValue() != 0) verificationResult.setIrodsErrorCode(code); } else if (result instanceof JargonException) { verificationResult.setJargonException((JargonException) result); } else { throw new Error("Unexpected result from verifyChecksumForObjectAndReplica: " + result); } // Record elapsed time for checksum verification verificationResult.setElapsed(new Double(elapsed)); // Interpret code as a result value. if (code != null) { if (code.intValue() == 0) { verificationResult.setResult(FixityVerificationResult.Result.OK); } else if (code.intValue() == ErrorEnum.USER_CHKSUM_MISMATCH.getInt()) { verificationResult.setResult(FixityVerificationResult.Result.FAILED); } else { verificationResult.setResult(FixityVerificationResult.Result.ERROR); } } else { verificationResult.setResult(FixityVerificationResult.Result.ERROR); } } return verificationResult; } // The following methods deal with Jargon directly. private IRODSServerProperties getServerProperties() { try { IRODSAccessObjectFactory accessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory(); EnvironmentalInfoAO environmentalInfo = accessObjectFactory.getEnvironmentalInfoAO(irodsAccount); return environmentalInfo.getIRODSServerPropertiesFromIRODSServer(); } catch (JargonException e) { LOG.error(e); throw new Error(e); } } private Map<String, String> getInfoForObjectOnResource(String objectPath, String resourceName) { int lastPathSeparatorIndex = objectPath.lastIndexOf("/"); String collName = objectPath.substring(0, lastPathSeparatorIndex); String dataName = objectPath.substring(lastPathSeparatorIndex + 1); try { IRODSAccessObjectFactory accessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory(); IRODSGenQueryExecutor genQueryExecutor = accessObjectFactory.getIRODSGenQueryExecutor(irodsAccount); IRODSGenQueryBuilder builder = new IRODSGenQueryBuilder(true, null); builder.addSelectAsGenQueryValue(RodsGenQueryEnum.COL_DATA_REPL_NUM); builder.addSelectAsGenQueryValue(RodsGenQueryEnum.COL_D_DATA_CHECKSUM); builder.addSelectAsGenQueryValue(RodsGenQueryEnum.COL_D_DATA_PATH); builder.addConditionAsGenQueryField(RodsGenQueryEnum.COL_DATA_NAME, QueryConditionOperators.EQUAL, dataName); builder.addConditionAsGenQueryField(RodsGenQueryEnum.COL_COLL_NAME, QueryConditionOperators.EQUAL, collName); builder.addConditionAsGenQueryField(RodsGenQueryEnum.COL_R_RESC_NAME, QueryConditionOperators.EQUAL, resourceName); IRODSGenQueryFromBuilder query = builder.exportIRODSQueryFromBuilder(1); IRODSQueryResultSet queryResultSet = genQueryExecutor.executeIRODSQueryAndCloseResult(query, 0); IRODSQueryResultRow row = queryResultSet.getFirstResult(); Map<String, String> info = new HashMap<String, String>(); info.put("DATA_REPL_NUM", row.getColumn("DATA_REPL_NUM")); info.put("DATA_CHECKSUM", row.getColumn("DATA_CHECKSUM")); info.put("DATA_PATH", row.getColumn("DATA_PATH")); return info; } catch (DataNotFoundException e) { return null; } catch (JargonException e) { LOG.error(e); throw new Error(e); } catch (GenQueryBuilderException e) { LOG.error(e); throw new Error(e); } catch (JargonQueryException e) { LOG.error(e); throw new Error(e); } } private Object verifyChecksumForObjectAndReplica(String objectPath, String replicaNumber) { try { IRODSAccessObjectFactory accessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory(); RuleProcessingAO ruleProcessingAO = accessObjectFactory.getRuleProcessingAO(irodsAccount); StringBuilder sb = new StringBuilder("main { msiDataObjChksum(*path, \"verifyChksum=++++replNum=*replNum\", \"null\") }\n"); sb.append("INPUT *path='-',*replNum='-'\n"); sb.append("OUTPUT ruleExecOut"); List<IRODSRuleParameter> inputOverrides = new ArrayList<IRODSRuleParameter>(); inputOverrides.add(new IRODSRuleParameter("*path", "\"" + objectPath.replaceAll("\"", "\\\\\"") + "\"")); inputOverrides.add(new IRODSRuleParameter("*replNum", "\"" + replicaNumber.replaceAll("\"", "\\\\\"") + "\"")); try { ruleProcessingAO.executeRule(sb.toString(), inputOverrides, RuleProcessingAO.RuleProcessingType.EXTERNAL); return new Integer(0); } catch (JargonException e) { int code = e.getUnderlyingIRODSExceptionCode(); // Since Jargon will sometimes translate error codes into exceptions without retaining the iRODS error code, most notably for USER_CHKSUM_MISMATCH, we will add a special case to restore the iRODS error code. See IRODSErrorScanner. if (code == 0 && e instanceof FileIntegrityException) { code = ErrorEnum.USER_CHKSUM_MISMATCH.getInt(); } // If we have a reliable error code, return that. Otherwise, return the exception. if (code == 0) { return e; } else { return new Integer(code); } } } catch (JargonException e) { LOG.error(e); throw new Error(e); } } private boolean isResourceAvailable(String resourceName) { try { IRODSAccessObjectFactory accessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory(); IRODSGenQueryExecutor genQueryExecutor = accessObjectFactory.getIRODSGenQueryExecutor(irodsAccount); RemoteExecutionOfCommandsAO remoteExecutionOfCommands = accessObjectFactory.getRemoteExecutionOfCommandsAO(irodsAccount); IRODSGenQueryBuilder builder = new IRODSGenQueryBuilder(true, null); builder.addSelectAsGenQueryValue(RodsGenQueryEnum.COL_R_LOC); builder.addConditionAsGenQueryField(RodsGenQueryEnum.COL_R_RESC_NAME, QueryConditionOperators.EQUAL, resourceName); IRODSGenQueryFromBuilder query = builder.exportIRODSQueryFromBuilder(1); IRODSQueryResultSet queryResultSet = genQueryExecutor.executeIRODSQueryAndCloseResult(query, 0); if (queryResultSet.getResults().size() < 1) { return false; } IRODSQueryResultRow row = queryResultSet.getFirstResult(); String host = row.getColumn(RodsGenQueryEnum.COL_R_LOC.getName()); boolean available = false; try { remoteExecutionOfCommands.executeARemoteCommandAndGetStreamGivingCommandNameAndArgsAndHost("hello", "", host); available = true; } catch (JargonException e) { } return available; } catch (GenQueryBuilderException e) { LOG.error(e); throw new Error(e); } catch (JargonQueryException e) { LOG.error(e); throw new Error(e); } catch (JargonException e) { LOG.error(e); throw new Error(e); } } private int getCurrentIcatTime() { try { IRODSAccessObjectFactory accessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory(); RuleProcessingAO ruleProcessingAO = accessObjectFactory.getRuleProcessingAO(irodsAccount); StringBuilder sb = new StringBuilder("main { msiGetIcatTime(*result, \"unix\") }\n"); sb.append("INPUT null\n"); sb.append("OUTPUT *result"); IRODSRuleExecResult result = ruleProcessingAO.executeRule(sb.toString()); IRODSRuleExecResultOutputParameter resultOutputParam = result.getOutputParameterResults().get("*result"); if (resultOutputParam == null) { throw new Error("Output parameter result was null"); } return Integer.parseInt((String) resultOutputParam.getResultObject()); } catch (JargonException e) { LOG.error(e); throw new Error(e); } } private List<String> getStaleObjectPaths(int staleTime, int objectLimit) { try { IRODSAccessObjectFactory accessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory(); IRODSGenQueryExecutor genQueryExecutor = accessObjectFactory.getIRODSGenQueryExecutor(irodsAccount); IRODSGenQueryBuilder builder = new IRODSGenQueryBuilder(true, null); builder.addSelectAsGenQueryValue(RodsGenQueryEnum.COL_COLL_NAME); builder.addSelectAsGenQueryValue(RodsGenQueryEnum.COL_DATA_NAME); builder.addSelectAsGenQueryValue(RodsGenQueryEnum.COL_META_DATA_ATTR_VALUE); builder.addOrderByGenQueryField(RodsGenQueryEnum.COL_META_DATA_ATTR_VALUE, GenQueryOrderByField.OrderByType.ASC); builder.addConditionAsGenQueryField(RodsGenQueryEnum.COL_META_DATA_ATTR_NAME, QueryConditionOperators.EQUAL, "cdrFixityTimestamp"); builder.addConditionAsGenQueryField(RodsGenQueryEnum.COL_COLL_NAME, QueryConditionOperators.NOT_LIKE, "/" + irodsAccount.getZone() + "/trash/%"); builder.addConditionAsGenQueryField(RodsGenQueryEnum.COL_META_DATA_ATTR_VALUE, QueryConditionOperators.NUMERIC_LESS_THAN_OR_EQUAL_TO, staleTime); IRODSGenQueryFromBuilder query = builder.exportIRODSQueryFromBuilder(objectLimit); IRODSQueryResultSet queryResultSet = genQueryExecutor.executeIRODSQueryAndCloseResult(query, 0); List<IRODSQueryResultRow> results = queryResultSet.getResults(); List<String> paths = new ArrayList<String>(); for (IRODSQueryResultRow row : results) { String collName = row.getColumn(RodsGenQueryEnum.COL_COLL_NAME.getName()); String dataName = row.getColumn(RodsGenQueryEnum.COL_DATA_NAME.getName()); paths.add(collName + "/" + dataName); } return paths; } catch (GenQueryBuilderException e) { LOG.error(e); throw new Error(e); } catch (JargonException e) { LOG.error(e); throw new Error(e); } catch (JargonQueryException e) { LOG.error(e); throw new Error(e); } } private void touchObjectFixityTimestamp(String objectPath) { try { IRODSAccessObjectFactory accessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory(); RuleProcessingAO ruleProcessingAO = accessObjectFactory.getRuleProcessingAO(irodsAccount); StringBuilder sb = new StringBuilder("main { msiGetIcatTime(*time, \"unix\"); msiAddKeyVal(*keyval, \"cdrFixityTimestamp\", str(int(*time))); msiSetKeyValuePairsToObj(*keyval, *path, \"-d\") }\n"); sb.append("INPUT *path='-'\n"); sb.append("OUTPUT ruleExecOut"); List<IRODSRuleParameter> inputOverrides = new ArrayList<IRODSRuleParameter>(); inputOverrides.add(new IRODSRuleParameter("*path", "\"" + objectPath.replaceAll("\"", "\\\\\"") + "\"")); ruleProcessingAO.executeRule(sb.toString(), inputOverrides, RuleProcessingAO.RuleProcessingType.EXTERNAL); } catch (JargonException e) { LOG.error(e); throw new Error(e); } } private boolean shouldVerifyPath(String objectPath) { try { IRODSAccessObjectFactory accessObjectFactory = irodsFileSystem.getIRODSAccessObjectFactory(); RuleProcessingAO ruleProcessingAO = accessObjectFactory.getRuleProcessingAO(irodsAccount); StringBuilder sb = new StringBuilder("main { *result = str(cdrShouldVerifyOrReplicate(*path)) }\n"); sb.append("INPUT *path='-'\n"); sb.append("OUTPUT *result"); List<IRODSRuleParameter> inputOverrides = new ArrayList<IRODSRuleParameter>(); inputOverrides.add(new IRODSRuleParameter("*path", "\"" + objectPath.replaceAll("\"", "\\\\\"") + "\"")); IRODSRuleExecResult result = ruleProcessingAO.executeRule(sb.toString(), inputOverrides, RuleProcessingAO.RuleProcessingType.EXTERNAL); IRODSRuleExecResultOutputParameter resultOutputParam = result.getOutputParameterResults().get("*result"); if (resultOutputParam == null) { throw new Error("Output parameter result was null"); } return resultOutputParam.getResultObject().equals("true"); } catch (JargonException e) { LOG.error(e); throw new Error(e); } } }