|
1 | | -from typing import Dict, List, Optional |
| 1 | +from typing import Any, Dict, List, Optional |
2 | 2 |
|
3 | 3 | from dstack._internal.core.backends.base.compute import ( |
4 | 4 | Compute, |
|
25 | 25 |
|
26 | 26 | logger = get_logger(__name__) |
27 | 27 |
|
| 28 | +CLOUDRIFT_VM_SSH_PORT = 22 |
| 29 | + |
28 | 30 |
|
29 | 31 | class CloudRiftCompute( |
30 | 32 | ComputeWithAllOffersCached, |
@@ -114,7 +116,7 @@ def create_instance( |
114 | 116 | region=instance_offer.region, |
115 | 117 | price=instance_offer.price, |
116 | 118 | username="riftuser", |
117 | | - ssh_port=22, |
| 119 | + ssh_port=None, |
118 | 120 | dockerized=True, |
119 | 121 | ssh_proxy=None, |
120 | 122 | backend_data=None, |
@@ -142,11 +144,115 @@ def update_provisioning_data( |
142 | 144 |
|
143 | 145 | vm_ready = vms[0].get("ready", False) |
144 | 146 | 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 |
146 | 153 |
|
147 | 154 | def terminate_instance( |
148 | 155 | self, instance_id: str, region: str, backend_data: Optional[str] = None |
149 | 156 | ): |
150 | 157 | terminated = self.client.terminate_instance(instance_id=instance_id) |
151 | 158 | if not terminated: |
152 | 159 | 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