Skip to content

Commit a69f326

Browse files
author
ikemilian-lewis
committed
feat(azure): rebase based on Main not Common Code
1 parent fb14547 commit a69f326

31 files changed

Lines changed: 1189 additions & 253 deletions

src/orb/infrastructure/scheduler/hostfactory/field_mappings.py

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -67,67 +67,6 @@ class HostFactoryFieldMappings:
6767
"launchTemplateId": "launch_template_id",
6868
"userDataScript": "user_data",
6969
},
70-
# Azure-specific fields (only mapped when Azure provider is active)
71-
"azure": {
72-
# Override generic AWS-centric meanings for Azure templates
73-
"vmType": "vm_size",
74-
"vmTypes": "vm_sizes",
75-
"keyName": "ssh_key_name",
76-
"subnetId": "network_config.subnet_id",
77-
"securityGroupIds": "network_config.network_security_group_id",
78-
# Azure resource targeting
79-
"resourceGroup": "resource_group",
80-
"subscriptionId": "subscription_id",
81-
# Azure VMSS / compute configuration
82-
"vmSize": "vm_size",
83-
"vmSizes": "vm_sizes",
84-
"vmSizePreferences": "vm_size_preferences",
85-
"vmssName": "vmss_name",
86-
"orchestrationMode": "orchestration_mode",
87-
"platformFaultDomainCount": "platform_fault_domain_count",
88-
"singlePlacementGroup": "single_placement_group",
89-
# Azure pricing / placement
90-
"evictionPolicy": "eviction_policy",
91-
"billingProfileMaxPrice": "billing_profile_max_price",
92-
"spotPercentage": "spot_percentage",
93-
"baseRegularPriorityCount": "base_regular_priority_count",
94-
"vmssAllocationStrategy": "vmss_allocation_strategy",
95-
"spotRestoreEnabled": "spot_restore_enabled",
96-
"spotRestoreTimeout": "spot_restore_timeout",
97-
"zoneBalance": "zone_balance",
98-
"proximityPlacementGroupId": "proximity_placement_group_id",
99-
"capacityReservationGroupId": "capacity_reservation_group_id",
100-
# Azure storage / network / security
101-
"osDisk": "os_disk",
102-
"dataDisks": "data_disks",
103-
"networkConfig": "network_config",
104-
"securityType": "security_type",
105-
"secureBootEnabled": "secure_boot_enabled",
106-
"vtpmEnabled": "vtpm_enabled",
107-
"encryptionAtHost": "encryption_at_host",
108-
"diskEncryptionSetId": "disk_encryption_set_id",
109-
# Azure identity / bootstrap
110-
"adminUsername": "admin_username",
111-
"sshKeyName": "ssh_key_name",
112-
"sshPublicKeys": "ssh_public_keys",
113-
"userAssignedIdentityIds": "user_assigned_identity_ids",
114-
"systemAssignedIdentity": "system_assigned_identity",
115-
"customData": "custom_data",
116-
"extensionProfile": "extension_profile",
117-
"upgradePolicyMode": "upgrade_policy_mode",
118-
# Azure native spec / metadata
119-
"providerApiSpec": "provider_api_spec",
120-
"providerApiSpecFile": "provider_api_spec_file",
121-
"nodeAttributes": "node_attributes",
122-
# Azure CycleCloud
123-
"clusterName": "cluster_name",
124-
"nodeArray": "node_array",
125-
"cyclecloudUrl": "cyclecloud_url",
126-
"cyclecloudCredentialPath": "cyclecloud_credential_path",
127-
"cyclecloudVerifySsl": "cyclecloud_verify_ssl",
128-
"cyclecloudAuthMode": "cyclecloud_auth_mode",
129-
"cyclecloudAadScope": "cyclecloud_aad_scope",
130-
},
13170
}
13271

13372
@classmethod

src/orb/infrastructure/scheduler/hostfactory/hostfactory_strategy.py

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,36 +1027,6 @@ def _resolve_api_alias(self, raw_api: str) -> str:
10271027

