Skip to content

Commit 0e976a8

Browse files
author
ikemilian-lewis
committed
feat: add Azure provider
1 parent 9de8835 commit 0e976a8

101 files changed

Lines changed: 25062 additions & 13 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,18 @@ dependencies = [
5353
"cryptography>=46.0.5", # Security: Fix CVE-2026-26007
5454
"filelock>=3.20.1", # Security: Fix CVE-2025-68146 race condition
5555
"requests>=2.31.0",
56+
"httpx>=0.27.0",
5657
"PyYAML>=6.0.0", # Exported API (lazy-loaded)
5758
"jsonschema>=4.17.0",
59+
"azure-core>=1.38.2",
60+
"azure-identity>=1.25.2",
61+
"azure-mgmt-authorization>=4.0.0",
62+
"azure-mgmt-compute>=37.2.0",
63+
"azure-mgmt-monitor>=7.0.0",
64+
"azure-mgmt-msi>=7.1.0",
65+
"azure-mgmt-network>=30.2.0",
66+
"azure-mgmt-resource>=25.0.0",
67+
"azure-mgmt-resource-subscriptions>=1.0.0b1",
5868
]
5969

6070
[project.optional-dependencies]

src/orb/bootstrap/infrastructure_services.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,31 @@ def create_template_factory(c: DIContainer):
7777

7878
factory = TemplateFactory(logger=c.get(LoggingPort))
7979
try:
80-
from orb.providers.aws.registration import register_aws_template_factory
80+
from orb.providers.aws.registration import (
81+
register_aws_extensions,
82+
register_aws_template_factory,
83+
)
8184

85+
register_aws_extensions(c.get(LoggingPort))
8286
register_aws_template_factory(factory, c.get(LoggingPort))
8387
except ImportError as exc:
8488
c.get(LoggingPort).debug(
8589
"AWS provider module not available; AWS-specific templates will not be registered: %s",
8690
exc,
8791
)
92+
try:
93+
from orb.providers.azure.registration import (
94+
register_azure_extensions,
95+
register_azure_template_factory,
96+
)
97+
98+
register_azure_extensions(c.get(LoggingPort))
99+
register_azure_template_factory(factory, c.get(LoggingPort))
100+
except ImportError as exc:
101+
c.get(LoggingPort).debug(
102+
"Azure provider module not available; Azure-specific templates will not be registered: %s",
103+
exc,
104+
)
88105
return factory
89106

90107
from orb.domain.template.factory import TemplateFactory, TemplateFactoryPort

src/orb/cli/args.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,33 @@ def add_provider_actions(subparsers):
304304
)
305305
providers_add.add_argument("--aws-profile", help="AWS profile name")
306306
providers_add.add_argument("--aws-region", help="AWS region")
307+
providers_add.add_argument("--azure-subscription-id", help="Azure subscription ID")
308+
providers_add.add_argument("--azure-resource-group", help="Azure resource group")
309+
providers_add.add_argument("--azure-location", help="Azure location")
310+
providers_add.add_argument("--azure-client-id", help="Azure managed identity client ID")
311+
providers_add.add_argument("--azure-cyclecloud-url", help="Azure CycleCloud URL")
312+
providers_add.add_argument(
313+
"--azure-cyclecloud-credential-path",
314+
help="Secret path or file path for CycleCloud credentials",
315+
)
316+
providers_add.add_argument(
317+
"--azure-cyclecloud-auth-mode",
318+
help="CycleCloud auth mode override",
319+
)
320+
providers_add.add_argument(
321+
"--azure-cyclecloud-aad-scope",
322+
help="CycleCloud AAD scope override",
323+
)
324+
providers_add.add_argument(
325+
"--azure-cyclecloud-verify-ssl",
326+
action="store_true",
327+
help="Verify TLS certificates for CycleCloud",
328+
)
329+
providers_add.add_argument(
330+
"--azure-cyclecloud-no-verify-ssl",
331+
action="store_true",
332+
help="Disable TLS certificate verification for CycleCloud",
333+
)
307334
providers_add.add_argument("--name", help="Provider instance name")
308335
providers_add.add_argument("--discover", action="store_true", help="Discover infrastructure")
309336

