Showing with 179 additions and 42 deletions.
  1. +9 −1 CHANGELOG.md
  2. +2 −2 Modulefile
  3. +44 −8 README.md
  4. +101 −27 lib/puppet/provider/windows_env/windows_env.rb
  5. +6 −1 lib/puppet/type/windows_env.rb
  6. +0 −3 manifests/init.pp
  7. +17 −0 tests/init.pp
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
v2.0.0
======
- Remove 'manifests' directory. This directory had nothing useful in it.
- Fixed name in Modulefile (was erroneously 'badgerious-puppet_env' now is 'badgerious-windows_env').
- Add 'user' parameter to allow user specific variables to be managed.
- Changed default 'broadcast_timeout' to 100ms. Puppet usually runs in the background, where the broadcasting
doesn't work anyway. There's no reason to be waiting for updates to go through that won't affect any users.

v1.0.0
======
- Ensure now defaults to 'present'.
- New paramater added, 'type'. Allows selection between REG_SZ or REG_EXPAND_SZ registry keys.
- New parameter added, 'type'. Allows selection between REG_SZ or REG_EXPAND_SZ registry keys.
4 changes: 2 additions & 2 deletions Modulefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name 'badgerious-puppet_env'
version '1.0.0'
name 'badgerious-windows_env'
version '2.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'
Expand Down
52 changes: 44 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Install from git (do this in your modulepath):

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

Changes
-------

Please see [CHANGELOG.md](https://github.com/badgerious/puppet-windows-env/blob/master/CHANGELOG.md)

Usage
-----

Expand All @@ -33,6 +38,13 @@ not given explicitly. The title can be of either the form `{variable}={value}`
The value of the environment variable. How this will treat existing content
depends on `mergemode`.

#### `user` (namevar)
The user whose environment will be modified. Default is `undef`, i.e. system
environment. The user can be local or domain, as long as they have a local
profile (typically `C:\users\{username}` on Vista+). There is no awareness of
network profiles in this module; knowing how changes to the local profile will
affect a distributed profile is up to you.

#### `separator`
How to split entries in environment variables with multiple values (such as
`PATH` or `PATHEXT`) . Default is `';'`.
Expand Down Expand Up @@ -85,7 +97,7 @@ 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%)
- 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
Expand All @@ -97,12 +109,16 @@ Valid values:

#### `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
when environment variables change. Default is 100ms. This probably doesn't
need changing unless you're having issues with the refreshes taking a long time
(they generally happen nearly instantly).
(they generally happen nearly instantly). Note that this only works for the user
that initiated the puppet run; if puppet runs in the background, updates to the
environment will not propagate to logged in users until they log out and back in
or refresh their environment by some other means.

### Examples

