mirror of https://github.com/apache/cassandra
Merge a3e7cccf6a into 10557d7ffe
This commit is contained in:
commit
cfba53f6a7
71
bin/nodetool
71
bin/nodetool
|
|
@ -43,30 +43,51 @@ if [ -z "$CASSANDRA_CONF" -o -z "$CLASSPATH" ]; then
|
|||
exit 1
|
||||
fi
|
||||
|
||||
JMX_PORT=""
|
||||
|
||||
# Try to parse port from configure_jmx method call, when not commented out
|
||||
if [ -f "$CASSANDRA_CONF/cassandra-env.sh" ]; then
|
||||
jmx_method_call=$(grep "^configure_jmx \+[0-9]\+$" "$CASSANDRA_CONF/cassandra-env.sh")
|
||||
if [ ! "x${jmx_method_call}" = "x" ]; then
|
||||
JMX_PORT=$(echo "${jmx_method_call}" | tr -s " " | cut -d " " -f2)
|
||||
# Check if protocol is cql (from environment variable)
|
||||
PROTOCOL_IS_CQL=""
|
||||
if [ "x$CASSANDRA_CLI_EXECUTION_PROTOCOL" != "x" ]; then
|
||||
protocol_lower=$(echo "$CASSANDRA_CLI_EXECUTION_PROTOCOL" | tr '[:upper:]' '[:lower:]')
|
||||
if [ "$protocol_lower" = "cql" ]; then
|
||||
PROTOCOL_IS_CQL="true"
|
||||
fi
|
||||
fi
|
||||
|
||||
# In case JMX_PORT is not set (when configure_jmx in cassandra-env.sh is commented out),
|
||||
# try to parse it from cassandra.yaml.
|
||||
if [ "x$JMX_PORT" = "x" ]; then
|
||||
# Check for cql protocol in system properties to determine if we should parse CQL or JMX port
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-Dcassandra.cli.execution.protocol=cql|-Dcassandra.cli.execution.protocol=CQL)
|
||||
PROTOCOL_IS_CQL="true"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Parse port from config files based on protocol
|
||||
CONNECTION_PORT=""
|
||||
if [ "$PROTOCOL_IS_CQL" = "true" ]; then
|
||||
# For CQL, parse native_transport_management_port from cassandra.yaml
|
||||
if [ -f "$CASSANDRA_CONF/cassandra.yaml" ]; then
|
||||
JMX_PORT=$(grep jmx_port "$CASSANDRA_CONF/cassandra.yaml" | cut -d ':' -f 2 | tr -d '[[:space:]]')
|
||||
CONNECTION_PORT=$(grep native_transport_management_port "$CASSANDRA_CONF/cassandra.yaml" | cut -d ':' -f 2 | tr -d '[[:space:]]')
|
||||
fi
|
||||
else
|
||||
# For JMX, parse port from configure_jmx method call, when not commented out
|
||||
if [ -f "$CASSANDRA_CONF/cassandra-env.sh" ]; then
|
||||
jmx_method_call=$(grep "^configure_jmx \+[0-9]\+$" "$CASSANDRA_CONF/cassandra-env.sh")
|
||||
if [ ! "x${jmx_method_call}" = "x" ]; then
|
||||
CONNECTION_PORT=$(echo "${jmx_method_call}" | tr -s " " | cut -d " " -f2)
|
||||
fi
|
||||
fi
|
||||
|
||||
# In case CONNECTION_PORT is not set (when configure_jmx in cassandra-env.sh is commented out),
|
||||
# try to parse it from cassandra.yaml.
|
||||
if [ "x$CONNECTION_PORT" = "x" ]; then
|
||||
if [ -f "$CASSANDRA_CONF/cassandra.yaml" ]; then
|
||||
CONNECTION_PORT=$(grep jmx_port "$CASSANDRA_CONF/cassandra.yaml" | cut -d ':' -f 2 | tr -d '[[:space:]]')
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# If, by any chance, it is not there either, set it to default.
|
||||
if [ "x$JMX_PORT" = "x" ]; then
|
||||
JMX_PORT=7199
|
||||
fi
|
||||
|
||||
# JMX Port passed via cmd line args (-p 9999 / --port 9999 / --port=9999)
|
||||
# Connection Port passed via cmd line args (-p 9999 / --port 9999 / --port=9999)
|
||||
# should override the value from cassandra-env.sh
|
||||
ARGS=""
|
||||
JVM_ARGS=""
|
||||
|
|
@ -76,14 +97,14 @@ do
|
|||
if [ ! $1 ]; then break; fi
|
||||
case $1 in
|
||||
-p)
|
||||
JMX_PORT=$2
|
||||
CONNECTION_PORT=$2
|
||||
shift
|
||||
;;
|
||||
--port=*)
|
||||
JMX_PORT=$(echo $1 | cut -d '=' -f 2)
|
||||
CONNECTION_PORT=$(echo $1 | cut -d '=' -f 2)
|
||||
;;
|
||||
--port)
|
||||
JMX_PORT=$2
|
||||
CONNECTION_PORT=$2
|
||||
shift
|
||||
;;
|
||||
--ssl)
|
||||
|
|
@ -108,6 +129,14 @@ do
|
|||
shift
|
||||
done
|
||||
|
||||
if [ "x$CONNECTION_PORT" = "x" ]; then
|
||||
if [ "$PROTOCOL_IS_CQL" = "true" ]; then
|
||||
CONNECTION_PORT=11211
|
||||
else
|
||||
CONNECTION_PORT=7199
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "x$MAX_HEAP_SIZE" = "x" ]; then
|
||||
MAX_HEAP_SIZE="128m"
|
||||
fi
|
||||
|
|
@ -123,7 +152,7 @@ CMD=$(echo "$JAVA" $JAVA_AGENT -ea -cp "$CLASSPATH" $JVM_OPTS -Xmx$MAX_HEAP_SIZE
|
|||
-Dcassandra.logdir="$CASSANDRA_LOG_DIR" \
|
||||
-Dlogback.configurationFile=logback-tools.xml \
|
||||
$JVM_ARGS \
|
||||
org.apache.cassandra.tools.NodeTool -p $JMX_PORT $ARGS)
|
||||
org.apache.cassandra.tools.NodeTool -p $CONNECTION_PORT $ARGS)
|
||||
|
||||
if [ "x$ARCHIVE_COMMAND" != "x" ]
|
||||
then
|
||||
|
|
|
|||
|
|
@ -1134,6 +1134,57 @@ rpc_address: localhost
|
|||
# enable or disable keepalive on rpc/native connections
|
||||
rpc_keepalive: true
|
||||
|
||||
# Whether to start the native management transport server.
|
||||
# The address on which the native management transport is bound can be configured
|
||||
# using rpc_management_address or rpc_management_interface. If neither is set,
|
||||
# it defaults to rpc_address (or rpc_interface if that was used).
|
||||
start_native_transport_management: false
|
||||
|
||||
# The address or interface to bind the native management transport server to.
|
||||
# This allows you to bind management operations to a different network interface
|
||||
# than regular client connections, providing better security isolation.
|
||||
#
|
||||
# Set rpc_management_address OR rpc_management_interface, not both.
|
||||
#
|
||||
# If neither rpc_management_address nor rpc_management_interface is set,
|
||||
# the management transport will use the same address as the regular native
|
||||
# transport (rpc_address or rpc_interface).
|
||||
#
|
||||
# For security reasons, you should not expose this port to the internet.
|
||||
# Firewall it if needed. Consider binding to a management network interface
|
||||
# that is not accessible from public networks.
|
||||
# rpc_management_address: localhost
|
||||
|
||||
# Set rpc_management_address OR rpc_management_interface, not both. Interfaces
|
||||
# must correspond to a single address, IP aliasing is not supported.
|
||||
#
|
||||
# This allows you to bind the management transport to a specific network
|
||||
# interface by name (e.g., eth1, eth0). The IP address will be automatically
|
||||
# resolved from the interface. This is useful when the interface IP may change
|
||||
# (e.g., DHCP) or when you want to bind to a specific interface without
|
||||
# hardcoding the IP address.
|
||||
# rpc_management_interface: eth1
|
||||
|
||||
# If you choose to specify the management interface by name and the interface
|
||||
# has both an ipv4 and an ipv6 address, you can specify which should be chosen
|
||||
# using rpc_management_interface_prefer_ipv6. If false the first ipv4 address
|
||||
# will be used. If true the first ipv6 address will be used. Defaults to false
|
||||
# preferring ipv4. If there is only one address it will be selected regardless
|
||||
# of ipv4/ipv6.
|
||||
# rpc_management_interface_prefer_ipv6: false
|
||||
|
||||
# Port for the management CQL native transport to listen for clients on.
|
||||
# For security reasons, you should not expose this port to the internet.
|
||||
# Firewall it if needed.
|
||||
native_transport_management_port: 11211
|
||||
|
||||
# The maximum threads for handling management requests are set
|
||||
# separately from regular requests to allow better isolation and
|
||||
# prioritization of management operations. This is important to
|
||||
# ensure that management operations can proceed even under a high
|
||||
# load of regular client requests. Defaults to 2.
|
||||
# native_transport_management_max_threads: 2
|
||||
|
||||
# Uncomment to set socket buffer size for internode communication
|
||||
# Note that when setting this, the buffer size is limited by net.core.wmem_max
|
||||
# and when not setting it it is defined by net.ipv4.tcp_wmem
|
||||
|
|
|
|||
|
|
@ -1119,6 +1119,57 @@ rpc_address: localhost
|
|||
# enable or disable keepalive on rpc/native connections
|
||||
rpc_keepalive: true
|
||||
|
||||
# Whether to start the native management transport server.
|
||||
# The address on which the native management transport is bound can be configured
|
||||
# using rpc_management_address or rpc_management_interface. If neither is set,
|
||||
# it defaults to rpc_address (or rpc_interface if that was used).
|
||||
start_native_transport_management: false
|
||||
|
||||
# The address or interface to bind the native management transport server to.
|
||||
# This allows you to bind management operations to a different network interface
|
||||
# than regular client connections, providing better security isolation.
|
||||
#
|
||||
# Set rpc_management_address OR rpc_management_interface, not both.
|
||||
#
|
||||
# If neither rpc_management_address nor rpc_management_interface is set,
|
||||
# the management transport will use the same address as the regular native
|
||||
# transport (rpc_address or rpc_interface).
|
||||
#
|
||||
# For security reasons, you should not expose this port to the internet.
|
||||
# Firewall it if needed. Consider binding to a management network interface
|
||||
# that is not accessible from public networks.
|
||||
# rpc_management_address: localhost
|
||||
|
||||
# Set rpc_management_address OR rpc_management_interface, not both. Interfaces
|
||||
# must correspond to a single address, IP aliasing is not supported.
|
||||
#
|
||||
# This allows you to bind the management transport to a specific network
|
||||
# interface by name (e.g., eth1, eth0). The IP address will be automatically
|
||||
# resolved from the interface. This is useful when the interface IP may change
|
||||
# (e.g., DHCP) or when you want to bind to a specific interface without
|
||||
# hardcoding the IP address.
|
||||
# rpc_management_interface: eth1
|
||||
|
||||
# If you choose to specify the management interface by name and the interface
|
||||
# has both an ipv4 and an ipv6 address, you can specify which should be chosen
|
||||
# using rpc_management_interface_prefer_ipv6. If false the first ipv4 address
|
||||
# will be used. If true the first ipv6 address will be used. Defaults to false
|
||||
# preferring ipv4. If there is only one address it will be selected regardless
|
||||
# of ipv4/ipv6.
|
||||
# rpc_management_interface_prefer_ipv6: false
|
||||
|
||||
# Port for the management CQL native transport to listen for clients on.
|
||||
# For security reasons, you should not expose this port to the internet.
|
||||
# Firewall it if needed.
|
||||
native_transport_management_port: 11211
|
||||
|
||||
# The maximum threads for handling management requests are set
|
||||
# separately from regular requests to allow better isolation and
|
||||
# prioritization of management operations. This is important to
|
||||
# ensure that management operations can proceed even under a high
|
||||
# load of regular client requests. Defaults to 2.
|
||||
# native_transport_management_max_threads: 2
|
||||
|
||||
# Uncomment to set socket buffer size for internode communication
|
||||
# Note that when setting this, the buffer size is limited by net.core.wmem_max
|
||||
# and when not setting it it is defined by net.ipv4.tcp_wmem
|
||||
|
|
|
|||
|
|
@ -1498,6 +1498,15 @@ Table of Contents
|
|||
acknowledged the request.
|
||||
<blockfor> is an [int] representing the number of replicas whose
|
||||
acknowledgement is required to achieve <cl>.
|
||||
0x1800 COMMAND_FAILED: An exception occurred during command execution. This error is returned
|
||||
when a command executed via the native management interface fails during execution.
|
||||
The exception may or may not include more detail in the accompanying error message.
|
||||
The rest of the ERROR message body will be
|
||||
<execution_id>
|
||||
where:
|
||||
<execution_id> is a [uuid] representing the unique identifier for the command
|
||||
execution that failed. This identifier can be used to correlate the
|
||||
error with command execution logs on the server.
|
||||
|
||||
0x2000 Syntax_error: The submitted query has a syntax error.
|
||||
0x2100 Unauthorized: The logged user doesn't have the right to perform
|
||||
|
|
|
|||
|
|
@ -148,6 +148,8 @@ K_MODIFY: M O D I F Y;
|
|||
K_AUTHORIZE: A U T H O R I Z E;
|
||||
K_DESCRIBE: D E S C R I B E;
|
||||
K_EXECUTE: E X E C U T E;
|
||||
K_COMMAND: C O M M A N D;
|
||||
K_INVOKE: I N V O K E;
|
||||
K_NORECURSIVE: N O R E C U R S I V E;
|
||||
K_MBEAN: M B E A N;
|
||||
K_MBEANS: M B E A N S;
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ cqlStatement returns [CQLStatement.Raw stmt]
|
|||
| st55=securityLabelOnUserTypeStatement { $stmt = st55; }
|
||||
| st56=commentOnUserTypeFieldStatement { $stmt = st56; }
|
||||
| st57=securityLabelOnUserTypeFieldStatement { $stmt = st57; }
|
||||
| st58=executeCommandStatement { $stmt = st58; }
|
||||
;
|
||||
|
||||
/*
|
||||
|
|
@ -1370,6 +1371,53 @@ securityLabelOnUserTypeFieldStatement returns [SecurityLabelOnUserTypeFieldState
|
|||
{ $stmt = new SecurityLabelOnUserTypeFieldStatement.Raw($typeFieldRef.typeName, $typeFieldRef.field, label != null ? $label.text : null, provider); }
|
||||
;
|
||||
|
||||
/**
|
||||
* INVOKE COMMAND <commandName> [WITH key1 = value1 AND key2 = value2];
|
||||
*/
|
||||
executeCommandStatement returns [ExecuteCommandStatement.Raw stmt]
|
||||
@init {
|
||||
stmtBegins();
|
||||
java.util.Map<String, Object> args = new java.util.LinkedHashMap<>();
|
||||
}
|
||||
: K_INVOKE K_COMMAND cmdName=noncol_ident
|
||||
(K_WITH commandProperties[args])?
|
||||
{
|
||||
$stmt = new ExecuteCommandStatement.Raw(cmdName.toString(), args);
|
||||
}
|
||||
;
|
||||
|
||||
commandProperties[java.util.Map<String, Object> args]
|
||||
: commandProperty[args] (K_AND commandProperty[args])*
|
||||
;
|
||||
|
||||
commandProperty[java.util.Map<String, Object> args]
|
||||
: k=noncol_ident '=' v=commandPropertyValue
|
||||
{
|
||||
String key = k.toString();
|
||||
Object value = v;
|
||||
if (args.put(key, value) != null)
|
||||
{
|
||||
addRecognitionError("Duplicate argument: " + key);
|
||||
}
|
||||
}
|
||||
;
|
||||
|
||||
commandPropertyValue returns [Object value]
|
||||
: s=STRING_LITERAL { $value = $s.text; }
|
||||
| i=INTEGER { $value = $i.text; }
|
||||
| f=FLOAT { $value = $f.text; }
|
||||
| b=BOOLEAN { $value = $b.text; }
|
||||
| l=commandListValue { $value = l; }
|
||||
;
|
||||
|
||||
commandListValue returns [java.util.List<String> value]
|
||||
@init { java.util.List<String> l = new java.util.ArrayList<>(); }
|
||||
: '[' ( s1=STRING_LITERAL { l.add($s1.text); }
|
||||
( ',' sn=STRING_LITERAL { l.add($sn.text); } )*
|
||||
)? ']'
|
||||
{ $value = l; }
|
||||
;
|
||||
|
||||
/**
|
||||
* DROP TABLE [IF EXISTS] <table>;
|
||||
*/
|
||||
|
|
@ -1980,7 +2028,7 @@ collectionLiteral returns [Term.Raw value]
|
|||
|
||||
listLiteral returns [Term.Raw value]
|
||||
@init {List<Term.Raw> l = new ArrayList<Term.Raw>();}
|
||||
@after {$value = new ArrayLiteral(l);}
|
||||
@after {$value = new ArrayLiteral(l);}
|
||||
: '[' ( t1=term { l.add(t1); } ( ',' tn=term { l.add(tn); } )* )? ']' { $value = new ArrayLiteral(l); }
|
||||
;
|
||||
|
||||
|
|
@ -2483,5 +2531,7 @@ basic_unreserved_keyword returns [String str]
|
|||
| K_LABELS
|
||||
| K_FIELD
|
||||
| K_COLUMN
|
||||
| K_COMMAND
|
||||
| K_INVOKE
|
||||
) { $str = $k.text; }
|
||||
;
|
||||
|
|
|
|||
|
|
@ -82,7 +82,8 @@ public enum AuditLogEntryType
|
|||
UNAUTHORIZED_ATTEMPT(AuditLogEntryCategory.AUTH),
|
||||
LOGIN_SUCCESS(AuditLogEntryCategory.AUTH),
|
||||
LIST_SUPERUSERS(AuditLogEntryCategory.DCL),
|
||||
JMX(AuditLogEntryCategory.JMX);
|
||||
JMX(AuditLogEntryCategory.JMX),
|
||||
EXECUTE_COMMAND(AuditLogEntryCategory.OTHER);
|
||||
|
||||
private final AuditLogEntryCategory category;
|
||||
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ import org.apache.cassandra.metrics.CIDRAuthorizerMetrics;
|
|||
*/
|
||||
public abstract class AbstractCIDRAuthorizer implements ICIDRAuthorizer
|
||||
{
|
||||
protected static CIDRPermissionsManager cidrPermissionsManager;
|
||||
protected static CIDRGroupsMappingManager cidrGroupsMappingManager;
|
||||
public static CIDRPermissionsManager cidrPermissionsManager;
|
||||
public static CIDRGroupsMappingManager cidrGroupsMappingManager;
|
||||
|
||||
protected static CIDRAuthorizerMetrics cidrAuthorizerMetrics;
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ import com.google.common.util.concurrent.Uninterruptibles;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
|
||||
import org.apache.cassandra.cache.UnweightedCacheSize;
|
||||
import org.apache.cassandra.concurrent.ExecutorPlus;
|
||||
import org.apache.cassandra.concurrent.ScheduledExecutors;
|
||||
|
|
@ -468,6 +469,36 @@ public class AuthCache<K, V> implements AuthCacheMBean, UnweightedCacheSize, Shu
|
|||
return Ints.checkedCast(getEstimatedSize());
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts a visitor for this cache instance. Subclasses should override this method to dispatch
|
||||
* to the appropriate visitor method based on the specific MBean interface they implement.
|
||||
* @param visitor the visitor to accept
|
||||
*/
|
||||
public void accept(MBeanVisitor visitor)
|
||||
{
|
||||
visitor.visit(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Visitor interface for processing auth cache MBeans.
|
||||
* Allows type-safe iteration over different cache types without instanceof checks.
|
||||
*/
|
||||
public interface MBeanVisitor
|
||||
{
|
||||
/** Visits a credentials cache MBean. */
|
||||
default void visitCredentials(PasswordAuthenticator.CredentialsCacheMBean cache) {}
|
||||
/** Visits a JMX permissions cache MBean. */
|
||||
default void visitJmxPermissions(AuthorizationProxy.JmxPermissionsCacheMBean cache) {}
|
||||
/** Visits a permissions cache MBean. */
|
||||
default void visitPermissions(PermissionsCacheMBean cache) {}
|
||||
/** Visits a network permissions cache MBean. */
|
||||
default void visitNetwork(NetworkPermissionsCacheMBean cache) {}
|
||||
/** Visits a roles cache MBean. */
|
||||
default void visitRoles(RolesCacheMBean cache) {}
|
||||
/** Visits a generic auth cache (fallback for caches that don't implement specific MBean interfaces).*/
|
||||
default void visit(AuthCacheMBean cache) {}
|
||||
}
|
||||
|
||||
private class MetricsUpdater implements StatsCounter
|
||||
{
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import javax.annotation.concurrent.ThreadSafe;
|
|||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
|
@ -60,6 +61,11 @@ public class AuthCacheService
|
|||
}
|
||||
}
|
||||
|
||||
public synchronized Set<AuthCache<?, ?>> getCaches()
|
||||
{
|
||||
return Sets.newHashSet(caches);
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: Can only be called once per instance run.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -52,4 +52,10 @@ public class NetworkPermissionsCache extends AuthCache<RoleResource, DCPermissio
|
|||
super.unregisterMBean();
|
||||
MBeanWrapper.instance.unregisterMBean(MBEAN_NAME_BASE + DEPRECATED_CACHE_NAME, MBeanWrapper.OnException.LOG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(MBeanVisitor visitor)
|
||||
{
|
||||
visitor.visitNetwork(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -384,6 +384,12 @@ public class PasswordAuthenticator implements IAuthenticator, AuthCache.BulkLoad
|
|||
{
|
||||
invalidate(roleName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(MBeanVisitor visitor)
|
||||
{
|
||||
visitor.visitCredentials(this);
|
||||
}
|
||||
}
|
||||
|
||||
public static interface CredentialsCacheMBean extends AuthCacheMBean
|
||||
|
|
|
|||
|
|
@ -50,4 +50,10 @@ public class PermissionsCache extends AuthCache<Pair<AuthenticatedUser, IResourc
|
|||
{
|
||||
invalidate(Pair.create(new AuthenticatedUser(roleName), Resources.fromName(resourceName)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(MBeanVisitor visitor)
|
||||
{
|
||||
visitor.visitPermissions(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,4 +82,10 @@ public class RolesCache extends AuthCache<RoleResource, Set<Role>> implements Ro
|
|||
{
|
||||
invalidate(RoleResource.role(roleName));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(MBeanVisitor visitor)
|
||||
{
|
||||
visitor.visitRoles(this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import org.slf4j.LoggerFactory;
|
|||
import org.apache.cassandra.audit.AuditLogManager;
|
||||
import org.apache.cassandra.auth.AuthCache;
|
||||
import org.apache.cassandra.auth.AuthCacheMBean;
|
||||
import org.apache.cassandra.auth.AuthCacheService;
|
||||
import org.apache.cassandra.auth.AuthenticatedUser;
|
||||
import org.apache.cassandra.auth.JMXResource;
|
||||
import org.apache.cassandra.auth.Permission;
|
||||
|
|
@ -596,6 +597,8 @@ public class AuthorizationProxy implements InvocationHandler
|
|||
() -> true);
|
||||
|
||||
MBeanWrapper.instance.registerMBean(this, MBEAN_NAME_BASE + DEPRECATED_CACHE_NAME);
|
||||
// Registration makes this cache discovearble by the management transport MBean accessor
|
||||
AuthCacheService.instance.register(this);
|
||||
}
|
||||
|
||||
public void invalidatePermissions(String roleName)
|
||||
|
|
@ -609,6 +612,12 @@ public class AuthorizationProxy implements InvocationHandler
|
|||
super.unregisterMBean();
|
||||
MBeanWrapper.instance.unregisterMBean(MBEAN_NAME_BASE + DEPRECATED_CACHE_NAME, MBeanWrapper.OnException.LOG);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(MBeanVisitor visitor)
|
||||
{
|
||||
visitor.visitJmxPermissions(this);
|
||||
}
|
||||
}
|
||||
|
||||
public static interface JmxPermissionsCacheMBean extends AuthCacheMBean
|
||||
|
|
|
|||
|
|
@ -36,6 +36,16 @@ public enum CassandraRelevantEnv
|
|||
JAVA_HOME ("JAVA_HOME"),
|
||||
CIRCLECI("CIRCLECI"),
|
||||
CASSANDRA_SKIP_SYNC("CASSANDRA_SKIP_SYNC"),
|
||||
/**
|
||||
* Defines the protocol used by the Cassandra CLI to connect to Cassandra nodes.
|
||||
* Possible values are {@code "static_mbeans"}, {@code "command_mbeans"} or {@code cql}.
|
||||
* By default, the Cassandra CLI uses the JMX protocol via static MBeans.
|
||||
*/
|
||||
CASSANDRA_CLI_EXECUTION_PROTOCOL("CASSANDRA_CLI_EXECUTION_PROTOCOL"),
|
||||
/** Whether to show the execution id for cli command output, useful for correlating commands with logs and tracing. */
|
||||
CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID("CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID"),
|
||||
/** How long the Cassandra CLI waits for a server response when executing a command over CQL, in seconds. */
|
||||
CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS("CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS"),
|
||||
/** By default, the standard Cassandra CLI layout is used for backward compatibility, however,
|
||||
* the new Picocli layout can be enabled by setting this property to the {@code "picocli"}. */
|
||||
CASSANDRA_CLI_LAYOUT("CASSANDRA_CLI_LAYOUT"),
|
||||
|
|
|
|||
|
|
@ -97,6 +97,16 @@ public enum CassandraRelevantProperties
|
|||
CACHEABLE_MUTATION_SIZE_LIMIT("cassandra.cacheable_mutation_size_limit_bytes", convertToString(1_000_000)),
|
||||
CASSANDRA_ALLOW_SIMPLE_STRATEGY("cassandra.allow_simplestrategy"),
|
||||
CASSANDRA_AVAILABLE_PROCESSORS("cassandra.available_processors"),
|
||||
/**
|
||||
* Defines the protocol used by the Cassandra CLI to connect to Cassandra nodes.
|
||||
* Possible values are {@code "static_mbean"}, {@code "command_mbean"} or {@code cql}.
|
||||
* By default, the Cassandra CLI uses the JMX protocol via static MBeans.
|
||||
*/
|
||||
CASSANDRA_CLI_EXECUTION_PROTOCOL("cassandra.cli.execution.protocol", "static_mbean"),
|
||||
/** Whether to show the execution id for cli command output, useful for correlating commands with logs and tracing. */
|
||||
CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID("cassandra.cli.execution.show_execution_id", "false"),
|
||||
/** How long the Cassandra CLI waits for a server response when executing a command over CQL, in seconds. */
|
||||
CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS("cassandra.cli.execution.timeout_seconds", "0"),
|
||||
/** By default, the standard Cassandra CLI layout is used for backward compatibility, however,
|
||||
* the new Picocli layout can be enabled by setting this property to the {@code "picocli"}. */
|
||||
CASSANDRA_CLI_LAYOUT("cassandra.cli.layout", "airline"),
|
||||
|
|
@ -419,6 +429,8 @@ public enum CassandraRelevantProperties
|
|||
MX4JPORT("mx4jport"),
|
||||
NANOTIMETOMILLIS_TIMESTAMP_UPDATE_INTERVAL("cassandra.NANOTIMETOMILLIS_TIMESTAMP_UPDATE_INTERVAL", "10000"),
|
||||
NATIVE_EPOLL_ENABLED("cassandra.native.epoll.enabled", "true"),
|
||||
/** This is the port used with RPC address for the management native protocol to communicate with clients that manage the node. */
|
||||
NATIVE_TRANSPORT_MANAGEMENT_PORT("cassandra.native_transport_management_port"),
|
||||
/** This is the port used with RPC address for the native protocol to communicate with clients. Now that thrift RPC is no longer in use there is no RPC port. */
|
||||
NATIVE_TRANSPORT_PORT("cassandra.native_transport_port"),
|
||||
NEVER_PURGE_TOMBSTONES("cassandra.never_purge_tombstones"),
|
||||
|
|
@ -580,6 +592,7 @@ public enum CassandraRelevantProperties
|
|||
SSL_ENABLE("ssl.enable"),
|
||||
SSL_STORAGE_PORT("cassandra.ssl_storage_port"),
|
||||
START_GOSSIP("cassandra.start_gossip", "true"),
|
||||
START_NATIVE_MANAGEMENT_TRANSPORT("cassandra.start_native_management_transport"),
|
||||
START_NATIVE_TRANSPORT("cassandra.start_native_transport"),
|
||||
STORAGE_DIR("cassandra.storagedir"),
|
||||
STORAGE_HOOK("cassandra.storage_hook"),
|
||||
|
|
|
|||
|
|
@ -370,6 +370,20 @@ public class Config
|
|||
@Replaces(oldName = "native_transport_receive_queue_capacity_in_bytes", converter = Converters.BYTES_DATASTORAGE, deprecated = true)
|
||||
public DataStorageSpec.IntBytesBound native_transport_receive_queue_capacity = new DataStorageSpec.IntBytesBound("1MiB");
|
||||
|
||||
/**
|
||||
* Management RPC address and interface refer to the address/interface used for the native management protocol
|
||||
* to communicate with management clients. If not explicitly configured, these default to the regular RPC address
|
||||
* configuration (rpc_address or rpc_interface).
|
||||
* <p>
|
||||
* native_transport_management_port is the port paired with the management RPC address to bind on.
|
||||
*/
|
||||
public String rpc_management_address;
|
||||
public String rpc_management_interface;
|
||||
public boolean rpc_management_interface_prefer_ipv6 = false;
|
||||
public boolean start_native_transport_management = false;
|
||||
public int native_transport_management_port = 11211;
|
||||
public int native_transport_management_max_threads = 2;
|
||||
|
||||
/**
|
||||
* Max size of values in SSTables, in MebiBytes.
|
||||
* Default is the same as the native protocol frame limit: 256MiB.
|
||||
|
|
|
|||
|
|
@ -148,6 +148,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.CONFIG_LOA
|
|||
import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_STCS_IN_L0;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.INITIAL_TOKEN;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.IO_NETTY_TRANSPORT_ESTIMATE_SIZE_ON_SUBMIT;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.NATIVE_TRANSPORT_MANAGEMENT_PORT;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.NATIVE_TRANSPORT_PORT;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.OS_ARCH;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.PARTITIONER;
|
||||
|
|
@ -216,6 +217,7 @@ public class DatabaseDescriptor
|
|||
private static InetAddress broadcastAddress;
|
||||
private static InetAddress rpcAddress;
|
||||
private static InetAddress broadcastRpcAddress;
|
||||
private static InetAddress rpcManagementAddress;
|
||||
private static SeedProvider seedProvider;
|
||||
private static IInternodeAuthenticator internodeAuthenticator = new AllowAllInternodeAuthenticator();
|
||||
|
||||
|
|
@ -1494,6 +1496,7 @@ public class DatabaseDescriptor
|
|||
rpcAddress = null;
|
||||
broadcastAddress = null;
|
||||
broadcastRpcAddress = null;
|
||||
rpcManagementAddress = null;
|
||||
|
||||
/* Local IP, hostname or interface to bind services to */
|
||||
if (config.listen_address != null && config.listen_interface != null)
|
||||
|
|
@ -1581,6 +1584,32 @@ public class DatabaseDescriptor
|
|||
throw new ConfigurationException("If rpc_address is set to a wildcard address (" + config.rpc_address + "), then " +
|
||||
"you must set broadcast_rpc_address to a value other than " + config.rpc_address, false);
|
||||
}
|
||||
|
||||
/* Local IP, hostname or interface to bind Management RPC server to */
|
||||
if (config.rpc_management_address != null && config.rpc_management_interface != null)
|
||||
{
|
||||
throw new ConfigurationException("Set rpc_management_address OR rpc_management_interface, not both", false);
|
||||
}
|
||||
else if (config.rpc_management_address != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
rpcManagementAddress = InetAddress.getByName(config.rpc_management_address);
|
||||
}
|
||||
catch (UnknownHostException e)
|
||||
{
|
||||
throw new ConfigurationException("Unknown host in rpc_management_address " + config.rpc_management_address, false);
|
||||
}
|
||||
}
|
||||
else if (config.rpc_management_interface != null)
|
||||
{
|
||||
rpcManagementAddress = getNetworkInterfaceAddress(config.rpc_management_interface, "rpc_management_interface", config.rpc_management_interface_prefer_ipv6);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default to regular rpc_address if not specified
|
||||
rpcManagementAddress = rpcAddress;
|
||||
}
|
||||
}
|
||||
|
||||
public static void applyEncryptionContext()
|
||||
|
|
@ -3606,6 +3635,18 @@ public class DatabaseDescriptor
|
|||
return rpcAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the address used to bind for the native management protocol to communicate with management clients.
|
||||
* If not explicitly configured via rpc_management_address or rpc_management_interface, defaults to the regular
|
||||
* rpc_address. The address alone is not enough to uniquely identify this instance because multiple instances
|
||||
* might use the same interface with different ports.
|
||||
*/
|
||||
public static InetAddress getRpcManagementAddress()
|
||||
{
|
||||
assert rpcManagementAddress != null;
|
||||
return rpcManagementAddress;
|
||||
}
|
||||
|
||||
public static void setBroadcastRpcAddress(InetAddress broadcastRPCAddr)
|
||||
{
|
||||
broadcastRpcAddress = broadcastRPCAddr;
|
||||
|
|
@ -3850,6 +3891,38 @@ public class DatabaseDescriptor
|
|||
);
|
||||
}
|
||||
|
||||
public static boolean startNativeTransportManagement()
|
||||
{
|
||||
return conf.start_native_transport_management;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setStartNativeTransportManagement(boolean start)
|
||||
{
|
||||
conf.start_native_transport_management = start;
|
||||
}
|
||||
|
||||
public static int getNativeTransportManagementPort()
|
||||
{
|
||||
return NATIVE_TRANSPORT_MANAGEMENT_PORT.getInt(conf.native_transport_management_port);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void setNativeTransportPortManagement(int port)
|
||||
{
|
||||
conf.native_transport_management_port = port;
|
||||
}
|
||||
|
||||
public static int getNativeTransportManagementMaxThreads()
|
||||
{
|
||||
return conf.native_transport_management_max_threads;
|
||||
}
|
||||
|
||||
public static void setNativeTransportManagementMaxThreads(int max_threads)
|
||||
{
|
||||
conf.native_transport_management_max_threads = max_threads;
|
||||
}
|
||||
|
||||
public static Config.PaxosVariant getPaxosVariant()
|
||||
{
|
||||
return conf.paxos_variant;
|
||||
|
|
|
|||
|
|
@ -20,12 +20,14 @@ package org.apache.cassandra.config;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
|
|
@ -49,7 +51,6 @@ import org.apache.cassandra.service.disk.usage.DiskUsageMonitor;
|
|||
import org.apache.cassandra.utils.LocalizeString;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static java.util.stream.Collectors.toSet;
|
||||
|
||||
/**
|
||||
* Configuration settings for guardrails populated from the Yaml file.
|
||||
|
|
@ -1581,12 +1582,24 @@ public class GuardrailsOptions implements GuardrailsConfig
|
|||
"than the fail threshold %s", warn, name, fail));
|
||||
}
|
||||
|
||||
/**
|
||||
* See {@code GuardrailsConfigCommandTest}, that relies on the fact that the error messages for invalid properties
|
||||
* are deterministic and contain the list of invalid properties in the same order as they were provided in the command.
|
||||
*
|
||||
* @param properties set of properties to validate.
|
||||
* @param name name of the property for error messages.
|
||||
* @return set of properties converted to lower case.
|
||||
*/
|
||||
private static Set<String> validateTableProperties(Set<String> properties, String name)
|
||||
{
|
||||
if (properties == null)
|
||||
throw new IllegalArgumentException(format("Invalid value for %s: null is not allowed", name));
|
||||
|
||||
Set<String> lowerCaseProperties = properties.stream().map(String::toLowerCase).collect(toSet());
|
||||
// We use LinkedHashSet to preserve the order of properties in error messages. This is
|
||||
// used to avoid confusion in an error message when the CQL/nodetool commands are invoked
|
||||
// with properties in a specific order.
|
||||
Set<String> lowerCaseProperties = properties.stream().map(LocalizeString::toLowerCaseLocalized)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
|
||||
Set<String> diff = Sets.difference(lowerCaseProperties, TableAttributes.allKeywords());
|
||||
|
||||
|
|
@ -1601,7 +1614,11 @@ public class GuardrailsOptions implements GuardrailsConfig
|
|||
if (properties == null)
|
||||
throw new IllegalArgumentException(format("Invalid value for %s: null is not allowed", name));
|
||||
|
||||
Set<String> lowerCaseProperties = properties.stream().map(LocalizeString::toLowerCaseLocalized).collect(toSet());
|
||||
// We use LinkedHashSet to preserve the order of properties in error messages. This is
|
||||
// used to avoid confusion in an error message when the CQL/nodetool commands are invoked
|
||||
// with properties in a specific order.
|
||||
Set<String> lowerCaseProperties = properties.stream().map(LocalizeString::toLowerCaseLocalized)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
|
||||
for (String requiredKeyword : KeyspaceAttributes.requiredKeywords())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.cql3.statements;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.cassandra.audit.AuditLogContext;
|
||||
import org.apache.cassandra.audit.AuditLogEntryType;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQLStatement;
|
||||
import org.apache.cassandra.cql3.ColumnIdentifier;
|
||||
import org.apache.cassandra.cql3.ColumnSpecification;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.cql3.ResultSet;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.db.marshal.UUIDType;
|
||||
import org.apache.cassandra.exceptions.CommandRequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.management.CommandAuthorizationException;
|
||||
import org.apache.cassandra.management.CommandExecutionArgsSerde;
|
||||
import org.apache.cassandra.management.CommandExecutionException;
|
||||
import org.apache.cassandra.management.CommandInvokerService;
|
||||
import org.apache.cassandra.management.CommandValidationException;
|
||||
import org.apache.cassandra.management.api.Command;
|
||||
import org.apache.cassandra.service.ClientState;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.Dispatcher;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
import static org.apache.cassandra.management.ManagementUtils.causeMessages;
|
||||
import static org.apache.cassandra.management.ManagementUtils.findRegistryCommand;
|
||||
import static org.apache.cassandra.utils.ByteBufferUtil.bytes;
|
||||
|
||||
public class ExecuteCommandStatement
|
||||
{
|
||||
public static final String COMMAND_RESULT_SCHEMA_EXECUTION_ID = "execution_id";
|
||||
public static final String COMMAND_RESULT_SCHEMA_OUTPUT = "output";
|
||||
|
||||
public static class Raw extends CQLStatement.Raw implements CQLStatement
|
||||
{
|
||||
private final String commandName;
|
||||
private final Map<String, Object> args;
|
||||
|
||||
public Raw(String commandName, Map<String, Object> args)
|
||||
{
|
||||
this.commandName = commandName;
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
public String commandName()
|
||||
{
|
||||
return commandName;
|
||||
}
|
||||
|
||||
public Map<String, Object> args()
|
||||
{
|
||||
return args;
|
||||
}
|
||||
|
||||
public CQLStatement prepare(ClientState state)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
public void authorize(ClientState state) throws UnauthorizedException
|
||||
{
|
||||
// TODO: CASSANDRA-XXXXX Restrict command execution to deployments without authentication
|
||||
// This is a temporary limitation until full authentication support is implemented.
|
||||
if (DatabaseDescriptor.getAuthenticator().requireAuthentication())
|
||||
{
|
||||
throw new UnauthorizedException("Command execution via management port is currently only supported " +
|
||||
"when authentication is disabled (AllowAllAuthenticator). " +
|
||||
"Full authentication and authorization support will be added in a " +
|
||||
"future release.");
|
||||
}
|
||||
|
||||
// Validate login (will succeed with AllowAllAuthenticator)
|
||||
state.validateLogin();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validate(ClientState state) throws InvalidRequestException
|
||||
{
|
||||
Command<?> command = findRegistryCommand(commandName, CommandInvokerService.instance.getRegistry());
|
||||
if (command == null)
|
||||
throw new InvalidRequestException("Command not found: " + commandName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultMessage execute(QueryState state, QueryOptions options, Dispatcher.RequestTime requestTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
ClientState clientState = state.getClientState();
|
||||
if (!clientState.isInternal && !clientState.isManagement())
|
||||
throw new InvalidRequestException("Command execution is only allowed via native management interface");
|
||||
|
||||
Command<?> command = findRegistryCommand(commandName, CommandInvokerService.instance.getRegistry());
|
||||
if (command == null)
|
||||
throw new InvalidRequestException("Command not found: " + commandName);
|
||||
|
||||
CommandInvokerService.CommandResult result = CommandInvokerService.instance
|
||||
.invokeCommand(commandName,
|
||||
() -> CommandExecutionArgsSerde.fromMap(args, command.metadata()));
|
||||
|
||||
ResultSet resultSet = getCommandResultSet();
|
||||
resultSet.addColumnValue(bytes(result.getExecutionId()));
|
||||
resultSet.addColumnValue(bytes(result.getOutput()));
|
||||
return new ResultMessage.Rows(resultSet);
|
||||
}
|
||||
catch (CommandAuthorizationException e)
|
||||
{
|
||||
throw new UnauthorizedException(e.getMessage());
|
||||
}
|
||||
catch (CommandValidationException e)
|
||||
{
|
||||
throw new InvalidRequestException(causeMessages(e), e.getCause());
|
||||
}
|
||||
catch (CommandExecutionException e)
|
||||
{
|
||||
throw new CommandRequestExecutionException(e.getExecutionId(),
|
||||
causeMessages(e),
|
||||
e.getCause());
|
||||
}
|
||||
}
|
||||
|
||||
private static ResultSet getCommandResultSet()
|
||||
{
|
||||
ColumnSpecification executionIdColumn = new ColumnSpecification("system",
|
||||
"command_output",
|
||||
new ColumnIdentifier(COMMAND_RESULT_SCHEMA_EXECUTION_ID, true),
|
||||
UUIDType.instance);
|
||||
ColumnSpecification outputColumn = new ColumnSpecification("system",
|
||||
"command_output",
|
||||
new ColumnIdentifier(COMMAND_RESULT_SCHEMA_OUTPUT, true),
|
||||
UTF8Type.instance);
|
||||
return new ResultSet(new ResultSet.ResultMetadata(List.of(executionIdColumn, outputColumn)));
|
||||
}
|
||||
|
||||
public ResultMessage executeLocally(QueryState state, QueryOptions options) throws InvalidRequestException
|
||||
{
|
||||
return execute(state, options, Dispatcher.RequestTime.forImmediateExecution());
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuditLogContext getAuditLogContext()
|
||||
{
|
||||
return new AuditLogContext(AuditLogEntryType.EXECUTE_COMMAND, commandName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -177,7 +177,8 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
|
|||
// Validate table supports dictionary compression
|
||||
if (!isEnabled)
|
||||
{
|
||||
throw new UnsupportedOperationException("Table " + keyspaceName + '.' + tableName + " does not support dictionary compression");
|
||||
throw new IllegalStateException(format("The compression on table %s.%s is not enabled or SSTable compressor is not a dictionary compressor.",
|
||||
keyspaceName, tableName));
|
||||
}
|
||||
|
||||
// resolve training config and fail fast when invalid, so we do not reach logic which would e.g. flush unnecessarily.
|
||||
|
|
@ -353,11 +354,11 @@ public class CompressionDictionaryManager implements CompressionDictionaryManage
|
|||
if (lastTraining.isAfter(now.minus(config.minTrainingFrequency, ChronoUnit.MINUTES)))
|
||||
{
|
||||
Instant nextEarliestTraining = lastTraining.plus(config.minTrainingFrequency, ChronoUnit.MINUTES);
|
||||
throw new IllegalArgumentException(format("The next training or importing can occur only at least after %s from the last training which happened at %s. " +
|
||||
"You can train again no earlier than at %s.",
|
||||
new DurationSpec.IntMinutesBound(config.minTrainingFrequency, TimeUnit.MINUTES),
|
||||
lastTraining,
|
||||
nextEarliestTraining));
|
||||
throw new RuntimeException(format("The next training or importing can occur only at least after %s from the last training which happened at %s. " +
|
||||
"You can train again no earlier than at %s.",
|
||||
new DurationSpec.IntMinutesBound(config.minTrainingFrequency, TimeUnit.MINUTES),
|
||||
lastTraining,
|
||||
nextEarliestTraining));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ public class CIDRFilteringMetricsTable implements CIDRFilteringMetricsTableMBean
|
|||
{
|
||||
public static final String MBEAN_NAME = "org.apache.cassandra.db:type=CIDRFilteringMetricsTable";
|
||||
|
||||
private static final CIDRFilteringMetricsTable instance = new CIDRFilteringMetricsTable();
|
||||
public static final CIDRFilteringMetricsTable instance = new CIDRFilteringMetricsTable();
|
||||
|
||||
CIDRFilteringMetricsTable()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.exceptions;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class CommandRequestExecutionException extends RequestExecutionException
|
||||
{
|
||||
private final UUID commandExecutionId;
|
||||
|
||||
public CommandRequestExecutionException(UUID commandExecutionId, String msg, Throwable cause)
|
||||
{
|
||||
super(ExceptionCode.COMMAND_FAILED, msg, cause);
|
||||
this.commandExecutionId = commandExecutionId;
|
||||
}
|
||||
|
||||
public CommandRequestExecutionException(UUID commandExecutionId, String msg)
|
||||
{
|
||||
super(ExceptionCode.COMMAND_FAILED, msg);
|
||||
this.commandExecutionId = commandExecutionId;
|
||||
}
|
||||
|
||||
public UUID getCommandExecutionId()
|
||||
{
|
||||
return commandExecutionId;
|
||||
}
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ public enum ExceptionCode
|
|||
WRITE_FAILURE (0x1500),
|
||||
CDC_WRITE_FAILURE (0x1600),
|
||||
CAS_WRITE_UNKNOWN (0x1700),
|
||||
COMMAND_FAILED (0x1800),
|
||||
|
||||
// 2xx: problem validating the request
|
||||
SYNTAX_ERROR (0x2000),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.management.api.Command;
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.CommandExecutionContext;
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.management.api.CommandRegistry;
|
||||
import org.apache.cassandra.management.api.CommandsProvider;
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
|
||||
import static org.apache.cassandra.management.ManagementUtils.loadService;
|
||||
|
||||
public class CassandraCommandRegistry implements CommandRegistry
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(CassandraCommandRegistry.class);
|
||||
|
||||
// TODO CASSANDRA-XXXXX These long-running commands block the calling thread for
|
||||
// the entire command duration. They are excluded from the management API until a
|
||||
// ProgressCommand (extends Command) interface is implemented to support long-running
|
||||
// command execution with progress reporting.
|
||||
static final Set<String> UNSUPPORTED_COMMANDS = Set.of("repair", "consensus_admin");
|
||||
|
||||
private final Map<String, Command<?>> commandMap = new ConcurrentHashMap<>();
|
||||
|
||||
public CassandraCommandRegistry()
|
||||
{
|
||||
for (CommandsProvider provider : loadService(CommandsProvider.class))
|
||||
provider.commands().forEach(this::register);
|
||||
}
|
||||
|
||||
public void register(Command<?> command)
|
||||
{
|
||||
if (UNSUPPORTED_COMMANDS.contains(command.name()))
|
||||
{
|
||||
logger.info("Skipping unsupported command '{}': long-running commands require " +
|
||||
"ProgressCommand support (not yet implemented)", command.name());
|
||||
return;
|
||||
}
|
||||
|
||||
Command<?> prev = commandMap.putIfAbsent(command.name(), command);
|
||||
|
||||
if (prev != null)
|
||||
throw new IllegalStateException(String.format("Command name conflict: '%s' is already registered",
|
||||
command.name()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> Command<T> command(String name)
|
||||
{
|
||||
return (Command<T>) commandMap.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Map.Entry<String, Command<?>>> commands()
|
||||
{
|
||||
return commandMap.entrySet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandMetadata metadata()
|
||||
{
|
||||
// Return metadata that reflects this is a registry, not a leaf command
|
||||
return new RootCommandMetadata();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(CommandExecutionArgs arguments, CommandExecutionContext context)
|
||||
{
|
||||
// CommandRegistry is not directly executable: routing happens at the invoker/CQL layer.
|
||||
throw new UnsupportedOperationException(
|
||||
String.format("CommandRegistry '%s' is not directly executable. " +
|
||||
"Specify a subcommand. Available commands: %s",
|
||||
name(),
|
||||
String.join(", ", commandMap.keySet())));
|
||||
}
|
||||
|
||||
private class RootCommandMetadata implements CommandMetadata
|
||||
{
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return "root";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
return "Root command registry - use a specific subcommand";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OptionMetadata> options()
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParameterMetadata> parameters()
|
||||
{
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandMetadata> subcommands()
|
||||
{
|
||||
return commandMap.values().stream()
|
||||
.map(Command::metadata)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
public class CommandAuthorizationException extends Exception
|
||||
{
|
||||
public CommandAuthorizationException(String msg)
|
||||
{
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,252 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.cassandra.management.api.ArgumentMetadata;
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
import org.apache.cassandra.management.picocli.PicocliCommandMetadata;
|
||||
import org.apache.cassandra.management.picocli.PicocliOptionMetadata;
|
||||
import org.apache.cassandra.management.picocli.PicocliParameterMetadata;
|
||||
import org.apache.cassandra.management.picocli.TypeConverterRegistry;
|
||||
import org.apache.cassandra.utils.JsonUtils;
|
||||
|
||||
import static org.apache.cassandra.management.ManagementUtils.normalizeOptionName;
|
||||
import static org.apache.cassandra.management.api.ParameterMetadata.COMMAND_POSITIONAL_PARAM_PREFIX;
|
||||
|
||||
public class CommandExecutionArgsSerde
|
||||
{
|
||||
public static String toJson(CommandExecutionArgs args)
|
||||
{
|
||||
Map<String, Object> params = new LinkedHashMap<>();
|
||||
|
||||
for (Map.Entry<OptionMetadata, Object> entry : args.options().entrySet())
|
||||
{
|
||||
OptionMetadata optionMetadata = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
String primaryName = normalizeOptionName(optionMetadata.paramLabel());
|
||||
params.put(primaryName, normalizeJsonValue(value));
|
||||
}
|
||||
|
||||
for (Map.Entry<ParameterMetadata, Object> entry : args.parameters().entrySet())
|
||||
{
|
||||
ParameterMetadata paramMetadata = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
int index = paramMetadata.index();
|
||||
String indexKey = COMMAND_POSITIONAL_PARAM_PREFIX + index;
|
||||
params.put(indexKey, normalizeJsonValue(value));
|
||||
}
|
||||
|
||||
return JsonUtils.writeAsJsonString(params);
|
||||
}
|
||||
|
||||
public static CommandExecutionArgs fromJson(String json, CommandMetadata metadata)
|
||||
{
|
||||
if (!(metadata instanceof PicocliCommandMetadata))
|
||||
throw new IllegalArgumentException("CommandMetadata must be PicocliCommandMetadata for picocli commands");
|
||||
|
||||
Map<String, Object> jsonMap = JsonUtils.fromJsonMap(json);
|
||||
Map<OptionMetadata, Object> options = new LinkedHashMap<>();
|
||||
Map<ParameterMetadata, Object> parameters = new LinkedHashMap<>();
|
||||
|
||||
convertFromMap(jsonMap, metadata, options, parameters);
|
||||
return new SimpleCommandExecutionArgs(options, parameters);
|
||||
}
|
||||
|
||||
public static CommandExecutionArgs fromMap(Map<String, Object> format, CommandMetadata metadata)
|
||||
{
|
||||
if (!(metadata instanceof PicocliCommandMetadata))
|
||||
throw new IllegalArgumentException("CommandMetadata must be PicocliCommandMetadata for picocli commands");
|
||||
|
||||
Map<OptionMetadata, Object> options = new LinkedHashMap<>();
|
||||
Map<ParameterMetadata, Object> parameters = new LinkedHashMap<>();
|
||||
|
||||
convertFromMap(format, metadata, options, parameters);
|
||||
return new SimpleCommandExecutionArgs(options, parameters);
|
||||
}
|
||||
|
||||
private static void convertFromMap(Map<String, Object> source,
|
||||
CommandMetadata metadata,
|
||||
Map<OptionMetadata, Object> options,
|
||||
Map<ParameterMetadata, Object> parameters)
|
||||
{
|
||||
for (Map.Entry<String, Object> entry : source.entrySet())
|
||||
{
|
||||
String paramName = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
OptionMetadata option = findOptionByNameIgnoreCase(metadata, paramName);
|
||||
if (option != null)
|
||||
{
|
||||
Object convertedValue = convertValue(value, option);
|
||||
options.put(option, convertedValue);
|
||||
continue;
|
||||
}
|
||||
|
||||
ParameterMetadata param = findParameterByName(metadata, paramName);
|
||||
if (param != null)
|
||||
{
|
||||
Object convertedValue = convertValue(value, param);
|
||||
parameters.put(param, convertedValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new IllegalArgumentException("Unknown parameter: " + paramName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert value using custom converter if provided, otherwise use basic conversion. */
|
||||
private static Object convertValue(Object value, ArgumentMetadata argSpec)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Custom type converters are only supported for picocli-based metadata.
|
||||
if (argSpec instanceof PicocliParameterMetadata)
|
||||
return ((PicocliParameterMetadata) argSpec).convertValue(value);
|
||||
else if (argSpec instanceof PicocliOptionMetadata)
|
||||
return ((PicocliOptionMetadata) argSpec).convertValue(value);
|
||||
|
||||
return TypeConverterRegistry.convertValueBasic(value, argSpec.type());
|
||||
}
|
||||
catch (IllegalArgumentException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new IllegalArgumentException(String.format("Failed to convert value '%s' to type %s: %s",
|
||||
value,
|
||||
argSpec.type().getName(),
|
||||
e.getMessage()), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method converts values from CommandExecutionArgs into JSON-serializable formats before
|
||||
* they're put into a Map(String, Object) that will be serialized to JSON. Different implementations
|
||||
* (e.g., Set, LinkedHashSet) should be normalized to List for consistent JSON output.
|
||||
* <p>
|
||||
* The conversion rules are:
|
||||
* - Converts primitive arrays (String[], int[], etc.) -> Object[] (JSON-serializable)
|
||||
* - Converts any Collection -> List (JSON-serializable)
|
||||
* - Leaves other types unchanged
|
||||
*
|
||||
* @param value the value to convert.
|
||||
* @return the converted value.
|
||||
*/
|
||||
private static Object normalizeJsonValue(Object value)
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
if (value.getClass().isArray())
|
||||
{
|
||||
int length = Array.getLength(value);
|
||||
Object[] array = new Object[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
array[i] = normalizeJsonValue(Array.get(value, i));
|
||||
return array;
|
||||
}
|
||||
|
||||
if (value instanceof java.util.Collection)
|
||||
{
|
||||
return ((java.util.Collection<?>) value).stream()
|
||||
.map(CommandExecutionArgsSerde::normalizeJsonValue)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find ParameterMetadata by name (supports both an index format and paramLabel).
|
||||
*/
|
||||
private static ParameterMetadata findParameterByName(CommandMetadata metadata, String name)
|
||||
{
|
||||
if (name.startsWith(COMMAND_POSITIONAL_PARAM_PREFIX))
|
||||
{
|
||||
try
|
||||
{
|
||||
int index = Integer.parseInt(name.substring(COMMAND_POSITIONAL_PARAM_PREFIX.length()));
|
||||
for (ParameterMetadata param : metadata.parameters())
|
||||
{
|
||||
if (param.index() == index)
|
||||
return param;
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
// Not a valid index format, continue to check paramLabel
|
||||
}
|
||||
}
|
||||
|
||||
for (ParameterMetadata param : metadata.parameters())
|
||||
{
|
||||
String paramLabel = param.paramLabel();
|
||||
if (paramLabel != null && paramLabel.equalsIgnoreCase(name))
|
||||
return param;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find OptionMetadata by name (case-insensitive, normalized).
|
||||
* Matches against paramLabel and all names/aliases.
|
||||
*/
|
||||
private static OptionMetadata findOptionByNameIgnoreCase(CommandMetadata metadata, String name)
|
||||
{
|
||||
String normalizedName = normalizeOptionName(name);
|
||||
|
||||
for (OptionMetadata option : metadata.options())
|
||||
{
|
||||
String paramLabel = normalizeOptionName(option.paramLabel());
|
||||
if (paramLabel.equalsIgnoreCase(normalizedName))
|
||||
return option;
|
||||
|
||||
for (String alias : option.names())
|
||||
{
|
||||
String normalizedAlias = normalizeOptionName(alias);
|
||||
if (normalizedAlias.equalsIgnoreCase(normalizedName))
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class CommandExecutionException extends Exception
|
||||
{
|
||||
private final UUID executionId;
|
||||
|
||||
public CommandExecutionException(String message, Throwable cause, UUID executionId)
|
||||
{
|
||||
super(message, cause);
|
||||
this.executionId = executionId;
|
||||
}
|
||||
|
||||
public UUID getExecutionId()
|
||||
{
|
||||
return executionId;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,508 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedDeque;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import javax.management.ObjectName;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.management.api.Command;
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.CommandExecutionContext;
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.management.api.CommandRegistry;
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
import org.apache.cassandra.serializers.MarshalException;
|
||||
import org.apache.cassandra.tools.NodeProbe;
|
||||
import org.apache.cassandra.tools.Output;
|
||||
import org.apache.cassandra.utils.Clock;
|
||||
import org.apache.cassandra.utils.MBeanWrapper;
|
||||
|
||||
import static java.lang.String.format;
|
||||
import static org.apache.cassandra.management.ManagementUtils.countCommands;
|
||||
import static org.apache.cassandra.management.ManagementUtils.findRegistryCommand;
|
||||
import static org.apache.cassandra.management.ManagementUtils.fullCommandName;
|
||||
|
||||
/**
|
||||
* Service that manages the command registry lifecycle and execution.
|
||||
* <p>
|
||||
* Similar to StorageService, SnapshotManager the service coordinates various aspects of command management:
|
||||
* - Manages lifecycle (initialization, shutdown)
|
||||
* - Coordinates MBean registration
|
||||
* - Provides command execution coordination
|
||||
* - Integrates with daemon startup
|
||||
* - Manages state (registry, MBean instances)
|
||||
*/
|
||||
public class CommandInvokerService implements CommandInvokerServiceMBean
|
||||
{
|
||||
private static final String MBEAN_DOMAIN = "org.apache.cassandra.management";
|
||||
private static final String MBEAN_TYPE_COMMAND = "Command";
|
||||
private static final int MAX_EXECUTION_HISTORY = 100;
|
||||
private static final Logger logger = LoggerFactory.getLogger(CommandInvokerService.class);
|
||||
|
||||
public static final CommandInvokerService instance = new CommandInvokerService();
|
||||
|
||||
private final BoundedExecutionHistory executionHistory = new BoundedExecutionHistory(MAX_EXECUTION_HISTORY);
|
||||
private final Map<String, ObjectName> commandMBeanNames = new ConcurrentHashMap<>();
|
||||
private final MBeanAccessor accessor = new InternalNodeMBeanAccessor();
|
||||
private final CommandRegistry registry;
|
||||
|
||||
private volatile boolean started = false;
|
||||
|
||||
private CommandInvokerService()
|
||||
{
|
||||
this.registry = new CassandraCommandRegistry();
|
||||
}
|
||||
|
||||
public synchronized void start()
|
||||
{
|
||||
if (started)
|
||||
return;
|
||||
|
||||
logger.info("Starting command service");
|
||||
|
||||
registerCommandMBeansRecursively(registry, "");
|
||||
MBeanWrapper.instance.registerMBean(this, MBEAN_NAME);
|
||||
|
||||
started = true;
|
||||
logger.info("Command service started with '{}' commands", countCommands(registry));
|
||||
}
|
||||
|
||||
public static void shutdown()
|
||||
{
|
||||
instance.stop();
|
||||
}
|
||||
|
||||
public synchronized void stop()
|
||||
{
|
||||
if (!started)
|
||||
return;
|
||||
|
||||
logger.info("Stopping command service");
|
||||
|
||||
unregisterCommandMBeans();
|
||||
|
||||
try
|
||||
{
|
||||
MBeanWrapper.instance.unregisterMBean(MBEAN_NAME);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.warn("Failed to unregister CommandInvokerService MBean", e);
|
||||
}
|
||||
|
||||
started = false;
|
||||
logger.info("Command service stopped");
|
||||
}
|
||||
|
||||
public CommandRegistry getRegistry()
|
||||
{
|
||||
return registry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param fullCommandName Full command name, including command demimiter for subcommands.
|
||||
* @param argumentsSupplier Supplier of command execution arguments.
|
||||
* This is used to defer argument construction until after all validation checks are passed.
|
||||
* @return captured output of the command execution.
|
||||
*/
|
||||
public CommandResult invokeCommand(String fullCommandName, Supplier<CommandExecutionArgs> argumentsSupplier)
|
||||
throws CommandExecutionException, CommandValidationException, CommandAuthorizationException
|
||||
{
|
||||
if (!started)
|
||||
throw new IllegalStateException("CommandInvokerService is not started");
|
||||
|
||||
Command<?> command = findRegistryCommand(fullCommandName, registry);
|
||||
if (command == null)
|
||||
throw new IllegalArgumentException("Command not found: " + fullCommandName);
|
||||
|
||||
return invokeCommand(fullCommandName, command, argumentsSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command by name with the given arguments.
|
||||
*
|
||||
* @param command to execute.
|
||||
* @param argumentsSupplier supplier of command execution arguments.
|
||||
* @return captured output of the command execution.
|
||||
*/
|
||||
private CommandResult invokeCommand(String fullCommandName, Command<?> command, Supplier<CommandExecutionArgs> argumentsSupplier)
|
||||
throws CommandExecutionException, CommandValidationException, CommandAuthorizationException
|
||||
{
|
||||
if (!started)
|
||||
throw new IllegalStateException("CommandInvokerService is not started");
|
||||
|
||||
UUID executionId = UUID.randomUUID();
|
||||
CapturingOutput captured = new CapturingOutput();
|
||||
ExecutionHistory record = new ExecutionHistory(executionId, fullCommandName, Clock.Global.currentTimeMillis());
|
||||
CommandExecutionContext executionContext = new ServerCommandExecutionContext(new NodeProbe(accessor, captured.createOutput()));
|
||||
executionHistory.add(record);
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: CASSANDRA-XXXXX. Restrict command execution to environments without authentication
|
||||
// This is a temporary limitation until full authentication support is implemented.
|
||||
if (DatabaseDescriptor.getAuthenticator().requireAuthentication())
|
||||
{
|
||||
throw new CommandAuthorizationException(format("Command execution '%s' via management port is currently " +
|
||||
"only supported when authentication is disabled " +
|
||||
"(AllowAllAuthenticator). Full authentication and authorization " +
|
||||
"support will be added in a future release.", fullCommandName));
|
||||
}
|
||||
|
||||
logger.info("Executing command '{}' with execution ID: {}", fullCommandName, executionId);
|
||||
|
||||
CommandExecutionArgs arguments = argumentsSupplier.get();
|
||||
validateArguments(arguments, command.metadata());
|
||||
|
||||
// Currently, for picocli-based commands in C*, which have no structured result,
|
||||
// the output is written to the Output in the context.
|
||||
Object ignore = command.execute(arguments, executionContext);
|
||||
|
||||
record.completed(Clock.Global.currentTimeMillis());
|
||||
logger.info("Command '{}' (execution ID: {}) completed successfully", fullCommandName, executionId);
|
||||
return new CommandResult(executionId,
|
||||
captured.getCapturedOutput(),
|
||||
record.startTime,
|
||||
record.endTime - record.startTime);
|
||||
}
|
||||
catch (CommandAuthorizationException e)
|
||||
{
|
||||
record.failed(Clock.Global.currentTimeMillis(), e);
|
||||
logger.error("Command '{}' (execution ID: {}) authorization failed", fullCommandName, executionId, e);
|
||||
throw e;
|
||||
}
|
||||
catch (IllegalStateException | IllegalArgumentException | MarshalException e)
|
||||
{
|
||||
record.failed(Clock.Global.currentTimeMillis(), e);
|
||||
String msg = format("Bad usage for command '%s' (execution ID: %s)", fullCommandName, executionId);
|
||||
logger.error(msg, e);
|
||||
throw new CommandValidationException(msg, e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
record.failed(Clock.Global.currentTimeMillis(), e);
|
||||
String msg = format("Command '%s' (execution ID: %s) execution failed", fullCommandName, executionId);
|
||||
logger.error(msg, e);
|
||||
throw new CommandExecutionException(msg, e, executionId);
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
record.failed(Clock.Global.currentTimeMillis(), e);
|
||||
logger.error("Command '{}' (execution ID: {}) unexpected error", fullCommandName, executionId, e);
|
||||
throw new CommandExecutionException(format("Unexpected error while executing '%s': %s",
|
||||
fullCommandName, e.getMessage()),
|
||||
e,
|
||||
executionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getCommandNames()
|
||||
{
|
||||
List<String> commandNames = new ArrayList<>();
|
||||
collectCommandNamesRecursively(registry, "", commandNames);
|
||||
return commandNames.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
List<ExecutionHistory> executionHistory()
|
||||
{
|
||||
return executionHistory.snapshot();
|
||||
}
|
||||
|
||||
/** Validate command arguments against metadata. */
|
||||
protected void validateArguments(CommandExecutionArgs arguments, CommandMetadata metadata)
|
||||
{
|
||||
for (OptionMetadata option : metadata.options())
|
||||
{
|
||||
if (option.required() && !arguments.hasOption(option))
|
||||
throw new IllegalArgumentException(String.format("Required option '%s' is missing", option.paramLabel()));
|
||||
}
|
||||
|
||||
for (ParameterMetadata param : metadata.parameters())
|
||||
{
|
||||
if (param.required() && !arguments.hasParameter(param))
|
||||
throw new IllegalArgumentException(String.format("Required parameter at index %d ('%s') is missing",
|
||||
param.index(), param.paramLabel()));
|
||||
}
|
||||
}
|
||||
|
||||
private void collectCommandNamesRecursively(CommandRegistry registry,
|
||||
String parentCommandName,
|
||||
List<String> result)
|
||||
{
|
||||
for (Map.Entry<String, Command<?>> entry : registry.commands())
|
||||
{
|
||||
String commandName = entry.getKey();
|
||||
Command<?> command = entry.getValue();
|
||||
|
||||
String fullCommandName = fullCommandName(parentCommandName, commandName);
|
||||
if (command instanceof CommandRegistry)
|
||||
collectCommandNamesRecursively((CommandRegistry) command, fullCommandName, result);
|
||||
else
|
||||
result.add(fullCommandName);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCommandCount()
|
||||
{
|
||||
return countCommands(registry);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCommandMBeanName(String fullCommandName)
|
||||
{
|
||||
ObjectName objectName = commandMBeanNames.get(fullCommandName);
|
||||
if (objectName == null)
|
||||
throw new IllegalArgumentException("Command MBean name not found: " + fullCommandName);
|
||||
return objectName.toString();
|
||||
}
|
||||
|
||||
public boolean isStarted()
|
||||
{
|
||||
return started;
|
||||
}
|
||||
|
||||
private void registerCommandMBeansRecursively(CommandRegistry registry, String parentCommandName)
|
||||
{
|
||||
for (Map.Entry<String, Command<?>> e : registry.commands())
|
||||
{
|
||||
String commandName = e.getKey();
|
||||
Command<?> command = e.getValue();
|
||||
String fullCommandName = fullCommandName(parentCommandName, commandName);
|
||||
|
||||
if (command instanceof CommandRegistry)
|
||||
{
|
||||
registerCommandMBeansRecursively((CommandRegistry) command, fullCommandName);
|
||||
}
|
||||
else
|
||||
{
|
||||
try
|
||||
{
|
||||
String escapedName = ObjectName.quote(fullCommandName);
|
||||
ObjectName objectName = new ObjectName(format("%s:type=%s,name=%s",
|
||||
MBEAN_DOMAIN, MBEAN_TYPE_COMMAND, escapedName));
|
||||
CommandMBeanAdapter commandMBean = new CommandMBeanAdapter(fullCommandName, command, this::invokeCommand);
|
||||
MBeanWrapper.instance.registerMBean(commandMBean, objectName, MBeanWrapper.OnException.LOG);
|
||||
|
||||
ObjectName prev = commandMBeanNames.putIfAbsent(fullCommandName, objectName);
|
||||
if (prev != null)
|
||||
{
|
||||
throw new IllegalStateException("Command name conflict during MBean registration: '" +
|
||||
fullCommandName + "' is already registered");
|
||||
}
|
||||
logger.debug("Registered command MBean: {} -> {}", fullCommandName, objectName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.warn("Failed to register MBean for command: {}", fullCommandName, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void unregisterCommandMBeans()
|
||||
{
|
||||
for (ObjectName objectName : commandMBeanNames.values())
|
||||
MBeanWrapper.instance.unregisterMBean(objectName, MBeanWrapper.OnException.LOG);
|
||||
commandMBeanNames.clear();
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class CapturingOutput
|
||||
{
|
||||
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
private final PrintStream output = new PrintStream(buffer, true, StandardCharsets.UTF_8);
|
||||
private final PrintStream error = new PrintStream(buffer, true, StandardCharsets.UTF_8);
|
||||
|
||||
public String getCapturedOutput()
|
||||
{
|
||||
output.flush();
|
||||
error.flush();
|
||||
return buffer.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
Output createOutput()
|
||||
{
|
||||
return new Output(output, error);
|
||||
}
|
||||
}
|
||||
|
||||
private static class ServerCommandExecutionContext implements CommandExecutionContext
|
||||
{
|
||||
private final Map<Class<?>, Object> services = new HashMap<>();
|
||||
|
||||
public ServerCommandExecutionContext(NodeProbe probe)
|
||||
{
|
||||
services.put(NodeProbe.class, probe);
|
||||
services.put(Output.class, probe.output());
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T service(Class<T> serviceType)
|
||||
{
|
||||
T svc = (T) services.get(serviceType);
|
||||
if (svc == null)
|
||||
throw new IllegalArgumentException("Service not available in this context: " + serviceType.getName());
|
||||
return svc;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<Class<?>> services()
|
||||
{
|
||||
return services.keySet();
|
||||
}
|
||||
}
|
||||
|
||||
public static class CommandResult
|
||||
{
|
||||
private final UUID executionId;
|
||||
private final String output;
|
||||
private final long startTime;
|
||||
private final long durationMillis;
|
||||
|
||||
public CommandResult(UUID executionId, String output, long startTime, long durationMillis)
|
||||
{
|
||||
this.executionId = executionId;
|
||||
this.output = output;
|
||||
this.startTime = startTime;
|
||||
this.durationMillis = durationMillis;
|
||||
}
|
||||
|
||||
public UUID getExecutionId()
|
||||
{
|
||||
return executionId;
|
||||
}
|
||||
|
||||
public String getOutput()
|
||||
{
|
||||
return output;
|
||||
}
|
||||
|
||||
public long getStartTime()
|
||||
{
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public long getDurationMillis()
|
||||
{
|
||||
return durationMillis;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record of a single command execution. Retained in {@link BoundedExecutionHistory} and is
|
||||
* surfaced to operators as command execution history via virtual tables.
|
||||
*/
|
||||
static class ExecutionHistory
|
||||
{
|
||||
final UUID executionId;
|
||||
final String commandName;
|
||||
final long startTime;
|
||||
volatile long endTime;
|
||||
volatile boolean success;
|
||||
volatile Throwable error;
|
||||
|
||||
ExecutionHistory(UUID executionId, String commandName, long startTime)
|
||||
{
|
||||
this.executionId = executionId;
|
||||
this.commandName = commandName;
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
void completed(long endTime)
|
||||
{
|
||||
this.endTime = endTime;
|
||||
this.success = true;
|
||||
}
|
||||
|
||||
void failed(long endTime, Throwable error)
|
||||
{
|
||||
this.endTime = endTime;
|
||||
this.success = false;
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public UUID executionId() { return executionId; }
|
||||
public String commandName() { return commandName; }
|
||||
public boolean isSuccess() { return success; }
|
||||
public Throwable error() { return error; }
|
||||
}
|
||||
|
||||
private static class BoundedExecutionHistory
|
||||
{
|
||||
private final Deque<ExecutionHistory> dq = new ConcurrentLinkedDeque<>();
|
||||
private final AtomicInteger size = new AtomicInteger(0);
|
||||
private final int maxSize;
|
||||
|
||||
public BoundedExecutionHistory(int maxSize)
|
||||
{
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public void add(ExecutionHistory info)
|
||||
{
|
||||
dq.offer(info);
|
||||
|
||||
if (size.incrementAndGet() > maxSize)
|
||||
{
|
||||
ExecutionHistory removed = dq.pollFirst();
|
||||
if (removed != null)
|
||||
size.decrementAndGet();
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns a point-in-time snapshot of the retained records, oldest first. */
|
||||
public List<ExecutionHistory> snapshot()
|
||||
{
|
||||
return new ArrayList<>(dq);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
public interface Executor
|
||||
{
|
||||
CommandResult execute(String commandName, Supplier<CommandExecutionArgs> argsSupplier)
|
||||
throws CommandExecutionException, CommandValidationException, CommandAuthorizationException;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
/**
|
||||
* MBean interface for CommandInvokerService.
|
||||
* Exposes registry-level operations (not individual commands).
|
||||
*/
|
||||
public interface CommandInvokerServiceMBean
|
||||
{
|
||||
String MBEAN_NAME = "org.apache.cassandra.management:type=CommandInvokerService";
|
||||
|
||||
/**
|
||||
* Get a list of all command names in the registry.
|
||||
* @return array of command names
|
||||
*/
|
||||
String[] getCommandNames();
|
||||
|
||||
/**
|
||||
* Get the total number of commands in the registry.
|
||||
* @return number of commands
|
||||
*/
|
||||
int getCommandCount();
|
||||
|
||||
/**
|
||||
* Get ObjectName string for a specific command MBean.
|
||||
* @param fullCommandName name of the command
|
||||
* @return ObjectName string for the command MBean
|
||||
* @throws IllegalArgumentException if command not found
|
||||
*/
|
||||
String getCommandMBeanName(String fullCommandName);
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You 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 org.apache.cassandra.management;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.management.Attribute;
|
||||
import javax.management.AttributeList;
|
||||
import javax.management.AttributeNotFoundException;
|
||||
import javax.management.DynamicMBean;
|
||||
import javax.management.InvalidAttributeValueException;
|
||||
import javax.management.MBeanException;
|
||||
import javax.management.MBeanInfo;
|
||||
import javax.management.MBeanOperationInfo;
|
||||
import javax.management.MBeanParameterInfo;
|
||||
import javax.management.ReflectionException;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.management.api.ArgumentMetadata;
|
||||
import org.apache.cassandra.management.api.Command;
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
import org.apache.cassandra.utils.JsonUtils;
|
||||
|
||||
import static javax.management.MBeanOperationInfo.ACTION;
|
||||
import static javax.management.MBeanOperationInfo.INFO;
|
||||
import static org.apache.cassandra.cql3.statements.ExecuteCommandStatement.COMMAND_RESULT_SCHEMA_EXECUTION_ID;
|
||||
import static org.apache.cassandra.cql3.statements.ExecuteCommandStatement.COMMAND_RESULT_SCHEMA_OUTPUT;
|
||||
import static org.apache.cassandra.management.api.ParameterMetadata.COMMAND_POSITIONAL_PARAM_PREFIX;
|
||||
import static org.apache.cassandra.utils.JsonUtils.convertDefaultValue;
|
||||
import static org.apache.cassandra.utils.JsonUtils.getJsonType;
|
||||
|
||||
/**
|
||||
* Command MBean exposes a single management command to the JMX interface.
|
||||
*
|
||||
* <p>Uses JSON-based parameter format:
|
||||
* <ul>
|
||||
* <li>Single "invoke" operation with JSON string parameter</li>
|
||||
* <li>JSON format: {"optionName": "value", "param0": "value", ...}</li>
|
||||
* <li>Option names: use option name or any alias (e.g., "concurrent-compactors", "--concurrent-compactors")</li>
|
||||
* <li>Positional parameters: use "param0", "param1", etc. or parameter name</li>
|
||||
* <li>Returns command output as String</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Invocation:
|
||||
* <pre>
|
||||
* // Command: setconcurrentcompactors --concurrent-compactors 4
|
||||
* mbean.invoke("invoke",
|
||||
* new Object[]{"{\"concurrent-compactors\": \"4\"}"},
|
||||
* new String[]{ "String" });
|
||||
*
|
||||
* // Command: getendpoints keyspace table
|
||||
* mbean.invoke("invoke",
|
||||
* new Object[]{"{\"param0\": \"mykeyspace\", \"param1\": \"mytable\"}"},
|
||||
* new String[]{ "String" });
|
||||
* </pre>
|
||||
*
|
||||
* <p>The {@code invoke} operation accepts exactly one argument: the JSON string above. Other forms
|
||||
* (e.g. name-value pairs spread across multiple arguments) are not supported and are rejected with an
|
||||
* {@link IllegalArgumentException}.
|
||||
*/
|
||||
public class CommandMBeanAdapter implements DynamicMBean
|
||||
{
|
||||
public static final String INVOKE_METHOD = "invoke";
|
||||
public static final String GET_JSON_SCHEMA_METHOD = "getJsonSchema";
|
||||
public static final String GET_PARAMETER_INFO_METHOD = "getParameterInfo";
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CommandMBeanAdapter.class);
|
||||
|
||||
private final String fullCommandName;
|
||||
private final CommandMetadata metadata;
|
||||
private final CommandInvokerService.Executor executor;
|
||||
|
||||
public CommandMBeanAdapter(String fullCommandName, Command<?> command, CommandInvokerService.Executor executor)
|
||||
{
|
||||
this.fullCommandName = fullCommandName;
|
||||
this.metadata = command.metadata();
|
||||
this.executor = executor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MBeanInfo getMBeanInfo()
|
||||
{
|
||||
List<MBeanOperationInfo> operations = new ArrayList<>();
|
||||
|
||||
operations.add(new MBeanOperationInfo(
|
||||
INVOKE_METHOD,
|
||||
"Execute command with JSON parameters. Format: {\"optionName\": \"value\", \"param0\": \"value\", ...}",
|
||||
new MBeanParameterInfo[]{
|
||||
new MBeanParameterInfo("jsonParameters",
|
||||
String.class.getName(),
|
||||
"JSON object with command parameters. Use getJsonSchema() to see available parameters and types.")
|
||||
},
|
||||
String.class.getName(),
|
||||
ACTION));
|
||||
|
||||
operations.add(new MBeanOperationInfo(
|
||||
GET_JSON_SCHEMA_METHOD,
|
||||
"Get JSON schema describing all available parameters (name -> type mapping)",
|
||||
new MBeanParameterInfo[0],
|
||||
String.class.getName(),
|
||||
INFO));
|
||||
|
||||
operations.add(new MBeanOperationInfo(
|
||||
GET_PARAMETER_INFO_METHOD,
|
||||
"Get information about all available parameters (human-readable format)",
|
||||
new MBeanParameterInfo[0],
|
||||
String.class.getName(),
|
||||
INFO));
|
||||
|
||||
return new MBeanInfo(CommandMBeanAdapter.class.getName(),
|
||||
metadata.description() != null ? metadata.description() : "Command: " + metadata.name(),
|
||||
null,
|
||||
null,
|
||||
operations.toArray(new MBeanOperationInfo[0]),
|
||||
null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(String actionName, Object[] params, String[] signature) throws MBeanException, ReflectionException
|
||||
{
|
||||
if (INVOKE_METHOD.equals(actionName))
|
||||
{
|
||||
if (params == null || params.length != 1 || !(params[0] instanceof String))
|
||||
throw new IllegalArgumentException("invoke requires exactly one parameter (JSON string)");
|
||||
|
||||
String jsonParams = (String) params[0];
|
||||
try
|
||||
{
|
||||
CommandInvokerService.CommandResult result = executor.execute(fullCommandName,
|
||||
() -> CommandExecutionArgsSerde.fromJson(jsonParams, metadata));
|
||||
|
||||
Map<String, Object> jsonResult = new TreeMap<>();
|
||||
jsonResult.put(COMMAND_RESULT_SCHEMA_EXECUTION_ID, result.getExecutionId().toString());
|
||||
jsonResult.put(COMMAND_RESULT_SCHEMA_OUTPUT, result.getOutput());
|
||||
return JsonUtils.writeAsJsonString(jsonResult);
|
||||
}
|
||||
catch (CommandAuthorizationException e)
|
||||
{
|
||||
logger.error("Authorization error executing command: {}", metadata.name(), e);
|
||||
throw new SecurityException("Access Denied: " + e.getMessage());
|
||||
}
|
||||
catch (CommandValidationException e)
|
||||
{
|
||||
logger.error("Validation error for command: {}", metadata.name(), e);
|
||||
throw new IllegalArgumentException(ManagementUtils.causeMessages(e));
|
||||
}
|
||||
catch (CommandExecutionException e)
|
||||
{
|
||||
logger.error("Error executing command: {}", metadata.name(), e);
|
||||
throw new RuntimeException(ManagementUtils.causeMessages(e));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.error("Unexpected error executing command: {}", metadata.name(), e);
|
||||
throw new RuntimeException("Unexpected error executing command: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
if (GET_JSON_SCHEMA_METHOD.equals(actionName))
|
||||
return getJsonSchema();
|
||||
|
||||
if (GET_PARAMETER_INFO_METHOD.equals(actionName))
|
||||
return getParameterInfo();
|
||||
|
||||
throw new UnsupportedOperationException("Unknown operation: " + actionName);
|
||||
}
|
||||
|
||||
public String getJsonSchema()
|
||||
{
|
||||
try
|
||||
{
|
||||
Map<String, Object> schema = new LinkedHashMap<>();
|
||||
schema.put("$schema", "http://json-schema.org/draft-07/schema#");
|
||||
schema.put("type", "object");
|
||||
schema.put("title", metadata.name());
|
||||
schema.put("description", metadata.description());
|
||||
|
||||
Map<String, Object> properties = new LinkedHashMap<>();
|
||||
List<String> required = new ArrayList<>();
|
||||
|
||||
for (OptionMetadata option : metadata.options())
|
||||
{
|
||||
String primaryName = option.paramLabel();
|
||||
properties.put(primaryName, buildJsonSchemaProperty(option));
|
||||
|
||||
if (option.required())
|
||||
required.add(primaryName);
|
||||
}
|
||||
|
||||
List<ParameterMetadata> sortedParams = new ArrayList<>(metadata.parameters());
|
||||
sortedParams.sort(Comparator.comparingInt(ParameterMetadata::index));
|
||||
|
||||
for (ParameterMetadata param : sortedParams)
|
||||
{
|
||||
String paramName = COMMAND_POSITIONAL_PARAM_PREFIX + param.index();
|
||||
properties.put(paramName, buildJsonSchemaProperty(param));
|
||||
|
||||
if (param.required())
|
||||
required.add(paramName);
|
||||
}
|
||||
|
||||
schema.put("properties", properties);
|
||||
|
||||
if (!required.isEmpty())
|
||||
schema.put("required", required);
|
||||
|
||||
return JsonUtils.writeAsPrettyJsonString(schema);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.error("Error generating JSON schema for command: {}", metadata.name(), e);
|
||||
throw new RuntimeException("Failed to generate JSON schema: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> buildJsonSchemaProperty(ArgumentMetadata arg)
|
||||
{
|
||||
Map<String, Object> prop = new LinkedHashMap<>();
|
||||
prop.put("type", getJsonType(arg.type()));
|
||||
|
||||
if (arg.names() != null && arg.names().length > 0)
|
||||
prop.put("aliases", Arrays.stream(arg.names())
|
||||
.filter(name -> !name.equals(arg.paramLabel()))
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
if (arg.description() != null && !arg.description().isEmpty())
|
||||
prop.put("description", arg.description());
|
||||
|
||||
String defaultValue = arg.defaultValue();
|
||||
if (defaultValue != null && !defaultValue.isEmpty())
|
||||
prop.put("default", convertDefaultValue(defaultValue, arg.type()));
|
||||
|
||||
if (arg.type().isArray() || List.class.isAssignableFrom(arg.type()))
|
||||
{
|
||||
prop.put("type", "array");
|
||||
Map<String, Object> items = new LinkedHashMap<>();
|
||||
items.put("type", "string");
|
||||
prop.put("items", items);
|
||||
}
|
||||
|
||||
if (arg.type().isEnum())
|
||||
{
|
||||
prop.put("enum", Arrays.stream(arg.type().getEnumConstants())
|
||||
.map(Object::toString)
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
return prop;
|
||||
}
|
||||
|
||||
private static String getTypeName(Class<?> type)
|
||||
{
|
||||
return type.getCanonicalName();
|
||||
}
|
||||
|
||||
private String getParameterInfo()
|
||||
{
|
||||
StringBuilder info = new StringBuilder();
|
||||
info.append("Command: ").append(metadata.name()).append('\n');
|
||||
info.append("Description: ").append(metadata.description()).append("\n\n");
|
||||
|
||||
info.append("Options:\n");
|
||||
for (OptionMetadata option : metadata.options())
|
||||
{
|
||||
info.append(" - ").append(option.paramLabel());
|
||||
if (option.names().length > 0)
|
||||
info.append(" (aliases: ").append(String.join(", ", option.names())).append(')');
|
||||
appendRequiredClause(info, getTypeName(option.type()), option.required(), option.description());
|
||||
}
|
||||
|
||||
info.append("\nPositional Parameters:\n");
|
||||
List<ParameterMetadata> sortedParams = new ArrayList<>(metadata.parameters());
|
||||
sortedParams.sort((a, b) -> Integer.compare(a.index(), b.index()));
|
||||
|
||||
for (ParameterMetadata param : sortedParams)
|
||||
{
|
||||
info.append(" - param").append(param.index());
|
||||
if (param.paramLabel() != null && !param.paramLabel().isEmpty())
|
||||
info.append(" (").append(param.paramLabel()).append(')');
|
||||
appendRequiredClause(info, getTypeName(param.type()), param.required(), param.description());
|
||||
}
|
||||
|
||||
return info.toString();
|
||||
}
|
||||
|
||||
private static void appendRequiredClause(StringBuilder info,
|
||||
String typeName,
|
||||
boolean required,
|
||||
String description)
|
||||
{
|
||||
info.append(" [").append(typeName).append(']');
|
||||
if (required)
|
||||
info.append(" [REQUIRED]");
|
||||
if (description != null && !description.isEmpty())
|
||||
info.append("\n ").append(description);
|
||||
info.append('\n');
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getAttribute(String attribute) throws AttributeNotFoundException, MBeanException, ReflectionException
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAttribute(Attribute attribute) throws AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeList getAttributes(String[] attributes)
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public AttributeList setAttributes(AttributeList attributes)
|
||||
{
|
||||
throw new UnsupportedOperationException("Not supported yet.");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
public class CommandValidationException extends Exception
|
||||
{
|
||||
public CommandValidationException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,508 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import javax.management.JMX;
|
||||
import javax.management.MBeanServer;
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
|
||||
import org.apache.cassandra.audit.AuditLogManager;
|
||||
import org.apache.cassandra.audit.AuditLogManagerMBean;
|
||||
import org.apache.cassandra.auth.AbstractCIDRAuthorizer;
|
||||
import org.apache.cassandra.auth.AuthCache;
|
||||
import org.apache.cassandra.auth.AuthCacheMBean;
|
||||
import org.apache.cassandra.auth.AuthCacheService;
|
||||
import org.apache.cassandra.auth.CIDRGroupsMappingManagerMBean;
|
||||
import org.apache.cassandra.auth.CIDRPermissionsManagerMBean;
|
||||
import org.apache.cassandra.auth.NetworkPermissionsCacheMBean;
|
||||
import org.apache.cassandra.auth.PasswordAuthenticator;
|
||||
import org.apache.cassandra.auth.PermissionsCacheMBean;
|
||||
import org.apache.cassandra.auth.RolesCacheMBean;
|
||||
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
|
||||
import org.apache.cassandra.batchlog.BatchlogManager;
|
||||
import org.apache.cassandra.batchlog.BatchlogManagerMBean;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.ColumnFamilyStore;
|
||||
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.db.compaction.CompactionManager;
|
||||
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
|
||||
import org.apache.cassandra.db.compression.CompressionDictionaryManager;
|
||||
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
|
||||
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||
import org.apache.cassandra.db.guardrails.GuardrailsMBean;
|
||||
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable;
|
||||
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTableMBean;
|
||||
import org.apache.cassandra.gms.FailureDetector;
|
||||
import org.apache.cassandra.gms.FailureDetectorMBean;
|
||||
import org.apache.cassandra.gms.Gossiper;
|
||||
import org.apache.cassandra.gms.GossiperMBean;
|
||||
import org.apache.cassandra.hints.HintsService;
|
||||
import org.apache.cassandra.hints.HintsServiceMBean;
|
||||
import org.apache.cassandra.index.Index;
|
||||
import org.apache.cassandra.index.SecondaryIndexManager;
|
||||
import org.apache.cassandra.locator.DynamicEndpointSnitch;
|
||||
import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
|
||||
import org.apache.cassandra.locator.EndpointSnitchInfo;
|
||||
import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
|
||||
import org.apache.cassandra.locator.LocationInfo;
|
||||
import org.apache.cassandra.locator.LocationInfoMBean;
|
||||
import org.apache.cassandra.locator.NodeProximity;
|
||||
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
|
||||
import org.apache.cassandra.metrics.ThreadPoolMetrics;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.MessagingServiceMBean;
|
||||
import org.apache.cassandra.profiler.AsyncProfilerMBean;
|
||||
import org.apache.cassandra.schema.Schema;
|
||||
import org.apache.cassandra.service.ActiveRepairService;
|
||||
import org.apache.cassandra.service.ActiveRepairServiceMBean;
|
||||
import org.apache.cassandra.service.AsyncProfilerService;
|
||||
import org.apache.cassandra.service.AutoRepairService;
|
||||
import org.apache.cassandra.service.AutoRepairServiceMBean;
|
||||
import org.apache.cassandra.service.CacheService;
|
||||
import org.apache.cassandra.service.CacheServiceMBean;
|
||||
import org.apache.cassandra.service.GCInspector;
|
||||
import org.apache.cassandra.service.GCInspectorMXBean;
|
||||
import org.apache.cassandra.service.StorageProxy;
|
||||
import org.apache.cassandra.service.StorageProxyMBean;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.service.StorageServiceMBean;
|
||||
import org.apache.cassandra.service.accord.AccordOperations;
|
||||
import org.apache.cassandra.service.accord.AccordOperationsMBean;
|
||||
import org.apache.cassandra.service.snapshot.SnapshotManager;
|
||||
import org.apache.cassandra.service.snapshot.SnapshotManagerMBean;
|
||||
import org.apache.cassandra.streaming.StreamManager;
|
||||
import org.apache.cassandra.streaming.StreamManagerMBean;
|
||||
import org.apache.cassandra.tcm.CMSOperations;
|
||||
import org.apache.cassandra.tcm.CMSOperationsMBean;
|
||||
import org.apache.cassandra.tools.RemoteJmxMBeanAccessor;
|
||||
import org.apache.cassandra.utils.MBeanWrapper;
|
||||
|
||||
import static org.apache.cassandra.metrics.CassandraMetricsRegistry.Metrics;
|
||||
import static org.apache.cassandra.service.CassandraDaemon.SKIP_GC_INSPECTOR;
|
||||
|
||||
/**
|
||||
* Server-side implementation of {@link MBeanAccessor} for in-process execution.
|
||||
*
|
||||
* <p>
|
||||
* This implementation provides direct access to MBean instances without using JMX,
|
||||
* eliminating the need for remote connections or JMX proxies. It is designed for
|
||||
* server-side command execution as part of CEP-38 Management API, where commands run
|
||||
* in the same JVM as the Cassandra daemon.
|
||||
*
|
||||
* <p>
|
||||
* Unlike {@link RemoteJmxMBeanAccessor}, this implementation:
|
||||
* <ul>
|
||||
* <li>Directly accesses singleton instances (e.g., {@code StorageService.instance})</li>
|
||||
* <li>Does not require network connections or JMX connectors</li>
|
||||
* <li>Provides better performance by avoiding serialization/deserialization</li>
|
||||
* <li>Has no connection state to manage</li>
|
||||
* <li>Works directly with {@link Keyspace} and {@link ColumnFamilyStore} instances</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* <b>Lazy Initialization</b>: MBean providers are initialized registered for known MBeans.
|
||||
*
|
||||
* @see MBeanAccessor
|
||||
* @see RemoteJmxMBeanAccessor
|
||||
* @since 5.1
|
||||
*/
|
||||
public class InternalNodeMBeanAccessor implements MBeanAccessor
|
||||
{
|
||||
private final Map<Class<?>, MBeanProvider<?>> mBeanProviders = new ConcurrentHashMap<>();
|
||||
private final Map<Class<?>, Object> mBeanCache = new ConcurrentHashMap<>();
|
||||
private final Map<String, Object> metricCache = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new InternalNodeMBeanAccessor using direct instance access.
|
||||
*/
|
||||
public InternalNodeMBeanAccessor()
|
||||
{
|
||||
initializeMBeanProviders();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all statically known MBean instances.
|
||||
*/
|
||||
private void initializeMBeanProviders()
|
||||
{
|
||||
registerMBeanProvider(AccordOperationsMBean.class, () -> AccordOperations.instance);
|
||||
registerMBeanProvider(ActiveRepairServiceMBean.class, ActiveRepairService::instance);
|
||||
registerMBeanProvider(AuditLogManagerMBean.class, () -> AuditLogManager.instance);
|
||||
registerMBeanProvider(AutoRepairServiceMBean.class, () -> AutoRepairService.instance);
|
||||
registerMBeanProvider(BatchlogManagerMBean.class, () -> BatchlogManager.instance);
|
||||
registerMBeanProvider(CMSOperationsMBean.class, () -> CMSOperations.instance);
|
||||
registerMBeanProvider(CacheServiceMBean.class, () -> CacheService.instance);
|
||||
registerMBeanProvider(CompactionManagerMBean.class, () -> CompactionManager.instance);
|
||||
registerMBeanProvider(DynamicEndpointSnitchMBean.class, this::resolveDynamicEndpointSnitch);
|
||||
registerMBeanProvider(FailureDetectorMBean.class, () -> (FailureDetectorMBean) FailureDetector.instance);
|
||||
registerMBeanProvider(GCInspectorMXBean.class, this::resolveGCInspector);
|
||||
registerMBeanProvider(GossiperMBean.class, () -> Gossiper.instance);
|
||||
registerMBeanProvider(GuardrailsMBean.class, () -> Guardrails.instance);
|
||||
registerMBeanProvider(HintsServiceMBean.class, () -> HintsService.instance);
|
||||
registerMBeanProvider(MemoryMXBean.class, ManagementFactory::getMemoryMXBean);
|
||||
registerMBeanProvider(MessagingServiceMBean.class, MessagingService::instance);
|
||||
registerMBeanProvider(RuntimeMXBean.class, ManagementFactory::getRuntimeMXBean);
|
||||
registerMBeanProvider(SnapshotManagerMBean.class, () -> SnapshotManager.instance);
|
||||
registerMBeanProvider(StorageProxyMBean.class, () -> StorageProxy.instance);
|
||||
registerMBeanProvider(StorageServiceMBean.class, () -> StorageService.instance);
|
||||
registerMBeanProvider(StreamManagerMBean.class, () -> StreamManager.instance);
|
||||
registerMBeanProvider(AsyncProfilerMBean.class, AsyncProfilerService::instance);
|
||||
|
||||
// Utility MBeans are stateless and can be created on demand.
|
||||
// They query DatabaseDescriptor for the current state, so new instances are fine
|
||||
registerMBeanProvider(EndpointSnitchInfoMBean.class, EndpointSnitchInfo::new);
|
||||
registerMBeanProvider(LocationInfoMBean.class, LocationInfo::new);
|
||||
|
||||
// AuthCache MBeans
|
||||
registerMBeanProvider(AuthorizationProxy.JmxPermissionsCacheMBean.class,
|
||||
() -> findAuthCache(AuthorizationProxy.JmxPermissionsCacheMBean.class));
|
||||
registerMBeanProvider(NetworkPermissionsCacheMBean.class,
|
||||
() -> findAuthCache(NetworkPermissionsCacheMBean.class));
|
||||
registerMBeanProvider(PasswordAuthenticator.CredentialsCacheMBean.class,
|
||||
() -> findAuthCache(PasswordAuthenticator.CredentialsCacheMBean.class));
|
||||
registerMBeanProvider(PermissionsCacheMBean.class,
|
||||
() -> findAuthCache(PermissionsCacheMBean.class));
|
||||
registerMBeanProvider(RolesCacheMBean.class,
|
||||
() -> findAuthCache(RolesCacheMBean.class));
|
||||
|
||||
// CIDR Auth MBeans
|
||||
registerMBeanProvider(CIDRFilteringMetricsTableMBean.class, () -> CIDRFilteringMetricsTable.instance);
|
||||
registerMBeanProvider(CIDRGroupsMappingManagerMBean.class, () -> AbstractCIDRAuthorizer.cidrGroupsMappingManager);
|
||||
registerMBeanProvider(CIDRPermissionsManagerMBean.class, () -> AbstractCIDRAuthorizer.cidrPermissionsManager);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets DynamicEndpointSnitch from DatabaseDescriptor if it's a DynamicEndpointSnitch,
|
||||
* otherwise returns null.
|
||||
*/
|
||||
private DynamicEndpointSnitchMBean resolveDynamicEndpointSnitch()
|
||||
{
|
||||
if (!DatabaseDescriptor.isDynamicEndpointSnitch())
|
||||
throw new IllegalStateException("DynamicEndpointSnitch has been requested but is not enabled");
|
||||
|
||||
NodeProximity proximity = DatabaseDescriptor.getNodeProximity();
|
||||
assert proximity instanceof DynamicEndpointSnitch;
|
||||
|
||||
return (DynamicEndpointSnitchMBean) proximity;
|
||||
}
|
||||
|
||||
private GCInspectorMXBean resolveGCInspector()
|
||||
{
|
||||
if (SKIP_GC_INSPECTOR)
|
||||
throw new IllegalStateException("GCInspector has been requested but is disabled via SKIP_GC_INSPECTOR flag");
|
||||
|
||||
try
|
||||
{
|
||||
MBeanServer mbs = MBeanWrapper.instance.getMBeanServer();
|
||||
if (mbs == null)
|
||||
return null;
|
||||
|
||||
ObjectName name = new ObjectName(GCInspector.MBEAN_NAME);
|
||||
if (mbs.isRegistered(name))
|
||||
return JMX.newMBeanProxy(mbs, name, GCInspectorMXBean.class);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Fall through to create a new instance
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Finds an auth cache MBean instance from AuthCacheService. */
|
||||
private <T> T findAuthCache(Class<T> clazz)
|
||||
{
|
||||
Set<AuthCache<?, ?>> caches = AuthCacheService.instance.getCaches();
|
||||
if (caches.isEmpty())
|
||||
return null;
|
||||
|
||||
AuthCacheFinder visitor = new AuthCacheFinder(clazz);
|
||||
for (AuthCache<?, ?> cache : caches)
|
||||
{
|
||||
cache.accept(visitor);
|
||||
Object found = visitor.getCache();
|
||||
if (found == null)
|
||||
continue;
|
||||
return clazz.cast(found);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private <T> void registerMBeanProvider(Class<T> clazz, MBeanProvider<T> locator)
|
||||
{
|
||||
Object prev = mBeanProviders.putIfAbsent(clazz, locator);
|
||||
assert prev == null : "MBean locator for " + clazz.getName() + " is already registered";
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findMBean(Class<T> clazz)
|
||||
{
|
||||
Object cached = mBeanCache.get(clazz);
|
||||
if (cached != null)
|
||||
return clazz.cast(cached);
|
||||
|
||||
Object created = mBeanCache.computeIfAbsent(clazz, k -> {
|
||||
MBeanProvider<?> provider = mBeanProviders.get(k);
|
||||
return provider == null ? null : provider.provide();
|
||||
});
|
||||
return created == null ? null : clazz.cast(created);
|
||||
}
|
||||
|
||||
private MBeanServer mbeanServer()
|
||||
{
|
||||
MBeanServer mbs = MBeanWrapper.instance.getMBeanServer();
|
||||
if (mbs == null)
|
||||
throw new IllegalStateException("The MBean server is not available (MBean registration is disabled on this node)");
|
||||
return mbs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findMBeanMetric(Class<T> clazz, Props props)
|
||||
{
|
||||
try
|
||||
{
|
||||
assert clazz.isInterface() && CassandraMetricsRegistry.MetricMBean.class.isAssignableFrom(clazz);
|
||||
|
||||
ObjectName objectName = buildObjectNameFromProps(props);
|
||||
String cacheKey = objectName.getCanonicalName();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
T cached = (T) metricCache.get(cacheKey);
|
||||
if (cached != null)
|
||||
return cached;
|
||||
|
||||
MBeanServer mbs = mbeanServer();
|
||||
|
||||
return clazz.cast(metricCache.computeIfAbsent(cacheKey, k -> JMX.newMBeanProxy(mbs, objectName, clazz)));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("Error accessing metric MBean: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMBeanMetricRegistered(Props props)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Use the internal MBean server to look up MBean by ObjectName. This leverages existing
|
||||
// JMX infrastructure and avoids the complexity of constructing metric names from Props.
|
||||
//
|
||||
// Alternatively, we could use CassandraMetricsRegistry to look up metrics by metric name
|
||||
// (e.g., "org.apache.cassandra.metrics.Keyspace.ReadLatency.mykeyspace"), but this requires
|
||||
// constructing the full metric name from Props, which is problematic.
|
||||
//
|
||||
// The reconstructing the scope problem: Different MetricNameFactory implementations
|
||||
// construct scopes differently:
|
||||
// - KeyspaceMetrics: scope = keyspace property
|
||||
// - TableMetrics: scope = keyspace + '.' + scope property
|
||||
// - DefaultNameFactory: scope = scope property
|
||||
// - SAI AbstractMetrics: scope = keyspace.table.index.scope (all combined)
|
||||
//
|
||||
// To construct metric names from Props, we would need to duplicate scope construction logic
|
||||
// from each factory or refactor to share it. Using ObjectName, in turn, avoids this. We query
|
||||
// MBeanServer using the ObjectName pattern already constructed by factories during registration.
|
||||
//
|
||||
// However, it will be beneficial to revisit this down the line for performance optimizations and
|
||||
// to avoid JMX entanglement, so we could switch it off for in-process access.
|
||||
|
||||
ObjectName objectName = buildObjectNameFromProps(props);
|
||||
return mbeanServer().isRegistered(objectName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("Error checking metric MBean registration: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static ObjectName buildObjectNameFromProps(Props props) throws MalformedObjectNameException
|
||||
{
|
||||
return new ObjectName("org.apache.cassandra.metrics", new Hashtable<>(props.toMap()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnFamilyStoreMBean findColumnFamily(String type, String keyspace, String columnFamily)
|
||||
{
|
||||
Keyspace ks = Schema.instance.getKeyspaceInstance(keyspace);
|
||||
if (ks == null)
|
||||
throw new IllegalArgumentException("Keyspace not found: " + keyspace);
|
||||
|
||||
if (!SecondaryIndexManager.isIndexColumnFamily(columnFamily))
|
||||
return ks.getColumnFamilyStore(columnFamily);
|
||||
|
||||
// Secondary-index stores are registered under "base.index" names and are only
|
||||
// reachable through the base table's index manager, not by keyspace lookup.
|
||||
ColumnFamilyStore base = ks.getColumnFamilyStore(SecondaryIndexManager.getParentCfsName(columnFamily));
|
||||
Index index = base.indexManager.getIndexByName(SecondaryIndexManager.getIndexName(columnFamily));
|
||||
if (index == null)
|
||||
throw new IllegalArgumentException(String.format("Index not found: %s.%s", keyspace, columnFamily));
|
||||
return index.getBackingTable()
|
||||
.orElseThrow(() -> new IllegalArgumentException(String.format("Index %s.%s has no backing table",
|
||||
keyspace, columnFamily)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompressionDictionaryManagerMBean findCompressionDictionary(String keyspace, String table)
|
||||
{
|
||||
Keyspace ks = Schema.instance.getKeyspaceInstance(keyspace);
|
||||
if (ks == null || Schema.instance.getTableMetadata(keyspace, table) == null)
|
||||
throw new IllegalArgumentException(String.format("Table %s.%s does not exist", keyspace, table));
|
||||
|
||||
CompressionDictionaryManager manager = ks.getColumnFamilyStore(table).compressionDictionaryManager();
|
||||
if (!manager.isEnabled())
|
||||
throw new IllegalStateException("The compression on table " + keyspace + '.' + table +
|
||||
" is not enabled or SSTable compressor is not a dictionary compressor.");
|
||||
return manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ThreadPoolInfo> threadPoolInfos()
|
||||
{
|
||||
List<ThreadPoolInfo> infos = new ArrayList<>();
|
||||
for (ThreadPoolMetrics metrics : Metrics.allThreadPoolMetrics())
|
||||
infos.add(new ThreadPoolInfo(metrics.path, metrics.poolName));
|
||||
return infos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map.Entry<String, ColumnFamilyStoreMBean>> findColumnFamilies(String type)
|
||||
{
|
||||
try
|
||||
{
|
||||
assert type.equals("IndexColumnFamilies") || type.equals("ColumnFamilies");
|
||||
|
||||
List<Map.Entry<String, ColumnFamilyStoreMBean>> mbeans = new ArrayList<>();
|
||||
|
||||
for (Keyspace keyspace : Keyspace.all())
|
||||
{
|
||||
for (ColumnFamilyStore cfs : keyspace.getColumnFamilyStores())
|
||||
{
|
||||
// Secondary-index backing stores are not registered in the keyspace,
|
||||
// they are only reachable through the base table's index manager.
|
||||
if (type.equals("IndexColumnFamilies"))
|
||||
for (ColumnFamilyStore indexCfs : cfs.indexManager.getAllIndexColumnFamilyStores())
|
||||
mbeans.add(new AbstractMap.SimpleImmutableEntry<>(keyspace.getName(), indexCfs));
|
||||
else
|
||||
mbeans.add(new AbstractMap.SimpleImmutableEntry<>(keyspace.getName(), cfs));
|
||||
}
|
||||
}
|
||||
|
||||
return mbeans;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("Error accessing column families", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
metricCache.clear();
|
||||
mBeanCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Functional interface for providing MBean instances lazily.
|
||||
* Used to defer MBean initialization until the MBean is actually accessed.
|
||||
*
|
||||
* @param <T> the MBean interface type
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface MBeanProvider<T>
|
||||
{
|
||||
/**
|
||||
* @return the MBean instance, or {@code null} if the MBean is not available
|
||||
* @throws RuntimeException if the MBean cannot be provided (e.g., not initialized yet)
|
||||
*/
|
||||
T provide();
|
||||
}
|
||||
|
||||
/** Visitor that finds a specific auth cache MBean type from AuthCacheService. */
|
||||
private static class AuthCacheFinder implements AuthCache.MBeanVisitor
|
||||
{
|
||||
private final Class<?> targetType;
|
||||
private Object foundCache;
|
||||
|
||||
AuthCacheFinder(Class<?> targetType)
|
||||
{
|
||||
this.targetType = targetType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitCredentials(PasswordAuthenticator.CredentialsCacheMBean cache)
|
||||
{
|
||||
if (targetType.equals(PasswordAuthenticator.CredentialsCacheMBean.class))
|
||||
foundCache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitJmxPermissions(AuthorizationProxy.JmxPermissionsCacheMBean cache)
|
||||
{
|
||||
if (targetType.equals(AuthorizationProxy.JmxPermissionsCacheMBean.class))
|
||||
foundCache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitPermissions(PermissionsCacheMBean cache)
|
||||
{
|
||||
if (targetType.equals(PermissionsCacheMBean.class))
|
||||
foundCache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitNetwork(NetworkPermissionsCacheMBean cache)
|
||||
{
|
||||
if (targetType.equals(NetworkPermissionsCacheMBean.class))
|
||||
foundCache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visitRoles(RolesCacheMBean cache)
|
||||
{
|
||||
if (targetType.equals(RolesCacheMBean.class))
|
||||
foundCache = cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(AuthCacheMBean cache)
|
||||
{
|
||||
// No-op. Used for caches without specific MBean types.
|
||||
}
|
||||
|
||||
Object getCache()
|
||||
{
|
||||
return foundCache;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
|
||||
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
|
||||
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
|
||||
|
||||
public interface MBeanAccessor extends AutoCloseable
|
||||
{
|
||||
/**
|
||||
* Finds statically known MBeans by MBean class name.
|
||||
* @param clazz the MBean class.
|
||||
* @return MBean class or {@code null} if not found.
|
||||
* @param <T> the MBean type.
|
||||
*/
|
||||
@Nullable
|
||||
<T> T findMBean(Class<T> clazz);
|
||||
<T> T findMBeanMetric(Class<T> clazz, Props props);
|
||||
boolean isMBeanMetricRegistered(Props props);
|
||||
|
||||
ColumnFamilyStoreMBean findColumnFamily(String type, String keyspace, String columnFamily);
|
||||
CompressionDictionaryManagerMBean findCompressionDictionary(String keyspace, String table);
|
||||
|
||||
/** List of thread pool MBeans with associated path and pool name. */
|
||||
List<ThreadPoolInfo> threadPoolInfos();
|
||||
/** List of column family MBeans with associated keyspace name. */
|
||||
List<Map.Entry<String, ColumnFamilyStoreMBean>> findColumnFamilies(String type);
|
||||
|
||||
/** {@inheritDoc} */
|
||||
@Override
|
||||
default void close() { }
|
||||
|
||||
default CassandraMetricsRegistry.JmxCounterMBean findMBeanCounter(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxCounterMBean.class, props);
|
||||
}
|
||||
|
||||
default CassandraMetricsRegistry.JmxGaugeMBean findMBeanGauge(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxGaugeMBean.class, props);
|
||||
}
|
||||
|
||||
default CassandraMetricsRegistry.JmxMeterMBean findMBeanMeter(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxMeterMBean.class, props);
|
||||
}
|
||||
|
||||
default CassandraMetricsRegistry.JmxTimerMBean findMBeanTimer(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxTimerMBean.class, props);
|
||||
}
|
||||
|
||||
default CassandraMetricsRegistry.JmxHistogramMBean findMBeanHistogram(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxHistogramMBean.class, props);
|
||||
}
|
||||
|
||||
class Props
|
||||
{
|
||||
private final Map<String, String> values = Maps.newLinkedHashMap();
|
||||
|
||||
public Props(String type, String path, String keyspace, String table, String scope, String name)
|
||||
{
|
||||
if (type == null || type.isEmpty())
|
||||
throw new IllegalArgumentException("type is required");
|
||||
values.put("type", type);
|
||||
if (path != null)
|
||||
values.put("path", path);
|
||||
if (keyspace != null)
|
||||
values.put("keyspace", keyspace);
|
||||
if (table != null)
|
||||
values.put("table", table);
|
||||
if (scope != null)
|
||||
values.put("scope", scope);
|
||||
if (name != null)
|
||||
values.put("name", name);
|
||||
}
|
||||
|
||||
public static Props metric(String type, String name)
|
||||
{
|
||||
return new Props(type, null, null, null, null, name);
|
||||
}
|
||||
|
||||
public static Props scoped(String type, String scope, String name)
|
||||
{
|
||||
return new Props(type, null, null, null, scope, name);
|
||||
}
|
||||
|
||||
public static Props threadPool(String type, String path, String scope, String name)
|
||||
{
|
||||
return new Props(type, path, null, null, scope, name);
|
||||
}
|
||||
|
||||
public static Props sai(String type, String keyspace, String table, String scope, String name)
|
||||
{
|
||||
return new Props(type, null, keyspace, table, scope, name);
|
||||
}
|
||||
|
||||
public static Props columnFamily(String type, String keyspace, String scope, String name)
|
||||
{
|
||||
return new Props(type, null, keyspace, null, scope, name);
|
||||
}
|
||||
|
||||
public static Props keyspace(String type, String keyspace, String name)
|
||||
{
|
||||
return new Props(type, null, keyspace, null, null, name);
|
||||
}
|
||||
|
||||
public Map<String, String> toMap()
|
||||
{
|
||||
return ImmutableMap.copyOf(values);
|
||||
}
|
||||
}
|
||||
|
||||
class ThreadPoolInfo
|
||||
{
|
||||
private final String path;
|
||||
private final String poolName;
|
||||
|
||||
public ThreadPoolInfo(String path, String poolName)
|
||||
{
|
||||
this.path = path;
|
||||
this.poolName = poolName;
|
||||
}
|
||||
|
||||
public String path()
|
||||
{
|
||||
return path;
|
||||
}
|
||||
public String poolName()
|
||||
{
|
||||
return poolName;
|
||||
}
|
||||
public String scope()
|
||||
{
|
||||
return path + '.' + poolName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
import java.security.AccessController;
|
||||
import java.security.PrivilegedAction;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.ServiceLoader;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
import org.apache.cassandra.management.api.Command;
|
||||
import org.apache.cassandra.management.api.CommandRegistry;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
public final class ManagementUtils
|
||||
{
|
||||
public static final String COMMAND_NAME_DELIMITER = ".";
|
||||
|
||||
public static <S> Iterable<S> loadService(Class<S> serviceClz)
|
||||
{
|
||||
return AccessController.doPrivileged((PrivilegedAction<Iterable<S>>) () -> ServiceLoader.load(serviceClz));
|
||||
}
|
||||
|
||||
public static int countCommands(CommandRegistry registry)
|
||||
{
|
||||
int count = 0;
|
||||
for (Map.Entry<String, Command<?>> entry : registry.commands())
|
||||
{
|
||||
Command<?> cmd = entry.getValue();
|
||||
if (cmd instanceof CommandRegistry)
|
||||
count += countCommands((CommandRegistry) cmd);
|
||||
else
|
||||
count++;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
public static String stripAngleBrackets(String name)
|
||||
{
|
||||
if (name == null || name.isEmpty())
|
||||
return name;
|
||||
|
||||
String trimmed = name.trim();
|
||||
|
||||
if (trimmed.length() >= 2 &&
|
||||
trimmed.charAt(0) == '<' &&
|
||||
trimmed.charAt(trimmed.length() - 1) == '>')
|
||||
{
|
||||
return trimmed.substring(1, trimmed.length() - 1).trim();
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
/** Normalize the option name by stripping leading dashes. */
|
||||
public static String normalizeOptionName(String name)
|
||||
{
|
||||
if (name.startsWith("--"))
|
||||
return name.substring(2);
|
||||
else if (name.startsWith("-"))
|
||||
return name.substring(1);
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a concise, client-safe error string from a throwable's cause chain: the distinct, non-empty
|
||||
* messages of the throwable and its causes, joined by {@code ": "}. Unlike a full stack trace, this
|
||||
* exposes only the human-readable messages (which command validation/usage errors rely on) without
|
||||
* leaking internal class names, file paths or line numbers. The full stack trace is logged separately
|
||||
* server-side.
|
||||
*/
|
||||
public static String causeMessages(Throwable t)
|
||||
{
|
||||
Set<String> messages = new LinkedHashSet<>();
|
||||
for (Throwable cause : Throwables.getCausalChain(t))
|
||||
{
|
||||
String message = cause.getMessage();
|
||||
if (Strings.isNullOrEmpty(message))
|
||||
continue;
|
||||
// Skip a message already represented by an ancestor.
|
||||
if (messages.stream().anyMatch(existing -> existing.contains(message)))
|
||||
continue;
|
||||
messages.add(message);
|
||||
}
|
||||
return messages.isEmpty() ? t.getClass().getSimpleName() : String.join(": ", messages);
|
||||
}
|
||||
|
||||
public static String fullCommandName(String parent, String name)
|
||||
{
|
||||
return Strings.isNullOrEmpty(parent) ? name : String.join(COMMAND_NAME_DELIMITER, parent, name);
|
||||
}
|
||||
|
||||
public static <T> String fullCommandName(List<T> parentCommands, Function<T, String> nameExtractor)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (T part : parentCommands)
|
||||
{
|
||||
String extracted = nameExtractor.apply(part);
|
||||
if (Strings.isNullOrEmpty(extracted))
|
||||
continue;
|
||||
if (sb.length() > 0)
|
||||
sb.append(COMMAND_NAME_DELIMITER);
|
||||
sb.append(extracted);
|
||||
}
|
||||
return sb.length() > 0 ? sb.toString() : null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T> T findRegistryCommand(String fullCommandName, CommandRegistry registry)
|
||||
{
|
||||
String[] parts = fullCommandName.split('\\' + COMMAND_NAME_DELIMITER, 2);
|
||||
Command<?> cmd = registry.command(parts[0]);
|
||||
if (cmd == null)
|
||||
return null;
|
||||
|
||||
if (parts.length == 1)
|
||||
return (T) cmd;
|
||||
|
||||
if (cmd instanceof CommandRegistry)
|
||||
return findRegistryCommand(parts[1], (CommandRegistry) cmd);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Class<?> elementType(CommandLine.Model.ArgSpec spec)
|
||||
{
|
||||
Class<?> type = spec.type();
|
||||
if (!type.isArray() && !java.util.Collection.class.isAssignableFrom(type))
|
||||
return null;
|
||||
|
||||
Class<?>[] auxiliaryTypes = spec.auxiliaryTypes();
|
||||
return auxiliaryTypes == null || auxiliaryTypes.length == 0 ? null : auxiliaryTypes[0];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
|
||||
public class SimpleCommandExecutionArgs implements CommandExecutionArgs
|
||||
{
|
||||
private final Map<OptionMetadata, Object> options;
|
||||
private final Map<ParameterMetadata, Object> parameters;
|
||||
|
||||
public SimpleCommandExecutionArgs(Map<OptionMetadata, Object> options,
|
||||
Map<ParameterMetadata, Object> parameters)
|
||||
{
|
||||
this.options = new LinkedHashMap<>(options);
|
||||
this.parameters = new LinkedHashMap<>(parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<ParameterMetadata, Object> parameters()
|
||||
{
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<OptionMetadata, Object> options()
|
||||
{
|
||||
return options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasParameter(ParameterMetadata param)
|
||||
{
|
||||
return parameters.containsKey(param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasOption(OptionMetadata option)
|
||||
{
|
||||
return options.containsKey(option);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "SimpleCommandExecutionArgs{" +
|
||||
"options=" + options +
|
||||
", parameters=" + parameters +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
public interface ArgumentMetadata
|
||||
{
|
||||
String[] names();
|
||||
String paramLabel();
|
||||
|
||||
String description();
|
||||
Class<?> type();
|
||||
String defaultValue();
|
||||
boolean required();
|
||||
/** "0" (flag), "1", "0..1", "0..*" */
|
||||
String arity();
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
/**
|
||||
* A management command following the Command java-pattern. Each command is a self-describing,
|
||||
* executable unit that exposes its {@link CommandMetadata} and can be invoked uniformly by
|
||||
* the {@link org.apache.cassandra.management.CommandInvokerService} regardless of the
|
||||
* transport: JMX or CQL.
|
||||
* <p>
|
||||
* The type parameter {@code R} is the structured result type returned by
|
||||
* {@link #execute(CommandExecutionArgs, CommandExecutionContext)}. Existing picocli-based
|
||||
* commands return {@code Void} and write output via services obtained from the context;
|
||||
* new commands may return a typed result for protocol-level serialization.
|
||||
*
|
||||
* @param <R> the command result type, or {@link Void} for commands that only produce
|
||||
* side-effect output through the execution context
|
||||
* @see CommandRegistry
|
||||
*/
|
||||
public interface Command<R>
|
||||
{
|
||||
/** Get command metadata - replaces argClass() from the cep-38 doc with richer information. */
|
||||
CommandMetadata metadata();
|
||||
|
||||
/**
|
||||
* Execute the command.
|
||||
*
|
||||
* @param arguments command arguments.
|
||||
* @param context execution context providing access to services via {@link CommandExecutionContext#service(Class)}.
|
||||
* @return structured data result, or {@code null} for commands that only produce
|
||||
* side effect output (e.g. existing picocli-based commands).
|
||||
*/
|
||||
R execute(CommandExecutionArgs arguments, CommandExecutionContext context);
|
||||
|
||||
default String name() { return metadata().name(); }
|
||||
default String description() { return metadata().description(); }
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface CommandExecutionArgs
|
||||
{
|
||||
/** @return Map of parameter metadata to their values. */
|
||||
Map<ParameterMetadata, Object> parameters();
|
||||
/** @return Map of option metadata to their values. */
|
||||
Map<OptionMetadata, Object> options();
|
||||
|
||||
boolean hasParameter(ParameterMetadata param);
|
||||
boolean hasOption(OptionMetadata option);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Provides execution-time services to a {@link Command}. The context acts as a typed service
|
||||
* locator so that the API package ({@code management.api}) carries no compile-time dependency
|
||||
* on tool-specific classes such as {@code NodeProbe} or {@code Output}.
|
||||
* <p>
|
||||
* Adapter layers retrieve the concrete services they need:
|
||||
* <pre>{@code
|
||||
* NodeProbe probe = context.service(NodeProbe.class);
|
||||
* Output out = context.service(Output.class);
|
||||
* }</pre>
|
||||
* New commands that do not require legacy tool types can request MBean interfaces directly:
|
||||
* <pre>{@code
|
||||
* StorageServiceMBean ss = context.service(StorageServiceMBean.class);
|
||||
* }</pre>
|
||||
*/
|
||||
public interface CommandExecutionContext
|
||||
{
|
||||
/**
|
||||
* Returns the service instance of the requested type, or throws
|
||||
* {@link IllegalArgumentException} if the service is not available in this context.
|
||||
*
|
||||
* @param serviceType the class of the desired service
|
||||
* @param <T> service type
|
||||
* @return a non-null service instance
|
||||
* @throws IllegalArgumentException if the service is not available
|
||||
*/
|
||||
<T> T service(Class<T> serviceType);
|
||||
|
||||
/**
|
||||
* Retrieves the set of all service types that are available within the current execution context.
|
||||
* @return a set of classes representing the types of services accessible via this context.
|
||||
*/
|
||||
Set<Class<?>> services();
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* CommandMetadata is a protocol-agnostic descriptor of a management command's structure
|
||||
* and metadata information.
|
||||
* <p>
|
||||
* Each {@link Command} exposes a {@code CommandMetadata} that describes the command's name,
|
||||
* human-readable description, optional aliases, accepted options (named flags/values),
|
||||
* positional parameters, and subcommands.
|
||||
* <p>
|
||||
* This metadata is used by:
|
||||
* <ul>
|
||||
* <li>{@link org.apache.cassandra.management.CommandExecutionArgsSerde} to serialize/deserialize
|
||||
* command arguments to and from JSON or CQL row maps.</li>
|
||||
* <li>{@link org.apache.cassandra.management.CommandInvokerService} to look up and invoke
|
||||
* commands over existing MBean interfaces (only intefaces and corresponding implementaions
|
||||
* are used, JMX might be disabled).;</li>
|
||||
* <li>CQL syntax {@code COMMAND} to validate and convert user-supplied arguments.</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* The interface intentionally has no {@code parent()} accessor. Parent-child relationships are
|
||||
* owned by the {@link org.apache.cassandra.management.CassandraCommandRegistry} (and its
|
||||
* underlying {@link CommandRegistry} contract) rather than by individual commands, so that
|
||||
* a command can be registered in different registries.
|
||||
*/
|
||||
public interface CommandMetadata
|
||||
{
|
||||
String name();
|
||||
String description();
|
||||
|
||||
List<OptionMetadata> options();
|
||||
List<ParameterMetadata> parameters();
|
||||
List<CommandMetadata> subcommands();
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Registry that knows all of its subcommands and acts as a composite command for hierarchy.
|
||||
* Allows registering and retrieving commands by name.
|
||||
* <p>
|
||||
* For example, nodetool commands structure is like:
|
||||
* <pre>
|
||||
* NodetoolCommand (parent)
|
||||
* ├── setconcurrentcompactors (leaf command, implemented via Command interface)
|
||||
* ├── compressiondictionary (parent command, implemented via CommandRegistry interface)
|
||||
* │ ├── train (leaf)
|
||||
* │ ├── list (leaf)
|
||||
* │ ├── export (leaf)
|
||||
* │ └── import (leaf)
|
||||
* ├── decommission (parent)
|
||||
* │ └── abort (leaf)
|
||||
* └── ... 100+ more commands
|
||||
* </pre>
|
||||
*/
|
||||
public interface CommandRegistry extends Command<String>
|
||||
{
|
||||
<T> Command<T> command(String name);
|
||||
Iterable<Map.Entry<String, Command<?>>> commands();
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Pluggable component that is responsible for providing a list of commands for management API.
|
||||
*/
|
||||
public interface CommandsProvider
|
||||
{
|
||||
/** Gets all supported by these provider commands. */
|
||||
Collection<Command<?>> commands();
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
public interface OptionMetadata extends ArgumentMetadata
|
||||
{
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.api;
|
||||
|
||||
public interface ParameterMetadata extends ArgumentMetadata
|
||||
{
|
||||
String COMMAND_POSITIONAL_PARAM_PREFIX = "param";
|
||||
int index();
|
||||
}
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.apache.cassandra.management.api.Command;
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.CommandExecutionContext;
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.tools.NodeProbe;
|
||||
import org.apache.cassandra.tools.Output;
|
||||
import org.apache.cassandra.tools.nodetool.AbstractCommand;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
public class PicocliCommandAdapter implements Command<Void>
|
||||
{
|
||||
private final Class<? extends AbstractCommand> commandClass;
|
||||
private final CommandMetadata commandMetadata;
|
||||
|
||||
public PicocliCommandAdapter(Class<? extends AbstractCommand> commandClass)
|
||||
{
|
||||
this.commandClass = commandClass;
|
||||
this.commandMetadata = PicocliCommandMetadata.from(commandClass);
|
||||
if (!(commandMetadata instanceof PicocliCommandMetadata))
|
||||
throw new IllegalStateException("CommandMetadata must be PicocliCommandMetadata for picocli commands");
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandMetadata metadata()
|
||||
{
|
||||
return commandMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void execute(CommandExecutionArgs arguments, CommandExecutionContext context)
|
||||
{
|
||||
Output output = context.service(Output.class);
|
||||
CommandLine commandLine = new CommandLine(commandClass, new InjectCassandraContext(output));
|
||||
Object userCommand = commandLine.getCommand();
|
||||
|
||||
if (userCommand instanceof AbstractCommand)
|
||||
((AbstractCommand) userCommand).probe(context.service(NodeProbe.class));
|
||||
|
||||
PicocliCommandArgsConverter.toCommand(arguments, userCommand);
|
||||
|
||||
// Alternatively, we can convert CommandExecutionArgs to String[] and let picocli handle
|
||||
// the parsing and execution; however, Cassandra validates a lot of arguments inside the
|
||||
// command's execution body (historically this was done because of Airline limitations).
|
||||
//
|
||||
// Double parsing and validation of arguments is not ideal, and it doesn't make sense
|
||||
// to rely on picocli either. Verdict: it doesn't make sense to rely on picocli argument
|
||||
// validation and error handling here.
|
||||
if (userCommand instanceof Runnable)
|
||||
{
|
||||
((Runnable) userCommand).run();
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException(String.format("Unsupported command type: %s. " +
|
||||
"Command class must implement Runnable to be executed.",
|
||||
userCommand.getClass().getName()));
|
||||
}
|
||||
}
|
||||
|
||||
private static class InjectCassandraContext implements CommandLine.IFactory
|
||||
{
|
||||
private final Output output;
|
||||
private final CommandLine.IFactory fallback;
|
||||
|
||||
public InjectCassandraContext(Output output)
|
||||
{
|
||||
this.fallback = CommandLine.defaultFactory();
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <K> K create(Class<K> cls)
|
||||
{
|
||||
try
|
||||
{
|
||||
K bean = this.fallback.create(cls);
|
||||
Class<?> beanClass = bean.getClass();
|
||||
do
|
||||
{
|
||||
Field[] fields = beanClass.getDeclaredFields();
|
||||
for (Field field : fields)
|
||||
{
|
||||
if (!field.isAnnotationPresent(Inject.class))
|
||||
continue;
|
||||
|
||||
field.setAccessible(true);
|
||||
if (field.getType().equals(Output.class))
|
||||
{
|
||||
field.set(bean, output);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new RuntimeException("Unsupported injectable field type: " + field.getType() +
|
||||
" in class " + beanClass.getName() + ". " +
|
||||
"Only Output is supported for injection.");
|
||||
}
|
||||
}
|
||||
}
|
||||
while ((beanClass = beanClass.getSuperclass()) != null);
|
||||
return bean;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new CommandLine.InitializationException("Failed to create instance of " + cls, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.cassandra.management.SimpleCommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
|
||||
public class PicocliCommandArgsConverter
|
||||
{
|
||||
/**
|
||||
* Extract command arguments from an AbstractCommand instance using picocli reflection.
|
||||
*
|
||||
* @param command the command instance to extract arguments from
|
||||
* @return CommandExecutionArgs containing all options and parameters from the command
|
||||
*/
|
||||
public static <T> CommandExecutionArgs fromCommand(T command)
|
||||
{
|
||||
CommandMetadata metadata = PicocliCommandMetadata.from(command);
|
||||
PicocliCommandMetadata picocliMetadata = (PicocliCommandMetadata) metadata;
|
||||
CommandSpec spec = picocliMetadata.getCommandSpec();
|
||||
|
||||
Map<OptionMetadata, Object> options = new LinkedHashMap<>();
|
||||
Map<ParameterMetadata, Object> parameters = new LinkedHashMap<>();
|
||||
|
||||
for (CommandLine.Model.OptionSpec optionSpec : spec.options())
|
||||
{
|
||||
if (!optionSpec.isOption())
|
||||
continue;
|
||||
|
||||
Object value = optionSpec.getValue();
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
if (optionSpec.type() == boolean.class || optionSpec.type() == Boolean.class)
|
||||
{
|
||||
if (Boolean.TRUE.equals(value))
|
||||
options.put(new PicocliOptionMetadata(optionSpec), Boolean.TRUE);
|
||||
}
|
||||
else
|
||||
{
|
||||
options.put(new PicocliOptionMetadata(optionSpec), value);
|
||||
}
|
||||
}
|
||||
|
||||
for (CommandLine.Model.PositionalParamSpec paramSpec : spec.positionalParameters())
|
||||
{
|
||||
if (!paramSpec.isPositional())
|
||||
continue;
|
||||
|
||||
Object value = paramSpec.getValue();
|
||||
if (value != null)
|
||||
parameters.put(new PicocliParameterMetadata(paramSpec), value);
|
||||
}
|
||||
|
||||
return new SimpleCommandExecutionArgs(options, parameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate an AbstractCommand instance with values from CommandExecutionArgs using picocli setters.
|
||||
*
|
||||
* @param args the CommandExecutionArgs containing values to set
|
||||
* @param command the command instance to populate
|
||||
*/
|
||||
public static <T> void toCommand(CommandExecutionArgs args, T command)
|
||||
{
|
||||
CommandMetadata metadata = PicocliCommandMetadata.from(command);
|
||||
PicocliCommandMetadata picocliMetadata = (PicocliCommandMetadata) metadata;
|
||||
CommandSpec spec = picocliMetadata.getCommandSpec();
|
||||
Map<OptionMetadata, CommandLine.Model.OptionSpec> optionMap = buildOptionSpecMap(spec);
|
||||
Map<ParameterMetadata, CommandLine.Model.PositionalParamSpec> paramMap = buildParameterSpecMap(spec);
|
||||
|
||||
for (Map.Entry<OptionMetadata, Object> entry : args.options().entrySet())
|
||||
{
|
||||
OptionMetadata optionMetadata = entry.getKey();
|
||||
// This value is already converted to the correct type in CommandExecutionArgs.
|
||||
Object convertedValue = entry.getValue();
|
||||
|
||||
if (convertedValue == null)
|
||||
continue;
|
||||
|
||||
CommandLine.Model.OptionSpec optionSpec = optionMap.get(optionMetadata);
|
||||
if (optionSpec == null)
|
||||
{
|
||||
optionSpec = findOptionSpecByName(spec, optionMetadata.names());
|
||||
if (optionSpec == null)
|
||||
throw new IllegalArgumentException("Option not found in command spec: " + Arrays.toString(optionMetadata.names()));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
optionSpec.setter().set(convertedValue);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("Failed to set option " + optionMetadata.names()[0] +
|
||||
" with value " + convertedValue, e);
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<ParameterMetadata, Object> entry : args.parameters().entrySet())
|
||||
{
|
||||
ParameterMetadata paramMetadata = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
CommandLine.Model.PositionalParamSpec paramSpec = paramMap.get(paramMetadata);
|
||||
if (paramSpec == null)
|
||||
{
|
||||
paramSpec = findParameterSpecByIndex(spec, paramMetadata.index());
|
||||
if (paramSpec == null)
|
||||
throw new IllegalArgumentException("Parameter not found in command spec at index: " + paramMetadata.index());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
paramSpec.setter().set(value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new RuntimeException("Failed to set parameter at index " + paramMetadata.index() +
|
||||
" with value " + value, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<OptionMetadata, CommandLine.Model.OptionSpec> buildOptionSpecMap(CommandSpec spec)
|
||||
{
|
||||
Map<OptionMetadata, CommandLine.Model.OptionSpec> map = new LinkedHashMap<>();
|
||||
for (CommandLine.Model.OptionSpec optionSpec : spec.options())
|
||||
{
|
||||
if (optionSpec.isOption())
|
||||
map.put(new PicocliOptionMetadata(optionSpec), optionSpec);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static Map<ParameterMetadata, CommandLine.Model.PositionalParamSpec> buildParameterSpecMap(CommandSpec spec)
|
||||
{
|
||||
Map<ParameterMetadata, CommandLine.Model.PositionalParamSpec> map = new LinkedHashMap<>();
|
||||
for (CommandLine.Model.PositionalParamSpec paramSpec : spec.positionalParameters())
|
||||
{
|
||||
if (paramSpec.isPositional())
|
||||
map.put(new PicocliParameterMetadata(paramSpec), paramSpec);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private static CommandLine.Model.OptionSpec findOptionSpecByName(CommandSpec spec, String[] names)
|
||||
{
|
||||
for (CommandLine.Model.OptionSpec optionSpec : spec.options())
|
||||
{
|
||||
for (String name : names)
|
||||
{
|
||||
if (java.util.Arrays.asList(optionSpec.names()).contains(name))
|
||||
return optionSpec;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static CommandLine.Model.PositionalParamSpec findParameterSpecByIndex(CommandSpec spec, int index)
|
||||
{
|
||||
for (CommandLine.Model.PositionalParamSpec paramSpec : spec.positionalParameters())
|
||||
{
|
||||
CommandLine.Range indexRange = paramSpec.index();
|
||||
if (index >= indexRange.min() && index <= indexRange.max())
|
||||
return paramSpec;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
|
||||
/**
|
||||
* Implementation of CommandMetadata that extracts metadata from picocli CommandSpec.
|
||||
*/
|
||||
public class PicocliCommandMetadata implements CommandMetadata
|
||||
{
|
||||
private final CommandSpec commandSpec;
|
||||
|
||||
public PicocliCommandMetadata(CommandSpec commandSpec)
|
||||
{
|
||||
this.commandSpec = commandSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CommandMetadata from a command class.
|
||||
*/
|
||||
public static CommandMetadata from(Class<?> commandClass)
|
||||
{
|
||||
CommandSpec spec = CommandSpec.forAnnotatedObject(commandClass);
|
||||
return new PicocliCommandMetadata(spec);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create CommandMetadata from a command instance.
|
||||
*/
|
||||
public static CommandMetadata from(Object commandInstance)
|
||||
{
|
||||
CommandSpec spec = CommandSpec.forAnnotatedObject(commandInstance);
|
||||
return new PicocliCommandMetadata(spec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String name()
|
||||
{
|
||||
return commandSpec.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
String[] description = commandSpec.usageMessage().description();
|
||||
if (description == null || description.length == 0)
|
||||
return "";
|
||||
return String.join("\n", description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<OptionMetadata> options()
|
||||
{
|
||||
List<OptionMetadata> options = new ArrayList<>();
|
||||
for (CommandLine.Model.OptionSpec option : commandSpec.options())
|
||||
{
|
||||
if (option.isOption())
|
||||
options.add(new PicocliOptionMetadata(option));
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ParameterMetadata> parameters()
|
||||
{
|
||||
List<ParameterMetadata> parameters = new ArrayList<>();
|
||||
for (CommandLine.Model.PositionalParamSpec positional : commandSpec.positionalParameters())
|
||||
{
|
||||
if (positional.isPositional())
|
||||
parameters.add(new PicocliParameterMetadata(positional));
|
||||
}
|
||||
return parameters;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CommandMetadata> subcommands()
|
||||
{
|
||||
return commandSpec.subcommands().values().stream()
|
||||
.map(subcommand -> new PicocliCommandMetadata(subcommand.getCommandSpec()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying CommandSpec for advanced usage.
|
||||
*/
|
||||
public CommandSpec getCommandSpec()
|
||||
{
|
||||
return commandSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (!(o instanceof PicocliCommandMetadata)) return false;
|
||||
PicocliCommandMetadata that = (PicocliCommandMetadata) o;
|
||||
return Objects.equals(commandSpec, that.commandSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(commandSpec);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.apache.cassandra.management.api.Command;
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.CommandExecutionContext;
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.management.api.CommandRegistry;
|
||||
import org.apache.cassandra.tools.nodetool.AbstractCommand;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
|
||||
/**
|
||||
* Adapter that wraps a picocli command with subcommands and implements CommandRegistry
|
||||
* using the Composite pattern. This allows hierarchical picocli commands to be used
|
||||
* as CommandRegistry instances.
|
||||
* <p>
|
||||
* For example, {@code CompressionDictionaryCommandGroup} has subcommands (train, list, export, import),
|
||||
* so it would be wrapped by this adapter to provide a CommandRegistry interface.
|
||||
* <p>
|
||||
* The adapter recursively adapts subcommands:
|
||||
* <ul>
|
||||
* <li>If a subcommand has subcommands -> creates another {@code PicocliCommandRegistryAdapter}</li>
|
||||
* <li>If a subcommand is a leaf -> creates a {@code PicocliCommandAdapter}</li>
|
||||
* </ul>
|
||||
*/
|
||||
public class PicocliCommandRegistryAdapter implements CommandRegistry
|
||||
{
|
||||
private final CommandSpec commandSpec;
|
||||
private final CommandMetadata commandMetadata;
|
||||
private final Map<String, Command<?>> subcommandMap = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Create an adapter for a picocli command class that has subcommands.
|
||||
* @param commandClass the picocli command class (must have subcommands)
|
||||
*/
|
||||
public PicocliCommandRegistryAdapter(Class<? extends AbstractCommand> commandClass)
|
||||
{
|
||||
this.commandSpec = CommandSpec.forAnnotatedObject(commandClass);
|
||||
this.commandMetadata = new PicocliCommandMetadata(commandSpec);
|
||||
|
||||
if (commandSpec.subcommands().isEmpty())
|
||||
{
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Command class %s does not have subcommands. " +
|
||||
"Use PicocliCommandAdapter for leaf commands.",
|
||||
commandClass.getName()));
|
||||
}
|
||||
|
||||
for (Map.Entry<String, CommandLine> e : commandSpec.subcommands().entrySet())
|
||||
adaptSubcommands(e.getKey(), e.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively adapt all subcommands from the picocli command. Uses a Composite pattern:
|
||||
* subcommands with subcommands become registries, leaf subcommands become command adapters.
|
||||
*/
|
||||
private void adaptSubcommands(String commandName, CommandLine command)
|
||||
{
|
||||
CommandSpec subcommandSpec = command.getCommandSpec();
|
||||
Command<?> adaptedCommand;
|
||||
|
||||
if (!subcommandSpec.subcommands().isEmpty())
|
||||
{
|
||||
adaptedCommand = new PicocliCommandRegistryAdapter(command.getCommand());
|
||||
}
|
||||
else
|
||||
{
|
||||
Class<?> subcommandClass = command.getCommand().getClass();
|
||||
if (!AbstractCommand.class.isAssignableFrom(subcommandClass))
|
||||
{
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Subcommand class %s is not an AbstractCommand and cannot be adapted. " +
|
||||
"Only AbstractCommand subclasses are supported for leaf commands.",
|
||||
subcommandClass.getName()));
|
||||
}
|
||||
|
||||
adaptedCommand = new PicocliCommandAdapter((Class<? extends AbstractCommand>) subcommandClass);
|
||||
}
|
||||
|
||||
subcommandMap.put(commandName, adaptedCommand);
|
||||
|
||||
for (String alias : subcommandSpec.aliases())
|
||||
subcommandMap.putIfAbsent(alias, adaptedCommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommandMetadata metadata()
|
||||
{
|
||||
return commandMetadata;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String execute(CommandExecutionArgs arguments, CommandExecutionContext context)
|
||||
{
|
||||
// CommandRegistry is not directly executable: routing happens at the invoker/CQL layer.
|
||||
throw new UnsupportedOperationException(
|
||||
String.format("CommandRegistry '%s' is not directly executable. " +
|
||||
"Specify a subcommand. Available commands: %s",
|
||||
name(),
|
||||
String.join(", ", subcommandMap.keySet())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Command<?> command(String name)
|
||||
{
|
||||
return subcommandMap.get(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterable<Map.Entry<String, Command<?>>> commands()
|
||||
{
|
||||
return subcommandMap.entrySet();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.cassandra.management.api.Command;
|
||||
import org.apache.cassandra.management.api.CommandsProvider;
|
||||
import org.apache.cassandra.tools.nodetool.AbstractCommand;
|
||||
import org.apache.cassandra.tools.nodetool.NodetoolCommand;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
public class PicocliCommandsProvider implements CommandsProvider
|
||||
{
|
||||
@Override
|
||||
public Collection<Command<?>> commands()
|
||||
{
|
||||
CommandLine commandLine = new CommandLine(NodetoolCommand.class);
|
||||
List<Command<?>> commands = new ArrayList<>();
|
||||
|
||||
commandLine.getSubcommands().forEach((name, subcommandLine) -> {
|
||||
if (!subcommandLine.getCommandSpec().subcommands().isEmpty())
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends AbstractCommand> abstractCommandClass =
|
||||
(Class<? extends AbstractCommand>) subcommandLine.getCommand().getClass();
|
||||
commands.add(new PicocliCommandRegistryAdapter(abstractCommandClass));
|
||||
}
|
||||
else
|
||||
{
|
||||
Class<?> commandClass = subcommandLine.getCommand().getClass();
|
||||
if (AbstractCommand.class.isAssignableFrom(commandClass))
|
||||
{
|
||||
@SuppressWarnings("unchecked")
|
||||
Class<? extends AbstractCommand> abstractCommandClass =
|
||||
(Class<? extends AbstractCommand>) commandClass;
|
||||
commands.add(new PicocliCommandAdapter(abstractCommandClass));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import org.apache.cassandra.management.api.CommandMetadata;
|
||||
import org.apache.cassandra.tools.nodetool.AbstractCommand;
|
||||
|
||||
/**
|
||||
* Utility class for extracting command metadata from picocli-annotated command classes.
|
||||
*/
|
||||
public class PicocliMetadataExtractor
|
||||
{
|
||||
/**
|
||||
* Extract CommandMetadata from an AbstractCommand class.
|
||||
*
|
||||
* @param commandClass The command class annotated with picocli @Command
|
||||
* @return CommandMetadata extracted from the command class
|
||||
*/
|
||||
public static CommandMetadata extract(Class<? extends AbstractCommand> commandClass)
|
||||
{
|
||||
return PicocliCommandMetadata.from(commandClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract CommandMetadata from a command instance.
|
||||
*
|
||||
* @param commandInstance The command instance
|
||||
* @return CommandMetadata extracted from the command instance
|
||||
*/
|
||||
public static CommandMetadata extract(Object commandInstance)
|
||||
{
|
||||
if (commandInstance instanceof AbstractCommand)
|
||||
return PicocliCommandMetadata.from(commandInstance);
|
||||
throw new IllegalArgumentException("Unsupported command instance type: " + commandInstance.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Model.OptionSpec;
|
||||
|
||||
import static org.apache.cassandra.management.ManagementUtils.elementType;
|
||||
import static org.apache.cassandra.management.ManagementUtils.stripAngleBrackets;
|
||||
|
||||
/**
|
||||
* Implementation of OptionMetadata that extracts metadata from picocli OptionSpec.
|
||||
*/
|
||||
public class PicocliOptionMetadata implements OptionMetadata
|
||||
{
|
||||
private final OptionSpec optionSpec;
|
||||
|
||||
public PicocliOptionMetadata(OptionSpec optionSpec)
|
||||
{
|
||||
this.optionSpec = optionSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] names()
|
||||
{
|
||||
return optionSpec.names();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
String[] description = optionSpec.description();
|
||||
if (description == null || description.length == 0)
|
||||
return "";
|
||||
return String.join("\n", description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> type()
|
||||
{
|
||||
return optionSpec.type();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String defaultValue()
|
||||
{
|
||||
Object defaultValue = optionSpec.defaultValue();
|
||||
if (defaultValue == null)
|
||||
{
|
||||
if (optionSpec.type() == boolean.class || optionSpec.type() == Boolean.class)
|
||||
return "false";
|
||||
return "";
|
||||
}
|
||||
|
||||
return defaultValue.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean required()
|
||||
{
|
||||
return optionSpec.required();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String arity()
|
||||
{
|
||||
CommandLine.Range arity = optionSpec.arity();
|
||||
int min = arity.min();
|
||||
int max = arity.max();
|
||||
|
||||
if (min == 0 && max == 0)
|
||||
return "0";
|
||||
|
||||
if (min == max)
|
||||
return String.valueOf(min);
|
||||
else if (max == Integer.MAX_VALUE)
|
||||
return min + "..*";
|
||||
else
|
||||
return min + ".." + max;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String paramLabel()
|
||||
{
|
||||
String paramLabel = optionSpec.paramLabel();
|
||||
Object userObject = optionSpec.userObject();
|
||||
|
||||
return Strings.isNullOrEmpty(paramLabel) ?
|
||||
((java.lang.reflect.Field) userObject).getName() :
|
||||
stripAngleBrackets(paramLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a boolean flag option.
|
||||
*/
|
||||
public boolean isFlag()
|
||||
{
|
||||
return optionSpec.arity().max() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying OptionSpec for advanced usage.
|
||||
*/
|
||||
public OptionSpec getOptionSpec()
|
||||
{
|
||||
return optionSpec;
|
||||
}
|
||||
|
||||
public Object convertValue(Object value) throws Exception
|
||||
{
|
||||
TypeConverter<?> customConverter = typeConverter() == null ? null : typeConverter()[0];
|
||||
return customConverter == null ?
|
||||
TypeConverterRegistry.convertValueBasic(value, type(), elementType(optionSpec)) :
|
||||
customConverter.convert(value.toString());
|
||||
}
|
||||
|
||||
private TypeConverter<?>[] typeConverter()
|
||||
{
|
||||
return TypeConverter.createFrom(optionSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (!(o instanceof PicocliOptionMetadata)) return false;
|
||||
PicocliOptionMetadata that = (PicocliOptionMetadata) o;
|
||||
return Objects.equals(optionSpec, that.optionSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(optionSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "PicocliOptionMetadata{" +
|
||||
"names=" + Arrays.toString(names()) +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Model.PositionalParamSpec;
|
||||
|
||||
import static org.apache.cassandra.management.ManagementUtils.elementType;
|
||||
import static org.apache.cassandra.management.ManagementUtils.stripAngleBrackets;
|
||||
|
||||
/**
|
||||
* Implementation of ParameterMetadata that extracts metadata from picocli PositionalParamSpec.
|
||||
*/
|
||||
public class PicocliParameterMetadata implements ParameterMetadata
|
||||
{
|
||||
private final PositionalParamSpec positionalParamSpec;
|
||||
|
||||
public PicocliParameterMetadata(PositionalParamSpec positionalParamSpec)
|
||||
{
|
||||
this.positionalParamSpec = positionalParamSpec;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] names()
|
||||
{
|
||||
String name;
|
||||
String paramLabel = positionalParamSpec.paramLabel();
|
||||
Object userObject = positionalParamSpec.userObject();
|
||||
|
||||
if (paramLabel != null && !paramLabel.isEmpty())
|
||||
name = stripAngleBrackets(paramLabel);
|
||||
else if (userObject instanceof java.lang.reflect.Field)
|
||||
name = ((java.lang.reflect.Field) userObject).getName();
|
||||
else
|
||||
name = COMMAND_POSITIONAL_PARAM_PREFIX + index();
|
||||
|
||||
return new String[] { name };
|
||||
}
|
||||
|
||||
@Override
|
||||
public String paramLabel()
|
||||
{
|
||||
return stripAngleBrackets(positionalParamSpec.paramLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String description()
|
||||
{
|
||||
String[] description = positionalParamSpec.description();
|
||||
if (description == null || description.length == 0)
|
||||
return "";
|
||||
return String.join("\n", description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> type()
|
||||
{
|
||||
return positionalParamSpec.type();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String defaultValue()
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int index()
|
||||
{
|
||||
return positionalParamSpec.index().min();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean required()
|
||||
{
|
||||
return positionalParamSpec.arity().min() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String arity()
|
||||
{
|
||||
CommandLine.Range arity = positionalParamSpec.arity();
|
||||
int min = arity.min();
|
||||
int max = arity.max();
|
||||
|
||||
if (min == max)
|
||||
return String.valueOf(min);
|
||||
else if (max == Integer.MAX_VALUE)
|
||||
return min + "..*";
|
||||
else
|
||||
return min + ".." + max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying PositionalParamSpec for advanced usage.
|
||||
*/
|
||||
public PositionalParamSpec getPositionalParamSpec()
|
||||
{
|
||||
return positionalParamSpec;
|
||||
}
|
||||
|
||||
public Object convertValue(Object value) throws Exception
|
||||
{
|
||||
TypeConverter<?> customConverter = typeConverter() == null ? null : typeConverter()[0];
|
||||
return customConverter == null ?
|
||||
TypeConverterRegistry.convertValueBasic(value, type(), elementType(positionalParamSpec)) :
|
||||
customConverter.convert(value.toString());
|
||||
}
|
||||
|
||||
private TypeConverter<?>[] typeConverter()
|
||||
{
|
||||
return TypeConverter.createFrom(positionalParamSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o)
|
||||
{
|
||||
if (!(o instanceof PicocliParameterMetadata)) return false;
|
||||
PicocliParameterMetadata that = (PicocliParameterMetadata) o;
|
||||
return Objects.equals(positionalParamSpec, that.positionalParamSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(positionalParamSpec);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
return "PicocliParameterMetadata{" +
|
||||
"names=" + Arrays.toString(names()) +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
public interface TypeConverter<R>
|
||||
{
|
||||
R convert(String value) throws Exception;
|
||||
|
||||
static TypeConverter<?>[] createFrom(CommandLine.Model.ArgSpec argSpec)
|
||||
{
|
||||
CommandLine.ITypeConverter<?>[] converters = argSpec.converters();
|
||||
return converters == null || converters.length == 0 ?
|
||||
null :
|
||||
Arrays.stream(converters)
|
||||
.map(c -> (TypeConverter<Object>) c::convert)
|
||||
.toArray(TypeConverter<?>[]::new);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.management.picocli;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class TypeConverterRegistry
|
||||
{
|
||||
private static final Map<Class<?>, TypeConverter<?>> converters = new LinkedHashMap<>();
|
||||
|
||||
static
|
||||
{
|
||||
register(boolean.class, (TypeConverter<Boolean>) Boolean::parseBoolean);
|
||||
register(int.class, (TypeConverter<Integer>) Integer::parseInt);
|
||||
register(long.class, (TypeConverter<Long>) Long::parseLong);
|
||||
register(double.class, (TypeConverter<Double>) Double::parseDouble);
|
||||
register(float.class, (TypeConverter<Float>) Float::parseFloat);
|
||||
register(byte.class, (TypeConverter<Byte>) Byte::parseByte);
|
||||
register(short.class, (TypeConverter<Short>) Short::parseShort);
|
||||
register(char.class, value -> {
|
||||
if (value.length() != 1)
|
||||
throw new IllegalArgumentException("Cannot convert to char: " + value);
|
||||
return value.charAt(0);
|
||||
});
|
||||
register(Boolean.class, (TypeConverter<Boolean>) Boolean::parseBoolean);
|
||||
register(Integer.class, (TypeConverter<Integer>) Integer::parseInt);
|
||||
register(Long.class, (TypeConverter<Long>) Long::parseLong);
|
||||
register(Double.class, (TypeConverter<Double>) Double::parseDouble);
|
||||
register(Float.class, (TypeConverter<Float>) Float::parseFloat);
|
||||
register(Byte.class, (TypeConverter<Byte>) Byte::parseByte);
|
||||
register(Short.class, (TypeConverter<Short>) Short::parseShort);
|
||||
register(String.class, (TypeConverter<String>) value -> value);
|
||||
register(Character.class, value -> {
|
||||
if (value.length() != 1)
|
||||
throw new IllegalArgumentException("Cannot convert to Character: " + value);
|
||||
return value.charAt(0);
|
||||
});
|
||||
register(Object.class, value -> value);
|
||||
}
|
||||
|
||||
private static void register(Class<?> type, TypeConverter<?> converter)
|
||||
{
|
||||
converters.put(type, converter);
|
||||
}
|
||||
|
||||
public static <T> Object convertValueBasic(Object value, Class<T> targetType) throws Exception
|
||||
{
|
||||
return convertValueBasic(value, targetType, null);
|
||||
}
|
||||
|
||||
public static <T> Object convertValueBasic(Object value, Class<T> targetType, Class<?> elementType) throws Exception
|
||||
{
|
||||
if (value == null)
|
||||
return null;
|
||||
|
||||
// Arrays and collections must be handled before the assignable short-circuit below: a
|
||||
// List<String> is already assignable to List, but its elements still need per-element
|
||||
// conversion when the declared element type is not String.
|
||||
if (targetType.isArray() || Collection.class.isAssignableFrom(targetType))
|
||||
return convertToArrayOrCollection(value, targetType, elementType);
|
||||
|
||||
if (targetType.isAssignableFrom(value.getClass()))
|
||||
return value;
|
||||
|
||||
String stringValue = value.toString();
|
||||
if (isBasicType(targetType))
|
||||
{
|
||||
TypeConverter<?> converter = converters.get(targetType);
|
||||
if (converter == null)
|
||||
throw new IllegalStateException(String.format("No converter registered for basic type: %s",
|
||||
targetType.getName()));
|
||||
return converter.convert(stringValue);
|
||||
}
|
||||
|
||||
if (targetType.isEnum())
|
||||
return getEnumTypeConverter(targetType).convert(stringValue);
|
||||
|
||||
throw new IllegalArgumentException(String.format("No converter available for type: %s.", targetType.getName()));
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static <T> TypeConverter<T> getEnumTypeConverter(Class<T> type)
|
||||
{
|
||||
return value -> {
|
||||
for (T constant : type.getEnumConstants())
|
||||
if (((Enum<?>) constant).name().equalsIgnoreCase(value))
|
||||
return constant;
|
||||
throw new IllegalArgumentException("No enum constant " + type.getCanonicalName() + '.' + value);
|
||||
};
|
||||
}
|
||||
|
||||
private static Object convertToArrayOrCollection(Object value, Class<?> targetType, Class<?> elementType) throws Exception
|
||||
{
|
||||
Collection<?> sourceCollection;
|
||||
if (value instanceof Collection)
|
||||
sourceCollection = (Collection<?>) value;
|
||||
else if (value.getClass().isArray())
|
||||
{
|
||||
int length = Array.getLength(value);
|
||||
List<Object> list = new ArrayList<>(length);
|
||||
for (int i = 0; i < length; i++)
|
||||
list.add(Array.get(value, i));
|
||||
sourceCollection = list;
|
||||
}
|
||||
else
|
||||
throw new IllegalArgumentException("Cannot convert " + value.getClass() + " to " + targetType);
|
||||
|
||||
if (targetType.isArray())
|
||||
{
|
||||
Class<?> componentType = targetType.getComponentType();
|
||||
Object targetArray = Array.newInstance(componentType, sourceCollection.size());
|
||||
int index = 0;
|
||||
for (Object item : sourceCollection)
|
||||
{
|
||||
Object convertedItem = convertValueBasic(item, componentType);
|
||||
Array.set(targetArray, index++, convertedItem);
|
||||
}
|
||||
return targetArray;
|
||||
}
|
||||
|
||||
Collection<Object> targetCollection;
|
||||
if (targetType == List.class || List.class.isAssignableFrom(targetType))
|
||||
targetCollection = new ArrayList<>();
|
||||
else if (SortedSet.class.isAssignableFrom(targetType))
|
||||
targetCollection = new TreeSet<>();
|
||||
else if (targetType == Set.class || Set.class.isAssignableFrom(targetType))
|
||||
targetCollection = new LinkedHashSet<>();
|
||||
else
|
||||
targetCollection = new ArrayList<>();
|
||||
|
||||
if (elementType == null)
|
||||
targetCollection.addAll(sourceCollection);
|
||||
else
|
||||
for (Object item : sourceCollection)
|
||||
targetCollection.add(convertValueBasic(item, elementType));
|
||||
|
||||
return targetCollection;
|
||||
}
|
||||
|
||||
private static boolean isBasicType(Class<?> type)
|
||||
{
|
||||
return type == String.class ||
|
||||
type == boolean.class || type == Boolean.class ||
|
||||
type == int.class || type == Integer.class ||
|
||||
type == long.class || type == Long.class ||
|
||||
type == double.class || type == Double.class ||
|
||||
type == float.class || type == Float.class ||
|
||||
type == byte.class || type == Byte.class ||
|
||||
type == short.class || type == Short.class ||
|
||||
type == char.class || type == Character.class;
|
||||
}
|
||||
}
|
||||
|
|
@ -80,6 +80,7 @@ import org.apache.cassandra.io.util.File;
|
|||
import org.apache.cassandra.io.util.FileUtils;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.locator.Locator;
|
||||
import org.apache.cassandra.management.CommandInvokerService;
|
||||
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
|
||||
import org.apache.cassandra.metrics.DefaultNameFactory;
|
||||
import org.apache.cassandra.net.StartupClusterConnectivityChecker;
|
||||
|
|
@ -99,6 +100,7 @@ import org.apache.cassandra.tcm.RegistrationStatus;
|
|||
import org.apache.cassandra.tcm.Startup;
|
||||
import org.apache.cassandra.tcm.membership.NodeId;
|
||||
import org.apache.cassandra.tcm.membership.NodeState;
|
||||
import org.apache.cassandra.transport.Dispatcher;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
import org.apache.cassandra.utils.JMXServerUtils;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
|
|
@ -218,6 +220,7 @@ public class CassandraDaemon
|
|||
|
||||
static final CassandraDaemon instance = new CassandraDaemon();
|
||||
|
||||
private volatile NativeTransportManagementService nativeTransportManagementService;
|
||||
private volatile NativeTransportService nativeTransportService;
|
||||
private JMXConnectorServer jmxServer;
|
||||
|
||||
|
|
@ -397,6 +400,9 @@ public class CassandraDaemon
|
|||
// re-enable auto-compaction after replay, so correct disk boundaries are used
|
||||
enableAutoCompaction(Schema.instance.getKeyspaces());
|
||||
|
||||
// Initialize command service (after JMX, before StorageService.initServer)
|
||||
CommandInvokerService.instance.start();
|
||||
|
||||
// start server internals
|
||||
StorageService.instance.registerDaemon(this);
|
||||
try
|
||||
|
|
@ -668,6 +674,9 @@ public class CassandraDaemon
|
|||
|
||||
public synchronized void initializeClientTransports()
|
||||
{
|
||||
if (nativeTransportManagementService == null)
|
||||
nativeTransportManagementService = new NativeTransportManagementService();
|
||||
|
||||
// Native transport
|
||||
if (nativeTransportService == null)
|
||||
nativeTransportService = new NativeTransportService();
|
||||
|
|
@ -755,6 +764,9 @@ public class CassandraDaemon
|
|||
Set<InetAddressAndPort> peers = new HashSet<>(ClusterMetadata.current().directory.allJoinedEndpoints());
|
||||
connectivityChecker.execute(peers, ep -> locator.location(ep).datacenter);
|
||||
|
||||
// start management transports first.
|
||||
startManagementTransport();
|
||||
|
||||
// check to see if transports may start else return without starting. This is needed when in survey mode or
|
||||
// when bootstrap has not completed.
|
||||
try
|
||||
|
|
@ -783,6 +795,16 @@ public class CassandraDaemon
|
|||
logger.info("Not starting native transport as requested. Use JMX (StorageService->startNativeTransport()) or nodetool (enablebinary) to start it");
|
||||
}
|
||||
|
||||
private void startManagementTransport()
|
||||
{
|
||||
if (NativeTransportManagementService.enabled())
|
||||
{
|
||||
if (nativeTransportManagementService == null)
|
||||
throw new IllegalStateException("setup() must be called first for CassandraDaemon");
|
||||
nativeTransportManagementService.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the daemon, ideally in an idempotent manner.
|
||||
*
|
||||
|
|
@ -794,6 +816,7 @@ public class CassandraDaemon
|
|||
// jsvc takes care of taking the rest down
|
||||
logger.info("Cassandra shutting down...");
|
||||
destroyClientTransports();
|
||||
CommandInvokerService.instance.stop();
|
||||
StorageService.instance.setRpcReady(false);
|
||||
|
||||
if (jmxServer != null)
|
||||
|
|
@ -815,6 +838,9 @@ public class CassandraDaemon
|
|||
stopNativeTransport();
|
||||
if (nativeTransportService != null)
|
||||
nativeTransportService.destroy();
|
||||
if (nativeTransportManagementService != null)
|
||||
nativeTransportManagementService.destroy();
|
||||
Dispatcher.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -995,12 +1021,18 @@ public class CassandraDaemon
|
|||
|
||||
public void clearConnectionHistory()
|
||||
{
|
||||
nativeTransportService.clearConnectionHistory();
|
||||
if (nativeTransportService != null)
|
||||
nativeTransportService.clearConnectionHistory();
|
||||
if (nativeTransportManagementService != null)
|
||||
nativeTransportManagementService.clearConnectionHistory();
|
||||
}
|
||||
|
||||
public void disconnectUser(Predicate<AuthenticatedUser> userPredicate)
|
||||
{
|
||||
nativeTransportService.disconnect(userPredicate);
|
||||
if (nativeTransportService != null)
|
||||
nativeTransportService.disconnect(userPredicate);
|
||||
if (nativeTransportManagementService != null)
|
||||
nativeTransportManagementService.disconnect(userPredicate);
|
||||
}
|
||||
|
||||
private void exitOrFail(int code, String message)
|
||||
|
|
|
|||
|
|
@ -148,6 +148,9 @@ public class ClientState
|
|||
private volatile String driverName;
|
||||
private volatile String driverVersion;
|
||||
|
||||
// Whether this client is connected via the management native transport port
|
||||
private volatile boolean isManagement;
|
||||
|
||||
// Options provided by the client
|
||||
private volatile Map<String,String> clientOptions;
|
||||
|
||||
|
|
@ -212,6 +215,7 @@ public class ClientState
|
|||
this.driverName = source.driverName;
|
||||
this.driverVersion = source.driverVersion;
|
||||
this.clientOptions = source.clientOptions;
|
||||
this.isManagement = source.isManagement;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -357,6 +361,16 @@ public class ClientState
|
|||
this.driverVersion = driverVersion;
|
||||
}
|
||||
|
||||
public boolean isManagement()
|
||||
{
|
||||
return isManagement;
|
||||
}
|
||||
|
||||
public void setManagement(boolean isManagement)
|
||||
{
|
||||
this.isManagement = isManagement;
|
||||
}
|
||||
|
||||
public void setClientOptions(Map<String,String> clientOptions)
|
||||
{
|
||||
this.clientOptions = ImmutableMap.copyOf(clientOptions);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.service;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.auth.AuthenticatedUser;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.transport.Server;
|
||||
|
||||
import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.epoll.EpollEventLoopGroup;
|
||||
import io.netty.channel.nio.NioEventLoopGroup;
|
||||
import io.netty.util.Version;
|
||||
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.START_NATIVE_MANAGEMENT_TRANSPORT;
|
||||
import static org.apache.cassandra.service.NativeTransportService.useEpoll;
|
||||
|
||||
/**
|
||||
* Manages the native management transport server on port {@code 11211}.
|
||||
* This service is independent of NativeTransportService and can only be
|
||||
* enabled/disabled via configuration or system property (not at runtime).
|
||||
*/
|
||||
public class NativeTransportManagementService implements CassandraDaemon.Server
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(NativeTransportManagementService.class);
|
||||
|
||||
private Server server = null;
|
||||
private EventLoopGroup workerGroup;
|
||||
|
||||
private volatile boolean initialized = false;
|
||||
|
||||
@VisibleForTesting
|
||||
synchronized void initialize()
|
||||
{
|
||||
if (initialized)
|
||||
return;
|
||||
|
||||
if (useEpoll())
|
||||
{
|
||||
workerGroup = new EpollEventLoopGroup();
|
||||
logger.info("Management transport Netty using native Epoll event loop");
|
||||
}
|
||||
else
|
||||
{
|
||||
workerGroup = new NioEventLoopGroup();
|
||||
logger.info("Management transport Netty using Java NIO event loop");
|
||||
}
|
||||
|
||||
logger.info("Management transport Netty Version: {}", Version.identify().entrySet());
|
||||
|
||||
int managementPort = DatabaseDescriptor.getNativeTransportManagementPort();
|
||||
InetAddress addr = DatabaseDescriptor.getRpcManagementAddress();
|
||||
|
||||
EncryptionOptions.TlsEncryptionPolicy encryptionPolicy = DatabaseDescriptor.getNativeProtocolEncryptionOptions()
|
||||
.tlsEncryptionPolicy();
|
||||
|
||||
server = new Server.Builder()
|
||||
.withEventLoopGroup(workerGroup)
|
||||
.withHost(addr)
|
||||
.withTlsEncryptionPolicy(encryptionPolicy)
|
||||
.withPort(managementPort)
|
||||
.withManagementConnectionFlag(true)
|
||||
.build();
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether the management transport should start: the
|
||||
* {@code cassandra.start_native_management_transport} system property wins when set,
|
||||
* otherwise the {@code start_native_transport_management} yaml setting decides.
|
||||
*/
|
||||
public static boolean enabled()
|
||||
{
|
||||
if (START_NATIVE_MANAGEMENT_TRANSPORT.getString() != null)
|
||||
return START_NATIVE_MANAGEMENT_TRANSPORT.getBoolean();
|
||||
return DatabaseDescriptor.startNativeTransportManagement();
|
||||
}
|
||||
|
||||
public void start()
|
||||
{
|
||||
if (!enabled())
|
||||
{
|
||||
logger.info("Management transport is disabled via configuration and will not start.");
|
||||
return;
|
||||
}
|
||||
|
||||
initialize();
|
||||
server.start();
|
||||
logger.info("Management transport started on port: {}", server.socket.getPort());
|
||||
}
|
||||
|
||||
public void stop()
|
||||
{
|
||||
if (server != null)
|
||||
server.stop(false);
|
||||
}
|
||||
|
||||
public void destroy()
|
||||
{
|
||||
stop();
|
||||
if (server == null)
|
||||
return;
|
||||
|
||||
server = null;
|
||||
if (workerGroup != null)
|
||||
workerGroup.shutdownGracefully(3, 5, TimeUnit.SECONDS).awaitUninterruptibly();
|
||||
|
||||
initialized = false;
|
||||
}
|
||||
|
||||
/** @return true if the management transport server is running. */
|
||||
public boolean isRunning()
|
||||
{
|
||||
return server != null && server.isRunning();
|
||||
}
|
||||
|
||||
/** Clears connection history for this service's server. */
|
||||
public void clearConnectionHistory()
|
||||
{
|
||||
if (server != null)
|
||||
server.clearConnectionHistory();
|
||||
}
|
||||
|
||||
/** Disconnects users matching the predicate from this service's server. */
|
||||
public void disconnect(Predicate<AuthenticatedUser> userPredicate)
|
||||
{
|
||||
if (server != null)
|
||||
server.disconnect(userPredicate);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public EventLoopGroup getWorkerGroup()
|
||||
{
|
||||
return workerGroup;
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,6 @@ import org.apache.cassandra.auth.AuthenticatedUser;
|
|||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.metrics.ClientMetrics;
|
||||
import org.apache.cassandra.transport.Dispatcher;
|
||||
import org.apache.cassandra.transport.Server;
|
||||
import org.apache.cassandra.utils.NativeLibrary;
|
||||
|
||||
|
|
@ -45,7 +44,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.NATIVE_EPO
|
|||
/**
|
||||
* Handles native transport server lifecycle and associated resources. Lazily initialized.
|
||||
*/
|
||||
public class NativeTransportService
|
||||
public class NativeTransportService implements CassandraDaemon.Server
|
||||
{
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(NativeTransportService.class);
|
||||
|
|
@ -126,8 +125,6 @@ public class NativeTransportService
|
|||
// shutdown executors used by netty for native transport server
|
||||
if (workerGroup != null)
|
||||
workerGroup.shutdownGracefully(3, 5, TimeUnit.SECONDS).awaitUninterruptibly();
|
||||
|
||||
Dispatcher.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -32,11 +32,11 @@ class NodeProbeFactory implements INodeProbeFactory
|
|||
|
||||
public NodeProbe create(String host, int port) throws IOException
|
||||
{
|
||||
return new NodeProbe(host, port);
|
||||
return new NodeProbe(new RemoteJmxMBeanAccessor(host, port));
|
||||
}
|
||||
|
||||
public NodeProbe create(String host, int port, String username, String password) throws IOException
|
||||
{
|
||||
return new NodeProbe(host, port, username, password);
|
||||
return new NodeProbe(new RemoteJmxMBeanAccessor(host, port, username, password));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -39,9 +39,13 @@ import org.apache.cassandra.config.CassandraRelevantProperties;
|
|||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.io.util.File;
|
||||
import org.apache.cassandra.io.util.FileWriter;
|
||||
import org.apache.cassandra.tools.nodetool.CqlConnect;
|
||||
import org.apache.cassandra.tools.nodetool.JmxConnect;
|
||||
import org.apache.cassandra.tools.nodetool.NodetoolCommand;
|
||||
import org.apache.cassandra.tools.nodetool.layout.CassandraCliHelpLayout;
|
||||
import org.apache.cassandra.tools.nodetool.strategy.CommandExecutionStrategy;
|
||||
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
|
||||
import org.apache.cassandra.tools.nodetool.strategy.ProtocolAwareExecutionStrategy;
|
||||
import org.apache.cassandra.utils.FBUtilities;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
|
@ -104,7 +108,7 @@ public class NodeTool
|
|||
commandLine.setErr(new PrintWriter(output.err, true));
|
||||
|
||||
configureCliLayout(commandLine);
|
||||
commandLine.setExecutionStrategy(JmxConnect::executionStrategy)
|
||||
commandLine.setExecutionStrategy(ProtocolAwareExecutionStrategy::executionStrategy)
|
||||
.setExecutionExceptionHandler((ex, c, arg) -> {
|
||||
// Used for backward compatibility, some commands are validated when a command is run.
|
||||
if (ex instanceof IllegalArgumentException |
|
||||
|
|
@ -114,7 +118,25 @@ public class NodeTool
|
|||
return 1;
|
||||
}
|
||||
|
||||
err(Throwables.getRootCause(ex));
|
||||
NodetoolConnectionException connectFailure = Throwables.getCausalChain(ex).stream()
|
||||
.filter(NodetoolConnectionException.class::isInstance)
|
||||
.map(NodetoolConnectionException.class::cast)
|
||||
.findFirst().orElse(null);
|
||||
if (connectFailure != null)
|
||||
{
|
||||
output.err.println("nodetool: " + connectFailure.getMessage());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// CASSANDRA-11537 friendly error message when server is not ready
|
||||
Throwable root = Throwables.getRootCause(ex);
|
||||
if (root instanceof InstanceNotFoundException)
|
||||
{
|
||||
badUse(new IllegalArgumentException("Server is not initialized yet, cannot run nodetool."));
|
||||
return 1;
|
||||
}
|
||||
|
||||
err(root);
|
||||
return 2;
|
||||
})
|
||||
.setParameterExceptionHandler((ex, arg) -> {
|
||||
|
|
@ -195,8 +217,18 @@ public class NodeTool
|
|||
|
||||
public static CommandLine createCommandLine(CommandLine.IFactory factory) throws Exception
|
||||
{
|
||||
return new CommandLine(new NodetoolCommand(), factory)
|
||||
.addMixin(JmxConnect.MIXIN_KEY, factory.create(JmxConnect.class));
|
||||
CommandLine commandLine = new CommandLine(new NodetoolCommand(), factory);
|
||||
CommandExecutionStrategy.Type strategyType = ProtocolAwareExecutionStrategy.getExecutionStrategyTypeFromEnvAndSys();
|
||||
switch (strategyType)
|
||||
{
|
||||
case CQL:
|
||||
return commandLine.addMixin(strategyType.toString(), factory.create(CqlConnect.class));
|
||||
case STATIC_MBEAN:
|
||||
case COMMAND_MBEAN:
|
||||
return commandLine.addMixin(strategyType.toString(), factory.create(JmxConnect.class));
|
||||
default:
|
||||
throw new IllegalStateException("Unknown execution strategy: " + strategyType);
|
||||
}
|
||||
}
|
||||
|
||||
private static void configureCliLayout(CommandLine commandLine)
|
||||
|
|
@ -229,10 +261,6 @@ public class NodeTool
|
|||
|
||||
protected void err(Throwable e)
|
||||
{
|
||||
// CASSANDRA-11537: friendly error message when server is not ready
|
||||
if (e instanceof InstanceNotFoundException)
|
||||
throw new IllegalArgumentException("Server is not initialized yet, cannot run nodetool.");
|
||||
|
||||
output.err.println("error: " + e.getMessage());
|
||||
output.err.println("-- StackTrace --");
|
||||
output.err.println(getStackTraceAsString(e));
|
||||
|
|
|
|||
|
|
@ -33,11 +33,21 @@ public class Output
|
|||
this.err = err;
|
||||
}
|
||||
|
||||
public void printInfo(String msg)
|
||||
{
|
||||
out.println(msg);
|
||||
}
|
||||
|
||||
public void printInfo(String msg, Object... args)
|
||||
{
|
||||
out.printf(msg, args);
|
||||
}
|
||||
|
||||
public void printError(String msg)
|
||||
{
|
||||
err.println(msg);
|
||||
}
|
||||
|
||||
public void printError(String msg, Object... args)
|
||||
{
|
||||
err.printf(msg, args);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,479 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.rmi.ConnectException;
|
||||
import java.rmi.server.RMIClientSocketFactory;
|
||||
import java.rmi.server.RMISocketFactory;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Hashtable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.management.JMX;
|
||||
import javax.management.MBeanServerConnection;
|
||||
import javax.management.MalformedObjectNameException;
|
||||
import javax.management.ObjectName;
|
||||
import javax.management.remote.JMXConnector;
|
||||
import javax.management.remote.JMXConnectorFactory;
|
||||
import javax.management.remote.JMXServiceURL;
|
||||
import javax.rmi.ssl.SslRMIClientSocketFactory;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.audit.AuditLogManager;
|
||||
import org.apache.cassandra.audit.AuditLogManagerMBean;
|
||||
import org.apache.cassandra.auth.AuthCache;
|
||||
import org.apache.cassandra.auth.CIDRGroupsMappingManager;
|
||||
import org.apache.cassandra.auth.CIDRGroupsMappingManagerMBean;
|
||||
import org.apache.cassandra.auth.CIDRPermissionsManager;
|
||||
import org.apache.cassandra.auth.CIDRPermissionsManagerMBean;
|
||||
import org.apache.cassandra.auth.NetworkPermissionsCache;
|
||||
import org.apache.cassandra.auth.NetworkPermissionsCacheMBean;
|
||||
import org.apache.cassandra.auth.PasswordAuthenticator;
|
||||
import org.apache.cassandra.auth.PermissionsCache;
|
||||
import org.apache.cassandra.auth.PermissionsCacheMBean;
|
||||
import org.apache.cassandra.auth.RolesCache;
|
||||
import org.apache.cassandra.auth.RolesCacheMBean;
|
||||
import org.apache.cassandra.auth.jmx.AuthorizationProxy;
|
||||
import org.apache.cassandra.batchlog.BatchlogManager;
|
||||
import org.apache.cassandra.batchlog.BatchlogManagerMBean;
|
||||
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
|
||||
import org.apache.cassandra.db.compaction.CompactionManager;
|
||||
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
|
||||
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
|
||||
import org.apache.cassandra.db.guardrails.Guardrails;
|
||||
import org.apache.cassandra.db.guardrails.GuardrailsMBean;
|
||||
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTable;
|
||||
import org.apache.cassandra.db.virtual.CIDRFilteringMetricsTableMBean;
|
||||
import org.apache.cassandra.gms.FailureDetector;
|
||||
import org.apache.cassandra.gms.FailureDetectorMBean;
|
||||
import org.apache.cassandra.gms.Gossiper;
|
||||
import org.apache.cassandra.gms.GossiperMBean;
|
||||
import org.apache.cassandra.hints.HintsService;
|
||||
import org.apache.cassandra.hints.HintsServiceMBean;
|
||||
import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
|
||||
import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
|
||||
import org.apache.cassandra.locator.LocationInfoMBean;
|
||||
import org.apache.cassandra.management.MBeanAccessor;
|
||||
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.MessagingServiceMBean;
|
||||
import org.apache.cassandra.profiler.AsyncProfilerMBean;
|
||||
import org.apache.cassandra.service.ActiveRepairServiceMBean;
|
||||
import org.apache.cassandra.service.AutoRepairService;
|
||||
import org.apache.cassandra.service.AutoRepairServiceMBean;
|
||||
import org.apache.cassandra.service.CacheService;
|
||||
import org.apache.cassandra.service.CacheServiceMBean;
|
||||
import org.apache.cassandra.service.GCInspector;
|
||||
import org.apache.cassandra.service.GCInspectorMXBean;
|
||||
import org.apache.cassandra.service.StorageProxy;
|
||||
import org.apache.cassandra.service.StorageProxyMBean;
|
||||
import org.apache.cassandra.service.StorageServiceMBean;
|
||||
import org.apache.cassandra.service.accord.AccordOperations;
|
||||
import org.apache.cassandra.service.accord.AccordOperationsMBean;
|
||||
import org.apache.cassandra.service.snapshot.SnapshotManagerMBean;
|
||||
import org.apache.cassandra.streaming.StreamManagerMBean;
|
||||
import org.apache.cassandra.tcm.CMSOperations;
|
||||
import org.apache.cassandra.tcm.CMSOperationsMBean;
|
||||
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
|
||||
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.SSL_ENABLE;
|
||||
|
||||
public class RemoteJmxMBeanAccessor implements MBeanAccessor
|
||||
{
|
||||
private final Map<Class<?>, Object> clazzMBanRegistry = new HashMap<>();
|
||||
private final Map<String, Object> namedMBeanRegistry = new ConcurrentHashMap<>();
|
||||
|
||||
public static final int defaultPort = 7199;
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(RemoteJmxMBeanAccessor.class);
|
||||
private static final String fmtUrl = "service:jmx:rmi:///jndi/rmi://%s:%d/jmxrmi";
|
||||
|
||||
final String host;
|
||||
final int port;
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
protected JMXConnector jmxc;
|
||||
protected MBeanServerConnection mbeanServerConn;
|
||||
|
||||
private volatile boolean connected = false;
|
||||
|
||||
/**
|
||||
* Creates a NodeProbe using the specified JMX host, port, username, and password.
|
||||
*
|
||||
* @param host hostname or IP address of the JMX agent
|
||||
* @param port TCP port of the remote JMX agent
|
||||
*/
|
||||
public RemoteJmxMBeanAccessor(String host, int port, String username, String password)
|
||||
{
|
||||
assert username != null && !username.isEmpty() && password != null && !password.isEmpty()
|
||||
: "neither username nor password can be blank";
|
||||
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a NodeProbe using the specified JMX host and port.
|
||||
*
|
||||
* @param host hostname or IP address of the JMX agent
|
||||
* @param port TCP port of the remote JMX agent
|
||||
*/
|
||||
public RemoteJmxMBeanAccessor(String host, int port)
|
||||
{
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a NodeProbe using the specified JMX host and default port.
|
||||
*
|
||||
* @param host hostname or IP address of the JMX agent
|
||||
*/
|
||||
public RemoteJmxMBeanAccessor(String host)
|
||||
{
|
||||
this(host, defaultPort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a connection to the JMX agent and set up the M[X]Bean proxies.
|
||||
*/
|
||||
protected void connect()
|
||||
{
|
||||
if (connected)
|
||||
return;
|
||||
|
||||
synchronized (this)
|
||||
{
|
||||
if (connected)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
String host = this.host;
|
||||
if (host.contains(":"))
|
||||
{
|
||||
// Use square brackets to surround IPv6 addresses to fix CASSANDRA-7669 and CASSANDRA-17581
|
||||
host = '[' + host + ']';
|
||||
}
|
||||
JMXServiceURL jmxUrl = new JMXServiceURL(String.format(fmtUrl, host, port));
|
||||
Map<String, Object> env = new HashMap<>();
|
||||
if (username != null)
|
||||
{
|
||||
String[] creds = { username, password };
|
||||
env.put(JMXConnector.CREDENTIALS, creds);
|
||||
}
|
||||
|
||||
env.put("com.sun.jndi.rmi.factory.socket", getRMIClientSocketFactory());
|
||||
|
||||
jmxc = JMXConnectorFactory.connect(jmxUrl, env);
|
||||
mbeanServerConn = jmxc.getMBeanServerConnection();
|
||||
|
||||
registerMBeanProxy(StorageServiceMBean.class, "org.apache.cassandra.db:type=StorageService");
|
||||
registerMBeanProxy(SnapshotManagerMBean.class, SnapshotManagerMBean.MBEAN_NAME);
|
||||
registerMBeanProxy(CMSOperationsMBean.class, CMSOperations.MBEAN_OBJECT_NAME);
|
||||
registerMBeanProxy(AccordOperationsMBean.class, AccordOperations.MBEAN_OBJECT_NAME);
|
||||
registerMBeanProxy(MessagingServiceMBean.class, MessagingService.MBEAN_NAME);
|
||||
registerMBeanProxy(StreamManagerMBean.class, StreamManagerMBean.OBJECT_NAME);
|
||||
registerMBeanProxy(CompactionManagerMBean.class, CompactionManager.MBEAN_OBJECT_NAME);
|
||||
registerMBeanProxy(FailureDetectorMBean.class, FailureDetector.MBEAN_NAME);
|
||||
registerMBeanProxy(CacheServiceMBean.class, CacheService.MBEAN_NAME);
|
||||
registerMBeanProxy(StorageProxyMBean.class, StorageProxy.MBEAN_NAME);
|
||||
registerMBeanProxy(HintsServiceMBean.class, HintsService.MBEAN_NAME);
|
||||
registerMBeanProxy(GCInspectorMXBean.class, GCInspector.MBEAN_NAME);
|
||||
registerMBeanProxy(GossiperMBean.class, Gossiper.MBEAN_NAME);
|
||||
registerMBeanProxy(BatchlogManagerMBean.class, BatchlogManager.MBEAN_NAME);
|
||||
registerMBeanProxy(ActiveRepairServiceMBean.class, ActiveRepairServiceMBean.MBEAN_NAME);
|
||||
registerMBeanProxy(AuditLogManagerMBean.class, AuditLogManager.MBEAN_NAME);
|
||||
registerMBeanProxy(PasswordAuthenticator.CredentialsCacheMBean.class,
|
||||
AuthCache.MBEAN_NAME_BASE + PasswordAuthenticator.CredentialsCacheMBean.CACHE_NAME);
|
||||
registerMBeanProxy(AuthorizationProxy.JmxPermissionsCacheMBean.class,
|
||||
AuthCache.MBEAN_NAME_BASE + AuthorizationProxy.JmxPermissionsCacheMBean.CACHE_NAME);
|
||||
registerMBeanProxy(NetworkPermissionsCacheMBean.class,
|
||||
AuthCache.MBEAN_NAME_BASE + NetworkPermissionsCache.CACHE_NAME);
|
||||
registerMBeanProxy(PermissionsCacheMBean.class,
|
||||
AuthCache.MBEAN_NAME_BASE + PermissionsCache.CACHE_NAME);
|
||||
registerMBeanProxy(RolesCacheMBean.class,
|
||||
AuthCache.MBEAN_NAME_BASE + RolesCache.CACHE_NAME);
|
||||
registerMBeanProxy(CIDRPermissionsManagerMBean.class, CIDRPermissionsManager.MBEAN_NAME);
|
||||
registerMBeanProxy(CIDRGroupsMappingManagerMBean.class, CIDRGroupsMappingManager.MBEAN_NAME);
|
||||
registerMBeanProxy(CIDRFilteringMetricsTableMBean.class, CIDRFilteringMetricsTable.MBEAN_NAME);
|
||||
registerMBeanProxy(AutoRepairServiceMBean.class, AutoRepairService.MBEAN_NAME);
|
||||
registerMBeanProxy(GuardrailsMBean.class, Guardrails.MBEAN_NAME);
|
||||
|
||||
registerPlatformMBeanProxy(MemoryMXBean.class, ManagementFactory.MEMORY_MXBEAN_NAME);
|
||||
registerPlatformMBeanProxy(RuntimeMXBean.class, ManagementFactory.RUNTIME_MXBEAN_NAME);
|
||||
|
||||
registerMBeanProxy(EndpointSnitchInfoMBean.class, "org.apache.cassandra.db:type=EndpointSnitchInfo");
|
||||
registerMBeanProxy(DynamicEndpointSnitchMBean.class, "org.apache.cassandra.db:type=DynamicEndpointSnitch");
|
||||
registerMBeanProxy(LocationInfoMBean.class, "org.apache.cassandra.db:type=LocationInfo");
|
||||
registerMBeanProxy(AsyncProfilerMBean.class, AsyncProfilerMBean.MBEAN_NAME);
|
||||
}
|
||||
catch (MalformedObjectNameException e)
|
||||
{
|
||||
close();
|
||||
throw new RuntimeException("Invalid ObjectName? Please report this as a bug.", e);
|
||||
}
|
||||
catch (IOException | SecurityException e)
|
||||
{
|
||||
close();
|
||||
Throwable rootCause = Throwables.getRootCause(e);
|
||||
throw new NodetoolConnectionException(String.format("Failed to connect to '%s:%s' - %s: '%s'.",
|
||||
host, port,
|
||||
rootCause.getClass().getSimpleName(),
|
||||
rootCause.getMessage()),
|
||||
e);
|
||||
}
|
||||
|
||||
connected = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> void registerMBean(Class<T> clazz, T mbean)
|
||||
{
|
||||
clazzMBanRegistry.put(clazz, mbean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findMBean(Class<T> clazz)
|
||||
{
|
||||
connect();
|
||||
return clazzMBanRegistry.get(clazz) == null ? null : clazz.cast(clazzMBanRegistry.get(clazz));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T findMBeanMetric(Class<T> clazz, Props props)
|
||||
{
|
||||
return withExceptionHandling(() -> {
|
||||
connect();
|
||||
ObjectName objectName = new ObjectName("org.apache.cassandra.metrics", new Hashtable<>(props.toMap()));
|
||||
T result = (T) namedMBeanRegistry.computeIfAbsent(objectName.getCanonicalName(),
|
||||
ignore -> JMX.newMBeanProxy(mbeanServerConn, objectName, clazz));
|
||||
return clazz.cast(result);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMBeanMetricRegistered(Props props)
|
||||
{
|
||||
return withExceptionHandling(() -> {
|
||||
connect();
|
||||
ObjectName objectName = new ObjectName("org.apache.cassandra.metrics", new Hashtable<>(props.toMap()));
|
||||
return mbeanServerConn.isRegistered(objectName);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraMetricsRegistry.JmxCounterMBean findMBeanCounter(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxCounterMBean.class, props);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraMetricsRegistry.JmxGaugeMBean findMBeanGauge(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxGaugeMBean.class, props);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraMetricsRegistry.JmxMeterMBean findMBeanMeter(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxMeterMBean.class, props);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraMetricsRegistry.JmxTimerMBean findMBeanTimer(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxTimerMBean.class, props);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CassandraMetricsRegistry.JmxHistogramMBean findMBeanHistogram(Props props)
|
||||
{
|
||||
return findMBeanMetric(CassandraMetricsRegistry.JmxHistogramMBean.class, props);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnFamilyStoreMBean findColumnFamily(String type, String keyspace, String columnFamily)
|
||||
{
|
||||
return withExceptionHandling(() -> {
|
||||
connect();
|
||||
Set<ObjectName> beans = mbeanServerConn.queryNames(new ObjectName("org.apache.cassandra.db:type=*" + type +
|
||||
",keyspace=" + keyspace +
|
||||
",columnfamily=" + columnFamily), null);
|
||||
if (beans.isEmpty())
|
||||
throw new MalformedObjectNameException("couldn't find that bean");
|
||||
|
||||
assert beans.size() == 1;
|
||||
return JMX.newMBeanProxy(mbeanServerConn, beans.iterator().next(), ColumnFamilyStoreMBean.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompressionDictionaryManagerMBean findCompressionDictionary(String keyspace, String table)
|
||||
{
|
||||
List<Map.Entry<String, ColumnFamilyStoreMBean>> keyspaces = findColumnFamilies("ColumnFamilies");
|
||||
Optional<ColumnFamilyStoreMBean> cfsMBean = keyspaces.stream()
|
||||
.filter(e -> e.getKey().equals(keyspace))
|
||||
.map(Map.Entry::getValue)
|
||||
.filter(mbean -> mbean.getTableName().equals(table))
|
||||
.findAny();
|
||||
if (keyspaces.isEmpty() || cfsMBean.isEmpty())
|
||||
throw new IllegalArgumentException(String.format("Table %s.%s does not exist", keyspace, table));
|
||||
|
||||
return withExceptionHandling(() -> {
|
||||
connect();
|
||||
String mbeanName = CompressionDictionaryManagerMBean.MBEAN_NAME + ",keyspace=" + keyspace + ",table=" + table;
|
||||
if (!mbeanServerConn.isRegistered(new ObjectName(mbeanName)))
|
||||
throw new IllegalStateException("The compression on table " + keyspace + '.' + table + " is not enabled or SSTable compressor is not a dictionary compressor.");
|
||||
return JMX.newMBeanProxy(mbeanServerConn, new ObjectName(mbeanName), CompressionDictionaryManagerMBean.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ThreadPoolInfo> threadPoolInfos()
|
||||
{
|
||||
return withExceptionHandling(() -> {
|
||||
connect();
|
||||
Set<ObjectName> threadPoolObjectNames = mbeanServerConn.queryNames(new ObjectName("org.apache.cassandra.metrics:type=ThreadPools,*"), null);
|
||||
return threadPoolObjectNames.stream()
|
||||
.map(oName -> new ThreadPoolInfo(oName.getKeyProperty("path"), oName.getKeyProperty("scope")))
|
||||
.collect(Collectors.toList());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map.Entry<String, ColumnFamilyStoreMBean>> findColumnFamilies(String type)
|
||||
{
|
||||
return withExceptionHandling(() -> {
|
||||
assert type.equals("IndexColumnFamilies") || type.equals("ColumnFamilies");
|
||||
connect();
|
||||
|
||||
ObjectName query = new ObjectName("org.apache.cassandra.db:type=" + type + ",*");
|
||||
Set<ObjectName> cfObjects = mbeanServerConn.queryNames(query, null);
|
||||
|
||||
List<Map.Entry<String, ColumnFamilyStoreMBean>> mbeans = new ArrayList<>(cfObjects.size());
|
||||
for (ObjectName objectName : cfObjects)
|
||||
{
|
||||
ColumnFamilyStoreMBean cfsProxy = JMX.newMBeanProxy(mbeanServerConn, objectName, ColumnFamilyStoreMBean.class);
|
||||
mbeans.add(new AbstractMap.SimpleImmutableEntry<>(objectName.getKeyProperty("keyspace"), cfsProxy));
|
||||
}
|
||||
return mbeans;
|
||||
});
|
||||
}
|
||||
|
||||
public JMXConnector getJmxConnector()
|
||||
{
|
||||
connect();
|
||||
return jmxc;
|
||||
}
|
||||
|
||||
public MBeanServerConnection getMBeanServerConnection()
|
||||
{
|
||||
connect();
|
||||
return mbeanServerConn;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close()
|
||||
{
|
||||
if (jmxc == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
jmxc.close();
|
||||
}
|
||||
catch (ConnectException e)
|
||||
{
|
||||
// result of stopdaemon command, if close() call fails, the daemon is shutdown
|
||||
logger.error("Cassandra has shutdown.");
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
logger.error("Failed to close connection to '{}:{}'.", host, port, e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
jmxc = null;
|
||||
mbeanServerConn = null;
|
||||
connected = false;
|
||||
clazzMBanRegistry.clear();
|
||||
namedMBeanRegistry.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private <T> void registerMBeanProxy(Class<T> clazz, String objectName) throws MalformedObjectNameException
|
||||
{
|
||||
registerMBean(clazz, JMX.newMBeanProxy(mbeanServerConn, new ObjectName(objectName), clazz));
|
||||
}
|
||||
|
||||
private <T> void registerPlatformMBeanProxy(Class<T> clazz, String objectName) throws IOException
|
||||
{
|
||||
registerMBean(clazz, ManagementFactory.newPlatformMXBeanProxy(mbeanServerConn, objectName, clazz));
|
||||
}
|
||||
|
||||
private RMIClientSocketFactory getRMIClientSocketFactory()
|
||||
{
|
||||
if (SSL_ENABLE.getBoolean())
|
||||
return new SslRMIClientSocketFactory();
|
||||
else
|
||||
return RMISocketFactory.getDefaultSocketFactory();
|
||||
}
|
||||
|
||||
private static <T> T withExceptionHandling(MBeanSupplier<T> op)
|
||||
{
|
||||
try
|
||||
{
|
||||
return op.get();
|
||||
}
|
||||
catch (MalformedObjectNameException e)
|
||||
{
|
||||
throw new RuntimeException("Invalid ObjectName? Requested MBean may not exist. " +
|
||||
"Please check the parameters e.g. keyspace name, table name and try again.", e);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
throw new RuntimeException("Could not connect to MBean server. Please check that the JMX port is correct and open.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface MBeanSupplier<T>
|
||||
{
|
||||
T get() throws MalformedObjectNameException, IOException;
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ import java.util.List;
|
|||
import javax.management.ListenerNotFoundException;
|
||||
import javax.management.remote.JMXConnector;
|
||||
|
||||
import org.apache.cassandra.management.MBeanAccessor;
|
||||
import org.apache.cassandra.service.ActiveRepairService.ParentRepairStatus;
|
||||
import org.apache.cassandra.service.StorageServiceMBean;
|
||||
import org.apache.cassandra.utils.Closeable;
|
||||
|
|
@ -34,9 +35,9 @@ import org.apache.cassandra.utils.progress.ProgressEventType;
|
|||
import org.apache.cassandra.utils.progress.jmx.JMXNotificationProgressListener;
|
||||
|
||||
import static java.util.concurrent.TimeUnit.SECONDS;
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS;
|
||||
import static org.apache.cassandra.service.ActiveRepairService.ParentRepairStatus.FAILED;
|
||||
import static org.apache.cassandra.service.ActiveRepairService.ParentRepairStatus.valueOf;
|
||||
import static org.apache.cassandra.tools.NodeProbe.JMX_NOTIFICATION_POLL_INTERVAL_SECONDS;
|
||||
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
|
||||
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
|
||||
import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition;
|
||||
|
|
@ -46,6 +47,8 @@ import static org.apache.cassandra.utils.progress.ProgressEventType.PROGRESS;
|
|||
|
||||
public class RepairRunner extends JMXNotificationProgressListener implements Closeable
|
||||
{
|
||||
private final long JMX_NOTIFICATION_POLL_INTERVAL_SECONDS = NODETOOL_JMX_NOTIFICATION_POLL_INTERVAL_SECONDS.getLong();
|
||||
|
||||
public static abstract class RepairCmd
|
||||
{
|
||||
private final String keyspace;
|
||||
|
|
@ -60,6 +63,7 @@ public class RepairRunner extends JMXNotificationProgressListener implements Clo
|
|||
private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss,SSS");
|
||||
|
||||
private final PrintStream out;
|
||||
/** The connector to monitor the JMX connection state, so it can be {@code null} for server-side tools. */
|
||||
private final JMXConnector jmxc;
|
||||
private final StorageServiceMBean ssProxy;
|
||||
private final Condition condition = newOneTimeCondition();
|
||||
|
|
@ -68,10 +72,10 @@ public class RepairRunner extends JMXNotificationProgressListener implements Clo
|
|||
private Integer cmd;
|
||||
private volatile Exception error;
|
||||
|
||||
public RepairRunner(PrintStream out, JMXConnector jmxc, StorageServiceMBean ssProxy, RepairCmd repairCmd)
|
||||
public RepairRunner(PrintStream out, MBeanAccessor accessor, StorageServiceMBean ssProxy, RepairCmd repairCmd)
|
||||
{
|
||||
this.out = out;
|
||||
this.jmxc = jmxc;
|
||||
this.jmxc = accessor instanceof RemoteJmxMBeanAccessor ? ((RemoteJmxMBeanAccessor) accessor).getJmxConnector() : null;
|
||||
this.ssProxy = ssProxy;
|
||||
this.repairCmd = repairCmd;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ public abstract class AbstractCommand implements Runnable
|
|||
* @return {@code true} if the command is required to connect to the node, {@code false} otherwise.
|
||||
* @throws ExecutionException if an error occurs during preparation and execution must be aborted.
|
||||
*/
|
||||
protected boolean shouldConnect() throws ExecutionException
|
||||
public boolean shouldConnect() throws ExecutionException
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -303,6 +303,8 @@ public class CMSAdmin extends AbstractCommand
|
|||
@Command(name = "dump", description = "Dumps cluster metadata into a file")
|
||||
public static class DumpClusterMetadata extends AbstractCommand
|
||||
{
|
||||
// TODO CASSANDRA-XXXXX @ArgGroup cannot be populated by the remote (CQL/MBean) execution path,
|
||||
// this should be avoided to keep the command hierarchy flat for all interfaces
|
||||
@ArgGroup(exclusive = false, multiplicity = "0..1")
|
||||
DumpOptions dumpOptions;
|
||||
|
||||
|
|
|
|||
|
|
@ -136,9 +136,10 @@ public class CompressionDictionaryCommandGroup
|
|||
else if (TrainingStatus.FAILED == status)
|
||||
{
|
||||
err.printf("%nTraining failed for %s.%s%n", keyspace, table);
|
||||
String failureMessage = null;
|
||||
try
|
||||
{
|
||||
String failureMessage = trainingState.getFailureMessage();
|
||||
failureMessage = trainingState.getFailureMessage();
|
||||
if (failureMessage != null && !failureMessage.isEmpty())
|
||||
{
|
||||
err.printf("Reason: %s%n", failureMessage);
|
||||
|
|
@ -148,19 +149,23 @@ public class CompressionDictionaryCommandGroup
|
|||
{
|
||||
// If we can't get the failure message, just continue without it
|
||||
}
|
||||
System.exit(1);
|
||||
throw new RuntimeException(String.format("Training failed for %s.%s. Reason: %s",
|
||||
keyspace, table, failureMessage == null ?
|
||||
"undefined" : failureMessage));
|
||||
}
|
||||
|
||||
Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
err.printf("%nTraining did not complete within expected timeframe (10 minutes).%n");
|
||||
System.exit(1);
|
||||
throw new RuntimeException("Training did not complete within expected timeframe (10 minutes).");
|
||||
}
|
||||
catch (IllegalArgumentException | IllegalStateException e)
|
||||
{
|
||||
throw new IllegalArgumentException("Failed to trigger training: " + e.getMessage(), e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
err.printf("Failed to trigger training: %s%n", e.getMessage());
|
||||
System.exit(1);
|
||||
throw new RuntimeException("Failed to trigger training: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +192,7 @@ public class CompressionDictionaryCommandGroup
|
|||
catch (Throwable t)
|
||||
{
|
||||
err.println("Invalid value for " + MAX_DICT_SIZE_PARAM_NAME + ": " + t.getMessage());
|
||||
System.exit(1);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -200,7 +205,7 @@ public class CompressionDictionaryCommandGroup
|
|||
catch (Throwable t)
|
||||
{
|
||||
err.println("Invalid value for " + MAX_TOTAL_SAMPLE_SIZE_PARAM_NAME + ": " + t.getMessage());
|
||||
System.exit(1);
|
||||
throw t;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -283,7 +288,7 @@ public class CompressionDictionaryCommandGroup
|
|||
if (dictId <= 0 && dictId != -1)
|
||||
{
|
||||
probe.output().err.printf("Dictionary id has to be strictly positive number.%n");
|
||||
System.exit(1);
|
||||
throw new IllegalArgumentException("Dictionary id has to be strictly positive number.");
|
||||
}
|
||||
|
||||
try
|
||||
|
|
@ -304,7 +309,9 @@ public class CompressionDictionaryCommandGroup
|
|||
probe.output().err.printf("Dictionary%s does not exist for %s.%s.%n",
|
||||
dictId == -1 ? "" : " with id " + dictId,
|
||||
keyspace, table);
|
||||
System.exit(1);
|
||||
throw new IllegalArgumentException(String.format("Dictionary%s does not exist for %s.%s.",
|
||||
dictId == -1 ? "" : " with id " + dictId,
|
||||
keyspace, table));
|
||||
}
|
||||
|
||||
CompressionDictionaryDataObject dataObject = CompressionDictionaryDetailsTabularData.fromCompositeData(compressionDictionary);
|
||||
|
|
@ -314,7 +321,7 @@ public class CompressionDictionaryCommandGroup
|
|||
catch (Throwable e)
|
||||
{
|
||||
probe.output().err.printf("Failed to export dictionary: %s%n", e.getMessage());
|
||||
System.exit(1);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -339,19 +346,22 @@ public class CompressionDictionaryCommandGroup
|
|||
CompositeData compositeData = CompressionDictionaryDetailsTabularData.fromCompressionDictionaryDataObject(dictionaryDataObject);
|
||||
probe.importCompressionDictionary(compositeData);
|
||||
}
|
||||
catch (IllegalStateException | IllegalArgumentException ex)
|
||||
{
|
||||
// These exceptions are threated as the command input was invalid, so we don't need to wrap them.
|
||||
throw ex;
|
||||
}
|
||||
catch (ValueInstantiationException ex)
|
||||
{
|
||||
// we catch this when validation of data object fails - that will happen when
|
||||
// JSON is invalid, and we attempt to deserialize it to data object by Jackson
|
||||
// We can fail fast on the client, so we will never reach Cassandra node with a payload
|
||||
// which would fail there too, so we do not contact a node unnecessarily.
|
||||
probe.output().err.printf("Unable to import dictionary JSON: %s%n", ex.getCause().getMessage());
|
||||
System.exit(1);
|
||||
throw new RuntimeException(String.format("Unable to import dictionary JSON: %s%n", ex.getCause().getMessage()), ex);
|
||||
}
|
||||
catch (Throwable t)
|
||||
{
|
||||
probe.output().err.printf("Unable to import dictionary JSON: %s%n", t.getMessage());
|
||||
System.exit(1);
|
||||
throw new RuntimeException(String.format("Unable to import dictionary JSON: %s%n", t.getMessage()), t);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -360,17 +370,17 @@ public class CompressionDictionaryCommandGroup
|
|||
if (!dictionaryFile.exists())
|
||||
{
|
||||
probe.output().err.printf("Path %s does not exist.%n", dictionaryPath);
|
||||
System.exit(1);
|
||||
throw new IllegalArgumentException("Path " + dictionaryPath + " does not exist.");
|
||||
}
|
||||
if (!dictionaryFile.isFile())
|
||||
{
|
||||
probe.output().err.printf("Path %s is not a file.%n", dictionaryPath);
|
||||
System.exit(1);
|
||||
throw new IllegalArgumentException("Path " + dictionaryPath + " is not a file.");
|
||||
}
|
||||
if (!dictionaryFile.isReadable())
|
||||
{
|
||||
probe.output().err.printf("Path %s is not readable.%n", dictionaryPath);
|
||||
System.exit(1);
|
||||
throw new IllegalArgumentException("Path " + dictionaryPath + " is not readable.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -431,8 +441,7 @@ public class CompressionDictionaryCommandGroup
|
|||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
probe.output().err.printf("Failed to list dictionaries: %s%n", e.getMessage());
|
||||
System.exit(1);
|
||||
throw new RuntimeException("Failed to list dictionaries: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.tools.nodetool;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
import org.apache.cassandra.config.CassandraRelevantEnv;
|
||||
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||
import org.apache.cassandra.tools.NodeProbe;
|
||||
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
|
||||
import org.apache.cassandra.transport.ProtocolVersion;
|
||||
import org.apache.cassandra.transport.SimpleClient;
|
||||
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.Spec;
|
||||
|
||||
/**
|
||||
* Command options for NodeTool commands that are executed via CQL.
|
||||
*/
|
||||
@Command(name = "cqlconnect", description = "Connect to a Cassandra node via CQL")
|
||||
public class CqlConnect extends AbstractCommand implements AutoCloseable
|
||||
{
|
||||
private static final int DEFAULT_CQL_PORT = 11211;
|
||||
|
||||
/** The command specification, used to access command-specific properties. */
|
||||
@Spec
|
||||
protected CommandSpec spec; // injected by picocli
|
||||
|
||||
@Option(names = { "-h", "--host" }, description = "Node hostname or ip address", arity = "0..1")
|
||||
private String host = "127.0.0.1";
|
||||
|
||||
@Option(names = { "-p", "--port" }, description = "Remote CQL native transport port number", arity = "0..1")
|
||||
private int port = DEFAULT_CQL_PORT;
|
||||
|
||||
@Option(names = { "--diagnostic" }, description = "Enable diagnostic output for troubleshooting connection issues")
|
||||
private boolean diagnostic = false;
|
||||
|
||||
private volatile SimpleClient client;
|
||||
|
||||
/**
|
||||
* Initialize the CQL connection to the Cassandra node using the provided options.
|
||||
*/
|
||||
public void run()
|
||||
{
|
||||
if (client != null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
if (diagnostic)
|
||||
output.printInfo("Connecting to %s:%s via CQL...%n", host, port);
|
||||
|
||||
SimpleClient.Builder builder = SimpleClient.builder(host, port)
|
||||
.protocolVersion(ProtocolVersion.V5)
|
||||
.requestTimeoutSeconds(requestTimeoutSeconds());
|
||||
client = builder.build();
|
||||
client.connect(false);
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
Throwable rootCause = Throwables.getRootCause(e);
|
||||
throw new NodetoolConnectionException(String.format("Failed to connect to '%s:%s' via CQL - %s: '%s'.",
|
||||
host, port,
|
||||
rootCause.getClass().getSimpleName(),
|
||||
rootCause.getMessage()),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
public SimpleClient client()
|
||||
{
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Long-running commands are executed asynchronously by default, but some commands (e.g. info)
|
||||
* are still synchronous and may need longer than {@link SimpleClient#TIMEOUT_SECONDS} to
|
||||
* respond; {@code 0} waits indefinitely.
|
||||
*/
|
||||
private static long requestTimeoutSeconds()
|
||||
{
|
||||
String env = CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getString();
|
||||
if (env == null)
|
||||
return CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getLong();
|
||||
|
||||
try
|
||||
{
|
||||
return Long.parseLong(env);
|
||||
}
|
||||
catch (NumberFormatException e)
|
||||
{
|
||||
throw new IllegalArgumentException(String.format("Invalid value for environment variable '%s': " +
|
||||
"expected a number of seconds but was '%s'",
|
||||
CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getKey(),
|
||||
env));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void execute(NodeProbe probe)
|
||||
{
|
||||
assert probe == null;
|
||||
run();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception
|
||||
{
|
||||
if (client != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
client.close();
|
||||
}
|
||||
finally
|
||||
{
|
||||
client = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,6 +61,7 @@ public class ForceCompact extends AbstractCommand
|
|||
try
|
||||
{
|
||||
probe.forceCompactionKeysIgnoringGcGrace(keyspaceName, tableName, partitionKeysIgnoreGcGrace);
|
||||
probe.output().printInfo("Force compaction performed for keyspace '%s', table '%s'", keyspaceName, tableName);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ public class History extends AbstractCommand implements LocalCommand
|
|||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldConnect() throws CommandLine.ExecutionException
|
||||
public boolean shouldConnect() throws CommandLine.ExecutionException
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import java.util.Map.Entry;
|
|||
|
||||
import javax.management.InstanceNotFoundException;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
|
||||
import org.apache.cassandra.io.util.FileUtils;
|
||||
import org.apache.cassandra.service.CacheServiceMBean;
|
||||
|
|
@ -73,7 +75,7 @@ public class Info extends AbstractCommand
|
|||
catch (RuntimeException e)
|
||||
{
|
||||
// offheap-metrics introduced in 2.1.3 - older versions do not have the appropriate mbeans
|
||||
if (!(e.getCause() instanceof InstanceNotFoundException))
|
||||
if (!(Throwables.getRootCause(e) instanceof InstanceNotFoundException))
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +137,7 @@ public class Info extends AbstractCommand
|
|||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
if (!(e.getCause() instanceof InstanceNotFoundException))
|
||||
if (!(Throwables.getRootCause(e) instanceof InstanceNotFoundException))
|
||||
throw e;
|
||||
|
||||
// Chunk cache is not on.
|
||||
|
|
@ -151,7 +153,7 @@ public class Info extends AbstractCommand
|
|||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
if (!(e.getCause() instanceof InstanceNotFoundException))
|
||||
if (!(Throwables.getRootCause(e) instanceof InstanceNotFoundException))
|
||||
throw e;
|
||||
|
||||
// network cache is not on.
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import java.io.Console;
|
|||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.List;
|
||||
import java.util.Scanner;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -32,21 +31,15 @@ import com.google.common.base.Throwables;
|
|||
import org.apache.cassandra.io.util.File;
|
||||
import org.apache.cassandra.tools.INodeProbeFactory;
|
||||
import org.apache.cassandra.tools.NodeProbe;
|
||||
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.ExecutionException;
|
||||
import picocli.CommandLine.IExecutionStrategy;
|
||||
import picocli.CommandLine.InitializationException;
|
||||
import picocli.CommandLine.Model.CommandSpec;
|
||||
import picocli.CommandLine.Option;
|
||||
import picocli.CommandLine.ParameterException;
|
||||
import picocli.CommandLine.ParseResult;
|
||||
import picocli.CommandLine.RunLast;
|
||||
import picocli.CommandLine.Spec;
|
||||
|
||||
import static java.lang.Integer.parseInt;
|
||||
import static org.apache.cassandra.tools.NodeProbe.defaultPort;
|
||||
import static org.apache.cassandra.tools.RemoteJmxMBeanAccessor.defaultPort;
|
||||
import static org.apache.commons.lang3.StringUtils.EMPTY;
|
||||
import static org.apache.commons.lang3.StringUtils.isEmpty;
|
||||
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
|
||||
|
|
@ -57,8 +50,6 @@ import static org.apache.commons.lang3.StringUtils.isNotEmpty;
|
|||
@Command(name = "connect", description = "Connect to a Cassandra node via JMX")
|
||||
public class JmxConnect extends AbstractCommand implements AutoCloseable
|
||||
{
|
||||
public static final String MIXIN_KEY = "jmx";
|
||||
|
||||
/** The command specification, used to access command-specific properties. */
|
||||
@Spec
|
||||
protected CommandSpec spec; // injected by picocli
|
||||
|
|
@ -81,30 +72,6 @@ public class JmxConnect extends AbstractCommand implements AutoCloseable
|
|||
@Inject
|
||||
private INodeProbeFactory nodeProbeFactory;
|
||||
|
||||
/**
|
||||
* This method is called by picocli and used depending on the execution strategy.
|
||||
* @param parseResult The parsed command line.
|
||||
* @return The exit code.
|
||||
*/
|
||||
public static int executionStrategy(ParseResult parseResult)
|
||||
{
|
||||
CommandSpec jmx = parseResult.commandSpec().mixins().get(MIXIN_KEY);
|
||||
if (jmx == null)
|
||||
throw new InitializationException("No JmxConnect command found in the top-level hierarchy");
|
||||
|
||||
try (JmxConnectionCommandInvoker invoker = new JmxConnectionCommandInvoker((JmxConnect) jmx.userObject()))
|
||||
{
|
||||
return invoker.execute(parseResult);
|
||||
}
|
||||
catch (JmxConnectionCommandInvoker.CloseException e)
|
||||
{
|
||||
jmx.commandLine()
|
||||
.getErr()
|
||||
.println("Failed to connect to JMX: " + e.getMessage());
|
||||
return jmx.commandLine().getExitCodeExceptionMapper().getExitCode(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the JMX connection to the Cassandra node using the provided options.
|
||||
*/
|
||||
|
|
@ -129,9 +96,11 @@ public class JmxConnect extends AbstractCommand implements AutoCloseable
|
|||
catch (IOException | SecurityException e)
|
||||
{
|
||||
Throwable rootCause = Throwables.getRootCause(e);
|
||||
output.printError("nodetool: Failed to connect to '%s:%s' - %s: '%s'.%n", host, port,
|
||||
rootCause.getClass().getSimpleName(), rootCause.getMessage());
|
||||
throw new InitializationException("Failed to connect to JMX", e);
|
||||
throw new NodetoolConnectionException(String.format("Failed to connect to '%s:%s' - %s: '%s'.",
|
||||
host, port,
|
||||
rootCause.getClass().getSimpleName(),
|
||||
rootCause.getMessage()),
|
||||
e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -183,61 +152,13 @@ public class JmxConnect extends AbstractCommand implements AutoCloseable
|
|||
return password;
|
||||
}
|
||||
|
||||
private static class JmxConnectionCommandInvoker implements IExecutionStrategy, AutoCloseable
|
||||
public String getHost()
|
||||
{
|
||||
private final JmxConnect connect;
|
||||
return host;
|
||||
}
|
||||
|
||||
public JmxConnectionCommandInvoker(JmxConnect connect)
|
||||
{
|
||||
this.connect = connect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execute(ParseResult parseResult) throws ExecutionException, ParameterException
|
||||
{
|
||||
CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
|
||||
if (lastParent.userObject() instanceof AbstractCommand)
|
||||
{
|
||||
AbstractCommand command = (AbstractCommand) lastParent.userObject();
|
||||
if (command.shouldConnect())
|
||||
connect.run();
|
||||
command.probe(connect.probe());
|
||||
}
|
||||
return new RunLast().execute(parseResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws CloseException
|
||||
{
|
||||
try
|
||||
{
|
||||
if (connect.probe() != null)
|
||||
((AutoCloseable) connect.probe()).close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new CloseException("Failed to close JMX connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static CommandLine.Model.CommandSpec lastExecutableSubcommandWithSameParent(List<CommandLine> parsedCommands)
|
||||
{
|
||||
int start = parsedCommands.size() - 1;
|
||||
for (int i = parsedCommands.size() - 2; i >= 0; i--)
|
||||
{
|
||||
if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent())
|
||||
break;
|
||||
start = i;
|
||||
}
|
||||
return parsedCommands.get(start).getCommandSpec();
|
||||
}
|
||||
|
||||
private static class CloseException extends RuntimeException
|
||||
{
|
||||
public CloseException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
public String getPort()
|
||||
{
|
||||
return port;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ package org.apache.cassandra.tools.nodetool;
|
|||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.URL;
|
||||
import java.net.URLDecoder;
|
||||
|
|
@ -47,6 +48,7 @@ import org.gridkit.jvmtool.cli.CommandLauncher;
|
|||
import org.apache.cassandra.io.util.File;
|
||||
import org.apache.cassandra.tools.NodeProbe;
|
||||
import org.apache.cassandra.tools.Output;
|
||||
import org.apache.cassandra.tools.RemoteJmxMBeanAccessor;
|
||||
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Command;
|
||||
|
|
@ -64,7 +66,7 @@ public class Sjk extends AbstractCommand
|
|||
private final Wrapper wrapper = new Wrapper();
|
||||
|
||||
@Override
|
||||
protected boolean shouldConnect() throws ExecutionException
|
||||
public boolean shouldConnect() throws ExecutionException
|
||||
{
|
||||
// We want to parse the given arguments in advance to determine if the SJK command requires an MBeanServerConnection or not.
|
||||
wrapper.prepare(args.isEmpty() ? new String[]{ "--help" } : args.toArray(new String[0]), output.out, output.err);
|
||||
|
|
@ -243,7 +245,9 @@ public class Sjk extends AbstractCommand
|
|||
{
|
||||
public MBeanServerConnection getMServer()
|
||||
{
|
||||
return probe.getMbeanServerConn();
|
||||
return probe.getMBeanAccessor() instanceof RemoteJmxMBeanAccessor ?
|
||||
((RemoteJmxMBeanAccessor) probe.getMBeanAccessor()).getMBeanServerConnection() :
|
||||
ManagementFactory.getPlatformMBeanServer();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,8 +18,11 @@
|
|||
package org.apache.cassandra.tools.nodetool;
|
||||
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.tools.NodeProbe;
|
||||
import org.apache.cassandra.tools.nodetool.strategy.NodetoolConnectionException;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
|
||||
import picocli.CommandLine.Command;
|
||||
|
|
@ -37,6 +40,12 @@ public class StopDaemon extends AbstractCommand
|
|||
} catch (Exception e)
|
||||
{
|
||||
JVMStabilityInspector.inspectThrowable(e);
|
||||
// The connection dropping while the daemon stops is expected and ignored.
|
||||
if (Throwables.getCausalChain(e).stream().anyMatch(NodetoolConnectionException.class::isInstance))
|
||||
{
|
||||
Throwables.throwIfUnchecked(e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import java.util.TimeZone;
|
|||
|
||||
import javax.management.InstanceNotFoundException;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
|
||||
import org.apache.commons.lang3.time.DurationFormatUtils;
|
||||
|
|
@ -341,7 +342,7 @@ public class TableStatsHolder implements StatsHolder
|
|||
catch (RuntimeException e)
|
||||
{
|
||||
// offheap-metrics introduced in 2.1.3 - older versions do not have the appropriate mbeans
|
||||
if (!(e.getCause() instanceof InstanceNotFoundException))
|
||||
if (!(Throwables.getRootCause(e) instanceof InstanceNotFoundException))
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.tools.nodetool.strategy;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
public interface CommandExecutionStrategy extends CommandLine.IExecutionStrategy, AutoCloseable
|
||||
{
|
||||
@Override
|
||||
default void close() throws ExecutionStrategyCloseException {}
|
||||
|
||||
class ExecutionStrategyCloseException extends RuntimeException
|
||||
{
|
||||
public ExecutionStrategyCloseException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
enum Type
|
||||
{
|
||||
CQL,
|
||||
STATIC_MBEAN,
|
||||
COMMAND_MBEAN,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.tools.nodetool.strategy;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.management.MBeanException;
|
||||
import javax.management.MBeanServerConnection;
|
||||
import javax.management.ObjectName;
|
||||
import javax.management.ReflectionException;
|
||||
import javax.management.RuntimeMBeanException;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import org.apache.cassandra.config.CassandraRelevantEnv;
|
||||
import org.apache.cassandra.cql3.statements.ExecuteCommandStatement;
|
||||
import org.apache.cassandra.management.CommandExecutionArgsSerde;
|
||||
import org.apache.cassandra.management.CommandMBeanAdapter;
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.picocli.PicocliCommandArgsConverter;
|
||||
import org.apache.cassandra.tools.NodeProbe;
|
||||
import org.apache.cassandra.tools.RemoteJmxMBeanAccessor;
|
||||
import org.apache.cassandra.tools.nodetool.AbstractCommand;
|
||||
import org.apache.cassandra.tools.nodetool.JmxConnect;
|
||||
import org.apache.cassandra.tools.nodetool.StopDaemon;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID;
|
||||
import static org.apache.cassandra.management.ManagementUtils.fullCommandName;
|
||||
import static org.apache.cassandra.utils.JsonUtils.fromJsonMap;
|
||||
|
||||
public class CommandMBeanExecutionStrategy implements CommandExecutionStrategy
|
||||
{
|
||||
private static final String MBEAN_DOMAIN = "org.apache.cassandra.management";
|
||||
private static final String MBEAN_TYPE_COMMAND = "Command";
|
||||
|
||||
private final JmxConnect connect;
|
||||
|
||||
public CommandMBeanExecutionStrategy(JmxConnect connect)
|
||||
{
|
||||
this.connect = connect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException, CommandLine.ParameterException
|
||||
{
|
||||
CommandLine.Model.CommandSpec lastParent = StaticMBeanExecutionStrategy.lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
|
||||
|
||||
NodeProbe probe = null;
|
||||
Object userObject = lastParent.userObject();
|
||||
|
||||
if (userObject instanceof AbstractCommand)
|
||||
{
|
||||
AbstractCommand command = (AbstractCommand) userObject;
|
||||
if (command.shouldConnect())
|
||||
connect.run();
|
||||
probe = connect.probe();
|
||||
}
|
||||
|
||||
// Local command execution with no JMX connection.
|
||||
if (probe == null || parseResult.isUsageHelpRequested() || parseResult.isVersionHelpRequested())
|
||||
return new CommandLine.RunLast().execute(parseResult);
|
||||
|
||||
String commandName = extractCommandName(parseResult);
|
||||
if (commandName == null || commandName.isEmpty())
|
||||
return new CommandLine.RunLast().execute(parseResult);
|
||||
|
||||
// Command is already populated with args from CommandLine.parseArgs(), convert to CommandExecutionArgs
|
||||
CommandExecutionArgs args = PicocliCommandArgsConverter.fromCommand(userObject);
|
||||
String jsonParams = CommandExecutionArgsSerde.toJson(args);
|
||||
|
||||
try
|
||||
{
|
||||
MBeanServerConnection mbs = ((RemoteJmxMBeanAccessor) probe.getMBeanAccessor()).getMBeanServerConnection();
|
||||
|
||||
if (!mbs.isRegistered(constructCommandMBeanName(commandName)))
|
||||
return new CommandLine.RunLast().execute(parseResult);
|
||||
|
||||
String rawResult = executeViaRemoteMBean(mbs, commandName, jsonParams);
|
||||
|
||||
Map<String, String> commandResult = fromJsonMap(rawResult);
|
||||
|
||||
String executionId = commandResult.get(ExecuteCommandStatement.COMMAND_RESULT_SCHEMA_EXECUTION_ID);
|
||||
String output = commandResult.get(ExecuteCommandStatement.COMMAND_RESULT_SCHEMA_OUTPUT);
|
||||
|
||||
if (Strings.isNullOrEmpty(executionId) || output == null)
|
||||
throw new RuntimeException("Invalid command result schema received from CommandMBeanAdapter: " + rawResult);
|
||||
|
||||
probe.output().printInfo(output);
|
||||
// The output needs to be consistent for the tests we already have and for thouse which heavily rely on
|
||||
// asserting command output, so we use this property to control whether the execution id is printed or not.
|
||||
if (CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID.getBoolean() ||
|
||||
CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID.getBoolean())
|
||||
probe.output().printInfo("Command execution id: " + executionId);
|
||||
return 0;
|
||||
}
|
||||
catch (NodetoolConnectionException e)
|
||||
{
|
||||
throw e;
|
||||
}
|
||||
catch (RuntimeMBeanException e)
|
||||
{
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof IllegalArgumentException || cause instanceof IllegalStateException)
|
||||
{
|
||||
throw new CommandLine.ParameterException(parseResult.commandSpec().commandLine(),
|
||||
"Invalid command parameters: " + cause.getMessage(), cause);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Include security exception, instance not found, invocation target exceptions.
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
|
||||
"Failed to execute command via MBean: " + cause.getMessage(), cause);
|
||||
}
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
if (userObject instanceof StopDaemon)
|
||||
{
|
||||
probe.output().printInfo("Cassandra has shutdown.");
|
||||
return 0;
|
||||
}
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
|
||||
String.format("JMX connection to the node was lost while executing command '%s'. " +
|
||||
"The command may still be running on the server.",
|
||||
commandName),
|
||||
e);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
|
||||
"Unexpected error during command execution: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws ExecutionStrategyCloseException
|
||||
{
|
||||
if (connect.probe() == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
connect.probe().close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ExecutionStrategyCloseException("Failed to close JMX connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
/** Handles nested commands (e.g. compressiondictionary.train). */
|
||||
public static String extractCommandName(CommandLine.ParseResult parseResult)
|
||||
{
|
||||
List<CommandLine> commandLineList = parseResult.asCommandLineList();
|
||||
if (commandLineList.size() <= 1) // Only root "nodetool"
|
||||
return null;
|
||||
|
||||
// Skip the root command ("nodetool") and concatenate the rest to form the full command name.
|
||||
return fullCommandName(commandLineList.subList(1, commandLineList.size()), CommandLine::getCommandName);
|
||||
}
|
||||
|
||||
private String executeViaRemoteMBean(MBeanServerConnection mbs, String commandName, String jsonParams) throws Exception
|
||||
{
|
||||
ObjectName mbeanName = constructCommandMBeanName(commandName);
|
||||
|
||||
try
|
||||
{
|
||||
Object result = mbs.invoke(mbeanName, CommandMBeanAdapter.INVOKE_METHOD,
|
||||
new Object[]{ jsonParams },
|
||||
new String[]{ String.class.getName() });
|
||||
|
||||
return result == null ? "" : result.toString();
|
||||
}
|
||||
catch (MBeanException | ReflectionException e)
|
||||
{
|
||||
throw new RuntimeMBeanException(new RuntimeException(e), "Failed to invoke CommandMBeanAdapter: " + commandName);
|
||||
}
|
||||
}
|
||||
|
||||
private ObjectName constructCommandMBeanName(String commandName) throws Exception
|
||||
{
|
||||
String escapedName = ObjectName.quote(commandName);
|
||||
String objectNameStr = String.format("%s:type=%s,name=%s", MBEAN_DOMAIN, MBEAN_TYPE_COMMAND, escapedName);
|
||||
return new ObjectName(objectNameStr);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.tools.nodetool.strategy;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
|
||||
import org.apache.cassandra.config.CassandraRelevantEnv;
|
||||
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||
import org.apache.cassandra.cql3.ColumnIdentifier;
|
||||
import org.apache.cassandra.cql3.CqlBuilder;
|
||||
import org.apache.cassandra.db.ConsistencyLevel;
|
||||
import org.apache.cassandra.db.marshal.ByteArrayAccessor;
|
||||
import org.apache.cassandra.db.marshal.UTF8Type;
|
||||
import org.apache.cassandra.db.marshal.UUIDType;
|
||||
import org.apache.cassandra.exceptions.CommandRequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.UnauthorizedException;
|
||||
import org.apache.cassandra.management.api.CommandExecutionArgs;
|
||||
import org.apache.cassandra.management.api.OptionMetadata;
|
||||
import org.apache.cassandra.management.api.ParameterMetadata;
|
||||
import org.apache.cassandra.management.picocli.PicocliCommandArgsConverter;
|
||||
import org.apache.cassandra.tools.nodetool.AbstractCommand;
|
||||
import org.apache.cassandra.tools.nodetool.CqlConnect;
|
||||
import org.apache.cassandra.tools.nodetool.StopDaemon;
|
||||
import org.apache.cassandra.transport.SimpleClient;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID;
|
||||
import static org.apache.cassandra.management.ManagementUtils.normalizeOptionName;
|
||||
import static org.apache.cassandra.management.api.ParameterMetadata.COMMAND_POSITIONAL_PARAM_PREFIX;
|
||||
import static org.apache.cassandra.tools.nodetool.strategy.CommandMBeanExecutionStrategy.extractCommandName;
|
||||
|
||||
public class CqlCommandExecutionStrategy implements CommandExecutionStrategy
|
||||
{
|
||||
private final CqlConnect connect;
|
||||
|
||||
public CqlCommandExecutionStrategy(CqlConnect connect)
|
||||
{
|
||||
this.connect = connect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException, CommandLine.ParameterException
|
||||
{
|
||||
CommandLine.Model.CommandSpec lastParent = StaticMBeanExecutionStrategy.lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
|
||||
|
||||
Object userObject = lastParent.userObject();
|
||||
|
||||
if (userObject instanceof AbstractCommand)
|
||||
{
|
||||
AbstractCommand command = (AbstractCommand) userObject;
|
||||
if (command.shouldConnect())
|
||||
connect.run();
|
||||
}
|
||||
|
||||
// Local command execution with no CQL connection.
|
||||
if (connect.client() == null || parseResult.isUsageHelpRequested() || parseResult.isVersionHelpRequested())
|
||||
return new CommandLine.RunLast().execute(parseResult);
|
||||
|
||||
String commandName = extractCommandName(parseResult);
|
||||
if (commandName == null || commandName.isEmpty())
|
||||
return new CommandLine.RunLast().execute(parseResult);
|
||||
|
||||
// Command is already populated with args from CommandLine.parseArgs(), converting it to CommandExecutionArgs
|
||||
CommandExecutionArgs args = PicocliCommandArgsConverter.fromCommand(userObject);
|
||||
|
||||
try
|
||||
{
|
||||
String cqlCommand = buildCqlCommandString(commandName, args);
|
||||
ResultMessage result = connect.client().execute(cqlCommand, ConsistencyLevel.ONE);
|
||||
|
||||
if (result instanceof ResultMessage.Rows)
|
||||
{
|
||||
ResultMessage.Rows rows = (ResultMessage.Rows) result;
|
||||
assert rows.result.size() == 1 : "Command execution result should have exactly 1 row, got " + rows.result.size();
|
||||
assert rows.result.metadata.getColumnCount() == 2 : "Command execution result schema has been changed. " +
|
||||
"Expected 2 columns in result, got " + rows.result.metadata.getColumnCount();
|
||||
|
||||
if (!rows.result.isEmpty() && rows.result.metadata.names.size() >= 2)
|
||||
{
|
||||
List<byte[]> firstRow = rows.result.rows.get(0);
|
||||
if (firstRow.size() >= 2)
|
||||
{
|
||||
UUID executionId = UUIDType.instance.getSerializer().deserialize(firstRow.get(0), ByteArrayAccessor.instance);
|
||||
String output = UTF8Type.instance.getSerializer().deserialize(firstRow.get(1), ByteArrayAccessor.instance);
|
||||
// NodeProbe instance is not available here, so print directly to the command output.
|
||||
parseResult.commandSpec().commandLine().getOut().println(output);
|
||||
if (CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID.getBoolean()
|
||||
|| CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_SHOW_EXECUTION_ID.getBoolean())
|
||||
parseResult.commandSpec().commandLine().getOut().println("Command execution id: " + executionId.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (SimpleClient.ConnectionClosedException e)
|
||||
{
|
||||
if (userObject instanceof StopDaemon)
|
||||
{
|
||||
parseResult.commandSpec().commandLine().getOut().println("Cassandra has shutdown.");
|
||||
return 0;
|
||||
}
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
|
||||
String.format("Connection to the node was closed before a response " +
|
||||
"was received for command '%s'. " +
|
||||
"The command may still be running on the server.",
|
||||
commandName),
|
||||
e);
|
||||
}
|
||||
catch (SimpleClient.TimeoutException e)
|
||||
{
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
|
||||
String.format("No response received for command '%s' within the client timeout. " +
|
||||
"The command may still be running on the server. " +
|
||||
"Increase the timeout via the %s environment variable " +
|
||||
"or the %s system property (0 waits indefinitely). %s",
|
||||
commandName,
|
||||
CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getKey(),
|
||||
CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_TIMEOUT_SECONDS.getKey(),
|
||||
e.getMessage()),
|
||||
e);
|
||||
}
|
||||
catch (RuntimeException e)
|
||||
{
|
||||
Throwable cause = e.getCause();
|
||||
if (cause instanceof UnauthorizedException)
|
||||
{
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
|
||||
"Unauthorized: " + cause.getMessage());
|
||||
}
|
||||
else if (cause instanceof InvalidRequestException)
|
||||
{
|
||||
// CQL invalid request (e.g. validating params and/or param values) translates into picocli param exection.
|
||||
throw new CommandLine.ParameterException(parseResult.commandSpec().commandLine(),
|
||||
"Invalid request: " + cause.getMessage());
|
||||
}
|
||||
else if (cause instanceof CommandRequestExecutionException)
|
||||
{
|
||||
CommandRequestExecutionException cree = (CommandRequestExecutionException) cause;
|
||||
String msg = String.format("Command execution failed (executionId: %s): %s",
|
||||
cree.getCommandExecutionId(), cree.getMessage());
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(), msg);
|
||||
}
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
|
||||
"Unknown command execution exception via CQL: " + e.getMessage(), e);
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Catch-all for checked exceptions thrown during command execution.
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(),
|
||||
"Unknown command execution exception via CQL: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws ExecutionStrategyCloseException
|
||||
{
|
||||
if (connect == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
connect.close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ExecutionStrategyCloseException("Failed to close CQL connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a CQL INVOKE COMMAND statement string from command name and CommandExecutionArgs.
|
||||
* The format is: INVOKE COMMAND commandName WITH "key1" = 'value1' AND "key2" = 'value2';
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static String buildCqlCommandString(String commandName, CommandExecutionArgs args)
|
||||
{
|
||||
CqlBuilder builder = new CqlBuilder();
|
||||
builder.append("INVOKE COMMAND ");
|
||||
builder.appendQuotingIfNeeded(commandName);
|
||||
|
||||
Map<String, Object> paramsMap = new LinkedHashMap<>();
|
||||
for (Map.Entry<OptionMetadata, Object> entry : args.options().entrySet())
|
||||
{
|
||||
OptionMetadata optionMetadata = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
String key = normalizeOptionName(optionMetadata.paramLabel());
|
||||
paramsMap.put(key, value);
|
||||
}
|
||||
|
||||
for (Map.Entry<ParameterMetadata, Object> entry : args.parameters().entrySet())
|
||||
{
|
||||
ParameterMetadata paramMetadata = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
|
||||
if (value == null)
|
||||
continue;
|
||||
|
||||
String key = COMMAND_POSITIONAL_PARAM_PREFIX + paramMetadata.index();
|
||||
paramsMap.put(key, value);
|
||||
}
|
||||
|
||||
if (!paramsMap.isEmpty())
|
||||
{
|
||||
builder.append(" WITH");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, Object> entry : paramsMap.entrySet())
|
||||
{
|
||||
if (first)
|
||||
builder.append(" ");
|
||||
else
|
||||
builder.append(" AND ");
|
||||
|
||||
// Using ColumnIdentifier.maybeQuote as some of the keys
|
||||
// may be reserved words in CQL (e.g., "keyspace", "table").
|
||||
builder.append(ColumnIdentifier.maybeQuote(entry.getKey()));
|
||||
builder.append(" = ");
|
||||
appendCqlValue(builder, entry.getValue());
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
builder.append(";");
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/** Append a value to CqlBuilder with proper type handling and escaping. */
|
||||
private static void appendCqlValue(CqlBuilder builder, Object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
builder.append("NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
if (value instanceof String)
|
||||
builder.appendWithSingleQuotes((String) value);
|
||||
else if (value instanceof Enum)
|
||||
builder.appendWithSingleQuotes(((Enum<?>) value).name());
|
||||
else if (value instanceof Number)
|
||||
builder.append(value);
|
||||
else if (value instanceof Boolean)
|
||||
builder.append(value);
|
||||
else if (value instanceof List)
|
||||
{
|
||||
List<?> list = (List<?>) value;
|
||||
builder.append("[");
|
||||
for (int i = 0; i < list.size(); i++)
|
||||
{
|
||||
if (i > 0)
|
||||
builder.append(", ");
|
||||
appendCqlValue(builder, list.get(i));
|
||||
}
|
||||
builder.append("]");
|
||||
}
|
||||
else if (value instanceof Map)
|
||||
{
|
||||
Map<String, Object> map = (Map<String, Object>) value;
|
||||
builder.append("{");
|
||||
boolean first = true;
|
||||
for (Map.Entry<String, Object> entry : map.entrySet())
|
||||
{
|
||||
if (!first)
|
||||
builder.append(", ");
|
||||
builder.appendWithSingleQuotes(entry.getKey());
|
||||
builder.append(": ");
|
||||
appendCqlValue(builder, entry.getValue());
|
||||
first = false;
|
||||
}
|
||||
builder.append("}");
|
||||
}
|
||||
else if (value instanceof Set)
|
||||
{
|
||||
Set<?> set = (Set<?>) value;
|
||||
builder.append("{");
|
||||
boolean first = true;
|
||||
for (Object item : set)
|
||||
{
|
||||
if (!first)
|
||||
builder.append(", ");
|
||||
appendCqlValue(builder, item);
|
||||
first = false;
|
||||
}
|
||||
builder.append("}");
|
||||
}
|
||||
else if (value.getClass().isArray())
|
||||
{
|
||||
int length = Array.getLength(value);
|
||||
builder.append("[");
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
builder.append(", ");
|
||||
appendCqlValue(builder, Array.get(value, i));
|
||||
}
|
||||
builder.append("]");
|
||||
}
|
||||
else
|
||||
builder.appendWithSingleQuotes(value.toString());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.tools.nodetool.strategy;
|
||||
|
||||
/**
|
||||
* Thrown when nodetool fails to establish its connection to the target node, over either JMX or CQL.
|
||||
*/
|
||||
public class NodetoolConnectionException extends RuntimeException
|
||||
{
|
||||
public NodetoolConnectionException(String message, Throwable cause)
|
||||
{
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.tools.nodetool.strategy;
|
||||
|
||||
import org.apache.cassandra.config.CassandraRelevantEnv;
|
||||
import org.apache.cassandra.config.CassandraRelevantProperties;
|
||||
import org.apache.cassandra.tools.nodetool.CqlConnect;
|
||||
import org.apache.cassandra.tools.nodetool.JmxConnect;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
import static org.apache.cassandra.utils.LocalizeString.toUpperCaseLocalized;
|
||||
|
||||
public class ProtocolAwareExecutionStrategy implements CommandExecutionStrategy
|
||||
{
|
||||
public static int executionStrategy(CommandLine.ParseResult parseResult)
|
||||
{
|
||||
try (ProtocolAwareExecutionStrategy strategy = new ProtocolAwareExecutionStrategy())
|
||||
{
|
||||
return strategy.execute(parseResult);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by picocli and used depending on the execution strategy.
|
||||
* @param parseResult The parsed command line.
|
||||
* @return The exit code.
|
||||
*/
|
||||
@Override
|
||||
public int execute(CommandLine.ParseResult parseResult)
|
||||
{
|
||||
CommandLine.Model.CommandSpec connectCmd = parseResult.commandSpec()
|
||||
.mixins()
|
||||
.get(getExecutionStrategyTypeFromEnvAndSys().toString());
|
||||
if (connectCmd == null)
|
||||
throw new CommandLine.InitializationException("No 'connection' command found in the top-level hierarchy");
|
||||
|
||||
try (CommandExecutionStrategy invoker = createInvoker(connectCmd.userObject()))
|
||||
{
|
||||
return invoker.execute(parseResult);
|
||||
}
|
||||
catch (NodetoolConnectionException e)
|
||||
{
|
||||
throw new CommandLine.ExecutionException(parseResult.commandSpec().commandLine(), e.getMessage(), e);
|
||||
}
|
||||
catch (ExecutionStrategyCloseException e)
|
||||
{
|
||||
CommandLine commandLine = parseResult.commandSpec().commandLine();
|
||||
commandLine.getErr().println("Failed to close connection: " + e.getMessage());
|
||||
CommandLine.IExitCodeExceptionMapper mapper = commandLine.getExitCodeExceptionMapper();
|
||||
return mapper != null ? mapper.getExitCode(e) : commandLine.getCommandSpec().exitCodeOnExecutionException();
|
||||
}
|
||||
}
|
||||
|
||||
public static Type getExecutionStrategyTypeFromEnvAndSys()
|
||||
{
|
||||
Type defaultStrategy = Type.valueOf(toUpperCaseLocalized(CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_PROTOCOL.getDefaultValue()));
|
||||
Type strategyEnv = CassandraRelevantEnv.CASSANDRA_CLI_EXECUTION_PROTOCOL.getEnum(true, Type.class, defaultStrategy.name());
|
||||
Type strategySys = CassandraRelevantProperties.CASSANDRA_CLI_EXECUTION_PROTOCOL.getEnum(true, Type.class);
|
||||
return strategyEnv != defaultStrategy ? strategyEnv : strategySys;
|
||||
}
|
||||
|
||||
protected CommandExecutionStrategy createInvoker(Object connectCmd)
|
||||
{
|
||||
Type strategy = getExecutionStrategyTypeFromEnvAndSys();
|
||||
switch (strategy)
|
||||
{
|
||||
case CQL:
|
||||
return new CqlCommandExecutionStrategy((CqlConnect) connectCmd);
|
||||
case COMMAND_MBEAN:
|
||||
return new CommandMBeanExecutionStrategy((JmxConnect) connectCmd);
|
||||
case STATIC_MBEAN:
|
||||
return new StaticMBeanExecutionStrategy((JmxConnect) connectCmd);
|
||||
default:
|
||||
throw new IllegalStateException("Unknown execution strategy: " + strategy);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.tools.nodetool.strategy;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.cassandra.tools.nodetool.AbstractCommand;
|
||||
import org.apache.cassandra.tools.nodetool.JmxConnect;
|
||||
|
||||
import picocli.CommandLine;
|
||||
|
||||
public class StaticMBeanExecutionStrategy implements CommandExecutionStrategy
|
||||
{
|
||||
private final JmxConnect connect;
|
||||
|
||||
public StaticMBeanExecutionStrategy(JmxConnect connect)
|
||||
{
|
||||
this.connect = connect;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int execute(CommandLine.ParseResult parseResult) throws CommandLine.ExecutionException, CommandLine.ParameterException
|
||||
{
|
||||
CommandLine.Model.CommandSpec lastParent = lastExecutableSubcommandWithSameParent(parseResult.asCommandLineList());
|
||||
if (lastParent.userObject() instanceof AbstractCommand)
|
||||
{
|
||||
AbstractCommand command = (AbstractCommand) lastParent.userObject();
|
||||
if (command.shouldConnect())
|
||||
connect.run();
|
||||
command.probe(connect.probe());
|
||||
}
|
||||
return new CommandLine.RunLast().execute(parseResult);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws ExecutionStrategyCloseException
|
||||
{
|
||||
if (connect.probe() == null)
|
||||
return;
|
||||
try
|
||||
{
|
||||
connect.probe().close();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new ExecutionStrategyCloseException("Failed to close JMX connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
static CommandLine.Model.CommandSpec lastExecutableSubcommandWithSameParent(List<CommandLine> parsedCommands)
|
||||
{
|
||||
int start = parsedCommands.size() - 1;
|
||||
for (int i = parsedCommands.size() - 2; i >= 0; i--)
|
||||
{
|
||||
if (parsedCommands.get(i).getParent() != parsedCommands.get(i + 1).getParent())
|
||||
break;
|
||||
start = i;
|
||||
}
|
||||
return parsedCommands.get(start).getCommandSpec();
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ import io.netty.util.AttributeKey;
|
|||
public class Connection
|
||||
{
|
||||
static final AttributeKey<Connection> attributeKey = AttributeKey.valueOf("CONN");
|
||||
static final AttributeKey<Boolean> managementKey = AttributeKey.valueOf("IS_MANAGEMENT_CONN");
|
||||
|
||||
private final Channel channel;
|
||||
private final ProtocolVersion version;
|
||||
|
|
|
|||
|
|
@ -33,9 +33,16 @@ import org.slf4j.LoggerFactory;
|
|||
import org.apache.cassandra.concurrent.DebuggableTask;
|
||||
import org.apache.cassandra.concurrent.LocalAwareExecutorPlus;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.cql3.CQLStatement;
|
||||
import org.apache.cassandra.cql3.QueryProcessor;
|
||||
import org.apache.cassandra.cql3.statements.ExecuteCommandStatement;
|
||||
import org.apache.cassandra.cql3.statements.SelectStatement;
|
||||
import org.apache.cassandra.cql3.statements.UseStatement;
|
||||
import org.apache.cassandra.exceptions.InvalidRequestException;
|
||||
import org.apache.cassandra.exceptions.OverloadedException;
|
||||
import org.apache.cassandra.metrics.ClientMetrics;
|
||||
import org.apache.cassandra.net.FrameEncoder;
|
||||
import org.apache.cassandra.schema.SchemaConstants;
|
||||
import org.apache.cassandra.service.ClientWarn;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.service.reads.thresholds.CoordinatorWarnings;
|
||||
|
|
@ -44,6 +51,7 @@ import org.apache.cassandra.transport.ClientResourceLimits.Overload;
|
|||
import org.apache.cassandra.transport.Flusher.FlushItem;
|
||||
import org.apache.cassandra.transport.messages.ErrorMessage;
|
||||
import org.apache.cassandra.transport.messages.EventMessage;
|
||||
import org.apache.cassandra.transport.messages.QueryMessage;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
import org.apache.cassandra.utils.MonotonicClock;
|
||||
import org.apache.cassandra.utils.NoSpamLogger;
|
||||
|
|
@ -53,6 +61,7 @@ import io.netty.channel.EventLoop;
|
|||
import io.netty.util.AttributeKey;
|
||||
|
||||
import static org.apache.cassandra.concurrent.SharedExecutorPool.SHARED;
|
||||
import static org.apache.cassandra.utils.LocalizeString.toLowerCaseLocalized;
|
||||
|
||||
public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Request>
|
||||
{
|
||||
|
|
@ -83,8 +92,43 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
|
|||
"transport",
|
||||
"Native-Transport-Auth-Requests");
|
||||
|
||||
/**
|
||||
* Executor for handling requests from management connections (part of CEP-38).
|
||||
*
|
||||
* <p>Management connections are identified via Connection's flag set by the management
|
||||
* transport server at initial connection setup. Request then are routed to a dedicated
|
||||
* executor instead of the standard {@link #requestExecutor}. This provides isolation and
|
||||
* prioritization of management operations, ensuring they can proceed even under
|
||||
* a high load of regular client requests.
|
||||
*
|
||||
* <p>The executor is configured separately via
|
||||
* {@link DatabaseDescriptor#getNativeTransportManagementMaxThreads()} to allow
|
||||
* independent tuning of management operation throughput.
|
||||
*
|
||||
* <p>Management connections are established through the management transport server
|
||||
* (see {@link org.apache.cassandra.service.NativeTransportManagementService}), which listens
|
||||
* on a separate port from the regular native transport.
|
||||
*
|
||||
* <p>Unlike auth requests, management requests have no fallback executor, so values
|
||||
* less than 1 are treated as 1: a zero-sized pool would queue management requests forever.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
static final LocalAwareExecutorPlus managementExecutor = SHARED.newExecutor(Math.max(1, DatabaseDescriptor.getNativeTransportManagementMaxThreads()),
|
||||
DatabaseDescriptor::setNativeTransportManagementMaxThreads,
|
||||
"transport",
|
||||
"Native-Transport-Management-Tasks");
|
||||
|
||||
private static final ConcurrentMap<EventLoop, Flusher> flusherLookup = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Set while a management request is executing on the current thread. A management command may itself
|
||||
* initiate the management server's stop (e.g. INVOKE COMMAND stopdaemon), in which case the drain-wait
|
||||
* in {@link Server#close} runs on this very thread and must not wait for its own task to complete.
|
||||
*/
|
||||
private static final ThreadLocal<Boolean> IN_MANAGEMENT_TASK = ThreadLocal.withInitial(() -> Boolean.FALSE);
|
||||
|
||||
private final boolean useLegacyFlusher;
|
||||
private final boolean isManagementDispatcher;
|
||||
|
||||
/**
|
||||
* Takes a Channel, Request and the Response produced by processRequest and outputs a FlushItem
|
||||
|
|
@ -98,9 +142,10 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
|
|||
FlushItem<?> toFlushItem(P param, Channel channel, Message.Request request, Message.Response response);
|
||||
}
|
||||
|
||||
public Dispatcher(boolean useLegacyFlusher)
|
||||
public Dispatcher(boolean useLegacyFlusher, boolean isManagementDispatcher)
|
||||
{
|
||||
this.useLegacyFlusher = useLegacyFlusher;
|
||||
this.isManagementDispatcher = isManagementDispatcher;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -123,10 +168,32 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
|
|||
boolean isAuthQuery = DatabaseDescriptor.getNativeTransportMaxAuthThreads() > 0 &&
|
||||
(request.type == Message.Type.AUTH_RESPONSE || request.type == Message.Type.CREDENTIALS);
|
||||
|
||||
// Importantly, the authExecutor will handle the AUTHENTICATE message which may be CPU intensive.
|
||||
LocalAwareExecutorPlus executor = isAuthQuery ? authExecutor : requestExecutor;
|
||||
if (isAuthQuery)
|
||||
{
|
||||
// Importantly, the authExecutor will handle the AUTHENTICATE message which may be CPU intensive.
|
||||
authExecutor.submit(new RequestProcessor<>(channel, request, forFlusher, param, backpressure));
|
||||
ClientMetrics.instance.markRequestDispatched();
|
||||
return;
|
||||
}
|
||||
|
||||
executor.submit(new RequestProcessor<>(channel, request, forFlusher, param, backpressure));
|
||||
// Use connection object to check for management connections, this could be faster than checking
|
||||
// channel attributies directly every time. For management connections, we route requests to
|
||||
// the management executor.
|
||||
Connection connection = request.connection();
|
||||
if (connection instanceof ServerConnection)
|
||||
{
|
||||
ServerConnection serverConnection = (ServerConnection) connection;
|
||||
if (serverConnection.isManagementConnection())
|
||||
{
|
||||
// Intentionally skipping ClientMetrics calls here: that meter tracks regular client request
|
||||
// dispatch, and management API requests are accounted for separately dedicated metrics rather
|
||||
// than mixing them into the client requests rate.
|
||||
managementExecutor.submit(new ManagementRequestProcessor<>(channel, request, forFlusher, param, backpressure));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
requestExecutor.submit(new RequestProcessor<>(channel, request, forFlusher, param, backpressure));
|
||||
ClientMetrics.instance.markRequestDispatched();
|
||||
}
|
||||
|
||||
|
|
@ -293,13 +360,13 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
|
|||
*/
|
||||
public class RequestProcessor<P> implements DebuggableTask.RunnableDebuggableTask
|
||||
{
|
||||
private final Channel channel;
|
||||
private final Message.Request request;
|
||||
private final FlushItemConverter<P> forFlusher;
|
||||
private final P flusherParam;
|
||||
private final Overload backpressure;
|
||||
protected final Channel channel;
|
||||
protected final Message.Request request;
|
||||
protected final FlushItemConverter<P> forFlusher;
|
||||
protected final P flusherParam;
|
||||
protected final Overload backpressure;
|
||||
|
||||
private volatile long startTimeNanos;
|
||||
protected volatile long startTimeNanos;
|
||||
|
||||
public RequestProcessor(Channel channel, Message.Request request, FlushItemConverter<P> forFlusher, P flusherParam, Overload backpressure)
|
||||
{
|
||||
|
|
@ -345,6 +412,124 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
|
|||
}
|
||||
}
|
||||
|
||||
/** RequestProcessor for management connections that validates before executing. */
|
||||
private class ManagementRequestProcessor<P> extends RequestProcessor<P> {
|
||||
|
||||
public ManagementRequestProcessor(Channel channel,
|
||||
Message.Request request,
|
||||
FlushItemConverter<P> forFlusher,
|
||||
P flusherParam,
|
||||
Overload backpressure) {
|
||||
super(channel, request, forFlusher, flusherParam, backpressure);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
startTimeNanos = MonotonicClock.Global.preciseTime.now();
|
||||
RequestTime requestTime = new RequestTime(request.createdAtNanos, startTimeNanos);
|
||||
|
||||
// Validate management request BEFORE executing
|
||||
Connection connection = request.connection();
|
||||
if (connection instanceof ServerConnection) {
|
||||
ServerConnection serverConnection = (ServerConnection) connection;
|
||||
if (serverConnection.isManagementConnection()) {
|
||||
if (!isManagementRequestAllowed(request)) {
|
||||
// The flush pipeline takes the stream id from the request envelope, so the
|
||||
// response must not carry one of its own.
|
||||
Message.Response response = ErrorMessage.fromExceptionNoStreamId(
|
||||
new InvalidRequestException(
|
||||
"Only executions of the INVOKE COMMAND statements are allowed on the management port."));
|
||||
response.attach(connection);
|
||||
FlushItem<?> toFlush = forFlusher.toFlushItem(flusherParam, channel, request, response);
|
||||
flush(toFlush);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IN_MANAGEMENT_TASK.set(Boolean.TRUE);
|
||||
try {
|
||||
// If validation passes, call the normal processRequest to execute
|
||||
// This calls the instance method processRequest() which does all the work
|
||||
processRequest(channel, request, forFlusher, flusherParam, backpressure, requestTime);
|
||||
} finally {
|
||||
IN_MANAGEMENT_TASK.set(Boolean.FALSE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static boolean isManagementRequestAllowed(Message.Request request)
|
||||
{
|
||||
switch (request.type)
|
||||
{
|
||||
case QUERY:
|
||||
try
|
||||
{
|
||||
// Early parse the query to check if it's an INVOKE COMMAND statement.
|
||||
// For management non-intensive operations double parsing is probably acceptable.
|
||||
CQLStatement.Raw rawStatement = QueryProcessor.parseStatement(((QueryMessage) request).query);
|
||||
if (rawStatement instanceof ExecuteCommandStatement.Raw)
|
||||
return true;
|
||||
|
||||
// Allow read-only SELECT queries on system keyspaces (needed for driver metadata
|
||||
// discovery), except system_auth (see isManagementReadableSystemKeyspace).
|
||||
if (rawStatement instanceof SelectStatement.RawStatement)
|
||||
{
|
||||
SelectStatement.RawStatement selectRaw = (SelectStatement.RawStatement) rawStatement;
|
||||
return selectRaw.isFullyQualified()
|
||||
&& isManagementReadableSystemKeyspace(selectRaw.keyspace());
|
||||
}
|
||||
|
||||
// This is also a corner case for the driver's behavior on the management port.
|
||||
// When connecting, the driver sends a USE statement for the keyspace provided
|
||||
// in driver.connect("system_schema").
|
||||
if (rawStatement instanceof UseStatement)
|
||||
{
|
||||
UseStatement useStatement = (UseStatement) rawStatement;
|
||||
return isManagementReadableSystemKeyspace(useStatement.keyspace());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.warn("The command request parsing failed. The command will not be executed: {}", e.getMessage());
|
||||
// If parsing fails (syntax error, etc.), it's not a valid command statement;
|
||||
// this is expected for non-command queries.
|
||||
return false;
|
||||
}
|
||||
case STARTUP:
|
||||
case CREDENTIALS:
|
||||
case AUTH_RESPONSE:
|
||||
case OPTIONS:
|
||||
case REGISTER:
|
||||
return true; // Protocol messages are always allowed.
|
||||
case EXECUTE:
|
||||
case PREPARE:
|
||||
case BATCH:
|
||||
default:
|
||||
return false; // Not supported and not allowed on management connections.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* System keyspaces a CQL driver may read/USE on the management port for metadata discovery. This is
|
||||
* {@link SchemaConstants#isSystemKeyspace(String)} (which also covers virtual system keyspaces) minus
|
||||
* {@code system_auth}: that keyspace is the credential store (bcrypt {@code salted_hash}, superuser
|
||||
* flags) and must never be readable over the management port, which is reachable without authorization
|
||||
* (if AllowAllAuthorizer is enabled).
|
||||
*/
|
||||
private static boolean isManagementReadableSystemKeyspace(String keyspace)
|
||||
{
|
||||
if (keyspace == null)
|
||||
return false;
|
||||
if (SchemaConstants.AUTH_KEYSPACE_NAME.equals(toLowerCaseLocalized(keyspace)))
|
||||
return false;
|
||||
return SchemaConstants.isSystemKeyspace(keyspace)
|
||||
|| SchemaConstants.isVirtualSystemKeyspace(keyspace);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the item in the head of the queue has spent more than allowed time in the queue.
|
||||
*/
|
||||
|
|
@ -355,7 +540,8 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
|
|||
if (threshold <= 0)
|
||||
return true;
|
||||
|
||||
return requestExecutor.oldestTaskQueueTime() < (DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS) * threshold);
|
||||
LocalAwareExecutorPlus executor = isManagementDispatcher ? managementExecutor : requestExecutor;
|
||||
return executor.oldestTaskQueueTime() < (DatabaseDescriptor.getNativeTransportTimeout(TimeUnit.NANOSECONDS) * threshold);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -500,15 +686,23 @@ public class Dispatcher implements CQLMessageHandler.MessageConsumer<Message.Req
|
|||
flusher.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true once the executor serving this dispatcher's server has drained: {@link #managementExecutor}
|
||||
* for the management transport, {@link #requestExecutor} otherwise. A management task running on the
|
||||
* calling thread is excluded — it is the task that initiated the stop and cannot drain before it.
|
||||
*/
|
||||
public boolean isDone()
|
||||
{
|
||||
return requestExecutor.getPendingTaskCount() == 0 && requestExecutor.getActiveTaskCount() == 0;
|
||||
LocalAwareExecutorPlus executor = isManagementDispatcher ? managementExecutor : requestExecutor;
|
||||
int self = isManagementDispatcher && IN_MANAGEMENT_TASK.get() ? 1 : 0;
|
||||
return executor.getPendingTaskCount() == 0 && executor.getActiveTaskCount() - self <= 0;
|
||||
}
|
||||
|
||||
public static void shutdown()
|
||||
{
|
||||
requestExecutor.shutdown();
|
||||
authExecutor.shutdown();
|
||||
managementExecutor.shutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -117,17 +117,20 @@ public class PipelineConfigurator
|
|||
private final Dispatcher dispatcher;
|
||||
// Shared between pre-v5 and CQLMessage handlers
|
||||
private final QueueBackpressure queueBackpressure;
|
||||
private final boolean isManagementConnection;
|
||||
|
||||
public PipelineConfigurator(boolean epoll,
|
||||
boolean keepAlive,
|
||||
EncryptionOptions.TlsEncryptionPolicy encryptionPolicy,
|
||||
Dispatcher dispatcher)
|
||||
Dispatcher dispatcher,
|
||||
boolean isManagementConnection)
|
||||
{
|
||||
this.epoll = epoll;
|
||||
this.keepAlive = keepAlive;
|
||||
this.tlsEncryptionPolicy = encryptionPolicy;
|
||||
this.dispatcher = dispatcher;
|
||||
this.queueBackpressure = QueueBackpressure.DEFAULT;
|
||||
this.queueBackpressure = isManagementConnection ? QueueBackpressure.MANAGEMENT_DEFAULT : QueueBackpressure.DEFAULT;
|
||||
this.isManagementConnection = isManagementConnection;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
@ -139,8 +142,9 @@ public class PipelineConfigurator
|
|||
this.epoll = epoll;
|
||||
this.keepAlive = keepAlive;
|
||||
this.tlsEncryptionPolicy = encryptionPolicy;
|
||||
this.dispatcher = new Dispatcher(useLegacyFlusher);
|
||||
this.dispatcher = new Dispatcher(useLegacyFlusher, false);
|
||||
this.queueBackpressure = QueueBackpressure.DEFAULT;
|
||||
this.isManagementConnection = false;
|
||||
}
|
||||
|
||||
public ChannelFuture initializeChannel(final EventLoopGroup workerGroup,
|
||||
|
|
@ -261,6 +265,9 @@ public class PipelineConfigurator
|
|||
pipeline.addFirst(CONNECTION_LIMIT_HANDLER, connectionLimitHandler);
|
||||
}
|
||||
|
||||
if (isManagementConnection)
|
||||
channel.attr(Connection.managementKey).set(Boolean.TRUE);
|
||||
|
||||
long idleTimeout = DatabaseDescriptor.nativeTransportIdleTimeout();
|
||||
if (idleTimeout > 0)
|
||||
{
|
||||
|
|
@ -369,11 +376,6 @@ public class PipelineConfigurator
|
|||
return new ClientResourceLimits.ResourceProvider.Default(allocator);
|
||||
}
|
||||
|
||||
protected Dispatcher dispatcher(boolean useLegacyFlusher)
|
||||
{
|
||||
return new Dispatcher(useLegacyFlusher);
|
||||
}
|
||||
|
||||
protected CQLMessageHandler.MessageConsumer<Message.Request> messageConsumer()
|
||||
{
|
||||
return dispatcher;
|
||||
|
|
|
|||
|
|
@ -48,16 +48,35 @@ public interface QueueBackpressure
|
|||
{
|
||||
QueueBackpressure NO_OP = timeUnit -> 0;
|
||||
|
||||
QueueBackpressure DEFAULT = new QueueBackpressure()
|
||||
{
|
||||
private final AtomicReference<Incident> state = new AtomicReference<>(noBackpressure(() -> DatabaseDescriptor.getNativeTransportMinBackoffOnQueueOverload(TimeUnit.NANOSECONDS),
|
||||
() -> DatabaseDescriptor.getNativeTransportMaxBackoffOnQueueOverload(TimeUnit.NANOSECONDS)));
|
||||
/** Shared incident state for connections served by {@link Dispatcher#requestExecutor}. */
|
||||
QueueBackpressure DEFAULT = newDefault();
|
||||
|
||||
public long markAndGetDelay(TimeUnit timeUnit)
|
||||
/**
|
||||
* Shared incident state for management connections ({@link Dispatcher#managementExecutor}). Kept separate
|
||||
* from {@link #DEFAULT} because incident severity describes a single executor's queue: an overload streak
|
||||
* on the client transport must not inflate delays applied to management connections, and vice versa.
|
||||
*/
|
||||
QueueBackpressure MANAGEMENT_DEFAULT = newDefault();
|
||||
|
||||
static QueueBackpressure newDefault()
|
||||
{
|
||||
return newDefault(() -> DatabaseDescriptor.getNativeTransportMinBackoffOnQueueOverload(TimeUnit.NANOSECONDS),
|
||||
() -> DatabaseDescriptor.getNativeTransportMaxBackoffOnQueueOverload(TimeUnit.NANOSECONDS));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static QueueBackpressure newDefault(LongSupplier minDelayNanos, LongSupplier maxDelayNanos)
|
||||
{
|
||||
return new QueueBackpressure()
|
||||
{
|
||||
return state.updateAndGet(Incident::mark).delay(timeUnit);
|
||||
}
|
||||
};
|
||||
private final AtomicReference<Incident> state = new AtomicReference<>(noBackpressure(minDelayNanos, maxDelayNanos));
|
||||
|
||||
public long markAndGetDelay(TimeUnit timeUnit)
|
||||
{
|
||||
return state.updateAndGet(Incident::mark).delay(timeUnit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
long markAndGetDelay(TimeUnit timeUnit);
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,8 @@ public class Server implements CassandraDaemon.Server
|
|||
{
|
||||
public Connection newConnection(Channel channel, ProtocolVersion version)
|
||||
{
|
||||
return new ServerConnection(channel, version, connectionTracker);
|
||||
boolean isManagementConnection = Boolean.TRUE.equals(channel.attr(Connection.managementKey).get());
|
||||
return new ServerConnection(channel, version, connectionTracker, isManagementConnection);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -111,13 +112,14 @@ public class Server implements CassandraDaemon.Server
|
|||
workerGroup = new NioEventLoopGroup();
|
||||
}
|
||||
|
||||
dispatcher = new Dispatcher(DatabaseDescriptor.useNativeTransportLegacyFlusher());
|
||||
dispatcher = new Dispatcher(DatabaseDescriptor.useNativeTransportLegacyFlusher(), builder.isManagementConnection);
|
||||
pipelineConfigurator = builder.pipelineConfigurator != null
|
||||
? builder.pipelineConfigurator
|
||||
: new PipelineConfigurator(useEpoll,
|
||||
DatabaseDescriptor.getRpcKeepAlive(),
|
||||
builder.tlsEncryptionPolicy,
|
||||
dispatcher);
|
||||
dispatcher,
|
||||
builder.isManagementConnection);
|
||||
|
||||
EventNotifier notifier = builder.eventNotifier != null ? builder.eventNotifier : new EventNotifier();
|
||||
connectionTracker = new ConnectionTracker(isRunning::get);
|
||||
|
|
@ -234,6 +236,7 @@ public class Server implements CassandraDaemon.Server
|
|||
private InetSocketAddress socket;
|
||||
private PipelineConfigurator pipelineConfigurator;
|
||||
private EventNotifier eventNotifier;
|
||||
private boolean isManagementConnection = false;
|
||||
|
||||
public Builder withTlsEncryptionPolicy(EncryptionOptions.TlsEncryptionPolicy tlsEncryptionPolicy)
|
||||
{
|
||||
|
|
@ -273,6 +276,12 @@ public class Server implements CassandraDaemon.Server
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder withManagementConnectionFlag(boolean management)
|
||||
{
|
||||
this.isManagementConnection = management;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Server build()
|
||||
{
|
||||
return new Server(this);
|
||||
|
|
|
|||
|
|
@ -43,13 +43,20 @@ public class ServerConnection extends Connection
|
|||
private final ClientState clientState;
|
||||
private volatile ConnectionStage stage;
|
||||
public final Counter requests = new ThreadLocalCounter();
|
||||
private final boolean isManagementConnection;
|
||||
|
||||
ServerConnection(Channel channel, ProtocolVersion version, Connection.Tracker tracker)
|
||||
{
|
||||
this(channel, version, tracker, false);
|
||||
}
|
||||
|
||||
ServerConnection(Channel channel, ProtocolVersion version, Connection.Tracker tracker, boolean isManagementConnection)
|
||||
{
|
||||
super(channel, version, tracker);
|
||||
|
||||
clientState = ClientState.forExternalCalls(channel.remoteAddress());
|
||||
stage = ConnectionStage.ESTABLISHED;
|
||||
this.isManagementConnection = isManagementConnection;
|
||||
}
|
||||
|
||||
public ClientState getClientState()
|
||||
|
|
@ -158,4 +165,9 @@ public class ServerConnection extends Connection
|
|||
.get("ssl");
|
||||
return sslHandler != null;
|
||||
}
|
||||
|
||||
public boolean isManagementConnection()
|
||||
{
|
||||
return isManagementConnection;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ import static org.apache.cassandra.net.SocketFactory.newSslHandler;
|
|||
import static org.apache.cassandra.transport.CQLMessageHandler.envelopeSize;
|
||||
import static org.apache.cassandra.transport.Flusher.MAX_FRAMED_PAYLOAD_SIZE;
|
||||
import static org.apache.cassandra.transport.PipelineConfigurator.SSL_FACTORY_CONTEXT_DESCRIPTION;
|
||||
import static org.apache.cassandra.utils.Clock.Global.currentTimeMillis;
|
||||
import static org.apache.cassandra.utils.Clock.Global.nanoTime;
|
||||
import static org.apache.cassandra.utils.concurrent.BlockingQueues.newBlockingQueue;
|
||||
import static org.apache.cassandra.utils.concurrent.NonBlockingRateLimiter.NO_OP_LIMITER;
|
||||
|
||||
|
|
@ -100,6 +100,23 @@ public class SimpleClient implements Closeable
|
|||
|
||||
public static final int TIMEOUT_SECONDS = 10;
|
||||
|
||||
public static class TimeoutException extends RuntimeException
|
||||
{
|
||||
TimeoutException(long seconds)
|
||||
{
|
||||
super("Timed out after waiting " + seconds + " seconds for a server response. " +
|
||||
"The request may still be executing on the server.");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConnectionClosedException extends RuntimeException
|
||||
{
|
||||
ConnectionClosedException()
|
||||
{
|
||||
super("Connection was closed by the server before a response was received.");
|
||||
}
|
||||
}
|
||||
|
||||
static
|
||||
{
|
||||
InternalLoggerFactory.setDefaultFactory(new Slf4JLoggerFactory());
|
||||
|
|
@ -111,6 +128,8 @@ public class SimpleClient implements Closeable
|
|||
public final int port;
|
||||
private final EncryptionOptions.ClientEncryptionOptions encryptionOptions;
|
||||
private final int largeMessageThreshold;
|
||||
/** How long {@link #execute(Message.Request)} waits for a server response; {@code <= 0} means wait indefinitely. */
|
||||
private final long requestTimeoutSeconds;
|
||||
|
||||
protected final ResponseHandler responseHandler = new ResponseHandler();
|
||||
protected final Connection.Tracker tracker = new ConnectionTracker();
|
||||
|
|
@ -131,6 +150,7 @@ public class SimpleClient implements Closeable
|
|||
private ProtocolVersion version = ProtocolVersion.CURRENT;
|
||||
private boolean useBeta = false;
|
||||
private int largeMessageThreshold = FrameEncoder.Payload.MAX_SIZE;
|
||||
private long requestTimeoutSeconds = TIMEOUT_SECONDS;
|
||||
|
||||
private Builder(String host, int port)
|
||||
{
|
||||
|
|
@ -162,6 +182,13 @@ public class SimpleClient implements Closeable
|
|||
return this;
|
||||
}
|
||||
|
||||
/** @param seconds response wait limit for each request; {@code <= 0} waits indefinitely */
|
||||
public Builder requestTimeoutSeconds(long seconds)
|
||||
{
|
||||
requestTimeoutSeconds = seconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SimpleClient build()
|
||||
{
|
||||
if (version.isBeta() && !useBeta)
|
||||
|
|
@ -182,6 +209,7 @@ public class SimpleClient implements Closeable
|
|||
this.version = builder.version;
|
||||
this.encryptionOptions = builder.encryptionOptions.applyConfig();
|
||||
this.largeMessageThreshold = builder.largeMessageThreshold;
|
||||
this.requestTimeoutSeconds = builder.requestTimeoutSeconds;
|
||||
}
|
||||
|
||||
public SimpleClient(String host, int port, ProtocolVersion version, EncryptionOptions.ClientEncryptionOptions encryptionOptions)
|
||||
|
|
@ -211,6 +239,7 @@ public class SimpleClient implements Closeable
|
|||
this.largeMessageThreshold = FrameEncoder.Payload.MAX_SIZE -
|
||||
Math.max(FrameEncoderCrc.HEADER_AND_TRAILER_LENGTH,
|
||||
FrameEncoderLZ4.HEADER_AND_TRAILER_LENGTH);
|
||||
this.requestTimeoutSeconds = TIMEOUT_SECONDS;
|
||||
}
|
||||
|
||||
public SimpleClient(String host, int port)
|
||||
|
|
@ -328,9 +357,9 @@ public class SimpleClient implements Closeable
|
|||
{
|
||||
request.attach(connection);
|
||||
lastWriteFuture = channel.writeAndFlush(Collections.singletonList(request));
|
||||
Message.Response msg = responseHandler.responses.poll(TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
Message.Response msg = awaitResponse(responseDeadlineNanos());
|
||||
if (msg == null)
|
||||
throw new RuntimeException("timeout");
|
||||
throw new TimeoutException(requestTimeoutSeconds);
|
||||
if (throwOnErrorResponse && msg instanceof ErrorMessage)
|
||||
throw new RuntimeException((Throwable)((ErrorMessage)msg).error);
|
||||
return msg;
|
||||
|
|
@ -357,12 +386,12 @@ public class SimpleClient implements Closeable
|
|||
}
|
||||
lastWriteFuture = channel.writeAndFlush(requests);
|
||||
|
||||
long deadline = currentTimeMillis() + TimeUnit.SECONDS.toMillis(TIMEOUT_SECONDS);
|
||||
long deadlineNanos = responseDeadlineNanos();
|
||||
for (int i = 0; i < requests.size(); i++)
|
||||
{
|
||||
Message.Response msg = responseHandler.responses.poll(deadline - currentTimeMillis(), TimeUnit.MILLISECONDS);
|
||||
Message.Response msg = awaitResponse(deadlineNanos);
|
||||
if (msg == null)
|
||||
throw new RuntimeException("timeout");
|
||||
throw new TimeoutException(requestTimeoutSeconds);
|
||||
if (msg instanceof ErrorMessage)
|
||||
throw new RuntimeException((Throwable) ((ErrorMessage) msg).error);
|
||||
rrMap.put(requests.get(msg.getSource().header.streamId), msg);
|
||||
|
|
@ -395,6 +424,26 @@ public class SimpleClient implements Closeable
|
|||
return source == null ? 0 : source.header.streamId;
|
||||
}
|
||||
|
||||
private long responseDeadlineNanos()
|
||||
{
|
||||
return requestTimeoutSeconds > 0 ? nanoTime() + TimeUnit.SECONDS.toNanos(requestTimeoutSeconds) : Long.MAX_VALUE;
|
||||
}
|
||||
|
||||
private Message.Response awaitResponse(long deadlineNanos) throws InterruptedException
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
long waitNanos = Math.min(TimeUnit.SECONDS.toNanos(1), deadlineNanos - nanoTime());
|
||||
Message.Response msg = waitNanos > 0 ? responseHandler.responses.poll(waitNanos, TimeUnit.NANOSECONDS) : null;
|
||||
if (msg != null)
|
||||
return msg;
|
||||
if (responseHandler.connectionClosed)
|
||||
throw new ConnectionClosedException();
|
||||
if (nanoTime() - deadlineNanos >= 0)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface EventHandler
|
||||
{
|
||||
void onEvent(Event event);
|
||||
|
|
@ -702,6 +751,14 @@ public class SimpleClient implements Closeable
|
|||
{
|
||||
public final BlockingQueue<Message.Response> responses = new SynchronousQueue<>(true);
|
||||
public EventHandler eventHandler;
|
||||
volatile boolean connectionClosed;
|
||||
|
||||
@Override
|
||||
public void channelInactive(ChannelHandlerContext ctx) throws Exception
|
||||
{
|
||||
connectionClosed = true;
|
||||
ctx.fireChannelInactive();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelRead0(ChannelHandlerContext ctx, Message.Response r)
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import java.net.InetAddress;
|
|||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.base.Predicate;
|
||||
|
|
@ -37,6 +38,7 @@ import org.apache.cassandra.exceptions.AuthenticationException;
|
|||
import org.apache.cassandra.exceptions.CDCWriteException;
|
||||
import org.apache.cassandra.exceptions.CasWriteTimeoutException;
|
||||
import org.apache.cassandra.exceptions.CasWriteUnknownResultException;
|
||||
import org.apache.cassandra.exceptions.CommandRequestExecutionException;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.exceptions.ExceptionCode;
|
||||
import org.apache.cassandra.exceptions.FunctionExecutionException;
|
||||
|
|
@ -217,6 +219,18 @@ public class ErrorMessage extends Message.Response
|
|||
int blockFor = body.readInt();
|
||||
te = new CasWriteUnknownResultException(cl, received, blockFor);
|
||||
break;
|
||||
case COMMAND_FAILED:
|
||||
if (version.isSmallerThan(ProtocolVersion.V5))
|
||||
{
|
||||
// Fallback for older clients - they'll get SERVER_ERROR instead
|
||||
te = new ServerError(msg);
|
||||
}
|
||||
else
|
||||
{
|
||||
UUID executionId = CBUtil.readUUID(body);
|
||||
te = new CommandRequestExecutionException(executionId, msg, null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
return new ErrorMessage(te);
|
||||
}
|
||||
|
|
@ -302,6 +316,11 @@ public class ErrorMessage extends Message.Response
|
|||
CBUtil.writeConsistencyLevel(cwue.consistency, dest);
|
||||
dest.writeInt(cwue.received);
|
||||
dest.writeInt(cwue.blockFor);
|
||||
break;
|
||||
case COMMAND_FAILED:
|
||||
CommandRequestExecutionException cree = (CommandRequestExecutionException)err;
|
||||
CBUtil.writeUUID(cree.getCommandExecutionId(), dest);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -371,6 +390,10 @@ public class ErrorMessage extends Message.Response
|
|||
CasWriteUnknownResultException cwue = (CasWriteUnknownResultException)err;
|
||||
size += CBUtil.sizeOfConsistencyLevel(cwue.consistency) + 4 + 4; // receivedFor: 4, blockFor: 4
|
||||
break;
|
||||
case COMMAND_FAILED:
|
||||
CommandRequestExecutionException cree = (CommandRequestExecutionException)err;
|
||||
size += CBUtil.sizeOfUUID(cree.getCommandExecutionId()); // 16 bytes
|
||||
break;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
|
@ -408,6 +431,13 @@ public class ErrorMessage extends Message.Response
|
|||
case CAS_WRITE_UNKNOWN:
|
||||
CasWriteUnknownResultException cwue = (CasWriteUnknownResultException) msg.error;
|
||||
return new WriteTimeoutException(WriteType.CAS, cwue.consistency, cwue.received, cwue.blockFor);
|
||||
case COMMAND_FAILED:
|
||||
// For older clients, the executionId is lost, but the message should contain it.
|
||||
CommandRequestExecutionException cree = (CommandRequestExecutionException) msg.error;
|
||||
String msgWithId = String.format("Command execution failed (executionId: %s): %s",
|
||||
cree.getCommandExecutionId(),
|
||||
cree.getMessage());
|
||||
return new ServerError(msgWithId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -134,6 +134,12 @@ public class StartupMessage extends Message.Request
|
|||
|
||||
Guardrails.minimumClientDriverVersion.guard(driverName, driverVersion, clientState);
|
||||
|
||||
if (connection instanceof ServerConnection)
|
||||
{
|
||||
ServerConnection serverConnection = (ServerConnection) connection;
|
||||
clientState.setManagement(serverConnection.isManagementConnection());
|
||||
}
|
||||
|
||||
IAuthenticator authenticator = DatabaseDescriptor.getAuthenticator();
|
||||
if (authenticator.requireAuthentication())
|
||||
{
|
||||
|
|
|
|||
|
|
@ -259,4 +259,51 @@ public final class JsonUtils
|
|||
valueMap.put(lowered, valueMap.remove(mapKey));
|
||||
}
|
||||
}
|
||||
|
||||
public static String getJsonType(Class<?> type)
|
||||
{
|
||||
if (type == String.class)
|
||||
return "string";
|
||||
else if (type == int.class || type == Integer.class)
|
||||
return "integer";
|
||||
else if (type == long.class || type == Long.class)
|
||||
return "integer";
|
||||
else if (type == boolean.class || type == Boolean.class)
|
||||
return "boolean";
|
||||
else if (type == double.class || type == Double.class ||
|
||||
type == float.class || type == Float.class)
|
||||
return "number";
|
||||
else if (type.isArray() || List.class.isAssignableFrom(type))
|
||||
return "array";
|
||||
else if (Map.class.isAssignableFrom(type))
|
||||
return "object";
|
||||
else if (type.isEnum())
|
||||
return "string";
|
||||
else
|
||||
return "string";
|
||||
}
|
||||
|
||||
public static Object convertDefaultValue(String defaultValue, Class<?> type)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (type == boolean.class || type == Boolean.class)
|
||||
return Boolean.parseBoolean(defaultValue);
|
||||
else if (type == int.class || type == Integer.class)
|
||||
return Integer.parseInt(defaultValue);
|
||||
else if (type == long.class || type == Long.class)
|
||||
return Long.parseLong(defaultValue);
|
||||
else if (type == double.class || type == Double.class)
|
||||
return Double.parseDouble(defaultValue);
|
||||
else if (type == float.class || type == Float.class)
|
||||
return Float.parseFloat(defaultValue);
|
||||
else
|
||||
return defaultValue;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// Fall back to string default value if parsing fails.
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you 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.
|
||||
#
|
||||
#
|
||||
|
||||
org.apache.cassandra.management.picocli.PicocliCommandsProvider
|
||||
|
||||
|
|
@ -118,6 +118,7 @@ import org.apache.cassandra.io.util.DataOutputPlus;
|
|||
import org.apache.cassandra.io.util.File;
|
||||
import org.apache.cassandra.io.util.PathUtils;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.management.CommandInvokerService;
|
||||
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
|
||||
import org.apache.cassandra.metrics.Sampler;
|
||||
import org.apache.cassandra.metrics.ThreadLocalMetrics;
|
||||
|
|
@ -1044,6 +1045,7 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance
|
|||
SnapshotManager.instance::close,
|
||||
() -> IndexStatusManager.instance.shutdownAndWait(1L, MINUTES),
|
||||
DiskErrorsHandlerService::close,
|
||||
CommandInvokerService::shutdown,
|
||||
() -> ThreadLocalMetrics.shutdownCleaner(1L, MINUTES)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,79 +19,69 @@
|
|||
package org.apache.cassandra.distributed.mock.nodetool;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryMXBean;
|
||||
import java.lang.management.RuntimeMXBean;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import org.apache.cassandra.batchlog.BatchlogManager;
|
||||
import org.apache.cassandra.batchlog.BatchlogManagerMBean;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.db.ColumnFamilyStoreMBean;
|
||||
import org.apache.cassandra.db.Keyspace;
|
||||
import org.apache.cassandra.db.compaction.CompactionManager;
|
||||
import org.apache.cassandra.db.compaction.CompactionManagerMBean;
|
||||
import org.apache.cassandra.db.compression.CompressionDictionaryManagerMBean;
|
||||
import org.apache.cassandra.gms.FailureDetector;
|
||||
import org.apache.cassandra.gms.FailureDetectorMBean;
|
||||
import org.apache.cassandra.gms.Gossiper;
|
||||
import org.apache.cassandra.gms.GossiperMBean;
|
||||
import org.apache.cassandra.hints.HintsService;
|
||||
import org.apache.cassandra.hints.HintsServiceMBean;
|
||||
import org.apache.cassandra.locator.DynamicEndpointSnitch;
|
||||
import org.apache.cassandra.locator.DynamicEndpointSnitchMBean;
|
||||
import org.apache.cassandra.locator.EndpointSnitchInfo;
|
||||
import org.apache.cassandra.locator.EndpointSnitchInfoMBean;
|
||||
import org.apache.cassandra.locator.SnitchAdapter;
|
||||
import org.apache.cassandra.management.MBeanAccessor;
|
||||
import org.apache.cassandra.metrics.CassandraMetricsRegistry;
|
||||
import org.apache.cassandra.net.MessagingService;
|
||||
import org.apache.cassandra.net.MessagingServiceMBean;
|
||||
import org.apache.cassandra.profiler.AsyncProfilerMBean;
|
||||
import org.apache.cassandra.service.ActiveRepairService;
|
||||
import org.apache.cassandra.service.ActiveRepairServiceMBean;
|
||||
import org.apache.cassandra.service.AsyncProfilerService;
|
||||
import org.apache.cassandra.service.CacheService;
|
||||
import org.apache.cassandra.service.CacheServiceMBean;
|
||||
import org.apache.cassandra.service.GCInspector;
|
||||
import org.apache.cassandra.service.GCInspectorMXBean;
|
||||
import org.apache.cassandra.service.StorageProxy;
|
||||
import org.apache.cassandra.service.StorageProxyMBean;
|
||||
import org.apache.cassandra.service.StorageService;
|
||||
import org.apache.cassandra.service.StorageServiceMBean;
|
||||
import org.apache.cassandra.service.accord.AccordOperations;
|
||||
import org.apache.cassandra.service.accord.AccordOperationsMBean;
|
||||
import org.apache.cassandra.service.snapshot.SnapshotManager;
|
||||
import org.apache.cassandra.service.snapshot.SnapshotManagerMBean;
|
||||
import org.apache.cassandra.streaming.StreamManager;
|
||||
import org.apache.cassandra.streaming.StreamManagerMBean;
|
||||
import org.apache.cassandra.tcm.CMSOperations;
|
||||
import org.apache.cassandra.tcm.CMSOperationsMBean;
|
||||
import org.apache.cassandra.tools.NodeProbe;
|
||||
|
||||
public class InternalNodeProbe extends NodeProbe
|
||||
{
|
||||
private final boolean withNotifications;
|
||||
private boolean previousSkipNotificationListeners = false;
|
||||
|
||||
public InternalNodeProbe(boolean withNotifications)
|
||||
{
|
||||
this.withNotifications = withNotifications;
|
||||
connect();
|
||||
}
|
||||
|
||||
protected void connect()
|
||||
{
|
||||
// note that we are not connecting via JMX for testing
|
||||
mbeanServerConn = null;
|
||||
jmxc = null;
|
||||
|
||||
super(new TestMockMBeanAccessor()); // host/port are unused in InternalNodeProbe
|
||||
previousSkipNotificationListeners = StorageService.instance.skipNotificationListeners;
|
||||
StorageService.instance.skipNotificationListeners = !withNotifications;
|
||||
|
||||
ssProxy = StorageService.instance;
|
||||
snapshotProxy = SnapshotManager.instance;
|
||||
cmsProxy = CMSOperations.instance;
|
||||
accordProxy = AccordOperations.instance;
|
||||
msProxy = MessagingService.instance();
|
||||
streamProxy = StreamManager.instance;
|
||||
compactionProxy = CompactionManager.instance;
|
||||
fdProxy = (FailureDetectorMBean) FailureDetector.instance;
|
||||
cacheService = CacheService.instance;
|
||||
spProxy = StorageProxy.instance;
|
||||
hsProxy = HintsService.instance;
|
||||
|
||||
gcProxy = new GCInspector();
|
||||
gossProxy = Gossiper.instance;
|
||||
bmProxy = BatchlogManager.instance;
|
||||
arsProxy = ActiveRepairService.instance();
|
||||
memProxy = ManagementFactory.getMemoryMXBean();
|
||||
runtimeProxy = ManagementFactory.getRuntimeMXBean();
|
||||
asyncProfilerProxy = AsyncProfilerService.instance();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -185,4 +175,78 @@ public class InternalNodeProbe extends NodeProbe
|
|||
{
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
private static class TestMockMBeanAccessor implements MBeanAccessor
|
||||
{
|
||||
private final Map<Class<?>, Object> mbeanRegistry = new HashMap<>();
|
||||
|
||||
public TestMockMBeanAccessor()
|
||||
{
|
||||
registerMBean(StorageServiceMBean.class, StorageService.instance);
|
||||
registerMBean(SnapshotManagerMBean.class, SnapshotManager.instance);
|
||||
registerMBean(CMSOperationsMBean.class, CMSOperations.instance);
|
||||
registerMBean(AccordOperationsMBean.class, AccordOperations.instance);
|
||||
registerMBean(MessagingServiceMBean.class, MessagingService.instance());
|
||||
registerMBean(StreamManagerMBean.class, StreamManager.instance);
|
||||
registerMBean(CompactionManagerMBean.class, CompactionManager.instance);
|
||||
registerMBean(FailureDetectorMBean.class, (FailureDetectorMBean) FailureDetector.instance);
|
||||
registerMBean(CacheServiceMBean.class, CacheService.instance);
|
||||
registerMBean(StorageProxyMBean.class, StorageProxy.instance);
|
||||
registerMBean(HintsServiceMBean.class, HintsService.instance);
|
||||
registerMBean(GCInspectorMXBean.class, new GCInspector());
|
||||
registerMBean(GossiperMBean.class, Gossiper.instance);
|
||||
registerMBean(BatchlogManagerMBean.class, BatchlogManager.instance);
|
||||
registerMBean(ActiveRepairServiceMBean.class, ActiveRepairService.instance());
|
||||
registerMBean(MemoryMXBean.class, ManagementFactory.getMemoryMXBean());
|
||||
registerMBean(RuntimeMXBean.class, ManagementFactory.getRuntimeMXBean());
|
||||
registerMBean(AsyncProfilerMBean.class, AsyncProfilerService.instance());
|
||||
}
|
||||
|
||||
protected <T> void registerMBean(Class<T> clazz, T mbean)
|
||||
{
|
||||
mbeanRegistry.put(clazz, mbean);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findMBean(Class<T> clazz)
|
||||
{
|
||||
return mbeanRegistry.get(clazz) == null ? null : clazz.cast(mbeanRegistry.get(clazz));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T findMBeanMetric(Class<T> clazz, Props props)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMBeanMetricRegistered(Props props)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ColumnFamilyStoreMBean findColumnFamily(String type, String keyspace, String columnFamily)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompressionDictionaryManagerMBean findCompressionDictionary(String keyspace, String table)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ThreadPoolInfo> threadPoolInfos()
|
||||
{
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map.Entry<String, ColumnFamilyStoreMBean>> findColumnFamilies(String type)
|
||||
{
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.distributed.test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import com.datastax.driver.core.ResultSet;
|
||||
import com.datastax.driver.core.Session;
|
||||
|
||||
import net.bytebuddy.ByteBuddy;
|
||||
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
|
||||
import net.bytebuddy.implementation.MethodDelegation;
|
||||
import net.bytebuddy.implementation.bind.annotation.SuperCall;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.concurrent.SEPExecutor;
|
||||
import org.apache.cassandra.concurrent.SharedExecutorPool;
|
||||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.cql3.statements.SelectStatement;
|
||||
import org.apache.cassandra.distributed.Cluster;
|
||||
import org.apache.cassandra.service.QueryState;
|
||||
import org.apache.cassandra.transport.Dispatcher;
|
||||
import org.apache.cassandra.transport.messages.ResultMessage;
|
||||
|
||||
import static net.bytebuddy.matcher.ElementMatchers.named;
|
||||
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
|
||||
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
|
||||
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
|
||||
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
/**
|
||||
* This test verifyes that management requests are served even when native transport is under a heavy
|
||||
* load by driver's SELECT requests. We block these requests and verify that management requests are
|
||||
* still processed.
|
||||
*/
|
||||
public class ManagementRequestServingTest extends TestBaseImpl
|
||||
{
|
||||
private static final int MANAGEMENT_PORT = 11211;
|
||||
private static final int NATIVE_PORT = 9042;
|
||||
private static final int SELECT_REQUESTS_NUM = 10;
|
||||
|
||||
@Test
|
||||
public void testManagementRequestsServedUnderRegularPortLoad() throws Exception
|
||||
{
|
||||
ExecutorService executor = Executors.newFixedThreadPool(SELECT_REQUESTS_NUM);
|
||||
try (Cluster cluster = init(Cluster.build(1)
|
||||
.withInstanceInitializer(BlockingSelectByteBuddy::install)
|
||||
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)
|
||||
.set("start_native_transport_management", true)
|
||||
.set("native_transport_management_port", MANAGEMENT_PORT))
|
||||
.start());
|
||||
com.datastax.driver.core.Cluster driver = com.datastax.driver.core.Cluster.builder()
|
||||
.addContactPoint("127.0.0.1")
|
||||
.withPort(NATIVE_PORT)
|
||||
.build();
|
||||
Session session = driver.connect())
|
||||
{
|
||||
cluster.schemaChange(withKeyspace("CREATE TABLE %s.tbl (pk int PRIMARY KEY, v int)"));
|
||||
|
||||
long mgmtBefore = cluster.get(1).callOnInstance(() -> getCompletedTaskCountByExecutor("Native-Transport-Management-Tasks"));
|
||||
long reqBefore = cluster.get(1).callOnInstance(() -> getCompletedTaskCountByExecutor("Native-Transport-Requests"));
|
||||
|
||||
cluster.get(1).runOnInstance(() -> assertTrue(BlockingSelectByteBuddy.enabled.compareAndSet(false, true)));
|
||||
|
||||
List<Future<?>> futures = new ArrayList<>();
|
||||
for (int i = 0; i < SELECT_REQUESTS_NUM; i++)
|
||||
futures.add(CompletableFuture.supplyAsync(() -> session.execute("SELECT * FROM " + KEYSPACE + ".tbl"), executor));
|
||||
|
||||
cluster.get(1).runOnInstance(() -> {
|
||||
try
|
||||
{
|
||||
BlockingSelectByteBuddy.latch.await();
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
|
||||
try (com.datastax.driver.core.Cluster mgmt = com.datastax.driver.core.Cluster.builder()
|
||||
.addContactPoint("127.0.0.1")
|
||||
.withPort(MANAGEMENT_PORT)
|
||||
.build();
|
||||
Session mgmtSession = mgmt.connect("system"))
|
||||
{
|
||||
ResultSet rs = mgmtSession.execute("SELECT * FROM system.local");
|
||||
assertFalse("Management query should return data", rs.all().isEmpty());
|
||||
}
|
||||
|
||||
long mgmtAfter = cluster.get(1).callOnInstance(() -> getCompletedTaskCountByExecutor("Native-Transport-Management-Tasks"));
|
||||
assertTrue("Management executor should have processed requests", mgmtAfter > mgmtBefore);
|
||||
|
||||
cluster.get(1).runOnInstance(() -> BlockingSelectByteBuddy.signal.countDown());
|
||||
|
||||
for (Future<?> f : futures)
|
||||
f.get();
|
||||
|
||||
long reqAfter = cluster.get(1).callOnInstance(() -> getCompletedTaskCountByExecutor("Native-Transport-Requests"));
|
||||
assertTrue("Request executor should have processed requests", reqAfter > reqBefore);
|
||||
}
|
||||
finally
|
||||
{
|
||||
executor.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static long getCompletedTaskCountByExecutor(String name)
|
||||
{
|
||||
return SharedExecutorPool.SHARED.executors.stream()
|
||||
.filter(e -> e.name.endsWith(name))
|
||||
.findFirst()
|
||||
.map(SEPExecutor::getCompletedTaskCount)
|
||||
.orElse(0L);
|
||||
}
|
||||
|
||||
public static class BlockingSelectByteBuddy
|
||||
{
|
||||
static CountDownLatch latch = new CountDownLatch(SELECT_REQUESTS_NUM);
|
||||
static CountDownLatch signal = new CountDownLatch(1);
|
||||
static AtomicBoolean enabled = new AtomicBoolean(false);
|
||||
|
||||
static void install(ClassLoader cl, int ignored)
|
||||
{
|
||||
new ByteBuddy().rebase(SelectStatement.class)
|
||||
.method(named("execute")
|
||||
.and(takesArguments(QueryState.class, QueryOptions.class, Dispatcher.RequestTime.class)))
|
||||
.intercept(MethodDelegation.to(BlockingSelectByteBuddy.class))
|
||||
.make()
|
||||
.load(cl, ClassLoadingStrategy.Default.INJECTION);
|
||||
}
|
||||
|
||||
public static ResultMessage.Rows execute(QueryState state,
|
||||
QueryOptions options,
|
||||
Dispatcher.RequestTime requestTime,
|
||||
@SuperCall Callable<ResultMessage.Rows> r) throws Exception
|
||||
{
|
||||
if (enabled.get() && !state.getClientState().isInternal && !state.getClientState().isManagement())
|
||||
{
|
||||
latch.countDown();
|
||||
signal.await();
|
||||
}
|
||||
return r.call();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.distributed.test;
|
||||
|
||||
import com.datastax.driver.core.Session;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.distributed.Cluster;
|
||||
|
||||
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
|
||||
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
|
||||
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
|
||||
public class ManagementTransportMultiNodeTest extends TestBaseImpl
|
||||
{
|
||||
private static final int MANAGEMENT_PORT = 11211;
|
||||
|
||||
@Test
|
||||
public void testMultiNodeManagementPort() throws Exception
|
||||
{
|
||||
int nodes = 3;
|
||||
try (Cluster cluster = init(Cluster.build(nodes)
|
||||
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)
|
||||
.set("start_native_transport_management", true)
|
||||
.set("native_transport_management_port", MANAGEMENT_PORT))
|
||||
.start()))
|
||||
{
|
||||
for (int i = 1; i <= nodes; i++)
|
||||
{
|
||||
String host = cluster.get(i).config().broadcastAddress().getAddress().getHostAddress();
|
||||
try (com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder()
|
||||
.addContactPoint(host)
|
||||
.withPort(MANAGEMENT_PORT)
|
||||
.build();
|
||||
Session s = c.connect())
|
||||
{
|
||||
assertFalse("Management port on node " + i + " should respond",
|
||||
s.execute("SELECT * FROM system.local").all().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
String node1Host = cluster.get(1).config().broadcastAddress().getAddress().getHostAddress();
|
||||
try (com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder()
|
||||
.addContactPoint(node1Host)
|
||||
.withPort(MANAGEMENT_PORT)
|
||||
.build();
|
||||
Session s = c.connect())
|
||||
{
|
||||
int peerCount = s.execute("SELECT * FROM system.peers").all().size();
|
||||
assertEquals("Node 1 should see other peers via management port",
|
||||
nodes - 1, peerCount);
|
||||
|
||||
assertFalse("Keyspaces should be visible via management port",
|
||||
s.execute("SELECT * FROM system_schema.keyspaces").all().isEmpty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you 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 org.apache.cassandra.distributed.test;
|
||||
|
||||
import com.datastax.driver.core.Session;
|
||||
import com.datastax.driver.core.exceptions.InvalidQueryException;
|
||||
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.BeforeClass;
|
||||
import org.junit.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.distributed.Cluster;
|
||||
import org.apache.cassandra.tools.ToolRunner;
|
||||
import org.apache.cassandra.tools.ToolRunner.ToolResult;
|
||||
|
||||
import static org.apache.cassandra.distributed.api.Feature.GOSSIP;
|
||||
import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL;
|
||||
import static org.apache.cassandra.distributed.api.Feature.NETWORK;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
/**
|
||||
* End-to-end tests for the management native transport protocol (CEP-38).
|
||||
*
|
||||
* @see org.apache.cassandra.service.NativeTransportManagementService
|
||||
*/
|
||||
public class ManagementTransportProtocolTest extends TestBaseImpl
|
||||
{
|
||||
private static final Logger logger = LoggerFactory.getLogger(ManagementTransportProtocolTest.class);
|
||||
|
||||
private static final int MANAGEMENT_PORT = 11211;
|
||||
private static final int NATIVE_PORT = 9042;
|
||||
private static Cluster CLUSTER;
|
||||
|
||||
@BeforeClass
|
||||
public static void setupCluster() throws Exception
|
||||
{
|
||||
CLUSTER = init(Cluster.build(1)
|
||||
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)
|
||||
.set("start_native_transport_management", true)
|
||||
.set("native_transport_management_port", MANAGEMENT_PORT))
|
||||
.start());
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void teardownCluster()
|
||||
{
|
||||
if (CLUSTER == null)
|
||||
return;
|
||||
CLUSTER.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testManagementPortRemainsUpWhenBinaryDisabled() throws Throwable
|
||||
{
|
||||
assertTrue("Regular port accessible", isAlive(NATIVE_PORT));
|
||||
assertTrue("Management port accessible", isAlive(MANAGEMENT_PORT));
|
||||
|
||||
ToolResult tool = ToolRunner.invokeNodetoolJvmDtest(CLUSTER.get(1), "disablebinary");
|
||||
assertEquals(0, tool.getExitCode());
|
||||
assertFalse("Regular port NOT accessible after disable", isAlive(NATIVE_PORT));
|
||||
assertTrue("Management port still accessible after disable", isAlive(MANAGEMENT_PORT));
|
||||
|
||||
tool = ToolRunner.invokeNodetoolJvmDtest(CLUSTER.get(1), "enablebinary");
|
||||
assertEquals(0, tool.getExitCode());
|
||||
assertTrue("Regular port accessible after re-enable", isAlive(NATIVE_PORT));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectOnSystemKeyspacesAllowed()
|
||||
{
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
|
||||
Session s = c.connect())
|
||||
{
|
||||
assertFalse(s.execute("SELECT * FROM system.local").all().isEmpty());
|
||||
// system.peers is empty on single-node
|
||||
assertTrue(s.execute("SELECT * FROM system.peers").all().isEmpty());
|
||||
assertFalse(s.execute("SELECT * FROM system_schema.keyspaces").all().isEmpty());
|
||||
assertFalse(s.execute("SELECT * FROM system_schema.tables").all().isEmpty());
|
||||
assertFalse(s.execute("SELECT * FROM system_schema.columns").all().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectOnVirtualKeyspacesAllowed()
|
||||
{
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
|
||||
Session s = c.connect())
|
||||
{
|
||||
s.execute("SELECT * FROM system_virtual_schema.keyspaces");
|
||||
s.execute("SELECT * FROM system_virtual_schema.tables");
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidQueryException.class)
|
||||
public void testSelectOnUserKeyspaceRejected()
|
||||
{
|
||||
CLUSTER.schemaChange("CREATE TABLE IF NOT EXISTS " + KEYSPACE + ".mgmt_test (pk int PRIMARY KEY, v int)");
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
|
||||
Session s = c.connect())
|
||||
{
|
||||
s.execute("SELECT * FROM " + KEYSPACE + ".mgmt_test");
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidQueryException.class)
|
||||
public void testInsertRejectedOnManagementPort()
|
||||
{
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
|
||||
Session s = c.connect())
|
||||
{
|
||||
s.execute("INSERT INTO system.local (key) VALUES ('test')");
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidQueryException.class)
|
||||
public void testDDLRejectedOnManagementPort()
|
||||
{
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
|
||||
Session s = c.connect())
|
||||
{
|
||||
s.execute("CREATE TABLE " + KEYSPACE + ".should_fail (pk int PRIMARY KEY)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test(expected = InvalidQueryException.class)
|
||||
public void testUnqualifiedSelectRejected()
|
||||
{
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
|
||||
Session s = c.connect())
|
||||
{
|
||||
s.execute("SELECT * FROM local");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is also a corner case for the driver's behavior on the management port.
|
||||
* When connecting, the driver sends a USE statement for the keyspace provided in connect().
|
||||
* If that keyspace is a system keyspace, the USE msut succeeds, and subsequent queries without
|
||||
* keyspace qualification are allowed. If that keyspace is a user keyspace, the USE should fail.
|
||||
*/
|
||||
@Test
|
||||
public void testConnectWithSystemKeyspaceAllowed()
|
||||
{
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
|
||||
Session s = c.connect("system_schema"))
|
||||
{
|
||||
// Driver sends USE system_schema, then we query a table without qualification
|
||||
assertFalse(s.execute("SELECT * FROM system_schema.keyspaces").all().isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testConnectWithUserKeyspaceRejected()
|
||||
{
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(MANAGEMENT_PORT);
|
||||
Session s = c.connect(KEYSPACE))
|
||||
{
|
||||
fail("Should not be able to USE a user keyspace on management port");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.debug("Expected failure when connecting with user keyspace: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static com.datastax.driver.core.Cluster driverOnPort(int port)
|
||||
{
|
||||
return com.datastax.driver.core.Cluster.builder()
|
||||
.addContactPoint("127.0.0.1")
|
||||
.withPort(port)
|
||||
.build();
|
||||
}
|
||||
|
||||
private static boolean isAlive(int port)
|
||||
{
|
||||
try (com.datastax.driver.core.Cluster c = driverOnPort(port);
|
||||
Session s = c.connect())
|
||||
{
|
||||
s.execute("SELECT * FROM system.local");
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,13 +18,10 @@
|
|||
|
||||
package org.apache.cassandra.distributed.test;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import com.datastax.driver.core.Cluster;
|
||||
import com.datastax.driver.core.Session;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.AfterClass;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.apache.cassandra.distributed.api.ICluster;
|
||||
|
|
@ -46,52 +43,96 @@ import static org.junit.Assert.assertTrue;
|
|||
*/
|
||||
public class NodeToolEnableDisableBinaryTest extends TestBaseImpl
|
||||
{
|
||||
private static ICluster cluster;
|
||||
|
||||
@Before
|
||||
public void setupEnv() throws IOException
|
||||
{
|
||||
if (cluster == null)
|
||||
{
|
||||
cluster = init(builder().withNodes(1)
|
||||
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL))
|
||||
.start());
|
||||
cluster.get(1).nodetool("disableautocompaction");
|
||||
}
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void teardownEnv() throws Exception
|
||||
{
|
||||
cluster.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnableDisableBinary() throws Throwable
|
||||
{
|
||||
// We can connect
|
||||
assertTrue(canConnect());
|
||||
try (ICluster<?> nodeCluster = init(builder().withNodes(1)
|
||||
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL))
|
||||
.start()))
|
||||
{
|
||||
nodeCluster.get(1).nodetool("disableautocompaction");
|
||||
|
||||
// We can't connect after disabling
|
||||
ToolResult tool = ToolRunner.invokeNodetoolJvmDtest(cluster.get(1), "disablebinary");
|
||||
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Stop listening for CQL clients");
|
||||
assertTrue(tool.getCleanedStderr().isEmpty());
|
||||
assertEquals(0, tool.getExitCode());
|
||||
assertFalse(canConnect());
|
||||
// We can connect
|
||||
assertTrue(canConnect());
|
||||
|
||||
// We can connect after re-enabling
|
||||
tool = ToolRunner.invokeNodetoolJvmDtest(cluster.get(1), "enablebinary");
|
||||
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Starting listening for CQL clients");
|
||||
assertTrue(tool.getCleanedStderr().isEmpty());
|
||||
assertEquals(0, tool.getExitCode());
|
||||
assertTrue(canConnect());
|
||||
// We can't connect after disabling
|
||||
ToolResult tool = ToolRunner.invokeNodetoolJvmDtest(nodeCluster.get(1), "disablebinary");
|
||||
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Stop listening for CQL clients");
|
||||
assertTrue(tool.getCleanedStderr().isEmpty());
|
||||
assertEquals(0, tool.getExitCode());
|
||||
assertFalse(canConnect());
|
||||
|
||||
// We can connect after re-enabling
|
||||
tool = ToolRunner.invokeNodetoolJvmDtest(nodeCluster.get(1), "enablebinary");
|
||||
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Starting listening for CQL clients");
|
||||
assertTrue(tool.getCleanedStderr().isEmpty());
|
||||
assertEquals(0, tool.getExitCode());
|
||||
assertTrue(canConnect());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is an important corner case to verify that when binary protocol is disabled,
|
||||
* we can still connect to the management port and query system tables. This case
|
||||
* must be explicitly tested because the management port is used by ops and other
|
||||
* monitoring tools to control the cluster.
|
||||
*
|
||||
* @throws Throwable If any exception occurs during the test.
|
||||
*/
|
||||
@Test
|
||||
public void testSystemQueriesViaManagementPortWhenBinaryDisabled() throws Throwable
|
||||
{
|
||||
int managementPort = 11211;
|
||||
int numberOfNodes = 1;
|
||||
try (ICluster<?> managementCluster = init(builder().withNodes(numberOfNodes)
|
||||
.withConfig(config -> config.with(NETWORK, GOSSIP, NATIVE_PROTOCOL)
|
||||
.set("start_native_transport_management", true)
|
||||
.set("native_transport_management_port", managementPort))
|
||||
.start()))
|
||||
{
|
||||
managementCluster.get(1).nodetool("disableautocompaction");
|
||||
assertTrue("Regular native CQL port should is accessible", canConnect());
|
||||
assertTrue("Management port should is accessible", canConnect(managementPort));
|
||||
|
||||
ToolResult tool = ToolRunner.invokeNodetoolJvmDtest(managementCluster.get(1), "disablebinary");
|
||||
Assertions.assertThat(tool.getStdout()).containsIgnoringCase("Stop listening for CQL clients");
|
||||
assertEquals(0, tool.getExitCode());
|
||||
assertFalse("Regular native CQL port should NOT be accessible", canConnect());
|
||||
assertTrue("Management port should is accessible", canConnect(managementPort));
|
||||
|
||||
try (Cluster c = Cluster.builder()
|
||||
.addContactPoint("127.0.0.1")
|
||||
.withPort(managementPort)
|
||||
.build();
|
||||
Session s = c.connect())
|
||||
{
|
||||
assertFalse("system.local should return data",
|
||||
s.execute("SELECT * FROM system.local").all().isEmpty());
|
||||
assertTrue("system.peers is NOT empty, howeverwe only have one node in the cluster",
|
||||
s.execute("SELECT * FROM system.peers").all().isEmpty());
|
||||
assertFalse("system_schema.keyspaces should return data",
|
||||
s.execute("SELECT * FROM system_schema.keyspaces").all().isEmpty());
|
||||
assertFalse("system_schema.tables should return data",
|
||||
s.execute("SELECT * FROM system_schema.tables").all().isEmpty());
|
||||
}
|
||||
|
||||
tool = ToolRunner.invokeNodetoolJvmDtest(managementCluster.get(1), "enablebinary");
|
||||
assertEquals(0, tool.getExitCode());
|
||||
assertTrue("Should connect to regular binary after re-enabling", canConnect());
|
||||
}
|
||||
}
|
||||
|
||||
private boolean canConnect()
|
||||
{
|
||||
return canConnect(9042);
|
||||
}
|
||||
|
||||
private boolean canConnect(int port)
|
||||
{
|
||||
boolean canConnect = false;
|
||||
try(com.datastax.driver.core.Cluster c = com.datastax.driver.core.Cluster.builder()
|
||||
.addContactPoint("127.0.0.1")
|
||||
.withPort(port)
|
||||
.build();
|
||||
Session s = c.connect("system_schema"))
|
||||
{
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue