Server IP : 85.214.239.14 / Your IP : 3.149.29.192 Web Server : Apache/2.4.62 (Debian) System : Linux h2886529.stratoserver.net 4.9.0 #1 SMP Tue Jan 9 19:45:01 MSK 2024 x86_64 User : www-data ( 33) PHP Version : 7.4.18 Disable Function : pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,pcntl_unshare, MySQL : OFF | cURL : OFF | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /lib/python3/dist-packages/ansible_collections/ansible/windows/plugins/modules/ |
Upload File : |
#!powershell # Copyright: (c) 2014, Trond Hindenes <trond@hindenes.com>, and others # Copyright: (c) 2017, Ansible Project # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) # AccessToken should be removed once the username/password options are gone #AnsibleRequires -CSharpUtil Ansible.AccessToken #AnsibleRequires -CSharpUtil Ansible.Basic #Requires -Module Ansible.ModuleUtils.AddType #AnsibleRequires -PowerShell ..module_utils.Process #AnsibleRequires -PowerShell ..module_utils.WebRequest Function Import-PInvokeCode { param ( [Object] $Module ) Add-CSharpType -AnsibleModule $Module -References @' using Microsoft.Win32.SafeHandles; using System; using System.Collections.Generic; using System.ComponentModel; using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; using System.Runtime.InteropServices.ComTypes; using System.Security.Principal; using System.Text; //AssemblyReference -Type System.Security.Principal.IdentityReference -CLR Core namespace Ansible.WinPackage { internal class NativeHelpers { [StructLayout(LayoutKind.Sequential)] public struct PACKAGE_VERSION { public UInt16 Revision; public UInt16 Build; public UInt16 Minor; public UInt16 Major; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct PACKAGE_ID { public UInt32 reserved; public MsixArchitecture processorArchitecture; public PACKAGE_VERSION version; public string name; public string publisher; public string resourceId; public string publisherId; } } internal class NativeMethods { [DllImport("Ole32.dll", CharSet = CharSet.Unicode)] public static extern UInt32 GetClassFile( [MarshalAs(UnmanagedType.LPWStr)] string szFilename, ref Guid pclsid); [DllImport("Msi.dll")] public static extern UInt32 MsiCloseHandle( IntPtr hAny); [DllImport("Msi.dll", CharSet = CharSet.Unicode)] public static extern UInt32 MsiEnumPatchesExW( [MarshalAs(UnmanagedType.LPWStr)] string szProductCode, [MarshalAs(UnmanagedType.LPWStr)] string szUserSid, InstallContext dwContext, PatchState dwFilter, UInt32 dwIndex, StringBuilder szPatchCode, StringBuilder szTargetProductCode, out InstallContext pdwTargetProductContext, StringBuilder szTargetUserSid, ref UInt32 pcchTargetUserSid); [DllImport("Msi.dll", CharSet = CharSet.Unicode)] public static extern UInt32 MsiGetPatchInfoExW( [MarshalAs(UnmanagedType.LPWStr)] string szPatchCode, [MarshalAs(UnmanagedType.LPWStr)] string szProductCode, [MarshalAs(UnmanagedType.LPWStr)] string szUserSid, InstallContext dwContext, [MarshalAs(UnmanagedType.LPWStr)] string szProperty, StringBuilder lpValue, ref UInt32 pcchValue); [DllImport("Msi.dll", CharSet = CharSet.Unicode)] public static extern UInt32 MsiGetPropertyW( SafeMsiHandle hInstall, [MarshalAs(UnmanagedType.LPWStr)] string szName, StringBuilder szValueBuf, ref UInt32 pcchValueBuf); [DllImport("Msi.dll", CharSet = CharSet.Unicode)] public static extern UInt32 MsiGetSummaryInformationW( IntPtr hDatabase, [MarshalAs(UnmanagedType.LPWStr)] string szDatabasePath, UInt32 uiUpdateCount, out SafeMsiHandle phSummaryInfo); [DllImport("Msi.dll", CharSet = CharSet.Unicode)] public static extern UInt32 MsiOpenPackageExW( [MarshalAs(UnmanagedType.LPWStr)] string szPackagePath, UInt32 dwOptions, out SafeMsiHandle hProduct); [DllImport("Msi.dll", CharSet = CharSet.Unicode)] public static extern InstallState MsiQueryProductStateW( [MarshalAs(UnmanagedType.LPWStr)] string szProduct); [DllImport("Msi.dll", CharSet = CharSet.Unicode)] public static extern UInt32 MsiSummaryInfoGetPropertyW( SafeHandle hSummaryInfo, UInt32 uiProperty, out UInt32 puiDataType, out Int32 piValue, ref System.Runtime.InteropServices.ComTypes.FILETIME pftValue, StringBuilder szValueBuf, ref UInt32 pcchValueBuf); [DllImport("Kernel32.dll", CharSet = CharSet.Unicode)] public static extern UInt32 PackageFullNameFromId( NativeHelpers.PACKAGE_ID packageId, ref UInt32 packageFamilyNameLength, StringBuilder packageFamilyName); } [Flags] public enum InstallContext : uint { None = 0x00000000, UserManaged = 0x00000001, UserUnmanaged = 0x00000002, Machine = 0x00000004, AllUserManaged = 0x00000008, All = UserManaged | UserUnmanaged | Machine, } public enum InstallState : int { NotUsed = -7, BadConfig = -6, Incomplete = -5, SourceAbsent = -4, MoreData = -3, InvalidArg = -2, Unknown = -1, Broken = 0, Advertised = 1, Absent = 2, Local = 3, Source = 4, Default = 5, } public enum MsixArchitecture : uint { X86 = 0, Arm = 5, X64 = 9, Neutral = 11, Arm64 = 12, } [Flags] public enum PatchState : uint { Invalid = 0x00000000, Applied = 0x00000001, Superseded = 0x00000002, Obsoleted = 0x00000004, Registered = 0x00000008, All = Applied | Superseded | Obsoleted | Registered, } public class SafeMsiHandle : SafeHandleZeroOrMinusOneIsInvalid { public SafeMsiHandle() : base(true) { } [ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)] protected override bool ReleaseHandle() { UInt32 res = NativeMethods.MsiCloseHandle(handle); return res == 0; } } public class PatchInfo { public string PatchCode; public string ProductCode; public InstallContext Context; public SecurityIdentifier UserSid; } public class MsixHelper { public static string GetPackageFullName(string identity, string version, string publisher, MsixArchitecture architecture, string resourceId) { string[] versionSplit = version.Split(new char[] {'.'}, 4); NativeHelpers.PACKAGE_ID id = new NativeHelpers.PACKAGE_ID() { processorArchitecture = architecture, version = new NativeHelpers.PACKAGE_VERSION() { Revision = Convert.ToUInt16(versionSplit.Length > 3 ? versionSplit[3] : "0"), Build = Convert.ToUInt16(versionSplit.Length > 2 ? versionSplit[2] : "0"), Minor = Convert.ToUInt16(versionSplit.Length > 1 ? versionSplit[1] : "0"), Major = Convert.ToUInt16(versionSplit[0]), }, name = identity, publisher = publisher, resourceId = resourceId, }; UInt32 fullNameLength = 0; UInt32 res = NativeMethods.PackageFullNameFromId(id, ref fullNameLength, null); if (res != 122) // ERROR_INSUFFICIENT_BUFFER throw new Win32Exception((int)res); StringBuilder fullName = new StringBuilder((int)fullNameLength); res = NativeMethods.PackageFullNameFromId(id, ref fullNameLength, fullName); if (res != 0) throw new Win32Exception((int)res); return fullName.ToString(); } } public class MsiHelper { public static UInt32 SUMMARY_PID_TEMPLATE = 7; public static UInt32 SUMMARY_PID_REVNUMBER = 9; private static Guid MSI_CLSID = new Guid("000c1084-0000-0000-c000-000000000046"); private static Guid MSP_CLSID = new Guid("000c1086-0000-0000-c000-000000000046"); public static IEnumerable<PatchInfo> EnumPatches(string productCode, string userSid, InstallContext context, PatchState filter) { // PowerShell -> .NET, $null for a string parameter becomes an empty string, make sure we convert back. productCode = String.IsNullOrEmpty(productCode) ? null : productCode; userSid = String.IsNullOrEmpty(userSid) ? null : userSid; UInt32 idx = 0; while (true) { StringBuilder targetPatchCode = new StringBuilder(39); StringBuilder targetProductCode = new StringBuilder(39); InstallContext targetContext; StringBuilder targetUserSid = new StringBuilder(0); UInt32 targetUserSidLength = 0; UInt32 res = NativeMethods.MsiEnumPatchesExW(productCode, userSid, context, filter, idx, targetPatchCode, targetProductCode, out targetContext, targetUserSid, ref targetUserSidLength); SecurityIdentifier sid = null; if (res == 0x000000EA) // ERROR_MORE_DATA { targetUserSidLength++; targetUserSid.EnsureCapacity((int)targetUserSidLength); res = NativeMethods.MsiEnumPatchesExW(productCode, userSid, context, filter, idx, targetPatchCode, targetProductCode, out targetContext, targetUserSid, ref targetUserSidLength); sid = new SecurityIdentifier(targetUserSid.ToString()); } if (res == 0x00000103) // ERROR_NO_MORE_ITEMS break; else if (res != 0) throw new Win32Exception((int)res); yield return new PatchInfo() { PatchCode = targetPatchCode.ToString(), ProductCode = targetProductCode.ToString(), Context = targetContext, UserSid = sid, }; idx++; } } public static string GetPatchInfo(string patchCode, string productCode, string userSid, InstallContext context, string property) { // PowerShell -> .NET, $null for a string parameter becomes an empty string, make sure we convert back. userSid = String.IsNullOrEmpty(userSid) ? null : userSid; StringBuilder buffer = new StringBuilder(0); UInt32 bufferLength = 0; NativeMethods.MsiGetPatchInfoExW(patchCode, productCode, userSid, context, property, buffer, ref bufferLength); bufferLength++; buffer.EnsureCapacity((int)bufferLength); UInt32 res = NativeMethods.MsiGetPatchInfoExW(patchCode, productCode, userSid, context, property, buffer, ref bufferLength); if (res != 0) throw new Win32Exception((int)res); return buffer.ToString(); } public static string GetProperty(SafeMsiHandle productHandle, string property) { StringBuilder buffer = new StringBuilder(0); UInt32 bufferLength = 0; NativeMethods.MsiGetPropertyW(productHandle, property, buffer, ref bufferLength); // Make sure we include the null byte char at the end. bufferLength += 1; buffer.EnsureCapacity((int)bufferLength); UInt32 res = NativeMethods.MsiGetPropertyW(productHandle, property, buffer, ref bufferLength); if (res != 0) throw new Win32Exception((int)res); return buffer.ToString(); } public static SafeMsiHandle GetSummaryHandle(string databasePath) { SafeMsiHandle summaryInfo = null; UInt32 res = NativeMethods.MsiGetSummaryInformationW(IntPtr.Zero, databasePath, 0, out summaryInfo); if (res != 0) throw new Win32Exception((int)res); return summaryInfo; } public static string GetSummaryPropertyString(SafeMsiHandle summaryHandle, UInt32 propertyId) { UInt32 dataType = 0; Int32 intPropValue = 0; System.Runtime.InteropServices.ComTypes.FILETIME propertyFiletime = new System.Runtime.InteropServices.ComTypes.FILETIME(); StringBuilder buffer = new StringBuilder(0); UInt32 bufferLength = 0; NativeMethods.MsiSummaryInfoGetPropertyW(summaryHandle, propertyId, out dataType, out intPropValue, ref propertyFiletime, buffer, ref bufferLength); // Make sure we include the null byte char at the end. bufferLength += 1; buffer.EnsureCapacity((int)bufferLength); UInt32 res = NativeMethods.MsiSummaryInfoGetPropertyW(summaryHandle, propertyId, out dataType, out intPropValue, ref propertyFiletime, buffer, ref bufferLength); if (res != 0) throw new Win32Exception((int)res); return buffer.ToString(); } public static bool IsMsi(string filename) { return GetClsid(filename) == MSI_CLSID; } public static bool IsMsp(string filename) { return GetClsid(filename) == MSP_CLSID; } public static SafeMsiHandle OpenPackage(string packagePath, bool ignoreMachineState) { SafeMsiHandle packageHandle = null; UInt32 options = 0; if (ignoreMachineState) options |= 1; // MSIOPENPACKAGEFLAGS_IGNOREMACHINESTATE UInt32 res = NativeMethods.MsiOpenPackageExW(packagePath, options, out packageHandle); if (res != 0) throw new Win32Exception((int)res); return packageHandle; } public static InstallState QueryProductState(string productCode) { return NativeMethods.MsiQueryProductStateW(productCode); } private static Guid GetClsid(string filename) { Guid clsid = Guid.Empty; NativeMethods.GetClassFile(filename, ref clsid); return clsid; } } } '@ } Function Add-SystemReadAce { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Failing to get or set the ACE is not critical, SYSTEM could still have access without it.')] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [String] $Path ) # Don't set the System ACE if the path is a UNC path as the SID won't be valid. if (([Uri]$Path).IsUnc) { return } # If $Path is on a read only file system or one that doesn't support ACLs then this will fail. SYSTEM might still # have access to the path so don't treat it as critical. # https://github.com/ansible-collections/ansible.windows/issues/142 try { $acl = Get-Acl -LiteralPath $Path } catch { return } $ace = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList @( (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList ('S-1-5-18')), [System.Security.AccessControl.FileSystemRights]::Read, [System.Security.AccessControl.AccessControlType]::Allow ) $acl.AddAccessRule($ace) try { $acl | Set-Acl -LiteralPath $path } catch {} } Function Copy-ItemWithCredential { [CmdletBinding(SupportsShouldProcess = $false)] param ( [String] $Path, [String] $Destination, [PSCredential] $Credential ) $filename = Split-Path -Path $Path -Leaf $targetPath = Join-Path -Path $Destination -ChildPath $filename # New-PSDrive with -Credentials seems to have lots of issues, just impersonate a NewCredentials token and copy the # file locally. NewCredentials will ensure the outbound auth to the UNC path is with the new credentials specified. $domain = [NullString]::Value $username = $Credential.UserName if ($username.Contains('\')) { $userSplit = $username.Split('\', 2) $domain = $userSplit[0] $username = $userSplit[1] } $impersonated = $false $token = [Ansible.AccessToken.TokenUtil]::LogonUser( $username, $domain, $Credential.GetNetworkCredential().Password, [Ansible.AccessToken.LogonType]::NewCredentials, [Ansible.AccessToken.LogonProvider]::WinNT50 ) try { [Ansible.AccessToken.TokenUtil]::ImpersonateToken($token) $impersonated = $true Copy-Item -LiteralPath $Path -Destination $targetPath } finally { if ($impersonated) { [Ansible.AccessToken.TokenUtil]::RevertToSelf() } $token.Dispose() } $targetPath } Function Get-UrlFile { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Object] $Module, [Parameter(Mandatory = $true)] [String] $Url ) $request = (Get-AnsibleWindowsWebRequest -Url $Url -Module $module) Invoke-AnsibleWindowsWebRequest -Module $module -Request $request -Script { Param ([System.Net.WebResponse]$Response, [System.IO.Stream]$Stream) $tempPath = Join-Path -Path $module.Tmpdir -ChildPath $Response.ResponseUri.Segments[-1] $fs = [System.IO.File]::Create($tempPath) try { $Stream.CopyTo($fs) $fs.Flush() } finally { $fs.Dispose() } $tempPath } } Function Format-PackageStatus { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [AllowEmptyString()] [String] $Id, [Parameter(Mandatory = $true)] [String] $Provider, [Switch] $Installed, [Switch] $Skip, [Switch] $SkipFileForRemove, [Hashtable] $ExtraInfo = @{} ) @{ Id = $Id Installed = $Installed.IsPresent Provider = $Provider Skip = $Skip.IsPresent SkipFileForRemove = $SkipFileForRemove.IsPresent ExtraInfo = $ExtraInfo } } Function Get-InstalledStatus { [CmdletBinding()] param ( [String] $Path, [String] $Id, [String] $Provider, [String] $CreatesPath, [String] $CreatesService, [String] $CreatesVersion ) if ($Path) { if ($Provider -eq 'auto') { foreach ($info in $providerInfo.GetEnumerator()) { if ((&$info.Value.FileSupported -Path $Path)) { $Provider = $info.Key break } } } $status = &$providerInfo."$Provider".Test -Path $Path -Id $Id } else { if ($Provider -eq 'auto') { # While we only technically support 2012+ this is a fairly small thing to do to ensure this # continues to run on Server 2008 and 2008 R2. This should be removed sometime in the future. # https://github.com/ansible-collections/ansible.windows/issues/362 $msixAvailable = [bool](Get-Command -Name Get-AppxPackage -ErrorAction SilentlyContinue) $providerList = [String[]]$providerInfo.Keys | Where-Object { $_ -ne 'msix' -or $msixAvailable } } else { $providerList = @($Provider) } foreach ($name in $providerList) { $status = &$providerInfo."$name".Test -Id $Id # If the package was installed for the provider (or was the last provider available). if ($status.Installed -or $providerList[-1] -eq $name) { break } } } if ($CreatesPath) { $exists = Test-Path -LiteralPath $CreatesPath $status.Installed = $exists if ($CreatesVersion -and $exists) { if (Test-Path -LiteralPath $CreatesPath -PathType Leaf) { $versionRaw = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($CreatesPath) $existingVersion = New-Object -TypeName System.Version -ArgumentList @( $versionRaw.FileMajorPart, $versionRaw.FileMinorPart, $versionRaw.FileBuildPart, $versionRaw.FilePrivatePart ) $status.Installed = $CreatesVersion -eq $existingVersion } else { throw "creates_path must be a file not a directory when creates_version is set" } } } if ($CreatesService) { $serviceInfo = Get-Service -Name $CreatesService -ErrorAction SilentlyContinue $status.Installed = $null -ne $serviceInfo } Format-PackageStatus @status } Function Invoke-Executable { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Object] $Module, [Parameter(Mandatory = $true)] [String] $CommandLine, [Int32[]] $ReturnCodes, [String] $LogPath, [String] $WorkingDirectory, [String] $ConsoleOutputEncoding, [Switch] $WaitChildren ) $commandArgs = @{ CommandLine = $CommandLine WaitChildren = $WaitForChildren } if ($WorkingDirectory) { $commandArgs.WorkingDirectory = $WorkingDirectory } if ($ConsoleOutputEncoding) { $commandArgs.OutputEncodingOverride = $ConsoleOutputEncoding } $result = Start-AnsibleWindowsProcess @commandArgs # Start-AnsibleWindowsProcess returns rc as a UInt32 but we need to compare it with a Int32, we get the byte # equivalent Int32 value instead. https://github.com/ansible-collections/ansible.windows/issues/46 $rc = [BitConverter]::ToInt32([BitConverter]::GetBytes($result.ExitCode), 0) $module.Result.rc = $rc if ($ReturnCodes -notcontains $rc) { $module.Result.stdout = $result.Stdout $module.Result.stderr = $result.Stderr if ($LogPath -and (Test-Path -LiteralPath $LogPath)) { $module.Result.log = (Get-Content -LiteralPath $LogPath | Out-String) } $module.FailJson("unexpected rc from '$($result.Command)': see rc, stdout, and stderr for more details") } else { $module.Result.failed = $false } if ($rc -eq 3010) { $module.Result.reboot_required = $true } } Function Invoke-Msiexec { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [Object] $Module, [Parameter(Mandatory = $true)] [String[]] $Actions, [String] $Arguments, [Int32[]] $ReturnCodes, [String] $LogPath, [String] $WorkingDirectory, [Switch] $WaitChildren ) $tempFile = $null try { if (-not $LogPath) { $tempFile = Join-Path -Path $module.Tmpdir -ChildPath "msiexec.log" $LogPath = $tempFile } $cmd = [System.Collections.Generic.List[String]]@("$env:SystemRoot\System32\msiexec.exe") $cmd.AddRange([System.Collections.Generic.List[String]]$Actions) $cmd.AddRange([System.Collections.Generic.List[String]]@( '/L*V', $LogPath, '/qn', '/norestart' )) $commandLine = @($cmd | ConvertTo-EscapedArgument) -join ' ' if ($Arguments) { $commandLine += " $Arguments" } $invokeParams = @{ Module = $Module CommandLine = $commandLine ReturnCodes = $ReturnCodes LogPath = $LogPath WorkingDirectory = $WorkingDirectory WaitChildren = $WaitChildren # Msiexec is not a console application but in the case of a fatal error it does still send messages back # over the stdout pipe. These messages are UTF-16 encoded so we override the default UTF-8. ConsoleOutputEncoding = 'Unicode' } Invoke-Executable @invokeParams } finally { if ($tempFile -and (Test-Path -LiteralPath $tempFile)) { Remove-Item -LiteralPath $tempFile -Force } } } $providerInfo = [Ordered]@{ msi = @{ FileSupported = { param ([String]$Path) [Ansible.WInPackage.MsiHelper]::IsMsi($Path) } Test = { param ([String]$Path, [String]$Id) if ($Path) { # MSIs have 2 types of ids that are important here # ProductCode: Unique id for the app, could change across major versions, minor stays the same # PackageCode: Unique id for the msi itself, no msi should have a matching package code # # Because we cannot install multiple msi's with the same product code we use this to determine if its # installed or not. When we open a handle to the package we also need to ignore the current machine # state, without that MsiOpenPackage will fail with ERROR_PRODUCT_VERSION if the ProductCode of the # msi is already installed but under a different PackageCode. When ignoring it we can still get the # ProductCode and check the status ourselves. # https://github.com/ansible-collections/ansible.windows/issues/166 $msiHandle = [Ansible.WinPackage.MsiHelper]::OpenPackage($Path, $true) try { $Id = [Ansible.WinPackage.MsiHelper]::GetProperty($msiHandle, 'ProductCode') } finally { $msiHandle.Dispose() } } $installState = [Ansible.WinPackage.MsiHelper]::QueryProductState($Id) @{ Provider = 'msi' Id = $Id Installed = $installState -eq [Ansible.WinPackage.InstallState]::Default SkipFileForRemove = $true } } Set = { param ( [String] $Arguments, [Int32[]] $ReturnCodes, [String] $Id, [String] $LogPath, [Object] $Module, [String] $Path, [String] $State, [String] $WorkingDirectory, [Switch] $WaitChildren ) if ($state -eq 'present') { $actions = @('/i', $Path) # $Module.Tmpdir only gives rights to the current user but msiexec (as SYSTEM) needs access. Add-SystemReadAce -Path $Path } else { $actions = @('/x', $Id) } $invokeParams = @{ Module = $Module Actions = $actions Arguments = $Arguments ReturnCodes = $ReturnCodes LogPath = $LogPath WorkingDirectory = $WorkingDirectory WaitChildren = $WaitChildren } Invoke-Msiexec @invokeParams } } msix = @{ FileSupported = { param ([String]$Path) $extension = [System.IO.Path]::GetExtension($Path) $extension -in @('.appx', '.appxbundle', '.msix', '.msixbundle') } Test = { param ([String]$Path, [String]$Id) $package = $null if ($Path) { # Cannot find a native way to get the package info from the actual path so we need to inspect the XML # manually. $null = Add-Type -AssemblyName System.IO.Compression $null = Add-Type -AssemblyName System.IO.Compression.FileSystem $archive = [System.IO.Compression.ZipFile]::Open($Path, [System.IO.Compression.ZipArchiveMode]::Read, [System.Text.Encoding]::UTF8) try { $manifestEntry = $archive.Entries | Where-Object { $_.FullName -in @('AppxManifest.xml', 'AppxMetadata/AppxBundleManifest.xml') } $manifestStream = New-Object -TypeName System.IO.StreamReader -ArgumentList $manifestEntry.Open() try { $manifest = [xml]$manifestStream.ReadToEnd() } finally { $manifestStream.Dispose() } } finally { $archive.Dispose() } if ($manifestEntry.Name -eq 'AppxBundleManifest.xml') { # https://docs.microsoft.com/en-us/uwp/schemas/bundlemanifestschema/element-identity $name = $manifest.Bundle.Identity.Name $publisher = $manifest.Bundle.Identity.Publisher $Ids = foreach ($p in $manifest.Bundle.Packages.Package) { $version = $p.Version $architecture = 'neutral' if ($p.HasAttribute('Architecture')) { $architecture = $p.Architecture } $resourceId = '' if ($p.HasAttribute('ResourceId')) { $resourceId = $p.ResourceId } [Ansible.WinPackage.MsixHelper]::GetPackageFullName($name, $version, $publisher, $architecture, $resourceId) } } else { # https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/element-identity $name = $manifest.Package.Identity.Name $version = $manifest.Package.Identity.Version $publisher = $manifest.Package.Identity.Publisher $architecture = 'neutral' if ($manifest.Package.Identity.HasAttribute('ProcessorArchitecture')) { $architecture = $manifest.Package.Identity.ProcessorArchitecture } $resourceId = '' if ($manifest.Package.Identity.HasAttribute('ResourceId')) { $resourceId = $manifest.$identityParent.Identity.ResourceId } $Ids = @(, [Ansible.WinPackage.MsixHelper]::GetPackageFullName($name, $version, $publisher, $architecture, $resourceId) ) } } else { $package = Get-AppxPackage -Name $Id -ErrorAction SilentlyContinue $Ids = @($Id) } # In the case when a file is specified or the user has set the full name and not the name, scan again for # PackageFullName. if ($null -eq $package) { $package = Get-AppxPackage | Where-Object { $_.PackageFullName -in $Ids } } # Make sure the Id is set to the PackageFullName so state=absent works. if ($package) { $Id = $package.PackageFullName } @{ Provider = 'msix' Id = $Id Installed = $null -ne $package } } Set = { param ( [String] $Id, [Object] $Module, [String] $Path, [String] $State ) $originalProgress = $ProgressPreference try { $ProgressPreference = 'SilentlyContinue' if ($State -eq 'present') { # Add-AppxPackage does not support a -LiteralPath parameter and it chokes on wildcard characters. # We need to escape those characters when calling the cmdlet. Add-AppxPackage -Path ([WildcardPattern]::Escape($Path)) } else { Remove-AppxPackage -Package $Id } } catch { # Replicate the same return values as the other providers. $module.Result.rc = $_.Exception.HResult $module.Result.stdout = "" $module.Result.stderr = $_.Exception.Message $msg = "unexpected status from $($_.InvocationInfo.InvocationName): see rc and stderr for more details" $module.FailJson($msg, $_) } finally { $ProgressPreference = $originalProgress } # Just set to 0 to align with other providers $module.Result.rc = 0 # It looks like the reboot checks are an insider feature so we can't do a check for that today. # https://docs.microsoft.com/en-us/windows/msix/packaging-tool/support-restart } } msp = @{ FileSupported = { param ([String]$Path) [Ansible.WInPackage.MsiHelper]::IsMsp($Path) } Test = { param ([String]$Path, [String]$Id) $productCodes = [System.Collections.Generic.List[System.String]]@() if ($Path) { $summaryInfo = [Ansible.WinPackage.MsiHelper]::GetSummaryHandle($Path) try { $productCodesRaw = [Ansible.WinPackage.MsiHelper]::GetSummaryPropertyString( $summaryInfo, [Ansible.WinPackage.MsiHelper]::SUMMARY_PID_TEMPLATE ) # Filter out product codes that are not installed on the host. foreach ($code in ($productCodesRaw -split ';')) { $productState = [Ansible.WinPackage.MsiHelper]::QueryProductState($code) if ($productState -eq [Ansible.WinPackage.InstallState]::Default) { $productCodes.Add($code) } } if ($productCodes.Count -eq 0) { throw "The specified patch does not apply to any installed MSI packages." } # The first guid in the REVNUMBER is the patch code, the subsequent values are obsoleted patches # which we don't care about. $Id = [Ansible.WinPackage.MsiHelper]::GetSummaryPropertyString($summaryInfo, [Ansible.WinPackage.MsiHelper]::SUMMARY_PID_REVNUMBER).Substring(0, 38) } finally { $summaryInfo.Dispose() } } else { foreach ($patch in ([Ansible.WinPackage.MsiHelper]::EnumPatches($null, $null, 'All', 'All'))) { if ($patch.PatchCode -eq $Id) { # We append "{guid}:{context}" so the check below checks the proper context, the context # is then stripped out there. $ProductCodes.Add("$($patch.ProductCode):$($patch.Context)") } } } # Filter the product list even further to only ones that are applied and not obsolete. $skipCodes = [System.Collections.Generic.List[System.String]]@() $productCodes = @(@(foreach ($product in $productCodes) { if ($product.Length -eq 38) { # Guid length with braces is 38 $contextList = @('UserManaged', 'UserUnmanaged', 'Machine') } else { # We already know the context and was appended to the product guid with ';context' $productInfo = $product.Split(':', 2) $product = $productInfo[0] $contextList = @($productInfo[1]) } foreach ($context in $contextList) { try { # GetPatchInfo('State') returns a string that is a number of an enum value. $state = [Ansible.WinPackage.PatchState][UInt32]([Ansible.WinPackage.MsiHelper]::GetPatchInfo( $Id, $product, $null, $context, 'State' )) } catch [System.ComponentModel.Win32Exception] { if ($_.Exception.NativeErrorCode -in @(0x00000645, 0x0000066F)) { # ERROR_UNKNOWN_PRODUCT can be raised if the product is not installed in the context # specified, just try the next one. # ERROR_UNKNOWN_PATCH can be raised if the patch is not installed but the product is. continue } throw } if ($state -eq [Ansible.WinPackage.PatchState]::Applied) { # The patch is applied to the product code, output the code for the outer list to capture. $product } elseif ($state.ToString() -in @('Obsoleted', 'Superseded')) { # If the patch is obsoleted or suprseded we cannot install or remove but consider it equal to # state=absent and present so we skip the set step. $skipCodes.Add($product) } } }) | Select-Object -Unique) @{ Provider = 'msp' Id = $Id Installed = $productCodes.Length -gt 0 Skip = $skipCodes.Length -eq $productCodes.Length SkipFileForRemove = $true ExtraInfo = @{ ProductCodes = $productCodes } } } Set = { param ( [String] $Arguments, [Int32[]] $ReturnCodes, [String] $Id, [String] $LogPath, [Object] $Module, [String] $Path, [String] $State, [String] $WorkingDirectory, [Switch] $WaitChildren, [String[]] $ProductCodes ) $tempLink = $null try { $actions = @(if ($state -eq 'present') { # $Module.Tmpdir only gives rights to the current user but msiexec (as SYSTEM) needs access. Add-SystemReadAce -Path $Path # MsiApplyPatchW fails if the path contains a ';', we need to use a temporary symlink instead. # https://docs.microsoft.com/en-us/windows/win32/api/msi/nf-msi-msiapplypatchw if ($Path.Contains(';')) { $tempLink = Join-Path -Path $env:TEMP -ChildPath "win_package-$([System.IO.Path]::GetRandomFileName()).msp" $res = Start-AnsibleWindowsProcess -FilePath cmd.exe -ArgumentList @('/c', 'mklink', $tempLink, $Path) if ($res.ExitCode -ne 0) { $Module.Result.rc = $res.ExitCode $Module.Result.stdout = $res.Stdout $Module.Result.stderr = $res.Stderr $msg = "Failed to create temporary symlink '$tempLink' -> '$Path' for msiexec patch install as path contains semicolon" $Module.FailJson($msg) } $Path = $tempLink } , @('/update', $Path) } else { foreach ($code in $ProductCodes) { , @('/uninstall', $Id, '/package', $code) } }) $invokeParams = @{ Arguments = $Arguments Module = $Module ReturnCodes = $ReturnCodes LogPath = $LogPath WorkingDirectory = $WorkingDirectory WaitChildren = $WaitChildren } foreach ($action in $actions) { Invoke-Msiexec -Actions $action @invokeParams } } finally { if ($tempLink -and (Test-Path -LiteralPath $tempLink)) { Remove-Item -LiteralPath $tempLink -Force } } } } # Should always be last as the FileSupported is a catch all. registry = @{ FileSupported = { $true } Test = { param ([String]$Id) $status = @{ Provider = 'registry' Id = $Id Installed = $false ExtraInfo = @{ RegistryPath = $null } } if ($Id) { :regLoop foreach ($hive in @("HKLM", "HKCU")) { # Search machine wide and user specific. foreach ($key in @("SOFTWARE", "SOFTWARE\Wow6432Node")) { # Search the 32 and 64-bit locations. $regPath = "$($hive):\$key\Microsoft\Windows\CurrentVersion\Uninstall\$Id" if (Test-Path -LiteralPath $regPath) { $status.Installed = $true $status.ExtraInfo.RegistryPath = $regPath break regLoop } } } } $status } Set = { param ( [String] $Arguments, [Int32[]] $ReturnCodes, [Object] $Module, [String] $Path, [String] $State, [String] $WorkingDirectory, [String] $RegistryPath, [Switch] $WaitChildren ) $invokeParams = @{ Module = $Module ReturnCodes = $ReturnCodes WorkingDirectory = $WorkingDirectory WaitChildren = $WaitChildren } if ($Path) { $invokeParams.CommandLine = ConvertTo-EscapedArgument -InputObject $Path } else { $registryProperties = Get-ItemProperty -LiteralPath $RegistryPath if ('QuietUninstallString' -in $registryProperties.PSObject.Properties.Name) { $command = $registryProperties.QuietUninstallString } elseif ('UninstallString' -in $registryProperties.PSObject.Properties.Name) { $command = $registryProperties.UninstallString } else { $module.FailJson("Failed to find registry uninstall string at registry path '$RegistryPath'") } # If the uninstall string starts with '%', we need to expand the env vars. if ($command.StartsWith('%') -or $command.StartsWith('"%')) { $command = [System.Environment]::ExpandEnvironmentVariables($command) } # If the command is not quoted and contains spaces we need to see if it needs to be manually quoted for the executable. if (-not $command.StartsWith('"') -and $command.Contains(' ')) { $rawArguments = [System.Collections.Generic.List[String]]@() $executable = New-Object -TypeName System.Text.StringBuilder foreach ($cmd in ($command | ConvertFrom-EscapedArgument)) { if ($rawArguments.Count -eq 0) { # Still haven't found the path, append the arg to the executable path and see if it exists. $null = $executable.Append($cmd) $exe = $executable.ToString() if (Test-Path -LiteralPath $exe -PathType Leaf) { $rawArguments.Add($exe) } else { $null = $executable.Append(" ") # The arg had a space and we need to preserve that. } } else { $rawArguments.Add($cmd) } } # If we still couldn't find a file just use the command literally and hope WIndows can handle it, # otherwise recombine the args which will also quote whatever is needed. if ($rawArguments.Count -gt 0) { $command = @($rawArguments | ConvertTo-EscapedArgument) -join ' ' } } $invokeParams.CommandLine = $command } if ($Arguments) { $invokeParams.CommandLine += " $Arguments" } Invoke-Executable @invokeParams } } } $spec = @{ options = @{ arguments = @{ type = "raw" } expected_return_code = @{ type = "list"; elements = "int"; default = @(0, 3010) } path = @{ type = "str" } chdir = @{ type = "path" } product_id = @{ type = "str" aliases = @("productid") deprecated_aliases = @( @{ name = "productid"; date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null); collection_name = 'ansible.windows' } ) } state = @{ type = "str" default = "present" choices = "absent", "present" aliases = @(, "ensure") deprecated_aliases = @( @{ name = "ensure"; date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null); collection_name = 'ansible.windows' } ) } username = @{ type = "str" aliases = @(, "user_name") removed_at_date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null) removed_from_collection = 'ansible.windows' } password = @{ type = "str" no_log = $true aliases = @(, "user_password") removed_at_date = [DateTime]::ParseExact("2022-07-01", "yyyy-MM-dd", $null) removed_from_collection = 'ansible.windows' } creates_path = @{ type = "path" } creates_version = @{ type = "str" } creates_service = @{ type = "str" } log_path = @{ type = "path" } provider = @{ type = "str"; default = "auto"; choices = $providerInfo.Keys + "auto" } wait_for_children = @{ type = 'bool'; default = $false } } required_by = @{ creates_version = "creates_path" } required_if = @( @("state", "present", @("path")), @("state", "absent", @("path", "product_id"), $true) ) required_together = @(, @("username", "password")) supports_check_mode = $true } $module = [Ansible.Basic.AnsibleModule]::Create($args, $spec, @(Get-AnsibleWindowsWebRequestSpec)) $arguments = $module.Params.arguments $expectedReturnCode = $module.Params.expected_return_code $path = $module.Params.path $chdir = $module.Params.chdir $productId = $module.Params.product_id $state = $module.Params.state $username = $module.Params.username $password = $module.Params.password $createsPath = $module.Params.creates_path $createsVersion = $module.Params.creates_version $createsService = $module.Params.creates_service $logPath = $module.Params.log_path $provider = $module.Params.provider $waitForChildren = $module.Params.wait_for_children $module.Result.reboot_required = $false if ($null -ne $arguments) { # convert a list to a string and escape the values if ($arguments -is [array]) { $arguments = @($arguments | ConvertTo-EscapedArgument) -join ' ' } } $credential = $null if ($null -ne $username) { $secPassword = ConvertTo-SecureString -String $password -AsPlainText -Force $credential = New-Object -TypeName PSCredential -ArgumentList $username, $secPassword } # This must be set after the module spec so the validate-modules sanity-test can get the arg spec. Import-PInvokeCode -Module $module $pathType = $null if ($path -and $path.StartsWith('http', [System.StringComparison]::InvariantCultureIgnoreCase)) { $pathType = 'url' } elseif ($path -and ($path.StartsWith('\\') -or $path.StartsWith('//') -and $username)) { $pathType = 'unc' } $tempFile = $null try { $getParams = @{ Id = $productId Provider = $provider CreatesPath = $createsPath CreatesVersion = $createsVersion CreatesService = $createsService } # If the packge is a remote file, productId is set and state is set to present # then check if the package is installed and avoid downloading the package to a temp file. if ($pathType -and $productId -and ($state -eq 'present')) { $packageStatus = Get-InstalledStatus @getParams } # If the path is a URL or UNC with credentials and no productId is set or we already checked and the package is not installed # then create a temp copy for idempotency checks. if (($pathType) -and (-not $productId -or -not $packageStatus.Installed)) { $tempFile = switch ($pathType) { url { Get-UrlFile -Module $module -Url $path } unc { Copy-ItemWithCredential -Path $path -Destination $module.Tmpdir -Credential $credential } } $path = $tempFile $getParams.Path = $path } elseif ($path -and -not $pathType) { if (-not (Test-Path -LiteralPath $path)) { $module.FailJson("the file at the path '$path' cannot be reached") } $getParams.Path = $path } # Check package installation status unless this was already done and we know the package is installed if (-not $packageStatus.Installed) { $packageStatus = Get-InstalledStatus @getParams } $changed = -not $packageStatus.Skip -and (($state -eq 'present') -ne $packageStatus.Installed) $module.Result.rc = 0 # Make sure rc is always set if ($changed -and -not $module.CheckMode) { # Make sure we get a temp copy of the file if the provider requires it and we haven't already done so. if ($pathType -and -not $tempFile -and ($state -eq 'present' -or -not $packageStatus.SkipFileForRemove)) { $tempFile = switch ($pathType) { url { Get-UrlFile -Module $module -Url $path } unc { Copy-ItemWithCredential -Path $path -Destination $module.Tmpdir -Credential $credential } } $path = $tempFile } $setParams = @{ Arguments = $arguments ReturnCodes = $expectedReturnCode Id = $packageStatus.Id LogPath = $logPath Module = $module Path = $path State = $state WorkingDirectory = $chdir WaitChildren = $waitForChildren } $setParams += $packageStatus.ExtraInfo &$providerInfo."$($packageStatus.Provider)".Set @setParams } $module.Result.changed = $changed } finally { if ($tempFile -and (Test-Path -LiteralPath $tempFile)) { Remove-Item -LiteralPath $tempFile -Force } } $module.ExitJson()