1 #!powershell
2 
3 # Copyright: (c) 2018, Micah Hunsberger (@mhunsber)
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 #AnsibleRequires -CSharpUtil Ansible.Basic
7 
8 Set-StrictMode -Version 2
9 $ErrorActionPreference = "Stop"
10 
11 $spec = @{
12     options = @{
13         state = @{ type = "str"; choices = "absent", "present"; default = "present" }
14         aliases = @{ type = "list"; elements = "str" }
15         canonical_name = @{ type = "str" }
16         ip_address = @{ type = "str" }
17         action = @{ type = "str"; choices = "add", "remove", "set"; default = "set" }
18     }
19     required_if = @(,@( "state", "present", @("canonical_name", "ip_address")))
20     supports_check_mode = $true
21 }
22 
23 $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
24 
25 $state = $module.Params.state
26 $aliases = $module.Params.aliases
27 $canonical_name = $module.Params.canonical_name
28 $ip_address = $module.Params.ip_address
29 $action = $module.Params.action
30 
31 $tmp = [ipaddress]::None
32 if($ip_address -and -not [ipaddress]::TryParse($ip_address, [ref]$tmp)){
33     $module.FailJson("win_hosts: Argument ip_address needs to be a valid ip address, but was $ip_address")
34 }
35 $ip_address_type = $tmp.AddressFamily
36 
37 $hosts_file = Get-Item -LiteralPath "$env:SystemRoot\System32\drivers\etc\hosts"
38 
Get-CommentIndex($line)39 Function Get-CommentIndex($line) {
40     $c_index = $line.IndexOf('#')
41     if($c_index -lt 0) {
42         $c_index = $line.Length
43     }
44     return $c_index
45 }
46 
Get-HostEntryParts($line)47 Function Get-HostEntryParts($line) {
48     $success = $true
49     $c_index = Get-CommentIndex -line $line
50     $pure_line = $line.Substring(0,$c_index).Trim()
51     $bits = $pure_line -split "\s+"
52     if($bits.Length -lt 2){
53         return @{
54             success = $false
55             ip_address = ""
56             ip_type = ""
57             canonical_name = ""
58             aliases = @()
59         }
60     }
61     $ip_obj = [ipaddress]::None
62     if(-not [ipaddress]::TryParse($bits[0], [ref]$ip_obj) ){
63         $success = $false
64     }
65     $cname = $bits[1]
66     $als = New-Object string[] ($bits.Length - 2)
67     [array]::Copy($bits, 2, $als, 0, $als.Length)
68     return @{
69         success = $success
70         ip_address = $ip_obj.IPAddressToString
71         ip_type = $ip_obj.AddressFamily
72         canonical_name = $cname
73         aliases = $als
74     }
75 }
76 
Find-HostName($line, $name)77 Function Find-HostName($line, $name) {
78     $c_idx = Get-CommentIndex -line $line
79     $re = New-Object regex ("\s+$($name.Replace('.',"\."))(\s|$)", [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
80     $match = $re.Match($line, 0, $c_idx)
81     return $match
82 }
83 
Remove-HostEntry($list, $idx)84 Function Remove-HostEntry($list, $idx) {
85     $module.Result.changed = $true
86     $list.RemoveAt($idx)
87 }
88 
Add-HostEntry($list, $cname, $aliases, $ip)89 Function Add-HostEntry($list, $cname, $aliases, $ip) {
90     $module.Result.changed = $true
91     $line = "$ip $cname $($aliases -join ' ')"
92     $list.Add($line) | Out-Null
93 }
94 
Remove-HostnamesFromEntry($list, $idx, $aliases)95 Function Remove-HostnamesFromEntry($list, $idx, $aliases) {
96     $line = $list[$idx]
97     $line_removed = $false
98 
99     foreach($name in $aliases){
100         $match = Find-HostName -line $line -name $name
101         if($match.Success){
102             $line = $line.Remove($match.Index + 1, $match.Length -1)
103             # was this the last alias? (check for space characters after trimming)
104             if($line.Substring(0,(Get-CommentIndex -line $line)).Trim() -inotmatch "\s") {
105                 $list.RemoveAt($idx)
106                 $line_removed = $true
107                 # we're done
108                 return @{
109                     line_removed = $line_removed
110                 }
111             }
112         }
113     }
114     if($line -ne $list[$idx]){
115         $module.Result.changed = $true
116         $list[$idx] = $line
117     }
118     return @{
119         line_removed = $line_removed
120     }
121 }
122 
Add-AliasesToEntry($list, $idx, $aliases)123 Function Add-AliasesToEntry($list, $idx, $aliases) {
124     $line = $list[$idx]
125     foreach($name in $aliases){
126         $match = Find-HostName -line $line -name $name
127         if(-not $match.Success) {
128             # just add the alias before the comment
129             $line = $line.Insert((Get-CommentIndex -line $line), " $name ")
130         }
131     }
132     if($line -ne $list[$idx]){
133         $module.Result.changed = $true
134         $list[$idx] = $line
135     }
136 }
137 
138 $hosts_lines = New-Object System.Collections.ArrayList
139 
140 Get-Content -LiteralPath $hosts_file.FullName | ForEach-Object { $hosts_lines.Add($_) } | Out-Null
141 $module.Diff.before = ($hosts_lines -join "`n") + "`n"
142 
143 if ($state -eq 'absent') {
144     # go through and remove canonical_name and ip
145     for($idx = 0; $idx -lt $hosts_lines.Count; $idx++) {
146         $entry = $hosts_lines[$idx]
147          # skip comment lines
148          if(-not $entry.Trim().StartsWith('#')) {
149             $entry_parts = Get-HostEntryParts -line $entry
150             if($entry_parts.success) {
151                 if(-not $ip_address -or $entry_parts.ip_address -eq $ip_address) {
152                     if(-not $canonical_name -or $entry_parts.canonical_name -eq $canonical_name) {
153                         if(Remove-HostEntry -list $hosts_lines -idx $idx){
154                             # keep index correct if we removed the line
155                             $idx = $idx - 1
156                         }
157                     }
158                 }
159             }
160         }
161     }
162 }
163 if($state -eq 'present') {
164     $entry_idx = -1
165     $aliases_to_keep = @()
166     # go through lines, find the entry and determine what to remove based on action
167     for($idx = 0; $idx -lt $hosts_lines.Count; $idx++) {
168         $entry = $hosts_lines[$idx]
169          # skip comment lines
170          if(-not $entry.Trim().StartsWith('#')) {
171             $entry_parts = Get-HostEntryParts -line $entry
172             if($entry_parts.success) {
173                 $aliases_to_remove = @()
174                 if($entry_parts.ip_address -eq $ip_address) {
175                     if($entry_parts.canonical_name -eq $canonical_name) {
176                         $entry_idx = $idx
177 
178                         if($action -eq 'set') {
179                             $aliases_to_remove = $entry_parts.aliases | Where-Object { $aliases -notcontains $_ }
180                         } elseif($action -eq 'remove') {
181                             $aliases_to_remove = $aliases
182                         }
183                     } else {
184                         # this is the right ip_address, but not the cname we were looking for.
185                         # we need to make sure none of aliases or canonical_name exist for this entry
186                         # since the given canonical_name should be an A/AAAA record,
187                         # and aliases should be cname records for the canonical_name.
188                         $aliases_to_remove = $aliases + $canonical_name
189                     }
190                 } else {
191                     # this is not the ip_address we are looking for
192                     if ($ip_address_type -eq $entry_parts.ip_type) {
193                         if ($entry_parts.canonical_name -eq $canonical_name) {
194                             Remove-HostEntry -list $hosts_lines -idx $idx
195                             $idx = $idx - 1
196                             if ($action -ne "set") {
197                                 # keep old aliases intact
198                                 $aliases_to_keep += $entry_parts.aliases | Where-Object { ($aliases + $aliases_to_keep + $canonical_name) -notcontains $_ }
199                             }
200                         } elseif ($action -eq "remove") {
201                             $aliases_to_remove = $canonical_name
202                         } elseif ($aliases -contains $entry_parts.canonical_name) {
203                             Remove-HostEntry -list $hosts_lines -idx $idx
204                             $idx = $idx - 1
205                             if ($action -eq "add") {
206                                 # keep old aliases intact
207                                 $aliases_to_keep += $entry_parts.aliases | Where-Object { ($aliases + $aliases_to_keep + $canonical_name) -notcontains $_ }
208                             }
209                         } else {
210                             $aliases_to_remove = $aliases + $canonical_name
211                         }
212                     } else {
213                         # TODO: Better ipv6 support. There is odd behavior for when an alias can be used for both ipv6 and ipv4
214                     }
215                 }
216 
217                 if($aliases_to_remove) {
218                     if((Remove-HostnamesFromEntry -list $hosts_lines -idx $idx -aliases $aliases_to_remove).line_removed) {
219                         $idx = $idx - 1
220                     }
221                 }
222             }
223         }
224     }
225 
226     if($entry_idx -ge 0) {
227         $aliases_to_add = @()
228         $entry_parts = Get-HostEntryParts -line $hosts_lines[$entry_idx]
229         if($action -eq 'remove') {
230             $aliases_to_add = $aliases_to_keep | Where-Object { $entry_parts.aliases -notcontains $_ }
231         } else {
232             $aliases_to_add = ($aliases + $aliases_to_keep) | Where-Object { $entry_parts.aliases -notcontains $_ }
233         }
234 
235         if($aliases_to_add) {
236             Add-AliasesToEntry -list $hosts_lines -idx $entry_idx -aliases $aliases_to_add
237         }
238     } else {
239         # add the entry at the end
240         if($action -eq 'remove') {
241             if($aliases_to_keep) {
242                 Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name -aliases $aliases_to_keep
243             } else {
244                 Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name
245             }
246         } else {
247             Add-HostEntry -list $hosts_lines -ip $ip_address -cname $canonical_name -aliases ($aliases + $aliases_to_keep)
248         }
249     }
250 }
251 
252 $module.Diff.after = ($hosts_lines -join "`n") + "`n"
253 if( $module.Result.changed -and -not $module.CheckMode ) {
254     Set-Content -LiteralPath $hosts_file.FullName -Value $hosts_lines
255 }
256 
257 $module.ExitJson()
258