Private/Get-AzLocalNetworkSettingsFromJson.ps1

Function Get-AzLocalNetworkSettingsFromJson {
    <#
    .SYNOPSIS
 
    Loads network settings from a JSON file or JSON string for non-interactive deployments.
 
    .DESCRIPTION
 
    Parses a JSON file path or inline JSON string containing network settings and validates
    the required fields. Expected JSON structure:
 
        {
            "subnetMask": "255.255.255.0",
            "defaultGateway": "10.0.0.1",
            "startingIPAddress": "10.0.0.10",
            "endingIPAddress": "10.0.0.50",
            "nodeIPAddresses": ["10.0.0.100", "10.0.0.101"],
            "dnsServers": ["10.0.0.5", "10.0.0.6"]
        }
 
    The 'dnsServers' field is optional. When present, it overrides the 'dnsServers' default
    in naming-standards-config.json. When absent (or an empty array), the config default is
    used. The -DnsServers parameter on Start-AzLocalTemplateDeployment still takes precedence
    over both.
    #>


    [OutputType([PSCustomObject])]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true, Position = 0)]
        [string]$NetworkSettingsJson,

        [Parameter(Mandatory = $true, Position = 1)]
        [string]$TypeOfDeployment,

        [Parameter(Mandatory = $false, Position = 2)]
        [int]$NodeCount = 0
    )

    # Determine effective node count for validation
    switch ($TypeOfDeployment) {
        "SingleNode" { $expectedNodes = 1 }
        default      { $expectedNodes = $NodeCount }
    }

    # Load JSON from file path or inline string
    if (Test-Path $NetworkSettingsJson -ErrorAction SilentlyContinue) {
        Write-Verbose "Loading network settings from file: $NetworkSettingsJson"
        try {
            $jsonContent = Get-Content $NetworkSettingsJson -Raw -ErrorAction Stop
            $settings = $jsonContent | ConvertFrom-Json -ErrorAction Stop
        } catch {
            Write-AzLocalLog "Failed to read or parse network settings file '$NetworkSettingsJson'." -Level Error
            throw "Failed to parse network settings file. $($_.Exception.Message)"
        }
    } else {
        Write-Verbose "Parsing inline network settings JSON..."
        try {
            $settings = $NetworkSettingsJson | ConvertFrom-Json -ErrorAction Stop
        } catch {
            Write-AzLocalLog "Failed to parse network settings JSON string." -Level Error
            throw "Failed to parse network settings JSON. $($_.Exception.Message)"
        }
    }

    # Validate required fields
    $requiredFields = @('subnetMask', 'defaultGateway', 'startingIPAddress', 'endingIPAddress', 'nodeIPAddresses')
    foreach ($field in $requiredFields) {
        if (-not $settings.PSObject.Properties[$field]) {
            Write-AzLocalLog "Network settings JSON is missing required field '$field'." -Level Error
            throw "Network settings JSON is missing required field '$field'."
        }
    }

    # Validate IP address formats (TryParse: no exception overhead, stable error messages)
    $ipFields = @('subnetMask', 'defaultGateway', 'startingIPAddress', 'endingIPAddress')
    $ipRef = [System.Net.IPAddress]::None
    foreach ($f in $ipFields) {
        $value = [string]$settings.$f
        if (-not [System.Net.IPAddress]::TryParse($value, [ref]$ipRef)) {
            Write-AzLocalLog "Invalid IP address '$value' for field '$f' in network settings JSON." -Level Error
            throw "Invalid IP address '$value' for field '$f' in network settings JSON. Provide a valid IPv4 or IPv6 address."
        }
    }

    # Validate node IP addresses
    $nodeIPs = @($settings.nodeIPAddresses)
    if ($nodeIPs.Count -ne $expectedNodes) {
        Write-AzLocalLog "Network settings JSON contains $($nodeIPs.Count) node IP addresses but $TypeOfDeployment deployment requires $expectedNodes." -Level Error
        throw "Expected $expectedNodes node IP addresses for $TypeOfDeployment deployment, but found $($nodeIPs.Count)."
    }
    foreach ($nodeIP in $nodeIPs) {
        if (-not [System.Net.IPAddress]::TryParse([string]$nodeIP, [ref]$ipRef)) {
            Write-AzLocalLog "Invalid node IP address '$nodeIP' in network settings JSON." -Level Error
            throw "Invalid node IP address '$nodeIP' in nodeIPAddresses. Provide a valid IPv4 or IPv6 address."
        }
    }

    # Optional 'dnsServers' override: when present and non-empty, callers will use these in
    # place of the dnsServers default from naming-standards-config.json. An absent property,
    # $null, or empty array all mean "no override" and return $null to the caller.
    $dnsServers = $null
    if ($settings.PSObject.Properties['dnsServers'] -and $null -ne $settings.dnsServers) {
        $dnsArray = @($settings.dnsServers)
        if ($dnsArray.Count -gt 0) {
            foreach ($dnsIP in $dnsArray) {
                if ([string]::IsNullOrWhiteSpace([string]$dnsIP)) {
                    Write-AzLocalLog "dnsServers entry in network settings JSON is empty or whitespace." -Level Error
                    throw "dnsServers entries cannot be empty. Provide one or more valid IP addresses, or omit the 'dnsServers' field to use the config default."
                }
                if (-not [System.Net.IPAddress]::TryParse([string]$dnsIP, [ref]$ipRef)) {
                    Write-AzLocalLog "Invalid DNS server IP address '$dnsIP' in network settings JSON." -Level Error
                    throw "Invalid dnsServers entry '$dnsIP'. Provide a valid IPv4 or IPv6 address."
                }
            }
            $dnsServers = [string[]]$dnsArray
            Write-Verbose "dnsServers override loaded from JSON ($($dnsServers.Count) server(s))."
        }
    }

    # For Disaggregated (SAN) deployments, also extract SAN-specific settings.
    # Expected optional sanSettings block:
    # "sanSettings": {
    # "infraVolLunId": "PURE1234567890ABCDEF",
    # "infraPerfLunId": "PURE0987654321MNOPQR",
    # "sanNetworkAdapterName": "ethernet 3",
    # "sanNetworkVlanId": 711,
    # "sanNetworkAddressPrefix": "10.10.30.0/24"
    # }
    $sanSettings = $null
    if ($TypeOfDeployment -eq 'Disaggregated') {
        if (-not $settings.PSObject.Properties['sanSettings'] -or -not $settings.sanSettings) {
            throw "Disaggregated deployments require a 'sanSettings' block in the network settings JSON (infraVolLunId, infraPerfLunId, sanNetworkAdapterName, sanNetworkVlanId, sanNetworkAddressPrefix)."
        }
        $san = $settings.sanSettings
        $requiredSan = @('infraVolLunId', 'infraPerfLunId', 'sanNetworkAdapterName', 'sanNetworkVlanId', 'sanNetworkAddressPrefix')
        foreach ($f in $requiredSan) {
            if (-not $san.PSObject.Properties[$f] -or [string]::IsNullOrWhiteSpace([string]$san.$f)) {
                throw "Disaggregated network settings JSON is missing required sanSettings field '$f'."
            }
        }
        $sanVlan = 0
        if (-not [int]::TryParse([string]$san.sanNetworkVlanId, [ref]$sanVlan) -or $sanVlan -lt 0 -or $sanVlan -gt 4095) {
            throw "sanSettings.sanNetworkVlanId must be an integer 0-4095."
        }
        if ([string]$san.sanNetworkAddressPrefix -notmatch '^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$') {
            throw "sanSettings.sanNetworkAddressPrefix must be valid CIDR notation (e.g. 10.10.30.0/24)."
        }
        $sanSettings = [PsCustomObject][Ordered]@{
            infraVolLunId           = $san.infraVolLunId
            infraPerfLunId          = $san.infraPerfLunId
            sanNetworkAdapterName   = $san.sanNetworkAdapterName
            sanNetworkVlanId        = $sanVlan
            sanNetworkAddressPrefix = $san.sanNetworkAddressPrefix
        }
    }

    Write-AzLocalLog "Network settings loaded from JSON successfully." -Level Success

    return [PsCustomObject][Ordered]@{
        subnetMask        = $settings.subnetMask
        defaultGateway    = $settings.defaultGateway
        startingIPAddress = $settings.startingIPAddress
        endingIPAddress   = $settings.endingIPAddress
        nodeIPAddresses   = $nodeIPs
        dnsServers        = $dnsServers
        sanSettings       = $sanSettings
    }
}