```puppet
# Title type #1. Variable name and value are extracted from title, splitting on '='.
# 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.
Expand All @@ -127,11 +143,23 @@ need changing unless you're having issues with the refreshes taking a long time
}
# Variables with 'type => REG_EXPAND_SZ' allow other environment variables to be used
# by enclosing them in parentheses.
# by enclosing them in percent symbols.
windows_env { 'JAVA_HOME=%ProgramFiles%\Java\jdk1.6.0_02':
type => REG_EXPAND_SZ,
}
# Create an environment variable for 'Administrator':
windows_env { 'KOOLVAR':
value => 'hi',
user => 'Administrator',
}
# Create an environment variable for 'Domain\FunUser':
windows_env { 'Funvar':
value => 'Funval',
user => 'Domain\FunUser',
}
# 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 @@ -144,17 +172,25 @@ need changing unless you're having issues with the refreshes taking a long time
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):
Certain conflicts can occur which may cause unexpected behavior (which you may not be warned about):

- Multiple resource declarations controlling the same environment variable with
at least one in 'clobber' mode. Toes will be stepped on.
at least one in `mergemode => clobber`. Toes will be stepped on.
- Multiple resource declarations controlling the same environment variable with
different types. More dead toes.
different `type`s. More squished toes.

If you find yourself using `mergemode => clobber` or `type`, I recommend using
the environment variable name as the resource title (like the example 'Title
type #2' above) if you can; this way puppet will flag duplicates for you and
help identify the conflicts.


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

Acknowledgements
----------------
Expand Down
128 changes: 101 additions & 27 deletions lib/puppet/provider/windows_env/windows_env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,62 @@
# 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'
require 'puppet/util/windows/security'
require 'win32/registry'
require 'windows/error'
module Win32
class Registry
KEY_WOW64_64KEY = 0x0100 unless defined?(KEY_WOW64_64KEY)
end
end
end

# This is apparently the "best" way to do unconditional cleanup for a provider.
# see https://groups.google.com/forum/#!topic/puppet-dev/Iqs5jEGfu_0
module Puppet
class Transaction
# added '_xhg62j' to make sure that if somebody else does this monkey patch, they don't
# choose the same name as I do, since that would cause ruby to blow up.
alias_method :evaluate_original_xhg62j, :evaluate
def evaluate
evaluate_original_xhg62j
Puppet::Type::Windows_env::ProviderWindows_env.unload_user_hives
end
end
end

Puppet::Type.type(:windows_env).provide(:windows_env) do
desc "Manage Windows environment variables"

confine :osfamily => :windows
defaultfor :osfamily => :windows

# http://msdn.microsoft.com/en-us/library/windows/desktop/ms681382%28v=vs.85%29.aspx
self::ERROR_FILE_NOT_FOUND = 2

# 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')
self::RegLoadKey = Win32API.new('Advapi32', 'RegLoadKey', 'LPP', 'L')
self::RegUnLoadKey = Win32API.new('Advapi32', 'RegUnLoadKey', 'LP', 'L')
self::FormatMessage = Win32API.new('kernel32', 'FormatMessage', 'LLLLPL', 'L')
end

# Instances can load hives with #load_user_hive . The class takes care of
# unloading all hives.
@loaded_hives = []
class << self
attr_reader :loaded_hives
end

def self.unload_user_hives
Puppet::Util::Windows::Security.with_privilege(Puppet::Util::Windows::Security::SE_RESTORE_NAME) do
@loaded_hives.each do |hash|
user_sid = hash[:user_sid]
username = hash[:username]
debug "Unloading NTUSER.DAT for '#{username}'"
result = self::RegUnLoadKey.call(Win32::Registry::HKEY_USERS.hkey, user_sid)
end
end
end

def exists?
Expand All @@ -38,6 +69,26 @@ def exists?
self.fail "'value' parameter must be provided when 'ensure => absent' and 'mergemode => #{@resource[:mergemode]}'"
end

if @resource[:user]
@reg_hive = Win32::Registry::HKEY_USERS
@user_sid = Puppet::Util::Windows::Security.name_to_sid(@resource[:user])
@user_sid or self.fail "Username '#{@resource[:user]}' could not be converted to a valid SID"
@reg_path = "#{@user_sid}\\Environment"

begin
@reg_hive.open(@reg_path) {}
rescue Win32::Registry::Error => error
if error.code == Windows::Error::ERROR_FILE_NOT_FOUND
load_user_hive
else
reg_fail("Can't access Environment for user '#{@resource[:user]}'. Opening", error)
end
end
else
@reg_hive = Win32::Registry::HKEY_LOCAL_MACHINE
@reg_path = 'System\CurrentControlSet\Control\Session Manager\Environment'
end

@sep = @resource[:separator]

@reg_types = { :REG_SZ => Win32::Registry::REG_SZ, :REG_EXPAND_SZ => Win32::Registry::REG_EXPAND_SZ }
Expand All @@ -49,9 +100,9 @@ def exists?

begin
# 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] }
@reg_hive.open(@reg_path) { |key| @value = key.read(@resource[:variable])[1] }
rescue Win32::Registry::Error => error
if error.code == self.class::ERROR_FILE_NOT_FOUND
if error.code == Windows::Error::ERROR_FILE_NOT_FOUND
debug "Environment variable #{@resource[:variable]} not found"
return false
end
Expand All @@ -75,6 +126,7 @@ def exists?
# don't bother checking the content in this case.
@resource[:ensure] == :present ? @value == @resource[:value] : true
when :insert
# FIXME: this is a weird way to do this
# 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
Expand Down Expand Up @@ -103,7 +155,7 @@ def create
when :clobber
@reg_type = Win32::Registry::REG_SZ unless @reg_type
begin
self.class::REG_HIVE.create(self.class::REG_PATH, Win32::Registry::KEY_ALL_ACCESS | Win32::Registry::KEY_WOW64_64KEY) do |key|
@reg_hive.create(@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
Expand Down Expand Up @@ -139,7 +191,7 @@ def destroy

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]
current_type = @reg_hive.open(@reg_path) { |key| Win32::Registry::API.QueryValue(key.hkey, @resource[:variable]) }[0]
@reg_types.invert[current_type]
end

Expand All @@ -152,7 +204,7 @@ def type=(newtype)
private

def reg_fail(action, error)
self.fail "#{action} '#{self.class::REG_HIVE.name}:\\#{self.class::REG_PATH}\\#{@resource[:variable]}' returned error #{error.code}: #{error.message}"
self.fail "#{action} '#{@reg_hive.name}:\\#{@reg_path}\\#{@resource[:variable]}' returned error #{error.code}: #{error.message}"
end

def remove_value
Expand All @@ -171,27 +223,49 @@ def key_write(&block)
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)
@reg_hive.open(@reg_path, Win32::Registry::KEY_WRITE | Win32::Registry::KEY_WOW64_64KEY, &block)
rescue Win32::Registry::Error => error
reg_fail('writing', error)
end

# Make new variable visible without logging off and on again.
#
# Make new variable visible without logging off and on again. This really only makes sense
# for debugging (i.e. with 'puppet agent -t') since you can only broadcast messages to your own
# windows, and not to those of other users.
# see: http://stackoverflow.com/questions/190168/persisting-an-environment-variable-through-ruby/190437#190437
# 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
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)
self.class::SendMessageTimeout.call(0xFFFF, 0x001A, 0, 'Environment', 2, @resource[:broadcast_timeout], 0)
_HWND_BROADCAST = 0xFFFF
_WM_SETTINGCHANGE = 0x1A
self.class::SendMessageTimeout.call(_HWND_BROADCAST, _WM_SETTINGCHANGE, 0, 'Environment', 2, @resource[:broadcast_timeout], 0)
end

# This is the best solution I found to (at least mostly) reliably locate a user's
# ntuser.dat: http://stackoverflow.com/questions/1059460/shgetfolderpath-for-a-specific-user
def load_user_hive
debug "Loading NTUSER.DAT for '#{@resource[:user]}'"

home_path = nil
begin
Win32::Registry::HKEY_LOCAL_MACHINE.open("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\ProfileList\\#{@user_sid}") do |key|
home_path = key['ProfileImagePath']
end
rescue Win32::Registry::Error => error
self.fail "Cannot find registry hive for user '#{@resource[:user]}'"
end

ntuser_path = File.join(home_path, 'NTUSER.DAT')

Puppet::Util::Windows::Security.with_privilege(Puppet::Util::Windows::Security::SE_RESTORE_NAME) do
result = self.class::RegLoadKey.call(Win32::Registry::HKEY_USERS.hkey, @user_sid, ntuser_path)
unless result == 0
_FORMAT_MESSAGE_FROM_SYSTEM = 0x1000
message = ' ' * 512
self.class::FormatMessage.call(_FORMAT_MESSAGE_FROM_SYSTEM, 0, result, 0, message, message.length)
self.fail "Could not load registry hive for user '#{@resource[:user]}'. RegLoadKey returned #{result}: #{message.strip}"
end
end

self.class.loaded_hives << { :user_sid => @user_sid, :username => @resource[:user] }
end
end

7 changes: 6 additions & 1 deletion lib/puppet/type/windows_env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def self.title_patterns
isnamevar
end

newparam(:user) do
desc "Set the user whose environment will be modified"
isnamevar
end

newparam(:mergemode) do
desc "How to set the value of the environment variable. E.g. replace existing value, append to existing value..."
newvalues(:clobber, :insert, :append, :prepend)
Expand All @@ -48,7 +53,7 @@ def self.title_patterns
end
end
munge { |val| Integer(val) }
defaultto(5000)
defaultto(100)
end

newproperty(:type) do
Expand Down
3 changes: 0 additions & 3 deletions manifests/init.pp

This file was deleted.

17 changes: 17 additions & 0 deletions tests/init.pp
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@
ensure => absent,
}

# nonexistent user
windows_env { 'SHOULD_FAIL3':
value => 'hello',
user => 'jibberishuserwhoshouldnotexist',
}

### SHOULD PASS ###

# Should insert 'C:\foo' at end of PATH
Expand Down Expand Up @@ -76,5 +82,16 @@
ensure => absent,
value => 'C:\path',
}

# Should add 'C:\somecode\bin' to Administrator account's PATH.
windows_env { 'PATH=C:\somecode\bin':
user => 'Administrator',
}

# Should remove 'C:\badcode\bin' from Administrator account's PATH.
windows_env { 'PATH=C:\badcode\bin':
user => 'Administrator',
ensure => absent,
}
}