Deploying Azure Local on unsupported hardware
How to skip Azure Local deployment health checks on lab hardware that fails them, and the traps that make the bypass silently do nothing.
Table of Contents
Preface
Let’s start by saying, this is not a great idea and you probably shouldn’t be doing this. BUT, sometimes you just need to get Azure Local stood up on some lab hardware for a quick test or one of the health checks may have a little bug in it. This is obviously not supported, and again you probably shouldn’t do this.
When you deploy Azure Local it runs many health checks, and that’s a good thing. Bypassing these health checks is not easy, nor should it be. Personally, I’ve had to do this for some logic issues in the Test-NetAdapter check, the Test-MemoryProperties test for some lab hardware without ECC, etc.
Everything below was re-verified against AzStackHci.EnvironmentChecker 10.2607.0.2064.
Bypass process
You need a file called ExcludeTests.txt containing the test names you want skipped, one per line, somewhere the checker will find it. Do it in PowerShell. (An earlier version of this post gave hardcoded paths instead; here’s why that was wrong.)
Run this on the node:
Get-Module AzStackHci.EnvironmentChecker -ListAvailable | ForEach-Object {
$f = Join-Path $_.ModuleBase 'ExcludeTests.txt'
Set-Content -Path $f -Value 'Test-MemoryProperties' -Encoding ASCII -Force
"wrote $f"
}Get-Module AzStackHci.EnvironmentChecker -ListAvailable | ForEach-Object {
$f = Join-Path $_.ModuleBase 'ExcludeTests.txt'
Set-Content -Path $f -Value 'Test-MemoryProperties' -Encoding ASCII -Force
"wrote $f"
}Swap in whichever test(s) you need — -Value takes an array if you want more than one:
-Value 'Test-MemoryProperties','Test-TpmVersion','Test-SecureBoot'-Value 'Test-MemoryProperties','Test-TpmVersion','Test-SecureBoot'Driving it off Get-Module -ListAvailable isn’t just laziness — that’s the same source the checker itself uses to build its search paths, so the file always lands somewhere it will actually get picked up. If a node has more than one version installed it writes one file per version, which is harmless since the contents are identical.
For the whole cluster at once:
Invoke-Command -ComputerName Node1,Node2,Node3 -ScriptBlock {
Get-Module AzStackHci.EnvironmentChecker -ListAvailable | ForEach-Object {
Set-Content -Path (Join-Path $_.ModuleBase 'ExcludeTests.txt') `
-Value 'Test-MemoryProperties' -Encoding ASCII -Force
}
}Invoke-Command -ComputerName Node1,Node2,Node3 -ScriptBlock {
Get-Module AzStackHci.EnvironmentChecker -ListAvailable | ForEach-Object {
Set-Content -Path (Join-Path $_.ModuleBase 'ExcludeTests.txt') `
-Value 'Test-MemoryProperties' -Encoding ASCII -Force
}
}And to confirm what the checker will actually read (see Mistakes that cost me time):
$p = @(Get-Module AzStackHci.EnvironmentChecker -ListAvailable | Select-Object -Expand ModuleBase)
Get-ChildItem -Path $p -Recurse -Filter ExcludeTests.txt -File |
Sort-Object LastWriteTime -Descending |
Select-Object FullName, LastWriteTime, @{n='Content';e={(Get-Content $_.FullName) -join ','}}$p = @(Get-Module AzStackHci.EnvironmentChecker -ListAvailable | Select-Object -Expand ModuleBase)
Get-ChildItem -Path $p -Recurse -Filter ExcludeTests.txt -File |
Sort-Object LastWriteTime -Descending |
Select-Object FullName, LastWriteTime, @{n='Content';e={(Get-Content $_.FullName) -join ','}}That mirrors the module’s own selection logic. The top row is the only file that counts.
Why not just browse to the module folder?
The checker searches every ModuleBase that Get-Module AzStackHci.EnvironmentChecker -ListAvailable returns and recurses down from each one. On a versioned install, ModuleBase is the version folder, so a file in the parent ...\Modules\AzStackHci.EnvironmentChecker\ folder is never found. Whether a node ends up with a flat install, a versioned one, or several versions varies even between identical deployments, so let PowerShell find the folders instead of assuming. To see what a node has:
Get-Module AzStackHci.EnvironmentChecker -ListAvailable |
Select-Object Version, ModuleBase, @{
n = 'Layout'
e = { if ($_.ModuleBase -match '\\AzStackHci\.EnvironmentChecker$') { 'flat' } else { 'versioned' } }
}Get-Module AzStackHci.EnvironmentChecker -ListAvailable |
Select-Object Version, ModuleBase, @{
n = 'Layout'
e = { if ($_.ModuleBase -match '\\AzStackHci\.EnvironmentChecker$') { 'flat' } else { 'versioned' } }
}If a node has more than one version, only one is used by the deployment, and test names occasionally change between releases. A name that doesn’t match anything is silently ignored, so dump the test list (below) from the version that’s actually in play.
Mistakes that cost me time
- The filename is
ExcludeTests.txt. NotExcludedTests.txt. It’s matched literally, so a misspelled file is silently ignored and validation just runs normally like you never did anything. - Only the newest file is ever read. The lookup searches recursively, sorts by
LastWriteTimedescending, and takes one. So a leftoverExcludeTests.txtfrom a previous bypass somewhere else in the module tree will silently shadow the one you just wrote if it happens to be newer. This is the single most annoying thing about the mechanism — use the verify snippet above. - Entries are treated as regex, and matched as substrings. Handy, but sharp: writing
Test-TpmkillsTest-TpmVersion,Test-TpmPropertiesandTest-TpmCertificatesin one go. Use full test names unless you mean it. - Don’t exclude everything. If the filter empties the list completely the validator bails out and runs nothing, which is not the same as passing.
- You don’t need to touch any signed files. Nothing in the module verifies its own signatures or a file catalog, so dropping in a new unsigned text file trips nothing. That’s exactly why this approach is preferable to editing a
.psm1— leave the signed files alone. - Check the severity before you bother. Not every failing check actually blocks a deployment; some are
WARNINGand are informational. Excluding one of those achieves nothing.
There are also two environment variables that do much the same job if you’d rather not drop a file: $ENV:envchkroverridetest takes a comma-separated list and skips tests the same way, and $ENV:envchkroverrideseverity takes a JSON name-to-severity map, which downgrades a result instead of skipping the test. The severity route is the gentler option since you keep the diagnostic output in the report.
Available tests
The hardware list is the one most people want, and it has grown since I first wrote this post:
Test-Processor, Test-NetAdapter, Test-MemoryCapacity, Test-MemoryProperties, Test-Gpu, Test-Baseboard, Test-Model, Test-PhysicalDisk, Test-TpmVersion, Test-TpmProperties, Test-TpmCertificates, Test-SecureBoot, Test-StoragePool, Test-SystemDriveFreeSpace, Test-Volume, Test-MinCoreCount, Test-VirtualDisk, Test-VirtualizationBasedSecurity, Test-MountedMedia
New since the original post: Test-TpmCertificates, Test-SystemDriveFreeSpace, Test-Volume, Test-VirtualDisk, Test-MountedMedia and Test-StoragePool. Note Test-StoragePool is already hard-excluded by the hardware validator itself, so it never runs regardless.
But hardware is only one validator out of 32. The full set of bypassable tests:
All tests by validator (10.2607.0.2064)
| Validator | Tests |
|---|---|
| Hardware | Test-Processor, Test-NetAdapter, Test-MemoryCapacity, Test-MemoryProperties, Test-Gpu, Test-Baseboard, Test-Model, Test-PhysicalDisk, Test-TpmVersion, Test-TpmProperties, Test-TpmCertificates, Test-SecureBoot, Test-StoragePool, Test-SystemDriveFreeSpace, Test-Volume, Test-MinCoreCount, Test-VirtualDisk, Test-VirtualizationBasedSecurity, Test-MountedMedia |
| Network | Test-NwkValidator_StorageAdapterReadiness, Test-NwkValidator_SanClusterNetworkAdapterReadiness, Test-NwkValidator_InfraIpPoolReadiness, Test-NwkValidator_AKS_CidrOverlaps, Test-NwkValidator_HostNetworkConfigurationReadiness, Test-NwkValidator_AdapterDriverMgmtAdapterReadiness, Test-NwkValidator_MgmtIpIpPoolRequirement, Test-NwkValidator_MgmtIPForNewNode, Test-NwkValidator_ClusterNetworkIntentStatus, Test-NwkValidator_StorageIntentExistence, Test-NwkValidator_NetworkATCFeatureStatusOnNewNode, Test-NwkValidator_NetworkIntentRequirement, Test-NwkValidator_StorageConnectivityType, Test-NwkValidator_StorageAdapterIPConfigurationPreUpdate, Test-NwkValidator_StorageVlanFor2NodeSwitchLessDeployment, Test-NwkValidator_NetworkGatewayRequirement, Test-NwkValidator_MgmtIpConfigurationForStaticDeployment, Test-NwkValidator_IntentVirtualAdapterExistence |
| Upgrade | Test-RequiredWindowsVersionForUpgrade, Test-RequiredWindowsFeature, Test-NetworkAtcIntents, Test-TPMHealth, Test-BitlockerSuspension, Test-WdacEnablement, Test-AzureSupportedCloudType, Test-AzureStackHCIRegistrationState, Test-AksHciInstallState, Test-MocInstallState, Test-MocServicesInstallState, Test-Language, Test-Storage, Test-LCMVersion, Test-FreeMemory, Test-AlreadyDeployed |
| NetworkSLB | Test-SLB_ValidateFCNCInstalled, Test-SLB_ValidateNumberOfSLBNodes, Test-SLB_ValidateSoftwareLoadBalancer, Test-SLB_ValidateHNVPANetwork, Test-SLB_ValidatePublicPrivateVIPNetworks, Test-SLB_ValidateBGPPeersReachable, Test-SLB_ValidateNCLoadBalancerMux, Test-SLB_ValidateNCLoadBalancerManager, Test-SLB_ValidateNCHNVPAIPPools, Test-SLB_ValidateNCServers, Test-SLB_ValidateOverlappingIPPools, Test-SLB_ValidateInfraIPPools, Test-SLB_ValidateDNSName |
| SAN | Test-SANFibreChannelConnectivity, Test-SANiSCSIConnectivity, Test-SANALUNVisibility, Test-SANLUNCapacity, Test-SANLUNPartitionStyle, Test-SANSCSIReservation, Test-SANMPIOInstalled, Test-SANMPIOHardwareSupported, Test-SANMPIOPaths |
| ArcIntegration | Test-ExistingArcResources, Test-ArcAgentNotConnectedToDifferentResource, Test-IsRegionValid, Test-ResourceGroupLimit, Test-ResourceCountLimit, Test-RoleAssignmentCountLimit, Test-ExistingHCIResource, Test-IsMandatoryRPRegistered, Test-AzureStackHCISubscriptionState |
| Cluster | Test-ClusterNameLength, Test-AzLocalClusterResources, Test-AzLocalClusterNodes, Test-AzLocalClusterStretched, Test-AzLocalClusterIpNotInPool, Test-AzLocalClusterIPV6, Test-ClusterFunctionalLevel, Test-ClusterPreReq |
| MOCStack | Test-MOCStackVolume, Test-MOCStackCPUCore, Test-MOCStackMemory, Test-MOCStackNetworkPort, Test-MOCStackFirewallUrl, Test-MOCStackNodeAgents, Test-MOCStackCloudAgent, Test-MOCStackClusterNode |
| OSImageRecipe | Test-InstalledPackages, Test-InstalledAdditionalFiles, Test-BaseOSimage, Test-BuildId, Test-ComposedBuildVersion, Test-SolutionVersion, Test-FeaturesOnDemand, Test-LatestCumulativeUpdate |
| AddNode | Test-ADCredential, Test-LocalCredential, Test-ClusterNodeName, Test-ComputerName, Test-Quorum, Test-DriveLetterConsistency |
| LLDP | Test-LLDPNbrTlvs, Test-MergedLLDPDataToJson, Test-LLDPConnections, Test-LLDPDcbxConfiguration, Test-LLDPAvailabilityZoneConnections, Test-StandardClusterSwitchConsistency |
| RemoteManagement | Test-CimSession, Test-NewPsSession, Test-NewPsSessionWithCredSSP, Test-RemoteEventLog, Test-RemoteWmiQuery |
| SBEHealth | Test-SBEPropertiesValid, Test-SBECredentialsValid, Test-SolutionExtensionModule, Test-SBEEndpointConnectivity, Test-S2DFirmwareBinariesPresent |
| Connectivity | Test-RootCA, Test-ProxySettingsConsistency, Test-ArcGatewayProxyConfig, Test-ProxyByPassListRecommendations |
| DNS | Test-ExternalDnsResolution, Test-ActiveDirectoryDomainName, Test-IPMapForClusterNodes, Test-LocalClusterDNSResolution |
| RDMA | Test-RDMAValidator_AdapterStatus, Test-RDMAValidator_MultichannelStatus, Test-RDMAValidator_NodeCompatibility, Test-RDMAValidator_Stress |
| Software | Test-OSVersion, Test-NtpServer, Test-LocalGroupEnumeration, Test-IsNotPartofDomain |
| ARBStack | Test-ARBStackBandwidth, Test-AzCLIExtensionsOutsideVR, Test-ARBIsNotLocked |
| Security | Test-AsrRuleConfiguration, Test-SecureBootUpdateStatus, Test-AsrRuleGPConflict |
| Storage | Test-HciStoragePool, Test-HciStorageVolumes, Test-StoragePoolCapacity |
| ValidatedRecipe | Test-PSModules, Test-AzCli, Test-ServicesVersion |
| ClusterWitness | Test-WitnessFileShareWithCredential, Test-WitnessCloudStorage |
| Observability | Test-LogCollection, Test-RemoteSupport |
| SDNNC | Test-NwkValidator_SDNNCDeployed, Test-NwkValidator_SDNNCServices |
| Bitlocker | Test-BitlockerKeysExist |
| Cloud | Test-AzStackHciRootCertChainVerification |
| HostTCPMetrics | Test-FullMeshTCP |
| LocalIdentity | Test-LocalUser_IsAdmin_IsNotBuiltInAdmin |
| VSRDriftDetection | Test-DriftDetection |
| ClusterSharedVolume | Test-ClusterSharedVolumeInfrastructureShareAccess |
| NetworkInfraConnection | Test-NwkInfraConnectionValidator_InfraIpPoolConnection |
| NetworkStorageConnection | Test-NwkStorageConnectionValidator_StorageAdapterConnection |
Don’t trust my list, dump your own
These lists drift between versions, so rather than trusting a blog post, dump the tests your node actually has. This uses the same regex the module uses internally to discover them:
Get-Module AzStackHci.EnvironmentChecker -ListAvailable | ForEach-Object {
Get-ChildItem $_.ModuleBase -Recurse -Filter '*.Helpers.psm1' | ForEach-Object {
$m = [regex]::Matches((Get-Content $_.FullName -Raw), 'function\s+(Test-[a-zA-Z0-9_-]+)')
if ($m.Count) {
[pscustomobject]@{
Validator = $_.BaseName -replace '\.Helpers$'
Tests = ($m | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique) -join ', '
}
}
}
} | Format-Table -WrapGet-Module AzStackHci.EnvironmentChecker -ListAvailable | ForEach-Object {
Get-ChildItem $_.ModuleBase -Recurse -Filter '*.Helpers.psm1' | ForEach-Object {
$m = [regex]::Matches((Get-Content $_.FullName -Raw), 'function\s+(Test-[a-zA-Z0-9_-]+)')
if ($m.Count) {
[pscustomobject]@{
Validator = $_.BaseName -replace '\.Helpers$'
Tests = ($m | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique) -join ', '
}
}
}
} | Format-Table -Wrap