1 #!powershell
2 
3 # Copyright: (c) 2017, Ansible Project
4 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5 
6 #Requires -Module Ansible.ModuleUtils.Legacy
7 #Requires -Module Ansible.ModuleUtils.FileUtil
8 
9 $ErrorActionPreference = "Stop"
10 
11 $params = Parse-Args -arguments $args -supports_check_mode $true
12 
13 $connect_timeout = Get-AnsibleParam -obj $params -name "connect_timeout" -type "int" -default 5
14 $delay = Get-AnsibleParam -obj $params -name "delay" -type "int"
15 $exclude_hosts = Get-AnsibleParam -obj $params -name "exclude_hosts" -type "list"
16 $hostname = Get-AnsibleParam -obj $params -name "host" -type "str" -default "127.0.0.1"
17 $path = Get-AnsibleParam -obj $params -name "path" -type "path"
18 $port = Get-AnsibleParam -obj $params -name "port" -type "int"
19 $regex = Get-AnsibleParam -obj $params -name "regex" -type "str" -aliases "search_regex","regexp"
20 $sleep = Get-AnsibleParam -obj $params -name "sleep" -type "int" -default 1
21 $state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "started" -validateset "present","started","stopped","absent","drained"
22 $timeout = Get-AnsibleParam -obj $params -name "timeout" -type "int" -default 300
23 
24 $result = @{
25     changed = $false
26     elapsed = 0
27 }
28 
29 # validate the input with the various options
30 if ($null -ne $port -and $null -ne $path) {
31     Fail-Json $result "port and path parameter can not both be passed to win_wait_for"
32 }
33 if ($null -ne $exclude_hosts -and $state -ne "drained") {
34     Fail-Json $result "exclude_hosts should only be with state=drained"
35 }
36 if ($null -ne $path) {
37     if ($state -in @("stopped","drained")) {
38         Fail-Json $result "state=$state should only be used for checking a port in the win_wait_for module"
39     }
40 
41     if ($null -ne $exclude_hosts) {
42         Fail-Json $result "exclude_hosts should only be used when checking a port and state=drained in the win_wait_for module"
43     }
44 }
45 
46 if ($null -ne $port) {
47     if ($null -ne $regex) {
48         Fail-Json $result "regex should by used when checking a string in a file in the win_wait_for module"
49     }
50 
51     if ($null -ne $exclude_hosts -and $state -ne "drained") {
52         Fail-Json $result "exclude_hosts should be used when state=drained in the win_wait_for module"
53     }
54 }
55 
Test-Port($hostname, $port)56 Function Test-Port($hostname, $port) {
57     $timeout = $connect_timeout * 1000
58     $socket = New-Object -TypeName System.Net.Sockets.TcpClient
59     $connect = $socket.BeginConnect($hostname, $port, $null, $null)
60     $wait = $connect.AsyncWaitHandle.WaitOne($timeout, $false)
61 
62     if ($wait) {
63         try {
64             $socket.EndConnect($connect) | Out-Null
65             $valid = $true
66         } catch {
67             $valid = $false
68         }
69     } else {
70         $valid = $false
71     }
72 
73     $socket.Close()
74     $socket.Dispose()
75 
76     $valid
77 }
78 
Get-PortConnections($hostname, $port)79 Function Get-PortConnections($hostname, $port) {
80     $connections = @()
81 
82     $conn_info = [Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
83     if ($hostname -eq "0.0.0.0") {
84         $active_connections = $conn_info.GetActiveTcpConnections() | Where-Object { $_.LocalEndPoint.Port -eq $port }
85     } else {
86         $active_connections = $conn_info.GetActiveTcpConnections() | Where-Object { $_.LocalEndPoint.Address -eq $hostname -and $_.LocalEndPoint.Port -eq $port }
87     }
88 
89     if ($null -ne $active_connections) {
90         foreach ($active_connection in $active_connections) {
91             $connections += $active_connection.RemoteEndPoint.Address
92         }
93     }
94 
95     $connections
96 }
97 
98 $module_start = Get-Date
99 
100 if ($null -ne $delay) {
101     Start-Sleep -Seconds $delay
102 }
103 
104 $attempts = 0
105 if ($null -eq $path -and $null -eq $port -and $state -ne "drained") {
106     Start-Sleep -Seconds $timeout
107 } elseif ($null -ne $path) {
108     if ($state -in @("present", "started")) {
109         # check if the file exists or string exists in file
110         $start_time = Get-Date
111         $complete = $false
112         while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
113             $attempts += 1
114             if (Test-AnsiblePath -Path $path) {
115                 if ($null -eq $regex) {
116                     $complete = $true
117                     break
118                 } else {
119                     $file_contents = Get-Content -Path $path -Raw
120                     if ($file_contents -match $regex) {
121                         $complete = $true
122                         break
123                     }
124                 }
125             }
126             Start-Sleep -Seconds $sleep
127         }
128 
129         if ($complete -eq $false) {
130             $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
131             $result.wait_attempts = $attempts
132             if ($null -eq $regex) {
133                 Fail-Json $result "timeout while waiting for file $path to be present"
134             } else {
135                 Fail-Json $result "timeout while waiting for string regex $regex in file $path to match"
136             }
137         }
138     } elseif ($state -in @("absent")) {
139         # check if the file is deleted or string doesn't exist in file
140         $start_time = Get-Date
141         $complete = $false
142         while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
143             $attempts += 1
144             if (Test-AnsiblePath -Path $path) {
145                 if ($null -ne $regex) {
146                     $file_contents = Get-Content -Path $path -Raw
147                     if ($file_contents -notmatch $regex) {
148                         $complete = $true
149                         break
150                     }
151                 }
152             } else {
153                 $complete = $true
154                 break
155             }
156 
157             Start-Sleep -Seconds $sleep
158         }
159 
160         if ($complete -eq $false) {
161             $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
162             $result.wait_attempts = $attempts
163             if ($null -eq $regex) {
164                 Fail-Json $result "timeout while waiting for file $path to be absent"
165             } else {
166                 Fail-Json $result "timeout while waiting for string regex $regex in file $path to not match"
167             }
168         }
169     }
170 } elseif ($null -ne $port) {
171     if ($state -in @("started","present")) {
172         # check that the port is online and is listening
173         $start_time = Get-Date
174         $complete = $false
175         while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
176             $attempts += 1
177             $port_result = Test-Port -hostname $hostname -port $port
178             if ($port_result -eq $true) {
179                 $complete = $true
180                 break
181             }
182 
183             Start-Sleep -Seconds $sleep
184         }
185 
186         if ($complete -eq $false) {
187             $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
188             $result.wait_attempts = $attempts
189             Fail-Json $result "timeout while waiting for $($hostname):$port to start listening"
190         }
191     } elseif ($state -in @("stopped","absent")) {
192         # check that the port is offline and is not listening
193         $start_time = Get-Date
194         $complete = $false
195         while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
196             $attempts += 1
197             $port_result = Test-Port -hostname $hostname -port $port
198             if ($port_result -eq $false) {
199                 $complete = $true
200                 break
201             }
202 
203             Start-Sleep -Seconds $sleep
204         }
205 
206         if ($complete -eq $false) {
207             $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
208             $result.wait_attempts = $attempts
209             Fail-Json $result "timeout while waiting for $($hostname):$port to stop listening"
210         }
211     } elseif ($state -eq "drained") {
212         # check that the local port is online but has no active connections
213         $start_time = Get-Date
214         $complete = $false
215         while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
216             $attempts += 1
217             $active_connections = Get-PortConnections -hostname $hostname -port $port
218             if ($null -eq $active_connections) {
219                 $complete = $true
220                 break
221             } elseif ($active_connections.Count -eq 0) {
222                 # no connections on port
223                 $complete = $true
224                 break
225             } else {
226                 # there are listeners, check if we should ignore any hosts
227                 if ($null -ne $exclude_hosts) {
228                     $connection_info = $active_connections
229                     foreach ($exclude_host in $exclude_hosts) {
230                         try {
231                             $exclude_ips = [System.Net.Dns]::GetHostAddresses($exclude_host) | ForEach-Object { Write-Output $_.IPAddressToString }
232                             $connection_info = $connection_info | Where-Object { $_ -notin $exclude_ips }
233                         } catch { # ignore invalid hostnames
234                             Add-Warning -obj $result -message "Invalid hostname specified $exclude_host"
235                         }
236                     }
237 
238                     if ($connection_info.Count -eq 0) {
239                         $complete = $true
240                         break
241                     }
242                 }
243             }
244 
245             Start-Sleep -Seconds $sleep
246         }
247 
248         if ($complete -eq $false) {
249             $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
250             $result.wait_attempts = $attempts
251             Fail-Json $result "timeout while waiting for $($hostname):$port to drain"
252         }
253     }
254 }
255 
256 $result.elapsed = ((Get-Date) - $module_start).TotalSeconds
257 $result.wait_attempts = $attempts
258 
259 Exit-Json $result
260