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