DSCResources/MSFT_xRoute/MSFT_xRoute.psm1

$script:ResourceRootPath = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent)

# Import the xNetworking Resource Module (to import the common modules)
Import-Module -Name (Join-Path -Path $script:ResourceRootPath -ChildPath 'xNetworking.psd1')

# Import Localization Strings
$localizedData = Get-LocalizedData `
    -ResourceName 'MSFT_xRoute' `
    -ResourcePath (Split-Path -Parent $Script:MyInvocation.MyCommand.Path)

<#
    .SYNOPSIS
    Returns the current state of a Route for an interface.
 
    .PARAMETER InterfaceAlias
    Specifies the alias of a network interface.
 
    .PARAMETER AddressFamily
    Specify the IP address family.
 
    .PARAMETER DestinationPrefix
    Specifies a destination prefix of an IP route.
    A destination prefix consists of an IP address prefix and a prefix length, separated by a slash (/).
 
    .PARAMETER NextHop
    Specifies the next hop for the IP route.
#>

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $InterfaceAlias,

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

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $DestinationPrefix,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String] $NextHop
    )

    Write-Verbose -Message ( @(
        "$($MyInvocation.MyCommand): "
        $($LocalizedData.GettingRouteMessage) `
            -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
        ) -join '' )

    # Lookup the existing Route
    $route = Get-Route @PSBoundParameters

    $returnValue = @{
        InterfaceAlias    = $InterfaceAlias
        AddressFamily     = $AddressFamily
        DestinationPrefix = $DestinationPrefix
        NextHop           = $NextHop
    }
    if ($route)
    {
        Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($LocalizedData.RouteExistsMessage) `
                -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
            ) -join '' )

        $returnValue += @{
            Ensure            = 'Present'
            RouteMetric       = [Uint16] $route.RouteMetric
            Publish           = $route.Publish
            PreferredLifetime = [Double] $route.PreferredLifetime.TotalSeconds
        }
    }
    else
    {
        Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($LocalizedData.RouteDoesNotExistMessage) `
                -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
            ) -join '' )

        $returnValue += @{
            Ensure = 'Absent'
        }
    }

    $returnValue
} # Get-TargetResource

<#
    .SYNOPSIS
    Sets a Route for an interface.
 
    .PARAMETER InterfaceAlias
    Specifies the alias of a network interface.
 
    .PARAMETER AddressFamily
    Specify the IP address family.
 
    .PARAMETER DestinationPrefix
    Specifies a destination prefix of an IP route.
    A destination prefix consists of an IP address prefix and a prefix length, separated by a slash (/).
 
    .PARAMETER NextHop
    Specifies the next hop for the IP route.
 
    .PARAMETER Ensure
    Specifies whether the route should exist.
 
    .PARAMETER RouteMetric
    Specifies an integer route metric for an IP route.
 
    .PARAMETER Publish
    Specifies the publish setting of an IP route.
 
    .PARAMETER PreferredLifetime
    Specifies a preferred lifetime in seconds of an IP route.
#>

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $InterfaceAlias,

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

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $DestinationPrefix,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $NextHop,

        [ValidateSet('Present', 'Absent')]
        [String]
        $Ensure = 'Present',

        [Uint16]
        $RouteMetric = 256,

        [ValidateSet('No', 'Yes', 'Age')]
        [String]
        $Publish = 'No',

        [Double]
        $PreferredLifetime
    )

    # Remove any parameters that can't be splatted.
    $null = $PSBoundParameters.Remove('Ensure')

    # Lookup the existing Route
    $route = Get-Route @PSBoundParameters

    if ($Ensure -eq 'Present')
    {
        Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($LocalizedData.EnsureRouteExistsMessage) `
                -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
            ) -join '' )

        if ($route)
        {
            # The Route exists - update it
            Set-NetRoute @PSBoundParameters `
                -Confirm:$false `
                -ErrorAction Stop

            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.RouteUpdatedMessage) `
                    -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
                ) -join '' )
        }
        else
        {
            # The Route does not exit - create it
            New-NetRoute @PSBoundParameters `
                -ErrorAction Stop

            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.RouteCreatedMessage) `
                    -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
                ) -join '' )
        }
    }
    else
    {
        Write-Verbose -Message ( @(
            "$($MyInvocation.MyCommand): "
            $($LocalizedData.EnsureRouteDoesNotExistMessage) `
                -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
            ) -join '' )

        if ($route)
        {
            <#
            The Route exists - remove it
            Use the parameters passed to Set-TargetResource to delete the appropriate route.
            Clear the Publish and PreferredLifetime parameters so they aren't passed to the
            Remove-NetRoute cmdlet.
            #>


            $null = $PSBoundParameters.Remove('Publish')
            $null = $PSBoundParameters.Remove('PreferredLifetime')

            Remove-NetRoute @PSBoundParameters `
                -Confirm:$false `
                -ErrorAction Stop

            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                $($LocalizedData.RouteRemovedMessage) `
                    -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
                ) -join '' )
        } # if
    } # if
} # Set-TargetResource

<#
    .SYNOPSIS
    Tests the state of a Route on an interface.
 
    .PARAMETER InterfaceAlias
    Specifies the alias of a network interface.
 
    .PARAMETER AddressFamily
    Specify the IP address family.
 
    .PARAMETER DestinationPrefix
    Specifies a destination prefix of an IP route.
    A destination prefix consists of an IP address prefix and a prefix length, separated by a slash (/).
 
    .PARAMETER NextHop
    Specifies the next hop for the IP route.
 
    .PARAMETER Ensure
    Specifies whether the route should exist.
 
    .PARAMETER RouteMetric
    Specifies an integer route metric for an IP route.
 
    .PARAMETER Publish
    Specifies the publish setting of an IP route.
 
    .PARAMETER PreferredLifetime
    Specifies a preferred lifetime in seconds of an IP route.
#>

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $InterfaceAlias,

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

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $DestinationPrefix,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $NextHop,

        [ValidateSet('Present', 'Absent')]
        [String]
        $Ensure = 'Present',

        [Uint16]
        $RouteMetric = 256,

        [ValidateSet('No', 'Yes', 'Age')]
        [String]
        $Publish = 'No',

        [Double]
        $PreferredLifetime
    )

    Write-Verbose -Message ( @(
        "$($MyInvocation.MyCommand): "
        $($LocalizedData.TestingRouteMessage) `
           -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
        ) -join '' )

    # Flag to signal whether settings are correct
    [Boolean] $desiredConfigurationMatch = $true

    # Remove any parameters that can't be splatted.
    $null = $PSBoundParameters.Remove('Ensure')

    # Check the parameters
    Assert-ResourceProperty @PSBoundParameters

    # Lookup the existing Route
    $route = Get-Route @PSBoundParameters

    if ($Ensure -eq 'Present')
    {
        # The route should exist
        if ($route)
        {
            # The route exists and does - but check the parameters
            if (($PSBoundParameters.ContainsKey('RouteMetric')) `
                -and ($route.RouteMetric -ne $RouteMetric))
            {
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.RoutePropertyNeedsUpdateMessage) `
                       -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop,'RouteMetric' `
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }

            if (($PSBoundParameters.ContainsKey('Publish')) `
                -and ($route.Publish -ne $Publish))
            {
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.RoutePropertyNeedsUpdateMessage) `
                       -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop,'Publish' `
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }

            if (($PSBoundParameters.ContainsKey('PreferredLifetime')) `
                -and ($route.PreferredLifetime.TotalSeconds -ne $PreferredLifetime))
            {
                Write-Verbose -Message ( @(
                    "$($MyInvocation.MyCommand): "
                    $($LocalizedData.RoutePropertyNeedsUpdateMessage) `
                       -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop,'PreferredLifetime' `
                    ) -join '' )
                $desiredConfigurationMatch = $false
            }
        }
        else
        {
            # The route doesn't exist but should
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                 $($LocalizedData.RouteDoesNotExistButShouldMessage) `
                    -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
                ) -join '' )
            $desiredConfigurationMatch = $false
        }
    }
    else
    {
        # The route should not exist
        if ($route)
        {
            # The route exists but should not
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                 $($LocalizedData.RouteExistsButShouldNotMessage) `
                    -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
                ) -join '' )
            $desiredConfigurationMatch = $false
        }
        else
        {
            # The route does not exist and should not
            Write-Verbose -Message ( @(
                "$($MyInvocation.MyCommand): "
                 $($LocalizedData.RouteDoesNotExistAndShouldNotMessage) `
                    -f $AddressFamily,$InterfaceAlias,$DestinationPrefix,$NextHop `
                ) -join '' )
        }
    } # if
    return $desiredConfigurationMatch
} # Test-TargetResource

<#
    .SYNOPSIS
    This function looks up the route using the parameters and returns
    it. If the route is not found $null is returned.
 
    .PARAMETER InterfaceAlias
    Specifies the alias of a network interface.
 
    .PARAMETER AddressFamily
    Specify the IP address family.
 
    .PARAMETER DestinationPrefix
    Specifies a destination prefix of an IP route.
    A destination prefix consists of an IP address prefix and a prefix length, separated by a slash (/).
 
    .PARAMETER NextHop
    Specifies the next hop for the IP route.
 
    .PARAMETER Ensure
    Specifies whether the route should exist.
 
    .PARAMETER RouteMetric
    Specifies an integer route metric for an IP route.
 
    .PARAMETER Publish
    Specifies the publish setting of an IP route.
 
    .PARAMETER PreferredLifetime
    Specifies a preferred lifetime in seconds of an IP route.
 
#>

Function Get-Route
{
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $InterfaceAlias,

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

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $DestinationPrefix,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $NextHop,

        [ValidateSet('Present', 'Absent')]
        [String]
        $Ensure = 'Present',

        [Uint16]
        $RouteMetric = 256,

        [ValidateSet('No', 'Yes', 'Age')]
        [String]
        $Publish = 'No',

        [Double]
        $PreferredLifetime
    )

    try
    {
        $route = Get-NetRoute `
            -InterfaceAlias $InterfaceAlias `
            -AddressFamily $AddressFamily `
            -DestinationPrefix $DestinationPrefix `
            -NextHop $NextHop `
            -ErrorAction Stop
    }
    catch [Microsoft.PowerShell.Cmdletization.Cim.CimJobException]
    {
        $route = $null
    }
    catch
    {
        Throw $_
    }
    Return $route
}

