diff options
Diffstat (limited to '')
15 files changed, 2424 insertions, 0 deletions
diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/async_status.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/async_status.ps1 new file mode 100644 index 0000000..1ce3ff4 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/async_status.ps1 @@ -0,0 +1,58 @@ +#!powershell + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +$results = @{changed=$false} + +$parsed_args = Parse-Args $args +$jid = Get-AnsibleParam $parsed_args "jid" -failifempty $true -resultobj $results +$mode = Get-AnsibleParam $parsed_args "mode" -Default "status" -ValidateSet "status","cleanup" + +# parsed in from the async_status action plugin +$async_dir = Get-AnsibleParam $parsed_args "_async_dir" -type "path" -failifempty $true + +$log_path = [System.IO.Path]::Combine($async_dir, $jid) + +If(-not $(Test-Path $log_path)) +{ + Fail-Json @{ansible_job_id=$jid; started=1; finished=1} "could not find job at '$async_dir'" +} + +If($mode -eq "cleanup") { + Remove-Item $log_path -Recurse + Exit-Json @{ansible_job_id=$jid; erased=$log_path} +} + +# NOT in cleanup mode, assume regular status mode +# no remote kill mode currently exists, but probably should +# consider log_path + ".pid" file and also unlink that above + +$data = $null +Try { + $data_raw = Get-Content $log_path + + # TODO: move this into module_utils/powershell.ps1? + $jss = New-Object System.Web.Script.Serialization.JavaScriptSerializer + $data = $jss.DeserializeObject($data_raw) +} +Catch { + If(-not $data_raw) { + # file not written yet? That means it is running + Exit-Json @{results_file=$log_path; ansible_job_id=$jid; started=1; finished=0} + } + Else { + Fail-Json @{ansible_job_id=$jid; results_file=$log_path; started=1; finished=1} "Could not parse job output: $data" + } +} + +If (-not $data.ContainsKey("started")) { + $data['finished'] = 1 + $data['ansible_job_id'] = $jid +} +ElseIf (-not $data.ContainsKey("finished")) { + $data['finished'] = 0 +} + +Exit-Json $data diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_acl.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_acl.ps1 new file mode 100644 index 0000000..e3c3813 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_acl.ps1 @@ -0,0 +1,225 @@ +#!powershell + +# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com> +# Copyright: (c) 2015, Trond Hindenes +# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.PrivilegeUtil +#Requires -Module Ansible.ModuleUtils.SID + +$ErrorActionPreference = "Stop" + +# win_acl module (File/Resources Permission Additions/Removal) + +#Functions +function Get-UserSID { + param( + [String]$AccountName + ) + + $userSID = $null + $searchAppPools = $false + + if ($AccountName.Split("\").Count -gt 1) { + if ($AccountName.Split("\")[0] -eq "IIS APPPOOL") { + $searchAppPools = $true + $AccountName = $AccountName.Split("\")[1] + } + } + + if ($searchAppPools) { + Import-Module -Name WebAdministration + $testIISPath = Test-Path -LiteralPath "IIS:" + if ($testIISPath) { + $appPoolObj = Get-ItemProperty -LiteralPath "IIS:\AppPools\$AccountName" + $userSID = $appPoolObj.applicationPoolSid + } + } + else { + $userSID = Convert-ToSID -account_name $AccountName + } + + return $userSID +} + +$params = Parse-Args $args + +Function SetPrivilegeTokens() { + # Set privilege tokens only if admin. + # Admins would have these privs or be able to set these privs in the UI Anyway + + $adminRole=[System.Security.Principal.WindowsBuiltInRole]::Administrator + $myWindowsID=[System.Security.Principal.WindowsIdentity]::GetCurrent() + $myWindowsPrincipal=new-object System.Security.Principal.WindowsPrincipal($myWindowsID) + + + if ($myWindowsPrincipal.IsInRole($adminRole)) { + # Need to adjust token privs when executing Set-ACL in certain cases. + # e.g. d:\testdir is owned by group in which current user is not a member and no perms are inherited from d:\ + # This also sets us up for setting the owner as a feature. + # See the following for details of each privilege + # https://msdn.microsoft.com/en-us/library/windows/desktop/bb530716(v=vs.85).aspx + $privileges = @( + "SeRestorePrivilege", # Grants all write access control to any file, regardless of ACL. + "SeBackupPrivilege", # Grants all read access control to any file, regardless of ACL. + "SeTakeOwnershipPrivilege" # Grants ability to take owernship of an object w/out being granted discretionary access + ) + foreach ($privilege in $privileges) { + $state = Get-AnsiblePrivilege -Name $privilege + if ($state -eq $false) { + Set-AnsiblePrivilege -Name $privilege -Value $true + } + } + } +} + + +$result = @{ + changed = $false +} + +$path = Get-AnsibleParam -obj $params -name "path" -type "str" -failifempty $true +$user = Get-AnsibleParam -obj $params -name "user" -type "str" -failifempty $true +$rights = Get-AnsibleParam -obj $params -name "rights" -type "str" -failifempty $true + +$type = Get-AnsibleParam -obj $params -name "type" -type "str" -failifempty $true -validateset "allow","deny" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present" + +$inherit = Get-AnsibleParam -obj $params -name "inherit" -type "str" +$propagation = Get-AnsibleParam -obj $params -name "propagation" -type "str" -default "None" -validateset "InheritOnly","None","NoPropagateInherit" + +# We mount the HKCR, HKU, and HKCC registry hives so PS can access them. +# Network paths have no qualifiers so we use -EA SilentlyContinue to ignore that +$path_qualifier = Split-Path -Path $path -Qualifier -ErrorAction SilentlyContinue +if ($path_qualifier -eq "HKCR:" -and (-not (Test-Path -LiteralPath HKCR:\))) { + New-PSDrive -Name HKCR -PSProvider Registry -Root HKEY_CLASSES_ROOT > $null +} +if ($path_qualifier -eq "HKU:" -and (-not (Test-Path -LiteralPath HKU:\))) { + New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS > $null +} +if ($path_qualifier -eq "HKCC:" -and (-not (Test-Path -LiteralPath HKCC:\))) { + New-PSDrive -Name HKCC -PSProvider Registry -Root HKEY_CURRENT_CONFIG > $null +} + +If (-Not (Test-Path -LiteralPath $path)) { + Fail-Json -obj $result -message "$path file or directory does not exist on the host" +} + +# Test that the user/group is resolvable on the local machine +$sid = Get-UserSID -AccountName $user +if (!$sid) { + Fail-Json -obj $result -message "$user is not a valid user or group on the host machine or domain" +} + +If (Test-Path -LiteralPath $path -PathType Leaf) { + $inherit = "None" +} +ElseIf ($null -eq $inherit) { + $inherit = "ContainerInherit, ObjectInherit" +} + +# Bug in Set-Acl, Get-Acl where -LiteralPath only works for the Registry provider if the location is in that root +# qualifier. We also don't have a qualifier for a network path so only change if not null +if ($null -ne $path_qualifier) { + Push-Location -LiteralPath $path_qualifier +} + +Try { + SetPrivilegeTokens + $path_item = Get-Item -LiteralPath $path -Force + If ($path_item.PSProvider.Name -eq "Registry") { + $colRights = [System.Security.AccessControl.RegistryRights]$rights + } + Else { + $colRights = [System.Security.AccessControl.FileSystemRights]$rights + } + + $InheritanceFlag = [System.Security.AccessControl.InheritanceFlags]$inherit + $PropagationFlag = [System.Security.AccessControl.PropagationFlags]$propagation + + If ($type -eq "allow") { + $objType =[System.Security.AccessControl.AccessControlType]::Allow + } + Else { + $objType =[System.Security.AccessControl.AccessControlType]::Deny + } + + $objUser = New-Object System.Security.Principal.SecurityIdentifier($sid) + If ($path_item.PSProvider.Name -eq "Registry") { + $objACE = New-Object System.Security.AccessControl.RegistryAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType) + } + Else { + $objACE = New-Object System.Security.AccessControl.FileSystemAccessRule ($objUser, $colRights, $InheritanceFlag, $PropagationFlag, $objType) + } + $objACL = Get-ACL -LiteralPath $path + + # Check if the ACE exists already in the objects ACL list + $match = $false + + ForEach($rule in $objACL.GetAccessRules($true, $true, [System.Security.Principal.SecurityIdentifier])){ + + If ($path_item.PSProvider.Name -eq "Registry") { + If (($rule.RegistryRights -eq $objACE.RegistryRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) { + $match = $true + Break + } + } else { + If (($rule.FileSystemRights -eq $objACE.FileSystemRights) -And ($rule.AccessControlType -eq $objACE.AccessControlType) -And ($rule.IdentityReference -eq $objACE.IdentityReference) -And ($rule.IsInherited -eq $objACE.IsInherited) -And ($rule.InheritanceFlags -eq $objACE.InheritanceFlags) -And ($rule.PropagationFlags -eq $objACE.PropagationFlags)) { + $match = $true + Break + } + } + } + + If ($state -eq "present" -And $match -eq $false) { + Try { + $objACL.AddAccessRule($objACE) + If ($path_item.PSProvider.Name -eq "Registry") { + Set-ACL -LiteralPath $path -AclObject $objACL + } else { + (Get-Item -LiteralPath $path).SetAccessControl($objACL) + } + $result.changed = $true + } + Catch { + Fail-Json -obj $result -message "an exception occurred when adding the specified rule - $($_.Exception.Message)" + } + } + ElseIf ($state -eq "absent" -And $match -eq $true) { + Try { + $objACL.RemoveAccessRule($objACE) + If ($path_item.PSProvider.Name -eq "Registry") { + Set-ACL -LiteralPath $path -AclObject $objACL + } else { + (Get-Item -LiteralPath $path).SetAccessControl($objACL) + } + $result.changed = $true + } + Catch { + Fail-Json -obj $result -message "an exception occurred when removing the specified rule - $($_.Exception.Message)" + } + } + Else { + # A rule was attempting to be added but already exists + If ($match -eq $true) { + Exit-Json -obj $result -message "the specified rule already exists" + } + # A rule didn't exist that was trying to be removed + Else { + Exit-Json -obj $result -message "the specified rule does not exist" + } + } +} +Catch { + Fail-Json -obj $result -message "an error occurred when attempting to $state $rights permission(s) on $path for $user - $($_.Exception.Message)" +} +Finally { + # Make sure we revert the location stack to the original path just for cleanups sake + if ($null -ne $path_qualifier) { + Pop-Location + } +} + +Exit-Json -obj $result diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_acl.py b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_acl.py new file mode 100644 index 0000000..14fbd82 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_acl.py @@ -0,0 +1,132 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Phil Schwartz <schwartzmx@gmail.com> +# Copyright: (c) 2015, Trond Hindenes +# Copyright: (c) 2015, Hans-Joachim Kliemeck <git@kliemeck.de> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'core'} + +DOCUMENTATION = r''' +--- +module: win_acl +version_added: "2.0" +short_description: Set file/directory/registry permissions for a system user or group +description: +- Add or remove rights/permissions for a given user or group for the specified + file, folder, registry key or AppPool identifies. +options: + path: + description: + - The path to the file or directory. + type: str + required: yes + user: + description: + - User or Group to add specified rights to act on src file/folder or + registry key. + type: str + required: yes + state: + description: + - Specify whether to add C(present) or remove C(absent) the specified access rule. + type: str + choices: [ absent, present ] + default: present + type: + description: + - Specify whether to allow or deny the rights specified. + type: str + required: yes + choices: [ allow, deny ] + rights: + description: + - The rights/permissions that are to be allowed/denied for the specified + user or group for the item at C(path). + - If C(path) is a file or directory, rights can be any right under MSDN + FileSystemRights U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.filesystemrights.aspx). + - If C(path) is a registry key, rights can be any right under MSDN + RegistryRights U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.registryrights.aspx). + type: str + required: yes + inherit: + description: + - Inherit flags on the ACL rules. + - Can be specified as a comma separated list, e.g. C(ContainerInherit), + C(ObjectInherit). + - For more information on the choices see MSDN InheritanceFlags enumeration + at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.inheritanceflags.aspx). + - Defaults to C(ContainerInherit, ObjectInherit) for Directories. + type: str + choices: [ ContainerInherit, ObjectInherit ] + propagation: + description: + - Propagation flag on the ACL rules. + - For more information on the choices see MSDN PropagationFlags enumeration + at U(https://msdn.microsoft.com/en-us/library/system.security.accesscontrol.propagationflags.aspx). + type: str + choices: [ InheritOnly, None, NoPropagateInherit ] + default: "None" +notes: +- If adding ACL's for AppPool identities (available since 2.3), the Windows + Feature "Web-Scripting-Tools" must be enabled. +seealso: +- module: win_acl_inheritance +- module: win_file +- module: win_owner +- module: win_stat +author: +- Phil Schwartz (@schwartzmx) +- Trond Hindenes (@trondhindenes) +- Hans-Joachim Kliemeck (@h0nIg) +''' + +EXAMPLES = r''' +- name: Restrict write and execute access to User Fed-Phil + win_acl: + user: Fed-Phil + path: C:\Important\Executable.exe + type: deny + rights: ExecuteFile,Write + +- name: Add IIS_IUSRS allow rights + win_acl: + path: C:\inetpub\wwwroot\MySite + user: IIS_IUSRS + rights: FullControl + type: allow + state: present + inherit: ContainerInherit, ObjectInherit + propagation: 'None' + +- name: Set registry key right + win_acl: + path: HKCU:\Bovine\Key + user: BUILTIN\Users + rights: EnumerateSubKeys + type: allow + state: present + inherit: ContainerInherit, ObjectInherit + propagation: 'None' + +- name: Remove FullControl AccessRule for IIS_IUSRS + win_acl: + path: C:\inetpub\wwwroot\MySite + user: IIS_IUSRS + rights: FullControl + type: allow + state: absent + inherit: ContainerInherit, ObjectInherit + propagation: 'None' + +- name: Deny Intern + win_acl: + path: C:\Administrator\Documents + user: Intern + rights: Read,Write,Modify,FullControl,Delete + type: deny + state: present +''' diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_copy.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_copy.ps1 new file mode 100644 index 0000000..6a26ee7 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_copy.ps1 @@ -0,0 +1,403 @@ +#!powershell + +# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk> +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.Backup + +$ErrorActionPreference = 'Stop' + +$params = Parse-Args -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$diff_mode = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false + +# there are 4 modes to win_copy which are driven by the action plugins: +# explode: src is a zip file which needs to be extracted to dest, for use with multiple files +# query: win_copy action plugin wants to get the state of remote files to check whether it needs to send them +# remote: all copy action is happening remotely (remote_src=True) +# single: a single file has been copied, also used with template +$copy_mode = Get-AnsibleParam -obj $params -name "_copy_mode" -type "str" -default "single" -validateset "explode","query","remote","single" + +# used in explode, remote and single mode +$src = Get-AnsibleParam -obj $params -name "src" -type "path" -failifempty ($copy_mode -in @("explode","process","single")) +$dest = Get-AnsibleParam -obj $params -name "dest" -type "path" -failifempty $true +$backup = Get-AnsibleParam -obj $params -name "backup" -type "bool" -default $false + +# used in single mode +$original_basename = Get-AnsibleParam -obj $params -name "_original_basename" -type "str" + +# used in query and remote mode +$force = Get-AnsibleParam -obj $params -name "force" -type "bool" -default $true + +# used in query mode, contains the local files/directories/symlinks that are to be copied +$files = Get-AnsibleParam -obj $params -name "files" -type "list" +$directories = Get-AnsibleParam -obj $params -name "directories" -type "list" + +$result = @{ + changed = $false +} + +if ($diff_mode) { + $result.diff = @{} +} + +Function Copy-File($source, $dest) { + $diff = "" + $copy_file = $false + $source_checksum = $null + if ($force) { + $source_checksum = Get-FileChecksum -path $source + } + + if (Test-Path -LiteralPath $dest -PathType Container) { + Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': dest is already a folder" + } elseif (Test-Path -LiteralPath $dest -PathType Leaf) { + if ($force) { + $target_checksum = Get-FileChecksum -path $dest + if ($source_checksum -ne $target_checksum) { + $copy_file = $true + } + } + } else { + $copy_file = $true + } + + if ($copy_file) { + $file_dir = [System.IO.Path]::GetDirectoryName($dest) + # validate the parent dir is not a file and that it exists + if (Test-Path -LiteralPath $file_dir -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': object at dest parent dir is not a folder" + } elseif (-not (Test-Path -LiteralPath $file_dir)) { + # directory doesn't exist, need to create + New-Item -Path $file_dir -ItemType Directory -WhatIf:$check_mode | Out-Null + $diff += "+$file_dir\`n" + } + + if ($backup) { + $result.backup_file = Backup-File -path $dest -WhatIf:$check_mode + } + + if (Test-Path -LiteralPath $dest -PathType Leaf) { + Remove-Item -LiteralPath $dest -Force -Recurse -WhatIf:$check_mode | Out-Null + $diff += "-$dest`n" + } + + if (-not $check_mode) { + # cannot run with -WhatIf:$check_mode as if the parent dir didn't + # exist and was created above would still not exist in check mode + Copy-Item -LiteralPath $source -Destination $dest -Force | Out-Null + } + $diff += "+$dest`n" + + $result.changed = $true + } + + # ugly but to save us from running the checksum twice, let's return it for + # the main code to add it to $result + return ,@{ diff = $diff; checksum = $source_checksum } +} + +Function Copy-Folder($source, $dest) { + $diff = "" + + if (-not (Test-Path -LiteralPath $dest -PathType Container)) { + $parent_dir = [System.IO.Path]::GetDirectoryName($dest) + if (Test-Path -LiteralPath $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy file from '$source' to '$dest': object at dest parent dir is not a folder" + } + if (Test-Path -LiteralPath $dest -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy folder from '$source' to '$dest': dest is already a file" + } + + New-Item -Path $dest -ItemType Container -WhatIf:$check_mode | Out-Null + $diff += "+$dest\`n" + $result.changed = $true + } + + $child_items = Get-ChildItem -LiteralPath $source -Force + foreach ($child_item in $child_items) { + $dest_child_path = Join-Path -Path $dest -ChildPath $child_item.Name + if ($child_item.PSIsContainer) { + $diff += (Copy-Folder -source $child_item.Fullname -dest $dest_child_path) + } else { + $diff += (Copy-File -source $child_item.Fullname -dest $dest_child_path).diff + } + } + + return $diff +} + +Function Get-FileSize($path) { + $file = Get-Item -LiteralPath $path -Force + if ($file.PSIsContainer) { + $size = (Get-ChildItem -Literalpath $file.FullName -Recurse -Force | ` + Where-Object { $_.PSObject.Properties.Name -contains 'Length' } | ` + Measure-Object -Property Length -Sum).Sum + if ($null -eq $size) { + $size = 0 + } + } else { + $size = $file.Length + } + + $size +} + +Function Extract-Zip($src, $dest) { + $archive = [System.IO.Compression.ZipFile]::Open($src, [System.IO.Compression.ZipArchiveMode]::Read, [System.Text.Encoding]::UTF8) + foreach ($entry in $archive.Entries) { + $archive_name = $entry.FullName + + # FullName may be appended with / or \, determine if it is padded and remove it + $padding_length = $archive_name.Length % 4 + if ($padding_length -eq 0) { + $is_dir = $false + $base64_name = $archive_name + } elseif ($padding_length -eq 1) { + $is_dir = $true + if ($archive_name.EndsWith("/") -or $archive_name.EndsWith("`\")) { + $base64_name = $archive_name.Substring(0, $archive_name.Length - 1) + } else { + throw "invalid base64 archive name '$archive_name'" + } + } else { + throw "invalid base64 length '$archive_name'" + } + + # to handle unicode character, win_copy action plugin has encoded the filename + $decoded_archive_name = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($base64_name)) + # re-add the / to the entry full name if it was a directory + if ($is_dir) { + $decoded_archive_name = "$decoded_archive_name/" + } + $entry_target_path = [System.IO.Path]::Combine($dest, $decoded_archive_name) + $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path) + + if (-not (Test-Path -LiteralPath $entry_dir)) { + New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null + } + + if ($is_dir -eq $false) { + if (-not $check_mode) { + [System.IO.Compression.ZipFileExtensions]::ExtractToFile($entry, $entry_target_path, $true) + } + } + } + $archive.Dispose() # release the handle of the zip file +} + +Function Extract-ZipLegacy($src, $dest) { + if (-not (Test-Path -LiteralPath $dest)) { + New-Item -Path $dest -ItemType Directory -WhatIf:$check_mode | Out-Null + } + $shell = New-Object -ComObject Shell.Application + $zip = $shell.NameSpace($src) + $dest_path = $shell.NameSpace($dest) + + foreach ($entry in $zip.Items()) { + $is_dir = $entry.IsFolder + $encoded_archive_entry = $entry.Name + # to handle unicode character, win_copy action plugin has encoded the filename + $decoded_archive_entry = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($encoded_archive_entry)) + if ($is_dir) { + $decoded_archive_entry = "$decoded_archive_entry/" + } + + $entry_target_path = [System.IO.Path]::Combine($dest, $decoded_archive_entry) + $entry_dir = [System.IO.Path]::GetDirectoryName($entry_target_path) + + if (-not (Test-Path -LiteralPath $entry_dir)) { + New-Item -Path $entry_dir -ItemType Directory -WhatIf:$check_mode | Out-Null + } + + if ($is_dir -eq $false -and (-not $check_mode)) { + # https://msdn.microsoft.com/en-us/library/windows/desktop/bb787866.aspx + # From Folder.CopyHere documentation, 1044 means: + # - 1024: do not display a user interface if an error occurs + # - 16: respond with "yes to all" for any dialog box that is displayed + # - 4: do not display a progress dialog box + $dest_path.CopyHere($entry, 1044) + + # once file is extraced, we need to rename it with non base64 name + $combined_encoded_path = [System.IO.Path]::Combine($dest, $encoded_archive_entry) + Move-Item -LiteralPath $combined_encoded_path -Destination $entry_target_path -Force | Out-Null + } + } +} + +if ($copy_mode -eq "query") { + # we only return a list of files/directories that need to be copied over + # the source of the local file will be the key used + $changed_files = @() + $changed_directories = @() + $changed_symlinks = @() + + foreach ($file in $files) { + $filename = $file.dest + $local_checksum = $file.checksum + + $filepath = Join-Path -Path $dest -ChildPath $filename + if (Test-Path -LiteralPath $filepath -PathType Leaf) { + if ($force) { + $checksum = Get-FileChecksum -path $filepath + if ($checksum -ne $local_checksum) { + $changed_files += $file + } + } + } elseif (Test-Path -LiteralPath $filepath -PathType Container) { + Fail-Json -obj $result -message "cannot copy file to dest '$filepath': object at path is already a directory" + } else { + $changed_files += $file + } + } + + foreach ($directory in $directories) { + $dirname = $directory.dest + + $dirpath = Join-Path -Path $dest -ChildPath $dirname + $parent_dir = [System.IO.Path]::GetDirectoryName($dirpath) + if (Test-Path -LiteralPath $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy folder to dest '$dirpath': object at parent directory path is already a file" + } + if (Test-Path -LiteralPath $dirpath -PathType Leaf) { + Fail-Json -obj $result -message "cannot copy folder to dest '$dirpath': object at path is already a file" + } elseif (-not (Test-Path -LiteralPath $dirpath -PathType Container)) { + $changed_directories += $directory + } + } + + # TODO: Handle symlinks + + $result.files = $changed_files + $result.directories = $changed_directories + $result.symlinks = $changed_symlinks +} elseif ($copy_mode -eq "explode") { + # a single zip file containing the files and directories needs to be + # expanded this will always result in a change as the calculation is done + # on the win_copy action plugin and is only run if a change needs to occur + if (-not (Test-Path -LiteralPath $src -PathType Leaf)) { + Fail-Json -obj $result -message "Cannot expand src zip file: '$src' as it does not exist" + } + + # Detect if the PS zip assemblies are available or whether to use Shell + $use_legacy = $false + try { + Add-Type -AssemblyName System.IO.Compression.FileSystem | Out-Null + Add-Type -AssemblyName System.IO.Compression | Out-Null + } catch { + $use_legacy = $true + } + if ($use_legacy) { + Extract-ZipLegacy -src $src -dest $dest + } else { + Extract-Zip -src $src -dest $dest + } + + $result.changed = $true +} elseif ($copy_mode -eq "remote") { + # all copy actions are happening on the remote side (windows host), need + # too copy source and dest using PS code + $result.src = $src + $result.dest = $dest + + if (-not (Test-Path -LiteralPath $src)) { + Fail-Json -obj $result -message "Cannot copy src file: '$src' as it does not exist" + } + + if (Test-Path -LiteralPath $src -PathType Container) { + # we are copying a directory or the contents of a directory + $result.operation = 'folder_copy' + if ($src.EndsWith("/") -or $src.EndsWith("`\")) { + # copying the folder's contents to dest + $diff = "" + $child_files = Get-ChildItem -LiteralPath $src -Force + foreach ($child_file in $child_files) { + $dest_child_path = Join-Path -Path $dest -ChildPath $child_file.Name + if ($child_file.PSIsContainer) { + $diff += Copy-Folder -source $child_file.FullName -dest $dest_child_path + } else { + $diff += (Copy-File -source $child_file.FullName -dest $dest_child_path).diff + } + } + } else { + # copying the folder and it's contents to dest + $dest = Join-Path -Path $dest -ChildPath (Get-Item -LiteralPath $src -Force).Name + $result.dest = $dest + $diff = Copy-Folder -source $src -dest $dest + } + } else { + # we are just copying a single file to dest + $result.operation = 'file_copy' + + $source_basename = (Get-Item -LiteralPath $src -Force).Name + $result.original_basename = $source_basename + + if ($dest.EndsWith("/") -or $dest.EndsWith("`\")) { + $dest = Join-Path -Path $dest -ChildPath (Get-Item -LiteralPath $src -Force).Name + $result.dest = $dest + } else { + # check if the parent dir exists, this is only done if src is a + # file and dest if the path to a file (doesn't end with \ or /) + $parent_dir = Split-Path -LiteralPath $dest + if (Test-Path -LiteralPath $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file" + } elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) { + Fail-Json -obj $result -message "Destination directory '$parent_dir' does not exist" + } + } + $copy_result = Copy-File -source $src -dest $dest + $diff = $copy_result.diff + $result.checksum = $copy_result.checksum + } + + # the file might not exist if running in check mode + if (-not $check_mode -or (Test-Path -LiteralPath $dest -PathType Leaf)) { + $result.size = Get-FileSize -path $dest + } else { + $result.size = $null + } + if ($diff_mode) { + $result.diff.prepared = $diff + } +} elseif ($copy_mode -eq "single") { + # a single file is located in src and we need to copy to dest, this will + # always result in a change as the calculation is done on the Ansible side + # before this is run. This should also never run in check mode + if (-not (Test-Path -LiteralPath $src -PathType Leaf)) { + Fail-Json -obj $result -message "Cannot copy src file: '$src' as it does not exist" + } + + # the dest parameter is a directory, we need to append original_basename + if ($dest.EndsWith("/") -or $dest.EndsWith("`\") -or (Test-Path -LiteralPath $dest -PathType Container)) { + $remote_dest = Join-Path -Path $dest -ChildPath $original_basename + $parent_dir = Split-Path -LiteralPath $remote_dest + + # when dest ends with /, we need to create the destination directories + if (Test-Path -LiteralPath $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file" + } elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) { + New-Item -Path $parent_dir -ItemType Directory | Out-Null + } + } else { + $remote_dest = $dest + $parent_dir = Split-Path -LiteralPath $remote_dest + + # check if the dest parent dirs exist, need to fail if they don't + if (Test-Path -LiteralPath $parent_dir -PathType Leaf) { + Fail-Json -obj $result -message "object at destination parent dir '$parent_dir' is currently a file" + } elseif (-not (Test-Path -LiteralPath $parent_dir -PathType Container)) { + Fail-Json -obj $result -message "Destination directory '$parent_dir' does not exist" + } + } + + if ($backup) { + $result.backup_file = Backup-File -path $remote_dest -WhatIf:$check_mode + } + + Copy-Item -LiteralPath $src -Destination $remote_dest -Force | Out-Null + $result.changed = $true +} + +Exit-Json -obj $result diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_copy.py b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_copy.py new file mode 100644 index 0000000..a55f4c6 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_copy.py @@ -0,0 +1,207 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk> +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['stableinterface'], + 'supported_by': 'core'} + +DOCUMENTATION = r''' +--- +module: win_copy +version_added: '1.9.2' +short_description: Copies files to remote locations on windows hosts +description: +- The C(win_copy) module copies a file on the local box to remote windows locations. +- For non-Windows targets, use the M(copy) module instead. +options: + content: + description: + - When used instead of C(src), sets the contents of a file directly to the + specified value. + - This is for simple values, for anything complex or with formatting please + switch to the M(template) module. + type: str + version_added: '2.3' + decrypt: + description: + - This option controls the autodecryption of source files using vault. + type: bool + default: yes + version_added: '2.5' + dest: + description: + - Remote absolute path where the file should be copied to. + - If C(src) is a directory, this must be a directory too. + - Use \ for path separators or \\ when in "double quotes". + - If C(dest) ends with \ then source or the contents of source will be + copied to the directory without renaming. + - If C(dest) is a nonexistent path, it will only be created if C(dest) ends + with "/" or "\", or C(src) is a directory. + - If C(src) and C(dest) are files and if the parent directory of C(dest) + doesn't exist, then the task will fail. + type: path + required: yes + backup: + description: + - Determine whether a backup should be created. + - When set to C(yes), create a backup file including the timestamp information + so you can get the original file back if you somehow clobbered it incorrectly. + - No backup is taken when C(remote_src=False) and multiple files are being + copied. + type: bool + default: no + version_added: '2.8' + force: + description: + - If set to C(yes), the file will only be transferred if the content + is different than destination. + - If set to C(no), the file will only be transferred if the + destination does not exist. + - If set to C(no), no checksuming of the content is performed which can + help improve performance on larger files. + type: bool + default: yes + version_added: '2.3' + local_follow: + description: + - This flag indicates that filesystem links in the source tree, if they + exist, should be followed. + type: bool + default: yes + version_added: '2.4' + remote_src: + description: + - If C(no), it will search for src at originating/master machine. + - If C(yes), it will go to the remote/target machine for the src. + type: bool + default: no + version_added: '2.3' + src: + description: + - Local path to a file to copy to the remote server; can be absolute or + relative. + - If path is a directory, it is copied (including the source folder name) + recursively to C(dest). + - If path is a directory and ends with "/", only the inside contents of + that directory are copied to the destination. Otherwise, if it does not + end with "/", the directory itself with all contents is copied. + - If path is a file and dest ends with "\", the file is copied to the + folder with the same filename. + - Required unless using C(content). + type: path +notes: +- Currently win_copy does not support copying symbolic links from both local to + remote and remote to remote. +- It is recommended that backslashes C(\) are used instead of C(/) when dealing + with remote paths. +- Because win_copy runs over WinRM, it is not a very efficient transfer + mechanism. If sending large files consider hosting them on a web service and + using M(win_get_url) instead. +seealso: +- module: assemble +- module: copy +- module: win_get_url +- module: win_robocopy +author: +- Jon Hawkesworth (@jhawkesworth) +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: Copy a single file + win_copy: + src: /srv/myfiles/foo.conf + dest: C:\Temp\renamed-foo.conf + +- name: Copy a single file, but keep a backup + win_copy: + src: /srv/myfiles/foo.conf + dest: C:\Temp\renamed-foo.conf + backup: yes + +- name: Copy a single file keeping the filename + win_copy: + src: /src/myfiles/foo.conf + dest: C:\Temp\ + +- name: Copy folder to C:\Temp (results in C:\Temp\temp_files) + win_copy: + src: files/temp_files + dest: C:\Temp + +- name: Copy folder contents recursively + win_copy: + src: files/temp_files/ + dest: C:\Temp + +- name: Copy a single file where the source is on the remote host + win_copy: + src: C:\Temp\foo.txt + dest: C:\ansible\foo.txt + remote_src: yes + +- name: Copy a folder recursively where the source is on the remote host + win_copy: + src: C:\Temp + dest: C:\ansible + remote_src: yes + +- name: Set the contents of a file + win_copy: + content: abc123 + dest: C:\Temp\foo.txt + +- name: Copy a single file as another user + win_copy: + src: NuGet.config + dest: '%AppData%\NuGet\NuGet.config' + vars: + ansible_become_user: user + ansible_become_password: pass + # The tmp dir must be set when using win_copy as another user + # This ensures the become user will have permissions for the operation + # Make sure to specify a folder both the ansible_user and the become_user have access to (i.e not %TEMP% which is user specific and requires Admin) + ansible_remote_tmp: 'c:\tmp' +''' + +RETURN = r''' +backup_file: + description: Name of the backup file that was created. + returned: if backup=yes + type: str + sample: C:\Path\To\File.txt.11540.20150212-220915.bak +dest: + description: Destination file/path. + returned: changed + type: str + sample: C:\Temp\ +src: + description: Source file used for the copy on the target machine. + returned: changed + type: str + sample: /home/httpd/.ansible/tmp/ansible-tmp-1423796390.97-147729857856000/source +checksum: + description: SHA1 checksum of the file after running copy. + returned: success, src is a file + type: str + sample: 6e642bb8dd5c2e027bf21dd923337cbb4214f827 +size: + description: Size of the target, after execution. + returned: changed, src is a file + type: int + sample: 1220 +operation: + description: Whether a single file copy took place or a folder copy. + returned: success + type: str + sample: file_copy +original_basename: + description: Basename of the copied file. + returned: changed, src is a file + type: str + sample: foo.txt +''' diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_file.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_file.ps1 new file mode 100644 index 0000000..5442754 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_file.ps1 @@ -0,0 +1,152 @@ +#!powershell + +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy + +$ErrorActionPreference = "Stop" + +$params = Parse-Args $args -supports_check_mode $true + +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -default $false +$_remote_tmp = Get-AnsibleParam $params "_ansible_remote_tmp" -type "path" -default $env:TMP + +$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true -aliases "dest","name" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -validateset "absent","directory","file","touch" + +# used in template/copy when dest is the path to a dir and source is a file +$original_basename = Get-AnsibleParam -obj $params -name "_original_basename" -type "str" +if ((Test-Path -LiteralPath $path -PathType Container) -and ($null -ne $original_basename)) { + $path = Join-Path -Path $path -ChildPath $original_basename +} + +$result = @{ + changed = $false +} + +# Used to delete symlinks as powershell cannot delete broken symlinks +$symlink_util = @" +using System; +using System.ComponentModel; +using System.Runtime.InteropServices; + +namespace Ansible.Command { + public class SymLinkHelper { + [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] + public static extern bool DeleteFileW(string lpFileName); + + [DllImport("kernel32.dll", CharSet=CharSet.Unicode, SetLastError=true)] + public static extern bool RemoveDirectoryW(string lpPathName); + + public static void DeleteDirectory(string path) { + if (!RemoveDirectoryW(path)) + throw new Exception(String.Format("RemoveDirectoryW({0}) failed: {1}", path, new Win32Exception(Marshal.GetLastWin32Error()).Message)); + } + + public static void DeleteFile(string path) { + if (!DeleteFileW(path)) + throw new Exception(String.Format("DeleteFileW({0}) failed: {1}", path, new Win32Exception(Marshal.GetLastWin32Error()).Message)); + } + } +} +"@ +$original_tmp = $env:TMP +$env:TMP = $_remote_tmp +Add-Type -TypeDefinition $symlink_util +$env:TMP = $original_tmp + +# Used to delete directories and files with logic on handling symbolic links +function Remove-File($file, $checkmode) { + try { + if ($file.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { + # Bug with powershell, if you try and delete a symbolic link that is pointing + # to an invalid path it will fail, using Win32 API to do this instead + if ($file.PSIsContainer) { + if (-not $checkmode) { + [Ansible.Command.SymLinkHelper]::DeleteDirectory($file.FullName) + } + } else { + if (-not $checkmode) { + [Ansible.Command.SymlinkHelper]::DeleteFile($file.FullName) + } + } + } elseif ($file.PSIsContainer) { + Remove-Directory -directory $file -checkmode $checkmode + } else { + Remove-Item -LiteralPath $file.FullName -Force -WhatIf:$checkmode + } + } catch [Exception] { + Fail-Json $result "Failed to delete $($file.FullName): $($_.Exception.Message)" + } +} + +function Remove-Directory($directory, $checkmode) { + foreach ($file in Get-ChildItem -LiteralPath $directory.FullName) { + Remove-File -file $file -checkmode $checkmode + } + Remove-Item -LiteralPath $directory.FullName -Force -Recurse -WhatIf:$checkmode +} + + +if ($state -eq "touch") { + if (Test-Path -LiteralPath $path) { + if (-not $check_mode) { + (Get-ChildItem -LiteralPath $path).LastWriteTime = Get-Date + } + $result.changed = $true + } else { + Write-Output $null | Out-File -LiteralPath $path -Encoding ASCII -WhatIf:$check_mode + $result.changed = $true + } +} + +if (Test-Path -LiteralPath $path) { + $fileinfo = Get-Item -LiteralPath $path -Force + if ($state -eq "absent") { + Remove-File -file $fileinfo -checkmode $check_mode + $result.changed = $true + } else { + if ($state -eq "directory" -and -not $fileinfo.PsIsContainer) { + Fail-Json $result "path $path is not a directory" + } + + if ($state -eq "file" -and $fileinfo.PsIsContainer) { + Fail-Json $result "path $path is not a file" + } + } + +} else { + + # If state is not supplied, test the $path to see if it looks like + # a file or a folder and set state to file or folder + if ($null -eq $state) { + $basename = Split-Path -Path $path -Leaf + if ($basename.length -gt 0) { + $state = "file" + } else { + $state = "directory" + } + } + + if ($state -eq "directory") { + try { + New-Item -Path $path -ItemType Directory -WhatIf:$check_mode | Out-Null + } catch { + if ($_.CategoryInfo.Category -eq "ResourceExists") { + $fileinfo = Get-Item -LiteralPath $_.CategoryInfo.TargetName + if ($state -eq "directory" -and -not $fileinfo.PsIsContainer) { + Fail-Json $result "path $path is not a directory" + } + } else { + Fail-Json $result $_.Exception.Message + } + } + $result.changed = $true + } elseif ($state -eq "file") { + Fail-Json $result "path $path will not be created" + } + +} + +Exit-Json $result diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_file.py b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_file.py new file mode 100644 index 0000000..2814957 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_file.py @@ -0,0 +1,70 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['stableinterface'], + 'supported_by': 'core'} + +DOCUMENTATION = r''' +--- +module: win_file +version_added: "1.9.2" +short_description: Creates, touches or removes files or directories +description: + - Creates (empty) files, updates file modification stamps of existing files, + and can create or remove directories. + - Unlike M(file), does not modify ownership, permissions or manipulate links. + - For non-Windows targets, use the M(file) module instead. +options: + path: + description: + - Path to the file being managed. + required: yes + type: path + aliases: [ dest, name ] + state: + description: + - If C(directory), all immediate subdirectories will be created if they + do not exist. + - If C(file), the file will NOT be created if it does not exist, see the M(copy) + or M(template) module if you want that behavior. + - If C(absent), directories will be recursively deleted, and files will be removed. + - If C(touch), an empty file will be created if the C(path) does not + exist, while an existing file or directory will receive updated file access and + modification times (similar to the way C(touch) works from the command line). + type: str + choices: [ absent, directory, file, touch ] +seealso: +- module: file +- module: win_acl +- module: win_acl_inheritance +- module: win_owner +- module: win_stat +author: +- Jon Hawkesworth (@jhawkesworth) +''' + +EXAMPLES = r''' +- name: Touch a file (creates if not present, updates modification time if present) + win_file: + path: C:\Temp\foo.conf + state: touch + +- name: Remove a file, if present + win_file: + path: C:\Temp\foo.conf + state: absent + +- name: Create directory structure + win_file: + path: C:\Temp\folder\subfolder + state: directory + +- name: Remove directory structure + win_file: + path: C:\Temp + state: absent +''' diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_ping.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_ping.ps1 new file mode 100644 index 0000000..c848b91 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_ping.ps1 @@ -0,0 +1,21 @@ +#!powershell + +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic + +$spec = @{ + options = @{ + data = @{ type = "str"; default = "pong" } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) +$data = $module.Params.data + +if ($data -eq "crash") { + throw "boom" +} + +$module.Result.ping = $data +$module.ExitJson() diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_ping.py b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_ping.py new file mode 100644 index 0000000..6d35f37 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_ping.py @@ -0,0 +1,55 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>, and others +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['stableinterface'], + 'supported_by': 'core'} + +DOCUMENTATION = r''' +--- +module: win_ping +version_added: "1.7" +short_description: A windows version of the classic ping module +description: + - Checks management connectivity of a windows host. + - This is NOT ICMP ping, this is just a trivial test module. + - For non-Windows targets, use the M(ping) module instead. + - For Network targets, use the M(net_ping) module instead. +options: + data: + description: + - Alternate data to return instead of 'pong'. + - If this parameter is set to C(crash), the module will cause an exception. + type: str + default: pong +seealso: +- module: ping +author: +- Chris Church (@cchurch) +''' + +EXAMPLES = r''' +# Test connectivity to a windows host +# ansible winserver -m win_ping + +- name: Example from an Ansible Playbook + win_ping: + +- name: Induce an exception to see what happens + win_ping: + data: crash +''' + +RETURN = r''' +ping: + description: Value provided with the data parameter. + returned: success + type: str + sample: pong +''' diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_shell.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_shell.ps1 new file mode 100644 index 0000000..54aef8d --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_shell.ps1 @@ -0,0 +1,138 @@ +#!powershell + +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.CommandUtil +#Requires -Module Ansible.ModuleUtils.FileUtil + +# TODO: add check mode support + +Set-StrictMode -Version 2 +$ErrorActionPreference = "Stop" + +# Cleanse CLIXML from stderr (sift out error stream data, discard others for now) +Function Cleanse-Stderr($raw_stderr) { + Try { + # NB: this regex isn't perfect, but is decent at finding CLIXML amongst other stderr noise + If($raw_stderr -match "(?s)(?<prenoise1>.*)#< CLIXML(?<prenoise2>.*)(?<clixml><Objs.+</Objs>)(?<postnoise>.*)") { + $clixml = [xml]$matches["clixml"] + + $merged_stderr = "{0}{1}{2}{3}" -f @( + $matches["prenoise1"], + $matches["prenoise2"], + # filter out just the Error-tagged strings for now, and zap embedded CRLF chars + ($clixml.Objs.ChildNodes | Where-Object { $_.Name -eq 'S' } | Where-Object { $_.S -eq 'Error' } | ForEach-Object { $_.'#text'.Replace('_x000D__x000A_','') } | Out-String), + $matches["postnoise"]) | Out-String + + return $merged_stderr.Trim() + + # FUTURE: parse/return other streams + } + Else { + $raw_stderr + } + } + Catch { + "***EXCEPTION PARSING CLIXML: $_***" + $raw_stderr + } +} + +$params = Parse-Args $args -supports_check_mode $false + +$raw_command_line = Get-AnsibleParam -obj $params -name "_raw_params" -type "str" -failifempty $true +$chdir = Get-AnsibleParam -obj $params -name "chdir" -type "path" +$executable = Get-AnsibleParam -obj $params -name "executable" -type "path" +$creates = Get-AnsibleParam -obj $params -name "creates" -type "path" +$removes = Get-AnsibleParam -obj $params -name "removes" -type "path" +$stdin = Get-AnsibleParam -obj $params -name "stdin" -type "str" +$no_profile = Get-AnsibleParam -obj $params -name "no_profile" -type "bool" -default $false +$output_encoding_override = Get-AnsibleParam -obj $params -name "output_encoding_override" -type "str" + +$raw_command_line = $raw_command_line.Trim() + +$result = @{ + changed = $true + cmd = $raw_command_line +} + +if ($creates -and $(Test-AnsiblePath -Path $creates)) { + Exit-Json @{msg="skipped, since $creates exists";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0} +} + +if ($removes -and -not $(Test-AnsiblePath -Path $removes)) { + Exit-Json @{msg="skipped, since $removes does not exist";cmd=$raw_command_line;changed=$false;skipped=$true;rc=0} +} + +$exec_args = $null +If(-not $executable -or $executable -eq "powershell") { + $exec_application = "powershell.exe" + + # force input encoding to preamble-free UTF8 so PS sub-processes (eg, Start-Job) don't blow up + $raw_command_line = "[Console]::InputEncoding = New-Object Text.UTF8Encoding `$false; " + $raw_command_line + + # Base64 encode the command so we don't have to worry about the various levels of escaping + $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_command_line)) + + if ($stdin) { + $exec_args = "-encodedcommand $encoded_command" + } else { + $exec_args = "-noninteractive -encodedcommand $encoded_command" + } + + if ($no_profile) { + $exec_args = "-noprofile $exec_args" + } +} +Else { + # FUTURE: support arg translation from executable (or executable_args?) to process arguments for arbitrary interpreter? + $exec_application = $executable + if (-not ($exec_application.EndsWith(".exe"))) { + $exec_application = "$($exec_application).exe" + } + $exec_args = "/c $raw_command_line" +} + +$command = "`"$exec_application`" $exec_args" +$run_command_arg = @{ + command = $command +} +if ($chdir) { + $run_command_arg['working_directory'] = $chdir +} +if ($stdin) { + $run_command_arg['stdin'] = $stdin +} +if ($output_encoding_override) { + $run_command_arg['output_encoding_override'] = $output_encoding_override +} + +$start_datetime = [DateTime]::UtcNow +try { + $command_result = Run-Command @run_command_arg +} catch { + $result.changed = $false + try { + $result.rc = $_.Exception.NativeErrorCode + } catch { + $result.rc = 2 + } + Fail-Json -obj $result -message $_.Exception.Message +} + +# TODO: decode CLIXML stderr output (and other streams?) +$result.stdout = $command_result.stdout +$result.stderr = Cleanse-Stderr $command_result.stderr +$result.rc = $command_result.rc + +$end_datetime = [DateTime]::UtcNow +$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") +$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") +$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff") + +If ($result.rc -ne 0) { + Fail-Json -obj $result -message "non-zero return code" +} + +Exit-Json $result diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_shell.py b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_shell.py new file mode 100644 index 0000000..ee2cd76 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_shell.py @@ -0,0 +1,167 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2016, Ansible, inc +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'core'} + +DOCUMENTATION = r''' +--- +module: win_shell +short_description: Execute shell commands on target hosts +version_added: 2.2 +description: + - The C(win_shell) module takes the command name followed by a list of space-delimited arguments. + It is similar to the M(win_command) module, but runs + the command via a shell (defaults to PowerShell) on the target host. + - For non-Windows targets, use the M(shell) module instead. +options: + free_form: + description: + - The C(win_shell) module takes a free form command to run. + - There is no parameter actually named 'free form'. See the examples! + type: str + required: yes + creates: + description: + - A path or path filter pattern; when the referenced path exists on the target host, the task will be skipped. + type: path + removes: + description: + - A path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped. + type: path + chdir: + description: + - Set the specified path as the current working directory before executing a command + type: path + executable: + description: + - Change the shell used to execute the command (eg, C(cmd)). + - The target shell must accept a C(/c) parameter followed by the raw command line to be executed. + type: path + stdin: + description: + - Set the stdin of the command directly to the specified value. + type: str + version_added: '2.5' + no_profile: + description: + - Do not load the user profile before running a command. This is only valid + when using PowerShell as the executable. + type: bool + default: no + version_added: '2.8' + output_encoding_override: + description: + - This option overrides the encoding of stdout/stderr output. + - You can use this option when you need to run a command which ignore the console's codepage. + - You should only need to use this option in very rare circumstances. + - This value can be any valid encoding C(Name) based on the output of C([System.Text.Encoding]::GetEncodings()). + See U(https://docs.microsoft.com/dotnet/api/system.text.encoding.getencodings). + type: str + version_added: '2.10' +notes: + - If you want to run an executable securely and predictably, it may be + better to use the M(win_command) module instead. Best practices when writing + playbooks will follow the trend of using M(win_command) unless C(win_shell) is + explicitly required. When running ad-hoc commands, use your best judgement. + - WinRM will not return from a command execution until all child processes created have exited. + Thus, it is not possible to use C(win_shell) to spawn long-running child or background processes. + Consider creating a Windows service for managing background processes. +seealso: +- module: psexec +- module: raw +- module: script +- module: shell +- module: win_command +- module: win_psexec +author: + - Matt Davis (@nitzmahone) +''' + +EXAMPLES = r''' +# Execute a command in the remote shell; stdout goes to the specified +# file on the remote. +- win_shell: C:\somescript.ps1 >> C:\somelog.txt + +# Change the working directory to somedir/ before executing the command. +- win_shell: C:\somescript.ps1 >> C:\somelog.txt chdir=C:\somedir + +# You can also use the 'args' form to provide the options. This command +# will change the working directory to somedir/ and will only run when +# somedir/somelog.txt doesn't exist. +- win_shell: C:\somescript.ps1 >> C:\somelog.txt + args: + chdir: C:\somedir + creates: C:\somelog.txt + +# Run a command under a non-Powershell interpreter (cmd in this case) +- win_shell: echo %HOMEDIR% + args: + executable: cmd + register: homedir_out + +- name: Run multi-lined shell commands + win_shell: | + $value = Test-Path -Path C:\temp + if ($value) { + Remove-Item -Path C:\temp -Force + } + New-Item -Path C:\temp -ItemType Directory + +- name: Retrieve the input based on stdin + win_shell: '$string = [Console]::In.ReadToEnd(); Write-Output $string.Trim()' + args: + stdin: Input message +''' + +RETURN = r''' +msg: + description: Changed. + returned: always + type: bool + sample: true +start: + description: The command execution start time. + returned: always + type: str + sample: '2016-02-25 09:18:26.429568' +end: + description: The command execution end time. + returned: always + type: str + sample: '2016-02-25 09:18:26.755339' +delta: + description: The command execution delta time. + returned: always + type: str + sample: '0:00:00.325771' +stdout: + description: The command standard output. + returned: always + type: str + sample: 'Clustering node rabbit@slave1 with rabbit@master ...' +stderr: + description: The command standard error. + returned: always + type: str + sample: 'ls: cannot access foo: No such file or directory' +cmd: + description: The command executed by the task. + returned: always + type: str + sample: 'rabbitmqctl join_cluster rabbit@master' +rc: + description: The command return code (0 means success). + returned: always + type: int + sample: 0 +stdout_lines: + description: The command standard output split in lines. + returned: always + type: list + sample: [u'Clustering node rabbit@slave1 with rabbit@master ...'] +''' diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1 new file mode 100644 index 0000000..071eb11 --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.ps1 @@ -0,0 +1,186 @@ +#!powershell + +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.FileUtil +#Requires -Module Ansible.ModuleUtils.LinkUtil + +function ConvertTo-Timestamp($start_date, $end_date) { + if ($start_date -and $end_date) { + return (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds + } +} + +function Get-FileChecksum($path, $algorithm) { + switch ($algorithm) { + 'md5' { $sp = New-Object -TypeName System.Security.Cryptography.MD5CryptoServiceProvider } + 'sha1' { $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider } + 'sha256' { $sp = New-Object -TypeName System.Security.Cryptography.SHA256CryptoServiceProvider } + 'sha384' { $sp = New-Object -TypeName System.Security.Cryptography.SHA384CryptoServiceProvider } + 'sha512' { $sp = New-Object -TypeName System.Security.Cryptography.SHA512CryptoServiceProvider } + default { Fail-Json -obj $result -message "Unsupported hash algorithm supplied '$algorithm'" } + } + + $fp = [System.IO.File]::Open($path, [System.IO.Filemode]::Open, [System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite) + try { + $hash = [System.BitConverter]::ToString($sp.ComputeHash($fp)).Replace("-", "").ToLower() + } finally { + $fp.Dispose() + } + + 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' ) } + follow = @{ type='bool'; default=$false } + } + supports_check_mode = $true +} + +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec) + +$path = $module.Params.path +$get_checksum = $module.Params.get_checksum +$checksum_algorithm = $module.Params.checksum_algorithm +$follow = $module.Params.follow + +$module.Result.stat = @{ exists=$false } + +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 ',')) { + $attributes += $attribute.Trim() + } + + # default values that are always set, specific values are set below this + # but are kept commented for easier readability + $stat = @{ + exists = $true + attributes = $info.Attributes.ToString() + isarchive = ($attributes -contains "Archive") + isdir = $false + ishidden = ($attributes -contains "Hidden") + isjunction = $false + islnk = $false + isreadonly = ($attributes -contains "ReadOnly") + isreg = $false + isshared = $false + nlink = 1 # Number of links to the file (hard links), overriden below if islnk + # 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 = (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 + # extension = a file + # owner = set outsite this dict in case it fails + # sharename = a directory and isshared is True + # checksum = a file and get_checksum: True + } + try { + $stat.owner = $info.GetAccessControl().Owner + } catch { + # may not have rights, historical behaviour was to just set to $null + # due to ErrorActionPreference being set to "Continue" + $stat.owner = $null + } + + # values that are set according to the type of file + if ($info.Attributes.HasFlag([System.IO.FileAttributes]::Directory)) { + $stat.isdir = $true + $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 + } + + try { + $size = 0 + foreach ($file in $info.EnumerateFiles("*", [System.IO.SearchOption]::AllDirectories)) { + $size += $file.Length + } + $stat.size = $size + } catch { + $stat.size = 0 + } + } else { + $stat.extension = $info.Extension + $stat.isreg = $true + $stat.size = $info.Length + + if ($get_checksum) { + try { + $stat.checksum = Get-FileChecksum -path $path -algorithm $checksum_algorithm + } catch { + $module.FailJson("Failed to get hash of file, set get_checksum to False to ignore this error: $($_.Exception.Message)", $_) + } + } + } + + # Get symbolic link, junction point, hard link info + if ($null -ne $link_info) { + switch ($link_info.Type) { + "SymbolicLink" { + $stat.islnk = $true + $stat.isreg = $false + $stat.lnk_target = $link_info.TargetPath + $stat.lnk_source = $link_info.AbsolutePath + break + } + "JunctionPoint" { + $stat.isjunction = $true + $stat.isreg = $false + $stat.lnk_target = $link_info.TargetPath + $stat.lnk_source = $link_info.AbsolutePath + break + } + "HardLink" { + $stat.lnk_type = "hard" + $stat.nlink = $link_info.HardTargets.Count + + # remove current path from the targets + $hlnk_targets = $link_info.HardTargets | Where-Object { $_ -ne $stat.path } + $stat.hlnk_targets = @($hlnk_targets) + break + } + } + } + + $module.Result.stat = $stat +} + +$module.ExitJson() + diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.py b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.py new file mode 100644 index 0000000..0676b5b --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_stat.py @@ -0,0 +1,236 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2017, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['stableinterface'], + 'supported_by': 'core'} + +DOCUMENTATION = r''' +--- +module: win_stat +version_added: "1.7" +short_description: Get information about Windows files +description: + - Returns information about a Windows file. + - For non-Windows targets, use the M(stat) module instead. +options: + path: + description: + - The full path of the file/object to get the facts of; both forward and + back slashes are accepted. + type: path + required: yes + aliases: [ dest, name ] + get_checksum: + description: + - Whether to return a checksum of the file (default sha1) + type: bool + default: yes + version_added: "2.1" + checksum_algorithm: + description: + - Algorithm to determine checksum of file. + - Will throw an error if the host is unable to use specified algorithm. + type: str + 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_acl +- module: win_file +- module: win_owner +author: +- Chris Church (@cchurch) +''' + +EXAMPLES = r''' +- name: Obtain information about a file + win_stat: + path: C:\foo.ini + register: file_info + +- name: Obtain information about a folder + win_stat: + path: C:\bar + register: folder_info + +- name: Get MD5 checksum of a file + win_stat: + path: C:\foo.ini + get_checksum: yes + checksum_algorithm: md5 + register: md5_checksum + +- debug: + var: md5_checksum.stat.checksum + +- name: Get SHA1 checksum of file + win_stat: + path: C:\foo.ini + get_checksum: yes + register: sha1_checksum + +- debug: + var: sha1_checksum.stat.checksum + +- name: Get SHA256 checksum of file + win_stat: + path: C:\foo.ini + get_checksum: yes + checksum_algorithm: sha256 + register: sha256_checksum + +- debug: + var: sha256_checksum.stat.checksum +''' + +RETURN = r''' +changed: + description: Whether anything was changed + returned: always + type: bool + sample: true +stat: + description: dictionary containing all the stat data + returned: success + type: complex + contains: + attributes: + description: Attributes of the file at path in raw form. + returned: success, path exists + type: str + sample: "Archive, Hidden" + checksum: + description: The checksum of a file based on checksum_algorithm specified. + returned: success, path exist, path is a file, get_checksum == True + checksum_algorithm specified is supported + type: str + sample: 09cb79e8fc7453c84a07f644e441fd81623b7f98 + creationtime: + description: The create time of the file represented in seconds since epoch. + returned: success, path exists + type: float + sample: 1477984205.15 + exists: + description: If the path exists or not. + returned: success + type: bool + sample: true + extension: + description: The extension of the file at path. + returned: success, path exists, path is a file + type: str + sample: ".ps1" + filename: + description: The name of the file (without path). + returned: success, path exists, path is a file + type: str + sample: foo.ini + hlnk_targets: + description: List of other files pointing to the same file (hard links), excludes the current file. + returned: success, path exists + type: list + sample: + - C:\temp\file.txt + - C:\Windows\update.log + isarchive: + description: If the path is ready for archiving or not. + returned: success, path exists + type: bool + sample: true + isdir: + description: If the path is a directory or not. + returned: success, path exists + type: bool + sample: true + ishidden: + description: If the path is hidden or not. + returned: success, path exists + type: bool + sample: true + isjunction: + description: If the path is a junction point or not. + returned: success, path exists + type: bool + sample: true + islnk: + description: If the path is a symbolic link or not. + returned: success, path exists + type: bool + sample: true + isreadonly: + description: If the path is read only or not. + returned: success, path exists + type: bool + sample: true + isreg: + description: If the path is a regular file. + returned: success, path exists + type: bool + sample: true + isshared: + description: If the path is shared or not. + returned: success, path exists + type: bool + sample: true + lastaccesstime: + description: The last access time of the file represented in seconds since epoch. + returned: success, path exists + type: float + sample: 1477984205.15 + lastwritetime: + description: The last modification time of the file represented in seconds since epoch. + returned: success, path exists + type: float + sample: 1477984205.15 + lnk_source: + description: Target of the symlink normalized for the remote filesystem. + returned: success, path exists and the path is a symbolic link or junction point + type: str + sample: C:\temp\link + lnk_target: + description: Target of the symlink. Note that relative paths remain relative. + returned: success, path exists and the path is a symbolic link or junction point + type: str + sample: ..\link + nlink: + description: Number of links to the file (hard links). + returned: success, path exists + type: int + sample: 1 + owner: + description: The owner of the file. + returned: success, path exists + type: str + sample: BUILTIN\Administrators + path: + description: The full absolute path to the file. + returned: success, path exists, file exists + type: str + sample: C:\foo.ini + sharename: + description: The name of share if folder is shared. + returned: success, path exists, file is a directory and isshared == True + type: str + sample: file-share + size: + description: The size in bytes of a file or folder. + returned: success, path exists, file is not a link + type: int + sample: 1024 +''' diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.ps1 b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.ps1 new file mode 100644 index 0000000..9d7c68b --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.ps1 @@ -0,0 +1,219 @@ +#!powershell + +# Copyright: (c) 2015, Corwin Brown <corwin@corwinbrown.com> +# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#AnsibleRequires -CSharpUtil Ansible.Basic +#Requires -Module Ansible.ModuleUtils.CamelConversion +#Requires -Module Ansible.ModuleUtils.FileUtil +#Requires -Module Ansible.ModuleUtils.Legacy +#AnsibleRequires -PowerShell ..module_utils.WebRequest + +$spec = @{ + options = @{ + url = @{ type = "str"; required = $true } + content_type = @{ type = "str" } + body = @{ type = "raw" } + dest = @{ type = "path" } + creates = @{ type = "path" } + removes = @{ type = "path" } + return_content = @{ type = "bool"; default = $false } + status_code = @{ type = "list"; elements = "int"; default = @(200) } + + # Defined for ease of use and backwards compatibility + url_timeout = @{ + aliases = "timeout" + } + url_method = @{ + aliases = "method" + default = "GET" + } + + # Defined for the alias backwards compatibility, remove once aliases are removed + url_username = @{ + aliases = @("user", "username") + deprecated_aliases = @( + @{ name = "user"; date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null); collection_name = 'ansible.windows' }, + @{ name = "username"; date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null); collection_name = 'ansible.windows' } + ) + } + url_password = @{ + aliases = @("password") + deprecated_aliases = @( + @{ name = "password"; date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null); collection_name = 'ansible.windows' } + ) + } + } + supports_check_mode = $true +} +$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-AnsibleWindowsWebRequestSpec)) + +$url = $module.Params.url +$method = $module.Params.url_method.ToUpper() +$content_type = $module.Params.content_type +$body = $module.Params.body +$dest = $module.Params.dest +$creates = $module.Params.creates +$removes = $module.Params.removes +$return_content = $module.Params.return_content +$status_code = $module.Params.status_code + +$JSON_CANDIDATES = @('text', 'json', 'javascript') + +$module.Result.elapsed = 0 +$module.Result.url = $url + +Function ConvertFrom-SafeJson { + <# + .SYNOPSIS + Safely convert a JSON string to an object, this is like ConvertFrom-Json except it respect -ErrorAction. + + .PAREMTER InputObject + The input object string to convert from. + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [AllowEmptyString()] + [AllowNull()] + [String] + $InputObject + ) + + if (-not $InputObject) { + return + } + + try { + # Make sure we output the actual object without unpacking with the unary comma + ,[Ansible.Basic.AnsibleModule]::FromJson($InputObject) + } catch [System.ArgumentException] { + Write-Error -Message "Invalid json string as input object: $($_.Exception.Message)" -Exception $_.Exception + } +} + +if (-not ($method -cmatch '^[A-Z]+$')) { + $module.FailJson("Parameter 'method' needs to be a single word in uppercase, like GET or POST.") +} + +if ($creates -and (Test-AnsiblePath -Path $creates)) { + $module.Result.skipped = $true + $module.Result.msg = "The 'creates' file or directory ($creates) already exists." + $module.ExitJson() +} + +if ($removes -and -not (Test-AnsiblePath -Path $removes)) { + $module.Result.skipped = $true + $module.Result.msg = "The 'removes' file or directory ($removes) does not exist." + $module.ExitJson() +} + +$client = Get-AnsibleWindowsWebRequest -Uri $url -Module $module + +if ($null -ne $content_type) { + $client.ContentType = $content_type +} + +$response_script = { + param($Response, $Stream) + + ForEach ($prop in $Response.PSObject.Properties) { + $result_key = Convert-StringToSnakeCase -string $prop.Name + $prop_value = $prop.Value + # convert and DateTime values to ISO 8601 standard + if ($prop_value -is [System.DateTime]) { + $prop_value = $prop_value.ToString("o", [System.Globalization.CultureInfo]::InvariantCulture) + } + $module.Result.$result_key = $prop_value + } + + # manually get the headers as not all of them are in the response properties + foreach ($header_key in $Response.Headers.GetEnumerator()) { + $header_value = $Response.Headers[$header_key] + $header_key = $header_key.Replace("-", "") # replace - with _ for snake conversion + $header_key = Convert-StringToSnakeCase -string $header_key + $module.Result.$header_key = $header_value + } + + # we only care about the return body if we need to return the content or create a file + if ($return_content -or $dest) { + # copy to a MemoryStream so we can read it multiple times + $memory_st = New-Object -TypeName System.IO.MemoryStream + try { + $Stream.CopyTo($memory_st) + + if ($return_content) { + $memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null + $content_bytes = $memory_st.ToArray() + $module.Result.content = [System.Text.Encoding]::UTF8.GetString($content_bytes) + if ($module.Result.ContainsKey("content_type") -and $module.Result.content_type -Match ($JSON_CANDIDATES -join '|')) { + $json = ConvertFrom-SafeJson -InputObject $module.Result.content -ErrorAction SilentlyContinue + if ($json) { + $module.Result.json = $json + } + } + } + + if ($dest) { + $memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null + $changed = $true + + if (Test-AnsiblePath -Path $dest) { + $actual_checksum = Get-FileChecksum -path $dest -algorithm "sha1" + + $sp = New-Object -TypeName System.Security.Cryptography.SHA1CryptoServiceProvider + $content_checksum = [System.BitConverter]::ToString($sp.ComputeHash($memory_st)).Replace("-", "").ToLower() + + if ($actual_checksum -eq $content_checksum) { + $changed = $false + } + } + + $module.Result.changed = $changed + if ($changed -and (-not $module.CheckMode)) { + $memory_st.Seek(0, [System.IO.SeekOrigin]::Begin) > $null + $file_stream = [System.IO.File]::Create($dest) + try { + $memory_st.CopyTo($file_stream) + } finally { + $file_stream.Flush() + $file_stream.Close() + } + } + } + } finally { + $memory_st.Close() + } + } + + if ($status_code -notcontains $Response.StatusCode) { + $module.FailJson("Status code of request '$([int]$Response.StatusCode)' is not in list of valid status codes $status_code : $($Response.StatusCode)'.") + } +} + +$body_st = $null +if ($null -ne $body) { + if ($body -is [System.Collections.IDictionary] -or $body -is [System.Collections.IList]) { + $body_string = ConvertTo-Json -InputObject $body -Compress + } elseif ($body -isnot [String]) { + $body_string = $body.ToString() + } else { + $body_string = $body + } + $buffer = [System.Text.Encoding]::UTF8.GetBytes($body_string) + + $body_st = New-Object -TypeName System.IO.MemoryStream -ArgumentList @(,$buffer) +} + +try { + Invoke-AnsibleWindowsWebRequest -Module $module -Request $client -Script $response_script -Body $body_st -IgnoreBadResponse +} catch { + $module.FailJson("Unhandled exception occurred when sending web request. Exception: $($_.Exception.Message)", $_) +} finally { + if ($null -ne $body_st) { + $body_st.Dispose() + } +} + +$module.ExitJson() diff --git a/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.py b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.py new file mode 100644 index 0000000..3b1094e --- /dev/null +++ b/test/support/windows-integration/collections/ansible_collections/ansible/windows/plugins/modules/win_uri.py @@ -0,0 +1,155 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2015, Corwin Brown <corwin@corwinbrown.com> +# Copyright: (c) 2017, Dag Wieers (@dagwieers) <dag@wieers.com> +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r''' +--- +module: win_uri +short_description: Interacts with webservices +description: +- Interacts with FTP, HTTP and HTTPS web services. +- Supports Digest, Basic and WSSE HTTP authentication mechanisms. +- For non-Windows targets, use the M(ansible.builtin.uri) module instead. +options: + url: + description: + - Supports FTP, HTTP or HTTPS URLs in the form of (ftp|http|https)://host.domain:port/path. + type: str + required: yes + content_type: + description: + - Sets the "Content-Type" header. + type: str + body: + description: + - The body of the HTTP request/response to the web service. + type: raw + dest: + description: + - Output the response body to a file. + type: path + creates: + description: + - A filename, when it already exists, this step will be skipped. + type: path + removes: + description: + - A filename, when it does not exist, this step will be skipped. + type: path + return_content: + description: + - Whether or not to return the body of the response as a "content" key in + the dictionary result. If the reported Content-type is + "application/json", then the JSON is additionally loaded into a key + called C(json) in the dictionary results. + type: bool + default: no + status_code: + description: + - A valid, numeric, HTTP status code that signifies success of the request. + - Can also be comma separated list of status codes. + type: list + elements: int + default: [ 200 ] + + url_method: + default: GET + aliases: + - method + url_timeout: + aliases: + - timeout + + # Following defined in the web_request fragment but the module contains deprecated aliases for backwards compatibility. + url_username: + description: + - The username to use for authentication. + - The alias I(user) and I(username) is deprecated and will be removed on + the major release after C(2022-07-01). + aliases: + - user + - username + url_password: + description: + - The password for I(url_username). + - The alias I(password) is deprecated and will be removed on the major + release after C(2022-07-01). + aliases: + - password +extends_documentation_fragment: +- ansible.windows.web_request + +seealso: +- module: ansible.builtin.uri +- module: ansible.windows.win_get_url +author: +- Corwin Brown (@blakfeld) +- Dag Wieers (@dagwieers) +''' + +EXAMPLES = r''' +- name: Perform a GET and Store Output + ansible.windows.win_uri: + url: http://example.com/endpoint + register: http_output + +# Set a HOST header to hit an internal webserver: +- name: Hit a Specific Host on the Server + ansible.windows.win_uri: + url: http://example.com/ + method: GET + headers: + host: www.somesite.com + +- name: Perform a HEAD on an Endpoint + ansible.windows.win_uri: + url: http://www.example.com/ + method: HEAD + +- name: POST a Body to an Endpoint + ansible.windows.win_uri: + url: http://www.somesite.com/ + method: POST + body: "{ 'some': 'json' }" +''' + +RETURN = r''' +elapsed: + description: The number of seconds that elapsed while performing the download. + returned: always + type: float + sample: 23.2 +url: + description: The Target URL. + returned: always + type: str + sample: https://www.ansible.com +status_code: + description: The HTTP Status Code of the response. + returned: success + type: int + sample: 200 +status_description: + description: A summary of the status. + returned: success + type: str + sample: OK +content: + description: The raw content of the HTTP response. + returned: success and return_content is True + type: str + sample: '{"foo": "bar"}' +content_length: + description: The byte size of the response. + returned: success + type: int + sample: 54447 +json: + description: The json structure returned under content as a dictionary. + returned: success and Content-Type is "application/json" or "application/javascript" and return_content is True + type: dict + sample: {"this-is-dependent": "on the actual return content"} +''' |