Hi, This patch has basic support for users and groups on Windows.
* It for version 0.24.7. * It does not include support for active directory. * It does not work with the 0.24.8 beta. The code is up at http://github.com/finalprefix/puppet/tree/0.24.7-win. I'd love any feedback you might have! Thanks, Joel. diff --git a/lib/puppet.rb b/lib/puppet.rb index 4b0091c..77dbdfe 100644 --- a/lib/puppet.rb +++ b/lib/puppet.rb @@ -190,53 +190,57 @@ module Puppet # Trap a couple of the main signals. This should probably be handled # in a way that anyone else can register callbacks for traps, but, eh. def self.settraps - [:INT, :TERM].each do |signal| - trap(signal) do - Puppet.notice "Caught #{signal}; shutting down" - Puppet.debug "Signal caught here:" - caller.each { |l| Puppet.debug l } - Puppet.shutdown - end - end - - # Handle restarting. - trap(:HUP) do - if client = @services.find { |s| s.is_a? Puppet::Network::Client.master } and client.running? - client.restart - else - Puppet.restart - end - end - - # Provide a hook for running clients where appropriate - trap(:USR1) do - done = 0 - Puppet.notice "Caught USR1; triggering client run" - @services.find_all { |s| s.is_a? Puppet::Network::Client }.each do |client| - if client.respond_to? :running? - if client.running? - Puppet.info "Ignoring running %s" % client.class - else - done += 1 - begin - client.runnow - rescue => detail - Puppet.err "Could not run client: %s" % detail - end - end - else - Puppet.info "Ignoring %s; cannot test whether it is running" % - client.class - end - end - - unless done > 0 - Puppet.notice "No clients were run" - end - end - - trap(:USR2) do - Puppet::Util::Log.reopen + begin + [:INT, :TERM].each do |signal| + trap(signal) do + Puppet.notice "Caught #{signal}; shutting down" + Puppet.debug "Signal caught here:" + caller.each { |l| Puppet.debug l } + Puppet.shutdown + end + end + + # Handle restarting. + trap(:HUP) do + if client = @services.find { |s| s.is_a? Puppet::Network::Client.master } and client.running? + client.restart + else + Puppet.restart + end + end + + # Provide a hook for running clients where appropriate + trap(:USR1) do + done = 0 + Puppet.notice "Caught USR1; triggering client run" + @services.find_all { |s| s.is_a? Puppet::Network::Client }.each do |client| + if client.respond_to? :running? + if client.running? + Puppet.info "Ignoring running %s" % client.class + else + done += 1 + begin + client.runnow + rescue => detail + Puppet.err "Could not run client: %s" % detail + end + end + else + Puppet.info "Ignoring %s; cannot test whether it is running" % + client.class + end + end + + unless done > 0 + Puppet.notice "No clients were run" + end + end + + trap(:USR2) do + Puppet::Util::Log.reopen + end + rescue ArgumentError => e + raise if e.to_s.index('SIGHUP') == nil end end diff --git a/lib/puppet/provider/group/groupadd_win.rb b/lib/puppet/ provider/group/groupadd_win.rb new file mode 100644 index 0000000..c1fa7f4 --- /dev/null +++ b/lib/puppet/provider/group/groupadd_win.rb @@ -0,0 +1,30 @@ +require 'puppet/util/windows_system' + +Puppet::Type.type(:group).provide :groupadd_win do + desc "Group management for windows" + + defaultfor :operatingsystem => :windows + + has_features :manages_members + + def members + Windows::Group.new(@resource[:name]).members + end + + def members=(members) + Windows::Group.new(@resource[:name]).set_members(members) + end + + def create + group = Windows::Group.create(@resource[:name]) + group.set_members(@resource[:members]) + end + + def exists? + Windows::Group.exists?(@resource[:name]) + end + + def delete + Windows::Group.delete(@resource[:name]) + end +end diff --git a/lib/puppet/provider/user/useradd_win.rb b/lib/puppet/ provider/user/useradd_win.rb new file mode 100644 index 0000000..5f9b969 --- /dev/null +++ b/lib/puppet/provider/user/useradd_win.rb @@ -0,0 +1,42 @@ +require 'puppet/provider' +require 'puppet/util/windows_system' + +raise "ERROR: A windowsuser resource can only be configured on Windows. This OS is is #{Facter['kernel'].value}" if Facter ['kernel'].value != 'windows' + +Puppet::Type.type(:user).provide :useradd_win do + desc "User management for windows" + + defaultfor :operatingsystem => :windows + + has_features :manages_passwords + + def password + name, password = @resource[:name], @resource[:password] + Windows::User.new(name).password_is?(password) ?password :"" rescue :absent + end + + def password=(pwd) + Windows::User.new(@resource[:name]).password = @resource[:password] + end + + def groups + Windows::User.new(@resource[:name]).groups.join(',') rescue :absent + end + + def groups=(groups) + Windows::User.new(@resource[:name]).set_groups(groups) + end + + def create + user = Windows::User.create(@resource[:name], @resource[:password]) + user.set_groups(@resource[:groups], @resource[:membership] == :minimum) + end + + def exists? + return Windows::User.exists?(@resource[:name]) + end + + def delete + Windows::User.delete(@resource[:name]) + end +end diff --git a/lib/puppet/util/windows_system.rb b/lib/puppet/util/ windows_system.rb new file mode 100644 index 0000000..83bc52e --- /dev/null +++ b/lib/puppet/util/windows_system.rb @@ -0,0 +1,205 @@ +if Facter.operatingsystem == "windows" + require 'win32ole' + require 'Win32API' +end + +module ADSI + def ADSI.connectable?(uri) + begin + adsi_obj = WIN32OLE.connect(uri) + return adsi_obj != nil; + rescue + end + + return false + end +end + +module Windows + class Resource + def Resource.uri(resource_name) + "#{Computer.resource_uri}/#{resource_name}" + end + end + + class User + def initialize(username, native_adsi_obj = nil) + @username = username + @user = native_adsi_obj + end + + def user + @user = WIN32OLE.connect(User.resource_uri(@username)) if @user == nil + return @user + end + + def password_is?(password) + fLOGON32_LOGON_NETWORK_CLEARTEXT = 8 + fLOGON32_PROVIDER_DEFAULT = 0 + + logon_user = Win32API.new("advapi32", "LogonUser", ['P', 'P', 'P', 'L', 'L', 'P'], 'L') + close_handle = Win32API.new("kernel32", "CloseHandle", ['P'], 'V') + + token = ' ' * 4 + if logon_user.call(@username, "", password, fLOGON32_LOGON_NETWORK_CLEARTEXT, fLOGON32_PROVIDER_DEFAULT, token) == 1 + close_handle.call(token.unpack('L')[0]) + return true + end + + return false + end + + def add_flag(flag_name, value) + flag = 0 + + begin + flag = user.Get(flag_name) + rescue + end + + user.Put(flag_name, flag | value) + user.SetInfo + end + + def password=(password) + user.SetPassword(password) + user.SetInfo + + fADS_UF_DONT_EXPIRE_PASSWD = 0x10000 + add_flag("UserFlags", fADS_UF_DONT_EXPIRE_PASSWD) + end + + def groups + groups = [] + user.Groups.each {|group| groups << group.name } + return groups + end + + def add_to_groups(group_names) + group_names.each {|name| Group.new(name).add_user(@username) } if group_names.length > 0 + end + + def remove_from_groups(group_names) + group_names.each {|name| Group.new(name).remove_user(@username) } if group_names.length > 0 + end + + def set_groups(names, minimal = true) + return if names == nil || names.strip.length == 0 + + names = names.strip.split(',') + current_groups = groups + + names.find_all {|name| !current_groups.include?(name) }.tap {| names_to_add| add_to_groups(names_to_add) } + current_groups.find_all {|name| !names.include?(name) }.tap {| names_to_remove| remove_from_groups(names_to_remove) } if minimal == false + end + + def User.resource_uri(username) + return "#{Resource.uri(username)},user" + end + + def User.exists?(username) + return ADSI::connectable?(User.resource_uri(username)) + end + + def User.create(username, password) + User.new(username, Computer.create("user", username)).tap {|user| user.password = password; yield user if block_given? } + end + + def User.delete(username) + Computer.delete("user", username) + end + end + + class Group + def initialize(groupname, native_adsi_obj = nil) + @groupname = groupname + @group = native_adsi_obj + end + + def resource_uri + Group.resource_uri(@groupname) + end + + def Group.resource_uri(name) + "#{Resource.uri(name)},group" + end + + def group + @group = WIN32OLE.connect(resource_uri) if @group == nil + return @group + end + + def add_user(username) + group.Add(User.resource_uri(username)) + group.SetInfo + end + + def remove_user(username) + group.Remove(User.resource_uri(username)) + group.SetInfo + end + + def add_member(name) + group.Add(Resource.uri(name)) + group.SetInfo + end + + def remove_member(name) + group.Remove(Resource.uri(name)) + group.SetInfo + end + + def members + list = [] + group.Members.each {|member| list << member.Name } + list + end + + def set_members(members) + return nil if members == nil || members.length == 0 + + current_members = self.members + + members.inject([]) {|members_to_add, member| current_members.include?(member) ? members_to_add : members_to_add << member }.each {|member| add_member(member) } + current_members.inject([]) {|members_to_remove, member| members.include?(member) ? members_to_remove : members_to_remove << member }.each {|member| remove_member(member) } + end + + def Group.create(name) + Windows::Group.new(name, Computer.create("group", name)).tap {| group| yield group if block_given? } + end + + def Group.exists?(name) + return ADSI::connectable?(Group.resource_uri(name)) + end + + def Group.delete(name) + Computer.delete("group", name) + end + end + + class Computer + def Computer.name + name = " " * 128 + size = "128" + Win32API.new('kernel32','GetComputerName',['P','P'],'I').call (name,size) + return name.unpack("A*") + end + + def Computer.resource_uri + computer_name = Computer.name + return "WinNT://#{computer_name}" + end + + def Computer.api + return WIN32OLE.connect(Computer.resource_uri) + end + + def Computer.create(resource_type, name) + Computer.api.create(resource_type, name).SetInfo + end + + def Computer.delete(resource_type, name) + Computer.api.Delete(resource_type, name) + end + end +end diff --git a/test/ral/providers/group_win.rb b/test/ral/providers/ group_win.rb new file mode 100644 index 0000000..d341fbd --- /dev/null +++ b/test/ral/providers/group_win.rb @@ -0,0 +1,39 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../lib/puppettest' +require 'windowstest' + +require File.dirname(__FILE__) + '/../../../lib/puppet/provider/group/ groupadd_win.rb' + +class TestGroupProvider < Test::Unit::TestCase + include WindowsTest + + def group_provider(resource_configuration) + Puppet::Type::Group::ProviderGroupadd_win.new.tap {|provider| provider.resource = resource_configuration } + end + + def test_groupGetsCreated + groupname = "randomgroup" + register_group groupname + + expected_members = ["test1", "test2"] + mkusers(expected_members) + + provider = group_provider :name => groupname, :members => ['test1', 'test2'] + + assert_nothing_raised { provider.create } + assert_no_missing_member(group(groupname), expected_members) + end + + def test_groupMembersGetSet + groupname = "randomgroup" + group = mkgroup(groupname) + expected_members = ["test1", "test2"] + mkusers(expected_members) + + provider = group_provider :name => groupname, :members => ['test1', 'test2'] + + assert_nothing_raised { provider.members = ['test1', 'test2'] } + assert_no_missing_member(group, expected_members) + end +end diff --git a/test/ral/providers/syslog.rb b/test/ral/providers/ syslog.rb new file mode 100644 index 0000000..396b45c --- /dev/null +++ b/test/ral/providers/syslog.rb @@ -0,0 +1,4 @@ +# The purpose of this file is merely to fake the syslog module on windows. +# I placed it here since I was running the tests from here. +# I'll probably leave the file here until I wrap the windows event log in a syslogish module for windows. +# I haven't thought about it much. I'm open to better ideas too. \ No newline at end of file diff --git a/test/ral/providers/user/useradd_win.rb b/test/ral/ providers/user/useradd_win.rb new file mode 100644 index 0000000..11d31e7 --- /dev/null +++ b/test/ral/providers/user/useradd_win.rb @@ -0,0 +1,63 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../lib/puppettest' +require 'windowstest' + +require File.dirname(__FILE__) + '/../../../../lib/puppet/provider/ user/useradd_win.rb' + +class TestUserProvider < Test::Unit::TestCase + include WindowsTest + + def user_provider(resource_configuration) + Puppet::Type::User::ProviderUseradd_win.new.tap {|provider| provider.resource = resource_configuration } + end + + def test_userIsCreated + expected_groups = ["randomgroup1", "randomgroup2"] + mkgroups(expected_groups) + + username = "testuser" + register_user username + + password = "1234" + + provider = user_provider :name => username, :password => password, :groups => expected_groups.join(",") + + assert_nothing_raised { provider.create } + + user = Windows::User.new(username) + assert(user.password_is?(password), "Password of user #{username} should be #{password}") + user.groups.tap {|groups| + expected_groups.each {|expected_group| assert(groups.include? (expected_group), "User should be a member of #{expected_group}") } + assert(expected_groups.length == groups.length, "The user should be a member of #{expected_groups.length} groups.") + } + end + + def test_userGroupsAreSet + expected_groups = ["randomgroup1", "randomgroup2"] + mkgroups expected_groups + + username = "testuser" + mkuser username + + provider = user_provider :name => username + provider.groups = expected_groups.join(",") + + provider.groups.split(',').collect {|group| group.strip }.tap {| groups| + assert(groups.length == expected_groups.length, "The user should be a member of #{expected_groups.length} groups.") + groups.each {|group| assert(expected_groups.include?(group), "The user should be a member of #{group}") } + } + end + + def test_usersPasswordIsSet + username = "testuser" + password = "11112222" + + user = mkuser username, password + + provider = user_provider :name => username, :password => password + provider.password = password + + assert(user.password_is?(password), "User #{username}'s password should be #{password}.") + end +end diff --git a/test/ral/providers/windowstest.rb b/test/ral/providers/ windowstest.rb new file mode 100644 index 0000000..b9ed1e5 --- /dev/null +++ b/test/ral/providers/windowstest.rb @@ -0,0 +1,95 @@ +require File.dirname(__FILE__) + '/../../../lib/puppet/util/ windows_system.rb' + +module WindowsTest + class List + def initialize + @list = [] + end + + def clear + destroy + @list = [] + end + + def register(item) + @list << item + end + end + + class Groups < List + def destroy + @list.each {|group| + begin + Windows::Group.delete(group) + rescue + puts "Group #{group} not found" + end + } + end + end + + class Users < List + def destroy + @list.each {|user| + begin + Windows::User.delete(user) + rescue + puts "User #{user} not found" + end + } + end + end + + def helper_users + @users = Users.new if @users == nil + @users + end + + def helper_groups + @groups = Groups.new if @groups == nil + @groups + end + + def clear + helper_groups.clear + helper_users.clear + end + + def register_group(name) + helper_groups.register name + end + + def register_user(name) + helper_users.register name + end + + def mkuser(name, password = "1234567") + Windows::User.create(name, password) { register_user name } + end + + def mkgroup(name) + Windows::Group.create(name) { register_group name } + end + + def mkusers(names) + names.collect {|name| mkuser name } + end + + def mkgroups(names) + names.collect {|name| mkgroup name } + end + + def group(name) + Windows::Group.new(name) + end + + def assert_no_missing_member(group, expected_members) + group.members.tap {|members| + expected_members.each {|member| assert(members.include?(member), "# {member} should be a member") } + } + end + + def teardown + clear + end +end --~--~---------~--~----~------------~-------~--~----~ You received this message because you are subscribed to the Google Groups "Puppet Developers" group. To post to this group, send email to [email protected] To unsubscribe from this group, send email to [email protected] For more options, visit this group at http://groups.google.com/group/puppet-dev?hl=en -~----------~----~----~----~------~----~------~--~---
