Showing with 167 additions and 59 deletions.
  1. +2 −0 .gitignore
  2. +4 −0 CHANGELOG.md
  3. +7 −0 Modulefile
  4. +58 −23 README.md
  5. +68 −24 lib/puppet/provider/windows_env/windows_env.rb
  6. +14 −4 lib/puppet/type/windows_env.rb
  7. +14 −8 tests/init.pp
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pkg/
*.swp
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
v1.0.0
======
- Ensure now defaults to 'present'.
- New paramater added, 'type'. Allows selection between REG_SZ or REG_EXPAND_SZ registry keys.
7 changes: 7 additions & 0 deletions Modulefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name 'badgerious-puppet_env'
version '1.0.0'
summary 'Manages Windows environment variables'
description 'Create, delete, and insert values into Windows environment variables'
project_page 'https://github.com/badgerious/puppet-windows-env'
license 'Apache License, Version 2.0'
author 'badgerious'
81 changes: 58 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,23 @@ This module manages Windows environment variables (currently only system environ
Installation
------------

Install the current head from the git repository by going to your puppet modules folder and do
Install from puppet forge:

puppet module install badgerious/windows_env

Install from git (do this in your modulepath):

git clone https://github.com/badgerious/puppet-windows-env windows_env

It is important that the folder where this module resisdes is named windows_env, not puppet-windows-env.
It is important that the folder where this module resides is named windows_env, not puppet-windows-env.

Usage
-----

### Parameters

#### `ensure`
Standard ensure, valid values are `absent` or `present`.
Standard ensure, valid values are `absent` or `present`. Defaults to `present`.

#### `variable` (namevar)
The name of the environment variable. This will be inferred from the title if
Expand Down Expand Up @@ -48,22 +52,22 @@ Valid values:
its value. If the variable already exists, the puppet resource provided
content will be merged with the existing content. The puppet provided
content will be placed at the beginning, and separated from existing
entires with `separator`. If the specified value is already in the
entries with `separator`. If the specified value is already in the
variable, but not at the beginning, it will be moved to the beginning. In
the case of multiple resources in `prepend` mode on the same variable, the
last to be run will be placed at the front of the variable. Note that with
multiple `prepend`s on the same resource, there will be shuffling around on
every puppet run, since each resource will place its own value at the front
of the list when it is run. If there are multiple values that need to be in
a specific order and at the beginning, an array can be provided to `value`.
The relative ordering of the array items will be maintained when they are
inserted into the variable.
the case of multiple resources in `prepend` mode managing the same
variable, the values will inserted in the order of evaluation (the last to
run will be listed first in the variable). Note that with multiple
`prepend`s on the same resource, there will be shuffling around on every
puppet run, since each resource will place its own value at the front of
the list when it is run. Alternatively, an array can be provided to
`value`. The relative ordering of the array items will be maintained when
they are inserted into the variable, and the shuffling will be avoided.
- When `ensure => absent`, the value provided by the puppet resource will be
removed from the environment variable. Other content will be left
unchanged. The environment variable will not be removed, even if its
contents are blank.
- `append`
- Same as `prepend`, except content will be placed at the end of the
- Same as `prepend`, except the new value will be placed at the end of the
variable's existing contents rather than the beginning.
- `insert`
- Same as `prepend` or `append`, except that content is not required to be
Expand All @@ -72,22 +76,40 @@ Valid values:
are some conflicts that need to be resolved (the conflicts may be better
resolved with an array given to `value` and with `mergemode => insert`).

#### `type`
The type of registry value to use. Default is `undef` for existing keys (i.e.
don't change the type) and `REG_SZ` when creating new keys.

Valid values:

- `REG_SZ`
- This is a regular registry string item with no substitution.
- `REG_EXPAND_SZ`
- Values of this type will expand '%' enclosed strings (e.g. %SystemRoot%)
derived from other environment variables. If you're on a 64-bit system, be
careful here; puppet runs as a 32-bit ruby process, and may be subject to
WoW64 registry redirection shenanigans. This module writes keys with the
KEY_WOW64_64KEY flag, which on Windows 7+ (Server 2008 R2) systems will
disable value rewriting. Older systems will rewrite certain values. The
gory details can be found here:
http://msdn.microsoft.com/en-us/library/windows/desktop/aa384232%28v=vs.85%29.aspx
.

#### `broadcast_timeout`
Specifies how long (in ms) to wait (per window) for refreshes to go through
when environment variables change. Default is 5000ms. This probably doesn't
need changing unless you're having issues with the refreshes.
need changing unless you're having issues with the refreshes taking a long time
(they generally happen nearly instantly).

### Examples

# Title type #1. Variable name and value are extracted from title, splitting on '='.
# Default 'insert' mergemode is selected, so this will add 'C:\code\bin' to
# PATH, merging it neatly with existing content.
windows_env { 'PATH=C:\code\bin':
ensure => present,
}
# Default 'insert' mergemode is selected and default 'present' ensure is selected,
# so this will add 'C:\code\bin' to PATH, merging it neatly with existing content.
windows_env { 'PATH=C:\code\bin': }

# Title type #2. Variable name is derived from the title, but not value. (because there is no '=').
# This will remove the environment variable 'BADVAR' competely.
# Title type #2. Variable name is derived from the title, but not value (because there is no '=').
# This will remove the environment variable 'BADVAR' completely.
windows_env { 'BADVAR':
ensure => absent,
mergemode => clobber,
Expand All @@ -96,13 +118,20 @@ need changing unless you're having issues with the refreshes.
# Title type #3. Title doesn't set parameters (because both 'variable' and 'value' have
# been supplied manually).
# This will create a new environment variable 'MyVariable' and set its value to 'stuff'.
# If the variable already exists, its value will be replaced with 'stuff'.
windows_env {'random_title':
ensure => present,
variable => 'MyVariable',
value => 'stuff',
mergemode => clobber,
}

# Variables with 'type => REG_EXPAND_SZ' allow other environment variables to be used
# by enclosing them in parentheses.
windows_env { 'JAVA_HOME=%ProgramFiles%\Java\jdk1.6.0_02':
type => REG_EXPAND_SZ,
}

# Creates (if needed) an enviroment variable 'VAR', and sticks 'VAL:VAL2' at
# the beginning. Separates with : instead of ;. The broadcast_timeout change
# probably won't make any difference.
Expand All @@ -115,11 +144,17 @@ need changing unless you're having issues with the refreshes.
broadcast_timeout => 2000,
}

### Things that won't end well
Certain conflicts can occur which may cause unexpected behavior (which you won't be warned about):

Things that won't end well
---------------------------
- Multiple resource declarations controlling the same environment variable with
at least one in 'clobber' mode. Toes will be stepped on.
- Multiple resource declarations controlling the same environment variable with
different types. More dead toes.

Compatibility
-------------
This module has been tested against a 3.2.1 puppetmaster, 2.7.21 and 3.2.1 agents.

Acknowledgements
----------------
Expand Down
92 changes: 68 additions & 24 deletions lib/puppet/provider/windows_env/windows_env.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
# Depending on puppet version, this feature may or may not include the libraries needed, but
# if some of them are present, the others should be too.
# if some of them are present, the others should be too. This check prevents errors from
# non Windows nodes that have had this module pluginsynced to them.
if Puppet.features.microsoft_windows?
require 'win32/registry.rb'
require 'Win32API'
module Win32
class Registry
KEY_WOW64_64KEY = 0x0100 unless defined?(KEY_WOW64_64KEY)
end
end
end
require 'set'

Puppet::Type.type(:windows_env).provide(:windows_env) do
desc "Manage Windows environment variables"
Expand All @@ -15,8 +20,14 @@
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx
self::ERROR_FILE_NOT_FOUND = 2

self::REG_HIVE = Win32::Registry::HKEY_LOCAL_MACHINE
self::REG_PATH = 'System\CurrentControlSet\Control\Session Manager\Environment'
# This feature check is necessary to make 'puppet module build' work, since
# it actually executes this code in building.
if Puppet.features.microsoft_windows?
self::REG_HIVE = Win32::Registry::HKEY_LOCAL_MACHINE
self::REG_PATH = 'System\CurrentControlSet\Control\Session Manager\Environment'
# see broadcast_changes method for more info about SendMessageTimeout
self::SendMessageTimeout = Win32API.new('user32', 'SendMessageTimeout', 'LLLPLLP', 'L')
end

def exists?
if @resource[:ensure] == :present && [nil, :nil].include?(@resource[:value])
Expand All @@ -29,12 +40,16 @@ def exists?

@sep = @resource[:separator]

@reg_types = { :REG_SZ => Win32::Registry::REG_SZ, :REG_EXPAND_SZ => Win32::Registry::REG_EXPAND_SZ }
@reg_type = @reg_types[@resource[:type]]

if @resource[:value].class != Array
@resource[:value] = [@resource[:value]]
end

begin
self.class::REG_HIVE.open(self.class::REG_PATH, Win32::Registry::KEY_READ) { |key| @value = key[@resource[:variable]] }
# key.read returns '[type, data]' and must be used instead of [] because [] expands %variables%.
self.class::REG_HIVE.open(self.class::REG_PATH) { |key| @value = key.read(@resource[:variable])[1] }
rescue Win32::Registry::Error => error
if error.code == self.class::ERROR_FILE_NOT_FOUND
debug "Environment variable #{@resource[:variable]} not found"
Expand All @@ -60,7 +75,13 @@ def exists?
# don't bother checking the content in this case.
@resource[:ensure] == :present ? @value == @resource[:value] : true
when :insert
Set.new(@resource[:value].map { |x| x.downcase }).subset?(Set.new(@value.map { |x| x.downcase }))
# verify all elements are present and they appear in the correct order
indexes = @resource[:value].map { |x| @value.find_index { |y| x.casecmp(y) == 0 } }
if indexes.count == 1
indexes == [nil] ? false : true
else
indexes.each_cons(2).all? { |a, b| a && b && a < b }
end
when :append
@value.map { |x| x.downcase }[(-1 * @resource[:value].count)..-1] == @resource[:value].map { |x| x.downcase }
when :prepend
Expand All @@ -80,26 +101,26 @@ def create

case @resource[:mergemode]
when :clobber
@reg_type = Win32::Registry::REG_SZ unless @reg_type
begin
self.class::REG_HIVE.create(self.class::REG_PATH, Win32::Registry::KEY_WRITE) do |key|
key[@resource[:variable]] = @resource[:value].join(@sep)
self.class::REG_HIVE.create(self.class::REG_PATH, Win32::Registry::KEY_ALL_ACCESS | Win32::Registry::KEY_WOW64_64KEY) do |key|
key[@resource[:variable], @reg_type] = @resource[:value].join(@sep)
end
rescue Win32::Registry::Error => error
reg_fail('creating', error)
end
# the position at which the new value will be inserted when using insert is
# arbitrary, so may as well group it with append.
when :insert, :append
# delete if already in the string and move to end. 'remove_value' will have no effect in 'insert' mode; we would not have
# reached this point if there were something to remove.
# delete if already in the string and move to end.
remove_value
@value = @value.concat(@resource[:value]).join(@sep)
key_write { |key| key[@resource[:variable]] = @value }
@value = @value.concat(@resource[:value])
key_write
when :prepend
# delete if already in the string and move to front
remove_value
@value = @resource[:value].concat(@value).join(@sep)
key_write { |key| key[@resource[:variable]] = @value }
@value = @resource[:value].concat(@value)
key_write
end
broadcast_changes
end
Expand All @@ -111,11 +132,23 @@ def destroy
key_write { |key| key.delete_value(@resource[:variable]) }
when :insert, :append, :prepend
remove_value
key_write { |key| key[@resource[:variable]] = @value.join(@sep) }
key_write
end
broadcast_changes
end

def type
# QueryValue returns '[type, value]'
current_type = self.class::REG_HIVE.open(self.class::REG_PATH) { |key| Win32::Registry::API.QueryValue(key.hkey, @resource[:variable]) }[0]
@reg_types.invert[current_type]
end

def type=(newtype)
newtype = @reg_types[newtype]
key_write { |key| key[@resource[:variable], newtype] = @value.join(@sep) }
broadcast_changes
end

private

def reg_fail(action, error)
Expand All @@ -127,7 +160,18 @@ def remove_value
end

def key_write(&block)
self.class::REG_HIVE.open(self.class::REG_PATH, Win32::Registry::KEY_WRITE, &block)
unless block_given?
if ! [nil, :nil, :undef].include?(@resource[:type]) && self.type != @resource[:type]
# It may be the case that #exists? returns false, but we're still not creating a
# new registry value (e.g. when mergmode => insert). In this case, the property getters/setters
# won't be called, so we'll go ahead and set type here manually.
newtype = @reg_types[@resource[:type]]
else
newtype = @reg_types[self.type]
end
block = proc { |key| key[@resource[:variable], newtype] = @value.join(@sep) }
end
self.class::REG_HIVE.open(self.class::REG_PATH, Win32::Registry::KEY_WRITE | Win32::Registry::KEY_WOW64_64KEY, &block)
rescue Win32::Registry::Error => error
reg_fail('writing', error)
end
Expand All @@ -138,16 +182,16 @@ def key_write(&block)
# and: http://msdn.microsoft.com/en-us/library/windows/desktop/ms644952%28v=vs.85%29.aspx
# and: http://msdn.microsoft.com/en-us/library/windows/desktop/ms725497%28v=vs.85%29.aspx
# and for good measure: http://ruby-doc.org/stdlib-1.9.2/libdoc/dl/rdoc/Win32API.html
self::SendMessageTimeout = Win32API.new('user32', 'SendMessageTimeout', 'LLLPLLP', 'L')
def broadcast_changes
debug "Broadcasting changes to environment"
# About the args: 0xFFFF = HWND_BROADCAST (send to all windows)
# 0x001A = WM_SETTINGCHANGE (the message to send, informs windows a system change has occurred)
# 0 = NULL (this should always be NULL with WM_SETTINGCHANGE)
# 'Environment' = (string indicating what changed. This refers to the 'Environment' registry key)
# 2 = SMTO_ABORTIFHUNG (return without waiting timeout period if receiver appears to hang)
# bcast timeout = (How long to wait for a window to respond to the event. Each window gets this amount of time)
# 0 = (Return value. We're ignoring it)
# About the args:
# 0xFFFF = HWND_BROADCAST (send to all windows)
# 0x001A = WM_SETTINGCHANGE (the message to send, informs windows a system change has occurred)
# 0 = NULL (this should always be NULL with WM_SETTINGCHANGE)
# 'Environment' = (string indicating what changed. This refers to the 'Environment' registry key)
# 2 = SMTO_ABORTIFHUNG (return without waiting timeout period if receiver appears to hang)
# bcast timeout = (How long to wait for a window to respond to the event. Each window gets this amount of time)
# 0 = (Return value. We're ignoring it)
self.class::SendMessageTimeout.call(0xFFFF, 0x001A, 0, 'Environment', 2, @resource[:broadcast_timeout], 0)
end
end
18 changes: 14 additions & 4 deletions lib/puppet/type/windows_env.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
Puppet::Type.newtype(:windows_env) do
desc "Manages Windows environment variables"

ensurable
ensurable do
newvalue(:present) { provider.create }
newvalue(:absent) { provider.destroy }
defaultto(:present)
end

# title will look like "#{variable}=#{value}" (The '=' is not permitted in
# environment variable names). If no '=' is present, user is giving only
# the variable name (for deletion purposes, say, or to provide an array),
# so value will be set to nil (and possibly overridden later).
def self.title_patterns
[[/^(.*?)=(.*)$/, [[:variable, proc{|x| x}], [:value, proc{|x| x }]]],
[[/^(.*?)=(.*)$/, [[:variable, proc{|x| x}], [:value, proc{|x| x}]]],
[/^([^=]+)$/ , [[:variable, proc{|x| x}]]]]
end

Expand Down Expand Up @@ -37,12 +41,18 @@ def self.title_patterns
desc "Set the timeout (in ms) for environment refreshes. This is per top level window, so delay may be longer than provided value."
validate do |val|
begin
Integer(val)
val = Integer(val)
val > 0 or raise ArgumentError
rescue ArgumentError
raise ArgumentError, "broadcast_timeout must be a valid integer"
raise ArgumentError, "broadcast_timeout must be a valid positive integer"
end
end
munge { |val| Integer(val) }
defaultto(5000)
end

newproperty(:type) do
desc "What type of registry key to use for the variable. Determines whether interpolation of '%' enclosed names will occur"
newvalues(:REG_SZ, :REG_EXPAND_SZ)
end
end
Loading