package diskCacheV111.services.space; import com.google.common.base.Joiner; import com.google.common.base.Throwables; import com.google.common.collect.Maps; import org.springframework.beans.factory.annotation.Required; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.transaction.annotation.Transactional; import java.io.Serializable; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import diskCacheV111.util.AccessLatency; import diskCacheV111.util.CacheException; import diskCacheV111.util.FileNotFoundCacheException; import diskCacheV111.util.FsPath; import diskCacheV111.util.PnfsHandler; import diskCacheV111.util.PnfsId; import diskCacheV111.util.RetentionPolicy; import diskCacheV111.util.VOInfo; import dmg.cells.nucleus.CellCommandListener; import dmg.util.CommandSyntaxException; import dmg.util.command.Argument; import dmg.util.command.Command; import dmg.util.command.DelayedCommand; import dmg.util.command.Option; import org.dcache.auth.FQAN; import org.dcache.util.ByteUnit; import org.dcache.util.CDCExecutorServiceDecorator; import org.dcache.util.ColumnWriter; import org.dcache.util.SqlGlob; import static com.google.common.base.Strings.emptyToNull; import static com.google.common.collect.Iterables.concat; import static com.google.common.primitives.Longs.tryParse; import static java.lang.Double.parseDouble; import static org.dcache.util.ByteUnit.BYTES; import static org.dcache.util.ByteUnits.isoPrefix; import static org.dcache.util.ByteUnits.isoSymbol; public class SpaceManagerCommandLineInterface implements CellCommandListener { private SpaceManagerDatabase db; private PnfsHandler pnfs; private LinkGroupLoader linkGroupLoader; private Executor executor; @Required public void setExecutor(ExecutorService executor) { this.executor = new CDCExecutorServiceDecorator<>(executor); } @Required public void setDatabase(SpaceManagerDatabase db) { this.db = db; } @Required public void setPnfs(PnfsHandler pnfs) { this.pnfs = pnfs; } @Required public void setLinkGroupLoader(LinkGroupLoader linkGroupLoader) { this.linkGroupLoader = linkGroupLoader; } @Transactional(rollbackFor = { Exception.class }) private <T extends Serializable> T callInTransaction(Callable<T> callable) throws Exception { return callable.call(); } /** * Base class for asynchronous commands. * * Executes all commands in a database transaction. */ private abstract class AsyncCommand extends DelayedCommand<String> { public AsyncCommand() { super(executor); } @Override protected final String execute() throws SpaceException, CacheException, CommandSyntaxException, DataAccessException, IllegalArgumentException { try { return callInTransaction(this::executeInTransaction); } catch (EmptyResultDataAccessException e) { // These are usually a result of the user querying for an object that doesn't exist. return e.getMessage(); } catch (Exception e) { Throwables.propagateIfInstanceOf(e, CommandSyntaxException.class); Throwables.propagateIfInstanceOf(e, SpaceException.class); Throwables.propagateIfInstanceOf(e, CacheException.class); throw Throwables.propagate(e); } } protected abstract String executeInTransaction() throws SpaceException, CacheException, DataAccessException, IllegalArgumentException; } @Command(name = "release space", hint = "release reservation", description = "Releases a space reservation. The files in the reservation are not deleted " + "from dCache, but the space occupied by those files is no longer accounted " + "for by the space manager. Such files will continue to appear as used space in " + "their link group.") public class ReleaseSpaceCommand extends AsyncCommand { @Argument(usage = "Space token of reservation to release.") long token; @Override public String executeInTransaction() throws DataAccessException { Space space = db.selectSpaceForUpdate(token); space.setState(SpaceState.RELEASED); db.updateSpace(space); return space.toString(); } } @Command(name = "update space", hint = "modify space reservation parameters") public class UpdateSpaceCommand extends AsyncCommand { @Option(name = "size", usage = "Size in bytes, with optional byte unit suffix using either SI or IEEE 1541 prefixes.", metaVar="bytes") String size; @Option(name = "owner", usage = "Changes the owner. Using the empty string makes the space reservation unowned.", valueSpec="USER|FQAN|GID") String owner; @Option(name = "lifetime", usage = "Lifetime in seconds from now.") Long lifetime; @Option(name = "eternal", usage = "Space reservation will never expire.") boolean eternal; @Option(name = "desc", usage = "Space token description.") String description; @Argument(metaVar="spacetoken", usage = "Token of space reservation to update.") Long token; @Override public String executeInTransaction() throws DataAccessException, SpaceReleasedException, SpaceExpiredException { Space space = db.selectSpaceForUpdate(token); if (space.getState() == SpaceState.RELEASED) { throw new SpaceReleasedException("Space reservation has been released and cannot be updated."); } if (space.getState() == SpaceState.EXPIRED) { throw new SpaceExpiredException("Space reservation has expired and cannot be updated."); } if (owner != null) { if (owner.isEmpty()) { space.setVoGroup(null); space.setVoRole(null); } else { String group; String role; // check that linkgroup allows this owner combination LinkGroup lg = db.getLinkGroup(space.getLinkGroupId()); FQAN fqan = new FQAN(owner); group = fqan.getGroup(); role = emptyToNull(fqan.getRole()); boolean foundMatch = false; for (VOInfo info : lg.getVOs()) { if (info.match(group, role)) { foundMatch = true; break; } } if (!foundMatch) { return "Cannot change owner to " + owner + ". " + "Authorized for this link group are:\n" + Joiner.on('\n').join(lg.getVOs()); } space.setVoGroup(group); space.setVoRole(role); } } if (eternal) { if (lifetime != null) { throw new IllegalArgumentException("Eternal reservations cannot have a lifetime."); } space.setExpirationTime(null); } else if (lifetime != null) { space.setExpirationTime(System.currentTimeMillis() + lifetime * 1000); } if (size != null) { space.setSizeInBytes(parseByteQuantity(size)); } if (description != null) { space.setDescription(description); } db.updateSpace(space); return space.toString(); } } @Command(name = "ls link groups", hint = "list link groups", description = "If an argument is given, the command displays all link groups with a name " + "matching the pattern. If no argument is given, all link groups are " + "displayed. The list can be further restricted using the options.\n\n" + "For each link group the following information is displayed left to right: " + "File types allowed in this link group (output(o), replica(r), custodial(c), " + "nearline (n), online(o)), number of reservations in link group, reserved " + "space, unreserved space, last refresh timestamp, and the link group name.\n\n" + "Link groups are periodically imported from pool manager. The last refresh time " + "indicates when the information was last updated.\n\n" + "Link groups don't have a size. Only the current amount of free space in online " + "pools accessible by the link group is known. Part of that free space is reserved " + "(but not used) by space reservations. This is reported as reserved space. The " + "remaining free space is reported as unreserved space. Unreserved space can be " + "reserved by creating new space reservations or by enlarging existing reservations. " + "Since pools may go offline, unreserved space can become negative. In this case " + "the link group is overallocated and the reserved space is no longer guaranteed.") public class ListLinkGroupsCommand extends AsyncCommand { @Option(name = "a", usage = "Include link groups that have not been refreshed recently.") boolean all; @Option(name = "l", usage = "Include additional details.") boolean verbose; @Option(name = "al", usage = "Only show link groups that allow this access latency.", values = { "online", "nearline" }) AccessLatency al; @Option(name = "rp", usage = "Only show link groups that allow this retention policy.", values = { "output", "replica", "custodial"}) RetentionPolicy rp; @Option(name = "h", usage = "Use unit suffixes Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and " + "Petabyte in order to reduce the number of digits to three or less " + "using base 10 for sizes.") boolean humanReadable; @Argument(required = false) SqlGlob name; @Override public String executeInTransaction() throws DataAccessException { SpaceManagerDatabase.LinkGroupCriterion linkgroups = db.linkGroups(); if (!all) { linkgroups.whereUpdateTimeAfter(linkGroupLoader.getLatestUpdateTime()); } if (al != null) { linkgroups.allowsAccessLatency(al); } if (rp != null) { linkgroups.allowsRetentionPolicy(rp); } if (name != null) { linkgroups.whereNameMatches(name); } ColumnWriter writer = new ColumnWriter() .abbreviateBytes(humanReadable) .header("FLAGS").fixed("-").left("output").left("replica").left("custodial").fixed(":").left("nearline").left("online") .space().header("CNT").right("spaces") .space().header("RESVD").bytes("reserved") .fixed(" + ") .header("AVAIL").bytes("available") .fixed(" = ") .header("FREE").bytes("free") .space().header("UPDATED").date("updated") .space().header("NAME").left("name"); for (LinkGroup group : db.get(linkgroups)) { writer.row() .value("output", group.isOutputAllowed() ? 'o' : '-') .value("replica", group.isReplicaAllowed() ? 'r' : '-') .value("custodial", group.isCustodialAllowed() ? 'c' : '-') .value("nearline", group.isNearlineAllowed() ? 'n' : '-') .value("online", group.isOnlineAllowed() ? 'o' : '-') .value("spaces", db.count(db.spaces().whereLinkGroupIs(group.getId()))) .value("reserved", group.getReservedSpace()) .value("available", group.getAvailableSpace()) .value("free", group.getFreeSpace()) .value("updated", group.getUpdateTime()) .value("name", group.getName()); if (verbose) { for (VOInfo voInfo : group.getVOs()) { writer.row(" " + voInfo); } } } return writer.toString(); } } @Command(name = "ls spaces", hint = "list space reservations", description = "If an argument is given, the command displays space reservations for which the " + "space description matches the pattern. If the argument is an integer, the argument " + "is interpreted as a space token and a matching space reservation is displayed." + "If no argument is given, all space reservations are displayed. The list can be " + "further restricted using the options.\n\n" + "For each space reservation the following information may be displayed left to right: " + "Space token, reservation state (reserved(-), released(r), expired(e)), default " + "retention policy, default access latency, number of files in space reservation, owner, " + "allocated bytes, used bytes, unused bytes, size of space, creation time, expiration " + "time, and space description.\n\n" + "Space reservations have a size. This size is partitioned into space that is " + "used by files stored in the space reservation, space that is allocated for named " + "files that have not yet been uploaded, and free space. The latter two make up the " + "reserved space of the link group within which the space exists. Note that ones " + "a file is uploaded to a space reservation, the space consumed by the file is " + "obviously not free anymore and will thus not appear in the link group statistics.\n\n") public class ListSpacesCommand extends AsyncCommand { @Option(name = "a", usage = "Include ephemeral, expired and released space reservations.") boolean all; @Option(name = "l", usage = "Include additional details.") boolean verbose; @Option(name = "e", usage = "Include ephemeral space reservations.") boolean ephemeral; @Option(name = "owner", usage = "Only show reservations whose owner matches this pattern.", valueSpec="USER|FQAN|GID") String owner; @Option(name = "al", usage = "Only show reservations with this default access latency.", values = { "online", "nearline" }) AccessLatency al; @Option(name = "rp", usage = "Only show reservations with this default retention policy.", values = { "replica", "custodial" }) RetentionPolicy rp; @Option(name = "state", values = { "reserved", "released", "expired" }, usage = "Only show reservations in one of these states.") SpaceState[] states; @Option(name = "lg", usage = "Only show reservations in the named link group.") String linkGroup; @Option(name = "h", usage = "Use unit suffixes Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and " + "Petabyte in order to reduce the number of digits to three or less " + "using base 10 for sizes.") boolean humanReadable; @Option(name = "limit", usage = "Limit output to this many space reservations.", metaVar = "rows") Integer limit = 10000; @Argument(required = false, usage = "Only show reservations with this token or a description matching this pattern.", valueSpec = "TOKEN|PATTERN") SqlGlob pattern; @Override public String executeInTransaction() throws DataAccessException { ColumnWriter writer = new ColumnWriter() .abbreviateBytes(humanReadable) .header("TOKEN").right("token"); if (all || verbose || states != null && states.length > 0 || pattern != null) { writer.space().header("S").left("status"); } if (verbose || linkGroup == null) { writer.space().header("LINKGROUP").left("linkgroup"); } writer .space().header("RETENTION").left("rp") .space().header("LATENCY").left("al"); if (verbose || owner != null) { writer.space().header("OWNER").left("owner"); } writer.space().header("ALLO").bytes("allocated") .fixed(" + ").header("USED").bytes("used") .fixed(" + ").header("FREE").bytes("free") .fixed(" = ").header("SIZE").bytes("size"); if (verbose) { writer.space().header("CREATED").date("created"); } if (ephemeral || all || verbose || pattern != null) { writer.space().header("EXPIRES").date("expires"); } writer.space().header("DESCRIPTION").left("description"); Iterable<Space> spaces; if (pattern == null) { spaces = db.get(spacesWhereOptionsMatch(), limit); } else { spaces = db.get(spacesWhereOptionsMatch().whereDescriptionMatches(pattern), limit); Long token = tryParse(pattern.toString()); if (token != null) { List<Space> moreSpaces = db.get(spacesWhereOptionsMatch().whereTokenIs(token), limit); spaces = concat(moreSpaces, spaces); } } Map<Long,String> linkGroups = Maps.transformValues(Maps.uniqueIndex(db.get(db.linkGroups()), LinkGroup::getId), LinkGroup::getName); for (Space space : spaces) { char status; switch (space.getState()) { case EXPIRED: status = 'e'; break; case RELEASED: status = 'r'; break; default: status = '-'; break; } writer.row() .value("token", space.getId()) .value("status", status) .value("linkgroup", linkGroups.get(space.getLinkGroupId())) .value("rp", space.getRetentionPolicy()) .value("al", space.getAccessLatency()) .value("allocated", space.getAllocatedSpaceInBytes()) .value("used", space.getUsedSizeInBytes()) .value("free", space.getAvailableSpaceInBytes()) .value("size", space.getSizeInBytes()) .value("created", space.getCreationTime()) .value("expires", space.getExpirationTime()) .value("description", space.getDescription()) .value("owner", toOwner(space.getVoGroup(), space.getVoRole())); } return writer.toString(); } private SpaceManagerDatabase.SpaceCriterion spacesWhereOptionsMatch() { SpaceManagerDatabase.SpaceCriterion spaces = db.spaces(); if (owner != null) { FQAN fqan = new FQAN(owner); spaces.whereGroupMatches(new SqlGlob(fqan.getGroup())); if (fqan.hasRole()) { spaces.whereRoleMatches(new SqlGlob(fqan.getRole())); } } if (linkGroup != null) { spaces.whereLinkGroupIs(db.getLinkGroupByName(linkGroup).getId()); } if (al != null) { spaces.whereAccessLatencyIs(al); } if (rp != null) { spaces.whereRetentionPolicyIs(rp); } if (states != null && states.length > 0) { spaces.whereStateIsIn(states); } else if (!all && pattern == null) { spaces.whereStateIsIn(SpaceState.RESERVED); } if (!ephemeral && !all && pattern == null) { spaces.thatNeverExpire(); } return spaces; } } private String toOwner(String voGroup, String voRole) { if (voGroup == null || voGroup.isEmpty()) { return null; } else if (voGroup.charAt(0) != '/' || voRole == null || voRole.isEmpty() || voRole.equals("*")) { return voGroup; } else { return voGroup + "/Role=" + voRole; } } @Command(name = "ls files", hint = "list file reservations", description = "If an argument is given, the command displays reserved files for which the " + "PNFS ID or path matches the argument. If no argument is given, all file reservations " + "in a transient state are displayed. The list can be further expanded or restricted " + "using the options.\n\n" + "For each file reservation the following information may be displayed left to right: " + "current state [transferring(t), stored(s), flushed(f)], space token, owner, size in " + "bytes, creation time, expiration time, PNFS ID, and path.\n\n" + "A space reservation contains file reservations that consume the reserved space. " + "Each file reservation is in one of three states: TRANSFERRING, STORED, or FLUSHED.\n\n" + "TRANSFERRING files are in the process of being uploaded. Such files have " + "a PNFS ID associated with them.\n\n" + "STORED files have finished uploading.\n\n" + "FLUSHED files have been flushed to tape and no longer consume space in the " + "space reservation.") public class ListFilesCommand extends AsyncCommand { @Option(name = "owner", usage = "Only show files whose owner matches this pattern.", valueSpec="USER|FQAN|GID") String owner; @Option(name = "token", usage = "Only show files in space reservation with this token.") Long token; @Option(name = "a", usage = "Include stored and flushed files.") boolean all; @Option(name = "p", usage = "Lookup file system path from PNFS ID. This may slow down listing " + "considerably.") boolean lookup; @Option(name = "limit", usage = "Limit output to this many space reservations.", metaVar = "rows") Integer limit = 10000; @Option(name = "h", usage = "Use unit suffixes Byte, Kilobyte, Megabyte, Gigabyte, Terabyte and " + "Petabyte in order to reduce the number of digits to three or less " + "using base 10 for sizes.") boolean humanReadable; @Option(name = "state", values = { "transferring", "stored", "flushed" }, usage = "Only show files in one of these states.") FileState[] states; @Argument(required = false, usage = "Only show files with this PNFSID or path.", valueSpec = "PNFSID|PATH") String file; @Override public String executeInTransaction() throws DataAccessException, CacheException { ColumnWriter writer = new ColumnWriter() .abbreviateBytes(humanReadable) .header("STATE").left("state") .space().header("TOKEN").right("token") .space().header("OWNER").left("owner") .space().header("SIZE").bytes("size") .space().header("CREATED").date("created"); writer.space().header("PNFSID").left("pnfsid"); if (lookup) { writer.space().header("PATH").left("path"); } Iterable<File> files; if (file != null) { PnfsId pnfsId = PnfsId.isValid(file) ? new PnfsId(file) : pnfs.getPnfsIdByPath(file); files = db.get(filesWhereOptionsMatch().wherePnfsIdIs(pnfsId), limit); } else { files = db.get(filesWhereOptionsMatch(), limit); } for (File file : files) { char state; switch (file.getState()) { case TRANSFERRING: state = 't'; break; case STORED: state = 's'; break; case FLUSHED: state = 'f'; break; default: state = '-'; break; } PnfsId pnfsId = file.getPnfsId(); FsPath path; try { path = lookup ? pnfs.getPathByPnfsId(pnfsId) : null; } catch (FileNotFoundCacheException e) { path = null; } writer.row() .value("owner", toOwner(file.getVoGroup(), file.getVoRole())) .value("created", file.getCreationTime()) .value("size", file.getSizeInBytes()) .value("pnfsid", pnfsId) .value("path", path) .value("token", file.getSpaceId()) .value("state", state); } return writer.toString(); } private SpaceManagerDatabase.FileCriterion filesWhereOptionsMatch() { SpaceManagerDatabase.FileCriterion files = db.files(); if (owner != null) { FQAN fqan = new FQAN(owner); files.whereGroupMatches(new SqlGlob(fqan.getGroup())); if (fqan.hasRole()) { files.whereRoleMatches(new SqlGlob(fqan.getRole())); } } if (token != null) { files.whereSpaceTokenIs(token); } if (states != null && states.length > 0) { files.whereStateIsIn(states); } else if (!all && file == null) { files.whereStateIsIn(FileState.TRANSFERRING); } return files; } } @Command(name = "reserve space", hint = "create space reservation", description = "A space reservation has a size, an access latency, and a retention policy. " + "It may have a description, a lifetime, and an owner. If the lifetime " + "is exceeded, the reservation expires and the files in it are released. " + "The owner is only used to authorize the release of the reservation - it is " + "not used to authorize uploads to the reservation. Reservations without an owner " + "can only be administratively released.\n\n" + "Space reservations are created in link groups. The link group authorizes " + "reservations. The owner of the reservation as well as its file type " + "(retention policy and access latency) must be allowed in the link group " + "within which the reservation is created.\n\n" + "The size argument accepts an optional byte unit suffix using either SI or " + "IEEE 1541 prefixes.") public class ReserveSpaceCommand extends AsyncCommand { @Option(name = "owner", usage = "Only the owner can release the space through SRM. If not specified, " + "the reservation becomes unowned. Unowned reservations can only be " + "released through the admin interface.", valueSpec="USER|FQAN|GID") String owner; @Option(name = "al", usage = "Access latency.", required = true, values = { "online", "nearline" }) AccessLatency al; @Option(name = "rp", usage = "Retention policy.", required = true, values = { "replica", "custodial"}) RetentionPolicy rp; @Option(name = "desc") String description; @Option(name = "lg", required = true, metaVar = "name", usage = "Link group within which to create the space reservation.") String lg; @Option(name = "lifetime", metaVar = "seconds", usage = "Lifetime in seconds. If no lifetime is given, the reservation will " + "never expire.") Long lifetime; @Argument( usage = "Size of reservation in bytes. Accepts an optional byte unit suffix using " + "either SI or IEEE 1541 prefixes.") String size; @Override public String executeInTransaction() throws DataAccessException { long sizeInBytes = parseByteQuantity(size); LinkGroup linkGroup = db.getLinkGroupByName(lg); if (linkGroup.getUpdateTime() < linkGroupLoader.getLatestUpdateTime()) { return "Link group " + lg + " has existed, but it is no longer published by pool manager."; } if (!linkGroup.isAllowed(al)) { return "Link group " + lg + " cannot accommodate " + al + " reservations."; } if (!linkGroup.isAllowed(rp)) { return "Link group " + lg + " cannot accommodate " + rp + " reservations."; } if (sizeInBytes > linkGroup.getAvailableSpace()) { return "Link group " + lg + " only has " + linkGroup.getAvailableSpace() + " bytes available."; } String group = null; String role = null; if (owner != null) { FQAN fqan = new FQAN(owner); group = fqan.getGroup(); role = emptyToNull(fqan.getRole()); } Space space = db.insertSpace(group, role, rp, al, linkGroup.getId(), sizeInBytes, (lifetime == null || lifetime == -1) ? -1 : lifetime * 1000, description, SpaceState.RESERVED, 0, 0); return space.toString(); } } @Command(name = "purge file", hint = "purge file from space", description = "Removes a file from its space reservation without deleting the file from " + "dCache. The space in the reservation that was set aside for the file will be " + "available to other files, assuming the link group has enough free space.\n\n" + "This command is the file level equivalent to releasing the entire space " + "reservation.") public class PurgeFileCommand extends AsyncCommand { @Argument(metaVar = "pnfsid", usage = "PNFS ID of file.") PnfsId pnfsId; @Override public String executeInTransaction() throws DataAccessException { File f = db.findFile(pnfsId); if (f == null) { return "No such file reservation: " + pnfsId; } db.removeFile(f.getId()); return "Purged " + f; } } @Command(name = "purge spaces", hint = "remove perished space reservations", description = "Space reservations that are expired or released are said to have perished. " + "Perished space is no longer considered reserved, but it is kept in the database " + "until purged. Until a space reservation is purged, the files it contained are " + "still tracked in the database and can be inspected using the 'ls files' command.\n\n" + "Purging a space reservation does not delete the files in dCache. They remain in " + "the linkgroup in which they were stored, however space manager no longer tracks " + "the files.\n\n" + "Space reservations that have an expiration date are purged automatically after " + "a configurable amount of time after they expire. Space reservations without an " + "expiration data have to be purged explicitly.") public class PurgeSpacesCommand extends AsyncCommand { @Override protected String executeInTransaction() throws DataAccessException { db.remove(db.files().in( db.spaces() .whereStateIsIn(SpaceState.EXPIRED, SpaceState.RELEASED))); int spaces = db.remove(db.spaces() .whereStateIsIn(SpaceState.EXPIRED, SpaceState.RELEASED) .thatHaveNoFiles()); return (spaces == 1) ? "One space reservation purged." : (spaces + " space reservations purged."); } } private static long parseByteQuantity(String arg) { // REVISIT: does this need really to be case insensitive? try { String s = arg.endsWith("B") || arg.endsWith("b") ? arg.substring(0, arg.length()-1).toUpperCase() : arg.toUpperCase(); ByteUnit units = Arrays.stream(ByteUnit.values()) .skip(1) .filter(u -> s.endsWith(isoPrefix().of(u).toUpperCase())) .findFirst().orElse(BYTES); String num = (units == BYTES) ? s : s.substring(0, s.length() - isoPrefix().of(units).length()); return checkNonNegative((long) (units.toBytes(parseDouble(num)) + 0.5)); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Cannot convert size specified (" + arg + ") to non-negative number. \n" + "Valid definitions of size:\n" + "\t\t - a number of bytes (long integer less than 2^64) \n" + "\t\t - a number with a prefix; e.g., 100 k, 100 kB,\n" + "\t\t 100 KiB, 100M, 100MB, 100MiB, 100G, 100GB,\n" + "\t\t 100GiB, 10T, 10.5TB, 100TiB, 2P, 2.3PB, 1PiB\n" + "see http://en.wikipedia.org/wiki/Gigabyte for an explanation."); } } private static long checkNonNegative(long size) { if (size < 0L) { throw new IllegalArgumentException("Size must be non-negative."); } return size; } }