diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index 973a62f6e..e2e68b2ca 100644 --- a/lib/puppet/provider/nameservice/directoryservice.rb +++ b/lib/puppet/provider/nameservice/directoryservice.rb @@ -1,338 +1,340 @@ # 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(':') + + # Split sets the values to nil if there's nothing after the : + ds_values_string ||= "" # 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]) + 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. # There are two ways to specify paths in 10.5. See man dscl. 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 # 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 diff --git a/spec/unit/file_serving/metadata.rb b/spec/unit/file_serving/metadata.rb index f7ab0c8d6..d31dd21f0 100755 --- a/spec/unit/file_serving/metadata.rb +++ b/spec/unit/file_serving/metadata.rb @@ -1,133 +1,134 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/file_serving/metadata' describe Puppet::FileServing::Metadata do it "should should be a subclass of FileBase" do Puppet::FileServing::Metadata.superclass.should equal(Puppet::FileServing::FileBase) end it "should indirect file_metadata" do Puppet::FileServing::Metadata.indirection.name.should == :file_metadata end it "should should include the IndirectionHooks module in its indirection" do Puppet::FileServing::Metadata.indirection.metaclass.included_modules.should include(Puppet::FileServing::IndirectionHooks) end end describe Puppet::FileServing::Metadata, " when finding the file to use for setting attributes" do before do @metadata = Puppet::FileServing::Metadata.new("my/path") @full = "/base/path/my/path" @metadata.path = @full # Use a symlink because it's easier to test -- no checksumming @stat = stub "stat", :uid => 10, :gid => 20, :mode => 0755, :ftype => "symlink" end it "should accept a base path path to which the file should be relative" do File.expects(:lstat).with(@full).returns @stat File.expects(:readlink).with(@full).returns "/what/ever" @metadata.collect_attributes end it "should use the set base path if one is not provided" do File.expects(:lstat).with(@full).returns @stat File.expects(:readlink).with(@full).returns "/what/ever" @metadata.collect_attributes() end it "should fail if a base path is neither set nor provided" do proc { @metadata.collect_attributes() }.should raise_error(Errno::ENOENT) end it "should raise an exception if the file does not exist" do File.expects(:lstat).with(@full).raises(Errno::ENOENT) proc { @metadata.collect_attributes()}.should raise_error(Errno::ENOENT) end end describe Puppet::FileServing::Metadata, " when collecting attributes" do before do @path = "/my/file" @stat = stub 'stat', :uid => 10, :gid => 20, :mode => 0755, :ftype => "file" File.stubs(:lstat).returns(@stat) @filehandle = mock 'filehandle' - @filehandle.expects(:each_line).yields("some content\n") + #@filehandle.expects(:read).with(512).returns("some content\n").then.returns(nil) File.stubs(:open).with(@path, 'r').yields(@filehandle) @checksum = Digest::MD5.hexdigest("some content\n") @metadata = Puppet::FileServing::Metadata.new("file", :path => "/my/file") + @metadata.expects(:md5_file).returns(@checksum) @metadata.collect_attributes end # LAK:FIXME This should actually change at some point it "should set the owner by id" do @metadata.owner.should be_instance_of(Fixnum) end # LAK:FIXME This should actually change at some point it "should set the group by id" do @metadata.group.should be_instance_of(Fixnum) end it "should set the owner to the file's current owner" do @metadata.owner.should == 10 end it "should set the group to the file's current group" do @metadata.group.should == 20 end it "should set the mode to a string version of the mode in octal" do @metadata.mode.should == "755" end it "should set the mode to the file's current mode" do @metadata.mode.should == "755" end it "should set the checksum to the file's current checksum" do @metadata.checksum.should == "{md5}" + @checksum end it "should default to a checksum of type MD5" do @metadata.checksum.should == "{md5}" + @checksum end end describe Puppet::FileServing::Metadata, " when pointing to a symlink" do it "should store the destination of the symlink in :destination if links are :manage" do file = Puppet::FileServing::Metadata.new("mykey", :links => :manage, :path => "/base/path/my/file") File.expects(:lstat).with("/base/path/my/file").returns stub("stat", :uid => 1, :gid => 2, :ftype => "symlink", :mode => 0755) File.expects(:readlink).with("/base/path/my/file").returns "/some/other/path" file.collect_attributes file.destination.should == "/some/other/path" end it "should not collect the checksum" do file = Puppet::FileServing::Metadata.new("my/file", :links => :manage, :path => "/base/path/my/file") File.expects(:lstat).with("/base/path/my/file").returns stub("stat", :uid => 1, :gid => 2, :ftype => "symlink", :mode => 0755) File.expects(:readlink).with("/base/path/my/file").returns "/some/other/path" file.collect_attributes file.checksum.should be_nil end end describe Puppet::FileServing::Metadata, " when converting from yaml" do # LAK:FIXME This isn't in the right place, but we need some kind of # control somewhere that requires that all REST connections only pull # from the file-server, thus guaranteeing they go through our authorization # hook. it "should set the URI scheme to 'puppetmounts'" do pending "We need to figure out where this should be" end end