diff --git a/lib/ansible/modules/windows/win_package.ps1 b/lib/ansible/modules/windows/win_package.ps1 index 817e0b60a2..e0545d6cb4 100644 --- a/lib/ansible/modules/windows/win_package.ps1 +++ b/lib/ansible/modules/windows/win_package.ps1 @@ -1,1329 +1,541 @@ #!powershell -# (c) 2014, Trond Hindenes , and others -# # This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . + +# (c) 2014, Trond Hindenes , 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) # WANT_JSON # POWERSHELL_COMMON -#region DSC - -data LocalizedData -{ - # culture="en-US" - # TODO: Support WhatIf - ConvertFrom-StringData @' -InvalidIdentifyingNumber=The specified IdentifyingNumber ({0}) is not a valid Guid -InvalidPath=The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, and HTTP -InvalidNameOrId=The specified Name ({0}) and IdentifyingNumber ({1}) do not match Name ({2}) and IdentifyingNumber ({3}) in the MSI file -NeedsMoreInfo=Either Name or ProductId is required -InvalidBinaryType=The specified Path ({0}) does not appear to specify an EXE or MSI file and as such is not supported -CouldNotOpenLog=The specified LogPath ({0}) could not be opened -CouldNotStartProcess=The process {0} could not be started -UnexpectedReturnCode=The return code {0} was not expected. Configuration is likely not correct -PathDoesNotExist=The given Path ({0}) could not be found -CouldNotOpenDestFile=Could not open the file {0} for writing -CouldNotGetHttpStream=Could not get the {0} stream for file {1} -ErrorCopyingDataToFile=Encountered error while writing the contents of {0} to {1} -PackageConfigurationComplete=Package configuration finished -PackageConfigurationStarting=Package configuration starting -InstalledPackage=Installed package -UninstalledPackage=Uninstalled package -NoChangeRequired=Package found in desired state, no action required -RemoveExistingLogFile=Remove existing log file -CreateLogFile=Create log file -MountSharePath=Mount share to get media -DownloadHTTPFile=Download the media over HTTP or HTTPS -StartingProcessMessage=Starting process {0} with arguments {1} -RemoveDownloadedFile=Remove the downloaded file -PackageInstalled=Package has been installed -PackageUninstalled=Package has been uninstalled -MachineRequiresReboot=The machine requires a reboot -PackageDoesNotAppearInstalled=The package {0} is not installed -PackageAppearsInstalled=The package {0} is already installed -PostValidationError=Package from {0} was installed, but the specified ProductId and/or Name does not match package details -'@ -} - -$Debug = $true -Function Trace-Message -{ - param([string] $Message) - if($Debug) - { - Write-Verbose $Message - } -} - -$CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_PackageResource" - -Function Throw-InvalidArgumentException -{ - param( - [string] $Message, - [string] $ParamName - ) - - $exception = new-object System.ArgumentException $Message,$ParamName - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null - throw $errorRecord -} - -Function Throw-InvalidNameOrIdException -{ - param( - [string] $Message - ) - - $exception = new-object System.ArgumentException $Message - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"NameOrIdNotInMSI","InvalidArgument",$null - throw $errorRecord -} - -Function Throw-TerminatingError -{ - param( - [string] $Message, - [System.Management.Automation.ErrorRecord] $ErrorRecord - ) - - if ($errorRecord) - { - $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception - } - Else - { - $exception = new-object "System.InvalidOperationException" $Message - } - - $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null - throw $errorRecord -} - -Function Get-RegistryValueIgnoreError -{ - param - ( - [parameter(Mandatory = $true)] - [Microsoft.Win32.RegistryHive] - $RegistryHive, - - [parameter(Mandatory = $true)] - [System.String] - $Key, - - [parameter(Mandatory = $true)] - [System.String] - $Value, - - [parameter(Mandatory = $true)] - [Microsoft.Win32.RegistryView] - $RegistryView - ) - - try - { - $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) - $subKey = $baseKey.OpenSubKey($Key) - if($subKey -ne $null) - { - return $subKey.GetValue($Value) - } - } - catch - { - $exceptionText = ($_ | Out-String).Trim() - Write-Verbose "Exception occurred in Get-RegistryValueIgnoreError: $exceptionText" - } - return $null -} - -Function Validate-StandardArguments -{ - param( - $Path, - $ProductId, - $Name - ) - - Trace-Message "Validate-StandardArguments, Path was $Path" - $uri = $null - try - { - $uri = [uri] $Path - } - catch - { - Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" - } - - if(-not @("file", "http", "https") -contains $uri.Scheme) - { - Trace-Message "The uri scheme was $uri.Scheme" - Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" - } - - $pathExt = [System.IO.Path]::GetExtension($Path) - Trace-Message "The path extension was $pathExt" - if(-not @(".msi",".exe") -contains $pathExt.ToLower()) - { - Throw-InvalidArgumentException ($LocalizedData.InvalidBinaryType -f $Path) "Path" - } - - $identifyingNumber = $null - if(-not $Name -and -not $ProductId) - { - #It's a tossup here which argument to blame, so just pick ProductId to encourage customers to use the most efficient version - Throw-InvalidArgumentException ($LocalizedData.NeedsMoreInfo -f $Path) "ProductId" - } - elseif($ProductId) - { - try - { - Trace-Message "Parsing $ProductId as an identifyingNumber" - $TestGuid = [system.guid]::NewGuid() - #Check to see if the productid is a guid - if ([guid]::TryParse($ProductId, [ref]$TestGuid)) - { - $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() - Trace-Message "Parsed $ProductId as $identifyingNumber (is guid)" - } - Else - { - $identifyingNumber = $ProductId - Trace-Message "Parsed $ProductId as $identifyingNumber (is not guid)" - } - - Trace-Message "Parsed $ProductId as $identifyingNumber" - } - catch - { - Throw-InvalidArgumentException ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) $ProductId - } - } - - return $uri, $identifyingNumber -} - -Function Get-ProductEntry -{ - param - ( - [string] $Name, - [string] $IdentifyingNumber, - [string] $InstalledCheckRegKey, - [string] $InstalledCheckRegValueName, - [string] $InstalledCheckRegValueData - ) - - $uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" - $uninstallKeyWow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" - - if($IdentifyingNumber) - { - $keyLocation = "$uninstallKey\$identifyingNumber" - $item = Get-Item $keyLocation -EA SilentlyContinue - if(-not $item) - { - $keyLocation = "$uninstallKeyWow64\$identifyingNumber" - $item = Get-Item $keyLocation -EA SilentlyContinue - } - - return $item - } - - foreach($item in (Get-ChildItem -EA Ignore $uninstallKey, $uninstallKeyWow64)) - { - if($Name -eq (Get-LocalizableRegKeyValue $item "DisplayName")) - { - return $item - } - } - - if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) - { - $installValue = $null - - #if 64bit OS, check 64bit registry view first - if ((Get-WmiObject -Class Win32_OperatingSystem -ComputerName "localhost" -ea 0).OSArchitecture -eq '64-bit') - { - $installValue = Get-RegistryValueIgnoreError LocalMachine "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry64 - } - - if($installValue -eq $null) - { - $installValue = Get-RegistryValueIgnoreError LocalMachine "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry32 - } - - if($installValue) - { - if($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) - { - return @{ - Installed = $true - } - } - } - } - - return $null -} - -function Test-TargetResource -{ - param - ( - [ValidateSet("Present", "Absent")] - [string] $Ensure = "Present", - - [parameter(Mandatory = $true)] - [AllowEmptyString()] - [string] $Name, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] $Path, - - [parameter(Mandatory = $true)] - [AllowEmptyString()] - [string] $ProductId, - - [string] $Arguments, - - [pscredential] $Credential, - - [int[]] $ReturnCode, - - [string] $LogPath, - - [pscredential] $RunAsCredential, - - [string] $InstalledCheckRegKey, - - [string] $InstalledCheckRegValueName, - - [string] $InstalledCheckRegValueData - ) - - $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name - $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData - Trace-Message "Ensure is $Ensure" - if($product) - { - Trace-Message "product found" - } - else - { - Trace-Message "product installation cannot be determined" - } - Trace-Message ("product as boolean is {0}" -f [boolean]$product) - $res = ($product -ne $null -and $Ensure -eq "Present") -or ($product -eq $null -and $Ensure -eq "Absent") - - # install registry test overrides the product id test and there is no true product information - # when doing a lookup via registry key - if ($product -and $InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) - { - Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $Name) - } - else - { - if ($product -ne $null) - { - $name = Get-LocalizableRegKeyValue $product "DisplayName" - Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $name) - } - else - { - $displayName = $null - if($Name) - { - $displayName = $Name - } - else - { - $displayName = $ProductId - } - - Write-Verbose ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName) - } - - } - - return $res -} - -function Get-LocalizableRegKeyValue -{ - param( - [object] $RegKey, - [string] $ValueName - ) - - $res = $RegKey.GetValue("{0}_Localized" -f $ValueName) - if(-not $res) - { - $res = $RegKey.GetValue($ValueName) - } - - return $res -} - -function Get-TargetResource -{ - param - ( - [parameter(Mandatory = $true)] - [AllowEmptyString()] - [string] $Name, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] $Path, - - [parameter(Mandatory = $true)] - [AllowEmptyString()] - [string] $ProductId, - - [string] $InstalledCheckRegKey, - - [string] $InstalledCheckRegValueName, - - [string] $InstalledCheckRegValueData - ) - - #If the user gave the ProductId then we derive $identifyingNumber - $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name - - $localMsi = $uri.IsFile -and -not $uri.IsUnc - - $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData - - if(-not $product) - { - return @{ - Ensure = "Absent" - Name = $Name - ProductId = $identifyingNumber - Installed = $false - InstalledCheckRegKey = $InstalledCheckRegKey - InstalledCheckRegValueName = $InstalledCheckRegValueName - InstalledCheckRegValueData = $InstalledCheckRegValueData - } - } - - if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) - { - return @{ - Ensure = "Present" - Name = $Name - ProductId = $identifyingNumber - Installed = $true - InstalledCheckRegKey = $InstalledCheckRegKey - InstalledCheckRegValueName = $InstalledCheckRegValueName - InstalledCheckRegValueData = $InstalledCheckRegValueData - } - } - - #$identifyingNumber can still be null here (e.g. remote MSI with Name specified, local EXE) - #If the user gave a ProductId just pass it through, otherwise fill it from the product - if(-not $identifyingNumber) - { - $identifyingNumber = Split-Path -Leaf $product.Name - } - - $date = $product.GetValue("InstallDate") - if($date) - { - try - { - $date = "{0:d}" -f [DateTime]::ParseExact($date, "yyyyMMdd",[System.Globalization.CultureInfo]::CurrentCulture).Date - } - catch - { - $date = $null - } - } - - $publisher = Get-LocalizableRegKeyValue $product "Publisher" - $size = $product.GetValue("EstimatedSize") - if($size) - { - $size = $size/1024 - } - - $version = $product.GetValue("DisplayVersion") - $description = $product.GetValue("Comments") - $name = Get-LocalizableRegKeyValue $product "DisplayName" - return @{ - Ensure = "Present" - Name = $name - Path = $Path - InstalledOn = $date - ProductId = $identifyingNumber - Size = $size - Installed = $true - Version = $version - PackageDescription = $description - Publisher = $publisher - } -} - -Function Get-MsiTools -{ - if($script:MsiTools) - { - return $script:MsiTools - } - - $sig = @' - [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] - private static extern UInt32 MsiOpenPackageW(string szPackagePath, out IntPtr hProduct); - - [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] - private static extern uint MsiCloseHandle(IntPtr hAny); - - [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] - private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength); - - private static string GetPackageProperty(string msi, string property) - { - IntPtr MsiHandle = IntPtr.Zero; - try - { - var res = MsiOpenPackageW(msi, out MsiHandle); - if (res != 0) - { - return null; - } - - int length = 256; - var buffer = new StringBuilder(length); - res = MsiGetPropertyW(MsiHandle, property, buffer, ref length); - return buffer.ToString(); - } - finally - { - if (MsiHandle != IntPtr.Zero) - { - MsiCloseHandle(MsiHandle); - } - } - } - public static string GetProductCode(string msi) - { - return GetPackageProperty(msi, "ProductCode"); - } - - public static string GetProductName(string msi) - { - return GetPackageProperty(msi, "ProductName"); - } -'@ - $script:MsiTools = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.PackageResource ` - -Name MsiTools -Using System.Text -MemberDefinition $sig - return $script:MsiTools -} - - -Function Get-MsiProductEntry -{ - param - ( - [string] $Path - ) - - if(-not (Test-Path -PathType Leaf $Path) -and ($fileExtension -ne ".msi")) - { - Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) - } - - $tools = Get-MsiTools - - $pn = $tools::GetProductName($Path) - - $pc = $tools::GetProductCode($Path) - - return $pn,$pc -} - - -function Set-TargetResource -{ - [CmdletBinding(SupportsShouldProcess=$true)] - param - ( - [ValidateSet("Present", "Absent")] - [string] $Ensure = "Present", - - [parameter(Mandatory = $true)] - [AllowEmptyString()] - [string] $Name, - - [parameter(Mandatory = $true)] - [ValidateNotNullOrEmpty()] - [string] $Path, - - [parameter(Mandatory = $true)] - [AllowEmptyString()] - [string] $ProductId, - - [string] $Arguments, - - [pscredential] $Credential, - - [int[]] $ReturnCode, - - [string] $LogPath, - - [pscredential] $RunAsCredential, - - [string] $InstalledCheckRegKey, - - [string] $InstalledCheckRegValueName, - - [string] $InstalledCheckRegValueData - ) - - $ErrorActionPreference = "Stop" - - if((Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId ` - -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName ` - -InstalledCheckRegValueData $InstalledCheckRegValueData)) - { - return - } - - $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name - - #Path gets overwritten in the download code path. Retain the user's original Path in case the install succeeded - #but the named package wasn't present on the system afterward so we can give a better message - $OrigPath = $Path - - Write-Verbose $LocalizedData.PackageConfigurationStarting - if(-not $ReturnCode) - { - $ReturnCode = @(0) - } - - $logStream = $null - $psdrive = $null - $downloadedFileName = $null - try - { - $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower() - if($LogPath) - { - try - { - if($fileExtension -eq ".msi") - { - #We want to pre-verify the path exists and is writable ahead of time - #even in the MSI case, as detecting WHY the MSI log doesn't exist would - #be rather problematic for the user - if((Test-Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile,$null,$null)) - { - rm $LogPath - } - - if($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) - { - New-Item -Type File $LogPath | Out-Null - } - } - elseif($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) - { - $logStream = new-object "System.IO.StreamWriter" $LogPath,$false - } - } - catch - { - Throw-TerminatingError ($LocalizedData.CouldNotOpenLog -f $LogPath) $_ - } - } - - #Download or mount file as necessary - if(-not ($fileExtension -eq ".msi" -and $Ensure -eq "Absent")) - { - if($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null)) - { - $psdriveArgs = @{Name=([guid]::NewGuid());PSProvider="FileSystem";Root=(Split-Path $uri.LocalPath)} - if($Credential) - { - #We need to optionally include these and then splat the hash otherwise - #we pass a null for Credential which causes the cmdlet to pop a dialog up - $psdriveArgs["Credential"] = $Credential - } - - $psdrive = New-PSDrive @psdriveArgs - $Path = Join-Path $psdrive.Root (Split-Path -Leaf $uri.LocalPath) #Necessary? - } - elseif(@("http", "https") -contains $uri.Scheme -and $Ensure -eq "Present" -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null)) - { - $scheme = $uri.Scheme - $outStream = $null - $responseStream = $null - - try - { - Trace-Message "Creating cache location" - - if(-not (Test-Path -PathType Container $CacheLocation)) - { - mkdir $CacheLocation | Out-Null - } - - $destName = Join-Path $CacheLocation (Split-Path -Leaf $uri.LocalPath) - - Trace-Message "Need to download file from $scheme, destination will be $destName" - - try - { - Trace-Message "Creating the destination cache file" - $outStream = New-Object System.IO.FileStream $destName, "Create" - } - catch - { - #Should never happen since we own the cache directory - Throw-TerminatingError ($LocalizedData.CouldNotOpenDestFile -f $destName) $_ - } - - try - { - Trace-Message "Creating the $scheme stream" - $request = [System.Net.WebRequest]::Create($uri) - Trace-Message "Setting default credential" - $request.Credentials = [System.Net.CredentialCache]::DefaultCredentials - if ($scheme -eq "http") - { - Trace-Message "Setting authentication level" - # default value is MutualAuthRequested, which applies to https scheme - $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None - } - if ($scheme -eq "https") - { - Trace-Message "Ignoring bad certificates" - $request.ServerCertificateValidationCallBack = {$true} - } - Trace-Message "Getting the $scheme response stream" - $responseStream = (([System.Net.HttpWebRequest]$request).GetResponse()).GetResponseStream() - } - catch - { - Trace-Message ("Error: " + ($_ | Out-String)) - Throw-TerminatingError ($LocalizedData.CouldNotGetHttpStream -f $scheme, $Path) $_ - } - - try - { - Trace-Message "Copying the $scheme stream bytes to the disk cache" - $responseStream.CopyTo($outStream) - $responseStream.Flush() - $outStream.Flush() - } - catch - { - Throw-TerminatingError ($LocalizedData.ErrorCopyingDataToFile -f $Path,$destName) $_ - } - } - finally - { - if($outStream) - { - $outStream.Close() - } - - if($responseStream) - { - $responseStream.Close() - } - } - Trace-Message "Redirecting package path to cache file location" - $Path = $downloadedFileName = $destName - } - } - - #At this point the Path ought to be valid unless it's an MSI uninstall case - if(-not (Test-Path -PathType Leaf $Path) -and -not ($Ensure -eq "Absent" -and $fileExtension -eq ".msi")) - { - Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) - } - - $startInfo = New-Object System.Diagnostics.ProcessStartInfo - $startInfo.UseShellExecute = $false #Necessary for I/O redirection and just generally a good idea - $process = New-Object System.Diagnostics.Process - $process.StartInfo = $startInfo - $errLogPath = $LogPath + ".err" #Concept only, will never touch disk - if($fileExtension -eq ".msi") - { - $startInfo.FileName = "$env:windir\system32\msiexec.exe" - if($Ensure -eq "Present") - { - # check if Msi package contains the ProductName and Code specified - <# - $pName,$pCode = Get-MsiProductEntry -Path $Path - - if ( - ( (-not [String]::IsNullOrEmpty($Name)) -and ($pName -ne $Name)) ` - -or ( (-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $pCode)) - ) - { - Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode) - } - #> - - $startInfo.Arguments = '/i "{0}"' -f $Path - } - else - { - $product = Get-ProductEntry $Name $identifyingNumber - $id = Split-Path -Leaf $product.Name #We may have used the Name earlier, now we need the actual ID - $startInfo.Arguments = ("/x{0}" -f $id) - } - - if($LogPath) - { - $startInfo.Arguments += ' /log "{0}"' -f $LogPath - } - - $startInfo.Arguments += " /quiet" - - if($Arguments) - { - $startInfo.Arguments += " " + $Arguments - } - } - else #EXE - { - Trace-Message "The binary is an EXE" - $startInfo.FileName = $Path - $startInfo.Arguments = $Arguments - if($LogPath) - { - Trace-Message "User has requested logging, need to attach event handlers to the process" - $startInfo.RedirectStandardError = $true - $startInfo.RedirectStandardOutput = $true - Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -SourceIdentifier $LogPath - Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -SourceIdentifier $errLogPath - } - } - - Trace-Message ("Starting {0} with {1}" -f $startInfo.FileName, $startInfo.Arguments) - - if($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) - { - try - { - $exitCode = 0 - - if($PSBoundParameters.ContainsKey("RunAsCredential")) - { - CallPInvoke - [Source.NativeMethods]::CreateProcessAsUser("""" + $startInfo.FileName + """ " + $startInfo.Arguments, ` - $RunAsCredential.GetNetworkCredential().Domain, $RunAsCredential.GetNetworkCredential().UserName, ` - $RunAsCredential.GetNetworkCredential().Password, [ref] $exitCode) - } - else - { - $process.Start() | Out-Null - - if($logStream) #Identical to $fileExtension -eq ".exe" -and $logPath - { - $process.BeginOutputReadLine(); - $process.BeginErrorReadLine(); - } - - $process.WaitForExit() - - if($process) - { - $exitCode = $process.ExitCode - $result.exit_code = $exitCode - } - } - } - catch - { - Throw-TerminatingError ($LocalizedData.CouldNotStartProcess -f $Path) $_ - } - - - if($logStream) - { - #We have to re-mux these since they appear to us as different streams - #The underlying Win32 APIs prevent this problem, as would constructing a script - #on the fly and executing it, but the former is highly problematic from PowerShell - #and the latter doesn't let us get the return code for UI-based EXEs - $outputEvents = Get-Event -SourceIdentifier $LogPath - $errorEvents = Get-Event -SourceIdentifier $errLogPath - $masterEvents = @() + $outputEvents + $errorEvents - $masterEvents = $masterEvents | Sort-Object -Property TimeGenerated - - foreach($event in $masterEvents) - { - $logStream.Write($event.SourceEventArgs.Data); - } - - Remove-Event -SourceIdentifier $LogPath - Remove-Event -SourceIdentifier $errLogPath - } - - if(-not ($ReturnCode -contains $exitCode)) - { - Throw-TerminatingError ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) - } - } - } - finally - { - if($psdrive) - { - Remove-PSDrive -Force $psdrive - } - - if($logStream) - { - $logStream.Dispose() - } - } - - if($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null)) - { - #This is deliberately not in the Finally block. We want to leave the downloaded file on disk - #in the error case as a debugging aid for the user - rm $downloadedFileName - } - - $operationString = $LocalizedData.PackageUninstalled - if($Ensure -eq "Present") - { - $operationString = $LocalizedData.PackageInstalled - } - - # Check if reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is missing on client SKUs - $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks - $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore - if(($featureData -and $featureData.RequiresReboot) -or $regData) - { - Write-Verbose $LocalizedData.MachineRequiresReboot - $global:DSCMachineStatus = 1 - } - - if($Ensure -eq "Present") - { - $productEntry = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData - if(-not $productEntry) - { - Throw-TerminatingError ($LocalizedData.PostValidationError -f $OrigPath) - } - } - - Write-Verbose $operationString - Write-Verbose $LocalizedData.PackageConfigurationComplete -} - -function CallPInvoke -{ -$script:ProgramSource = @" -using System; -using System.Collections.Generic; -using System.Text; -using System.Security; -using System.Runtime.InteropServices; -using System.Diagnostics; -using System.Security.Principal; -using System.ComponentModel; -using System.IO; - -namespace Source -{ - [SuppressUnmanagedCodeSecurity] - public static class NativeMethods - { - //The following structs and enums are used by the various Win32 API's that are used in the code below - - [StructLayout(LayoutKind.Sequential)] - public struct STARTUPINFO - { - public Int32 cb; - public string lpReserved; - public string lpDesktop; - public string lpTitle; - public Int32 dwX; - public Int32 dwY; - public Int32 dwXSize; - public Int32 dwXCountChars; - public Int32 dwYCountChars; - public Int32 dwFillAttribute; - public Int32 dwFlags; - public Int16 wShowWindow; - public Int16 cbReserved2; - public IntPtr lpReserved2; - public IntPtr hStdInput; - public IntPtr hStdOutput; - public IntPtr hStdError; - } - - [StructLayout(LayoutKind.Sequential)] - public struct PROCESS_INFORMATION - { - public IntPtr hProcess; - public IntPtr hThread; - public Int32 dwProcessID; - public Int32 dwThreadID; - } - - [Flags] - public enum LogonType - { - LOGON32_LOGON_INTERACTIVE = 2, - LOGON32_LOGON_NETWORK = 3, - LOGON32_LOGON_BATCH = 4, - LOGON32_LOGON_SERVICE = 5, - LOGON32_LOGON_UNLOCK = 7, - LOGON32_LOGON_NETWORK_CLEARTEXT = 8, - LOGON32_LOGON_NEW_CREDENTIALS = 9 - } - - [Flags] - public enum LogonProvider - { - LOGON32_PROVIDER_DEFAULT = 0, - LOGON32_PROVIDER_WINNT35, - LOGON32_PROVIDER_WINNT40, - LOGON32_PROVIDER_WINNT50 - } - [StructLayout(LayoutKind.Sequential)] - public struct SECURITY_ATTRIBUTES - { - public Int32 Length; - public IntPtr lpSecurityDescriptor; - public bool bInheritHandle; - } - - public enum SECURITY_IMPERSONATION_LEVEL - { - SecurityAnonymous, - SecurityIdentification, - SecurityImpersonation, - SecurityDelegation - } - - public enum TOKEN_TYPE - { - TokenPrimary = 1, - TokenImpersonation - } - - [StructLayout(LayoutKind.Sequential, Pack = 1)] - internal struct TokPriv1Luid - { - public int Count; - public long Luid; - public int Attr; - } - - public const int GENERIC_ALL_ACCESS = 0x10000000; - public const int CREATE_NO_WINDOW = 0x08000000; - internal const int SE_PRIVILEGE_ENABLED = 0x00000002; - internal const int TOKEN_QUERY = 0x00000008; - internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; - internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; - - [DllImport("kernel32.dll", - EntryPoint = "CloseHandle", SetLastError = true, - CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] - public static extern bool CloseHandle(IntPtr handle); - - [DllImport("advapi32.dll", - EntryPoint = "CreateProcessAsUser", SetLastError = true, - CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] - public static extern bool CreateProcessAsUser( - IntPtr hToken, - string lpApplicationName, - string lpCommandLine, - ref SECURITY_ATTRIBUTES lpProcessAttributes, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - bool bInheritHandle, - Int32 dwCreationFlags, - IntPtr lpEnvrionment, - string lpCurrentDirectory, - ref STARTUPINFO lpStartupInfo, - ref PROCESS_INFORMATION lpProcessInformation - ); - - [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] - public static extern bool DuplicateTokenEx( - IntPtr hExistingToken, - Int32 dwDesiredAccess, - ref SECURITY_ATTRIBUTES lpThreadAttributes, - Int32 ImpersonationLevel, - Int32 dwTokenType, - ref IntPtr phNewToken - ); - - [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] - public static extern Boolean LogonUser( - String lpszUserName, - String lpszDomain, - String lpszPassword, - LogonType dwLogonType, - LogonProvider dwLogonProvider, - out IntPtr phToken - ); - - [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern bool AdjustTokenPrivileges( - IntPtr htok, - bool disall, - ref TokPriv1Luid newst, - int len, - IntPtr prev, - IntPtr relen - ); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern IntPtr GetCurrentProcess(); - - [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] - internal static extern bool OpenProcessToken( - IntPtr h, - int acc, - ref IntPtr phtok - ); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern int WaitForSingleObject( - IntPtr h, - int milliseconds - ); - - [DllImport("kernel32.dll", ExactSpelling = true)] - internal static extern bool GetExitCodeProcess( - IntPtr h, - out int exitcode - ); - - [DllImport("advapi32.dll", SetLastError = true)] - internal static extern bool LookupPrivilegeValue( - string host, - string name, - ref long pluid - ); - - public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword, ref int ExitCode ) - { - var hToken = IntPtr.Zero; - var hDupedToken = IntPtr.Zero; - TokPriv1Luid tp; - var pi = new PROCESS_INFORMATION(); - var sa = new SECURITY_ATTRIBUTES(); - sa.Length = Marshal.SizeOf(sa); - Boolean bResult = false; - try - { - bResult = LogonUser( - strName, - strDomain, - strPassword, - LogonType.LOGON32_LOGON_BATCH, - LogonProvider.LOGON32_PROVIDER_DEFAULT, - out hToken - ); - if (!bResult) - { - throw new Win32Exception("Logon error #" + Marshal.GetLastWin32Error().ToString()); - } - IntPtr hproc = GetCurrentProcess(); - IntPtr htok = IntPtr.Zero; - bResult = OpenProcessToken( - hproc, - TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, - ref htok - ); - if(!bResult) - { - throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString()); - } - tp.Count = 1; - tp.Luid = 0; - tp.Attr = SE_PRIVILEGE_ENABLED; - bResult = LookupPrivilegeValue( - null, - SE_INCRASE_QUOTA, - ref tp.Luid - ); - if(!bResult) - { - throw new Win32Exception("Lookup privilege error #" + Marshal.GetLastWin32Error().ToString()); - } - bResult = AdjustTokenPrivileges( - htok, - false, - ref tp, - 0, - IntPtr.Zero, - IntPtr.Zero - ); - if(!bResult) - { - throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString()); - } - - bResult = DuplicateTokenEx( - hToken, - GENERIC_ALL_ACCESS, - ref sa, - (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, - (int)TOKEN_TYPE.TokenPrimary, - ref hDupedToken - ); - if(!bResult) - { - throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString()); - } - var si = new STARTUPINFO(); - si.cb = Marshal.SizeOf(si); - si.lpDesktop = ""; - bResult = CreateProcessAsUser( - hDupedToken, - null, - strCommand, - ref sa, - ref sa, - false, - 0, - IntPtr.Zero, - null, - ref si, - ref pi - ); - if(!bResult) - { - throw new Win32Exception("Create process as user error #" + Marshal.GetLastWin32Error().ToString()); - } - - int status = WaitForSingleObject(pi.hProcess, -1); - if(status == -1) - { - throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString()); - } - - bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); - if(!bResult) - { - throw new Win32Exception("Retrieving status error #" + Marshal.GetLastWin32Error().ToString()); - } - } - finally - { - if (pi.hThread != IntPtr.Zero) - { - CloseHandle(pi.hThread); - } - if (pi.hProcess != IntPtr.Zero) - { - CloseHandle(pi.hProcess); - } - if (hDupedToken != IntPtr.Zero) - { - CloseHandle(hDupedToken); - } - } - } - } -} - -"@ - Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" -} - -#endregion - - -$params = Parse-Args $args; +$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 + +$arguments = Get-AnsibleParam -obj $params -name "arguments" -type "str" +$expected_return_code = Get-AnsibleParam -obj $params -name "expected_return_code" -type "list" -default @(0, 3010) +$name = Get-AnsibleParam -obj $params -name "name" -type "str" +$path = Get-AnsibleParam -obj $params -name "path" -type "str" +$product_id = Get-AnsibleParam -obj $params -name "product_id" -type "str" -aliases "productid" +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent","present" -aliases "ensure" +$username = Get-AnsibleParam -obj $params -name "username" -type "str" -aliases "user_name" +$password = Get-AnsibleParam -obj $params -name "password" -type "str" -failifempty ($username -ne $null) -aliases "user_password" +$validate_certs = Get-AnsibleParam -obj $params -name "validate_certs" -type "bool" -default $true +$creates_path = Get-AnsibleParam -obj $params -name "creates_path" -type "path" +$creates_version = Get-AnsibleParam -obj $params -name "creates_version" -type "str" +$creates_service = Get-AnsibleParam -obj $params -name "creates_service" -type "str" $result = @{ changed = $false + reboot_required = $false + restart_required = $false # deprecate in 2.6 } -$path = Get-Attr -obj $params -name path -failifempty $true -resultobj $result -$name = Get-Attr -obj $params -name name -default $path -$productid = Get-Attr -obj $params -name productid -if ($productid -eq $null) -{ - #Alias added for backwards compat. - $productid = Get-Attr -obj $params -name product_id -failifempty $true -resultobj $result -} -$arguments = Get-Attr -obj $params -name arguments -$ensure = Get-Attr -obj $params -name state -default "present" -if ($ensure -eq $null) -{ - $ensure = Get-Attr -obj $params -name ensure -default "present" -} -$username = Get-Attr -obj $params -name user_name -$password = Get-Attr -obj $params -name user_password -$return_code = Get-Attr -obj $params -name expected_return_code -default 0 - -#Construct the DSC param hashtable -$dscparams = @{ - name=$name - path=$path - productid = $productid - arguments = $arguments - ensure = $ensure - returncode = $return_code +if (-not $validate_certs) { + [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } } -if (($username -ne $null) -and ($password -ne $null)) -{ - #Add network credential to the list - $secpassword = $password | ConvertTo-SecureString -AsPlainText -Force - $credential = New-Object pscredential -ArgumentList $username, $secpassword - $dscparams.add("Credential",$credential) +$credential = $null +if ($username -ne $null) { + $sec_user_password = ConvertTo-SecureString -String $password -AsPlainText -Force + $credential = New-Object -TypeName PSCredential -ArgumentList $username, $sec_user_password } -#Always return the name -$result.name = $name - -$testdscresult = Test-TargetResource @dscparams -if ($testdscresult -eq $true) -{ - Exit-Json -obj $result +if ($name -ne $null) { + Add-DeprecationWarning -obj $result -message "the use of name has been deprecated, please remove from the task options" -version 2.6 } -Else -{ - try - { - set-TargetResource @dscparams + +# validate initial arguments, more is done after analysing the exec path +if ($expected_return_code -eq "") { + Add-DeprecationWarning -obj $result -message "an empty string for expected_return_code will be deprecated in the future, omit the value or set it explicitly if you wish to override it" -version 2.6 + $expected_return_code = @(0, 3010) +} +$valid_return_codes = @() +foreach ($rc in ($expected_return_code)) { + try { + $int_rc = [Int32]::Parse($rc) + $valid_return_codes += $int_rc + } catch { + Fail-Json -obj $result -message "failed to parse expected return code $rc as an integer" } - catch - { - $errormsg = $_ - Fail-Json -obj $result -message $errormsg.ToString() +} + +if ($path -eq $null) { + if (-not ($state -eq "absent" -and $product_id -ne $null)) { + Fail-Json -obj $result -message "path can only be null when state=absent and product_id is not null" + } +} + +if ($creates_version -ne $null -and $creates_path -eq $null) { + Fail-Json -obj $result -Message "creates_path must be set when creates_version is set" +} + +# run module +# used when installing a local process +$process_util = @" +using System; +using System.IO; +using System.Threading; + +namespace Ansible { + public static class ProcessUtil { + + public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr) { + var sowait = new EventWaitHandle(false, EventResetMode.ManualReset); + var sewait = new EventWaitHandle(false, EventResetMode.ManualReset); + + string so = null, se = null; + + ThreadPool.QueueUserWorkItem((s) => { + so = stdoutStream.ReadToEnd(); + sowait.Set(); + }); + + ThreadPool.QueueUserWorkItem((s) => { + se = stderrStream.ReadToEnd(); + sewait.Set(); + }); + + foreach (var wh in new WaitHandle[] { sowait, sewait }) + wh.WaitOne(); + + stdout = so; + stderr = se; + } + } +} +"@ + +$msi_tools = @" +using System; +using System.Runtime.InteropServices; +using System.Text; + +namespace Ansible { + public static class MsiTools { + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern UInt32 MsiOpenPackageW(string szPackagePath, out IntPtr hProduct); + + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiCloseHandle(IntPtr hAny); + + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength); + + public static string GetPackageProperty(string msi, string property) { + IntPtr MsiHandle = IntPtr.Zero; + try { + uint res = MsiOpenPackageW(msi, out MsiHandle); + if (res != 0) + return null; + + int length = 256; + var buffer = new StringBuilder(length); + res = MsiGetPropertyW(MsiHandle, property, buffer, ref length); + return buffer.ToString(); + } finally { + if (MsiHandle != IntPtr.Zero) + MsiCloseHandle(MsiHandle); + } + } + } +} +"@ + +Add-Type -TypeDefinition @" +public enum LocationType { + Empty, + Local, + Unc, + Http +} +"@ + +Function Download-File($url, $path) { + $web_client = New-Object -TypeName System.Net.WebClient + try { + $web_client.DownloadFile($url, $path) + } catch { + Fail-Json -obj $result -message "failed to download $url to $($path): $($_.Exception.Message)" + } +} + +Function Run-Process($executable, $arguments) { + Add-Type -TypeDefinition $process_util + $proc = New-Object -TypeName System.Diagnostics.Process + $psi = $proc.StartInfo + $psi.FileName = $executable + $psi.Arguments = $arguments + $psi.RedirectStandardOutput = $true + $psi.RedirectStandardError = $true + $psi.UseShellExecute = $false + + try { + $proc.Start() | Out-Null + } catch [System.ComponentModel.Win32Exception] { + Fail-Json $result "failed to start executable $($executable): $($_.Exception.Message)" } - #Check if DSC thinks the computer needs a reboot: - if ((get-variable DSCMachinestatus -Scope Global -ea 0) -and ($global:DSCMachineStatus -eq 1)) - { - $result.restart_required = $true + $stdout = [string]$null + $stderr = [string]$null + [Ansible.ProcessUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null + $proc.WaitForExit() | Out-Null + + $process_result = @{ + stdout = $stdout + stderr = $stderr + rc = $proc.ExitCode } - #Set-TargetResource did its job. We can assume a change has happened - $result.changed = $true - Exit-Json -obj $result - + return $process_result } +Function Test-RegistryProperty($path, $name) { + # will validate if the registry key contains the property, returns true + # if the property exists and false if the property does not + try { + $value = (Get-Item -Path $path).GetValue($name) + # need to do it this way return ($value -eq $null) does not work + if ($value -eq $null) { + return $false + } else { + return $true + } + } catch [System.Management.Automation.ItemNotFoundException] { + # key didn't exist so the property mustn't + return $false + } +} + +Function Get-ProgramMetadata($state, $path, $product_id, $credential, $creates_path, $creates_version, $creates_service) { + # will get some metadata about the program we are trying to install or remove + $metadata = @{ + installed = $false + product_id = $null + location_type = $null + msi = $false + uninstall_string = $null + path_error = $null + } + + # set the location type and validate the path + if ($path -ne $null) { + if ($path.EndsWith(".msi")) { + $metadata.msi = $true + } else { + $metadata.msi = $false + } + + if ($path.StartsWith("http")) { + $metadata.location_type = [LocationType]::Http + try { + Invoke-WebRequest -Uri $path -DisableKeepAlive -UseBasicParsing -Method HEAD | Out-Null + } catch { + $metadata.path_error = "the file at the URL $path cannot be reached: $($_.Exception.Message)" + } + } elseif ($path.StartsWith("/") -or $path.StartsWith("\\")) { + $metadata.location_type = [LocationType]::Unc + if ($credential -ne $null) { + # Test-Path doesn't support supplying -Credentials, need to create PSDrive before testing + $file_path = Split-Path -Path $path + $file_name = Split-Path -Path $path -Leaf + try { + New-PSDrive -Name win_package -PSProvider FileSystem -Root $file_path -Credential $credential -Scope Script + } catch { + Fail-Json -obj $result -message "failed to connect network drive with credentials: $($_.Exception.Message)" + } + $test_path = "win_package:\$file_name" + } else { + # Someone is using an auth that supports credential delegation, at least it will fail otherwise + $test_path = $path + } + + $valid_path = Test-Path -Path $test_path -PathType Leaf + if ($valid_path -ne $true) { + $metadata.path_error = "the file at the UNC path $path cannot be reached, ensure the user_name account has access to this path or use an auth transport with credential delegation" + } + } else { + $metadata.location_type = [LocationType]::Local + $valid_path = Test-Path -Path $path -PathType Leaf + if ($valid_path -ne $true) { + $metadata.path_error = "the file at the local path $path cannot be reached" + } + } + } else { + # should only occur when state=absent and product_id is not null, we can get the uninstall string from the reg value + $metadata.location_type = [LocationType]::Empty + } + + # try and get the product id + if ($product_id -ne $null) { + $metadata.product_id = $product_id + } else { + # we can get the product_id if the path is an msi and is either a local file or unc file with credential delegation + if (($metadata.msi -eq $true) -and (($metadata.location_type -eq [LocationType]::Local) -or ($metadata.location_type -eq [LocationType]::Unc -and $credential -eq $null))) { + Add-Type -TypeDefinition $msi_tools + try { + $metadata.product_id = [Ansible.MsiTools]::GetPackageProperty($path, "ProductCode") + } catch { + Fail-Json -obj $result -message "failed to get product_id from MSI at $($path): $($_.Exception.Message)" + } + } elseif ($creates_path -eq $null -and $creates_service -eq $null) { + # we need to fail without the product id at this point + Fail-Json $result "product_id is required when the path is not an MSI or the path is an MSI but not local" + } + } + + if ($metadata.product_id -ne $null) { + $uninstall_key = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\$($metadata.product_id)" + $uninstall_key_wow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\$($metadata.product_id)" + if (Test-Path -Path $uninstall_key) { + $metadata.installed = $true + } elseif (Test-Path -Path $uninstall_key_wow64) { + $metadata.installed = $true + $uninstall_key = $uninstall_key_wow64 + } + + # if the reg key exists, try and get the uninstall string and check if it is an MSI + if ($metadata.installed -eq $true -and $metadata.location_type -eq [LocationType]::Empty) { + if (Test-RegistryProperty -path $uninstall_key -name "UninstallString") { + $metadata.uninstall_string = (Get-ItemProperty -Path $uninstall_key -Name "UninstallString").UninstallString + if ($metadata.uninstall_string.StartsWith("MsiExec")) { + $metadata.msi = $true + } + } + } + } + + # use the creates_* to determine if the program is installed + if ($creates_path -ne $null) { + $path_exists = Test-Path -Path $creates_path + $metadata.installed = $path_exists + + if ($creates_version -ne $null -and $path_exists -eq $true) { + if (Test-Path -Path $creates_path -PathType Leaf) { + $existing_version = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($creates_path).FileVersion + $version_matched = $creates_version -eq $existing_version + $metadata.installed = $version_matched + } else { + Fail-Json -obj $result -message "creates_path must be a file not a directory when creates_version is set" + } + } + } + if ($creates_service -ne $null) { + $existing_service = Get-Service -Name $creates_service -ErrorAction SilentlyContinue + $service_exists = $existing_service -ne $null + $metadata.installed = $service_exists + } + + + # finally throw error if path is not valid unless we want to uninstall the package and it already is + if ($metadata.path_error -ne $null -and (-not ($state -eq "absent" -and $metadata.installed -eq $false))) { + Fail-Json -obj $result -message $metadata.path_error + } + + return $metadata +} + +Function Convert-Encoding($string) { + # this will attempt to detect UTF-16 encoding and convert to UTF-8 for + # processes like msiexec + $bytes = ([System.Text.Encoding]::Default).GetBytes($string) + $is_utf16 = $true + for ($i = 0; $i -lt $bytes.Count; $i = $i + 2) { + $char = $bytes[$i + 1] + if ($char -ne [byte]0) { + $is_utf16 = $false + break + } + } + + if ($is_utf16 -eq $true) { + return ([System.Text.Encoding]::Unicode).GetString($bytes) + } else { + return $string + } +} + +$program_metadata = Get-ProgramMetadata -state $state -path $path -product_id $product_id -credential $credential -creates_path $creates_path -creates_version $creates_version -creates_service $creates_service +if ($state -eq "absent") { + if ($program_metadata.installed -eq $true) { + # artifacts we create that must be cleaned up + $cleanup_artifacts = @() + try { + # If path is on a network and we specify credentials or path is a + # URL and not an MSI we need to get a temp local copy + if ($program_metadata.location_type -eq [LocationType]::Unc -and $credential -ne $null) { + $file_name = Split-Path -Path $path -Leaf + $local_path = [System.IO.Path]::GetRandomFileName() + Copy-Item -Path "win_package:\$file_name" -Destination $local_path -WhatIf:$check_mode + $cleanup_artifacts += $local_path + } elseif ($program_metadata.location_type -eq [LocationType]::Http -and $program_metadata.msi -ne $true) { + $local_path = [System.IO.Path]::GetRandomFileName() + + if (-not $check_mode) { + Download-File -url $path -path $local_path + } + $cleanup_artifacts += $local_path + } elseif ($program_metadata.location_type -eq [LocationType]::Empty -and $program_metadata.msi -ne $true) { + # TODO validate the uninstall_string to see if there are extra args in there + $local_path = $program_metadata.uninstall_string + } else { + $local_path = $path + } + + if ($program_metadata.msi -eq $true) { + # we are installing an msi + $uninstall_exe = "$env:windir\system32\msiexec.exe" + $temp_path = [System.IO.Path]::GetTempPath() + $log_file = [System.IO.Path]::GetRandomFileName() + $log_path = Join-Path -Path $temp_path -ChildPath $log_file + $cleanup_artifacts += $log_path + + if ($program_metadata.product_id -ne $null) { + $id = $program_metadata.product_id + } else { + $id = "`"$local_path`""` + } + + $uninstall_arguments = @("/x", $id, "/L*V", "`"$log_path`"", "/qn", "/norestart") + if ($arguments -ne $null) { + $uninstall_arguments += $arguments + } + $uninstall_arguments = $uninstall_arguments -join " " + } else { + $log_path = $null + + $uninstall_exe = $local_path + if ($arguments -ne $null) { + $uninstall_arguments = $arguments + } else { + $uninstall_arguments = "" + } + } + + if (-not $check_mode) { + $process_result = Run-Process -executable $uninstall_exe -arguments $uninstall_arguments + + if (($log_path -ne $null) -and (Test-Path -Path $log_path)) { + $log_content = Get-Content -Path $log_path | Out-String + } else { + $log_content = $null + } + + $result.rc = $process_result.rc + $result.exit_code = $process_result.rc # deprecate in 2.6 + if ($valid_return_codes -notcontains $process_result.rc) { + $result.stdout = Convert-Encoding -string $process_result.stdout + $result.stderr = Convert-Encoding -string $process_result.stderr + if ($log_content -ne $null) { + $result.log = $log_content + } + Fail-Json -obj $result -message "unexpected rc from uninstall $uninstall_exe $($uninstall_arguments): see exit_code, stdout and stderr for more details" + } else { + $result.failed = $false + } + + if ($process_result.rc -eq 3010) { + $result.reboot_required = $true + $result.restart_required = $true + } + } + } finally { + # make sure we cleanup any remaining artifacts + foreach ($cleanup_artifact in $cleanup_artifacts) { + if (Test-Path -Path $cleanup_artifact) { + Remove-Item -Path $cleanup_artifact -Recurse -Force -WhatIf:$check_mode + } + } + } + + $result.changed = $true + } +} else { + if ($program_metadata.installed -eq $false) { + # artifacts we create that must be cleaned up + $cleanup_artifacts = @() + try { + # If path is on a network and we specify credentials or path is a + # URL and not an MSI we need to get a temp local copy + if ($program_metadata.location_type -eq [LocationType]::Unc -and $credential -ne $null) { + $file_name = Split-Path -Path $path -Leaf + $local_path = [System.IO.Path]::GetRandomFileName() + Copy-Item -Path "win_package:\$file_name" -Destination $local_path -WhatIf:$check_mode + $cleanup_artifacts += $local_path + } elseif ($program_metadata.location_type -eq [LocationType]::Http -and $program_metadata.msi -ne $true) { + $local_path = [System.IO.Path]::GetRandomFileName() + + if (-not $check_mode) { + Download-File -url $path -path $local_path + } + $cleanup_artifacts += $local_path + } else { + $local_path = $path + } + + + if ($program_metadata.msi -eq $true) { + # we are installing an msi + $install_exe = "$env:windir\system32\msiexec.exe" + $temp_path = [System.IO.Path]::GetTempPath() + $log_file = [System.IO.Path]::GetRandomFileName() + $log_path = Join-Path -Path $temp_path -ChildPath $log_file + + $cleanup_artifacts += $log_path + $install_arguments = @("/i", "`"$($local_path)`"", "/L*V", "`"$log_path`"", "/qn", "/norestart") + if ($arguments -ne $null) { + $install_arguments += $arguments + } + $install_arguments = $install_arguments -join " " + } else { + $log_path = $null + $install_exe = $local_path + if ($arguments -ne $null) { + $install_arguments = $arguments + } else { + $install_arguments = "" + } + } + + if (-not $check_mode) { + $process_result = Run-Process -executable $install_exe -arguments $install_arguments + + if (($log_path -ne $null) -and (Test-Path -Path $log_path)) { + $log_content = Get-Content -Path $log_path | Out-String + } else { + $log_content = $null + } + + $result.rc = $process_result.rc + $result.exit_code = $process_result.rc # deprecate in 2.6 + if ($valid_return_codes -notcontains $process_result.rc) { + $result.stdout = Convert-Encoding -string $process_result.stdout + $result.stderr = Convert-Encoding -string $process_result.stderr + if ($log_content -ne $null) { + $result.log = $log_content + } + Fail-Json -obj $result -message "unexpected rc from install $install_exe $($install_arguments): see exit_code, stdout and stderr for more details" + } else { + $result.failed = $false + } + + if ($process_result.rc -eq 3010) { + $result.reboot_required = $true + $result.restart_required = $true + } + } + } finally { + # make sure we cleanup any remaining artifacts + foreach ($cleanup_artifact in $cleanup_artifacts) { + if (Test-Path -Path $cleanup_artifact) { + Remove-Item -Path $cleanup_artifact -Recurse -Force -WhatIf:$check_mode + } + } + } + + $result.changed = $true + } +} + +Exit-Json -obj $result diff --git a/lib/ansible/modules/windows/win_package.py b/lib/ansible/modules/windows/win_package.py index a02bda04e1..029f82622d 100644 --- a/lib/ansible/modules/windows/win_package.py +++ b/lib/ansible/modules/windows/win_package.py @@ -25,78 +25,135 @@ ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'core'} - DOCUMENTATION = r''' --- module: win_package version_added: "1.7" -author: Trond Hindenes -short_description: Installs/Uninstalls an installable package, either from local file system or url +short_description: Installs/uninstalls an installable package description: - - Installs or uninstalls a package. - - > - Use a product_id to check if the package needs installing. You can find product ids for installed programs in the windows registry - either in C(HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall) or for 32 bit programs - C(HKLM:Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall) - - For non-Windows targets, use the M(package) module instead. +- Installs or uninstalls a package in either an MSI or EXE format. +- These packages can be sources from the local file system, network file share + or a url. +- Please read the notes section around some caveats with this module. options: - path: - description: - - Location of the package to be installed (either on file system, network share or url) - required: true - name: - description: - - Name of the package, if name isn't specified the path will be used for log messages - required: false - default: null - product_id: - description: - - Product id of the installed package (used for checking if already installed) - - > - You can find product ids for installed programs in the windows registry either in C(HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall) - or for 32 bit programs C(HKLM:Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall) - required: true - aliases: [productid] arguments: description: - - Any arguments the installer needs - default: null - required: false - state: + - Any arguments the installer needs to either install or uninstall the + package. + - If the package is an MSI do not supply the C(/qn), C(/log) or + C(/norestart) arguments. + creates_path: description: - - Install or Uninstall - choices: - - present - - absent - default: present - required: false - aliases: [ensure] - user_name: + - Will check the existance of the path specified and use the result to + determine whether the package is already installed. + - You can use this in conjunction with C(product_id) and other C(creates_*). + version_added: '2.4' + creates_service: description: - - Username of an account with access to the package if it's located on a file share. Only needed if the winrm user doesn't have access to the package. - Also specify user_password for this to function properly. - default: null - required: false - user_password: + - Will check the existing of the service specified and use the result to + determine whether the package is already installed. + - You can use this in conjunction with C(product_id) and other C(creates_*). + version_added: '2.4' + creates_version: description: - - Password of an account with access to the package if it's located on a file share. Only needed if the winrm user doesn't have access to the package. - Also specify user_name for this to function properly. - default: null - required: false + - Will check the file version property of the file at C(creates_path) and + use the result to determine whether the package is already installed. + - C(creates_path) MUST be set and is a file. + - You can use this in conjunction with C(product_id) and other C(creates_*). + version_added: '2.4' expected_return_code: description: - - One or more return codes from the package installation that indicates success. - - If not provided, defaults to 0 - required: no - default: 0 + - One or more return codes from the package installation that indicates + success. + - Before Ansible 2.4 this was just 0 but since 2.4 this is both C(0) and + C(3010). + - A return code of C(3010) usually means that a reboot is required, the + C(reboot_required) return value is set if the return code is C(3010). + default: [0, 3010] + name: + description: + - Name of the package, if name isn't specified the path will be used for + log messages. + - As of Ansible 2.4 this is deprecated and no longer required. + password: + description: + - The password for C(user_name), must be set when C(user_name) is. + aliases: [ user_password ] + path: + description: + - Location of the package to be installed or uninstalled. + - This package can either be on the local file system, network share or a + url. + - If the path is on a network share and the current WinRM transport doesn't + support credential delegation, then C(user_name) and C(user_password) + must be set to access the file. + - There are cases where this file will be copied locally to the server so + it can access it, see the notes for more info. + - If C(state=present) then this value MUST be set. + - If C(state=absent) then this value does not need to be set if + C(product_id) is. + product_id: + description: + - The product id of the installed packaged. + - This is used for checking whether the product is already installed and + getting the uninstall information if C(state=absent). + - You can find product ids for installed programs in the Windows registry + editor either at + C(HKLM:Software\Microsoft\Windows\CurrentVersion\Uninstall) or for 32 bit + programs at + C(HKLM:Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall). + - This SHOULD be set when the package is not an MSI, or the path is a url + or a network share and credential delegation is not being used. The + C(creates_*) options can be used instead but is not recommended. + aliases: [ productid ] + state: + description: + - Whether to install or uninstall the package. + - The module uses C(product_id) and whether it exists at the registry path + to see whether it needs to install or uninstall the package. + default: present + aliases: [ ensure ] + username: + description: + - Username of an account with access to the package if it is located on a + file share. + - This is only needed if the WinRM transport is over an auth method that + does not support credential delegation like Basic or NTLM. + aliases: [ user_name ] + validate_certs: + description: + - If C(no), SSL certificates will not be validated. This should only be + used on personally controlled sites using self-signed certificates. + - Before Ansible 2.4 this defaulted to C(no). + type: bool + default: 'yes' + version_added: '2.4' notes: - - For non-Windows targets, use the M(package) module instead. +- For non Windows targets, use the M(package) module instead. +- When C(state=absent) and the product is an exe, the path may be different + from what was used to install the package originally. If path is not set then + the path used will be what is set under C(UninstallString) in the registry + for that product_id. +- Not all product ids are in a GUID form, some programs incorrectly use a + different structure but this module should support any format. +- By default all msi installs and uninstalls will be run with the options + C(/log, /qn, /norestart). +- It is recommended you download the pacakge first from the URL using the + M(win_get_url) module as it opens up more flexibility with what must be set + when calling C(win_package). +- Packages will be temporarily downloaded or copied locally when path is a + network location and credential delegation is not set, or path is a URL + and the file is not an MSI. +- All the installation checks under C(product_id) and C(creates_*) add + together, if one fails then the program is considered to be absent. +author: +- Trond Hindenes (@trondhindenes) +- Jordan Borean (@jborean93) ''' EXAMPLES = r''' - name: Install the Visual C thingy win_package: - name: Microsoft Visual C thingy path: http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe product_id: '{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}' arguments: /install /passive /norestart @@ -105,30 +162,98 @@ EXAMPLES = r''' win_package: path: https://download.microsoft.com/download/A/F/0/AF0071F3-B198-4A35-AA90-C68D103BDCCF/rdcman.msi product_id: '{0240359E-6A4C-4884-9E94-B397A02D893C}' + state: present -- name: Uninstall Remote Desktop Connection Manager installed from msi +- name: Uninstall Remote Desktop Connection Manager win_package: - path: https://download.microsoft.com/download/A/F/0/AF0071F3-B198-4A35-AA90-C68D103BDCCF/rdcman.msi product_id: '{0240359E-6A4C-4884-9E94-B397A02D893C}' state: absent -# Specify the expected non-zero return code when successful -# In this case 3010 indicates 'reboot required' -- name: 'Microsoft .NET Framework 4.5.1' +- name: Install Remote Desktop Connection Manager locally omitting the product_id win_package: - path: https://download.microsoft.com/download/1/6/7/167F0D79-9317-48AE-AEDB-17120579F8E2/NDP451-KB2858728-x86-x64-AllOS-ENU.exe - productid: '{7DEBE4EB-6B40-3766-BB35-5CBBC385DA37}' - arguments: '/q /norestart' - ensure: present - expected_return_code: 3010 + path: C:\temp\rdcman.msi + state: present -# Specify multiple non-zero return codes when successful -# In this case we can say that both 0 (SUCCESSFUL) and 3010 (REBOOT REQUIRED) codes are acceptable -- name: 'Microsoft .NET Framework 4.5.1' +- name: Uninstall Remote Desktop Connection Manager from local MSI omitting the product_id + win_package: + path: C:\temp\rdcman.msi + state: absent + +# 7-Zip exe doesn't use a guid for the Product ID +- name: Install 7zip from a network share specifying the credentials + win_package: + path: \\domain\programs\7z.exe + product_id: 7-Zip + arguments: /S + state: present + user_name: DOMAIN\User + user_password: Password + +- name: Install 7zip and use a file version for the installation check + win_package: + path: C:\temp\7z.exe + creates_path: C:\Program Files\7-Zip\7z.exe + creates_version: 16.04 + state: present + +- name: Uninstall 7zip from the exe + win_package: + path: C:\Program Files\7-Zip\Uninstall.exe + product_id: 7-Zip + arguments: /S + state: absent + +- name: Uninstall 7zip without specifying the path + win_package: + product_id: 7-Zip + arguments: /S + state: absent + +- name: Install application and override expected return codes win_package: path: https://download.microsoft.com/download/1/6/7/167F0D79-9317-48AE-AEDB-17120579F8E2/NDP451-KB2858728-x86-x64-AllOS-ENU.exe - productid: '{7DEBE4EB-6B40-3766-BB35-5CBBC385DA37}' + product_id: '{7DEBE4EB-6B40-3766-BB35-5CBBC385DA37}' arguments: '/q /norestart' - ensure: present - expected_return_code: [0,3010] + state: present + expected_return_code: [0, 666, 3010] +''' + +RETURN = r''' +exit_code: + description: See rc, this will be removed in favour of rc in Ansible 2.6. + returned: change occured + type: int + sample: 0 +log: + description: The contents of the MSI log. + returned: change occured and package is an MSI + type: str + sample: Installation completed successfully +rc: + description: The return code of the pacakge process. + returned: change occured + type: int + sample: 0 +reboot_required: + description: Whether a reboot is required to finalise package. This is set + to true if the executable return code is 3010. + returned: always + type: bool + sample: True +restart_required: + description: See reboot_required, this will be removed in favour of + reboot_required in Ansible 2.6 + returned: always + type: bool + sample: True +stdout: + description: The stdout stream of the package process. + returned: failure during install or uninstall + type: str + sample: Installing program +stderr: + description: The stderr stream of the package process. + returned: failure during install or uninstall + type: str + sample: Failed to install program ''' diff --git a/test/integration/targets/win_package/defaults/main.yml b/test/integration/targets/win_package/defaults/main.yml index ebc97b84cc..c8ea8238e5 100644 --- a/test/integration/targets/win_package/defaults/main.yml +++ b/test/integration/targets/win_package/defaults/main.yml @@ -1,6 +1,18 @@ --- +# spaces are tricky, let's have one by default +test_win_package_path: C:\ansible\win package +test_win_package_good_url: https://s3.amazonaws.com/ansible-ci-files/test/integration/roles/test_win_package/good.msi +test_win_package_reboot_url: https://s3.amazonaws.com/ansible-ci-files/test/integration/roles/test_win_package/reboot.msi +test_win_package_bad_url: https://s3.amazonaws.com/ansible-ci-files/test/integration/roles/test_win_package/bad.msi +test_win_package_exe_url: https://s3.amazonaws.com/ansible-ci-files/test/integration/roles/test_win_package/7z.exe # TODO: change to it's own executable -msi_url: https://ansible-ci-files.s3.amazonaws.com/test/integration/roles/test_win_msi/7z922-x64.msi -msi_download_path: "C:\\Program Files\\7z922-x64.msi" -msi_install_path: "C:\\Program Files\\7-Zip" -msi_product_code: "{23170F69-40C1-2702-0922-000001000000}" +test_win_package_good_id: '{223D9A13-653B-4231-A365-EDDC30B4F226}' +test_win_package_reboot_id: '{223D9A13-653B-4231-A365-EDDC30B4F227}' +test_win_package_exe_id: 7-Zip + +# define the below to run the network tests, all 3 msi's should exist in this path +# test_win_package_network_path: \\ANSIBLE\network + +# set the below to test a network path without credential delegation like Basic or NTLM +# test_win_package_network_username: ANSIBLE\User +# test_win_package_network_password: Password \ No newline at end of file diff --git a/test/integration/targets/win_package/files/bad.wsx b/test/integration/targets/win_package/files/bad.wsx new file mode 100644 index 0000000000..5014610940 --- /dev/null +++ b/test/integration/targets/win_package/files/bad.wsx @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/integration/targets/win_package/files/good.wsx b/test/integration/targets/win_package/files/good.wsx new file mode 100644 index 0000000000..ee90745687 --- /dev/null +++ b/test/integration/targets/win_package/files/good.wsx @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/integration/targets/win_package/files/reboot.wsx b/test/integration/targets/win_package/files/reboot.wsx new file mode 100644 index 0000000000..c6ded2a67d --- /dev/null +++ b/test/integration/targets/win_package/files/reboot.wsx @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/integration/targets/win_package/tasks/exe_tests.yml b/test/integration/targets/win_package/tasks/exe_tests.yml new file mode 100644 index 0000000000..ed3c88801c --- /dev/null +++ b/test/integration/targets/win_package/tasks/exe_tests.yml @@ -0,0 +1,303 @@ +--- +- name: install local exe (check mode) + win_package: + path: '{{test_win_package_path}}\7z.exe' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + register: install_local_exe_check + check_mode: yes + +- name: get result of install local exe (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: install_local_exe_actual_check + +- name: assert install local exe (check mode) + assert: + that: + - install_local_exe_check|changed + - install_local_exe_check.reboot_required == False + - install_local_exe_actual_check.exists == False + +- name: install local exe + win_package: + path: '{{test_win_package_path}}\7z.exe' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + register: install_local_exe + +- name: get result of install local exe + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: install_local_exe_actual + +- name: assert install local exe + assert: + that: + - install_local_exe|changed + - install_local_exe.reboot_required == False + - install_local_exe.exit_code == 0 + - install_local_exe_actual.exists == True + +- name: install local exe (idempotent) + win_package: + path: '{{test_win_package_path}}\7z.exe' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + register: install_local_exe_idempotent + +- name: assert install local exe (idempotent) + assert: + that: + - not install_local_exe_idempotent|changed + +- name: uninstall local exe with path (check mode) + win_package: + path: C:\Program Files\7-Zip\Uninstall.exe + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + register: uninstall_path_local_exe_check + check_mode: yes + +- name: get result of uninstall local exe with path (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: uninstall_path_local_exe_actual_check + +- name: assert uninstall local exe with path (check mode) + assert: + that: + - uninstall_path_local_exe_check|changed + - uninstall_path_local_exe_check.reboot_required == False + - uninstall_path_local_exe_actual_check.exists == True + +- name: uninstall local exe with path + win_package: + path: C:\Program Files\7-Zip\Uninstall.exe + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + register: uninstall_path_local_exe + +- name: get result of uninstall local exe with path + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: uninstall_path_local_exe_actual + +- name: assert uninstall local exe with path + assert: + that: + - uninstall_path_local_exe|changed + - uninstall_path_local_exe.reboot_required == False + - uninstall_path_local_exe.exit_code == 0 + - uninstall_path_local_exe_actual.exists == False + +- name: uninstall local exe with path (idempotent) + win_package: + path: C:\Program Files\7-Zip\Uninstall.exe + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + register: uninstall_path_local_exe_idempotent + +- name: assert uninstall local exe with path (idempotent) + assert: + that: + - not uninstall_path_local_exe_idempotent|changed + +- name: install url exe (check mode) + win_package: + path: '{{test_win_package_exe_url}}' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + register: install_url_exe_check + check_mode: yes + +- name: get result of install url exe (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: install_url_exe_actual_check + +- name: assert install url exe (check mode) + assert: + that: + - install_url_exe_check|changed + - install_url_exe_check.reboot_required == False + - install_url_exe_actual_check.exists == False + +- name: install url exe + win_package: + path: '{{test_win_package_exe_url}}' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + register: install_url_exe + +- name: get result of install url exe + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: install_url_exe_actual + +- name: assert install url exe + assert: + that: + - install_url_exe|changed + - install_url_exe.reboot_required == False + - install_url_exe.exit_code == 0 + - install_url_exe_actual.exists == True + +- name: install url exe (idempotent) + win_package: + path: '{{test_win_package_exe_url}}' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + register: install_url_exe_again + +- name: assert install url exe (idempotent) + assert: + that: + - not install_url_exe_again|changed + +- name: uninstall local exe with product_id (check mode) + win_package: + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + register: uninstall_id_local_exe_check + check_mode: yes + +- name: get result of uninstall local exe with product_id (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: uninstall_id_local_exe_actual_check + +- name: assert uninstall local exe with product_id (check mode) + assert: + that: + - uninstall_id_local_exe_check|changed + - uninstall_id_local_exe_check.reboot_required == False + - uninstall_id_local_exe_actual_check.exists == True + +- name: uninstall local exe with product_id + win_package: + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + register: uninstall_id_local_exe + +- name: get result of uninstall local exe with product_id + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: uninstall_id_local_exe_actual + +- name: assert uninstall local exe with product_id + assert: + that: + - uninstall_id_local_exe|changed + - uninstall_id_local_exe.reboot_required == False + - uninstall_id_local_exe.exit_code == 0 + - uninstall_id_local_exe_actual.exists == False + +- name: uninstall local exe with product_id (idempotent) + win_package: + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + register: uninstall_id_local_exe_idempotent + +- name: assert uninstall local exe with product_id (idempotent) + assert: + that: + - not uninstall_id_local_exe_idempotent|changed + +- name: install exe checking path + win_package: + path: '{{test_win_package_path}}\7z.exe' + arguments: /S + creates_path: C:\Program Files\7-Zip\7z.exe + register: install_exe_create_path + +- name: get result of install exe checking path + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: install_exe_create_path_actual + +- name: assert install exe checking path + assert: + that: + - install_exe_create_path.changed == True + - install_exe_create_path_actual.exists == True + +- name: install exe checking path (idempotent) + win_package: + path: '{{test_win_package_path}}\7z.exe' + arguments: /S + creates_path: C:\Program Files\7-Zip\7z.exe + register: install_exe_create_path_again + +- name: assert install exe checking path (idempotent) + assert: + that: + - not install_exe_create_path_again.changed == True + +- name: install exe checking path and version + win_package: + path: '{{test_win_package_path}}\7z.exe' + arguments: /S + creates_path: C:\Program Files\7-Zip\7z.exe + creates_version: '16.04' + register: install_exe_create_version_match + +- name: assert install exe checking path and version + assert: + that: + - not install_exe_create_version_match|changed + +- name: install exe checking path and version mismatch + win_package: + path: '{{test_win_package_path}}\7z.exe' + arguments: /S + creates_path: C:\Program Files\7-Zip\7z.exe + creates_version: fail-version + register: install_exe_create_version_mismatch + +- name: assert install exe checking path and version mistmatch + assert: + that: + - install_exe_create_version_mismatch|changed + +- name: install exe checking service + win_package: + path: '{{test_win_package_path}}\7z.exe' + arguments: /S + creates_service: Netlogon + register: install_exe_create_service_match + +- name: assert install exe checking service + assert: + that: + - not install_exe_create_service_match|changed + +- name: install exe checking service mismatch + win_package: + path: '{{test_win_package_path}}\7z.exe' + arguments: /S + creates_service: fake-service + register: install_exe_create_service_mismatch + +- name: assert install exe checking service mismatch + assert: + that: + - install_exe_create_service_mismatch|changed + +- name: uninstall exe post tests + win_package: + arguments: /S + product_id: '{{test_win_package_exe_id}}' + state: absent diff --git a/test/integration/targets/win_package/tasks/failure_tests.yml b/test/integration/targets/win_package/tasks/failure_tests.yml new file mode 100644 index 0000000000..ac067c18b3 --- /dev/null +++ b/test/integration/targets/win_package/tasks/failure_tests.yml @@ -0,0 +1,82 @@ +# these tests are invalid arguments and failure states for win_package +--- +- name: fail to install broken msi + win_package: + path: '{{test_win_package_path}}\bad.msi' + state: present + register: fail_bad_rc + failed_when: "'unexpected rc from install' not in fail_bad_rc.msg and fail_bad_rc.exit_code != 1603" + +- name: fail when not using an int for a return code + win_package: + path: '{{test_win_package_path}}\good.msi' + state: present + expected_return_code: 0,abc + register: fail_invalid_return_code + failed_when: fail_invalid_return_code.msg != 'failed to parse expected return code abc as an integer' + +- name: fail when path is not set and state!= absent + win_package: + state: present + register: fail_no_path + failed_when: fail_no_path.msg != 'path can only be null when state=absent and product_id is not null' + +- name: fail when path is not set and state=absent but product_id is null + win_package: + state: absent + register: fail_no_path_state_absent_no_id + failed_when: fail_no_path_state_absent_no_id.msg != 'path can only be null when state=absent and product_id is not null' + +- name: fail when product_id is not set and path is not a local MSI + win_package: + path: '{{test_win_package_good_url}}' + state: present + register: fail_install_url_no_id + failed_when: fail_install_url_no_id.msg != 'product_id is required when the path is not an MSI or the path is an MSI but not local' + +- name: fail invalid local path + win_package: + path: '{{test_win_package_path}}\no file.msi' + state: present + register: fail_invalid_local_path + failed_when: fail_invalid_local_path.msg != 'the file at the local path ' + test_win_package_path + '\\no file.msi cannot be reached' + +- name: fail invalid URL + win_package: + path: http://fakeurl/file.msi + product_id: 'id' + state: present + register: fail_invalid_url_path + failed_when: "fail_invalid_url_path.msg != 'the file at the URL http://fakeurl/file.msi cannot be reached: The remote name could not be resolved: \\'fakeurl\\''" + +- name: fail invalid UNC path + win_package: + path: \\fakenetwork\unc file.msi + product_id: 'id' + state: present + register: fail_invalid_unc_path + failed_when: fail_invalid_unc_path.msg != 'the file at the UNC path \\\\fakenetwork\\unc file.msi cannot be reached, ensure the user_name account has access to this path or use an auth transport with credential delegation' + +- name: fail when product_id is not set and path is not a local MSI + win_package: + path: '{{test_win_package_good_url}}' + state: present + register: fail_no_id_not_local_msi + failed_when: fail_no_id_not_local_msi.msg != 'product_id is required when the path is not an MSI or the path is an MSI but not local' + +- name: fail to check version without creates_path + win_package: + path: '{{test_win_package_path}}\7z.exe' + state: present + creates_version: 1 + register: fail_creates_version_without_path + failed_when: fail_creates_version_without_path.msg != 'creates_path must be set when creates_version is set' + +- name: fail to check version without when path is not a file + win_package: + path: '{{test_win_package_path}}\7z.exe' + state: present + creates_path: C:\Windows + creates_version: 1 + register: fail_creates_version_not_a_file + failed_when: fail_creates_version_not_a_file.msg != 'creates_path must be a file not a directory when creates_version is set' diff --git a/test/integration/targets/win_package/tasks/main.yml b/test/integration/targets/win_package/tasks/main.yml index 3df394fc75..2d5a76e21e 100644 --- a/test/integration/targets/win_package/tasks/main.yml +++ b/test/integration/targets/win_package/tasks/main.yml @@ -1,81 +1,52 @@ -# test code for the win_package module -# (c) 2014, Chris Church +--- +- name: ensure testing folder exists + win_file: + path: '{{test_win_package_path}}' + state: directory -# This file is part of Ansible -# -# Ansible is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# Ansible is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with Ansible. If not, see . - -- name: use win_get_url module to download msi +- name: download msi files from S3 bucket win_get_url: - url: "{{msi_url}}" - dest: "{{msi_download_path}}" - register: win_get_url_result + url: '{{item.url}}' + dest: '{{test_win_package_path}}\{{item.name}}' + with_items: + - { url: '{{test_win_package_good_url}}', name: 'good.msi' } + - { url: '{{test_win_package_reboot_url}}', name: 'reboot.msi' } + - { url: '{{test_win_package_bad_url}}', name: 'bad.msi' } + # - { url: '{{test_win_package_exe_url}}', name: '7z.exe' } -- name: make sure msi is uninstalled +- name: make sure all test msi's are uninstalled before test win_package: - path: "{{msi_download_path}}" - product_id: "{{msi_product_code}}" + product_id: '{{item.id}}' + arguments: '{{item.args|default(omit)}}' state: absent + with_items: + - { id: '{{test_win_package_good_id}}' } + - { id: '{{test_win_package_reboot_id}}' } + # - { id: '{{test_win_package_exe_id}}', args: '/S' } -- name: install msi - win_package: - path: "{{msi_download_path}}" - product_id: "{{msi_product_code}}" - state: present - register: win_package_install_result +- block: + - name: run tests for expected failures + include_tasks: failure_tests.yml -- name: check win_package install result - assert: - that: - - "not win_package_install_result|failed" - - "win_package_install_result|changed" + - name: run tests for local and URL msi files + include_tasks: msi_tests.yml -- name: install msi again (check for no change) - win_package: - path: "{{msi_download_path}}" - product_id: "{{msi_product_code}}" - state: present - register: win_package_install_again_result + # doesn't work 100% on AWS hosts, disabling for now until we get a better exe example + # - name: run tests for local and URL exe files + # include_tasks: exe_tests.yml -- name: check win_package install again result - assert: - that: - - "not win_package_install_again_result|failed" - - "not win_package_install_again_result|changed" + # these tests can be run manually by defining test_win_package_network_path + - name: run tests for network msi files (manual) + include_tasks: network_tests.yml + when: test_win_package_network_path is defined -- name: uninstall msi - win_package: - path: "{{msi_download_path}}" - product_id: "{{msi_product_code}}" - state: absent - register: win_package_uninstall_result - -- name: check win_package uninstall result - assert: - that: - - "not win_package_uninstall_result|failed" - - "win_package_uninstall_result|changed" - -- name: uninstall msi again (check for no change) - win_package: - path: "{{msi_download_path}}" - product_id: "{{msi_product_code}}" - state: absent - register: win_package_uninstall_again_result - -- name: check win_package uninstall result - assert: - that: - - "not win_package_uninstall_result|failed" - - "not win_package_uninstall_again_result|changed" + always: + - name: make sure all test msi's are uninstalled after test + win_package: + product_id: '{{item.id}}' + arguments: '{{item.args|default(omit)}}' + state: absent + with_items: + - { id: '{{test_win_package_good_id}}' } + - { id: '{{test_win_package_reboot_id}}' } + # - { id: '{{test_win_package_exe_id}}', args: '/S' } diff --git a/test/integration/targets/win_package/tasks/msi_tests.yml b/test/integration/targets/win_package/tasks/msi_tests.yml new file mode 100644 index 0000000000..5dcfdb0523 --- /dev/null +++ b/test/integration/targets/win_package/tasks/msi_tests.yml @@ -0,0 +1,335 @@ +--- +# this test just makes sure the task doesn't fail when we set out expected rc +- name: install broken msi override expected rc + win_package: + path: '{{test_win_package_path}}\bad.msi' + state: present + expected_return_code: 0,1603 + +- name: install local msi (check mode) + win_package: + path: '{{test_win_package_path}}\good.msi' + state: present + register: install_local_msi_check + check_mode: yes + +- name: get result of install local msi (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: install_local_msi_actual_check + +- name: assert install local msi (check mode) + assert: + that: + - install_local_msi_check|changed + - install_local_msi_check.reboot_required == False + - install_local_msi_actual_check.exists == False + +- name: install local msi + win_package: + path: '{{test_win_package_path}}\good.msi' + state: present + expected_return_code: "" # historical, an empty string means 0, 3010 + register: install_local_msi + +- name: get result of install local msi + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: install_local_msi_actual + +- name: assert install local msi + assert: + that: + - install_local_msi|changed + - install_local_msi.reboot_required == False + - install_local_msi.exit_code == 0 + - install_local_msi_actual.exists == True + +- name: install local msi (idempotent) + win_package: + path: '{{test_win_package_path}}\good.msi' + state: present + register: install_local_msi_idempotent + +- name: assert install local msi (idempotent) + assert: + that: + - not install_local_msi_idempotent|changed + +- name: uninstall local msi with path (check mode) + win_package: + path: '{{test_win_package_path}}\good.msi' + state: absent + register: uninstall_path_local_msi_check + check_mode: yes + +- name: get result of uninstall local msi with path (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: uninstall_path_local_msi_actual_check + +- name: assert uninstall local msi with path (check mode) + assert: + that: + - uninstall_path_local_msi_check|changed + - uninstall_path_local_msi_check.reboot_required == False + - uninstall_path_local_msi_actual_check.exists == True + +- name: uninstall local msi with path + win_package: + path: '{{test_win_package_path}}\good.msi' + state: absent + register: uninstall_path_local_msi + +- name: get result of uninstall local msi with path + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: uninstall_path_local_msi_actual + +- name: assert uninstall local msi with path + assert: + that: + - uninstall_path_local_msi|changed + - uninstall_path_local_msi.reboot_required == False + - uninstall_path_local_msi.exit_code == 0 + - uninstall_path_local_msi_actual.exists == False + +- name: uninstall local msi with path (idempotent) + win_package: + path: '{{test_win_package_path}}\good.msi' + state: absent + register: uninstall_path_local_msi_idempotent + +- name: assert uninstall local msi with path (idempotent) + assert: + that: + - not uninstall_path_local_msi_idempotent|changed + +- name: install url msi (check mode) + win_package: + path: '{{test_win_package_good_url}}' + product_id: '{{test_win_package_good_id}}' + state: present + register: install_url_msi_check + check_mode: yes + +- name: get result of install url msi (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: install_url_msi_actual_check + +- name: assert install url msi (check mode) + assert: + that: + - install_url_msi_check|changed + - install_url_msi_check.reboot_required == False + - install_url_msi_actual_check.exists == False + +- name: install url msi + win_package: + path: '{{test_win_package_good_url}}' + product_id: '{{test_win_package_good_id}}' + state: present + register: install_url_msi + +- name: get result of install url msi + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: install_url_msi_actual + +- name: assert install url msi + assert: + that: + - install_url_msi|changed + - install_url_msi.reboot_required == False + - install_url_msi.exit_code == 0 + - install_url_msi_actual.exists == True + +- name: install url msi (idempotent) + win_package: + path: '{{test_win_package_good_url}}' + product_id: '{{test_win_package_good_id}}' + state: present + register: install_url_msi_again + +- name: assert install url msi (idempotent) + assert: + that: + - not install_url_msi_again|changed + +- name: uninstall local msi with product_id (check mode) + win_package: + product_id: '{{test_win_package_good_id}}' + state: absent + register: uninstall_id_local_msi_check + check_mode: yes + +- name: get result of uninstall local msi with product_id (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: uninstall_id_local_msi_actual_check + +- name: assert uninstall local msi with product_id (check mode) + assert: + that: + - uninstall_id_local_msi_check|changed + - uninstall_id_local_msi_check.reboot_required == False + - uninstall_id_local_msi_actual_check.exists == True + +- name: uninstall local msi with product_id + win_package: + product_id: '{{test_win_package_good_id}}' + state: absent + register: uninstall_id_local_msi + +- name: get result of uninstall local msi with product_id + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: uninstall_id_local_msi_actual + +- name: assert uninstall local msi with product_id + assert: + that: + - uninstall_id_local_msi|changed + - uninstall_id_local_msi.reboot_required == False + - uninstall_id_local_msi.exit_code == 0 + - uninstall_id_local_msi_actual.exists == False + +- name: uninstall local msi with product_id (idempotent) + win_package: + product_id: '{{test_win_package_good_id}}' + state: absent + register: uninstall_id_local_msi_idempotent + +- name: assert uninstall local msi with product_id (idempotent) + assert: + that: + - not uninstall_id_local_msi_idempotent|changed + +- name: install local reboot msi (check mode) + win_package: + path: '{{test_win_package_path}}\reboot.msi' + state: present + register: install_local_reboot_msi_check + check_mode: yes + +- name: get result of install local reboot msi (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_reboot_id}} + register: install_local_reboot_msi_actual_check + +- name: assert install local reboot msi (check mode) + assert: + that: + - install_local_reboot_msi_check|changed + - install_local_reboot_msi_check.reboot_required == False + - install_local_reboot_msi_actual_check.exists == False + +- name: install local reboot msi + win_package: + path: '{{test_win_package_path}}\reboot.msi' + state: present + register: install_local_reboot_msi + +- name: get result of install local reboot msi + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_reboot_id}} + register: install_local_reboot_msi_actual + +- name: assert install local reboot msi + assert: + that: + - install_local_reboot_msi|changed + - install_local_reboot_msi.reboot_required == True + - install_local_reboot_msi.exit_code == 3010 + - install_local_reboot_msi_actual.exists == True + +- name: install local reboot msi (idempotent) + win_package: + path: '{{test_win_package_path}}\reboot.msi' + state: present + register: install_local_reboot_msi_idempotent + +- name: assert install local reboot msi (idempotent) + assert: + that: + - not install_local_reboot_msi_idempotent|changed + +- name: uninstall reboot msi after test + win_package: + path: '{{test_win_package_path}}\reboot.msi' + state: absent + +- name: ensure the install folder is cleaned in case uninstall didn't work + win_file: + path: '%ProgramFiles(x86)%\Bovine University' + state: absent + +- name: install local msi with arguments (check mode) + win_package: + path: '{{test_win_package_path}}\good.msi' + state: present + arguments: ADDLOCAL=Cow + register: install_msi_argument_check + check_mode: yes + +- name: get result of moo file after install local msi with arguments (check mode) + win_stat: + path: '%ProgramFiles(x86)%\Bovine University\moo.exe' + register: install_msi_argument_moo_check + +- name: get result of cow file after install local msi with arguments (check mode) + win_stat: + path: '%ProgramFiles(x86)%\Bovine University\cow.exe' + register: install_msi_argument_cow_check + +- name: assert install local msi with arguments (check mode) + assert: + that: + - install_msi_argument_check|changed + - install_msi_argument_check.reboot_required == False + - install_msi_argument_moo_check.stat.exists == False + - install_msi_argument_cow_check.stat.exists == False + +- name: install local msi with arguments + win_package: + path: '{{test_win_package_path}}\good.msi' + state: present + arguments: ADDLOCAL=Cow + register: install_msi_argument + +- name: get result of moo file after install local msi with arguments + win_stat: + path: '%ProgramFiles(x86)%\Bovine University\moo.exe' + register: install_msi_argument_moo + +- name: get result of cow file after install local msi with arguments + win_stat: + path: '%ProgramFiles(x86)%\Bovine University\cow.exe' + register: install_msi_argument_cow + +- name: assert install local msi with arguments + assert: + that: + - install_msi_argument|changed + - install_msi_argument.reboot_required == False + - install_msi_argument.exit_code == 0 + - install_msi_argument_moo.stat.exists == False + - install_msi_argument_cow.stat.exists == True + +- name: install local msi with arguments (idempotent) + win_package: + path: '{{test_win_package_path}}\good.msi' + state: present + arguments: ADDLOCAL=Cow + register: install_msi_argument_again + +- name: assert install local msi with arguments (idempotent) + assert: + that: + - not install_msi_argument_again|changed + +- name: uninstall good msi after test + win_package: + path: '{{test_win_package_path}}\good.msi' + state: absent diff --git a/test/integration/targets/win_package/tasks/network_tests.yml b/test/integration/targets/win_package/tasks/network_tests.yml new file mode 100644 index 0000000000..922ee90daa --- /dev/null +++ b/test/integration/targets/win_package/tasks/network_tests.yml @@ -0,0 +1,427 @@ +--- +- name: install network msi (check mode) + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_msi_check + check_mode: yes + +- name: get result of install network msi (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: install_network_msi_actual_check + +- name: assert install network msi (check mode) + assert: + that: + - install_network_msi_check|changed + - install_network_msi_check.reboot_required == False + - install_network_msi_actual_check.exists == False + +- name: install network msi + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_msi + +- name: get result of install network msi + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: install_network_msi_actual + +- name: assert install network msi + assert: + that: + - install_network_msi|changed + - install_network_msi.reboot_required == False + - install_network_msi.exit_code == 0 + - install_network_msi_actual.exists == True + +- name: install network msi (idempotent) + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_msi_idempotent + +- name: assert install network msi (idempotent) + assert: + that: + - not install_network_msi_idempotent|changed + +- name: uninstall network msi with path (check mode) + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: absent + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: uninstall_path_network_msi_check + check_mode: yes + +- name: get result of uninstall network msi with path (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: uninstall_path_network_msi_actual_check + +- name: assert uninstall network msi with path (check mode) + assert: + that: + - uninstall_path_network_msi_check|changed + - uninstall_path_network_msi_check.reboot_required == False + - uninstall_path_network_msi_actual_check.exists == True + +- name: uninstall network msi with path + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: absent + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: uninstall_path_network_msi + +- name: get result of uninstall network msi with path + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_good_id}} + register: uninstall_path_network_msi_actual + +- name: assert uninstall network msi with path + assert: + that: + - uninstall_path_network_msi|changed + - uninstall_path_network_msi.reboot_required == False + - uninstall_path_network_msi.exit_code == 0 + - uninstall_path_network_msi_actual.exists == False + +- name: uninstall network msi with path (idempotent) + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: absent + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: uninstall_path_network_msi_idempotent + +- name: assert uninstall network msi with path (idempotent) + assert: + that: + - not uninstall_path_network_msi_idempotent|changed + +- name: install network reboot msi (check mode) + win_package: + path: '{{test_win_package_network_path}}\reboot.msi' + product_id: '{{test_win_package_reboot_id}}' + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_reboot_msi_check + check_mode: yes + +- name: get result of install network reboot msi (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_reboot_id}} + register: install_network_reboot_msi_actual_check + +- name: assert install network reboot msi (check mode) + assert: + that: + - install_network_reboot_msi_check|changed + - install_network_reboot_msi_check.reboot_required == False + - install_network_reboot_msi_actual_check.exists == False + +- name: install network reboot msi + win_package: + path: '{{test_win_package_network_path}}\reboot.msi' + product_id: '{{test_win_package_reboot_id}}' + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_reboot_msi + +- name: get result of install network reboot msi + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_reboot_id}} + register: install_network_reboot_msi_actual + +- name: assert install network reboot msi + assert: + that: + - install_network_reboot_msi|changed + - install_network_reboot_msi.reboot_required == True + - install_network_reboot_msi.exit_code == 3010 + - install_network_reboot_msi_actual.exists == True + +- name: install network reboot msi (idempotent) + win_package: + path: '{{test_win_package_network_path}}\reboot.msi' + product_id: '{{test_win_package_reboot_id}}' + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_reboot_msi_idempotent + +- name: assert install network reboot msi (idempotent) + assert: + that: + - not install_network_reboot_msi_idempotent|changed + +- name: uninstall network msi with product_id (check mode) + win_package: + product_id: '{{test_win_package_reboot_id}}' + state: absent + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: uninstall_id_network_msi_check + check_mode: yes + +- name: get result of uninstall network msi with product_id (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_reboot_id}} + register: uninstall_id_network_msi_actual_check + +- name: assert uninstall network msi with product_id (check mode) + assert: + that: + - uninstall_id_network_msi_check|changed + - uninstall_id_network_msi_check.reboot_required == False + - uninstall_id_network_msi_actual_check.exists == True + +- name: uninstall network msi with product_id + win_package: + product_id: '{{test_win_package_reboot_id}}' + state: absent + register: uninstall_id_network_msi + +- name: get result of uninstall network msi with product_id + win_reg_stat: + path: HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_reboot_id}} + register: uninstall_id_network_msi_actual + +- name: assert uninstall network msi with product_id + assert: + that: + - uninstall_id_network_msi|changed + - uninstall_id_network_msi.reboot_required == True + - uninstall_id_network_msi.exit_code == 3010 + - uninstall_id_network_msi_actual.exists == False + +- name: uninstall network msi with product_id (idempotent) + win_package: + product_id: '{{test_win_package_reboot_id}}' + state: absent + register: uninstall_id_network_msi_idempotent + +- name: assert uninstall network msi with product_id (idempotent) + assert: + that: + - not uninstall_id_network_msi_idempotent|changed + +- name: ensure the install folder is cleaned in case uninstall didn't work + win_file: + path: '%ProgramFiles(x86)%\Bovine University' + state: absent + +- name: install network msi with arguments (check mode) + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: present + arguments: ADDLOCAL=Cow + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_msi_argument_check + check_mode: yes + +- name: get result of moo file after install network msi with arguments (check mode) + win_stat: + path: '%ProgramFiles(x86)%\Bovine University\moo.exe' + register: install_network_msi_argument_moo_check + +- name: get result of cow file after install network msi with arguments (check mode) + win_stat: + path: '%ProgramFiles(x86)%\Bovine University\cow.exe' + register: install_network_msi_argument_cow_check + +- name: assert install network msi with arguments (check mode) + assert: + that: + - install_network_msi_argument_check|changed + - install_network_msi_argument_check.reboot_required == False + - install_network_msi_argument_moo_check.stat.exists == False + - install_network_msi_argument_cow_check.stat.exists == False + +- name: install network msi with arguments + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: present + arguments: ADDLOCAL=Cow + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_msi_argument + +- name: get result of moo file after install network msi with arguments + win_stat: + path: '%ProgramFiles(x86)%\Bovine University\moo.exe' + register: install_network_msi_argument_moo + +- name: get result of cow file after install network msi with arguments + win_stat: + path: '%ProgramFiles(x86)%\Bovine University\cow.exe' + register: install_network_msi_argument_cow + +- name: assert install network msi with arguments + assert: + that: + - install_network_msi_argument|changed + - install_network_msi_argument.reboot_required == False + - install_network_msi_argument.exit_code == 0 + - install_network_msi_argument_moo.stat.exists == False + - install_network_msi_argument_cow.stat.exists == True + +- name: install network msi with arguments (idempotent) + win_package: + path: '{{test_win_package_network_path}}\good.msi' + product_id: '{{test_win_package_good_id}}' + state: present + arguments: ADDLOCAL=Cow + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_msi_argument_again + +- name: assert install network msi with arguments (idempotent) + assert: + that: + - not install_network_msi_argument_again|changed + +- name: uninstall msi after test + win_package: + product_id: '{{test_win_package_good_id}}' + state: absent + +- name: install network exe (check mode) + win_package: + path: '{{test_win_package_network_path}}\7z.exe' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_exe_check + check_mode: yes + +- name: get result of install network exe (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: install_network_exe_actual_check + +- name: assert install network exe (check mode) + assert: + that: + - install_network_exe_check|changed + - install_network_exe_check.reboot_required == False + - install_network_exe_actual_check.exists == False + +- name: install network exe + win_package: + path: '{{test_win_package_network_path}}\7z.exe' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_exe + +- name: get result of install network exe + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: install_network_exe_actual + +- name: assert install network exe + assert: + that: + - install_network_exe|changed + - install_network_exe.reboot_required == False + - install_network_exe.exit_code == 0 + - install_network_exe_actual.exists == True + +- name: install network exe (idempotent) + win_package: + path: '{{test_win_package_network_path}}\7z.exe' + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: present + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: install_network_exe_idempotent + +- name: assert install network exe (idempotent) + assert: + that: + - not install_network_exe_idempotent|changed + +- name: uninstall network exe (check mode) + win_package: + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: uninstall_network_exe_check + check_mode: yes + +- name: get result of uninstall network exe (check mode) + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: uninstall_network_exe_actual_check + +- name: assert uninstall network exe (check mode) + assert: + that: + - uninstall_network_exe_check|changed + - uninstall_network_exe_check.reboot_required == False + - uninstall_network_exe_actual_check.exists == True + +- name: uninstall network exe + win_package: + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: uninstall_network_exe + +- name: get result of uninstall network exe + win_reg_stat: + path: HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{{test_win_package_exe_id}} + register: uninstall_network_exe_actual + +- name: assert uninstall network exe + assert: + that: + - uninstall_network_exe|changed + - uninstall_network_exe.reboot_required == False + - uninstall_network_exe.exit_code == 0 + - uninstall_network_exe_actual.exists == False + +- name: uninstall network exe (idempotent) + win_package: + product_id: '{{test_win_package_exe_id}}' + arguments: /S + state: absent + user_name: '{{test_win_package_network_username|default(omit)}}' + user_password: '{{test_win_package_network_password|default(omit)}}' + register: uninstall_network_exe_idempotent + +- name: assert uninstall network exe (idempotent) + assert: + that: + - not uninstall_network_exe_idempotent|changed \ No newline at end of file