On Sun, Mar 22, 2009 at 2:24 AM, Luke Kanies <[email protected]> wrote:
>
> This is really fantastic - the code looks great, and provides a great
> starting point for people who want to provide other aspects of Windows
> support.
>
> Paul's already done a once-over on this, but there are a couple of
> other points worth making.
>
> On Mar 20, 2009, at 3:01 PM, finalprefix wrote:
>
> >
> > 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
> >
>
> All of this code was completely redone in the master branch, so I'm
> interested in seeing how your rebase looks; can you publish it?
Done. You should be able to see my changes now.
>
> All of the traps should be in the daemon.rb class, I think.
I haven't fixed the traps in daemon.rb. I guess I'm being driven right
now by whether or not I can run puppet. If I don't hit a choke point,
I don't fix it, yet. And after rebasing with the master, I haven't hit
any issues with the traps in daemon.rb.
> > 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
>
> You should have a 'confine' here that fkeeps this from being used
> anywhere that doesn't have Windows::Group; I think it's something like:
>
> confine :operatingsystem => :windows
Done.
> > 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'
>
> Same as above, you should use 'confine' to do this restriction, rather
> than raising this exception. These files get loaded by every host, in
> order to find the default provider, so this exception would get
> triggered on every host.
Will do.
> > 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
>
> Given where this file is located, I think the module should be
> Puppet::Util::Windows. You can alias it in your providers for less
> typing, but when I see 'Windows::User', I assume it's an external
> dependency.
Done. Not as cleanly as I would like, but I'll refactor in a bit.
> > + 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
>
> This stuff all looks really good - minimal, straightforward, and
> provides a really clean interface for the providers.
>
> In fact, it might be possible to make this even simpler by making the
> Windows::User and Windows::Group classes into modules that could be
> included in the providers - there's a small amount of duplication as
> it stands, but this would allow your modules to implement the minimal
> provider API but outside the provider hierarchy, just like it
> currently is.
Very interesting idea.
> > + 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
>
> As Paul mentioned, we're doing all new test development in RSpec,
> whose tests are in spec/. We're glad to help you figure out how to
> use it, though. I'll skip reviewing the tests until they're ported to
> RSpec.
I haven't moved the tests to rspec yet, but I will before adding any
more providers.
> > +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
> >
> > >
>
>
> --
> I cannot and will not cut my conscience to fit this year's fashions.
> -- Lillian Hellman
> ---------------------------------------------------------------------
> Luke Kanies | http://reductivelabs.com | http://madstop.com
>
>
> >
--~--~---------~--~----~------------~-------~--~----~
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
-~----------~----~----~----~------~----~------~--~---