This is an automated email from the ASF dual-hosted git repository.

frankgh pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new c1b572e3 CASSSIDECAR-253: add start/stop binary transport&gossip (#225)
c1b572e3 is described below

commit c1b572e3271aeb022bdefe588ecc0648546eab63
Author: Yuntong Qu <[email protected]>
AuthorDate: Sun Jun 22 18:42:53 2025 -0400

    CASSSIDECAR-253: add start/stop binary transport&gossip (#225)
    
    Patch by Yuntong Qu; reviewed by Francisco Guerrero; Saranya Krishnakumar; 
Yifan Cai; Arjun Ashok for CASSSIDECAR-253
---
 .../adapters/base/CassandraStorageOperations.java  |  40 +++++
 .../jmx/GossipDependentStorageJmxOperations.java   |  24 +++
 .../adapters/base/jmx/StorageJmxOperations.java    |  20 +++
 .../cassandra/sidecar/common/ApiEndpointsV1.java   |   7 +-
 .../sidecar/common/request/GossipInfoRequest.java  |   2 +-
 ...ipInfoRequest.java => GossipUpdateRequest.java} |  29 ++--
 ...ipInfoRequest.java => NativeUpdateRequest.java} |  31 ++--
 .../request/data/NodeCommandRequestPayload.java    | 103 ++++++++++++
 .../cassandra/sidecar/client/RequestContext.java   |  28 ++++
 .../cassandra/sidecar/client/SidecarClient.java    |  42 +++++
 .../sidecar/client/SidecarClientTest.java          |  39 ++++-
 .../request/GossipInfoRequestTestParameters.java   |   2 +-
 .../NodeGossipAndNativeModifyIntegrationTest.java  | 128 +++++++++++++++
 .../sidecar/common/server/StorageOperations.java   |  20 +++
 .../acl/authorization/BasicPermissions.java        |   3 +
 .../sidecar/handlers/CassandraHealthHandler.java   |   4 +-
 .../sidecar/handlers/GossipHealthHandler.java      |   4 +-
 .../sidecar/handlers/GossipUpdateHandler.java      |  87 +++++++++++
 .../sidecar/handlers/NativeUpdateHandler.java      |  88 +++++++++++
 .../sidecar/handlers/NodeCommandHandler.java       |  76 +++++++++
 .../sidecar/handlers/ReportSchemaHandler.java      |   2 +-
 .../handlers/cdc/DeleteServiceConfigHandler.java   |   2 +-
 .../cassandra/sidecar/modules/ApiModule.java       |   9 ++
 .../sidecar/modules/CassandraOperationsModule.java |  24 +++
 .../sidecar/modules/HealthCheckModule.java         |   8 +-
 .../modules/multibindings/VertxRouteMapKeys.java   |  13 +-
 .../sidecar/handlers/GossipUpdateHandlerTest.java  | 173 +++++++++++++++++++++
 .../sidecar/handlers/NativeUpdateHandlerTest.java  | 169 ++++++++++++++++++++
 .../sidecar/handlers/RouteBuilderTest.java         |   2 +-
 29 files changed, 1137 insertions(+), 42 deletions(-)

diff --git 
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
 
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
index 88635d80..bcf06677 100644
--- 
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
+++ 
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/CassandraStorageOperations.java
@@ -254,4 +254,44 @@ public class CassandraStorageOperations implements 
StorageOperations
         return jmxClient.proxy(StorageJmxOperations.class, 
STORAGE_SERVICE_OBJ_NAME)
                         .getClusterName();
     }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void stopNativeTransport()
+    {
+        jmxClient.proxy(StorageJmxOperations.class, STORAGE_SERVICE_OBJ_NAME)
+                 .stopNativeTransport();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void startNativeTransport()
+    {
+        jmxClient.proxy(StorageJmxOperations.class, STORAGE_SERVICE_OBJ_NAME)
+                 .startNativeTransport();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void stopGossiping()
+    {
+        jmxClient.proxy(StorageJmxOperations.class, STORAGE_SERVICE_OBJ_NAME)
+                 .stopGossiping();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void startGossiping()
+    {
+        jmxClient.proxy(StorageJmxOperations.class, STORAGE_SERVICE_OBJ_NAME)
+                 .startGossiping();
+    }
 }
diff --git 
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/GossipDependentStorageJmxOperations.java
 
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/GossipDependentStorageJmxOperations.java
index 526e8a03..7d1ecc73 100644
--- 
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/GossipDependentStorageJmxOperations.java
+++ 
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/GossipDependentStorageJmxOperations.java
@@ -177,4 +177,28 @@ public class GossipDependentStorageJmxOperations 
implements StorageJmxOperations
     {
         return delegate.getClusterName();
     }
+
+    @Override
+    public void stopNativeTransport()
+    {
+        delegate.stopNativeTransport();
+    }
+
+    @Override
+    public void startNativeTransport()
+    {
+        delegate.startNativeTransport();
+    }
+
+    @Override
+    public void stopGossiping()
+    {
+        delegate.stopGossiping();
+    }
+
+    @Override
+    public void startGossiping()
+    {
+        delegate.startGossiping();
+    }
 }
diff --git 
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/StorageJmxOperations.java
 
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/StorageJmxOperations.java
index 37cff021..a3517d7a 100644
--- 
a/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/StorageJmxOperations.java
+++ 
b/adapters/adapters-base/src/main/java/org/apache/cassandra/sidecar/adapters/base/jmx/StorageJmxOperations.java
@@ -178,4 +178,24 @@ public interface StorageJmxOperations
      * @return the name of the cluster
      */
     String getClusterName();
+
+    /**
+     * Triggers stop native transport
+     */
+    void stopNativeTransport();
+
+    /**
+     * Triggers start native transport
+     */
+    void startNativeTransport();
+
+    /**
+     * Triggers stop gossip
+     */
+    void stopGossiping();
+
+    /**
+     * Triggers start gossip
+     */
+    void startGossiping();
 }
diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
index cd1c62ab..cd47c812 100644
--- 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/ApiEndpointsV1.java
@@ -59,7 +59,8 @@ public final class ApiEndpointsV1
      */
     @Deprecated
     public static final String CASSANDRA_HEALTH_ROUTE = API_V1 + CASSANDRA + 
HEALTH;
-    public static final String CASSANDRA_NATIVE_HEALTH_ROUTE = API_V1 + 
CASSANDRA + NATIVE + HEALTH;
+    public static final String CASSANDRA_NATIVE_ROUTE = API_V1 + CASSANDRA + 
NATIVE;
+    public static final String CASSANDRA_NATIVE_HEALTH_ROUTE = 
CASSANDRA_NATIVE_ROUTE + HEALTH;
     public static final String CASSANDRA_JMX_HEALTH_ROUTE = API_V1 + CASSANDRA 
+ JMX + HEALTH;
 
     @Deprecated  // NOTE: Uses singular forms of "keyspace" and "table"
@@ -96,8 +97,8 @@ public final class ApiEndpointsV1
     public static final String SSTABLE_IMPORT_ROUTE = API_V1 + PER_UPLOAD + 
