Skip to content

Commit

Permalink
Merge pull request #240 from at88mph/quota-plugin-configurable
Browse files Browse the repository at this point in the history
Quota plugin configurable
  • Loading branch information
pdowler authored May 27, 2024
2 parents 000fa2a + 5ddf521 commit 62bafc8
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 28 deletions.
20 changes: 16 additions & 4 deletions cavern/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ org.opencadc.cavern.resourceID = ivo://{authority}/{name}
# (optional) identify which container nodes are allocations
org.opencadc.cavern.allocationParent = {top level node}
# (optional) provide a class implementing the org.opencadc.cavern.nodes.QuotaPlugin interface to control Quotas
# for users.
# Optional, but the default of NoQuotaPlugin will get used if not specified, and users will have free reign.
# - CephFSQuotaPlugin: Use CephFS's extended attributes to determine current quota and folder sizes.
# - NoQuotaPlugin: Default - no quota checking
#
org.opencadc.cavern.nodes.QuotaPlugin = CephFSQuotaPlugin | NoQuotaPlugin
# base directory for cavern files
org.opencadc.cavern.filesystem.baseDir = {persistent data directory in container}
org.opencadc.cavern.filesystem.subPath = {relative path to the node/file content that could be mounted in other containers}
Expand All @@ -78,8 +86,8 @@ org.opencadc.cavern.sshfs.serverBase = {server}[:{port}]:{path}
The _resourceID_ is the resourceID of _this_ `cavern` service.

The _allocationParent_ is a path to a container node (directory) which contains space allocations. An allocation
is owned by a user (uisually different from the _rootOwner_ admin user) who is responsible for the allocation
and all conntent therein. The owner of an allocation is granted additional permissions within their
is owned by a user (usually different from the _rootOwner_ admin user) who is responsible for the allocation
and all content therein. The owner of an allocation is granted additional permissions within their
allocation (they can read/write/delete anything) so the owner cannot be blocked from access to any content
within their allocation. This probably only matters for multi-user projects. Multiple _allocationParent_(s) may
be configured to organise the top level of the content (e.g. /home and /projects). Paths configured to be
Expand All @@ -91,11 +99,15 @@ The _filesystem.baseDir_ is the path to a base directory containing the `cavern`
The _filesystem.subPath_ is the relative path to the node/file content that could be mounted in other containers.

The _filesystem.rootOwner_ is the username of the owner of the root container in the VOSpace. The root owner has some admin
priviledges: can create allocations (create a container node owned by another user) and can set the quota property
privileges: can create allocations (create a container node owned by another user) and can set the quota property
on such containers. Note: quota is not currently implemented in `cavern`.

The _org.opencadc.cavern.nodes.QuotaPlugin_ is the concrete class that implements the
[QuotaPlugin](./src/main/java/org/opencadc/cavern/nodes/QuotaPlugin.java) interface. Absences of this property
assumes no quota support and users can fill underlying storage in an uncontrolled way.

The `cavern` service must be able to resolve the root owner username to a POSIX uid and gid pair during startup. If
the configured IdentityManager does not suport priviledged access to user info, the correct values must be configured
the configured IdentityManager does not support privileged access to user info, the correct values must be configured
using the optional _filesystem.rootOwner.uid_ and _filesystem.rootOwner.gid_ properties.

NOT FUNCTIONAL: The optional _sshfs.serverBase_ is the host name, port, and path to the sshfs mount of the `cavern` content. Clients
Expand Down
88 changes: 75 additions & 13 deletions cavern/src/main/java/org/opencadc/cavern/CavernConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,24 @@
import ca.nrc.cadc.util.MultiValuedProperties;
import ca.nrc.cadc.util.PropertiesReader;
import java.io.File;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.net.URI;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import javax.security.auth.Subject;

import ca.nrc.cadc.util.StringUtil;
import org.apache.log4j.Logger;
import org.opencadc.cavern.nodes.NoQuotaPlugin;
import org.opencadc.cavern.nodes.QuotaPlugin;

