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. Not ExcludedTests.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 LastWriteTime descending, and takes one. So a leftover ExcludeTests.txt from 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-Tpm kills Test-TpmVersion, Test-TpmProperties and Test-TpmCertificates in 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 WARNING and 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 -Wrap
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 -Wrap