Skip to content

Commit 6ed3d90

Browse files
authored
Merge pull request #1963 from transformerlab/add/nebius-cloud
Add nebius as a cloud provider
2 parents ca1f219 + 86f02c4 commit 6ed3d90

19 files changed

Lines changed: 1728 additions & 26 deletions

File tree

api/install.sh

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,18 @@ multiuser_setup() {
593593
echo "Installing SkyPilot with Kubernetes support..."
594594
uv pip install --python "${GENERAL_UV_ENV_DIR}/bin/python" "skypilot[kubernetes]==0.12.1"
595595

596+
# Nebius AI Cloud CLI (used by the Nebius compute provider; default install path ~/.nebius/bin/nebius).
597+
if command -v curl &> /dev/null; then
598+
if [[ -x "${HOME}/.nebius/bin/nebius" ]]; then
599+
ohai "Nebius CLI already installed at ${HOME}/.nebius/bin/nebius"
600+
else
601+
ohai "Installing Nebius CLI..."
602+
curl -sSL https://storage.eu-north1.nebius.cloud/cli/install.sh | bash
603+
fi
604+
else
605+
warn "curl is not available; skipping Nebius CLI install. Install manually if you use Nebius providers."
606+
fi
607+
596608
echo "Multiuser setup complete."
597609

598610
}

