You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by sd...@apache.org on 2022/07/26 07:51:20 UTC

[ignite-3] branch main updated: IGNITE-17092: Implement topology command

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

sdanilov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/ignite-3.git


The following commit(s) were added to refs/heads/main by this push:
     new 7fffd484b IGNITE-17092: Implement topology command
7fffd484b is described below

commit 7fffd484b3d55ac7428d970e051bba1fd9d92e20
Author: Aleksandr Pakhomov <ap...@gmail.com>
AuthorDate: Thu Jun 23 16:53:34 2022 +0300

    IGNITE-17092: Implement topology command
    
    New commands are added: `ignite cluster topology physical`
    and `ignite cluster topology logical`. To support these
    commands REST endpoints added as well.
    
    Also, added REST integration tests to runner and cli modules.
---
 .../org/apache/ignite/cli/IntegrationTestBase.java |  20 +--
 ...ava => CallInitializedIntegrationTestBase.java} |  21 ++-
 .../configuration/ItShowConfigurationCallTest.java |   4 +-
 .../ItUpdateConfigurationCallTest.java             |   4 +-
 ... CliCommandTestInitializedIntegrationBase.java} |  34 ++--
 ...iCommandTestNotInitializedIntegrationBase.java} |  35 +++-
 ...ItClusterStatusReplCommandInitializedTest.java} |  28 ++-
 ...lusterStatusReplCommandNotInitializedTest.java} |  15 +-
 .../ItTopologyCommandInitializedClusterTest.java}  |  33 ++--
 ...tTopologyCommandNotInitializedClusterTest.java} |  34 ++--
 .../configuration/ItConfigCommandTest.java         |   4 +-
 .../cli/commands/connect/ItConnectCommandTest.java |   4 +-
 .../ignite/cli/commands/sql/ItSqlCommandTest.java  |   4 +-
 .../cli/commands/version/ItVersionCommandTest.java |   4 +-
 .../ignite/rest/ItGeneratedRestClientTest.java     |  28 ++-
 .../cli/call/cluster/status/ClusterStatusCall.java |  29 ++--
 .../call/cluster/topology/LogicalTopologyCall.java |  49 ++++++
 .../cluster/topology/PhysicalTopologyCall.java     |  49 ++++++
 .../call/cluster/topology/TopologyCallInput.java   |  67 ++++++++
 .../call/cluster/topology/TopologyCallOutput.java  |  70 ++++++++
 .../cli/commands/cluster/ClusterCommand.java       |   6 +-
 .../cli/commands/cluster/ClusterReplCommand.java   |   4 +-
 .../cli/commands/decorators/TopologyDecorator.java |  51 ++++++
 .../node/status/NodeStatusReplSubCommand.java      |   2 +-
 .../commands/node/status/NodeStatusSubCommand.java |   2 +-
 .../LogicalTopologyReplSubCommand.java}            |  48 +++---
 .../LogicalTopologySubCommand.java}                |  31 ++--
 .../PhysicalTopologyReplSubCommand.java}           |  46 +++--
 .../PhysicalTopologySubCommand.java}               |  31 ++--
 .../cli/commands/topology/TopologyCommand.java     |  16 +-
 .../commands/topology/TopologyReplCommand.java}    |  20 ++-
 .../rest/ItClusterManagementControllerTest.java    |  32 +---
 .../cluster/management/rest/RestTestBase.java      |  94 ++++++++++
 .../rest/ClusterManagementController.java          |   3 +-
 .../rest/ClusterManagementRestFactory.java         |  18 +-
 .../management/rest/TopologyController.java        |  76 ++++++++
 .../rest/api/cluster/ClusterManagementApi.java     |  28 ++-
 .../internal/rest/api/cluster/ClusterNodeDto.java  |  86 ++++++++++
 .../rest/api/cluster/NetworkAddressDto.java        |  89 ++++++++++
 .../internal/rest/api/cluster/TopologyApi.java     |  60 +++++++
 .../api/configuration/ClusterConfigurationApi.java |  47 +++--
 .../api/configuration/NodeConfigurationApi.java    |  52 +++---
 modules/rest/openapi/openapi.yaml                  |  70 ++++++++
 .../apache/ignite/internal/rest/RestComponent.java |   4 +-
 modules/runner/pom.xml                             |   6 +
 .../ignite/internal/rest/AbstractRestTestBase.java | 173 +++++++++++++++++++
 .../rest/ItInitializedClusterRestTest.java         | 191 +++++++++++++++++++++
 .../rest/ItNotInitializedClusterRestTest.java      | 157 +++++++++++++++++
 parent/pom.xml                                     |   6 +
 49 files changed, 1642 insertions(+), 343 deletions(-)

diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/IntegrationTestBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/IntegrationTestBase.java
index 5dc36c0e1..13f036e3b 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/IntegrationTestBase.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/IntegrationTestBase.java
@@ -57,10 +57,7 @@ import org.apache.ignite.schema.definition.builder.TableDefinitionBuilder;
 import org.apache.ignite.table.RecordView;
 import org.apache.ignite.table.Table;
 import org.apache.ignite.table.Tuple;