10281028
def _transform_machine_types_input(self, hf_data: dict) -> dict:
10291029
"""Transform HF vmType/vmTypes to internal machine_types."""
1030-
provider_type = self.field_mapper.provider_type
1031-
1032-
if provider_type == "azure":
1033-
primary_vm_type = hf_data.get("vmType")
1034-
if "vmTypes" in hf_data:
1035-
raw_vm_types = hf_data["vmTypes"]
1036-
candidate_sizes: list[str] = []
1037-
if isinstance(raw_vm_types, dict):
1038-
candidate_sizes = [str(vm_size) for vm_size in raw_vm_types.keys()]
1039-
1040-
if candidate_sizes:
1041-
if primary_vm_type:
1042-
candidate_sizes = [
1043-
str(primary_vm_type),
1044-
*[
1045-
vm_size
1046-
for vm_size in candidate_sizes
1047-
if vm_size != str(primary_vm_type)
1048-
],
1049-
]
1050-
1051-
primary_vm_size, *fallback_vm_sizes = candidate_sizes
1052-
result: dict[str, Any] = {"vm_size": primary_vm_size}
1053-
if fallback_vm_sizes:
1054-
result["vm_sizes"] = fallback_vm_sizes
1055-
return result
1056-
if primary_vm_type:
1057-
return {"vm_size": primary_vm_type}
1058-
return {}
1059-
10601030
if "vmType" in hf_data:
10611031
return {"machine_types": {hf_data["vmType"]: 1}}
10621032
elif "vmTypes" in hf_data:

src/orb/providers/azure/configuration/template_extension.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
AzureVmSizePreference,
2323
AzureVMSSOrchestrationMode,
2424
)
25+
from orb.providers.azure.services.spot_placement_planner import PlacementSplitStrategy
2526

2627

2728
class AzureTemplateExtensionConfig(BaseModel):
@@ -72,6 +73,21 @@ class AzureTemplateExtensionConfig(BaseModel):
7273
spot_restore_timeout: Optional[str] = Field(
7374
None, description="ISO 8601 Spot Try-Restore timeout"
7475
)
76+
spot_placement_score_enabled: Optional[bool] = Field(
77+
None, description="Enable Azure Spot Placement Score planning before launch"
78+
)
79+
placement_split_strategy: Optional[PlacementSplitStrategy] = Field(
80+
None, description="How Spot Placement Score launches split capacity"
81+
)
82+
placement_primary_share_percent: Optional[int] = Field(
83+
None, description="Capacity percentage assigned to the top placement candidate"
84+
)
85+
placement_regions: Optional[list[str]] = Field(
86+
None, description="Azure regions considered for Spot Placement Score planning"
87+
)
88+
placement_zones: Optional[list[str]] = Field(
89+
None, description="Azure zones considered for Spot Placement Score planning"
90+
)
7591
zones: Optional[list[str]] = Field(None, description="Availability zones")
7692
zone_balance: Optional[bool] = Field(None, description="Enable zone balancing")
7793
proximity_placement_group_id: Optional[AzureProximityPlacementGroupId] = Field(
@@ -146,9 +162,9 @@ class AzureTemplateExtensionConfig(BaseModel):
146162
)
147163

