DSCResources/Helper.psm1

# Localized messages
data LocalizedData
{
    # culture="en-US"
    ConvertFrom-StringData @'
RoleNotFound = Please ensure that the PowerShell module for role {0} is installed
InvalidIPAddressFormat = Value of {0} property is not in a valid IP address format. Specify a valid IP address format and try again.
InvalidIPAddressFamily = The IP address {0} is not a valid {1} address. Specify a valid IP address in {1} format and try again.
InvalidTimeSpanFormat = Value of {0} property is not in a valid timespan format. Specify the timespan in days.hrs:mins:secs format and try again.
InvalidScopeIdSubnetMask = Value of byte {0} in {1} ({2}) is not valid. Binary AND with byte {0} in SubnetMask ({3}) should be equal to byte {0} in ScopeId ({4}).
InvalidStartAndEndRangeMessage = Value of IPStartRange ({0}) and IPEndRange ({1}) are not valid. Start should be lower than end.
'@

}

# Internal function to throw terminating error with specified ErrorCategory, ErrorId and ErrorMessage
function New-TerminatingError
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $ErrorId,
        
        [Parameter(Mandatory = $true)]
        [String]
        $ErrorMessage,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ErrorCategory]
        $ErrorCategory
    )
    
    $exception = New-Object -TypeName System.InvalidOperationException -ArgumentList $ErrorMessage
    $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord -ArgumentList $exception, $ErrorId, $ErrorCategory, $null
    throw $errorRecord
}

# Internal function to translate a string to valid IPAddress format
function Get-ValidIPAddress
{
    [CmdletBinding()]
    [OutputType([System.Net.IPAddress])]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $IpString,

        [Parameter(Mandatory = $true)]
        [ValidateSet('IPv4')]
        [String]
        $AddressFamily,

        [Parameter(Mandatory = $true)]
        [string]
        $ParameterName
    )

    $ipAddressFamily = ''
    if($AddressFamily -eq 'IPv4')
    {
        $ipAddressFamily = 'InterNetwork'
    }
    else
    {
        $ipAddressFamily = 'InterNetworkV6'
    }

    [System.Net.IPAddress]$ipAddress = $null
    $result = [System.Net.IPAddress]::TryParse($IpString, [ref]$ipAddress)
    if(-not $result)
    {
        $errorMsg = $($LocalizedData.InvalidIPAddressFormat) -f $ParameterName
        New-TerminatingError -ErrorId 'NotValidIPAddress' -ErrorMessage $errorMsg -ErrorCategory InvalidType
    }

    if($ipAddress.AddressFamily -ne $ipAddressFamily)
    {
        $errorMsg = $($LocalizedData.InvalidIPAddressFamily) -f $ipAddress,$AddressFamily
        New-TerminatingError -ErrorId 'InvalidIPAddressFamily' -ErrorMessage $errorMsg -ErrorCategory SyntaxError
    } 
    
    $ipAddress
}

# Internal function to assert if the role specific module is installed or not
function Assert-Module
{
    [CmdletBinding()]
    param
    (
        [Parameter()]
        [String]
        $ModuleName = 'DHCPServer'
    )

    if(! (Get-Module -Name $ModuleName -ListAvailable))
    {
        $errorMsg = $($LocalizedData.RoleNotFound) -f $ModuleName
        New-TerminatingError -ErrorId 'ModuleNotFound' -ErrorMessage $errorMsg -ErrorCategory ObjectNotFound
    }
}

<#
    .SYNOPSIS
        Internal function to assert if values of ScopeId/SubnetMask/IPStartRange/IPEndRange make sense.
 
    .DESCRIPTION
        Internal function used to assert if value of following parameters are correct:
        - ScopeID
        - SubnetMask
        - IPStartRange
        - IPEndRange
 
        It validates them against simple rules:
        - Has to be correct (IPv4) address
        - Anything but SubnetMask has to follow the rule that:
        (TokenFromParameter) -band (TokenFromSubnetMask) = (TokenFromScopeId)
        - IPStartRange has to be before IPEndRange
        Implementation for IPv4.
 
    .PARAMETER ScopeId
    String version of ScopeId.
 
    .PARAMETER SubnetMask
    String version of SubnetMask.
 
    .PARAMETER IPStartRange
    String version of StartRange.
 
    .PARAMETER IPEndRange
    String version of EndRange.
 
    .PARAMETER AddressFamily
    AddressFamily that IPs should validate against.
 
    .EXAMPLE
    Assert-ScopeParameter -ScopeId 192.168.1.0 -SubnetMask 255.255.255.0 -IPStartRange 192.168.1.1 -IPEndRange 192.168.1.254 -AddressFamily IPv4
    Validates all parameters against rules and returns nothing (all parameters are correct).
 
    .EXAMPLE
    Assert-ScopeParameter -ScopeId 192.168.1.0 -SubnetMask 255.255.240.0 -IPStartRange 192.168.1.1 -IPEndRange 192.168.1.254 -AddressFamily IPv4
    Returns error informing that using specified SubnetMask with specified ScopeId is incorrect:
    Value of byte 3 in ScopeId (1) is not valid. Binary AND with byte 3 in SubnetMask (240) should be equal to byte 3 in ScopeId (1).
