From 45fe29b248439c13b490bebc51c801310f8d08cb Mon Sep 17 00:00:00 2001 From: Pepe Barbe Date: Sun, 7 Jun 2015 13:18:33 -0500 Subject: [PATCH] Refactor win_chocolatey module * Refactor code to be more robust. Run main logic inside a try {} catch {} block. If there is any error, bail out and log all the command output automatically. * Rely on error code generated by chocolatey instead of scraping text output to determine success/failure. * Add support for unattended installs: (`-y` flag is a requirement by chocolatey) * Before (un)installing, check existence of files. * Use functions to abstract logic * The great rewrite of 0.9.9, the `choco` interface has changed, check if chocolatey is installed and an older version. If so upgrade to latest. * Allow upgrading packages that are already installed * Use verbose logging for chocolate actions * Adding functionality to specify a source for a chocolatey repository. (@smadam813) * Removing pre-determined sources and adding specified source url in it's place. (@smadam813) Contains contributions from: * Adam Keech (@smadam813) --- .../modules/extras/windows/win_chocolatey.ps1 | 401 ++++++++++-------- .../modules/extras/windows/win_chocolatey.py | 45 +- 2 files changed, 250 insertions(+), 196 deletions(-) diff --git a/lib/ansible/modules/extras/windows/win_chocolatey.ps1 b/lib/ansible/modules/extras/windows/win_chocolatey.ps1 index de42434da76..4a033d23157 100644 --- a/lib/ansible/modules/extras/windows/win_chocolatey.ps1 +++ b/lib/ansible/modules/extras/windows/win_chocolatey.ps1 @@ -16,25 +16,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +$ErrorActionPreference = "Stop" + # WANT_JSON # POWERSHELL_COMMON -function Write-Log -{ - param - ( - [parameter(mandatory=$false)] - [System.String] - $message - ) - - $date = get-date -format 'yyyy-MM-dd hh:mm:ss.zz' - - Write-Host "$date | $message" - - Out-File -InputObject "$date $message" -FilePath $global:LoggingFile -Append -} - $params = Parse-Args $args; $result = New-Object PSObject; Set-Attr $result "changed" $false; @@ -48,14 +34,6 @@ Else Fail-Json $result "missing required argument: name" } -if(($params.logPath).length -gt 0) -{ - $global:LoggingFile = $params.logPath -} -else -{ - $global:LoggingFile = "c:\ansible-playbook.log" -} If ($params.force) { $force = $params.force | ConvertTo-Bool @@ -65,6 +43,15 @@ Else $force = $false } +If ($params.upgrade) +{ + $upgrade = $params.upgrade | ConvertTo-Bool +} +Else +{ + $upgrade = $false +} + If ($params.version) { $version = $params.version @@ -74,6 +61,15 @@ Else $version = $null } +If ($params.source) +{ + $source = $params.source.ToString().ToLower() +} +Else +{ + $source = $null +} + If ($params.showlog) { $showlog = $params.showlog | ConvertTo-Bool @@ -96,157 +92,230 @@ Else $state = "present" } -$ChocoAlreadyInstalled = get-command choco -ErrorAction 0 -if ($ChocoAlreadyInstalled -eq $null) +Function Chocolatey-Install-Upgrade { - #We need to install chocolatey - $install_choco_result = iex ((new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1")) - $result.changed = $true - $executable = "C:\ProgramData\chocolatey\bin\choco.exe" -} -Else -{ - $executable = "choco.exe" -} + [CmdletBinding()] -If ($params.source) -{ - $source = $params.source.ToString().ToLower() - If (($source -ne "chocolatey") -and ($source -ne "webpi") -and ($source -ne "windowsfeatures") -and ($source -ne "ruby") -and (!$source.startsWith("http://", "CurrentCultureIgnoreCase")) -and (!$source.startsWith("https://", "CurrentCultureIgnoreCase"))) - { - Fail-Json $result "source is $source - must be one of chocolatey, ruby, webpi, windowsfeatures or a custom source url." - } -} -Elseif (!$params.source) -{ - $source = "chocolatey" -} + param() -if ($source -eq "webpi") -{ - # check whether 'webpi' installation source is available; if it isn't, install it - $webpi_check_cmd = "$executable list webpicmd -localonly" - $webpi_check_result = invoke-expression $webpi_check_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_check_cmd" $webpi_check_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_check_log" $webpi_check_result - if ( - ( - ($webpi_check_result.GetType().Name -eq "String") -and - ($webpi_check_result -match "No packages found") - ) -or - ($webpi_check_result -contains "No packages found.") - ) - { - #lessmsi is a webpicmd dependency, but dependency resolution fails unless it's installed separately - $lessmsi_install_cmd = "$executable install lessmsi" - $lessmsi_install_result = invoke-expression $lessmsi_install_cmd - Set-Attr $result "chocolatey_bootstrap_lessmsi_install_cmd" $lessmsi_install_cmd - Set-Attr $result "chocolatey_bootstrap_lessmsi_install_log" $lessmsi_install_result - - $webpi_install_cmd = "$executable install webpicmd" - $webpi_install_result = invoke-expression $webpi_install_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_install_cmd" $webpi_install_cmd - Set-Attr $result "chocolatey_bootstrap_webpi_install_log" $webpi_install_result - - if (($webpi_install_result | select-string "already installed").length -gt 0) - { - #no change - } - elseif (($webpi_install_result | select-string "webpicmd has finished successfully").length -gt 0) - { - $result.changed = $true - } - Else - { - Fail-Json $result "WebPI install error: $webpi_install_result" - } - } -} -$expression = $executable -if ($state -eq "present") -{ - $expression += " install $package" -} -Elseif ($state -eq "absent") -{ - $expression += " uninstall $package" -} -if ($force) -{ - if ($state -eq "present") - { - $expression += " -force" - } -} -if ($version) -{ - $expression += " -version $version" -} -if ($source -eq "chocolatey") -{ - $expression += " -source https://chocolatey.org/api/v2/" -} -elseif (($source -eq "windowsfeatures") -or ($source -eq "webpi") -or ($source -eq "ruby")) -{ - $expression += " -source $source" -} -elseif(($source -ne $Null) -and ($source -ne "")) -{ - $expression += " -source $source" -} - -Set-Attr $result "chocolatey command" $expression -$op_result = invoke-expression $expression -if ($state -eq "present") -{ - if ( - (($op_result | select-string "already installed").length -gt 0) -or - # webpi has different text output, and that doesn't include the package name but instead the human-friendly name - (($op_result | select-string "No products to be installed").length -gt 0) - ) - { - #no change - } - elseif ( - (($op_result | select-string "has finished successfully").length -gt 0) -or - # webpi has different text output, and that doesn't include the package name but instead the human-friendly name - (($op_result | select-string "Install of Products: SUCCESS").length -gt 0) -or - (($op_result | select-string "gem installed").length -gt 0) -or - (($op_result | select-string "gems installed").length -gt 0) - ) - { - $result.changed = $true - } - Else - { - Fail-Json $result "Install error: $op_result" - } -} -Elseif ($state -eq "absent") -{ - $op_result = invoke-expression "$executable uninstall $package" - # HACK: Misleading - 'Uninstalling from folder' appears in output even when package is not installed, hence order of checks this way - if ( - (($op_result | select-string "not installed").length -gt 0) -or - (($op_result | select-string "Cannot find path").length -gt 0) - ) - { - #no change - } - elseif (($op_result | select-string "Uninstalling from folder").length -gt 0) + $ChocoAlreadyInstalled = get-command choco -ErrorAction 0 + if ($ChocoAlreadyInstalled -eq $null) { + #We need to install chocolatey + iex ((new-object net.webclient).DownloadString("https://chocolatey.org/install.ps1")) $result.changed = $true + $script:executable = "C:\ProgramData\chocolatey\bin\choco.exe" } else { - Fail-Json $result "Uninstall error: $op_result" + $script:executable = "choco.exe" + + if ((choco --version) -lt '0.9.9') + { + Choco-Upgrade chocolatey + } } } -if ($showlog) -{ - Set-Attr $result "chocolatey_log" $op_result -} -Set-Attr $result "chocolatey_success" "true" -Exit-Json $result; +Function Choco-IsInstalled +{ + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package + ) + + $cmd = "$executable list --local-only $package" + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + + Throw "Error checking installation status for $package" + } + + If ("$results" -match " $package .* (\d+) packages installed.") + { + return $matches[1] -gt 0 + } + + $false +} + +Function Choco-Upgrade +{ + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [string]$source, + [Parameter(Mandatory=$false, Position=4)] + [bool]$force + ) + + if (-not (Choco-IsInstalled $package)) + { + throw "$package is not installed, you cannot upgrade" + } + + $cmd = "$executable upgrade -dv -y $package" + + if ($version) + { + $cmd += " -version $version" + } + + if ($source) + { + $cmd += " -source $source" + } + + if ($force) + { + $cmd += " -force" + } + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error installing $package" + } + + if ("$results" -match ' upgraded (\d+)/\d+ package\(s\)\. ') + { + if ($matches[1] -gt 0) + { + $result.changed = $true + } + } +} + +Function Choco-Install +{ + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [string]$source, + [Parameter(Mandatory=$false, Position=4)] + [bool]$force, + [Parameter(Mandatory=$false, Position=5)] + [bool]$upgrade + ) + + if (Choco-IsInstalled $package) + { + if ($upgrade) + { + Choco-Upgrade -package $package -version $version -source $source -force $force + } + + return + } + + $cmd = "$executable install -dv -y $package" + + if ($version) + { + $cmd += " -version $version" + } + + if ($source) + { + $cmd += " -source $source" + } + + if ($force) + { + $cmd += " -force" + } + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error installing $package" + } + + $result.changed = $true +} + +Function Choco-Uninstall +{ + [CmdletBinding()] + + param( + [Parameter(Mandatory=$true, Position=1)] + [string]$package, + [Parameter(Mandatory=$false, Position=2)] + [string]$version, + [Parameter(Mandatory=$false, Position=3)] + [bool]$force + ) + + if (-not (Choco-IsInstalled $package)) + { + return + } + + $cmd = "$executable uninstall -dv -y $package" + + if ($version) + { + $cmd += " -version $version" + } + + if ($force) + { + $cmd += " -force" + } + + $results = invoke-expression $cmd + + if ($LastExitCode -ne 0) + { + Set-Attr $result "choco_error_cmd" $cmd + Set-Attr $result "choco_error_log" "$results" + Throw "Error uninstalling $package" + } + + $result.changed = $true +} +Try +{ + Chocolatey-Install-Upgrade + + if ($state -eq "present") + { + Choco-Install -package $package -version $version -source $source ` + -force $force -upgrade $upgrade + } + else + { + Choco-Uninstall -package $package -version $version -force $force + } + + Exit-Json $result; +} +Catch +{ + Fail-Json $result $_.Exception.Message +} + diff --git a/lib/ansible/modules/extras/windows/win_chocolatey.py b/lib/ansible/modules/extras/windows/win_chocolatey.py index 260dbd867f6..dc0b415ac87 100644 --- a/lib/ansible/modules/extras/windows/win_chocolatey.py +++ b/lib/ansible/modules/extras/windows/win_chocolatey.py @@ -53,6 +53,15 @@ options: - no default: no aliases: [] + upgrade: + description: + - If package is already installed it, try to upgrade to the latest version or to the specified version + required: false + choices: + - yes + - no + default: no + aliases: [] version: description: - Specific version of the package to be installed @@ -60,35 +69,13 @@ options: required: false default: null aliases: [] - showlog: - description: - - Outputs the chocolatey log inside a chocolatey_log property. - required: false - choices: - - yes - - no - default: no - aliases: [] source: description: - - Which source to install from + - Specify source rather than using default chocolatey repository require: false - choices: - - chocolatey - - ruby - - webpi - - windowsfeatures - default: chocolatey + default: null aliases: [] - logPath: - description: - - Where to log command output to - require: false - default: c:\\ansible-playbook.log - aliases: [] -author: - - '"Trond Hindenes (@trondhindenes)" ' - - '"Peter Mounce (@petemounce)" ' +author: Trond Hindenes, Peter Mounce, Pepe Barbe, Adam Keech ''' # TODO: @@ -111,10 +98,8 @@ EXAMPLES = ''' name: git state: absent - # Install Application Request Routing v3 from webpi - # Logically, this requires that you install IIS first (see win_feature) - # To find a list of packages available via webpi source, `choco list -source webpi` + # Install git from specified repository win_chocolatey: - name: ARRv3 - source: webpi + name: git + source: https://someserver/api/v2/ '''