diff --git a/changelogs/fragments/win_dns_client_ipv6.yaml b/changelogs/fragments/win_dns_client_ipv6.yaml new file mode 100644 index 00000000000..07b6a2976b5 --- /dev/null +++ b/changelogs/fragments/win_dns_client_ipv6.yaml @@ -0,0 +1,2 @@ +minor_changes: +- win_dns_client - Added support for setting IPv6 DNS servers - https://github.com/ansible/ansible/issues/55962 diff --git a/lib/ansible/modules/windows/win_dns_client.ps1 b/lib/ansible/modules/windows/win_dns_client.ps1 index fd885bac85d..46b5751d26f 100644 --- a/lib/ansible/modules/windows/win_dns_client.ps1 +++ b/lib/ansible/modules/windows/win_dns_client.ps1 @@ -12,6 +12,21 @@ Set-StrictMode -Version 2 $ErrorActionPreference = "Stop" $ConfirmPreference = "None" +Set-Variable -Visibility Public -Option ReadOnly,AllScope,Constant -Name "AddressFamilies" -Value @( + [System.Net.Sockets.AddressFamily]::InterNetworkV6, + [System.Net.Sockets.AddressFamily]::InterNetwork +) +$result = @{changed=$false} + +$params = Parse-Args -arguments $args -supports_check_mode $true +Set-Variable -Visibility Public -Option ReadOnly,AllScope,Constant -Name "log_path" -Value ( + Get-AnsibleParam $params "log_path" +) +$adapter_names = Get-AnsibleParam $params "adapter_names" -Default "*" +$dns_servers = Get-AnsibleParam $params "dns_servers" -aliases "ipv4_addresses","ip_addresses","addresses" -FailIfEmpty $result +$check_mode = Get-AnsibleParam $params "_ansible_check_mode" -Default $false + + Function Write-DebugLog { Param( [string]$msg @@ -23,10 +38,6 @@ Function Write-DebugLog { $msg = "$date_str $msg" Write-Debug $msg - - $log_path = $null - $log_path = Get-AnsibleParam -obj $params -name "log_path" - if($log_path) { Add-Content $log_path $msg } @@ -74,7 +85,7 @@ Function Get-DnsClientServerAddressLegacy { return @( # IPv4 values [PSCustomObject]@{InterfaceAlias=$InterfaceAlias;InterfaceIndex=$idx;AddressFamily=2;ServerAddresses=$adapter_config.DNSServerSearchOrder}; - # IPv6, only here for completeness since we don't support it yet + # IPv6 values [PSCustomObject]@{InterfaceAlias=$InterfaceAlias;InterfaceIndex=$idx;AddressFamily=23;ServerAddresses=@()}; ) } @@ -99,7 +110,7 @@ Function Set-DnsClientServerAddressLegacy { $arguments = @{} } Else { - $arguments = @{ DNSServerSearchOrder = $ServerAddresses } + $arguments = @{ DNSServerSearchOrder = [string[]]$ServerAddresses } } $res = Invoke-CimMethod -InputObject $adapter_config -MethodName SetDNSServerSearchOrder -Arguments $arguments @@ -112,40 +123,47 @@ If(-not $(Get-Command Set-DnsClientServerAddress -ErrorAction SilentlyContinue)) New-Alias Set-DnsClientServerAddress Set-DnsClientServerAddressLegacy } -Function Get-DnsClientMatch { +Function Test-DnsClientMatch { Param( [string] $adapter_name, - [string[]] $ipv4_addresses + [System.Net.IPAddress[]] $dns_servers ) - Write-DebugLog ("Getting DNS config for adapter {0}" -f $adapter_name) - $current_dns_all = Get-DnsClientServerAddress -InterfaceAlias $adapter_name - - Write-DebugLog ("Current DNS settings: " + $($current_dns_all | Out-String)) - - $current_dns_v4 = ($current_dns_all | Where-Object AddressFamily -eq 2 <# IPv4 #>).ServerAddresses - - If (($null -eq $current_dns_v4) -and ($null -eq $ipv4_addresses)) { - $v4_match = $True - } - - ElseIf (($null -eq $current_dns_v4) -or ($null -eq $ipv4_addresses)) { - $v4_match = $False + $current_dns = ([System.Net.IPAddress[]]( + (Get-DnsClientServerAddress -InterfaceAlias $adapter_name).ServerAddresses) | Where-Object { + (Assert-IPAddress $_) -and ($_.AddressFamily -in $AddressFamilies) + } + ) + Write-DebugLog ("Current DNS settings: {0}" -f ([string[]]$dns_servers -join ", ")) + + if(($null -eq $current_dns) -and ($null -eq $dns_servers)) { + Write-DebugLog "Neither are dns servers configured nor specified within the playbook." + return $true + } elseif ($null -eq $current_dns) { + Write-DebugLog "There are currently no dns servers specified, but they should be present." + return $false + } elseif ($null -eq $dns_servers) { + Write-DebugLog "There are currently dns servers specified, but they should be absent." + return $false + } + foreach($address in $current_dns) { + if($address -notin $dns_servers) { + Write-DebugLog "There are currently fewer dns servers present than specified within the playbook." + return $false + } } - - Else { - $v4_match = @(Compare-Object $current_dns_v4 $ipv4_addresses -SyncWindow 0).Count -eq 0 + foreach($address in $dns_servers) { + if($address -notin $current_dns) { + Write-DebugLog "There are currently further dns servers present than specified within the playbook." + return $false + } } - - # TODO: implement IPv6 - - Write-DebugLog ("Current DNS settings match ({0}) : {1}" -f ($ipv4_addresses -join ", "), $v4_match) - - return $v4_match + Write-DebugLog ("Current DNS settings match ({0})." -f ([string[]]$dns_servers -join ", ")) + return $true } -Function Validate-IPAddress { +Function Assert-IPAddress { Param([string] $address) $addrout = $null @@ -157,72 +175,54 @@ Function Set-DnsClientAddresses { Param( [string] $adapter_name, - [string[]] $ipv4_addresses + [System.Net.IPAddress[]] $dns_servers ) - Write-DebugLog ("Setting DNS addresses for adapter {0} to ({1})" -f $adapter_name, ($ipv4_addresses -join ", ")) + Write-DebugLog ("Setting DNS addresses for adapter {0} to ({1})" -f $adapter_name, ([string[]]$dns_servers -join ", ")) - If ($null -eq $ipv4_addresses) { + If ($dns_servers) { + Set-DnsClientServerAddress -InterfaceAlias $adapter_name -ServerAddresses $dns_servers + } Else { Set-DnsClientServerAddress -InterfaceAlias $adapter_name -ResetServerAddress } - - Else { - # this silently ignores invalid IPs, so we validate parseability ourselves up front... - Set-DnsClientServerAddress -InterfaceAlias $adapter_name -ServerAddresses $ipv4_addresses - } - - # TODO: implement IPv6 } -$result = @{changed=$false} - -$params = Parse-Args -arguments $args -supports_check_mode $true - -$adapter_names = Get-AnsibleParam $params "adapter_names" -Default "*" -$ipv4_addresses = Get-AnsibleParam $params "ipv4_addresses" -FailIfEmpty $result - -If($ipv4_addresses -is [string]) { - If($ipv4_addresses.Length -gt 0) { - $ipv4_addresses = @($ipv4_addresses) - } - Else { - $ipv4_addresses = @() +if($dns_servers -is [string]) { + if($dns_servers.Length -gt 0) { + $dns_servers = @($dns_servers) + } else { + $dns_servers = @() } } +# Using object equals here, to check for exact match (without implicit type conversion) +if([System.Object]::Equals($adapter_names, "*")) { + $adapter_names = Get-NetAdapter | Select-Object -ExpandProperty Name +} +if($adapter_names -is [string]) { + $adapter_names = @($adapter_names) +} -$check_mode = Get-AnsibleParam $params "_ansible_check_mode" -Default $false Try { - Write-DebugLog ("Validating adapter name {0}" -f $adapter_names) - - $adapters = @($adapter_names) - - If($adapter_names -eq "*") { - $adapters = Get-NetAdapter | Select-Object -ExpandProperty Name - } - # TODO: add support for an actual list of adapter names - # validate network adapter names - ElseIf(@(Get-NetAdapter | Where-Object Name -eq $adapter_names).Count -eq 0) { - throw "Invalid network adapter name: {0}" -f $adapter_names - } - - Write-DebugLog ("Validating IP addresses ({0})" -f ($ipv4_addresses -join ", ")) - - $invalid_addresses = @($ipv4_addresses | Where-Object { -not (Validate-IPAddress $_) }) - - If($invalid_addresses.Count -gt 0) { + Write-DebugLog ("Validating IP addresses ({0})" -f ($dns_servers -join ", ")) + $invalid_addresses = @($dns_servers | Where-Object { -not (Assert-IPAddress $_) }) + if($invalid_addresses.Count -gt 0) { throw "Invalid IP address(es): ({0})" -f ($invalid_addresses -join ", ") } - ForEach($adapter_name in $adapters) { - $result.changed = $result.changed -or (-not (Get-DnsClientMatch $adapter_name $ipv4_addresses)) - - If($result.changed) { - If(-not $check_mode) { - Set-DnsClientAddresses $adapter_name $ipv4_addresses - } - Else { + foreach($adapter_name in $adapter_names) { + Write-DebugLog ("Validating adapter name {0}" -f $adapter_name) + if(-not (Get-DnsClientServerAddress -InterfaceAlias $adapter_name)) { + # TODO: add support for an actual list of adapter names + # validate network adapter names + throw "Invalid network adapter name: {0}" -f $adapter_name + } + if(-not (Test-DnsClientMatch $adapter_name $dns_servers)) { + $result.changed = $true + if(-not $check_mode) { + Set-DnsClientAddresses $adapter_name $dns_servers + } else { Write-DebugLog "Check mode, skipping" } } diff --git a/lib/ansible/modules/windows/win_dns_client.py b/lib/ansible/modules/windows/win_dns_client.py index 9483a23e76e..c42085fbeab 100644 --- a/lib/ansible/modules/windows/win_dns_client.py +++ b/lib/ansible/modules/windows/win_dns_client.py @@ -19,15 +19,18 @@ options: adapter_names: description: - Adapter name or list of adapter names for which to manage DNS settings ('*' is supported as a wildcard value). - - The adapter name used is the connection caption in the Network Control Panel or via C(Get-NetAdapter), eg C(Local Area Connection). - type: str + - The adapter name used is the connection caption in the Network Control Panel or the InterfaceAlias of C(Get-DnsClientServerAddress). + type: list required: yes - ipv4_addresses: + dns_servers: description: - - Single or ordered list of DNS server IPv4 addresses to configure for lookup. An empty list will configure the adapter to use the + - Single or ordered list of DNS servers (IPv4 and IPv6 addresses) to configure for lookup. An empty list will configure the adapter to use the DHCP-assigned values on connections where DHCP is enabled, or disable DNS lookup on statically-configured connections. - type: str + - IPv6 DNS servers can only be set on Windows Server 2012 or newer, older hosts can only set IPv4 addresses. + - Before 2.10 use ipv4_addresses instead. + type: list required: yes + aliases: [ "ipv4_addresses", "ip_addresses", "addresses" ] notes: - When setting an empty list of DNS server addresses on an adapter with DHCP enabled, a change will always be registered, since it is not possible to detect the difference between a DHCP-sourced server value and one that is statically set. @@ -39,20 +42,27 @@ EXAMPLES = r''' - name: Set a single address on the adapter named Ethernet win_dns_client: adapter_names: Ethernet - ipv4_addresses: 192.168.34.5 + dns_servers: 192.168.34.5 - name: Set multiple lookup addresses on all visible adapters (usually physical adapters that are in the Up state), with debug logging to a file win_dns_client: adapter_names: '*' - ipv4_addresses: + dns_servers: - 192.168.34.5 - 192.168.34.6 log_path: C:\dns_log.txt +- name: Set IPv6 DNS servers on the adapter named Ethernet + win_dns_client: + adapter_names: Ethernet + dns_servers: + - '2001:db8::2' + - '2001:db8::3' + - name: Configure all adapters whose names begin with Ethernet to use DHCP-assigned DNS values win_dns_client: adapter_names: 'Ethernet*' - ipv4_addresses: [] + dns_servers: [] ''' RETURN = r''' diff --git a/test/integration/targets/win_dns_client/tasks/main.yml b/test/integration/targets/win_dns_client/tasks/main.yml index c9cdfe8f1b9..d38e80865a0 100644 --- a/test/integration/targets/win_dns_client/tasks/main.yml +++ b/test/integration/targets/win_dns_client/tasks/main.yml @@ -176,3 +176,31 @@ that: - set_dhcp is changed - set_dhcp_actual.stdout_lines == [] + +# Legacy WMI does not support setting IPv6 addresses so we can only test this on newer hosts that have the new cmdlets +- name: check if server supports IPv6 + win_shell: if (Get-Command -Name Get-NetAdapter -ErrorAction SilentlyContinue) { $true } else { $false } + changed_when: no + register: new_os + +- name: run IPv6 tests + when: new_os.stdout | trim | bool + block: + - name: set IPv6 DNS address + win_dns_client: + adapter_names: '{{ network_adapter_name }}' + dns_servers: + - 2001:db8::1 + - 2001:db8::2 + register: set_ipv6 + + - name: get result of set IPv6 DNS address + win_shell: (Get-DnsClientServerAddress -InterfaceAlias '{{ network_adapter_name }}' -AddressFAmily IPv6).ServerAddresses + changed_when: no + register: set_ipv6_actual + + - name: assert set IPv6 DNS address + assert: + that: + - set_ipv6 is changed + - set_ipv6_actual.stdout_lines == ['2001:db8::1', '2001:db8::2'] diff --git a/test/sanity/ignore.txt b/test/sanity/ignore.txt index a733280de33..fecc835ea1a 100644 --- a/test/sanity/ignore.txt +++ b/test/sanity/ignore.txt @@ -4987,7 +4987,6 @@ lib/ansible/modules/windows/win_copy.ps1 pslint:PSUseApprovedVerbs lib/ansible/modules/windows/win_credential.ps1 pslint:PSCustomUseLiteralPath lib/ansible/modules/windows/win_credential.ps1 validate-modules:parameter-type-not-in-doc lib/ansible/modules/windows/win_dns_client.ps1 pslint:PSCustomUseLiteralPath -lib/ansible/modules/windows/win_dns_client.ps1 pslint:PSUseApprovedVerbs lib/ansible/modules/windows/win_domain.ps1 pslint:PSAvoidUsingEmptyCatchBlock # Keep lib/ansible/modules/windows/win_domain.ps1 pslint:PSUseApprovedVerbs lib/ansible/modules/windows/win_domain_controller.ps1 pslint:PSAvoidGlobalVars # New PR