new module: win_path (#20073)

pull/20384/head
Matt Davis 8 years ago committed by GitHub
parent 712be24a74
commit b2a16379c8

@ -83,6 +83,7 @@ Ansible Changes By Release
- windows:
* win_say
* win_shortcut
* win_path
- openstack
* os_quota
- zfs:

@ -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

@ -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

@ -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
'''

@ -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")'

@ -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 }

Loading…
Cancel
Save