public class CavernConfig {

private static final Logger log = Logger.getLogger(CavernConfig.class);

public static final String DEFAULT_CONFIG_DIR = System.getProperty("user.home") + "/config/";
public static final String CAVERN_PROPERTIES = "cavern.properties";
private static final String CAVERN_KEY = CavernConfig.class.getPackage().getName();
public static final String RESOURCE_ID = CAVERN_KEY + ".resourceID";
Expand All @@ -104,6 +109,8 @@ public class CavernConfig {
public static final String ROOT_OWNER_GID = CAVERN_KEY + ".filesystem.rootOwner.gid";

public static final String ALLOCATION_PARENT = CAVERN_KEY + ".allocationParent";

public static final String QUOTA_PLUGIN_IMPLEMENTATION = QuotaPlugin.class.getName();

private final URI resourceID;
private final List<String> allocationParents = new ArrayList<>();
Expand Down Expand Up @@ -134,7 +141,8 @@ public CavernConfig() {
boolean rootOwnerProp = checkProperty(mvp, sb, ROOT_OWNER, true);
boolean sshfsServerBaseProp = checkProperty(mvp, sb, SSHFS_SERVER_BASE, false);
boolean allocProp = checkProperty(mvp, sb, ALLOCATION_PARENT, false);

checkProperty(mvp, sb, QUOTA_PLUGIN_IMPLEMENTATION, false);

if (!resourceProp || !baseDirProp || !subPathProp || !rootOwnerProp) {
throw new InvalidConfigException(sb.toString());
}
Expand All @@ -143,18 +151,15 @@ public CavernConfig() {

String baseDir = mvp.getFirstPropertyValue(CavernConfig.FILESYSTEM_BASE_DIR);
String subPath = mvp.getFirstPropertyValue(CavernConfig.FILESYSTEM_SUB_PATH);
String sep = "/";
if (baseDir.endsWith("/") || subPath.startsWith("/")) {
sep = "";
}

this.root = Paths.get(baseDir, subPath);
this.resourceID = URI.create(s);
for (String sap : mvp.getProperty(ALLOCATION_PARENT)) {
String ap = sap;
if (ap.charAt(0) == '/') {
ap = ap.substring(1);
}
if (ap.length() > 0 && ap.charAt(ap.length() - 1) == '/') {
if (!ap.isEmpty() && ap.charAt(ap.length() - 1) == '/') {
ap = ap.substring(0, ap.length() - 1);
}
if (ap.indexOf('/') >= 0) {
Expand All @@ -164,11 +169,7 @@ public CavernConfig() {
// empty string means root, otherwise child of root
allocationParents.add(ap);
}

sep = "/";
if (baseDir.endsWith("/")) {
sep = "";
}

this.secrets = Paths.get(baseDir, "secrets");
}

Expand Down Expand Up @@ -220,7 +221,68 @@ public Subject getRootOwner() {
}
return ret;
}


/**
* Obtain the QuotaPlugin class instance.
* @return QuotaPlugin implementation, or NoQuotaPlugin instance if none set.
*/
public QuotaPlugin getQuotaPlugin() {
String cname = mvp.getFirstPropertyValue(CavernConfig.QUOTA_PLUGIN_IMPLEMENTATION);
if (!StringUtil.hasText(cname)) {
log.debug("getQuotaPlugin: defaulting to NoQuotaPlugin");
return CavernConfig.loadPlugin(NoQuotaPlugin.class.getName());
} else {
return CavernConfig.loadPlugin(QuotaPlugin.class.getPackage().getName() + "." + cname);
}
}

/**
* TODO: Generify this? Storage Inventory uses a plugin loader as well, which this duplicates.
* TODO: jenkinsd 2024.05.14
*
* <p>Load and instantiate an instance of the specified Java concrete class.
*
* <p>It assumes that the requested Class contains a constructor with an argument length matching the length of
* the provided constructorArgs. No argument type checking is performed.
*
* @param <T> Class type of the instantiated class
* @param implementationClassName Class name to create
* @param constructorArgs The constructor arguments
* @return configured implementation of the interface
*
* @throws IllegalStateException if an instance cannot be created
*/
@SuppressWarnings("unchecked")
public static <T> T loadPlugin(final String implementationClassName, final Object... constructorArgs)
throws IllegalStateException {
if (implementationClassName == null) {
throw new IllegalStateException("Implementation class name cannot be null.");
}
try {
Class<?> c = Class.forName(implementationClassName);
for (final Constructor<?> constructor : c.getDeclaredConstructors()) {
if (constructor.getParameterCount() == constructorArgs.length) {
return (T) constructor.newInstance(constructorArgs);
}
}
throw new IllegalStateException("No matching constructor found.");
} catch (ClassNotFoundException ex) {
throw new IllegalStateException("CONFIG: " + implementationClassName + " implementation not found in classpath: " + implementationClassName,
ex);
} catch (InstantiationException ex) {
throw new IllegalStateException(
"CONFIG: " + implementationClassName + " implementation " + implementationClassName + " does not have a matching constructor", ex);
} catch (InvocationTargetException ex) {
Throwable cause = ex.getCause();
if (cause != null) { // it has to be, but just to be safe
throw new IllegalStateException("CONFIG: " + implementationClassName + " init failed: " + cause.getMessage(), cause);
}
throw new IllegalStateException("CONFIG: " + implementationClassName + " init failed: " + ex.getMessage(), ex);
} catch (IllegalAccessException ex) {
throw new IllegalStateException("CONFIG: failed to instantiate " + implementationClassName, ex);
}
}

// for non-mandatory prop use
public MultiValuedProperties getProperties() {
return mvp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.Comparator;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeSet;
Expand Down Expand Up @@ -135,9 +134,8 @@ public class FileSystemNodePersistence implements NodePersistence {
));

private final PosixIdentityManager identityManager;
private final PosixMapperClient posixMapper;
private final GroupCache groupCache;
private final QuotaPlugin quotaImpl = new NoQuotaPlugin(); // TODO: configurable
private final QuotaPlugin quotaImpl;

private final ContainerNode root;
private final Set<ContainerNode> allocationParents = new TreeSet<>();
Expand All @@ -149,6 +147,7 @@ public class FileSystemNodePersistence implements NodePersistence {
public FileSystemNodePersistence() {
this.config = new CavernConfig();
this.rootPath = config.getRoot();
this.quotaImpl = config.getQuotaPlugin();

LocalServiceURI loc = new LocalServiceURI(config.getResourceID());
this.rootURI = loc.getVOSBase();
Expand Down Expand Up @@ -183,15 +182,16 @@ public FileSystemNodePersistence() {
// only require a group mapper because IVOA GMS does not include numeric gid
// assume user mapper is the same service
URI posixMapperID = la.getServiceURI(Standards.POSIX_GROUPMAP.toASCIIString());
PosixMapperClient posixMapper;
if ("https".equals(posixMapperID.getScheme())) {
try {
URL baseURL = posixMapperID.toURL();
this.posixMapper = new MyPosixMapperClient(baseURL);
posixMapper = new MyPosixMapperClient(baseURL);
} catch (MalformedURLException ex) {
throw new InvalidConfigException("invalid " + Standards.POSIX_GROUPMAP.toASCIIString() + " base URL: " + posixMapperID, ex);
}
} else {
this.posixMapper = new MyPosixMapperClient(posixMapperID);
posixMapper = new MyPosixMapperClient(posixMapperID);
}
this.groupCache = new GroupCache(posixMapper);
this.localGroupsOnly = true;
Expand Down Expand Up @@ -320,13 +320,13 @@ public ResourceIterator<Node> iterator(ContainerNode parent, Integer limit, Stri
LocalServiceURI loc = new LocalServiceURI(getResourceID());
VOSURI vu = loc.getURI(parent);
ResourceIterator<Node> ni = nut.list(vu);
return new IdentWrapper(parent, ni, nut);
return new IdentWrapper(parent, ni);
} catch (IOException ex) {
throw new RuntimeException("oops", ex);
}
}

private class EmptyNodeIterator implements ResourceIterator<Node> {
private static class EmptyNodeIterator implements ResourceIterator<Node> {

@Override
public boolean hasNext() {
Expand All @@ -348,12 +348,10 @@ private class IdentWrapper implements ResourceIterator<Node> {

private final ContainerNode parent;
private ResourceIterator<Node> childIter;
private final NodeUtil nut;

IdentWrapper(ContainerNode parent, ResourceIterator<Node> childIter, NodeUtil nut) {

IdentWrapper(ContainerNode parent, ResourceIterator<Node> childIter) {
this.parent = parent;
this.childIter = childIter;
this.nut = nut;
}

@Override
Expand Down

0 comments on commit 62bafc8

Please sign in to comment.