summaryrefslogtreecommitdiffstats
path: root/lib/puppet/provider/nameservice
diff options
context:
space:
mode:
authormccune <mccune@980ebf18-57e1-0310-9a29-db15c13687c0>2007-08-03 20:36:49 +0000
committermccune <mccune@980ebf18-57e1-0310-9a29-db15c13687c0>2007-08-03 20:36:49 +0000
commit5a5d2411265443a9331179c9410271e51b9d98f7 (patch)
treeca1a360b85943488d5744a35679bd7539e234407 /lib/puppet/provider/nameservice
parent08d89455aa848f95fcfa4ae35451c0a86563401d (diff)
downloadpuppet-5a5d2411265443a9331179c9410271e51b9d98f7.tar.gz
puppet-5a5d2411265443a9331179c9410271e51b9d98f7.tar.xz
puppet-5a5d2411265443a9331179c9410271e51b9d98f7.zip
DirectoryService provider for users and groups. Alternative to netinfo, as apple has indicated NetInfo may go away at some point in the future. It might happen in October.
FIXME: implement groups and groups= instances methods for Puppet::Type::User::ProviderDirectoryservice git-svn-id: https://reductivelabs.com/svn/puppet/trunk@2739 980ebf18-57e1-0310-9a29-db15c13687c0
Diffstat (limited to 'lib/puppet/provider/nameservice')
-rw-r--r--lib/puppet/provider/nameservice/directoryservice.rb344
1 files changed, 344 insertions, 0 deletions
diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb
new file mode 100644
index 000000000..f7ab5ce12
--- /dev/null
+++ b/lib/puppet/provider/nameservice/directoryservice.rb
@@ -0,0 +1,344 @@
+# Created by Jeff McCune on 2007-07-22
+# Copyright (c) 2007. All rights reserved.
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation (version 2 of the License)
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston MA 02110-1301 USA
+
+require 'puppet'
+require 'puppet/provider/nameservice'
+
+class Puppet::Provider::NameService
+class DirectoryService < Puppet::Provider::NameService
+ # JJM: Dive into the eigenclass
+ class << self
+ # JJM: This allows us to pass information when calling
+ # Puppet::Type.type
+ # e.g. Puppet::Type.type(:user).provide :directoryservice, :ds_path => "Users"
+ # This is referenced in the get_ds_path class method
+ attr_writer :ds_path
+ end
+
+ # JJM 2007-07-24: Not yet sure what initvars() does. I saw it in netinfo.rb
+ # I do know, however, that it makes methods "work" =)
+ # e.g. addcmd isn't available if this method call isn't present.
+ #
+ # JJM: Also, where this method is defined seems to impact the visibility
+ # of methods. If I put initvars after commands, confine and defaultfor,
+ # then getinfo is called from the parent class, not this class.
+ initvars()
+
+ commands :dscl => "/usr/bin/dscl"
+ confine :operatingsystem => :darwin
+ # JJM FIXME: This will need to be the default around October 2007.
+ # defaultfor :operatingsystem => :darwin
+
+
+ # JJM 2007-07-25: This map is used to map NameService attributes to their
+ # corresponding DirectoryService attribute names.
+ # See: http://images.apple.com/server/docs/Open_Directory_v10.4.pdf
+ # JJM: Note, this is de-coupled from the Puppet::Type, and must
+ # be actively maintained. There may also be collisions with different
+ # types (Users, Groups, Mounts, Hosts, etc...)
+ @@ds_to_ns_attribute_map = {
+ 'RecordName' => :name,
+ 'PrimaryGroupID' => :gid,
+ 'NFSHomeDirectory' => :home,
+ 'UserShell' => :shell,
+ 'UniqueID' => :uid,
+ 'RealName' => :comment,
+ 'Password' => :password,
+ }
+ # JJM The same table as above, inverted.
+ @@ns_to_ds_attribute_map = {
+ :name => 'RecordName',
+ :gid => 'PrimaryGroupID',
+ :home => 'NFSHomeDirectory',
+ :shell => 'UserShell',
+ :uid => 'UniqueID',
+ :comment => 'RealName',
+ :password => 'Password',
+ }
+
+ def self.instances
+ # JJM Class method that provides an array of instance objects of this
+ # type.
+
+ # JJM: Properties are dependent on the Puppet::Type we're managine.
+ type_property_array = [:name] + @resource_type.validproperties
+ # JJM: No sense reporting the password. It's hashed.
+ type_property_array.delete(:password) if type_property_array.include? :password
+
+ # Create a new instance of this Puppet::Type for each object present
+ # on the system.
+ list_all_present.collect do |name_string|
+ self.new(single_report(name_string, *type_property_array))
+ end
+ end
+
+ def self.get_ds_path
+ # JJM: 2007-07-24 This method dynamically returns the DS path we're concerned with.
+ # For example, if we're working with an user type, this will be /Users
+ # with a group type, this will be /Groups.
+ # @ds_path is an attribute of the class itself.
+ if defined? @ds_path
+ return @ds_path
+ else
+ # JJM: "Users" or "Groups" etc ... (Based on the Puppet::Type)
+ # Remember this is a class method, so self.class is Class
+ # Also, @resource_type seems to be the reference to the
+ # Puppet::Type this class object is providing for.
+ return @resource_type.name.to_s.capitalize + "s"
+ end
+ end
+
+ def self.list_all_present
+ # JJM: List all objects of this Puppet::Type already present on the system.
+ begin
+ dscl_output = execute(get_exec_preamble("-list"))
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not get %s list from DirectoryService" % [ @resource_type.name.to_s ]
+ end
+ return dscl_output.split("\n")
+ end
+
+ def self.single_report(resource_name, *type_properties)
+ # JJM 2007-07-24:
+ # Given a the name of an object and a list of properties of that
+ # object, return all property values in a hash.
+ #
+ # This class method returns nil if the object doesn't exist
+ # Otherwise, it returns a hash of the object properties.
+
+ all_present_str_array = list_all_present()
+
+ # JJM: Return nil if the named object isn't present.
+ return nil unless all_present_str_array.include? resource_name
+
+ dscl_vector = get_exec_preamble("-read", resource_name)
+ begin
+ dscl_output = execute(dscl_vector)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not get report. command execution failed."
+ end
+
+ # JJM: We need a new hash to return back to our caller.
+ attribute_hash = Hash.new
+
+ # JJM: First, the output string goes into an array.
+ # Then, the each array element is split
+ # If you want to figure out what this is doing, I suggest
+ # ruby-debug, and stepping through it.
+ dscl_output.split("\n").each do |line|
+ # JJM: Split the attribute name and the list of values.
+ ds_attribute, ds_values_string = line.split(':')
+
+ # JJM: skip this attribute line if the Puppet::Type doesn't care about it.
+ next unless (@@ds_to_ns_attribute_map.keys.include?(ds_attribute) \
+ and type_properties.include? @@ds_to_ns_attribute_map[ds_attribute])
+
+ # JJM: We asked dscl to output url encoded values so we're able
+ # to machine parse on whitespace. We need to urldecode:
+ # " Jeff%20McCune John%20Doe " => ["Jeff McCune", "John Doe"]
+ ds_value_array = ds_values_string.scan(/[^\s]+/).collect do |v|
+ url_decoded_value = CGI::unescape v
+ if url_decoded_value =~ /^[-0-9]+$/
+ url_decoded_value.to_i
+ else
+ url_decoded_value
+ end
+ end
+
+ # JJM: Finally, we're able to build up our attribute hash.
+ # Remember, the hash is keyed by NameService attribute names,
+ # not DirectoryService attribute names.
+ # NOTE: We're also sort of cheating here... DirectoryService
+ # is robust enough to allow multiple values for almost every
+ # attribute in the system. Traditional NameService things
+ # really don't handle this case, so we'll always pull thet first
+ # value returned from DirectoryService.
+ # THERE MAY BE AN ORDERING ISSUE HERE, but I think it's ok...
+ attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value_array[0]
+ end
+ return attribute_hash
+ end
+
+ def self.get_exec_preamble(ds_action, resource_name = nil)
+ # JJM 2007-07-24
+ # DSCL commands are often repetitive and contain the same positional
+ # arguments over and over. See http://developer.apple.com/documentation/Porting/Conceptual/PortingUnix/additionalfeatures/chapter_10_section_9.html
+ # for an example of what I mean.
+ # This method spits out proper DSCL commands for us.
+ # We EXPECT name to be @resource[:name] when called from an instance object.
+
+ # JJM: With dscl, the domain "/" is always the default local domain.
+ # The domain /Search will search all domains, and you may
+ # get at specific domains with /LDAPv3/server1.foobar.com,
+ # /LDAPv3/server2.foobar.com, etc...
+ command_vector = [ command(:dscl), "-url", "/" ]
+ # JJM: The actual action to perform. See "man dscl"
+ # Common actiosn: -create, -delete, -merge, -append, -passwd
+ command_vector << ds_action
+ # JJM: get_ds_path will spit back "Users" or "Groups",
+ # etc... Depending on the Puppet::Type of our self.
+ if resource_name
+ command_vector << "/%s/%s" % [ get_ds_path, resource_name ]
+ else
+ command_vector << "/%s" % [ get_ds_path ]
+ end
+ # JJM: This returns most of the preamble of the command.
+ # e.g. 'dscl / -create /Users/mccune'
+ return command_vector
+ end
+
+ def ensure=(ensure_value)
+ super
+
+ debugger
+
+ # JJM: Modeled after nameservice/netinfo.rb, we need to
+ # loop over all valid properties for the type we're managing
+ # and call the method which sets that property value
+ # Like netinfo, dscl can't create everything at once, afaik.
+ if ensure_value == :present
+ @resource.class.validproperties.each do |name|
+ next if name == :ensure
+ next unless val = @resource.should(name) || autogen(name)
+ # JJM: This calls the method.
+ self.send(name.to_s + "=", val)
+ end
+ end
+ end
+
+ def password=(passphrase)
+ # JJM: Setting the password is a special case. We don't just
+ # set the attribute because we need to update the password
+ # databases.
+ # FIRST, make sure the AuthenticationAuthority is ;ShadowHash; If
+ # we don't do this, we don't get a shadow hash account. ("Obviously...")
+ dscl_vector = self.class.get_exec_preamble("-create", @resource[:name])
+ dscl_vector << "AuthenticationAuthority" << ";ShadowHash;"
+ begin
+ dscl_output = execute(dscl_vector)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not set AuthenticationAuthority."
+ end
+
+ # JJM: Second, we need to actually set the password. dscl does
+ # some magic, creating the proper hash for us based on the
+ # AuthenticationAuthority attribute, set above.
+ dscl_vector = self.class.get_exec_preamble("-passwd", @resource[:name])
+ dscl_vector << passphrase
+ # JJM: Should we not log the password string? This may be a security
+ # risk...
+ begin
+ dscl_output = execute(dscl_vector)
+ rescue Puppet::ExecutionFailure => detail
+ raise Puppet::Error, "Could not set password using command vector: %{dscl_vector.inspect}"
+ end
+ end
+
+ # JJM: nameservice.rb defines methods for each attribute of the type.
+ # We implement these methods here, by implementing get() and set()
+ # See the resource_type= method defined in nameservice.rb
+ # I'm not sure what the implications are of doing things this way.
+ # It was a bit difficult to sort out what was happening in my head,
+ # but ruby-debug makes this process much more transparent.
+ #
+ def set(property, value)
+ # JJM: As it turns out, the set method defined in our parent class
+ # is fine. It just calls the modifycmd() method, which
+ # I'll implement here.
+ super
+ end
+
+ def get(param)
+ hash = getinfo(false)
+ if hash
+ return hash[param]
+ else
+ return :absent
+ end
+ end
+
+ def modifycmd(property, value)
+ # JJM: This method will assemble a exec vector which modifies
+ # a single property and it's value using dscl.
+ # JJM: With /usr/bin/dscl, the -create option will destroy an
+ # existing property record if it exists
+ exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name])
+ # JJM: The following line just maps the NS name to the DS name
+ # e.g. { :uid => 'UniqueID' }
+ exec_arg_vector << @@ns_to_ds_attribute_map[symbolize(property)]
+ # JJM: The following line sends the actual value to set the property to
+ exec_arg_vector << value.to_s
+ return exec_arg_vector
+ end
+
+ def addcmd
+ # JJM 2007-07-24:
+ # - addcmd returns an array to be executed to create a new object.
+ # - This method is probably being called from the
+ # ensure= method in nameservice.rb, or here...
+ # - This should only be called if the object doesn't exist.
+ # JJM: Blame nameservice.rb for the terse method name. =)
+ #
+ self.class.get_exec_preamble("-create", @resource[:name])
+ end
+
+ def deletecmd
+ # JJM: Like addcmd, only called when deleting the object itself
+ # Note, this isn't used to delete properties of the object,
+ # at least that's how I understand it...
+ self.class.get_exec_preamble("-delete", @resource[:name])
+ end
+
+ def getinfo(refresh = false)
+ # JJM 2007-07-24:
+ # Override the getinfo method, which is also defined in nameservice.rb
+ # This method returns and sets @infohash, which looks like:
+ # (NetInfo provider, user type...)
+ # @infohash = {:comment=>"Jeff McCune", :home=>"/Users/mccune",
+ # :shell=>"/bin/zsh", :password=>"********", :uid=>502, :gid=>502,
+ # :name=>"mccune"}
+ #
+ # I'm not re-factoring the name "getinfo" because this method will be
+ # most likely called by nameservice.rb, which I didn't write.
+ if refresh or (! defined?(@property_value_cache_hash) or ! @property_value_cache_hash)
+ # JJM 2007-07-24: OK, there's a bit of magic that's about to
+ # happen... Let's see how strong my grip has become... =)
+ #
+ # self is a provider instance of some Puppet::Type, like
+ # Puppet::Type::User::ProviderDirectoryservice for the case of the
+ # user type and this provider.
+ #
+ # self.class looks like "user provider directoryservice", if that
+ # helps you ...
+ #
+ # self.class.resource_type is a reference to the Puppet::Type class,
+ # probably Puppet::Type::User or Puppet::Type::Group, etc...
+ #
+ # self.class.resource_type.validproperties is a class method,
+ # returning an Array of the valid properties of that specific
+ # Puppet::Type.
+ #
+ # So... something like [:comment, :home, :password, :shell, :uid,
+ # :groups, :ensure, :gid]
+ #
+ # Ultimately, we add :name to the list, delete :ensure from the
+ # list, then report on the remaining list. Pretty whacky, ehh?
+ type_properties = [:name] + self.class.resource_type.validproperties
+ type_properties.delete(:ensure) if type_properties.include? :ensure
+ @property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties)
+ end
+ return @property_value_cache_hash
+ end
+end
+end