diff --git a/CHANGELOG.md b/CHANGELOG.md index 62b9ef67c2..6da3c32471 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,8 @@ Ansible Changes By Release * Those using ansible as a library should note that the `ansible.vars.unsafe_proxy` module is deprecated and slated to go away in 2.8. The functionality has been moved to `ansible.utils.unsafe_proxy` to avoid a circular import. +* The win_get_url module has the dictionary 'win_get_url' in its results deprecated, + its content is now also available directly in the resulting output, like other modules. #### Deprecated Modules (to be removed in 2.8): * ec2_facts: replaced by ec2_metadata_facts diff --git a/lib/ansible/modules/windows/win_get_url.ps1 b/lib/ansible/modules/windows/win_get_url.ps1 index 0dcde7d4cf..bd2134c200 100644 --- a/lib/ansible/modules/windows/win_get_url.ps1 +++ b/lib/ansible/modules/windows/win_get_url.ps1 @@ -1,33 +1,126 @@ #!powershell # This file is part of Ansible. # -# (c)) 2015, Paul Durivage , Tal Auslander -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . +# Copyright: (c) 2015, Paul Durivage , Tal Auslander +# Copyright: (c) 2017, Dag Wieers +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # WANT_JSON # POWERSHELL_COMMON +$ErrorActionPreference = 'Stop' + + +Function CheckModified-File($url, $dest, $headers, $credentials, $timeout, $use_proxy, $proxy) { + + $fileLastMod = ([System.IO.FileInfo]$dest).LastWriteTimeUtc + $webLastMod = $null + + $webRequest = [System.Net.HttpWebRequest]::Create($url) + + foreach ($header in $headers.GetEnumerator()) { + $webRequest.Headers.Add($header.Name, $header.Value) + } + + if ($timeout) { + $webRequest.Timeout = $timeout * 1000 + } + + if (-not $use_proxy) { + # Ignore the system proxy settings + $webRequest.Proxy = $null + } elseif ($proxy) { + $webRequest.Proxy = $proxy + } + + if ($credentials) { + $webRequest.Credentials = $credentials + } + + $webRequest.Method = "HEAD" + Try { + [System.Net.HttpWebResponse]$webResponse = $webRequest.GetResponse() + + $webLastMod = $webResponse.GetResponseHeader("Last-Modified") + } Catch [System.Net.WebException] { + $result.status_code = $_.Exception.Response.StatusCode + Fail-Json -obj $result -message "Error requesting '$url'. $($_.Exception.Message)" + } Catch { + Fail-Json -obj $result -message "Error when requesting 'Last-Modified' date from '$url'. $($_.Exception.Message)" + } + $result.status_code = $webResponse.StatusCode + $result.msg = $webResponse.StatusDescription + $webResponse.Close() + + if ($webLastMod -and ((Get-Date -Date $webLastMod) -lt $fileLastMod)) { + return $false + } else { + return $true + } +} + + +Function Download-File($result, $url, $dest, $headers, $credentials, $timeout, $use_proxy, $proxy, $whatif) { + + # Check $dest parent folder exists before attempting download, which avoids unhelpful generic error message. + $dest_parent = Split-Path -LiteralPath $dest + if (-not (Test-Path -LiteralPath $dest_parent -PathType Container)) { + Fail-Json -obj $result -message "The path '$dest_parent' does not exist for destination '$dest', or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs." + } + + # TODO: Replace this with WebRequest + $webClient = New-Object System.Net.WebClient + + foreach ($header in $headers.GetEnumerator()) { + $webClient.Headers.Add($header.Name, $header.Value) + } + +# FIXME: WebClient has no Timeout property ? Should be replaced with WebRequest +# if ($timeout) { +# $webClient.Timeout = $timeout * 1000 +# } + + if (-not $use_proxy) { + # Ignore the system proxy settings + $webClient.Proxy = $null + } elseif ($proxy) { + $webClient.Proxy = $proxy + } + + if ($credentials) { + $webClient.Credentials = $credentials + } + + Try { + if (-not $whatif) { + $webClient.DownloadFile($url, $dest) + } + $result.changed = $true + } Catch [System.Net.WebException] { + $result.status_code = $_.Exception.Response.StatusCode + Fail-Json -obj $result -message "Error downloading '$url' to '$dest'. $($_.Exception.Message)" + } Catch { + Fail-Json -obj $result -message "Unknown error downloading '$url' to '$dest'. $($_.Exception.Message)" + } + # FIXME: Reimplement DownloadFile() using WebRequest so we get the real information + $result.status_code = 200 + $result.msg = 'OK' + $result.dest = $dest +} + + $params = Parse-Args $args -supports_check_mode $true $check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false $url = Get-AnsibleParam -obj $params -name "url" -type "str" -failifempty $true $dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true +$timeout = Get-AnsibleParam -obj $params -name "timeout" -type "int" -default 10 +$headers = Get-AnsibleParam -obj $params -name "headers" -type "dict" -default @{} $skip_certificate_validation = Get-AnsibleParam -obj $params -name "skip_certificate_validation" -type "bool" $validate_certs = Get-AnsibleParam -obj $params -name "validate_certs" -type "bool" -default $true -$username = Get-AnsibleParam -obj $params -name "username" -type "str" -$password = Get-AnsibleParam -obj $params -name "password" -type "str" +$url_username = Get-AnsibleParam -obj $params -name "url_username" -type "str" -aliases "username" +$url_password = Get-AnsibleParam -obj $params -name "url_password" -type "str" -aliases "password" +$use_proxy = Get-AnsibleParam -obj $params -name "use_proxy" -type "bool" -default $true $proxy_url = Get-AnsibleParam -obj $params -name "proxy_url" -type "str" $proxy_username = Get-AnsibleParam -obj $params -name "proxy_username" -type "str" $proxy_password = Get-AnsibleParam -obj $params -name "proxy_password" -type "str" @@ -35,12 +128,33 @@ $force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $true $result = @{ changed = $false + dest = $dest + url = $url + # This is deprecated as of v2.4, remove in v2.8 win_get_url = @{ dest = $dest url = $url } } +if (-not $use_proxy -and ($proxy_url -or $proxy_username -or $proxy_password)) { + Add-Warning -obj $result -msg "Not using a proxy on request, however a 'proxy_url', 'proxy_username' or 'proxy_password' was defined." +} + +$proxy = $null +if ($proxy_url) { + $proxy = New-Object System.Net.WebProxy($proxy_url, $true) + if ($proxy_username -and $proxy_password) { + $proxy_credential = New-Object System.Net.NetworkCredential($proxy_username, $proxy_password) + $proxy.Credentials = $proxy_credential + } +} + +$credentials = $null +if ($url_username -and $url_password) { + $credentials = New-Object System.Net.NetworkCredential($url_username, $url_password) +} + # If skip_certificate_validation was specified, use validate_certs if ($skip_certificate_validation -ne $null) { Add-DeprecationWarning -obj $result -message "The parameter 'skip_certificate_validation' is being replaced with 'validate_certs'" -version 2.8 @@ -48,50 +162,25 @@ if ($skip_certificate_validation -ne $null) { } if (-not $validate_certs) { - [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } - -Function Download-File($result, $url, $dest, $username, $password, $proxy_url, $proxy_username, $proxy_password) { - # use last part of url for dest file name if a directory is supplied for $dest - If ( Test-Path -PathType Container $dest ) { - $url_basename = Split-Path -leaf $url - If ( $url_basename.Length -gt 0 ) { - $dest = Join-Path -Path $dest -ChildPath $url_basename - $result.win_get_url.actual_dest = $dest - } - } - # check $dest parent folder exists before attempting download, which avoids unhelpful generic error message. - $dest_parent = Split-Path -Path $dest - $result.win_get_url.dest_parent = $dest_parent - If ( -not (Test-Path -Path $dest_parent -PathType Container)) { - $result.changed = $false - Fail-Json $result "The path '$dest_parent' does not exist for destination '$dest', or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs." - } - $webClient = New-Object System.Net.WebClient - if($proxy_url) { - $proxy_server = New-Object System.Net.WebProxy($proxy_url, $true) - if($proxy_username -and $proxy_password){ - $proxy_credential = New-Object System.Net.NetworkCredential($proxy_username, $proxy_password) - $proxy_server.Credentials = $proxy_credential - } - $webClient.Proxy = $proxy_server - } - - if($username -and $password){ - $webClient.Credentials = New-Object System.Net.NetworkCredential($username, $password) - } - - Try { - if (-not $check_mode) { - $webClient.DownloadFile($url, $dest) - } - $result.changed = $true - } - Catch { - Fail-Json $result "Error downloading $url to $dest $($_.Exception.Message)" +# Use last part of url for dest file name if a directory is supplied for $dest +if (Test-Path -LiteralPath $dest -PathType Container) { + $uri = [System.Uri]$url + $basename = Split-Path -Path $uri.LocalPath -Leaf + if ($uri.LocalPath -and $uri.LocalPath -ne '/' -and $basename) { + $url_basename = Split-Path -Path $uri.LocalPath -Leaf + $dest = Join-Path -Path $dest -ChildPath $url_basename + } else { + $dest = Join-Path -Path $dest -ChildPath $uri.Host } +} elseif (([System.IO.Path]::GetFileName($dest)) -eq '') { + # We have a trailing path separator + Fail-Json -obj $result -message "The destination path '$dest' does not exist, or is not visible to the current user. Ensure download destination folder exists (perhaps using win_file state=directory) before win_get_url runs." } +$result.dest = $dest +$result.win_get_url.dest = $dest # Enable TLS1.1/TLS1.2 if they're available but disabled (eg. .NET 4.5) $security_protcols = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::SystemDefault @@ -103,45 +192,24 @@ if ([Net.SecurityProtocolType].GetMember("Tls12").Count -gt 0) { } [Net.ServicePointManager]::SecurityProtocol = $security_protcols -If ($force -or -not (Test-Path -Path $dest)) { - - Download-File -result $result -url $url -dest $dest -username $username -password $password -proxy_url $proxy_url -proxy_username $proxy_username -proxy_password $proxy_password -} -Else { - $fileLastMod = ([System.IO.FileInfo]$dest).LastWriteTimeUtc - $webLastMod = $null +if ($force -or -not (Test-Path -LiteralPath $dest)) { - Try { - $webRequest = [System.Net.HttpWebRequest]::Create($url) - if ($proxy_url) { - $proxy_server = New-Object System.Net.WebProxy($proxy_url, $true) - if ($proxy_username -and $proxy_password) { - $proxy_credential = New-Object System.Net.NetworkCredential($proxy_username, $proxy_password) - $proxy_server.Credentials = $proxy_credential - } - $webRequest.Proxy = $proxy_server - } + Download-File -result $result -url $url -dest $dest -credentials $credentials ` + -headers $headers -timeout $timeout -use_proxy $use_proxy -proxy $proxy ` + -whatif $check_mode - if($username -and $password){ - $webRequest.Credentials = New-Object System.Net.NetworkCredential($username, $password) - } +} else { - $webRequest.Method = "HEAD" - [System.Net.HttpWebResponse]$webResponse = $webRequest.GetResponse() + $is_modified = CheckModified-File -result $result -url $url -dest $dest -credentials $credentials ` + -headers $headers -timeout $timeout -use_proxy $use_proxy -proxy $proxy + + if ($is_modified) { + + Download-File -result $result -url $url -dest $dest -credentials $credentials ` + -headers $headers -timeout $timeout -use_proxy $use_proxy -proxy $proxy ` + -whatif $check_mode - $webLastMod = $webResponse.GetResponseHeader("Last-Modified") - $webResponse.Close() } - Catch { - Fail-Json $result "Error when requesting Last-Modified date from $url $($_.Exception.Message)" - } - - If (($webLastMod) -and ((Get-Date -Date $webLastMod ) -lt $fileLastMod)) { - $result.changed = $false - } Else { - Download-File -result $result -url $url -dest $dest -username $username -password $password -proxy_url $proxy_url -proxy_username $proxy_username -proxy_password $proxy_password - } - } Exit-Json $result diff --git a/lib/ansible/modules/windows/win_get_url.py b/lib/ansible/modules/windows/win_get_url.py index f286c0d803..82e542f3b0 100644 --- a/lib/ansible/modules/windows/win_get_url.py +++ b/lib/ansible/modules/windows/win_get_url.py @@ -31,75 +31,84 @@ module: win_get_url version_added: "1.7" short_description: Fetches a file from a given URL description: - - Fetches a file from a URL and saves to locally - - For non-Windows targets, use the M(get_url) module instead. +- Fetches a file from a URL and saves it locally. +- For non-Windows targets, use the M(get_url) module instead. author: - - "Paul Durivage (@angstwad)" - - "Takeshi Kuramochi (tksarah)" +- Paul Durivage (@angstwad) +- Takeshi Kuramochi (tksarah) options: url: description: - - The full URL of a file to download - required: true - default: null + - The full URL of a file to download. + required: yes dest: description: - - The absolute path of the location to save the file at the URL. Be sure - to include a filename and extension as appropriate. - required: true - default: null + - The location to save the file at the URL. + - Be sure to include a filename and extension as appropriate. + required: yes force: description: - - If C(yes), will always download the file. If C(no), will only - download the file if it does not exist or the remote file has been - modified more recently than the local file. This works by sending - an http HEAD request to retrieve last modified time of the requested - resource, so for this to work, the remote web server must support - HEAD requests. + - If C(yes), will always download the file. If C(no), will only + download the file if it does not exist or the remote file has been + modified more recently than the local file. + - This works by sending an http HEAD request to retrieve last modified + time of the requested resource, so for this to work, the remote web + server must support HEAD requests. + type: bool + default: 'yes' version_added: "2.0" - required: false - choices: [ "yes", "no" ] - default: yes - username: + headers: description: - - Basic authentication username - required: false - default: null - password: + - Add custom HTTP headers to a request (as a dictionary). + version_added: '2.4' + url_username: description: - - Basic authentication password - required: false - default: null + - Basic authentication username. + aliases: [ username ] + url_password: + description: + - Basic authentication password. + aliases: [ password ] skip_certificate_validation: description: - This option is deprecated since v2.4, please use C(validate_certs) instead. - If C(yes), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. - default: 'no' type: bool + default: 'no' validate_certs: description: - If C(no), SSL certificates will not be validated. This should only be used on personally controlled sites using self-signed certificates. - If C(skip_certificate_validation) was set, it overrides this option. - default: 'yes' type: bool + default: 'yes' version_added: '2.4' proxy_url: description: - - The full URL of the proxy server to download through. + - The full URL of the proxy server to download through. version_added: "2.0" - required: false proxy_username: description: - - Proxy authentication username + - Proxy authentication username. version_added: "2.0" - required: false proxy_password: description: - - Proxy authentication password + - Proxy authentication password. version_added: "2.0" - required: false + use_proxy: + description: + - If C(no), it will not use a proxy, even if one is defined in an environment + variable on the target hosts. + type: bool + default: 'yes' + version_added: '2.4' +# TODO: Once we have implemented DownloadFile() using $WebRequest, enable timeout again +# timeout: +# description: +# - Timeout in seconds for URL request. +# default: 10 +# version_added : '2.5' notes: - For non-Windows targets, use the M(get_url) module instead. ''' @@ -126,14 +135,24 @@ EXAMPLES = r''' ''' RETURN = r''' -url: - description: requested url - returned: always - type: string - sample: http://www.example.com/earthrise.jpg dest: description: destination file/path returned: always type: string sample: C:\Users\RandomUser\earthrise.jpg +url: + description: requested url + returned: always + type: string + sample: http://www.example.com/earthrise.jpg +msg: + description: Error message, or HTTP status message from web-server + returned: always + type: string + sample: OK +status_code: + description: HTTP status code + returned: always + type: int + sample: 200 ''' diff --git a/test/integration/targets/win_get_url/defaults/main.yml b/test/integration/targets/win_get_url/defaults/main.yml index c912ed24e8..36f5a76451 100644 --- a/test/integration/targets/win_get_url/defaults/main.yml +++ b/test/integration/targets/win_get_url/defaults/main.yml @@ -1,8 +1,8 @@ --- test_win_get_url_host: www.redhat.com -test_win_get_url_link: "https://{{test_win_get_url_host}}" +test_win_get_url_link: "https://{{ test_win_get_url_host }}" test_win_get_url_invalid_link: https://www.redhat.com/skynet_module.html test_win_get_url_invalid_path: 'Q:\Filez\Cyberdyne.html' test_win_get_url_invalid_path_dir: 'Q:\Filez\' -test_win_get_url_path: '{{ test_win_get_url_dir_path }}\docs_index.html' +test_win_get_url_path: '%TEMP%\docs_index.html' diff --git a/test/integration/targets/win_get_url/tasks/main.yml b/test/integration/targets/win_get_url/tasks/main.yml index dfbfd90c60..7440e1b549 100644 --- a/test/integration/targets/win_get_url/tasks/main.yml +++ b/test/integration/targets/win_get_url/tasks/main.yml @@ -16,120 +16,128 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -- name: get tempdir path - raw: $env:TEMP - register: tempdir +- setup: -- name: set output path dynamically - set_fact: - test_win_get_url_dir_path: "{{ tempdir.stdout_lines[0] }}" +- name: Remove test file if it exists + win_file: + path: '{{ test_win_get_url_path }}' + state: absent -- name: remove test file if it exists - raw: > - PowerShell -Command Remove-Item "{{test_win_get_url_path}}" -Force - ignore_errors: true - -- name: test win_get_url module +- name: Test win_get_url module win_get_url: - url: "{{test_win_get_url_link}}" - dest: "{{test_win_get_url_path}}" + url: '{{ test_win_get_url_link }}' + dest: '{{ test_win_get_url_path }}' register: win_get_url_result -- name: check that url was downloaded +- name: Check that url was downloaded assert: that: - - "not win_get_url_result|failed" - - "win_get_url_result|changed" - - "win_get_url_result.win_get_url.url" - - "win_get_url_result.win_get_url.dest" + - not win_get_url_result|failed + - win_get_url_result|changed + - win_get_url_result.url + - win_get_url_result.dest -- name: test win_get_url module again (force should be yes by default) +- name: Test win_get_url module again (force should be yes by default) win_get_url: - url: "{{test_win_get_url_link}}" - dest: "{{test_win_get_url_path}}" + url: '{{ test_win_get_url_link }}' + dest: '{{ test_win_get_url_path }}' register: win_get_url_result_again -- name: check that url was downloaded again +- name: Check that url was downloaded again assert: that: - - "not win_get_url_result_again|failed" - - "win_get_url_result_again|changed" + - not win_get_url_result_again|failed + - win_get_url_result_again|changed -- name: test win_get_url module again with force=no +- name: Test win_get_url module again with force=no win_get_url: - url: "{{test_win_get_url_link}}" - dest: "{{test_win_get_url_path}}" + url: '{{ test_win_get_url_link }}' + dest: '{{ test_win_get_url_path }}' force: no register: win_get_url_result_noforce -- name: check that url was not downloaded again +- name: Check that url was not downloaded again assert: that: - - "not win_get_url_result_noforce|failed" - - "not win_get_url_result_noforce|changed" + - not win_get_url_result_noforce|failed + - not win_get_url_result_noforce|changed -- name: test win_get_url module with url that returns a 404 +- name: Test win_get_url module with url that returns a 404 win_get_url: - url: "{{test_win_get_url_invalid_link}}" - dest: "{{test_win_get_url_path}}" + url: '{{ test_win_get_url_invalid_link }}' + dest: '{{ test_win_get_url_path }}' register: win_get_url_result_invalid_link ignore_errors: true -- name: check that the download failed for an invalid url +- name: Check that the download failed for an invalid url assert: that: - - "win_get_url_result_invalid_link|failed" + - win_get_url_result_invalid_link|failed + - win_get_url_result_invalid_link.status_code == 404 -- name: test win_get_url module with an invalid path +- name: Test win_get_url module with an invalid path win_get_url: - url: "{{test_win_get_url_link}}" - dest: "{{test_win_get_url_invalid_path}}" + url: '{{ test_win_get_url_link }}' + dest: '{{ test_win_get_url_invalid_path }}' register: win_get_url_result_invalid_path ignore_errors: true -- name: check that the download failed for an invalid path +- name: Check that the download failed for an invalid path assert: that: - - "win_get_url_result_invalid_path|failed" + - win_get_url_result_invalid_path|failed -- name: test win_get_url module with a valid path that is a directory +- name: Test win_get_url module with a valid path that is a directory win_get_url: - url: "{{test_win_get_url_link}}" - dest: "{{test_win_get_url_dir_path}}" + url: '{{ test_win_get_url_link }}' + dest: '%TEMP%' register: win_get_url_result_dir_path ignore_errors: true -- name: check that the download did NOT fail, even though dest was directory +- name: Check that the download did NOT fail, even though dest was directory assert: that: - - "win_get_url_result_dir_path|changed" + - win_get_url_result_dir_path|changed -- name: test win_get_url with a valid url path and a dest that is a directory (from 2.4 should use url path as filename) +- name: Test win_get_url with a valid url path and a dest that is a directory (from 2.4 should use url path as filename) win_get_url: - url: "{{test_win_get_url_link}}" - dest: "{{test_win_get_url_dir_path}}" + url: '{{ test_win_get_url_link }}' + dest: '%TEMP%' register: win_get_url_result_dir_path_urlpath ignore_errors: true -- name: set expected destination path fact +- name: Set expected destination path fact set_fact: - expected_dest_path: '{{test_win_get_url_dir_path}}\{{test_win_get_url_host}}' + expected_dest_path: '{{ ansible_env.TEMP }}\{{ test_win_get_url_host }}' -- name: check that the download succeeded (changed) and dest is as expected +- name: Check that the download succeeded (changed) and dest is as expected assert: that: - - "win_get_url_result_dir_path_urlpath|changed" - - "win_get_url_result_dir_path_urlpath.win_get_url.actual_dest==expected_dest_path" + - win_get_url_result_dir_path_urlpath|changed + - win_get_url_result_dir_path_urlpath.dest == expected_dest_path -#- name: since 2.4 check you get a helpful message if the parent folder of the dest doesnt exist -# win_get_url: -# url: "{{test_win_get_url_link}}" -# dest: "{{test_win_get_url_invalid_path_dir}}" -# register: win_get_url_result_invalid_dest -# ignore_errors: true -# -#- name: check if dest parent dir does not exist, module fails and you get a specific error message -# assert: -# that: -# - "win_get_url_result_invalid_dest|failed" -# - "win_get_url_result_invalid_dest.msg is search('does not exist')" +- name: Check you get a helpful message if the parent folder of the dest doesn't exist + win_get_url: + url: '{{ test_win_get_url_link }}' + dest: 'Q:\Filez\' + register: win_get_url_result_invalid_dest + ignore_errors: true + +- name: Check if dest parent dir does not exist, module fails and you get a specific error message + assert: + that: + - win_get_url_result_invalid_dest|failed + - win_get_url_result_invalid_dest.msg is search('invalid path') + +- name: Check you get a helpful message if the parent folder of the dest doesn't exist + win_get_url: + url: '{{ test_win_get_url_link }}' + dest: 'C:\Filez\' + register: win_get_url_result_invalid_dest2 + ignore_errors: true + +- name: Check if dest parent dir does not exist, module fails and you get a specific error message + assert: + that: + - win_get_url_result_invalid_dest2|failed + - win_get_url_result_invalid_dest2.msg is search('does not exist')