diff --git a/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.LinkUtil.psm1 b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.LinkUtil.psm1 new file mode 100644 index 00000000000..6467bec22cb --- /dev/null +++ b/lib/ansible/module_utils/powershell/Ansible.ModuleUtils.LinkUtil.psm1 @@ -0,0 +1,506 @@ + # Copyright (c) 2017 Ansible Project + # Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause) + +Function Load-LinkUtils() { + Add-Type -TypeDefinition @' +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible +{ + public enum LinkType + { + SymbolicLink, + JunctionPoint, + HardLink + } + + public class LinkUtilWin32Exception : System.ComponentModel.Win32Exception + { + private string _msg; + + public LinkUtilWin32Exception(string message) : this(Marshal.GetLastWin32Error(), message) { } + + public LinkUtilWin32Exception(int errorCode, string message) : base(errorCode) + { + _msg = String.Format("{0} ({1}, Win32ErrorCode {2})", message, base.Message, errorCode); + } + + public override string Message { get { return _msg; } } + public static explicit operator LinkUtilWin32Exception(string message) { return new LinkUtilWin32Exception(message); } + } + + public class LinkInfo + { + public LinkType Type { get; internal set; } + public string PrintName { get; internal set; } + public string SubstituteName { get; internal set; } + public string AbsolutePath { get; internal set; } + public string TargetPath { get; internal set; } + public string[] HardTargets { get; internal set; } + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public UInt32 LowPart; + public Int32 HighPart; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES + { + public UInt32 PrivilegeCount; + public LUID Luid; + public UInt32 Attributes; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct REPARSE_DATA_BUFFER + { + public UInt32 ReparseTag; + public UInt16 ReparseDataLength; + public UInt16 Reserved; + public UInt16 SubstituteNameOffset; + public UInt16 SubstituteNameLength; + public UInt16 PrintNameOffset; + public UInt16 PrintNameLength; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = LinkUtil.MAXIMUM_REPARSE_DATA_BUFFER_SIZE)] + public char[] PathBuffer; + } + + public class LinkUtil + { + public const int MAXIMUM_REPARSE_DATA_BUFFER_SIZE = 1024 * 16; + + private const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + private const int TOKEN_QUERY = 0x00000008; + private const int SE_PRIVILEGE_ENABLED = 0x00000002; + + private const UInt32 FILE_FLAG_BACKUP_SEMANTICS = 0x02000000; + private const UInt32 FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000; + + private const UInt32 FSCTL_GET_REPARSE_POINT = 0x000900A8; + private const UInt32 FSCTL_SET_REPARSE_POINT = 0x000900A4; + private const UInt32 FILE_DEVICE_FILE_SYSTEM = 0x00090000; + + private const UInt32 IO_REPARSE_TAG_MOUNT_POINT = 0xA0000003; + private const UInt32 IO_REPARSE_TAG_SYMLINK = 0xA000000C; + + private const UInt32 SYMLINK_FLAG_RELATIVE = 0x00000001; + + private const Int64 INVALID_HANDLE_VALUE = -1; + + private const UInt32 SIZE_OF_WCHAR = 2; + + private const UInt32 SYMBOLIC_LINK_FLAG_FILE = 0x00000000; + private const UInt32 SYMBOLIC_LINK_FLAG_DIRECTORY = 0x00000001; + + [DllImport("kernel32.dll")] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("kernel32.dll")] + private static extern bool CloseHandle( + IntPtr hObject); + + [DllImport("advapi32.dll")] + private static extern bool OpenProcessToken( + IntPtr ProcessHandle, + UInt32 DesiredAccess, + out IntPtr TokenHandle); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + private static extern bool LookupPrivilegeValue( + string lpSystemName, + string lpName, + [MarshalAs(UnmanagedType.Struct)] out LUID lpLuid); + + [DllImport("advapi32.dll")] + private static extern bool AdjustTokenPrivileges( + IntPtr TokenHandle, + [MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges, + ref TOKEN_PRIVILEGES NewState, + UInt32 BufferLength, + IntPtr PreviousState, + IntPtr ReturnLength); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern SafeFileHandle CreateFile( + string lpFileName, + [MarshalAs(UnmanagedType.U4)] FileAccess dwDesiredAccess, + [MarshalAs(UnmanagedType.U4)] FileShare dwShareMode, + IntPtr lpSecurityAttributes, + [MarshalAs(UnmanagedType.U4)] FileMode dwCreationDisposition, + UInt32 dwFlagsAndAttributes, + IntPtr hTemplateFile); + + // Used by GetReparsePointInfo() + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool DeviceIoControl( + SafeFileHandle hDevice, + UInt32 dwIoControlCode, + IntPtr lpInBuffer, + UInt32 nInBufferSize, + out REPARSE_DATA_BUFFER lpOutBuffer, + UInt32 nOutBufferSize, + out UInt32 lpBytesReturned, + IntPtr lpOverlapped); + + // Used by CreateJunctionPoint() + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool DeviceIoControl( + SafeFileHandle hDevice, + UInt32 dwIoControlCode, + REPARSE_DATA_BUFFER lpInBuffer, + UInt32 nInBufferSize, + IntPtr lpOutBuffer, + UInt32 nOutBufferSize, + out UInt32 lpBytesReturned, + IntPtr lpOverlapped); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool GetVolumePathName( + string lpszFileName, + StringBuilder lpszVolumePathName, + ref UInt32 cchBufferLength); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern IntPtr FindFirstFileNameW( + string lpFileName, + UInt32 dwFlags, + ref UInt32 StringLength, + StringBuilder LinkName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool FindNextFileNameW( + IntPtr hFindStream, + ref UInt32 StringLength, + StringBuilder LinkName); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool FindClose( + IntPtr hFindFile); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool RemoveDirectory( + string lpPathName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool DeleteFile( + string lpFileName); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool CreateSymbolicLink( + string lpSymlinkFileName, + string lpTargetFileName, + UInt32 dwFlags); + + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern bool CreateHardLink( + string lpFileName, + string lpExistingFileName, + IntPtr lpSecurityAttributes); + + public static void EnablePrivilege(string privilege) + { + TOKEN_PRIVILEGES tkpPrivileges; + + IntPtr hToken; + if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out hToken)) + throw new LinkUtilWin32Exception("OpenProcessToken failed"); + + try + { + LUID luid; + if (!LookupPrivilegeValue(null, privilege, out luid)) + throw new LinkUtilWin32Exception(String.Format("LookupPrivilegeValue({0}) failed", privilege)); + + tkpPrivileges.PrivilegeCount = 1; + tkpPrivileges.Luid = luid; + tkpPrivileges.Attributes = SE_PRIVILEGE_ENABLED; + + if (!AdjustTokenPrivileges(hToken, false, ref tkpPrivileges, 0, IntPtr.Zero, IntPtr.Zero)) + throw new LinkUtilWin32Exception(String.Format("AdjustTokenPrivileges({0}) failed", privilege)); + } + finally + { + CloseHandle(hToken); + } + } + + public static LinkInfo GetLinkInfo(string linkPath) + { + FileAttributes attr = File.GetAttributes(linkPath); + if (attr.HasFlag(FileAttributes.ReparsePoint)) + return GetReparsePointInfo(linkPath); + + if (!attr.HasFlag(FileAttributes.Directory)) + return GetHardLinkInfo(linkPath); + + return null; + } + + public static void DeleteLink(string linkPath) + { + bool success; + FileAttributes attr = File.GetAttributes(linkPath); + if (attr.HasFlag(FileAttributes.Directory)) + { + success = RemoveDirectory(linkPath); + } + else + { + success = DeleteFile(linkPath); + } + + if (!success) + throw new LinkUtilWin32Exception(String.Format("Failed to delete link at {0}", linkPath)); + } + + public static void CreateLink(string linkPath, String linkTarget, LinkType linkType) + { + switch (linkType) + { + case LinkType.SymbolicLink: + UInt32 linkFlags; + FileAttributes attr = File.GetAttributes(linkTarget); + if (attr.HasFlag(FileAttributes.Directory)) + linkFlags = SYMBOLIC_LINK_FLAG_DIRECTORY; + else + linkFlags = SYMBOLIC_LINK_FLAG_FILE; + + if (!CreateSymbolicLink(linkPath, linkTarget, linkFlags)) + throw new LinkUtilWin32Exception(String.Format("CreateSymbolicLink({0}, {1}, {2}) failed", linkPath, linkTarget, linkFlags)); + break; + case LinkType.JunctionPoint: + CreateJunctionPoint(linkPath, linkTarget); + break; + case LinkType.HardLink: + if (!CreateHardLink(linkPath, linkTarget, IntPtr.Zero)) + throw new LinkUtilWin32Exception(String.Format("CreateHardLink({0}, {1}) failed", linkPath, linkTarget)); + break; + } + } + + private static LinkInfo GetHardLinkInfo(string linkPath) + { + UInt32 maxPath = 260; + List result = new List(); + + StringBuilder sb = new StringBuilder((int)maxPath); + UInt32 stringLength = maxPath; + if (!GetVolumePathName(linkPath, sb, ref stringLength)) + throw new LinkUtilWin32Exception("GetVolumePathName() failed"); + string volume = sb.ToString(); + + stringLength = maxPath; + IntPtr findHandle = FindFirstFileNameW(linkPath, 0, ref stringLength, sb); + if (findHandle.ToInt64() != INVALID_HANDLE_VALUE) + { + try + { + do + { + string hardLinkPath = sb.ToString(); + if (hardLinkPath.StartsWith("\\")) + hardLinkPath = hardLinkPath.Substring(1, hardLinkPath.Length - 1); + + result.Add(Path.Combine(volume, hardLinkPath)); + stringLength = maxPath; + + } while (FindNextFileNameW(findHandle, ref stringLength, sb)); + } + finally + { + FindClose(findHandle); + } + } + + if (result.Count > 1) + return new LinkInfo + { + Type = LinkType.HardLink, + HardTargets = result.ToArray() + }; + + return null; + } + + private static LinkInfo GetReparsePointInfo(string linkPath) + { + SafeFileHandle fileHandle = CreateFile( + linkPath, + FileAccess.Read, + FileShare.None, + IntPtr.Zero, + FileMode.Open, + FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, + IntPtr.Zero); + + if (fileHandle.IsInvalid) + throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath)); + + REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER(); + UInt32 bytesReturned; + try + { + if (!DeviceIoControl( + fileHandle, + FSCTL_GET_REPARSE_POINT, + IntPtr.Zero, + 0, + out buffer, + MAXIMUM_REPARSE_DATA_BUFFER_SIZE, + out bytesReturned, + IntPtr.Zero)) + throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed for file at {0}", linkPath)); + } + finally + { + fileHandle.Dispose(); + } + + bool isRelative = false; + int pathOffset = 0; + LinkType linkType; + if (buffer.ReparseTag == IO_REPARSE_TAG_SYMLINK) + { + UInt32 bufferFlags = Convert.ToUInt32(buffer.PathBuffer[0]) + Convert.ToUInt32(buffer.PathBuffer[1]); + if (bufferFlags == SYMLINK_FLAG_RELATIVE) + isRelative = true; + pathOffset = 2; + linkType = LinkType.SymbolicLink; + } + else if (buffer.ReparseTag == IO_REPARSE_TAG_MOUNT_POINT) + { + linkType = LinkType.JunctionPoint; + } + else + { + string errorMessage = String.Format("Invalid Reparse Tag: {0}", buffer.ReparseTag.ToString()); + throw new Exception(errorMessage); + } + + string printName = new string(buffer.PathBuffer, (int)(buffer.PrintNameOffset / SIZE_OF_WCHAR) + pathOffset, (int)(buffer.PrintNameLength / SIZE_OF_WCHAR)); + string substituteName = new string(buffer.PathBuffer, (int)(buffer.SubstituteNameOffset / SIZE_OF_WCHAR) + pathOffset, (int)(buffer.SubstituteNameLength / SIZE_OF_WCHAR)); + + // TODO: should we check for \?\UNC\server for convert it to the NT style \\server path + // Remove the leading Windows object directory \?\ from the path if present + string targetPath = substituteName; + if (targetPath.StartsWith("\\??\\")) + targetPath = targetPath.Substring(4, targetPath.Length - 4); + + string absolutePath = targetPath; + if (isRelative) + absolutePath = Path.GetFullPath(Path.Combine(new FileInfo(linkPath).Directory.FullName, targetPath)); + + return new LinkInfo + { + Type = linkType, + PrintName = printName, + SubstituteName = substituteName, + AbsolutePath = absolutePath, + TargetPath = targetPath + }; + } + + private static void CreateJunctionPoint(string linkPath, string linkTarget) + { + // We need to create the link as a dir beforehand + Directory.CreateDirectory(linkPath); + SafeFileHandle fileHandle = CreateFile( + linkPath, + FileAccess.Write, + FileShare.Read | FileShare.Write | FileShare.None, + IntPtr.Zero, + FileMode.Open, + FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT, + IntPtr.Zero); + + if (fileHandle.IsInvalid) + throw new LinkUtilWin32Exception(String.Format("CreateFile({0}) failed", linkPath)); + + try + { + string substituteName = "\\??\\" + Path.GetFullPath(linkTarget); + string printName = linkTarget; + + REPARSE_DATA_BUFFER buffer = new REPARSE_DATA_BUFFER(); + buffer.SubstituteNameOffset = 0; + buffer.SubstituteNameLength = (UInt16)(substituteName.Length * SIZE_OF_WCHAR); + buffer.PrintNameOffset = (UInt16)(buffer.SubstituteNameLength + 2); + buffer.PrintNameLength = (UInt16)(printName.Length * SIZE_OF_WCHAR); + + buffer.ReparseTag = IO_REPARSE_TAG_MOUNT_POINT; + buffer.ReparseDataLength = (UInt16)(buffer.SubstituteNameLength + buffer.PrintNameLength + 12); + buffer.PathBuffer = new char[MAXIMUM_REPARSE_DATA_BUFFER_SIZE]; + + byte[] unicodeBytes = Encoding.Unicode.GetBytes(substituteName + "\0" + printName); + char[] pathBuffer = Encoding.Unicode.GetChars(unicodeBytes); + Array.Copy(pathBuffer, buffer.PathBuffer, pathBuffer.Length); + + UInt32 bytesReturned; + if (!DeviceIoControl( + fileHandle, + FSCTL_SET_REPARSE_POINT, + buffer, + (UInt32)(buffer.ReparseDataLength + 8), + IntPtr.Zero, 0, + out bytesReturned, + IntPtr.Zero)) + throw new LinkUtilWin32Exception(String.Format("DeviceIoControl() failed to create junction point at {0} to {1}", linkPath, linkTarget)); + } + finally + { + fileHandle.Dispose(); + } + } + } +} +'@ + + [Ansible.LinkUtil]::EnablePrivilege("SeBackupPrivilege") +} + +Function Get-Link($link_path) { + $link_info = [Ansible.LinkUtil]::GetLinkInfo($link_path) + return $link_info +} + +Function Remove-Link($link_path) { + [Ansible.LinkUtil]::DeleteLink($link_path) +} + +Function New-Link($link_path, $link_target, $link_type) { + if (-not (Test-Path -Path $link_target)) { + throw "link_target '$link_target' does not exist, cannot create link" + } + + switch($link_type) { + "link" { + $type = [Ansible.LinkType]::SymbolicLink + } + "junction" { + if (Test-Path -Path $link_target -PathType Leaf) { + throw "cannot set the target for a junction point to a file" + } + $type = [Ansible.LinkType]::JunctionPoint + } + "hard" { + if (Test-Path -Path $link_target -PathType Container) { + throw "cannot set the target for a hard link to a directory" + } + $type = [Ansible.LinkType]::HardLink + } + default { throw "invalid link_type option $($link_type): expecting link, junction, hard" } + } + [Ansible.LinkUtil]::CreateLink($link_path, $link_target, $type) +} + +# this line must stay at the bottom to ensure all defined module parts are exported +Export-ModuleMember -Alias * -Function * -Cmdlet * diff --git a/test/integration/targets/win_module_utils/library/symbolic_link_test.ps1 b/test/integration/targets/win_module_utils/library/symbolic_link_test.ps1 new file mode 100644 index 00000000000..c70ab1d23b0 --- /dev/null +++ b/test/integration/targets/win_module_utils/library/symbolic_link_test.ps1 @@ -0,0 +1,167 @@ +#!powershell + +#Requires -Module Ansible.ModuleUtils.Legacy +#Requires -Module Ansible.ModuleUtils.LinkUtil +#Requires -Module Ansible.ModuleUtils.CommandUtil + +$ErrorActionPreference = 'Stop' + +$params = Parse-Args $args; +$path = Get-AnsibleParam -obj $params -name "path" -type "path" -failifempty $true + +$folder_target = "$path\folder" +$file_target = "$path\file" +$symlink_file_path = "$path\file-symlink" +$symlink_folder_path = "$path\folder-symlink" +$hardlink_path = "$path\hardlink" +$hardlink_path_2 = "$path\hardlink2" +$junction_point_path = "$path\junction" + +if (Test-Path -Path $path) { + Remove-Item -Path $path -Force -Recurse | Out-Null +} +New-Item -Path $path -ItemType Directory | Out-Null +New-Item -Path $folder_target -ItemType Directory | Out-Null +New-Item -Path $file_target -ItemType File | Out-Null +Set-Content -Path $file_target -Value "a" + +Function Assert-Equals($actual, $expected) { + if ($actual -ne $expected) { + Fail-Json @{} "actual != expected`nActual: $actual`nExpected: $expected" + } +} + +Function Assert-True($expression, $message) { + if ($expression -ne $true) { + Fail-Json @{} $message + } +} + +# need to manually set this +Load-LinkUtils + +# path is not a link +$no_link_result = Get-Link -link_path $path +Assert-True -expression ($no_link_result -eq $null) -message "did not return null result for a non link" + +# fail to create hard link pointed to a directory +try { + New-Link -link_path "$path\folder-hard" -link_target $folder_target -link_type "hard" + Assert-True -expression $false -message "creation of hard link should have failed if target was a directory" +} catch { + Assert-Equals -actual $_.Exception.Message -expected "cannot set the target for a hard link to a directory" +} + +# fail to create a junction point pointed to a file +try { + New-Link -link_path "$path\junction-fail" -link_target $file_target -link_type "junction" + Assert-True -expression $false -message "creation of junction point should have failed if target was a file" +} catch { + Assert-Equals -actual $_.Exception.Message -expected "cannot set the target for a junction point to a file" +} + +# fail to create a symbolic link with non-existent target +try { + New-Link -link_path "$path\symlink-fail" -link_target "$path\fake-folder" -link_type "link" + Assert-True -expression $false -message "creation of symbolic link should have failed if target did not exist" +} catch { + Assert-Equals -actual $_.Exception.Message -expected "link_target '$path\fake-folder' does not exist, cannot create link" +} + +# create recursive symlink +Run-Command -command "cmd.exe /c mklink /D symlink-rel folder" -working_directory $path | Out-Null +$rel_link_result = Get-Link -link_path "$path\symlink-rel" +Assert-Equals -actual $rel_link_result.Type -expected "SymbolicLink" +Assert-Equals -actual $rel_link_result.SubstituteName -expected "folder" +Assert-Equals -actual $rel_link_result.PrintName -expected "folder" +Assert-Equals -actual $rel_link_result.TargetPath -expected "folder" +Assert-Equals -actual $rel_link_result.AbsolutePath -expected $folder_target +Assert-Equals -actual $rel_link_result.HardTargets -expected $null + +# create a symbolic file test +New-Link -link_path $symlink_file_path -link_target $file_target -link_type "link" +$file_link_result = Get-Link -link_path $symlink_file_path +Assert-Equals -actual $file_link_result.Type -expected "SymbolicLink" +Assert-Equals -actual $file_link_result.SubstituteName -expected "\??\$file_target" +Assert-Equals -actual $file_link_result.PrintName -expected $file_target +Assert-Equals -actual $file_link_result.TargetPath -expected $file_target +Assert-Equals -actual $file_link_result.AbsolutePath -expected $file_target +Assert-Equals -actual $file_link_result.HardTargets -expected $null + +# create a symbolic link folder test +New-Link -link_path $symlink_folder_path -link_target $folder_target -link_type "link" +$folder_link_result = Get-Link -link_path $symlink_folder_path +Assert-Equals -actual $folder_link_result.Type -expected "SymbolicLink" +Assert-Equals -actual $folder_link_result.SubstituteName -expected "\??\$folder_target" +Assert-Equals -actual $folder_link_result.PrintName -expected $folder_target +Assert-Equals -actual $folder_link_result.TargetPath -expected $folder_target +Assert-Equals -actual $folder_link_result.AbsolutePath -expected $folder_target +Assert-Equals -actual $folder_link_result.HardTargets -expected $null + +# create a junction point test +New-Link -link_path $junction_point_path -link_target $folder_target -link_type "junction" +$junction_point_result = Get-Link -link_path $junction_point_path +Assert-Equals -actual $junction_point_result.Type -expected "JunctionPoint" +Assert-Equals -actual $junction_point_result.SubstituteName -expected "\??\$folder_target" +Assert-Equals -actual $junction_point_result.PrintName -expected $folder_target +Assert-Equals -actual $junction_point_result.TargetPath -expected $folder_target +Assert-Equals -actual $junction_point_result.AbsolutePath -expected $folder_target +Assert-Equals -actual $junction_point_result.HardTargets -expected $null + +# create a hard link test +New-Link -link_path $hardlink_path -link_target $file_target -link_type "hard" +$hardlink_result = Get-Link -link_path $hardlink_path +Assert-Equals -actual $hardlink_result.Type -expected "HardLink" +Assert-Equals -actual $hardlink_result.SubstituteName -expected $null +Assert-Equals -actual $hardlink_result.PrintName -expected $null +Assert-Equals -actual $hardlink_result.TargetPath -expected $null +Assert-Equals -actual $hardlink_result.AbsolutePath -expected $null +if ($hardlink_result.HardTargets[0] -ne $hardlink_path -and $hardlink_result.HardTargets[1] -ne $hardlink_path) { + Assert-True -expression $false -message "file $hardlink_path is not a target of the hard link" +} +if ($hardlink_result.HardTargets[0] -ne $file_target -and $hardlink_result.HardTargets[1] -ne $file_target) { + Assert-True -expression $false -message "file $file_target is not a target of the hard link" +} +Assert-equals -actual (Get-Content -Path $hardlink_path -Raw) -expected (Get-Content -Path $file_target -Raw) + +# create a new hard link and verify targets go to 3 +New-Link -link_path $hardlink_path_2 -link_target $file_target -link_type "hard" +$hardlink_result_2 = Get-Link -link_path $hardlink_path +Assert-True -expression ($hardlink_result_2.HardTargets.Count -eq 3) -message "did not return 3 targets for the hard link, actual $($hardlink_result_2.Targets.Count)" + +# check if broken symbolic link still works +Remove-Item -Path $folder_target -Force | Out-Null +$broken_link_result = Get-Link -link_path $symlink_folder_path +Assert-Equals -actual $broken_link_result.Type -expected "SymbolicLink" +Assert-Equals -actual $broken_link_result.SubstituteName -expected "\??\$folder_target" +Assert-Equals -actual $broken_link_result.PrintName -expected $folder_target +Assert-Equals -actual $broken_link_result.TargetPath -expected $folder_target +Assert-Equals -actual $broken_link_result.AbsolutePath -expected $folder_target +Assert-Equals -actual $broken_link_result.HardTargets -expected $null + +# check if broken junction point still works +$broken_junction_result = Get-Link -link_path $junction_point_path +Assert-Equals -actual $broken_junction_result.Type -expected "JunctionPoint" +Assert-Equals -actual $broken_junction_result.SubstituteName -expected "\??\$folder_target" +Assert-Equals -actual $broken_junction_result.PrintName -expected $folder_target +Assert-Equals -actual $broken_junction_result.TargetPath -expected $folder_target +Assert-Equals -actual $broken_junction_result.AbsolutePath -expected $folder_target +Assert-Equals -actual $broken_junction_result.HardTargets -expected $null + +# delete file symbolic link +Remove-Link -link_path $symlink_file_path +Assert-True -expression (-not (Test-Path -Path $symlink_file_path)) -message "failed to delete file symbolic link" + +# delete folder symbolic link +Remove-Link -link_path $symlink_folder_path +Assert-True -expression (-not (Test-Path -Path $symlink_folder_path)) -message "failed to delete folder symbolic link" + +# delete junction point +Remove-Link -link_path $junction_point_path +Assert-True -expression (-not (Test-Path -Path $junction_point_path)) -message "failed to delete junction point" + +# delete hard link +Remove-Link -link_path $hardlink_path +Assert-True -expression (-not (Test-Path -Path $hardlink_path)) -message "failed to delete hard link" + +Exit-Json @{ data = "success" } diff --git a/test/integration/targets/win_module_utils/tasks/main.yml b/test/integration/targets/win_module_utils/tasks/main.yml index 05ad17dd634..945de767db2 100644 --- a/test/integration/targets/win_module_utils/tasks/main.yml +++ b/test/integration/targets/win_module_utils/tasks/main.yml @@ -76,6 +76,15 @@ that: - argv_test.data == 'success' +- name: call module with symbolic link tests + symbolic_link_test: + path: C:\ansible testing + register: symbolic_link + +- assert: + that: + - symbolic_link.data == 'success' + - name: remove testing folder win_file: path: C:\ansible testing