22import time
33from collections import defaultdict
44from collections .abc import Container as ContainerT
5- from collections .abc import Generator
5+ from collections .abc import Generator , Iterable , Sequence
66from contextlib import contextmanager
77from tempfile import NamedTemporaryFile
8+ from typing import Optional
89
910from nebius .aio .authorization .options import options_to_metadata
1011from nebius .aio .operation import Operation as SDKOperation
4041from nebius .api .nebius .vpc .v1 import ListSubnetsRequest , Subnet , SubnetServiceClient
4142from nebius .sdk import SDK
4243
43- from dstack ._internal .core .backends .nebius .models import NebiusServiceAccountCreds
44+ from dstack ._internal .core .backends .base .configurator import raise_invalid_credentials_error
45+ from dstack ._internal .core .backends .nebius .models import (
46+ DEFAULT_PROJECT_NAME_PREFIX ,
47+ NebiusServiceAccountCreds ,
48+ )
4449from dstack ._internal .core .errors import BackendError , NoCapacityError
4550from dstack ._internal .utils .event_loop import DaemonEventLoop
4651from dstack ._internal .utils .logging import get_logger
@@ -110,7 +115,37 @@ def wait_for_operation(
110115 LOOP .await_ (op .update (timeout = REQUEST_TIMEOUT , metadata = REQUEST_MD ))
111116
112117
113- def get_region_to_project_id_map (sdk : SDK ) -> dict [str , str ]:
118+ def get_region_to_project_id_map (
119+ sdk : SDK , configured_regions : Optional [list [str ]], configured_project_ids : Optional [list [str ]]
120+ ) -> dict [str , str ]:
121+ """Validate backend settings and build region->project_id map"""
122+
123+ projects = list_tenant_projects (sdk )
124+ if configured_regions :
125+ validate_regions (
126+ configured = set (configured_regions ), available = {p .status .region for p in projects }
127+ )
128+ if configured_project_ids is not None :
129+ return _get_region_to_configured_project_id_map (
130+ projects , configured_project_ids , configured_regions
131+ )
132+ else :
133+ return _get_region_to_default_project_id_map (projects , configured_regions )
134+
135+
136+ def validate_regions (configured : set [str ], available : set [str ]) -> None :
137+ if invalid := set (configured ) - available :
138+ raise_invalid_credentials_error (
139+ fields = [["regions" ]],
140+ details = (
141+ f"Configured regions { invalid } do not exist in this Nebius tenancy."
142+ " Omit `regions` to use all regions or select some of the available regions:"
143+ f" { available } "
144+ ),
145+ )
146+
147+
148+ def list_tenant_projects (sdk : SDK ) -> Sequence [Container ]:
114149 tenants = LOOP .await_ (
115150 TenantServiceClient (sdk ).list (
116151 ListTenantsRequest (), timeout = REQUEST_TIMEOUT , metadata = REQUEST_MD
@@ -126,26 +161,72 @@ def get_region_to_project_id_map(sdk: SDK) -> dict[str, str]:
126161 metadata = REQUEST_MD ,
127162 )
128163 )
164+ return projects .items
165+
166+
167+ def _get_region_to_default_project_id_map (
168+ all_tenant_projects : Iterable [Container ], configured_regions : Optional [list [str ]]
169+ ) -> dict [str , str ]:
129170 region_to_projects : defaultdict [str , list [Container ]] = defaultdict (list )
130- for project in projects . items :
171+ for project in all_tenant_projects :
131172 region_to_projects [project .status .region ].append (project )
132173 region_to_project_id = {}
133174 for region , region_projects in region_to_projects .items ():
175+ if configured_regions and region not in configured_regions :
176+ continue
134177 if len (region_projects ) != 1 :
135- # Currently, there can only be one project per region.
136- # This condition is implemented just in case Nebius suddenly allows more projects.
137178 region_projects = [
138- p for p in region_projects if p .metadata .name .startswith ("default-project" )
179+ p
180+ for p in region_projects
181+ if p .metadata .name .startswith (DEFAULT_PROJECT_NAME_PREFIX )
139182 ]
140183 if len (region_projects ) != 1 :
141- logger .warning (
142- "Could not find the default project in region %s, tenant %s" , region , tenant_id
184+ raise_invalid_credentials_error (
185+ ["regions" ],
186+ (
187+ f"Could not find the default project in region { region } ."
188+ " Consider setting the `projects` property in backend settings"
189+ ),
143190 )
144- continue
145191 region_to_project_id [region ] = region_projects [0 ].metadata .id
146192 return region_to_project_id
147193
148194
195+ def _get_region_to_configured_project_id_map (
196+ all_tenant_projects : Iterable [Container ],
197+ configured_project_ids : list [str ],
198+ configured_regions : Optional [list [str ]],
199+ ) -> dict [str , str ]:
200+ project_id_to_project = {p .metadata .id : p for p in all_tenant_projects }
201+ region_to_project_id = {}
202+ for project_id in configured_project_ids :
203+ project = project_id_to_project .get (project_id )
204+ if project is None :
205+ raise_invalid_credentials_error (
206+ ["projects" ],
207+ f"Configured project ID { project_id !r} not found in this Nebius tenancy" ,
208+ )
209+ duplicate_project_id = region_to_project_id .get (project .status .region )
210+ if duplicate_project_id :
211+ raise_invalid_credentials_error (
212+ ["projects" ],
213+ (
214+ f"Configured projects { project_id } and { duplicate_project_id } "
215+ f" both belong to the same region { project .status .region } ."
216+ " Only one project per region is allowed"
217+ ),
218+ )
219+ region_to_project_id [project .status .region ] = project_id
220+ if configured_regions :
221+ # only filter by region after validating all project IDs
222+ return {
223+ region : project_id
224+ for region , project_id in region_to_project_id .items ()
225+ if region in configured_regions
226+ }
227+ return region_to_project_id
228+
229+
149230def get_default_subnet (sdk : SDK , project_id : str ) -> Subnet :
150231 subnets = LOOP .await_ (
151232 SubnetServiceClient (sdk ).list (
0 commit comments