/* dCache - http://www.dcache.org/ * * Copyright (C) 2007-2013 Deutsches Elektronen-Synchrotron * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.dcache.pool.classic; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Maps; import java.io.IOException; import java.io.PrintWriter; import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import diskCacheV111.util.CacheException; import diskCacheV111.util.ChecksumFactory; import diskCacheV111.util.FileCorruptedCacheException; import dmg.cells.nucleus.CellCommandListener; import dmg.cells.nucleus.CellInfoProvider; import dmg.cells.nucleus.CellSetupProvider; import dmg.util.command.Argument; import dmg.util.command.Command; import dmg.util.command.Option; import org.dcache.pool.repository.ReplicaDescriptor; import org.dcache.pool.repository.RepositoryChannel; import org.dcache.util.Checksum; import org.dcache.util.ChecksumType; import org.dcache.util.Checksums; import static com.google.common.collect.Iterables.concat; import static com.google.common.collect.Iterables.isEmpty; import static org.dcache.pool.classic.ChecksumModule.PolicyFlag.*; import static org.dcache.util.ByteUnit.BYTES; import static org.dcache.util.ByteUnit.MiB; import static org.dcache.util.ChecksumType.ADLER32; import static org.dcache.util.ChecksumType.getChecksumType; public class ChecksumModuleV1 implements CellCommandListener, ChecksumModule, CellSetupProvider, CellInfoProvider { private final EnumSet<PolicyFlag> _policy = EnumSet.of(ON_TRANSFER, ENFORCE_CRC); private double _throughputLimit = Double.POSITIVE_INFINITY; private long _scrubPeriod = TimeUnit.HOURS.toMillis(24L); private ChecksumType _defaultChecksumType = ADLER32; private final CopyOnWriteArrayList<Runnable> listeners = new CopyOnWriteArrayList<>(); public void addListener(Runnable listener) { listeners.add(listener); } public void removeListener(Runnable listener) { listeners.remove(listener); } public synchronized ChecksumType getDefaultChecksumType() { return _defaultChecksumType; } public synchronized long getScrubPeriod() { return _scrubPeriod; } public synchronized double getThroughputLimit() { return _throughputLimit; } @Override public synchronized void printSetup(PrintWriter pw) { pw.println("csm set checksumtype " + _defaultChecksumType); if (hasPolicy(SCRUB)) { pw.print("csm set policy -scrub=on"); pw.print(" -limit=" + (Double.isInfinite(_throughputLimit) ? "off" : BYTES.toMiB(_throughputLimit))); pw.println(" -period=" + TimeUnit.MILLISECONDS.toHours(_scrubPeriod)); } else { pw.println("csm set policy -scrub=off"); } pw.print("csm set policy"); pw.print(" -onread="); pw.print(getPolicy(ON_READ)); pw.print(" -onwrite="); pw.print(getPolicy(ON_WRITE)); pw.print(" -onflush="); pw.print(getPolicy(ON_FLUSH)); pw.print(" -onrestore="); pw.print(getPolicy(ON_RESTORE)); pw.print(" -enforcecrc="); pw.print(getPolicy(ENFORCE_CRC)); pw.print(" -getcrcfromhsm="); pw.print(getPolicy(GET_CRC_FROM_HSM)); pw.println(""); } @Override public synchronized void getInfo(PrintWriter pw) { pw.println(" Checksum type : " + _defaultChecksumType); pw.print(" Checkum calculation on : transfer "); for (PolicyFlag flag: _policy) { switch (flag) { case ON_READ: pw.print("read "); break; case ON_WRITE: pw.print("write "); break; case ON_FLUSH: pw.print("flush "); break; case ON_RESTORE: pw.print("restore "); break; case ENFORCE_CRC: pw.print("enforceCRC "); break; case GET_CRC_FROM_HSM: pw.print("getcrcfromhsm "); break; case SCRUB: pw.print("scrub("); pw.print("limit=" + (Double.isInfinite(_throughputLimit) ? "off" : BYTES.toMiB(_throughputLimit))); pw.print(","); pw.print("period=" + TimeUnit.MILLISECONDS.toHours(_scrubPeriod)); pw.print(") "); } } pw.println(""); } private synchronized String getPolicies() { StringBuilder sb = new StringBuilder(); sb.append(" Policies :\n"). append(" on read : ").append(getPolicy(ON_READ)).append("\n"). append(" on write : ").append(getPolicy(ON_WRITE)).append("\n"). append(" on flush : ").append(getPolicy(ON_FLUSH)).append("\n"). append(" on restore : ").append(getPolicy(ON_RESTORE)).append("\n"). append(" on transfer : ").append("on").append("\n"). append(" enforce crc : ").append(getPolicy(ENFORCE_CRC)).append("\n"). append(" getcrcfromhsm : ").append(getPolicy(GET_CRC_FROM_HSM)).append("\n"). append(" scrub : ").append(getPolicy(SCRUB)).append("\n"); if (hasPolicy(SCRUB)) { if (Double.isInfinite(_throughputLimit)) { sb.append(" limit = off\n"); } else { sb.append(" limit = ").append(BYTES.toMiB(_throughputLimit)).append(" MiB/s\n"); } sb.append(" period = ").append(TimeUnit.MILLISECONDS.toHours(_scrubPeriod)).append(" hours\n"); } return sb.toString(); } @Command(name = "csm info", description = "Shows current checksum module configuration.") public class InfoCommand implements Callable<String> { @Override public String call() throws Exception { return getPolicies(); } } @AffectsSetup @Command(name = "csm set policy", description = "Define the checksum policy of the pool.") public class SetPolicyCommand implements Callable<String> { @Option(name = "scrub", category = "Scrubber options", usage = "Periodically verify pool data against checksums.", values = { "", "on", "off" }, valueSpec = "on|off") String scrub; @Option(name = "limit", category = "Scrubber options", usage = "Checksum computation throughput limit.", valueSpec = "<MiB/s>|off") String limit; @Option(name = "period", category = "Scrubber options", usage = "Run scrubber every HOURS hours.", metaVar = "hours") Integer period; @Option(name = "onread", category = "Transfer options", usage = "Not implemented.", values = { "", "on", "off" }, valueSpec = "on|off") String onRead; @Option(name = "onwrite", category = "Transfer options", usage = "Compute checksum after receiving the file form the client. In contrast to " + "-ontransfer, -onwrite will read back the file from disk or the disk cache " + "after the transfer has completed. Be aware that this will introduce a delay " + "after the transfer and that some clients may time out in the meantime.", values = { "", "on", "off" }, valueSpec = "on|off") String onWrite; @Option(name = "ontransfer", category = "Transfer options", usage = "Deprecated. Always ON." + "Compute checksum while receiving data from the client. If not supported " + "by the transfer protocol, the checksum is computed after the upload has " + "completed.", values = { "", "on", "off" }, valueSpec = "on|off") String onTransfer; @Option(name = "enforcecrc", category = "Transfer options", usage = "If no checksum was calculated during the transfer and no checksum was provided " + "by the client, then a checksum will be computed from the uploaded file.", values = { "", "on", "off" }, valueSpec = "on|off") String enforceCrc; @Option(name = "onflush", category = "HSM options", usage = "Compute checksum before flush to HSM.", values = { "", "on", "off" }, valueSpec = "on|off") String onFlush; @Option(name = "onrestore", category = "HSM options", usage = "Compute checksum after restore from HSM.", values = { "", "on", "off" }, valueSpec = "on|off") String onRestore; @Option(name = "getcrcfromhsm", category = "HSM options", usage = "If enabled, the pool will collect any checksum provided by the HSM and " + "store it in the name space.", values = { "", "on", "off" }, valueSpec = "on|off") String getCrcFromHsm; @Option(name = "v", usage = "Verbose.") boolean verbose; @Option(name = "frequently", metaVar = "IGNORED_VALUE", usage = "This option is accepted but ignored. It exists only " + "for backwards compatibility with older dCache pool 'setup' files") String ignoredValue; private void updatePolicy(String value, PolicyFlag flag) { if (value != null) { switch (value) { case "on": case "": _policy.add(flag); break; case "off": _policy.remove(flag); break; default: throw new IllegalArgumentException("Invalid value: " + value); } } } @Override public String call() throws IllegalArgumentException { synchronized (ChecksumModuleV1.this) { updatePolicy(onRead, ON_READ); updatePolicy(onWrite, ON_WRITE); updatePolicy(onFlush, ON_FLUSH); updatePolicy(onRestore, ON_RESTORE); updatePolicy(enforceCrc, ENFORCE_CRC); updatePolicy(getCrcFromHsm, GET_CRC_FROM_HSM); updatePolicy(scrub, SCRUB); if (limit != null) { if (limit.equals("off")) { _throughputLimit = Double.POSITIVE_INFINITY; } else { double value = MiB.toBytes(Double.parseDouble(limit)); if (value <= 0) { throw new IllegalArgumentException("Throughput limit must be > 0"); } _throughputLimit = value; } } if (period != null) { long value = TimeUnit.HOURS.toMillis(period); if (value <= 0) { throw new IllegalArgumentException("Scrub interval must be > 0"); } _scrubPeriod = value; } } listeners.forEach(Runnable::run); return verbose ? getPolicies() : ""; } } @AffectsSetup @Command(name = "csm set checksumtype", description = "Sets the default checksum type to compute and store for new files.\n\n" + "Unless the client has specified a checksum of a different type, the default " + "checksum defines the checksum that is computed for each new file and stored " + "in the name space.") public class SetChecksumTypeCommand implements Callable<String> { @Argument(valueSpec = "adler32|md5") String type; @Override public String call() throws IllegalArgumentException { ChecksumType checksumType = getChecksumType(type); synchronized (ChecksumModuleV1.this) { _defaultChecksumType = checksumType; } listeners.forEach(Runnable::run); return "New checksumtype : "+ checksumType; } } private synchronized String getPolicy(PolicyFlag flag) { return hasPolicy(flag) ? "on" : "off"; } @Override public synchronized boolean hasPolicy(PolicyFlag flag) { return _policy.contains(flag); } @Override public ChecksumFactory getPreferredChecksumFactory(ReplicaDescriptor handle) throws NoSuchAlgorithmException, CacheException { List<Checksum> existingChecksumsByPreference = Checksums.preferrredOrder().sortedCopy(handle.getChecksums()); return ChecksumFactory.getFactory(existingChecksumsByPreference, getDefaultChecksumType()); } @Override public void enforcePostTransferPolicy( ReplicaDescriptor handle, Iterable<Checksum> actualChecksums) throws CacheException, NoSuchAlgorithmException, IOException, InterruptedException { Iterable<Checksum> expectedChecksums = handle.getChecksums(); if (hasPolicy(ON_WRITE) || (hasPolicy(ENFORCE_CRC) && isEmpty(expectedChecksums) && isEmpty(actualChecksums))) { ChecksumFactory factory = ChecksumFactory.getFactory( concat(expectedChecksums, actualChecksums), getDefaultChecksumType()); try (RepositoryChannel channel = handle.createChannel()) { actualChecksums = concat(actualChecksums, Collections.singleton(factory.computeChecksum(channel))); } } compareChecksums(expectedChecksums, actualChecksums); handle.addChecksums(actualChecksums); } @Override public void enforcePreFlushPolicy(ReplicaDescriptor handle) throws CacheException, InterruptedException, NoSuchAlgorithmException, IOException { if (hasPolicy(ON_FLUSH)) { verifyChecksum(handle); } } @Override public void enforcePostRestorePolicy(ReplicaDescriptor handle) throws CacheException, NoSuchAlgorithmException, IOException, InterruptedException { if (hasPolicy(ON_RESTORE)) { handle.addChecksums(verifyChecksum(handle)); } } @Override public Iterable<Checksum> verifyChecksum(ReplicaDescriptor handle) throws NoSuchAlgorithmException, IOException, InterruptedException, CacheException { try (RepositoryChannel channel = handle.createChannel()) { return verifyChecksum(channel, handle.getChecksums()); } } @Override public Iterable<Checksum> verifyChecksum(RepositoryChannel channel, Iterable<Checksum> expectedChecksums) throws NoSuchAlgorithmException, IOException, InterruptedException, CacheException { return verifyChecksum(channel, expectedChecksums, Double.POSITIVE_INFINITY); } public Iterable<Checksum> verifyChecksumWithThroughputLimit(ReplicaDescriptor handle) throws IOException, InterruptedException, NoSuchAlgorithmException, CacheException { try (RepositoryChannel channel = handle.createChannel()) { return verifyChecksum(channel, handle.getChecksums(), getThroughputLimit()); } } private Iterable<Checksum> verifyChecksum(RepositoryChannel channel, Iterable<Checksum> expectedChecksums, double throughputLimit) throws NoSuchAlgorithmException, IOException, InterruptedException, CacheException { ChecksumFactory factory = ChecksumFactory.getFactory(expectedChecksums, getDefaultChecksumType()); Iterable<Checksum> actualChecksums = Collections.singleton(factory.computeChecksum(channel, throughputLimit)); compareChecksums(expectedChecksums, actualChecksums); return actualChecksums; } private void compareChecksums(Iterable<Checksum> expected, Iterable<Checksum> actual) throws CacheException { Map<ChecksumType, Checksum> checksumByType = Maps.newHashMap(); for (Checksum checksum: concat(expected, actual)) { Checksum otherChecksum = checksumByType.get(checksum.getType()); if (otherChecksum != null && !otherChecksum.equals(checksum)) { throw new FileCorruptedCacheException(ImmutableSet.copyOf(expected), ImmutableSet.copyOf(actual)); } checksumByType.put(checksum.getType(), checksum); } } }