148164
# VM configuration
149-
vm_size: str = Field(
150-
"Standard_D4s_v5",
151-
description="Default Azure VM size",
165+
vm_size: Optional[str] = Field(
166+
default=None,
167+
description="Explicit Azure VM size default",
152168
)
153169
vm_sizes: Optional[list[str]] = Field(
154170
default=None,
@@ -184,17 +200,28 @@ class AzureTemplateExtensionConfig(BaseModel):
184200
def to_template_defaults(self) -> dict[str, Any]:
185201
"""Convert extension to a dict of default values for template creation."""
186202
defaults: dict[str, Any] = {
187-
"vm_size": self.vm_size,
188203
"priority": self.priority,
189204
"admin_username": self.admin_username,
190205
"node_attributes": self.node_attributes,
191206
}
207+
if self.vm_size:
208+
defaults["vm_size"] = self.vm_size
192209
if self.vm_sizes:
193210
defaults["vm_sizes"] = self.vm_sizes
194211
if self.vm_size_preferences:
195212
defaults["vm_size_preferences"] = self.vm_size_preferences
196213
if self.vmss_allocation_strategy:
197214
defaults["vmss_allocation_strategy"] = self.vmss_allocation_strategy
215+
if self.spot_placement_score_enabled is not None:
216+
defaults["spot_placement_score_enabled"] = self.spot_placement_score_enabled
217+
if self.placement_split_strategy:
218+
defaults["placement_split_strategy"] = self.placement_split_strategy
219+
if self.placement_primary_share_percent is not None:
220+
defaults["placement_primary_share_percent"] = self.placement_primary_share_percent
221+
if self.placement_regions:
222+
defaults["placement_regions"] = self.placement_regions
223+
if self.placement_zones:
224+
defaults["placement_zones"] = self.placement_zones
198225
if self.os_disk_size_gb is not None:
199226
defaults["os_disk"] = {
200227
"storage_account_type": self.os_disk_type or "Premium_LRS",

src/orb/providers/azure/defaults_loader.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,32 @@
33
from __future__ import annotations
44

55
from orb.domain.base.ports.provider_defaults_loader_port import ProviderDefaultsLoaderPort
6+
from orb.providers.azure.capabilities import get_supported_api_capabilities
67
from orb.providers.azure.registration import get_azure_extension_defaults
78

89

10+
_HANDLER_CLASSES = {
11+
"VMSS": "VMSSHandler",
12+
"VMSSUniform": "VMSSHandler",
13+
"SingleVM": "SingleVMHandler",
14+
"CycleCloud": "CycleCloudHandler",
15+
}
16+
17+
918
class AzureDefaultsLoader:
1019
"""Loads Azure provider defaults from the provider-owned template extension."""
1120

1221
def load_defaults(self) -> dict:
1322
"""Return Azure provider defaults contributed by the Azure provider."""
23+
handlers = {
24+
api: {"handler_class": _HANDLER_CLASSES[api], **capabilities}
25+
for api, capabilities in get_supported_api_capabilities().items()
26+
}
1427
return {
1528
"provider": {
1629
"provider_defaults": {
1730
"azure": {
31+
"handlers": handlers,
1832
"template_defaults": get_azure_extension_defaults(),
1933
}
2034
}

src/orb/providers/azure/domain/template/azure_template_aggregate.py

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,9 @@
2222

2323
from typing import Any, Optional
2424

25-
from pydantic import AliasChoices, ConfigDict, Field, model_validator
25+
from pydantic import AliasChoices, ConfigDict, Field, field_validator, model_validator
2626

2727
from orb.domain.template.template_aggregate import Template
28-
from orb.domain.base.value_objects import AllocationStrategy
2928
from orb.providers.azure.domain.template.value_objects import (
3029
AzureAllocationStrategy,
3130
AzureCapacityReservationGroupId,
@@ -45,6 +44,7 @@
4544
AzureVmSizePreference,
4645
AzureVMSSOrchestrationMode,
4746
)
47+
from orb.providers.azure.services.spot_placement_planner import PlacementSplitStrategy
4848

4949

5050

@@ -129,6 +129,11 @@ class AzureTemplate(Template):
129129
description="Primary Azure VM size, e.g. 'Standard_D4s_v5'.",
130130
validation_alias=AliasChoices("vm_size", "vmSize"),
131131
)
132+
vm_sizes: list[str] = Field(
133+
default_factory=list,
134+
description="Additional unranked Azure VM size candidates.",
135+
validation_alias=AliasChoices("vm_sizes", "vmSizes"),
136+
)
132137
vm_size_preferences: list[AzureVmSizePreference] = Field(
133138
default_factory=list,
134139
description=(
@@ -201,6 +206,37 @@ class AzureTemplate(Template):
201206
description="ISO 8601 duration for spot restore timeout, e.g. 'PT1H'.",
202207
validation_alias=AliasChoices("spot_restore_timeout", "spotRestoreTimeout"),
203208
)
209+
spot_placement_score_enabled: bool = Field(
210+
default=False,
211+
description="Enable Azure Spot Placement Score planning before launch.",
212+
validation_alias=AliasChoices(
213+
"spot_placement_score_enabled", "spotPlacementScoreEnabled"
214+
),
215+
)
216+
placement_split_strategy: PlacementSplitStrategy = Field(
217+
default=PlacementSplitStrategy.HYBRID,
218+
description="How spot placement score launches split capacity across candidates.",
219+
validation_alias=AliasChoices("placement_split_strategy", "placementSplitStrategy"),
220+
)
221+
placement_primary_share_percent: int = Field(
222+
default=80,
223+
ge=0,
224+
le=100,
225+
description="Capacity percentage assigned to the top spot placement candidate.",
226+
validation_alias=AliasChoices(
227+
"placement_primary_share_percent", "placementPrimarySharePercent"
228+
),
229+
)
230+
placement_regions: list[str] = Field(
231+
default_factory=list,
232+
description="Azure regions considered for spot placement score planning.",
233+
validation_alias=AliasChoices("placement_regions", "placementRegions"),
234+
)
235+
placement_zones: list[str] = Field(
236+
default_factory=list,
237+
description="Azure zones considered for spot placement score planning.",
238+
validation_alias=AliasChoices("placement_zones", "placementZones"),
239+
)
204240

205241
# ------------------------------------------------------------------
206242
# Availability
@@ -404,6 +440,10 @@ class AzureTemplate(Template):
404440
description="AAD scope used to resolve a bearer token for CycleCloud.",
405441
validation_alias=AliasChoices("cyclecloud_aad_scope", "cyclecloudAadScope"),
406442
)
443+
version: Optional[str] = Field(
444+
default=None,
445+
description="Template catalog or DTO version metadata.",
446+
)
407447

408448
# ------------------------------------------------------------------
409449
# Initialisation
@@ -418,6 +458,14 @@ def __init__(self, **data: Any) -> None:
418458
# Validators
419459
# ------------------------------------------------------------------
420460

461+
@field_validator("vm_sizes", mode="before")
462+
@classmethod
463+
def normalize_vm_sizes(cls, value: Any) -> Any:
464+
"""Accept HostFactory ``vmTypes`` dictionaries as Azure VM size lists."""
465+
if isinstance(value, dict):
466+
return [str(vm_size) for vm_size in value.keys()]
467+
return value
468+
421469
@model_validator(mode="before")
422470
@classmethod
423471
def apply_implied_defaults(cls, data: Any) -> Any:
@@ -438,6 +486,17 @@ def apply_implied_defaults(cls, data: Any) -> Any:
438486
return data
439487

440488
data = dict(data)
489+
raw_provider_config = data.pop("provider_config", None)
490+
if raw_provider_config is not None:
491+
if hasattr(raw_provider_config, "model_dump"):
492+
provider_config = raw_provider_config.model_dump(mode="json")
493+
elif isinstance(raw_provider_config, dict):
494+
provider_config = dict(raw_provider_config)
495+
else:
496+
provider_config = {}
497+
for key, value in provider_config.items():
498+
if value is not None and data.get(key) in (None, ""):
499+
data[key] = value
441500

442501
location = data.get("location")
443502
region = data.get("region")
@@ -458,12 +517,16 @@ def apply_implied_defaults(cls, data: Any) -> Any:
458517
data["max_instances"] = data["max_number"]
459518
data.pop("max_number", None)
460519

461-
if data.get("allocation_strategy") in (None, ""):
462-
data["allocation_strategy"] = (
463-
AllocationStrategy.PRICE_CAPACITY_OPTIMIZED.value
464-
if data.get("price_type") == "spot"
465-
else AllocationStrategy.LOWEST_PRICE.value
520+
if data.get("allocation_strategy") not in (None, ""):
521+
raise ValueError(
522+
"Azure templates do not support allocation_strategy; use "
523+
"vmss_allocation_strategy for Azure VMSS instance mix or "
524+
"spot_placement_score_enabled for Spot Placement Score planning"
466525
)
526+
# The shared Template base still owns an AWS-shaped allocation_strategy
527+
# field and mutates it when absent. Azure does not use that contract, so
528+
# set an inert value here after rejecting explicit non-empty input.
529+
data["allocation_strategy"] = ""
467530

468531
if data.get("spot_percentage") is not None and data.get("priority") in (
469532
None,
@@ -495,11 +558,11 @@ def validate_azure_template(self) -> "AzureTemplate":
495558
"provider_api 'VMSSUniform' requires orchestration_mode 'Uniform'"
496559
)
497560

498-
if self.allocation_strategy == AllocationStrategy.SPOT_PLACEMENT_SCORE.value:
561+
if self.spot_placement_score_enabled:
499562
candidate_sizes = self.candidate_vm_sizes
500563
if len(candidate_sizes) < 2:
501564
raise ValueError(
502-
"spotPlacementScore allocation strategy requires at least two candidate vm sizes"
565+
"spot_placement_score_enabled requires at least two candidate vm sizes"
503566
)
504567

505568
if self.vm_sizes and self.vm_size_preferences:

src/orb/providers/azure/domain/template/value_objects.py

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
model_serializer,
1515
)
1616

17-
from orb.domain.base.value_objects import AllocationStrategy, PriceType, ValueObject
17+
from orb.domain.base.value_objects import PriceType, ValueObject
1818

1919

2020
# ---------------------------------------------------------------------------
@@ -88,18 +88,6 @@ class AzureAllocationStrategy(str, Enum):
8888
CAPACITY_OPTIMIZED = "CapacityOptimized"
8989
PRIORITIZED = "Prioritized"
9090

91-
@classmethod
92-
def from_core(cls, strategy: AllocationStrategy) -> "AzureAllocationStrategy":
93-
"""Map a core AllocationStrategy to Azure-specific value."""
94-
mapping = {
95-
AllocationStrategy.LOWEST_PRICE: cls.LOWEST_PRICE,
96-
AllocationStrategy.CAPACITY_OPTIMIZED: cls.CAPACITY_OPTIMIZED,
97-
AllocationStrategy.CAPACITY_OPTIMIZED_PRIORITIZED: cls.PRIORITIZED,
98-
AllocationStrategy.PRICE_CAPACITY_OPTIMIZED: cls.CAPACITY_OPTIMIZED,
99-
AllocationStrategy.DIVERSIFIED: cls.LOWEST_PRICE,
100-
}
101-
return mapping.get(strategy, cls.LOWEST_PRICE)
102-
10391
def to_arm_value(self) -> str:
10492
"""Return the value expected by the ARM / REST API."""
10593
return self.value

0 commit comments

Comments
 (0)