-import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeAll;
-import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.TestInfo;
 import org.junit.jupiter.api.TestInstance;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -80,6 +77,9 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest {
     /** Cluster nodes. */
     protected static final List<Ignite> CLUSTER_NODES = new ArrayList<>();
 
+    /** Futures that is going to be completed when all nodes are started and the cluster is inintialized. */
+    private static List<CompletableFuture<Ignite>> futures = new ArrayList<>();
+
     private static final IgniteLogger LOG = Loggers.forClass(IntegrationTestBase.class);
 
     /** Base port number. */
@@ -239,11 +239,10 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest {
      *
      * @param testInfo Test information oject.
      */
-    @BeforeAll
-    void startNodes(TestInfo testInfo) throws ExecutionException, InterruptedException {
+    protected void startNodes(TestInfo testInfo) throws ExecutionException, InterruptedException {
         String connectNodeAddr = "\"localhost:" + BASE_PORT + '\"';
 
-        List<CompletableFuture<Ignite>> futures = IntStream.range(0, nodes())
+        futures = IntStream.range(0, nodes())
                 .mapToObj(i -> {
                     String nodeName = testNodeName(testInfo, i);
 
@@ -252,9 +251,9 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest {
                     return IgnitionManager.start(nodeName, config, WORK_DIR.resolve(nodeName));
                 })
                 .collect(toList());
+    }
 
-        String metaStorageNodeName = testNodeName(testInfo, 0);
-
+    protected void initializeCluster(String metaStorageNodeName) {
         IgnitionManager.init(metaStorageNodeName, List.of(metaStorageNodeName), "cluster");
 
         for (CompletableFuture<Ignite> future : futures) {
@@ -276,8 +275,7 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest {
     /**
      * After all.
      */
-    @AfterAll
-    void stopNodes(TestInfo testInfo) throws Exception {
+    protected void stopNodes(TestInfo testInfo) throws Exception {
         LOG.info("Start tearDown()");
 
         CLUSTER_NODES.clear();
@@ -326,7 +324,6 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest {
      * @param testInfo Test information object.
      * @throws Exception If failed.
      */
-    @BeforeEach
     public void setUp(TestInfo testInfo) throws Exception {
         setupBase(testInfo, WORK_DIR);
     }
@@ -339,7 +336,6 @@ public class IntegrationTestBase extends BaseIgniteAbstractTest {
     @AfterEach
     public void tearDown(TestInfo testInfo) {
         tearDownBase(testInfo);
-        dropAllTables();
     }
 }
 
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallIntegrationTestBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallInitializedIntegrationTestBase.java
similarity index 52%
copy from modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallIntegrationTestBase.java
copy to modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallInitializedIntegrationTestBase.java
index d5c0f37b3..483246698 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallIntegrationTestBase.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallInitializedIntegrationTestBase.java
@@ -17,10 +17,27 @@
 
 package org.apache.ignite.cli.call;
 
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+
+import java.util.concurrent.ExecutionException;
 import org.apache.ignite.cli.IntegrationTestBase;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestInfo;
 
 /**
- * Base class for call integration tests. Contains common methods and useful assertions.
+ * Base class for call integration tests that needs initialized ignite cluster. Contains common methods and useful assertions.
  */
-public class CallIntegrationTestBase extends IntegrationTestBase {
+public class CallInitializedIntegrationTestBase extends IntegrationTestBase {
+    @BeforeAll
+    void beforeAll(TestInfo testInfo) throws ExecutionException, InterruptedException {
+        startNodes(testInfo);
+        String metaStorageNodeName = testNodeName(testInfo, 0);
+        initializeCluster(metaStorageNodeName);
+    }
+
+    @AfterAll
+    void afterAll(TestInfo testInfo) throws Exception {
+        stopNodes(testInfo);
+    }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItShowConfigurationCallTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItShowConfigurationCallTest.java
index a19f5182c..3b6f67ff9 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItShowConfigurationCallTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItShowConfigurationCallTest.java
@@ -20,7 +20,7 @@ package org.apache.ignite.cli.call.configuration;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import jakarta.inject.Inject;
-import org.apache.ignite.cli.call.CallIntegrationTestBase;
+import org.apache.ignite.cli.call.CallInitializedIntegrationTestBase;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
@@ -28,7 +28,7 @@ import org.junit.jupiter.api.Test;
 /**
  * Tests for {@link NodeConfigShowCall}.
  */
-class ItShowConfigurationCallTest extends CallIntegrationTestBase {
+class ItShowConfigurationCallTest extends CallInitializedIntegrationTestBase {
 
     @Inject
     NodeConfigShowCall nodeConfigShowCall;
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItUpdateConfigurationCallTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItUpdateConfigurationCallTest.java
index 46055736b..cc5f8cc8d 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItUpdateConfigurationCallTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/configuration/ItUpdateConfigurationCallTest.java
@@ -20,7 +20,7 @@ package org.apache.ignite.cli.call.configuration;
 import static org.assertj.core.api.Assertions.assertThat;
 
 import jakarta.inject.Inject;
-import org.apache.ignite.cli.call.CallIntegrationTestBase;
+import org.apache.ignite.cli.call.CallInitializedIntegrationTestBase;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
@@ -28,7 +28,7 @@ import org.junit.jupiter.api.Test;
 /**
  * Tests for {@link NodeConfigUpdateCall}.
  */
-public class ItUpdateConfigurationCallTest extends CallIntegrationTestBase {
+public class ItUpdateConfigurationCallTest extends CallInitializedIntegrationTestBase {
 
     @Inject
     ClusterConfigUpdateCall updateCall;
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/version/ItVersionCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestInitializedIntegrationBase.java
similarity index 51%
copy from modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/version/ItVersionCommandTest.java
copy to modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestInitializedIntegrationBase.java
index 99ebea901..0933f75ba 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/version/ItVersionCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestInitializedIntegrationBase.java
@@ -15,27 +15,27 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.version;
+package org.apache.ignite.cli.commands;
 
-import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
 
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
+import java.util.concurrent.ExecutionException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestInfo;
 
-class ItVersionCommandTest extends CliCommandTestIntegrationBase {
+/**
+ * Extends {@link CliCommandTestNotInitializedIntegrationBase} and initializes ignite cluster.
+ */
+public class CliCommandTestInitializedIntegrationBase extends CliCommandTestNotInitializedIntegrationBase {
 
-    @Test
-    @DisplayName("Should print cli version that is got from pom.xml")
-    void printVersion() {
-        // When
-        execute("--version");
+    @BeforeAll
+    @Override
+    void beforeAll(TestInfo testInfo) throws ExecutionException, InterruptedException {
+        startNodes(testInfo);
+        super.initializeCluster(metaStorageNodeName(testInfo));
+    }
 
-        // Then
-        assertAll(
-                this::assertExitCodeIsZero,
-                this::assertErrOutputIsEmpty,
-                () -> assertOutputContains("Apache Ignite CLI ver")
-        );
+    protected String metaStorageNodeName(TestInfo testInfo) {
+        return testNodeName(testInfo, 0);
     }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestIntegrationBase.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestNotInitializedIntegrationBase.java
similarity index 83%
rename from modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestIntegrationBase.java
rename to modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestNotInitializedIntegrationBase.java
index 7d3a86ce0..4012df058 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestIntegrationBase.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/CliCommandTestNotInitializedIntegrationBase.java
@@ -24,34 +24,37 @@ import io.micronaut.context.ApplicationContext;
 import jakarta.inject.Inject;
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.util.concurrent.ExecutionException;
 import org.apache.ignite.cli.IntegrationTestBase;
 import org.apache.ignite.cli.commands.cliconfig.TestConfigManagerHelper;
 import org.apache.ignite.cli.commands.cliconfig.TestConfigManagerProvider;
 import org.apache.ignite.cli.config.ConfigDefaultValueProvider;
 import org.apache.ignite.cli.config.ini.IniConfigManager;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.TestInfo;
 import picocli.CommandLine;
 
 /**
- * Integration test base for cli commands. Setup commands, ignite cluster, and provides useful fixtures and assertions.
+ * Integration test base for cli commands. Setup commands, ignite cluster, and provides useful fixtures and assertions. Note: ignite cluster
+ * won't be initialized. If you want to use initialized cluster use {@link CliCommandTestInitializedIntegrationBase}.
  */
-public class CliCommandTestIntegrationBase extends IntegrationTestBase {
+public class CliCommandTestNotInitializedIntegrationBase extends IntegrationTestBase {
     /** Correct ignite jdbc url. */
     protected static final String JDBC_URL = "jdbc:ignite:thin://127.0.0.1:10800";
-
-    @Inject
-    private ApplicationContext context;
-
     @Inject
     ConfigDefaultValueProvider configDefaultValueProvider;
-
     @Inject
     TestConfigManagerProvider configManagerProvider;
-
+    @Inject
+    private ApplicationContext context;
     private CommandLine cmd;
+
     private StringWriter sout;
+
     private StringWriter serr;
+
     private int exitCode = Integer.MIN_VALUE;
 
     /**
@@ -72,6 +75,16 @@ public class CliCommandTestIntegrationBase extends IntegrationTestBase {
         cmd.setErr(new PrintWriter(serr));
     }
 
+    @BeforeAll
+    void beforeAll(TestInfo testInfo) throws ExecutionException, InterruptedException {
+        startNodes(testInfo);
+    }
+
+    @AfterAll
+    void afterAll(TestInfo testInfo) throws Exception {
+        stopNodes(testInfo);
+    }
+
     protected Class<?> getCommandClass() {
         return TopLevelCliCommand.class;
     }
@@ -131,4 +144,10 @@ public class CliCommandTestIntegrationBase extends IntegrationTestBase {
                 .as("Expected command error output to be equal to: " + expectedErrOutput)
                 .isEqualTo(expectedErrOutput);
     }
+
+    protected void assertErrOutputContains(String expectedErrOutput) {
+        assertThat(serr.toString())
+                .as("Expected command error output to contain: " + expectedErrOutput)
+                .contains(expectedErrOutput);
+    }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/ItClusterStatusReplCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/status/ItClusterStatusReplCommandInitializedTest.java
similarity index 56%
copy from modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/ItClusterStatusReplCommandTest.java
copy to modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/status/ItClusterStatusReplCommandInitializedTest.java
index f01917ffa..3891d663a 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/ItClusterStatusReplCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/status/ItClusterStatusReplCommandInitializedTest.java
@@ -15,31 +15,41 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.cluster;
+package org.apache.ignite.cli.commands.cluster.status;
 
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
-import org.apache.ignite.cli.commands.cluster.status.ClusterStatusReplSubCommand;
-import org.junit.jupiter.api.Disabled;
+import org.apache.ignite.cli.commands.CliCommandTestInitializedIntegrationBase;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
 
 /**
- * Tests for {@link ClusterStatusReplSubCommand}.
+ * Tests for {@link ClusterStatusReplSubCommand} for the cluster that is initialized.
  */
-class ItClusterStatusReplCommandTest extends CliCommandTestIntegrationBase {
+class ItClusterStatusReplCommandInitializedTest extends CliCommandTestInitializedIntegrationBase {
+
+    String metaStorageNodeName;
+
+    @BeforeAll
+    void setUpMsNodeName(TestInfo testInfo) {
+        metaStorageNodeName = metaStorageNodeName(testInfo);
+    }
 
     @Test
-    @DisplayName("Should print status when valid cluster url is given")
-    @Disabled("Use topology call to get the number of nodes https://issues.apache.org/jira/browse/IGNITE-17092")
+    @DisplayName("Should print status when valid cluster url is given but cluster is initialized")
     void printStatus() {
         execute("cluster", "status", "--cluster-url", NODE_URL);
 
         assertAll(
                 this::assertExitCodeIsZero,
                 this::assertErrOutputIsEmpty,
-                () -> assertOutputContains("Cluster status:")
+                () -> assertOutputContains("name: cluster"),
+                () -> assertOutputContains("nodes: 3"),
+                () -> assertOutputContains("status: active"),
+                () -> assertOutputContains("cmgNodes: [" + metaStorageNodeName + "]"),
+                () -> assertOutputContains("msNodes: [" + metaStorageNodeName + "]")
         );
     }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/ItClusterStatusReplCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/status/ItClusterStatusReplCommandNotInitializedTest.java
similarity index 69%
rename from modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/ItClusterStatusReplCommandTest.java
rename to modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/status/ItClusterStatusReplCommandNotInitializedTest.java
index f01917ffa..c1f4ab7f0 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/ItClusterStatusReplCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/status/ItClusterStatusReplCommandNotInitializedTest.java
@@ -15,31 +15,28 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.cluster;
+package org.apache.ignite.cli.commands.cluster.status;
 
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
-import org.apache.ignite.cli.commands.cluster.status.ClusterStatusReplSubCommand;
-import org.junit.jupiter.api.Disabled;
+import org.apache.ignite.cli.commands.CliCommandTestNotInitializedIntegrationBase;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
 /**
- * Tests for {@link ClusterStatusReplSubCommand}.
+ * Tests for {@link ClusterStatusReplSubCommand} for the cluster that is not initialized.
  */
-class ItClusterStatusReplCommandTest extends CliCommandTestIntegrationBase {
+class ItClusterStatusReplCommandNotInitializedTest extends CliCommandTestNotInitializedIntegrationBase {
 
     @Test
-    @DisplayName("Should print status when valid cluster url is given")
-    @Disabled("Use topology call to get the number of nodes https://issues.apache.org/jira/browse/IGNITE-17092")
+    @DisplayName("Should print status when valid cluster url is given but cluster is not initialized")
     void printStatus() {
         execute("cluster", "status", "--cluster-url", NODE_URL);
 
         assertAll(
                 this::assertExitCodeIsZero,
                 this::assertErrOutputIsEmpty,
-                () -> assertOutputContains("Cluster status:")
+                () -> assertOutputContains("[nodes: 3, status: not initialized]")
         );
     }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/topology/ItTopologyCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/topology/ItTopologyCommandInitializedClusterTest.java
similarity index 56%
copy from modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/topology/ItTopologyCommandTest.java
copy to modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/topology/ItTopologyCommandInitializedClusterTest.java
index 95cca688a..c7c556b8e 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/topology/ItTopologyCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/topology/ItTopologyCommandInitializedClusterTest.java
@@ -15,26 +15,39 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.topology;
+package org.apache.ignite.cli.commands.cluster.topology;
 
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
-import org.junit.jupiter.api.Disabled;
+import org.apache.ignite.cli.commands.CliCommandTestInitializedIntegrationBase;
+import org.apache.ignite.cli.commands.topology.TopologyCommand;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
 /**
  * Tests for {@link TopologyCommand}.
  */
-@Disabled("https://issues.apache.org/jira/browse/IGNITE-17092")
-class ItTopologyCommandTest extends CliCommandTestIntegrationBase {
+class ItTopologyCommandInitializedClusterTest extends CliCommandTestInitializedIntegrationBase {
 
     @Test
-    @DisplayName("Should print topology when valid cluster url is provided")
-    void printTopology() {
+    @DisplayName("Should print physical topology when valid cluster url is provided")
+    void printPhysicalTopology() {
         // When
-        execute("topology", "--cluster-url", NODE_URL);
+        execute("cluster", "topology", "physical", "--cluster-url", NODE_URL);
+
+        // Then
+        assertAll(
+                this::assertExitCodeIsZero,
+                this::assertErrOutputIsEmpty,
+                this::assertOutputIsNotEmpty
+        );
+    }
+
+    @Test
+    @DisplayName("Should print logical topology when valid cluster url is provided but cluster")
+    void printLogicalTopology() {
+        // When
+        execute("cluster", "topology", "logical", "--cluster-url", NODE_URL);
 
         // Then
         assertAll(
@@ -42,9 +55,5 @@ class ItTopologyCommandTest extends CliCommandTestIntegrationBase {
                 this::assertErrOutputIsEmpty,
                 this::assertOutputIsNotEmpty
         );
-        // TODO: https://issues.apache.org/jira/browse/IGNITE-17092
-        //consistent ID, ID, address, status
-        //node 1, e2d4988a-b836-4e7e-a888-2639e6f79ef0, 127.0.0.1, RUNNING
-        //node 2, 5cb561fc-1963-4f95-98f8-deb407669a86, 127.0.0.2, RECOVERY
     }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/topology/ItTopologyCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/topology/ItTopologyCommandNotInitializedClusterTest.java
similarity index 53%
rename from modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/topology/ItTopologyCommandTest.java
rename to modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/topology/ItTopologyCommandNotInitializedClusterTest.java
index 95cca688a..0c660969c 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/topology/ItTopologyCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/cluster/topology/ItTopologyCommandNotInitializedClusterTest.java
@@ -15,26 +15,25 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.topology;
+package org.apache.ignite.cli.commands.cluster.topology;
 
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
-import org.junit.jupiter.api.Disabled;
+import org.apache.ignite.cli.commands.CliCommandTestNotInitializedIntegrationBase;
+import org.apache.ignite.cli.commands.topology.TopologyCommand;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
 /**
  * Tests for {@link TopologyCommand}.
  */
-@Disabled("https://issues.apache.org/jira/browse/IGNITE-17092")
-class ItTopologyCommandTest extends CliCommandTestIntegrationBase {
+class ItTopologyCommandNotInitializedClusterTest extends CliCommandTestNotInitializedIntegrationBase {
 
     @Test
-    @DisplayName("Should print topology when valid cluster url is provided")
-    void printTopology() {
+    @DisplayName("Should print physical topology when valid cluster url is provided")
+    void printPhysicalTopology() {
         // When
-        execute("topology", "--cluster-url", NODE_URL);
+        execute("cluster", "topology", "physical", "--cluster-url", NODE_URL);
 
         // Then
         assertAll(
@@ -42,9 +41,20 @@ class ItTopologyCommandTest extends CliCommandTestIntegrationBase {
                 this::assertErrOutputIsEmpty,
                 this::assertOutputIsNotEmpty
         );
-        // TODO: https://issues.apache.org/jira/browse/IGNITE-17092
-        //consistent ID, ID, address, status
-        //node 1, e2d4988a-b836-4e7e-a888-2639e6f79ef0, 127.0.0.1, RUNNING
-        //node 2, 5cb561fc-1963-4f95-98f8-deb407669a86, 127.0.0.2, RECOVERY
+    }
+
+    @Test
+    @DisplayName("Should not print logical topology when valid cluster url is provided but cluster is not initialized")
+    void printLogicalTopology() {
+        // When
+        execute("cluster", "topology", "logical", "--cluster-url", NODE_URL);
+
+        // Then prints nothing
+        assertAll(
+                this::assertOutputIsEmpty,
+                () -> assertErrOutputContains(
+                        "Cluster not initialized. Call /management/v1/cluster/init in order to initialize cluster"
+                )
+        );
     }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/configuration/ItConfigCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/configuration/ItConfigCommandTest.java
index 3ced546a6..f596b5973 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/configuration/ItConfigCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/configuration/ItConfigCommandTest.java
@@ -20,14 +20,14 @@ package org.apache.ignite.cli.commands.configuration;
 
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
+import org.apache.ignite.cli.commands.CliCommandTestInitializedIntegrationBase;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
 /**
  * Tests for cluster/node config commands.
  */
-class ItConfigCommandTest extends CliCommandTestIntegrationBase {
+class ItConfigCommandTest extends CliCommandTestInitializedIntegrationBase {
 
     @Test
     @DisplayName("Should read config when valid cluster-url is given")
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/connect/ItConnectCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/connect/ItConnectCommandTest.java
index f948f30bf..d49e0699f 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/connect/ItConnectCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/connect/ItConnectCommandTest.java
@@ -21,14 +21,14 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.junit.jupiter.api.Assertions.assertAll;
 
 import jakarta.inject.Inject;
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
+import org.apache.ignite.cli.commands.CliCommandTestInitializedIntegrationBase;
 import org.apache.ignite.cli.commands.TopLevelCliReplCommand;
 import org.apache.ignite.cli.core.repl.prompt.PromptProvider;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 import picocli.CommandLine.Help.Ansi;
 
-class ItConnectCommandTest extends CliCommandTestIntegrationBase {
+class ItConnectCommandTest extends CliCommandTestInitializedIntegrationBase {
     @Inject
     PromptProvider promptProvider;
 
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/sql/ItSqlCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/sql/ItSqlCommandTest.java
index 5cb924b30..861e9f8b3 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/sql/ItSqlCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/sql/ItSqlCommandTest.java
@@ -20,7 +20,7 @@ package org.apache.ignite.cli.commands.sql;
 import static org.apache.ignite.cli.core.exception.handler.SqlExceptionHandler.CLIENT_CONNECTION_FAILED_MESSAGE;
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
+import org.apache.ignite.cli.commands.CliCommandTestInitializedIntegrationBase;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
@@ -30,7 +30,7 @@ import org.junit.jupiter.api.TestInfo;
 /**
  * Tests for {@link SqlCommand}.
  */
-class ItSqlCommandTest extends CliCommandTestIntegrationBase {
+class ItSqlCommandTest extends CliCommandTestInitializedIntegrationBase {
 
     @BeforeEach
     public void setUp(TestInfo testInfo) throws Exception {
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/version/ItVersionCommandTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/version/ItVersionCommandTest.java
index 99ebea901..b22ccba13 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/version/ItVersionCommandTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/cli/commands/version/ItVersionCommandTest.java
@@ -19,11 +19,11 @@ package org.apache.ignite.cli.commands.version;
 
 import static org.junit.jupiter.api.Assertions.assertAll;
 
-import org.apache.ignite.cli.commands.CliCommandTestIntegrationBase;
+import org.apache.ignite.cli.commands.CliCommandTestInitializedIntegrationBase;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Test;
 
-class ItVersionCommandTest extends CliCommandTestIntegrationBase {
+class ItVersionCommandTest extends CliCommandTestInitializedIntegrationBase {
 
     @Test
     @DisplayName("Should print cli version that is got from pom.xml")
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/rest/ItGeneratedRestClientTest.java b/modules/cli/src/integrationTest/java/org/apache/ignite/rest/ItGeneratedRestClientTest.java
index 500c9aa53..ed0b89ee0 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/rest/ItGeneratedRestClientTest.java
+++ b/modules/cli/src/integrationTest/java/org/apache/ignite/rest/ItGeneratedRestClientTest.java
@@ -24,6 +24,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@@ -48,6 +49,7 @@ import org.apache.ignite.rest.client.api.ClusterConfigurationApi;
 import org.apache.ignite.rest.client.api.ClusterManagementApi;
 import org.apache.ignite.rest.client.api.NodeConfigurationApi;
 import org.apache.ignite.rest.client.api.NodeManagementApi;
+import org.apache.ignite.rest.client.api.TopologyApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
 import org.apache.ignite.rest.client.invoker.Configuration;
@@ -89,6 +91,8 @@ public class ItGeneratedRestClientTest {
 
     private NodeManagementApi nodeManagementApi;
 
+    private TopologyApi topologyApi;
+
     private ObjectMapper objectMapper;
 
     private String firstNodeName;
@@ -130,6 +134,7 @@ public class ItGeneratedRestClientTest {
         nodeConfigurationApi = new NodeConfigurationApi(client);
         clusterManagementApi = new ClusterManagementApi(client);
         nodeManagementApi = new NodeManagementApi(client);
+        topologyApi = new TopologyApi(client);
 
         objectMapper = new ObjectMapper();
     }
@@ -281,14 +286,23 @@ public class ItGeneratedRestClientTest {
     }
 
     @Test
-    void nodeState() {
-        assertDoesNotThrow(() -> {
-            NodeState nodeState = nodeManagementApi.nodeState();
+    void nodeState() throws ApiException {
+        NodeState nodeState = nodeManagementApi.nodeState();
 
-            assertThat(nodeState, is(notNullValue()));
-            assertThat(nodeState.getState(), is(notNullValue()));
-            assertThat(nodeState.getName(), is(firstNodeName));
-        });
+        assertThat(nodeState, is(notNullValue()));
+        assertThat(nodeState.getState(), is(notNullValue()));
+        assertThat(nodeState.getName(), is(firstNodeName));
+
+    }
+
+    @Test
+    void logicalTopology() throws ApiException {
+        assertThat(topologyApi.logical(), hasSize(3));
+    }
+
+    @Test
+    void physicalTopology() throws ApiException {
+        assertThat(topologyApi.physical(), hasSize(3));
     }
 
     private CompletableFuture<Ignite> startNodeAsync(TestInfo testInfo, int index) {
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/status/ClusterStatusCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/status/ClusterStatusCall.java
index 059822bba..6d216b8f4 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/status/ClusterStatusCall.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/status/ClusterStatusCall.java
@@ -19,14 +19,13 @@ package org.apache.ignite.cli.call.cluster.status;
 
 import jakarta.inject.Singleton;
 import org.apache.ignite.cli.call.cluster.status.ClusterStatus.ClusterStatusBuilder;
+import org.apache.ignite.cli.call.cluster.topology.PhysicalTopologyCall;
+import org.apache.ignite.cli.call.cluster.topology.TopologyCallInput;
 import org.apache.ignite.cli.core.call.Call;
 import org.apache.ignite.cli.core.call.CallOutput;
 import org.apache.ignite.cli.core.call.DefaultCallOutput;
 import org.apache.ignite.cli.core.call.StatusCallInput;
 import org.apache.ignite.cli.core.exception.IgniteCliApiException;
-import org.apache.ignite.cli.deprecated.CliPathsConfigLoader;
-import org.apache.ignite.cli.deprecated.IgnitePaths;
-import org.apache.ignite.cli.deprecated.builtins.node.NodeManager;
 import org.apache.ignite.rest.client.api.ClusterManagementApi;
 import org.apache.ignite.rest.client.invoker.ApiClient;
 import org.apache.ignite.rest.client.invoker.ApiException;
@@ -38,34 +37,26 @@ import org.apache.ignite.rest.client.model.ClusterState;
 @Singleton
 public class ClusterStatusCall implements Call<StatusCallInput, ClusterStatus> {
 
-    private final NodeManager nodeManager;
+    private final PhysicalTopologyCall physicalTopologyCall;
 
-    private final CliPathsConfigLoader cliPathsCfgLdr;
-
-    /**
-     * Default constructor.
-     */
-    public ClusterStatusCall(NodeManager nodeManager, CliPathsConfigLoader cliPathsCfgLdr) {
-        this.nodeManager = nodeManager;
-        this.cliPathsCfgLdr = cliPathsCfgLdr;
+    public ClusterStatusCall(PhysicalTopologyCall physicalTopologyCall) {
+        this.physicalTopologyCall = physicalTopologyCall;
     }
 
     @Override
     public CallOutput<ClusterStatus> execute(StatusCallInput input) {
-        IgnitePaths paths = cliPathsCfgLdr.loadIgnitePathsOrThrowError();
-        ClusterStatusBuilder clusterStatusBuilder = ClusterStatus.builder()
-                .nodeCount(nodeManager.getRunningNodes(paths.logDir, paths.cliPidsDir()).size());
-
+        ClusterStatusBuilder clusterStatusBuilder = ClusterStatus.builder();
         try {
             ClusterState clusterState = fetchClusterState(input.getClusterUrl());
             clusterStatusBuilder
+                    .nodeCount(fetchNumberOfAllNodes(input.getClusterUrl()))
                     .initialized(true)
                     .name(clusterState.getClusterTag().getClusterName())
                     .metadataStorageNodes(clusterState.getMsNodes())
                     .cmgNodes(clusterState.getCmgNodes());
         } catch (ApiException e) {
             if (e.getCode() == 404) { // NOT_FOUND means the cluster is not initialized yet
-                clusterStatusBuilder.initialized(false);
+                clusterStatusBuilder.initialized(false).nodeCount(fetchNumberOfAllNodes(input.getClusterUrl()));
             } else {
                 return DefaultCallOutput.failure(new IgniteCliApiException(e, input.getClusterUrl()));
             }
@@ -74,6 +65,10 @@ public class ClusterStatusCall implements Call<StatusCallInput, ClusterStatus> {
         return DefaultCallOutput.success(clusterStatusBuilder.build());
     }
 
+    private int fetchNumberOfAllNodes(String url) {
+        return physicalTopologyCall.execute(TopologyCallInput.builder().clusterUrl(url).build()).body().size();
+    }
+
     private ClusterState fetchClusterState(String url) throws ApiException {
         return new ClusterManagementApi(new ApiClient().setBasePath(url)).clusterState();
     }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/LogicalTopologyCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/LogicalTopologyCall.java
new file mode 100644
index 000000000..453b58b1f
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/LogicalTopologyCall.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ignite.cli.call.cluster.topology;
+
+import jakarta.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.ignite.cli.core.call.Call;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
+import org.apache.ignite.rest.client.api.TopologyApi;
+import org.apache.ignite.rest.client.invoker.ApiException;
+import org.apache.ignite.rest.client.invoker.Configuration;
+import org.apache.ignite.rest.client.model.ClusterNode;
+
+/**
+ * Shows logical cluster topology.
+ */
+@Singleton
+public class LogicalTopologyCall implements Call<TopologyCallInput, List<ClusterNode>> {
+
+    /** {@inheritDoc} */
+    @Override
+    public TopologyCallOutput execute(TopologyCallInput input) {
+        try {
+            return TopologyCallOutput.success(fetchLogicalTopology(input));
+        } catch (ApiException | IllegalArgumentException e) {
+            return TopologyCallOutput.failure(new IgniteCliApiException(e, input.getClusterUrl()));
+        }
+    }
+
+    private List<ClusterNode> fetchLogicalTopology(TopologyCallInput input) throws ApiException {
+        return new ArrayList<>(new TopologyApi(Configuration.getDefaultApiClient().setBasePath(input.getClusterUrl())).logical());
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/PhysicalTopologyCall.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/PhysicalTopologyCall.java
new file mode 100644
index 000000000..314607065
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/PhysicalTopologyCall.java
@@ -0,0 +1,49 @@
+/*
+ * 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.ignite.cli.call.cluster.topology;
+
+import jakarta.inject.Singleton;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.ignite.cli.core.call.Call;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
+import org.apache.ignite.rest.client.api.TopologyApi;
+import org.apache.ignite.rest.client.invoker.ApiException;
+import org.apache.ignite.rest.client.invoker.Configuration;
+import org.apache.ignite.rest.client.model.ClusterNode;
+
+/**
+ * Shows physical cluster topology.
+ */
+@Singleton
+public class PhysicalTopologyCall implements Call<TopologyCallInput, List<ClusterNode>> {
+
+    /** {@inheritDoc} */
+    @Override
+    public TopologyCallOutput execute(TopologyCallInput input) {
+        try {
+            return TopologyCallOutput.success(fetchPhysicalTopology(input));
+        } catch (ApiException | IllegalArgumentException e) {
+            return TopologyCallOutput.failure(new IgniteCliApiException(e, input.getClusterUrl()));
+        }
+    }
+
+    private List<ClusterNode> fetchPhysicalTopology(TopologyCallInput input) throws ApiException {
+        return new ArrayList<>(new TopologyApi(Configuration.getDefaultApiClient().setBasePath(input.getClusterUrl())).physical());
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/TopologyCallInput.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/TopologyCallInput.java
new file mode 100644
index 000000000..4d9470d95
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/TopologyCallInput.java
@@ -0,0 +1,67 @@
+/*
+ * 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.ignite.cli.call.cluster.topology;
+
+import org.apache.ignite.cli.core.call.CallInput;
+
+/**
+ * Input for physical or logical topology call.
+ */
+public class TopologyCallInput implements CallInput {
+    /**
+     * Cluster url.
+     */
+    private final String clusterUrl;
+
+    private TopologyCallInput(String clusterUrl) {
+        this.clusterUrl = clusterUrl;
+    }
+
+    /**
+     * Builder for {@link TopologyCallInput}.
+     */
+    public static TopologyCallInputBuilder builder() {
+        return new TopologyCallInputBuilder();
+    }
+
+    /**
+     * Get cluster URL.
+     *
+     * @return Cluster URL.
+     */
+    public String getClusterUrl() {
+        return clusterUrl;
+    }
+
+    /**
+     * Builder for {@link org.apache.ignite.cli.call.configuration.ClusterConfigShowCallInput}.
+     */
+    public static class TopologyCallInputBuilder {
+
+        private String clusterUrl;
+
+        public TopologyCallInputBuilder clusterUrl(String clusterUrl) {
+            this.clusterUrl = clusterUrl;
+            return this;
+        }
+
+        public TopologyCallInput build() {
+            return new TopologyCallInput(clusterUrl);
+        }
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/TopologyCallOutput.java b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/TopologyCallOutput.java
new file mode 100644
index 000000000..525d0aa3e
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/call/cluster/topology/TopologyCallOutput.java
@@ -0,0 +1,70 @@
+/*
+ * 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.ignite.cli.call.cluster.topology;
+
+import java.util.List;
+import org.apache.ignite.cli.core.call.CallOutput;
+import org.apache.ignite.cli.core.exception.IgniteCliApiException;
+import org.apache.ignite.rest.client.model.ClusterNode;
+
+/**
+ * Output fot the topology calls.
+ */
+public class TopologyCallOutput implements CallOutput<List<ClusterNode>> {
+    private final Throwable error;
+
+    private final List<ClusterNode> topology;
+
+    private TopologyCallOutput(List<ClusterNode> topology) {
+        this.topology = topology;
+        error = null;
+    }
+
+    private TopologyCallOutput(Exception error) {
+        this.error = error;
+        this.topology = null;
+    }
+
+    public static TopologyCallOutput success(List<ClusterNode> topology) {
+        return new TopologyCallOutput(topology);
+    }
+
+    public static TopologyCallOutput failure(IgniteCliApiException e) {
+        return new TopologyCallOutput(e);
+    }
+
+    @Override
+    public List<ClusterNode> body() {
+        return topology;
+    }
+
+    @Override
+    public boolean hasError() {
+        return error != null;
+    }
+
+    @Override
+    public boolean isEmpty() {
+        return topology != null && topology.isEmpty();
+    }
+
+    @Override
+    public Throwable errorCause() {
+        return error;
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cluster/ClusterCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cluster/ClusterCommand.java
index e985709ec..fb73a8be4 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cluster/ClusterCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cluster/ClusterCommand.java
@@ -20,16 +20,18 @@ package org.apache.ignite.cli.commands.cluster;
 import org.apache.ignite.cli.commands.cluster.config.ClusterConfigSubCommand;
 import org.apache.ignite.cli.commands.cluster.init.ClusterInitSubCommand;
 import org.apache.ignite.cli.commands.cluster.status.ClusterStatusSubCommand;
+import org.apache.ignite.cli.commands.topology.TopologyCommand;
 import picocli.CommandLine.Command;
 
 /**
- * Node command.
+ * Cluster command.
  */
 @Command(name = "cluster",
         subcommands = {
                 ClusterConfigSubCommand.class,
                 ClusterInitSubCommand.class,
-                ClusterStatusSubCommand.class},
+                ClusterStatusSubCommand.class,
+                TopologyCommand.class},
         description = "Manages an Ignite cluster.")
 public class ClusterCommand {
 }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cluster/ClusterReplCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cluster/ClusterReplCommand.java
index 461a2a9b0..44e517bb7 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/cluster/ClusterReplCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/cluster/ClusterReplCommand.java
@@ -20,6 +20,7 @@ package org.apache.ignite.cli.commands.cluster;
 import org.apache.ignite.cli.commands.cluster.config.ClusterConfigReplSubCommand;
 import org.apache.ignite.cli.commands.cluster.init.ClusterInitReplSubCommand;
 import org.apache.ignite.cli.commands.cluster.status.ClusterStatusReplSubCommand;
+import org.apache.ignite.cli.commands.topology.TopologyReplCommand;
 import picocli.CommandLine.Command;
 
 /**
@@ -29,7 +30,8 @@ import picocli.CommandLine.Command;
         subcommands = {
                 ClusterConfigReplSubCommand.class,
                 ClusterInitReplSubCommand.class,
-                ClusterStatusReplSubCommand.class
+                ClusterStatusReplSubCommand.class,
+                TopologyReplCommand.class
         },
         description = "Manages an Ignite cluster.")
 public class ClusterReplCommand {
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/decorators/TopologyDecorator.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/decorators/TopologyDecorator.java
new file mode 100644
index 000000000..af5426eb8
--- /dev/null
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/decorators/TopologyDecorator.java
@@ -0,0 +1,51 @@
+/*
+ * 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.ignite.cli.commands.decorators;
+
+import com.jakewharton.fliptables.FlipTable;
+import java.util.List;
+import org.apache.ignite.cli.core.decorator.Decorator;
+import org.apache.ignite.cli.core.decorator.TerminalOutput;
+import org.apache.ignite.rest.client.model.ClusterNode;
+
+/**
+ * Implementation of {@link Decorator} for the list of {@link ClusterNode}.
+ */
+public class TopologyDecorator implements Decorator<List<ClusterNode>, TerminalOutput> {
+
+    /**
+     * Transform list of {@link ClusterNode} to {@link TerminalOutput}.
+     *
+     * @param topology incoming list of {@link ClusterNode}.
+     * @return User-friendly interpretation of list of {@link ClusterNode} in {@link TerminalOutput}.
+     */
+    @Override
+    public TerminalOutput decorate(List<ClusterNode> topology) {
+        String[] headers = {"name", "host", "port", "consistent id", "id"};
+        String[][] content = topology.stream().map(
+                node -> new String[]{
+                        node.getName(),
+                        node.getAddress().getHost(),
+                        String.valueOf(node.getAddress().getPort()),
+                        node.getAddress().getConsistentId() == null ? "(empty)" : node.getAddress().getConsistentId(),
+                        node.getId()
+                }
+        ).toArray(String[][]::new);
+        return () -> FlipTable.of(headers, content);
+    }
+}
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java
index 58e6fd321..ec526518e 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java
@@ -42,7 +42,7 @@ public class NodeStatusReplSubCommand extends BaseCommand implements Callable<In
      */
     @SuppressWarnings("PMD.UnusedPrivateField")
     @Option(
-            names = {"--node-url"}, description = "Url to node.", descriptionKey = "ignite.node-url"
+            names = {"--node-url"}, description = "Url to node.", descriptionKey = "ignite.cluster-url"
     )
     private String nodeUrl;
 
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java
index d3eae42ce..78abc3620 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java
@@ -40,7 +40,7 @@ public class NodeStatusSubCommand extends BaseCommand implements Callable<Intege
      * Node url option.
      */
     @SuppressWarnings("PMD.UnusedPrivateField")
-    @Option(names = {"--node-url"}, description = "Url to node.", descriptionKey = "ignite.node-url")
+    @Option(names = {"--node-url"}, description = "Url to node.", descriptionKey = "ignite.cluster-url")
     private String nodeUrl;
 
     @Inject
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/LogicalTopologyReplSubCommand.java
similarity index 56%
copy from modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/LogicalTopologyReplSubCommand.java
index 58e6fd321..2cf9ccec6 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/LogicalTopologyReplSubCommand.java
@@ -15,62 +15,56 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.node.status;
+package org.apache.ignite.cli.commands.topology;
 
 import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
 import java.util.concurrent.Callable;
-import org.apache.ignite.cli.call.node.status.NodeStatusCall;
+import org.apache.ignite.cli.call.cluster.topology.LogicalTopologyCall;
+import org.apache.ignite.cli.call.cluster.topology.TopologyCallInput;
+import org.apache.ignite.cli.call.cluster.topology.TopologyCallInput.TopologyCallInputBuilder;
 import org.apache.ignite.cli.commands.BaseCommand;
-import org.apache.ignite.cli.commands.decorators.NodeStatusDecorator;
+import org.apache.ignite.cli.commands.decorators.TopologyDecorator;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.cli.core.call.StatusCallInput;
 import org.apache.ignite.cli.core.repl.Session;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 
 /**
- * Display the node status in REPL.
+ * Command that show logical cluster topology in REPL mode.
  */
-@Command(name = "status",
-        description = "Prints status of the node.")
-@Singleton
-public class NodeStatusReplSubCommand extends BaseCommand implements Callable<Integer> {
-
+@Command(name = "logical")
+public class LogicalTopologyReplSubCommand extends BaseCommand implements Callable<Integer> {
     /**
      * Node url option.
      */
-    @SuppressWarnings("PMD.UnusedPrivateField")
-    @Option(
-            names = {"--node-url"}, description = "Url to node.", descriptionKey = "ignite.node-url"
-    )
-    private String nodeUrl;
+    @Option(names = {"--cluster-url"}, description = "Url to ignite node.", descriptionKey = "ignite.cluster-url")
+    private String clusterUrl;
 
     @Inject
-    private NodeStatusCall nodeStatusCall;
+    private Session session;
 
     @Inject
-    private Session session;
+    private LogicalTopologyCall call;
 
     /** {@inheritDoc} */
     @Override
     public Integer call() {
-        String inputUrl;
+        TopologyCallInputBuilder inputBuilder = TopologyCallInput.builder();
 
-        if (nodeUrl != null) {
-            inputUrl = nodeUrl;
-        } else if (session.isConnectedToNode()) {
-            inputUrl = session.nodeUrl();
+        if (clusterUrl != null) {
+            inputBuilder.clusterUrl(clusterUrl);
+        } else if (session.isConnectedToNode())  {
+            inputBuilder.clusterUrl(session.nodeUrl());
         } else {
-            spec.commandLine().getErr().println("You are not connected to node. Run 'connect' command or use '--node-url' option.");
+            spec.commandLine().getErr().println("You are not connected to node. Run 'connect' command or use '--cluster-url' option.");
             return 2;
         }
 
-        return CallExecutionPipeline.builder(nodeStatusCall)
-                .inputProvider(() -> new StatusCallInput(inputUrl))
+        return CallExecutionPipeline.builder(call)
+                .inputProvider(inputBuilder::build)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
-                .decorator(new NodeStatusDecorator())
+                .decorator(new TopologyDecorator())
                 .build()
                 .runPipeline();
     }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/LogicalTopologySubCommand.java
similarity index 60%
copy from modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/LogicalTopologySubCommand.java
index d3eae42ce..08de59243 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/LogicalTopologySubCommand.java
@@ -15,45 +15,40 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.node.status;
+package org.apache.ignite.cli.commands.topology;
 
 import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
 import java.util.concurrent.Callable;
-import org.apache.ignite.cli.call.node.status.NodeStatusCall;
+import org.apache.ignite.cli.call.cluster.topology.LogicalTopologyCall;
+import org.apache.ignite.cli.call.cluster.topology.TopologyCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
-import org.apache.ignite.cli.commands.decorators.NodeStatusDecorator;
+import org.apache.ignite.cli.commands.decorators.TopologyDecorator;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.cli.core.call.StatusCallInput;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 
 /**
- * Display the node status.
+ * Command that show logical cluster topology.
  */
-@Command(name = "status",
-        description = "Prints status of the node.")
-@Singleton
-public class NodeStatusSubCommand extends BaseCommand implements Callable<Integer> {
-
+@Command(name = "logical")
+public class LogicalTopologySubCommand extends BaseCommand implements Callable<Integer> {
     /**
      * Node url option.
      */
-    @SuppressWarnings("PMD.UnusedPrivateField")
-    @Option(names = {"--node-url"}, description = "Url to node.", descriptionKey = "ignite.node-url")
-    private String nodeUrl;
+    @Option(names = {"--cluster-url"}, description = "Url to ignite node.", descriptionKey = "ignite.cluster-url")
+    private String clusterUrl;
 
     @Inject
-    private NodeStatusCall nodeStatusCall;
+    private LogicalTopologyCall call;
 
     /** {@inheritDoc} */
     @Override
     public Integer call() {
-        return CallExecutionPipeline.builder(nodeStatusCall)
-                .inputProvider(() -> new StatusCallInput(nodeUrl))
+        return CallExecutionPipeline.builder(call)
+                .inputProvider(() -> TopologyCallInput.builder().clusterUrl(clusterUrl).build())
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
-                .decorator(new NodeStatusDecorator())
+                .decorator(new TopologyDecorator())
                 .build()
                 .runPipeline();
     }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/PhysicalTopologyReplSubCommand.java
similarity index 56%
copy from modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/PhysicalTopologyReplSubCommand.java
index 58e6fd321..988433c6b 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusReplSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/PhysicalTopologyReplSubCommand.java
@@ -15,39 +15,33 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.node.status;
+package org.apache.ignite.cli.commands.topology;
 
 import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
 import java.util.concurrent.Callable;
-import org.apache.ignite.cli.call.node.status.NodeStatusCall;
+import org.apache.ignite.cli.call.cluster.topology.PhysicalTopologyCall;
+import org.apache.ignite.cli.call.cluster.topology.TopologyCallInput;
+import org.apache.ignite.cli.call.cluster.topology.TopologyCallInput.TopologyCallInputBuilder;
 import org.apache.ignite.cli.commands.BaseCommand;
-import org.apache.ignite.cli.commands.decorators.NodeStatusDecorator;
+import org.apache.ignite.cli.commands.decorators.TopologyDecorator;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.cli.core.call.StatusCallInput;
 import org.apache.ignite.cli.core.repl.Session;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 
 /**
- * Display the node status in REPL.
+ * Command that show physical cluster topology in REPL mode.
  */
-@Command(name = "status",
-        description = "Prints status of the node.")
-@Singleton
-public class NodeStatusReplSubCommand extends BaseCommand implements Callable<Integer> {
-
+@Command(name = "physical")
+public class PhysicalTopologyReplSubCommand extends BaseCommand implements Callable<Integer> {
     /**
      * Node url option.
      */
-    @SuppressWarnings("PMD.UnusedPrivateField")
-    @Option(
-            names = {"--node-url"}, description = "Url to node.", descriptionKey = "ignite.node-url"
-    )
-    private String nodeUrl;
+    @Option(names = {"--cluster-url"}, description = "Url to ignite node.", descriptionKey = "ignite.cluster-url")
+    private String clusterUrl;
 
     @Inject
-    private NodeStatusCall nodeStatusCall;
+    private PhysicalTopologyCall call;
 
     @Inject
     private Session session;
@@ -55,22 +49,22 @@ public class NodeStatusReplSubCommand extends BaseCommand implements Callable<In
     /** {@inheritDoc} */
     @Override
     public Integer call() {
-        String inputUrl;
+        TopologyCallInputBuilder inputBuilder = TopologyCallInput.builder();
 
-        if (nodeUrl != null) {
-            inputUrl = nodeUrl;
-        } else if (session.isConnectedToNode()) {
-            inputUrl = session.nodeUrl();
+        if (clusterUrl != null) {
+            inputBuilder.clusterUrl(clusterUrl);
+        } else if (session.isConnectedToNode())  {
+            inputBuilder.clusterUrl(session.nodeUrl());
         } else {
-            spec.commandLine().getErr().println("You are not connected to node. Run 'connect' command or use '--node-url' option.");
+            spec.commandLine().getErr().println("You are not connected to node. Run 'connect' command or use '--cluster-url' option.");
             return 2;
         }
 
-        return CallExecutionPipeline.builder(nodeStatusCall)
-                .inputProvider(() -> new StatusCallInput(inputUrl))
+        return CallExecutionPipeline.builder(call)
+                .inputProvider(inputBuilder::build)
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
-                .decorator(new NodeStatusDecorator())
+                .decorator(new TopologyDecorator())
                 .build()
                 .runPipeline();
     }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/PhysicalTopologySubCommand.java
similarity index 60%
copy from modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java
copy to modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/PhysicalTopologySubCommand.java
index d3eae42ce..a76be9dc9 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/node/status/NodeStatusSubCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/PhysicalTopologySubCommand.java
@@ -15,45 +15,40 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.commands.node.status;
+package org.apache.ignite.cli.commands.topology;
 
 import jakarta.inject.Inject;
-import jakarta.inject.Singleton;
 import java.util.concurrent.Callable;
-import org.apache.ignite.cli.call.node.status.NodeStatusCall;
+import org.apache.ignite.cli.call.cluster.topology.PhysicalTopologyCall;
+import org.apache.ignite.cli.call.cluster.topology.TopologyCallInput;
 import org.apache.ignite.cli.commands.BaseCommand;
-import org.apache.ignite.cli.commands.decorators.NodeStatusDecorator;
+import org.apache.ignite.cli.commands.decorators.TopologyDecorator;
 import org.apache.ignite.cli.core.call.CallExecutionPipeline;
-import org.apache.ignite.cli.core.call.StatusCallInput;
 import picocli.CommandLine.Command;
 import picocli.CommandLine.Option;
 
 /**
- * Display the node status.
+ * Command that show physical cluster topology.
  */
-@Command(name = "status",
-        description = "Prints status of the node.")
-@Singleton
-public class NodeStatusSubCommand extends BaseCommand implements Callable<Integer> {
-
+@Command(name = "physical")
+public class PhysicalTopologySubCommand extends BaseCommand implements Callable<Integer> {
     /**
      * Node url option.
      */
-    @SuppressWarnings("PMD.UnusedPrivateField")
-    @Option(names = {"--node-url"}, description = "Url to node.", descriptionKey = "ignite.node-url")
-    private String nodeUrl;
+    @Option(names = {"--cluster-url"}, description = "Url to ignite node.", descriptionKey = "ignite.cluster-url")
+    private String clusterUrl;
 
     @Inject
-    private NodeStatusCall nodeStatusCall;
+    private PhysicalTopologyCall call;
 
     /** {@inheritDoc} */
     @Override
     public Integer call() {
-        return CallExecutionPipeline.builder(nodeStatusCall)
-                .inputProvider(() -> new StatusCallInput(nodeUrl))
+        return CallExecutionPipeline.builder(call)
+                .inputProvider(() -> TopologyCallInput.builder().clusterUrl(clusterUrl).build())
                 .output(spec.commandLine().getOut())
                 .errOutput(spec.commandLine().getErr())
-                .decorator(new NodeStatusDecorator())
+                .decorator(new TopologyDecorator())
                 .build()
                 .runPipeline();
     }
diff --git a/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyCommand.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyCommand.java
index e01a888a4..5da14720f 100644
--- a/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyCommand.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyCommand.java
@@ -21,30 +21,18 @@ import jakarta.inject.Singleton;
 import java.util.concurrent.Callable;
 import org.apache.ignite.cli.commands.BaseCommand;
 import picocli.CommandLine.Command;
-import picocli.CommandLine.Option;
 
 /**
  * Command that prints ignite cluster topology.
  */
-@Command(name = "topology", description = "Prints topology information.")
+@Command(name = "topology", description = "Prints topology information.",
+        subcommands = {PhysicalTopologySubCommand.class, LogicalTopologySubCommand.class })
 @Singleton
 public class TopologyCommand extends BaseCommand implements Callable<Integer> {
 
-    /**
-     * Cluster url option.
-     */
-    @SuppressWarnings("PMD.UnusedPrivateField")
-    @Option(
-            names = {"--cluster-url"}, description = "Url to cluster node.",
-            descriptionKey = "ignite.cluster-url", defaultValue = "http://localhost:10300"
-    )
-    private String clusterUrl;
-
     /** {@inheritDoc} */
     @Override
     public Integer call() {
-        //TODO: https://issues.apache.org/jira/browse/IGNITE-17092
-        spec.commandLine().getOut().println("Topology command is not implemented yet.");
         return 0;
     }
 }
diff --git a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallIntegrationTestBase.java b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyReplCommand.java
similarity index 56%
rename from modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallIntegrationTestBase.java
rename to modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyReplCommand.java
index d5c0f37b3..fa2c9ed05 100644
--- a/modules/cli/src/integrationTest/java/org/apache/ignite/cli/call/CallIntegrationTestBase.java
+++ b/modules/cli/src/main/java/org/apache/ignite/cli/commands/topology/TopologyReplCommand.java
@@ -15,12 +15,24 @@
  * limitations under the License.
  */
 
-package org.apache.ignite.cli.call;
+package org.apache.ignite.cli.commands.topology;
 
-import org.apache.ignite.cli.IntegrationTestBase;
+import jakarta.inject.Singleton;
+import java.util.concurrent.Callable;
+import org.apache.ignite.cli.commands.BaseCommand;
+import picocli.CommandLine.Command;
 
 /**
- * Base class for call integration tests. Contains common methods and useful assertions.
+ * Command that prints ignite cluster topology in REPL mode.
  */
-public class CallIntegrationTestBase extends IntegrationTestBase {
+@Command(name = "topology", description = "Prints topology information.",
+        subcommands = {PhysicalTopologyReplSubCommand.class, LogicalTopologyReplSubCommand.class })
+@Singleton
+public class TopologyReplCommand extends BaseCommand implements Callable<Integer> {
+
+    /** {@inheritDoc} */
+    @Override
+    public Integer call() {
+        return 0;
+    }
 }
diff --git a/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/rest/ItClusterManagementControllerTest.java b/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/rest/ItClusterManagementControllerTest.java
index bd805bc6d..5d48c86ce 100644
--- a/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/rest/ItClusterManagementControllerTest.java
+++ b/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/rest/ItClusterManagementControllerTest.java
@@ -34,49 +34,23 @@ import io.micronaut.http.HttpStatus;
 import io.micronaut.http.client.HttpClient;
 import io.micronaut.http.client.annotation.Client;
 import io.micronaut.http.client.exceptions.HttpClientResponseException;
-import io.micronaut.runtime.server.EmbeddedServer;
-import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
 import jakarta.inject.Inject;
 import java.io.IOException;
-import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.List;
-import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
 import org.apache.ignite.internal.cluster.management.MockNode;
-import org.apache.ignite.internal.rest.api.Problem;
 import org.apache.ignite.internal.rest.api.cluster.ClusterManagementApi;
 import org.apache.ignite.internal.rest.api.cluster.ClusterStateDto;
-import org.apache.ignite.internal.testframework.WorkDirectory;
-import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
-import org.apache.ignite.network.ClusterService;
 import org.apache.ignite.network.NetworkAddress;
 import org.apache.ignite.network.StaticNodeFinder;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.TestInfo;
-import org.junit.jupiter.api.extension.ExtendWith;
 
 /**
  * Cluster management REST test.
  */
-@MicronautTest
-@ExtendWith(WorkDirectoryExtension.class)
-public class ItClusterManagementControllerTest {
-
-    private static final int PORT_BASE = 10000;
-
-    private static final List<MockNode> cluster = new ArrayList<>();
-
-    static ClusterService clusterService;
-
-    static ClusterManagementGroupManager clusterManager;
-
-    @WorkDirectory
-    private static Path workDir;
-
-    @Inject
-    private EmbeddedServer server;
+public class ItClusterManagementControllerTest extends RestTestBase {
 
     @Inject
     @Client("/management/v1/cluster")
@@ -194,8 +168,4 @@ public class ItClusterManagementControllerTest {
     public ClusterManagementRestFactory clusterManagementRestFactory() {
         return new ClusterManagementRestFactory(clusterService, clusterManager);
     }
-
-    private Problem getProblem(HttpClientResponseException exception) {
-        return exception.getResponse().getBody(Problem.class).orElseThrow();
-    }
 }
diff --git a/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/rest/RestTestBase.java b/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/rest/RestTestBase.java
new file mode 100644
index 000000000..83d7b80f8
--- /dev/null
+++ b/modules/cluster-management/src/integrationTest/java/org/apache/ignite/internal/cluster/management/rest/RestTestBase.java
@@ -0,0 +1,94 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.cluster.management.rest;
+
+import io.micronaut.http.client.exceptions.HttpClientResponseException;
+import io.micronaut.runtime.server.EmbeddedServer;
+import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
+import org.apache.ignite.internal.cluster.management.MockNode;
+import org.apache.ignite.internal.rest.api.Problem;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.NetworkAddress;
+import org.apache.ignite.network.StaticNodeFinder;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Cluster management REST test.
+ */
+@MicronautTest
+@ExtendWith(WorkDirectoryExtension.class)
+public class RestTestBase {
+
+    static final int PORT_BASE = 10000;
+
+    static final List<MockNode> cluster = new ArrayList<>();
+
+    static ClusterService clusterService;
+
+    static ClusterManagementGroupManager clusterManager;
+
+    @WorkDirectory
+    static Path workDir;
+
+    @Inject
+    EmbeddedServer server;
+
+    @BeforeAll
+    static void setUp(TestInfo testInfo) throws IOException {
+        var addr1 = new NetworkAddress("localhost", PORT_BASE);
+        var addr2 = new NetworkAddress("localhost", PORT_BASE + 1);
+
+        var nodeFinder = new StaticNodeFinder(List.of(addr1, addr2));
+
+        cluster.add(new MockNode(testInfo, addr1, nodeFinder, workDir.resolve("node0")));
+        cluster.add(new MockNode(testInfo, addr2, nodeFinder, workDir.resolve("node1")));
+
+        for (MockNode node : cluster) {
+            node.start();
+        }
+
+        clusterService = cluster.get(0).clusterService();
+        clusterManager = cluster.get(0).clusterManager();
+    }
+
+    @AfterAll
+    static void tearDown() {
+        for (MockNode node : cluster) {
+            node.beforeNodeStop();
+        }
+
+        for (MockNode node : cluster) {
+            node.stop();
+        }
+    }
+
+    Problem getProblem(HttpClientResponseException exception) {
+        return exception.getResponse().getBody(Problem.class).orElseThrow();
+    }
+}
diff --git a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementController.java b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementController.java
index 1aea9f91a..83aec6c4a 100644
--- a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementController.java
+++ b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementController.java
@@ -54,8 +54,7 @@ public class ClusterManagementController implements ClusterManagementApi {
      * @param clusterInitializer cluster initializer.
      * @param clusterManagementGroupManager cluster management group manager.
      */
-    public ClusterManagementController(ClusterInitializer clusterInitializer,
-            ClusterManagementGroupManager clusterManagementGroupManager) {
+    public ClusterManagementController(ClusterInitializer clusterInitializer, ClusterManagementGroupManager clusterManagementGroupManager) {
         this.clusterInitializer = clusterInitializer;
         this.clusterManagementGroupManager = clusterManagementGroupManager;
     }
diff --git a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementRestFactory.java b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementRestFactory.java
index b124e1768..b0348f6a5 100644
--- a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementRestFactory.java
+++ b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/ClusterManagementRestFactory.java
@@ -24,6 +24,7 @@ import org.apache.ignite.internal.cluster.management.ClusterInitializer;
 import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
 import org.apache.ignite.internal.rest.RestFactory;
 import org.apache.ignite.network.ClusterService;
+import org.apache.ignite.network.TopologyService;
 
 /**
  * Factory that creates beans that are needed for {@link ClusterManagementController}.
@@ -31,11 +32,12 @@ import org.apache.ignite.network.ClusterService;
 @Factory
 public class ClusterManagementRestFactory implements RestFactory {
     private final ClusterService clusterService;
-    private final ClusterManagementGroupManager clusterManagementGroupManager;
 
-    public ClusterManagementRestFactory(ClusterService clusterService, ClusterManagementGroupManager clusterManagementGroupManager) {
+    private final ClusterManagementGroupManager cmgManager;
+
+    public ClusterManagementRestFactory(ClusterService clusterService, ClusterManagementGroupManager cmgManager) {
         this.clusterService = clusterService;
-        this.clusterManagementGroupManager = clusterManagementGroupManager;
+        this.cmgManager = cmgManager;
     }
 
     @Bean
@@ -46,7 +48,13 @@ public class ClusterManagementRestFactory implements RestFactory {
 
     @Bean
     @Singleton
-    public ClusterManagementGroupManager clusterManagementGroupManager() {
-        return clusterManagementGroupManager;
+    public ClusterManagementGroupManager cmgManager() {
+        return cmgManager;
+    }
+
+    @Bean
+    @Singleton
+    public TopologyService topologyService() {
+        return clusterService.topologyService();
     }
 }
diff --git a/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.java
new file mode 100644
index 000000000..4833ffd45
--- /dev/null
+++ b/modules/cluster-management/src/main/java/org/apache/ignite/internal/cluster/management/rest/TopologyController.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.ignite.internal.cluster.management.rest;
+
+import io.micronaut.http.annotation.Controller;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+import org.apache.ignite.internal.cluster.management.ClusterManagementGroupManager;
+import org.apache.ignite.internal.cluster.management.rest.exception.ClusterNotInitializedException;
+import org.apache.ignite.internal.rest.api.cluster.ClusterNodeDto;
+import org.apache.ignite.internal.rest.api.cluster.NetworkAddressDto;
+import org.apache.ignite.internal.rest.api.cluster.TopologyApi;
+import org.apache.ignite.lang.IgniteException;
+import org.apache.ignite.network.TopologyService;
+
+/**
+ * Cluster topology endpoint implementation.
+ */
+@Controller("/management/v1/cluster/topology")
+public class TopologyController implements TopologyApi {
+    private final TopologyService topologyService;
+
+    private final ClusterManagementGroupManager cmgManager;
+
+    public TopologyController(TopologyService topologyService, ClusterManagementGroupManager cmgManager) {
+        this.topologyService = topologyService;
+        this.cmgManager = cmgManager;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<ClusterNodeDto> physicalTopology() {
+        return topologyService.allMembers()
+                .stream().map(cn -> new ClusterNodeDto(cn.id(), cn.name(),
+                        new NetworkAddressDto(cn.address().host(), cn.address().port(), cn.address().consistentId()))
+                ).collect(Collectors.toList());
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public Collection<ClusterNodeDto> logicalTopology() {
+        try {
+            return cmgManager.clusterState()
+                    .thenApply(state -> {
+                        if (state == null) {
+                            throw new ClusterNotInitializedException();
+                        }
+                        return cmgManager.logicalTopology();
+                    }).get().get().stream()
+                    .map(cn -> new ClusterNodeDto(cn.id(), cn.name(),
+                            new NetworkAddressDto(cn.address().host(), cn.address().port(), cn.address().consistentId()))
+                    ).collect(Collectors.toList());
+        } catch (ExecutionException | InterruptedException e) {
+            if (e.getCause() instanceof ClusterNotInitializedException) {
+                throw (ClusterNotInitializedException) e.getCause();
+            }
+            throw new IgniteException(e);
+        }
+    }
+}
diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterManagementApi.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterManagementApi.java
index 91d01fca1..2c5ac222b 100644
--- a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterManagementApi.java
+++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterManagementApi.java
@@ -27,7 +27,6 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.media.Content;
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.rest.api.Problem;
@@ -45,14 +44,12 @@ public interface ClusterManagementApi {
      */
     @Get("state")
     @Operation(operationId = "clusterState")
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "Return cluster state",
-                    content = @Content(mediaType = MediaType.APPLICATION_JSON,
-                            schema = @Schema(implementation = ClusterStateDto.class))),
-            @ApiResponse(responseCode = "404", description = "Cluster state not found, it means that the cluster is not initialized"),
-            @ApiResponse(responseCode = "500", description = "Internal error",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
-    })
+    @ApiResponse(responseCode = "200", description = "Return cluster state",
+            content = @Content(mediaType = MediaType.APPLICATION_JSON,
+                    schema = @Schema(implementation = ClusterStateDto.class)))
+    @ApiResponse(responseCode = "404", description = "Cluster state not found, it means that the cluster is not initialized")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
     @Produces({
             MediaType.APPLICATION_JSON,
             MediaType.PROBLEM_JSON
@@ -66,14 +63,11 @@ public interface ClusterManagementApi {
      */
     @Post("init")
     @Operation(operationId = "init")
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "Cluster initialized"),
-            @ApiResponse(responseCode = "500", description = "Internal error",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class))),
-            @ApiResponse(responseCode = "400", description = "Incorrect configuration",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
-
-    })
+    @ApiResponse(responseCode = "200", description = "Cluster initialized")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "400", description = "Incorrect configuration",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
     @Consumes(MediaType.APPLICATION_JSON)
     CompletableFuture<Void> init(@Body InitCommand initCommand);
 }
diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterNodeDto.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterNodeDto.java
new file mode 100644
index 000000000..05cd8c2b4
--- /dev/null
+++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/ClusterNodeDto.java
@@ -0,0 +1,86 @@
+/*
+ * 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.ignite.internal.rest.api.cluster;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.ignite.network.ClusterNode;
+
+/**
+ * REST representation of {@link ClusterNode}.
+ */
+@Schema(name = "ClusterNode")
+public class ClusterNodeDto {
+    /** Local id assigned to this node instance. Changes between restarts. */
+    private final String id;
+
+    /** Unique name of member in the cluster. Consistent between restarts. */
+    private final String name;
+
+    /** Network address of this node. */
+    private final NetworkAddressDto address;
+
+    /**
+     * Constructor.
+     *
+     * @param id Local id that changes between restarts.
+     * @param name Unique name of a member in a cluster.
+     * @param address Node address.
+     */
+    @JsonCreator
+    public ClusterNodeDto(
+            @JsonProperty("id") String id,
+            @JsonProperty("name") String name,
+            @JsonProperty("address") NetworkAddressDto address) {
+        this.id = id;
+        this.name = name;
+        this.address = address;
+    }
+
+    /**
+     * Returns this node's local ID.
+     *
+     * @return Node's local id.
+     */
+    @JsonGetter("id")
+    public String id() {
+        return id;
+    }
+
+    /**
+     * Returns the unique name (consistent id) of this node in a cluster. Doesn't change between restarts.
+     *
+     * @return Unique name of the member in a cluster.
+     */
+    @JsonGetter("name")
+    public String name() {
+        return name;
+    }
+
+    /**
+     * Returns the network address of this node.
+     *
+     * @return Network address of this node.
+     */
+    @JsonGetter("address")
+    public NetworkAddressDto address() {
+        return address;
+    }
+}
diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NetworkAddressDto.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NetworkAddressDto.java
new file mode 100644
index 000000000..90e7bb616
--- /dev/null
+++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/NetworkAddressDto.java
@@ -0,0 +1,89 @@
+/*
+ * 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.ignite.internal.rest.api.cluster;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.apache.ignite.network.NetworkAddress;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * REST representation of {@link NetworkAddress}.
+ */
+@Schema(name = "NetworkAddress")
+public class NetworkAddressDto {
+    /** Host. */
+    private final String host;
+
+    /** Port. */
+    private final int port;
+
+    /** Consistent id. TODO: IGNITE-16373 Temporary until ticket is not resolved. */
+    @Nullable
+    private final String consistentId;
+
+    /**
+     * Constructor.
+     *
+     * @param host Host.
+     * @param port Port.
+     * @param consistentId Consistent id.
+     */
+    @JsonCreator
+    public NetworkAddressDto(
+            @JsonProperty("host") String host,
+            @JsonProperty("port") int port,
+            @JsonProperty("consistentId") String consistentId) {
+        this.host = host;
+        this.port = port;
+        this.consistentId = consistentId;
+    }
+
+    /**
+     * Returns the host name.
+     *
+     * @return Host name.
+     */
+    @JsonGetter("host")
+    public String host() {
+        return host;
+    }
+
+    /**
+     * Returns the network port.
+     *
+     * @return Port.
+     */
+    @JsonGetter("port")
+    public int port() {
+        return port;
+    }
+
+    /**
+     * Returns the consistent id.
+     *
+     * @return Consistent id.
+     */
+    @Nullable
+    @JsonGetter("consistentId")
+    public String consistentId() {
+        return consistentId;
+    }
+}
diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/TopologyApi.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/TopologyApi.java
new file mode 100644
index 000000000..3741ee395
--- /dev/null
+++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/cluster/TopologyApi.java
@@ -0,0 +1,60 @@
+/*
+ * 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.ignite.internal.rest.api.cluster;
+
+import io.micronaut.http.annotation.Controller;
+import io.micronaut.http.annotation.Get;
+import io.micronaut.http.annotation.Produces;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import java.util.Collection;
+import org.apache.ignite.internal.rest.api.Problem;
+import org.apache.ignite.internal.rest.constants.MediaType;
+
+/**
+ * Cluster topology endpoint.
+ */
+@Controller("/management/v1/cluster/topology")
+@Tag(name = "topology")
+public interface TopologyApi {
+
+    /**
+     * Cluster physical topology.
+     */
+    @Get("physical")
+    @Operation(operationId = "physical")
+    @ApiResponse(responseCode = "200", description = "Physical topology returned")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @Produces(MediaType.APPLICATION_JSON)
+    Collection<ClusterNodeDto> physicalTopology();
+
+    /**
+     * Cluster logical topology.
+     */
+    @Get("logical")
+    @Operation(operationId = "logical")
+    @ApiResponse(responseCode = "200", description = "Logical topology returned")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @Produces(MediaType.APPLICATION_JSON)
+    Collection<ClusterNodeDto> logicalTopology();
+}
diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/configuration/ClusterConfigurationApi.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/configuration/ClusterConfigurationApi.java
index 8a79027fd..e3c033ca6 100644
--- a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/configuration/ClusterConfigurationApi.java
+++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/configuration/ClusterConfigurationApi.java
@@ -28,7 +28,6 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.media.Content;
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.rest.api.Problem;
@@ -47,16 +46,14 @@ public interface ClusterConfigurationApi {
      * @return the whole cluster configuration in HOCON format.
      */
     @Operation(operationId = "getClusterConfiguration")
-    @ApiResponses({
-            @ApiResponse(
-                    responseCode = "200",
-                    content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
-                    description = "Get cluster configuration"),
-            @ApiResponse(responseCode = "500", description = "Internal error",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class))),
-            @ApiResponse(responseCode = "400", description = "Incorrect configuration",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
-    })
+    @ApiResponse(
+            responseCode = "200",
+            content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
+            description = "Get cluster configuration")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "400", description = "Incorrect configuration",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
     @Produces({
             MediaType.TEXT_PLAIN, // todo: IGNITE-17082
             MediaType.PROBLEM_JSON
@@ -70,13 +67,11 @@ public interface ClusterConfigurationApi {
      * @param updatedConfiguration the cluster configuration to update.
      */
     @Operation(operationId = "updateClusterConfiguration")
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "Configuration updated"),
-            @ApiResponse(responseCode = "500", description = "Internal error",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class))),
-            @ApiResponse(responseCode = "400", description = "Incorrect configuration",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
-    })
+    @ApiResponse(responseCode = "200", description = "Configuration updated")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "400", description = "Incorrect configuration",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
     @Consumes(MediaType.TEXT_PLAIN) // todo: IGNITE-17082
     @Produces(MediaType.PROBLEM_JSON)
     @Patch
@@ -89,15 +84,13 @@ public interface ClusterConfigurationApi {
      * @return system configuration in HOCON format represented by given path.
      */
     @Operation(operationId = "getClusterConfigurationByPath")
-    @ApiResponses({
-            @ApiResponse(responseCode = "200",
-                    content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
-                    description = "Configuration represented by path"),
-            @ApiResponse(responseCode = "500", description = "Internal error",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class))),
-            @ApiResponse(responseCode = "400", description = "Incorrect configuration",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
-    })
+    @ApiResponse(responseCode = "200",
+            content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string")),
+            description = "Configuration represented by path")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "400", description = "Incorrect configuration",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
     @Produces({
             MediaType.TEXT_PLAIN, // todo: IGNITE-17082
             MediaType.PROBLEM_JSON
diff --git a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/configuration/NodeConfigurationApi.java b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/configuration/NodeConfigurationApi.java
index b5507582d..de001d57e 100644
--- a/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/configuration/NodeConfigurationApi.java
+++ b/modules/rest-api/src/main/java/org/apache/ignite/internal/rest/api/configuration/NodeConfigurationApi.java
@@ -28,7 +28,6 @@ import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.media.Content;
 import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.responses.ApiResponses;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import java.util.concurrent.CompletableFuture;
 import org.apache.ignite.internal.rest.api.Problem;
@@ -47,17 +46,14 @@ public interface NodeConfigurationApi {
      * @return the whole node configuration in HOCON format.
      */
     @Operation(operationId = "getNodeConfiguration")
-    @ApiResponses({
-            @ApiResponse(responseCode = "200",
-                    content = @Content(mediaType = MediaType.TEXT_PLAIN,
-                            schema = @Schema(type = "string")),
-                    description = "Whole node configuration"),
-            @ApiResponse(responseCode = "500", description = "Internal error",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class))),
-            @ApiResponse(responseCode = "400", description = "Incorrect configuration",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
-
-    })
+    @ApiResponse(responseCode = "200",
+            content = @Content(mediaType = MediaType.TEXT_PLAIN,
+                    schema = @Schema(type = "string")),
+            description = "Whole node configuration")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "400", description = "Incorrect configuration",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
     @Produces({
             MediaType.TEXT_PLAIN, // todo: IGNITE-17082
             MediaType.PROBLEM_JSON
@@ -72,17 +68,14 @@ public interface NodeConfigurationApi {
      * @return system configuration in HOCON format represented by given path.
      */
     @Operation(operationId = "getNodeConfigurationByPath")
-    @ApiResponses({
-            @ApiResponse(responseCode = "200",
-                    content = @Content(mediaType = MediaType.TEXT_PLAIN,
-                            schema = @Schema(type = "string")),
-                    description = "Configuration represented by path"),
-            @ApiResponse(responseCode = "500", description = "Internal error",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class))),
-            @ApiResponse(responseCode = "400", description = "Incorrect configuration",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
-
-    })
+    @ApiResponse(responseCode = "200",
+            content = @Content(mediaType = MediaType.TEXT_PLAIN,
+                    schema = @Schema(type = "string")),
+            description = "Configuration represented by path")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "400", description = "Incorrect configuration",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
     @Produces({
             MediaType.TEXT_PLAIN, // todo: IGNITE-17082
             MediaType.PROBLEM_JSON
@@ -96,14 +89,11 @@ public interface NodeConfigurationApi {
      * @param updatedConfiguration the node configuration to update. This is represented as a plain text.
      */
     @Operation(operationId = "updateNodeConfiguration")
-    @ApiResponses({
-            @ApiResponse(responseCode = "200", description = "Configuration updated"),
-            @ApiResponse(responseCode = "500", description = "Internal error",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class))),
-            @ApiResponse(responseCode = "400", description = "Incorrect configuration",
-                    content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
-
-    })
+    @ApiResponse(responseCode = "200", description = "Configuration updated")
+    @ApiResponse(responseCode = "500", description = "Internal error",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
+    @ApiResponse(responseCode = "400", description = "Incorrect configuration",
+            content = @Content(mediaType = MediaType.PROBLEM_JSON, schema = @Schema(implementation = Problem.class)))
     @Consumes(MediaType.TEXT_PLAIN) // todo: IGNITE-17082
     @Produces(MediaType.PROBLEM_JSON)
     @Patch
diff --git a/modules/rest/openapi/openapi.yaml b/modules/rest/openapi/openapi.yaml
index 797c2e4ce..c609314a7 100644
--- a/modules/rest/openapi/openapi.yaml
+++ b/modules/rest/openapi/openapi.yaml
@@ -61,6 +61,48 @@ paths:
             application/problem+json:
               schema:
                 $ref: '#/components/schemas/Problem'
+  /management/v1/cluster/topology/logical:
+    get:
+      tags:
+      - topology
+      operationId: logical
+      parameters: []
+      responses:
+        "200":
+          description: Logical topology returned
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/ClusterNode'
+        "500":
+          description: Internal error
+          content:
+            application/problem+json:
+              schema:
+                $ref: '#/components/schemas/Problem'
+  /management/v1/cluster/topology/physical:
+    get:
+      tags:
+      - topology
+      operationId: physical
+      parameters: []
+      responses:
+        "200":
+          description: Physical topology returned
+          content:
+            application/json:
+              schema:
+                type: array
+                items:
+                  $ref: '#/components/schemas/ClusterNode'
+        "500":
+          description: Internal error
+          content:
+            application/problem+json:
+              schema:
+                $ref: '#/components/schemas/Problem'
   /management/v1/configuration/cluster:
     get:
       tags:
@@ -252,6 +294,19 @@ paths:
                 $ref: '#/components/schemas/Problem'
 components:
   schemas:
+    ClusterNode:
+      required:
+      - address
+      - id
+      - name
+      type: object
+      properties:
+        id:
+          type: string
+        name:
+          type: string
+        address:
+          $ref: '#/components/schemas/NetworkAddress'
     ClusterState:
       required:
       - clusterTag
@@ -317,6 +372,21 @@ components:
             type: string
         clusterName:
           type: string
+    NetworkAddress:
+      required:
+      - consistentId
+      - host
+      - port
+      type: object
+      properties:
+        host:
+          type: string
+        port:
+          type: integer
+          format: int32
+        consistentId:
+          type: string
+          nullable: true
     NodeState:
       required:
       - name
diff --git a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
index be5f4b1bf..b1d7b2747 100644
--- a/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
+++ b/modules/rest/src/main/java/org/apache/ignite/internal/rest/RestComponent.java
@@ -35,6 +35,7 @@ import org.apache.ignite.internal.logger.IgniteLogger;
 import org.apache.ignite.internal.logger.Loggers;
 import org.apache.ignite.internal.manager.IgniteComponent;
 import org.apache.ignite.internal.rest.api.cluster.ClusterManagementApi;
+import org.apache.ignite.internal.rest.api.cluster.TopologyApi;
 import org.apache.ignite.internal.rest.api.configuration.ClusterConfigurationApi;
 import org.apache.ignite.internal.rest.api.configuration.NodeConfigurationApi;
 import org.apache.ignite.internal.rest.api.node.NodeManagementApi;
@@ -56,7 +57,8 @@ import org.jetbrains.annotations.Nullable;
         ClusterConfigurationApi.class,
         NodeConfigurationApi.class,
         ClusterManagementApi.class,
-        NodeManagementApi.class
+        NodeManagementApi.class,
+        TopologyApi.class
 })
 public class RestComponent implements IgniteComponent {
     /** Default port. */
diff --git a/modules/runner/pom.xml b/modules/runner/pom.xml
index 33b3158c2..0565870ae 100644
--- a/modules/runner/pom.xml
+++ b/modules/runner/pom.xml
@@ -221,6 +221,12 @@
             <type>test-jar</type>
             <scope>test</scope>
         </dependency>
+
+        <dependency>
+            <groupId>com.jayway.jsonpath</groupId>
+            <artifactId>json-path-assert</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
     <build>
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/AbstractRestTestBase.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/AbstractRestTestBase.java
new file mode 100644
index 000000000..fea4aa8bb
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/AbstractRestTestBase.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.ignite.internal.rest;
+
+import static org.apache.ignite.internal.testframework.IgniteTestUtils.testNodeName;
+import static org.apache.ignite.internal.testframework.matchers.CompletableFutureMatcher.willCompleteSuccessfully;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import org.apache.ignite.Ignite;
+import org.apache.ignite.IgnitionManager;
+import org.apache.ignite.internal.testframework.WorkDirectory;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.apache.ignite.internal.util.IgniteUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Base class for integration REST test.
+ */
+@ExtendWith(WorkDirectoryExtension.class)
+abstract class AbstractRestTestBase {
+    /** Network ports of the test nodes. */
+    static final int[] PORTS = {3344, 3345, 3346};
+
+    /** HTTP host and port url part. */
+    static final String HTTP_HOST_PORT = "http://localhost:10300";
+
+    /** Nodes bootstrap configuration. */
+    final Map<String, String> nodesBootstrapCfg = new LinkedHashMap<>();
+
+    /** Node names that is starting (before init) or started (after init). */
+    final List<String> nodeNames = new ArrayList<>();
+
+    /** Collection of started nodes. */
+    final List<Ignite> startedNodes = new ArrayList<>();
+
+    /** Collection of starting nodes. */
+    final List<CompletableFuture<Ignite>> startingNodes = new ArrayList<CompletableFuture<Ignite>>();
+
+    /** HTTP client that is expected to be defined in subclasses. */
+    HttpClient client;
+
+    /** Path to the working directory. */
+    @WorkDirectory
+    private Path workDir;
+
+    static HttpRequest get(String path) {
+        return HttpRequest.newBuilder(URI.create(HTTP_HOST_PORT + path)).build();
+    }
+
+    static HttpRequest patch(String path, String body) {
+        return HttpRequest.newBuilder(URI.create(HTTP_HOST_PORT + path))
+                .method("PATCH", BodyPublishers.ofString(body))
+                .build();
+    }
+
+    static HttpRequest post(String path, String body) {
+        return HttpRequest.newBuilder(URI.create(HTTP_HOST_PORT + path))
+                .header("content-type", "application/json")
+                .POST(BodyPublishers.ofString(body))
+                .build();
+    }
+
+    /**
+     * Before each.
+     */
+    @BeforeEach
+    void setUp(TestInfo testInfo) throws IOException, InterruptedException {
+        client = HttpClient.newBuilder().build();
+
+        startAllNodesWithoutInit(testInfo);
+    }
+
+    private void startAllNodesWithoutInit(TestInfo testInfo) {
+        String node0Name = testNodeName(testInfo, PORTS[0]);
+        String node1Name = testNodeName(testInfo, PORTS[1]);
+        String node2Name = testNodeName(testInfo, PORTS[2]);
+
+        nodesBootstrapCfg.put(
+                node0Name,
+                "{\n"
+                        + "  network: {\n"
+                        + "    port: " + PORTS[0] + ",\n"
+                        + "    nodeFinder: {\n"
+                        + "      netClusterNodes: [ \"localhost:3344\", \"localhost:3345\", \"localhost:3346\" ]\n"
+                        + "    }\n"
+                        + "  }\n"
+                        + "}"
+        );
+
+        nodesBootstrapCfg.put(
+                node1Name,
+                "{\n"
+                        + "  network: {\n"
+                        + "    port: " + PORTS[1] + ",\n"
+                        + "    nodeFinder: {\n"
+                        + "      netClusterNodes: [ \"localhost:3344\", \"localhost:3345\", \"localhost:3346\" ]\n"
+                        + "    }\n"
+                        + "  }\n"
+                        + "}"
+        );
+
+        nodesBootstrapCfg.put(
+                node2Name,
+                "{\n"
+                        + "  network: {\n"
+                        + "    port: " + PORTS[2] + ",\n"
+                        + "    nodeFinder: {\n"
+                        + "      netClusterNodes: [ \"localhost:3344\", \"localhost:3345\", \"localhost:3346\" ]\n"
+                        + "    }\n"
+                        + "  }\n"
+                        + "}"
+        );
+
+        for (Map.Entry<String, String> e : nodesBootstrapCfg.entrySet()) {
+            startNodeWithoutInit(e.getKey(), name -> IgnitionManager.start(name, e.getValue(), workDir.resolve(name)));
+        }
+    }
+
+    /**
+     * After each.
+     */
+    @AfterEach
+    void tearDown() throws Exception {
+        List<AutoCloseable> closeables = nodeNames.stream()
+                .map(name -> (AutoCloseable) () -> IgnitionManager.stop(name))
+                .collect(Collectors.toList());
+
+        IgniteUtils.closeAll(closeables);
+    }
+
+    void startNodeWithoutInit(String nodeName, Function<String, CompletableFuture<Ignite>> starter) {
+        nodeNames.add(nodeName);
+
+        CompletableFuture<Ignite> future = starter.apply(nodeName);
+
+        startingNodes.add(future);
+    }
+
+    void checkAllNodesStarted() {
+        startingNodes.forEach(future -> assertThat(future, willCompleteSuccessfully()));
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItInitializedClusterRestTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItInitializedClusterRestTest.java
new file mode 100644
index 000000000..f1ad9091d
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItInitializedClusterRestTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.ignite.internal.rest;
+
+import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import java.io.IOException;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+
+/**
+ * Test for the REST endpoints in case cluster is initialized.
+ */
+public class ItInitializedClusterRestTest extends AbstractRestTestBase {
+    @BeforeEach
+    void setUp(TestInfo testInfo) throws IOException, InterruptedException {
+        super.setUp(testInfo);
+
+        // For each test case the cluster is already initialized
+        HttpResponse<String> response = client.send(
+                post("/management/v1/cluster/init",
+                        "{\"metaStorageNodes\": [\"" + nodeNames.get(0) + "\"], \"clusterName\": \"cluster\"}"),
+                BodyHandlers.ofString()
+        );
+
+        assertThat(response.statusCode(), is(200));
+        checkAllNodesStarted();
+    }
+
+    @Test
+    @DisplayName("Node configuration is available when the cluster is initialized")
+    void nodeConfiguration() throws IOException, InterruptedException {
+        // When GET /management/v1/configuration/node
+        HttpResponse<String> response = client.send(
+                get("/management/v1/configuration/node"),
+                BodyHandlers.ofString()
+        );
+
+        // Expect node configuration can be parsed to hocon format
+        Config config = ConfigFactory.parseString(response.body());
+        // And has rest.port config value
+        assertThat(config.getInt("rest.port"), is(equalTo(10300)));
+    }
+
+    @Test
+    @DisplayName("Node configuration can be changed when the cluster is initialized")
+    void nodeConfigurationUpdate() throws IOException, InterruptedException {
+        // When PATCH /management/v1/configuration/node rest.port=10333
+        HttpResponse<String> pathResponce = client.send(
+                patch("/management/v1/configuration/node", "rest.port=10333"),
+                BodyHandlers.ofString()
+        );
+        // Then
+        assertThat(pathResponce.statusCode(), is(200));
+
+        // And GET /management/v1/configuration/node
+        HttpResponse<String> getResponse = client.send(
+                get("/management/v1/configuration/node"),
+                BodyHandlers.ofString()
+        );
+
+        // Then node configuration can be parsed to hocon format
+        Config config = ConfigFactory.parseString(getResponse.body());
+        // And rest.port is updated
+        assertThat(config.getInt("rest.port"), is(equalTo(10333)));
+    }
+
+    @Test
+    @DisplayName("Cluster configuration is available when the cluster is initialized")
+    void clusterConfiguration() throws IOException, InterruptedException {
+        // When GET /management/v1/configuration/cluster
+        HttpResponse<String> response = client.send(get("/management/v1/configuration/cluster"), BodyHandlers.ofString());
+
+        // Then cluster configuration is not available
+        assertThat(response.statusCode(), is(200));
+        // And configuration can be parsed to hocon format
+        Config config = ConfigFactory.parseString(response.body());
+        // And rocksDb.defaultRegion.cache can be read
+        assertThat(config.getString("rocksDb.defaultRegion.cache"), is(equalTo("lru")));
+    }
+
+    @Test
+    @DisplayName("Cluster configuration can be updated when the cluster is initialized")
+    void clusterConfigurationUpdate() throws IOException, InterruptedException {
+        // When PATCH /management/v1/configuration/cluster
+        HttpResponse<String> patchRequest = client.send(
+                patch("/management/v1/configuration/cluster", "rocksDb.defaultRegion.writeBufferSize=1024"),
+                BodyHandlers.ofString()
+        );
+
+        // Then
+        assertThat(patchRequest.statusCode(), is(200));
+        // And value was updated
+        HttpResponse<String> getResponse = client.send(get("/management/v1/configuration/cluster"), BodyHandlers.ofString());
+        assertThat(getResponse.statusCode(), is(200));
+        // And
+        Config config = ConfigFactory.parseString(getResponse.body());
+        assertThat(config.getInt("rocksDb.defaultRegion.writeBufferSize"), is(1024));
+    }
+
+    @Test
+    @DisplayName("Logical topology is available on initialized cluster")
+    void logicalTopology() throws IOException, InterruptedException {
+        // When GET /management/v1/cluster/topology/logical
+        HttpResponse<String> response = client.send(get("/management/v1/cluster/topology/logical"), BodyHandlers.ofString());
+
+        // Then
+        assertThat(response.statusCode(), is(200));
+        assertAll(
+                () -> assertThat(response.body(), hasJsonPath("$", hasSize(3))),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].name")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].id")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].address.host")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].address.port"))
+        );
+    }
+
+    @Test
+    @DisplayName("Physical topology is available on initialized cluster")
+    void physicalTopology() throws IOException, InterruptedException {
+        // When GET /management/v1/cluster/topology/physical
+        HttpResponse<String> response = client.send(get("/management/v1/cluster/topology/physical"), BodyHandlers.ofString());
+
+        // Then
+        assertThat(response.statusCode(), is(200));
+        assertAll(
+                () -> assertThat(response.body(), hasJsonPath("$", hasSize(3))),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].name")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].id")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].address.host")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].address.port"))
+        );
+    }
+
+    @Test
+    @DisplayName("Cluster state is available on initialized cluster")
+    void clusterState() throws IOException, InterruptedException {
+        // When GET /management/v1/cluster/status
+        HttpResponse<String> response = client.send(get("/management/v1/cluster/state"), BodyHandlers.ofString());
+
+        // Then
+        assertThat(response.statusCode(), is(200));
+        assertAll(
+                () -> assertThat(response.body(), hasJsonPath("$.clusterTag.clusterName", is(equalTo("cluster")))),
+                () -> assertThat(response.body(), hasJsonPath("$.clusterTag.clusterId")),
+                () -> assertThat(response.body(), hasJsonPath("$.igniteVersion")),
+                () -> assertThat(response.body(), hasJsonPath("$.msNodes")),
+                () -> assertThat(response.body(), hasJsonPath("$.cmgNodes"))
+        );
+    }
+
+    @Test
+    @DisplayName("Node state is available on initialized cluster")
+    void nodeState() throws IOException, InterruptedException {
+        // When GET /management/v1/node/status
+        HttpResponse<String> response = client.send(get("/management/v1/node/state"), BodyHandlers.ofString());
+
+        // Then
+        assertThat(response.statusCode(), is(200));
+        assertAll(
+                () -> assertThat(response.body(), hasJsonPath("$.name")),
+                () -> assertThat(response.body(), hasJsonPath("$.state", is(equalTo("STARTED"))))
+        );
+    }
+}
diff --git a/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItNotInitializedClusterRestTest.java b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItNotInitializedClusterRestTest.java
new file mode 100644
index 000000000..a6a97ec03
--- /dev/null
+++ b/modules/runner/src/integrationTest/java/org/apache/ignite/internal/rest/ItNotInitializedClusterRestTest.java
@@ -0,0 +1,157 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.ignite.internal.rest;
+
+import static com.jayway.jsonpath.matchers.JsonPathMatchers.hasJsonPath;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.hasSize;
+import static org.hamcrest.Matchers.is;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import com.typesafe.config.Config;
+import com.typesafe.config.ConfigFactory;
+import java.io.IOException;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
+import org.apache.ignite.internal.testframework.WorkDirectoryExtension;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+/**
+ * Test for the REST endpoints in case cluster is not initialized.
+ */
+@ExtendWith(WorkDirectoryExtension.class)
+public class ItNotInitializedClusterRestTest extends AbstractRestTestBase {
+
+    @Test
+    @DisplayName("Node configuration is available when the cluster in not initialized")
+    void nodeConfiguration() throws IOException, InterruptedException {
+        // When GET /management/v1/configuration/node
+        HttpResponse<String> response = client.send(
+                get("/management/v1/configuration/node"),
+                BodyHandlers.ofString()
+        );
+
+        // Expect node configuration can be parsed to hocon format
+        Config config = ConfigFactory.parseString(response.body());
+        // And has rest.port config value
+        assertThat(config.getInt("rest.port"), is(equalTo(10300)));
+    }
+
+    @Test
+    @DisplayName("Node configuration can be changed when the cluster in not initialized")
+    void nodeConfigurationUpdate() throws IOException, InterruptedException {
+        // When PATCH /management/v1/configuration/node rest.port=10333
+        HttpResponse<String> pathResponce = client.send(
+                patch("/management/v1/configuration/node", "rest.port=10333"),
+                BodyHandlers.ofString()
+        );
+        // Then
+        assertThat(pathResponce.statusCode(), is(200));
+
+        // And GET /management/v1/configuration/node
+        HttpResponse<String> getResponse = client.send(
+                get("/management/v1/configuration/node"),
+                BodyHandlers.ofString()
+        );
+
+        // Then node configuration can be parsed to hocon format
+        Config config = ConfigFactory.parseString(getResponse.body());
+        // And rest.port is updated
+        assertThat(config.getInt("rest.port"), is(equalTo(10333)));
+    }
+
+    @Test
+    @DisplayName("Cluster configuration is not available on not initialized cluster")
+    void clusterConfiguration() throws IOException, InterruptedException {
+        // When GET /management/v1/configuration/cluster
+        HttpResponse<String> response = client.send(get("/management/v1/configuration/cluster"), BodyHandlers.ofString());
+
+        // Expect cluster configuration is not available
+        assertThat(response.statusCode(), is(500));
+        // todo: check that human-readable problem json is returned
+        // "{\"message\":\"Internal Server Error\",\"_embedded\":{\"errors\":[{\"message\":\"Internal Server Error: null\"}]},
+        // \"_links\":{\"self\":{\"href\":\"/management/v1/configuration/cluster\",\"templated\":false}}}"
+    }
+
+    @Test
+    @DisplayName("Cluster configuration could not be updated on not initialized cluster")
+    void clusterConfigurationUpdate() throws IOException, InterruptedException {
+        // When PATCH /management/v1/configuration/cluster
+        HttpResponse<String> response = client.send(
+                patch("/management/v1/configuration/cluster", "any.key=any-value"),
+                BodyHandlers.ofString()
+        );
+
+        // Expect
+        assertThat(response.statusCode(), is(500)); //todo
+    }
+
+    @Test
+    @DisplayName("Logical topology is not available on not initialized cluster")
+    void logicalTopology() throws IOException, InterruptedException {
+        // When GET /management/v1/cluster/topology/logical
+        HttpResponse<String> response = client.send(get("/management/v1/cluster/topology/logical"), BodyHandlers.ofString());
+
+        // Then
+        assertThat(response.statusCode(), is(404));
+    }
+
+    @Test
+    @DisplayName("Physical topology is available on not initialized cluster")
+    void physicalTopology() throws IOException, InterruptedException {
+        // When GET /management/v1/cluster/topology/physical
+        HttpResponse<String> response = client.send(get("/management/v1/cluster/topology/physical"), BodyHandlers.ofString());
+
+        // Then
+        assertThat(response.statusCode(), is(200));
+        assertAll(
+                () -> assertThat(response.body(), hasJsonPath("$", hasSize(3))),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].name")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].id")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].address.host")),
+                () -> assertThat(response.body(), hasJsonPath("$[0:2].address.port"))
+        );
+    }
+
+    @Test
+    @DisplayName("Node state is available on not initialized cluster and it is STARTING")
+    void nodeState() throws IOException, InterruptedException {
+        // When GET /management/v1/node/state
+        HttpResponse<String> response = client.send(get("/management/v1/node/state"), BodyHandlers.ofString());
+
+        // Then
+        assertThat(response.statusCode(), is(200));
+        assertAll(
+                () -> assertThat(response.body(), hasJsonPath("$.name")),
+                () -> assertThat(response.body(), hasJsonPath("$.state", is(equalTo("STARTING"))))
+        );
+    }
+
+    @Test
+    @DisplayName("Cluster state is not available on not initialized cluster")
+    void clusterState() throws IOException, InterruptedException {
+        // When GET /management/v1/cluster/state
+        HttpResponse<String> response = client.send(get("/management/v1/cluster/state"), BodyHandlers.ofString());
+
+        // Then
+        assertThat(response.statusCode(), is(404));
+    }
+}
diff --git a/parent/pom.xml b/parent/pom.xml
index 96f1d6d65..c849c3346 100644
--- a/parent/pom.xml
+++ b/parent/pom.xml
@@ -514,6 +514,12 @@
                 <version>${jsonpath.version}</version>
             </dependency>
 
+            <dependency>
+                <groupId>com.jayway.jsonpath</groupId>
+                <artifactId>json-path-assert</artifactId>
+                <version>${jsonpath.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>io.github.classgraph</groupId>
                 <artifactId>classgraph</artifactId>