diff --git a/changelogs/fragments/win_stat-follow.yaml b/changelogs/fragments/win_stat-follow.yaml new file mode 100644 index 00000000000..b4f9a01b62d --- /dev/null +++ b/changelogs/fragments/win_stat-follow.yaml @@ -0,0 +1,2 @@ +minor_changes: +- win_stat - added the ``follow`` module option to follow ``path`` when getting the file or directory info diff --git a/lib/ansible/modules/windows/win_stat.ps1 b/lib/ansible/modules/windows/win_stat.ps1 index a9706f2b2bc..0a184e7ab15 100644 --- a/lib/ansible/modules/windows/win_stat.ps1 +++ b/lib/ansible/modules/windows/win_stat.ps1 @@ -7,7 +7,7 @@ #Requires -Module Ansible.ModuleUtils.FileUtil #Requires -Module Ansible.ModuleUtils.LinkUtil -function DateTo-Timestamp($start_date, $end_date) { +function ConvertTo-Timestamp($start_date, $end_date) { if ($start_date -and $end_date) { return (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds } @@ -33,12 +33,34 @@ function Get-FileChecksum($path, $algorithm) { return $hash } +function Get-FileInfo { + param([String]$Path, [Switch]$Follow) + + $info = Get-AnsibleItem -Path $Path -ErrorAction SilentlyContinue + $link_info = $null + if ($null -ne $info) { + try { + $link_info = Get-Link -link_path $info.FullName + } catch { + $module.Warn("Failed to check/get link info for file: $($_.Exception.Message)") + } + + # If follow=true we want to follow the link all the way back to root object + if ($Follow -and $null -ne $link_info -and $link_info.Type -in @("SymbolicLink", "JunctionPoint")) { + $info, $link_info = Get-FileInfo -Path $link_info.AbsolutePath -Follow + } + } + + return $info, $link_info +} + $spec = @{ options = @{ path = @{ type='path'; required=$true; aliases=@( 'dest', 'name' ) } get_checksum = @{ type='bool'; default=$true } checksum_algorithm = @{ type='str'; default='sha1'; choices=@( 'md5', 'sha1', 'sha256', 'sha384', 'sha512' ) } get_md5 = @{ type='bool'; default=$false; removed_in_version='2.9' } + follow = @{ type='bool'; default=$false } } supports_check_mode = $true } @@ -49,11 +71,13 @@ $path = $module.Params.path $get_md5 = $module.Params.get_md5 $get_checksum = $module.Params.get_checksum $checksum_algorithm = $module.Params.checksum_algorithm +$follow = $module.Params.follow $module.Result.stat = @{ exists=$false } -$info = Get-AnsibleItem -Path $path -ErrorAction SilentlyContinue -If ($null -ne $info) { +Load-LinkUtils +$info, $link_info = Get-FileInfo -Path $path -Follow:$follow +If ($null -ne $info) { $epoch_date = Get-Date -Date "01/01/1970" $attributes = @() foreach ($attribute in ($info.Attributes -split ',')) { @@ -77,9 +101,9 @@ If ($null -ne $info) { # lnk_target = islnk or isjunction Target of the symlink. Note that relative paths remain relative # lnk_source = islnk os isjunction Target of the symlink normalized for the remote filesystem hlnk_targets = @() - creationtime = (DateTo-Timestamp -start_date $epoch_date -end_date $info.CreationTime) - lastaccesstime = (DateTo-Timestamp -start_date $epoch_date -end_date $info.LastAccessTime) - lastwritetime = (DateTo-Timestamp -start_date $epoch_date -end_date $info.LastWriteTime) + creationtime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.CreationTime) + lastaccesstime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.LastAccessTime) + lastwritetime = (ConvertTo-Timestamp -start_date $epoch_date -end_date $info.LastWriteTime) # size = a file and directory - calculated below path = $info.FullName filename = $info.Name @@ -100,8 +124,8 @@ If ($null -ne $info) { # values that are set according to the type of file if ($info.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) { $stat.isdir = $true - $share_info = Get-WmiObject -Class Win32_Share -Filter "Path='$($stat.path -replace '\\', '\\')'" - if ($share_info -ne $null) { + $share_info = Get-CimInstance -ClassName Win32_Share -Filter "Path='$($stat.path -replace '\\', '\\')'" + if ($null -ne $share_info) { $stat.isshared = $true $stat.sharename = $share_info.Name } @@ -137,13 +161,7 @@ If ($null -ne $info) { } # Get symbolic link, junction point, hard link info - Load-LinkUtils - try { - $link_info = Get-Link -link_path $info.FullName - } catch { - $module.Warn("Failed to check/get link info for file: $($_.Exception.Message)") - } - if ($link_info -ne $null) { + if ($null -ne $link_info) { switch ($link_info.Type) { "SymbolicLink" { $stat.islnk = $true @@ -175,3 +193,4 @@ If ($null -ne $info) { } $module.ExitJson() + diff --git a/lib/ansible/modules/windows/win_stat.py b/lib/ansible/modules/windows/win_stat.py index 973c1357c57..cf054f05e98 100644 --- a/lib/ansible/modules/windows/win_stat.py +++ b/lib/ansible/modules/windows/win_stat.py @@ -52,6 +52,14 @@ options: default: sha1 choices: [ md5, sha1, sha256, sha384, sha512 ] version_added: "2.3" + follow: + description: + - Whether to follow symlinks or junction points. + - In the case of C(path) pointing to another link, then that will + be followed until no more links are found. + type: bool + default: no + version_added: "2.8" seealso: - module: stat - module: win_file diff --git a/lib/ansible/plugins/action/__init__.py b/lib/ansible/plugins/action/__init__.py index 5b11f256db1..745f0c1a2f3 100644 --- a/lib/ansible/plugins/action/__init__.py +++ b/lib/ansible/plugins/action/__init__.py @@ -509,7 +509,7 @@ class ActionBase(with_metaclass(ABCMeta, object)): path=path, follow=follow, get_checksum=checksum, - checksum_algo='sha1', + checksum_algorithm='sha1', ) mystat = self._execute_module(module_name='stat', module_args=module_args, task_vars=all_vars, wrap_async=False) diff --git a/test/integration/targets/win_stat/tasks/main.yml b/test/integration/targets/win_stat/tasks/main.yml index 51d2aa4da06..a016cde6557 100644 --- a/test/integration/targets/win_stat/tasks/main.yml +++ b/test/integration/targets/win_stat/tasks/main.yml @@ -54,7 +54,8 @@ cmd.exe /c mklink /H "{{win_stat_dir}}\nested\hard-link.ps1" "{{win_stat_dir}}\nested\hard-target.txt" cmd.exe /c mklink /J "{{win_stat_dir}}\junction-link" "{{win_stat_dir}}\junction-dest" cmd.exe /c mklink /D "{{win_stat_dir}}\nested\nested\link-rel" "..\..\link-dest" - + cmd.exe /c mklink /D "{{win_stat_dir}}\outer-link" "{{win_stat_dir}}\nested\nested\link-rel" + $date = Get-Date -Year 2016 -Month 11 -Day 1 -Hour 7 -Minute 10 -Second 5 -Millisecond 0 Get-ChildItem -Path "{{win_stat_dir}}" -Recurse | ForEach-Object { $_.CreationTime = $date diff --git a/test/integration/targets/win_stat/tasks/tests.yml b/test/integration/targets/win_stat/tasks/tests.yml index 9777d645f48..d8eb111280a 100644 --- a/test/integration/targets/win_stat/tasks/tests.yml +++ b/test/integration/targets/win_stat/tasks/tests.yml @@ -140,6 +140,7 @@ - name: test win_stat on hard link file win_stat: path: '{{win_stat_dir}}\nested\hard-link.ps1' + follow: True # just verifies we don't do any weird follow logic for hard links register: stat_hard_link - name: check actual for hard link file @@ -386,6 +387,37 @@ - stat_file_symlink.stat.owner == 'BUILTIN\\Administrators' - stat_file_symlink.stat.path == win_stat_dir + '\\file-link.txt' +- name: test win_stat of file symlink with follow + win_stat: + path: '{{win_stat_dir}}\file-link.txt' + follow: True + register: stat_file_symlink_follow + +- name: assert file system with follow actual + assert: + that: + - stat_file_symlink_follow.stat.attributes == 'Archive' + - stat_file_symlink_follow.stat.checksum == 'a9993e364706816aba3e25717850c26c9cd0d89d' + - stat_file_symlink_follow.stat.creationtime is defined + - stat_file_symlink_follow.stat.exists == True + - stat_file_symlink_follow.stat.extension == '.ps1' + - stat_file_symlink_follow.stat.filename == 'file.ps1' + - stat_file_symlink_follow.stat.hlnk_targets == [] + - stat_file_symlink_follow.stat.isarchive == True + - stat_file_symlink_follow.stat.isdir == False + - stat_file_symlink_follow.stat.ishidden == False + - stat_file_symlink_follow.stat.isjunction == False + - stat_file_symlink_follow.stat.islnk == False + - stat_file_symlink_follow.stat.isreadonly == False + - stat_file_symlink_follow.stat.isreg == True + - stat_file_symlink_follow.stat.isshared == False + - stat_file_symlink_follow.stat.lastaccesstime is defined + - stat_file_symlink_follow.stat.lastwritetime is defined + - stat_file_symlink_follow.stat.md5 is not defined + - stat_file_symlink_follow.stat.nlink == 1 + - stat_file_symlink_follow.stat.owner == 'BUILTIN\\Administrators' + - stat_file_symlink_follow.stat.path == win_stat_dir + '\\nested\\file.ps1' + - name: test win_stat on relative symlink win_stat: path: '{{win_stat_dir}}\nested\nested\link-rel' @@ -417,6 +449,36 @@ - stat_rel_symlink.stat.checksum is not defined - stat_rel_symlink.stat.md5 is not defined +- name: test win_stat on relative multiple symlink with follow + win_stat: + path: '{{win_stat_dir}}\outer-link' + follow: True + register: stat_symlink_follow + +- name: assert directory relative symlink actual + assert: + that: + - stat_symlink_follow.stat.attributes == 'Directory' + - stat_symlink_follow.stat.creationtime is defined + - stat_symlink_follow.stat.exists == True + - stat_symlink_follow.stat.filename == 'link-dest' + - stat_symlink_follow.stat.hlnk_targets == [] + - stat_symlink_follow.stat.isarchive == False + - stat_symlink_follow.stat.isdir == True + - stat_symlink_follow.stat.ishidden == False + - stat_symlink_follow.stat.isjunction == False + - stat_symlink_follow.stat.islnk == False + - stat_symlink_follow.stat.isreadonly == False + - stat_symlink_follow.stat.isreg == False + - stat_symlink_follow.stat.isshared == False + - stat_symlink_follow.stat.lastaccesstime is defined + - stat_symlink_follow.stat.lastwritetime is defined + - stat_symlink_follow.stat.nlink == 1 + - stat_symlink_follow.stat.owner == 'BUILTIN\\Administrators' + - stat_symlink_follow.stat.path == win_stat_dir + '\\link-dest' + - stat_symlink_follow.stat.checksum is not defined + - stat_symlink_follow.stat.md5 is not defined + - name: test win_stat on junction win_stat: path: '{{win_stat_dir}}\junction-link' @@ -447,6 +509,35 @@ - stat_junction_point.stat.path == win_stat_dir + '\\junction-link' - stat_junction_point.stat.size == 0 +- name: test win_stat on junction with follow + win_stat: + path: '{{win_stat_dir}}\junction-link' + follow: True + register: stat_junction_point_follow + +- name: assert junction with follow actual + assert: + that: + - stat_junction_point_follow.stat.attributes == 'Directory' + - stat_junction_point_follow.stat.creationtime is defined + - stat_junction_point_follow.stat.exists == True + - stat_junction_point_follow.stat.filename == 'junction-dest' + - stat_junction_point_follow.stat.hlnk_targets == [] + - stat_junction_point_follow.stat.isarchive == False + - stat_junction_point_follow.stat.isdir == True + - stat_junction_point_follow.stat.ishidden == False + - stat_junction_point_follow.stat.isjunction == False + - stat_junction_point_follow.stat.islnk == False + - stat_junction_point_follow.stat.isreadonly == False + - stat_junction_point_follow.stat.isreg == False + - stat_junction_point_follow.stat.isshared == False + - stat_junction_point_follow.stat.lastaccesstime is defined + - stat_junction_point_follow.stat.lastwritetime is defined + - stat_junction_point_follow.stat.nlink == 1 + - stat_junction_point_follow.stat.owner == 'BUILTIN\\Administrators' + - stat_junction_point_follow.stat.path == win_stat_dir + '\\junction-dest' + - stat_junction_point_follow.stat.size == 0 + - name: test win_stat module non-existent path win_stat: path: '{{win_stat_dir}}\this_file_should_not_exist' diff --git a/test/sanity/pslint/ignore.txt b/test/sanity/pslint/ignore.txt index f64bf8f4475..e123acfcb14 100644 --- a/test/sanity/pslint/ignore.txt +++ b/test/sanity/pslint/ignore.txt @@ -66,8 +66,6 @@ lib/ansible/modules/windows/win_security_policy.ps1 PSUseApprovedVerbs lib/ansible/modules/windows/win_security_policy.ps1 PSUseDeclaredVarsMoreThanAssignments lib/ansible/modules/windows/win_shell.ps1 PSAvoidUsingCmdletAliases lib/ansible/modules/windows/win_shell.ps1 PSUseApprovedVerbs -lib/ansible/modules/windows/win_stat.ps1 PSAvoidUsingWMICmdlet -lib/ansible/modules/windows/win_stat.ps1 PSUseApprovedVerbs lib/ansible/modules/windows/win_unzip.ps1 PSAvoidUsingCmdletAliases lib/ansible/modules/windows/win_unzip.ps1 PSUseApprovedVerbs lib/ansible/modules/windows/win_uri.ps1 PSAvoidUsingEmptyCatchBlock