@@ -316,6 +343,33 @@ def add_provider_actions(subparsers):
316343
providers_update.add_argument("provider_name", help="Provider instance name")
317344
providers_update.add_argument("--aws-region", help="Update region")
318345
providers_update.add_argument("--aws-profile", help="Update profile")
346+
providers_update.add_argument("--azure-subscription-id", help="Update Azure subscription ID")
347+
providers_update.add_argument("--azure-resource-group", help="Update Azure resource group")
348+
providers_update.add_argument("--azure-location", help="Update Azure location")
349+
providers_update.add_argument("--azure-client-id", help="Update Azure managed identity client ID")
350+
providers_update.add_argument("--azure-cyclecloud-url", help="Update CycleCloud URL")
351+
providers_update.add_argument(
352+
"--azure-cyclecloud-credential-path",
353+
help="Update CycleCloud credential path",
354+
)
355+
providers_update.add_argument(
356+
"--azure-cyclecloud-auth-mode",
357+
help="Update CycleCloud auth mode override",
358+
)
359+
providers_update.add_argument(
360+
"--azure-cyclecloud-aad-scope",
361+
help="Update CycleCloud AAD scope override",
362+
)
363+
providers_update.add_argument(
364+
"--azure-cyclecloud-verify-ssl",
365+
action="store_true",
366+
help="Enable TLS certificate verification for CycleCloud",
367+
)
368+
providers_update.add_argument(
369+
"--azure-cyclecloud-no-verify-ssl",
370+
action="store_true",
371+
help="Disable TLS certificate verification for CycleCloud",
372+
)
319373

