summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatt Davis <nitzmahone@users.noreply.github.com>2017-01-17 20:21:04 -0800
committerGitHub <noreply@github.com>2017-01-17 20:21:04 -0800
commitb2a16379c8628a9d0c7dacfd3ec6de3e358318d7 (patch)
treec7b23f33f27e27a57a0e29c1878cc9b6e936112a
parent712be24a74df92de1d806bafb2bd5e30455a12a5 (diff)
downloadansible-b2a16379c8628a9d0c7dacfd3ec6de3e358318d7.tar.gz
new module: win_path (#20073)
-rw-r--r--CHANGELOG.md1
-rw-r--r--lib/ansible/modules/windows/win_environment.py5
-rw-r--r--lib/ansible/modules/windows/win_path.ps1158
-rw-r--r--lib/ansible/modules/windows/win_path.py87
-rw-r--r--test/integration/targets/win_path/aliases1
-rw-r--r--test/integration/targets/win_path/tasks/main.yml183
-rw-r--r--test/integration/test_win_group2.yml1
7 files changed, 435 insertions, 1 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fec47d3e5b..7dcfc372e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -83,6 +83,7 @@ Ansible Changes By Release
- windows:
* win_say
* win_shortcut
+ * win_path
- openstack
* os_quota
- zfs:
diff --git a/lib/ansible/modules/windows/win_environment.py b/lib/ansible/modules/windows/win_environment.py
index f66771a758..484d9e6987 100644
--- a/lib/ansible/modules/windows/win_environment.py
+++ b/lib/ansible/modules/windows/win_environment.py
@@ -65,7 +65,10 @@ options:
- process
- user
author: "Jon Hawkesworth (@jhawkesworth)"
-notes:
+notes:
+ - This module is best-suited for setting the entire value of an
+ environment variable. For safe element-based management of
+ path-like environment vars, use the M(win_path) module.
- This module does not broadcast change events.
This means that the minority of windows applications which can have
their environment changed without restarting will not be notified and
diff --git a/lib/ansible/modules/windows/win_path.ps1 b/lib/ansible/modules/windows/win_path.ps1
new file mode 100644
index 0000000000..219f5a3486
--- /dev/null
+++ b/lib/ansible/modules/windows/win_path.ps1
@@ -0,0 +1,158 @@
+#!powershell
+# 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 <http://www.gnu.org/licenses/>.
+
+# WANT_JSON
+# POWERSHELL_COMMON
+
+Set-StrictMode -Version 2
+$ErrorActionPreference = "Stop"
+
+$system_path = "System\CurrentControlSet\Control\Session Manager\Environment"
+$user_path = "Environment"
+
+# list/arraylist methods don't allow IEqualityComparer override for case/backslash/quote-insensitivity, roll our own search
+Function Get-IndexOfPathElement ($list, [string]$value) {
+ $idx = 0
+ $value = $value.Trim('"').Trim('\')
+ ForEach($el in $list) {
+ If ([string]$el.Trim('"').Trim('\') -ieq $value) {
+ return $idx
+ }
+
+ $idx++
+ }
+
+ return -1
+}
+
+# alters list in place, returns true if at least one element was added
+Function Add-Elements ($existing_elements, $elements_to_add) {
+ $last_idx = -1
+ $changed = $false
+
+ ForEach($el in $elements_to_add) {
+ $idx = Get-IndexOfPathElement $existing_elements $el
+
+ # add missing elements at the end
+ If ($idx -eq -1) {
+ $last_idx = $existing_elements.Add($el)
+ $changed = $true
+ }
+ ElseIf ($idx -lt $last_idx) {
+ $existing_elements.RemoveAt($idx) | Out-Null
+ $existing_elements.Add($el) | Out-Null
+ $last_idx = $existing_elements.Count - 1
+ $changed = $true
+ }
+ Else {
+ $last_idx = $idx
+ }
+ }
+
+ return $changed
+}
+
+# alters list in place, returns true if at least one element was removed
+Function Remove-Elements ($existing_elements, $elements_to_remove) {
+ $count = $existing_elements.Count
+
+ ForEach($el in $elements_to_remove) {
+ $idx = Get-IndexOfPathElement $existing_elements $el
+ $result.removed_idx = $idx
+ If ($idx -gt -1) {
+ $existing_elements.RemoveAt($idx)
+ }
+ }
+
+ return $count -ne $existing_elements.Count
+}
+
+# PS registry provider doesn't allow access to unexpanded REG_EXPAND_SZ; fall back to .NET
+Function Get-RawPathVar ($scope) {
+ If ($scope -eq "user") {
+ $env_key = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey($user_path)
+ }
+ ElseIf ($scope -eq "machine") {
+ $env_key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($system_path)
+ }
+
+ return $env_key.GetValue($var_name, "", [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
+}
+
+Function Set-RawPathVar($path_value, $scope) {
+ If ($scope -eq "user") {
+ $var_path = "HKCU:\" + $user_path
+ }
+ ElseIf ($scope -eq "machine") {
+ $var_path = "HKLM:\" + $system_path
+ }
+
+ Set-ItemProperty $var_path -Name $var_name -Value $path_value -Type ExpandString | Out-Null
+
+ return $path_value
+}
+
+$parsed_args = Parse-Args $args -supports_check_mode $true
+
+$result = @{changed=$false}
+
+$var_name = Get-AnsibleParam $parsed_args "name" -Default "PATH"
+$elements = Get-AnsibleParam $parsed_args "elements" -FailIfEmpty $result
+$state = Get-AnsibleParam $parsed_args "state" -Default "present" -ValidateSet "present","absent"
+$scope = Get-AnsibleParam $parsed_args "scope" -Default "machine" -ValidateSet "machine","user"
+
+$check_mode = Get-AnsibleParam $parsed_args "_ansible_check_mode" -Default $false
+
+If ($elements -is [string]) {
+ $elements = @($elements)
+}
+
+If ($elements -isnot [Array]) {
+ Fail-Json $result "elements must be a string or list of path strings"
+}
+
+$current_value = Get-RawPathVar $scope
+$result.path_value = $current_value
+
+# TODO: test case-canonicalization on wacky unicode values (eg turkish i)
+# TODO: detect and warn/fail on unparseable path? (eg, unbalanced quotes, invalid path chars)
+# TODO: detect and warn/fail if system path and Powershell isn't on it?
+
+$existing_elements = New-Object System.Collections.ArrayList
+
+# split on semicolons, accounting for quoted values with embedded semicolons (which may or may not be wrapped in whitespace)
+$pathsplit_re = [regex] '((?<q>\s*"[^"]+"\s*)|(?<q>[^;]+))(;$|$|;)'
+
+ForEach ($m in $pathsplit_re.Matches($current_value)) {
+ $existing_elements.Add($m.Groups['q'].Value) | Out-Null
+}
+
+If ($state -eq "absent") {
+ $result.changed = Remove-Elements $existing_elements $elements
+}
+ElseIf ($state -eq "present") {
+ $result.changed = Add-Elements $existing_elements $elements
+}
+
+# calculate the new path value from the existing elements
+$path_value = [String]::Join(";", $existing_elements.ToArray())
+$result.path_value = $path_value
+
+If ($result.changed -and -not $check_mode) {
+ Set-RawPathVar $path_value $scope | Out-Null
+}
+
+Exit-Json $result
diff --git a/lib/ansible/modules/windows/win_path.py b/lib/ansible/modules/windows/win_path.py
new file mode 100644
index 0000000000..d775f5e527
--- /dev/null
+++ b/lib/ansible/modules/windows/win_path.py
@@ -0,0 +1,87 @@
+#!/usr/bin/python
+#
+# Copyright 2016 Red Hat | Ansible
+#
+# 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 <http://www.gnu.org/licenses/>.
+
+# This is a windows documentation stub. Actual code lives in the .ps1
+# file of the same name
+
+ANSIBLE_METADATA = {'status': ['preview'],
+ 'supported_by': 'core',
+ 'version': '1.0'}
+
+DOCUMENTATION = '''
+---
+module: win_path
+version_added: "2.3"
+short_description: Manage Windows path environment variables
+description:
+ - Allows element-based ordering, addition, and removal of Windows path environment variables.
+options:
+ name:
+ description:
+ - Target path environment variable name
+ default: PATH
+ elements:
+ description:
+ - A single path element, or a list of path elements (ie, directories) to add or remove.
+ - When multiple elements are included in the list (and C(state) is C(present)), the elements are guaranteed to appear in the same relative order in the resultant path value.
+ - Variable expansions (eg, C(%VARNAME%)) are allowed, and are stored unexpanded in the target path element.
+ - Any existing path elements not mentioned in C(elements) are always preserved in their current order.
+ - New path elements are appended to the path, and existing path elements may be moved closer to the end to satisfy the requested ordering.
+ - Paths are compared in a case-insensitive fashion, and trailing backslashes are ignored for comparison purposes. However, note that trailing backslashes in YAML require quotes.
+ required: true
+ state:
+ description:
+ - Whether the path elements specified in C(elements) should be present or absent.
+ choices:
+ - present
+ - absent
+ scope:
+ description:
+ - The level at which the environment variable specified by C(name) should be managed (either for the current user or global machine scope).
+ choices:
+ - machine
+ - user
+ default: machine
+author: "Matt Davis (@nitzmahone)"
+notes:
+ - This module is for modifying indidvidual elements of path-like
+ environment variables. For general-purpose management of other
+ environment vars, use the M(win_environment) module.
+ - This module does not broadcast change events.
+ This means that the minority of windows applications which can have
+ their environment changed without restarting will not be notified and
+ therefore will need restarting to pick up new environment settings.
+ User level environment variables will require an interactive user to
+ log out and in again before they become available.
+'''
+
+EXAMPLES = r'''
+- name: Ensure that system32 and Powershell are present on the global system path, and in the specified order
+ win_path:
+ elements:
+ - %SystemRoot%\system32
+ - %SystemRoot%\system32\WindowsPowerShell\v1.0
+
+- name: Ensure that C:\Program Files\MyJavaThing is not on the current user's CLASSPATH
+ win_path
+ name: CLASSPATH
+ elements: C:\Program Files\MyJavaThing
+ scope: user
+ state: absent
+'''
diff --git a/test/integration/targets/win_path/aliases b/test/integration/targets/win_path/aliases
new file mode 100644
index 0000000000..ee0ed5974e
--- /dev/null
+++ b/test/integration/targets/win_path/aliases
@@ -0,0 +1 @@
+windows/ci/group2
diff --git a/test/integration/targets/win_path/tasks/main.yml b/test/integration/targets/win_path/tasks/main.yml
new file mode 100644
index 0000000000..c51e0b0377
--- /dev/null
+++ b/test/integration/targets/win_path/tasks/main.yml
@@ -0,0 +1,183 @@
+- set_fact:
+ varname: WINPATH_TEST
+
+- name: Remove {{ varname }} vars from user and machine scope
+ raw: '[Environment]::SetEnvironmentVariable("{{ varname }}", $null, "User"); [Environment]::SetEnvironmentVariable("{{ varname }}", $null, "Machine")'
+
+- name: Set a var at the machine and user levels
+ win_path:
+ name: "{{ varname }}"
+ elements: C:\{{ item }}Path
+ scope: "{{ item }}"
+ with_items:
+ - machine
+ - user
+ register: pathout
+
+- name: Get path value from machine and user levels
+ raw: '[Environment]::GetEnvironmentVariable("{{ varname }}","{{ item.item }}")'
+ with_items: "{{ pathout.results }}"
+ register: varout
+
+- name: Ensure output
+ assert:
+ that:
+ - item.0 | changed
+ - item.0.path_value == "C:\\{{ item.0.item }}Path"
+ - item.1.stdout_lines[0] == 'C:\\{{ item.0.item }}Path'
+ with_together:
+ - "{{ pathout.results }}"
+ - "{{ varout.results }}"
+
+- name: Remove {{ varname }} vars from user and machine scope
+ raw: '[Environment]::SetEnvironmentVariable("{{ varname }}", $null, "User"); [Environment]::SetEnvironmentVariable("{{ varname }}", $null, "Machine")'
+
+- name: Create multi-element path
+ win_path:
+ name: "{{ varname }}"
+ elements:
+ - C:\PathZ
+ - C:\PathA
+ register: multiout
+
+- name: Get path value
+ raw: $env:{{ varname }}
+ register: varout
+
+- name: Ensure output
+ assert:
+ that:
+ - multiout | changed
+ - multiout.path_value == "C:\\PathZ;C:\\PathA"
+ - varout.stdout_lines[0] == "C:\\PathZ;C:\\PathA"
+
+- name: Add value to middle and end
+ win_path:
+ name: "{{ varname }}"
+ elements:
+ - C:\NewPath
+ - C:\PathA
+ - 'C:\PathWithTrailingBackslash\' # store with a trailing backslash
+ - '"C:\Quoted;With;Semicolons"' # embedded semicolon, wrapped in quotes
+ - '%SystemRoot%\stuff'
+ register: addout
+
+- name: Get path value
+ raw: $env:{{ varname }}
+ register: varout
+
+- name: Test idempotence- retry values to middle and end, test case-insensitive comparison, backslash canonicalization
+ win_path:
+ name: "{{ varname }}"
+ elements:
+ - c:\nEwPaTh
+ - c:\patha
+ - C:\pathwithtrailingbackslash # no trailing backslash, should be the same
+ - '"C:\Quoted;With;Semicolons"'
+ - '%SystemRoot%\stuff'
+ register: idemout
+
+- name: Get path value
+ raw: $env:{{ varname }}
+ register: idemvarout
+
+- name: Ensure output
+ assert:
+ that:
+ - addout | changed
+ - addout.path_value == 'C:\\PathZ;C:\\NewPath;C:\\PathA;C:\\PathWithTrailingBackslash\\;"C:\Quoted;With;Semicolons";%SystemRoot%\stuff'
+ - varout.stdout_lines[0] == ('C:\\PathZ;C:\\NewPath;C:\\PathA;C:\\PathWithTrailingBackslash\\;"C:\Quoted;With;Semicolons";C:\Windows\stuff')
+ - not idemout | changed
+ - idemout.path_value == 'C:\\PathZ;C:\\NewPath;C:\\PathA;C:\\PathWithTrailingBackslash\\;"C:\Quoted;With;Semicolons";%SystemRoot%\stuff'
+ - idemvarout.stdout_lines[0] == ('C:\\PathZ;C:\\NewPath;C:\\PathA;C:\\PathWithTrailingBackslash\\;"C:\Quoted;With;Semicolons";C:\Windows\stuff')
+
+- name: Remove single element
+ win_path:
+ name: "{{ varname }}"
+ elements: C:\NewPath
+ state: absent
+ register: removeout
+
+- name: Get path value
+ raw: $env:{{ varname }}
+ register: varout
+
+- name: Test idempotence- retry remove single element
+ win_path:
+ name: "{{ varname }}"
+ elements: C:\NewPath
+ state: absent
+ register: idemremoveout
+
+- name: Get path value
+ raw: $env:{{ varname }}
+ register: idemvarout
+
+- name: Ensure output
+ assert:
+ that:
+ - removeout | changed
+ - removeout.path_value == 'C:\\PathZ;C:\\PathA;C:\\PathWithTrailingBackslash\\;"C:\Quoted;With;Semicolons";%SystemRoot%\stuff'
+ - varout.stdout_lines[0] == 'C:\\PathZ;C:\\PathA;C:\\PathWithTrailingBackslash\\;"C:\Quoted;With;Semicolons";C:\Windows\stuff'
+ - not idemremoveout | changed
+ - idemremoveout.path_value == 'C:\\PathZ;C:\\PathA;C:\\PathWithTrailingBackslash\\;"C:\Quoted;With;Semicolons";%SystemRoot%\stuff'
+ - idemvarout.stdout_lines[0] == 'C:\\PathZ;C:\\PathA;C:\\PathWithTrailingBackslash\\;"C:\Quoted;With;Semicolons";C:\Windows\stuff'
+
+- name: Remove multiple elements
+ win_path:
+ name: "{{ varname }}"
+ elements:
+ - C:\PathWithTrailingBackslash # no trailing backslash
+ - c:\pathz
+ - '"C:\Quoted;With;Semicolons"'
+ - '%SystemRoot%\stuff\' # add trailing backslash
+ state: absent
+ register: removeout
+
+- name: Get path value
+ raw: $env:{{ varname }}
+ register: varout
+
+- name: Ensure output
+ assert:
+ that:
+ - removeout | changed
+ - removeout.path_value == "C:\\PathA"
+ - varout.stdout_lines[0] == "C:\\PathA"
+
+- name: Test check mode add
+ check_mode: yes
+ win_path:
+ name: "{{ varname }}"
+ elements:
+ - C:\MissingPath
+ register: checkadd
+
+- name: Get path value
+ raw: $env:{{ varname }}
+ register: checkaddvarout
+
+- name: Test check mode remove
+ check_mode: yes
+ win_path:
+ name: "{{ varname }}"
+ elements: C:\PathA
+ state: absent
+ register: checkremove
+
+- name: Get path value
+ raw: $env:{{ varname }}
+ register: checkremovevarout
+
+- name: Ensure output
+ assert:
+ that:
+ - checkadd | changed
+ - checkadd.path_value == "C:\\PathA;C:\\MissingPath"
+ - checkaddvarout.stdout_lines[0] == "C:\\PathA" # shouldn't have actually changed the value
+ - checkremove | changed
+ - checkremove.path_value == ""
+ - checkremovevarout.stdout_lines[0] == "C:\\PathA" # shouldn't have actually changed the value
+
+- name: Remove {{ varname }} vars from user and machine scope
+ raw: '[Environment]::SetEnvironmentVariable("{{ varname }}", $null, "User"); [Environment]::SetEnvironmentVariable("{{ varname }}", $null, "Machine")'
diff --git a/test/integration/test_win_group2.yml b/test/integration/test_win_group2.yml
index 5f669630e0..94724ad073 100644
--- a/test/integration/test_win_group2.yml
+++ b/test/integration/test_win_group2.yml
@@ -10,3 +10,4 @@
- { role: win_get_url, tags: test_win_get_url }
- { role: win_msi, tags: test_win_msi }
- { role: win_package, tags: test_win_package }
+ - { role: win_path, tags: test_win_path }