Repository: iterative/dvc

Issue:
machine: list status of created instances
* [x] `dvc machine status` should list status of running (`dvc machine create`'d ) instances

Output should be similar to what is displayed in `terraform show` (cloud, IP, instance sizes, etc)

Related to #6263


Discussion:

Resolving patch:
diff --git a/dvc/command/machine.py b/dvc/command/machine.py
--- a/dvc/command/machine.py
+++ b/dvc/command/machine.py
@@ -143,6 +143,22 @@ def run(self):
         return 0
 
 
+class CmdMachineStatus(CmdBase):
+    SHOWN_FIELD = ["name", "cloud", "instance_hdd_size", "instance_ip"]
+
+    def run(self):
+        if self.repo.machine is None:
+            raise MachineDisabledError
+
+        all_status = self.repo.machine.status(self.args.name)
+        for i, status in enumerate(all_status, start=1):
+            ui.write(f"instance_num_{i}:")
+            for field in self.SHOWN_FIELD:
+                value = status.get(field, None)
+                ui.write(f"\t{field:20}: {value}")
+        return 0
+
+
 class CmdMachineDestroy(CmdBase):
     def run(self):
         if self.repo.machine is None:
@@ -306,6 +322,19 @@ def add_parser(subparsers, parent_parser):
     )
     machine_create_parser.set_defaults(func=CmdMachineCreate)
 
+    machine_STATUS_HELP = "List the status of a running machine."
+    machine_status_parser = machine_subparsers.add_parser(
+        "status",
+        parents=[parent_config_parser, parent_parser],
+        description=append_doc_link(machine_STATUS_HELP, "machine/status"),
+        help=machine_STATUS_HELP,
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+    )
+    machine_status_parser.add_argument(
+        "name", help="Name of the running machine."
+    )
+    machine_status_parser.set_defaults(func=CmdMachineStatus)
+
     machine_DESTROY_HELP = "Destroy an machine instance."
     machine_destroy_parser = machine_subparsers.add_parser(
         "destroy",
diff --git a/dvc/machine/__init__.py b/dvc/machine/__init__.py
--- a/dvc/machine/__init__.py
+++ b/dvc/machine/__init__.py
@@ -2,8 +2,10 @@
 import os
 from typing import (
     TYPE_CHECKING,
+    Any,
     Dict,
     Iterable,
+    Iterator,
     Mapping,
     Optional,
     Tuple,
@@ -200,3 +202,7 @@ def get_sshfs(self, name: Optional[str]):
     def run_shell(self, name: Optional[str]):
         config, backend = self.get_config_and_backend(name)
         return backend.run_shell(**config)
+
+    def status(self, name: str) -> Iterator[Dict[Any, Any]]:
+        config, backend = self.get_config_and_backend(name)
+        return backend.instances(**config)


Test patch:
diff --git a/tests/func/machine/__init__.py b/tests/func/machine/__init__.py
--- a/tests/func/machine/__init__.py
+++ b/tests/func/machine/__init__.py
@@ -0,0 +1,10 @@
+import textwrap
+
+CONFIG_TEXT = textwrap.dedent(
+    """\
+        [feature]
+            machine = true
+        ['machine "foo"']
+            cloud = aws
+    """
+)
diff --git a/tests/func/machine/test_machine_config.py b/tests/func/machine/test_machine_config.py
--- a/tests/func/machine/test_machine_config.py
+++ b/tests/func/machine/test_machine_config.py
@@ -5,14 +5,7 @@
 
 from dvc.main import main
 
-CONFIG_TEXT = textwrap.dedent(
-    """\
-        [feature]
-            machine = true
-        ['machine \"foo\"']
-            cloud = aws
-    """
-)
+from . import CONFIG_TEXT
 
 
 @pytest.mark.parametrize(
diff --git a/tests/func/machine/test_machine_status.py b/tests/func/machine/test_machine_status.py
new file mode 100644
--- /dev/null
+++ b/tests/func/machine/test_machine_status.py
@@ -0,0 +1,43 @@
+import json
+
+from tpi.terraform import TerraformBackend
+
+from dvc.command.machine import CmdMachineStatus
+from dvc.main import main
+
+from . import CONFIG_TEXT
+
+STATUS = """{
+    "aws_security_group": null,
+    "cloud": "aws",
+    "id": "iterative-2jyhw8j9ieov6",
+    "image": "ubuntu-bionic-18.04-amd64-server-20210818",
+    "instance_gpu": null,
+    "instance_hdd_size": 35,
+    "instance_ip": "123.123.123.123",
+    "instance_launch_time": "2021-08-25T07:13:03Z",
+    "instance_type": "m",
+    "name": "test-resource",
+    "region": "us-west",
+    "spot": false,
+    "spot_price": -1,
+    "ssh_name": null,
+    "ssh_private": "-----BEGIN RSA PRIVATE KEY-----\\n",
+    "startup_script": "IyEvYmluL2Jhc2g=",
+    "timeouts": null
+}"""
+
+
+def test_status(tmp_dir, scm, dvc, mocker, capsys):
+    (tmp_dir / ".dvc" / "config").write_text(CONFIG_TEXT)
+    status = json.loads(STATUS)
+
+    mocker.patch.object(
+        TerraformBackend, "instances", autospec=True, return_value=[status]
+    )
+
+    assert main(["machine", "status", "foo"]) == 0
+    cap = capsys.readouterr()
+    assert "instance_num_1:" in cap.out
+    for key in CmdMachineStatus.SHOWN_FIELD:
+        assert f"\t{key:20}: {status[key]}" in cap.out
diff --git a/tests/unit/command/test_machine.py b/tests/unit/command/test_machine.py
--- a/tests/unit/command/test_machine.py
+++ b/tests/unit/command/test_machine.py
@@ -9,6 +9,7 @@
     CmdMachineModify,
     CmdMachineRemove,
     CmdMachineSsh,
+    CmdMachineStatus,
 )
 
 DATA = {
@@ -56,6 +57,19 @@ def test_create(tmp_dir, dvc, mocker):
     m.assert_called_once_with("foo")
 
 
+def test_status(tmp_dir, dvc, mocker):
+    cli_args = parse_args(["machine", "status", "foo"])
+    assert cli_args.func == CmdMachineStatus
+
+    cmd = cli_args.func(cli_args)
+    m = mocker.patch.object(
+        cmd.repo.machine, "status", autospec=True, return_value=[]
+    )
+
+    assert cmd.run() == 0
+    m.assert_called_once_with("foo")
+
+
 def test_destroy(tmp_dir, dvc, mocker):
     cli_args = parse_args(["machine", "destroy", "foo"])
     assert cli_args.func == CmdMachineDestroy