<#
    .SYNOPSIS
    This function validates the parameters passed. Called by Test-Resource.
    Will throw an error if any parameters are invalid.
 
    .PARAMETER InterfaceAlias
    Specifies the alias of a network interface.
 
    .PARAMETER AddressFamily
    Specify the IP address family.
 
    .PARAMETER DestinationPrefix
    Specifies a destination prefix of an IP route.
    A destination prefix consists of an IP address prefix and a prefix length, separated by a slash (/).
 
    .PARAMETER NextHop
    Specifies the next hop for the IP route.
 
    .PARAMETER Ensure
    Specifies whether the route should exist.
 
    .PARAMETER RouteMetric
    Specifies an integer route metric for an IP route.
 
    .PARAMETER Publish
    Specifies the publish setting of an IP route.
 
    .PARAMETER PreferredLifetime
    Specifies a preferred lifetime in seconds of an IP route.
#>

Function Assert-ResourceProperty
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $InterfaceAlias,

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

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $DestinationPrefix,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [String]
        $NextHop,

        [ValidateSet('Present', 'Absent')]
        [String]
        $Ensure = 'Present',

        [Uint16]
        $RouteMetric = 256,

        [ValidateSet('No', 'Yes', 'Age')]
        [String]
        $Publish = 'No',

        [Double]
        $PreferredLifetime
    )

    # Validate the Adapter exists
    if (-not (Get-NetAdapter | Where-Object -Property Name -EQ $InterfaceAlias ))
    {
        $errorId = 'InterfaceNotAvailable'
        $errorCategory = [System.Management.Automation.ErrorCategory]::DeviceError
        $errorMessage = $($LocalizedData.InterfaceNotAvailableError) -f $InterfaceAlias
        $exception = New-Object -TypeName System.InvalidOperationException `
            -ArgumentList $errorMessage
        $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
            -ArgumentList $exception, $errorId, $errorCategory, $null

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    # Validate the DestinationPrefix Parameter
    $Components = $DestinationPrefix -split '/'
    $Prefix = $Components[0]
    $Subnet = $Components[1]

    if (-not ([System.Net.Ipaddress]::TryParse($Prefix, [ref]0)))
    {
        $errorId = 'AddressFormatError'
        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
        $errorMessage = $($LocalizedData.AddressFormatError) -f $Prefix
        $exception = New-Object -TypeName System.InvalidOperationException `
            -ArgumentList $errorMessage
        $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
            -ArgumentList $exception, $errorId, $errorCategory, $null

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    $detectedAddressFamily = ([System.Net.IPAddress] $Prefix).AddressFamily.ToString()
    if (($detectedAddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork.ToString()) `
        -and ($AddressFamily -ne 'IPv4'))
    {
        $errorId = 'AddressMismatchError'
        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
        $errorMessage = $($LocalizedData.AddressIPv4MismatchError) -f $Prefix,$AddressFamily
        $exception = New-Object -TypeName System.InvalidOperationException `
            -ArgumentList $errorMessage
        $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
            -ArgumentList $exception, $errorId, $errorCategory, $null

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    if (($detectedAddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6.ToString()) `
        -and ($AddressFamily -ne 'IPv6'))
    {
        $errorId = 'AddressMismatchError'
        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
        $errorMessage = $($LocalizedData.AddressIPv6MismatchError) -f $Prefix,$AddressFamily
        $exception = New-Object -TypeName System.InvalidOperationException `
            -ArgumentList $errorMessage
        $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
            -ArgumentList $exception, $errorId, $errorCategory, $null

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    # Validate the NextHop Parameter
    if (-not ([System.Net.Ipaddress]::TryParse($NextHop, [ref]0)))
    {
        $errorId = 'AddressFormatError'
        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
        $errorMessage = $($LocalizedData.AddressFormatError) -f $NextHop
        $exception = New-Object -TypeName System.InvalidOperationException `
            -ArgumentList $errorMessage
        $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
            -ArgumentList $exception, $errorId, $errorCategory, $null

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    $detectedAddressFamily = ([System.Net.IPAddress] $NextHop).AddressFamily.ToString()
    if (($detectedAddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork.ToString()) `
        -and ($AddressFamily -ne 'IPv4'))
    {
        $errorId = 'AddressMismatchError'
        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
        $errorMessage = $($LocalizedData.AddressIPv4MismatchError) -f $NextHop,$AddressFamily
        $exception = New-Object -TypeName System.InvalidOperationException `
            -ArgumentList $errorMessage
        $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
            -ArgumentList $exception, $errorId, $errorCategory, $null

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }

    if (($detectedAddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetworkV6.ToString()) `
        -and ($AddressFamily -ne 'IPv6'))
    {
        $errorId = 'AddressMismatchError'
        $errorCategory = [System.Management.Automation.ErrorCategory]::InvalidArgument
        $errorMessage = $($LocalizedData.AddressIPv6MismatchError) -f $NextHop,$AddressFamily
        $exception = New-Object -TypeName System.InvalidOperationException `
            -ArgumentList $errorMessage
        $errorRecord = New-Object -TypeName System.Management.Automation.ErrorRecord `
            -ArgumentList $exception, $errorId, $errorCategory, $null

        $PSCmdlet.ThrowTerminatingError($errorRecord)
    }
}

Export-ModuleMember -Function *-TargetResource