Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- lib/python3.12/site-packages/ray/autoscaler/__init__.py +8 -0
- lib/python3.12/site-packages/ray/autoscaler/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/__pycache__/batching_node_provider.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/__pycache__/command_runner.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/__pycache__/launch_and_verify_cluster.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/__pycache__/node_provider.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__init__.py +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/autoscaler.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger_demoall.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cluster_dump.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/command_runner.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/commands.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/constants.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/docker.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_summarizer.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_system.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/legacy_info_string.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/load_metrics.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/loader.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/log_timer.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/monitor.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_launcher.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_provider_availability_tracker.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_tracker.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/prom_metrics.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/providers.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/resource_demand_scheduler.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/subprocess_output_util.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/updater.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/util.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__init__.py +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/config.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/node_provider.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-config-template.json +137 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-vm-template.json +318 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/_azure/config.py +580 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/_azure/node_provider.py +942 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__init__.py +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/__init__.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/config.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/node_provider.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/utils.cpython-312.pyc +0 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/config.py +116 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/node_provider.py +324 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/utils.py +461 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/autoscaler.py +1522 -0
- lib/python3.12/site-packages/ray/autoscaler/_private/aws/__init__.py +0 -0
lib/python3.12/site-packages/ray/autoscaler/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
|
| 4 |
+
from ray.autoscaler import sdk
|
| 5 |
+
|
| 6 |
+
__all__ = ["sdk"]
|
| 7 |
+
|
| 8 |
+
AUTOSCALER_DIR_PATH = Path(os.path.abspath(os.path.dirname(__file__)))
|
lib/python3.12/site-packages/ray/autoscaler/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (525 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/__pycache__/batching_node_provider.cpython-312.pyc
ADDED
|
Binary file (11.8 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/__pycache__/command_runner.cpython-312.pyc
ADDED
|
Binary file (4.55 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/__pycache__/launch_and_verify_cluster.cpython-312.pyc
ADDED
|
Binary file (21.1 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/__pycache__/node_provider.cpython-312.pyc
ADDED
|
Binary file (13.1 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (202 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/autoscaler.cpython-312.pyc
ADDED
|
Binary file (62.8 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger.cpython-312.pyc
ADDED
|
Binary file (31.9 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cli_logger_demoall.cpython-312.pyc
ADDED
|
Binary file (2.69 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/cluster_dump.cpython-312.pyc
ADDED
|
Binary file (26.9 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/command_runner.cpython-312.pyc
ADDED
|
Binary file (40.2 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/commands.cpython-312.pyc
ADDED
|
Binary file (60.7 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/constants.cpython-312.pyc
ADDED
|
Binary file (3.45 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/docker.cpython-312.pyc
ADDED
|
Binary file (5.16 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_summarizer.cpython-312.pyc
ADDED
|
Binary file (4.12 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/event_system.cpython-312.pyc
ADDED
|
Binary file (5.18 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/legacy_info_string.cpython-312.pyc
ADDED
|
Binary file (1.95 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/load_metrics.cpython-312.pyc
ADDED
|
Binary file (17.9 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/loader.cpython-312.pyc
ADDED
|
Binary file (930 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/log_timer.cpython-312.pyc
ADDED
|
Binary file (1.78 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/monitor.cpython-312.pyc
ADDED
|
Binary file (29.4 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_launcher.cpython-312.pyc
ADDED
|
Binary file (8.64 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_provider_availability_tracker.cpython-312.pyc
ADDED
|
Binary file (7.09 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/node_tracker.cpython-312.pyc
ADDED
|
Binary file (4 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/prom_metrics.cpython-312.pyc
ADDED
|
Binary file (8.44 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/providers.cpython-312.pyc
ADDED
|
Binary file (11.1 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/resource_demand_scheduler.cpython-312.pyc
ADDED
|
Binary file (38.6 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/subprocess_output_util.cpython-312.pyc
ADDED
|
Binary file (13.2 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/updater.cpython-312.pyc
ADDED
|
Binary file (24.4 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/__pycache__/util.cpython-312.pyc
ADDED
|
Binary file (40.4 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (209 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (20.5 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/_azure/__pycache__/node_provider.cpython-312.pyc
ADDED
|
Binary file (41.5 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-config-template.json
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
| 3 |
+
"contentVersion": "1.0.0.0",
|
| 4 |
+
"parameters": {
|
| 5 |
+
"clusterId": {
|
| 6 |
+
"type": "string",
|
| 7 |
+
"metadata": {
|
| 8 |
+
"description": "Unique string appended to resource names to isolate resources from different ray clusters."
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"subnet": {
|
| 12 |
+
"type": "string",
|
| 13 |
+
"metadata": {
|
| 14 |
+
"description": "Subnet parameters."
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"msiName": {
|
| 18 |
+
"type": "string",
|
| 19 |
+
"metadata": {
|
| 20 |
+
"description": "Managed service identity."
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"msiResourceGroup": {
|
| 24 |
+
"type": "string",
|
| 25 |
+
"metadata": {
|
| 26 |
+
"description": "Managed service identity resource group."
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"createMsi": {
|
| 30 |
+
"type": "bool",
|
| 31 |
+
"defaultValue": "true"
|
| 32 |
+
},
|
| 33 |
+
"roleAssignmentGuid": {
|
| 34 |
+
"type": "string",
|
| 35 |
+
"metadata": {
|
| 36 |
+
"description": "Deterministic resource name for the MSI role assignment."
|
| 37 |
+
}
|
| 38 |
+
}
|
| 39 |
+
},
|
| 40 |
+
"variables": {
|
| 41 |
+
"contributor": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]",
|
| 42 |
+
"location": "[resourceGroup().location]",
|
| 43 |
+
"roleAssignmentName": "[concat('ray-', parameters('clusterId'), '-ra')]",
|
| 44 |
+
"nsgName": "[concat('ray-', parameters('clusterId'), '-nsg')]",
|
| 45 |
+
"nsg": "[resourceId('Microsoft.Network/networkSecurityGroups', variables('nsgName'))]",
|
| 46 |
+
"vnetName": "[concat('ray-', parameters('clusterId'), '-vnet')]",
|
| 47 |
+
"subnetName": "[concat('ray-', parameters('clusterId'), '-subnet')]"
|
| 48 |
+
},
|
| 49 |
+
"resources": [
|
| 50 |
+
{
|
| 51 |
+
"condition": "[parameters('createMsi')]",
|
| 52 |
+
"type": "Microsoft.ManagedIdentity/userAssignedIdentities",
|
| 53 |
+
"apiVersion": "2024-11-30",
|
| 54 |
+
"location": "[variables('location')]",
|
| 55 |
+
"name": "[parameters('msiName')]"
|
| 56 |
+
},
|
| 57 |
+
{
|
| 58 |
+
"type": "Microsoft.Authorization/roleAssignments",
|
| 59 |
+
"apiVersion": "2022-04-01",
|
| 60 |
+
"name": "[parameters('roleAssignmentGuid')]",
|
| 61 |
+
"properties": {
|
| 62 |
+
"principalId": "[reference(resourceId(parameters('msiResourceGroup'), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('msiName')), '2024-11-30').principalId]",
|
| 63 |
+
"roleDefinitionId": "[variables('contributor')]",
|
| 64 |
+
"scope": "[resourceGroup().id]",
|
| 65 |
+
"principalType": "ServicePrincipal",
|
| 66 |
+
"description": "[concat('Ray autoscaler cluster ', parameters('clusterId'))]"
|
| 67 |
+
},
|
| 68 |
+
"dependsOn": [
|
| 69 |
+
"[parameters('msiName')]"
|
| 70 |
+
]
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
"type": "Microsoft.Network/networkSecurityGroups",
|
| 74 |
+
"apiVersion": "2024-10-01",
|
| 75 |
+
"name": "[variables('nsgName')]",
|
| 76 |
+
"location": "[variables('location')]",
|
| 77 |
+
"properties": {
|
| 78 |
+
"securityRules": [
|
| 79 |
+
{
|
| 80 |
+
"name": "SSH",
|
| 81 |
+
"properties": {
|
| 82 |
+
"priority": 1000,
|
| 83 |
+
"protocol": "Tcp",
|
| 84 |
+
"access": "Allow",
|
| 85 |
+
"direction": "Inbound",
|
| 86 |
+
"sourceAddressPrefix": "*",
|
| 87 |
+
"sourcePortRange": "*",
|
| 88 |
+
"destinationAddressPrefix": "*",
|
| 89 |
+
"destinationPortRange": "22"
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
]
|
| 93 |
+
}
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"type": "Microsoft.Network/virtualNetworks",
|
| 97 |
+
"apiVersion": "2024-10-01",
|
| 98 |
+
"name": "[variables('vnetName')]",
|
| 99 |
+
"location": "[variables('location')]",
|
| 100 |
+
"properties": {
|
| 101 |
+
"addressSpace": {
|
| 102 |
+
"addressPrefixes": [
|
| 103 |
+
"[parameters('subnet')]"
|
| 104 |
+
]
|
| 105 |
+
},
|
| 106 |
+
"subnets": [
|
| 107 |
+
{
|
| 108 |
+
"name": "[variables('subnetName')]",
|
| 109 |
+
"properties": {
|
| 110 |
+
"addressPrefix": "[parameters('subnet')]",
|
| 111 |
+
"networkSecurityGroup": {
|
| 112 |
+
"id": "[variables('nsg')]"
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
}
|
| 116 |
+
]
|
| 117 |
+
},
|
| 118 |
+
"dependsOn": [
|
| 119 |
+
"[variables('nsg')]"
|
| 120 |
+
]
|
| 121 |
+
}
|
| 122 |
+
],
|
| 123 |
+
"outputs": {
|
| 124 |
+
"subnet": {
|
| 125 |
+
"type": "string",
|
| 126 |
+
"value": "[resourceId('Microsoft.Network/virtualNetworks/subnets', variables('vnetName'), variables('subnetName'))]"
|
| 127 |
+
},
|
| 128 |
+
"nsg": {
|
| 129 |
+
"type": "string",
|
| 130 |
+
"value": "[variables('nsg')]"
|
| 131 |
+
},
|
| 132 |
+
"msi": {
|
| 133 |
+
"type": "string",
|
| 134 |
+
"value": "[resourceId(parameters('msiResourceGroup'), 'Microsoft.ManagedIdentity/userAssignedIdentities', parameters('msiName'))]"
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
}
|
lib/python3.12/site-packages/ray/autoscaler/_private/_azure/azure-vm-template.json
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
| 3 |
+
"contentVersion": "1.0.0.0",
|
| 4 |
+
"parameters": {
|
| 5 |
+
"vmName": {
|
| 6 |
+
"type": "string",
|
| 7 |
+
"metadata": {
|
| 8 |
+
"description": "The name of you Virtual Machine."
|
| 9 |
+
}
|
| 10 |
+
},
|
| 11 |
+
"adminUsername": {
|
| 12 |
+
"type": "string",
|
| 13 |
+
"metadata": {
|
| 14 |
+
"description": "Username for the Virtual Machine."
|
| 15 |
+
}
|
| 16 |
+
},
|
| 17 |
+
"publicKey": {
|
| 18 |
+
"type": "securestring",
|
| 19 |
+
"metadata": {
|
| 20 |
+
"description": "SSH Key for the Virtual Machine"
|
| 21 |
+
}
|
| 22 |
+
},
|
| 23 |
+
"imagePublisher": {
|
| 24 |
+
"type": "string",
|
| 25 |
+
"metadata": {
|
| 26 |
+
"description": "The publisher of the VM image"
|
| 27 |
+
},
|
| 28 |
+
"defaultValue": ""
|
| 29 |
+
},
|
| 30 |
+
"imageOffer": {
|
| 31 |
+
"type": "string",
|
| 32 |
+
"metadata": {
|
| 33 |
+
"description": "The offer of the VM image"
|
| 34 |
+
},
|
| 35 |
+
"defaultValue": ""
|
| 36 |
+
},
|
| 37 |
+
"imageSku": {
|
| 38 |
+
"type": "string",
|
| 39 |
+
"metadata": {
|
| 40 |
+
"description": "The sku of the VM image"
|
| 41 |
+
},
|
| 42 |
+
"defaultValue": ""
|
| 43 |
+
},
|
| 44 |
+
"imageVersion": {
|
| 45 |
+
"type": "string",
|
| 46 |
+
"metadata": {
|
| 47 |
+
"description": "The version of the VM image"
|
| 48 |
+
},
|
| 49 |
+
"defaultValue": ""
|
| 50 |
+
},
|
| 51 |
+
"imageId": {
|
| 52 |
+
"type": "string",
|
| 53 |
+
"metadata": {
|
| 54 |
+
"description": "The resource id of the VM image. If provided, it will override the imagePublisher, imageOffer, imageSku and imageVersion parameters."
|
| 55 |
+
},
|
| 56 |
+
"defaultValue": ""
|
| 57 |
+
},
|
| 58 |
+
"vmSize": {
|
| 59 |
+
"type": "string",
|
| 60 |
+
"metadata": {
|
| 61 |
+
"description": "The size of the VM"
|
| 62 |
+
}
|
| 63 |
+
},
|
| 64 |
+
"vmTags": {
|
| 65 |
+
"type": "object",
|
| 66 |
+
"metadata": {
|
| 67 |
+
"description": "Tags for the VM"
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
"vmCount": {
|
| 71 |
+
"type": "int",
|
| 72 |
+
"metadata": {
|
| 73 |
+
"description": "Number of VMs to deploy"
|
| 74 |
+
}
|
| 75 |
+
},
|
| 76 |
+
"osDiskSize": {
|
| 77 |
+
"type": "int",
|
| 78 |
+
"defaultValue": 0,
|
| 79 |
+
"metadata": {
|
| 80 |
+
"description": "Size of the OS disk in GB"
|
| 81 |
+
}
|
| 82 |
+
},
|
| 83 |
+
"provisionPublicIp": {
|
| 84 |
+
"type": "bool",
|
| 85 |
+
"defaultValue": true,
|
| 86 |
+
"metadata": {
|
| 87 |
+
"description": "If true creates a public ip"
|
| 88 |
+
}
|
| 89 |
+
},
|
| 90 |
+
"priority": {
|
| 91 |
+
"type": "string",
|
| 92 |
+
"defaultValue": "Regular",
|
| 93 |
+
"metadata": {
|
| 94 |
+
"description": "Specifies the priority for the virtual machine."
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
"evictionPolicy": {
|
| 98 |
+
"type": "string",
|
| 99 |
+
"defaultValue": "Delete",
|
| 100 |
+
"metadata": {
|
| 101 |
+
"description": "Specifies the eviction policy for the virtual machine."
|
| 102 |
+
}
|
| 103 |
+
},
|
| 104 |
+
"billingProfile": {
|
| 105 |
+
"type": "object",
|
| 106 |
+
"defaultValue": {},
|
| 107 |
+
"metadata": {
|
| 108 |
+
"description": "Specifies the maximum price to pay for Azure Spot VM."
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"msi": {
|
| 112 |
+
"type": "string",
|
| 113 |
+
"metadata": {
|
| 114 |
+
"description": "Managed service identity resource id."
|
| 115 |
+
}
|
| 116 |
+
},
|
| 117 |
+
"nsg": {
|
| 118 |
+
"type": "string",
|
| 119 |
+
"metadata": {
|
| 120 |
+
"description": "Network security group resource id."
|
| 121 |
+
}
|
| 122 |
+
},
|
| 123 |
+
"subnet": {
|
| 124 |
+
"type": "string",
|
| 125 |
+
"metadata": {
|
| 126 |
+
"descriptions": "Subnet resource id."
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
"enableAcceleratedNetworking": {
|
| 130 |
+
"type": "bool",
|
| 131 |
+
"defaultValue": false,
|
| 132 |
+
"metadata": {
|
| 133 |
+
"descriptions": "Whether to enable accelerated networking."
|
| 134 |
+
}
|
| 135 |
+
},
|
| 136 |
+
"zones": {
|
| 137 |
+
"type": "array",
|
| 138 |
+
"defaultValue": [],
|
| 139 |
+
"metadata": {
|
| 140 |
+
"description": "Availability zones for the virtual machine. If empty, no zones will be specified."
|
| 141 |
+
}
|
| 142 |
+
}
|
| 143 |
+
},
|
| 144 |
+
"variables": {
|
| 145 |
+
"location": "[resourceGroup().location]",
|
| 146 |
+
"useZones": "[greater(length(parameters('zones')), 0)]",
|
| 147 |
+
"networkInterfaceNamePrivate": "[concat(parameters('vmName'), '-nic')]",
|
| 148 |
+
"networkInterfaceNamePublic": "[concat(parameters('vmName'), '-nic-public')]",
|
| 149 |
+
"networkInterfaceName": "[if(parameters('provisionPublicIp'), variables('networkInterfaceNamePublic'), variables('networkInterfaceNamePrivate'))]",
|
| 150 |
+
"networkIpConfig": "[guid(resourceGroup().id, parameters('vmName'))]",
|
| 151 |
+
"osDiskType": "Standard_LRS",
|
| 152 |
+
"publicIpAddressName": "[concat(parameters('vmName'), '-ip')]"
|
| 153 |
+
},
|
| 154 |
+
"resources": [
|
| 155 |
+
{
|
| 156 |
+
"type": "Microsoft.Network/networkInterfaces",
|
| 157 |
+
"apiVersion": "2024-07-01",
|
| 158 |
+
"name": "[concat(variables('networkInterfaceNamePublic'), copyIndex())]",
|
| 159 |
+
"location": "[variables('location')]",
|
| 160 |
+
"dependsOn": [
|
| 161 |
+
"[resourceId('Microsoft.Network/publicIpAddresses/', concat(variables('publicIpAddressName'), copyIndex()))]"
|
| 162 |
+
],
|
| 163 |
+
"copy": {
|
| 164 |
+
"name": "NICPublicCopy",
|
| 165 |
+
"count": "[parameters('vmCount')]"
|
| 166 |
+
},
|
| 167 |
+
"properties": {
|
| 168 |
+
"ipConfigurations": [
|
| 169 |
+
{
|
| 170 |
+
"name": "[variables('networkIpConfig')]",
|
| 171 |
+
"properties": {
|
| 172 |
+
"subnet": {
|
| 173 |
+
"id": "[parameters('subnet')]"
|
| 174 |
+
},
|
| 175 |
+
"privateIPAllocationMethod": "Dynamic",
|
| 176 |
+
"publicIpAddress": {
|
| 177 |
+
"id": "[resourceId('Microsoft.Network/publicIPAddresses', concat(variables('publicIPAddressName'), copyIndex()))]"
|
| 178 |
+
}
|
| 179 |
+
}
|
| 180 |
+
}
|
| 181 |
+
],
|
| 182 |
+
"networkSecurityGroup": {
|
| 183 |
+
"id": "[parameters('nsg')]"
|
| 184 |
+
},
|
| 185 |
+
"enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]"
|
| 186 |
+
},
|
| 187 |
+
"condition": "[parameters('provisionPublicIp')]"
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"type": "Microsoft.Network/networkInterfaces",
|
| 191 |
+
"apiVersion": "2024-07-01",
|
| 192 |
+
"name": "[concat(variables('networkInterfaceNamePrivate'), copyIndex())]",
|
| 193 |
+
"location": "[variables('location')]",
|
| 194 |
+
"copy": {
|
| 195 |
+
"name": "NICPrivateCopy",
|
| 196 |
+
"count": "[parameters('vmCount')]"
|
| 197 |
+
},
|
| 198 |
+
"properties": {
|
| 199 |
+
"ipConfigurations": [
|
| 200 |
+
{
|
| 201 |
+
"name": "[variables('networkIpConfig')]",
|
| 202 |
+
"properties": {
|
| 203 |
+
"subnet": {
|
| 204 |
+
"id": "[parameters('subnet')]"
|
| 205 |
+
},
|
| 206 |
+
"privateIPAllocationMethod": "Dynamic"
|
| 207 |
+
}
|
| 208 |
+
}
|
| 209 |
+
],
|
| 210 |
+
"networkSecurityGroup": {
|
| 211 |
+
"id": "[parameters('nsg')]"
|
| 212 |
+
},
|
| 213 |
+
"enableAcceleratedNetworking": "[parameters('enableAcceleratedNetworking')]"
|
| 214 |
+
},
|
| 215 |
+
"condition": "[not(parameters('provisionPublicIp'))]"
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"type": "Microsoft.Network/publicIpAddresses",
|
| 219 |
+
"apiVersion": "2024-07-01",
|
| 220 |
+
"name": "[concat(variables('publicIpAddressName'), copyIndex())]",
|
| 221 |
+
"location": "[variables('location')]",
|
| 222 |
+
"properties": {
|
| 223 |
+
"publicIpAllocationMethod": "Static",
|
| 224 |
+
"publicIPAddressVersion": "IPv4"
|
| 225 |
+
},
|
| 226 |
+
"copy": {
|
| 227 |
+
"name": "PublicIpCopy",
|
| 228 |
+
"count": "[parameters('vmCount')]"
|
| 229 |
+
},
|
| 230 |
+
"zones": "[if(variables('useZones'), createArray(string(parameters('zones')[mod(copyIndex(), length(parameters('zones')))])), json('null'))]",
|
| 231 |
+
"sku": {
|
| 232 |
+
"name": "Standard",
|
| 233 |
+
"tier": "Regional"
|
| 234 |
+
},
|
| 235 |
+
"condition": "[parameters('provisionPublicIp')]"
|
| 236 |
+
},
|
| 237 |
+
{
|
| 238 |
+
"type": "Microsoft.Compute/virtualMachines",
|
| 239 |
+
"apiVersion": "2024-07-01",
|
| 240 |
+
"name": "[concat(parameters('vmName'), copyIndex())]",
|
| 241 |
+
"location": "[variables('location')]",
|
| 242 |
+
"dependsOn": [
|
| 243 |
+
"[resourceId('Microsoft.Network/networkInterfaces/', concat(variables('networkInterfaceName'), copyIndex()))]"
|
| 244 |
+
],
|
| 245 |
+
"copy": {
|
| 246 |
+
"name": "VmCopy",
|
| 247 |
+
"count": "[parameters('vmCount')]"
|
| 248 |
+
},
|
| 249 |
+
"tags": "[parameters('vmTags')]",
|
| 250 |
+
"zones": "[if(variables('useZones'), createArray(string(parameters('zones')[mod(copyIndex(), length(parameters('zones')))])), json('null'))]",
|
| 251 |
+
"properties": {
|
| 252 |
+
"hardwareProfile": {
|
| 253 |
+
"vmSize": "[parameters('vmSize')]"
|
| 254 |
+
},
|
| 255 |
+
"storageProfile": {
|
| 256 |
+
"osDisk": {
|
| 257 |
+
"createOption": "fromImage",
|
| 258 |
+
"diskSizeGB": "[if(equals(parameters('osDiskSize'), 0), json('null'), parameters('osDiskSize'))]",
|
| 259 |
+
"managedDisk": {
|
| 260 |
+
"storageAccountType": "[variables('osDiskType')]"
|
| 261 |
+
}
|
| 262 |
+
},
|
| 263 |
+
"imageReference": "[if(equals(parameters('imageId'), ''), json(concat('{\"publisher\":\"', parameters('imagePublisher'), '\",\"offer\":\"', parameters('imageOffer'), '\",\"sku\":\"', parameters('imageSku'), '\",\"version\":\"', parameters('imageVersion'), '\"}')), json(concat('{\"id\":\"', parameters('imageId'), '\"}')))]"
|
| 264 |
+
},
|
| 265 |
+
"networkProfile": {
|
| 266 |
+
"networkInterfaces": [
|
| 267 |
+
{
|
| 268 |
+
"id": "[resourceId('Microsoft.Network/networkInterfaces', concat(variables('networkInterfaceName'), copyIndex()))]"
|
| 269 |
+
}
|
| 270 |
+
]
|
| 271 |
+
},
|
| 272 |
+
"osProfile": {
|
| 273 |
+
"computerName": "[concat(parameters('vmName'), copyIndex())]",
|
| 274 |
+
"adminUsername": "[parameters('adminUsername')]",
|
| 275 |
+
"adminPassword": "[parameters('publicKey')]",
|
| 276 |
+
"linuxConfiguration": {
|
| 277 |
+
"disablePasswordAuthentication": true,
|
| 278 |
+
"ssh": {
|
| 279 |
+
"publicKeys": [
|
| 280 |
+
{
|
| 281 |
+
"path": "[concat('/home/', parameters('adminUsername'), '/.ssh/authorized_keys')]",
|
| 282 |
+
"keyData": "[parameters('publicKey')]"
|
| 283 |
+
}
|
| 284 |
+
]
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
},
|
| 288 |
+
"priority": "[parameters('priority')]",
|
| 289 |
+
"evictionPolicy": "[if(equals(parameters('priority'), 'Spot'), parameters('evictionPolicy'), '')]",
|
| 290 |
+
"billingProfile": "[parameters('billingProfile')]"
|
| 291 |
+
},
|
| 292 |
+
"identity": {
|
| 293 |
+
"type": "UserAssigned",
|
| 294 |
+
"userAssignedIdentities": {
|
| 295 |
+
"[parameters('msi')]": {
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
}
|
| 299 |
+
}
|
| 300 |
+
],
|
| 301 |
+
"outputs": {
|
| 302 |
+
"publicIp": {
|
| 303 |
+
"type": "array",
|
| 304 |
+
"copy": {
|
| 305 |
+
"count": "[parameters('vmCount')]",
|
| 306 |
+
"input": "[reference(concat(variables('publicIpAddressName'), copyIndex())).ipAddress]"
|
| 307 |
+
},
|
| 308 |
+
"condition": "[parameters('provisionPublicIp')]"
|
| 309 |
+
},
|
| 310 |
+
"privateIp": {
|
| 311 |
+
"type": "array",
|
| 312 |
+
"copy": {
|
| 313 |
+
"count": "[parameters('vmCount')]",
|
| 314 |
+
"input": "[reference(concat(variables('networkInterfaceName'), copyIndex())).ipConfigurations[0].properties.privateIPAddress]"
|
| 315 |
+
}
|
| 316 |
+
}
|
| 317 |
+
}
|
| 318 |
+
}
|
lib/python3.12/site-packages/ray/autoscaler/_private/_azure/config.py
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import random
|
| 5 |
+
import time
|
| 6 |
+
from hashlib import md5, sha256
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Any, Callable
|
| 9 |
+
from uuid import UUID
|
| 10 |
+
|
| 11 |
+
from azure.common.credentials import get_cli_profile
|
| 12 |
+
from azure.core.exceptions import HttpResponseError, ResourceNotFoundError
|
| 13 |
+
from azure.identity import AzureCliCredential
|
| 14 |
+
from azure.mgmt.resource import ResourceManagementClient
|
| 15 |
+
from azure.mgmt.resource.resources.models import DeploymentMode
|
| 16 |
+
|
| 17 |
+
from ray.autoscaler._private.util import (
|
| 18 |
+
generate_rsa_key_pair,
|
| 19 |
+
generate_ssh_key_name,
|
| 20 |
+
generate_ssh_key_paths,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# Built-in Azure Contributor role definition ID used for role assignments.
|
| 24 |
+
CONTRIBUTOR_ROLE_DEFINITION_ID = "b24988ac-6180-42a0-ab88-20f7382dd24c"
|
| 25 |
+
|
| 26 |
+
UNIQUE_ID_LEN = 4
|
| 27 |
+
|
| 28 |
+
logger = logging.getLogger(__name__)
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def get_azure_sdk_function(client: Any, function_name: str) -> Callable:
|
| 32 |
+
"""Retrieve a callable function from Azure SDK client object.
|
| 33 |
+
|
| 34 |
+
Newer versions of the various client SDKs renamed function names to
|
| 35 |
+
have a begin_ prefix. This function supports both the old and new
|
| 36 |
+
versions of the SDK by first trying the old name and falling back to
|
| 37 |
+
the prefixed new name.
|
| 38 |
+
"""
|
| 39 |
+
func = getattr(
|
| 40 |
+
client, function_name, getattr(client, f"begin_{function_name}", None)
|
| 41 |
+
)
|
| 42 |
+
if func is None:
|
| 43 |
+
raise AttributeError(
|
| 44 |
+
"'{obj}' object has no {func} or begin_{func} attribute".format(
|
| 45 |
+
obj={client.__name__}, func=function_name
|
| 46 |
+
)
|
| 47 |
+
)
|
| 48 |
+
return func
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def bootstrap_azure(config):
|
| 52 |
+
config = _configure_key_pair(config)
|
| 53 |
+
config = _configure_resource_group(config)
|
| 54 |
+
return config
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _configure_resource_group(config):
|
| 58 |
+
# TODO: look at availability sets
|
| 59 |
+
# https://docs.microsoft.com/en-us/azure/virtual-machines/windows/tutorial-availability-sets
|
| 60 |
+
subscription_id = config["provider"].get("subscription_id")
|
| 61 |
+
if subscription_id is None:
|
| 62 |
+
subscription_id = get_cli_profile().get_subscription_id()
|
| 63 |
+
resource_client = ResourceManagementClient(AzureCliCredential(), subscription_id)
|
| 64 |
+
config["provider"]["subscription_id"] = subscription_id
|
| 65 |
+
logger.info("Using subscription id: %s", subscription_id)
|
| 66 |
+
|
| 67 |
+
assert (
|
| 68 |
+
"resource_group" in config["provider"]
|
| 69 |
+
), "Provider config must include resource_group field"
|
| 70 |
+
resource_group = config["provider"]["resource_group"]
|
| 71 |
+
|
| 72 |
+
assert (
|
| 73 |
+
"location" in config["provider"]
|
| 74 |
+
), "Provider config must include location field"
|
| 75 |
+
params = {"location": config["provider"]["location"]}
|
| 76 |
+
|
| 77 |
+
if "tags" in config["provider"]:
|
| 78 |
+
params["tags"] = config["provider"]["tags"]
|
| 79 |
+
|
| 80 |
+
logger.info("Creating/Updating resource group: %s", resource_group)
|
| 81 |
+
rg_create_or_update = get_azure_sdk_function(
|
| 82 |
+
client=resource_client.resource_groups, function_name="create_or_update"
|
| 83 |
+
)
|
| 84 |
+
rg_create_or_update(resource_group_name=resource_group, parameters=params)
|
| 85 |
+
|
| 86 |
+
# load the template file
|
| 87 |
+
current_path = Path(__file__).parent
|
| 88 |
+
template_path = current_path.joinpath("azure-config-template.json")
|
| 89 |
+
with open(template_path, "r") as template_fp:
|
| 90 |
+
template = json.load(template_fp)
|
| 91 |
+
|
| 92 |
+
logger.info("Using cluster name: %s", config["cluster_name"])
|
| 93 |
+
|
| 94 |
+
# set unique id for resources in this cluster
|
| 95 |
+
unique_id = config["provider"].get("unique_id")
|
| 96 |
+
if unique_id is None:
|
| 97 |
+
hasher = sha256()
|
| 98 |
+
hasher.update(config["provider"]["resource_group"].encode("utf-8"))
|
| 99 |
+
unique_id = hasher.hexdigest()[:UNIQUE_ID_LEN]
|
| 100 |
+
else:
|
| 101 |
+
unique_id = str(unique_id)
|
| 102 |
+
config["provider"]["unique_id"] = unique_id
|
| 103 |
+
logger.info("Using unique id: %s", unique_id)
|
| 104 |
+
cluster_id = "{}-{}".format(config["cluster_name"], unique_id)
|
| 105 |
+
role_assignment_name = f"ray-{cluster_id}-ra"
|
| 106 |
+
role_assignment_guid = _generate_arm_guid(role_assignment_name)
|
| 107 |
+
role_assignment_resource_id = (
|
| 108 |
+
f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers"
|
| 109 |
+
f"/Microsoft.Authorization/roleAssignments/{role_assignment_guid}"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
subnet_mask = config["provider"].get("subnet_mask")
|
| 113 |
+
if subnet_mask is None:
|
| 114 |
+
# choose a random subnet, skipping most common value of 0
|
| 115 |
+
random.seed(unique_id)
|
| 116 |
+
subnet_mask = "10.{}.0.0/16".format(random.randint(1, 254))
|
| 117 |
+
logger.info("Using subnet mask: %s", subnet_mask)
|
| 118 |
+
|
| 119 |
+
# Copy over properties from existing subnet.
|
| 120 |
+
# Addresses issue (https://github.com/Azure/azure-quickstart-templates/issues/2786)
|
| 121 |
+
# where existing subnet properties will get overwritten unless explicitly specified
|
| 122 |
+
# during multiple deployments even if vnet/subnet do not change.
|
| 123 |
+
# May eventually be fixed by passing empty subnet list if they already exist:
|
| 124 |
+
# https://techcommunity.microsoft.com/t5/azure-networking-blog/azure-virtual-network-now-supports-updates-without-subnet/ba-p/4067952
|
| 125 |
+
list_by_rg = get_azure_sdk_function(
|
| 126 |
+
client=resource_client.resources, function_name="list_by_resource_group"
|
| 127 |
+
)
|
| 128 |
+
existing_vnets = list(
|
| 129 |
+
list_by_rg(
|
| 130 |
+
resource_group,
|
| 131 |
+
f"substringof('{unique_id}', name) and "
|
| 132 |
+
"resourceType eq 'Microsoft.Network/virtualNetworks'",
|
| 133 |
+
)
|
| 134 |
+
)
|
| 135 |
+
if len(existing_vnets) > 0:
|
| 136 |
+
vnid = existing_vnets[0].id
|
| 137 |
+
get_by_id = get_azure_sdk_function(
|
| 138 |
+
client=resource_client.resources, function_name="get_by_id"
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
# Query for supported API versions for Microsoft.Network/virtualNetworks
|
| 142 |
+
# because resource_client.DEFAULT_API_VERSION is not always supported.
|
| 143 |
+
# (Example: "2024-11-01" is the default at the time of this writing)
|
| 144 |
+
# Use "2024-10-01" as a fallback if we can't determine the latest stable version.
|
| 145 |
+
vnet_api_version = "2024-10-01"
|
| 146 |
+
try:
|
| 147 |
+
# Get supported API versions for Microsoft.Network provider
|
| 148 |
+
providers = resource_client.providers.get("Microsoft.Network")
|
| 149 |
+
vnet_resource_type = next(
|
| 150 |
+
(
|
| 151 |
+
rt
|
| 152 |
+
for rt in providers.resource_types
|
| 153 |
+
if rt.resource_type == "virtualNetworks"
|
| 154 |
+
),
|
| 155 |
+
None,
|
| 156 |
+
)
|
| 157 |
+
if vnet_resource_type and vnet_resource_type.api_versions:
|
| 158 |
+
stable_versions = [
|
| 159 |
+
v for v in vnet_resource_type.api_versions if "preview" not in v
|
| 160 |
+
]
|
| 161 |
+
versions_to_consider = (
|
| 162 |
+
stable_versions or vnet_resource_type.api_versions
|
| 163 |
+
)
|
| 164 |
+
vnet_api_version = sorted(versions_to_consider)[-1]
|
| 165 |
+
logger.info(
|
| 166 |
+
"Using API version: %s for virtualNetworks", vnet_api_version
|
| 167 |
+
)
|
| 168 |
+
else:
|
| 169 |
+
logger.warning(
|
| 170 |
+
"Could not determine supported API versions for virtualNetworks, using fallback version %s",
|
| 171 |
+
vnet_api_version,
|
| 172 |
+
)
|
| 173 |
+
except Exception as e:
|
| 174 |
+
logger.warning(
|
| 175 |
+
"Failed to query Microsoft.Network provider: %s. Using fallback API version 2024-10-01",
|
| 176 |
+
str(e),
|
| 177 |
+
)
|
| 178 |
+
|
| 179 |
+
subnet = get_by_id(vnid, vnet_api_version).properties["subnets"][0]
|
| 180 |
+
template_vnet = next(
|
| 181 |
+
(
|
| 182 |
+
rs
|
| 183 |
+
for rs in template["resources"]
|
| 184 |
+
if rs["type"] == "Microsoft.Network/virtualNetworks"
|
| 185 |
+
),
|
| 186 |
+
None,
|
| 187 |
+
)
|
| 188 |
+
if template_vnet is not None:
|
| 189 |
+
template_subnets = template_vnet["properties"].get("subnets")
|
| 190 |
+
if template_subnets is not None:
|
| 191 |
+
template_subnets[0]["properties"].update(subnet["properties"])
|
| 192 |
+
|
| 193 |
+
# Get or create an MSI name and resource group.
|
| 194 |
+
# Defaults to current resource group if not provided.
|
| 195 |
+
use_existing_msi = (
|
| 196 |
+
"msi_name" in config["provider"] and "msi_resource_group" in config["provider"]
|
| 197 |
+
)
|
| 198 |
+
msi_resource_group = config["provider"].get("msi_resource_group", resource_group)
|
| 199 |
+
msi_name = config["provider"].get("msi_name", f"ray-{cluster_id}-msi")
|
| 200 |
+
logger.info(
|
| 201 |
+
"Using msi_name: %s from msi_resource_group: %s", msi_name, msi_resource_group
|
| 202 |
+
)
|
| 203 |
+
|
| 204 |
+
existing_principal_id = None
|
| 205 |
+
if not use_existing_msi:
|
| 206 |
+
# When creating a MSI for managing Azure resources, we first need to clean up
|
| 207 |
+
# any role assignments from the previous MSI's principal ID to avoid
|
| 208 |
+
# orphaned permissions when the MSI gets recreated with a new principal ID.
|
| 209 |
+
# Role assignments cannot be updated, only created/removed, so cleanup is required.
|
| 210 |
+
msi_resource_id = (
|
| 211 |
+
f"/subscriptions/{subscription_id}/resourceGroups/{msi_resource_group}"
|
| 212 |
+
f"/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{msi_name}"
|
| 213 |
+
)
|
| 214 |
+
try:
|
| 215 |
+
get_identity = get_azure_sdk_function(
|
| 216 |
+
client=resource_client.resources, function_name="get_by_id"
|
| 217 |
+
)
|
| 218 |
+
existing_msi = get_identity(msi_resource_id, "2023-01-31")
|
| 219 |
+
existing_principal_id = getattr(existing_msi, "properties", {}).get(
|
| 220 |
+
"principalId"
|
| 221 |
+
)
|
| 222 |
+
except ResourceNotFoundError:
|
| 223 |
+
existing_principal_id = None
|
| 224 |
+
except Exception as exc:
|
| 225 |
+
logger.warning(
|
| 226 |
+
"Failed to query MSI %s for existing principal: %s",
|
| 227 |
+
msi_name,
|
| 228 |
+
exc,
|
| 229 |
+
)
|
| 230 |
+
|
| 231 |
+
if existing_principal_id:
|
| 232 |
+
logger.info(
|
| 233 |
+
"Removing existing role assignments for MSI principal %s before recreation",
|
| 234 |
+
existing_principal_id,
|
| 235 |
+
)
|
| 236 |
+
_delete_role_assignments_for_principal(
|
| 237 |
+
resource_client,
|
| 238 |
+
resource_group,
|
| 239 |
+
existing_principal_id,
|
| 240 |
+
)
|
| 241 |
+
|
| 242 |
+
delete_role_assignment = get_azure_sdk_function(
|
| 243 |
+
client=resource_client.resources, function_name="delete_by_id"
|
| 244 |
+
)
|
| 245 |
+
get_role_assignment = get_azure_sdk_function(
|
| 246 |
+
client=resource_client.resources, function_name="get_by_id"
|
| 247 |
+
)
|
| 248 |
+
|
| 249 |
+
role_assignment_known_missing = False
|
| 250 |
+
initial_query_failed = False
|
| 251 |
+
try:
|
| 252 |
+
get_role_assignment(
|
| 253 |
+
role_assignment_resource_id,
|
| 254 |
+
"2022-04-01",
|
| 255 |
+
)
|
| 256 |
+
except ResourceNotFoundError:
|
| 257 |
+
role_assignment_known_missing = True
|
| 258 |
+
logger.debug(
|
| 259 |
+
"Role assignment %s not found before MSI creation; skipping deletion",
|
| 260 |
+
role_assignment_guid,
|
| 261 |
+
)
|
| 262 |
+
except Exception as exc:
|
| 263 |
+
logger.warning(
|
| 264 |
+
"Failed to query role assignment %s before deletion: %s",
|
| 265 |
+
role_assignment_guid,
|
| 266 |
+
exc,
|
| 267 |
+
)
|
| 268 |
+
initial_query_failed = True
|
| 269 |
+
|
| 270 |
+
if not role_assignment_known_missing:
|
| 271 |
+
try:
|
| 272 |
+
delete_lro = delete_role_assignment(
|
| 273 |
+
resource_id=role_assignment_resource_id,
|
| 274 |
+
api_version="2022-04-01",
|
| 275 |
+
)
|
| 276 |
+
if hasattr(delete_lro, "wait"):
|
| 277 |
+
delete_lro.wait()
|
| 278 |
+
logger.info(
|
| 279 |
+
"Deleted existing role assignment %s before recreating MSI",
|
| 280 |
+
role_assignment_guid,
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
if initial_query_failed:
|
| 284 |
+
logger.debug(
|
| 285 |
+
"Retrying verification for role assignment %s",
|
| 286 |
+
role_assignment_guid,
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
if not _wait_for_role_assignment_deletion(
|
| 290 |
+
get_role_assignment,
|
| 291 |
+
role_assignment_resource_id,
|
| 292 |
+
role_assignment_guid,
|
| 293 |
+
):
|
| 294 |
+
logger.warning(
|
| 295 |
+
"Role assignment %s not confirmed deleted",
|
| 296 |
+
role_assignment_guid,
|
| 297 |
+
)
|
| 298 |
+
except ResourceNotFoundError:
|
| 299 |
+
logger.debug(
|
| 300 |
+
"Role assignment %s disappeared before deletion attempt; continuing",
|
| 301 |
+
role_assignment_guid,
|
| 302 |
+
)
|
| 303 |
+
except Exception as e:
|
| 304 |
+
logger.warning(
|
| 305 |
+
"Failed to delete role assignment %s before MSI creation: %s",
|
| 306 |
+
role_assignment_guid,
|
| 307 |
+
e,
|
| 308 |
+
)
|
| 309 |
+
|
| 310 |
+
parameters = {
|
| 311 |
+
"properties": {
|
| 312 |
+
"mode": DeploymentMode.incremental,
|
| 313 |
+
"template": template,
|
| 314 |
+
"parameters": {
|
| 315 |
+
"subnet": {"value": subnet_mask},
|
| 316 |
+
"clusterId": {"value": cluster_id},
|
| 317 |
+
"msiName": {"value": msi_name},
|
| 318 |
+
"msiResourceGroup": {"value": msi_resource_group},
|
| 319 |
+
"createMsi": {"value": not use_existing_msi},
|
| 320 |
+
"roleAssignmentGuid": {"value": role_assignment_guid},
|
| 321 |
+
},
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
create_or_update = get_azure_sdk_function(
|
| 326 |
+
client=resource_client.deployments, function_name="create_or_update"
|
| 327 |
+
)
|
| 328 |
+
outputs = (
|
| 329 |
+
create_or_update(
|
| 330 |
+
resource_group_name=resource_group,
|
| 331 |
+
deployment_name="ray-config",
|
| 332 |
+
parameters=parameters,
|
| 333 |
+
)
|
| 334 |
+
.result()
|
| 335 |
+
.properties.outputs
|
| 336 |
+
)
|
| 337 |
+
|
| 338 |
+
# append output resource ids to be used with vm creation
|
| 339 |
+
config["provider"]["msi"] = outputs["msi"]["value"]
|
| 340 |
+
config["provider"]["nsg"] = outputs["nsg"]["value"]
|
| 341 |
+
config["provider"]["subnet"] = outputs["subnet"]["value"]
|
| 342 |
+
|
| 343 |
+
return config
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
def _configure_key_pair(config):
|
| 347 |
+
"""
|
| 348 |
+
Configure SSH keypair. Use user specified custom paths, otherwise,
|
| 349 |
+
generate Ray-specific keypair in this format: "ray-autoscaler_azure_{region}_{resource_group}_{ssh_user}_{index}"
|
| 350 |
+
"""
|
| 351 |
+
ssh_user = config["auth"]["ssh_user"]
|
| 352 |
+
public_key = None
|
| 353 |
+
|
| 354 |
+
# Check if user specified custom SSH key paths
|
| 355 |
+
user_specified_private_key = "ssh_private_key" in config["auth"]
|
| 356 |
+
user_specified_public_key = "ssh_public_key" in config["auth"]
|
| 357 |
+
|
| 358 |
+
# Validate that the user either specfied both keys or none, but not just one
|
| 359 |
+
if user_specified_private_key != user_specified_public_key:
|
| 360 |
+
if user_specified_private_key:
|
| 361 |
+
missing_key, specified_key = "ssh_public_key", "ssh_private_key"
|
| 362 |
+
else:
|
| 363 |
+
missing_key, specified_key = "ssh_private_key", "ssh_public_key"
|
| 364 |
+
raise ValueError(
|
| 365 |
+
f"{specified_key} is specified but {missing_key} is missing. "
|
| 366 |
+
"Both SSH key paths must be specified together, or omit both from "
|
| 367 |
+
"your config to use auto-generated keys."
|
| 368 |
+
)
|
| 369 |
+
|
| 370 |
+
if user_specified_private_key and user_specified_public_key:
|
| 371 |
+
# User specified custom paths
|
| 372 |
+
private_key_path = Path(config["auth"]["ssh_private_key"]).expanduser()
|
| 373 |
+
public_key_path = Path(config["auth"]["ssh_public_key"]).expanduser()
|
| 374 |
+
|
| 375 |
+
# Validate that user-specified keys exist
|
| 376 |
+
missing_keys = []
|
| 377 |
+
if not private_key_path.is_file():
|
| 378 |
+
missing_keys.append(f"ssh_private_key: {private_key_path}")
|
| 379 |
+
if not public_key_path.is_file():
|
| 380 |
+
missing_keys.append(f"ssh_public_key: {public_key_path}")
|
| 381 |
+
|
| 382 |
+
if missing_keys:
|
| 383 |
+
raise ValueError(
|
| 384 |
+
"SSH key files from config do not exist: {}. "
|
| 385 |
+
"Please create the keys or remove the custom paths from your config "
|
| 386 |
+
"to use auto-generated keys.".format(", ".join(missing_keys))
|
| 387 |
+
)
|
| 388 |
+
logger.info(
|
| 389 |
+
"Using specified SSH keys from config: {} and {}".format(
|
| 390 |
+
private_key_path, public_key_path
|
| 391 |
+
)
|
| 392 |
+
)
|
| 393 |
+
|
| 394 |
+
with open(public_key_path, "r") as f:
|
| 395 |
+
public_key = f.read()
|
| 396 |
+
else:
|
| 397 |
+
# Generate Ray-specific keys
|
| 398 |
+
region = config["provider"]["location"]
|
| 399 |
+
resource_group = config["provider"]["resource_group"]
|
| 400 |
+
|
| 401 |
+
# Generate single deterministic key name for this configuration
|
| 402 |
+
key_name = generate_ssh_key_name(
|
| 403 |
+
"azure", None, region, resource_group, ssh_user
|
| 404 |
+
)
|
| 405 |
+
public_key_path, private_key_path = generate_ssh_key_paths(key_name)
|
| 406 |
+
|
| 407 |
+
# Check if this key pair already exists
|
| 408 |
+
if os.path.exists(private_key_path) and os.path.exists(public_key_path):
|
| 409 |
+
logger.info(
|
| 410 |
+
"Using existing Ray-specific SSH keys: {} and {}".format(
|
| 411 |
+
private_key_path, public_key_path
|
| 412 |
+
)
|
| 413 |
+
)
|
| 414 |
+
with open(public_key_path, "r") as f:
|
| 415 |
+
public_key = f.read()
|
| 416 |
+
else:
|
| 417 |
+
# Create a key pair since it doesn't exist locally
|
| 418 |
+
logger.info(
|
| 419 |
+
"Generating new Ray-specific SSH key pair at {} and {}".format(
|
| 420 |
+
private_key_path, public_key_path
|
| 421 |
+
)
|
| 422 |
+
)
|
| 423 |
+
os.makedirs(os.path.dirname(private_key_path), exist_ok=True)
|
| 424 |
+
public_key, private_key = generate_rsa_key_pair()
|
| 425 |
+
with open(
|
| 426 |
+
private_key_path,
|
| 427 |
+
"w",
|
| 428 |
+
opener=lambda path, flags: os.open(path, flags, 0o600),
|
| 429 |
+
) as f:
|
| 430 |
+
f.write(private_key)
|
| 431 |
+
with open(public_key_path, "w") as f:
|
| 432 |
+
f.write(public_key)
|
| 433 |
+
|
| 434 |
+
assert os.path.exists(
|
| 435 |
+
private_key_path
|
| 436 |
+
), "Private key file {} not found for user {}".format(
|
| 437 |
+
private_key_path, ssh_user
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
config["auth"]["ssh_private_key"] = str(private_key_path)
|
| 441 |
+
# Remove public key path because bootstrap config must only contain paths that exist on head node
|
| 442 |
+
config["auth"].pop("ssh_public_key", None)
|
| 443 |
+
|
| 444 |
+
for node_type in config["available_node_types"].values():
|
| 445 |
+
azure_arm_parameters = node_type["node_config"].setdefault(
|
| 446 |
+
"azure_arm_parameters", {}
|
| 447 |
+
)
|
| 448 |
+
azure_arm_parameters["adminUsername"] = ssh_user
|
| 449 |
+
azure_arm_parameters["publicKey"] = public_key
|
| 450 |
+
|
| 451 |
+
return config
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
def _delete_role_assignments_for_principal(
|
| 455 |
+
resource_client: ResourceManagementClient,
|
| 456 |
+
resource_group: str,
|
| 457 |
+
principal_id: str,
|
| 458 |
+
) -> None:
|
| 459 |
+
"""Delete all role assignments in the resource group for the given principal.
|
| 460 |
+
|
| 461 |
+
Uses the generic ResourceManagementClient so we avoid depending on
|
| 462 |
+
azure-mgmt-authorization. All role assignments associated with the
|
| 463 |
+
provided principal ID are removed.
|
| 464 |
+
"""
|
| 465 |
+
|
| 466 |
+
if not principal_id:
|
| 467 |
+
return
|
| 468 |
+
|
| 469 |
+
list_by_rg = get_azure_sdk_function(
|
| 470 |
+
client=resource_client.resources, function_name="list_by_resource_group"
|
| 471 |
+
)
|
| 472 |
+
delete_role_assignment = get_azure_sdk_function(
|
| 473 |
+
client=resource_client.resources, function_name="delete_by_id"
|
| 474 |
+
)
|
| 475 |
+
|
| 476 |
+
try:
|
| 477 |
+
assignments = list(
|
| 478 |
+
list_by_rg(
|
| 479 |
+
resource_group,
|
| 480 |
+
"resourceType eq 'Microsoft.Authorization/roleAssignments'",
|
| 481 |
+
)
|
| 482 |
+
)
|
| 483 |
+
logger.debug(
|
| 484 |
+
"Found %d role assignments in resource group %s while cleaning up principal %s",
|
| 485 |
+
len(assignments),
|
| 486 |
+
resource_group,
|
| 487 |
+
principal_id,
|
| 488 |
+
)
|
| 489 |
+
except HttpResponseError as exc:
|
| 490 |
+
logger.warning(
|
| 491 |
+
"Failed to enumerate role assignments for resource group %s: %s",
|
| 492 |
+
resource_group,
|
| 493 |
+
exc,
|
| 494 |
+
)
|
| 495 |
+
return
|
| 496 |
+
|
| 497 |
+
for assignment in assignments:
|
| 498 |
+
properties = getattr(assignment, "properties", {}) or {}
|
| 499 |
+
logger.debug(
|
| 500 |
+
"Inspecting role assignment %s with principalId=%s roleDefinitionId=%s",
|
| 501 |
+
getattr(assignment, "name", "<unknown>"),
|
| 502 |
+
properties.get("principalId"),
|
| 503 |
+
properties.get("roleDefinitionId"),
|
| 504 |
+
)
|
| 505 |
+
if properties.get("principalId") != principal_id:
|
| 506 |
+
continue
|
| 507 |
+
|
| 508 |
+
try:
|
| 509 |
+
delete_lro = delete_role_assignment(
|
| 510 |
+
resource_id=assignment.id,
|
| 511 |
+
api_version="2022-04-01",
|
| 512 |
+
)
|
| 513 |
+
if hasattr(delete_lro, "wait"):
|
| 514 |
+
delete_lro.wait()
|
| 515 |
+
logger.info(
|
| 516 |
+
"Deleted existing role assignment %s for principal %s",
|
| 517 |
+
assignment.name,
|
| 518 |
+
principal_id,
|
| 519 |
+
)
|
| 520 |
+
except ResourceNotFoundError:
|
| 521 |
+
logger.debug(
|
| 522 |
+
"Role assignment %s not found while processing principal %s",
|
| 523 |
+
assignment.name,
|
| 524 |
+
principal_id,
|
| 525 |
+
)
|
| 526 |
+
except Exception as exc:
|
| 527 |
+
logger.warning(
|
| 528 |
+
"Failed to delete role assignment %s for principal %s: %s",
|
| 529 |
+
assignment.name,
|
| 530 |
+
principal_id,
|
| 531 |
+
exc,
|
| 532 |
+
)
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def _wait_for_role_assignment_deletion(
|
| 536 |
+
get_role_assignment: Callable[..., Any],
|
| 537 |
+
resource_id: str,
|
| 538 |
+
role_assignment_guid: str,
|
| 539 |
+
*,
|
| 540 |
+
max_attempts: int = 10,
|
| 541 |
+
delay_seconds: int = 2,
|
| 542 |
+
) -> bool:
|
| 543 |
+
"""Poll until a role assignment disappears after deletion.
|
| 544 |
+
|
| 545 |
+
Returns True if the assignment is confirmed deleted, False otherwise.
|
| 546 |
+
Logs detailed progress to aid troubleshooting when transient errors occur.
|
| 547 |
+
"""
|
| 548 |
+
|
| 549 |
+
for attempt in range(1, max_attempts + 1):
|
| 550 |
+
try:
|
| 551 |
+
get_role_assignment(resource_id, "2022-04-01")
|
| 552 |
+
except ResourceNotFoundError:
|
| 553 |
+
return True
|
| 554 |
+
except Exception as exc: # noqa: BLE001
|
| 555 |
+
logger.debug(
|
| 556 |
+
"Attempt %d/%d to verify removal of role assignment %s failed: %s",
|
| 557 |
+
attempt,
|
| 558 |
+
max_attempts,
|
| 559 |
+
role_assignment_guid,
|
| 560 |
+
exc,
|
| 561 |
+
)
|
| 562 |
+
else:
|
| 563 |
+
logger.debug(
|
| 564 |
+
"Role assignment %s still present after deletion (attempt %d/%d)",
|
| 565 |
+
role_assignment_guid,
|
| 566 |
+
attempt,
|
| 567 |
+
max_attempts,
|
| 568 |
+
)
|
| 569 |
+
|
| 570 |
+
if attempt < max_attempts:
|
| 571 |
+
time.sleep(delay_seconds)
|
| 572 |
+
|
| 573 |
+
return False
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def _generate_arm_guid(*values: Any) -> str:
|
| 577 |
+
"""Replicates ARM template guid() function for creating deterministic IDs."""
|
| 578 |
+
|
| 579 |
+
concatenated = "".join(str(v) for v in values)
|
| 580 |
+
return str(UUID(md5(concatenated.encode("utf-8")).hexdigest()))
|
lib/python3.12/site-packages/ray/autoscaler/_private/_azure/node_provider.py
ADDED
|
@@ -0,0 +1,942 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
import os
|
| 4 |
+
import time
|
| 5 |
+
from concurrent.futures import Future, ThreadPoolExecutor
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
from threading import RLock
|
| 8 |
+
from typing import List, Optional
|
| 9 |
+
from uuid import uuid4
|
| 10 |
+
|
| 11 |
+
from azure.common.credentials import get_cli_profile
|
| 12 |
+
from azure.core.exceptions import ResourceNotFoundError
|
| 13 |
+
from azure.identity import DefaultAzureCredential
|
| 14 |
+
from azure.mgmt.compute import ComputeManagementClient
|
| 15 |
+
from azure.mgmt.network import NetworkManagementClient
|
| 16 |
+
from azure.mgmt.resource import ResourceManagementClient
|
| 17 |
+
from azure.mgmt.resource.resources.models import DeploymentMode
|
| 18 |
+
|
| 19 |
+
from ray._common.usage.usage_lib import get_cloud_from_metadata_requests
|
| 20 |
+
from ray.autoscaler._private._azure.config import (
|
| 21 |
+
_delete_role_assignments_for_principal,
|
| 22 |
+
_generate_arm_guid,
|
| 23 |
+
bootstrap_azure,
|
| 24 |
+
get_azure_sdk_function,
|
| 25 |
+
)
|
| 26 |
+
from ray.autoscaler._private.constants import (
|
| 27 |
+
AUTOSCALER_NODE_START_WAIT_S,
|
| 28 |
+
AUTOSCALER_NODE_TERMINATE_WAIT_S,
|
| 29 |
+
MAX_PARALLEL_SHUTDOWN_WORKERS,
|
| 30 |
+
)
|
| 31 |
+
from ray.autoscaler.node_provider import NodeProvider
|
| 32 |
+
from ray.autoscaler.tags import (
|
| 33 |
+
NODE_KIND_HEAD,
|
| 34 |
+
TAG_RAY_CLUSTER_NAME,
|
| 35 |
+
TAG_RAY_LAUNCH_CONFIG,
|
| 36 |
+
TAG_RAY_NODE_KIND,
|
| 37 |
+
TAG_RAY_NODE_NAME,
|
| 38 |
+
TAG_RAY_USER_NODE_TYPE,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
VM_NAME_MAX_LEN = 64
|
| 42 |
+
UNIQUE_ID_LEN = 4
|
| 43 |
+
|
| 44 |
+
logger = logging.getLogger(__name__)
|
| 45 |
+
azure_logger = logging.getLogger("azure.core.pipeline.policies.http_logging_policy")
|
| 46 |
+
azure_logger.setLevel(logging.WARNING)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def synchronized(f):
|
| 50 |
+
def wrapper(self, *args, **kwargs):
|
| 51 |
+
self.lock.acquire()
|
| 52 |
+
try:
|
| 53 |
+
return f(self, *args, **kwargs)
|
| 54 |
+
finally:
|
| 55 |
+
self.lock.release()
|
| 56 |
+
|
| 57 |
+
return wrapper
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class AzureNodeProvider(NodeProvider):
|
| 61 |
+
"""Node Provider for Azure
|
| 62 |
+
|
| 63 |
+
This provider assumes Azure credentials are set by running ``az login``
|
| 64 |
+
and the default subscription is configured through ``az account``
|
| 65 |
+
or set in the ``provider`` field of the autoscaler configuration.
|
| 66 |
+
|
| 67 |
+
Nodes may be in one of three states: {pending, running, terminated}. Nodes
|
| 68 |
+
appear immediately once started by ``create_node``, and transition
|
| 69 |
+
immediately to terminated when ``terminate_node`` is called.
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
def __init__(self, provider_config, cluster_name):
|
| 73 |
+
NodeProvider.__init__(self, provider_config, cluster_name)
|
| 74 |
+
subscription_id = provider_config.get("subscription_id")
|
| 75 |
+
if subscription_id is None:
|
| 76 |
+
# Get subscription from logged in azure profile
|
| 77 |
+
# if it isn't provided in the provider_config
|
| 78 |
+
# so operations like `get-head-ip` will work
|
| 79 |
+
subscription_id = get_cli_profile().get_subscription_id()
|
| 80 |
+
logger.info(
|
| 81 |
+
"subscription_id not found in provider config, falling back "
|
| 82 |
+
f"to subscription_id from the logged in azure profile: {subscription_id}"
|
| 83 |
+
)
|
| 84 |
+
self.cache_stopped_nodes = provider_config.get("cache_stopped_nodes", True)
|
| 85 |
+
|
| 86 |
+
# Detect cloud environment to optimize Azure credential chain.
|
| 87 |
+
# On non-Azure clouds (AWS, GCP), skip Azure-specific auth methods
|
| 88 |
+
# (managed identity, workload identity) to avoid IMDS timeout delays / failures.
|
| 89 |
+
detected_cloud = get_cloud_from_metadata_requests()
|
| 90 |
+
on_azure = detected_cloud == "azure"
|
| 91 |
+
|
| 92 |
+
if on_azure:
|
| 93 |
+
logger.info(
|
| 94 |
+
"Initializing Azure node provider for Azure infrastructure "
|
| 95 |
+
"running on Azure cloud environment"
|
| 96 |
+
)
|
| 97 |
+
else:
|
| 98 |
+
logger.info(
|
| 99 |
+
f"Initializing Azure node provider for Azure infrastructure "
|
| 100 |
+
f"but detected this is running on a '{detected_cloud}' environment. "
|
| 101 |
+
f"Skipping Azure-specific authentication methods to avoid timeouts."
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
credential = DefaultAzureCredential(
|
| 105 |
+
exclude_shared_token_cache_credential=True,
|
| 106 |
+
exclude_managed_identity_credential=not on_azure,
|
| 107 |
+
exclude_workload_identity_credential=not on_azure,
|
| 108 |
+
)
|
| 109 |
+
self.compute_client = ComputeManagementClient(credential, subscription_id)
|
| 110 |
+
self.network_client = NetworkManagementClient(credential, subscription_id)
|
| 111 |
+
self.resource_client = ResourceManagementClient(credential, subscription_id)
|
| 112 |
+
|
| 113 |
+
self.lock = RLock()
|
| 114 |
+
|
| 115 |
+
# cache node objects
|
| 116 |
+
self.cached_nodes = {}
|
| 117 |
+
|
| 118 |
+
# Cache terminating node operations
|
| 119 |
+
self.terminating_nodes: dict[str, Future] = {}
|
| 120 |
+
self.termination_executor = ThreadPoolExecutor(
|
| 121 |
+
max_workers=MAX_PARALLEL_SHUTDOWN_WORKERS
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
@synchronized
|
| 125 |
+
def _get_filtered_nodes(self, tag_filters):
|
| 126 |
+
# add cluster name filter to only get nodes from this cluster
|
| 127 |
+
cluster_tag_filters = {**tag_filters, TAG_RAY_CLUSTER_NAME: self.cluster_name}
|
| 128 |
+
|
| 129 |
+
def match_tags(tags):
|
| 130 |
+
for k, v in cluster_tag_filters.items():
|
| 131 |
+
if tags.get(k) != v:
|
| 132 |
+
return False
|
| 133 |
+
return True
|
| 134 |
+
|
| 135 |
+
vms = self.compute_client.virtual_machines.list(
|
| 136 |
+
resource_group_name=self.provider_config["resource_group"]
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
nodes = [self._extract_metadata(vm) for vm in vms]
|
| 140 |
+
self.cached_nodes = {node["name"]: node for node in nodes}
|
| 141 |
+
|
| 142 |
+
# Update terminating nodes list by removing nodes that
|
| 143 |
+
# have finished termination.
|
| 144 |
+
self.terminating_nodes = {
|
| 145 |
+
k: v for k, v in self.terminating_nodes.items() if not v.done()
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
return {
|
| 149 |
+
k: v
|
| 150 |
+
for k, v in self.cached_nodes.items()
|
| 151 |
+
if v["tags"] is not None and match_tags(v["tags"])
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
def _extract_metadata(self, vm):
|
| 155 |
+
# get tags
|
| 156 |
+
metadata = {"name": vm.name, "tags": vm.tags, "status": ""}
|
| 157 |
+
|
| 158 |
+
# get status
|
| 159 |
+
resource_group = self.provider_config["resource_group"]
|
| 160 |
+
try:
|
| 161 |
+
instance = self.compute_client.virtual_machines.instance_view(
|
| 162 |
+
resource_group_name=resource_group, vm_name=vm.name
|
| 163 |
+
).as_dict()
|
| 164 |
+
except ResourceNotFoundError:
|
| 165 |
+
return metadata
|
| 166 |
+
|
| 167 |
+
for status in instance["statuses"]:
|
| 168 |
+
# If ProvisioningState is "failed" (e.g.,
|
| 169 |
+
# ProvisioningState/failed/RetryableError), we can get a third
|
| 170 |
+
# string here, so we need to limit to the first two outputs.
|
| 171 |
+
code, state = status["code"].split("/")[:2]
|
| 172 |
+
# skip provisioning status
|
| 173 |
+
if code == "PowerState":
|
| 174 |
+
metadata["status"] = state
|
| 175 |
+
break
|
| 176 |
+
|
| 177 |
+
# get ip data
|
| 178 |
+
nic_id = vm.network_profile.network_interfaces[0].id
|
| 179 |
+
metadata["nic_name"] = nic_id.split("/")[-1]
|
| 180 |
+
nic = self.network_client.network_interfaces.get(
|
| 181 |
+
resource_group_name=resource_group,
|
| 182 |
+
network_interface_name=metadata["nic_name"],
|
| 183 |
+
)
|
| 184 |
+
ip_config = nic.ip_configurations[0]
|
| 185 |
+
|
| 186 |
+
# Get public IP if not using internal IPs or if this is the
|
| 187 |
+
# head node and use_external_head_ip is True
|
| 188 |
+
if not self.provider_config.get("use_internal_ips", False) or (
|
| 189 |
+
self.provider_config.get("use_external_head_ip", False)
|
| 190 |
+
and metadata["tags"][TAG_RAY_NODE_KIND] == NODE_KIND_HEAD
|
| 191 |
+
):
|
| 192 |
+
public_ip_id = ip_config.public_ip_address.id
|
| 193 |
+
metadata["public_ip_name"] = public_ip_id.split("/")[-1]
|
| 194 |
+
public_ip = self.network_client.public_ip_addresses.get(
|
| 195 |
+
resource_group_name=resource_group,
|
| 196 |
+
public_ip_address_name=metadata["public_ip_name"],
|
| 197 |
+
)
|
| 198 |
+
metadata["external_ip"] = public_ip.ip_address
|
| 199 |
+
|
| 200 |
+
metadata["internal_ip"] = ip_config.private_ip_address
|
| 201 |
+
|
| 202 |
+
return metadata
|
| 203 |
+
|
| 204 |
+
def _get_zones_for_vm_size(self, vm_size, location):
|
| 205 |
+
"""Get usable availability zones for a given VM size in a specific location."""
|
| 206 |
+
try:
|
| 207 |
+
# Note: Azure ResourceSKUs API filters don't work reliably(?), so we query all SKUs
|
| 208 |
+
# and filter in code. Each SKU object represents one location for the VM size.
|
| 209 |
+
skus = self.compute_client.resource_skus.list()
|
| 210 |
+
|
| 211 |
+
for sku in skus:
|
| 212 |
+
if sku.name == vm_size and sku.location_info:
|
| 213 |
+
# Each SKU object represents one location, check if it matches our target
|
| 214 |
+
for location_info in sku.location_info:
|
| 215 |
+
if location_info.location.lower() == location.lower():
|
| 216 |
+
zones = location_info.zones if location_info.zones else []
|
| 217 |
+
logger.debug(
|
| 218 |
+
f"Found {vm_size} in {location} with zones: {zones}"
|
| 219 |
+
)
|
| 220 |
+
return sorted(zones)
|
| 221 |
+
|
| 222 |
+
logger.warning(f"No zones found for {vm_size} in {location}")
|
| 223 |
+
return [] # No zones available for this VM size
|
| 224 |
+
except Exception as e:
|
| 225 |
+
logger.warning(
|
| 226 |
+
f"Failed to get zones for VM size {vm_size} in location {location}: {str(e)}"
|
| 227 |
+
)
|
| 228 |
+
return []
|
| 229 |
+
|
| 230 |
+
def _parse_availability_zones(
|
| 231 |
+
self, availability_zone_config: Optional[str]
|
| 232 |
+
) -> Optional[List[str]]:
|
| 233 |
+
"""Parse availability_zone configuration from comma-separated string format.
|
| 234 |
+
|
| 235 |
+
Args:
|
| 236 |
+
availability_zone_config: Can be:
|
| 237 |
+
- String: comma-separated zones like "1,2,3"
|
| 238 |
+
- "none": explicitly disable zones
|
| 239 |
+
- "auto": let Azure automatically pick zones
|
| 240 |
+
- None: no zones specified (defaults to letting Azure pick)
|
| 241 |
+
|
| 242 |
+
Returns:
|
| 243 |
+
List of zone strings, or None if zones explicitly disabled, or [] if auto/unspecified
|
| 244 |
+
"""
|
| 245 |
+
if availability_zone_config is None:
|
| 246 |
+
return [] # Auto - let Azure pick
|
| 247 |
+
|
| 248 |
+
# Handle string format (AWS-style comma-separated)
|
| 249 |
+
if isinstance(availability_zone_config, str):
|
| 250 |
+
# Strip whitespace and split by comma
|
| 251 |
+
zones = [zone.strip() for zone in availability_zone_config.split(",")]
|
| 252 |
+
|
| 253 |
+
# Handle special cases (case-insensitive)
|
| 254 |
+
if len(zones) == 1:
|
| 255 |
+
zone_lower = zones[0].lower()
|
| 256 |
+
if zone_lower in ["none", "null"]:
|
| 257 |
+
return None # Explicitly disabled
|
| 258 |
+
elif zone_lower == "auto":
|
| 259 |
+
return [] # Auto - let Azure pick
|
| 260 |
+
|
| 261 |
+
# Handle empty string or whitespace-only
|
| 262 |
+
if not zones or all(not zone for zone in zones):
|
| 263 |
+
return [] # Auto - let Azure pick
|
| 264 |
+
return zones
|
| 265 |
+
|
| 266 |
+
# Unsupported format
|
| 267 |
+
raise ValueError(
|
| 268 |
+
f"availability_zone must be a string, got {type(availability_zone_config).__name__}: {availability_zone_config!r}"
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
def _validate_zones_for_node_pool(self, zones, location, vm_size):
|
| 272 |
+
"""Validate that the specified zones are available for the given VM size in the location."""
|
| 273 |
+
# Special case: zones=None means explicitly disabled availability zones
|
| 274 |
+
if zones is None:
|
| 275 |
+
logger.info(
|
| 276 |
+
"Zones explicitly disabled with 'none' - will create VM without an availability zone"
|
| 277 |
+
)
|
| 278 |
+
return None # Special return value to indicate "no zones by choice"
|
| 279 |
+
|
| 280 |
+
vm_zones = self._get_zones_for_vm_size(vm_size, location)
|
| 281 |
+
|
| 282 |
+
available_zones = set(vm_zones)
|
| 283 |
+
if not available_zones:
|
| 284 |
+
logger.warning("No zones available for this VM size and location")
|
| 285 |
+
return []
|
| 286 |
+
|
| 287 |
+
if zones:
|
| 288 |
+
requested_zones = {str(z) for z in zones}
|
| 289 |
+
intersection = sorted(available_zones.intersection(requested_zones))
|
| 290 |
+
return intersection
|
| 291 |
+
|
| 292 |
+
return sorted(available_zones)
|
| 293 |
+
|
| 294 |
+
def stopped_nodes(self, tag_filters):
|
| 295 |
+
"""Return a list of stopped node ids filtered by the specified tags dict."""
|
| 296 |
+
nodes = self._get_filtered_nodes(tag_filters=tag_filters)
|
| 297 |
+
return [k for k, v in nodes.items() if v["status"].startswith("deallocat")]
|
| 298 |
+
|
| 299 |
+
def non_terminated_nodes(self, tag_filters):
|
| 300 |
+
"""Return a list of node ids filtered by the specified tags dict.
|
| 301 |
+
|
| 302 |
+
This list must not include terminated nodes. For performance reasons,
|
| 303 |
+
providers are allowed to cache the result of a call to nodes() to
|
| 304 |
+
serve single-node queries (e.g. is_running(node_id)). This means that
|
| 305 |
+
nodes() must be called again to refresh results.
|
| 306 |
+
|
| 307 |
+
Examples:
|
| 308 |
+
>>> from ray.autoscaler.tags import TAG_RAY_NODE_KIND
|
| 309 |
+
>>> provider = ... # doctest: +SKIP
|
| 310 |
+
>>> provider.non_terminated_nodes( # doctest: +SKIP
|
| 311 |
+
... {TAG_RAY_NODE_KIND: "worker"})
|
| 312 |
+
["node-1", "node-2"]
|
| 313 |
+
"""
|
| 314 |
+
nodes = self._get_filtered_nodes(tag_filters=tag_filters)
|
| 315 |
+
return [
|
| 316 |
+
k
|
| 317 |
+
for k, v in nodes.items()
|
| 318 |
+
if not v["status"].startswith("deallocat") or k in self.terminating_nodes
|
| 319 |
+
]
|
| 320 |
+
|
| 321 |
+
def is_running(self, node_id):
|
| 322 |
+
"""Return whether the specified node is running."""
|
| 323 |
+
# always get current status
|
| 324 |
+
node = self._get_node(node_id=node_id)
|
| 325 |
+
return node["status"] == "running"
|
| 326 |
+
|
| 327 |
+
def is_terminated(self, node_id):
|
| 328 |
+
"""Return whether the specified node is terminated."""
|
| 329 |
+
# always get current status
|
| 330 |
+
node = self._get_node(node_id=node_id)
|
| 331 |
+
return node["status"].startswith("deallocat")
|
| 332 |
+
|
| 333 |
+
def node_tags(self, node_id):
|
| 334 |
+
"""Returns the tags of the given node (string dict)."""
|
| 335 |
+
return self._get_cached_node(node_id=node_id)["tags"]
|
| 336 |
+
|
| 337 |
+
def external_ip(self, node_id):
|
| 338 |
+
"""Returns the external ip of the given node."""
|
| 339 |
+
ip = (
|
| 340 |
+
self._get_cached_node(node_id=node_id)["external_ip"]
|
| 341 |
+
or self._get_node(node_id=node_id)["external_ip"]
|
| 342 |
+
)
|
| 343 |
+
return ip
|
| 344 |
+
|
| 345 |
+
def internal_ip(self, node_id):
|
| 346 |
+
"""Returns the internal ip (Ray ip) of the given node."""
|
| 347 |
+
ip = (
|
| 348 |
+
self._get_cached_node(node_id=node_id)["internal_ip"]
|
| 349 |
+
or self._get_node(node_id=node_id)["internal_ip"]
|
| 350 |
+
)
|
| 351 |
+
return ip
|
| 352 |
+
|
| 353 |
+
def create_node(self, node_config, tags, count):
|
| 354 |
+
resource_group = self.provider_config["resource_group"]
|
| 355 |
+
|
| 356 |
+
if self.cache_stopped_nodes:
|
| 357 |
+
VALIDITY_TAGS = [
|
| 358 |
+
TAG_RAY_CLUSTER_NAME,
|
| 359 |
+
TAG_RAY_NODE_KIND,
|
| 360 |
+
TAG_RAY_LAUNCH_CONFIG,
|
| 361 |
+
TAG_RAY_USER_NODE_TYPE,
|
| 362 |
+
]
|
| 363 |
+
filters = {tag: tags[tag] for tag in VALIDITY_TAGS if tag in tags}
|
| 364 |
+
reuse_nodes = self.stopped_nodes(filters)[:count]
|
| 365 |
+
logger.info(
|
| 366 |
+
f"Reusing nodes {list(reuse_nodes)}. "
|
| 367 |
+
"To disable reuse, set `cache_stopped_nodes: False` "
|
| 368 |
+
"under `provider` in the cluster configuration.",
|
| 369 |
+
)
|
| 370 |
+
start = get_azure_sdk_function(
|
| 371 |
+
client=self.compute_client.virtual_machines, function_name="start"
|
| 372 |
+
)
|
| 373 |
+
for node_id in reuse_nodes:
|
| 374 |
+
start(resource_group_name=resource_group, vm_name=node_id).wait()
|
| 375 |
+
self.set_node_tags(node_id, tags)
|
| 376 |
+
count -= len(reuse_nodes)
|
| 377 |
+
|
| 378 |
+
if count:
|
| 379 |
+
self._create_node(node_config, tags, count)
|
| 380 |
+
|
| 381 |
+
def _create_node(self, node_config, tags, count):
|
| 382 |
+
"""Creates a number of nodes within the namespace."""
|
| 383 |
+
resource_group = self.provider_config["resource_group"]
|
| 384 |
+
location = self.provider_config["location"]
|
| 385 |
+
vm_size = node_config["azure_arm_parameters"]["vmSize"]
|
| 386 |
+
|
| 387 |
+
# Determine availability zones with precedence: node-level > provider-level
|
| 388 |
+
# Check for "availability_zone" field in node config first
|
| 389 |
+
node_availability_zone = node_config.get("azure_arm_parameters", {}).get(
|
| 390 |
+
"availability_zone"
|
| 391 |
+
)
|
| 392 |
+
# Then check provider-level "availability_zone"
|
| 393 |
+
provider_availability_zone = self.provider_config.get("availability_zone")
|
| 394 |
+
|
| 395 |
+
requested_zones = []
|
| 396 |
+
zone_source = "default"
|
| 397 |
+
|
| 398 |
+
# Precedence: node availability_zone > provider availability_zone
|
| 399 |
+
if node_availability_zone is not None:
|
| 400 |
+
requested_zones = self._parse_availability_zones(node_availability_zone)
|
| 401 |
+
zone_source = "node config availability_zone"
|
| 402 |
+
elif provider_availability_zone is not None:
|
| 403 |
+
requested_zones = self._parse_availability_zones(provider_availability_zone)
|
| 404 |
+
zone_source = "provider availability_zone"
|
| 405 |
+
|
| 406 |
+
logger.info(f"Requested zones from {zone_source}: {requested_zones}")
|
| 407 |
+
|
| 408 |
+
# Get actually available zones for this VM size
|
| 409 |
+
available_zones = self._validate_zones_for_node_pool(
|
| 410 |
+
requested_zones, location, vm_size
|
| 411 |
+
)
|
| 412 |
+
|
| 413 |
+
# Handle explicit zone disabling
|
| 414 |
+
zones_explicitly_disabled = available_zones is None
|
| 415 |
+
|
| 416 |
+
if requested_zones and not zones_explicitly_disabled and not available_zones:
|
| 417 |
+
raise ValueError(
|
| 418 |
+
f"No available zones for VM size {vm_size} in {location}. "
|
| 419 |
+
f"Requested: {requested_zones}, but none are available for this VM size."
|
| 420 |
+
)
|
| 421 |
+
|
| 422 |
+
# load the template file
|
| 423 |
+
current_path = Path(__file__).parent
|
| 424 |
+
template_path = current_path.joinpath("azure-vm-template.json")
|
| 425 |
+
with open(template_path, "r") as template_fp:
|
| 426 |
+
template = json.load(template_fp)
|
| 427 |
+
|
| 428 |
+
# get the tags
|
| 429 |
+
config_tags = node_config.get("tags", {}).copy()
|
| 430 |
+
config_tags.update(tags)
|
| 431 |
+
config_tags[TAG_RAY_CLUSTER_NAME] = self.cluster_name
|
| 432 |
+
|
| 433 |
+
deployment_name = "{node}-{unique_id}-{vm_id}".format(
|
| 434 |
+
node=config_tags.get(TAG_RAY_NODE_NAME, "node"),
|
| 435 |
+
unique_id=self.provider_config["unique_id"],
|
| 436 |
+
vm_id=uuid4().hex[:UNIQUE_ID_LEN],
|
| 437 |
+
)[:VM_NAME_MAX_LEN]
|
| 438 |
+
|
| 439 |
+
template_params = node_config["azure_arm_parameters"].copy()
|
| 440 |
+
# Remove availability_zone from template params since ARM template expects "zones"
|
| 441 |
+
template_params.pop("availability_zone", None)
|
| 442 |
+
# Use deployment_name for the vmName template parameter since
|
| 443 |
+
# the template will append copyIndex() for each VM that gets created
|
| 444 |
+
# to guarantee uniqueness.
|
| 445 |
+
template_params["vmName"] = deployment_name
|
| 446 |
+
# Provision public IP if not using internal IPs or if this is the
|
| 447 |
+
# head node and use_external_head_ip is True
|
| 448 |
+
template_params["provisionPublicIp"] = not self.provider_config.get(
|
| 449 |
+
"use_internal_ips", False
|
| 450 |
+
) or (
|
| 451 |
+
self.provider_config.get("use_external_head_ip", False)
|
| 452 |
+
and config_tags[TAG_RAY_NODE_KIND] == NODE_KIND_HEAD
|
| 453 |
+
)
|
| 454 |
+
template_params["vmTags"] = config_tags
|
| 455 |
+
template_params["vmCount"] = count
|
| 456 |
+
template_params["msi"] = self.provider_config["msi"]
|
| 457 |
+
template_params["nsg"] = self.provider_config["nsg"]
|
| 458 |
+
template_params["subnet"] = self.provider_config["subnet"]
|
| 459 |
+
|
| 460 |
+
# Add zone information based on availability and requested zones
|
| 461 |
+
if zones_explicitly_disabled:
|
| 462 |
+
# User explicitly disabled zones with ["None"]
|
| 463 |
+
template_params["zones"] = []
|
| 464 |
+
logger.info(
|
| 465 |
+
f"Creating {count} VMs with zones explicitly disabled (no availability zone)"
|
| 466 |
+
)
|
| 467 |
+
elif available_zones:
|
| 468 |
+
# Pass the list of available zones to the template
|
| 469 |
+
template_params["zones"] = available_zones
|
| 470 |
+
logger.info(
|
| 471 |
+
f"Creating {count} VMs, distributed across availability zones: {available_zones}"
|
| 472 |
+
)
|
| 473 |
+
else:
|
| 474 |
+
# For non-zonal deployments (no zones available), use empty array
|
| 475 |
+
template_params["zones"] = []
|
| 476 |
+
logger.info(f"Creating {count} VMs without specific availability zone")
|
| 477 |
+
|
| 478 |
+
parameters = {
|
| 479 |
+
"properties": {
|
| 480 |
+
"mode": DeploymentMode.incremental,
|
| 481 |
+
"template": template,
|
| 482 |
+
"parameters": {
|
| 483 |
+
key: {"value": value} for key, value in template_params.items()
|
| 484 |
+
},
|
| 485 |
+
}
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
# TODO: we could get the private/public ips back directly
|
| 489 |
+
create_or_update = get_azure_sdk_function(
|
| 490 |
+
client=self.resource_client.deployments,
|
| 491 |
+
function_name="create_or_update",
|
| 492 |
+
)
|
| 493 |
+
create_or_update(
|
| 494 |
+
resource_group_name=resource_group,
|
| 495 |
+
deployment_name=deployment_name,
|
| 496 |
+
parameters=parameters,
|
| 497 |
+
).wait(timeout=AUTOSCALER_NODE_START_WAIT_S)
|
| 498 |
+
|
| 499 |
+
@synchronized
|
| 500 |
+
def set_node_tags(self, node_id, tags):
|
| 501 |
+
"""Sets the tag values (string dict) for the specified node."""
|
| 502 |
+
node_tags = self._get_cached_node(node_id)["tags"]
|
| 503 |
+
node_tags.update(tags)
|
| 504 |
+
update = get_azure_sdk_function(
|
| 505 |
+
client=self.compute_client.virtual_machines, function_name="update"
|
| 506 |
+
)
|
| 507 |
+
update(
|
| 508 |
+
resource_group_name=self.provider_config["resource_group"],
|
| 509 |
+
vm_name=node_id,
|
| 510 |
+
parameters={"tags": node_tags},
|
| 511 |
+
)
|
| 512 |
+
self.cached_nodes[node_id]["tags"] = node_tags
|
| 513 |
+
|
| 514 |
+
def terminate_node(self, node_id):
|
| 515 |
+
"""Terminates the specified node. This will delete the VM and
|
| 516 |
+
associated resources (NIC, IP, Storage) for the specified node."""
|
| 517 |
+
|
| 518 |
+
resource_group = self.provider_config["resource_group"]
|
| 519 |
+
|
| 520 |
+
if self.cache_stopped_nodes:
|
| 521 |
+
try:
|
| 522 |
+
# stop machine and leave all resources
|
| 523 |
+
logger.info(
|
| 524 |
+
f"Stopping instance {node_id}"
|
| 525 |
+
"(to fully terminate instead, "
|
| 526 |
+
"set `cache_stopped_nodes: False` "
|
| 527 |
+
"under `provider` in the cluster configuration)"
|
| 528 |
+
)
|
| 529 |
+
stop = get_azure_sdk_function(
|
| 530 |
+
client=self.compute_client.virtual_machines,
|
| 531 |
+
function_name="deallocate",
|
| 532 |
+
)
|
| 533 |
+
stop(resource_group_name=resource_group, vm_name=node_id)
|
| 534 |
+
except Exception as e:
|
| 535 |
+
logger.warning("Failed to stop VM: {}".format(e))
|
| 536 |
+
|
| 537 |
+
# If node_id is in terminating nodes dict, it's already terminating
|
| 538 |
+
# Otherwise, kick off termination and add it to the dict
|
| 539 |
+
elif node_id not in self.terminating_nodes:
|
| 540 |
+
self.terminating_nodes[node_id] = self.termination_executor.submit(
|
| 541 |
+
self._delete_node_and_resources, resource_group, node_id
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
def _delete_node_and_resources(self, resource_group, node_id):
|
| 545 |
+
try:
|
| 546 |
+
vm = self.compute_client.virtual_machines.get(
|
| 547 |
+
resource_group_name=resource_group, vm_name=node_id
|
| 548 |
+
)
|
| 549 |
+
except ResourceNotFoundError as e:
|
| 550 |
+
# Node no longer exists
|
| 551 |
+
logger.warning("Failed to delete VM: {}".format(e))
|
| 552 |
+
return
|
| 553 |
+
|
| 554 |
+
# Gather dependent disks
|
| 555 |
+
disks = set()
|
| 556 |
+
if vm.storage_profile is not None and vm.storage_profile.data_disks is not None:
|
| 557 |
+
for d in vm.storage_profile.data_disks:
|
| 558 |
+
if d.name is not None:
|
| 559 |
+
disks.add(d.name)
|
| 560 |
+
if (
|
| 561 |
+
vm.storage_profile is not None
|
| 562 |
+
and vm.storage_profile.os_disk is not None
|
| 563 |
+
and vm.storage_profile.os_disk.name is not None
|
| 564 |
+
):
|
| 565 |
+
disks.add(vm.storage_profile.os_disk.name)
|
| 566 |
+
|
| 567 |
+
# Gather dependent NICs and public IPs
|
| 568 |
+
nics = set()
|
| 569 |
+
ips = set()
|
| 570 |
+
if (
|
| 571 |
+
vm.network_profile is not None
|
| 572 |
+
and vm.network_profile.network_interfaces is not None
|
| 573 |
+
):
|
| 574 |
+
for nint in vm.network_profile.network_interfaces:
|
| 575 |
+
if nint.id is not None:
|
| 576 |
+
nic_name = nint.id.split("/")[-1]
|
| 577 |
+
nics.add(nic_name)
|
| 578 |
+
# Get public IP if not using internal IPs or if this is the
|
| 579 |
+
# head node and use_external_head_ip is True
|
| 580 |
+
if not self.provider_config.get("use_internal_ips", False) or (
|
| 581 |
+
self.provider_config.get("use_external_head_ip", False)
|
| 582 |
+
and vm.tags[TAG_RAY_NODE_KIND] == NODE_KIND_HEAD
|
| 583 |
+
):
|
| 584 |
+
nic = self.network_client.network_interfaces.get(
|
| 585 |
+
resource_group_name=resource_group,
|
| 586 |
+
network_interface_name=nic_name,
|
| 587 |
+
)
|
| 588 |
+
if nic.ip_configurations is not None:
|
| 589 |
+
for ipc in nic.ip_configurations:
|
| 590 |
+
if ipc.public_ip_address.id is not None:
|
| 591 |
+
ips.add(ipc.public_ip_address.id.split("/")[-1])
|
| 592 |
+
|
| 593 |
+
# Delete VM
|
| 594 |
+
st = time.monotonic()
|
| 595 |
+
delete = get_azure_sdk_function(
|
| 596 |
+
client=self.compute_client.virtual_machines,
|
| 597 |
+
function_name="delete",
|
| 598 |
+
)
|
| 599 |
+
try:
|
| 600 |
+
delete(resource_group_name=resource_group, vm_name=node_id).wait(
|
| 601 |
+
timeout=AUTOSCALER_NODE_TERMINATE_WAIT_S
|
| 602 |
+
)
|
| 603 |
+
except Exception as e:
|
| 604 |
+
logger.warning("Failed to delete VM: {}".format(e))
|
| 605 |
+
|
| 606 |
+
# Delete disks (no need to wait for these, but gather the LROs for end)
|
| 607 |
+
disk_lros = []
|
| 608 |
+
delete = get_azure_sdk_function(
|
| 609 |
+
client=self.compute_client.disks, function_name="delete"
|
| 610 |
+
)
|
| 611 |
+
for d in disks:
|
| 612 |
+
try:
|
| 613 |
+
disk_lros.append(
|
| 614 |
+
delete(
|
| 615 |
+
resource_group_name=resource_group,
|
| 616 |
+
disk_name=d,
|
| 617 |
+
)
|
| 618 |
+
)
|
| 619 |
+
except Exception as e:
|
| 620 |
+
logger.warning("Failed to delete disk: {}".format(e))
|
| 621 |
+
|
| 622 |
+
# Delete NICs
|
| 623 |
+
nic_lros = []
|
| 624 |
+
delete = get_azure_sdk_function(
|
| 625 |
+
client=self.network_client.network_interfaces, function_name="delete"
|
| 626 |
+
)
|
| 627 |
+
for n in nics:
|
| 628 |
+
try:
|
| 629 |
+
nic_lros.append(
|
| 630 |
+
delete(
|
| 631 |
+
resource_group_name=resource_group,
|
| 632 |
+
network_interface_name=n,
|
| 633 |
+
)
|
| 634 |
+
)
|
| 635 |
+
except Exception as e:
|
| 636 |
+
logger.warning("Failed to delete NIC: {}".format(e))
|
| 637 |
+
|
| 638 |
+
while (
|
| 639 |
+
not all(nlro.done() for nlro in nic_lros)
|
| 640 |
+
and (time.monotonic() - st) < AUTOSCALER_NODE_TERMINATE_WAIT_S
|
| 641 |
+
):
|
| 642 |
+
time.sleep(0.1)
|
| 643 |
+
|
| 644 |
+
# Delete Public IPs
|
| 645 |
+
delete = get_azure_sdk_function(
|
| 646 |
+
client=self.network_client.public_ip_addresses,
|
| 647 |
+
function_name="delete",
|
| 648 |
+
)
|
| 649 |
+
ip_lros = []
|
| 650 |
+
for ip in ips:
|
| 651 |
+
try:
|
| 652 |
+
ip_lros.append(
|
| 653 |
+
delete(
|
| 654 |
+
resource_group_name=resource_group,
|
| 655 |
+
public_ip_address_name=ip,
|
| 656 |
+
)
|
| 657 |
+
)
|
| 658 |
+
except Exception as e:
|
| 659 |
+
logger.warning("Failed to delete public IP: {}".format(e))
|
| 660 |
+
|
| 661 |
+
while (
|
| 662 |
+
not all(dlro.done() for dlro in disk_lros)
|
| 663 |
+
and (time.monotonic() - st) < AUTOSCALER_NODE_TERMINATE_WAIT_S
|
| 664 |
+
):
|
| 665 |
+
time.sleep(0.1)
|
| 666 |
+
while (
|
| 667 |
+
not all(iplro.done() for iplro in ip_lros)
|
| 668 |
+
and (time.monotonic() - st) < AUTOSCALER_NODE_TERMINATE_WAIT_S
|
| 669 |
+
):
|
| 670 |
+
time.sleep(0.1)
|
| 671 |
+
|
| 672 |
+
def cleanup_cluster_resources(self):
|
| 673 |
+
"""Delete shared cluster infrastructure (MSI, NSG, Subnet, VNet)."""
|
| 674 |
+
|
| 675 |
+
resource_group = self.provider_config["resource_group"]
|
| 676 |
+
|
| 677 |
+
msi_principal_id = self._cleanup_managed_identity(
|
| 678 |
+
resource_group, self.provider_config.get("msi")
|
| 679 |
+
)
|
| 680 |
+
|
| 681 |
+
subnet_id = self.provider_config.get("subnet")
|
| 682 |
+
vnet_name = self._cleanup_subnet(resource_group, subnet_id)
|
| 683 |
+
|
| 684 |
+
nsg_id = self.provider_config.get("nsg")
|
| 685 |
+
self._cleanup_nsg(resource_group, nsg_id)
|
| 686 |
+
|
| 687 |
+
self._cleanup_vnet(resource_group, subnet_id, vnet_name)
|
| 688 |
+
|
| 689 |
+
self._cleanup_role_assignments(resource_group, msi_principal_id)
|
| 690 |
+
|
| 691 |
+
self._prune_provider_config_entries()
|
| 692 |
+
self._cleanup_config_cache()
|
| 693 |
+
|
| 694 |
+
@staticmethod
|
| 695 |
+
def _get_resource_name_from_id(resource_id: Optional[str]) -> Optional[str]:
|
| 696 |
+
if resource_id:
|
| 697 |
+
return resource_id.split("/")[-1]
|
| 698 |
+
return None
|
| 699 |
+
|
| 700 |
+
@staticmethod
|
| 701 |
+
def _retry_delete(delete_fn, max_retries: int = 5, initial_delay: int = 2):
|
| 702 |
+
"""Retry a delete operation with exponential backoff."""
|
| 703 |
+
|
| 704 |
+
delay = initial_delay
|
| 705 |
+
for attempt in range(max_retries):
|
| 706 |
+
try:
|
| 707 |
+
return delete_fn()
|
| 708 |
+
except Exception as exc: # noqa: BLE001
|
| 709 |
+
error_msg = str(exc)
|
| 710 |
+
if "InUse" in error_msg and attempt < max_retries - 1:
|
| 711 |
+
logger.info(
|
| 712 |
+
"Resource still in use, retrying in %ss (attempt %s/%s)...",
|
| 713 |
+
delay,
|
| 714 |
+
attempt + 1,
|
| 715 |
+
max_retries,
|
| 716 |
+
)
|
| 717 |
+
time.sleep(delay)
|
| 718 |
+
delay *= 2
|
| 719 |
+
else:
|
| 720 |
+
raise
|
| 721 |
+
|
| 722 |
+
def _cleanup_managed_identity(
|
| 723 |
+
self, resource_group: str, msi_id: Optional[str]
|
| 724 |
+
) -> Optional[str]:
|
| 725 |
+
if not msi_id:
|
| 726 |
+
return None
|
| 727 |
+
|
| 728 |
+
msi_name = self._get_resource_name_from_id(msi_id)
|
| 729 |
+
if not msi_name:
|
| 730 |
+
return None
|
| 731 |
+
|
| 732 |
+
msi_principal_id: Optional[str] = None
|
| 733 |
+
try:
|
| 734 |
+
get_identity = get_azure_sdk_function(
|
| 735 |
+
client=self.resource_client.resources,
|
| 736 |
+
function_name="get_by_id",
|
| 737 |
+
)
|
| 738 |
+
existing_msi = get_identity(msi_id, "2023-01-31")
|
| 739 |
+
msi_principal_id = getattr(existing_msi, "properties", {}).get(
|
| 740 |
+
"principalId"
|
| 741 |
+
)
|
| 742 |
+
except ResourceNotFoundError:
|
| 743 |
+
msi_principal_id = None
|
| 744 |
+
except Exception as exc: # noqa: BLE001
|
| 745 |
+
logger.warning(
|
| 746 |
+
"Failed to query MSI %s for principal ID prior to deletion: %s",
|
| 747 |
+
msi_name,
|
| 748 |
+
exc,
|
| 749 |
+
)
|
| 750 |
+
|
| 751 |
+
try:
|
| 752 |
+
logger.info("Deleting Managed Service Identity: %s", msi_name)
|
| 753 |
+
delete = get_azure_sdk_function(
|
| 754 |
+
client=self.resource_client.resources,
|
| 755 |
+
function_name="delete_by_id",
|
| 756 |
+
)
|
| 757 |
+
delete(resource_id=msi_id, api_version="2023-01-31").wait()
|
| 758 |
+
logger.info("Successfully deleted MSI: %s", msi_name)
|
| 759 |
+
except ResourceNotFoundError:
|
| 760 |
+
logger.info("MSI %s not found, may have been already deleted", msi_name)
|
| 761 |
+
except Exception as exc: # noqa: BLE001
|
| 762 |
+
logger.warning("Failed to delete MSI %s: %s", msi_name, exc)
|
| 763 |
+
|
| 764 |
+
return msi_principal_id
|
| 765 |
+
|
| 766 |
+
def _cleanup_subnet(
|
| 767 |
+
self, resource_group: str, subnet_id: Optional[str]
|
| 768 |
+
) -> Optional[str]:
|
| 769 |
+
if not subnet_id:
|
| 770 |
+
return None
|
| 771 |
+
|
| 772 |
+
subnet_name = self._get_resource_name_from_id(subnet_id)
|
| 773 |
+
vnet_name: Optional[str] = None
|
| 774 |
+
|
| 775 |
+
if subnet_id and "/virtualNetworks/" in subnet_id:
|
| 776 |
+
parts = subnet_id.split("/")
|
| 777 |
+
vnet_idx = parts.index("virtualNetworks")
|
| 778 |
+
if vnet_idx + 1 < len(parts):
|
| 779 |
+
vnet_name = parts[vnet_idx + 1]
|
| 780 |
+
|
| 781 |
+
if not subnet_name or not vnet_name:
|
| 782 |
+
return None
|
| 783 |
+
|
| 784 |
+
try:
|
| 785 |
+
logger.info("Deleting Subnet: %s in VNet: %s", subnet_name, vnet_name)
|
| 786 |
+
|
| 787 |
+
def delete_subnet():
|
| 788 |
+
delete = get_azure_sdk_function(
|
| 789 |
+
client=self.network_client.subnets, function_name="delete"
|
| 790 |
+
)
|
| 791 |
+
delete(
|
| 792 |
+
resource_group_name=resource_group,
|
| 793 |
+
virtual_network_name=vnet_name,
|
| 794 |
+
subnet_name=subnet_name,
|
| 795 |
+
).wait()
|
| 796 |
+
|
| 797 |
+
self._retry_delete(delete_subnet)
|
| 798 |
+
logger.info("Successfully deleted Subnet: %s", subnet_name)
|
| 799 |
+
except ResourceNotFoundError:
|
| 800 |
+
logger.info(
|
| 801 |
+
"Subnet %s not found, may have been already deleted", subnet_name
|
| 802 |
+
)
|
| 803 |
+
except Exception as exc: # noqa: BLE001
|
| 804 |
+
logger.warning("Failed to delete Subnet %s: %s", subnet_name, exc)
|
| 805 |
+
|
| 806 |
+
return vnet_name
|
| 807 |
+
|
| 808 |
+
def _cleanup_nsg(self, resource_group: str, nsg_id: Optional[str]) -> None:
|
| 809 |
+
if not nsg_id:
|
| 810 |
+
return
|
| 811 |
+
|
| 812 |
+
nsg_name = self._get_resource_name_from_id(nsg_id)
|
| 813 |
+
if not nsg_name:
|
| 814 |
+
return
|
| 815 |
+
|
| 816 |
+
try:
|
| 817 |
+
logger.info("Deleting Network Security Group: %s", nsg_name)
|
| 818 |
+
|
| 819 |
+
def delete_nsg():
|
| 820 |
+
delete = get_azure_sdk_function(
|
| 821 |
+
client=self.network_client.network_security_groups,
|
| 822 |
+
function_name="delete",
|
| 823 |
+
)
|
| 824 |
+
delete(
|
| 825 |
+
resource_group_name=resource_group,
|
| 826 |
+
network_security_group_name=nsg_name,
|
| 827 |
+
).wait()
|
| 828 |
+
|
| 829 |
+
self._retry_delete(delete_nsg)
|
| 830 |
+
logger.info("Successfully deleted NSG: %s", nsg_name)
|
| 831 |
+
except ResourceNotFoundError:
|
| 832 |
+
logger.info("NSG %s not found, may have been already deleted", nsg_name)
|
| 833 |
+
except Exception as exc: # noqa: BLE001
|
| 834 |
+
logger.warning("Failed to delete NSG %s: %s", nsg_name, exc)
|
| 835 |
+
|
| 836 |
+
def _cleanup_vnet(
|
| 837 |
+
self,
|
| 838 |
+
resource_group: str,
|
| 839 |
+
subnet_id: Optional[str],
|
| 840 |
+
vnet_name: Optional[str],
|
| 841 |
+
) -> None:
|
| 842 |
+
if not subnet_id or not vnet_name:
|
| 843 |
+
return
|
| 844 |
+
|
| 845 |
+
try:
|
| 846 |
+
logger.info("Deleting Virtual Network: %s", vnet_name)
|
| 847 |
+
|
| 848 |
+
def delete_vnet():
|
| 849 |
+
delete = get_azure_sdk_function(
|
| 850 |
+
client=self.network_client.virtual_networks,
|
| 851 |
+
function_name="delete",
|
| 852 |
+
)
|
| 853 |
+
delete(
|
| 854 |
+
resource_group_name=resource_group,
|
| 855 |
+
virtual_network_name=vnet_name,
|
| 856 |
+
).wait()
|
| 857 |
+
|
| 858 |
+
self._retry_delete(delete_vnet)
|
| 859 |
+
logger.info("Successfully deleted VNet: %s", vnet_name)
|
| 860 |
+
except ResourceNotFoundError:
|
| 861 |
+
logger.info("VNet %s not found, may have been already deleted", vnet_name)
|
| 862 |
+
except Exception as exc: # noqa: BLE001
|
| 863 |
+
logger.warning("Failed to delete VNet %s: %s", vnet_name, exc)
|
| 864 |
+
|
| 865 |
+
def _cleanup_role_assignments(
|
| 866 |
+
self, resource_group: str, msi_principal_id: Optional[str]
|
| 867 |
+
) -> None:
|
| 868 |
+
subscription_id = self.provider_config.get("subscription_id")
|
| 869 |
+
unique_id = self.provider_config.get("unique_id")
|
| 870 |
+
if not subscription_id or not unique_id:
|
| 871 |
+
return
|
| 872 |
+
|
| 873 |
+
cluster_id = f"{self.cluster_name}-{unique_id}"
|
| 874 |
+
role_assignment_name = f"ray-{cluster_id}-ra"
|
| 875 |
+
role_assignment_guid = _generate_arm_guid(role_assignment_name)
|
| 876 |
+
role_assignment_id = (
|
| 877 |
+
f"/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers"
|
| 878 |
+
f"/Microsoft.Authorization/roleAssignments/{role_assignment_guid}"
|
| 879 |
+
)
|
| 880 |
+
|
| 881 |
+
if msi_principal_id:
|
| 882 |
+
_delete_role_assignments_for_principal(
|
| 883 |
+
self.resource_client, resource_group, msi_principal_id
|
| 884 |
+
)
|
| 885 |
+
|
| 886 |
+
delete_role_assignment = get_azure_sdk_function(
|
| 887 |
+
client=self.resource_client.resources, function_name="delete_by_id"
|
| 888 |
+
)
|
| 889 |
+
try:
|
| 890 |
+
delete_lro = delete_role_assignment(
|
| 891 |
+
resource_id=role_assignment_id,
|
| 892 |
+
api_version="2022-04-01",
|
| 893 |
+
)
|
| 894 |
+
if hasattr(delete_lro, "wait"):
|
| 895 |
+
delete_lro.wait()
|
| 896 |
+
logger.info(
|
| 897 |
+
"Deleted role assignment %s for cluster %s",
|
| 898 |
+
role_assignment_guid,
|
| 899 |
+
self.cluster_name,
|
| 900 |
+
)
|
| 901 |
+
except ResourceNotFoundError:
|
| 902 |
+
logger.debug(
|
| 903 |
+
"Role assignment %s not found during cleanup", role_assignment_guid
|
| 904 |
+
)
|
| 905 |
+
except Exception as exc: # noqa: BLE001
|
| 906 |
+
logger.warning(
|
| 907 |
+
"Failed to delete role assignment %s: %s",
|
| 908 |
+
role_assignment_guid,
|
| 909 |
+
exc,
|
| 910 |
+
)
|
| 911 |
+
|
| 912 |
+
def _prune_provider_config_entries(self) -> None:
|
| 913 |
+
for key in ("msi", "nsg", "subnet"):
|
| 914 |
+
self.provider_config.pop(key, None)
|
| 915 |
+
|
| 916 |
+
def _cleanup_config_cache(self) -> None:
|
| 917 |
+
cache_path = self.provider_config.get("_config_cache_path")
|
| 918 |
+
if not cache_path:
|
| 919 |
+
return
|
| 920 |
+
|
| 921 |
+
try:
|
| 922 |
+
if os.path.exists(cache_path):
|
| 923 |
+
os.remove(cache_path)
|
| 924 |
+
logger.info(
|
| 925 |
+
"Deleted cached Ray config at %s after resource cleanup",
|
| 926 |
+
cache_path,
|
| 927 |
+
)
|
| 928 |
+
except Exception as exc: # noqa: BLE001
|
| 929 |
+
logger.warning("Failed to delete cached Ray config %s: %s", cache_path, exc)
|
| 930 |
+
finally:
|
| 931 |
+
self.provider_config.pop("_config_cache_path", None)
|
| 932 |
+
|
| 933 |
+
def _get_node(self, node_id):
|
| 934 |
+
self._get_filtered_nodes({}) # Side effect: updates cache
|
| 935 |
+
return self.cached_nodes[node_id]
|
| 936 |
+
|
| 937 |
+
def _get_cached_node(self, node_id):
|
| 938 |
+
return self.cached_nodes.get(node_id) or self._get_node(node_id=node_id)
|
| 939 |
+
|
| 940 |
+
@staticmethod
|
| 941 |
+
def bootstrap_config(cluster_config):
|
| 942 |
+
return bootstrap_azure(cluster_config)
|
lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__init__.py
ADDED
|
File without changes
|
lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (209 Bytes). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/config.cpython-312.pyc
ADDED
|
Binary file (5.06 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/node_provider.cpython-312.pyc
ADDED
|
Binary file (16.2 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/__pycache__/utils.cpython-312.pyc
ADDED
|
Binary file (22 kB). View file
|
|
|
lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/config.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import os
|
| 3 |
+
import stat
|
| 4 |
+
|
| 5 |
+
from ray.autoscaler._private.aliyun.utils import AcsClient
|
| 6 |
+
|
| 7 |
+
# instance status
|
| 8 |
+
PENDING = "Pending"
|
| 9 |
+
RUNNING = "Running"
|
| 10 |
+
STARTING = "Starting"
|
| 11 |
+
STOPPING = "Stopping"
|
| 12 |
+
STOPPED = "Stopped"
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def bootstrap_aliyun(config):
|
| 18 |
+
# print(config["provider"])
|
| 19 |
+
# create vpc
|
| 20 |
+
_get_or_create_vpc(config)
|
| 21 |
+
# create security group id
|
| 22 |
+
_get_or_create_security_group(config)
|
| 23 |
+
# create vswitch
|
| 24 |
+
_get_or_create_vswitch(config)
|
| 25 |
+
# create key pair
|
| 26 |
+
_get_or_import_key_pair(config)
|
| 27 |
+
# print(config["provider"])
|
| 28 |
+
return config
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def _client(config):
|
| 32 |
+
return AcsClient(
|
| 33 |
+
access_key=config["provider"].get("access_key"),
|
| 34 |
+
access_key_secret=config["provider"].get("access_key_secret"),
|
| 35 |
+
region_id=config["provider"]["region"],
|
| 36 |
+
max_retries=1,
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def _get_or_create_security_group(config):
|
| 41 |
+
cli = _client(config)
|
| 42 |
+
security_groups = cli.describe_security_groups(vpc_id=config["provider"]["vpc_id"])
|
| 43 |
+
if security_groups is not None and len(security_groups) > 0:
|
| 44 |
+
config["provider"]["security_group_id"] = security_groups[0]["SecurityGroupId"]
|
| 45 |
+
return config
|
| 46 |
+
|
| 47 |
+
security_group_id = cli.create_security_group(vpc_id=config["provider"]["vpc_id"])
|
| 48 |
+
|
| 49 |
+
for rule in config["provider"].get("security_group_rule", {}):
|
| 50 |
+
cli.authorize_security_group(
|
| 51 |
+
security_group_id=security_group_id,
|
| 52 |
+
port_range=rule["port_range"],
|
| 53 |
+
source_cidr_ip=rule["source_cidr_ip"],
|
| 54 |
+
ip_protocol=rule["ip_protocol"],
|
| 55 |
+
)
|
| 56 |
+
config["provider"]["security_group_id"] = security_group_id
|
| 57 |
+
return
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
def _get_or_create_vpc(config):
|
| 61 |
+
cli = _client(config)
|
| 62 |
+
vpcs = cli.describe_vpcs()
|
| 63 |
+
if vpcs is not None and len(vpcs) > 0:
|
| 64 |
+
config["provider"]["vpc_id"] = vpcs[0].get("VpcId")
|
| 65 |
+
return
|
| 66 |
+
|
| 67 |
+
vpc_id = cli.create_vpc()
|
| 68 |
+
if vpc_id is not None:
|
| 69 |
+
config["provider"]["vpc_id"] = vpc_id
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _get_or_create_vswitch(config):
|
| 73 |
+
cli = _client(config)
|
| 74 |
+
vswitches = cli.describe_v_switches(vpc_id=config["provider"]["vpc_id"])
|
| 75 |
+
if vswitches is not None and len(vswitches) > 0:
|
| 76 |
+
config["provider"]["v_switch_id"] = vswitches[0].get("VSwitchId")
|
| 77 |
+
return
|
| 78 |
+
|
| 79 |
+
v_switch_id = cli.create_v_switch(
|
| 80 |
+
vpc_id=config["provider"]["vpc_id"],
|
| 81 |
+
zone_id=config["provider"]["zone_id"],
|
| 82 |
+
cidr_block=config["provider"]["cidr_block"],
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
if v_switch_id is not None:
|
| 86 |
+
config["provider"]["v_switch_id"] = v_switch_id
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _get_or_import_key_pair(config):
|
| 90 |
+
cli = _client(config)
|
| 91 |
+
key_name = config["provider"].get("key_name", "ray")
|
| 92 |
+
key_path = os.path.expanduser("~/.ssh/{}".format(key_name))
|
| 93 |
+
keypairs = cli.describe_key_pairs(key_pair_name=key_name)
|
| 94 |
+
|
| 95 |
+
if keypairs is not None and len(keypairs) > 0:
|
| 96 |
+
if "ssh_private_key" not in config["auth"]:
|
| 97 |
+
logger.info(
|
| 98 |
+
"{} keypair exists, use {} as local ssh key".format(key_name, key_path)
|
| 99 |
+
)
|
| 100 |
+
config["auth"]["ssh_private_key"] = key_path
|
| 101 |
+
else:
|
| 102 |
+
if "ssh_private_key" not in config["auth"]:
|
| 103 |
+
# create new keypair
|
| 104 |
+
resp = cli.create_key_pair(key_pair_name=key_name)
|
| 105 |
+
if resp is not None:
|
| 106 |
+
with open(key_path, "w+") as f:
|
| 107 |
+
f.write(resp.get("PrivateKeyBody"))
|
| 108 |
+
os.chmod(key_path, stat.S_IRUSR)
|
| 109 |
+
config["auth"]["ssh_private_key"] = key_path
|
| 110 |
+
else:
|
| 111 |
+
public_key_file = config["auth"]["ssh_private_key"] + ".pub"
|
| 112 |
+
# create new keypair, from local file
|
| 113 |
+
with open(public_key_file) as f:
|
| 114 |
+
public_key = f.readline().strip("\n")
|
| 115 |
+
cli.import_key_pair(key_pair_name=key_name, public_key_body=public_key)
|
| 116 |
+
return
|
lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/node_provider.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import random
|
| 3 |
+
import threading
|
| 4 |
+
import time
|
| 5 |
+
from collections import defaultdict
|
| 6 |
+
from typing import Any, Dict, List, Optional
|
| 7 |
+
|
| 8 |
+
from ray.autoscaler._private.aliyun.config import (
|
| 9 |
+
PENDING,
|
| 10 |
+
RUNNING,
|
| 11 |
+
STOPPED,
|
| 12 |
+
STOPPING,
|
| 13 |
+
bootstrap_aliyun,
|
| 14 |
+
)
|
| 15 |
+
from ray.autoscaler._private.aliyun.utils import AcsClient
|
| 16 |
+
from ray.autoscaler._private.cli_logger import cli_logger
|
| 17 |
+
from ray.autoscaler._private.constants import BOTO_MAX_RETRIES
|
| 18 |
+
from ray.autoscaler._private.log_timer import LogTimer
|
| 19 |
+
from ray.autoscaler.node_provider import NodeProvider
|
| 20 |
+
from ray.autoscaler.tags import (
|
| 21 |
+
TAG_RAY_CLUSTER_NAME,
|
| 22 |
+
TAG_RAY_LAUNCH_CONFIG,
|
| 23 |
+
TAG_RAY_NODE_KIND,
|
| 24 |
+
TAG_RAY_NODE_NAME,
|
| 25 |
+
TAG_RAY_NODE_STATUS,
|
| 26 |
+
TAG_RAY_USER_NODE_TYPE,
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
logger = logging.getLogger(__name__)
|
| 30 |
+
|
| 31 |
+
TAG_BATCH_DELAY = 1
|
| 32 |
+
STOPPING_NODE_DELAY = 1
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class AliyunNodeProvider(NodeProvider):
|
| 36 |
+
def __init__(self, provider_config, cluster_name):
|
| 37 |
+
NodeProvider.__init__(self, provider_config, cluster_name)
|
| 38 |
+
self.cache_stopped_nodes = provider_config.get("cache_stopped_nodes", True)
|
| 39 |
+
self.acs = AcsClient(
|
| 40 |
+
access_key=provider_config["access_key"],
|
| 41 |
+
access_key_secret=provider_config["access_key_secret"],
|
| 42 |
+
region_id=provider_config["region"],
|
| 43 |
+
max_retries=BOTO_MAX_RETRIES,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
# Try availability zones round-robin, starting from random offset
|
| 47 |
+
self.subnet_idx = random.randint(0, 100)
|
| 48 |
+
|
| 49 |
+
# Tags that we believe to actually be on the node.
|
| 50 |
+
self.tag_cache = {}
|
| 51 |
+
# Tags that we will soon upload.
|
| 52 |
+
self.tag_cache_pending = defaultdict(dict)
|
| 53 |
+
# Number of threads waiting for a batched tag update.
|
| 54 |
+
self.batch_thread_count = 0
|
| 55 |
+
self.batch_update_done = threading.Event()
|
| 56 |
+
self.batch_update_done.set()
|
| 57 |
+
self.ready_for_new_batch = threading.Event()
|
| 58 |
+
self.ready_for_new_batch.set()
|
| 59 |
+
self.tag_cache_lock = threading.Lock()
|
| 60 |
+
self.count_lock = threading.Lock()
|
| 61 |
+
|
| 62 |
+
# Cache of node objects from the last nodes() call. This avoids
|
| 63 |
+
# excessive DescribeInstances requests.
|
| 64 |
+
self.cached_nodes = {}
|
| 65 |
+
|
| 66 |
+
def non_terminated_nodes(self, tag_filters: Dict[str, str]) -> List[str]:
|
| 67 |
+
tags = [
|
| 68 |
+
{
|
| 69 |
+
"Key": TAG_RAY_CLUSTER_NAME,
|
| 70 |
+
"Value": self.cluster_name,
|
| 71 |
+
},
|
| 72 |
+
]
|
| 73 |
+
for k, v in tag_filters.items():
|
| 74 |
+
tags.append(
|
| 75 |
+
{
|
| 76 |
+
"Key": k,
|
| 77 |
+
"Value": v,
|
| 78 |
+
}
|
| 79 |
+
)
|
| 80 |
+
|
| 81 |
+
instances = self.acs.describe_instances(tags=tags)
|
| 82 |
+
non_terminated_instance = []
|
| 83 |
+
for instance in instances:
|
| 84 |
+
if instance.get("Status") == RUNNING or instance.get("Status") == PENDING:
|
| 85 |
+
non_terminated_instance.append(instance.get("InstanceId"))
|
| 86 |
+
self.cached_nodes[instance.get("InstanceId")] = instance
|
| 87 |
+
return non_terminated_instance
|
| 88 |
+
|
| 89 |
+
def is_running(self, node_id: str) -> bool:
|
| 90 |
+
instances = self.acs.describe_instances(instance_ids=[node_id])
|
| 91 |
+
if instances is not None:
|
| 92 |
+
instance = instances[0]
|
| 93 |
+
return instance.get("Status") == "Running"
|
| 94 |
+
cli_logger.error("Invalid node id: %s", node_id)
|
| 95 |
+
return False
|
| 96 |
+
|
| 97 |
+
def is_terminated(self, node_id: str) -> bool:
|
| 98 |
+
instances = self.acs.describe_instances(instance_ids=[node_id])
|
| 99 |
+
if instances is not None:
|
| 100 |
+
assert len(instances) == 1
|
| 101 |
+
instance = instances[0]
|
| 102 |
+
return instance.get("Status") == "Stopped"
|
| 103 |
+
cli_logger.error("Invalid node id: %s", node_id)
|
| 104 |
+
return False
|
| 105 |
+
|
| 106 |
+
def node_tags(self, node_id: str) -> Dict[str, str]:
|
| 107 |
+
instances = self.acs.describe_instances(instance_ids=[node_id])
|
| 108 |
+
if instances is not None:
|
| 109 |
+
assert len(instances) == 1
|
| 110 |
+
instance = instances[0]
|
| 111 |
+
if instance.get("Tags") is not None:
|
| 112 |
+
node_tags = dict()
|
| 113 |
+
for tag in instance.get("Tags").get("Tag"):
|
| 114 |
+
node_tags[tag.get("TagKey")] = tag.get("TagValue")
|
| 115 |
+
return node_tags
|
| 116 |
+
return dict()
|
| 117 |
+
|
| 118 |
+
def external_ip(self, node_id: str) -> str:
|
| 119 |
+
while True:
|
| 120 |
+
instances = self.acs.describe_instances(instance_ids=[node_id])
|
| 121 |
+
if instances is not None:
|
| 122 |
+
assert len(instances)
|
| 123 |
+
instance = instances[0]
|
| 124 |
+
if (
|
| 125 |
+
instance.get("PublicIpAddress") is not None
|
| 126 |
+
and instance.get("PublicIpAddress").get("IpAddress") is not None
|
| 127 |
+
):
|
| 128 |
+
if len(instance.get("PublicIpAddress").get("IpAddress")) > 0:
|
| 129 |
+
return instance.get("PublicIpAddress").get("IpAddress")[0]
|
| 130 |
+
cli_logger.error("PublicIpAddress attribute is not exist. %s" % instance)
|
| 131 |
+
time.sleep(STOPPING_NODE_DELAY)
|
| 132 |
+
|
| 133 |
+
def internal_ip(self, node_id: str) -> str:
|
| 134 |
+
while True:
|
| 135 |
+
instances = self.acs.describe_instances(instance_ids=[node_id])
|
| 136 |
+
if instances is not None:
|
| 137 |
+
assert len(instances) == 1
|
| 138 |
+
instance = instances[0]
|
| 139 |
+
if (
|
| 140 |
+
instance.get("VpcAttributes") is not None
|
| 141 |
+
and instance.get("VpcAttributes").get("PrivateIpAddress")
|
| 142 |
+
is not None
|
| 143 |
+
and len(
|
| 144 |
+
instance.get("VpcAttributes")
|
| 145 |
+
.get("PrivateIpAddress")
|
| 146 |
+
.get("IpAddress")
|
| 147 |
+
)
|
| 148 |
+
> 0
|
| 149 |
+
):
|
| 150 |
+
return (
|
| 151 |
+
instance.get("VpcAttributes")
|
| 152 |
+
.get("PrivateIpAddress")
|
| 153 |
+
.get("IpAddress")[0]
|
| 154 |
+
)
|
| 155 |
+
cli_logger.error("InnerIpAddress attribute is not exist. %s" % instance)
|
| 156 |
+
time.sleep(STOPPING_NODE_DELAY)
|
| 157 |
+
|
| 158 |
+
def set_node_tags(self, node_id: str, tags: Dict[str, str]) -> None:
|
| 159 |
+
is_batching_thread = False
|
| 160 |
+
with self.tag_cache_lock:
|
| 161 |
+
if not self.tag_cache_pending:
|
| 162 |
+
is_batching_thread = True
|
| 163 |
+
# Wait for threads in the last batch to exit
|
| 164 |
+
self.ready_for_new_batch.wait()
|
| 165 |
+
self.ready_for_new_batch.clear()
|
| 166 |
+
self.batch_update_done.clear()
|
| 167 |
+
self.tag_cache_pending[node_id].update(tags)
|
| 168 |
+
|
| 169 |
+
if is_batching_thread:
|
| 170 |
+
time.sleep(TAG_BATCH_DELAY)
|
| 171 |
+
with self.tag_cache_lock:
|
| 172 |
+
self._update_node_tags()
|
| 173 |
+
self.batch_update_done.set()
|
| 174 |
+
|
| 175 |
+
with self.count_lock:
|
| 176 |
+
self.batch_thread_count += 1
|
| 177 |
+
self.batch_update_done.wait()
|
| 178 |
+
|
| 179 |
+
with self.count_lock:
|
| 180 |
+
self.batch_thread_count -= 1
|
| 181 |
+
if self.batch_thread_count == 0:
|
| 182 |
+
self.ready_for_new_batch.set()
|
| 183 |
+
|
| 184 |
+
def _update_node_tags(self):
|
| 185 |
+
batch_updates = defaultdict(list)
|
| 186 |
+
|
| 187 |
+
for node_id, tags in self.tag_cache_pending.items():
|
| 188 |
+
for x in tags.items():
|
| 189 |
+
batch_updates[x].append(node_id)
|
| 190 |
+
self.tag_cache[node_id] = tags
|
| 191 |
+
|
| 192 |
+
self.tag_cache_pending = defaultdict(dict)
|
| 193 |
+
|
| 194 |
+
self._create_tags(batch_updates)
|
| 195 |
+
|
| 196 |
+
def _create_tags(self, batch_updates):
|
| 197 |
+
|
| 198 |
+
for (k, v), node_ids in batch_updates.items():
|
| 199 |
+
m = "Set tag {}={} on {}".format(k, v, node_ids)
|
| 200 |
+
with LogTimer("AliyunNodeProvider: {}".format(m)):
|
| 201 |
+
if k == TAG_RAY_NODE_NAME:
|
| 202 |
+
k = "Name"
|
| 203 |
+
|
| 204 |
+
self.acs.tag_resource(node_ids, [{"Key": k, "Value": v}])
|
| 205 |
+
|
| 206 |
+
def create_node(
|
| 207 |
+
self, node_config: Dict[str, Any], tags: Dict[str, str], count: int
|
| 208 |
+
) -> Optional[Dict[str, Any]]:
|
| 209 |
+
filter_tags = [
|
| 210 |
+
{
|
| 211 |
+
"Key": TAG_RAY_CLUSTER_NAME,
|
| 212 |
+
"Value": self.cluster_name,
|
| 213 |
+
},
|
| 214 |
+
{"Key": TAG_RAY_NODE_KIND, "Value": tags[TAG_RAY_NODE_KIND]},
|
| 215 |
+
{"Key": TAG_RAY_USER_NODE_TYPE, "Value": tags[TAG_RAY_USER_NODE_TYPE]},
|
| 216 |
+
{"Key": TAG_RAY_LAUNCH_CONFIG, "Value": tags[TAG_RAY_LAUNCH_CONFIG]},
|
| 217 |
+
{"Key": TAG_RAY_NODE_NAME, "Value": tags[TAG_RAY_NODE_NAME]},
|
| 218 |
+
]
|
| 219 |
+
|
| 220 |
+
reused_nodes_dict = {}
|
| 221 |
+
if self.cache_stopped_nodes:
|
| 222 |
+
reuse_nodes_candidate = self.acs.describe_instances(tags=filter_tags)
|
| 223 |
+
if reuse_nodes_candidate:
|
| 224 |
+
with cli_logger.group("Stopping instances to reuse"):
|
| 225 |
+
reuse_node_ids = []
|
| 226 |
+
for node in reuse_nodes_candidate:
|
| 227 |
+
node_id = node.get("InstanceId")
|
| 228 |
+
status = node.get("Status")
|
| 229 |
+
if status != STOPPING and status != STOPPED:
|
| 230 |
+
continue
|
| 231 |
+
if status == STOPPING:
|
| 232 |
+
# wait for node stopped
|
| 233 |
+
while (
|
| 234 |
+
self.acs.describe_instances(instance_ids=[node_id])[
|
| 235 |
+
0
|
| 236 |
+
].get("Status")
|
| 237 |
+
== STOPPING
|
| 238 |
+
):
|
| 239 |
+
logging.info("wait for %s stop" % node_id)
|
| 240 |
+
time.sleep(STOPPING_NODE_DELAY)
|
| 241 |
+
# logger.info("reuse %s" % node_id)
|
| 242 |
+
reuse_node_ids.append(node_id)
|
| 243 |
+
reused_nodes_dict[node.get("InstanceId")] = node
|
| 244 |
+
self.acs.start_instance(node_id)
|
| 245 |
+
self.tag_cache[node_id] = node.get("Tags")
|
| 246 |
+
self.set_node_tags(node_id, tags)
|
| 247 |
+
if len(reuse_node_ids) == count:
|
| 248 |
+
break
|
| 249 |
+
count -= len(reuse_node_ids)
|
| 250 |
+
|
| 251 |
+
created_nodes_dict = {}
|
| 252 |
+
if count > 0:
|
| 253 |
+
filter_tags.append(
|
| 254 |
+
{"Key": TAG_RAY_NODE_STATUS, "Value": tags[TAG_RAY_NODE_STATUS]}
|
| 255 |
+
)
|
| 256 |
+
instance_id_sets = self.acs.run_instances(
|
| 257 |
+
instance_type=node_config["InstanceType"],
|
| 258 |
+
image_id=node_config["ImageId"],
|
| 259 |
+
tags=filter_tags,
|
| 260 |
+
amount=count,
|
| 261 |
+
vswitch_id=self.provider_config["v_switch_id"],
|
| 262 |
+
security_group_id=self.provider_config["security_group_id"],
|
| 263 |
+
key_pair_name=self.provider_config["key_name"],
|
| 264 |
+
)
|
| 265 |
+
instances = self.acs.describe_instances(instance_ids=instance_id_sets)
|
| 266 |
+
|
| 267 |
+
if instances is not None:
|
| 268 |
+
for instance in instances:
|
| 269 |
+
created_nodes_dict[instance.get("InstanceId")] = instance
|
| 270 |
+
|
| 271 |
+
all_created_nodes = reused_nodes_dict
|
| 272 |
+
all_created_nodes.update(created_nodes_dict)
|
| 273 |
+
return all_created_nodes
|
| 274 |
+
|
| 275 |
+
def terminate_node(self, node_id: str) -> None:
|
| 276 |
+
logger.info("terminate node: %s" % node_id)
|
| 277 |
+
if self.cache_stopped_nodes:
|
| 278 |
+
logger.info(
|
| 279 |
+
"Stopping instance {} (to terminate instead, "
|
| 280 |
+
"set `cache_stopped_nodes: False` "
|
| 281 |
+
"under `provider` in the cluster configuration)"
|
| 282 |
+
).format(node_id)
|
| 283 |
+
self.acs.stop_instance(node_id)
|
| 284 |
+
else:
|
| 285 |
+
self.acs.delete_instance(node_id)
|
| 286 |
+
|
| 287 |
+
def terminate_nodes(self, node_ids: List[str]) -> None:
|
| 288 |
+
if not node_ids:
|
| 289 |
+
return
|
| 290 |
+
if self.cache_stopped_nodes:
|
| 291 |
+
logger.info(
|
| 292 |
+
"Stopping instances {} (to terminate instead, "
|
| 293 |
+
"set `cache_stopped_nodes: False` "
|
| 294 |
+
"under `provider` in the cluster configuration)".format(node_ids)
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
self.acs.stop_instances(node_ids)
|
| 298 |
+
else:
|
| 299 |
+
self.acs.delete_instances(node_ids)
|
| 300 |
+
|
| 301 |
+
def _get_node(self, node_id):
|
| 302 |
+
"""Refresh and get info for this node, updating the cache."""
|
| 303 |
+
self.non_terminated_nodes({}) # Side effect: updates cache
|
| 304 |
+
|
| 305 |
+
if node_id in self.cached_nodes:
|
| 306 |
+
return self.cached_nodes[node_id]
|
| 307 |
+
|
| 308 |
+
# Node not in {pending, running} -- retry with a point query. This
|
| 309 |
+
# usually means the node was recently preempted or terminated.
|
| 310 |
+
matches = self.acs.describe_instances(instance_ids=[node_id])
|
| 311 |
+
|
| 312 |
+
assert len(matches) == 1, "Invalid instance id {}".format(node_id)
|
| 313 |
+
return matches[0]
|
| 314 |
+
|
| 315 |
+
def _get_cached_node(self, node_id):
|
| 316 |
+
"""Return node info from cache if possible, otherwise fetches it."""
|
| 317 |
+
if node_id in self.cached_nodes:
|
| 318 |
+
return self.cached_nodes[node_id]
|
| 319 |
+
|
| 320 |
+
return self._get_node(node_id)
|
| 321 |
+
|
| 322 |
+
@staticmethod
|
| 323 |
+
def bootstrap_config(cluster_config):
|
| 324 |
+
return bootstrap_aliyun(cluster_config)
|
lib/python3.12/site-packages/ray/autoscaler/_private/aliyun/utils.py
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
|
| 4 |
+
from aliyunsdkcore import client
|
| 5 |
+
from aliyunsdkcore.acs_exception.exceptions import ClientException, ServerException
|
| 6 |
+
from aliyunsdkecs.request.v20140526.AllocatePublicIpAddressRequest import (
|
| 7 |
+
AllocatePublicIpAddressRequest,
|
| 8 |
+
)
|
| 9 |
+
from aliyunsdkecs.request.v20140526.AuthorizeSecurityGroupRequest import (
|
| 10 |
+
AuthorizeSecurityGroupRequest,
|
| 11 |
+
)
|
| 12 |
+
from aliyunsdkecs.request.v20140526.CreateInstanceRequest import CreateInstanceRequest
|
| 13 |
+
from aliyunsdkecs.request.v20140526.CreateKeyPairRequest import CreateKeyPairRequest
|
| 14 |
+
from aliyunsdkecs.request.v20140526.CreateSecurityGroupRequest import (
|
| 15 |
+
CreateSecurityGroupRequest,
|
| 16 |
+
)
|
| 17 |
+
from aliyunsdkecs.request.v20140526.CreateVpcRequest import CreateVpcRequest
|
| 18 |
+
from aliyunsdkecs.request.v20140526.CreateVSwitchRequest import CreateVSwitchRequest
|
| 19 |
+
from aliyunsdkecs.request.v20140526.DeleteInstanceRequest import DeleteInstanceRequest
|
| 20 |
+
from aliyunsdkecs.request.v20140526.DeleteInstancesRequest import DeleteInstancesRequest
|
| 21 |
+
from aliyunsdkecs.request.v20140526.DeleteKeyPairsRequest import DeleteKeyPairsRequest
|
| 22 |
+
from aliyunsdkecs.request.v20140526.DescribeInstancesRequest import (
|
| 23 |
+
DescribeInstancesRequest,
|
| 24 |
+
)
|
| 25 |
+
from aliyunsdkecs.request.v20140526.DescribeKeyPairsRequest import (
|
| 26 |
+
DescribeKeyPairsRequest,
|
| 27 |
+
)
|
| 28 |
+
from aliyunsdkecs.request.v20140526.DescribeSecurityGroupsRequest import (
|
| 29 |
+
DescribeSecurityGroupsRequest,
|
| 30 |
+
)
|
| 31 |
+
from aliyunsdkecs.request.v20140526.DescribeVpcsRequest import DescribeVpcsRequest
|
| 32 |
+
from aliyunsdkecs.request.v20140526.DescribeVSwitchesRequest import (
|
| 33 |
+
DescribeVSwitchesRequest,
|
| 34 |
+
)
|
| 35 |
+
from aliyunsdkecs.request.v20140526.ImportKeyPairRequest import ImportKeyPairRequest
|
| 36 |
+
from aliyunsdkecs.request.v20140526.RunInstancesRequest import RunInstancesRequest
|
| 37 |
+
from aliyunsdkecs.request.v20140526.StartInstanceRequest import StartInstanceRequest
|
| 38 |
+
from aliyunsdkecs.request.v20140526.StopInstanceRequest import StopInstanceRequest
|
| 39 |
+
from aliyunsdkecs.request.v20140526.StopInstancesRequest import StopInstancesRequest
|
| 40 |
+
from aliyunsdkecs.request.v20140526.TagResourcesRequest import TagResourcesRequest
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class AcsClient:
|
| 44 |
+
"""
|
| 45 |
+
A wrapper around Aliyun SDK. We use this wrapper in aliyun node provider.
|
| 46 |
+
|
| 47 |
+
Parameters:
|
| 48 |
+
access_key: The AccessKey ID of your aliyun account.
|
| 49 |
+
access_key_secret: The AccessKey secret of your aliyun account.
|
| 50 |
+
region_id: A region is a geographic area where a data center resides.
|
| 51 |
+
Region_id is the ID of region (e.g., cn-hangzhou,
|
| 52 |
+
us-west-1, etc.)
|
| 53 |
+
max_retries: The maximum number of retries each connection.
|
| 54 |
+
"""
|
| 55 |
+
|
| 56 |
+
def __init__(self, access_key, access_key_secret, region_id, max_retries):
|
| 57 |
+
self.cli = client.AcsClient(
|
| 58 |
+
ak=access_key,
|
| 59 |
+
secret=access_key_secret,
|
| 60 |
+
max_retry_time=max_retries,
|
| 61 |
+
region_id=region_id,
|
| 62 |
+
)
|
| 63 |
+
|
| 64 |
+
def describe_instances(self, tags=None, instance_ids=None):
|
| 65 |
+
"""Query the details of one or more Elastic Compute Service (ECS) instances.
|
| 66 |
+
|
| 67 |
+
:param tags: The tags of the instance.
|
| 68 |
+
:param instance_ids: The IDs of ECS instances
|
| 69 |
+
:return: ECS instance list
|
| 70 |
+
"""
|
| 71 |
+
request = DescribeInstancesRequest()
|
| 72 |
+
if tags is not None:
|
| 73 |
+
request.set_Tags(tags)
|
| 74 |
+
if instance_ids is not None:
|
| 75 |
+
request.set_InstanceIds(instance_ids)
|
| 76 |
+
response = self._send_request(request)
|
| 77 |
+
if response is not None:
|
| 78 |
+
instance_list = response.get("Instances").get("Instance")
|
| 79 |
+
return instance_list
|
| 80 |
+
return None
|
| 81 |
+
|
| 82 |
+
def create_instance(
|
| 83 |
+
self,
|
| 84 |
+
instance_type,
|
| 85 |
+
image_id,
|
| 86 |
+
tags,
|
| 87 |
+
key_pair_name,
|
| 88 |
+
optimized="optimized",
|
| 89 |
+
instance_charge_type="PostPaid",
|
| 90 |
+
spot_strategy="SpotWithPriceLimit",
|
| 91 |
+
internet_charge_type="PayByTraffic",
|
| 92 |
+
internet_max_bandwidth_out=5,
|
| 93 |
+
):
|
| 94 |
+
"""Create a subscription or pay-as-you-go ECS instance.
|
| 95 |
+
|
| 96 |
+
:param instance_type: The instance type of the ECS.
|
| 97 |
+
:param image_id: The ID of the image used to create the instance.
|
| 98 |
+
:param tags: The tags of the instance.
|
| 99 |
+
:param key_pair_name: The name of the key pair to be bound to
|
| 100 |
+
the instance.
|
| 101 |
+
:param optimized: Specifies whether the instance is I/O optimized
|
| 102 |
+
:param instance_charge_type: The billing method of the instance.
|
| 103 |
+
Default value: PostPaid.
|
| 104 |
+
:param spot_strategy: The preemption policy for the pay-as-you-go
|
| 105 |
+
instance.
|
| 106 |
+
:param internet_charge_type: The billing method for network usage.
|
| 107 |
+
Default value: PayByTraffic.
|
| 108 |
+
:param internet_max_bandwidth_out: The maximum inbound public
|
| 109 |
+
bandwidth. Unit: Mbit/s.
|
| 110 |
+
:return: The created instance ID.
|
| 111 |
+
"""
|
| 112 |
+
request = CreateInstanceRequest()
|
| 113 |
+
request.set_InstanceType(instance_type)
|
| 114 |
+
request.set_ImageId(image_id)
|
| 115 |
+
request.set_IoOptimized(optimized)
|
| 116 |
+
request.set_InstanceChargeType(instance_charge_type)
|
| 117 |
+
request.set_SpotStrategy(spot_strategy)
|
| 118 |
+
request.set_InternetChargeType(internet_charge_type)
|
| 119 |
+
request.set_InternetMaxBandwidthOut(internet_max_bandwidth_out)
|
| 120 |
+
request.set_KeyPairName(key_pair_name)
|
| 121 |
+
request.set_Tags(tags)
|
| 122 |
+
|
| 123 |
+
response = self._send_request(request)
|
| 124 |
+
if response is not None:
|
| 125 |
+
instance_id = response.get("InstanceId")
|
| 126 |
+
logging.info("instance %s created task submit successfully.", instance_id)
|
| 127 |
+
return instance_id
|
| 128 |
+
logging.error("instance created failed.")
|
| 129 |
+
return None
|
| 130 |
+
|
| 131 |
+
def run_instances(
|
| 132 |
+
self,
|
| 133 |
+
instance_type,
|
| 134 |
+
image_id,
|
| 135 |
+
tags,
|
| 136 |
+
security_group_id,
|
| 137 |
+
vswitch_id,
|
| 138 |
+
key_pair_name,
|
| 139 |
+
amount=1,
|
| 140 |
+
optimized="optimized",
|
| 141 |
+
instance_charge_type="PostPaid",
|
| 142 |
+
spot_strategy="SpotWithPriceLimit",
|
| 143 |
+
internet_charge_type="PayByTraffic",
|
| 144 |
+
internet_max_bandwidth_out=1,
|
| 145 |
+
):
|
| 146 |
+
"""Create one or more pay-as-you-go or subscription
|
| 147 |
+
Elastic Compute Service (ECS) instances
|
| 148 |
+
|
| 149 |
+
:param instance_type: The instance type of the ECS.
|
| 150 |
+
:param image_id: The ID of the image used to create the instance.
|
| 151 |
+
:param tags: The tags of the instance.
|
| 152 |
+
:param security_group_id: The ID of the security group to which to
|
| 153 |
+
assign the instance. Instances in the same
|
| 154 |
+
security group can communicate with
|
| 155 |
+
each other.
|
| 156 |
+
:param vswitch_id: The ID of the vSwitch to which to connect
|
| 157 |
+
the instance.
|
| 158 |
+
:param key_pair_name: The name of the key pair to be bound to
|
| 159 |
+
the instance.
|
| 160 |
+
:param amount: The number of instances that you want to create.
|
| 161 |
+
:param optimized: Specifies whether the instance is I/O optimized
|
| 162 |
+
:param instance_charge_type: The billing method of the instance.
|
| 163 |
+
Default value: PostPaid.
|
| 164 |
+
:param spot_strategy: The preemption policy for the pay-as-you-go
|
| 165 |
+
instance.
|
| 166 |
+
:param internet_charge_type: The billing method for network usage.
|
| 167 |
+
Default value: PayByTraffic.
|
| 168 |
+
:param internet_max_bandwidth_out: The maximum inbound public
|
| 169 |
+
bandwidth. Unit: Mbit/s.
|
| 170 |
+
:return: The created instance IDs.
|
| 171 |
+
"""
|
| 172 |
+
request = RunInstancesRequest()
|
| 173 |
+
request.set_InstanceType(instance_type)
|
| 174 |
+
request.set_ImageId(image_id)
|
| 175 |
+
request.set_IoOptimized(optimized)
|
| 176 |
+
request.set_InstanceChargeType(instance_charge_type)
|
| 177 |
+
request.set_SpotStrategy(spot_strategy)
|
| 178 |
+
request.set_InternetChargeType(internet_charge_type)
|
| 179 |
+
request.set_InternetMaxBandwidthOut(internet_max_bandwidth_out)
|
| 180 |
+
request.set_Tags(tags)
|
| 181 |
+
request.set_Amount(amount)
|
| 182 |
+
request.set_SecurityGroupId(security_group_id)
|
| 183 |
+
request.set_VSwitchId(vswitch_id)
|
| 184 |
+
request.set_KeyPairName(key_pair_name)
|
| 185 |
+
|
| 186 |
+
response = self._send_request(request)
|
| 187 |
+
if response is not None:
|
| 188 |
+
instance_ids = response.get("InstanceIdSets").get("InstanceIdSet")
|
| 189 |
+
return instance_ids
|
| 190 |
+
logging.error("instance created failed.")
|
| 191 |
+
return None
|
| 192 |
+
|
| 193 |
+
def create_security_group(self, vpc_id):
|
| 194 |
+
"""Create a security group
|
| 195 |
+
|
| 196 |
+
:param vpc_id: The ID of the VPC in which to create
|
| 197 |
+
the security group.
|
| 198 |
+
:return: The created security group ID.
|
| 199 |
+
"""
|
| 200 |
+
request = CreateSecurityGroupRequest()
|
| 201 |
+
request.set_VpcId(vpc_id)
|
| 202 |
+
response = self._send_request(request)
|
| 203 |
+
if response is not None:
|
| 204 |
+
security_group_id = response.get("SecurityGroupId")
|
| 205 |
+
return security_group_id
|
| 206 |
+
return None
|
| 207 |
+
|
| 208 |
+
def describe_security_groups(self, vpc_id=None, tags=None):
|
| 209 |
+
"""Query basic information of security groups.
|
| 210 |
+
|
| 211 |
+
:param vpc_id: The ID of the VPC to which the security group belongs.
|
| 212 |
+
:param tags: The tags of the security group.
|
| 213 |
+
:return: Security group list.
|
| 214 |
+
"""
|
| 215 |
+
request = DescribeSecurityGroupsRequest()
|
| 216 |
+
if vpc_id is not None:
|
| 217 |
+
request.set_VpcId(vpc_id)
|
| 218 |
+
if tags is not None:
|
| 219 |
+
request.set_Tags(tags)
|
| 220 |
+
response = self._send_request(request)
|
| 221 |
+
if response is not None:
|
| 222 |
+
security_groups = response.get("SecurityGroups").get("SecurityGroup")
|
| 223 |
+
return security_groups
|
| 224 |
+
logging.error("describe security group failed.")
|
| 225 |
+
return None
|
| 226 |
+
|
| 227 |
+
def authorize_security_group(
|
| 228 |
+
self, ip_protocol, port_range, security_group_id, source_cidr_ip
|
| 229 |
+
):
|
| 230 |
+
"""Create an inbound security group rule.
|
| 231 |
+
|
| 232 |
+
:param ip_protocol: The transport layer protocol.
|
| 233 |
+
:param port_range: The range of destination ports relevant to
|
| 234 |
+
the transport layer protocol.
|
| 235 |
+
:param security_group_id: The ID of the destination security group.
|
| 236 |
+
:param source_cidr_ip: The range of source IPv4 addresses.
|
| 237 |
+
CIDR blocks and IPv4 addresses are supported.
|
| 238 |
+
"""
|
| 239 |
+
request = AuthorizeSecurityGroupRequest()
|
| 240 |
+
request.set_IpProtocol(ip_protocol)
|
| 241 |
+
request.set_PortRange(port_range)
|
| 242 |
+
request.set_SecurityGroupId(security_group_id)
|
| 243 |
+
request.set_SourceCidrIp(source_cidr_ip)
|
| 244 |
+
self._send_request(request)
|
| 245 |
+
|
| 246 |
+
def create_v_switch(self, vpc_id, zone_id, cidr_block):
|
| 247 |
+
"""Create vSwitches to divide the VPC into one or more subnets
|
| 248 |
+
|
| 249 |
+
:param vpc_id: The ID of the VPC to which the VSwitch belongs.
|
| 250 |
+
:param zone_id: The ID of the zone to which
|
| 251 |
+
the target VSwitch belongs.
|
| 252 |
+
:param cidr_block: The CIDR block of the VSwitch.
|
| 253 |
+
:return:
|
| 254 |
+
"""
|
| 255 |
+
request = CreateVSwitchRequest()
|
| 256 |
+
request.set_ZoneId(zone_id)
|
| 257 |
+
request.set_VpcId(vpc_id)
|
| 258 |
+
request.set_CidrBlock(cidr_block)
|
| 259 |
+
response = self._send_request(request)
|
| 260 |
+
if response is not None:
|
| 261 |
+
return response.get("VSwitchId")
|
| 262 |
+
else:
|
| 263 |
+
logging.error("create_v_switch vpc_id %s failed.", vpc_id)
|
| 264 |
+
return None
|
| 265 |
+
|
| 266 |
+
def create_vpc(self):
|
| 267 |
+
"""Creates a virtual private cloud (VPC).
|
| 268 |
+
|
| 269 |
+
:return: The created VPC ID.
|
| 270 |
+
"""
|
| 271 |
+
request = CreateVpcRequest()
|
| 272 |
+
response = self._send_request(request)
|
| 273 |
+
if response is not None:
|
| 274 |
+
return response.get("VpcId")
|
| 275 |
+
return None
|
| 276 |
+
|
| 277 |
+
def describe_vpcs(self):
|
| 278 |
+
"""Queries one or more VPCs in a region.
|
| 279 |
+
|
| 280 |
+
:return: VPC list.
|
| 281 |
+
"""
|
| 282 |
+
request = DescribeVpcsRequest()
|
| 283 |
+
response = self._send_request(request)
|
| 284 |
+
if response is not None:
|
| 285 |
+
return response.get("Vpcs").get("Vpc")
|
| 286 |
+
return None
|
| 287 |
+
|
| 288 |
+
def tag_resource(self, resource_ids, tags, resource_type="instance"):
|
| 289 |
+
"""Create and bind tags to specified ECS resources.
|
| 290 |
+
|
| 291 |
+
:param resource_ids: The IDs of N resources.
|
| 292 |
+
:param tags: The tags of the resource.
|
| 293 |
+
:param resource_type: The type of the resource.
|
| 294 |
+
"""
|
| 295 |
+
request = TagResourcesRequest()
|
| 296 |
+
request.set_Tags(tags)
|
| 297 |
+
request.set_ResourceType(resource_type)
|
| 298 |
+
request.set_ResourceIds(resource_ids)
|
| 299 |
+
response = self._send_request(request)
|
| 300 |
+
if response is not None:
|
| 301 |
+
logging.info("instance %s create tag successfully.", resource_ids)
|
| 302 |
+
else:
|
| 303 |
+
logging.error("instance %s create tag failed.", resource_ids)
|
| 304 |
+
|
| 305 |
+
def start_instance(self, instance_id):
|
| 306 |
+
"""Start an ECS instance.
|
| 307 |
+
|
| 308 |
+
:param instance_id: The Ecs instance ID.
|
| 309 |
+
"""
|
| 310 |
+
request = StartInstanceRequest()
|
| 311 |
+
request.set_InstanceId(instance_id)
|
| 312 |
+
response = self._send_request(request)
|
| 313 |
+
|
| 314 |
+
if response is not None:
|
| 315 |
+
logging.info("instance %s start successfully.", instance_id)
|
| 316 |
+
else:
|
| 317 |
+
logging.error("instance %s start failed.", instance_id)
|
| 318 |
+
|
| 319 |
+
def stop_instance(self, instance_id, force_stop=False):
|
| 320 |
+
"""Stop an ECS instance that is in the Running state.
|
| 321 |
+
|
| 322 |
+
:param instance_id: The Ecs instance ID.
|
| 323 |
+
:param force_stop: Specifies whether to forcibly stop the instance.
|
| 324 |
+
:return:
|
| 325 |
+
"""
|
| 326 |
+
request = StopInstanceRequest()
|
| 327 |
+
request.set_InstanceId(instance_id)
|
| 328 |
+
request.set_ForceStop(force_stop)
|
| 329 |
+
logging.info("Stop %s command submit successfully.", instance_id)
|
| 330 |
+
self._send_request(request)
|
| 331 |
+
|
| 332 |
+
def stop_instances(self, instance_ids, stopped_mode="StopCharging"):
|
| 333 |
+
"""Stop one or more ECS instances that are in the Running state.
|
| 334 |
+
|
| 335 |
+
:param instance_ids: The IDs of instances.
|
| 336 |
+
:param stopped_mode: Specifies whether billing for the instance
|
| 337 |
+
continues after the instance is stopped.
|
| 338 |
+
"""
|
| 339 |
+
request = StopInstancesRequest()
|
| 340 |
+
request.set_InstanceIds(instance_ids)
|
| 341 |
+
request.set_StoppedMode(stopped_mode)
|
| 342 |
+
response = self._send_request(request)
|
| 343 |
+
if response is None:
|
| 344 |
+
logging.error("stop_instances failed")
|
| 345 |
+
|
| 346 |
+
def delete_instance(self, instance_id):
|
| 347 |
+
"""Release a pay-as-you-go instance or
|
| 348 |
+
an expired subscription instance.
|
| 349 |
+
|
| 350 |
+
:param instance_id: The ID of the instance that you want to release.
|
| 351 |
+
"""
|
| 352 |
+
request = DeleteInstanceRequest()
|
| 353 |
+
request.set_InstanceId(instance_id)
|
| 354 |
+
request.set_Force(True)
|
| 355 |
+
logging.info("Delete %s command submit successfully", instance_id)
|
| 356 |
+
self._send_request(request)
|
| 357 |
+
|
| 358 |
+
def delete_instances(self, instance_ids):
|
| 359 |
+
"""Release one or more pay-as-you-go instances or
|
| 360 |
+
expired subscription instances.
|
| 361 |
+
|
| 362 |
+
:param instance_ids: The IDs of instances that you want to release.
|
| 363 |
+
"""
|
| 364 |
+
request = DeleteInstancesRequest()
|
| 365 |
+
request.set_Force(True)
|
| 366 |
+
request.set_InstanceIds(instance_ids)
|
| 367 |
+
self._send_request(request)
|
| 368 |
+
|
| 369 |
+
def allocate_public_address(self, instance_id):
|
| 370 |
+
"""Assign a public IP address to an ECS instance.
|
| 371 |
+
|
| 372 |
+
:param instance_id: The ID of the instance to which you want to
|
| 373 |
+
assign a public IP address.
|
| 374 |
+
:return: The assigned ip.
|
| 375 |
+
"""
|
| 376 |
+
request = AllocatePublicIpAddressRequest()
|
| 377 |
+
request.set_InstanceId(instance_id)
|
| 378 |
+
response = self._send_request(request)
|
| 379 |
+
if response is not None:
|
| 380 |
+
return response.get("IpAddress")
|
| 381 |
+
|
| 382 |
+
def create_key_pair(self, key_pair_name):
|
| 383 |
+
"""Create an SSH key pair.
|
| 384 |
+
|
| 385 |
+
:param key_pair_name: The name of the key pair.
|
| 386 |
+
:return: The created keypair data.
|
| 387 |
+
"""
|
| 388 |
+
request = CreateKeyPairRequest()
|
| 389 |
+
request.set_KeyPairName(key_pair_name)
|
| 390 |
+
response = self._send_request(request)
|
| 391 |
+
if response is not None:
|
| 392 |
+
logging.info("Create Key Pair %s Successfully", response.get("KeyPairId"))
|
| 393 |
+
return response
|
| 394 |
+
else:
|
| 395 |
+
logging.error("Create Key Pair Failed")
|
| 396 |
+
return None
|
| 397 |
+
|
| 398 |
+
def import_key_pair(self, key_pair_name, public_key_body):
|
| 399 |
+
"""Import the public key of an RSA-encrypted key pair
|
| 400 |
+
that is generated by a third-party tool.
|
| 401 |
+
|
| 402 |
+
:param key_pair_name: The name of the key pair.
|
| 403 |
+
:param public_key_body: The public key of the key pair.
|
| 404 |
+
"""
|
| 405 |
+
request = ImportKeyPairRequest()
|
| 406 |
+
request.set_KeyPairName(key_pair_name)
|
| 407 |
+
request.set_PublicKeyBody(public_key_body)
|
| 408 |
+
self._send_request(request)
|
| 409 |
+
|
| 410 |
+
def delete_key_pairs(self, key_pair_names):
|
| 411 |
+
"""Delete one or more SSH key pairs.
|
| 412 |
+
|
| 413 |
+
:param key_pair_names: The name of the key pair.
|
| 414 |
+
:return:
|
| 415 |
+
"""
|
| 416 |
+
request = DeleteKeyPairsRequest()
|
| 417 |
+
request.set_KeyPairNames(key_pair_names)
|
| 418 |
+
self._send_request(request)
|
| 419 |
+
|
| 420 |
+
def describe_key_pairs(self, key_pair_name=None):
|
| 421 |
+
"""Query one or more key pairs.
|
| 422 |
+
|
| 423 |
+
:param key_pair_name: The name of the key pair.
|
| 424 |
+
:return:
|
| 425 |
+
"""
|
| 426 |
+
request = DescribeKeyPairsRequest()
|
| 427 |
+
if key_pair_name is not None:
|
| 428 |
+
request.set_KeyPairName(key_pair_name)
|
| 429 |
+
response = self._send_request(request)
|
| 430 |
+
if response is not None:
|
| 431 |
+
return response.get("KeyPairs").get("KeyPair")
|
| 432 |
+
else:
|
| 433 |
+
return None
|
| 434 |
+
|
| 435 |
+
def describe_v_switches(self, vpc_id=None):
|
| 436 |
+
"""Queries one or more VSwitches.
|
| 437 |
+
|
| 438 |
+
:param vpc_id: The ID of the VPC to which the VSwitch belongs.
|
| 439 |
+
:return: VSwitch list.
|
| 440 |
+
"""
|
| 441 |
+
request = DescribeVSwitchesRequest()
|
| 442 |
+
if vpc_id is not None:
|
| 443 |
+
request.set_VpcId(vpc_id)
|
| 444 |
+
response = self._send_request(request)
|
| 445 |
+
if response is not None:
|
| 446 |
+
return response.get("VSwitches").get("VSwitch")
|
| 447 |
+
else:
|
| 448 |
+
logging.error("Describe VSwitches Failed.")
|
| 449 |
+
return None
|
| 450 |
+
|
| 451 |
+
def _send_request(self, request):
|
| 452 |
+
"""send open api request"""
|
| 453 |
+
request.set_accept_format("json")
|
| 454 |
+
try:
|
| 455 |
+
response_str = self.cli.do_action_with_exception(request)
|
| 456 |
+
response_detail = json.loads(response_str)
|
| 457 |
+
return response_detail
|
| 458 |
+
except (ClientException, ServerException) as e:
|
| 459 |
+
logging.error(request.get_action_name())
|
| 460 |
+
logging.error(e)
|
| 461 |
+
return None
|
lib/python3.12/site-packages/ray/autoscaler/_private/autoscaler.py
ADDED
|
@@ -0,0 +1,1522 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import copy
|
| 2 |
+
import logging
|
| 3 |
+
import math
|
| 4 |
+
import operator
|
| 5 |
+
import os
|
| 6 |
+
import queue
|
| 7 |
+
import subprocess
|
| 8 |
+
import threading
|
| 9 |
+
import time
|
| 10 |
+
from collections import Counter, defaultdict, namedtuple
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from enum import Enum
|
| 13 |
+
from typing import Any, Callable, Dict, FrozenSet, List, Optional, Set, Tuple, Union
|
| 14 |
+
|
| 15 |
+
import yaml
|
| 16 |
+
|
| 17 |
+
import ray
|
| 18 |
+
from ray._common.utils import PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME
|
| 19 |
+
from ray.autoscaler._private.constants import (
|
| 20 |
+
AUTOSCALER_HEARTBEAT_TIMEOUT_S,
|
| 21 |
+
AUTOSCALER_MAX_CONCURRENT_LAUNCHES,
|
| 22 |
+
AUTOSCALER_MAX_LAUNCH_BATCH,
|
| 23 |
+
AUTOSCALER_MAX_NUM_FAILURES,
|
| 24 |
+
AUTOSCALER_STATUS_LOG,
|
| 25 |
+
AUTOSCALER_UPDATE_INTERVAL_S,
|
| 26 |
+
DISABLE_LAUNCH_CONFIG_CHECK_KEY,
|
| 27 |
+
DISABLE_NODE_UPDATERS_KEY,
|
| 28 |
+
FOREGROUND_NODE_LAUNCH_KEY,
|
| 29 |
+
WORKER_LIVENESS_CHECK_KEY,
|
| 30 |
+
)
|
| 31 |
+
from ray.autoscaler._private.event_summarizer import EventSummarizer
|
| 32 |
+
from ray.autoscaler._private.legacy_info_string import legacy_log_info_string
|
| 33 |
+
from ray.autoscaler._private.load_metrics import LoadMetrics
|
| 34 |
+
from ray.autoscaler._private.local.node_provider import (
|
| 35 |
+
LocalNodeProvider,
|
| 36 |
+
record_local_head_state_if_needed,
|
| 37 |
+
)
|
| 38 |
+
from ray.autoscaler._private.node_launcher import BaseNodeLauncher, NodeLauncher
|
| 39 |
+
from ray.autoscaler._private.node_provider_availability_tracker import (
|
| 40 |
+
NodeAvailabilitySummary,
|
| 41 |
+
NodeProviderAvailabilityTracker,
|
| 42 |
+
)
|
| 43 |
+
from ray.autoscaler._private.node_tracker import NodeTracker
|
| 44 |
+
from ray.autoscaler._private.prom_metrics import AutoscalerPrometheusMetrics
|
| 45 |
+
from ray.autoscaler._private.providers import _get_node_provider
|
| 46 |
+
from ray.autoscaler._private.resource_demand_scheduler import (
|
| 47 |
+
ResourceDemandScheduler,
|
| 48 |
+
ResourceDict,
|
| 49 |
+
get_bin_pack_residual,
|
| 50 |
+
placement_groups_to_resource_demands,
|
| 51 |
+
)
|
| 52 |
+
from ray.autoscaler._private.updater import NodeUpdaterThread
|
| 53 |
+
from ray.autoscaler._private.util import (
|
| 54 |
+
ConcurrentCounter,
|
| 55 |
+
NodeCount,
|
| 56 |
+
NodeID,
|
| 57 |
+
NodeIP,
|
| 58 |
+
NodeType,
|
| 59 |
+
NodeTypeConfigDict,
|
| 60 |
+
format_info_string,
|
| 61 |
+
hash_launch_conf,
|
| 62 |
+
hash_runtime_conf,
|
| 63 |
+
validate_config,
|
| 64 |
+
with_head_node_ip,
|
| 65 |
+
)
|
| 66 |
+
from ray.autoscaler.node_provider import NodeProvider
|
| 67 |
+
from ray.autoscaler.tags import (
|
| 68 |
+
NODE_KIND_HEAD,
|
| 69 |
+
NODE_KIND_UNMANAGED,
|
| 70 |
+
NODE_KIND_WORKER,
|
| 71 |
+
STATUS_UP_TO_DATE,
|
| 72 |
+
STATUS_UPDATE_FAILED,
|
| 73 |
+
TAG_RAY_FILE_MOUNTS_CONTENTS,
|
| 74 |
+
TAG_RAY_LAUNCH_CONFIG,
|
| 75 |
+
TAG_RAY_NODE_KIND,
|
| 76 |
+
TAG_RAY_NODE_STATUS,
|
| 77 |
+
TAG_RAY_RUNTIME_CONFIG,
|
| 78 |
+
TAG_RAY_USER_NODE_TYPE,
|
| 79 |
+
)
|
| 80 |
+
from ray.exceptions import RpcError
|
| 81 |
+
|
| 82 |
+
logger = logging.getLogger(__name__)
|
| 83 |
+
|
| 84 |
+
# Status of a node e.g. "up-to-date", see ray/autoscaler/tags.py
|
| 85 |
+
NodeStatus = str
|
| 86 |
+
|
| 87 |
+
# Tuple of modified fields for the given node_id returned by should_update
|
| 88 |
+
# that will be passed into a NodeUpdaterThread.
|
| 89 |
+
UpdateInstructions = namedtuple(
|
| 90 |
+
"UpdateInstructions",
|
| 91 |
+
["node_id", "setup_commands", "ray_start_commands", "docker_config"],
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
NodeLaunchData = Tuple[NodeTypeConfigDict, NodeCount, Optional[NodeType]]
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@dataclass
|
| 98 |
+
class AutoscalerSummary:
|
| 99 |
+
active_nodes: Dict[NodeType, int]
|
| 100 |
+
idle_nodes: Optional[Dict[NodeType, int]]
|
| 101 |
+
pending_nodes: List[Tuple[NodeIP, NodeType, NodeStatus]]
|
| 102 |
+
pending_launches: Dict[NodeType, int]
|
| 103 |
+
failed_nodes: List[Tuple[NodeIP, NodeType]]
|
| 104 |
+
node_availability_summary: NodeAvailabilitySummary = field(
|
| 105 |
+
default_factory=lambda: NodeAvailabilitySummary({})
|
| 106 |
+
)
|
| 107 |
+
# A dictionary of node IP to a list of reasons the node is not idle.
|
| 108 |
+
node_activities: Optional[Dict[str, Tuple[NodeIP, List[str]]]] = None
|
| 109 |
+
pending_resources: Dict[str, int] = field(default_factory=lambda: {})
|
| 110 |
+
# A mapping from node name (the same key as `usage_by_node`) to node type.
|
| 111 |
+
# Optional for deployment modes which have the concept of node types and
|
| 112 |
+
# backwards compatibility.
|
| 113 |
+
node_type_mapping: Optional[Dict[str, str]] = None
|
| 114 |
+
# Whether the autoscaler summary is v1 or v2.
|
| 115 |
+
legacy: bool = False
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
class NonTerminatedNodes:
|
| 119 |
+
"""Class to extract and organize information on non-terminated nodes."""
|
| 120 |
+
|
| 121 |
+
def __init__(self, provider: NodeProvider):
|
| 122 |
+
start_time = time.time()
|
| 123 |
+
# All non-terminated nodes
|
| 124 |
+
self.all_node_ids = provider.non_terminated_nodes({})
|
| 125 |
+
|
| 126 |
+
# Managed worker nodes (node kind "worker"):
|
| 127 |
+
self.worker_ids: List[NodeID] = []
|
| 128 |
+
# The head node (node kind "head")
|
| 129 |
+
self.head_id: Optional[NodeID] = None
|
| 130 |
+
|
| 131 |
+
for node in self.all_node_ids:
|
| 132 |
+
node_kind = provider.node_tags(node)[TAG_RAY_NODE_KIND]
|
| 133 |
+
if node_kind == NODE_KIND_WORKER:
|
| 134 |
+
self.worker_ids.append(node)
|
| 135 |
+
elif node_kind == NODE_KIND_HEAD:
|
| 136 |
+
self.head_id = node
|
| 137 |
+
|
| 138 |
+
# Note: For typical use-cases, self.all_node_ids == self.worker_ids +
|
| 139 |
+
# [self.head_id]. The difference being in the case of unmanaged nodes.
|
| 140 |
+
|
| 141 |
+
# Record the time of the non_terminated nodes call. This typically
|
| 142 |
+
# translates to a "describe" or "list" call on most cluster managers
|
| 143 |
+
# which can be quite expensive. Note that we include the processing
|
| 144 |
+
# time because on some clients, there may be pagination and the
|
| 145 |
+
# underlying api calls may be done lazily.
|
| 146 |
+
self.non_terminated_nodes_time = time.time() - start_time
|
| 147 |
+
logger.info(
|
| 148 |
+
f"The autoscaler took {round(self.non_terminated_nodes_time, 3)}"
|
| 149 |
+
" seconds to fetch the list of non-terminated nodes."
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
def remove_terminating_nodes(self, terminating_nodes: List[NodeID]) -> None:
|
| 153 |
+
"""Remove nodes we're in the process of terminating from internal
|
| 154 |
+
state."""
|
| 155 |
+
|
| 156 |
+
def not_terminating(node):
|
| 157 |
+
return node not in terminating_nodes
|
| 158 |
+
|
| 159 |
+
self.worker_ids = list(filter(not_terminating, self.worker_ids))
|
| 160 |
+
self.all_node_ids = list(filter(not_terminating, self.all_node_ids))
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
# Whether a worker should be kept based on the min_workers and
|
| 164 |
+
# max_workers constraints.
|
| 165 |
+
#
|
| 166 |
+
# keep: should keep the worker
|
| 167 |
+
# terminate: should terminate the worker
|
| 168 |
+
# decide_later: the worker can be terminated if needed
|
| 169 |
+
KeepOrTerminate = Enum("KeepOrTerminate", "keep terminate decide_later")
|
| 170 |
+
|
| 171 |
+
|
| 172 |
+
class StandardAutoscaler:
|
| 173 |
+
"""The autoscaling control loop for a Ray cluster.
|
| 174 |
+
|
| 175 |
+
There are two ways to start an autoscaling cluster: manually by running
|
| 176 |
+
`ray start --head --autoscaling-config=/path/to/config.yaml` on a instance
|
| 177 |
+
that has permission to launch other instances, or you can also use `ray up
|
| 178 |
+
/path/to/config.yaml` from your laptop, which will configure the right
|
| 179 |
+
AWS/Cloud roles automatically. See the Ray documentation
|
| 180 |
+
(https://docs.ray.io/en/latest/) for a full definition of autoscaling behavior.
|
| 181 |
+
StandardAutoscaler's `update` method is periodically called in
|
| 182 |
+
`monitor.py`'s monitoring loop.
|
| 183 |
+
|
| 184 |
+
StandardAutoscaler is also used to bootstrap clusters (by adding workers
|
| 185 |
+
until the cluster size that can handle the resource demand is met).
|
| 186 |
+
"""
|
| 187 |
+
|
| 188 |
+
def __init__(
|
| 189 |
+
self,
|
| 190 |
+
# TODO(ekl): require config reader to be a callable always.
|
| 191 |
+
config_reader: Union[str, Callable[[], dict]],
|
| 192 |
+
load_metrics: LoadMetrics,
|
| 193 |
+
gcs_client: "ray._raylet.GcsClient",
|
| 194 |
+
session_name: Optional[str] = None,
|
| 195 |
+
max_launch_batch: int = AUTOSCALER_MAX_LAUNCH_BATCH,
|
| 196 |
+
max_concurrent_launches: int = AUTOSCALER_MAX_CONCURRENT_LAUNCHES,
|
| 197 |
+
max_failures: int = AUTOSCALER_MAX_NUM_FAILURES,
|
| 198 |
+
process_runner: Any = subprocess,
|
| 199 |
+
update_interval_s: int = AUTOSCALER_UPDATE_INTERVAL_S,
|
| 200 |
+
prefix_cluster_info: bool = False,
|
| 201 |
+
event_summarizer: Optional[EventSummarizer] = None,
|
| 202 |
+
prom_metrics: Optional[AutoscalerPrometheusMetrics] = None,
|
| 203 |
+
):
|
| 204 |
+
"""Create a StandardAutoscaler.
|
| 205 |
+
|
| 206 |
+
Args:
|
| 207 |
+
config_reader: Path to a Ray Autoscaler YAML, or a function to read
|
| 208 |
+
and return the latest config.
|
| 209 |
+
load_metrics: Provides metrics for the Ray cluster.
|
| 210 |
+
session_name: The current Ray session name when this autoscaler
|
| 211 |
+
is deployed.
|
| 212 |
+
max_launch_batch: Max number of nodes to launch in one request.
|
| 213 |
+
max_concurrent_launches: Max number of nodes that can be
|
| 214 |
+
concurrently launched. This value and `max_launch_batch`
|
| 215 |
+
determine the number of batches that are used to launch nodes.
|
| 216 |
+
max_failures: Number of failures that the autoscaler will tolerate
|
| 217 |
+
before exiting.
|
| 218 |
+
process_runner: Subproc-like interface used by the CommandRunner.
|
| 219 |
+
update_interval_s: Seconds between running the autoscaling loop.
|
| 220 |
+
prefix_cluster_info: Whether to add the cluster name to info strs.
|
| 221 |
+
event_summarizer: Utility to consolidate duplicated messages.
|
| 222 |
+
prom_metrics: Prometheus metrics for autoscaler-related operations.
|
| 223 |
+
gcs_client: client for interactions with the GCS. Used to drain nodes
|
| 224 |
+
before termination.
|
| 225 |
+
"""
|
| 226 |
+
|
| 227 |
+
if isinstance(config_reader, str):
|
| 228 |
+
# Auto wrap with file reader.
|
| 229 |
+
def read_fn():
|
| 230 |
+
with open(config_reader) as f:
|
| 231 |
+
new_config = yaml.safe_load(f.read())
|
| 232 |
+
return new_config
|
| 233 |
+
|
| 234 |
+
self.config_reader = read_fn
|
| 235 |
+
else:
|
| 236 |
+
self.config_reader = config_reader
|
| 237 |
+
|
| 238 |
+
self.node_provider_availability_tracker = NodeProviderAvailabilityTracker()
|
| 239 |
+
# Prefix each line of info string with cluster name if True
|
| 240 |
+
self.prefix_cluster_info = prefix_cluster_info
|
| 241 |
+
# Keep this before self.reset (self.provider needs to be created
|
| 242 |
+
# exactly once).
|
| 243 |
+
self.provider = None
|
| 244 |
+
# Keep this before self.reset (if an exception occurs in reset
|
| 245 |
+
# then prom_metrics must be instantitiated to increment the
|
| 246 |
+
# exception counter)
|
| 247 |
+
self.prom_metrics = prom_metrics or AutoscalerPrometheusMetrics(
|
| 248 |
+
session_name=session_name
|
| 249 |
+
) # noqa
|
| 250 |
+
self.resource_demand_scheduler = None
|
| 251 |
+
self.reset(errors_fatal=True)
|
| 252 |
+
self.load_metrics = load_metrics
|
| 253 |
+
|
| 254 |
+
self.max_failures = max_failures
|
| 255 |
+
self.max_launch_batch = max_launch_batch
|
| 256 |
+
self.max_concurrent_launches = max_concurrent_launches
|
| 257 |
+
self.process_runner = process_runner
|
| 258 |
+
self.event_summarizer = event_summarizer or EventSummarizer()
|
| 259 |
+
|
| 260 |
+
# Map from node_id to NodeUpdater threads
|
| 261 |
+
self.updaters: Dict[NodeID, NodeUpdaterThread] = {}
|
| 262 |
+
self.num_failed_updates: Dict[NodeID, int] = defaultdict(int)
|
| 263 |
+
self.num_successful_updates: Dict[NodeID, int] = defaultdict(int)
|
| 264 |
+
self.num_failures = 0
|
| 265 |
+
self.last_update_time = 0.0
|
| 266 |
+
self.update_interval_s = update_interval_s
|
| 267 |
+
|
| 268 |
+
# Keeps track of pending and running nodes
|
| 269 |
+
self.non_terminated_nodes: Optional[NonTerminatedNodes] = None
|
| 270 |
+
|
| 271 |
+
# Tracks nodes scheduled for termination
|
| 272 |
+
self.nodes_to_terminate: List[NodeID] = []
|
| 273 |
+
|
| 274 |
+
# Disable NodeUpdater threads if true.
|
| 275 |
+
# Should be set to true in situations where another component, such as
|
| 276 |
+
# a Kubernetes operator, is responsible for Ray setup on nodes.
|
| 277 |
+
self.disable_node_updaters = self.config["provider"].get(
|
| 278 |
+
DISABLE_NODE_UPDATERS_KEY, False
|
| 279 |
+
)
|
| 280 |
+
logger.info(f"{DISABLE_NODE_UPDATERS_KEY}:{self.disable_node_updaters}")
|
| 281 |
+
|
| 282 |
+
# Disable launch configuration checking if set to true.
|
| 283 |
+
# This setting is used in scenarios where there is no meaningful node type
|
| 284 |
+
# to enforce, such as in fake multinode situations. When this option is enabled,
|
| 285 |
+
# outdated nodes will not be terminated.
|
| 286 |
+
self.disable_launch_config_check = self.config["provider"].get(
|
| 287 |
+
DISABLE_LAUNCH_CONFIG_CHECK_KEY, False
|
| 288 |
+
)
|
| 289 |
+
logger.info(
|
| 290 |
+
f"{DISABLE_LAUNCH_CONFIG_CHECK_KEY}:{self.disable_launch_config_check}"
|
| 291 |
+
)
|
| 292 |
+
|
| 293 |
+
# By default, the autoscaler launches nodes in batches asynchronously in
|
| 294 |
+
# background threads.
|
| 295 |
+
# When the following flag is set, that behavior is disabled, so that nodes
|
| 296 |
+
# are launched in the main thread, all in one batch, blocking until all
|
| 297 |
+
# NodeProvider.create_node calls have returned.
|
| 298 |
+
self.foreground_node_launch = self.config["provider"].get(
|
| 299 |
+
FOREGROUND_NODE_LAUNCH_KEY, False
|
| 300 |
+
)
|
| 301 |
+
logger.info(f"{FOREGROUND_NODE_LAUNCH_KEY}:{self.foreground_node_launch}")
|
| 302 |
+
|
| 303 |
+
# By default, the autoscaler kills and/or tries to recover
|
| 304 |
+
# a worker node if it hasn't produced a resource heartbeat in the last 30
|
| 305 |
+
# seconds. The worker_liveness_check flag allows disabling this behavior in
|
| 306 |
+
# settings where another component, such as a Kubernetes operator, is
|
| 307 |
+
# responsible for healthchecks.
|
| 308 |
+
self.worker_liveness_check = self.config["provider"].get(
|
| 309 |
+
WORKER_LIVENESS_CHECK_KEY, True
|
| 310 |
+
)
|
| 311 |
+
logger.info(f"{WORKER_LIVENESS_CHECK_KEY}:{self.worker_liveness_check}")
|
| 312 |
+
|
| 313 |
+
# Node launchers
|
| 314 |
+
self.foreground_node_launcher: Optional[BaseNodeLauncher] = None
|
| 315 |
+
self.launch_queue: Optional[queue.Queue[NodeLaunchData]] = None
|
| 316 |
+
self.pending_launches = ConcurrentCounter()
|
| 317 |
+
if self.foreground_node_launch:
|
| 318 |
+
self.foreground_node_launcher = BaseNodeLauncher(
|
| 319 |
+
provider=self.provider,
|
| 320 |
+
pending=self.pending_launches,
|
| 321 |
+
event_summarizer=self.event_summarizer,
|
| 322 |
+
node_provider_availability_tracker=self.node_provider_availability_tracker, # noqa: E501 Flake and black disagree how to format this.
|
| 323 |
+
session_name=session_name,
|
| 324 |
+
node_types=self.available_node_types,
|
| 325 |
+
prom_metrics=self.prom_metrics,
|
| 326 |
+
)
|
| 327 |
+
else:
|
| 328 |
+
self.launch_queue = queue.Queue()
|
| 329 |
+
max_batches = math.ceil(max_concurrent_launches / float(max_launch_batch))
|
| 330 |
+
for i in range(int(max_batches)):
|
| 331 |
+
node_launcher = NodeLauncher(
|
| 332 |
+
provider=self.provider,
|
| 333 |
+
queue=self.launch_queue,
|
| 334 |
+
index=i,
|
| 335 |
+
pending=self.pending_launches,
|
| 336 |
+
event_summarizer=self.event_summarizer,
|
| 337 |
+
node_provider_availability_tracker=self.node_provider_availability_tracker, # noqa: E501 Flake and black disagreee how to format this.
|
| 338 |
+
session_name=session_name,
|
| 339 |
+
node_types=self.available_node_types,
|
| 340 |
+
prom_metrics=self.prom_metrics,
|
| 341 |
+
)
|
| 342 |
+
node_launcher.daemon = True
|
| 343 |
+
node_launcher.start()
|
| 344 |
+
|
| 345 |
+
# NodeTracker maintains soft state to track the number of recently
|
| 346 |
+
# failed nodes. It is best effort only.
|
| 347 |
+
self.node_tracker = NodeTracker()
|
| 348 |
+
|
| 349 |
+
# Expand local file_mounts to allow ~ in the paths. This can't be done
|
| 350 |
+
# earlier when the config is written since we might be on different
|
| 351 |
+
# platform and the expansion would result in wrong path.
|
| 352 |
+
self.config["file_mounts"] = {
|
| 353 |
+
remote: os.path.expanduser(local)
|
| 354 |
+
for remote, local in self.config["file_mounts"].items()
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
self.gcs_client = gcs_client
|
| 358 |
+
|
| 359 |
+
for local_path in self.config["file_mounts"].values():
|
| 360 |
+
assert os.path.exists(local_path)
|
| 361 |
+
logger.info("StandardAutoscaler: {}".format(self.config))
|
| 362 |
+
|
| 363 |
+
@property
|
| 364 |
+
def all_node_types(self) -> Set[str]:
|
| 365 |
+
return self.config["available_node_types"].keys()
|
| 366 |
+
|
| 367 |
+
def update(self):
|
| 368 |
+
try:
|
| 369 |
+
self.reset(errors_fatal=False)
|
| 370 |
+
self._update()
|
| 371 |
+
except Exception as e:
|
| 372 |
+
self.prom_metrics.update_loop_exceptions.inc()
|
| 373 |
+
logger.exception("StandardAutoscaler: Error during autoscaling.")
|
| 374 |
+
self.num_failures += 1
|
| 375 |
+
if self.num_failures > self.max_failures:
|
| 376 |
+
logger.critical("StandardAutoscaler: Too many errors, abort.")
|
| 377 |
+
raise e
|
| 378 |
+
|
| 379 |
+
def _update(self):
|
| 380 |
+
# For type checking, assert that these objects have been instantitiated.
|
| 381 |
+
assert self.provider
|
| 382 |
+
assert self.resource_demand_scheduler
|
| 383 |
+
|
| 384 |
+
now = time.time()
|
| 385 |
+
# Throttle autoscaling updates to this interval to avoid exceeding
|
| 386 |
+
# rate limits on API calls.
|
| 387 |
+
if now - self.last_update_time < self.update_interval_s:
|
| 388 |
+
return
|
| 389 |
+
|
| 390 |
+
self.last_update_time = now
|
| 391 |
+
|
| 392 |
+
# Query the provider to update the list of non-terminated nodes
|
| 393 |
+
self.non_terminated_nodes = NonTerminatedNodes(self.provider)
|
| 394 |
+
|
| 395 |
+
# Back off the update if the provider says it's not safe to proceed.
|
| 396 |
+
if not self.provider.safe_to_scale():
|
| 397 |
+
logger.info(
|
| 398 |
+
"Backing off of autoscaler update."
|
| 399 |
+
f" Will try again in {self.update_interval_s} seconds."
|
| 400 |
+
)
|
| 401 |
+
return
|
| 402 |
+
|
| 403 |
+
# This will accumulate the nodes we need to terminate.
|
| 404 |
+
self.nodes_to_terminate = []
|
| 405 |
+
|
| 406 |
+
# Update status strings
|
| 407 |
+
if AUTOSCALER_STATUS_LOG:
|
| 408 |
+
logger.info(self.info_string())
|
| 409 |
+
legacy_log_info_string(self, self.non_terminated_nodes.worker_ids)
|
| 410 |
+
|
| 411 |
+
if not self.provider.is_readonly():
|
| 412 |
+
self.terminate_nodes_to_enforce_config_constraints(now)
|
| 413 |
+
|
| 414 |
+
if self.disable_node_updaters:
|
| 415 |
+
# Don't handle unhealthy nodes if the liveness check is disabled.
|
| 416 |
+
# self.worker_liveness_check is True by default.
|
| 417 |
+
if self.worker_liveness_check:
|
| 418 |
+
self.terminate_unhealthy_nodes(now)
|
| 419 |
+
else:
|
| 420 |
+
self.process_completed_updates()
|
| 421 |
+
self.update_nodes()
|
| 422 |
+
# Don't handle unhealthy nodes if the liveness check is disabled.
|
| 423 |
+
# self.worker_liveness_check is True by default.
|
| 424 |
+
if self.worker_liveness_check:
|
| 425 |
+
self.attempt_to_recover_unhealthy_nodes(now)
|
| 426 |
+
self.set_prometheus_updater_data()
|
| 427 |
+
|
| 428 |
+
# Update running nodes gauge
|
| 429 |
+
num_workers = len(self.non_terminated_nodes.worker_ids)
|
| 430 |
+
self.prom_metrics.running_workers.set(num_workers)
|
| 431 |
+
|
| 432 |
+
# Remove IPs from LoadMetrics that are not known to the NodeProvider.
|
| 433 |
+
self.load_metrics.prune_active_ips(
|
| 434 |
+
active_ips=[
|
| 435 |
+
self.provider.internal_ip(node_id)
|
| 436 |
+
for node_id in self.non_terminated_nodes.all_node_ids
|
| 437 |
+
]
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
# Dict[NodeType, int], List[ResourceDict]
|
| 441 |
+
to_launch, unfulfilled = self.resource_demand_scheduler.get_nodes_to_launch(
|
| 442 |
+
self.non_terminated_nodes.all_node_ids,
|
| 443 |
+
self.pending_launches.breakdown(),
|
| 444 |
+
self.load_metrics.get_resource_demand_vector(),
|
| 445 |
+
self.load_metrics.get_resource_utilization(),
|
| 446 |
+
self.load_metrics.get_pending_placement_groups(),
|
| 447 |
+
self.load_metrics.get_static_node_resources_by_ip(),
|
| 448 |
+
ensure_min_cluster_size=self.load_metrics.get_resource_requests(),
|
| 449 |
+
node_availability_summary=self.node_provider_availability_tracker.summary(),
|
| 450 |
+
)
|
| 451 |
+
self._report_pending_infeasible(unfulfilled)
|
| 452 |
+
|
| 453 |
+
if not self.provider.is_readonly():
|
| 454 |
+
self.launch_required_nodes(to_launch)
|
| 455 |
+
|
| 456 |
+
# Execute optional end-of-update logic.
|
| 457 |
+
# Keep this method call at the end of autoscaler._update().
|
| 458 |
+
self.provider.post_process()
|
| 459 |
+
|
| 460 |
+
# Record the amount of time the autoscaler took for
|
| 461 |
+
# this _update() iteration.
|
| 462 |
+
update_time = time.time() - self.last_update_time
|
| 463 |
+
logger.info(
|
| 464 |
+
f"The autoscaler took {round(update_time, 3)}"
|
| 465 |
+
" seconds to complete the update iteration."
|
| 466 |
+
)
|
| 467 |
+
self.prom_metrics.update_time.observe(update_time)
|
| 468 |
+
|
| 469 |
+
def terminate_nodes_to_enforce_config_constraints(self, now: float):
|
| 470 |
+
"""Terminates nodes to enforce constraints defined by the autoscaling
|
| 471 |
+
config.
|
| 472 |
+
|
| 473 |
+
(1) Terminates nodes in excess of `max_workers`.
|
| 474 |
+
(2) Terminates nodes idle for longer than `idle_timeout_minutes`.
|
| 475 |
+
(3) Terminates outdated nodes,
|
| 476 |
+
namely nodes whose configs don't match `node_config` for the
|
| 477 |
+
relevant node type.
|
| 478 |
+
|
| 479 |
+
Avoids terminating non-outdated nodes required by
|
| 480 |
+
autoscaler.sdk.request_resources().
|
| 481 |
+
"""
|
| 482 |
+
# For type checking, assert that these objects have been instantitiated.
|
| 483 |
+
assert self.non_terminated_nodes
|
| 484 |
+
assert self.provider
|
| 485 |
+
|
| 486 |
+
last_used = self.load_metrics.ray_nodes_last_used_time_by_ip
|
| 487 |
+
|
| 488 |
+
idle_timeout_s = 60 * self.config["idle_timeout_minutes"]
|
| 489 |
+
|
| 490 |
+
last_used_cutoff = now - idle_timeout_s
|
| 491 |
+
|
| 492 |
+
# Sort based on last used to make sure to keep min_workers that
|
| 493 |
+
# were most recently used. Otherwise, _keep_min_workers_of_node_type
|
| 494 |
+
# might keep a node that should be terminated.
|
| 495 |
+
sorted_node_ids = self._sort_based_on_last_used(
|
| 496 |
+
self.non_terminated_nodes.worker_ids, last_used
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
# Don't terminate nodes needed by request_resources()
|
| 500 |
+
nodes_not_allowed_to_terminate = self._get_nodes_needed_for_request_resources(
|
| 501 |
+
sorted_node_ids
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
# Tracks counts of nodes we intend to keep for each node type.
|
| 505 |
+
node_type_counts = defaultdict(int)
|
| 506 |
+
|
| 507 |
+
def keep_node(node_id: NodeID) -> None:
|
| 508 |
+
assert self.provider
|
| 509 |
+
# Update per-type counts.
|
| 510 |
+
tags = self.provider.node_tags(node_id)
|
| 511 |
+
if TAG_RAY_USER_NODE_TYPE in tags:
|
| 512 |
+
node_type = tags[TAG_RAY_USER_NODE_TYPE]
|
| 513 |
+
node_type_counts[node_type] += 1
|
| 514 |
+
|
| 515 |
+
# Nodes that we could terminate, if needed.
|
| 516 |
+
nodes_we_could_terminate: List[NodeID] = []
|
| 517 |
+
|
| 518 |
+
for node_id in sorted_node_ids:
|
| 519 |
+
# Make sure to not kill idle node types if the number of workers
|
| 520 |
+
# of that type is lower/equal to the min_workers of that type
|
| 521 |
+
# or it is needed for request_resources().
|
| 522 |
+
should_keep_or_terminate, reason = self._keep_worker_of_node_type(
|
| 523 |
+
node_id, node_type_counts
|
| 524 |
+
)
|
| 525 |
+
if should_keep_or_terminate == KeepOrTerminate.terminate:
|
| 526 |
+
self.schedule_node_termination(node_id, reason, logger.info)
|
| 527 |
+
continue
|
| 528 |
+
if (
|
| 529 |
+
should_keep_or_terminate == KeepOrTerminate.keep
|
| 530 |
+
or node_id in nodes_not_allowed_to_terminate
|
| 531 |
+
) and self.launch_config_ok(node_id):
|
| 532 |
+
keep_node(node_id)
|
| 533 |
+
continue
|
| 534 |
+
|
| 535 |
+
node_ip = self.provider.internal_ip(node_id)
|
| 536 |
+
|
| 537 |
+
if node_ip in last_used and last_used[node_ip] < last_used_cutoff:
|
| 538 |
+
self.schedule_node_termination(node_id, "idle", logger.info)
|
| 539 |
+
# Get the local time of the node's last use as a string.
|
| 540 |
+
formatted_last_used_time = time.asctime(
|
| 541 |
+
time.localtime(last_used[node_ip])
|
| 542 |
+
)
|
| 543 |
+
logger.info(f"Node last used: {formatted_last_used_time}.")
|
| 544 |
+
# Note that the current time will appear in the log prefix.
|
| 545 |
+
elif not self.launch_config_ok(node_id):
|
| 546 |
+
self.schedule_node_termination(node_id, "outdated", logger.info)
|
| 547 |
+
else:
|
| 548 |
+
keep_node(node_id)
|
| 549 |
+
nodes_we_could_terminate.append(node_id)
|
| 550 |
+
|
| 551 |
+
# Terminate nodes if there are too many
|
| 552 |
+
num_workers = len(self.non_terminated_nodes.worker_ids)
|
| 553 |
+
num_extra_nodes_to_terminate = (
|
| 554 |
+
num_workers - len(self.nodes_to_terminate) - self.config["max_workers"]
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
if num_extra_nodes_to_terminate > len(nodes_we_could_terminate):
|
| 558 |
+
logger.warning(
|
| 559 |
+
"StandardAutoscaler: trying to terminate "
|
| 560 |
+
f"{num_extra_nodes_to_terminate} nodes, while only "
|
| 561 |
+
f"{len(nodes_we_could_terminate)} are safe to terminate."
|
| 562 |
+
" Inconsistent config is likely."
|
| 563 |
+
)
|
| 564 |
+
num_extra_nodes_to_terminate = len(nodes_we_could_terminate)
|
| 565 |
+
|
| 566 |
+
# If num_extra_nodes_to_terminate is negative or zero,
|
| 567 |
+
# we would have less than max_workers nodes after terminating
|
| 568 |
+
# nodes_to_terminate and we do not need to terminate anything else.
|
| 569 |
+
if num_extra_nodes_to_terminate > 0:
|
| 570 |
+
extra_nodes_to_terminate = nodes_we_could_terminate[
|
| 571 |
+
-num_extra_nodes_to_terminate:
|
| 572 |
+
]
|
| 573 |
+
for node_id in extra_nodes_to_terminate:
|
| 574 |
+
self.schedule_node_termination(node_id, "max workers", logger.info)
|
| 575 |
+
|
| 576 |
+
self.terminate_scheduled_nodes()
|
| 577 |
+
|
| 578 |
+
def schedule_node_termination(
|
| 579 |
+
self, node_id: NodeID, reason_opt: Optional[str], logger_method: Callable
|
| 580 |
+
) -> None:
|
| 581 |
+
# For type checking, assert that this object has been instantitiated.
|
| 582 |
+
assert self.provider
|
| 583 |
+
|
| 584 |
+
if reason_opt is None:
|
| 585 |
+
raise Exception("reason should be not None.")
|
| 586 |
+
reason: str = reason_opt
|
| 587 |
+
node_ip = self.provider.internal_ip(node_id)
|
| 588 |
+
# Log, record an event, and add node_id to nodes_to_terminate.
|
| 589 |
+
logger_method(
|
| 590 |
+
"StandardAutoscaler: "
|
| 591 |
+
f"Terminating the node with id {node_id}"
|
| 592 |
+
f" and ip {node_ip}."
|
| 593 |
+
f" ({reason})"
|
| 594 |
+
)
|
| 595 |
+
self.event_summarizer.add(
|
| 596 |
+
"Removing {} nodes of type "
|
| 597 |
+
+ self._get_node_type(node_id)
|
| 598 |
+
+ " ({}).".format(reason),
|
| 599 |
+
quantity=1,
|
| 600 |
+
aggregate=operator.add,
|
| 601 |
+
)
|
| 602 |
+
self.nodes_to_terminate.append(node_id)
|
| 603 |
+
|
| 604 |
+
def terminate_scheduled_nodes(self):
|
| 605 |
+
"""Terminate scheduled nodes and clean associated autoscaler state."""
|
| 606 |
+
# For type checking, assert that these objects have been instantitiated.
|
| 607 |
+
assert self.provider
|
| 608 |
+
assert self.non_terminated_nodes
|
| 609 |
+
|
| 610 |
+
if not self.nodes_to_terminate:
|
| 611 |
+
return
|
| 612 |
+
|
| 613 |
+
# Drain the nodes
|
| 614 |
+
self.drain_nodes_via_gcs(self.nodes_to_terminate)
|
| 615 |
+
# Terminate the nodes
|
| 616 |
+
self.provider.terminate_nodes(self.nodes_to_terminate)
|
| 617 |
+
for node in self.nodes_to_terminate:
|
| 618 |
+
self.node_tracker.untrack(node)
|
| 619 |
+
self.prom_metrics.stopped_nodes.inc()
|
| 620 |
+
|
| 621 |
+
# Update internal node lists
|
| 622 |
+
self.non_terminated_nodes.remove_terminating_nodes(self.nodes_to_terminate)
|
| 623 |
+
|
| 624 |
+
self.nodes_to_terminate = []
|
| 625 |
+
|
| 626 |
+
def drain_nodes_via_gcs(self, provider_node_ids_to_drain: List[NodeID]):
|
| 627 |
+
"""Send an RPC request to the GCS to drain (prepare for termination)
|
| 628 |
+
the nodes with the given node provider ids.
|
| 629 |
+
|
| 630 |
+
note: The current implementation of DrainNode on the GCS side is to
|
| 631 |
+
de-register and gracefully shut down the Raylets. In the future,
|
| 632 |
+
the behavior may change to better reflect the name "Drain."
|
| 633 |
+
See https://github.com/ray-project/ray/pull/19350.
|
| 634 |
+
"""
|
| 635 |
+
# For type checking, assert that this object has been instantitiated.
|
| 636 |
+
assert self.provider
|
| 637 |
+
|
| 638 |
+
# The GCS expects Node ids in the request, rather than NodeProvider
|
| 639 |
+
# ids. To get the Node ids of the nodes to we're draining, we make
|
| 640 |
+
# the following translations of identifiers:
|
| 641 |
+
# node provider node id -> ip -> node id
|
| 642 |
+
|
| 643 |
+
# Convert node provider node ids to ips.
|
| 644 |
+
node_ips = set()
|
| 645 |
+
failed_ip_fetch = False
|
| 646 |
+
for provider_node_id in provider_node_ids_to_drain:
|
| 647 |
+
# If the provider's call to fetch ip fails, the exception is not
|
| 648 |
+
# fatal. Log the exception and proceed.
|
| 649 |
+
try:
|
| 650 |
+
ip = self.provider.internal_ip(provider_node_id)
|
| 651 |
+
node_ips.add(ip)
|
| 652 |
+
except Exception:
|
| 653 |
+
logger.exception(
|
| 654 |
+
"Failed to get ip of node with id"
|
| 655 |
+
f" {provider_node_id} during scale-down."
|
| 656 |
+
)
|
| 657 |
+
failed_ip_fetch = True
|
| 658 |
+
if failed_ip_fetch:
|
| 659 |
+
self.prom_metrics.drain_node_exceptions.inc()
|
| 660 |
+
|
| 661 |
+
# Only attempt to drain connected nodes, i.e. nodes with ips in
|
| 662 |
+
# LoadMetrics.
|
| 663 |
+
connected_node_ips = node_ips & self.load_metrics.node_id_by_ip.keys()
|
| 664 |
+
|
| 665 |
+
# Convert ips to Node ids.
|
| 666 |
+
# (The assignment ip->node_id is well-defined under current
|
| 667 |
+
# assumptions. See "use_node_id_as_ip" in monitor.py)
|
| 668 |
+
node_ids_to_drain = {
|
| 669 |
+
self.load_metrics.node_id_by_ip[ip] for ip in connected_node_ips
|
| 670 |
+
}
|
| 671 |
+
|
| 672 |
+
if not node_ids_to_drain:
|
| 673 |
+
return
|
| 674 |
+
|
| 675 |
+
logger.info(f"Draining {len(node_ids_to_drain)} raylet(s).")
|
| 676 |
+
try:
|
| 677 |
+
# A successful response indicates that the GCS has marked the
|
| 678 |
+
# desired nodes as "drained." The cloud provider can then terminate
|
| 679 |
+
# the nodes without the GCS printing an error.
|
| 680 |
+
# Check if we succeeded in draining all of the intended nodes by
|
| 681 |
+
# looking at the RPC response.
|
| 682 |
+
drained_node_ids = set(
|
| 683 |
+
self.gcs_client.drain_nodes(node_ids_to_drain, timeout=5)
|
| 684 |
+
)
|
| 685 |
+
failed_to_drain = node_ids_to_drain - drained_node_ids
|
| 686 |
+
if failed_to_drain:
|
| 687 |
+
self.prom_metrics.drain_node_exceptions.inc()
|
| 688 |
+
logger.error(f"Failed to drain {len(failed_to_drain)} raylet(s).")
|
| 689 |
+
# If we get a gRPC error with an UNIMPLEMENTED code, fail silently.
|
| 690 |
+
# This error indicates that the GCS is using Ray version < 1.8.0,
|
| 691 |
+
# for which DrainNode is not implemented.
|
| 692 |
+
except RpcError as e:
|
| 693 |
+
# If the code is UNIMPLEMENTED, pass.
|
| 694 |
+
if e.rpc_code == ray._raylet.GRPC_STATUS_CODE_UNIMPLEMENTED:
|
| 695 |
+
pass
|
| 696 |
+
# Otherwise, it's a plain old gRPC error and we should log it.
|
| 697 |
+
else:
|
| 698 |
+
self.prom_metrics.drain_node_exceptions.inc()
|
| 699 |
+
logger.exception("Failed to drain Ray nodes. Traceback follows.")
|
| 700 |
+
except Exception:
|
| 701 |
+
# We don't need to interrupt the autoscaler update with an
|
| 702 |
+
# exception, but we should log what went wrong and record the
|
| 703 |
+
# failure in Prometheus.
|
| 704 |
+
self.prom_metrics.drain_node_exceptions.inc()
|
| 705 |
+
logger.exception("Failed to drain Ray nodes. Traceback follows.")
|
| 706 |
+
|
| 707 |
+
def launch_required_nodes(self, to_launch: Dict[NodeType, int]) -> None:
|
| 708 |
+
if to_launch:
|
| 709 |
+
for node_type, count in to_launch.items():
|
| 710 |
+
self.launch_new_node(count, node_type=node_type)
|
| 711 |
+
|
| 712 |
+
def update_nodes(self):
|
| 713 |
+
"""Run NodeUpdaterThreads to run setup commands, sync files,
|
| 714 |
+
and/or start Ray.
|
| 715 |
+
"""
|
| 716 |
+
# Update nodes with out-of-date files.
|
| 717 |
+
# TODO(edoakes): Spawning these threads directly seems to cause
|
| 718 |
+
# problems. They should at a minimum be spawned as daemon threads.
|
| 719 |
+
# See https://github.com/ray-project/ray/pull/5903 for more info.
|
| 720 |
+
T = []
|
| 721 |
+
for node_id, setup_commands, ray_start_commands, docker_config in (
|
| 722 |
+
self.should_update(node_id)
|
| 723 |
+
for node_id in self.non_terminated_nodes.worker_ids
|
| 724 |
+
):
|
| 725 |
+
if node_id is not None:
|
| 726 |
+
resources = self._node_resources(node_id)
|
| 727 |
+
labels = self._node_labels(node_id)
|
| 728 |
+
logger.debug(f"{node_id}: Starting new thread runner.")
|
| 729 |
+
T.append(
|
| 730 |
+
threading.Thread(
|
| 731 |
+
target=self.spawn_updater,
|
| 732 |
+
args=(
|
| 733 |
+
node_id,
|
| 734 |
+
setup_commands,
|
| 735 |
+
ray_start_commands,
|
| 736 |
+
resources,
|
| 737 |
+
labels,
|
| 738 |
+
docker_config,
|
| 739 |
+
),
|
| 740 |
+
)
|
| 741 |
+
)
|
| 742 |
+
for t in T:
|
| 743 |
+
t.start()
|
| 744 |
+
for t in T:
|
| 745 |
+
t.join()
|
| 746 |
+
|
| 747 |
+
def process_completed_updates(self):
|
| 748 |
+
"""Clean up completed NodeUpdaterThreads."""
|
| 749 |
+
completed_nodes = []
|
| 750 |
+
for node_id, updater in self.updaters.items():
|
| 751 |
+
if not updater.is_alive():
|
| 752 |
+
completed_nodes.append(node_id)
|
| 753 |
+
if completed_nodes:
|
| 754 |
+
failed_nodes = []
|
| 755 |
+
for node_id in completed_nodes:
|
| 756 |
+
updater = self.updaters[node_id]
|
| 757 |
+
if updater.exitcode == 0:
|
| 758 |
+
self.num_successful_updates[node_id] += 1
|
| 759 |
+
self.prom_metrics.successful_updates.inc()
|
| 760 |
+
if updater.for_recovery:
|
| 761 |
+
self.prom_metrics.successful_recoveries.inc()
|
| 762 |
+
if updater.update_time:
|
| 763 |
+
self.prom_metrics.worker_update_time.observe(
|
| 764 |
+
updater.update_time
|
| 765 |
+
)
|
| 766 |
+
# Mark the node as active to prevent the node recovery
|
| 767 |
+
# logic immediately trying to restart Ray on the new node.
|
| 768 |
+
self.load_metrics.mark_active(self.provider.internal_ip(node_id))
|
| 769 |
+
else:
|
| 770 |
+
failed_nodes.append(node_id)
|
| 771 |
+
self.num_failed_updates[node_id] += 1
|
| 772 |
+
self.prom_metrics.failed_updates.inc()
|
| 773 |
+
if updater.for_recovery:
|
| 774 |
+
self.prom_metrics.failed_recoveries.inc()
|
| 775 |
+
self.node_tracker.untrack(node_id)
|
| 776 |
+
del self.updaters[node_id]
|
| 777 |
+
|
| 778 |
+
if failed_nodes:
|
| 779 |
+
# Some nodes in failed_nodes may already have been terminated
|
| 780 |
+
# during an update (for being idle after missing a heartbeat).
|
| 781 |
+
|
| 782 |
+
# Update the list of non-terminated workers.
|
| 783 |
+
for node_id in failed_nodes:
|
| 784 |
+
# Check if the node has already been terminated.
|
| 785 |
+
if node_id in self.non_terminated_nodes.worker_ids:
|
| 786 |
+
self.schedule_node_termination(
|
| 787 |
+
node_id, "launch failed", logger.error
|
| 788 |
+
)
|
| 789 |
+
else:
|
| 790 |
+
logger.warning(
|
| 791 |
+
f"StandardAutoscaler: {node_id}:"
|
| 792 |
+
" Failed to update node."
|
| 793 |
+
" Node has already been terminated."
|
| 794 |
+
)
|
| 795 |
+
self.terminate_scheduled_nodes()
|
| 796 |
+
|
| 797 |
+
def set_prometheus_updater_data(self):
|
| 798 |
+
"""Record total number of active NodeUpdaterThreads and how many of
|
| 799 |
+
these are being run to recover nodes.
|
| 800 |
+
"""
|
| 801 |
+
self.prom_metrics.updating_nodes.set(len(self.updaters))
|
| 802 |
+
num_recovering = 0
|
| 803 |
+
for updater in self.updaters.values():
|
| 804 |
+
if updater.for_recovery:
|
| 805 |
+
num_recovering += 1
|
| 806 |
+
self.prom_metrics.recovering_nodes.set(num_recovering)
|
| 807 |
+
|
| 808 |
+
def _report_pending_infeasible(self, unfulfilled: List[ResourceDict]):
|
| 809 |
+
"""Emit event messages for infeasible or unschedulable tasks.
|
| 810 |
+
|
| 811 |
+
This adds messages to the event summarizer for warning on infeasible
|
| 812 |
+
or "cluster full" resource requests.
|
| 813 |
+
|
| 814 |
+
Args:
|
| 815 |
+
unfulfilled: List of resource demands that would be unfulfilled
|
| 816 |
+
even after full scale-up.
|
| 817 |
+
"""
|
| 818 |
+
# For type checking, assert that this object has been instantitiated.
|
| 819 |
+
assert self.resource_demand_scheduler
|
| 820 |
+
pending = []
|
| 821 |
+
infeasible = []
|
| 822 |
+
for bundle in unfulfilled:
|
| 823 |
+
placement_group = any(
|
| 824 |
+
"_group_" in k or k == PLACEMENT_GROUP_BUNDLE_RESOURCE_NAME
|
| 825 |
+
for k in bundle
|
| 826 |
+
)
|
| 827 |
+
if placement_group:
|
| 828 |
+
continue
|
| 829 |
+
if self.resource_demand_scheduler.is_feasible(bundle):
|
| 830 |
+
pending.append(bundle)
|
| 831 |
+
else:
|
| 832 |
+
infeasible.append(bundle)
|
| 833 |
+
if pending:
|
| 834 |
+
if self.load_metrics.cluster_full_of_actors_detected:
|
| 835 |
+
for request in pending:
|
| 836 |
+
self.event_summarizer.add_once_per_interval(
|
| 837 |
+
"Warning: The following resource request cannot be "
|
| 838 |
+
"scheduled right now: {}. This is likely due to all "
|
| 839 |
+
"cluster resources being claimed by actors. Consider "
|
| 840 |
+
"creating fewer actors or adding more nodes "
|
| 841 |
+
"to this Ray cluster.".format(request),
|
| 842 |
+
key="pending_{}".format(sorted(request.items())),
|
| 843 |
+
interval_s=30,
|
| 844 |
+
)
|
| 845 |
+
if infeasible:
|
| 846 |
+
for request in infeasible:
|
| 847 |
+
self.event_summarizer.add_once_per_interval(
|
| 848 |
+
"Error: No available node types can fulfill resource "
|
| 849 |
+
"request {}. Add suitable node types to this cluster to "
|
| 850 |
+
"resolve this issue.".format(request),
|
| 851 |
+
key="infeasible_{}".format(sorted(request.items())),
|
| 852 |
+
interval_s=30,
|
| 853 |
+
)
|
| 854 |
+
|
| 855 |
+
def _sort_based_on_last_used(
|
| 856 |
+
self, nodes: List[NodeID], last_used: Dict[str, float]
|
| 857 |
+
) -> List[NodeID]:
|
| 858 |
+
"""Sort the nodes based on the last time they were used.
|
| 859 |
+
|
| 860 |
+
The first item in the return list is the most recently used.
|
| 861 |
+
"""
|
| 862 |
+
last_used_copy = copy.deepcopy(last_used)
|
| 863 |
+
# Add the unconnected nodes as the least recently used (the end of
|
| 864 |
+
# list). This prioritizes connected nodes.
|
| 865 |
+
least_recently_used = -1
|
| 866 |
+
|
| 867 |
+
def last_time_used(node_id: NodeID):
|
| 868 |
+
assert self.provider
|
| 869 |
+
node_ip = self.provider.internal_ip(node_id)
|
| 870 |
+
if node_ip not in last_used_copy:
|
| 871 |
+
return least_recently_used
|
| 872 |
+
else:
|
| 873 |
+
return last_used_copy[node_ip]
|
| 874 |
+
|
| 875 |
+
return sorted(nodes, key=last_time_used, reverse=True)
|
| 876 |
+
|
| 877 |
+
def _get_nodes_needed_for_request_resources(
|
| 878 |
+
self, sorted_node_ids: List[NodeID]
|
| 879 |
+
) -> FrozenSet[NodeID]:
|
| 880 |
+
# TODO(ameer): try merging this with resource_demand_scheduler
|
| 881 |
+
# code responsible for adding nodes for get_resource_requests() and get_pending_placement_groups().
|
| 882 |
+
"""Returns the nodes NOT allowed to terminate due to get_resource_requests() and get_pending_placement_groups().
|
| 883 |
+
|
| 884 |
+
Args:
|
| 885 |
+
sorted_node_ids: the node ids sorted based on last used (LRU last).
|
| 886 |
+
|
| 887 |
+
Returns:
|
| 888 |
+
FrozenSet[NodeID]: a set of nodes (node ids) that
|
| 889 |
+
we should NOT terminate.
|
| 890 |
+
"""
|
| 891 |
+
# For type checking, assert that this object has been instantitiated.
|
| 892 |
+
assert self.provider
|
| 893 |
+
|
| 894 |
+
nodes_not_allowed_to_terminate: Set[NodeID] = set()
|
| 895 |
+
|
| 896 |
+
resource_demands, strict_spreads = placement_groups_to_resource_demands(
|
| 897 |
+
self.load_metrics.get_pending_placement_groups()
|
| 898 |
+
)
|
| 899 |
+
resource_demands.extend(self.load_metrics.get_resource_requests())
|
| 900 |
+
if not resource_demands and not strict_spreads:
|
| 901 |
+
return frozenset(nodes_not_allowed_to_terminate)
|
| 902 |
+
|
| 903 |
+
static_node_resources: Dict[
|
| 904 |
+
NodeIP, ResourceDict
|
| 905 |
+
] = self.load_metrics.get_static_node_resources_by_ip()
|
| 906 |
+
|
| 907 |
+
head_node_resources: ResourceDict = copy.deepcopy(
|
| 908 |
+
self.available_node_types[self.config["head_node_type"]]["resources"]
|
| 909 |
+
)
|
| 910 |
+
# TODO(ameer): this is somewhat duplicated in
|
| 911 |
+
# resource_demand_scheduler.py.
|
| 912 |
+
if not head_node_resources:
|
| 913 |
+
# Legacy yaml might include {} in the resources field.
|
| 914 |
+
head_node_ip = self.provider.internal_ip(self.non_terminated_nodes.head_id)
|
| 915 |
+
head_node_resources = static_node_resources.get(head_node_ip, {})
|
| 916 |
+
|
| 917 |
+
node_total_resources: List[ResourceDict] = [head_node_resources]
|
| 918 |
+
resource_demand_vector_worker_node_ids = []
|
| 919 |
+
# Get max resources on all the non terminated nodes.
|
| 920 |
+
for node_id in sorted_node_ids:
|
| 921 |
+
tags = self.provider.node_tags(node_id)
|
| 922 |
+
if TAG_RAY_USER_NODE_TYPE in tags:
|
| 923 |
+
node_type = tags[TAG_RAY_USER_NODE_TYPE]
|
| 924 |
+
node_resources: ResourceDict = copy.deepcopy(
|
| 925 |
+
self.available_node_types[node_type]["resources"]
|
| 926 |
+
)
|
| 927 |
+
if not node_resources:
|
| 928 |
+
# Legacy yaml might include {} in the resources field.
|
| 929 |
+
node_ip = self.provider.internal_ip(node_id)
|
| 930 |
+
node_resources = static_node_resources.get(node_ip, {})
|
| 931 |
+
node_total_resources.append(node_resources)
|
| 932 |
+
resource_demand_vector_worker_node_ids.append(node_id)
|
| 933 |
+
# Since it is sorted based on last used, we "keep" nodes that are
|
| 934 |
+
# most recently used when we binpack. We assume get_bin_pack_residual
|
| 935 |
+
# is following the given order here.
|
| 936 |
+
node_remaining_resources = copy.deepcopy(node_total_resources)
|
| 937 |
+
|
| 938 |
+
for strict_spread in strict_spreads:
|
| 939 |
+
unfulfilled, updated_node_remaining_resources = get_bin_pack_residual(
|
| 940 |
+
node_remaining_resources, strict_spread, strict_spread=True
|
| 941 |
+
)
|
| 942 |
+
if unfulfilled:
|
| 943 |
+
continue
|
| 944 |
+
node_remaining_resources = updated_node_remaining_resources
|
| 945 |
+
|
| 946 |
+
_, node_remaining_resources = get_bin_pack_residual(
|
| 947 |
+
node_remaining_resources, resource_demands
|
| 948 |
+
)
|
| 949 |
+
# Remove the first entry (the head node).
|
| 950 |
+
node_total_resources.pop(0)
|
| 951 |
+
# Remove the first entry (the head node).
|
| 952 |
+
node_remaining_resources.pop(0)
|
| 953 |
+
for i, node_id in enumerate(resource_demand_vector_worker_node_ids):
|
| 954 |
+
if (
|
| 955 |
+
node_remaining_resources[i] == node_total_resources[i]
|
| 956 |
+
and node_total_resources[i]
|
| 957 |
+
):
|
| 958 |
+
# No resources of the node were needed for request_resources().
|
| 959 |
+
# node_total_resources[i] is an empty dict for legacy yamls
|
| 960 |
+
# before the node is connected.
|
| 961 |
+
pass
|
| 962 |
+
else:
|
| 963 |
+
nodes_not_allowed_to_terminate.add(node_id)
|
| 964 |
+
return frozenset(nodes_not_allowed_to_terminate)
|
| 965 |
+
|
| 966 |
+
def _keep_worker_of_node_type(
|
| 967 |
+
self, node_id: NodeID, node_type_counts: Dict[NodeType, int]
|
| 968 |
+
) -> Tuple[KeepOrTerminate, Optional[str]]:
|
| 969 |
+
"""Determines if a worker should be kept based on the min_workers
|
| 970 |
+
and max_workers constraint of the worker's node_type.
|
| 971 |
+
|
| 972 |
+
Returns KeepOrTerminate.keep when both of the following hold:
|
| 973 |
+
(a) The worker's node_type is present among the keys of the current
|
| 974 |
+
config's available_node_types dict.
|
| 975 |
+
(b) Deleting the node would violate the min_workers constraint for that
|
| 976 |
+
worker's node_type.
|
| 977 |
+
|
| 978 |
+
Returns KeepOrTerminate.terminate when both the following hold:
|
| 979 |
+
(a) The worker's node_type is not present among the keys of the current
|
| 980 |
+
config's available_node_types dict.
|
| 981 |
+
(b) Keeping the node would violate the max_workers constraint for that
|
| 982 |
+
worker's node_type.
|
| 983 |
+
|
| 984 |
+
Return KeepOrTerminate.decide_later otherwise.
|
| 985 |
+
|
| 986 |
+
Args:
|
| 987 |
+
node_type_counts(Dict[NodeType, int]): The non_terminated node
|
| 988 |
+
types counted so far.
|
| 989 |
+
Returns:
|
| 990 |
+
KeepOrTerminate: keep if the node should be kept, terminate if the
|
| 991 |
+
node should be terminated, decide_later if we are allowed
|
| 992 |
+
to terminate it, but do not have to.
|
| 993 |
+
Optional[str]: reason for termination. Not None on
|
| 994 |
+
KeepOrTerminate.terminate, None otherwise.
|
| 995 |
+
"""
|
| 996 |
+
# For type checking, assert that this object has been instantitiated.
|
| 997 |
+
assert self.provider
|
| 998 |
+
|
| 999 |
+
tags = self.provider.node_tags(node_id)
|
| 1000 |
+
if TAG_RAY_USER_NODE_TYPE in tags:
|
| 1001 |
+
node_type = tags[TAG_RAY_USER_NODE_TYPE]
|
| 1002 |
+
|
| 1003 |
+
min_workers = self.available_node_types.get(node_type, {}).get(
|
| 1004 |
+
"min_workers", 0
|
| 1005 |
+
)
|
| 1006 |
+
max_workers = self.available_node_types.get(node_type, {}).get(
|
| 1007 |
+
"max_workers", 0
|
| 1008 |
+
)
|
| 1009 |
+
if node_type not in self.available_node_types:
|
| 1010 |
+
# The node type has been deleted from the cluster config.
|
| 1011 |
+
# Allow terminating it if needed.
|
| 1012 |
+
available_node_types = list(self.available_node_types.keys())
|
| 1013 |
+
return (
|
| 1014 |
+
KeepOrTerminate.terminate,
|
| 1015 |
+
f"not in available_node_types: {available_node_types}",
|
| 1016 |
+
)
|
| 1017 |
+
new_count = node_type_counts[node_type] + 1
|
| 1018 |
+
if new_count <= min(min_workers, max_workers):
|
| 1019 |
+
return KeepOrTerminate.keep, None
|
| 1020 |
+
if new_count > max_workers:
|
| 1021 |
+
return KeepOrTerminate.terminate, "max_workers_per_type"
|
| 1022 |
+
|
| 1023 |
+
return KeepOrTerminate.decide_later, None
|
| 1024 |
+
|
| 1025 |
+
def _node_resources(self, node_id):
|
| 1026 |
+
node_type = self.provider.node_tags(node_id).get(TAG_RAY_USER_NODE_TYPE)
|
| 1027 |
+
if self.available_node_types:
|
| 1028 |
+
return self.available_node_types.get(node_type, {}).get("resources", {})
|
| 1029 |
+
else:
|
| 1030 |
+
return {}
|
| 1031 |
+
|
| 1032 |
+
def _node_labels(self, node_id):
|
| 1033 |
+
node_type = self.provider.node_tags(node_id).get(TAG_RAY_USER_NODE_TYPE)
|
| 1034 |
+
if self.available_node_types:
|
| 1035 |
+
return self.available_node_types.get(node_type, {}).get("labels", {})
|
| 1036 |
+
else:
|
| 1037 |
+
return {}
|
| 1038 |
+
|
| 1039 |
+
def reset(self, errors_fatal=False):
|
| 1040 |
+
sync_continuously = False
|
| 1041 |
+
if hasattr(self, "config"):
|
| 1042 |
+
sync_continuously = self.config.get("file_mounts_sync_continuously", False)
|
| 1043 |
+
try:
|
| 1044 |
+
new_config = self.config_reader()
|
| 1045 |
+
if new_config != getattr(self, "config", None):
|
| 1046 |
+
try:
|
| 1047 |
+
validate_config(new_config)
|
| 1048 |
+
except Exception as e:
|
| 1049 |
+
self.prom_metrics.config_validation_exceptions.inc()
|
| 1050 |
+
logger.debug(
|
| 1051 |
+
"Cluster config validation failed. The version of "
|
| 1052 |
+
"the ray CLI you launched this cluster with may "
|
| 1053 |
+
"be higher than the version of ray being run on "
|
| 1054 |
+
"the cluster. Some new features may not be "
|
| 1055 |
+
"available until you upgrade ray on your cluster.",
|
| 1056 |
+
exc_info=e,
|
| 1057 |
+
)
|
| 1058 |
+
logger.debug(
|
| 1059 |
+
f"New config after validation: {new_config},"
|
| 1060 |
+
f" of type: {type(new_config)}"
|
| 1061 |
+
)
|
| 1062 |
+
(new_runtime_hash, new_file_mounts_contents_hash) = hash_runtime_conf(
|
| 1063 |
+
new_config["file_mounts"],
|
| 1064 |
+
new_config["cluster_synced_files"],
|
| 1065 |
+
[
|
| 1066 |
+
new_config["worker_setup_commands"],
|
| 1067 |
+
new_config["worker_start_ray_commands"],
|
| 1068 |
+
],
|
| 1069 |
+
generate_file_mounts_contents_hash=sync_continuously,
|
| 1070 |
+
)
|
| 1071 |
+
self.config = new_config
|
| 1072 |
+
self.runtime_hash = new_runtime_hash
|
| 1073 |
+
self.file_mounts_contents_hash = new_file_mounts_contents_hash
|
| 1074 |
+
if not self.provider:
|
| 1075 |
+
self.provider = _get_node_provider(
|
| 1076 |
+
self.config["provider"], self.config["cluster_name"]
|
| 1077 |
+
)
|
| 1078 |
+
|
| 1079 |
+
# If using the LocalNodeProvider, make sure the head node is marked
|
| 1080 |
+
# non-terminated.
|
| 1081 |
+
if isinstance(self.provider, LocalNodeProvider):
|
| 1082 |
+
record_local_head_state_if_needed(self.provider)
|
| 1083 |
+
|
| 1084 |
+
self.available_node_types = self.config["available_node_types"]
|
| 1085 |
+
upscaling_speed = self.config.get("upscaling_speed")
|
| 1086 |
+
aggressive = self.config.get("autoscaling_mode") == "aggressive"
|
| 1087 |
+
target_utilization_fraction = self.config.get("target_utilization_fraction")
|
| 1088 |
+
if upscaling_speed:
|
| 1089 |
+
upscaling_speed = float(upscaling_speed)
|
| 1090 |
+
# TODO(ameer): consider adding (if users ask) an option of
|
| 1091 |
+
# initial_upscaling_num_workers.
|
| 1092 |
+
elif aggressive:
|
| 1093 |
+
upscaling_speed = 99999
|
| 1094 |
+
logger.warning(
|
| 1095 |
+
"Legacy aggressive autoscaling mode "
|
| 1096 |
+
"detected. Replacing it by setting upscaling_speed to "
|
| 1097 |
+
"99999."
|
| 1098 |
+
)
|
| 1099 |
+
elif target_utilization_fraction:
|
| 1100 |
+
upscaling_speed = 1 / max(target_utilization_fraction, 0.001) - 1
|
| 1101 |
+
logger.warning(
|
| 1102 |
+
"Legacy target_utilization_fraction config "
|
| 1103 |
+
"detected. Replacing it by setting upscaling_speed to "
|
| 1104 |
+
+ "1 / target_utilization_fraction - 1."
|
| 1105 |
+
)
|
| 1106 |
+
else:
|
| 1107 |
+
upscaling_speed = 1.0
|
| 1108 |
+
if self.resource_demand_scheduler:
|
| 1109 |
+
# The node types are autofilled internally for legacy yamls,
|
| 1110 |
+
# overwriting the class will remove the inferred node resources
|
| 1111 |
+
# for legacy yamls.
|
| 1112 |
+
self.resource_demand_scheduler.reset_config(
|
| 1113 |
+
self.provider,
|
| 1114 |
+
self.available_node_types,
|
| 1115 |
+
self.config["max_workers"],
|
| 1116 |
+
self.config["head_node_type"],
|
| 1117 |
+
upscaling_speed,
|
| 1118 |
+
)
|
| 1119 |
+
else:
|
| 1120 |
+
self.resource_demand_scheduler = ResourceDemandScheduler(
|
| 1121 |
+
self.provider,
|
| 1122 |
+
self.available_node_types,
|
| 1123 |
+
self.config["max_workers"],
|
| 1124 |
+
self.config["head_node_type"],
|
| 1125 |
+
upscaling_speed,
|
| 1126 |
+
)
|
| 1127 |
+
|
| 1128 |
+
except Exception as e:
|
| 1129 |
+
self.prom_metrics.reset_exceptions.inc()
|
| 1130 |
+
if errors_fatal:
|
| 1131 |
+
raise e
|
| 1132 |
+
else:
|
| 1133 |
+
logger.exception("StandardAutoscaler: Error parsing config.")
|
| 1134 |
+
|
| 1135 |
+
def launch_config_ok(self, node_id):
|
| 1136 |
+
if self.disable_launch_config_check:
|
| 1137 |
+
return True
|
| 1138 |
+
node_tags = self.provider.node_tags(node_id)
|
| 1139 |
+
tag_launch_conf = node_tags.get(TAG_RAY_LAUNCH_CONFIG)
|
| 1140 |
+
node_type = node_tags.get(TAG_RAY_USER_NODE_TYPE)
|
| 1141 |
+
if node_type not in self.available_node_types:
|
| 1142 |
+
# The node type has been deleted from the cluster config.
|
| 1143 |
+
# Don't keep the node.
|
| 1144 |
+
return False
|
| 1145 |
+
|
| 1146 |
+
# The `worker_nodes` field is deprecated in favor of per-node-type
|
| 1147 |
+
# node_configs. We allow it for backwards-compatibility.
|
| 1148 |
+
launch_config = copy.deepcopy(self.config.get("worker_nodes", {}))
|
| 1149 |
+
if node_type:
|
| 1150 |
+
launch_config.update(
|
| 1151 |
+
self.config["available_node_types"][node_type]["node_config"]
|
| 1152 |
+
)
|
| 1153 |
+
calculated_launch_hash = hash_launch_conf(launch_config, self.config["auth"])
|
| 1154 |
+
|
| 1155 |
+
if calculated_launch_hash != tag_launch_conf:
|
| 1156 |
+
return False
|
| 1157 |
+
return True
|
| 1158 |
+
|
| 1159 |
+
def files_up_to_date(self, node_id):
|
| 1160 |
+
node_tags = self.provider.node_tags(node_id)
|
| 1161 |
+
applied_config_hash = node_tags.get(TAG_RAY_RUNTIME_CONFIG)
|
| 1162 |
+
applied_file_mounts_contents_hash = node_tags.get(TAG_RAY_FILE_MOUNTS_CONTENTS)
|
| 1163 |
+
if applied_config_hash != self.runtime_hash or (
|
| 1164 |
+
self.file_mounts_contents_hash is not None
|
| 1165 |
+
and self.file_mounts_contents_hash != applied_file_mounts_contents_hash
|
| 1166 |
+
):
|
| 1167 |
+
logger.info(
|
| 1168 |
+
"StandardAutoscaler: "
|
| 1169 |
+
"{}: Runtime state is ({},{}), want ({},{})".format(
|
| 1170 |
+
node_id,
|
| 1171 |
+
applied_config_hash,
|
| 1172 |
+
applied_file_mounts_contents_hash,
|
| 1173 |
+
self.runtime_hash,
|
| 1174 |
+
self.file_mounts_contents_hash,
|
| 1175 |
+
)
|
| 1176 |
+
)
|
| 1177 |
+
return False
|
| 1178 |
+
return True
|
| 1179 |
+
|
| 1180 |
+
def heartbeat_on_time(self, node_id: NodeID, now: float) -> bool:
|
| 1181 |
+
"""Determine whether we've received a heartbeat from a node within the
|
| 1182 |
+
last AUTOSCALER_HEARTBEAT_TIMEOUT_S seconds.
|
| 1183 |
+
"""
|
| 1184 |
+
# For type checking, assert that this object has been instantitiated.
|
| 1185 |
+
assert self.provider
|
| 1186 |
+
|
| 1187 |
+
key = self.provider.internal_ip(node_id)
|
| 1188 |
+
|
| 1189 |
+
if key in self.load_metrics.last_heartbeat_time_by_ip:
|
| 1190 |
+
last_heartbeat_time = self.load_metrics.last_heartbeat_time_by_ip[key]
|
| 1191 |
+
delta = now - last_heartbeat_time
|
| 1192 |
+
if delta < AUTOSCALER_HEARTBEAT_TIMEOUT_S:
|
| 1193 |
+
return True
|
| 1194 |
+
return False
|
| 1195 |
+
|
| 1196 |
+
def terminate_unhealthy_nodes(self, now: float):
|
| 1197 |
+
"""Terminated nodes for which we haven't received a heartbeat on time.
|
| 1198 |
+
These nodes are subsequently terminated.
|
| 1199 |
+
"""
|
| 1200 |
+
# For type checking, assert that these objects have been instantitiated.
|
| 1201 |
+
assert self.provider
|
| 1202 |
+
assert self.non_terminated_nodes
|
| 1203 |
+
|
| 1204 |
+
for node_id in self.non_terminated_nodes.worker_ids:
|
| 1205 |
+
node_status = self.provider.node_tags(node_id)[TAG_RAY_NODE_STATUS]
|
| 1206 |
+
# We're not responsible for taking down
|
| 1207 |
+
# nodes with pending or failed status:
|
| 1208 |
+
if not node_status == STATUS_UP_TO_DATE:
|
| 1209 |
+
continue
|
| 1210 |
+
# This node is up-to-date. If it hasn't had the chance to produce
|
| 1211 |
+
# a heartbeat, fake the heartbeat now (see logic for completed node
|
| 1212 |
+
# updaters).
|
| 1213 |
+
ip = self.provider.internal_ip(node_id)
|
| 1214 |
+
if ip not in self.load_metrics.last_heartbeat_time_by_ip:
|
| 1215 |
+
self.load_metrics.mark_active(ip)
|
| 1216 |
+
# Heartbeat indicates node is healthy:
|
| 1217 |
+
if self.heartbeat_on_time(node_id, now):
|
| 1218 |
+
continue
|
| 1219 |
+
self.schedule_node_termination(
|
| 1220 |
+
node_id, "lost contact with raylet", logger.warning
|
| 1221 |
+
)
|
| 1222 |
+
self.terminate_scheduled_nodes()
|
| 1223 |
+
|
| 1224 |
+
def attempt_to_recover_unhealthy_nodes(self, now):
|
| 1225 |
+
for node_id in self.non_terminated_nodes.worker_ids:
|
| 1226 |
+
self.recover_if_needed(node_id, now)
|
| 1227 |
+
|
| 1228 |
+
def recover_if_needed(self, node_id, now):
|
| 1229 |
+
if not self.can_update(node_id):
|
| 1230 |
+
return
|
| 1231 |
+
if self.heartbeat_on_time(node_id, now):
|
| 1232 |
+
return
|
| 1233 |
+
|
| 1234 |
+
logger.warning(
|
| 1235 |
+
"StandardAutoscaler: "
|
| 1236 |
+
"{}: No recent heartbeat, "
|
| 1237 |
+
"restarting Ray to recover...".format(node_id)
|
| 1238 |
+
)
|
| 1239 |
+
self.event_summarizer.add(
|
| 1240 |
+
"Restarting {} nodes of type "
|
| 1241 |
+
+ self._get_node_type(node_id)
|
| 1242 |
+
+ " (lost contact with raylet).",
|
| 1243 |
+
quantity=1,
|
| 1244 |
+
aggregate=operator.add,
|
| 1245 |
+
)
|
| 1246 |
+
head_node_ip = self.provider.internal_ip(self.non_terminated_nodes.head_id)
|
| 1247 |
+
updater = NodeUpdaterThread(
|
| 1248 |
+
node_id=node_id,
|
| 1249 |
+
provider_config=self.config["provider"],
|
| 1250 |
+
provider=self.provider,
|
| 1251 |
+
auth_config=self.config["auth"],
|
| 1252 |
+
cluster_name=self.config["cluster_name"],
|
| 1253 |
+
file_mounts={},
|
| 1254 |
+
initialization_commands=[],
|
| 1255 |
+
setup_commands=[],
|
| 1256 |
+
ray_start_commands=with_head_node_ip(
|
| 1257 |
+
self.config["worker_start_ray_commands"], head_node_ip
|
| 1258 |
+
),
|
| 1259 |
+
runtime_hash=self.runtime_hash,
|
| 1260 |
+
file_mounts_contents_hash=self.file_mounts_contents_hash,
|
| 1261 |
+
process_runner=self.process_runner,
|
| 1262 |
+
use_internal_ip=True,
|
| 1263 |
+
is_head_node=False,
|
| 1264 |
+
docker_config=self._get_node_specific_docker_config(node_id),
|
| 1265 |
+
node_resources=self._node_resources(node_id),
|
| 1266 |
+
node_labels=self._node_labels(node_id),
|
| 1267 |
+
for_recovery=True,
|
| 1268 |
+
)
|
| 1269 |
+
updater.start()
|
| 1270 |
+
self.updaters[node_id] = updater
|
| 1271 |
+
|
| 1272 |
+
def _get_node_type(self, node_id: str) -> str:
|
| 1273 |
+
# For type checking, assert that this object has been instantitiated.
|
| 1274 |
+
assert self.provider
|
| 1275 |
+
|
| 1276 |
+
node_tags = self.provider.node_tags(node_id)
|
| 1277 |
+
if TAG_RAY_USER_NODE_TYPE in node_tags:
|
| 1278 |
+
return node_tags[TAG_RAY_USER_NODE_TYPE]
|
| 1279 |
+
else:
|
| 1280 |
+
return "unknown_node_type"
|
| 1281 |
+
|
| 1282 |
+
def _get_node_type_specific_fields(self, node_id: str, fields_key: str) -> Any:
|
| 1283 |
+
# For type checking, assert that this object has been instantitiated.
|
| 1284 |
+
assert self.provider
|
| 1285 |
+
|
| 1286 |
+
fields = self.config[fields_key]
|
| 1287 |
+
node_tags = self.provider.node_tags(node_id)
|
| 1288 |
+
if TAG_RAY_USER_NODE_TYPE in node_tags:
|
| 1289 |
+
node_type = node_tags[TAG_RAY_USER_NODE_TYPE]
|
| 1290 |
+
if node_type not in self.available_node_types:
|
| 1291 |
+
raise ValueError(f"Unknown node type tag: {node_type}.")
|
| 1292 |
+
node_specific_config = self.available_node_types[node_type]
|
| 1293 |
+
if fields_key in node_specific_config:
|
| 1294 |
+
fields = node_specific_config[fields_key]
|
| 1295 |
+
return fields
|
| 1296 |
+
|
| 1297 |
+
def _get_node_specific_docker_config(self, node_id):
|
| 1298 |
+
if "docker" not in self.config:
|
| 1299 |
+
return {}
|
| 1300 |
+
docker_config = copy.deepcopy(self.config.get("docker", {}))
|
| 1301 |
+
node_specific_docker = self._get_node_type_specific_fields(node_id, "docker")
|
| 1302 |
+
docker_config.update(node_specific_docker)
|
| 1303 |
+
return docker_config
|
| 1304 |
+
|
| 1305 |
+
def should_update(self, node_id):
|
| 1306 |
+
if not self.can_update(node_id):
|
| 1307 |
+
return UpdateInstructions(None, None, None, None) # no update
|
| 1308 |
+
|
| 1309 |
+
status = self.provider.node_tags(node_id).get(TAG_RAY_NODE_STATUS)
|
| 1310 |
+
if status == STATUS_UP_TO_DATE and self.files_up_to_date(node_id):
|
| 1311 |
+
return UpdateInstructions(None, None, None, None) # no update
|
| 1312 |
+
|
| 1313 |
+
successful_updated = self.num_successful_updates.get(node_id, 0) > 0
|
| 1314 |
+
if successful_updated and self.config.get("restart_only", False):
|
| 1315 |
+
setup_commands = []
|
| 1316 |
+
ray_start_commands = self.config["worker_start_ray_commands"]
|
| 1317 |
+
elif successful_updated and self.config.get("no_restart", False):
|
| 1318 |
+
setup_commands = self._get_node_type_specific_fields(
|
| 1319 |
+
node_id, "worker_setup_commands"
|
| 1320 |
+
)
|
| 1321 |
+
ray_start_commands = []
|
| 1322 |
+
else:
|
| 1323 |
+
setup_commands = self._get_node_type_specific_fields(
|
| 1324 |
+
node_id, "worker_setup_commands"
|
| 1325 |
+
)
|
| 1326 |
+
ray_start_commands = self.config["worker_start_ray_commands"]
|
| 1327 |
+
|
| 1328 |
+
docker_config = self._get_node_specific_docker_config(node_id)
|
| 1329 |
+
return UpdateInstructions(
|
| 1330 |
+
node_id=node_id,
|
| 1331 |
+
setup_commands=setup_commands,
|
| 1332 |
+
ray_start_commands=ray_start_commands,
|
| 1333 |
+
docker_config=docker_config,
|
| 1334 |
+
)
|
| 1335 |
+
|
| 1336 |
+
def spawn_updater(
|
| 1337 |
+
self,
|
| 1338 |
+
node_id,
|
| 1339 |
+
setup_commands,
|
| 1340 |
+
ray_start_commands,
|
| 1341 |
+
node_resources,
|
| 1342 |
+
node_labels,
|
| 1343 |
+
docker_config,
|
| 1344 |
+
):
|
| 1345 |
+
logger.info(
|
| 1346 |
+
f"Creating new (spawn_updater) updater thread for node" f" {node_id}."
|
| 1347 |
+
)
|
| 1348 |
+
ip = self.provider.internal_ip(node_id)
|
| 1349 |
+
node_type = self._get_node_type(node_id)
|
| 1350 |
+
self.node_tracker.track(node_id, ip, node_type)
|
| 1351 |
+
head_node_ip = self.provider.internal_ip(self.non_terminated_nodes.head_id)
|
| 1352 |
+
updater = NodeUpdaterThread(
|
| 1353 |
+
node_id=node_id,
|
| 1354 |
+
provider_config=self.config["provider"],
|
| 1355 |
+
provider=self.provider,
|
| 1356 |
+
auth_config=self.config["auth"],
|
| 1357 |
+
cluster_name=self.config["cluster_name"],
|
| 1358 |
+
file_mounts=self.config["file_mounts"],
|
| 1359 |
+
initialization_commands=with_head_node_ip(
|
| 1360 |
+
self._get_node_type_specific_fields(node_id, "initialization_commands"),
|
| 1361 |
+
head_node_ip,
|
| 1362 |
+
),
|
| 1363 |
+
setup_commands=with_head_node_ip(setup_commands, head_node_ip),
|
| 1364 |
+
ray_start_commands=with_head_node_ip(ray_start_commands, head_node_ip),
|
| 1365 |
+
runtime_hash=self.runtime_hash,
|
| 1366 |
+
file_mounts_contents_hash=self.file_mounts_contents_hash,
|
| 1367 |
+
is_head_node=False,
|
| 1368 |
+
cluster_synced_files=self.config["cluster_synced_files"],
|
| 1369 |
+
rsync_options={
|
| 1370 |
+
"rsync_exclude": self.config.get("rsync_exclude"),
|
| 1371 |
+
"rsync_filter": self.config.get("rsync_filter"),
|
| 1372 |
+
},
|
| 1373 |
+
process_runner=self.process_runner,
|
| 1374 |
+
use_internal_ip=True,
|
| 1375 |
+
docker_config=docker_config,
|
| 1376 |
+
node_resources=node_resources,
|
| 1377 |
+
node_labels=node_labels,
|
| 1378 |
+
)
|
| 1379 |
+
updater.start()
|
| 1380 |
+
self.updaters[node_id] = updater
|
| 1381 |
+
|
| 1382 |
+
def can_update(self, node_id):
|
| 1383 |
+
if self.disable_node_updaters:
|
| 1384 |
+
return False
|
| 1385 |
+
if node_id in self.updaters:
|
| 1386 |
+
return False
|
| 1387 |
+
if not self.launch_config_ok(node_id):
|
| 1388 |
+
return False
|
| 1389 |
+
if self.num_failed_updates.get(node_id, 0) > 0: # TODO(ekl) retry?
|
| 1390 |
+
return False
|
| 1391 |
+
logger.debug(
|
| 1392 |
+
f"{node_id} is not being updated and "
|
| 1393 |
+
"passes config check (can_update=True)."
|
| 1394 |
+
)
|
| 1395 |
+
return True
|
| 1396 |
+
|
| 1397 |
+
def launch_new_node(self, count: int, node_type: str) -> None:
|
| 1398 |
+
logger.info("StandardAutoscaler: Queue {} new nodes for launch".format(count))
|
| 1399 |
+
self.pending_launches.inc(node_type, count)
|
| 1400 |
+
config = copy.deepcopy(self.config)
|
| 1401 |
+
if self.foreground_node_launch:
|
| 1402 |
+
assert self.foreground_node_launcher is not None
|
| 1403 |
+
# Launch in the main thread and block.
|
| 1404 |
+
self.foreground_node_launcher.launch_node(config, count, node_type)
|
| 1405 |
+
else:
|
| 1406 |
+
assert self.launch_queue is not None
|
| 1407 |
+
# Split into individual launch requests of the max batch size.
|
| 1408 |
+
while count > 0:
|
| 1409 |
+
# Enqueue launch data for the background NodeUpdater threads.
|
| 1410 |
+
self.launch_queue.put(
|
| 1411 |
+
(config, min(count, self.max_launch_batch), node_type)
|
| 1412 |
+
)
|
| 1413 |
+
count -= self.max_launch_batch
|
| 1414 |
+
|
| 1415 |
+
def kill_workers(self):
|
| 1416 |
+
logger.error("StandardAutoscaler: kill_workers triggered")
|
| 1417 |
+
nodes = self.workers()
|
| 1418 |
+
if nodes:
|
| 1419 |
+
self.provider.terminate_nodes(nodes)
|
| 1420 |
+
for node in nodes:
|
| 1421 |
+
self.node_tracker.untrack(node)
|
| 1422 |
+
self.prom_metrics.stopped_nodes.inc()
|
| 1423 |
+
logger.error("StandardAutoscaler: terminated {} node(s)".format(len(nodes)))
|
| 1424 |
+
|
| 1425 |
+
def summary(self) -> Optional[AutoscalerSummary]:
|
| 1426 |
+
"""Summarizes the active, pending, and failed node launches.
|
| 1427 |
+
|
| 1428 |
+
An active node is a node whose raylet is actively reporting heartbeats.
|
| 1429 |
+
A pending node is non-active node whose node tag is uninitialized,
|
| 1430 |
+
waiting for ssh, syncing files, or setting up.
|
| 1431 |
+
If a node is not pending or active, it is failed.
|
| 1432 |
+
|
| 1433 |
+
Returns:
|
| 1434 |
+
AutoscalerSummary: The summary.
|
| 1435 |
+
"""
|
| 1436 |
+
# For type checking, assert that this object has been instantitiated.
|
| 1437 |
+
assert self.provider
|
| 1438 |
+
|
| 1439 |
+
if not self.non_terminated_nodes:
|
| 1440 |
+
return None
|
| 1441 |
+
active_nodes: Dict[NodeType, int] = Counter()
|
| 1442 |
+
pending_nodes = []
|
| 1443 |
+
failed_nodes = []
|
| 1444 |
+
non_failed = set()
|
| 1445 |
+
|
| 1446 |
+
node_type_mapping = {}
|
| 1447 |
+
now = time.time()
|
| 1448 |
+
for node_id in self.non_terminated_nodes.all_node_ids:
|
| 1449 |
+
ip = self.provider.internal_ip(node_id)
|
| 1450 |
+
node_tags = self.provider.node_tags(node_id)
|
| 1451 |
+
|
| 1452 |
+
if not all(
|
| 1453 |
+
tag in node_tags
|
| 1454 |
+
for tag in (
|
| 1455 |
+
TAG_RAY_NODE_KIND,
|
| 1456 |
+
TAG_RAY_USER_NODE_TYPE,
|
| 1457 |
+
TAG_RAY_NODE_STATUS,
|
| 1458 |
+
)
|
| 1459 |
+
):
|
| 1460 |
+
# In some node providers, creation of a node and tags is not
|
| 1461 |
+
# atomic, so just skip it.
|
| 1462 |
+
continue
|
| 1463 |
+
|
| 1464 |
+
if node_tags[TAG_RAY_NODE_KIND] == NODE_KIND_UNMANAGED:
|
| 1465 |
+
continue
|
| 1466 |
+
node_type = node_tags[TAG_RAY_USER_NODE_TYPE]
|
| 1467 |
+
|
| 1468 |
+
node_type_mapping[ip] = node_type
|
| 1469 |
+
|
| 1470 |
+
is_active = self.heartbeat_on_time(node_id, now)
|
| 1471 |
+
if is_active:
|
| 1472 |
+
active_nodes[node_type] += 1
|
| 1473 |
+
non_failed.add(node_id)
|
| 1474 |
+
else:
|
| 1475 |
+
status = node_tags[TAG_RAY_NODE_STATUS]
|
| 1476 |
+
completed_states = [STATUS_UP_TO_DATE, STATUS_UPDATE_FAILED]
|
| 1477 |
+
is_pending = status not in completed_states
|
| 1478 |
+
if is_pending:
|
| 1479 |
+
pending_nodes.append((node_id, ip, node_type, status))
|
| 1480 |
+
non_failed.add(node_id)
|
| 1481 |
+
|
| 1482 |
+
failed_nodes = self.node_tracker.get_all_failed_node_info(non_failed)
|
| 1483 |
+
|
| 1484 |
+
# The concurrent counter leaves some 0 counts in, so we need to
|
| 1485 |
+
# manually filter those out.
|
| 1486 |
+
pending_launches = {}
|
| 1487 |
+
for node_type, count in self.pending_launches.breakdown().items():
|
| 1488 |
+
if count:
|
| 1489 |
+
pending_launches[node_type] = count
|
| 1490 |
+
|
| 1491 |
+
pending_resources = {}
|
| 1492 |
+
for node_resources in self.resource_demand_scheduler.calculate_node_resources(
|
| 1493 |
+
nodes=[node_id for node_id, _, _, _ in pending_nodes],
|
| 1494 |
+
pending_nodes=pending_launches,
|
| 1495 |
+
# We don't fill this field out because we're intentionally only
|
| 1496 |
+
# passing pending nodes (which aren't tracked by load metrics
|
| 1497 |
+
# anyways).
|
| 1498 |
+
unused_resources_by_ip={},
|
| 1499 |
+
)[0]:
|
| 1500 |
+
for key, value in node_resources.items():
|
| 1501 |
+
pending_resources[key] = value + pending_resources.get(key, 0)
|
| 1502 |
+
|
| 1503 |
+
return AutoscalerSummary(
|
| 1504 |
+
# Convert active_nodes from counter to dict for later serialization
|
| 1505 |
+
active_nodes=dict(active_nodes),
|
| 1506 |
+
idle_nodes=None,
|
| 1507 |
+
pending_nodes=[
|
| 1508 |
+
(ip, node_type, status) for _, ip, node_type, status in pending_nodes
|
| 1509 |
+
],
|
| 1510 |
+
pending_launches=pending_launches,
|
| 1511 |
+
failed_nodes=failed_nodes,
|
| 1512 |
+
node_availability_summary=self.node_provider_availability_tracker.summary(),
|
| 1513 |
+
pending_resources=pending_resources,
|
| 1514 |
+
node_type_mapping=node_type_mapping,
|
| 1515 |
+
legacy=True,
|
| 1516 |
+
)
|
| 1517 |
+
|
| 1518 |
+
def info_string(self):
|
| 1519 |
+
lm_summary = self.load_metrics.summary()
|
| 1520 |
+
autoscaler_summary = self.summary()
|
| 1521 |
+
assert autoscaler_summary
|
| 1522 |
+
return "\n" + format_info_string(lm_summary, autoscaler_summary)
|
lib/python3.12/site-packages/ray/autoscaler/_private/aws/__init__.py
ADDED
|
File without changes
|