1 #!powershell 2 # Copyright: (c) 2021 Sebastian Gruber ,dacoso GmbH All Rights Reserved. 3 # Copyright: (c) 2019, Hitachi ID Systems, Inc. 4 # SPDX-License-Identifier: GPL-3.0-only 5 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 #AnsibleRequires -CSharpUtil Ansible.Basic 7 8 $spec = @{ 9 options = @{ 10 name = @{ type = "str"; required = $true } 11 port = @{ type = "int"} 12 priority = @{ type = "int"} 13 state = @{ type = "str"; choices = "absent", "present"; default = "present" } 14 ttl = @{ type = "int"; default = "3600" } 15 type = @{ type = "str"; choices = "A","AAAA","CNAME","NS","PTR","SRV","TXT"; required = $true } 16 value = @{ type = "list"; elements = "str"; default = @() ; aliases=@( 'values' )} 17 weight = @{ type = "int"} 18 zone = @{ type = "str"; required = $true } 19 computer_name = @{ type = "str" } 20 } 21 required_if = @(, @("type", "SRV", @("port", "priority", "weight"))) 22 supports_check_mode = $true 23 } 24 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) 25 $name = $module.Params.name 26 $port = $module.Params.port 27 $priority = $module.Params.priority 28 $state = $module.Params.state 29 $ttl = $module.Params.ttl 30 $type = $module.Params.type 31 $values = $module.Params.value 32 $weight = $module.Params.weight 33 $zone = $module.Params.zone 34 $dns_computer_name = $module.Params.computer_name 35 $extra_args = @{} 36 if ($null -ne $dns_computer_name) { 37 $extra_args.ComputerName = $dns_computer_name 38 } 39 if ($state -eq 'present') { 40 if ($values.Count -eq 0) { 41 $module.FailJson("Parameter 'values' must be non-empty when state='present'") 42 } 43 } else { 44 if ($values.Count -ne 0) { 45 $module.FailJson("Parameter 'values' must be undefined or empty when state='absent'") 46 } 47 } 48 # TODO: add warning for forest minTTL override -- see https://docs.microsoft.com/en-us/windows/desktop/ad/configuration-of-ttl-limits 49 if ($ttl -lt 1 -or $ttl -gt 31557600) { 50 $module.FailJson("Parameter 'ttl' must be between 1 and 31557600") 51 } 52 $ttl = New-TimeSpan -Seconds $ttl 53 if (($type -eq 'CNAME' -or $type -eq 'NS' -or $type -eq 'PTR' -or $type -eq 'SRV') -and $null -ne $values -and $values.Count -gt 0 -and $zone[-1] -ne '.') { 54 # CNAMEs and PTRs should be '.'-terminated, or record matching will fail 55 $values = $values | ForEach-Object { 56 if ($_ -Like "*.") { $_ } else { "$_." } 57 } 58 } 59 $record_argument_name = @{ 60 A = "IPv4Address"; 61 AAAA = "IPv6Address"; 62 CNAME = "HostNameAlias"; 63 # MX = "MailExchange"; 64 NS = "NameServer"; 65 PTR = "PtrDomainName"; 66 SRV = "DomainName"; 67 TXT = "DescriptiveText" 68 }[$type] 69 $changes = @{ 70 before = ""; 71 after = "" 72 } 73 $records = Get-DnsServerResourceRecord -ZoneName $zone -Name $name -RRType $type -Node -ErrorAction:Ignore @extra_args | Sort-Object 74 if ($null -ne $records) { 75 # We use [Hashtable]$required_values below as a set rather than a map. 76 # It provides quick lookup to test existing DNS record against. By removing 77 # items as each is processed, whatever remains at the end is missing 78 # content (that needs to be added). 79 $required_values = @{} 80 foreach ($value in $values) { 81 $required_values[$value.ToString()] = $null 82 } 83 foreach ($record in $records) { 84 $record_value = $record.RecordData.$record_argument_name.ToString() 85 if (-Not $required_values.ContainsKey($record_value)) { 86 $record | Remove-DnsServerResourceRecord -ZoneName $zone -Force -WhatIf:$module.CheckMode @extra_args 87 $changes.before += "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN $type $record_value`n" 88 $module.Result.changed = $true 89 } else { 90 if ($type -eq 'SRV') { 91 $record_port_old = $record.RecordData.Port.ToString() 92 $record_priority_old = $record.RecordData.Priority.ToString() 93 $record_weight_old = $record.RecordData.Weight.ToString() 94 if ($record.TimeToLive -ne $ttl -or $port -ne $record_port_old -or $priority -ne $record_priority_old -or $weight -ne $record_weight_old) { 95 $new_record = $record.Clone() 96 $new_record.TimeToLive = $ttl 97 $new_record.RecordData.Port = $port 98 $new_record.RecordData.Priority = $priority 99 $new_record.RecordData.Weight = $weight 100 Set-DnsServerResourceRecord -ZoneName $zone -OldInputObject $record -NewInputObject $new_record -WhatIf:$module.CheckMode @extra_args 101 102 $changes.before += "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN $type $record_value $record_port_old $record_weight_old $record_priority_old`n" 103 $changes.after += "[$zone] $($record.HostName) $($ttl.TotalSeconds) IN $type $record_value $port $weight $priority`n" 104 $module.Result.changed = $true 105 } 106 } elseif ($type -eq "TXT") { 107 $record_descriptivetext_old = $record.RecordData.DescriptiveText.ToString() 108 if ($value -ne $record_descriptivetext_old) { 109 $new_record = $record.Clone() 110 $new_record.RecordData.DescriptiveText = $value 111 Set-DnsServerResourceRecord -ZoneName $zone -OldInputObject $record -NewInputObject $new_record -WhatIf:$module.CheckMode @extra_args 112 $changes.before += "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN $type $record_value $record_descriptivetext_old`n" 113 $changes.after += "[$zone] $($record.HostName) $($ttl.TotalSeconds) IN $type $record_value $value`n" 114 $module.Result.changed = $true 115 } 116 } else{ 117 # This record matches one of the values; but does it match the TTL? 118 if ($record.TimeToLive -ne $ttl) { 119 $new_record = $record.Clone() 120 $new_record.TimeToLive = $ttl 121 Set-DnsServerResourceRecord -ZoneName $zone -OldInputObject $record -NewInputObject $new_record -WhatIf:$module.CheckMode @extra_args 122 $changes.before += "[$zone] $($record.HostName) $($record.TimeToLive.TotalSeconds) IN $type $record_value`n" 123 $changes.after += "[$zone] $($record.HostName) $($ttl.TotalSeconds) IN $type $record_value`n" 124 $module.Result.changed = $true 125 } 126 } 127 # Cross this one off the list, so we don't try adding it late 128 $required_values.Remove($record_value) 129 # Whatever is left in $required_values needs to be added 130 $values = $required_values.Keys 131 } 132 } 133 } 134 if ($null -ne $values -and $values.Count -gt 0) { 135 foreach ($value in $values) { 136 $splat_args = @{ $type = $true; $record_argument_name = $value } 137 $module.Result.debug_splat_args = $splat_args 138 $srv_args = @{ 139 DomainName = $value 140 Weight = $weight 141 Priority = $priority 142 Port = $port 143 } 144 try { 145 if ($type -eq 'SRV') { 146 Add-DnsServerResourceRecord -SRV -Name $name -ZoneName $zone @srv_args @extra_args -WhatIf:$module.CheckMode 147 }elseif ($type -eq 'TXT') { 148 Add-DnsServerResourceRecord -TXT -Name $name -DescriptiveText $value -ZoneName $zone -TimeToLive $ttl @extra_args -WhatIf:$module.CheckMode 149 } else { 150 Add-DnsServerResourceRecord -Name $name -AllowUpdateAny -ZoneName $zone -TimeToLive $ttl @splat_args -WhatIf:$module.CheckMode @extra_args 151 } 152 } catch { 153 $module.FailJson("Error adding DNS $type resource $name in zone $zone with value $value", $_) 154 } 155 $changes.after += "[$zone] $name $($ttl.TotalSeconds) IN $type $value`n" 156 } 157 $module.Result.changed = $true 158 } 159 160 if ($module.CheckMode) { 161 # Simulated changes 162 $module.Diff.before = $changes.before 163 $module.Diff.after = $changes.after 164 } else { 165 # Real changes 166 $records_end = Get-DnsServerResourceRecord -ZoneName $zone -Name $name -RRType $type -Node -ErrorAction:Ignore @extra_args | Sort-Object 167 $module.Diff.before = @($records | ForEach-Object { "[$zone] $($_.HostName) $($_.TimeToLive.TotalSeconds) IN $type $($_.RecordData.$record_argument_name.ToString())`n" }) -join '' 168 $module.Diff.after = @($records_end | ForEach-Object { "[$zone] $($_.HostName) $($_.TimeToLive.TotalSeconds) IN $type $($_.RecordData.$record_argument_name.ToString())`n" }) -join '' 169 } 170 171 $module.ExitJson() 172