anyscale 0.26.29__py3-none-any.whl → 0.26.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- anyscale/__init__.py +10 -0
- anyscale/_private/anyscale_client/anyscale_client.py +76 -60
- anyscale/_private/anyscale_client/common.py +39 -1
- anyscale/_private/anyscale_client/fake_anyscale_client.py +11 -0
- anyscale/_private/docgen/__main__.py +4 -0
- anyscale/_private/docgen/models.md +2 -2
- anyscale/client/README.md +2 -0
- anyscale/client/openapi_client/__init__.py +1 -0
- anyscale/client/openapi_client/api/default_api.py +118 -0
- anyscale/client/openapi_client/models/__init__.py +1 -0
- anyscale/client/openapi_client/models/baseimagesenum.py +68 -1
- anyscale/client/openapi_client/models/get_or_create_build_from_image_uri_request.py +207 -0
- anyscale/client/openapi_client/models/supportedbaseimagesenum.py +68 -1
- anyscale/cluster_compute.py +3 -8
- anyscale/commands/command_examples.py +10 -0
- anyscale/commands/job_queue_commands.py +295 -104
- anyscale/commands/list_util.py +14 -1
- anyscale/commands/machine_pool_commands.py +14 -2
- anyscale/commands/service_commands.py +6 -12
- anyscale/commands/workspace_commands_v2.py +462 -25
- anyscale/controllers/compute_config_controller.py +3 -19
- anyscale/controllers/job_controller.py +5 -210
- anyscale/job_queue/__init__.py +89 -0
- anyscale/job_queue/_private/job_queue_sdk.py +158 -0
- anyscale/job_queue/commands.py +130 -0
- anyscale/job_queue/models.py +284 -0
- anyscale/scripts.py +1 -1
- anyscale/sdk/anyscale_client/models/baseimagesenum.py +68 -1
- anyscale/sdk/anyscale_client/models/supportedbaseimagesenum.py +68 -1
- anyscale/shared_anyscale_utils/latest_ray_version.py +1 -1
- anyscale/utils/ssh_websocket_proxy.py +178 -0
- anyscale/version.py +1 -1
- {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/METADATA +3 -1
- {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/RECORD +39 -33
- {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/LICENSE +0 -0
- {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/NOTICE +0 -0
- {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/WHEEL +0 -0
- {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/entry_points.txt +0 -0
- {anyscale-0.26.29.dist-info → anyscale-0.26.31.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,15 @@
|
|
1
|
+
from dataclasses import dataclass
|
2
|
+
from enum import Enum
|
3
|
+
import importlib.resources
|
1
4
|
from io import StringIO
|
2
5
|
from json import dumps as json_dumps
|
3
6
|
import pathlib
|
7
|
+
import shlex
|
4
8
|
import subprocess
|
9
|
+
import sys
|
5
10
|
import tempfile
|
6
|
-
from typing import Optional, Tuple
|
11
|
+
from typing import List, Optional, Tuple
|
12
|
+
from urllib.parse import urlparse
|
7
13
|
|
8
14
|
import click
|
9
15
|
import yaml
|
@@ -26,6 +32,12 @@ from anyscale.workspace.models import (
|
|
26
32
|
|
27
33
|
log = BlockLogger() # CLI Logger
|
28
34
|
|
35
|
+
# Constants for SSH configuration
|
36
|
+
HTTPS_PORT = "443"
|
37
|
+
SSH_TIMEOUT_SECONDS = 45
|
38
|
+
WSS_PATH = "/sshws"
|
39
|
+
PREFERRED_AUTH_METHOD = "PreferredAuthentications=publickey"
|
40
|
+
|
29
41
|
|
30
42
|
def _validate_workspace_name_and_id(
|
31
43
|
name: Optional[str], id: Optional[str] # noqa: A002
|
@@ -37,6 +49,339 @@ def _validate_workspace_name_and_id(
|
|
37
49
|
raise click.ClickException("Only one of '--name' and '--id' can be provided.")
|
38
50
|
|
39
51
|
|
52
|
+
def _check_workspace_is_running(
|
53
|
+
name: Optional[str],
|
54
|
+
id: Optional[str], # noqa: A002
|
55
|
+
cloud: Optional[str],
|
56
|
+
project: Optional[str],
|
57
|
+
) -> None:
|
58
|
+
"""Verify that the workspace is in RUNNING state."""
|
59
|
+
try:
|
60
|
+
workspace_status = anyscale.workspace.status(
|
61
|
+
name=name, id=id, cloud=cloud, project=project
|
62
|
+
)
|
63
|
+
if workspace_status != WorkspaceState.RUNNING:
|
64
|
+
raise click.ClickException(
|
65
|
+
f"Workspace must be running to SSH into it. Current status: {workspace_status}"
|
66
|
+
)
|
67
|
+
except ValueError as e:
|
68
|
+
# Handle workspace not found or other value errors
|
69
|
+
error_msg = str(e)
|
70
|
+
if "not found" in error_msg.lower():
|
71
|
+
workspace_identifier = name if name else id
|
72
|
+
raise click.ClickException(
|
73
|
+
f"Workspace '{workspace_identifier}' not found. Please check the workspace name/id and try again."
|
74
|
+
)
|
75
|
+
else:
|
76
|
+
raise click.ClickException(f"Error checking workspace status: {error_msg}")
|
77
|
+
except (AttributeError, KeyError, TypeError):
|
78
|
+
# Handle any other errors from status check
|
79
|
+
raise click.ClickException(
|
80
|
+
"Failed to check workspace status. Please ensure the workspace exists and you have access to it."
|
81
|
+
)
|
82
|
+
|
83
|
+
|
84
|
+
def _get_workspace_directory_name(
|
85
|
+
name: Optional[str],
|
86
|
+
id: Optional[str], # noqa: A002
|
87
|
+
cloud: Optional[str],
|
88
|
+
project: Optional[str],
|
89
|
+
) -> str:
|
90
|
+
"""Get the default directory name for the workspace."""
|
91
|
+
try:
|
92
|
+
workspace_private_sdk = _LAZY_SDK_SINGLETONS[_WORKSPACE_SDK_SINGLETON_KEY]
|
93
|
+
return workspace_private_sdk.get_default_dir_name(
|
94
|
+
name=name, id=id, cloud=cloud, project=project
|
95
|
+
)
|
96
|
+
except (AttributeError, KeyError, ValueError, TypeError):
|
97
|
+
# Handle errors getting default directory name
|
98
|
+
raise click.ClickException(
|
99
|
+
"Failed to retrieve workspace configuration. Please try again later."
|
100
|
+
)
|
101
|
+
|
102
|
+
|
103
|
+
def _create_directory_setup_command(dir_name: str) -> str:
|
104
|
+
"""Create shell command to set up the workspace directory."""
|
105
|
+
# Even though dir_name comes from our API, we should still shell escape it.
|
106
|
+
dir_name_escaped = shlex.quote(dir_name)
|
107
|
+
return (
|
108
|
+
f'if [ -d "$HOME/{dir_name_escaped}" ]; then '
|
109
|
+
f'cd "$HOME/{dir_name_escaped}"; '
|
110
|
+
f"else "
|
111
|
+
f'mkdir -p "$HOME/{dir_name_escaped}" 2>/dev/null && '
|
112
|
+
f'cd "$HOME/{dir_name_escaped}" && '
|
113
|
+
f'echo "Created directory $HOME/{dir_name_escaped} (it did not exist)." >&2 || '
|
114
|
+
f'echo "Warning: Could not access or create directory $HOME/{dir_name_escaped}. Staying in home directory." >&2; '
|
115
|
+
f"fi"
|
116
|
+
)
|
117
|
+
|
118
|
+
|
119
|
+
class ConnectionType(Enum):
|
120
|
+
HTTPS = "https"
|
121
|
+
LEGACY = "legacy"
|
122
|
+
|
123
|
+
|
124
|
+
@dataclass
|
125
|
+
class SSHConfig:
|
126
|
+
"""Configuration for SSH connection to workspace."""
|
127
|
+
|
128
|
+
target_host: str
|
129
|
+
config_file: str
|
130
|
+
connection_type: ConnectionType
|
131
|
+
proxy_command: Optional[str] = None
|
132
|
+
port: Optional[str] = None
|
133
|
+
|
134
|
+
@property
|
135
|
+
def ssh_options(self) -> List[str]:
|
136
|
+
"""Get SSH options based on connection type."""
|
137
|
+
if self.connection_type == ConnectionType.HTTPS:
|
138
|
+
options = [
|
139
|
+
"-p",
|
140
|
+
self.port or HTTPS_PORT,
|
141
|
+
"-o",
|
142
|
+
PREFERRED_AUTH_METHOD,
|
143
|
+
]
|
144
|
+
if self.proxy_command:
|
145
|
+
options.extend(["-o", f"ProxyCommand={self.proxy_command}"])
|
146
|
+
return options
|
147
|
+
return []
|
148
|
+
|
149
|
+
|
150
|
+
def _setup_https_connection(
|
151
|
+
workspace_obj: Workspace, workspace_private_sdk, host_name: str, config_file: str
|
152
|
+
) -> SSHConfig:
|
153
|
+
"""Set up HTTPS connection and return SSHConfig object."""
|
154
|
+
cluster = workspace_private_sdk.client.get_workspace_cluster(workspace_obj.id)
|
155
|
+
|
156
|
+
if not cluster:
|
157
|
+
raise click.ClickException(
|
158
|
+
"Could not retrieve cluster details for the workspace."
|
159
|
+
)
|
160
|
+
|
161
|
+
# Get hostname with multiple fallback methods
|
162
|
+
public_hostname = _get_public_hostname(cluster)
|
163
|
+
|
164
|
+
# Get cluster access token
|
165
|
+
cluster_access_token = _get_cluster_access_token(cluster, workspace_private_sdk)
|
166
|
+
|
167
|
+
# Set up proxy command
|
168
|
+
proxy_cmd = _create_proxy_command(public_hostname, cluster_access_token)
|
169
|
+
|
170
|
+
return SSHConfig(
|
171
|
+
target_host=f"ray@{host_name}",
|
172
|
+
config_file=config_file,
|
173
|
+
connection_type=ConnectionType.HTTPS,
|
174
|
+
proxy_command=proxy_cmd,
|
175
|
+
port=HTTPS_PORT,
|
176
|
+
)
|
177
|
+
|
178
|
+
|
179
|
+
def _get_public_hostname(cluster) -> str:
|
180
|
+
"""Extract public hostname from cluster with fallback methods."""
|
181
|
+
# Attempt 1: Use cluster.hostname
|
182
|
+
if hasattr(cluster, "hostname") and cluster.hostname:
|
183
|
+
return cluster.hostname
|
184
|
+
|
185
|
+
# Attempt 2: Fallback to parsing webterminal_auth_url
|
186
|
+
if hasattr(cluster, "webterminal_auth_url") and cluster.webterminal_auth_url:
|
187
|
+
parsed_url = urlparse(cluster.webterminal_auth_url)
|
188
|
+
if parsed_url.netloc:
|
189
|
+
return parsed_url.netloc
|
190
|
+
|
191
|
+
# webterminal_auth_url was present but parsing failed
|
192
|
+
raise click.ClickException(
|
193
|
+
"Could not extract hostname from cluster configuration. "
|
194
|
+
"The URL appears to be malformed."
|
195
|
+
)
|
196
|
+
|
197
|
+
# Both methods failed
|
198
|
+
raise click.ClickException(
|
199
|
+
"Could not retrieve hostname for HTTPS connection. "
|
200
|
+
"Required cluster configuration is not available."
|
201
|
+
)
|
202
|
+
|
203
|
+
|
204
|
+
def _get_cluster_access_token(cluster, workspace_private_sdk) -> str:
|
205
|
+
"""Get cluster access token for HTTPS connection."""
|
206
|
+
if not hasattr(cluster, "id") or not cluster.id:
|
207
|
+
raise click.ClickException(
|
208
|
+
"Cluster configuration is incomplete, cannot retrieve access token."
|
209
|
+
)
|
210
|
+
|
211
|
+
from anyscale.client.openapi_client.exceptions import (
|
212
|
+
ApiException,
|
213
|
+
ApiTypeError,
|
214
|
+
ApiValueError,
|
215
|
+
)
|
216
|
+
|
217
|
+
try:
|
218
|
+
# We need to use the internal API here as there's no public API available
|
219
|
+
# This might be updated in future SDK versions
|
220
|
+
cluster_access_token = workspace_private_sdk.client._internal_api_client.get_cluster_access_token_api_v2_authentication_cluster_id_cluster_access_token_get( # noqa: SLF001
|
221
|
+
cluster_id=cluster.id
|
222
|
+
)
|
223
|
+
|
224
|
+
if not cluster_access_token:
|
225
|
+
raise click.ClickException(
|
226
|
+
"Failed to retrieve authentication token. Please try again."
|
227
|
+
)
|
228
|
+
|
229
|
+
return cluster_access_token
|
230
|
+
|
231
|
+
except (ApiException, ApiTypeError, ApiValueError):
|
232
|
+
# Don't expose API details in error messages
|
233
|
+
raise click.ClickException(
|
234
|
+
"Failed to authenticate. Please check your permissions and try again."
|
235
|
+
) from None
|
236
|
+
except click.ClickException:
|
237
|
+
raise # Re-raise click exceptions as-is
|
238
|
+
except (AttributeError, KeyError, ValueError, TypeError, RuntimeError) as e:
|
239
|
+
# Generic error without exposing internal details
|
240
|
+
raise click.ClickException(
|
241
|
+
"An error occurred during authentication. Please try again."
|
242
|
+
) from e
|
243
|
+
|
244
|
+
|
245
|
+
def _create_proxy_command(public_hostname: str, cluster_access_token: str) -> str:
|
246
|
+
"""Create the proxy command for HTTPS connection."""
|
247
|
+
wss_url = f"wss://{public_hostname}{WSS_PATH}"
|
248
|
+
|
249
|
+
try:
|
250
|
+
with importlib.resources.path(
|
251
|
+
"anyscale.utils", "ssh_websocket_proxy.py"
|
252
|
+
) as proxy_path:
|
253
|
+
proxy_script_path = str(proxy_path)
|
254
|
+
except (ModuleNotFoundError, ImportError) as e:
|
255
|
+
raise click.ClickException(f"Could not locate SSH proxy script: {e}") from e
|
256
|
+
|
257
|
+
# Properly escape the proxy command arguments
|
258
|
+
return " ".join(
|
259
|
+
[
|
260
|
+
shlex.quote(sys.executable),
|
261
|
+
shlex.quote(proxy_script_path),
|
262
|
+
shlex.quote(wss_url),
|
263
|
+
shlex.quote(cluster_access_token),
|
264
|
+
]
|
265
|
+
)
|
266
|
+
|
267
|
+
|
268
|
+
def _build_ssh_command(
|
269
|
+
ssh_config: SSHConfig, user_args: List[str], shell_command: str,
|
270
|
+
) -> List[str]:
|
271
|
+
"""Build the final SSH command with all options."""
|
272
|
+
# Build SSH command with basic options
|
273
|
+
base_cmd = (
|
274
|
+
["ssh"]
|
275
|
+
+ ANYSCALE_WORKSPACES_SSH_OPTIONS
|
276
|
+
+ ssh_config.ssh_options
|
277
|
+
+ [ssh_config.target_host, "-F", ssh_config.config_file]
|
278
|
+
)
|
279
|
+
|
280
|
+
# Process user-supplied arguments
|
281
|
+
if not user_args:
|
282
|
+
# No user args, use default interactive shell
|
283
|
+
return base_cmd + ["-tt", f"bash -c '{shell_command} && exec bash -i'"]
|
284
|
+
|
285
|
+
# Parse user arguments into options and commands
|
286
|
+
user_options, user_command = _parse_user_args(user_args)
|
287
|
+
|
288
|
+
ssh_cmd = base_cmd + user_options
|
289
|
+
|
290
|
+
if user_command:
|
291
|
+
# User supplied their own command, use it directly
|
292
|
+
ssh_cmd.extend(user_command)
|
293
|
+
else:
|
294
|
+
# Only options provided, add interactive shell
|
295
|
+
# Use -tt for interactive shell unless -T or -N was specified
|
296
|
+
if not any(opt in {"-T", "-N"} for opt in user_options):
|
297
|
+
ssh_cmd.append("-tt")
|
298
|
+
ssh_cmd.append(f"bash -c '{shell_command} && exec bash -i'")
|
299
|
+
|
300
|
+
return ssh_cmd
|
301
|
+
|
302
|
+
|
303
|
+
def _parse_user_args(user_args: List[str]) -> Tuple[List[str], List[str]]:
|
304
|
+
"""Parse user arguments into options and commands."""
|
305
|
+
# Find where command section starts (first non-option argument)
|
306
|
+
command_start_idx = None
|
307
|
+
for i, arg in enumerate(user_args):
|
308
|
+
if arg and not arg.startswith("-"):
|
309
|
+
command_start_idx = i
|
310
|
+
break
|
311
|
+
|
312
|
+
if command_start_idx is not None:
|
313
|
+
user_options = [arg for arg in user_args[:command_start_idx] if arg]
|
314
|
+
user_command = [arg for arg in user_args[command_start_idx:] if arg]
|
315
|
+
else:
|
316
|
+
user_options = [arg for arg in user_args if arg]
|
317
|
+
user_command = []
|
318
|
+
|
319
|
+
return user_options, user_command
|
320
|
+
|
321
|
+
|
322
|
+
def _try_https_connection(
|
323
|
+
workspace_obj: Workspace,
|
324
|
+
workspace_private_sdk,
|
325
|
+
host_name: str,
|
326
|
+
config_file: str,
|
327
|
+
ctx_args: List[str],
|
328
|
+
shell_command: str,
|
329
|
+
) -> bool:
|
330
|
+
"""Attempt HTTPS SSH connection. Returns True if successful."""
|
331
|
+
try:
|
332
|
+
cluster = workspace_private_sdk.client.get_workspace_cluster(workspace_obj.id)
|
333
|
+
if not cluster:
|
334
|
+
return False
|
335
|
+
|
336
|
+
ssh_config = _setup_https_connection(
|
337
|
+
workspace_obj, workspace_private_sdk, host_name, config_file
|
338
|
+
)
|
339
|
+
|
340
|
+
ssh_cmd = _build_ssh_command(ssh_config, ctx_args, shell_command)
|
341
|
+
|
342
|
+
# Try HTTPS connection with a timeout
|
343
|
+
result = subprocess.run(
|
344
|
+
ssh_cmd,
|
345
|
+
check=False,
|
346
|
+
timeout=SSH_TIMEOUT_SECONDS,
|
347
|
+
stderr=subprocess.DEVNULL,
|
348
|
+
)
|
349
|
+
|
350
|
+
return result.returncode == 0
|
351
|
+
|
352
|
+
except subprocess.TimeoutExpired:
|
353
|
+
print(
|
354
|
+
"HTTPS connection timed out or failed (SSH proxy may not be available), "
|
355
|
+
"falling back to Legacy SSH connection..."
|
356
|
+
)
|
357
|
+
return False
|
358
|
+
except OSError:
|
359
|
+
print(
|
360
|
+
"HTTPS connection timed out or failed (SSH proxy may not be available), "
|
361
|
+
"falling back to Legacy SSH connection..."
|
362
|
+
)
|
363
|
+
return False
|
364
|
+
except (click.ClickException, ValueError, AttributeError, KeyError, TypeError):
|
365
|
+
print("HTTPS setup failed, falling back to Legacy SSH connection...")
|
366
|
+
return False
|
367
|
+
|
368
|
+
|
369
|
+
def _execute_legacy_ssh(
|
370
|
+
ssh_target_host: str, config_file: str, ctx_args: List[str], shell_command: str,
|
371
|
+
) -> None:
|
372
|
+
"""Execute legacy SSH connection."""
|
373
|
+
legacy_ssh_config = SSHConfig(
|
374
|
+
target_host=ssh_target_host,
|
375
|
+
config_file=config_file,
|
376
|
+
connection_type=ConnectionType.LEGACY,
|
377
|
+
proxy_command=None,
|
378
|
+
port=None,
|
379
|
+
)
|
380
|
+
|
381
|
+
ssh_cmd = _build_ssh_command(legacy_ssh_config, ctx_args, shell_command)
|
382
|
+
subprocess.run(ssh_cmd, check=False)
|
383
|
+
|
384
|
+
|
40
385
|
@click.group("workspace_v2", help="Anyscale workspace commands V2.")
|
41
386
|
def workspace_cli() -> None:
|
42
387
|
pass
|
@@ -416,6 +761,12 @@ id should be used, specifying both will result in an error.
|
|
416
761
|
type=str,
|
417
762
|
help="Named project to use for the workpsace. If not provided, the default project for the cloud will be used.",
|
418
763
|
)
|
764
|
+
@click.option(
|
765
|
+
"--legacy",
|
766
|
+
is_flag=True,
|
767
|
+
default=False,
|
768
|
+
help="Use legacy SSH connection method, bypassing HTTPS SSH.",
|
769
|
+
)
|
419
770
|
@click.pass_context
|
420
771
|
def ssh(
|
421
772
|
ctx,
|
@@ -423,6 +774,7 @@ def ssh(
|
|
423
774
|
name: Optional[str],
|
424
775
|
cloud: Optional[str],
|
425
776
|
project: Optional[str],
|
777
|
+
legacy: bool,
|
426
778
|
) -> None:
|
427
779
|
"""SSH into a workspace.
|
428
780
|
|
@@ -431,34 +783,119 @@ id should be used, specifying both will result in an error.
|
|
431
783
|
|
432
784
|
You may pass extra args for the ssh command, for example to setup port forwarding:
|
433
785
|
anyscale workspace_v2 ssh -n workspace-name -- -L 9000:localhost:9000
|
786
|
+
|
787
|
+
Use the --legacy flag to bypass HTTPS SSH and use the legacy connection method directly.
|
434
788
|
"""
|
789
|
+
try:
|
790
|
+
_validate_workspace_name_and_id(name=name, id=id)
|
435
791
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
name=name, id=id, cloud=cloud, project=project
|
444
|
-
)
|
445
|
-
command = f"cd {dir_name} && /bin/bash"
|
446
|
-
with tempfile.TemporaryDirectory() as tmpdirname:
|
447
|
-
host_name, config_file = anyscale.workspace.generate_ssh_config_file(
|
448
|
-
name=name, id=id, cloud=cloud, project=project, ssh_config_path=tmpdirname
|
449
|
-
)
|
450
|
-
args = ctx.args
|
451
|
-
ssh_command = (
|
452
|
-
["ssh"]
|
453
|
-
+ ANYSCALE_WORKSPACES_SSH_OPTIONS
|
454
|
-
+ [host_name]
|
455
|
-
+ ["-F", config_file]
|
456
|
-
+ ["-tt"]
|
457
|
-
+ (args if args and len(args) > 0 else [""])
|
458
|
-
+ [f"bash -i -c {command}"]
|
792
|
+
# Verify workspace is running
|
793
|
+
_check_workspace_is_running(name, id, cloud, project)
|
794
|
+
|
795
|
+
# Inform user that connection might take time (earliest point after verifying workspace is running)
|
796
|
+
connection_mode = " (Legacy SSH)" if legacy else ""
|
797
|
+
print(
|
798
|
+
f"Connecting to workspace{connection_mode}... This might take a while. Press Ctrl+C to cancel."
|
459
799
|
)
|
460
800
|
|
461
|
-
|
801
|
+
# Get workspace directory name
|
802
|
+
dir_name = _get_workspace_directory_name(name, id, cloud, project)
|
803
|
+
|
804
|
+
# Create the shell command that will:
|
805
|
+
# 1. Try to cd to the directory
|
806
|
+
# 2. If that fails, create it and cd to it
|
807
|
+
# 3. If that fails too, just stay in home directory with a warning
|
808
|
+
shell_command = _create_directory_setup_command(dir_name)
|
809
|
+
|
810
|
+
with tempfile.TemporaryDirectory() as tmpdirname:
|
811
|
+
try:
|
812
|
+
host_name, config_file = anyscale.workspace.generate_ssh_config_file(
|
813
|
+
name=name,
|
814
|
+
id=id,
|
815
|
+
cloud=cloud,
|
816
|
+
project=project,
|
817
|
+
ssh_config_path=tmpdirname,
|
818
|
+
)
|
819
|
+
except ValueError as e:
|
820
|
+
error_msg = str(e)
|
821
|
+
if "not found" in error_msg.lower():
|
822
|
+
workspace_identifier = name if name else id
|
823
|
+
raise click.ClickException(
|
824
|
+
f"Workspace '{workspace_identifier}' not found or not accessible."
|
825
|
+
)
|
826
|
+
else:
|
827
|
+
raise click.ClickException("Failed to generate SSH configuration.")
|
828
|
+
except (
|
829
|
+
OSError,
|
830
|
+
IOError,
|
831
|
+
RuntimeError,
|
832
|
+
AttributeError,
|
833
|
+
KeyError,
|
834
|
+
TypeError,
|
835
|
+
):
|
836
|
+
# Handle any other errors from SSH config generation
|
837
|
+
raise click.ClickException(
|
838
|
+
"Failed to generate SSH configuration. Please check your network connection and try again."
|
839
|
+
)
|
840
|
+
|
841
|
+
# Get workspace and cluster information to determine connection method
|
842
|
+
ssh_target_host = host_name
|
843
|
+
|
844
|
+
# Skip HTTPS if --legacy flag is used
|
845
|
+
https_connection_successful = False
|
846
|
+
if not legacy:
|
847
|
+
# Try HTTPS first (unless legacy flag is set)
|
848
|
+
try:
|
849
|
+
workspace_obj = anyscale.workspace.get(
|
850
|
+
name=name, id=id, cloud=cloud, project=project
|
851
|
+
)
|
852
|
+
workspace_private_sdk = _LAZY_SDK_SINGLETONS[
|
853
|
+
_WORKSPACE_SDK_SINGLETON_KEY
|
854
|
+
]
|
855
|
+
cluster = workspace_private_sdk.client.get_workspace_cluster(
|
856
|
+
workspace_obj.id
|
857
|
+
)
|
858
|
+
|
859
|
+
if cluster:
|
860
|
+
https_connection_successful = _try_https_connection(
|
861
|
+
workspace_obj,
|
862
|
+
workspace_private_sdk,
|
863
|
+
host_name,
|
864
|
+
config_file,
|
865
|
+
ctx.args,
|
866
|
+
shell_command,
|
867
|
+
)
|
868
|
+
|
869
|
+
except (ValueError, AttributeError, KeyError, TypeError):
|
870
|
+
# If we can't get workspace/cluster info, proceed with legacy SSH
|
871
|
+
pass
|
872
|
+
|
873
|
+
# Run legacy SSH command if HTTPS wasn't successful or --legacy was specified
|
874
|
+
if not https_connection_successful:
|
875
|
+
_execute_legacy_ssh(
|
876
|
+
ssh_target_host, config_file, ctx.args, shell_command
|
877
|
+
)
|
878
|
+
|
879
|
+
except click.ClickException:
|
880
|
+
# Re-raise click exceptions as they already have user-friendly messages
|
881
|
+
raise
|
882
|
+
except KeyboardInterrupt:
|
883
|
+
# Handle Ctrl+C gracefully
|
884
|
+
raise click.ClickException("SSH connection cancelled by user.")
|
885
|
+
except (
|
886
|
+
OSError,
|
887
|
+
IOError,
|
888
|
+
RuntimeError,
|
889
|
+
ValueError,
|
890
|
+
AttributeError,
|
891
|
+
KeyError,
|
892
|
+
TypeError,
|
893
|
+
):
|
894
|
+
# Catch any unexpected exceptions and provide a generic user-friendly message
|
895
|
+
raise click.ClickException(
|
896
|
+
"An unexpected error occurred while establishing SSH connection. "
|
897
|
+
"Please try again or contact support if the issue persists."
|
898
|
+
)
|
462
899
|
|
463
900
|
|
464
901
|
@workspace_cli.command(
|
@@ -166,19 +166,6 @@ class ComputeConfigController(BaseController):
|
|
166
166
|
|
167
167
|
Information in output: Link to cluster compute in UI, cluster compute id
|
168
168
|
"""
|
169
|
-
enable_compute_config_versioning = self.api_client.check_is_feature_flag_on_api_v2_userinfo_check_is_feature_flag_on_get(
|
170
|
-
"compute-config-versioning"
|
171
|
-
).result.is_on
|
172
|
-
|
173
|
-
if not enable_compute_config_versioning and name and ":" in name:
|
174
|
-
log.warning(
|
175
|
-
"The compute config's name contains colons (`:`). "
|
176
|
-
"Colons will be a reserved character after 04/01/2024, "
|
177
|
-
"and before then, we are discouraging using colons in the name of compute configs. "
|
178
|
-
"Also, all existing colons will be replaced by hyphens at 04/01/2024 "
|
179
|
-
"as the versioning feature is enabled."
|
180
|
-
)
|
181
|
-
|
182
169
|
try:
|
183
170
|
cluster_compute: Dict[str, Any] = yaml.load(
|
184
171
|
cluster_compute_file, Loader=SafeLoader
|
@@ -212,9 +199,7 @@ class ComputeConfigController(BaseController):
|
|
212
199
|
|
213
200
|
cluster_compute_response = self.anyscale_api_client.create_cluster_compute(
|
214
201
|
CreateClusterCompute(
|
215
|
-
name=name,
|
216
|
-
config=cluster_compute_config,
|
217
|
-
new_version=enable_compute_config_versioning,
|
202
|
+
name=name, config=cluster_compute_config, new_version=True,
|
218
203
|
)
|
219
204
|
)
|
220
205
|
created_cluster_compute = cluster_compute_response.result
|
@@ -222,7 +207,7 @@ class ComputeConfigController(BaseController):
|
|
222
207
|
cluster_compute_name = created_cluster_compute.name
|
223
208
|
cluster_compute_version = created_cluster_compute.version
|
224
209
|
url = get_endpoint(f"/configurations/cluster-computes/{cluster_compute_id}")
|
225
|
-
if
|
210
|
+
if cluster_compute_version > 1:
|
226
211
|
log.info(f"A new version of {cluster_compute_name} was created.")
|
227
212
|
else:
|
228
213
|
log.info("A new compute config was created.")
|
@@ -230,8 +215,7 @@ class ComputeConfigController(BaseController):
|
|
230
215
|
log.info(f"View this compute config at: {url}.")
|
231
216
|
log.info(f"Compute config id: {cluster_compute_id}.")
|
232
217
|
log.info(f"Compute config name: {cluster_compute_name}.")
|
233
|
-
|
234
|
-
log.info(f"Compute config version: {cluster_compute_version}.")
|
218
|
+
log.info(f"Compute config version: {cluster_compute_version}.")
|
235
219
|
|
236
220
|
def archive(
|
237
221
|
self, compute_config_entity: Union[IdBasedEntity, NameBasedEntity]
|