PER_KEYSPACE + PER_TABLE + "/import";
     public static final String SSTABLE_CLEANUP_ROUTE = API_V1 + PER_UPLOAD;
 
-    public static final String GOSSIP_INFO_ROUTE = API_V1 + CASSANDRA + 
"/gossip";
-    public static final String GOSSIP_HEALTH_ROUTE = GOSSIP_INFO_ROUTE + 
HEALTH;
+    public static final String GOSSIP_ROUTE = API_V1 + CASSANDRA + "/gossip";
+    public static final String GOSSIP_HEALTH_ROUTE = GOSSIP_ROUTE + HEALTH;
     public static final String TIME_SKEW_ROUTE = API_V1 + "/time-skew";
 
     public static final String KEYSPACE_TOKEN_MAPPING_ROUTE = API_V1 + 
PER_KEYSPACE + "/token-range-replicas";
diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
index 4947a8f0..2f9a2920 100644
--- 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
@@ -32,7 +32,7 @@ public class GossipInfoRequest extends 
JsonRequest<GossipInfoResponse>
      */
     public GossipInfoRequest()
     {
-        super(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+        super(ApiEndpointsV1.GOSSIP_ROUTE);
     }
 
     /**
diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipUpdateRequest.java
similarity index 59%
copy from 
client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
copy to 
client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipUpdateRequest.java
index 4947a8f0..0482fb1f 100644
--- 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipUpdateRequest.java
@@ -20,27 +20,36 @@ package org.apache.cassandra.sidecar.common.request;
 
 import io.netty.handler.codec.http.HttpMethod;
 import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
-import org.apache.cassandra.sidecar.common.response.GossipInfoResponse;
+import 
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.common.response.HealthResponse;
 
 /**
- * Represents a request to retrieve the Cassandra gossip information
+ * Gossip update request
  */
-public class GossipInfoRequest extends JsonRequest<GossipInfoResponse>
+public class GossipUpdateRequest extends JsonRequest<HealthResponse>
 {
+    private final NodeCommandRequestPayload requestPayload;
+
     /**
-     * Constructs a request to retrieve the Cassandra gossip information
+     * Constructs a gossip update request with the provided parameters
+     *
+     * @param state START or STOP
      */
-    public GossipInfoRequest()
+    public GossipUpdateRequest(NodeCommandRequestPayload.State state)
     {
-        super(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+        super(ApiEndpointsV1.GOSSIP_ROUTE);
+        this.requestPayload = new NodeCommandRequestPayload(state.toValue());
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public HttpMethod method()
     {
-        return HttpMethod.GET;
+        return HttpMethod.PUT;
+    }
+
+    @Override
+    public Object requestBody()
+    {
+        return requestPayload;
     }
 }
diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/NativeUpdateRequest.java
similarity index 58%
copy from 
client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
copy to 
client-common/src/main/java/org/apache/cassandra/sidecar/common/request/NativeUpdateRequest.java
index 4947a8f0..cdbd7464 100644
--- 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/GossipInfoRequest.java
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/NativeUpdateRequest.java
@@ -18,29 +18,40 @@
 
 package org.apache.cassandra.sidecar.common.request;
 
+
 import io.netty.handler.codec.http.HttpMethod;
 import org.apache.cassandra.sidecar.common.ApiEndpointsV1;
-import org.apache.cassandra.sidecar.common.response.GossipInfoResponse;
+import 
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.common.response.HealthResponse;
 
 /**
- * Represents a request to retrieve the Cassandra gossip information
+ * native transport update request
  */
-public class GossipInfoRequest extends JsonRequest<GossipInfoResponse>
+public class NativeUpdateRequest extends JsonRequest<HealthResponse>
 {
+    private final NodeCommandRequestPayload requestPayload;
+
     /**
-     * Constructs a request to retrieve the Cassandra gossip information
+     * Constructs a gossip update request with the provided parameters
+     *
+     * @param state START or STOP
      */
-    public GossipInfoRequest()
+    public NativeUpdateRequest(NodeCommandRequestPayload.State state)
     {
-        super(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+        super(ApiEndpointsV1.CASSANDRA_NATIVE_ROUTE);
+        this.requestPayload = new NodeCommandRequestPayload(state.toValue());
     }
 
-    /**
-     * {@inheritDoc}
-     */
     @Override
     public HttpMethod method()
     {
-        return HttpMethod.GET;
+        return HttpMethod.PUT;
+    }
+
+    @Override
+    public Object requestBody()
+    {
+        return requestPayload;
     }
 }
+
diff --git 
a/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/data/NodeCommandRequestPayload.java
 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/data/NodeCommandRequestPayload.java
new file mode 100644
index 00000000..9a75f4e3
--- /dev/null
+++ 
b/client-common/src/main/java/org/apache/cassandra/sidecar/common/request/data/NodeCommandRequestPayload.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.common.request.data;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.cassandra.sidecar.common.utils.Preconditions;
+import org.apache.cassandra.sidecar.common.utils.StringUtils;
+
+/**
+ * Request payload for start/stop operations (gossip, native transport, etc.).
+ *
+ * <p>Valid JSON:</p>
+ * <pre>
+ *   { "state": "start" }
+ *   { "state": "stop"  }
+ * </pre>
+ */
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class NodeCommandRequestPayload
+{
+    /**
+     * Node Command State
+     */
+    public enum State
+    {
+        @JsonProperty("start")
+        START,
+
+        @JsonProperty("stop")
+        STOP;
+
+        @JsonCreator
+        public static State fromString(String s)
+        {
+            if (s == null)
+                throw new IllegalArgumentException("Null state");
+
+            switch (s.trim().toLowerCase())
+            {
+                case "start":
+                    return START;
+                case "stop":
+                    return STOP;
+                default:
+                    throw new IllegalArgumentException("Unknown state: " + s);
+            }
+        }
+
+        @JsonProperty
+        public String toValue()
+        {
+            return name().toLowerCase();
+        }
+    }
+
+    private final State state;
+
+    /**
+     * @param state the desired operation, must be "start" or "stop"
+     */
+    @JsonCreator
+    public NodeCommandRequestPayload(
+    @JsonProperty(value = "state", required = true) String state
+    )
+    {
+        Preconditions.checkArgument(StringUtils.isNotEmpty(state),
+                                    "state must be provided and non-empty");
+        this.state = State.fromString(state);
+    }
+
+    /**
+     * @return the parsed enum (START or STOP)
+     */
+    @JsonProperty("state")
+    public State state()
+    {
+        return state;
+    }
+
+    @Override
+    public String toString()
+    {
+        return "NodeCommandRequestPayload{state=" + state + '}';
+    }
+}
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java 
b/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
index 21ee1822..3909b74e 100644
--- 
a/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/RequestContext.java
@@ -36,9 +36,11 @@ import 
org.apache.cassandra.sidecar.common.request.ConnectedClientStatsRequest;
 import org.apache.cassandra.sidecar.common.request.CreateSnapshotRequest;
 import org.apache.cassandra.sidecar.common.request.GossipHealthRequest;
 import org.apache.cassandra.sidecar.common.request.GossipInfoRequest;
+import org.apache.cassandra.sidecar.common.request.GossipUpdateRequest;
 import org.apache.cassandra.sidecar.common.request.ImportSSTableRequest;
 import org.apache.cassandra.sidecar.common.request.ListOperationalJobsRequest;
 import org.apache.cassandra.sidecar.common.request.ListSnapshotFilesRequest;
+import org.apache.cassandra.sidecar.common.request.NativeUpdateRequest;
 import org.apache.cassandra.sidecar.common.request.NodeDecommissionRequest;
 import org.apache.cassandra.sidecar.common.request.NodeSettingsRequest;
 import org.apache.cassandra.sidecar.common.request.OperationalJobRequest;
@@ -54,8 +56,10 @@ import 
org.apache.cassandra.sidecar.common.request.TimeSkewRequest;
 import org.apache.cassandra.sidecar.common.request.TokenRangeReplicasRequest;
 import org.apache.cassandra.sidecar.common.request.UploadSSTableRequest;
 import org.apache.cassandra.sidecar.common.request.data.Digest;
+import 
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
 import org.apache.cassandra.sidecar.common.response.ListSnapshotFilesResponse;
 import org.apache.cassandra.sidecar.common.utils.HttpRange;
+import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
 /**
@@ -563,6 +567,30 @@ public class RequestContext
             return request(NODE_DECOMMISSION_REQUEST);
         }
 
+        /**
+         * Sets the {@code request} to be a {@link GossipUpdateRequest} for the
+         * given {@link NodeCommandRequestPayload.State state}, and returns a 
reference to this Builder enabling method chaining.
+         *
+         * @param state  the desired state for gossip
+         * @return a reference to this Builder
+         */
+        public Builder nodeGossipUpdateRequest(@NotNull 
NodeCommandRequestPayload.State state)
+        {
+            return request(new GossipUpdateRequest(state));
+        }
+
+        /**
+         * Sets the {@code request} to be a {@link NativeUpdateRequest} for the
+         * given {@link NodeCommandRequestPayload.State state}, and returns a 
reference to this Builder enabling method chaining.
+         *
+         * @param state  the desired state for native transport
+         * @return a reference to this Builder
+         */
+        public Builder nodeNativeUpdateRequest(@NotNull 
NodeCommandRequestPayload.State state)
+        {
+            return request(new NativeUpdateRequest(state));
+        }
+
         /**
          * Sets the {@code request} to be a {@link StreamStatsRequest} and 
returns a reference to this Builder
          * enabling method chaining.
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java 
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
index 56b3b31c..b022ef89 100644
--- 
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
@@ -54,6 +54,7 @@ import 
org.apache.cassandra.sidecar.common.request.data.AllServicesConfigPayload
 import 
org.apache.cassandra.sidecar.common.request.data.CreateRestoreJobRequestPayload;
 import 
org.apache.cassandra.sidecar.common.request.data.CreateSliceRequestPayload;
 import org.apache.cassandra.sidecar.common.request.data.Digest;
+import 
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
 import 
org.apache.cassandra.sidecar.common.request.data.RestoreJobProgressRequestParams;
 import 
org.apache.cassandra.sidecar.common.request.data.UpdateCdcServiceConfigPayload;
 import 
org.apache.cassandra.sidecar.common.request.data.UpdateRestoreJobRequestPayload;
@@ -802,6 +803,47 @@ public class SidecarClient implements AutoCloseable, 
SidecarClientBlobRestoreExt
                                             .build());
     }
 
+    /**
+     * Sends a request to start or stop Cassandra gossiping on the provided 
instance.
+     * <p>
+     * This operation asynchronously triggers start or stop of gossip via 
Cassandra's JMX interface.
+     * The request body must contain a JSON payload with a "state" field, 
which can be either "start" or "stop".
+     * On success, the server responds with HTTP 200 OK and payload {@code 
{"status":"OK"}}.
+     * </p>
+     *
+     * @param instance the instance where the request will be executed
+     * @param state the desired gossip state: {@link 
NodeCommandRequestPayload.State#START} or {@link 
NodeCommandRequestPayload.State#STOP}
+     * @return a CompletableFuture representing the completion of the operation
+     */
+    public CompletableFuture<HealthResponse> nodeUpdateGossip(SidecarInstance 
instance, NodeCommandRequestPayload.State state)
+    {
+        return executor.executeRequestAsync(requestBuilder()
+                                            
.singleInstanceSelectionPolicy(instance)
+                                            .nodeGossipUpdateRequest(state)
+                                            .build());
+    }
+
+    /**
+     * Sends a request to start or stop Cassandra native transport on the 
provided instance.
+     * <p>
+     * This operation asynchronously triggers start or stop of native 
transport via Cassandra's JMX interface.
+     * The request body must contain a JSON payload with a "state" field, 
which can be either "start" or "stop".
+     * On success, the server responds with HTTP 200 OK and payload {@code 
{"status":"OK"}}.
+     * </p>
+     *
+     * @param instance the instance where the request will be executed
+     * @param state the desired native transport state: {@link 
NodeCommandRequestPayload.State#START} or {@link 
NodeCommandRequestPayload.State#STOP}
+     * @return a CompletableFuture representing the completion of the operation
+     */
+    public CompletableFuture<HealthResponse> nodeUpdateNative(SidecarInstance 
instance, NodeCommandRequestPayload.State state)
+    {
+        return executor.executeRequestAsync(requestBuilder()
+                                            
.singleInstanceSelectionPolicy(instance)
+                                            .nodeNativeUpdateRequest(state)
+                                            .build());
+    }
+
+
     /**
      * Returns a copy of the request builder with the default parameters 
configured for the client.
      *
diff --git 
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
 
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
index ad709b91..ad888878 100644
--- 
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
+++ 
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/SidecarClientTest.java
@@ -72,6 +72,7 @@ import org.apache.cassandra.sidecar.common.request.Service;
 import 
org.apache.cassandra.sidecar.common.request.data.AllServicesConfigPayload;
 import 
org.apache.cassandra.sidecar.common.request.data.CreateRestoreJobRequestPayload;
 import org.apache.cassandra.sidecar.common.request.data.MD5Digest;
+import 
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
 import 
org.apache.cassandra.sidecar.common.request.data.UpdateCdcServiceConfigPayload;
 import org.apache.cassandra.sidecar.common.request.data.XXHash32Digest;
 import 
org.apache.cassandra.sidecar.common.response.ConnectedClientStatsResponse;
@@ -397,7 +398,7 @@ abstract class SidecarClientTest
         assertThat(gossipInfo.releaseVersion()).isEqualTo("4.0.7");
         
assertThat(gossipInfo.sstableVersions()).isEqualTo(Collections.singletonList("big-nb"));
 
-        validateResponseServed(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+        validateResponseServed(ApiEndpointsV1.GOSSIP_ROUTE);
     }
 
     @Test
@@ -1774,6 +1775,42 @@ abstract class SidecarClientTest
         }
     }
 
+    @Test
+    void testNodeUpdateGossip() throws Exception
+    {
+        MockResponse response = new 
MockResponse().setResponseCode(200).setBody("{\"status\":\"OK\"}");
+        enqueue(response);
+
+        SidecarInstanceImpl sidecarInstance = instances.get(0);
+        HealthResponse result = client.nodeUpdateGossip(sidecarInstance, 
NodeCommandRequestPayload.State.STOP).get(30, TimeUnit.SECONDS);
+        assertThat(result).isNotNull();
+        assertThat(result.status()).isEqualToIgnoringCase("OK");
+        assertThat(result.isOk()).isTrue();
+
+        validateResponseServed(ApiEndpointsV1.GOSSIP_ROUTE, request -> {
+            String requestBody = request.getBody().readUtf8();
+            assertThat(requestBody).isEqualTo("{\"state\":\"stop\"}");
+        });
+    }
+
+    @Test
+    void testNodeUpdateNative() throws Exception
+    {
+        MockResponse response = new 
MockResponse().setResponseCode(200).setBody("{\"status\":\"OK\"}");
+        enqueue(response);
+
+        SidecarInstanceImpl sidecarInstance = instances.get(0);
+        HealthResponse result = client.nodeUpdateNative(sidecarInstance, 
NodeCommandRequestPayload.State.STOP).get(30, TimeUnit.SECONDS);
+        assertThat(result).isNotNull();
+        assertThat(result.status()).isEqualToIgnoringCase("OK");
+        assertThat(result.isOk()).isTrue();
+
+        validateResponseServed(ApiEndpointsV1.CASSANDRA_NATIVE_ROUTE, request 
-> {
+            String requestBody = request.getBody().readUtf8();
+            assertThat(requestBody).isEqualTo("{\"state\":\"stop\"}");
+        });
+    }
+
     private void enqueue(MockResponse response)
     {
         for (MockWebServer server : servers)
diff --git 
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/request/GossipInfoRequestTestParameters.java
 
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/request/GossipInfoRequestTestParameters.java
index 9b4b89f4..604a6692 100644
--- 
a/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/request/GossipInfoRequestTestParameters.java
+++ 
b/client/src/testFixtures/java/org/apache/cassandra/sidecar/client/request/GossipInfoRequestTestParameters.java
@@ -61,7 +61,7 @@ public class GossipInfoRequestTestParameters implements 
RequestTestParameters<Go
     @Override
     public String expectedEndpointPath()
     {
-        return ApiEndpointsV1.GOSSIP_INFO_ROUTE;
+        return ApiEndpointsV1.GOSSIP_ROUTE;
     }
 
     @Override
diff --git 
a/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/NodeGossipAndNativeModifyIntegrationTest.java
 
b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/NodeGossipAndNativeModifyIntegrationTest.java
new file mode 100644
index 00000000..622c188f
--- /dev/null
+++ 
b/integration-tests/src/integrationTest/org/apache/cassandra/sidecar/routes/NodeGossipAndNativeModifyIntegrationTest.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.routes;
+
+import java.util.concurrent.TimeUnit;
+
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.junit.jupiter.api.Test;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.ext.web.client.HttpResponse;
+import 
org.apache.cassandra.sidecar.testing.SharedClusterSidecarIntegrationTestBase;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static 
io.netty.handler.codec.http.HttpResponseStatus.SERVICE_UNAVAILABLE;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Combines gossip‐toggle and native‐transport‐toggle route tests into a 
single suite
+ * so that Cassandra+Sidecar come up exactly once per JVM.
+ */
+public class NodeGossipAndNativeModifyIntegrationTest extends 
SharedClusterSidecarIntegrationTestBase
+{
+
+    @Test
+    void testGossipToggle()
+    {
+        // 1) STOP gossip
+        HttpResponse<Buffer> gossipStopResponse = getBlocking(
+        trustedClient()
+        .put(serverWrapper.serverPort, "localhost", "/api/v1/cassandra/gossip")
+        .sendBuffer(Buffer.buffer("{\"state\":\"stop\"}")));
+        assertThat(gossipStopResponse.statusCode()).isEqualTo(OK.code());
+        
assertThat(gossipStopResponse.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+
+        Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+
+        // 2) gossip should now be NOT_OK
+        HttpResponse<Buffer> gossipHealthDown = getBlocking(
+        trustedClient()
+        .get(serverWrapper.serverPort, "localhost", 
"/api/v1/cassandra/gossip/__health")
+        .send());
+        assertThat(gossipHealthDown.statusCode()).isEqualTo(OK.code());
+        
assertThat(gossipHealthDown.bodyAsJsonObject().getString("status")).isEqualTo("NOT_OK");
+
+        // 3) START gossip
+        HttpResponse<Buffer> startGossipResponse = getBlocking(
+        trustedClient()
+        .put(serverWrapper.serverPort, "localhost", "/api/v1/cassandra/gossip")
+        .sendBuffer(Buffer.buffer("{\"state\":\"start\"}")));
+        assertThat(startGossipResponse.statusCode()).isEqualTo(OK.code());
+        
assertThat(startGossipResponse.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+
+        // wait for native-transport to actually stop
+        Uninterruptibles.sleepUninterruptibly(1, TimeUnit.SECONDS);
+
+        // 4) gossip should now be OK
+        HttpResponse<Buffer> gossipHealthUp = getBlocking(
+        trustedClient()
+        .get(serverWrapper.serverPort, "localhost", 
"/api/v1/cassandra/gossip/__health")
+        .send());
+        assertThat(gossipHealthUp.statusCode()).isEqualTo(OK.code());
+        
assertThat(gossipHealthUp.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+    }
+
+    @Test
+    void testNativeTransportToggle()
+    {
+        // 1) STOP native
+        HttpResponse<Buffer> nativeStopResponse = getBlocking(
+        trustedClient()
+        .put(serverWrapper.serverPort, "localhost", "/api/v1/cassandra/native")
+        .sendBuffer(Buffer.buffer("{\"state\":\"stop\"}")));
+        assertThat(nativeStopResponse.statusCode()).isEqualTo(OK.code());
+        
assertThat(nativeStopResponse.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+
+        Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS);
+
+        // 2) native should now be NOT_OK
+        HttpResponse<Buffer> nativeHealthDown = getBlocking(
+        trustedClient()
+        .get(serverWrapper.serverPort, "localhost", 
"/api/v1/cassandra/native/__health")
+        .send());
+        
assertThat(nativeHealthDown.statusCode()).isEqualTo(SERVICE_UNAVAILABLE.code());
+        
assertThat(nativeHealthDown.bodyAsJsonObject().getString("status")).isEqualTo("NOT_OK");
+
+        // 3) START native
+        HttpResponse<Buffer> startNativeResponse = getBlocking(
+        trustedClient()
+        .put(serverWrapper.serverPort, "localhost", "/api/v1/cassandra/native")
+        .sendBuffer(Buffer.buffer("{\"state\":\"start\"}")));
+        assertThat(startNativeResponse.statusCode()).isEqualTo(OK.code());
+        
assertThat(startNativeResponse.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+
+        Uninterruptibles.sleepUninterruptibly(3, TimeUnit.SECONDS);
+
+        // 4) native should now be OK
+        HttpResponse<Buffer> nativeHealthUp = getBlocking(
+        trustedClient()
+        .get(serverWrapper.serverPort, "localhost", 
"/api/v1/cassandra/native/__health")
+        .send());
+        assertThat(nativeHealthUp.statusCode()).isEqualTo(OK.code());
+        
assertThat(nativeHealthUp.bodyAsJsonObject().getString("status")).isEqualTo("OK");
+    }
+
+    @Override
+    protected void initializeSchemaForTest()
+    {
+        // Do nothing
+    }
+}
diff --git 
a/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/StorageOperations.java
 
b/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/StorageOperations.java
index 58ed83ec..184c6818 100644
--- 
a/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/StorageOperations.java
+++ 
b/server-common/src/main/java/org/apache/cassandra/sidecar/common/server/StorageOperations.java
@@ -129,4 +129,24 @@ public interface StorageOperations
      * @return the name of the cluster
      */
     String clusterName();
+
+    /**
+     * Triggers stop native transport of the Cassandra node
+     */
+    void stopNativeTransport();
+
+    /**
+     * Triggers start native transport of the Cassandra node
+     */
+    void startNativeTransport();
+
+    /**
+     * Triggers stop gossip of the Cassandra node
+     */
+    void stopGossiping();
+
+    /**
+     * Triggers start gossip of the Cassandra node
+     */
+    void startGossiping();
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
index a76affc9..a9ec98bf 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/acl/authorization/BasicPermissions.java
@@ -70,9 +70,12 @@ public class BasicPermissions
     public static final Permission READ_SCHEMA = new 
DomainAwarePermission("SCHEMA:READ", CLUSTER_SCOPE);
     public static final Permission READ_SCHEMA_KEYSPACE_SCOPED = new 
DomainAwarePermission("SCHEMA:READ", KEYSPACE_SCOPE);
     public static final Permission READ_GOSSIP = new 
DomainAwarePermission("GOSSIP:READ", CLUSTER_SCOPE);
+    public static final Permission MODIFY_GOSSIP = new 
DomainAwarePermission("GOSSIP:MODIFY", CLUSTER_SCOPE);
     public static final Permission READ_RING = new 
DomainAwarePermission("RING:READ", CLUSTER_SCOPE);
     public static final Permission READ_RING_KEYSPACE_SCOPED = new 
DomainAwarePermission("RING:READ", KEYSPACE_SCOPE);
     public static final Permission READ_TOPOLOGY = new 
DomainAwarePermission("TOPOLOGY:READ", KEYSPACE_SCOPE);
+    public static final Permission MODIFY_NATIVE = new 
DomainAwarePermission("NATIVE:MODIFY", CLUSTER_SCOPE);
+
 
     // cassandra stats permissions
     public static final Permission STATS_CLUSTER_SCOPED = new 
StandardPermission("STATS", CLUSTER_SCOPE);
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/CassandraHealthHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/CassandraHealthHandler.java
index 39584b61..7beea488 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/CassandraHealthHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/CassandraHealthHandler.java
@@ -33,8 +33,8 @@ import 
org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 import org.jetbrains.annotations.NotNull;
 
 import static org.apache.cassandra.sidecar.common.ApiEndpointsV1.JMX;
-import static 
org.apache.cassandra.sidecar.modules.HealthCheckModule.NOT_OK_STATUS;
-import static org.apache.cassandra.sidecar.modules.HealthCheckModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.NOT_OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
 
 /**
  * Provides a simple REST endpoint to determine if a Cassandra node is 
available
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipHealthHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipHealthHandler.java
index f18d292f..726817f9 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipHealthHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipHealthHandler.java
@@ -27,8 +27,8 @@ import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 import org.jetbrains.annotations.NotNull;
 
-import static 
org.apache.cassandra.sidecar.modules.HealthCheckModule.NOT_OK_STATUS;
-import static org.apache.cassandra.sidecar.modules.HealthCheckModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.NOT_OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
 
 /**
  * Handler to retrieve gossip health
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandler.java
new file mode 100644
index 00000000..cebd0ab0
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandler.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+import java.util.Collections;
+import java.util.Set;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.auth.authorization.Authorization;
+import io.vertx.ext.web.RoutingContext;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import 
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
+
+/**
+ * Handles {@code PUT /api/v1/cassandra/gossip} requests to start or stop 
Cassandra gossip.
+ *
+ * <p>Expects a JSON payload:
+ * { "state": "start" } or { "state": "stop" }
+ * and will asynchronously invoke the corresponding JMX operation.</p>
+ */
+@Singleton
+public class GossipUpdateHandler extends NodeCommandHandler implements 
AccessProtected
+{
+    @Inject
+    public GossipUpdateHandler(InstanceMetadataFetcher metadataFetcher, 
ExecutorPools executorPools)
+    {
+        super(metadataFetcher, executorPools, null);
+    }
+
+    @Override
+    public Set<Authorization> requiredAuthorizations()
+    {
+        return 
Collections.singleton(BasicPermissions.MODIFY_GOSSIP.toAuthorization());
+    }
+
+    @Override
+    protected void handleInternal(RoutingContext context,
+                                  HttpServerRequest httpRequest,
+                                  @NotNull String host,
+                                  SocketAddress remoteAddress,
+                                  NodeCommandRequestPayload request)
+    {
+        StorageOperations storageOps = 
metadataFetcher.delegate(host).storageOperations();
+
+        executorPools.service().runBlocking(() -> {
+            switch (request.state())
+            {
+                case START:
+                    storageOps.startGossiping();
+                    break;
+                case STOP:
+                    storageOps.stopGossiping();
+                    break;
+                default:
+                    throw new IllegalStateException("Unknown state: " + 
request.state());
+            }
+        })
+                     .onSuccess(ignored -> context.json(OK_STATUS))
+                     .onFailure(cause -> processFailure(cause, context, host, 
remoteAddress, request));
+    }
+}
+
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandler.java
new file mode 100644
index 00000000..bf43b3d3
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandler.java
@@ -0,0 +1,88 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+import java.util.Collections;
+import java.util.Set;
+
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import io.vertx.core.http.HttpServerRequest;
+import io.vertx.core.net.SocketAddress;
+import io.vertx.ext.auth.authorization.Authorization;
+import io.vertx.ext.web.RoutingContext;
+import org.apache.cassandra.sidecar.acl.authorization.BasicPermissions;
+import 
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+import org.jetbrains.annotations.NotNull;
+
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
+
+
+/**
+ * Handler for starting or stopping native transport on a Cassandra node.
+ * <p>
+ * Expects a JSON body:
+ * { "state": "start" }  or  { "state": "stop" }
+ * </p>
+ */
+@Singleton
+public class NativeUpdateHandler extends NodeCommandHandler implements 
AccessProtected
+{
+    @Inject
+    public NativeUpdateHandler(InstanceMetadataFetcher metadataFetcher, 
ExecutorPools executorPools)
+    {
+        super(metadataFetcher, executorPools, null);
+    }
+
+    @Override
+    public Set<Authorization> requiredAuthorizations()
+    {
+        return 
Collections.singleton(BasicPermissions.MODIFY_NATIVE.toAuthorization());
+    }
+
+    @Override
+    protected void handleInternal(RoutingContext context,
+                                  HttpServerRequest httpRequest,
+                                  @NotNull String host,
+                                  SocketAddress remoteAddress,
+                                  NodeCommandRequestPayload request)
+    {
+        StorageOperations storageOps = 
metadataFetcher.delegate(host).storageOperations();
+
+        executorPools.service().runBlocking(() -> {
+            switch (request.state())
+            {
+                case START:
+                    storageOps.startNativeTransport();
+                    break;
+                case STOP:
+                    storageOps.stopNativeTransport();
+                    break;
+                default:
+                    throw new IllegalStateException("Unknown state: " + 
request.state());
+            }
+        })
+                     .onSuccess(ignored -> context.json(OK_STATUS))
+                     .onFailure(cause -> processFailure(cause, context, host, 
remoteAddress, request));
+    }
+}
+
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeCommandHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeCommandHandler.java
new file mode 100644
index 00000000..c1bce4ef
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/NodeCommandHandler.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.json.DecodeException;
+import io.vertx.core.json.Json;
+import io.vertx.ext.web.RoutingContext;
+import 
org.apache.cassandra.sidecar.common.request.data.NodeCommandRequestPayload;
+import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
+import org.apache.cassandra.sidecar.utils.CassandraInputValidator;
+import org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
+
+import static 
org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException;
+
+/**
+ * Base handler for "node command" endpoints (e.g. start/stop gossip or native 
transport).
+ * Extracts a {@link NodeCommandRequestPayload} from the request body, 
validates it,
+ * and defers to subclasses in {@link #handleInternal} to perform the actual 
operation.
+ */
+public abstract class NodeCommandHandler extends 
AbstractHandler<NodeCommandRequestPayload>
+{
+    /**
+     * Constructs a handler with the provided {@code metadataFetcher}
+     *
+     * @param metadataFetcher the interface to retrieve instance metadata
+     * @param executorPools   the executor pools for blocking executions
+     * @param validator       a validator instance to validate 
Cassandra-specific input
+     */
+    protected NodeCommandHandler(InstanceMetadataFetcher metadataFetcher, 
ExecutorPools executorPools, CassandraInputValidator validator)
+    {
+        super(metadataFetcher, executorPools, validator);
+    }
+
+    /**
+     * extract node command from RoutingContext
+     *
+     * @param ctx the request context
+     * @return parsed NodeCommandRequestPayload
+     */
+    @Override
+    protected NodeCommandRequestPayload extractParamsOrThrow(RoutingContext 
ctx)
+    {
+        String body = ctx.body().asString();
+        if (body == null || body.equalsIgnoreCase("null"))
+        {
+            logger.warn("Bad request. Received null payload.");
+            throw wrapHttpException(HttpResponseStatus.BAD_REQUEST, "Request 
body must be JSON with a non-null \"state\" field");
+        }
+        try
+        {
+            return Json.decodeValue(body, NodeCommandRequestPayload.class);
+        }
+        catch (DecodeException e)
+        {
+            logger.warn("Bad request. Received invalid JSON payload.");
+            throw wrapHttpException(HttpResponseStatus.BAD_REQUEST, "Invalid 
state value: " + e.getMessage());
+        }
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/ReportSchemaHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/ReportSchemaHandler.java
index 7a009b38..614e7353 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/ReportSchemaHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/ReportSchemaHandler.java
@@ -35,7 +35,7 @@ import 
org.apache.cassandra.sidecar.utils.InstanceMetadataFetcher;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
 
-import static org.apache.cassandra.sidecar.modules.HealthCheckModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
 
 /**
  * An implementation of {@link AbstractHandler} used to trigger an immediate,
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/cdc/DeleteServiceConfigHandler.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/cdc/DeleteServiceConfigHandler.java
index 5180c4f5..deb1a117 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/handlers/cdc/DeleteServiceConfigHandler.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/handlers/cdc/DeleteServiceConfigHandler.java
@@ -33,7 +33,7 @@ import org.apache.cassandra.sidecar.db.ConfigAccessor;
 import org.apache.cassandra.sidecar.db.ConfigAccessorFactory;
 import org.apache.cassandra.sidecar.handlers.AccessProtected;
 
-import static org.apache.cassandra.sidecar.modules.HealthCheckModule.OK_STATUS;
+import static org.apache.cassandra.sidecar.modules.ApiModule.OK_STATUS;
 import static 
org.apache.cassandra.sidecar.utils.HttpExceptions.wrapHttpException;
 
 /**
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/ApiModule.java 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/ApiModule.java
index 291d75f2..4472718d 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/ApiModule.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/ApiModule.java
@@ -18,6 +18,9 @@
 
 package org.apache.cassandra.sidecar.modules;
 
+import java.util.Collections;
+import java.util.Map;
+
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
@@ -59,6 +62,12 @@ import static 
org.apache.cassandra.sidecar.common.ApiEndpointsV1.API_V1_ALL_ROUT
  */
 public class ApiModule extends AbstractModule
 {
+    /**
+     * Basic response body
+     */
+    public static final Map<String, String> OK_STATUS = 
Collections.singletonMap("status", "OK");
+    public static final Map<String, String> NOT_OK_STATUS = 
Collections.singletonMap("status", "NOT_OK");
+
     @Provides
     @Singleton
     Router vertxRouter(Vertx vertx, MultiBindingTypeResolver<VertxRoute> 
resolver)
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
index 4c48238e..29f69214 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/CassandraOperationsModule.java
@@ -22,9 +22,11 @@ import com.google.inject.AbstractModule;
 import com.google.inject.multibindings.ProvidesIntoMap;
 import org.apache.cassandra.sidecar.handlers.ConnectedClientStatsHandler;
 import org.apache.cassandra.sidecar.handlers.GossipInfoHandler;
+import org.apache.cassandra.sidecar.handlers.GossipUpdateHandler;
 import org.apache.cassandra.sidecar.handlers.KeyspaceRingHandler;
 import org.apache.cassandra.sidecar.handlers.KeyspaceSchemaHandler;
 import org.apache.cassandra.sidecar.handlers.ListOperationalJobsHandler;
+import org.apache.cassandra.sidecar.handlers.NativeUpdateHandler;
 import org.apache.cassandra.sidecar.handlers.NodeDecommissionHandler;
 import org.apache.cassandra.sidecar.handlers.OperationalJobHandler;
 import org.apache.cassandra.sidecar.handlers.RingHandler;
@@ -173,4 +175,26 @@ public class CassandraOperationsModule extends 
AbstractModule
                       .handler(validateTableExistenceHandler)
                       .handler(tableStatsHandler).build();
     }
+
+    @ProvidesIntoMap
+    @KeyClassMapKey(VertxRouteMapKeys.UpdateNodeGossipStateRouteKey.class)
+    VertxRoute cassandraChangeGossipStateRoute(RouteBuilder.Factory factory,
+                                    GossipUpdateHandler nodeGossipHandler)
+    {
+        return factory.builderForRoute()
+                      .setBodyHandler(true)
+                      .handler(nodeGossipHandler)
+                      .build();
+    }
+
+    @ProvidesIntoMap
+    @KeyClassMapKey(VertxRouteMapKeys.UpdateNodeNativeStateRouteKey.class)
+    VertxRoute cassandraChangeNativeStateRoute(RouteBuilder.Factory factory,
+                                    NativeUpdateHandler nodeNativeHandler)
+    {
+        return factory.builderForRoute()
+                      .setBodyHandler(true)
+                      .handler(nodeNativeHandler)
+                      .build();
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java
index 91a74c17..8e8e64fc 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/HealthCheckModule.java
@@ -18,9 +18,6 @@
 
 package org.apache.cassandra.sidecar.modules;
 
-import java.util.Collections;
-import java.util.Map;
-
 import com.google.inject.AbstractModule;
 import com.google.inject.multibindings.ProvidesIntoMap;
 import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
@@ -42,9 +39,6 @@ import org.apache.cassandra.sidecar.tasks.PeriodicTask;
  */
 public class HealthCheckModule extends AbstractModule
 {
-    public static final Map<String, String> OK_STATUS = 
Collections.singletonMap("status", "OK");
-    public static final Map<String, String> NOT_OK_STATUS = 
Collections.singletonMap("status", "NOT_OK");
-
     @ProvidesIntoMap
     @KeyClassMapKey(PeriodicTaskMapKeys.HealthCheckPeriodicTaskKey.class)
     PeriodicTask healthCheckPeriodicTask(SidecarConfiguration configuration,
@@ -60,7 +54,7 @@ public class HealthCheckModule extends AbstractModule
     VertxRoute sidecarHealthRoute(RouteBuilder.Factory factory)
     {
         return factory.builderForUnauthorizedRoute()
-                      .handler(context -> context.json(OK_STATUS))
+                      .handler(context -> context.json(ApiModule.OK_STATUS))
                       .build();
     }
 
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
index 3a4e7431..1706cc92 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/VertxRouteMapKeys.java
@@ -15,7 +15,6 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
 package org.apache.cassandra.sidecar.modules.multibindings;
 
 import io.vertx.core.http.HttpMethod;
@@ -56,7 +55,7 @@ public interface VertxRouteMapKeys
     interface CassandraGossipInfoRouteKey extends RouteClassKey
     {
         HttpMethod HTTP_METHOD = HttpMethod.GET;
-        String ROUTE_URI = ApiEndpointsV1.GOSSIP_INFO_ROUTE;
+        String ROUTE_URI = ApiEndpointsV1.GOSSIP_ROUTE;
     }
     interface CassandraHealthRouteKey extends RouteClassKey
     {
@@ -243,6 +242,16 @@ public interface VertxRouteMapKeys
         HttpMethod HTTP_METHOD = HttpMethod.GET;
         String ROUTE_URI = ApiEndpointsV1.TIME_SKEW_ROUTE;
     }
+    interface UpdateNodeGossipStateRouteKey extends RouteClassKey
+    {
+        HttpMethod HTTP_METHOD = HttpMethod.PUT;
+        String    ROUTE_URI   = ApiEndpointsV1.GOSSIP_ROUTE;
+    }
+    interface UpdateNodeNativeStateRouteKey extends RouteClassKey
+    {
+        HttpMethod HTTP_METHOD = HttpMethod.PUT;
+        String    ROUTE_URI   = ApiEndpointsV1.CASSANDRA_NATIVE_ROUTE;
+    }
     interface UpdateRestoreJobRouteKey extends RouteClassKey
     {
         HttpMethod HTTP_METHOD = HttpMethod.PATCH;
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandlerTest.java
new file mode 100644
index 00000000..da8abd47
--- /dev/null
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/GossipUpdateHandlerTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.util.Modules;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.vertx.core.buffer.Buffer.buffer;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link GossipUpdateHandler}
+ */
+@ExtendWith(VertxExtension.class)
+public class GossipUpdateHandlerTest
+{
+    Vertx vertx;
+    Server server;
+    StorageOperations mockStorageOperations = mock(StorageOperations.class);
+
+    @BeforeEach
+    void before() throws InterruptedException
+    {
+        Injector injector;
+        Module testOverride = Modules.override(new TestModule()).with(new 
GossipUpdateHandlerTest.GossipUpdateHandlerTestModule());
+        injector = 
Guice.createInjector(Modules.override(SidecarModules.all()).with(testOverride));
+        vertx = injector.getInstance(Vertx.class);
+        server = injector.getInstance(Server.class);
+        VertxTestContext context = new VertxTestContext();
+        server.start().onSuccess(s -> 
context.completeNow()).onFailure(context::failNow);
+        context.awaitCompletion(5, TimeUnit.SECONDS);
+    }
+
+    @AfterEach
+    void after() throws InterruptedException
+    {
+        getBlocking(TestResourceReaper.create().with(server).close(), 60, 
TimeUnit.SECONDS, "Closing server");
+    }
+
+    @Test
+    void testStartGossip(VertxTestContext ctx)
+    {
+        WebClient client = WebClient.create(vertx);
+        String payload = "{\"state\":\"start\"}";
+        client.put(server.actualPort(), "127.0.0.1", 
"/api/v1/cassandra/gossip")
+              .sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+                  ctx.verify(() -> {
+                      verify(mockStorageOperations, times(1)).startGossiping();
+
+                      assertThat(resp.statusCode()).isEqualTo(OK.code());
+                      JsonObject json = resp.bodyAsJsonObject();
+                      assertThat(json.getMap().get("status")).isEqualTo("OK");
+                  });
+                  ctx.completeNow();
+              }));
+    }
+
+    @Test
+    void testStopGossip(VertxTestContext ctx)
+    {
+        WebClient client = WebClient.create(vertx);
+        String payload = "{\"state\":\"STOP\"}";
+        client.put(server.actualPort(), "127.0.0.1", 
"/api/v1/cassandra/gossip")
+              .sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+                  ctx.verify(() -> {
+                      verify(mockStorageOperations, times(1)).stopGossiping();
+
+                      assertThat(resp.statusCode()).isEqualTo(OK.code());
+                      JsonObject json = resp.bodyAsJsonObject();
+                      assertThat(json.getMap().get("status")).isEqualTo("OK");
+                  });
+                  ctx.completeNow();
+              }));
+    }
+
+    @Test
+    void testInvalidState(VertxTestContext ctx)
+    {
+        WebClient client = WebClient.create(vertx);
+        String payload = "{\"state\":\"foo\"}";
+        client.put(server.actualPort(), "127.0.0.1", 
"/api/v1/cassandra/gossip")
+              .sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+                  ctx.verify(() -> {
+                      
assertThat(resp.statusCode()).isEqualTo(BAD_REQUEST.code());
+                      verify(mockStorageOperations, times(0)).startGossiping();
+                      verify(mockStorageOperations, times(0)).stopGossiping();
+                  });
+                  ctx.completeNow();
+              }));
+    }
+
+
+    /**
+     * Test guice module for {@link GossipUpdateHandler} tests
+     */
+    class GossipUpdateHandlerTestModule extends AbstractModule
+    {
+        @Provides
+        @Singleton
+        public InstancesMetadata instanceMetadata()
+        {
+            final int instanceId = 100;
+            final String host = "127.0.0.1";
+            final InstanceMetadata instanceMetadata = 
mock(InstanceMetadata.class);
+            when(instanceMetadata.host()).thenReturn(host);
+            when(instanceMetadata.port()).thenReturn(9042);
+            when(instanceMetadata.id()).thenReturn(instanceId);
+            when(instanceMetadata.stagingDir()).thenReturn("");
+
+            CassandraAdapterDelegate delegate = 
mock(CassandraAdapterDelegate.class);
+
+            
when(delegate.storageOperations()).thenReturn(mockStorageOperations);
+            when(instanceMetadata.delegate()).thenReturn(delegate);
+
+            InstancesMetadata mockInstancesMetadata = 
mock(InstancesMetadata.class);
+            
when(mockInstancesMetadata.instances()).thenReturn(Collections.singletonList(instanceMetadata));
+            
when(mockInstancesMetadata.instanceFromId(instanceId)).thenReturn(instanceMetadata);
+            
when(mockInstancesMetadata.instanceFromHost(host)).thenReturn(instanceMetadata);
+
+            return mockInstancesMetadata;
+        }
+    }
+}
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandlerTest.java
new file mode 100644
index 00000000..b84573bb
--- /dev/null
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/NativeUpdateHandlerTest.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.handlers;
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+import com.google.inject.util.Modules;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.TestResourceReaper;
+import org.apache.cassandra.sidecar.cluster.CassandraAdapterDelegate;
+import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
+import org.apache.cassandra.sidecar.cluster.instance.InstanceMetadata;
+import org.apache.cassandra.sidecar.common.server.StorageOperations;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+
+import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
+import static io.netty.handler.codec.http.HttpResponseStatus.OK;
+import static io.vertx.core.buffer.Buffer.buffer;
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * Test for {@link NativeUpdateHandler}
+ */
+@ExtendWith(VertxExtension.class)
+public class NativeUpdateHandlerTest
+{
+    Vertx vertx;
+    Server server;
+    StorageOperations mockStorageOperations = mock(StorageOperations.class);
+
+    @BeforeEach
+    void before() throws InterruptedException
+    {
+        Injector injector;
+        Module testOverride = Modules.override(new TestModule()).with(new 
NativeUpdateHandlerTest.NativeUpdateHandlerTestModule());
+        injector = 
Guice.createInjector(Modules.override(SidecarModules.all()).with(testOverride));
+        vertx = injector.getInstance(Vertx.class);
+        server = injector.getInstance(Server.class);
+        VertxTestContext context = new VertxTestContext();
+        server.start().onSuccess(s -> 
context.completeNow()).onFailure(context::failNow);
+        context.awaitCompletion(5, TimeUnit.SECONDS);
+    }
+
+    @AfterEach
+    void after() throws InterruptedException
+    {
+        getBlocking(TestResourceReaper.create().with(server).close(), 60, 
TimeUnit.SECONDS, "Closing server");
+    }
+
+    @Test
+    void testStartNative(VertxTestContext ctx)
+    {
+        WebClient client = WebClient.create(vertx);
+        String payload = "{\"state\":\"start\"}";
+        client.put(server.actualPort(), "127.0.0.1", 
"/api/v1/cassandra/native").sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+            ctx.verify(() -> {
+                verify(mockStorageOperations, times(1)).startNativeTransport();
+
+                assertThat(resp.statusCode()).isEqualTo(OK.code());
+                JsonObject json = resp.bodyAsJsonObject();
+                assertThat(json.getMap().get("status")).isEqualTo("OK");
+            });
+            ctx.completeNow();
+        }));
+    }
+
+    @Test
+    void testStopNative(VertxTestContext ctx)
+    {
+        WebClient client = WebClient.create(vertx);
+        String payload = "{\"state\":\"stop\"}";
+        client.put(server.actualPort(), "127.0.0.1", 
"/api/v1/cassandra/native").sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+            ctx.verify(() -> {
+                verify(mockStorageOperations, times(1)).stopNativeTransport();
+
+                assertThat(resp.statusCode()).isEqualTo(OK.code());
+                JsonObject json = resp.bodyAsJsonObject();
+                assertThat(json.getMap().get("status")).isEqualTo("OK");
+            });
+            ctx.completeNow();
+        }));
+    }
+
+    @Test
+    void testInvalidState(VertxTestContext ctx)
+    {
+        WebClient client = WebClient.create(vertx);
+        String payload = "{\"state\":\"foo\"}";
+        client.put(server.actualPort(), "127.0.0.1", 
"/api/v1/cassandra/native").sendBuffer(buffer(payload), ctx.succeeding(resp -> {
+            ctx.verify(() -> {
+                assertThat(resp.statusCode()).isEqualTo(BAD_REQUEST.code());
+                verify(mockStorageOperations, times(0)).startNativeTransport();
+                verify(mockStorageOperations, times(0)).stopNativeTransport();
+            });
+            ctx.completeNow();
+        }));
+    }
+
+
+    /**
+     * Test guice module for {@link NativeUpdateHandler}
+     */
+    class NativeUpdateHandlerTestModule extends AbstractModule
+    {
+        @Provides
+        @Singleton
+        public InstancesMetadata instanceMetadata()
+        {
+            int instanceId = 100;
+            String host = "127.0.0.1";
+            InstanceMetadata instanceMetadata = mock(InstanceMetadata.class);
+            when(instanceMetadata.host()).thenReturn(host);
+            when(instanceMetadata.port()).thenReturn(9042);
+            when(instanceMetadata.id()).thenReturn(instanceId);
+            when(instanceMetadata.stagingDir()).thenReturn("");
+
+            CassandraAdapterDelegate delegate = 
mock(CassandraAdapterDelegate.class);
+
+            
when(delegate.storageOperations()).thenReturn(mockStorageOperations);
+            when(instanceMetadata.delegate()).thenReturn(delegate);
+
+            InstancesMetadata mockInstancesMetadata = 
mock(InstancesMetadata.class);
+            
when(mockInstancesMetadata.instances()).thenReturn(Collections.singletonList(instanceMetadata));
+            
when(mockInstancesMetadata.instanceFromId(instanceId)).thenReturn(instanceMetadata);
+            
when(mockInstancesMetadata.instanceFromHost(host)).thenReturn(instanceMetadata);
+
+            return mockInstancesMetadata;
+        }
+    }
+}
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/RouteBuilderTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/RouteBuilderTest.java
index f3f081a5..f8aeb3a0 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/handlers/RouteBuilderTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/handlers/RouteBuilderTest.java
@@ -96,7 +96,7 @@ class RouteBuilderTest
                   Factory::builderForUnauthorizedRoute,
                   route -> {
                       route.setHttpMethod(HttpMethod.GET);
-                      route.setRouteURI(ApiEndpointsV1.GOSSIP_INFO_ROUTE);
+                      route.setRouteURI(ApiEndpointsV1.GOSSIP_ROUTE);
                       assertThatThrownBy(() -> route.mountTo(mockRouter))
                       .isInstanceOf(ConfigurationException.class)
                       .hasMessage("Unauthorized route must not have required 
authorizations declared");


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to