Skip to content

Commit 0d300c8

Browse files
parahluwaliaCopilotaudreytttCopilotCopilot
authored
[Compute] Add VMSS Lifecycle Hooks cmdlets (Az.Compute preview) (#29744)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: audreyttt <225061541+audreyttt@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent e6ac0dc commit 0d300c8

23 files changed

Lines changed: 10823 additions & 7 deletions

src/Compute/Compute.Test/ScenarioTests/StrategiesVmssTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,5 +137,33 @@ public void SimpleNewVmssSkipExtOverprovision()
137137
{
138138
TestRunner.RunTestScript("Test-SimpleNewVmssSkipExtOverprovision");
139139
}
140+
141+
[Fact]
142+
[Trait(Category.AcceptanceType, Category.CheckIn)]
143+
public void TestVmssLifecycleHookConfig()
144+
{
145+
TestRunner.RunTestScript("Test-VmssLifecycleHookConfig");
146+
}
147+
148+
[Fact]
149+
[Trait(Category.AcceptanceType, Category.CheckIn)]
150+
public void TestSetVmssLifecycleHooksProfile()
151+
{
152+
TestRunner.RunTestScript("Test-SetVmssLifecycleHooksProfile");
153+
}
154+
155+
[Fact]
156+
[Trait(Category.AcceptanceType, Category.CheckIn)]
157+
public void TestNewVmssConfigWithLifecycleHooksProfile()
158+
{
159+
TestRunner.RunTestScript("Test-NewVmssConfigWithLifecycleHooksProfile");
160+
}
161+
162+
[Fact]
163+
[Trait(Category.AcceptanceType, Category.CheckIn)]
164+
public void TestVmssLifecycleHookEventEndToEnd()
165+
{
166+
TestRunner.RunTestScript("Test-VmssLifecycleHookEventEndToEnd");
167+
}
140168
}
141169
}

src/Compute/Compute.Test/ScenarioTests/StrategiesVmssTests.ps1