api/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies = [
1919
"fastapi==0.125.0",
2020
"fastapi-users[sqlalchemy,oauth]==15.0.5",
2121
"httpx-oauth==0.16.1",
22+
"nebius==0.3.65",
2223
"packaging==26.2",
2324
"psutil==7.2.2",
2425
"pydantic>=2.13.4,<3.0",
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Tests for Nebius automatic subnet resolution."""
2+
3+
from __future__ import annotations
4+
5+
from unittest.mock import MagicMock
6+
7+
import pytest
8+
9+
from transformerlab.compute_providers.models import ClusterConfig
10+
from transformerlab.compute_providers.nebius import NEBIUS_AUTO_SUBNET_NAME, NebiusProvider
11+
12+
13+
def test_resolve_subnet_uses_configured_id() -> None:
14+
p = NebiusProvider(team_id="t1", parent_id="proj", subnet_id="subnet-fixed")
15+
assert p._resolve_subnet_id_for_launch() == "subnet-fixed"
16+
17+
18+
def test_resolve_subnet_reuses_existing_list() -> None:
19+
p = NebiusProvider(team_id="t1", parent_id="proj-a", subnet_id=None)
20+
p._run_nebius = MagicMock(return_value={"subnets": [{"metadata": {"id": "sn-1", "name": "s1"}}]})
21+
assert p._resolve_subnet_id_for_launch() == "sn-1"
22+
p._run_nebius.assert_called_once()
23+
assert p._resolved_subnet_id == "sn-1"
24+
25+
26+
def test_resolve_subnet_creates_network_then_subnet() -> None:
27+
p = NebiusProvider(team_id="t1", parent_id="proj-b", subnet_id=None)
28+
subnet_list_returns: list = [[], []]
29+
30+
def fake_run(args: list, stdin_json=None, timeout=120):
31+
joined = " ".join(args)
32+
if "subnet list" in joined:
33+
return {"subnets": subnet_list_returns.pop(0)}
34+
if "network list" in joined:
35+
return {"networks": []}
36+
if "create-default" in joined:
37+
return {"metadata": {"id": "net-new"}}
38+
if "subnet create" in joined:
39+
return {"metadata": {"id": "sn-created"}}
40+
raise AssertionError(f"unexpected nebius call: {joined}")
41+
42+
p._run_nebius = MagicMock(side_effect=fake_run)
43+
assert p._resolve_subnet_id_for_launch() == "sn-created"
44+
create_calls = [c for c in p._run_nebius.call_args_list if "subnet create" in " ".join(c[0][0])]
45+
assert len(create_calls) == 1
46+
create_args = create_calls[0][0][0]
47+
assert NEBIUS_AUTO_SUBNET_NAME in create_args
48+
49+
50+
def test_resolve_subnet_requires_parent_when_no_subnet() -> None:
51+
p = NebiusProvider(team_id="t1", parent_id=None, subnet_id=None)
52+
with pytest.raises(ValueError, match="parent_id"):
53+
p._resolve_subnet_id_for_launch()
54+
55+
56+
def test_cloud_init_includes_self_termination_trap() -> None:
57+
p = NebiusProvider(team_id="t1")
58+
user_data = p._build_cloud_init("cluster-a", ClusterConfig(run="echo hello"), public_key="ssh-ed25519 AAAATEST")
59+
assert "_tfl_self_terminate" in user_data
60+
assert "trap _tfl_self_terminate EXIT" in user_data
61+
62+
63+
def test_cloud_init_self_termination_fallbacks() -> None:
64+
p = NebiusProvider(team_id="t1")
65+
user_data = p._build_cloud_init("cluster-a", ClusterConfig(run="echo hello"), public_key="ssh-ed25519 AAAATEST")
66+
assert "_tfl_self_delete_instance" in user_data
67+
assert "_tfl_ensure_nebius_cli" in user_data
68+
assert "https://storage.eu-north1.nebius.cloud/cli/install.sh | bash" in user_data
69+
assert 'export PATH="$HOME/.nebius/bin:$PATH"' in user_data
70+
assert 'nebius compute instance delete --id "$_iid" --async' in user_data
71+
assert "shutdown -h now || poweroff || true" in user_data

api/transformerlab/compute_providers/config.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,16 @@ class ComputeProviderConfig(BaseModel):
5656
service_account_json: Optional[Dict[str, Any]] = None
5757
service_account_email: Optional[str] = None
5858

59+
# Nebius-specific config
60+
nebius_profile: Optional[str] = None # Nebius CLI profile name
61+
nebius_config_path: Optional[str] = None # Provider-scoped Nebius CLI config path
62+
parent_id: Optional[str] = None # Nebius project/parent ID
63+
subnet_id: Optional[str] = None # Nebius VPC subnet ID
64+
default_platform: Optional[str] = None # e.g. "gpu-h100-sxm" or "cpu-d3"
65+
default_preset: Optional[str] = None # e.g. "1gpu-16vcpu-200gb"
66+
boot_image_family: Optional[str] = None # e.g. "ubuntu24.04-cuda13.0"
67+
disk_size_gib: Optional[int] = None
68+
5969
# Additional provider-specific config
6070
extra_config: Dict[str, Any] = Field(default_factory=dict)
6171

@@ -265,6 +275,29 @@ def create_compute_provider(config: ComputeProviderConfig) -> "ComputeProvider":
265275
team_id=config.team_id,
266276
extra_config=config.extra_config,
267277
)
278+
elif config.type == "nebius":
279+
from .nebius import NebiusProvider
280+
281+
if not config.team_id:
282+
raise ValueError("Nebius provider requires team_id in config")
283+
if not config.subnet_id and not config.parent_id:
284+
raise ValueError(
285+
"Nebius provider requires parent_id (Nebius project) for automatic default network/subnet, "
286+
"or subnet_id if you use your own VPC subnet"
287+
)
288+
return NebiusProvider(
289+
team_id=config.team_id,
290+
parent_id=config.parent_id,
291+
subnet_id=config.subnet_id,
292+
profile=config.nebius_profile,
293+
config_path=config.nebius_config_path,
294+
default_platform=config.default_platform,
295+
default_preset=config.default_preset,
296+
boot_image_family=config.boot_image_family,
297+
disk_size_gib=config.disk_size_gib,
298+
ssh_user=config.ssh_user,
299+
extra_config=config.extra_config,
300+
)
268301
elif config.type == "vastai":
269302
from .vastai import VastAIProvider
270303

0 commit comments

Comments
 (0)