320374
providers_set_default = subparsers.add_parser("set-default", help="Set default provider")
321375
add_global_arguments(providers_set_default)
@@ -691,6 +745,33 @@ def build_parser() -> tuple[argparse.ArgumentParser, dict]:
691745
init_parser.add_argument("--provider", default="aws", help="Provider type")
692746
init_parser.add_argument("--region", help="AWS region")
693747
init_parser.add_argument("--profile", help="AWS profile")
748+
init_parser.add_argument("--azure-subscription-id", help="Azure subscription ID")
749+
init_parser.add_argument("--azure-resource-group", help="Azure resource group")
750+
init_parser.add_argument("--azure-location", help="Azure location")
751+
init_parser.add_argument("--azure-client-id", help="Azure managed identity client ID")
752+
init_parser.add_argument("--azure-cyclecloud-url", help="Azure CycleCloud URL")
753+
init_parser.add_argument(
754+
"--azure-cyclecloud-credential-path",
755+
help="Secret path or file path for CycleCloud credentials",
756+
)
757+
init_parser.add_argument(
758+
"--azure-cyclecloud-auth-mode",
759+
help="CycleCloud auth mode override",
760+
)
761+
init_parser.add_argument(
762+
"--azure-cyclecloud-aad-scope",
763+
help="CycleCloud AAD scope override",
764+
)
765+
init_parser.add_argument(
766+
"--azure-cyclecloud-verify-ssl",
767+
action="store_true",
768+
help="Verify TLS certificates for CycleCloud",
769+
)
770+
init_parser.add_argument(
771+
"--azure-cyclecloud-no-verify-ssl",
772+
action="store_true",
773+
help="Disable TLS certificate verification for CycleCloud",
774+
)
694775
init_parser.add_argument("--config-dir", help="Custom configuration directory")
695776
init_parser.add_argument(
696777
"--scripts-dir",

src/orb/config/loader.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ def _load_provider_defaults(cls) -> dict[str, Any]:
206206
merged: dict[str, Any] = {}
207207
provider_default_loaders = {
208208
"aws": cls._load_aws_provider_defaults,
209+
"azure": cls._load_azure_provider_defaults,
209210
}
210211

211212
for provider_type, load_defaults in provider_default_loaders.items():
@@ -223,6 +224,20 @@ def _load_aws_provider_defaults() -> dict[str, Any]:
223224

224225
return AWSProviderStrategy.get_defaults_config()
225226

227+
@staticmethod
228+
def _load_azure_provider_defaults() -> dict[str, Any]:
229+
from orb.providers.azure.registration import get_azure_extension_defaults
230+
231+
return {
232+
"provider": {
233+
"provider_defaults": {
234+
"azure": {
235+
"template_defaults": get_azure_extension_defaults(),
236+
}
237+
}
238+
}
239+
}
240+
226241
@classmethod
227242
def _load_default_config(cls) -> dict[str, Any]:
228243
"""

src/orb/domain/base/provider_interfaces.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class ProviderType(str, Enum):
99
"""Supported provider types."""
1010

1111
AWS = "aws"
12+
AZURE = "azure"
1213
PROVIDER1 = "provider1"
1314
Provider2 = "provider2"
1415

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

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

64125
@classmethod

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

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -960,8 +960,10 @@ def format_template_for_display(self, template: TemplateDTO) -> dict[str, Any]:
960960
return self.field_mapper.map_output_fields(internal_dict, copy_unmapped=False)
961961

962962
def format_template_for_provider(self, template: TemplateDTO) -> dict[str, Any]:
963-
"""Format template for provider operations using internal format (no field mapping)."""
964-
return template.to_dict()
963+
"""Format either a stored template DTO or a domain template for provider operations."""
964+
if isinstance(template, TemplateDTO):
965+
return template.to_template_config()
966+
return template.model_dump(mode="json", exclude_none=True)
965967

966968
def format_machine_for_display(self, machine_dict: dict[str, Any]) -> dict[str, Any]:
967969
"""Format machine dict for display using HostFactory field mapper."""
@@ -989,6 +991,36 @@ def _resolve_api_alias(self, raw_api: str) -> str:
989991

990992
def _transform_machine_types_input(self, hf_data: dict) -> dict:
991993
"""Transform HF vmType/vmTypes to internal machine_types."""
994+
provider_type = self.field_mapper.provider_type
995+
996+
if provider_type == "azure":
997+
primary_vm_type = hf_data.get("vmType")
998+
if "vmTypes" in hf_data:
999+
raw_vm_types = hf_data["vmTypes"]
1000+
candidate_sizes: list[str] = []
1001+
if isinstance(raw_vm_types, dict):
1002+
candidate_sizes = [str(vm_size) for vm_size in raw_vm_types.keys()]
1003+
1004+
if candidate_sizes:
1005+
if primary_vm_type:
1006+
candidate_sizes = [
1007+
str(primary_vm_type),
1008+
*[
1009+
vm_size
1010+
for vm_size in candidate_sizes
1011+
if vm_size != str(primary_vm_type)
1012+
],
1013+
]
1014+
1015+
primary_vm_size, *fallback_vm_sizes = candidate_sizes
1016+
result: dict[str, Any] = {"vm_size": primary_vm_size}
1017+
if fallback_vm_sizes:
1018+
result["vm_sizes"] = fallback_vm_sizes
1019+
return result
1020+
if primary_vm_type:
1021+
return {"vm_size": primary_vm_type}
1022+
return {}
1023+
9921024
if "vmType" in hf_data:
9931025
return {"machine_types": {hf_data["vmType"]: 1}}
9941026
elif "vmTypes" in hf_data:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Azure Provider implementation."""
2+
3+
from orb.providers.azure.configuration.config import AzureProviderConfig
4+
from orb.providers.azure.configuration.template_extension import AzureTemplateExtensionConfig
5+
from orb.providers.azure.registration import (
6+
get_azure_extension_defaults,
7+
initialize_azure_provider,
8+
is_azure_provider_registered,
9+
register_azure_extensions,
10+
register_azure_template_factory,
11+
)
12+
from orb.providers.azure.strategy.azure_provider_strategy import AzureProviderStrategy
13+
14+
__all__: list[str] = [
15+
"AzureProviderConfig",
16+
"AzureProviderStrategy",
17+
"AzureTemplateExtensionConfig",
18+
"get_azure_extension_defaults",
19+
"initialize_azure_provider",
20+
"is_azure_provider_registered",
21+
"register_azure_extensions",
22+
"register_azure_template_factory",
23+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Azure authentication utilities."""

0 commit comments

Comments
 (0)