Skip to content

Commit b11f807

Browse files
cyhapunTruongQuangPhat
authored andcommitted
[autoscaler] Clarify head node commands in ray up output
When running 'ray up', the terminal mixes commands from two different contexts: head node commands (from 'ray start --head') and local machine commands (from 'ray up' itself). This causes confusion because users may copy-paste head node commands (with private IPs) on their local machine. Changes: - Extract output helpers into cli_output_helpers.py (no ray C++ deps) - Add dimmed note in 'Next steps' (ray start) clarifying commands are for the head node or cluster network - Add dimmed separator and context note in 'ray up' output between head node output and local commands (no [WARN] prefix) - Split context notes into separate print calls to ensure record mode appends the correct line prefix - Gate context note on ray_start_commands so it isn't printed for --no-restart - Rename 'Useful commands:' to 'Useful commands for your local machine:' - Use ASCII '-' separator for terminal compatibility - Add test_cli_output_context.py with 11 tests that call helpers directly and assert on captured cli_logger output (and AST checks for gating) - Update test_cli_patterns to match the new output strings Signed-off-by: cyhapun <cyhapun242@gmail.com> Signed-off-by: phattruong <23120318@student.hcmus.edu.vn>
1 parent 1fd5697 commit b11f807

9 files changed

Lines changed: 242 additions & 4 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""Lightweight CLI output helpers for cluster launcher context notes.
2+
3+
These functions produce informational output that disambiguates head-node
4+
commands from local-machine commands. They have NO dependency on the ``ray``
5+
C++ runtime and accept ``cli_logger`` / ``cf`` as explicit parameters so they
6+
can be tested in isolation.
7+
"""
8+
9+
10+
def print_next_steps_context_note(cli_logger, cf):
11+
"""Print a dimmed note at the top of the 'Next steps' block.
12+
13+
Informs the user that the commands below are intended for the head node
14+
or for machines within the cluster network.
15+
"""
16+
cli_logger.print(
17+
cf.dimmed("Note: The following commands are intended for use on")
18+
)
19+
cli_logger.print(
20+
cf.dimmed("the head node or within the cluster network.")
21+
)
22+
cli_logger.newline()
23+
24+
25+
def print_head_node_context_separator(cli_logger, cf):
26+
"""Print a visual separator and context note after head-node output.
27+
28+
Used by ``ray up`` to separate the streamed ``ray start`` output from
29+
the local-machine commands that follow.
30+
"""
31+
cli_logger.print(cf.dimmed("-" * 60))
32+
cli_logger.print(
33+
cf.dimmed(
34+
"Note: The output above is from the head node (via `ray start`)."
35+
)
36+
)
37+
cli_logger.print(
38+
cf.dimmed(
39+
" Commands shown in 'Next steps' may only work from the head node"
40+
)
41+
)
42+
cli_logger.print(cf.dimmed(" or from within the cluster network."))
43+
cli_logger.newline()
44+
45+
46+
# Group heading used by ``ray up`` for the local commands section.
47+
USEFUL_COMMANDS_HEADING = "Useful commands for your local machine:"

python/ray/autoscaler/_private/commands.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@
2222
from ray.autoscaler._private import subprocess_output_util as cmd_output_util
2323
from ray.autoscaler._private.autoscaler import AutoscalerSummary
2424
from ray.autoscaler._private.cli_logger import cf, cli_logger
25+
from ray.autoscaler._private.cli_output_helpers import (
26+
USEFUL_COMMANDS_HEADING,
27+
print_head_node_context_separator,
28+
)
2529
from ray.autoscaler._private.cluster_dump import (
2630
Archive,
2731
GetParameters,
@@ -932,7 +936,9 @@ def get_or_create_head_node(
932936
modifiers = ""
933937

934938
cli_logger.newline()
935-
with cli_logger.group("Useful commands:"):
939+
if ray_start_commands:
940+
print_head_node_context_separator(cli_logger, cf)
941+
with cli_logger.group(USEFUL_COMMANDS_HEADING):
936942
printable_config_file = os.path.abspath(printable_config_file)
937943

938944
cli_logger.print("To terminate the cluster:")

python/ray/scripts/scripts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
parse_resources_json,
4242
)
4343
from ray.autoscaler._private.cli_logger import add_click_logging_options, cf, cli_logger
44+
from ray.autoscaler._private.cli_output_helpers import print_next_steps_context_note
4445
from ray.autoscaler._private.commands import (
4546
RUN_ENV_TYPES,
4647
attach_cluster,
@@ -1089,6 +1090,7 @@ def start(
10891090
cli_logger.success("-" * len(startup_msg))
10901091
cli_logger.newline()
10911092
with cli_logger.group("Next steps"):
1093+
print_next_steps_context_note(cli_logger, cf)
10921094
dashboard_url = node.address_info["webui_url"]
10931095
if ray_constants.ENABLE_RAY_CLUSTER:
10941096
cli_logger.print("To add another node to this Ray cluster, run")
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
"""Tests for CLI output context disambiguation (ray start / ray up).
2+
3+
These tests directly import and call the output helper functions from
4+
``cli_output_helpers``, capturing ``cli_logger`` calls via mocks.
5+
6+
No Ray C++ runtime (``_raylet``) is required. The helpers module is
7+
imported by path to avoid triggering ``import ray``.
8+
9+
Validates that:
10+
1. ``print_next_steps_context_note`` emits "head node" and "cluster network".
11+
2. ``print_head_node_context_separator`` emits "head node" and "cluster network".
12+
3. ``USEFUL_COMMANDS_HEADING`` contains "local machine".
13+
4. Neither helper references ``ray up`` (valid for direct ``ray start`` use).
14+
"""
15+
16+
import importlib.util
17+
import os
18+
import unittest
19+
from unittest.mock import MagicMock
20+
21+
# ---------------------------------------------------------------------------
22+
# Import cli_output_helpers by file path so we bypass ``import ray`` entirely.
23+
# ---------------------------------------------------------------------------
24+
_HELPERS_PATH = os.path.join(
25+
os.path.dirname(__file__),
26+
os.pardir,
27+
"autoscaler",
28+
"_private",
29+
"cli_output_helpers.py",
30+
)
31+
_spec = importlib.util.spec_from_file_location(
32+
"cli_output_helpers", os.path.abspath(_HELPERS_PATH)
33+
)
34+
_helpers = importlib.util.module_from_spec(_spec)
35+
_spec.loader.exec_module(_helpers)
36+
37+
print_next_steps_context_note = _helpers.print_next_steps_context_note
38+
print_head_node_context_separator = _helpers.print_head_node_context_separator
39+
USEFUL_COMMANDS_HEADING = _helpers.USEFUL_COMMANDS_HEADING
40+
41+
42+
def _capture_printed_text(helper_fn):
43+
"""Call *helper_fn(cli_logger, cf)* with mocks and return all text
44+
that was passed to ``cli_logger.print`` as a single concatenated string.
45+
46+
``cf.dimmed`` is mocked as an identity function so the raw text
47+
passes through unchanged.
48+
"""
49+
mock_logger = MagicMock()
50+
mock_cf = MagicMock()
51+
# cf.dimmed should return its argument so we can inspect the raw text
52+
mock_cf.dimmed = lambda x: x
53+
54+
helper_fn(mock_logger, mock_cf)
55+
56+
parts = []
57+
for c in mock_logger.print.call_args_list:
58+
args, _ = c
59+
parts.extend(str(a) for a in args)
60+
return "\n".join(parts)
61+
62+
63+
class TestNextStepsContextNote(unittest.TestCase):
64+
"""Tests for ``print_next_steps_context_note``."""
65+
66+
def test_mentions_head_node(self):
67+
text = _capture_printed_text(print_next_steps_context_note)
68+
self.assertIn("head node", text)
69+
70+
def test_mentions_cluster_network(self):
71+
text = _capture_printed_text(print_next_steps_context_note)
72+
self.assertIn("cluster network", text)
73+
74+
def test_calls_newline(self):
75+
mock_logger = MagicMock()
76+
mock_cf = MagicMock()
77+
mock_cf.dimmed = lambda x: x
78+
print_next_steps_context_note(mock_logger, mock_cf)
79+
mock_logger.newline.assert_called()
80+
81+
def test_does_not_mention_ray_up(self):
82+
text = _capture_printed_text(print_next_steps_context_note)
83+
self.assertNotIn("ray up", text)
84+
85+
86+
class TestHeadNodeContextSeparator(unittest.TestCase):
87+
"""Tests for ``print_head_node_context_separator``."""
88+
89+
def test_mentions_head_node(self):
90+
text = _capture_printed_text(print_head_node_context_separator)
91+
self.assertIn("head node", text)
92+
93+
def test_mentions_cluster_network(self):
94+
text = _capture_printed_text(print_head_node_context_separator)
95+
self.assertIn("cluster network", text)
96+
97+
def test_prints_ascii_separator(self):
98+
text = _capture_printed_text(print_head_node_context_separator)
99+
# Should contain a run of ASCII dashes as a visual separator
100+
self.assertIn("----", text)
101+
102+
def test_calls_newline(self):
103+
mock_logger = MagicMock()
104+
mock_cf = MagicMock()
105+
mock_cf.dimmed = lambda x: x
106+
print_head_node_context_separator(mock_logger, mock_cf)
107+
mock_logger.newline.assert_called()
108+
109+
110+
class TestUsefulCommandsHeading(unittest.TestCase):
111+
"""Tests for the ``USEFUL_COMMANDS_HEADING`` constant."""
112+
113+
def test_contains_local_machine(self):
114+
self.assertIn("local machine", USEFUL_COMMANDS_HEADING)
115+
116+
def test_does_not_use_old_label(self):
117+
# The old heading was exactly "Useful commands:" with no qualifier
118+
self.assertNotEqual(USEFUL_COMMANDS_HEADING, "Useful commands:")
119+
120+
121+
class TestHeadNodeContextGating(unittest.TestCase):
122+
"""Verify that the head node context separator is only printed when
123+
`ray start` commands are actually executed.
124+
"""
125+
126+
def test_separator_gated_by_ray_start_commands(self):
127+
import ast
128+
129+
# Parse commands.py to verify the gating logic without importing it
130+
filepath = os.path.join(
131+
os.path.dirname(__file__),
132+
os.pardir,
133+
"autoscaler",
134+
"_private",
135+
"commands.py",
136+
)
137+
with open(filepath, "r", encoding="utf-8") as f:
138+
source = f.read()
139+
140+
tree = ast.parse(source)
141+
142+
call_found = False
143+
# Find the call to print_head_node_context_separator
144+
for node in ast.walk(tree):
145+
if isinstance(node, ast.If):
146+
# Check if this If block contains our call
147+
for child in ast.walk(node):
148+
if isinstance(child, ast.Call) and getattr(
149+
child.func, "id", None
150+
) == "print_head_node_context_separator":
151+
# Verify the condition of the If statement is ray_start_commands
152+
test_node = node.test
153+
self.assertIsInstance(test_node, ast.Name)
154+
self.assertEqual(test_node.id, "ray_start_commands")
155+
call_found = True
156+
157+
self.assertTrue(
158+
call_found, "Could not find conditional call to print_head_node_context_separator"
159+
)
160+
161+
162+
if __name__ == "__main__":
163+
unittest.main()

python/ray/tests/test_cli_patterns/test_ray_start.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Ray runtime started.
66
--------------------
77

88
Next steps
9+
Note: The following commands are intended for use on
10+
the head node or within the cluster network\.
11+
912
To add another node to this Ray cluster, run
1013
ray start --address='.+'
1114

python/ray/tests/test_cli_patterns/test_ray_start_windows_osx.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ Ray runtime started.
77
--------------------
88

99
Next steps
10+
Note: The following commands are intended for use on
11+
the head node or within the cluster network\.
12+
1013
To add another node to this Ray cluster, run
1114
RAY_ENABLE_WINDOWS_OR_OSX_CLUSTER=1 ray start --address='.+'
1215

python/ray/tests/test_cli_patterns/test_ray_up.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ Acquiring an up-to-date head node
4141
\[7/7\] Starting the Ray runtime
4242
New status: up-to-date
4343

44-
Useful commands:
44+
-{60}
45+
Note: The output above is from the head node \(via `ray start`\)\.
46+
Commands shown in 'Next steps' may only work from the head node
47+
or from within the cluster network\.
48+
49+
Useful commands for your local machine:
4550
To terminate the cluster:
4651
ray down .+
4752

python/ray/tests/test_cli_patterns/test_ray_up_docker.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,12 @@ Acquiring an up-to-date head node
4141
\[7/7\] Starting the Ray runtime
4242
New status: up-to-date
4343

44-
Useful commands:
44+
-{60}
45+
Note: The output above is from the head node \(via `ray start`\)\.
46+
Commands shown in 'Next steps' may only work from the head node
47+
or from within the cluster network\.
48+
49+
Useful commands for your local machine:
4550
To terminate the cluster:
4651
ray down .+
4752

python/ray/tests/test_cli_patterns/test_ray_up_record.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,11 @@
8484
.+\.py.*AWSNodeProvider: Set tag ray-runtime-config=.+ on \['.+'\] \[LogTimer=.+\]
8585
.+\.py.*AWSNodeProvider: Set tag ray-file-mounts-contents=.+ on \['.+'\] \[LogTimer=.+\]
8686
.+\.py.*New status: up-to-date
87-
.+\.py.*Useful commands:
87+
.+\.py.*-{60}
88+
.+\.py.*Note: The output above is from the head node \(via `ray start`\)\.
89+
.+\.py.* Commands shown in 'Next steps' may only work from the head node
90+
.+\.py.* or from within the cluster network\.
91+
.+\.py.*Useful commands for your local machine:
8892
.+\.py.*To terminate the cluster:
8993
.+\.py.* ray down .+
9094
.+\.py.*To retrieve the IP address of the cluster head:

0 commit comments

Comments
 (0)