Lines changed: 271 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ----------------------------------------------------------------------------------
1+
# ----------------------------------------------------------------------------------
22
#
33
# Copyright Microsoft Corporation
44
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -551,3 +551,273 @@ function Test-SimpleNewVmssSkipExtOverprovision
551551
Clean-ResourceGroup $vmssname
552552
}
553553
}
554+
555+
<#
556+
.SYNOPSIS
557+
Test New-AzVmssLifecycleHookConfig creates an in-memory lifecycle hook config object.
558+
#>
559+
function Test-VmssLifecycleHookConfig
560+
{
561+
# Test creating a lifecycle hook config object in memory (no Azure calls needed)
562+
$hook = New-AzVmssLifecycleHookConfig -Type 'UpgradeAutoOSScheduling' -WaitDuration 'PT8H'
563+
Assert-NotNull $hook
564+
Assert-AreEqual 'UpgradeAutoOSScheduling' $hook.Type
565+
Assert-AreEqual ([System.TimeSpan]::FromHours(8)) $hook.WaitDuration
566+
# When -DefaultAction is omitted, the cmdlet leaves it null so the SDK omits the field
567+
# from the request and the service applies its own default.
568+
Assert-Null $hook.DefaultAction
569+
570+
# With explicit default action
571+
$hook2 = New-AzVmssLifecycleHookConfig -Type 'UpgradeAutoOSRollingBatchStarting' -WaitDuration 'PT30M' -DefaultAction 'Approve'
572+
Assert-NotNull $hook2
573+
Assert-AreEqual 'UpgradeAutoOSRollingBatchStarting' $hook2.Type
574+
Assert-AreEqual ([System.TimeSpan]::FromMinutes(30)) $hook2.WaitDuration
575+
Assert-AreEqual 'Approve' $hook2.DefaultAction
576+
577+
# Without wait duration
578+
$hook3 = New-AzVmssLifecycleHookConfig -Type 'UpgradeAutoOSScheduling'
579+
Assert-NotNull $hook3
580+
Assert-Null $hook3.WaitDuration
581+
Assert-Null $hook3.DefaultAction
582+
}
583+
584+
<#
585+
.SYNOPSIS
586+
Test Set-AzVmssLifecycleHooksProfile attaches hooks to a VMSS config and round-trips through the service.
587+
#>
588+
function Test-SetVmssLifecycleHooksProfile
589+
{
590+
# Tests the Set-AzVmssLifecycleHooksProfile builder cmdlet end-to-end:
591+
# Build VMSS config -> attach hook via Set-AzVmssLifecycleHooksProfile -> deploy -> verify hook round-tripped from service.
592+
$rgname = Get-ComputeTestResourceName
593+
594+
try
595+
{
596+
$loc = "eastus2euap"
597+
New-AzResourceGroup -Name $rgname -Location $loc -Force
598+
599+
# NRP
600+
$subnet = New-AzVirtualNetworkSubnetConfig -Name ('subnet' + $rgname) -AddressPrefix "10.0.0.0/24" -DefaultOutboundAccess $false
601+
$vnet = New-AzVirtualNetwork -Force -Name ('vnet' + $rgname) -ResourceGroupName $rgname -Location $loc -AddressPrefix "10.0.0.0/16" -Subnet $subnet
602+
$subnetId = $vnet.Subnets[0].Id
603+
604+
$vmssName = 'vmss' + $rgname
605+
$adminUsername = 'Foo12'
606+
$adminPassword = $PLACEHOLDER
607+
$ipCfg = New-AzVmssIPConfig -Name 'test' -SubnetId $subnetId -Primary
608+
609+
# Build a basic VMSS config WITHOUT hooks
610+
$vmss = New-AzVmssConfig -Location $loc -SkuCapacity 1 -SkuName 'Standard_DS1_v2' -UpgradePolicyMode 'Automatic' `
611+
| Add-AzVmssNetworkInterfaceConfiguration -Name 'test' -Primary $true -IPConfiguration $ipCfg `
612+
| Set-AzVmssOSProfile -ComputerNamePrefix 'test' -AdminUsername $adminUsername -AdminPassword $adminPassword `
613+
| Set-AzVmssStorageProfile -OsDiskCreateOption 'FromImage' -OsDiskCaching 'None' `
614+
-ImageReferenceOffer 'WindowsServer' -ImageReferenceSku '2022-Datacenter' `
615+
-ImageReferenceVersion 'latest' -ImageReferencePublisher 'MicrosoftWindowsServer'
616+
617+
# Attach hook via the builder cmdlet under test
618+
$hook = New-AzVmssLifecycleHookConfig -Type 'UpgradeAutoOSScheduling' -WaitDuration 'PT8H' -DefaultAction 'Approve'
619+
$vmss = Set-AzVmssLifecycleHooksProfile -VirtualMachineScaleSet $vmss -LifecycleHook $hook
620+
621+
# Pre-deploy assertion: hook is on the in-memory object
622+
Assert-NotNull $vmss.LifecycleHooksProfile
623+
Assert-AreEqual 1 $vmss.LifecycleHooksProfile.LifecycleHooks.Count
624+
625+
# Deploy
626+
$created = New-AzVmss -ResourceGroupName $rgname -Name $vmssName -VirtualMachineScaleSet $vmss
627+
Assert-NotNull $created
628+
629+
# Read back from service and verify the hook round-tripped
630+
$read = Get-AzVmss -ResourceGroupName $rgname -VMScaleSetName $vmssName
631+
Assert-NotNull $read.LifecycleHooksProfile
632+
Assert-AreEqual 1 $read.LifecycleHooksProfile.LifecycleHooks.Count
633+
Assert-AreEqual 'UpgradeAutoOSScheduling' $read.LifecycleHooksProfile.LifecycleHooks[0].Type
634+
Assert-AreEqual ([System.TimeSpan]::FromHours(8)) $read.LifecycleHooksProfile.LifecycleHooks[0].WaitDuration
635+
Assert-AreEqual 'Approve' $read.LifecycleHooksProfile.LifecycleHooks[0].DefaultAction
636+
}
637+
finally
638+
{
639+
Clean-ResourceGroup $rgname
640+
}
641+
}
642+
643+
<#
644+
.SYNOPSIS
645+
Test New-AzVmssConfig with -LifecycleHooksProfile parameter round-trips through the service.
646+
#>
647+
function Test-NewVmssConfigWithLifecycleHooksProfile
648+
{
649+
# Tests the inline -LifecycleHooksProfile parameter on New-AzVmssConfig end-to-end:
650+
# Build profile -> New-AzVmssConfig -LifecycleHooksProfile -> deploy -> verify hook round-tripped from service.
651+
$rgname = Get-ComputeTestResourceName
652+
653+
try
654+
{
655+
$loc = "eastus2euap"
656+
New-AzResourceGroup -Name $rgname -Location $loc -Force
657+
658+
# NRP
659+
$subnet = New-AzVirtualNetworkSubnetConfig -Name ('subnet' + $rgname) -AddressPrefix "10.0.0.0/24" -DefaultOutboundAccess $false
660+
$vnet = New-AzVirtualNetwork -Force -Name ('vnet' + $rgname) -ResourceGroupName $rgname -Location $loc -AddressPrefix "10.0.0.0/16" -Subnet $subnet
661+
$subnetId = $vnet.Subnets[0].Id
662+
663+
$vmssName = 'vmss' + $rgname
664+
$adminUsername = 'Foo12'
665+
$adminPassword = $PLACEHOLDER
666+
$ipCfg = New-AzVmssIPConfig -Name 'test' -SubnetId $subnetId -Primary
667+
668+
# Build a LifecycleHooksProfile inline and pass it directly to New-AzVmssConfig
669+
$hook = New-AzVmssLifecycleHookConfig -Type 'UpgradeAutoOSScheduling' -WaitDuration 'PT8H' -DefaultAction 'Approve'
670+
$hooksProfile = [Microsoft.Azure.Management.Compute.Models.LifecycleHooksProfile]::new()
671+
$hooksProfile.LifecycleHooks = [System.Collections.Generic.List[Microsoft.Azure.Management.Compute.Models.LifecycleHook]]::new()
672+
$hooksProfile.LifecycleHooks.Add($hook)
673+
674+
$vmss = New-AzVmssConfig -Location $loc -SkuCapacity 1 -SkuName 'Standard_DS1_v2' -UpgradePolicyMode 'Automatic' -LifecycleHooksProfile $hooksProfile `
675+
| Add-AzVmssNetworkInterfaceConfiguration -Name 'test' -Primary $true -IPConfiguration $ipCfg `
676+
| Set-AzVmssOSProfile -ComputerNamePrefix 'test' -AdminUsername $adminUsername -AdminPassword $adminPassword `
677+
| Set-AzVmssStorageProfile -OsDiskCreateOption 'FromImage' -OsDiskCaching 'None' `
678+
-ImageReferenceOffer 'WindowsServer' -ImageReferenceSku '2022-Datacenter' `
679+
-ImageReferenceVersion 'latest' -ImageReferencePublisher 'MicrosoftWindowsServer'
680+
681+
# Pre-deploy assertion: profile is on the in-memory object
682+
Assert-NotNull $vmss.LifecycleHooksProfile
683+
Assert-AreEqual 1 $vmss.LifecycleHooksProfile.LifecycleHooks.Count
684+
685+
# Deploy
686+
$created = New-AzVmss -ResourceGroupName $rgname -Name $vmssName -VirtualMachineScaleSet $vmss
687+
Assert-NotNull $created
688+
689+
# Read back from service and verify
690+
$read = Get-AzVmss -ResourceGroupName $rgname -VMScaleSetName $vmssName
691+
Assert-NotNull $read.LifecycleHooksProfile
692+
Assert-AreEqual 1 $read.LifecycleHooksProfile.LifecycleHooks.Count
693+
Assert-AreEqual 'UpgradeAutoOSScheduling' $read.LifecycleHooksProfile.LifecycleHooks[0].Type
694+
Assert-AreEqual ([System.TimeSpan]::FromHours(8)) $read.LifecycleHooksProfile.LifecycleHooks[0].WaitDuration
695+
Assert-AreEqual 'Approve' $read.LifecycleHooksProfile.LifecycleHooks[0].DefaultAction
696+
}
697+
finally
698+
{
699+
Clean-ResourceGroup $rgname
700+
}
701+
}
702+
703+
704+
<#
705+
.SYNOPSIS
706+
End-to-end test for Get-AzVmssLifecycleHookEvent (List + Get-single) and Update-AzVmssLifecycleHookEvent
707+
(WaitUntil delay + ActionState=Approved).
708+
709+
Uses the seekAutoOSUpgradeApproval admin REST endpoint (via Invoke-AzRestMethod) to synthesize a
710+
lifecycle hook event on demand, instead of waiting for the platform's AutoOSUpgrade scheduler.
711+
712+
Flow:
713+
1. Deploy a 1-instance Uniform VMSS with AutomaticOSUpgrade enabled, Application Health (Linux)
714+
extension, and a UpgradeAutoOSScheduling lifecycle hook attached.
715+
2. POST to /seekAutoOSUpgradeApproval with the same platformImageReference used for the VMSS.
716+
3. LIST events -- should now contain exactly 1.
717+
4. GET the event by name.
718+
5. UPDATE -WaitUntil by +10 min.
719+
6. UPDATE -ActionState 'Approved'.
720+
7. GET the event again -- assert both updates persisted.
721+
#>
722+
function Test-VmssLifecycleHookEventEndToEnd
723+
{
724+
$rgname = Get-ComputeTestResourceName
725+
726+
try
727+
{
728+
$loc = "eastus2euap"
729+
New-AzResourceGroup -Name $rgname -Location $loc -Force
730+
731+
# NRP (subnet must be defaultOutboundAccess=$false for the sub's policy)
732+
$subnet = New-AzVirtualNetworkSubnetConfig -Name ('subnet' + $rgname) -AddressPrefix "10.0.0.0/24" -DefaultOutboundAccess $false
733+
$vnet = New-AzVirtualNetwork -Force -Name ('vnet' + $rgname) -ResourceGroupName $rgname -Location $loc -AddressPrefix "10.0.0.0/16" -Subnet $subnet
734+
$subnetId = $vnet.Subnets[0].Id
735+
736+
# Image reference (must match the body of seekAutoOSUpgradeApproval below)
737+
$publisher = 'canonical'
738+
$offer = '0001-com-ubuntu-server-focal'
739+
$sku = '20_04-lts-gen2'
740+
$version = 'latest'
741+
742+
$vmssName = 'vmss' + $rgname
743+
$adminUsername = 'foo12'
744+
$adminPassword = $PLACEHOLDER
745+
$ipCfg = New-AzVmssIPConfig -Name 'test' -SubnetId $subnetId -Primary
746+
747+
# 1-instance Uniform VMSS with AutoOSUpgrade + Application Health extension + lifecycle hook attached.
748+
$hook = New-AzVmssLifecycleHookConfig -Type 'UpgradeAutoOSScheduling' -WaitDuration 'PT1H' -DefaultAction 'Approve'
749+
750+
$vmss = New-AzVmssConfig -Location $loc -SkuCapacity 1 -SkuName 'Standard_DS1_v2' `
751+
-OrchestrationMode 'Uniform' -UpgradePolicyMode 'Automatic' -EnableAutomaticOSUpgrade `
752+
| Add-AzVmssNetworkInterfaceConfiguration -Name 'test' -Primary $true -IPConfiguration $ipCfg `
753+
| Set-AzVmssOSProfile -ComputerNamePrefix 'test' -AdminUsername $adminUsername -AdminPassword $adminPassword `
754+
| Set-AzVmssStorageProfile -OsDiskCreateOption 'FromImage' -OsDiskCaching 'None' `
755+
-ImageReferenceOffer $offer -ImageReferenceSku $sku `
756+
-ImageReferenceVersion $version -ImageReferencePublisher $publisher
757+
758+
# AutoOSUpgrade requires a health probe or health extension.
759+
Add-AzVmssExtension -VirtualMachineScaleSet $vmss `
760+
-Name 'AppHealth' `
761+
-Publisher 'Microsoft.ManagedServices' `
762+
-Type 'ApplicationHealthLinux' `
763+
-TypeHandlerVersion '1.0' `
764+
-Setting @{ protocol = 'http'; port = 80; requestPath = '/' } `
765+
-AutoUpgradeMinorVersion $true | Out-Null
766+
767+
$vmss = Set-AzVmssLifecycleHooksProfile -VirtualMachineScaleSet $vmss -LifecycleHook $hook
768+
769+
New-AzVmss -ResourceGroupName $rgname -Name $vmssName -VirtualMachineScaleSet $vmss
770+
771+
# Synthesize a lifecycle hook event via the admin REST endpoint.
772+
# The platformImageReference must match the image used to deploy the VMSS.
773+
$subId = (Get-AzContext).Subscription.Id
774+
$apiPath = "/subscriptions/$subId/resourceGroups/$rgname/providers/Microsoft.Compute/virtualMachineScaleSets/$vmssName/seekAutoOSUpgradeApproval?api-version=2025-11-01"
775+
$body = @{
776+
platformImageReference = @{
777+
publisher = $publisher
778+
offer = $offer
779+
sku = $sku
780+
version = $version
781+
}
782+
} | ConvertTo-Json -Depth 5
783+
$approval = Invoke-AzRestMethod -Method POST -Path $apiPath -Payload $body
784+
Assert-True { $approval.StatusCode -ge 200 -and $approval.StatusCode -lt 300 } "seekAutoOSUpgradeApproval returned HTTP $($approval.StatusCode): $($approval.Content)"
785+
786+
# Bounded short poll for the synthesized event to materialize (event count must reach 1).
787+
$events = $null
788+
for ($i = 0; $i -lt 10; $i++)
789+
{
790+
$events = Get-AzVmssLifecycleHookEvent -ResourceGroupName $rgname -VMScaleSetName $vmssName
791+
if ($events -and @($events).Count -gt 0) { break }
792+
Start-TestSleep -Seconds 30
793+
}
794+
Assert-NotNull $events "Expected Get-AzVmssLifecycleHookEvent to return at least one event after seekAutoOSUpgradeApproval"
795+
Assert-AreEqual 1 @($events).Count
796+
$event = @($events)[0]
797+
798+
# GET single event by name and verify
799+
$fetched = Get-AzVmssLifecycleHookEvent -ResourceGroupName $rgname -VMScaleSetName $vmssName -Name $event.Name
800+
Assert-NotNull $fetched
801+
Assert-AreEqual $event.Name $fetched.Name
802+
803+
# UPDATE WaitUntil: delay by 10 min
804+
$currentWaitUntil = [DateTime]::Parse($fetched.Properties.WaitUntil).ToUniversalTime()
805+
$newWaitUntilStr = $currentWaitUntil.AddMinutes(10).ToString("yyyy-MM-ddTHH:mm:ssZ")
806+
Update-AzVmssLifecycleHookEvent -ResourceGroupName $rgname -VMScaleSetName $vmssName -Name $event.Name -WaitUntil $newWaitUntilStr
807+
808+
# UPDATE ActionState: mark the single target resource Approved
809+
Update-AzVmssLifecycleHookEvent -ResourceGroupName $rgname -VMScaleSetName $vmssName -Name $event.Name -ActionState 'Approved'
810+
811+
# Re-GET and verify both updates persisted
812+
$fetched2 = Get-AzVmssLifecycleHookEvent -ResourceGroupName $rgname -VMScaleSetName $vmssName -Name $event.Name
813+
Assert-NotNull $fetched2
814+
Assert-AreEqual 1 $fetched2.Properties.TargetResources.Count
815+
Assert-AreEqual 'Approved' $fetched2.Properties.TargetResources[0].ActionState
816+
$updatedWaitUntil = [DateTime]::Parse($fetched2.Properties.WaitUntil).ToUniversalTime()
817+
Assert-True { $updatedWaitUntil -gt $currentWaitUntil } "Expected WaitUntil to be updated to a later timestamp"
818+
}
819+
finally
820+
{
821+
Clean-ResourceGroup $rgname
822+
}
823+
}

0 commit comments

Comments
 (0)