Skip to content

Commit 898c411

Browse files
adamrtalbotclaude
andcommitted
Fix Azure Low Priority VM deprecation for Batch Managed accounts (#6258)
Azure Low Priority VMs are deprecated in Batch Managed pool allocation mode but still supported in User Subscription mode. This adds validation to: - Block low priority VMs for Batch Managed accounts with clear error message - Allow low priority VMs for User Subscription accounts - Gracefully handle unknown allocation modes with warnings Adds subscriptionId to config to enable retrieval of this value. If not supplied Nextflow will raise a warning and continue. This does open us up to being able to do more with Azure Batch because we have access to the management API. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Signed-off-by: adamrtalbot <[email protected]>
1 parent 6328613 commit 898c411

File tree

6 files changed

+290
-0
lines changed

6 files changed

+290
-0
lines changed

plugins/nf-azure/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ dependencies {
4646
api('com.azure:azure-identity:1.15.1') {
4747
exclude group: 'org.slf4j', module: 'slf4j-api'
4848
}
49+
api('com.azure.resourcemanager:azure-resourcemanager-batch:1.1.0-beta.4') {
50+
exclude group: 'org.slf4j', module: 'slf4j-api'
51+
}
4952

5053
// address security vulnerabilities
5154
runtimeOnly 'io.netty:netty-handler:4.1.118.Final'

plugins/nf-azure/src/main/nextflow/cloud/azure/batch/AzBatchExecutor.groovy

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,38 @@ class AzBatchExecutor extends Executor implements ExtensionPoint {
8686
}
8787
}
8888

89+
protected void validateLowPriorityVMs() {
90+
// Check if any pool has lowPriority enabled
91+
def lowPriorityPools = config.batch().pools.findAll { poolName, poolOpts ->
92+
poolOpts.lowPriority
93+
}
94+
95+
if( lowPriorityPools ) {
96+
def poolNames = lowPriorityPools.keySet().join(', ')
97+
98+
// Get the pool allocation mode to determine if low priority VMs are allowed
99+
def poolAllocationMode = batchService.getPoolAllocationMode()
100+
log.debug "[AZURE BATCH] Pool allocation mode determined as: ${poolAllocationMode}"
101+
102+
if( poolAllocationMode == 'BATCH_SERVICE' || poolAllocationMode == 'BatchService' ) {
103+
throw new AbortOperationException("Azure Low Priority VMs are deprecated and no longer supported for Batch Managed pool allocation mode. " +
104+
"Please update your configuration to use standard VMs instead, or migrate to User Subscription pool allocation mode. " +
105+
"Affected pools: ${poolNames}. " +
106+
"Remove 'lowPriority: true' from your pool configuration or set 'lowPriority: false'.")
107+
} else if( poolAllocationMode == 'USER_SUBSCRIPTION' || poolAllocationMode == 'UserSubscription' ) {
108+
// Low Priority VMs are still supported in User Subscription mode, proceed without warning
109+
log.debug "[AZURE BATCH] User Subscription mode detected, allowing low priority VMs in pools: ${poolNames}"
110+
} else {
111+
// If we can't determine the pool allocation mode, show a warning but allow execution
112+
log.warn "[AZURE BATCH] Unable to determine pool allocation mode (got: ${poolAllocationMode}). " +
113+
"Low Priority VMs are configured in pools: ${poolNames}. " +
114+
"Please note that Low Priority VMs are deprecated in Batch Managed accounts. " +
115+
"If you're using a Batch Managed account, please update your configuration to use standard VMs. " +
116+
"To enable automatic detection, set azure.batch.subscriptionId in your config or AZURE_SUBSCRIPTION_ID environment variable."
117+
}
118+
}
119+
}
120+
89121
protected void uploadBinDir() {
90122
/*
91123
* upload local binaries
@@ -120,6 +152,7 @@ class AzBatchExecutor extends Executor implements ExtensionPoint {
120152
initBatchService()
121153
validateWorkDir()
122154
validatePathDir()
155+
validateLowPriorityVMs()
123156
uploadBinDir()
124157
}
125158

plugins/nf-azure/src/main/nextflow/cloud/azure/batch/AzBatchService.groovy

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,102 @@ class AzBatchService implements Closeable {
388388
return builder.buildClient()
389389
}
390390

391+
/**
392+
* Determines the pool allocation mode of the Azure Batch account
393+
* @return The pool allocation mode ('BatchService' or 'UserSubscription'), or null if it cannot be determined
394+
*/
395+
@Memoized
396+
protected String getPoolAllocationMode() {
397+
try {
398+
// Get batch account name from endpoint
399+
final accountName = extractAccountName(config.batch().endpoint)
400+
if (!accountName) {
401+
log.debug "[AZURE BATCH] Cannot extract account name from endpoint"
402+
return null
403+
}
404+
405+
// Get subscription ID
406+
final subscriptionId = config.batch().subscriptionId
407+
if (!subscriptionId) {
408+
log.debug "[AZURE BATCH] No subscription ID configured. Set azure.batch.subscriptionId or AZURE_SUBSCRIPTION_ID"
409+
return null
410+
}
411+
412+
// Get Azure credentials
413+
final credential = getAzureCredential()
414+
if (!credential) {
415+
log.debug "[AZURE BATCH] No valid credentials for Azure Resource Manager"
416+
return null
417+
}
418+
419+
// Create BatchManager with proper configuration
420+
final batchManager = createBatchManager(credential, subscriptionId)
421+
422+
// Find the batch account
423+
return findBatchAccountPoolMode(batchManager, accountName)
424+
425+
} catch (Exception e) {
426+
log.warn "[AZURE BATCH] Failed to determine pool allocation mode: ${e.message}", e
427+
return null
428+
}
429+
}
430+
431+
/**
432+
* Extract account name from batch endpoint URL
433+
*/
434+
private String extractAccountName(String endpoint) {
435+
if (!endpoint) return null
436+
// Format: https://accountname.region.batch.azure.com
437+
return endpoint.split('\\.')[0].replace('https://', '')
438+
}
439+
440+
/**
441+
* Get Azure credentials based on configuration
442+
*/
443+
private TokenCredential getAzureCredential() {
444+
if (config.managedIdentity().isConfigured()) {
445+
return createBatchCredentialsWithManagedIdentity()
446+
} else if (config.activeDirectory().isConfigured()) {
447+
return createBatchCredentialsWithServicePrincipal()
448+
}
449+
return null
450+
}
451+
452+
/**
453+
* Create and configure BatchManager
454+
*/
455+
private com.azure.resourcemanager.batch.BatchManager createBatchManager(TokenCredential credential, String subscriptionId) {
456+
// AzureProfile constructor: (tenantId, subscriptionId, environment)
457+
final profile = new com.azure.core.management.profile.AzureProfile(
458+
null, // tenantId - null to use default
459+
subscriptionId,
460+
com.azure.core.management.AzureEnvironment.AZURE
461+
)
462+
463+
// Use configure().authenticate() pattern to ensure proper initialization
464+
return com.azure.resourcemanager.batch.BatchManager
465+
.configure()
466+
.authenticate(credential, profile)
467+
}
468+
469+
/**
470+
* Find batch account and return its pool allocation mode
471+
*/
472+
private String findBatchAccountPoolMode(com.azure.resourcemanager.batch.BatchManager batchManager, String accountName) {
473+
log.debug "[AZURE BATCH] Searching for account '${accountName}'"
474+
475+
for (batchAccount in batchManager.batchAccounts().list()) {
476+
if (batchAccount.name() == accountName) {
477+
final poolMode = batchAccount.poolAllocationMode()
478+
log.debug "[AZURE BATCH] Found account with pool allocation mode: ${poolMode}"
479+
return poolMode?.toString()
480+
}
481+
}
482+
483+
log.debug "[AZURE BATCH] Account '${accountName}' not found in subscription"
484+
return null
485+
}
486+
391487
AzTaskKey submitTask(TaskRun task) {
392488
final poolId = getOrCreatePool(task)
393489
final jobId = getOrCreateJob(poolId, task)

plugins/nf-azure/src/main/nextflow/cloud/azure/config/AzBatchOpts.groovy

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class AzBatchOpts implements CloudTransferOptions {
4848
String accountKey
4949
String endpoint
5050
String location
51+
String subscriptionId
5152
Boolean autoPoolMode
5253
Boolean allowPoolCreation
5354
Boolean terminateJobsOnCompletion
@@ -67,6 +68,7 @@ class AzBatchOpts implements CloudTransferOptions {
6768
accountKey = config.accountKey ?: sysEnv.get('AZURE_BATCH_ACCOUNT_KEY')
6869
endpoint = config.endpoint
6970
location = config.location
71+
subscriptionId = config.subscriptionId ?: sysEnv.get('AZURE_SUBSCRIPTION_ID')
7072
autoPoolMode = config.autoPoolMode
7173
allowPoolCreation = config.allowPoolCreation
7274
terminateJobsOnCompletion = config.terminateJobsOnCompletion != Boolean.FALSE
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package nextflow.cloud.azure.batch
2+
3+
import nextflow.Session
4+
import nextflow.cloud.azure.config.AzConfig
5+
import nextflow.cloud.azure.config.AzBatchOpts
6+
import nextflow.cloud.azure.config.AzPoolOpts
7+
import nextflow.exception.AbortOperationException
8+
import spock.lang.Specification
9+
10+
/**
11+
* Test for AzBatchExecutor validation logic
12+
*/
13+
class AzBatchExecutorTest extends Specification {
14+
15+
def 'should validate low priority VMs for BatchService allocation mode'() {
16+
given:
17+
def CONFIG = [
18+
batch: [
19+
endpoint: 'https://testaccount.eastus.batch.azure.com',
20+
pools: [
21+
'pool1': [vmType: 'Standard_D2_v2', lowPriority: true],
22+
'pool2': [vmType: 'Standard_D2_v2', lowPriority: false]
23+
]
24+
]
25+
]
26+
27+
and:
28+
def config = new AzConfig(CONFIG)
29+
def batchService = Mock(AzBatchService) {
30+
getPoolAllocationMode() >> 'BATCH_SERVICE'
31+
}
32+
33+
and:
34+
def executor = new AzBatchExecutor()
35+
executor.config = config
36+
executor.batchService = batchService
37+
38+
when:
39+
executor.validateLowPriorityVMs()
40+
41+
then:
42+
def e = thrown(AbortOperationException)
43+
e.message.contains('Azure Low Priority VMs are deprecated and no longer supported for Batch Managed pool allocation mode')
44+
e.message.contains('pool1')
45+
}
46+
47+
def 'should allow low priority VMs for UserSubscription allocation mode'() {
48+
given:
49+
def CONFIG = [
50+
batch: [
51+
endpoint: 'https://testaccount.eastus.batch.azure.com',
52+
pools: [
53+
'pool1': [vmType: 'Standard_D2_v2', lowPriority: true]
54+
]
55+
]
56+
]
57+
58+
and:
59+
def config = new AzConfig(CONFIG)
60+
def batchService = Mock(AzBatchService) {
61+
getPoolAllocationMode() >> 'USER_SUBSCRIPTION'
62+
}
63+
64+
and:
65+
def executor = new AzBatchExecutor()
66+
executor.config = config
67+
executor.batchService = batchService
68+
69+
when:
70+
executor.validateLowPriorityVMs()
71+
72+
then:
73+
noExceptionThrown()
74+
}
75+
76+
def 'should handle unknown allocation mode gracefully'() {
77+
given:
78+
def CONFIG = [
79+
batch: [
80+
endpoint: 'https://testaccount.eastus.batch.azure.com',
81+
pools: [
82+
'pool1': [vmType: 'Standard_D2_v2', lowPriority: true]
83+
]
84+
]
85+
]
86+
87+
and:
88+
def config = new AzConfig(CONFIG)
89+
def batchService = Mock(AzBatchService) {
90+
getPoolAllocationMode() >> null
91+
}
92+
93+
and:
94+
def executor = new AzBatchExecutor()
95+
executor.config = config
96+
executor.batchService = batchService
97+
98+
when:
99+
executor.validateLowPriorityVMs()
100+
101+
then:
102+
noExceptionThrown()
103+
}
104+
105+
def 'should not validate when no low priority VMs configured'() {
106+
given:
107+
def CONFIG = [
108+
batch: [
109+
endpoint: 'https://testaccount.eastus.batch.azure.com',
110+
pools: [
111+
'pool1': [vmType: 'Standard_D2_v2', lowPriority: false],
112+
'pool2': [vmType: 'Standard_D2_v2']
113+
]
114+
]
115+
]
116+
117+
and:
118+
def config = new AzConfig(CONFIG)
119+
def batchService = Mock(AzBatchService) {
120+
getPoolAllocationMode() >> 'BATCH_SERVICE'
121+
}
122+
123+
and:
124+
def executor = new AzBatchExecutor()
125+
executor.config = config
126+
executor.batchService = batchService
127+
128+
when:
129+
executor.validateLowPriorityVMs()
130+
131+
then:
132+
noExceptionThrown()
133+
}
134+
}

plugins/nf-azure/src/test/nextflow/cloud/azure/config/AzBatchOptsTest.groovy

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,26 @@ class AzBatchOptsTest extends Specification {
105105
then:
106106
opts3.jobMaxWallClockTime.toString() == '12h'
107107
}
108+
109+
def 'should set subscription ID from config or environment' () {
110+
when:
111+
def opts1 = new AzBatchOpts([:], [:])
112+
then:
113+
opts1.subscriptionId == null
114+
115+
when:
116+
def opts2 = new AzBatchOpts([subscriptionId: 'config-sub-id'], [:])
117+
then:
118+
opts2.subscriptionId == 'config-sub-id'
119+
120+
when:
121+
def opts3 = new AzBatchOpts([:], [AZURE_SUBSCRIPTION_ID: 'env-sub-id'])
122+
then:
123+
opts3.subscriptionId == 'env-sub-id'
124+
125+
when:
126+
def opts4 = new AzBatchOpts([subscriptionId: 'config-sub-id'], [AZURE_SUBSCRIPTION_ID: 'env-sub-id'])
127+
then:
128+
opts4.subscriptionId == 'config-sub-id' // config takes precedence over environment
129+
}
108130
}

0 commit comments

Comments
 (0)