From 933d36b25f40400acd864a24894b0c307cb43e67 Mon Sep 17 00:00:00 2001 From: Jordan Borean Date: Wed, 18 Jul 2018 10:36:21 +1000 Subject: [PATCH] win_chocolatey_source: add new module to manage Chocolatey sources (#42790) * win_chocolatey_source: add new module to manage Chocolatey sources * Added examples and fix diff run * Minor fixes from review * When editing a source, recreate with the explicit options instead of using the existing source * Fixed up copyright header in PowerShell file --- .../modules/windows/win_chocolatey_source.ps1 | 313 ++++++++++++++++++ .../modules/windows/win_chocolatey_source.py | 121 +++++++ .../targets/win_chocolatey_source/aliases | 1 + .../win_chocolatey_source/defaults/main.yml | 3 + .../win_chocolatey_source/tasks/main.yml | 31 ++ .../win_chocolatey_source/tasks/tests.yml | 243 ++++++++++++++ 6 files changed, 712 insertions(+) create mode 100644 lib/ansible/modules/windows/win_chocolatey_source.ps1 create mode 100644 lib/ansible/modules/windows/win_chocolatey_source.py create mode 100644 test/integration/targets/win_chocolatey_source/aliases create mode 100644 test/integration/targets/win_chocolatey_source/defaults/main.yml create mode 100644 test/integration/targets/win_chocolatey_source/tasks/main.yml create mode 100644 test/integration/targets/win_chocolatey_source/tasks/tests.yml diff --git a/lib/ansible/modules/windows/win_chocolatey_source.ps1 b/lib/ansible/modules/windows/win_chocolatey_source.ps1 new file mode 100644 index 00000000000..4795373ecab --- /dev/null +++ b/lib/ansible/modules/windows/win_chocolatey_source.ps1 @@ -0,0 +1,313 @@ +#!powershell + +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +#Requires -Module Ansible.ModuleUtils.ArgvParser +#Requires -Module Ansible.ModuleUtils.CommandUtil +#Requires -Module Ansible.ModuleUtils.Legacy + +$ErrorActionPreference = "Stop" + +$params = Parse-Args -arguments $args -supports_check_mode $true +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$diff = Get-AnsibleParam -obj $params -name "_ansible_diff" -type "bool" -default $false + +$name = Get-AnsibleParam -obj $params -name "name" -type "str" -failifempty $true +$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "present" -validateset "absent", "disabled", "present" + +$admin_only = Get-AnsibleParam -obj $params -name "admin_only" -type "bool" +$allow_self_service = Get-AnsibleParam -obj $params -name "allow_self_service" -type "bool" +$bypass_proxy = Get-AnsibleParam -obj $params -name "bypass_proxy" -type "bool" +$certificate = Get-AnsibleParam -obj $params -name "certificate" -type "str" +$certificate_password = Get-AnsibleParam -obj $params -name "certificate_password" -type "str" +$priority = Get-AnsibleParam -obj $params -name "priority" -type "int" +$source = Get-AnsibleParam -obj $params -name "source" -type "str" -failifempty ($state -ne "absent") +$source_username = Get-AnsibleParam -obj $params -name "source_username" -type "str" +$source_password = Get-AnsibleParam -obj $params -name "source_password" -type "str" -failifempty ($null -ne $source_username) +$update_password = Get-AnsibleParam -obj $params -name "update_password" -type "str" -default "always" -validateset "always", "on_create" + +$result = @{ + changed = $false +} +if ($diff) { + $result.diff = @{ + before = @{} + after = @{} + } +} + +Function Get-ChocolateySources { + param($choco_app) + + $choco_config_path = "$(Split-Path -Path (Split-Path -Path $choco_app.Path))\config\chocolatey.config" + if (-not (Test-Path -LiteralPath $choco_config_path)) { + Fail-Json -obj $result -message "Expecting Chocolatey config file to exist at '$choco_config_path'" + } + + # would prefer to enumerate the existing sources with an actual API but the + # only stable interface is choco.exe source list and that does not output + # the sources in an easily parsable list. Using -r will split each entry by + # | like a psv but does not quote values that have a | already in it making + # it inadequete for our tasks. Instead we will parse the chocolatey.config + # file and get the values from there + try { + [xml]$choco_config = Get-Content -Path $choco_config_path + } catch { + Fail-Json -obj $result -message "Failed to parse Chocolatey config file at '$choco_config_path': $($_.Exception.Message)" + } + + $sources = [System.Collections.ArrayList]@() + foreach ($xml_source in $choco_config.chocolatey.sources.GetEnumerator()) { + $source_username = $xml_source.Attributes.GetNamedItem("user") + if ($null -ne $source_username) { + $source_username = $source_username.Value + } + + # 0.9.9.9+ + $priority = $xml_source.Attributes.GetNamedItem("priority") + if ($null -ne $priority) { + $priority = [int]$priority.Value + } + + # 0.9.10+ + $certificate = $xml_source.Attributes.GetNamedItem("certificate") + if ($null -ne $certificate) { + $certificate = $certificate.Value + } + + # 0.10.4+ + $bypass_proxy = $xml_source.Attributes.GetNamedItem("bypassProxy") + if ($null -ne $bypass_proxy) { + $bypass_proxy = [System.Convert]::ToBoolean($bypass_proxy.Value) + } + $allow_self_service = $xml_source.Attributes.GetNamedItem("selfService") + if ($null -ne $allow_self_service) { + $allow_self_service = [System.Convert]::ToBoolean($allow_self_service.Value) + } + + # 0.10.8+ + $admin_only = $xml_source.Attributes.GetNamedItem("adminOnly") + if ($null -ne $admin_only) { + $admin_only = [System.Convert]::ToBoolean($admin_only.Value) + } + + $source_info = @{ + name = $xml_source.id + source = $xml_source.value + disabled = [System.Convert]::ToBoolean($xml_source.disabled) + source_username = $source_username + priority = $priority + certificate = $certificate + bypass_proxy = $bypass_proxy + allow_self_service = $allow_self_service + admin_only = $admin_only + } + $sources.Add($source_info) > $null + } + return ,$sources +} + +Function New-ChocolateySource { + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingUserNameAndPassWordParams", "", Justification="We need to use the plaintext pass in the cmdline, also using a SecureString here doesn't make sense considering the source is not secure")] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingPlainTextForPassword", "", Justification="See above")] + param( + $choco_app, + $name, + $source, + $source_username, + $source_password, + $certificate, + $certificate_password, + $priority, + $bypass_proxy, + $allow_self_service, + $admin_only + ) + # build the base arguments + $arguments = [System.Collections.ArrayList]@($choco_app.Path, + "source", "add", "--name", $name, "--source", $source + ) + + # add optional arguments from user input + if ($null -ne $source_username) { + $arguments.Add("--user") > $null + $arguments.Add($source_username) > $null + $arguments.Add("--password") > $null + $arguments.Add($source_password) > $null + } + if ($null -ne $certificate) { + $arguments.Add("--cert") > $null + $arguments.Add($certificate) > $null + } + if ($null -ne $certificate_password) { + $arguments.Add("--certpassword") > $null + $arguments.Add($certificate_password) > $null + } + if ($null -ne $priority) { + $arguments.Add("--priority") > $null + $arguments.Add($priority) > $null + } else { + $priority = 0 + } + if ($bypass_proxy -eq $true) { + $arguments.Add("--bypass-proxy") > $null + } else { + $bypass_proxy = $false + } + if ($allow_self_service -eq $true) { + $arguments.Add("--allow-self-service") > $null + } else { + $allow_self_service = $false + } + if ($admin_only -eq $true) { + $arguments.Add("--admin-only") > $null + } else { + $admin_only = $false + } + + if ($check_mode) { + $arguments.Add("--what-if") > $null + } + + $command = Argv-ToString -arguments $arguments + $res = Run-Command -command $command + if ($res.rc -ne 0) { + Fail-Json -obj $result -message "Failed to add Chocolatey source '$name': $($res.stderr)" + } + + $source_info = @{ + name = $name + source = $source + disabled = $false + source_username = $source_username + priority = $priority + certificate = $certificate + bypass_proxy = $bypass_proxy + allow_self_service = $allow_self_service + admin_only = $admin_only + } + return ,$source_info +} + +Function Remove-ChocolateySource { + param( + $choco_app, + $name + ) + $arguments = [System.Collections.ArrayList]@($choco_app.Path, "source", "remove", "--name", $name) + if ($check_mode) { + $arguments.Add("--what-if") > $null + } + $command = Argv-ToString -arguments $arguments + $res = Run-Command -command $command + if ($res.rc -ne 0) { + Fail-Json -obj $result -message "Failed to remove Chocolatey source '$name': $($_.res.stderr)" + } +} + +$choco_app = Get-Command -Name choco.exe -CommandType Application -ErrorAction SilentlyContinue +if (-not $choco_app) { + Fail-Json -obj $result -message "Failed to find Chocolatey installation, make sure choco.exe is in the PATH env value" +} +$actual_sources = Get-ChocolateySources -choco_app $choco_app +$actual_source = $actual_sources | Where-Object { $_.name -eq $name } +if ($diff) { + if ($null -ne $actual_source) { + $before = $actual_source.Clone() + } else { + $before = @{} + } + $result.diff.before = $before +} + +if ($state -eq "absent" -and $null -ne $actual_source) { + Remove-ChocolateySource -choco_app $choco_app -name $name + $result.changed = $true +} elseif ($state -in ("disabled", "present")) { + $change = $false + if ($null -eq $actual_source) { + $change = $true + } else { + if ($source -ne $actual_source.source) { + $change = $true + } + if ($null -ne $source_username -and $source_username -ne $actual_source.source_username) { + $change = $true + } + if ($null -ne $source_password -and $update_password -eq "always") { + $change = $true + } + if ($null -ne $certificate -and $certificate -ne $actual_source.certificate) { + $change = $true + } + if ($null -ne $certificate_password -and $update_password -eq "always") { + $change = $true + } + if ($null -ne $priority -and $priority -ne $actual_source.priority) { + $change = $true + } + if ($null -ne $bypass_proxy -and $bypass_proxy -ne $actual_source.bypass_proxy) { + $change = $true + } + if ($null -ne $allow_self_service -and $allow_self_service -ne $actual_source.allow_self_service) { + $change = $true + } + if ($null -ne $admin_only -and $admin_only -ne $actual_source.admin_only) { + $change = $true + } + + if ($change) { + Remove-ChocolateySource -choco_app $choco_app -name $name + $result.changed = $true + } + } + + if ($change) { + $actual_source = New-ChocolateySource -choco_app $choco_app -name $name -source $source ` + -source_username $source_username -source_password $source_password ` + -certificate $certificate -certificate_password $certificate_password ` + -priority $priority -bypass_proxy $bypass_proxy -allow_self_service $allow_self_service ` + -admin_only $admin_only + $result.changed = $true + } + + # enable/disable the source if necessary + if ($state -ne "disabled" -and $actual_source.disabled) { + $arguments = [System.Collections.ArrayList]@($choco_app.Path, "source", "enable", "--name", $name) + if ($check_mode) { + $arguments.Add("--what-if") > $null + } + $command = Argv-ToString -arguments $arguments + $res = Run-Command -command $command + if ($res.rc -ne 0) { + Fail-Json -obj $result -message "Failed to enable Chocolatey source '$name': $($res.stderr)" + } + $actual_source.disabled = $false + $result.changed = $true + } elseif ($state -eq "disabled" -and (-not $actual_source.disabled)) { + $arguments = [System.Collections.ArrayList]@($choco_app.Path, "source", "disable", "--name", $name) + if ($check_mode) { + $arguments.Add("--what-if") > $null + } + $command = Argv-ToString -arguments $arguments + $res = Run-Command -command $command + if ($res.rc -ne 0) { + Fail-Json -obj $result -message "Failed to disable Chocolatey source '$name': $($res.stderr)" + } + $actual_source.disabled = $true + $result.changed = $true + } + + if ($diff) { + $after = $actual_source + $result.diff.after = $after + } +} + +# finally remove the diff if there was no change +if (-not $result.changed -and $diff) { + $result.diff = @{} +} + +Exit-Json -obj $result diff --git a/lib/ansible/modules/windows/win_chocolatey_source.py b/lib/ansible/modules/windows/win_chocolatey_source.py new file mode 100644 index 00000000000..7ed805ff5fe --- /dev/null +++ b/lib/ansible/modules/windows/win_chocolatey_source.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2018, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_chocolatey_source +version_added: '2.7' +short_description: Manages Chocolatey sources +description: +- Used to managed Chocolatey sources configured on the client. +- Requires Chocolatey to be already installed on the remote host. +options: + admin_only: + description: + - Makes the source visible to Administrators only. + - Requires Chocolatey >= 0.10.8. + - When creating a new source, this defaults to C(False). + type: bool + allow_self_service: + description: + - Allow the source to be used with self-service + - Requires Chocolatey >= 0.10.4. + - When creating a new source, this defaults to C(False). + type: bool + bypass_proxy: + description: + - Bypass the proxy when using this source. + - Requires Chocolatey >= 0.10.4. + - When creating a new source, this defaults to C(False). + type: bool + certificate: + description: + - The path to a .pfx file to use for X509 authenticated feeds. + - Requires Chocolatey >= 0.9.10. + certificate_password: + description: + - The password for I(certificate) if required. + - Requires Chocolatey >= 0.9.10. + name: + description: + - The name of the source to configure. + required: yes + priority: + description: + - The priority order of this source compared to other sources, lower is + better. + - All priorities above C(0) will be evaluated first, then zero-based values + will be evaluated in config file order. + - Requires Chocolatey >= 0.9.9.9. + - When creating a new source, this defaults to C(0). + type: int + source: + description: + - The file/folder/url of the source. + - Required when I(state) is C(present) or C(disabled). + source_username: + description: + - The username used to access I(source). + source_password: + description: + - The password for I(source_username). + - Required if I(source_username) is set. + state: + description: + - When C(absent), will remove the source. + - When C(disabled), will ensure the source exists but is disabled. + - When C(present), will ensure the source exists and is enabled. + choices: + - absent + - disabled + - present + default: present + update_password: + description: + - When C(always), the module will always set the password and report a + change if I(certificate_password) or I(source_password) is set. + - When C(on_create), the module will only set the password if the source + is being created. + choices: + - always + - on_create + default: always +author: +- Jordan Borean (@jborean93) +''' + +EXAMPLES = r''' +- name: remove the default public source + win_chocolatey_source: + name: chocolatey + state: absent + +- name: add new internal source + win_chocolatey_source: + name: internal repo + state: present + source: http://chocolatey-server/chocolatey + +- name: create HTTP source with credentials + win_chocolatey_source: + name: internal repo + state: present + source: https://chocolatey-server/chocolatey + source_username: username + source_password: password + +- name: disable Chocolatey source + win_chocolatey_source: + name: chocoaltey + state: disabled +''' + +RETURN = r''' +''' diff --git a/test/integration/targets/win_chocolatey_source/aliases b/test/integration/targets/win_chocolatey_source/aliases new file mode 100644 index 00000000000..10e03fc2bf7 --- /dev/null +++ b/test/integration/targets/win_chocolatey_source/aliases @@ -0,0 +1 @@ +windows/ci/group1 diff --git a/test/integration/targets/win_chocolatey_source/defaults/main.yml b/test/integration/targets/win_chocolatey_source/defaults/main.yml new file mode 100644 index 00000000000..0d8c67825e0 --- /dev/null +++ b/test/integration/targets/win_chocolatey_source/defaults/main.yml @@ -0,0 +1,3 @@ +--- +# use some weird chars to test out the parser +test_chocolatey_name: test'|"source 123^ diff --git a/test/integration/targets/win_chocolatey_source/tasks/main.yml b/test/integration/targets/win_chocolatey_source/tasks/main.yml new file mode 100644 index 00000000000..0b57cc55548 --- /dev/null +++ b/test/integration/targets/win_chocolatey_source/tasks/main.yml @@ -0,0 +1,31 @@ +--- +- name: ensure Chocolatey is installed + win_chocolatey: + name: chocolatey + state: present + +- name: remove original Chocolatey source at the start of the test + win_chocolatey_source: + name: Chocolatey + state: absent + +- name: ensure test Chocolatey source is removed + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + state: absent + +- block: + - name: run tests + include_tasks: tests.yml + + always: + - name: ensure original Chocolatey source is re-added + win_chocolatey_source: + name: Chocolatey + source: https://chocolatey.org/api/v2/ + state: present + + - name: remove test Chocolatey source + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + state: absent diff --git a/test/integration/targets/win_chocolatey_source/tasks/tests.yml b/test/integration/targets/win_chocolatey_source/tasks/tests.yml new file mode 100644 index 00000000000..0a574c4e945 --- /dev/null +++ b/test/integration/targets/win_chocolatey_source/tasks/tests.yml @@ -0,0 +1,243 @@ +--- +- name: create source (check mode) + win_chocolatey_source: + name: chocolatey + source: https://chocolatey.org/api/v2/ + state: present + register: create_check + check_mode: yes + +- name: check if source exists (check mode) + win_command: choco.exe source list -r + register: create_actual_check + +- name: assert create source (check mode) + assert: + that: + - create_check is changed + - create_actual_check.stdout_lines == [] + +- name: create source + win_chocolatey_source: + name: chocolatey + source: https://chocolatey.org/api/v2/ + state: present + register: create + +- name: check if source exists + win_command: choco.exe source list -r + register: create_actual + +- name: assert create source + assert: + that: + - create is changed + - create_actual.stdout_lines == ["chocolatey|https://chocolatey.org/api/v2/|False|||0|False|False|False"] + +- name: create source (idempotent) + win_chocolatey_source: + name: chocolatey + source: https://chocolatey.org/api/v2/ + state: present + register: create_again + +- name: assert create source (idempotent) + assert: + that: + - not create_again is changed + +- name: remove source (check mode) + win_chocolatey_source: + name: chocolatey + state: absent + register: remove_check + check_mode: yes + +- name: check if source is removed (check mode) + win_command: choco.exe source list -r + register: remove_actual_check + +- name: assert remove source (check mode) + assert: + that: + - remove_check is changed + - remove_actual_check.stdout == create_actual.stdout + +- name: remove source + win_chocolatey_source: + name: chocolatey + state: absent + register: remove + +- name: check if source is removed + win_command: choco.exe source list -r + register: remove_actual + +- name: assert remove source + assert: + that: + - remove is changed + - remove_actual.stdout_lines == [] + +- name: remove source (idempotent) + win_chocolatey_source: + name: chocolatey + state: absent + register: remove_again + +- name: assert remove source (idempotent) + assert: + that: + - not remove_again is changed + +- name: create a disabled service (check mode) + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + source: C:\chocolatey repos + source_username: username + source_password: password + certificate: C:\cert.pfx + certificate_password: password + bypass_proxy: yes + priority: 1 + state: disabled + register: create_special_check + check_mode: yes + +- name: check if source is created (check mode) + win_command: choco.exe source list -r + register: create_special_actual_check + +- name: assert create a disabled service (check mode) + assert: + that: + - create_special_check is changed + - create_special_actual_check.stdout_lines == [] + +- name: create a disabled service + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + source: C:\chocolatey repos + source_username: username + source_password: password + certificate: C:\cert.pfx + certificate_password: password + bypass_proxy: yes + priority: 1 + state: disabled + register: create_special + +- name: check if source is created + win_command: choco.exe source list -r + register: create_special_actual + +- name: assert create a disabled service + assert: + that: + - create_special is changed + - create_special_actual.stdout_lines == ["test'|\"source 123^|C:\\chocolatey repos|True|username|C:\\cert.pfx|1|True|False|False"] + +- name: create a disabled service pass always update + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + source: C:\chocolatey repos + source_username: username + source_password: password + certificate: C:\cert.pfx + certificate_password: password + bypass_proxy: yes + priority: 1 + state: disabled + register: create_special_pass_always + +- name: assert create a disabled service pass always update + assert: + that: + - create_special_pass_always is changed + +- name: create a disabled service (idempotent) + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + source: C:\chocolatey repos + source_username: username + source_password: password + certificate: C:\cert.pfx + certificate_password: password + bypass_proxy: yes + priority: 1 + state: disabled + update_password: on_create + register: create_special_again + +- name: assert create a disabled service (idempotent) + assert: + that: + - not create_special_again is changed + +- name: edit an existing source (check mode) + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + source: C:\chocolatey repos2 + source_username: username2 + source_password: password2 + certificate: C:\cert2.pfx + priority: '5' + state: present + update_password: on_create + admin_only: yes + allow_self_service: yes + register: modify_source_check + check_mode: yes + +- name: check if source is changed (check mode) + win_command: choco.exe source list -r + register: modify_source_check_actual + +- name: assert edit an existing source (check mode) + assert: + that: + - modify_source_check is changed + - modify_source_check_actual.stdout_lines == create_special_actual.stdout_lines + +- name: edit an existing source + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + source: C:\chocolatey repos2 + source_username: username2 + source_password: password2 + certificate: C:\cert2.pfx + priority: '5' + state: present + update_password: on_create + admin_only: yes + allow_self_service: yes + register: modify_source + +- name: check if source is changed + win_command: choco.exe source list -r + register: modify_source_actual + +- name: assert edit an existing source + assert: + that: + - modify_source is changed + - modify_source_actual.stdout_lines == ["test'|\"source 123^|C:\\chocolatey repos2|False|username2|C:\\cert2.pfx|5|False|True|True"] + +- name: edit an existing source (idempotent) + win_chocolatey_source: + name: '{{ test_chocolatey_name }}' + source: C:\chocolatey repos2 + source_username: username2 + source_password: password2 + certificate: C:\cert2.pfx + priority: '5' + state: present + update_password: on_create + admin_only: yes + allow_self_service: yes + register: modify_source_again + +- name: assert edit an existing source (idempotent) + assert: + that: + - not modify_source_again is changed