Showing with 108 additions and 39 deletions.
  1. +4 −0 CHANGELOG.md
  2. +1 −1 LICENSE
  3. +47 −22 README.md
  4. +5 −0 lib/puppet/feature/windows_env.rb
  5. +50 −15 lib/puppet/provider/windows_env/windows_env.rb
  6. +1 −1 metadata.json
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
v2.2.0
------
- Puppet 3.7 / Ruby 64 bit compatibility changes.

v2.1.0
------
- Use `post_resource_eval` hook instead of monkey patching if possible.
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Copyright 2013 Eric Badger
Copyright 2013-2014 Eric Badger

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
69 changes: 47 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,28 @@ Install from git (do this in your modulepath):

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

This module also requires the 'ffi' gem. This gem is included
with Puppet 3.3.0+. On older versions, you'll need to do something like:

```puppet
package { 'ffi':
ensure => installed,
provider => gem,
}
```

Changes
-------

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

Compatibility
-------------

Puppet 3.7 or greater requires version 2.2.0 or greater of this module.

Usage
-----

Expand Down Expand Up @@ -57,34 +74,32 @@ Valid values:
- When `ensure => present`, creates a new variable (if necessary) and sets
its value. If the variable already exists, its value will be overwritten.
- When `ensure => absent`, the environment variable will be deleted entirely.
- `prepend`
- `insert`
- When `ensure => present`, creates a new variable (if necessary) and sets
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
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 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.
content will be merged with the existing content. The puppet provided content
will be placed at the end, and separated from existing entries with
`separator`. If the specified value is already somewhere in the variable, no
change will occur.
- 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.
- `prepend`
- Same as `insert`, except Puppet will ensure the value appears **first**. 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 managing the same variable, the values will be 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.
- `append`
- Same as `prepend`, except the new value will be placed at the end of the
- Same as `prepend`, except the new value will be placed at (or be moved to) 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
anywhere in particular. New content will be added at the end, but existing
content will not be moved. This is probably the mode to use unless there
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.
Expand All @@ -96,8 +111,8 @@ Valid values:
- 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
derived from other environment variables. If you're on a 64-bit system and
running 32-bit puppet, be careful here; registry writes 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
Expand Down Expand Up @@ -171,6 +186,16 @@ or refresh their environment by some other means.
broadcast_timeout => 2000,
}
# Exec doStuff.bat whenever environment variable KOOLVAR changes.
# Note that if you have multiple windows_env resources managing one
# variable, you'll need to either subscribe to all of them or combine
# the windows_env resources into one (by passing an array to 'value')
# and subscribing to that one resource.
exec { 'C:\doStuff.bat':
subscribe => Windows_env['KOOLVAR'],
refreshonly => true,
}
```

Acknowledgements
Expand Down
5 changes: 5 additions & 0 deletions lib/puppet/feature/windows_env.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
require 'puppet/util/feature'
# This file might seem like it should be called 'ffi.rb'. Little bit of
# discussion here:
# https://groups.google.com/forum/#!topic/puppet-dev/I6YMtNHmykU
Puppet.features.add(:windows_env, :libs => ['ffi'])
65 changes: 50 additions & 15 deletions lib/puppet/provider/windows_env/windows_env.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@
# 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 'Win32API'
require 'puppet/util/windows/error'
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

require 'puppet/feature/windows_env'

if Puppet.version < '3.4.0'
# This is the best pre-3.4.0 way to do unconditional cleanup for a provider.
# see https://groups.google.com/forum/#!topic/puppet-dev/Iqs5jEGfu_0
Expand All @@ -38,15 +38,27 @@ def evaluate
Puppet::Type.type(:windows_env).provide(:windows_env) do
desc "Manage Windows environment variables"

confine :feature => :windows_env
confine :osfamily => :windows
defaultfor :osfamily => :windows

# 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::SendMessageTimeout = Win32API.new('user32', 'SendMessageTimeout', 'LLLPLLP', 'L')
self::RegLoadKey = Win32API.new('Advapi32', 'RegLoadKey', 'LPP', 'L')
self::RegUnLoadKey = Win32API.new('Advapi32', 'RegUnLoadKey', 'LP', 'L')
# The 'windows_env' feature includes FFI. Here we need to be able to fully
# load the provider even if FFI is absent so that the catalog can continue
# (and hopefully install FFI).
if Puppet.features.windows_env?
module self::WinAPI
extend FFI::Library

ffi_convention :stdcall

ffi_lib :User32
attach_function :SendMessageTimeout, :SendMessageTimeoutA, [:uintptr_t, :uint, :pointer, :pointer, :uint, :uint, :pointer], :pointer

ffi_lib :Advapi32
attach_function :RegLoadKey, :RegLoadKeyA, [:uintptr_t, :pointer, :pointer], :long

attach_function :RegUnLoadKey, :RegUnLoadKeyA, [:uintptr_t, :pointer], :long
end
end

# Instances can load hives with #load_user_hive . The class takes care of
Expand All @@ -62,22 +74,25 @@ def self.post_resource_eval
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)
result = self::WinAPI.RegUnLoadKey(Win32::Registry::HKEY_USERS.hkey, user_sid)
end
end
end

def exists?
# For testing registry open result
_ERROR_FILE_NOT_FOUND = 2

if @resource[:user]
@reg_hive = Win32::Registry::HKEY_USERS
@user_sid = Puppet::Util::Windows::Security.name_to_sid(@resource[:user])
@user_sid = 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
if error.code == _ERROR_FILE_NOT_FOUND
load_user_hive
else
reg_fail("Can't access Environment for user '#{@resource[:user]}'. Opening", error)
Expand All @@ -97,7 +112,7 @@ def exists?
# key.read returns '[type, data]' and must be used instead of [] because [] expands %variables%.
@reg_hive.open(@reg_path) { |key| @value = key.read(@resource[:variable])[1] }
rescue Win32::Registry::Error => error
if error.code == Windows::Error::ERROR_FILE_NOT_FOUND
if error.code == _ERROR_FILE_NOT_FOUND
debug "Environment variable #{@resource[:variable]} not found"
return false
end
Expand Down Expand Up @@ -198,6 +213,26 @@ def type=(newtype)

private

# name_to_sid moved from 'security' to 'sid' in Puppet 3.7.
# 'puppet/util/windows/sid' is not guaranteed to exist on older 3.x Puppets.
use_util_windows_sid = false
begin
require 'puppet/util/windows/sid'
if Puppet::Util::Windows::SID.respond_to?(:name_to_sid)
use_util_windows_sid = true
end
rescue LoadError
end
if use_util_windows_sid
def name_to_sid(name)
Puppet::Util::Windows::SID.name_to_sid(name)
end
else
def name_to_sid(name)
Puppet::Util::Windows::Security.name_to_sid(name)
end
end

def reg_fail(action, error)
self.fail "#{action} '#{@reg_hive.name}:\\#{@reg_path}\\#{@resource[:variable]}' returned error #{error.code}: #{error.message}"
end
Expand Down Expand Up @@ -231,8 +266,8 @@ def broadcast_changes
debug "Broadcasting changes to environment"
_HWND_BROADCAST = 0xFFFF
_WM_SETTINGCHANGE = 0x1A
self.class::SendMessageTimeout.call(_HWND_BROADCAST, _WM_SETTINGCHANGE, 0, 'Environment', 2, @resource[:broadcast_timeout], 0)
end
self.class::WinAPI.SendMessageTimeout(_HWND_BROADCAST, _WM_SETTINGCHANGE, nil, 'Environment', 2, @resource[:broadcast_timeout], nil)
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
Expand All @@ -251,7 +286,7 @@ def load_user_hive
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)
result = self.class::WinAPI.RegLoadKey(Win32::Registry::HKEY_USERS.hkey, @user_sid, ntuser_path)
unless result == 0
raise Puppet::Util::Windows::Error.new("Could not load registry hive for user '#{@resource[:user]}'", result)
end
Expand Down
2 changes: 1 addition & 1 deletion metadata.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "badgerious-windows_env",
"version": "2.1.0",
"version": "2.2.0",
"author": "badgerious",
"summary": "Manages Windows environment variables",
"license": "Apache License, Version 2.0",
Expand Down