lib/puppet/provider/nameservice/directoryservice.rb in puppet-2.7.26 vs lib/puppet/provider/nameservice/directoryservice.rb in puppet-3.0.0.rc4

- old
+ new

@@ -1,13 +1,11 @@ require 'puppet' require 'puppet/provider/nameservice' require 'facter/util/plist' -require 'cgi' require 'fileutils' -class Puppet::Provider::NameService -class DirectoryService < Puppet::Provider::NameService +class Puppet::Provider::NameService::DirectoryService < Puppet::Provider::NameService # JJM: Dive into the singleton_class 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" @@ -30,41 +28,40 @@ # 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, - 'GeneratedUID' => :guid, - 'IPAddress' => :ip_address, - 'ENetAddress' => :en_address, - 'GroupMembership' => :members, - } + def ds_to_ns_attribute_map; self.class.ds_to_ns_attribute_map; end + def self.ds_to_ns_attribute_map + { + 'RecordName' => :name, + 'PrimaryGroupID' => :gid, + 'NFSHomeDirectory' => :home, + 'UserShell' => :shell, + 'UniqueID' => :uid, + 'RealName' => :comment, + 'Password' => :password, + 'GeneratedUID' => :guid, + 'IPAddress' => :ip_address, + 'ENetAddress' => :en_address, + 'GroupMembership' => :members, + } + end + # 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', - :guid => 'GeneratedUID', - :en_address => 'ENetAddress', - :ip_address => 'IPAddress', - :members => 'GroupMembership', - } + def ns_to_ds_attribute_map; self.class.ns_to_ds_attribute_map end + def self.ns_to_ds_attribute_map + @ns_to_ds_attribute_map ||= ds_to_ns_attribute_map.invert + end - @@password_hash_dir = "/var/db/shadow/hash" - @@users_plist_dir = '/var/db/dslocal/nodes/Default/users' + def self.password_hash_dir + '/var/db/shadow/hash' + end + def self.users_plist_dir + '/var/db/dslocal/nodes/Default/users' + end 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. @@ -94,20 +91,13 @@ return @macosx_version_major if defined?(@macosx_version_major) begin # Make sure we've loaded all of the facts Facter.loadfacts - if Facter.value(:macosx_productversion_major) - product_version_major = Facter.value(:macosx_productversion_major) - else - # TODO: remove this code chunk once we require Facter 1.5.5 or higher. - Puppet.warning("DEPRECATION WARNING: Future versions of the directoryservice provider will require Facter 1.5.5 or newer.") - product_version = Facter.value(:macosx_productversion) - fail("Could not determine OS X version from Facter") if product_version.nil? - product_version_major = product_version.scan(/(\d+)\.(\d+)./).join(".") - end - fail("#{product_version_major} is not supported by the directoryservice provider") if %w{10.0 10.1 10.2 10.3}.include?(product_version_major) + product_version_major = Facter.value(:macosx_productversion_major) + + fail("#{product_version_major} is not supported by the directoryservice provider") if %w{10.0 10.1 10.2 10.3 10.4}.include?(product_version_major) @macosx_version_major = product_version_major return @macosx_version_major rescue Puppet::ExecutionFailure => detail fail("Could not determine OS X version: #{detail}") end @@ -122,61 +112,21 @@ fail("Could not get #{@resource_type.name} list from DirectoryService") end dscl_output.split("\n") end - def self.parse_dscl_url_data(dscl_output) - # we need to construct a Hash from the dscl -url output to match - # that returned by the dscl -plist output for 10.5+ clients. - # - # Nasty assumptions: - # a) no values *end* in a colon ':', only keys - # b) if a line ends in a colon and the next line does start with - # a space, then the second line is a value of the first. - # c) (implied by (b)) keys don't start with spaces. - - dscl_plist = {} - dscl_output.split("\n").inject([]) do |array, line| - if line =~ /^\s+/ # it's a value - array[-1] << line # add the value to the previous key - else - array << line - end - array - end.compact - - dscl_output.each do |line| - # This should be a 'normal' entry. key and value on one line. - # We split on ': ' to deal with keys/values with a colon in them. - split_array = line.split(/:\s+/) - key = split_array.first - value = CGI::unescape(split_array.last.strip.chomp) - # We need to treat GroupMembership separately as it is currently - # the only attribute we care about multiple values for, and - # the values can never contain spaces (shortnames) - # We also make every value an array to be consistent with the - # output of dscl -plist under 10.5 - if key == "GroupMembership" - dscl_plist[key] = value.split(/\s/) - else - dscl_plist[key] = [value] - end - end - dscl_plist - end - def self.parse_dscl_plist_data(dscl_output) Plist.parse_xml(dscl_output) end def self.generate_attribute_hash(input_hash, *type_properties) attribute_hash = {} input_hash.keys.each do |key| ds_attribute = key.sub("dsAttrTypeStandard:", "") - 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]) ds_value = input_hash[key] - case @@ds_to_ns_attribute_map[ds_attribute] + case ds_to_ns_attribute_map[ds_attribute] when :members ds_value = ds_value # only members uses arrays so far when :gid, :uid # OS X stores objects like uid/gid as strings. # Try casting to an integer for these cases to be @@ -187,11 +137,11 @@ rescue ArgumentError ds_value = ds_value[0] end else ds_value = ds_value[0] end - attribute_hash[@@ds_to_ns_attribute_map[ds_attribute]] = ds_value + attribute_hash[ds_to_ns_attribute_map[ds_attribute]] = ds_value end # NBK: need to read the existing password here as it's not actually # stored in the user record. It is stored at a path that involves the # UUID of the user record for non-Mobile local acccounts. @@ -218,44 +168,32 @@ dscl_output = execute(dscl_vector) rescue Puppet::ExecutionFailure => detail fail("Could not get report. command execution failed.") end - # Two code paths is ugly, but until we can drop 10.4 support we don't - # have a lot of choice. Ultimately this should all be done using Ruby - # to access the DirectoryService APIs directly, but that's simply not - # feasible for a while yet. - if self.get_macosx_version_major > "10.4" - dscl_plist = self.parse_dscl_plist_data(dscl_output) - elsif self.get_macosx_version_major == "10.4" - dscl_plist = self.parse_dscl_url_data(dscl_output) - else - fail("Puppet does not support OS X versions < 10.4") - end + # (#11593) Remove support for OS X 10.4 and earlier + fail_if_wrong_version + dscl_plist = self.parse_dscl_plist_data(dscl_output) self.generate_attribute_hash(dscl_plist, *type_properties) end + def self.fail_if_wrong_version + fail("Puppet does not support OS X versions < 10.5") unless self.get_macosx_version_major >= "10.5" + 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. - # 10.4 doesn't support the -plist option for dscl, and 10.5 has a - # different format for the -url output with objects with spaces in - # their values. *sigh*. Use -url for 10.4 in the hope this can be - # deprecated one day, and use -plist for 10.5 and higher. - if self.get_macosx_version_major > "10.4" - command_vector = [ command(:dscl), "-plist", "." ] - elsif self.get_macosx_version_major == "10.4" - command_vector = [ command(:dscl), "-url", "." ] - else - fail("Puppet does not support OS X versions < 10.4") - end + # (#11593) Remove support for OS X 10.4 and earlier + fail_if_wrong_version + command_vector = [ command(:dscl), "-plist", "." ] # 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", @@ -273,11 +211,11 @@ def self.set_password(resource_name, guid, password_hash) # Use Puppet::Util::Package.versioncmp() to catch the scenario where a # version '10.10' would be < '10.7' with simple string comparison. This # if-statement only executes if the current version is less-than 10.7 if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1) - password_hash_file = "#{@@password_hash_dir}/#{guid}" + password_hash_file = "#{password_hash_dir}/#{guid}" begin File.open(password_hash_file, 'w') { |f| f.write(password_hash)} rescue Errno::EACCES => detail fail("Could not write to password hash file: #{detail}") end @@ -311,17 +249,17 @@ if password_hash.length != 136 fail("OS X 10.7 requires a Salted SHA512 hash password of 136 characters. \ Please check your password and try again.") end - if File.exists?("#{@@users_plist_dir}/#{resource_name}.plist") + if File.exists?("#{users_plist_dir}/#{resource_name}.plist") # If a plist already exists in /var/db/dslocal/nodes/Default/users, then # we will need to extract the binary plist from the 'ShadowHashData' # key, log the new password into the resultant plist's 'SALTED-SHA512' # key, and then save the entire structure back. users_plist = Plist::parse_xml(plutil( '-convert', 'xml1', '-o', '/dev/stdout', \ - "#{@@users_plist_dir}/#{resource_name}.plist")) + "#{users_plist_dir}/#{resource_name}.plist")) # users_plist['ShadowHashData'][0].string is actually a binary plist # that's nested INSIDE the user's plist (which itself is a binary # plist). If we encounter a user plist that DOESN'T have a # ShadowHashData field, create one. @@ -343,36 +281,36 @@ # Finally, we can convert the nested plist back to binary, embed it # into the user's plist, and convert the resultant plist back to # a binary plist. changed_plist = convert_xml_to_binary(converted_hash_plist) users_plist['ShadowHashData'][0].string = changed_plist - Plist::Emit.save_plist(users_plist, "#{@@users_plist_dir}/#{resource_name}.plist") - plutil('-convert', 'binary1', "#{@@users_plist_dir}/#{resource_name}.plist") + Plist::Emit.save_plist(users_plist, "#{users_plist_dir}/#{resource_name}.plist") + plutil('-convert', 'binary1', "#{users_plist_dir}/#{resource_name}.plist") end end end def self.get_password(guid, username) # Use Puppet::Util::Package.versioncmp() to catch the scenario where a # version '10.10' would be < '10.7' with simple string comparison. This # if-statement only executes if the current version is less-than 10.7 if (Puppet::Util::Package.versioncmp(get_macosx_version_major, '10.7') == -1) password_hash = nil - password_hash_file = "#{@@password_hash_dir}/#{guid}" + password_hash_file = "#{password_hash_dir}/#{guid}" if File.exists?(password_hash_file) and File.file?(password_hash_file) fail("Could not read password hash file at #{password_hash_file}") if not File.readable?(password_hash_file) f = File.new(password_hash_file) password_hash = f.read f.close end password_hash else - if File.exists?("#{@@users_plist_dir}/#{username}.plist") + if File.exists?("#{users_plist_dir}/#{username}.plist") # If a plist exists in /var/db/dslocal/nodes/Default/users, we will # extract the binary plist from the 'ShadowHashData' key, decode the # salted-SHA512 password hash, and then return it. - users_plist = Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{@@users_plist_dir}/#{username}.plist")) + users_plist = Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{username}.plist")) if users_plist['ShadowHashData'] # users_plist['ShadowHashData'][0].string is actually a binary plist # that's nested INSIDE the user's plist (which itself is a binary # plist). password_hash_plist = users_plist['ShadowHashData'][0].string @@ -465,18 +403,18 @@ end end def password=(passphrase) exec_arg_vector = self.class.get_exec_preamble("-read", @resource.name) - exec_arg_vector << @@ns_to_ds_attribute_map[:guid] + exec_arg_vector << ns_to_ds_attribute_map[:guid] begin guid_output = execute(exec_arg_vector) guid_plist = Plist.parse_xml(guid_output) # Although GeneratedUID like all DirectoryService values can be multi-valued # according to the schema, in practice user accounts cannot have multiple UUIDs # otherwise Bad Things Happen, so we just deal with the first value. - guid = guid_plist["dsAttrTypeStandard:#{@@ns_to_ds_attribute_map[:guid]}"][0] + guid = guid_plist["dsAttrTypeStandard:#{ns_to_ds_attribute_map[:guid]}"][0] self.class.set_password(@resource.name, guid, passphrase) rescue Puppet::ExecutionFailure => detail fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}") end end @@ -498,11 +436,11 @@ add_members(current_members, value) else 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[param.intern] + exec_arg_vector << ns_to_ds_attribute_map[param.intern] # JJM: The following line sends the actual value to set the property to exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail @@ -527,11 +465,11 @@ # This should be revisited if Puppet starts managing UUIDs for other platform # user records. guid = %x{/usr/bin/uuidgen}.chomp exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) - exec_arg_vector << @@ns_to_ds_attribute_map[:guid] << guid + exec_arg_vector << ns_to_ds_attribute_map[:guid] << guid begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set GeneratedUID for #{@resource.class.name} #{@resource.name}: #{detail}") end @@ -553,11 +491,11 @@ if value != "" and not value.nil? if property == :members add_members(nil, value) else exec_arg_vector = self.class.get_exec_preamble("-create", @resource[:name]) - exec_arg_vector << @@ns_to_ds_attribute_map[property.intern] + exec_arg_vector << ns_to_ds_attribute_map[property.intern] next if property == :password # skip setting the password here exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail @@ -645,8 +583,7 @@ @property_value_cache_hash[param] = @property_value_cache_hash[param].to_i if @property_value_cache_hash and @property_value_cache_hash.include?(param) end end @property_value_cache_hash end -end end