Skip to content

Commit 0673ace

Browse files
peterschmidt85Andrey Cheptsov
andauthored
Fix CloudRift SSH port mapping (#4005)
Co-authored-by: Andrey Cheptsov <andrey.cheptsov@github.com>
1 parent 971b866 commit 0673ace

2 files changed

Lines changed: 391 additions & 3 deletions

File tree

src/dstack/_internal/core/backends/cloudrift/compute.py

Lines changed: 109 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, List, Optional
1+
from typing import Any, Dict, List, Optional
22

33
from dstack._internal.core.backends.base.compute import (
44
Compute,
@@ -25,6 +25,8 @@
2525

2626
logger = get_logger(__name__)
2727

28+
CLOUDRIFT_VM_SSH_PORT = 22
29+
2830

2931
class CloudRiftCompute(
3032
ComputeWithAllOffersCached,
@@ -114,7 +116,7 @@ def create_instance(
114116
region=instance_offer.region,
115117
price=instance_offer.price,
116118
username="riftuser",
117-
ssh_port=22,
119+
ssh_port=None,
118120
dockerized=True,
119121
ssh_proxy=None,
120122
backend_data=None,
@@ -142,11 +144,115 @@ def update_provisioning_data(
142144

143145
vm_ready = vms[0].get("ready", False)
144146
if vm_ready:
145-
provisioning_data.hostname = instance_info.get("host_address", None)
147+
hostname = instance_info.get("host_address", None)
148+
ssh_port = _get_vm_ssh_port(instance_info)
149+
if hostname is None or ssh_port is None:
150+
return
151+
provisioning_data.hostname = hostname
152+
provisioning_data.ssh_port = ssh_port
146153

147154
def terminate_instance(
148155
self, instance_id: str, region: str, backend_data: Optional[str] = None
149156
):
150157
terminated = self.client.terminate_instance(instance_id=instance_id)
151158
if not terminated:
152159
raise ComputeError(f"Failed to terminate instance {instance_id} in region {region}.")
160+
161+
162+
def _get_vm_ssh_port(instance_info: Dict) -> Optional[int]:
163+
if ssh_port := _get_vm_ssh_port_from_port_mappings(instance_info):
164+
return ssh_port
165+
if ssh_port := _get_vm_ssh_port_from_instructions(instance_info):
166+
return ssh_port
167+
if _has_unusable_vm_ssh_port_data(instance_info):
168+
return None
169+
return CLOUDRIFT_VM_SSH_PORT
170+
171+
172+
def _get_vm_ssh_port_from_port_mappings(instance_info: Dict) -> Optional[int]:
173+
port_mappings = instance_info.get("port_mappings")
174+
if not isinstance(port_mappings, list):
175+
return None
176+
for port_mapping in port_mappings:
177+
parsed_mapping = _parse_port_mapping(port_mapping)
178+
if parsed_mapping is None:
179+
continue
180+
vm_port, host_port = parsed_mapping
181+
if vm_port == CLOUDRIFT_VM_SSH_PORT:
182+
return host_port
183+
return None
184+
185+
186+
def _get_vm_ssh_port_from_instructions(instance_info: Dict) -> Optional[int]:
187+
instructions = instance_info.get("instructions")
188+
if not isinstance(instructions, dict):
189+
return None
190+
placeholder_values = instructions.get("placeholder_values")
191+
if not isinstance(placeholder_values, list):
192+
return None
193+
for placeholder_value in placeholder_values:
194+
ssh_port = _parse_ssh_port_placeholder_value(placeholder_value)
195+
if ssh_port is not None:
196+
return ssh_port
197+
return None
198+
199+
200+
def _has_unusable_vm_ssh_port_data(instance_info: Dict) -> bool:
201+
return isinstance(instance_info.get("port_mappings"), list) or _has_ssh_port_placeholder(
202+
instance_info
203+
)
204+
205+
206+
def _has_ssh_port_placeholder(instance_info: Dict) -> bool:
207+
instructions = instance_info.get("instructions")
208+
if not isinstance(instructions, dict):
209+
return False
210+
placeholder_values = instructions.get("placeholder_values")
211+
if not isinstance(placeholder_values, list):
212+
return False
213+
return any(_is_ssh_port_placeholder_value(value) for value in placeholder_values)
214+
215+
216+
def _parse_ssh_port_placeholder_value(placeholder_value: Any) -> Optional[int]:
217+
if not _is_ssh_port_placeholder_value(placeholder_value):
218+
return None
219+
return _parse_ssh_port_option(placeholder_value[1])
220+
221+
222+
def _is_ssh_port_placeholder_value(placeholder_value: Any) -> bool:
223+
return (
224+
isinstance(placeholder_value, (list, tuple))
225+
and len(placeholder_value) >= 2
226+
and placeholder_value[0] == "SSH_PORT"
227+
)
228+
229+
230+
def _parse_ssh_port_option(ssh_port_option: Any) -> Optional[int]:
231+
if not isinstance(ssh_port_option, str):
232+
return None
233+
if ssh_port_option.strip() == "":
234+
return CLOUDRIFT_VM_SSH_PORT
235+
parts = ssh_port_option.split()
236+
if len(parts) != 2 or parts[0] != "-p":
237+
return None
238+
return _parse_port(parts[1])
239+
240+
241+
def _parse_port_mapping(port_mapping: Any) -> Optional[tuple[int, int]]:
242+
if not isinstance(port_mapping, (list, tuple)) or len(port_mapping) < 2:
243+
return None
244+
vm_port = _parse_port(port_mapping[0])
245+
host_port = _parse_port(port_mapping[1])
246+
if vm_port is None or host_port is None:
247+
return None
248+
return vm_port, host_port
249+
250+
251+
def _parse_port(port: Any) -> Optional[int]:
252+
try:
253+
parsed_port = int(port)
254+
except (TypeError, ValueError):
255+
return None
256+
if parsed_port <= 0 or parsed_port > 65535:
257+
return None
258+
return parsed_port

0 commit comments

Comments
 (0)