#>

function Assert-ScopeParameter
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [String]
        $ScopeId,

        [Parameter(Mandatory = $true)]
        [String]
        $SubnetMask,

        [Parameter(Mandatory = $true)]
        [String]
        $IPStartRange,

        [Parameter(Mandatory = $true)]
        [String]
        $IPEndRange,

        [Parameter(Mandatory = $true)]
        [String]
        $AddressFamily
    )

    # Convert the Subnet Mask to be a valid IPAddress
    $netMask = Get-ValidIpAddress -IpString $SubnetMask -AddressFamily $AddressFamily -ParameterName SubnetMask

    # Convert the ScopeID to be a valid IPAddress
    $scope = Get-ValidIPAddress -IpString $ScopeId -AddressFamily $AddressFamily -ParameterName ScopeId

    # Convert the Start Range to be a valid IPAddress
    $startRange = Get-ValidIpAddress -IpString $IPStartRange -AddressFamily $AddressFamily -ParameterName IPStartRange

    # Convert the End Range to be a valid IPAddress
    $endRange = Get-ValidIpAddress -IpString $IPEndRange -AddressFamily $AddressFamily -ParameterName IPEndRange

    # Check to ensure startRange is smaller than endRange
    if($endRange.Address -lt $startRange.Address)
    {
        $errorMsg = $LocalizedData.InvalidStartAndEndRangeMessage -f $IPStartRange, $IPEndRange
        New-TerminatingError -ErrorId RangeNotCorrect -ErrorMessage $errorMsg -ErrorCategory InvalidArgument
    }

    $addressBytes = @{
        ScopeId      = $scope.GetAddressBytes()
        SubnetMask   = $netMask.GetAddressBytes()
        IPStartRange = $startRange.GetAddressBytes()
        IPEndRange   = $endRange.GetAddressBytes()
    }

    foreach ($parameter in $addressBytes.Keys.Where{ $_ -ne 'SubnetMask' })
    {
        foreach ($ipTokenIndex in 0..3)
        {
            $parameterByte = $addressBytes[$parameter][$ipTokenIndex]
            $subnetMaskByte = $addressBytes['SubnetMask'][$ipTokenIndex]
            $scopeIdByte = $addressBytes['ScopeId'][$ipTokenIndex]
            if(($parameterByte -band $subnetMaskByte) -ne $scopeIdByte)
            {
                $errorMsg = $($LocalizedData.InvalidScopeIdSubnetMask) -f ($ipTokenIndex + 1), $parameter, $parameterByte, $subnetMaskByte, $scopeIdByte
                New-TerminatingError -ErrorId ScopeIdOrMaskIncorrect -ErrorMessage $errorMsg -ErrorCategory InvalidArgument
            }
        }
    }
}

# Internal function to write verbose messages for collection of properties
function Write-PropertyMessage
{
    param
    (
        [Parameter(Mandatory = $true)]
        [Hashtable]
        $Parameters,

        [Parameter(Mandatory = $true)]
        [String[]]
        $KeysToSkip,

        [Parameter(Mandatory = $true)]
        [String]
        $MessageTemplate
    )

    foreach($key in $parameters.keys)
    {
        if($keysToSkip -notcontains $key)
        {
            $msg = $MessageTemplate -f $key,$parameters[$key]
            Write-Verbose -Message $msg
        }
    }
}

# Internal function to translate a string to valid IPAddress format
function Get-ValidTimeSpan
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $TsString,

        [Parameter(Mandatory = $true)]
        [string]
        $ParameterName
    )

    [System.TimeSpan]$timeSpan = New-TimeSpan
    $result = [System.TimeSpan]::TryParse($TsString, [ref]$timeSpan)
    if(-not $result)
    {
        $errorMsg = $($LocalizedData.InvalidTimeSpanFormat) -f $ParameterName
        New-TerminatingError -ErrorId 'NotValidTimeSpan' -ErrorMessage $errorMsg -ErrorCategory InvalidType
    }

    $timeSpan
}