This is my second pass on support for users and groups on Windows. I've moved tests to rspec, and refactored a little bit here and there. Nothing has changed since my first pass in essentials.
The code is to be found at http://github.com/finalprefix/puppet/tree/win. All comments and help are welcome. - Joel. diff --git a/lib/puppet/external/event-loop/event-loop.rb b/lib/puppet/external/event-loop/event-loop.rb index 17a520e..bb7b94c 100644 --- a/lib/puppet/external/event-loop/event-loop.rb +++ b/lib/puppet/external/event-loop/event-loop.rb @@ -74,9 +74,11 @@ class EventLoop @notify_src, @notify_snk = IO.pipe - # prevent file descriptor leaks - @notify_src.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) - @notify_snk.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + if !Puppet.features.windows? + # prevent file descriptor leaks + @notify_src.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + @notify_snk.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) + end @notify_src.will_block = false @notify_snk.will_block = false @@ -244,6 +246,9 @@ class IO def will_block= (wants_blocking) require "fcntl" + + return if Puppet.features.windows? + flags = fcntl(Fcntl::F_GETFL, 0) if wants_blocking flags &= ~Fcntl::O_NONBLOCK diff --git a/lib/puppet/feature/windows.rb b/lib/puppet/feature/windows.rb new file mode 100644 index 0000000..d5e4f38 --- /dev/null +++ b/lib/puppet/feature/windows.rb @@ -0,0 +1,16 @@ +require 'puppet/util/feature' + +Puppet.features.add(:windows) do + result = false + + if Facter.operatingsystem == 'windows' + begin + require 'win32ole' + require 'Win32API' + result = true + rescue + end + end + + result +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..398b076 --- /dev/null +++ b/lib/puppet/provider/group/groupadd_win.rb @@ -0,0 +1,29 @@ +Puppet::Type.type(:group).provide :groupadd_win do + desc "Group management for windows" + + confine :true => Puppet.features.windows? + require 'puppet/util/windows_system' + + has_features :manages_members + + def members + Puppet::Util::Windows::Group.new(@resource[:name]).members + end + + def members=(members) + Puppet::Util::Windows::Group.new(@resource[:name]).set_members(members) + end + + def create + group = Puppet::Util::Windows::Group.create(@resource[:name]) + group.set_members(@resource[:members]) + end + + def exists? + Puppet::Util::Windows::Group.exists?(@resource[:name]) + end + + def delete + Puppet::Util::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..e839104 --- /dev/null +++ b/lib/puppet/provider/user/useradd_win.rb @@ -0,0 +1,40 @@ +require 'puppet/provider' + +Puppet::Type.type(:user).provide :useradd_win do + desc "User management for windows" + + confine :true => Puppet.features.windows? + require 'puppet/util/windows_system' + + has_features :manages_passwords + + def password + name, password = @resource[:name], @resource[:password] + Puppet::Util::Windows::User.new(name).password_is?(password) ?password :"" rescue :absent + end + + def password=(pwd) + Puppet::Util::Windows::User.new(@resource[:name]).password = @resource[:password] + end + + def groups + Puppet::Util::Windows::User.new(@resource[:name]).groups.join(',') rescue :absent + end + + def groups=(groups) + Puppet::Util::Windows::User.new(@resource[:name]).set_groups(groups) + end + + def create + user = Puppet::Util::Windows::User.create(@resource[:name], @resource[:password]) + user.set_groups(@resource[:groups], @resource[:membership] == :minimum) + end + + def exists? + return Puppet::Util::Windows::User.exists?(@resource[:name]) + end + + def delete + Puppet::Util::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..1ea42a0 --- /dev/null +++ b/lib/puppet/util/windows_system.rb @@ -0,0 +1,215 @@ +if Puppet.features.windows? + require 'win32ole' + require 'Win32API' +end + +module Puppet::Util::ADSI + def self.connectable?(uri) + begin + adsi_obj = WIN32OLE.connect(uri) + return adsi_obj != nil; + rescue + end + + return false + end +end + +module Puppet::Util::Windows + include Puppet::Util::ADSI + + 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_to_add = names.find_all {|name| !current_groups.include?(name) } + add_to_groups(names_to_add) + + names_to_remove = current_groups.find_all {|name| !names.include?(name) } + 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 Puppet::Util::ADSI::connectable?(User.resource_uri(username)) + end + + def User.create(username, password) + newuser = new(username, Computer.create("user", username)) + newuser.password = password + yield newuser if block_given? + return newuser + 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) + newgroup = new(name, Computer.create("group", name)) + yield newgroup if block_given? + return newgroup + end + + def Group.exists?(name) + return Puppet::Util::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/spec/integration/provider/group/groupadd_win.rb b/spec/integration/provider/group/groupadd_win.rb new file mode 100644 index 0000000..51d1df7 --- /dev/null +++ b/spec/integration/provider/group/groupadd_win.rb @@ -0,0 +1,48 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +describe "Provider for windows groups" do + confine :true => Puppet.features.windows? + + require File.dirname(__FILE__) + '/../windowstest' + require File.dirname(__FILE__) + '/../../../../lib/puppet/provider/group/groupadd_win.rb' + + include WindowsTest + + def group_provider(resource_configuration) + provider = Puppet::Type::Group::ProviderGroupadd_win.new + provider.resource = resource_configuration + return provider + end + + after(:each) do + clear + end + + it 'should create a group with configured members' do + groupname = "randomgroup" + register_group groupname + + expected_members = ["test1", "test2"] + mkusers(expected_members) + + provider = group_provider :name => groupname, :members => ['test1', 'test2'] + provider.create + + should_have_no_missing_member(group(groupname), expected_members) + end + + it 'should set a groups members' do + groupname = "randomgroup" + expected_members = ["test1", "test2"] + + testgroup = mkgroup(groupname) + mkusers(expected_members) + + provider = group_provider :name => groupname, :members => ['test1', 'test2'] + provider.members = ['test1', 'test2'] + + should_have_no_missing_member(testgroup, expected_members) + end +end diff --git a/spec/integration/provider/user/useradd_win.rb b/spec/integration/provider/user/useradd_win.rb new file mode 100644 index 0000000..6b0eac1 --- /dev/null +++ b/spec/integration/provider/user/useradd_win.rb @@ -0,0 +1,69 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../../spec_helper' + +describe "Provider for windows users" do + confine :true => Puppet.features.windows? + + require File.dirname(__FILE__) + '/../windowstest' + require File.dirname(__FILE__) + '/../../../../lib/puppet/provider/user/useradd_win.rb' + + include WindowsTest + + def user_provider(resource_configuration) + provider = Puppet::Type::User::ProviderUseradd_win.new + provider.resource = resource_configuration + return provider + end + + after(:each) do + clear + end + + it 'should add a user with the given password and group membership' do + expected_groups = ["randomgroup1", "randomgroup2"] + username = "testuser" + password = "1234" + + mkgroups(expected_groups) + register_user username + + provider = user_provider :name => username, :password => password, :groups => expected_groups.join(",") + provider.create + + testuser = user(username) + testuser.password_is?(password).should be_true + + groups = testuser.groups + + expected_groups.each {|expected_group| groups.include?(expected_group).should be_true } + expected_groups.length.should be_eql(groups.length) + end + + it 'should set the group membership of an existing user' do + expected_groups = ["randomgroup1", "randomgroup2"] + username = "testuser" + + mkgroups expected_groups + mkuser username + + provider = user_provider :name => username + provider.groups = expected_groups.join(",") + + groups = provider.groups.split(',').collect {|group| group.strip } + groups.length.should be_eql(expected_groups.length) + groups.each {|group| expected_groups.include?(group).should be_true } + end + + it 'should set a users password' do + username = "testuser" + password = "11112222" + + testuser = mkuser username, password + + provider = user_provider :name => username, :password => password + provider.password = password + + testuser.password_is?(password).should be_true + end +end diff --git a/spec/integration/provider/windowstest.rb b/spec/integration/provider/windowstest.rb new file mode 100644 index 0000000..e667531 --- /dev/null +++ b/spec/integration/provider/windowstest.rb @@ -0,0 +1,109 @@ +require File.dirname(__FILE__) + '/../../../lib/puppet/util/windows_system.rb' + +module WindowsTest + include Puppet::Util::Windows + + class List + def initialize + @list = [] + end + + def clear + destroy + @list = [] + end + + def register(item) + @list << item + end + end + + class Groups < List + include Puppet::Util::Windows + + def destroy + @list.each {|group| + begin + Group.delete(group) + rescue + puts "Group #{group} not found" + end + } + end + end + + class Users < List + include Puppet::Util::Windows + + def destroy + @list.each {|user| + begin + 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") + User.create(name, password) { register_user name } + end + + def mkgroup(name) + 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) + Group.new(name) + end + + def user(name) + User.new(name) + end + + def assert_no_missing_member(group, expected_members) + members = group.members + expected_members.each {|member| assert(members.include?(member), "#{member} should be a member") } + end + + def should_have_no_missing_member(testgroup, expected_members) + members = testgroup.members + expected_members.each {|member| members.include?(member).should be_true } + end + + def teardown + clear + 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 --~--~---------~--~----~------------~-------~--~----~ 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 -~----------~----~----~----~------~----~------~--~---
