diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 3af8d77c..8d3d9b49 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ jobs: fail-fast: false matrix: cassandra-version: ['3.11', '4.0', '4.1', '3.11_ubi', '4.0_ubi', '4.1_ubi'] - itTest : ['LifecycleIT', 'KeepAliveIT', 'NonDestructiveOpsIT', 'DestructiveOpsIT'] + itTest : ['LifecycleIT', 'KeepAliveIT', 'NonDestructiveOpsIT', 'DestructiveOpsIT', 'NonDestructiveOpsResourcesV2IT'] include: - cassandra-version: '3.11' run311tests: true @@ -94,7 +94,7 @@ jobs: fail-fast: false matrix: platform-version: ['jdk8', 'ubi'] - itTest : ['LifecycleIT', 'KeepAliveIT', 'NonDestructiveOpsIT', 'DestructiveOpsIT', 'DSESpecificIT'] + itTest : ['LifecycleIT', 'KeepAliveIT', 'NonDestructiveOpsIT', 'DestructiveOpsIT', 'DSESpecificIT', 'NonDestructiveOpsResourcesV2IT'] include: - platform-version: 'jdk8' runDSEtests: true diff --git a/CHANGELOG.md b/CHANGELOG.md index b2bf656a..44de88e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ Changelog for Management API, new PRs should update the `main / unreleased` sect [CHANGE] [#323](https://github.com/k8ssandra/management-api-for-apache-cassandra/issues/323) Rename Maven artifact groupId from com.datastax to io.k8ssandra [FEATURE] [#323](https://github.com/k8ssandra/management-api-for-apache-cassandra/issues/323) Add OpenAPI Java client generation [FEATURE] [#337](https://github.com/k8ssandra/management-api-for-apache-cassandra/issues/337) Publish Maven artifacts to Cloudsmith.io +[FEATURE] [#326](https://github.com/k8ssandra/management-api-for-apache-cassandra/issues/326) Provide topology endpoints in OpenAPI client +[FEATURE] [#345](https://github.com/k8ssandra/management-api-for-apache-cassandra/issues/345) Implement host related methods [BUGFIX] [#339](https://github.com/k8ssandra/management-api-for-apache-cassandra/issues/339) OpenAPI client publish does not also publish other artifacts diff --git a/dse-68/Dockerfile.ubi8 b/dse-68/Dockerfile.ubi8 index 40aa39f9..cd8a5071 100644 --- a/dse-68/Dockerfile.ubi8 +++ b/dse-68/Dockerfile.ubi8 @@ -4,6 +4,46 @@ ARG UBI_BASETAG=latest FROM datastax/dse-mgmtapi-6_8:${DSE_VERSION} AS dse-server-base +FROM --platform=$BUILDPLATFORM maven:3.8.7-eclipse-temurin-11 as mgmtapi-setup + +WORKDIR / + +ENV MAAC_PATH /opt/management-api +ENV DSE_HOME /opt/dse + +COPY pom.xml /tmp/pom.xml +COPY management-api-agent-common/pom.xml /tmp/management-api-agent-common/pom.xml +COPY management-api-agent-3.x/pom.xml /tmp/management-api-agent-3.x/pom.xml +COPY management-api-agent-4.x/pom.xml /tmp/management-api-agent-4.x/pom.xml +COPY management-api-agent-4.1.x/pom.xml /tmp/management-api-agent-4.1.x/pom.xml +COPY management-api-agent-dse-6.8/pom.xml tmp/management-api-agent-dse-6.8/pom.xml +COPY management-api-common/pom.xml /tmp/management-api-common/pom.xml +COPY management-api-server/pom.xml /tmp/management-api-server/pom.xml +COPY settings.xml settings.xml /root/.m2/ +# this duplicates work done in the next steps, but this should provide +# a solid cache layer that only gets reset on pom.xml changes +RUN cd /tmp && mvn -q -ff -T 1C install -DskipOpenApi -P dse && rm -rf target + +COPY management-api-agent-common /tmp/management-api-agent-common +COPY management-api-agent-3.x /tmp/management-api-agent-3.x +COPY management-api-agent-4.x /tmp/management-api-agent-4.x +COPY management-api-agent-4.1.x /tmp/management-api-agent-4.1.x +COPY management-api-agent-dse-6.8 /tmp/management-api-agent-dse-6.8 +COPY management-api-common /tmp/management-api-common +COPY management-api-server /tmp/management-api-server +RUN mkdir -m 775 $MAAC_PATH \ + && cd /tmp \ + && mvn -q -ff package -DskipTests -DskipOpenApi -P dse \ + && find /tmp -type f -name "datastax-*.jar" -exec mv -t $MAAC_PATH -i '{}' + \ + && rm $MAAC_PATH/datastax-mgmtapi-agent-3* \ + && rm $MAAC_PATH/datastax-mgmtapi-agent-4* \ + && rm $MAAC_PATH/datastax-mgmtapi-*common* \ + && cd ${MAAC_PATH} \ + && ln -s datastax-mgmtapi-agent-dse-6.8-0.1.0-SNAPSHOT.jar datastax-mgmtapi-agent-0.1.0-SNAPSHOT.jar \ + && ln -s datastax-mgmtapi-agent-0.1.0-SNAPSHOT.jar datastax-mgmtapi-agent.jar \ + && ln -s datastax-mgmtapi-server-0.1.0-SNAPSHOT.jar datastax-mgmtapi-server.jar && \ + chmod -R g+w ${MAAC_PATH} + ############################################################# # Using UBI8 with Python 2 support, eventually we may switch to Python 3 @@ -64,7 +104,7 @@ RUN chmod 0555 /entrypoint.sh /overwritable-conf-files /licenses /base-checks.sh # Use OSS Management API ENV CASSANDRA_CONF ${DSE_HOME}/resources/cassandra/conf ENV MAAC_PATH /opt/management-api -COPY --chown=dse:root --from=dse-server-base $MAAC_PATH $MAAC_PATH +COPY --chown=dse:root --from=mgmtapi-setup $MAAC_PATH $MAAC_PATH # Add CDC Agent ENV CDC_AGENT_PATH=/opt/cdc_agent COPY --chown=dse:root --from=dse-server-base $CDC_AGENT_PATH $CDC_AGENT_PATH diff --git a/management-api-agent-3.x/pom.xml b/management-api-agent-3.x/pom.xml index eadc9d8b..58f46afa 100644 --- a/management-api-agent-3.x/pom.xml +++ b/management-api-agent-3.x/pom.xml @@ -60,6 +60,19 @@ ${junit.version} test + + org.assertj + assertj-core + ${assertj.version} + test + + + io.k8ssandra + datastax-mgmtapi-agent-common + ${project.version} + tests + test + diff --git a/management-api-agent-3.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer3x.java b/management-api-agent-3.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer3x.java index f7af30bd..56ed09a7 100644 --- a/management-api-agent-3.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer3x.java +++ b/management-api-agent-3.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer3x.java @@ -73,7 +73,7 @@ public ObjectSerializer3x(Class clazz, Type genericType) { field -> field.getName(), field -> new FieldSerializer( - GenericSerializer3x.getType(field.getType()), field)))); + GenericSerializer3x.getType(field.getGenericType()), field)))); // currently not recursive; multiple ways to do it } diff --git a/management-api-agent-3.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI3x.java b/management-api-agent-3.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI3x.java index 3f1ce5fb..d69a22c6 100644 --- a/management-api-agent-3.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI3x.java +++ b/management-api-agent-3.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI3x.java @@ -218,9 +218,12 @@ public List> getEndpointStates() { states.put(s.getKey().name(), value); } - states.put("ENDPOINT_IP", entry.getKey().getHostAddress()); + InetAddress endpoint = entry.getKey(); + states.put("ENDPOINT_IP", endpoint.getHostAddress()); states.put("IS_ALIVE", Boolean.toString(entry.getValue().isAlive())); states.put("PARTITIONER", partitioner.getClass().getName()); + states.put("CLUSTER_NAME", getStorageService().getClusterName()); + states.put("IS_LOCAL", Boolean.toString(endpoint.equals(FBUtilities.getBroadcastAddress()))); result.add(states); } diff --git a/management-api-agent-3.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer3xTest.java b/management-api-agent-3.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer3xTest.java new file mode 100644 index 00000000..1adb8ffa --- /dev/null +++ b/management-api-agent-3.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer3xTest.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.rpc; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ObjectSerializer3xTest extends ObjectSerializerTestBase> { + + @Override + protected ObjectSerializer3x createExampleSerializer() { + return new ObjectSerializer3x<>(Example.class); + } + + @Override + protected String getCqlType(ObjectSerializer3x serializer, String fieldName) { + assertThat(serializer.serializers).containsKey(fieldName); + return serializer.serializers.get(fieldName).type.toString(); + } +} diff --git a/management-api-agent-4.1.x/pom.xml b/management-api-agent-4.1.x/pom.xml index d46db5e8..79303182 100644 --- a/management-api-agent-4.1.x/pom.xml +++ b/management-api-agent-4.1.x/pom.xml @@ -45,6 +45,19 @@ ${junit.version} test + + org.assertj + assertj-core + ${assertj.version} + test + + + io.k8ssandra + datastax-mgmtapi-agent-common + ${project.version} + tests + test + diff --git a/management-api-agent-4.1.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer41x.java b/management-api-agent-4.1.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer41x.java index 4f87f1de..d900f87f 100644 --- a/management-api-agent-4.1.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer41x.java +++ b/management-api-agent-4.1.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer41x.java @@ -73,7 +73,7 @@ public ObjectSerializer41x(Class clazz, Type genericType) { field -> field.getName(), field -> new FieldSerializer( - GenericSerializer41x.getType(field.getType()), field)))); + GenericSerializer41x.getType(field.getGenericType()), field)))); // currently not recursive; multiple ways to do it } diff --git a/management-api-agent-4.1.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI41x.java b/management-api-agent-4.1.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI41x.java index e8c5552f..069a8fc6 100644 --- a/management-api-agent-4.1.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI41x.java +++ b/management-api-agent-4.1.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI41x.java @@ -255,6 +255,9 @@ public List> getEndpointStates() { states.put("ENDPOINT_IP", endpoint.getHostAddress(false)); states.put("IS_ALIVE", Boolean.toString(state.isAlive())); states.put("PARTITIONER", partitioner.getClass().getName()); + states.put("CLUSTER_NAME", getStorageService().getClusterName()); + states.put( + "IS_LOCAL", Boolean.toString(endpoint.equals(FBUtilities.getBroadcastAddressAndPort()))); result.add(states); } diff --git a/management-api-agent-4.1.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer41xTest.java b/management-api-agent-4.1.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer41xTest.java new file mode 100644 index 00000000..32714640 --- /dev/null +++ b/management-api-agent-4.1.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer41xTest.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.rpc; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ObjectSerializer41xTest + extends ObjectSerializerTestBase> { + + @Override + protected ObjectSerializer41x createExampleSerializer() { + return new ObjectSerializer41x<>(Example.class); + } + + @Override + protected String getCqlType(ObjectSerializer41x serializer, String fieldName) { + assertThat(serializer.serializers).containsKey(fieldName); + return serializer.serializers.get(fieldName).type.toString(); + } +} diff --git a/management-api-agent-4.x/pom.xml b/management-api-agent-4.x/pom.xml index dfff22ca..cec5fbf8 100644 --- a/management-api-agent-4.x/pom.xml +++ b/management-api-agent-4.x/pom.xml @@ -42,6 +42,19 @@ ${junit.version} test + + org.assertj + assertj-core + ${assertj.version} + test + + + io.k8ssandra + datastax-mgmtapi-agent-common + ${project.version} + tests + test + diff --git a/management-api-agent-4.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer4x.java b/management-api-agent-4.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer4x.java index b8ff1061..0cb1a33a 100644 --- a/management-api-agent-4.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer4x.java +++ b/management-api-agent-4.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer4x.java @@ -73,7 +73,7 @@ public ObjectSerializer4x(Class clazz, Type genericType) { field -> field.getName(), field -> new FieldSerializer( - GenericSerializer4x.getType(field.getType()), field)))); + GenericSerializer4x.getType(field.getGenericType()), field)))); // currently not recursive; multiple ways to do it } diff --git a/management-api-agent-4.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI4x.java b/management-api-agent-4.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI4x.java index 096a2f7e..5122c50e 100644 --- a/management-api-agent-4.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI4x.java +++ b/management-api-agent-4.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI4x.java @@ -255,6 +255,9 @@ public List> getEndpointStates() { states.put("ENDPOINT_IP", endpoint.address.getHostAddress()); states.put("IS_ALIVE", Boolean.toString(state.isAlive())); states.put("PARTITIONER", partitioner.getClass().getName()); + states.put("CLUSTER_NAME", getStorageService().getClusterName()); + states.put( + "IS_LOCAL", Boolean.toString(endpoint.equals(FBUtilities.getBroadcastAddressAndPort()))); result.add(states); } diff --git a/management-api-agent-4.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer4xTest.java b/management-api-agent-4.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer4xTest.java new file mode 100644 index 00000000..c024451d --- /dev/null +++ b/management-api-agent-4.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer4xTest.java @@ -0,0 +1,22 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.rpc; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ObjectSerializer4xTest extends ObjectSerializerTestBase> { + + @Override + protected ObjectSerializer4x createExampleSerializer() { + return new ObjectSerializer4x<>(Example.class); + } + + @Override + protected String getCqlType(ObjectSerializer4x serializer, String fieldName) { + assertThat(serializer.serializers).containsKey(fieldName); + return serializer.serializers.get(fieldName).type.toString(); + } +} diff --git a/management-api-agent-5.0.x/pom.xml b/management-api-agent-5.0.x/pom.xml index db5cc3a6..9a721654 100644 --- a/management-api-agent-5.0.x/pom.xml +++ b/management-api-agent-5.0.x/pom.xml @@ -45,6 +45,19 @@ ${junit.version} test + + org.assertj + assertj-core + ${assertj.version} + test + + + io.k8ssandra + datastax-mgmtapi-agent-common + ${project.version} + tests + test + diff --git a/management-api-agent-5.0.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer50x.java b/management-api-agent-5.0.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer50x.java index 5ef61d17..67fb6ca8 100644 --- a/management-api-agent-5.0.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer50x.java +++ b/management-api-agent-5.0.x/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializer50x.java @@ -73,7 +73,7 @@ public ObjectSerializer50x(Class clazz, Type genericType) { field -> field.getName(), field -> new FieldSerializer( - GenericSerializer50x.getType(field.getType()), field)))); + GenericSerializer50x.getType(field.getGenericType()), field)))); // currently not recursive; multiple ways to do it } diff --git a/management-api-agent-5.0.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI50x.java b/management-api-agent-5.0.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI50x.java index e5abee3a..77ecb85d 100644 --- a/management-api-agent-5.0.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI50x.java +++ b/management-api-agent-5.0.x/src/main/java/com/datastax/mgmtapi/shim/CassandraAPI50x.java @@ -255,6 +255,9 @@ public List> getEndpointStates() { states.put("ENDPOINT_IP", endpoint.getHostAddress(false)); states.put("IS_ALIVE", Boolean.toString(state.isAlive())); states.put("PARTITIONER", partitioner.getClass().getName()); + states.put("CLUSTER_NAME", getStorageService().getClusterName()); + states.put( + "IS_LOCAL", Boolean.toString(endpoint.equals(FBUtilities.getBroadcastAddressAndPort()))); result.add(states); } diff --git a/management-api-agent-5.0.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer50xTest.java b/management-api-agent-5.0.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer50xTest.java new file mode 100644 index 00000000..8cb90262 --- /dev/null +++ b/management-api-agent-5.0.x/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializer50xTest.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.rpc; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ObjectSerializer50xTest + extends ObjectSerializerTestBase> { + + @Override + protected ObjectSerializer50x createExampleSerializer() { + return new ObjectSerializer50x<>(Example.class); + } + + @Override + protected String getCqlType(ObjectSerializer50x serializer, String fieldName) { + assertThat(serializer.serializers).containsKey(fieldName); + return serializer.serializers.get(fieldName).type.toString(); + } +} diff --git a/management-api-agent-common/pom.xml b/management-api-agent-common/pom.xml index 385da8ab..53a36ea9 100644 --- a/management-api-agent-common/pom.xml +++ b/management-api-agent-common/pom.xml @@ -60,6 +60,12 @@ ${junit.version} test + + org.assertj + assertj-core + ${assertj.version} + test + io.netty netty-all @@ -177,6 +183,18 @@ + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + test-jar + + + + diff --git a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java index fe0d9ab5..eb74e3e9 100644 --- a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java +++ b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/NodeOpsProvider.java @@ -38,6 +38,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import javax.management.NotificationFilter; import javax.management.openmbean.CompositeDataSupport; import javax.management.openmbean.TabularData; import org.apache.cassandra.auth.AuthenticatedUser; @@ -52,6 +53,7 @@ import org.apache.cassandra.repair.messages.RepairOption; import org.apache.cassandra.service.StorageProxy; import org.apache.cassandra.utils.Pair; +import org.apache.cassandra.utils.progress.ProgressEventType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -379,6 +381,12 @@ public String forceKeyspaceCompaction( return submitJob(OperationType.COMPACTION.name(), compactionOperation, async); } + @Rpc(name = "getCompactions") + public List> getCompactions() { + logger.debug("Getting active compactions"); + return ShimLoader.instance.get().getCompactionManager().getCompactions(); + } + @Rpc(name = "garbageCollect") public void garbageCollect( @RpcParam(name = "tombstoneOption") String tombstoneOption, @@ -534,17 +542,18 @@ public Map getReplication(@RpcParam(name = "keyspaceName") Strin return rows.one().getMap("replication", UTF8Type.instance, UTF8Type.instance); } - @Rpc(name = "getTables") - public List getTables(@RpcParam(name = "keyspaceName") String keyspaceName) { + @Rpc(name = "getTables", multiRow = true) + public List getTables(@RpcParam(name = "keyspaceName") String keyspaceName) { String query = QueryBuilder.selectFrom("system_schema", "tables") .column("table_name") + .column("compaction") .where(Relation.column("keyspace_name").isEqualTo(QueryBuilder.literal(keyspaceName))) .asCql(); UntypedResultSet rows = ShimLoader.instance.get().processQuery(query, ConsistencyLevel.ONE); - List tables = new ArrayList<>(); + List
tables = new ArrayList<>(); for (UntypedResultSet.Row row : rows) { - tables.add(row.getString("table_name")); + tables.add(new Table(row.getString("table_name"), row.getFrozenTextMap("compaction"))); } return tables; } @@ -733,31 +742,127 @@ public void clearSnapshots( } @Rpc(name = "repair") - public void repair( + public String repair( @RpcParam(name = "keyspaceName") String keyspace, @RpcParam(name = "tables") List tables, - @RpcParam(name = "full") Boolean full) + @RpcParam(name = "full") Boolean full, + @RpcParam(name = "notifications") boolean notifications, + @RpcParam(name = "repairParallelism") String repairParallelism, + @RpcParam(name = "datacenters") List datacenters, + @RpcParam(name = "associatedTokens") String ringRangeString, + @RpcParam(name = "repairThreadCount") Integer repairThreadCount) throws IOException { // At least one keyspace is required - if (keyspace != null) { - // create the repair spec - Map repairSpec = new HashMap<>(); - - // add any specified tables to the repair spec - if (tables != null && !tables.isEmpty()) { - // set the tables/column families - repairSpec.put(RepairOption.COLUMNFAMILIES_KEY, String.join(",", tables)); + assert (keyspace != null); + Map repairSpec = new HashMap<>(); + // add tables/column families + if (tables != null && !tables.isEmpty()) { + repairSpec.put(RepairOption.COLUMNFAMILIES_KEY, String.join(",", tables)); + } + // set incremental reapir + repairSpec.put(RepairOption.INCREMENTAL_KEY, Boolean.toString(!full)); + // Parallelism should be set if it's requested OR if incremental repair is requested. + if (!full) { + // Incremental repair requested, make sure parallelism is correct + if (repairParallelism != null + && !RepairParallelism.PARALLEL.getName().equals(repairParallelism)) { + throw new IOException( + "Invalid repair combination. Incremental repair if Parallelism is not set"); } - - // handle incremental vs full - boolean isIncremental = Boolean.FALSE.equals(full); - repairSpec.put(RepairOption.INCREMENTAL_KEY, Boolean.toString(isIncremental)); - if (isIncremental) { - // incremental repairs will fail if parallelism is not set - repairSpec.put(RepairOption.PARALLELISM_KEY, RepairParallelism.PARALLEL.getName()); + // Incremental repair and parallelism should be set + repairSpec.put(RepairOption.PARALLELISM_KEY, RepairParallelism.PARALLEL.getName()); + } + if (repairThreadCount != null) { + // if specified, the value should be at least 1 + if (repairThreadCount.compareTo(Integer.valueOf(0)) <= 0) { + throw new IOException( + "Invalid repari thread count: " + + repairThreadCount + + ". Value should be greater than 0"); } - ShimLoader.instance.get().getStorageService().repairAsync(keyspace, repairSpec); + repairSpec.put(RepairOption.JOB_THREADS_KEY, repairThreadCount.toString()); + } + repairSpec.put(RepairOption.TRACE_KEY, Boolean.toString(Boolean.FALSE)); + + if (ringRangeString != null && !ringRangeString.isEmpty()) { + repairSpec.put(RepairOption.RANGES_KEY, ringRangeString); + } + // add datacenters to the repair spec + if (datacenters != null && !datacenters.isEmpty()) { + repairSpec.put(RepairOption.DATACENTERS_KEY, String.join(",", datacenters)); + } + + // Since Cassandra provides us with a async, we don't need to use our executor interface for + // this. + final int repairJobId = + ShimLoader.instance.get().getStorageService().repairAsync(keyspace, repairSpec); + + if (!notifications) { + return Integer.valueOf(repairJobId).toString(); } + + String jobId = String.format("repair-%d", repairJobId); + final Job job = service.createJob("repair", jobId); + + if (repairJobId == 0) { + // Job is done and won't continue + job.setStatusChange(ProgressEventType.COMPLETE, ""); + job.setStatus(Job.JobStatus.COMPLETED); + job.setFinishedTime(System.currentTimeMillis()); + service.updateJob(job); + return job.getJobId(); + } + + ShimLoader.instance + .get() + .getStorageService() + .addNotificationListener( + (notification, handback) -> { + if (notification.getType().equals("progress")) { + Map data = (Map) notification.getUserData(); + ProgressEventType progress = ProgressEventType.values()[data.get("type")]; + + switch (progress) { + case START: + job.setStatusChange(progress, notification.getMessage()); + job.setStartTime(System.currentTimeMillis()); + break; + case NOTIFICATION: + case PROGRESS: + break; + case ERROR: + case ABORT: + job.setError(new RuntimeException(notification.getMessage())); + job.setStatus(Job.JobStatus.ERROR); + job.setFinishedTime(System.currentTimeMillis()); + break; + case SUCCESS: + job.setStatusChange(progress, notification.getMessage()); + // SUCCESS / ERROR does not mean the job has completed yet (COMPLETE is that) + break; + case COMPLETE: + job.setStatusChange(progress, notification.getMessage()); + job.setStatus(Job.JobStatus.COMPLETED); + job.setFinishedTime(System.currentTimeMillis()); + break; + } + service.updateJob(job); + } + }, + (NotificationFilter) + notification -> { + final int repairNo = + Integer.parseInt(((String) notification.getSource()).split(":")[1]); + return repairNo == repairJobId; + }, + null); + + return job.getJobId(); + } + + @Rpc(name = "stopAllRepairs") + public void stopAllRepairs() { + ShimLoader.instance.get().getStorageService().forceTerminateAllRepairSessions(); } @Rpc(name = "move") @@ -778,4 +883,10 @@ public String move( return submitJob("move", moveOperation, async); } + + @Rpc(name = "getRangeToEndpointMap") + public Map, List> getRangeToEndpointMap( + @RpcParam(name = "keyspaceName") String keyspaceName) { + return ShimLoader.instance.get().getStorageService().getRangeToEndpointMap(keyspaceName); + } } diff --git a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/Table.java b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/Table.java new file mode 100644 index 00000000..dd301049 --- /dev/null +++ b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/Table.java @@ -0,0 +1,18 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi; + +import java.util.Map; + +public class Table { + public final String name; + public final Map compaction; + + public Table(String name, Map compaction) { + this.name = name; + this.compaction = compaction; + } +} diff --git a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/util/Job.java b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/util/Job.java index 439304bd..20e4e9dd 100644 --- a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/util/Job.java +++ b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/util/Job.java @@ -6,7 +6,9 @@ package com.datastax.mgmtapi.util; import com.google.common.annotations.VisibleForTesting; -import java.util.UUID; +import java.util.ArrayList; +import java.util.List; +import org.apache.cassandra.utils.progress.ProgressEventType; public class Job { public enum JobStatus { @@ -19,14 +21,43 @@ public enum JobStatus { private String jobType; private JobStatus status; private long submitTime; + private long startTime; private long finishedTime; private Throwable error; - public Job(String jobType) { + public class StatusChange { + ProgressEventType status; + long changeTime; + + String message; + + public StatusChange(ProgressEventType type, String message) { + changeTime = System.currentTimeMillis(); + status = type; + this.message = message; + } + + public ProgressEventType getStatus() { + return status; + } + + public long getChangeTime() { + return changeTime; + } + + public String getMessage() { + return message; + } + } + + private List statusChanges; + + public Job(String jobType, String jobId) { this.jobType = jobType; - jobId = UUID.randomUUID().toString(); + this.jobId = jobId; submitTime = System.currentTimeMillis(); status = JobStatus.WAITING; + statusChanges = new ArrayList<>(); } @VisibleForTesting @@ -51,6 +82,14 @@ public void setStatus(JobStatus status) { this.status = status; } + public void setStatusChange(ProgressEventType type, String message) { + statusChanges.add(new StatusChange(type, message)); + } + + public List getStatusChanges() { + return statusChanges; + } + public long getSubmitTime() { return submitTime; } @@ -70,4 +109,8 @@ public Throwable getError() { public void setError(Throwable error) { this.error = error; } + + public void setStartTime(long startTime) { + this.startTime = startTime; + } } diff --git a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/util/JobExecutor.java b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/util/JobExecutor.java index f51e4f9e..4be189b5 100644 --- a/management-api-agent-common/src/main/java/com/datastax/mgmtapi/util/JobExecutor.java +++ b/management-api-agent-common/src/main/java/com/datastax/mgmtapi/util/JobExecutor.java @@ -7,6 +7,7 @@ import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; +import java.util.UUID; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; @@ -19,8 +20,9 @@ public class JobExecutor { public Pair> submit(String jobType, Runnable runnable) { // Where do I create the job details? Here? Add it to the Cache first? // Update the status on the callbacks and do nothing else? - final Job job = new Job(jobType); - jobCache.put(job.getJobId(), job); + + String jobId = UUID.randomUUID().toString(); + final Job job = createJob(jobType, jobId); CompletableFuture submittedJob = CompletableFuture.runAsync(runnable, executorService) @@ -28,20 +30,30 @@ public Pair> submit(String jobType, Runnable run empty -> { job.setStatus(Job.JobStatus.COMPLETED); job.setFinishedTime(System.currentTimeMillis()); - jobCache.put(job.getJobId(), job); + updateJob(job); }) .exceptionally( t -> { job.setStatus(Job.JobStatus.ERROR); job.setError(t); job.setFinishedTime(System.currentTimeMillis()); - jobCache.put(job.getJobId(), job); + updateJob(job); return null; }); return Pair.create(job.getJobId(), submittedJob); } + public Job createJob(String jobType, String jobId) { + final Job job = new Job(jobType, jobId); + jobCache.put(jobId, job); + return job; + } + + public void updateJob(Job job) { + jobCache.put(job.getJobId(), job); + } + public Job getJobWithId(String jobId) { return jobCache.getIfPresent(jobId); } diff --git a/management-api-agent-common/src/test/java/com/datastax/mgmtapi/rpc/Example.java b/management-api-agent-common/src/test/java/com/datastax/mgmtapi/rpc/Example.java new file mode 100644 index 00000000..5cbdcbe0 --- /dev/null +++ b/management-api-agent-common/src/test/java/com/datastax/mgmtapi/rpc/Example.java @@ -0,0 +1,17 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.rpc; + +import java.util.List; +import java.util.Map; + +/** Example class used for serializer tests. */ +@SuppressWarnings("unused") +public class Example { + public String stringField; + public Map mapField; + public List> listField; +} diff --git a/management-api-agent-common/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerTestBase.java b/management-api-agent-common/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerTestBase.java new file mode 100644 index 00000000..d5281d09 --- /dev/null +++ b/management-api-agent-common/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerTestBase.java @@ -0,0 +1,46 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.rpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +/** A template for ObjectSerializer tests in implementation modules. */ +public abstract class ObjectSerializerTestBase> { + + /** Create a concrete serializer for the {@link Example} class. */ + protected abstract S createExampleSerializer(); + + /** Return the CQL type that the serializer inferred for a particular field. */ + protected abstract String getCqlType(S serializer, String fieldName); + + @Test + public void testFieldTypes() { + S exampleSerializer = createExampleSerializer(); + + expectType(exampleSerializer, "stringField", "org.apache.cassandra.db.marshal.UTF8Type"); + expectType( + exampleSerializer, + "mapField", + "org.apache.cassandra.db.marshal.FrozenType(" + + "org.apache.cassandra.db.marshal.MapType(" + + "org.apache.cassandra.db.marshal.UTF8Type," + + "org.apache.cassandra.db.marshal.UTF8Type))"); + expectType( + exampleSerializer, + "listField", + "org.apache.cassandra.db.marshal.FrozenType(" + + "org.apache.cassandra.db.marshal.ListType(" + + "org.apache.cassandra.db.marshal.ListType(" + + "org.apache.cassandra.db.marshal.Int32Type)))"); + } + + private void expectType(S exampleSerializer, String fieldName, String expectedType) { + String actualType = getCqlType(exampleSerializer, fieldName); + assertThat(actualType).isEqualTo(expectedType); + } +} diff --git a/management-api-agent-dse-6.8/pom.xml b/management-api-agent-dse-6.8/pom.xml index 5b1a6f16..5be0fd3c 100644 --- a/management-api-agent-dse-6.8/pom.xml +++ b/management-api-agent-dse-6.8/pom.xml @@ -61,6 +61,19 @@ ${junit.version} test + + org.assertj + assertj-core + ${assertj.version} + test + + + io.k8ssandra + datastax-mgmtapi-agent-common + ${project.version} + tests + test + diff --git a/management-api-agent-dse-6.8/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse68.java b/management-api-agent-dse-6.8/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse68.java index 971ea697..651aa25d 100644 --- a/management-api-agent-dse-6.8/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse68.java +++ b/management-api-agent-dse-6.8/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse68.java @@ -73,7 +73,8 @@ public ObjectSerializerDse68(Class clazz, Type genericType) { field -> field.getName(), field -> new FieldSerializer( - GenericSerializerDse68.getType(field.getType()), field)))); + GenericSerializerDse68.getType(field.getGenericType()), + field)))); // currently not recursive; multiple ways to do it } diff --git a/management-api-agent-dse-6.8/src/main/java/com/datastax/mgmtapi/shim/DseAPI68.java b/management-api-agent-dse-6.8/src/main/java/com/datastax/mgmtapi/shim/DseAPI68.java index dc21338b..81aa42fe 100644 --- a/management-api-agent-dse-6.8/src/main/java/com/datastax/mgmtapi/shim/DseAPI68.java +++ b/management-api-agent-dse-6.8/src/main/java/com/datastax/mgmtapi/shim/DseAPI68.java @@ -229,6 +229,8 @@ public List> getEndpointStates() { states.put("ENDPOINT_IP", endpoint.getHostAddress()); states.put("IS_ALIVE", Boolean.toString(state.isAlive())); states.put("PARTITIONER", partitioner.getClass().getName()); + states.put("CLUSTER_NAME", getStorageService().getClusterName()); + states.put("IS_LOCAL", Boolean.toString(endpoint.equals(FBUtilities.getBroadcastAddress()))); result.add(states); } diff --git a/management-api-agent-dse-6.8/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse68Test.java b/management-api-agent-dse-6.8/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse68Test.java new file mode 100644 index 00000000..c3096ccb --- /dev/null +++ b/management-api-agent-dse-6.8/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse68Test.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.rpc; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ObjectSerializerDse68Test + extends ObjectSerializerTestBase> { + + @Override + protected ObjectSerializerDse68 createExampleSerializer() { + return new ObjectSerializerDse68<>(Example.class); + } + + @Override + protected String getCqlType(ObjectSerializerDse68 serializer, String fieldName) { + assertThat(serializer.serializers).containsKey(fieldName); + return serializer.serializers.get(fieldName).type.toString(); + } +} diff --git a/management-api-agent-dse7/pom.xml b/management-api-agent-dse7/pom.xml index d6b2d576..86a0d047 100644 --- a/management-api-agent-dse7/pom.xml +++ b/management-api-agent-dse7/pom.xml @@ -48,6 +48,19 @@ ${junit.version} test + + org.assertj + assertj-core + ${assertj.version} + test + + + io.k8ssandra + datastax-mgmtapi-agent-common + ${project.version} + tests + test + diff --git a/management-api-agent-dse7/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse7.java b/management-api-agent-dse7/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse7.java index 2379b11f..2af68c37 100644 --- a/management-api-agent-dse7/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse7.java +++ b/management-api-agent-dse7/src/main/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse7.java @@ -73,7 +73,8 @@ public ObjectSerializerDse7(Class clazz, Type genericType) { field -> field.getName(), field -> new FieldSerializer( - GenericSerializerDse7.getType(field.getType()), field)))); + GenericSerializerDse7.getType(field.getGenericType()), + field)))); // currently not recursive; multiple ways to do it } diff --git a/management-api-agent-dse7/src/main/java/com/datastax/mgmtapi/shim/CassandraAPIDse7.java b/management-api-agent-dse7/src/main/java/com/datastax/mgmtapi/shim/CassandraAPIDse7.java index 0b827d64..16e37a4f 100644 --- a/management-api-agent-dse7/src/main/java/com/datastax/mgmtapi/shim/CassandraAPIDse7.java +++ b/management-api-agent-dse7/src/main/java/com/datastax/mgmtapi/shim/CassandraAPIDse7.java @@ -255,6 +255,9 @@ public List> getEndpointStates() { states.put("ENDPOINT_IP", endpoint.address.getHostAddress()); states.put("IS_ALIVE", Boolean.toString(state.isAlive())); states.put("PARTITIONER", partitioner.getClass().getName()); + states.put("CLUSTER_NAME", getStorageService().getClusterName()); + states.put( + "IS_LOCAL", Boolean.toString(endpoint.equals(FBUtilities.getBroadcastAddressAndPort()))); result.add(states); } diff --git a/management-api-agent-dse7/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse7Test.java b/management-api-agent-dse7/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse7Test.java new file mode 100644 index 00000000..8ceca905 --- /dev/null +++ b/management-api-agent-dse7/src/test/java/com/datastax/mgmtapi/rpc/ObjectSerializerDse7Test.java @@ -0,0 +1,23 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.rpc; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ObjectSerializerDse7Test + extends ObjectSerializerTestBase> { + + @Override + protected ObjectSerializerDse7 createExampleSerializer() { + return new ObjectSerializerDse7<>(Example.class); + } + + @Override + protected String getCqlType(ObjectSerializerDse7 serializer, String fieldName) { + assertThat(serializer.serializers).containsKey(fieldName); + return serializer.serializers.get(fieldName).type.toString(); + } +} diff --git a/management-api-server/doc/openapi.json b/management-api-server/doc/openapi.json index c17230a5..781e8f82 100644 --- a/management-api-server/doc/openapi.json +++ b/management-api-server/doc/openapi.json @@ -274,7 +274,7 @@ } } }, - "description" : "Cassandra version'" + "description" : "Cassandra version" } }, "summary" : "Returns the Cassandra release version" @@ -1011,49 +1011,12 @@ "200" : { "content" : { "application/json" : { - "example" : { - "entity" : [ { - "Column family name" : "size_estimates", - "Keyspace name" : "system", - "Size on disk" : "13 bytes", - "Snapshot name" : "truncated-1639687082845-size_estimates", - "True size" : "0 bytes" - }, { - "Column family name" : "table_estimates", - "Keyspace name" : "system", - "Size on disk" : "13 bytes", - "Snapshot name" : "truncated-1639687082982-table_estimates", - "True size" : "0 bytes" - } ], - "variant" : { - "language" : null, - "mediaType" : { - "type" : "application", - "subtype" : "json", - "parameters" : { }, - "wildcardType" : false, - "wildcardSubtype" : false - }, - "encoding" : null, - "languageString" : null - }, - "annotations" : [ ], - "mediaType" : { - "type" : "application", - "subtype" : "json", - "parameters" : { }, - "wildcardType" : false, - "wildcardSubtype" : false - }, - "language" : null, - "encoding" : null - }, "schema" : { - "type" : "string" + "$ref" : "#/components/schemas/SnapshotDetails" } } }, - "description" : "Cassandra snapshot details" + "description" : "Cassandra snapshot details. Use 'null' values for query parameters to exclude result filtering against the parameter." } }, "summary" : "Retrieve snapshot details" @@ -1103,33 +1066,8 @@ "200" : { "content" : { "application/json" : { - "example" : { - "entity" : [ ], - "variant" : { - "language" : null, - "mediaType" : { - "type" : "application", - "subtype" : "json", - "parameters" : { }, - "wildcardType" : false, - "wildcardSubtype" : false - }, - "encoding" : null, - "languageString" : null - }, - "annotations" : [ ], - "mediaType" : { - "type" : "application", - "subtype" : "json", - "parameters" : { }, - "wildcardType" : false, - "wildcardSubtype" : false - }, - "language" : null, - "encoding" : null - }, "schema" : { - "type" : "string" + "$ref" : "#/components/schemas/StreamingInfo" } } }, @@ -1175,7 +1113,10 @@ "application/json" : { "example" : [ "table_1", "table_2" ], "schema" : { - "type" : "string" + "type" : "array", + "items" : { + "type" : "string" + } } } }, @@ -1582,6 +1523,45 @@ "summary" : "Rebuild data by streaming data from other nodes. This operation returns immediately with a job id." } }, + "/api/v1/ops/node/repair" : { + "post" : { + "operationId" : "repair_1", + "requestBody" : { + "content" : { + "*/*" : { + "schema" : { + "$ref" : "#/components/schemas/RepairRequest" + } + } + } + }, + "responses" : { + "202" : { + "content" : { + "text/plain" : { + "example" : "repair-1234567", + "schema" : { + "type" : "string" + } + } + }, + "description" : "Job ID for successfully scheduled Cassandra repair request" + }, + "400" : { + "content" : { + "text/plain" : { + "example" : "keyspaceName must be specified", + "schema" : { + "type" : "string" + } + } + }, + "description" : "Repair request missing Keyspace name" + } + }, + "summary" : "Execute a nodetool repair operation" + } + }, "/api/v1/ops/node/schema/versions" : { "get" : { "operationId" : "getSchemaVersions", @@ -1601,6 +1581,46 @@ "summary" : "Get schema versions." } }, + "/api/v1/ops/tables" : { + "get" : { + "operationId" : "listTablesV1", + "parameters" : [ { + "in" : "query", + "name" : "keyspaceName", + "required" : true, + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Table" + } + } + } + }, + "description" : "Table list" + }, + "400" : { + "content" : { + "text/plain" : { + "example" : "List tables failed. Non-empty 'keyspaceName' must be provided", + "schema" : { + "type" : "string" + } + } + }, + "description" : "Keyspace name not provided" + } + }, + "summary" : "List the table names in the given keyspace" + } + }, "/api/v1/ops/tables/compact" : { "post" : { "operationId" : "compact_1", @@ -1637,6 +1657,27 @@ "summary" : "Force a (major) compaction on one or more tables or user-defined compaction on given SSTables" } }, + "/api/v1/ops/tables/compactions" : { + "get" : { + "operationId" : "getCompactions", + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/Compaction" + } + } + } + }, + "description" : "Compactions" + } + }, + "summary" : "Returns active compactions" + } + }, "/api/v1/ops/tables/scrub" : { "post" : { "operationId" : "scrub_1", @@ -1699,6 +1740,111 @@ }, "summary" : "Rewrite sstables (for the requested tables) that are not on the current version (thus upgrading them to said current version). This operation is asynchronous and returns immediately." } + }, + "/api/v2/repairs" : { + "delete" : { + "operationId" : "deleteRepairsV2", + "responses" : { + "202" : { + "content" : { + "application/json" : { + "example" : "Accepted", + "schema" : { + "$ref" : "#/components/schemas/RepairRequestResponse" + } + } + }, + "description" : "Cancel repairs Successfully requested" + } + }, + "summary" : "Cancel all repairs" + }, + "put" : { + "operationId" : "putRepairV2", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/RepairRequest" + } + } + } + }, + "responses" : { + "202" : { + "content" : { + "application/json" : { + "example" : "Accepted", + "schema" : { + "$ref" : "#/components/schemas/RepairRequestResponse" + } + } + }, + "description" : "Repair Successfully requested" + }, + "400" : { + "content" : { + "text/plain" : { + "example" : "keyspace must be specified", + "schema" : { + "type" : "string", + "enum" : [ "OK", "Created", "Accepted", "No Content", "Reset Content", "Partial Content", "Moved Permanently", "Found", "See Other", "Not Modified", "Use Proxy", "Temporary Redirect", "Bad Request", "Unauthorized", "Payment Required", "Forbidden", "Not Found", "Method Not Allowed", "Not Acceptable", "Proxy Authentication Required", "Request Timeout", "Conflict", "Gone", "Length Required", "Precondition Failed", "Request Entity Too Large", "Request-URI Too Long", "Unsupported Media Type", "Requested Range Not Satisfiable", "Expectation Failed", "Precondition Required", "Too Many Requests", "Request Header Fields Too Large", "Internal Server Error", "Not Implemented", "Bad Gateway", "Service Unavailable", "Gateway Timeout", "HTTP Version Not Supported", "Network Authentication Required" ] + } + } + }, + "description" : "Repair request missing Keyspace name" + }, + "500" : { + "content" : { + "text/plain" : { + "example" : "internal error, we did not receive the expected repair ID from Cassandra.", + "schema" : { + "type" : "string", + "enum" : [ "OK", "Created", "Accepted", "No Content", "Reset Content", "Partial Content", "Moved Permanently", "Found", "See Other", "Not Modified", "Use Proxy", "Temporary Redirect", "Bad Request", "Unauthorized", "Payment Required", "Forbidden", "Not Found", "Method Not Allowed", "Not Acceptable", "Proxy Authentication Required", "Request Timeout", "Conflict", "Gone", "Length Required", "Precondition Failed", "Request Entity Too Large", "Request-URI Too Long", "Unsupported Media Type", "Requested Range Not Satisfiable", "Expectation Failed", "Precondition Required", "Too Many Requests", "Request Header Fields Too Large", "Internal Server Error", "Not Implemented", "Bad Gateway", "Service Unavailable", "Gateway Timeout", "HTTP Version Not Supported", "Network Authentication Required" ] + } + } + }, + "description" : "internal error, we did not receive the expected repair ID from Cassandra." + } + }, + "summary" : "Initiate a new repair" + } + }, + "/api/v2/tokens/rangetoendpoint" : { + "get" : { + "operationId" : "getRangeToEndpointMapV2", + "parameters" : [ { + "in" : "query", + "name" : "keyspaceName", + "schema" : { + "type" : "string" + } + } ], + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/TokenRangeToEndpointResponse" + } + } + }, + "description" : "Token range retrieval was successful" + }, + "404" : { + "content" : { + "text/plain" : { + "example" : "keyspace not found", + "schema" : { + "type" : "string" + } + } + }, + "description" : "Keyspace not found" + } + }, + "summary" : "Retrieve a mapping of Token ranges to endpoints" + } } }, "components" : { @@ -1760,6 +1906,52 @@ }, "required" : [ "keyspace_name", "split_output", "user_defined" ] }, + "Compaction" : { + "type" : "object", + "properties" : { + "columnfamily" : { + "type" : "string" + }, + "compactionId" : { + "type" : "string" + }, + "completed" : { + "type" : "integer", + "format" : "int64" + }, + "description" : { + "type" : "string" + }, + "id" : { + "type" : "string" + }, + "keyspace" : { + "type" : "string" + }, + "operationId" : { + "type" : "string" + }, + "operationType" : { + "type" : "string" + }, + "sstables" : { + "type" : "string" + }, + "targetDirectory" : { + "type" : "string" + }, + "taskType" : { + "type" : "string" + }, + "total" : { + "type" : "integer", + "format" : "int64" + }, + "unit" : { + "type" : "string" + } + } + }, "CreateOrAlterKeyspaceRequest" : { "type" : "object", "properties" : { @@ -1867,6 +2059,12 @@ "type" : "string", "enum" : [ "ERROR", "COMPLETED", "WAITING" ] }, + "status_changes" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/StatusChange" + } + }, "submit_time" : { "type" : "integer", "format" : "int64" @@ -1921,12 +2119,35 @@ "RepairRequest" : { "type" : "object", "properties" : { - "full" : { + "associated_tokens" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/RingRange" + } + }, + "datacenters" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "full_repair" : { "type" : "boolean" }, - "keyspace_name" : { + "keyspace" : { "type" : "string" }, + "notifications" : { + "type" : "boolean" + }, + "repair_parallelism" : { + "type" : "string", + "enum" : [ "sequential", "parallel", "dc_parallel" ] + }, + "repair_thread_count" : { + "type" : "integer", + "format" : "int32" + }, "tables" : { "type" : "array", "items" : { @@ -1934,7 +2155,16 @@ } } }, - "required" : [ "keyspace_name" ] + "required" : [ "keyspace" ] + }, + "RepairRequestResponse" : { + "type" : "object", + "properties" : { + "repair_id" : { + "type" : "string" + } + }, + "required" : [ "repair_id" ] }, "ReplicationSetting" : { "type" : "object", @@ -1949,6 +2179,20 @@ }, "required" : [ "dc_name", "replication_factor" ] }, + "RingRange" : { + "type" : "object", + "properties" : { + "end" : { + "type" : "integer", + "format" : "int64" + }, + "start" : { + "type" : "integer", + "format" : "int64" + } + }, + "required" : [ "end", "start" ] + }, "ScrubRequest" : { "type" : "object", "properties" : { @@ -1980,6 +2224,108 @@ }, "required" : [ "check_data", "disable_snapshot", "jobs", "keyspace_name", "reinsert_overflowed_ttl", "skip_corrupted", "tables" ] }, + "SnapshotDetails" : { + "type" : "object", + "properties" : { + "annotations" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "encoding" : { + "type" : "string" + }, + "entity" : { + "type" : "array", + "items" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + }, + "language" : { + "type" : "string" + }, + "mediaType" : { + "$ref" : "#/components/schemas/MediaType" + }, + "variant" : { + "$ref" : "#/components/schemas/Variant" + } + }, + "required" : [ "entity" ] + }, + "StatusChange" : { + "type" : "object", + "properties" : { + "change_time" : { + "type" : "integer", + "format" : "int64" + }, + "message" : { + "type" : "string" + }, + "status" : { + "type" : "string" + } + } + }, + "StreamingInfo" : { + "type" : "object", + "properties" : { + "annotations" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "encoding" : { + "type" : "string" + }, + "entity" : { + "type" : "array", + "items" : { + "type" : "object", + "additionalProperties" : { + "type" : "array", + "items" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + } + } + } + }, + "language" : { + "type" : "string" + }, + "mediaType" : { + "$ref" : "#/components/schemas/MediaType" + }, + "variant" : { + "$ref" : "#/components/schemas/Variant" + } + }, + "required" : [ "entity" ] + }, + "Table" : { + "type" : "object", + "properties" : { + "compaction" : { + "type" : "object", + "additionalProperties" : { + "type" : "string" + } + }, + "name" : { + "type" : "string" + } + }, + "required" : [ "name" ] + }, "TakeSnapshotRequest" : { "type" : "object", "properties" : { @@ -2006,6 +2352,37 @@ } } }, + "TokenRangeToEndpointResponse" : { + "type" : "object", + "properties" : { + "token_range_to_endpoints" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/TokenRangeToEndpoints" + } + } + }, + "required" : [ "token_range_to_endpoints" ] + }, + "TokenRangeToEndpoints" : { + "type" : "object", + "properties" : { + "endpoints" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "tokens" : { + "type" : "array", + "items" : { + "type" : "integer", + "format" : "int64" + } + } + }, + "required" : [ "endpoints", "tokens" ] + }, "Variant" : { "type" : "object", "properties" : { diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/ManagementApplication.java b/management-api-server/src/main/java/com/datastax/mgmtapi/ManagementApplication.java index ad502b8c..3d0e0125 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/ManagementApplication.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/ManagementApplication.java @@ -12,6 +12,8 @@ import com.datastax.mgmtapi.resources.MetadataResources; import com.datastax.mgmtapi.resources.NodeOpsResources; import com.datastax.mgmtapi.resources.TableOpsResources; +import com.datastax.mgmtapi.resources.v2.RepairResourcesV2; +import com.datastax.mgmtapi.resources.v2.TokenResourcesV2; import com.google.common.collect.ImmutableSet; import io.swagger.v3.jaxrs2.SwaggerSerializers; import io.swagger.v3.jaxrs2.integration.resources.OpenApiResource; @@ -65,6 +67,8 @@ public ManagementApplication( new TableOpsResources(this), new com.datastax.mgmtapi.resources.v1.TableOpsResources(this), new AuthResources(this), + new RepairResourcesV2(this), + new TokenResourcesV2(this), new OpenApiResource(), new SwaggerSerializers()); } diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/MetadataResources.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/MetadataResources.java index c06d98c3..52699c69 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/MetadataResources.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/MetadataResources.java @@ -35,7 +35,7 @@ public MetadataResources(ManagementApplication application) { @Produces(MediaType.TEXT_PLAIN) @ApiResponse( responseCode = "200", - description = "Cassandra version'", + description = "Cassandra version", content = @Content( mediaType = MediaType.TEXT_PLAIN, diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/NodeOpsResources.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/NodeOpsResources.java index b49b3451..ecc581f2 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/NodeOpsResources.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/NodeOpsResources.java @@ -11,6 +11,8 @@ import com.datastax.mgmtapi.resources.common.BaseResources; import com.datastax.mgmtapi.resources.helpers.ResponseTools; import com.datastax.mgmtapi.resources.models.RepairRequest; +import com.datastax.mgmtapi.resources.models.SnapshotDetails; +import com.datastax.mgmtapi.resources.models.StreamingInfo; import com.datastax.mgmtapi.resources.models.TakeSnapshotRequest; import com.datastax.oss.driver.api.core.cql.Row; import com.google.common.collect.ImmutableList; @@ -329,8 +331,7 @@ public Response reloadLocalSchema() { content = @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = String.class), - examples = @ExampleObject(value = STREAMING_INFO_RESPONSE_EXAMPLE))) + schema = @Schema(implementation = StreamingInfo.class))) @Operation(summary = "Retrieve Streaming status information", operationId = "getStreamInfo") public Response getStreamInfo() { return handle( @@ -351,12 +352,12 @@ public Response getStreamInfo() { @Produces(MediaType.APPLICATION_JSON) @ApiResponse( responseCode = "200", - description = "Cassandra snapshot details", + description = + "Cassandra snapshot details. Use 'null' values for query parameters to exclude result filtering against the parameter.", content = @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = String.class), - examples = @ExampleObject(value = SNAPSHOT_DETAILS_RESPONSE_EXAMPLE))) + schema = @Schema(implementation = SnapshotDetails.class))) @Operation(summary = "Retrieve snapshot details", operationId = "getSnapshotDetails") public Response getSnapshotDetails( @QueryParam("snapshotNames") List snapshotNames, @@ -502,10 +503,17 @@ public Response repair(RepairRequest repairRequest) { } app.cqlService.executePreparedStatement( app.dbUnixSocketFile, - "CALL NodeOps.repair(?, ?, ?)", + "CALL NodeOps.repair(?, ?, ?, ?, ?, ?, ?, ?)", repairRequest.keyspaceName, repairRequest.tables, - repairRequest.full); + repairRequest.full, + false, + // The default repair does not allow for specifying things like parallelism, + // threadCounts, source DCs or ranges etc. + null, + null, + null, + null); return Response.ok("OK").build(); }); @@ -609,75 +617,6 @@ public Response move(@QueryParam(value = "newToken") String newToken) { }); } - private static final String STREAMING_INFO_RESPONSE_EXAMPLE = - "{\n" - + " \"entity\": [],\n" - + " \"variant\": {\n" - + " \"language\": null,\n" - + " \"mediaType\": {\n" - + " \"type\": \"application\",\n" - + " \"subtype\": \"json\",\n" - + " \"parameters\": {},\n" - + " \"wildcardType\": false,\n" - + " \"wildcardSubtype\": false\n" - + " },\n" - + " \"encoding\": null,\n" - + " \"languageString\": null\n" - + " },\n" - + " \"annotations\": [],\n" - + " \"mediaType\": {\n" - + " \"type\": \"application\",\n" - + " \"subtype\": \"json\",\n" - + " \"parameters\": {},\n" - + " \"wildcardType\": false,\n" - + " \"wildcardSubtype\": false\n" - + " },\n" - + " \"language\": null,\n" - + " \"encoding\": null\n" - + "}"; - - private static final String SNAPSHOT_DETAILS_RESPONSE_EXAMPLE = - "{\n" - + " \"entity\": [\n" - + " {\n" - + " \"Column family name\": \"size_estimates\",\n" - + " \"Keyspace name\": \"system\",\n" - + " \"Size on disk\": \"13 bytes\",\n" - + " \"Snapshot name\": \"truncated-1639687082845-size_estimates\",\n" - + " \"True size\": \"0 bytes\"\n" - + " },\n" - + " {\n" - + " \"Column family name\": \"table_estimates\",\n" - + " \"Keyspace name\": \"system\",\n" - + " \"Size on disk\": \"13 bytes\",\n" - + " \"Snapshot name\": \"truncated-1639687082982-table_estimates\",\n" - + " \"True size\": \"0 bytes\"\n" - + " }\n" - + " ],\n" - + " \"variant\": {\n" - + " \"language\": null,\n" - + " \"mediaType\": {\n" - + " \"type\": \"application\",\n" - + " \"subtype\": \"json\",\n" - + " \"parameters\": {},\n" - + " \"wildcardType\": false,\n" - + " \"wildcardSubtype\": false\n" - + " },\n" - + " \"encoding\": null,\n" - + " \"languageString\": null\n" - + " },\n" - + " \"annotations\": [],\n" - + " \"mediaType\": {\n" - + " \"type\": \"application\",\n" - + " \"subtype\": \"json\",\n" - + " \"parameters\": {},\n" - + " \"wildcardType\": false,\n" - + " \"wildcardSubtype\": false\n" - + " },\n" - + " \"language\": null,\n" - + " \"encoding\": null\n" - + "}"; - private static final String FQL_QUERY_RESPONSE_EXAMPLE = "{\n" + " \"entity\": false,\n" diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/TableOpsResources.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/TableOpsResources.java index 1c16f392..190d0b3c 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/TableOpsResources.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/TableOpsResources.java @@ -12,15 +12,16 @@ import com.datastax.mgmtapi.resources.models.KeyspaceRequest; import com.datastax.mgmtapi.resources.models.ScrubRequest; import com.datastax.oss.driver.api.core.cql.ResultSet; -import com.datastax.oss.driver.api.core.cql.Row; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -326,7 +327,7 @@ public Response flush(KeyspaceRequest keyspaceRequest) { content = @Content( mediaType = MediaType.APPLICATION_JSON, - schema = @Schema(implementation = String.class), + array = @ArraySchema(schema = @Schema(implementation = String.class)), examples = @ExampleObject(value = "[\n \"table_1\",\n \"table_2\"\n]"))) @ApiResponse( responseCode = "400", @@ -352,9 +353,8 @@ public Response list( ResultSet result = app.cqlService.executePreparedStatement( app.dbUnixSocketFile, "CALL NodeOps.getTables(?)", keyspaceName); - Row row = result.one(); - assert row != null; - List tables = row.getList(0, String.class); + List tables = + result.all().stream().map(row -> row.getString("name")).collect(Collectors.toList()); return Response.ok(tables, MediaType.APPLICATION_JSON).build(); }); } diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/common/BaseResources.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/common/BaseResources.java index 6d4b227d..625a31d3 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/common/BaseResources.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/common/BaseResources.java @@ -8,6 +8,8 @@ import com.datastax.mgmtapi.ManagementApplication; import com.datastax.mgmtapi.resources.helpers.ResponseTools; import com.datastax.oss.driver.api.core.NoNodeAvailableException; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; import java.util.concurrent.Callable; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Response; @@ -65,9 +67,31 @@ protected Response handle(Callable action) { .entity("Internal connection to Cassandra closed") .build(); } catch (Throwable t) { + t.printStackTrace(); return Response.status(HttpStatus.SC_INTERNAL_SERVER_ERROR) .entity(t.getLocalizedMessage()) .build(); } } + + /** + * Returns true if the specified keyspaceName is not null and a keyspace with the name exists. + * Returns false if the keyspaceName is null or if no keyspace with the name exists. Throws a + * ConnectionClosedException if there is an issue executing the RPC call to the Cassandra agent. + * + * @param keyspaceName The name of a keyspace you are looking for. + * @return True if the keyspace is found, false otherwise. + */ + protected boolean keyspaceExists(String keyspaceName) throws ConnectionClosedException { + if (keyspaceName != null) { + ResultSet result = + app.cqlService.executePreparedStatement( + app.dbUnixSocketFile, "CALL NodeOps.getKeyspaces()"); + Row row = result.one(); + if (row != null) { + return row.getList(0, String.class).contains(keyspaceName); + } + } + return false; + } } diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/BaseEntity.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/BaseEntity.java new file mode 100644 index 00000000..ccf9661a --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/BaseEntity.java @@ -0,0 +1,213 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public abstract class BaseEntity { + + @JsonProperty(value = "variant", required = false) + public final Variant variant; + + @JsonProperty(value = "annotations", required = false) + public final List annotations; + + @JsonProperty(value = "mediaType", required = false) + public final MediaType mediaType; + + @JsonProperty(value = "language", required = false) + public final String language; + + @JsonProperty(value = "encoding", required = false) + public final String encoding; + + @JsonCreator + public BaseEntity( + @JsonProperty("variant") Variant variant, + @JsonProperty("annotations") List annotations, + @JsonProperty("mediaType") MediaType mediaType, + @JsonProperty("language") String language, + @JsonProperty("encoding") String encoding) { + this.variant = variant; + this.annotations = annotations; + this.mediaType = mediaType; + this.language = language; + this.encoding = encoding; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 83 * hash + Objects.hashCode(this.variant); + hash = 83 * hash + Objects.hashCode(this.annotations); + hash = 83 * hash + Objects.hashCode(this.mediaType); + hash = 83 * hash + Objects.hashCode(this.language); + hash = 83 * hash + Objects.hashCode(this.encoding); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final BaseEntity other = (BaseEntity) obj; + if (!Objects.equals(this.variant, other.variant)) { + return false; + } + if (!Objects.equals(this.annotations, other.annotations)) { + return false; + } + if (!Objects.equals(this.mediaType, other.mediaType)) { + return false; + } + if (!Objects.equals(this.language, other.language)) { + return false; + } + return Objects.equals(this.encoding, other.encoding); + } + + public static class MediaType { + + @JsonProperty(value = "type", required = false) + public final String type; + + @JsonProperty(value = "subtype", required = false) + public final String subtype; + + @JsonProperty(value = "parameters", required = false) + public final Map parameters; + + @JsonProperty(value = "wildcardType", required = false) + public final String wildcardType; + + @JsonProperty(value = "wildcardSubtype", required = false) + public final String wildcardSubtype; + + @JsonCreator + public MediaType( + @JsonProperty("type") String type, + @JsonProperty("subtype") String subtype, + @JsonProperty("parameters") Map parameters, + @JsonProperty("wildcardType") String wildcardType, + @JsonProperty("wildcardSubtype") String wildcardSubtype) { + this.type = type; + this.subtype = subtype; + this.parameters = parameters; + this.wildcardType = wildcardType; + this.wildcardSubtype = wildcardSubtype; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 83 * hash + Objects.hashCode(this.type); + hash = 83 * hash + Objects.hashCode(this.subtype); + hash = 83 * hash + Objects.hashCode(this.parameters); + hash = 83 * hash + Objects.hashCode(this.wildcardType); + hash = 83 * hash + Objects.hashCode(this.wildcardSubtype); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final MediaType other = (MediaType) obj; + if (!Objects.equals(this.type, other.type)) { + return false; + } + if (!Objects.equals(this.subtype, other.subtype)) { + return false; + } + if (!Objects.equals(this.parameters, other.parameters)) { + return false; + } + if (!Objects.equals(this.wildcardType, other.wildcardType)) { + return false; + } + return Objects.equals(this.wildcardSubtype, other.wildcardSubtype); + } + } + + public static class Variant { + + @JsonProperty(value = "language", required = false) + public final String language; + + @JsonProperty(value = "mediaType", required = false) + public final MediaType mediaType; + + @JsonProperty(value = "encoding", required = false) + public final String encoding; + + @JsonProperty(value = "languageString", required = false) + public final String languageString; + + @JsonCreator + public Variant( + @JsonProperty("language") String language, + @JsonProperty("mediaType") MediaType mediaType, + @JsonProperty("encoding") String encoding, + @JsonProperty("languageString") String languageString) { + this.language = language; + this.mediaType = mediaType; + this.encoding = encoding; + this.languageString = languageString; + } + + @Override + public int hashCode() { + int hash = 5; + hash = 83 * hash + Objects.hashCode(this.language); + hash = 83 * hash + Objects.hashCode(this.mediaType); + hash = 83 * hash + Objects.hashCode(this.encoding); + hash = 83 * hash + Objects.hashCode(this.languageString); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final Variant other = (Variant) obj; + if (!Objects.equals(this.language, other.language)) { + return false; + } + if (!Objects.equals(this.mediaType, other.mediaType)) { + return false; + } + if (!Objects.equals(this.encoding, other.encoding)) { + return false; + } + return Objects.equals(this.languageString, other.languageString); + } + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Compaction.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Compaction.java new file mode 100644 index 00000000..6c648a92 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Compaction.java @@ -0,0 +1,191 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import java.util.Objects; + +/** + * Describes the state of an active compaction running on the server. + * + *

Some fields are specific to certain Cassandra versions, this is indicated in their comment. + */ +public class Compaction { + + // Note: for simplicity, we use the same keys in our JSON payload as the map returned by + // Cassandra. These constants are used for both, do not change them or the corresponding JSON + // fields will always be empty. + private static final String ID_KEY = "id"; + private static final String KEYSPACE_KEY = "keyspace"; + private static final String COLUMN_FAMILY_KEY = "columnfamily"; + private static final String COMPLETED_KEY = "completed"; + private static final String TOTAL_KEY = "total"; + private static final String TASK_TYPE_KEY = "taskType"; + private static final String UNIT_KEY = "unit"; + private static final String COMPACTION_ID_KEY = "compactionId"; + private static final String SSTABLES_KEY = "sstables"; + private static final String TARGET_DIRECTORY_KEY = "targetDirectory"; + private static final String OPERATION_TYPE_KEY = "operationType"; + private static final String OPERATION_ID_KEY = "operationId"; + private static final String DESCRIPTION_KEY = "description"; + + @JsonProperty(ID_KEY) + public final String id; + + @JsonProperty(KEYSPACE_KEY) + public final String keyspace; + + @JsonProperty(COLUMN_FAMILY_KEY) + public final String columnFamily; + + @JsonProperty(COMPLETED_KEY) + public final Long completed; + + @JsonProperty(TOTAL_KEY) + public final Long total; + + /** Only present in OSS Cassandra. */ + @JsonProperty(TASK_TYPE_KEY) + public final String taskType; + + @JsonProperty(UNIT_KEY) + public final String unit; + + /** Only present in OSS Cassandra. */ + @JsonProperty(COMPACTION_ID_KEY) + public final String compactionId; + + /** Only present in OSS Cassandra 4 or above. */ + @JsonProperty(SSTABLES_KEY) + public final String ssTables; + + /** Only present in OSS Cassandra 5. */ + @JsonProperty(TARGET_DIRECTORY_KEY) + public final String targetDirectory; + + /** Only present in DSE. */ + @JsonProperty(OPERATION_TYPE_KEY) + public final String operationType; + + /** Only present in DSE. */ + @JsonProperty(OPERATION_ID_KEY) + public final String operationId; + + /** Only present in DSE 6.8. */ + @JsonProperty(DESCRIPTION_KEY) + public final String description; + + @JsonCreator + public Compaction( + @JsonProperty(ID_KEY) String id, + @JsonProperty(KEYSPACE_KEY) String keyspace, + @JsonProperty(COLUMN_FAMILY_KEY) String columnFamily, + @JsonProperty(COMPLETED_KEY) Long completed, + @JsonProperty(TOTAL_KEY) Long total, + @JsonProperty(TASK_TYPE_KEY) String taskType, + @JsonProperty(UNIT_KEY) String unit, + @JsonProperty(COMPACTION_ID_KEY) String compactionId, + @JsonProperty(SSTABLES_KEY) String ssTables, + @JsonProperty(TARGET_DIRECTORY_KEY) String targetDirectory, + @JsonProperty(OPERATION_TYPE_KEY) String operationType, + @JsonProperty(OPERATION_ID_KEY) String operationId, + @JsonProperty(DESCRIPTION_KEY) String description) { + + this.id = id; + this.keyspace = keyspace; + this.columnFamily = columnFamily; + this.completed = completed; + this.total = total; + this.taskType = taskType; + this.unit = unit; + this.compactionId = compactionId; + this.ssTables = ssTables; + this.targetDirectory = targetDirectory; + this.operationId = operationId; + this.operationType = operationType; + this.description = description; + } + + public static Compaction fromMap(Map m) { + return new Compaction( + m.get(ID_KEY), + m.get(KEYSPACE_KEY), + m.get(COLUMN_FAMILY_KEY), + parseLongOrNull(m.get(COMPLETED_KEY)), + parseLongOrNull(m.get(TOTAL_KEY)), + m.get(TASK_TYPE_KEY), + m.get(UNIT_KEY), + m.get(COMPACTION_ID_KEY), + m.get(SSTABLES_KEY), + m.get(TARGET_DIRECTORY_KEY), + m.get(OPERATION_TYPE_KEY), + m.get(OPERATION_ID_KEY), + m.get(DESCRIPTION_KEY)); + } + + private static Long parseLongOrNull(String s) { + try { + return Long.parseLong(s); + } catch (NumberFormatException e) { + return null; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Compaction that = (Compaction) o; + return Objects.equals(id, that.id) + && Objects.equals(keyspace, that.keyspace) + && Objects.equals(columnFamily, that.columnFamily) + && Objects.equals(completed, that.completed) + && Objects.equals(total, that.total) + && Objects.equals(taskType, that.taskType) + && Objects.equals(unit, that.unit) + && Objects.equals(compactionId, that.compactionId) + && Objects.equals(ssTables, that.ssTables) + && Objects.equals(targetDirectory, that.targetDirectory) + && Objects.equals(operationType, that.operationType) + && Objects.equals(operationId, that.operationId) + && Objects.equals(description, that.description); + } + + @Override + public int hashCode() { + return Objects.hash( + id, + keyspace, + columnFamily, + completed, + total, + taskType, + unit, + compactionId, + ssTables, + targetDirectory, + operationType, + operationId, + description); + } + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException je) { + return String.format("Unable to format compaction (%s)", je.getMessage()); + } + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/EndpointStates.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/EndpointStates.java index 6591f5f2..54f44e84 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/EndpointStates.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/EndpointStates.java @@ -7,30 +7,17 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; import java.util.Map; import java.util.Objects; -public class EndpointStates { +public class EndpointStates extends BaseEntity { @JsonProperty(value = "entity", required = true) public final List> entity; - @JsonProperty(value = "variant", required = false) - public final Variant variant; - - @JsonProperty(value = "annotations", required = false) - public final List annotations; - - @JsonProperty(value = "mediaType", required = false) - public final MediaType mediaType; - - @JsonProperty(value = "language", required = false) - public final String language; - - @JsonProperty(value = "encoding", required = false) - public final String encoding; - @JsonCreator public EndpointStates( @JsonProperty("entity") List> entity, @@ -39,23 +26,14 @@ public EndpointStates( @JsonProperty("mediaType") MediaType mediaType, @JsonProperty("language") String language, @JsonProperty("encoding") String encoding) { + super(variant, annotations, mediaType, language, encoding); this.entity = entity; - this.variant = variant; - this.annotations = annotations; - this.mediaType = mediaType; - this.language = language; - this.encoding = encoding; } @Override public int hashCode() { - int hash = 5; + int hash = super.hashCode(); hash = 83 * hash + Objects.hashCode(this.entity); - hash = 83 * hash + Objects.hashCode(this.variant); - hash = 83 * hash + Objects.hashCode(this.annotations); - hash = 83 * hash + Objects.hashCode(this.mediaType); - hash = 83 * hash + Objects.hashCode(this.language); - hash = 83 * hash + Objects.hashCode(this.encoding); return hash; } @@ -74,149 +52,15 @@ public boolean equals(Object obj) { if (!Objects.equals(this.entity, other.entity)) { return false; } - if (!Objects.equals(this.variant, other.variant)) { - return false; - } - if (!Objects.equals(this.annotations, other.annotations)) { - return false; - } - if (!Objects.equals(this.mediaType, other.mediaType)) { - return false; - } - if (!Objects.equals(this.language, other.language)) { - return false; - } - return Objects.equals(this.encoding, other.encoding); - } - - public static class MediaType { - - @JsonProperty(value = "type", required = false) - public final String type; - - @JsonProperty(value = "subtype", required = false) - public final String subtype; - - @JsonProperty(value = "parameters", required = false) - public final Map parameters; - - @JsonProperty(value = "wildcardType", required = false) - public final String wildcardType; - - @JsonProperty(value = "wildcardSubtype", required = false) - public final String wildcardSubtype; - - @JsonCreator - public MediaType( - @JsonProperty("type") String type, - @JsonProperty("subtype") String subtype, - @JsonProperty("parameters") Map parameters, - @JsonProperty("wildcardType") String wildcardType, - @JsonProperty("wildcardSubtype") String wildcardSubtype) { - this.type = type; - this.subtype = subtype; - this.parameters = parameters; - this.wildcardType = wildcardType; - this.wildcardSubtype = wildcardSubtype; - } - - @Override - public int hashCode() { - int hash = 5; - hash = 83 * hash + Objects.hashCode(this.type); - hash = 83 * hash + Objects.hashCode(this.subtype); - hash = 83 * hash + Objects.hashCode(this.parameters); - hash = 83 * hash + Objects.hashCode(this.wildcardType); - hash = 83 * hash + Objects.hashCode(this.wildcardSubtype); - return hash; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final MediaType other = (MediaType) obj; - if (!Objects.equals(this.type, other.type)) { - return false; - } - if (!Objects.equals(this.subtype, other.subtype)) { - return false; - } - if (!Objects.equals(this.parameters, other.parameters)) { - return false; - } - if (!Objects.equals(this.wildcardType, other.wildcardType)) { - return false; - } - return Objects.equals(this.wildcardSubtype, other.wildcardSubtype); - } + return super.equals(obj); } - public static class Variant { - - @JsonProperty(value = "language", required = false) - public final String language; - - @JsonProperty(value = "mediaType", required = false) - public final MediaType mediaType; - - @JsonProperty(value = "encoding", required = false) - public final String encoding; - - @JsonProperty(value = "languageString", required = false) - public final String languageString; - - @JsonCreator - public Variant( - @JsonProperty("language") String language, - @JsonProperty("mediaType") MediaType mediaType, - @JsonProperty("encoding") String encoding, - @JsonProperty("languageString") String languageString) { - this.language = language; - this.mediaType = mediaType; - this.encoding = encoding; - this.languageString = languageString; - } - - @Override - public int hashCode() { - int hash = 5; - hash = 83 * hash + Objects.hashCode(this.language); - hash = 83 * hash + Objects.hashCode(this.mediaType); - hash = 83 * hash + Objects.hashCode(this.encoding); - hash = 83 * hash + Objects.hashCode(this.languageString); - return hash; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - final Variant other = (Variant) obj; - if (!Objects.equals(this.language, other.language)) { - return false; - } - if (!Objects.equals(this.mediaType, other.mediaType)) { - return false; - } - if (!Objects.equals(this.encoding, other.encoding)) { - return false; - } - return Objects.equals(this.languageString, other.languageString); + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException je) { + return "Unable to parse endpoint states into an entity"; } } } diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Job.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Job.java index 9dbc82f4..d2ead968 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Job.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Job.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import java.io.Serializable; +import java.util.List; public class Job implements Serializable { public enum JobStatus { @@ -34,6 +35,38 @@ public enum JobStatus { @JsonProperty(value = "error") private String error; + public class StatusChange { + @JsonProperty(value = "status") + String status; + + @JsonProperty(value = "change_time") + long changeTime; + + @JsonProperty(value = "message") + String message; + + public StatusChange(String type, String message) { + changeTime = System.currentTimeMillis(); + status = type; + this.message = message; + } + + public String getStatus() { + return status; + } + + public long getChangeTime() { + return changeTime; + } + + public String getMessage() { + return message; + } + } + + @JsonProperty(value = "status_changes") + private List statusChanges; + @JsonCreator public Job( @JsonProperty(value = "id") String jobId, @@ -41,13 +74,15 @@ public Job( @JsonProperty(value = "status") String status, @JsonProperty(value = "submit_time") long submitTime, @JsonProperty(value = "end_time") long finishedTime, - @JsonProperty(value = "error") String error) { + @JsonProperty(value = "error") String error, + @JsonProperty(value = "status_changes") List changes) { this.jobId = jobId; this.jobType = jobType; this.status = JobStatus.valueOf(status); this.submitTime = submitTime; this.finishedTime = finishedTime; this.error = error; + this.statusChanges = changes; } public String getJobId() { @@ -70,6 +105,10 @@ public long getFinishedTime() { return finishedTime; } + public List getStatusChanges() { + return statusChanges; + } + public String getError() { return error; } diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/SnapshotDetails.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/SnapshotDetails.java new file mode 100644 index 00000000..7a90f758 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/SnapshotDetails.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class SnapshotDetails extends BaseEntity { + + @JsonProperty(value = "entity", required = true) + public final List> entity; + + @JsonCreator + public SnapshotDetails( + @JsonProperty("entity") List> entity, + @JsonProperty("variant") BaseEntity.Variant variant, + @JsonProperty("annotations") List annotations, + @JsonProperty("mediaType") BaseEntity.MediaType mediaType, + @JsonProperty("language") String language, + @JsonProperty("encoding") String encoding) { + super(variant, annotations, mediaType, language, encoding); + this.entity = entity; + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 83 * hash + Objects.hashCode(this.entity); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SnapshotDetails other = (SnapshotDetails) obj; + if (!Objects.equals(this.entity, other.entity)) { + return false; + } + return super.equals(obj); + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/StreamingInfo.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/StreamingInfo.java new file mode 100644 index 00000000..42f468a3 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/StreamingInfo.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class StreamingInfo extends BaseEntity { + + @JsonProperty(value = "entity", required = true) + public final List>>> entity; + + @JsonCreator + public StreamingInfo( + @JsonProperty("entity") List>>> entity, + @JsonProperty("variant") Variant variant, + @JsonProperty("annotations") List annotations, + @JsonProperty("mediaType") MediaType mediaType, + @JsonProperty("language") String language, + @JsonProperty("encoding") String encoding) { + super(variant, annotations, mediaType, language, encoding); + this.entity = entity; + } + + @Override + public int hashCode() { + int hash = super.hashCode(); + hash = 83 * hash + Objects.hashCode(this.entity); + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final StreamingInfo other = (StreamingInfo) obj; + if (!Objects.equals(this.entity, other.entity)) { + return false; + } + return super.equals(obj); + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Table.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Table.java new file mode 100644 index 00000000..41b1e1d4 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/models/Table.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; +import java.util.Objects; + +public class Table { + @JsonProperty(value = "name", required = true) + public final String name; + + @JsonProperty(value = "compaction") + public final Map compaction; + + @JsonCreator + public Table( + @JsonProperty("name") String name, + @JsonProperty("compaction") Map compaction) { + this.name = name; + this.compaction = compaction; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Table table = (Table) o; + return Objects.equals(name, table.name) && Objects.equals(compaction, table.compaction); + } + + @Override + public int hashCode() { + return Objects.hash(name, compaction); + } + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException je) { + return String.format("Unable to format table (%s)", je.getMessage()); + } + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/NodeOpsResources.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/NodeOpsResources.java index ed8f5d6c..0b1c70a8 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/NodeOpsResources.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/NodeOpsResources.java @@ -8,6 +8,7 @@ import com.datastax.mgmtapi.ManagementApplication; import com.datastax.mgmtapi.resources.common.BaseResources; import com.datastax.mgmtapi.resources.helpers.ResponseTools; +import com.datastax.mgmtapi.resources.models.RepairRequest; import com.datastax.oss.driver.api.core.cql.Row; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.media.Content; @@ -120,4 +121,49 @@ public Response schemaVersions() { return Response.ok(schemaVersions).build(); }); } + + @POST + @Path("/repair") + @Produces(MediaType.TEXT_PLAIN) + @ApiResponse( + responseCode = "202", + description = "Job ID for successfully scheduled Cassandra repair request", + content = + @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema(implementation = String.class), + examples = @ExampleObject(value = "repair-1234567"))) + @ApiResponse( + responseCode = "400", + description = "Repair request missing Keyspace name", + content = + @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema(implementation = String.class), + examples = @ExampleObject(value = "keyspaceName must be specified"))) + @Operation(summary = "Execute a nodetool repair operation", operationId = "repair") + public Response repair(RepairRequest repairRequest) { + return handle( + () -> { + if (repairRequest.keyspaceName == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("keyspaceName must be specified") + .build(); + } + return Response.accepted( + ResponseTools.getSingleRowStringResponse( + app.dbUnixSocketFile, + app.cqlService, + "CALL NodeOps.repair(?, ?, ?, ?, ?, ?, ?, ?)", + repairRequest.keyspaceName, + repairRequest.tables, + repairRequest.full, + true, + null, + null, + null, + null)) + .build(); + }); + } } diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/TableOpsResources.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/TableOpsResources.java index 9d4917fc..e48e080c 100644 --- a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/TableOpsResources.java +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v1/TableOpsResources.java @@ -9,16 +9,27 @@ import com.datastax.mgmtapi.resources.common.BaseResources; import com.datastax.mgmtapi.resources.helpers.ResponseTools; import com.datastax.mgmtapi.resources.models.CompactRequest; +import com.datastax.mgmtapi.resources.models.Compaction; import com.datastax.mgmtapi.resources.models.KeyspaceRequest; import com.datastax.mgmtapi.resources.models.ScrubRequest; +import com.datastax.mgmtapi.resources.models.Table; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import com.datastax.oss.driver.api.core.type.reflect.GenericType; +import com.google.common.collect.Lists; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.ExampleObject; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.util.ArrayList; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; import javax.ws.rs.Consumes; +import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -32,6 +43,9 @@ @Path("/api/v1/ops/tables") public class TableOpsResources extends BaseResources { + private static final GenericType>> LIST_OF_MAP_OF_STRINGS = + GenericType.listOf(GenericType.mapOf(String.class, String.class)); + public TableOpsResources(ManagementApplication application) { super(application); } @@ -228,4 +242,74 @@ public Response compact(CompactRequest compactRequest) { .build(); }); } + + @GET + @Path("/compactions") + @Operation(summary = "Returns active compactions", operationId = "getCompactions") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + @ApiResponse( + responseCode = "200", + description = "Compactions", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + array = @ArraySchema(schema = @Schema(implementation = Compaction.class)))) + public Response getCompactions() { + return handle( + () -> { + ResultSet result = + app.cqlService.executeCql(app.dbUnixSocketFile, "CALL NodeOps.getCompactions()"); + Row row = result.one(); + assert row != null; + List compactions = + row.get(0, LIST_OF_MAP_OF_STRINGS).stream() + .map(Compaction::fromMap) + .collect(Collectors.toList()); + return Response.ok(compactions, MediaType.APPLICATION_JSON).build(); + }); + } + + @GET + @Produces({MediaType.TEXT_PLAIN, MediaType.APPLICATION_JSON}) + @ApiResponse( + responseCode = "200", + description = "Table list", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + array = @ArraySchema(schema = @Schema(implementation = Table.class)))) + @ApiResponse( + responseCode = "400", + description = "Keyspace name not provided", + content = + @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema(implementation = String.class), + examples = + @ExampleObject( + value = "List tables failed. Non-empty 'keyspaceName' must be provided"))) + @Consumes(MediaType.APPLICATION_JSON) + @Operation(summary = "List the table names in the given keyspace", operationId = "listTablesV1") + public Response list( + @Parameter(required = true) @QueryParam(value = "keyspaceName") String keyspaceName) { + if (StringUtils.isBlank(keyspaceName)) { + return Response.status(HttpStatus.SC_BAD_REQUEST) + .entity("List tables failed. Non-empty 'keyspaceName' must be provided") + .build(); + } + return handle( + () -> { + ResultSet result = + app.cqlService.executePreparedStatement( + app.dbUnixSocketFile, "CALL NodeOps.getTables(?)", keyspaceName); + List

tables = Lists.newArrayList(); + for (Row row : result) { + tables.add( + new Table( + row.getString("name"), row.getMap("compaction", String.class, String.class))); + } + return Response.ok(tables, MediaType.APPLICATION_JSON).build(); + }); + } } diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/RepairResourcesV2.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/RepairResourcesV2.java new file mode 100644 index 00000000..98e2559a --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/RepairResourcesV2.java @@ -0,0 +1,140 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.v2; + +import com.datastax.mgmtapi.ManagementApplication; +import com.datastax.mgmtapi.resources.common.BaseResources; +import com.datastax.mgmtapi.resources.v2.models.RepairParallelism; +import com.datastax.mgmtapi.resources.v2.models.RepairRequest; +import com.datastax.mgmtapi.resources.v2.models.RepairRequestResponse; +import com.datastax.mgmtapi.resources.v2.models.RingRange; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.util.List; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path("/api/v2/repairs") +public class RepairResourcesV2 extends BaseResources { + + public RepairResourcesV2(ManagementApplication application) { + super(application); + } + + @PUT + @Operation(summary = "Initiate a new repair", operationId = "putRepairV2") + @Produces(MediaType.APPLICATION_JSON) + @Consumes("application/json") + @ApiResponse( + responseCode = "202", + description = "Repair Successfully requested", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = RepairRequestResponse.class), + examples = @ExampleObject(value = "Accepted"))) + @ApiResponse( + responseCode = "400", + description = "Repair request missing Keyspace name", + content = + @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema(implementation = Response.Status.class), + examples = @ExampleObject(value = "keyspace must be specified"))) + @ApiResponse( + responseCode = "500", + description = "internal error, we did not receive the expected repair ID from Cassandra.", + content = + @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema(implementation = Response.Status.class), + examples = + @ExampleObject( + value = + "internal error, we did not receive the expected repair ID from Cassandra."))) + public final Response repair(RepairRequest request) { + return handle( + () -> { + if (request.keyspace == null) { + return Response.status(Response.Status.BAD_REQUEST) + .entity("keyspaceName must be specified") + .build(); + } + + ResultSet res = + app.cqlService.executePreparedStatement( + app.dbUnixSocketFile, + "CALL NodeOps.repair(?, ?, ?, ?, ?, ?, ?, ?)", + request.keyspace, + request.tables, + request.fullRepair, + request.notifications, + getParallelismName(request.repairParallelism), + request.datacenters, + getRingRangeString(request.associatedTokens), + request.repairThreadCount); + try { + Row row = res.one(); + String repairID = row.getString(0); + return Response.accepted(new RepairRequestResponse(repairID)).build(); + } catch (Exception e) { + return Response.status(Response.Status.INTERNAL_SERVER_ERROR) + .entity("Repair request failed: " + e.getMessage()) + .build(); + } + }); + } + + @DELETE + @Operation(summary = "Cancel all repairs", operationId = "deleteRepairsV2") + @Produces(MediaType.APPLICATION_JSON) + @ApiResponse( + responseCode = "202", + description = "Cancel repairs Successfully requested", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = RepairRequestResponse.class), + examples = @ExampleObject(value = "Accepted"))) + public Response cancelAllRepairs() { + return handle( + () -> { + app.cqlService.executePreparedStatement( + app.dbUnixSocketFile, "CALL NodeOps.stopAllRepairs()"); + return Response.accepted().build(); + }); + } + + private String getParallelismName(RepairParallelism parallelism) { + return parallelism != null ? parallelism.getName() : null; + } + + private String getRingRangeString(List associatedTokens) { + if (associatedTokens != null && !associatedTokens.isEmpty()) { + StringBuilder sb = new StringBuilder(); + for (RingRange ringRange : associatedTokens) { + sb.append(toRangeString(ringRange)).append(","); + } + // remove trailing comma + return sb.substring(0, sb.length() - 2); + } + return null; + } + + private String toRangeString(RingRange ringRange) { + return String.join(":", ringRange.start.toString(), ringRange.end.toString()); + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/TokenResourcesV2.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/TokenResourcesV2.java new file mode 100644 index 00000000..8595ab25 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/TokenResourcesV2.java @@ -0,0 +1,92 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.v2; + +import com.datastax.mgmtapi.ManagementApplication; +import com.datastax.mgmtapi.resources.common.BaseResources; +import com.datastax.mgmtapi.resources.helpers.ResponseTools; +import com.datastax.mgmtapi.resources.v2.models.TokenRangeToEndpointResponse; +import com.datastax.mgmtapi.resources.v2.models.TokenRangeToEndpoints; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.ExampleObject; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +@Path("/api/v2/tokens") +public class TokenResourcesV2 extends BaseResources { + + public TokenResourcesV2(ManagementApplication application) { + super(application); + } + + @GET + @Path("/rangetoendpoint") + @Produces(MediaType.APPLICATION_JSON) + @Operation( + summary = "Retrieve a mapping of Token ranges to endpoints", + operationId = "getRangeToEndpointMapV2") + @ApiResponse( + responseCode = "200", + description = "Token range retrieval was successful", + content = + @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(implementation = TokenRangeToEndpointResponse.class))) + @ApiResponse( + responseCode = "404", + description = "Keyspace not found", + content = + @Content( + mediaType = MediaType.TEXT_PLAIN, + schema = @Schema(implementation = String.class), + examples = @ExampleObject(value = "keyspace not found"))) + public Response getRangeToEndpointMap(@QueryParam(value = "keyspaceName") String keyspaceName) { + return handle( + () -> { + if (keyspaceName != null && !keyspaceExists(keyspaceName)) { + return Response.status(Response.Status.NOT_FOUND).entity("keyspace not found").build(); + } + + Map, List> map = + (Map, List>) + ResponseTools.getSingleRowResponse( + app.dbUnixSocketFile, + app.cqlService, + "CALL NodeOps.getRangeToEndpointMap(?)", + keyspaceName); + return Response.ok(convert(map)).build(); + }); + } + + private TokenRangeToEndpointResponse convert(Map, List> map) { + List rangesToEndpoints = new ArrayList<>(map.size()); + map.entrySet() + .forEach( + (Map.Entry, List> e) -> { + rangesToEndpoints.add( + new TokenRangeToEndpoints(convertRanges(e.getKey()), e.getValue())); + }); + return new TokenRangeToEndpointResponse(rangesToEndpoints); + } + + private List convertRanges(List range) { + // each Range should be exactly 2 strings: start, end + assert range.size() == 2; + List tokenRange = Arrays.asList(Long.valueOf(range.get(0)), Long.parseLong(range.get(1))); + return tokenRange; + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairParallelism.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairParallelism.java new file mode 100644 index 00000000..69670d73 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairParallelism.java @@ -0,0 +1,39 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ + +package com.datastax.mgmtapi.resources.v2.models; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum RepairParallelism { + SEQUENTIAL("sequential"), + PARALLEL("parallel"), + DATACENTER_AWARE("dc_parallel"); + + private final String name; + + public static RepairParallelism fromName(String name) { + if (PARALLEL.getName().equals(name)) { + return PARALLEL; + } else { + return DATACENTER_AWARE.getName().equals(name) ? DATACENTER_AWARE : SEQUENTIAL; + } + } + + private RepairParallelism(String name) { + this.name = name; + } + + @JsonValue + public String getName() { + return this.name; + } + + @Override + public String toString() { + return this.getName(); + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairRequest.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairRequest.java new file mode 100644 index 00000000..f6dd47ce --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairRequest.java @@ -0,0 +1,93 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.v2.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nullable; + +public class RepairRequest { + + @JsonProperty(value = "keyspace", required = true) + public final String keyspace; + + @Nullable + @JsonProperty(value = "tables") + public final List tables; + + @JsonProperty(value = "full_repair", defaultValue = "true") + public final Boolean fullRepair; + + @JsonProperty(value = "notifications", defaultValue = "true") + public final Boolean notifications; + + @Nullable + @JsonProperty(value = "associated_tokens") + public final List associatedTokens; + + @Nullable + @JsonProperty(value = "repair_parallelism") + public final RepairParallelism repairParallelism; + + @Nullable + @JsonProperty(value = "datacenters") + public final List datacenters; + + @Nullable + @JsonProperty(value = "repair_thread_count") + public final Integer repairThreadCount; + + @JsonCreator + public RepairRequest( + @JsonProperty(value = "keyspace", required = true) String keyspace, + @JsonProperty(value = "tables") List tables, + @JsonProperty(value = "full_repair", defaultValue = "true") Boolean fullRepair, + @JsonProperty(value = "notifications", defaultValue = "true") boolean notifications, + @JsonProperty(value = "associated_tokens") List associatedTokens, + @JsonProperty(value = "repair_parallelism") RepairParallelism repairParallelism, + @JsonProperty(value = "datacenters") List datacenters, + @JsonProperty(value = "repair_thread_count") Integer repairThreadCount) { + this.keyspace = keyspace; + this.tables = tables; + this.fullRepair = fullRepair; + this.notifications = notifications; + this.associatedTokens = associatedTokens; + this.datacenters = datacenters; + this.repairParallelism = repairParallelism; + this.repairThreadCount = repairThreadCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + RepairRequest other = (RepairRequest) o; + return Objects.equals(keyspace, other.keyspace) + && Objects.equals(tables, other.tables) + && Objects.equals(fullRepair, other.fullRepair) + && Objects.equals(associatedTokens, other.associatedTokens) + && Objects.equals(datacenters, other.datacenters) + && Objects.equals(repairParallelism, other.repairParallelism) + && Objects.equals(repairThreadCount, other.repairThreadCount); + } + + @Override + public int hashCode() { + return Objects.hashCode(keyspace) + + Objects.hashCode(tables) + + Objects.hashCode(fullRepair) + + Objects.hashCode(associatedTokens) + + Objects.hashCode(datacenters) + + Objects.hashCode(repairParallelism) + + Objects.hashCode(repairThreadCount); + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairRequestResponse.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairRequestResponse.java new file mode 100644 index 00000000..0e57f45b --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RepairRequestResponse.java @@ -0,0 +1,48 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ + +package com.datastax.mgmtapi.resources.v2.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Objects; + +public class RepairRequestResponse { + @JsonProperty(value = "repair_id", required = true) + public final String repairID; + + @JsonCreator + public RepairRequestResponse( + @JsonProperty(value = "repair_id", required = true) String repairID) { + this.repairID = repairID; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return Objects.equals(repairID, ((RepairRequestResponse) o).repairID); + } + + @Override + public int hashCode() { + return Objects.hashCode(repairID); + } + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (Exception e) { + return this.repairID; + } + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RingRange.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RingRange.java new file mode 100644 index 00000000..f685897c --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/RingRange.java @@ -0,0 +1,56 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ + +package com.datastax.mgmtapi.resources.v2.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Comparator; +import java.util.Objects; + +public final class RingRange { + public static final Comparator START_COMPARATOR = + (RingRange o1, RingRange o2) -> o1.start.compareTo(o2.start); + + @JsonProperty(value = "start", required = true) + public final Long start; + + @JsonProperty(value = "end", required = true) + public final Long end; + + public RingRange( + @JsonProperty(value = "start", required = true) Long start, + @JsonProperty(value = "end", required = true) Long end) { + this.start = start; + this.end = end; + } + + public RingRange(String... range) { + start = Long.valueOf(range[0]); + end = Long.valueOf(range[1]); + } + + public Long getStart() { + return start; + } + + public Long getEnd() { + return end; + } + + @Override + public int hashCode() { + return Objects.hashCode(start) + Objects.hashCode(end); + } + + @Override + public boolean equals(Object o) { + if (o == null || !(o instanceof RingRange)) { + return false; + } + RingRange other = (RingRange) o; + return Objects.equals(start, other.start) && Objects.equals(end, other.end); + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/TokenRangeToEndpointResponse.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/TokenRangeToEndpointResponse.java new file mode 100644 index 00000000..819a9858 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/TokenRangeToEndpointResponse.java @@ -0,0 +1,55 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.v2.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Objects; + +public class TokenRangeToEndpointResponse { + + @JsonProperty(value = "token_range_to_endpoints", required = true) + public final List tokenRangeToEndpoints; + + @JsonCreator + public TokenRangeToEndpointResponse( + @JsonProperty(value = "token_range_to_endpoints", required = true) + List list) { + this.tokenRangeToEndpoints = list; + } + + @Override + public int hashCode() { + return 83 * Objects.hashCode(tokenRangeToEndpoints); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TokenRangeToEndpointResponse other = (TokenRangeToEndpointResponse) obj; + return Objects.equals(this.tokenRangeToEndpoints, other.tokenRangeToEndpoints); + } + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + return String.format("Unable to format TokenRangeToEndpointResponse (%s)", e.getMessage()); + } + } +} diff --git a/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/TokenRangeToEndpoints.java b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/TokenRangeToEndpoints.java new file mode 100644 index 00000000..ddb73242 --- /dev/null +++ b/management-api-server/src/main/java/com/datastax/mgmtapi/resources/v2/models/TokenRangeToEndpoints.java @@ -0,0 +1,62 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.v2.models; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Objects; + +public class TokenRangeToEndpoints { + + @JsonProperty(value = "tokens", required = true) + public final List tokens; + + @JsonProperty(value = "endpoints", required = true) + public final List endpoints; + + @JsonCreator + public TokenRangeToEndpoints( + @JsonProperty(value = "tokens", required = true) List tokens, + @JsonProperty(value = "endpoints", required = true) List endpoints) { + this.tokens = tokens; + this.endpoints = endpoints; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + TokenRangeToEndpoints other = (TokenRangeToEndpoints) obj; + if (!Objects.equals(this.tokens, other.tokens)) { + return false; + } + return Objects.equals(this.endpoints, other.endpoints); + } + + @Override + public int hashCode() { + return 83 * Objects.hashCode(this.tokens) * Objects.hashCode(this.endpoints); + } + + @Override + public String toString() { + try { + return new ObjectMapper().writeValueAsString(this); + } catch (JsonProcessingException e) { + return String.format("Unable to format TokenRangeToEndpoints (%s)", e.getMessage()); + } + } +} diff --git a/management-api-server/src/test/java/com/datastax/mgmtapi/BaseDockerIntegrationTest.java b/management-api-server/src/test/java/com/datastax/mgmtapi/BaseDockerIntegrationTest.java index 99cd1398..030ed480 100644 --- a/management-api-server/src/test/java/com/datastax/mgmtapi/BaseDockerIntegrationTest.java +++ b/management-api-server/src/test/java/com/datastax/mgmtapi/BaseDockerIntegrationTest.java @@ -5,17 +5,30 @@ */ package com.datastax.mgmtapi; +import static io.netty.util.CharsetUtil.UTF_8; +import static org.junit.Assert.assertTrue; + import com.datastax.mgmtapi.helpers.DockerHelper; import com.datastax.mgmtapi.helpers.NettyHttpClient; +import com.datastax.mgmtapi.resources.models.CreateOrAlterKeyspaceRequest; +import com.datastax.mgmtapi.resources.models.ReplicationSetting; +import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.collect.Lists; +import io.netty.handler.codec.http.FullHttpResponse; import java.io.File; import java.io.IOError; import java.io.IOException; import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.net.ssl.SSLException; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.http.HttpStatus; +import org.apache.http.client.utils.URIBuilder; import org.junit.AfterClass; import org.junit.AssumptionViolatedException; import org.junit.Before; @@ -26,13 +39,14 @@ import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runners.Parameterized; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; public abstract class BaseDockerIntegrationTest { - protected static final Logger logger = LoggerFactory.getLogger(BaseDockerIntegrationTest.class); protected static final String BASE_PATH = "http://localhost:8080/api/v0"; + protected static final String BASE_PATH_V1 = "http://localhost:8080/api/v1"; + protected static final String BASE_PATH_V2 = "http://localhost:8080/api/v2"; + protected static final String BASE_HOST = "http://localhost:8080"; protected static final URL BASE_URL; + protected static final ObjectMapper JSON_MAPPER = new ObjectMapper(); static { try { @@ -59,7 +73,7 @@ protected void failed(Throwable e, Description description) { System.err.flush(); if (null != docker) { - int numberOfLines = 100; + int numberOfLines = 1000; System.out.printf("=====> Showing last %d entries of system.log%n", numberOfLines); docker.tailSystemLog(numberOfLines); System.out.printf("=====> End of last %d entries of system.log%n", numberOfLines); @@ -162,4 +176,54 @@ protected static File getTempDir() { protected NettyHttpClient getClient() throws SSLException { return new NettyHttpClient(BASE_URL); } + + protected void createKeyspace(NettyHttpClient client, String localDc, String keyspaceName, int rf) + throws IOException, URISyntaxException { + CreateOrAlterKeyspaceRequest request = + new CreateOrAlterKeyspaceRequest( + keyspaceName, Arrays.asList(new ReplicationSetting(localDc, rf))); + String requestAsJSON = JSON_MAPPER.writeValueAsString(request); + + URI uri = new URIBuilder(BASE_PATH + "/ops/keyspace/create").build(); + boolean requestSuccessful = + client + .post(uri.toURL(), requestAsJSON) + .thenApply(r -> r.status().code() == HttpStatus.SC_OK) + .join(); + assertTrue(requestSuccessful); + } + + protected String responseAsString(FullHttpResponse r) { + if (r.status().code() == HttpStatus.SC_OK) { + byte[] result = new byte[r.content().readableBytes()]; + r.content().readBytes(result); + + return new String(result); + } + + return null; + } + + protected Pair responseAsCodeAndBody(FullHttpResponse r) { + FullHttpResponse copy = r.copy(); + if (copy.content().readableBytes() > 0) { + return Pair.of(copy.status().code(), copy.content().toString(UTF_8)); + } + + return Pair.of(copy.status().code(), null); + } + + protected int getNumTokenRanges() { + if (this.version.startsWith("3")) { + return 256; + } + if (this.version.startsWith("dse-68")) { + return 1; + } + if (this.version.startsWith("4")) { + return 16; + } + // unsupported Cassandra/DSE version + throw new UnsupportedOperationException("Cassandra version " + this.version + " not supported"); + } } diff --git a/management-api-server/src/test/java/com/datastax/mgmtapi/DSESpecificIT.java b/management-api-server/src/test/java/com/datastax/mgmtapi/DSESpecificIT.java index a52ceacb..fc2ef7af 100644 --- a/management-api-server/src/test/java/com/datastax/mgmtapi/DSESpecificIT.java +++ b/management-api-server/src/test/java/com/datastax/mgmtapi/DSESpecificIT.java @@ -9,7 +9,6 @@ import static com.datastax.mgmtapi.BaseDockerIntegrationTest.BASE_URL; import static com.datastax.mgmtapi.NonDestructiveOpsIT.ensureStarted; import static com.datastax.oss.driver.api.core.config.DefaultDriverOption.LOAD_BALANCING_LOCAL_DATACENTER; -import static io.netty.util.CharsetUtil.UTF_8; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; @@ -29,7 +28,6 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import io.netty.handler.codec.http.FullHttpResponse; import java.io.IOException; import java.net.InetSocketAddress; import java.net.URI; @@ -160,19 +158,4 @@ private void createKeyspace(NettyHttpClient client, String localDc, String keysp .join(); assertTrue(requestSuccessful); } - - private String responseAsString(FullHttpResponse r) { - if (r.status().code() == HttpStatus.SC_OK) { - byte[] result = new byte[r.content().readableBytes()]; - r.content().readBytes(result); - - return new String(result); - } - - return null; - } - - private Pair responseAsCodeAndBody(FullHttpResponse r) { - return Pair.of(r.status().code(), r.content().toString(UTF_8)); - } } diff --git a/management-api-server/src/test/java/com/datastax/mgmtapi/K8OperatorResourcesTest.java b/management-api-server/src/test/java/com/datastax/mgmtapi/K8OperatorResourcesTest.java index cc201e69..ba3dddfc 100644 --- a/management-api-server/src/test/java/com/datastax/mgmtapi/K8OperatorResourcesTest.java +++ b/management-api-server/src/test/java/com/datastax/mgmtapi/K8OperatorResourcesTest.java @@ -45,6 +45,7 @@ import com.fasterxml.jackson.databind.json.JsonMapper; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; import java.io.IOException; import java.net.URISyntaxException; import java.util.ArrayList; @@ -1657,7 +1658,59 @@ public void testRepair() throws Exception { assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); verify(context.cqlService) .executePreparedStatement( - any(), eq("CALL NodeOps.repair(?, ?, ?)"), eq("test_ks"), eq(null), eq(true)); + any(), + eq("CALL NodeOps.repair(?, ?, ?, ?, ?, ?, ?, ?)"), + eq("test_ks"), + eq(null), + eq(true), + eq(false), + eq(null), + eq(null), + eq(null), + eq(null)); + } + + @Test + public void testRepairAsync() throws Exception { + Context context = setup(); + when(context.cqlService.executePreparedStatement(any(), anyString())).thenReturn(null); + + RepairRequest repairRequest = new RepairRequest("test_ks", null, Boolean.TRUE); + String repairRequestAsJSON = WriterUtility.asString(repairRequest, MediaType.APPLICATION_JSON); + + ResultSet mockResultSet = mock(ResultSet.class); + Row mockRow = mock(Row.class); + + when(context.cqlService.executePreparedStatement(any(), any(), any())) + .thenReturn(mockResultSet); + + when(mockResultSet.one()).thenReturn(mockRow); + + when(mockRow.getString(0)).thenReturn("0fe65b47-98c2-47d8-9c3c-5810c9988e10"); + + MockHttpRequest request = + MockHttpRequest.post("/api/v1/ops/node/repair") + .content(repairRequestAsJSON.getBytes()) + .accept(MediaType.TEXT_PLAIN) + .contentType(MediaType.APPLICATION_JSON_TYPE); + + MockHttpResponse response = context.invoke(request); + + Assert.assertEquals(HttpStatus.SC_ACCEPTED, response.getStatus()); + Assert.assertEquals("0fe65b47-98c2-47d8-9c3c-5810c9988e10", response.getContentAsString()); + + verify(context.cqlService) + .executePreparedStatement( + any(), + eq("CALL NodeOps.repair(?, ?, ?, ?, ?, ?, ?, ?)"), + eq("test_ks"), + eq(null), + eq(true), + eq(true), + eq(null), + eq(null), + eq(null), + eq(null)); } @Test @@ -1737,20 +1790,21 @@ public void testGetReplicationKeyspaceDoesNotExist() throws Exception { public void testListTables() throws Exception { Context context = setup(); ResultSet mockResultSet = mock(ResultSet.class); - Row mockRow = mock(Row.class); + Row mockRow1 = mock(Row.class); + Row mockRow2 = mock(Row.class); MockHttpRequest request = MockHttpRequest.get(ROOT_PATH + "/ops/tables?keyspaceName=ks1"); when(context.cqlService.executePreparedStatement(any(), anyString(), anyString())) .thenReturn(mockResultSet); - when(mockResultSet.one()).thenReturn(mockRow); - List result = ImmutableList.of("table1", "table2"); - when(mockRow.getList(0, String.class)).thenReturn(result); + when(mockResultSet.all()).thenReturn(Lists.newArrayList(mockRow1, mockRow2)); + when(mockRow1.getString("name")).thenReturn("table1"); + when(mockRow2.getString("name")).thenReturn("table2"); MockHttpResponse response = context.invoke(request); assertThat(response.getStatus()).isEqualTo(HttpStatus.SC_OK); String[] actual = new JsonMapper().readValue(response.getContentAsString(), String[].class); - assertThat(actual).containsExactlyElementsOf(result); + assertThat(actual).containsExactly("table1", "table2"); verify(context.cqlService) .executePreparedStatement(any(), eq("CALL NodeOps.getTables(?)"), eq("ks1")); } diff --git a/management-api-server/src/test/java/com/datastax/mgmtapi/MetadataResourcesTest.java b/management-api-server/src/test/java/com/datastax/mgmtapi/MetadataResourcesTest.java index 03e32ef1..c7a901a3 100644 --- a/management-api-server/src/test/java/com/datastax/mgmtapi/MetadataResourcesTest.java +++ b/management-api-server/src/test/java/com/datastax/mgmtapi/MetadataResourcesTest.java @@ -24,17 +24,15 @@ import org.apache.http.HttpStatus; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; -import org.junit.Assert; import org.junit.Test; public class MetadataResourcesTest { - @Test public void testGetReleaseVersion() throws Exception { K8OperatorResourcesTest.Context context = setup(); MockHttpResponse response = getMockHttpResponse(context, "/metadata/versions/release"); assertEquals(HttpStatus.SC_OK, response.getStatus()); - Assert.assertTrue(response.getContentAsString().contains("1.2.3")); + assertTrue(response.getContentAsString().contains("1.2.3")); } @Test diff --git a/management-api-server/src/test/java/com/datastax/mgmtapi/NonDestructiveOpsIT.java b/management-api-server/src/test/java/com/datastax/mgmtapi/NonDestructiveOpsIT.java index 598c40c2..ccb7feca 100644 --- a/management-api-server/src/test/java/com/datastax/mgmtapi/NonDestructiveOpsIT.java +++ b/management-api-server/src/test/java/com/datastax/mgmtapi/NonDestructiveOpsIT.java @@ -5,7 +5,7 @@ */ package com.datastax.mgmtapi; -import static io.netty.util.CharsetUtil.UTF_8; +import static java.util.concurrent.TimeUnit.SECONDS; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; import static org.awaitility.Awaitility.await; @@ -30,7 +30,10 @@ import com.datastax.mgmtapi.resources.models.RepairRequest; import com.datastax.mgmtapi.resources.models.ReplicationSetting; import com.datastax.mgmtapi.resources.models.ScrubRequest; +import com.datastax.mgmtapi.resources.models.Table; import com.datastax.mgmtapi.resources.models.TakeSnapshotRequest; +import com.datastax.mgmtapi.resources.v2.models.RepairParallelism; +import com.datastax.mgmtapi.resources.v2.models.RepairRequestResponse; import com.datastax.oss.driver.api.core.metadata.schema.ClusteringOrder; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -40,7 +43,7 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.Uninterruptibles; -import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.util.IllegalReferenceCountException; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; @@ -49,14 +52,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.concurrent.TimeUnit; -import javax.ws.rs.core.MediaType; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.HttpStatus; import org.apache.http.client.utils.URIBuilder; import org.assertj.core.util.Lists; -import org.jboss.resteasy.core.messagebody.ReaderUtility; -import org.jboss.resteasy.core.messagebody.WriterUtility; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -115,7 +114,7 @@ public static void ensureStarted() throws IOException { if (ready) break; - Uninterruptibles.sleepUninterruptibly(10, TimeUnit.SECONDS); + Uninterruptibles.sleepUninterruptibly(10, SECONDS); } logger.info("CASSANDRA ALIVE: {}", ready); @@ -347,6 +346,8 @@ public void testGetEndpoints() throws IOException, URISyntaxException { List> entity = (List>) response.get("entity"); Map endpoint = entity.get(0); assertThat(endpoint.get("PARTITIONER")).endsWith("Murmur3Partitioner"); + assertThat(endpoint.get("CLUSTER_NAME")).matches("Test Cluster"); + assertThat(endpoint.get("IS_LOCAL")).isEqualTo("true"); Iterable tokens = Splitter.on(",").split(endpoint.get("TOKENS")); assertThat(tokens) .allSatisfy( @@ -362,8 +363,7 @@ public void testCleanup() throws IOException, URISyntaxException, InterruptedExc KeyspaceRequest keyspaceRequest = new KeyspaceRequest(1, "system_traces", Collections.singletonList("events")); - String keyspaceRequestAsJSON = - WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON); + String keyspaceRequestAsJSON = JSON_MAPPER.writeValueAsString(keyspaceRequest); URI uri = new URIBuilder(BASE_PATH + "/ops/keyspace/cleanup").build(); // Get job_id here.. @@ -437,7 +437,7 @@ public void testScrub() throws IOException, URISyntaxException { ScrubRequest scrubRequest = new ScrubRequest( true, true, true, true, 2, "system_traces", Collections.singletonList("events")); - String requestAsJSON = WriterUtility.asString(scrubRequest, MediaType.APPLICATION_JSON); + String requestAsJSON = JSON_MAPPER.writeValueAsString(scrubRequest); URI uri = new URIBuilder(BASE_PATH + "/ops/tables/scrub").build(); boolean requestSuccessful = client @@ -457,7 +457,7 @@ public void testCompact() throws IOException, URISyntaxException { CompactRequest compactRequest = new CompactRequest( false, false, null, null, "system_traces", null, Collections.singletonList("events")); - String requestAsJSON = WriterUtility.asString(compactRequest, MediaType.APPLICATION_JSON); + String requestAsJSON = JSON_MAPPER.writeValueAsString(compactRequest); URI uri = new URIBuilder(BASE_PATH + "/ops/tables/compact").build(); boolean requestSuccessful = client @@ -476,8 +476,7 @@ public void testGarbageCollect() throws IOException, URISyntaxException { KeyspaceRequest keyspaceRequest = new KeyspaceRequest(1, "system_traces", Collections.singletonList("events")); - String keyspaceRequestAsJSON = - WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON); + String keyspaceRequestAsJSON = JSON_MAPPER.writeValueAsString(keyspaceRequest); URI uri = new URIBuilder(BASE_PATH + "/ops/tables/garbagecollect").build(); boolean requestSuccessful = client @@ -495,8 +494,7 @@ public void testFlush() throws IOException, URISyntaxException { NettyHttpClient client = new NettyHttpClient(BASE_URL); KeyspaceRequest keyspaceRequest = new KeyspaceRequest(1, null, null); - String keyspaceRequestAsJSON = - WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON); + String keyspaceRequestAsJSON = JSON_MAPPER.writeValueAsString(keyspaceRequest); URI uri = new URIBuilder(BASE_PATH + "/ops/tables/flush").build(); boolean requestSuccessful = client @@ -514,8 +512,7 @@ public void testUpgradeSSTables() throws IOException, URISyntaxException { NettyHttpClient client = new NettyHttpClient(BASE_URL); KeyspaceRequest keyspaceRequest = new KeyspaceRequest(1, "", null); - String keyspaceRequestAsJSON = - WriterUtility.asString(keyspaceRequest, MediaType.APPLICATION_JSON); + String keyspaceRequestAsJSON = JSON_MAPPER.writeValueAsString(keyspaceRequest); URI uri = new URIBuilder(BASE_PATH + "/ops/tables/sstables/upgrade").build(); boolean requestSuccessful = client @@ -550,7 +547,7 @@ public void testCreateKeyspace() throws IOException, URISyntaxException { .thenApply(this::responseAsString) .join(); - createKeyspace(client, localDc, "someTestKeyspace"); + createKeyspace(client, localDc, "someTestKeyspace", 1); } @Test @@ -566,11 +563,11 @@ public void testAlterKeyspace() throws IOException, URISyntaxException { .join(); String ks = "alteringKeyspaceTest"; - createKeyspace(client, localDc, ks); + createKeyspace(client, localDc, ks, 1); CreateOrAlterKeyspaceRequest request = new CreateOrAlterKeyspaceRequest(ks, Arrays.asList(new ReplicationSetting(localDc, 3))); - String requestAsJSON = WriterUtility.asString(request, MediaType.APPLICATION_JSON); + String requestAsJSON = JSON_MAPPER.writeValueAsString(request); boolean requestSuccessful = client @@ -593,7 +590,7 @@ public void testGetKeyspaces() throws IOException, URISyntaxException { .join(); String ks = "getkeyspacestest"; - createKeyspace(client, localDc, ks); + createKeyspace(client, localDc, ks, 1); URI uri = new URIBuilder(BASE_PATH + "/ops/keyspace").build(); String response = client.get(uri.toURL()).thenApply(this::responseAsString).join(); @@ -620,7 +617,7 @@ public void testGetSchemaVersions() throws IOException, URISyntaxException { NettyHttpClient client = new NettyHttpClient(BASE_URL); - URIBuilder uriBuilder = new URIBuilder("http://localhost:8080/api/v1/ops/node/schema/versions"); + URIBuilder uriBuilder = new URIBuilder(BASE_PATH_V1 + "/ops/node/schema/versions"); URI uri = uriBuilder.build(); Pair response = @@ -666,7 +663,7 @@ public void testGetSnapshotDetails() null, null, null); - String requestAsJSON = WriterUtility.asString(takeSnapshotRequest, MediaType.APPLICATION_JSON); + String requestAsJSON = JSON_MAPPER.writeValueAsString(takeSnapshotRequest); boolean takeSnapshotSuccessful = client @@ -680,8 +677,7 @@ public void testGetSnapshotDetails() String getSnapshotResponse = client.get(getSnapshotsUri.toURL()).thenApply(this::responseAsString).join(); assertNotNull(getSnapshotResponse); - Object responseObject = - ReaderUtility.read(Object.class, MediaType.APPLICATION_JSON, getSnapshotResponse); + Object responseObject = JSON_MAPPER.readValue(getSnapshotResponse, Object.class); assertTrue(responseObject instanceof Map); Map responseObj = (Map) responseObject; assertTrue(responseObj.containsKey("entity")); @@ -710,8 +706,7 @@ public void testGetSnapshotDetails() getSnapshotResponse = client.get(getSnapshotsUri.toURL()).thenApply(this::responseAsString).join(); assertNotNull(getSnapshotResponse); - responseObject = - ReaderUtility.read(Object.class, MediaType.APPLICATION_JSON, getSnapshotResponse); + responseObject = JSON_MAPPER.readValue(getSnapshotResponse, Object.class); assertTrue(responseObject instanceof Map); responseObj = (Map) responseObject; assertTrue(responseObj.containsKey("entity")); @@ -726,14 +721,23 @@ public void testRepair() throws IOException, URISyntaxException, InterruptedExce assumeTrue(IntegrationTestUtils.shouldRun()); ensureStarted(); + // create a keyspace with RF of at least 2 NettyHttpClient client = new NettyHttpClient(BASE_URL); + String localDc = + client + .get(new URIBuilder(BASE_PATH + "/metadata/localdc").build().toURL()) + .thenApply(this::responseAsString) + .join(); + + String ks = "someTestKeyspace"; + createKeyspace(client, localDc, ks, 2); URIBuilder uriBuilder = new URIBuilder(BASE_PATH + "/ops/node/repair"); URI repairUri = uriBuilder.build(); // execute repair - RepairRequest repairRequest = new RepairRequest("system_auth", null, Boolean.TRUE); - String requestAsJSON = WriterUtility.asString(repairRequest, MediaType.APPLICATION_JSON); + RepairRequest repairRequest = new RepairRequest(ks, null, Boolean.TRUE); + String requestAsJSON = JSON_MAPPER.writeValueAsString(repairRequest); boolean repairSuccessful = client @@ -743,6 +747,68 @@ public void testRepair() throws IOException, URISyntaxException, InterruptedExce assertTrue("Repair request was not successful", repairSuccessful); } + @Test + public void testAsyncRepair() throws IOException, URISyntaxException, InterruptedException { + assumeTrue(IntegrationTestUtils.shouldRun()); + ensureStarted(); + + // create a keyspace with RF of at least 2 + NettyHttpClient client = new NettyHttpClient(BASE_URL); + String localDc = + client + .get(new URIBuilder(BASE_PATH + "/metadata/localdc").build().toURL()) + .thenApply(this::responseAsString) + .join(); + + String ks = "someTestKeyspace"; + createKeyspace(client, localDc, ks, 2); + + URIBuilder uriBuilder = new URIBuilder(BASE_PATH_V1 + "/ops/node/repair"); + URI repairUri = uriBuilder.build(); + + // execute repair + RepairRequest repairRequest = new RepairRequest("someTestKeyspace", null, Boolean.TRUE); + String requestAsJSON = JSON_MAPPER.writeValueAsString(repairRequest); + + Pair repairResponse = + client.post(repairUri.toURL(), requestAsJSON).thenApply(this::responseAsCodeAndBody).join(); + assertThat(repairResponse.getLeft()).isEqualTo(HttpStatus.SC_ACCEPTED); + String jobId = repairResponse.getRight(); + assertThat(jobId).isNotEmpty(); + + URI getJobDetailsUri = + new URIBuilder(BASE_PATH + "/ops/executor/job").addParameter("job_id", jobId).build(); + + await() + .atMost(Duration.ofMinutes(5)) + .untilAsserted( + () -> { + Pair getJobDetailsResponse; + try { + getJobDetailsResponse = + client + .get(getJobDetailsUri.toURL()) + .thenApply(this::responseAsCodeAndBody) + .join(); + } catch (IllegalReferenceCountException e) { + // Just retry + assertFalse(true); + return; + } + assertThat(getJobDetailsResponse.getLeft()).isEqualTo(HttpStatus.SC_OK); + Map jobDetails = + new JsonMapper() + .readValue( + getJobDetailsResponse.getRight(), + new TypeReference>() {}); + assertThat(jobDetails) + .hasEntrySatisfying("id", value -> assertThat(value).isEqualTo(jobId)) + .hasEntrySatisfying("type", value -> assertThat(value).isEqualTo("repair")) + .hasEntrySatisfying( + "status", value -> assertThat(value).isIn("COMPLETED", "ERROR")); + }); + } + @Test public void testGetReplication() throws IOException, URISyntaxException { assumeTrue(IntegrationTestUtils.shouldRun()); @@ -756,7 +822,7 @@ public void testGetReplication() throws IOException, URISyntaxException { .join(); String ks = "getreplicationtest"; - createKeyspace(client, localDc, ks); + createKeyspace(client, localDc, ks, 1); // missing keyspace URI uri = new URIBuilder(BASE_PATH + "/ops/keyspace/replication").build(); @@ -825,6 +891,54 @@ public void testGetTables() throws IOException, URISyntaxException { "views"); } + @Test + public void testGetTablesV1() throws IOException, URISyntaxException { + assumeTrue(IntegrationTestUtils.shouldRun()); + ensureStarted(); + + NettyHttpClient client = new NettyHttpClient(BASE_URL); + + // missing keyspace + URI uri = new URIBuilder(BASE_PATH_V1 + "/ops/tables").build(); + Pair response = + client.get(uri.toURL()).thenApply(this::responseAsCodeAndBody).join(); + assertThat(response.getLeft()).isEqualTo(HttpStatus.SC_BAD_REQUEST); + assertThat(response.getRight()).contains("Non-empty 'keyspaceName' must be provided"); + + // non existent keyspace + uri = new URIBuilder(BASE_PATH_V1 + "/ops/tables?keyspaceName=nonexistent").build(); + response = client.get(uri.toURL()).thenApply(this::responseAsCodeAndBody).join(); + assertThat(response.getLeft()).isEqualTo(HttpStatus.SC_OK); + assertThat(response.getRight()).isEqualTo("[]"); + + // existing keyspace + uri = new URIBuilder(BASE_PATH_V1 + "/ops/tables?keyspaceName=system_schema").build(); + response = client.get(uri.toURL()).thenApply(this::responseAsCodeAndBody).join(); + assertThat(response.getLeft()).isEqualTo(HttpStatus.SC_OK); + + List
actual = + new JsonMapper().readValue(response.getRight(), new TypeReference>() {}); + assertThat(actual) + .extracting("name") + .contains( + "aggregates", + "columns", + "functions", + "indexes", + "keyspaces", + "tables", + "triggers", + "types", + "views"); + assertThat(actual) + .allSatisfy( + table -> + assertThat(table.compaction) + .containsEntry( + "class", + "org.apache.cassandra.db.compaction.SizeTieredCompactionStrategy")); + } + @Test public void testCreateTable() throws IOException, URISyntaxException { assumeTrue(IntegrationTestUtils.shouldRun()); @@ -839,7 +953,7 @@ public void testCreateTable() throws IOException, URISyntaxException { // this test also tests case sensitivity in CQL identifiers. String ks = "CreateTableTest"; - createKeyspace(client, localDc, ks); + createKeyspace(client, localDc, ks, 1); CreateTableRequest request = new CreateTableRequest( @@ -922,34 +1036,62 @@ public void testMoveNode() throws IOException, URISyntaxException { }); } - private void createKeyspace(NettyHttpClient client, String localDc, String keyspaceName) - throws IOException, URISyntaxException { - CreateOrAlterKeyspaceRequest request = - new CreateOrAlterKeyspaceRequest( - keyspaceName, Arrays.asList(new ReplicationSetting(localDc, 1))); - String requestAsJSON = WriterUtility.asString(request, MediaType.APPLICATION_JSON); + @Test + public void testEnsureStatusChanges() throws Exception { + assumeTrue(IntegrationTestUtils.shouldRun()); + ensureStarted(); + NettyHttpClient client = new NettyHttpClient(BASE_URL); - URI uri = new URIBuilder(BASE_PATH + "/ops/keyspace/create").build(); - boolean requestSuccessful = + com.datastax.mgmtapi.resources.v2.models.RepairRequest req = + new com.datastax.mgmtapi.resources.v2.models.RepairRequest( + "system_distributed", + null, + true, + true, + Collections.singletonList( + new com.datastax.mgmtapi.resources.v2.models.RingRange(-1L, 100L)), + RepairParallelism.SEQUENTIAL, + null, + null); + + logger.info("Sending repair request: {}", req); + URI repairUri = new URIBuilder(BASE_PATH_V2 + "/repairs").build(); + Pair repairResp = client - .post(uri.toURL(), requestAsJSON) - .thenApply(r -> r.status().code() == HttpStatus.SC_OK) + .put(repairUri.toURL(), new ObjectMapper().writeValueAsString(req)) + .thenApply(this::responseAsCodeAndBody) .join(); - assertTrue(requestSuccessful); - } - - private String responseAsString(FullHttpResponse r) { - if (r.status().code() == HttpStatus.SC_OK) { - byte[] result = new byte[r.content().readableBytes()]; - r.content().readBytes(result); - - return new String(result); - } - - return null; - } - - private Pair responseAsCodeAndBody(FullHttpResponse r) { - return Pair.of(r.status().code(), r.content().toString(UTF_8)); + System.out.println("repairResp was " + repairResp); + String jobID = + new ObjectMapper().readValue(repairResp.getRight(), RepairRequestResponse.class).repairID; + Integer repairID = + Integer.parseInt( + jobID.substring(7) // Trimming off "repair-" prefix. + ); + logger.info("Repair ID: {}", repairID); + assertThat(repairID).isNotNull(); + assertThat(repairID).isGreaterThan(0); + + URI statusUri = + new URIBuilder(BASE_PATH_V2 + "/ops/executor/job").addParameter("job_id", jobID).build(); + Pair statusResp = + client.get(statusUri.toURL()).thenApply(this::responseAsCodeAndBody).join(); + logger.info("Repair job status: {}", statusResp); + Job jobStatus = new ObjectMapper().readValue(statusResp.getRight(), Job.class); + + assertThat(jobStatus.getStatus()).isNotNull(); + assertThat(jobStatus.getStatusChanges()).isNotNull(); + await() + .atMost(5, SECONDS) + .until( + () -> { + Pair statusResp2 = + client.get(statusUri.toURL()).thenApply(this::responseAsCodeAndBody).join(); + logger.info("Repair job status: {}", statusResp); + Job jobStatus2 = new ObjectMapper().readValue(statusResp.getRight(), Job.class); + return jobStatus2.getStatusChanges().size() > 0 + && jobStatus2.getStatus() + == com.datastax.mgmtapi.resources.models.Job.JobStatus.COMPLETED; + }); } } diff --git a/management-api-server/src/test/java/com/datastax/mgmtapi/helpers/NettyHttpClient.java b/management-api-server/src/test/java/com/datastax/mgmtapi/helpers/NettyHttpClient.java index c56d4f70..027f638e 100644 --- a/management-api-server/src/test/java/com/datastax/mgmtapi/helpers/NettyHttpClient.java +++ b/management-api-server/src/test/java/com/datastax/mgmtapi/helpers/NettyHttpClient.java @@ -157,6 +157,36 @@ public CompletableFuture post( return result; } + public CompletableFuture put( + URL url, final CharSequence body, String contentType) throws UnsupportedEncodingException { + CompletableFuture result = new CompletableFuture<>(); + + if (!activeRequestFuture.compareAndSet(null, result)) + throw new RuntimeException("outstanding request"); + + DefaultFullHttpRequest request = + new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.PUT, url.getFile()); + request.headers().set(HttpHeaders.Names.CONTENT_TYPE, contentType); + request.headers().set(HttpHeaderNames.HOST, url.getHost()); + + if (body != null) { + request.content().writeBytes(body.toString().getBytes(CharsetUtil.UTF_8.name())); + request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, request.content().readableBytes()); + } else { + request.headers().set(HttpHeaders.Names.CONTENT_LENGTH, 0); + } + + // Send the HTTP request. + client.writeAndFlush(request); + + return result; + } + + public CompletableFuture put(URL url, final CharSequence body) + throws UnsupportedEncodingException { + return post(url, body, "application/json"); + } + public CompletableFuture delete(URL url) { return buildAndSendRequest(HttpMethod.DELETE, url); } diff --git a/management-api-server/src/test/java/com/datastax/mgmtapi/resources/v2/NonDestructiveOpsResourcesV2IT.java b/management-api-server/src/test/java/com/datastax/mgmtapi/resources/v2/NonDestructiveOpsResourcesV2IT.java new file mode 100644 index 00000000..f248b545 --- /dev/null +++ b/management-api-server/src/test/java/com/datastax/mgmtapi/resources/v2/NonDestructiveOpsResourcesV2IT.java @@ -0,0 +1,54 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.v2; + +import static com.datastax.mgmtapi.NonDestructiveOpsIT.ensureStarted; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assume.assumeTrue; + +import com.datastax.mgmtapi.BaseDockerIntegrationTest; +import com.datastax.mgmtapi.helpers.IntegrationTestUtils; +import com.datastax.mgmtapi.helpers.NettyHttpClient; +import com.datastax.mgmtapi.resources.v2.models.TokenRangeToEndpointResponse; +import java.io.IOException; +import java.net.URI; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.http.HttpStatus; +import org.apache.http.client.utils.URIBuilder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +@RunWith(Parameterized.class) +public class NonDestructiveOpsResourcesV2IT extends BaseDockerIntegrationTest { + + public NonDestructiveOpsResourcesV2IT(String version) throws IOException { + super(version); + } + + @Test + public void testGetTokenRangeToEndpointMap() throws Exception { + assumeTrue(IntegrationTestUtils.shouldRun()); + ensureStarted(); + + NettyHttpClient client = new NettyHttpClient(BASE_URL); + final URIBuilder uriBuilder = new URIBuilder(BASE_PATH_V2 + "/tokens/rangetoendpoint"); + // test keyspace not found + URI uri = uriBuilder.setParameter("keyspaceName", "notfoundkeyspace").build(); + Pair response = + client.get(uri.toURL()).thenApply(this::responseAsCodeAndBody).join(); + assertThat(response.getLeft()).isEqualTo(HttpStatus.SC_NOT_FOUND); + // test keyspace exists + uri = uriBuilder.setParameter("keyspaceName", "system_schema").build(); + response = client.get(uri.toURL()).thenApply(this::responseAsCodeAndBody).join(); + assertThat(response.getLeft()).isEqualTo(HttpStatus.SC_OK); + String mappingString = response.getRight(); + assertThat(mappingString).isNotNull().isNotEmpty(); + TokenRangeToEndpointResponse mapping = + JSON_MAPPER.readValue(mappingString, TokenRangeToEndpointResponse.class); + assertThat(mapping.tokenRangeToEndpoints).isNotNull().isNotEmpty().hasSize(getNumTokenRanges()); + } +} diff --git a/management-api-server/src/test/java/com/datastax/mgmtapi/resources/v2/RepairResourcesV2Test.java b/management-api-server/src/test/java/com/datastax/mgmtapi/resources/v2/RepairResourcesV2Test.java new file mode 100644 index 00000000..12abe689 --- /dev/null +++ b/management-api-server/src/test/java/com/datastax/mgmtapi/resources/v2/RepairResourcesV2Test.java @@ -0,0 +1,115 @@ +/* + * Copyright DataStax, Inc. + * + * Please see the included license file for details. + */ +package com.datastax.mgmtapi.resources.v2; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.datastax.mgmtapi.CqlService; +import com.datastax.mgmtapi.ManagementApplication; +import com.datastax.mgmtapi.resources.v2.models.RepairParallelism; +import com.datastax.mgmtapi.resources.v2.models.RepairRequest; +import com.datastax.mgmtapi.resources.v2.models.RepairRequestResponse; +import com.datastax.oss.driver.api.core.cql.ResultSet; +import com.datastax.oss.driver.api.core.cql.Row; +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import javax.ws.rs.core.Response; +import org.junit.Test; + +public class RepairResourcesV2Test { + + @Test + public void testRepairResourcesSuccess() throws Exception { + CqlService mockCqlService = mock(CqlService.class); + ManagementApplication app = + new ManagementApplication( + null, null, new File("/tmp/cassandra.sock"), mockCqlService, null); + ResultSet mockResultSet = mock(ResultSet.class); + Row mockRow = mock(Row.class); + when(mockResultSet.one()).thenReturn(mockRow); + when(mockRow.getString(anyInt())).thenReturn("mockRepairID"); + when(mockCqlService.executePreparedStatement( + any(), anyString(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(mockResultSet); + RepairResourcesV2 unit = new RepairResourcesV2(app); + RepairRequest req = + new RepairRequest( + "keyspace", + Collections.singletonList("table1"), + false, + true, + Collections.EMPTY_LIST, + RepairParallelism.DATACENTER_AWARE, + Collections.EMPTY_LIST, + 1); + Response resp = unit.repair(req); + assertEquals(202, resp.getStatus()); + assertEquals("mockRepairID", ((RepairRequestResponse) resp.getEntity()).repairID); + verify(mockCqlService) + .executePreparedStatement( + any(), + eq("CALL NodeOps.repair(?, ?, ?, ?, ?, ?, ?, ?)"), + eq("keyspace"), + eq(Collections.singletonList("table1")), + eq(false), + eq(true), + eq(RepairParallelism.DATACENTER_AWARE.getName()), + eq(Collections.EMPTY_LIST), + eq(null), + eq(Integer.valueOf(1))); + } + + @Test + public void testRepairResourcesFail() throws Exception { + CqlService mockCqlService = mock(CqlService.class); + ManagementApplication app = + new ManagementApplication( + null, null, new File("/tmp/cassandra.sock"), mockCqlService, null); + ResultSet mockResultSet = mock(ResultSet.class); + Row mockRow = mock(Row.class); + when(mockRow.getString(anyString())).thenReturn("mockrepairID"); + when(mockCqlService.executePreparedStatement( + any(), anyString(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(mockResultSet); + RepairResourcesV2 unit = new RepairResourcesV2(app); + List tables = new ArrayList<>(); + tables.add("table1"); + tables.add("table2"); + RepairRequest req = + new RepairRequest( + "", + tables, + false, + true, + Collections.EMPTY_LIST, + RepairParallelism.DATACENTER_AWARE, + Collections.EMPTY_LIST, + 1); + Response resp = unit.repair(req); + assertEquals(500, resp.getStatus()); + } + + @Test + public void testCancelAllRepairs() throws Exception { + CqlService mockCqlService = mock(CqlService.class); + ManagementApplication app = + new ManagementApplication( + null, null, new File("/tmp/cassandra.sock"), mockCqlService, null); + RepairResourcesV2 unit = new RepairResourcesV2(app); + Response resp = unit.cancelAllRepairs(); + assertEquals(202, resp.getStatus()); + verify(mockCqlService).executePreparedStatement(any(), eq("CALL NodeOps.stopAllRepairs()")); + } +} diff --git a/pom.xml b/pom.xml index 63a400d1..95fa28a8 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,7 @@ 4.0.10 3.2.13 4.13.2 + 3.17.2 1.12.19 build_version.sh 1.7.25