diff --git a/lib/puppet/provider.rb b/lib/puppet/provider.rb index 61b8f38ed..04d18233b 100644 --- a/lib/puppet/provider.rb +++ b/lib/puppet/provider.rb @@ -1,375 +1,404 @@ # The container class for implementations. class Puppet::Provider - include Puppet::Util::Execution include Puppet::Util include Puppet::Util::Errors include Puppet::Util::Warnings extend Puppet::Util::Warnings require 'puppet/provider/confiner' require 'puppet/provider/command' extend Puppet::Provider::Confiner Puppet::Util.logmethods(self, true) class << self # Include the util module so we have access to things like 'which' include Puppet::Util, Puppet::Util::Docs include Puppet::Util::Logging attr_accessor :name # The source parameter exists so that providers using the same # source can specify this, so reading doesn't attempt to read the # same package multiple times. attr_writer :source # LAK 2007-05-09: Keep the model stuff around for backward compatibility attr_reader :model attr_accessor :resource_type attr_writer :doc end # LAK 2007-05-09: Keep the model stuff around for backward compatibility attr_reader :model attr_accessor :resource + # Provide access to execution of arbitrary commands in providers. Execution methods are + # available on both the instance and the class of a provider because it seems that a lot of + # providers switch between these contexts fairly freely. + # + # @see Puppet::Util::Execution for how to use these methods + def execute(*args) + Puppet::Util::Execution.execute(*args) + end + + def self.execute(*args) + Puppet::Util::Execution.execute(*args) + end + + def execpipe(*args, &block) + Puppet::Util::Execution.execpipe(*args, &block) + end + + def self.execpipe(*args) + Puppet::Util::Execution.execpipe(*args) + end + + def execfail(*args) + Puppet::Util::Execution.execfail(*args) + end + + def self.execfail(*args) + Puppet::Util::Execution.execfail(*args) + end + ######### + def self.command(name) name = symbolize(name) if defined?(@commands) and command = @commands[name] # nothing elsif superclass.respond_to? :command and command = superclass.command(name) # nothing else raise Puppet::DevError, "No command #{name} defined for provider #{self.name}" end which(command) end # Define commands that are not optional. # # @param [Hash{String => String}] command_specs Named commands that the provider will # be executing on the system. Each command is specified with a name and the path of the executable. # (@see #has_command) def self.commands(command_specs) command_specs.each do |name, path| has_command(name, path) end end # Define commands that are optional. # # @param [Hash{String => String}] command_specs Named commands that the provider will # be executing on the system. Each command is specified with a name and the path of the executable. # (@see #has_command) def self.optional_commands(hash) hash.each do |name, target| has_command(name, target) do is_optional end end end # Define a single command # # A method will be generated on the provider that allows easy execution of the command. The generated # method can take arguments that will be passed through to the executable as the command line arguments # when it is run. # # has_command(:echo, "/bin/echo") # def some_method # echo("arg 1", "arg 2") # end # # # or # # has_command(:echo, "/bin/echo") do # is_optional # environment :HOME => "/var/tmp", :PWD => "/tmp" # end # # @param [Symbol] name Name of the command (will be the name of the generated method to call the command) # @param [String] path The path to the executable for the command # @yield A block that configures the command (@see Puppet::Provider::Command) def self.has_command(name, path, &block) name = symbolize(name) configuration = block_given? ? block : Proc.new {} command = CommandDefiner.define(name, path, self, &configuration) @commands[name] = command.executable # Now define the class and instance methods. create_class_and_instance_method(name) do |*args| return command.execute(*args) end end class CommandDefiner private_class_method :new def self.define(name, path, confiner, &block) definer = new(name, path, confiner) definer.instance_eval &block definer.command end def initialize(name, path, confiner) @name = name @path = path @optional = false @confiner = confiner @custom_environment = {} end def is_optional @optional = true end def environment(env) @custom_environment = @custom_environment.merge(env) end def command if not @optional @confiner.confine :exists => @path, :for_binary => true end Puppet::Provider::Command.new(@name, @path, Puppet::Util, Puppet::Util::Execution, { :custom_environment => @custom_environment }) end end # Is the provided feature a declared feature? def self.declared_feature?(name) defined?(@declared_features) and @declared_features.include?(name) end # Does this implementation match all of the default requirements? If # defaults are empty, we return false. def self.default? return false if @defaults.empty? if @defaults.find do |fact, values| values = [values] unless values.is_a? Array if fval = Facter.value(fact).to_s and fval != "" fval = fval.to_s.downcase.intern else return false end # If any of the values match, we're a default. if values.find do |value| fval == value.to_s.downcase.intern end false else true end end return false else return true end end # Store how to determine defaults. def self.defaultfor(hash) hash.each do |d,v| @defaults[d] = v end end def self.specificity (@defaults.length * 100) + ancestors.select { |a| a.is_a? Class }.length end def self.initvars @defaults = {} @commands = {} end # The method for returning a list of provider instances. Note that it returns providers, preferably with values already # filled in, not resources. def self.instances raise Puppet::DevError, "Provider #{self.name} has not defined the 'instances' class method" end # Create the methods for a given command. # # @deprecated Use {#commands}, {#optional_commands}, or {#has_command} instead. This was not meant to be part of a public API def self.make_command_methods(name) Puppet.deprecation_warning "Provider.make_command_methods is deprecated; use Provider.commands or Provider.optional_commands instead for creating command methods" # Now define a method for that command unless singleton_class.method_defined?(name) meta_def(name) do |*args| # This might throw an ExecutionFailure, but the system above # will catch it, if so. command = Puppet::Provider::Command.new(name, command(name), Puppet::Util, Puppet::Util::Execution) return command.execute(*args) end # And then define an instance method that just calls the class method. # We need both, so both instances and classes can easily run the commands. unless method_defined?(name) define_method(name) do |*args| self.class.send(name, *args) end end end end # Create getter/setter methods for each property our resource type supports. # They all get stored in @property_hash. This method is useful # for those providers that use prefetch and flush. def self.mkmodelmethods Puppet.deprecation_warning "Provider.mkmodelmethods is deprecated; use Provider.mk_resource_methods" mk_resource_methods end # Create getter/setter methods for each property our resource type supports. # They all get stored in @property_hash. This method is useful # for those providers that use prefetch and flush. def self.mk_resource_methods [resource_type.validproperties, resource_type.parameters].flatten.each do |attr| attr = symbolize(attr) next if attr == :name define_method(attr) do @property_hash[attr] || :absent end define_method(attr.to_s + "=") do |val| @property_hash[attr] = val end end end self.initvars def self.create_class_and_instance_method(name, &block) unless singleton_class.method_defined?(name) meta_def(name, &block) end unless method_defined?(name) define_method(name) do |*args| self.class.send(name, *args) end end end private_class_method :create_class_and_instance_method # Retrieve the data source. Defaults to the provider name. def self.source @source ||= self.name end # Does this provider support the specified parameter? def self.supports_parameter?(param) if param.is_a?(Class) klass = param else unless klass = resource_type.attrclass(param) raise Puppet::DevError, "'#{param}' is not a valid parameter for #{resource_type.name}" end end return true unless features = klass.required_features !!satisfies?(*features) end # def self.to_s # unless defined?(@str) # if self.resource_type # @str = "#{resource_type.name} provider #{self.name}" # else # @str = "unattached provider #{self.name}" # end # end # @str # end dochook(:defaults) do if @defaults.length > 0 return "Default for " + @defaults.collect do |f, v| "`#{f}` == `#{[v].flatten.join(', ')}`" end.join(" and ") + "." end end dochook(:commands) do if @commands.length > 0 return "Required binaries: " + @commands.collect do |n, c| "`#{c}`" end.join(", ") + "." end end dochook(:features) do if features.length > 0 return "Supported features: " + features.collect do |f| "`#{f}`" end.join(", ") + "." end end # Remove the reference to the resource, so GC can clean up. def clear @resource = nil @model = nil end # Retrieve a named command. def command(name) self.class.command(name) end # Get a parameter value. def get(param) @property_hash[symbolize(param)] || :absent end def initialize(resource = nil) if resource.is_a?(Hash) # We don't use a duplicate here, because some providers (ParsedFile, at least) # use the hash here for later events. @property_hash = resource elsif resource @resource = resource # LAK 2007-05-09: Keep the model stuff around for backward compatibility @model = resource @property_hash = {} else @property_hash = {} end end def name if n = @property_hash[:name] return n elsif self.resource resource.name else raise Puppet::DevError, "No resource and no name in property hash in #{self.class.name} instance" end end # Set passed params as the current values. def set(params) params.each do |param, value| @property_hash[symbolize(param)] = value end end def to_s "#{@resource}(provider=#{self.class.name})" end # Make providers comparable. include Comparable def <=>(other) # We can only have ordering against other providers. return nil unless other.is_a? Puppet::Provider # Otherwise, order by the providers class name. return self.class.name <=> other.class.name end end diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index 5281d59a3..77b59870d 100644 --- a/lib/puppet/provider/nameservice/directoryservice.rb +++ b/lib/puppet/provider/nameservice/directoryservice.rb @@ -1,598 +1,596 @@ require 'puppet' require 'puppet/provider/nameservice' require 'facter/util/plist' 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" # This is referenced in the get_ds_path class method attr_writer :ds_path attr_writer :macosx_version_major end initvars commands :dscl => "/usr/bin/dscl" commands :dseditgroup => "/usr/sbin/dseditgroup" commands :sw_vers => "/usr/bin/sw_vers" commands :plutil => '/usr/bin/plutil' confine :operatingsystem => :darwin 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...) 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. 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 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. type_property_array = [:name] + @resource_type.validproperties # 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. return @ds_path if defined?(@ds_path) # 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. @resource_type.name.to_s.capitalize + "s" end def self.get_macosx_version_major 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 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 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 fail("Could not get #{@resource_type.name} list from DirectoryService") end dscl_output.split("\n") 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]) ds_value = input_hash[key] 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 # consistent with the other providers and the group type # validation begin ds_value = Integer(ds_value[0]) 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 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. # Mobile Accounts are out of scope for this provider for now attribute_hash[:password] = self.get_password(attribute_hash[:guid], attribute_hash[:name]) if @resource_type.validproperties.include?(:password) and Puppet.features.root? attribute_hash 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 # NBK: shortcut the process if the resource is missing 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 fail("Could not get report. command execution failed.") 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. # (#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", # etc... Depending on the Puppet::Type of our self. if resource_name command_vector << "/#{get_ds_path}/#{resource_name}" else command_vector << "/#{get_ds_path}" end # JJM: This returns most of the preamble of the command. # e.g. 'dscl / -create /Users/mccune' command_vector end 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}" 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 # NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of # ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it # will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if # missing. Thus we make sure we only set ;ShadowHash; if it is missing, and # we can do this with the merge command. This allows people to continue to # use other custom AuthenticationAuthority attributes without stomping on them. # # There is a potential problem here in that we're only doing this when setting # the password, and the attribute could get modified at other times while the # hash doesn't change and so this doesn't get called at all... but # without switching all the other attributes to merge instead of create I can't # see a simple enough solution for this that doesn't modify the user record # every single time. This should be a rather rare edge case. (famous last words) dscl_vector = self.get_exec_preamble("-merge", resource_name) dscl_vector << "AuthenticationAuthority" << ";ShadowHash;" begin dscl_output = execute(dscl_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set AuthenticationAuthority.") end else # 10.7 uses salted SHA512 password hashes which are 128 characters plus # an 8 character salt. Previous versions used a SHA1 hash padded with # zeroes. If someone attempts to use a password hash that worked with # a previous version of OX X, we will fail early and warn them. 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 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['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. if users_plist['ShadowHashData'] password_hash_plist = users_plist['ShadowHashData'][0].string converted_hash_plist = convert_binary_to_xml(password_hash_plist) else users_plist['ShadowHashData'] = [StringIO.new] converted_hash_plist = {'SALTED-SHA512' => StringIO.new} end # converted_hash_plist['SALTED-SHA512'].string expects a Base64 encoded # string. The password_hash provided as a resource attribute is a # hex value. We need to convert the provided hex value to a Base64 # encoded string to nest it in the converted hash plist. converted_hash_plist['SALTED-SHA512'].string = \ password_hash.unpack('a2'*(password_hash.size/2)).collect { |i| i.hex.chr }.join # 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") 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}" 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 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")) 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 converted_hash_plist = convert_binary_to_xml(password_hash_plist) # converted_hash_plist['SALTED-SHA512'].string is a Base64 encoded # string. The password_hash provided as a resource attribute is a # hex value. We need to convert the Base64 encoded string to a # hex value and provide it back to Puppet. password_hash = converted_hash_plist['SALTED-SHA512'].string.unpack("H*")[0] password_hash end end end end # This method will accept a hash that has been returned from Plist::parse_xml # and convert it to a binary plist (string value). def self.convert_xml_to_binary(plist_data) Puppet.debug('Converting XML plist to binary') Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'') IO.popen('plutil -convert binary1 -o - -', mode='r+') do |io| io.write plist_data.to_plist io.close_write @converted_plist = io.read end @converted_plist end # This method will accept a binary plist (as a string) and convert it to a # hash via Plist::parse_xml. def self.convert_binary_to_xml(plist_data) Puppet.debug('Converting binary plist to XML') Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'') IO.popen('plutil -convert xml1 -o - -', mode='r+') do |io| io.write plist_data io.close_write @converted_plist = io.read end Puppet.debug('Converting XML values to a hash.') @plist_hash = Plist::parse_xml(@converted_plist) @plist_hash end # Unlike most other *nixes, OS X doesn't provide built in functionality # for automatically assigning uids and gids to accounts, so we set up these # methods for consumption by functionality like --mkusers # By default we restrict to a reasonably sane range for system accounts def self.next_system_id(id_type, min_id=20) dscl_args = ['.', '-list'] if id_type == 'uid' dscl_args << '/Users' << 'uid' elsif id_type == 'gid' dscl_args << '/Groups' << 'gid' else fail("Invalid id_type #{id_type}. Only 'uid' and 'gid' supported") end dscl_out = dscl(dscl_args) # We're ok with throwing away negative uids here. ids = dscl_out.split.compact.collect { |l| l.to_i if l.match(/^\d+$/) } ids.compact!.sort! { |a,b| a.to_f <=> b.to_f } # We're just looking for an unused id in our sorted array. ids.each_index do |i| next_id = ids[i] + 1 return next_id if ids[i+1] != next_id and next_id >= min_id end end def ensure=(ensure_value) super # We need to loop over all valid properties for the type we're # managing and call the method which sets that property value # dscl can't create everything at once unfortunately. if ensure_value == :present @resource.class.validproperties.each do |name| next if name == :ensure # LAK: We use property.sync here rather than directly calling # the settor method because the properties might do some kind # of conversion. In particular, the user gid property might # have a string and need to convert it to a number if @resource.should(name) @resource.property(name).sync elsif value = autogen(name) self.send(name.to_s + "=", value) else next end end 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] 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] 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 # NBK: we override @parent.set as we need to execute a series of commands # to deal with array values, rather than the single command nameservice.rb # expects to be returned by modifycmd. Thus we don't bother defining modifycmd. def set(param, value) self.class.validate(param, value) current_members = @property_value_cache_hash[:members] if param == :members # If we are meant to be authoritative for the group membership # then remove all existing members who haven't been specified # in the manifest. remove_unwanted_members(current_members, value) if @resource[:auth_membership] and not current_members.nil? # if they're not a member, make them one. 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[symbolize(param)] # 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 fail("Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}") end end end # NBK: we override @parent.create as we need to execute a series of commands # to create objects with dscl, rather than the single command nameservice.rb # expects to be returned by addcmd. Thus we don't bother defining addcmd. def create if exists? info "already exists" return nil end # NBK: First we create the object with a known guid so we can set the contents # of the password hash if required # Shelling out sucks, but for a single use case it doesn't seem worth # requiring people install a UUID library that doesn't come with the system. # 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 begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not set GeneratedUID for #{@resource.class.name} #{@resource.name}: #{detail}") end if value = @resource.should(:password) and value != "" self.class.set_password(@resource[:name], guid, value) end # Now we create all the standard properties Puppet::Type.type(@resource.class.name).validproperties.each do |property| next if property == :ensure value = @resource.should(property) if property == :gid and value.nil? value = self.class.next_system_id(id_type='gid') end if property == :uid and value.nil? value = self.class.next_system_id(id_type='uid') end 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[symbolize(property)] next if property == :password # skip setting the password here exec_arg_vector << value.to_s begin execute(exec_arg_vector) rescue Puppet::ExecutionFailure => detail fail("Could not create #{@resource.class.name} #{@resource.name}: #{detail}") end end end end end def remove_unwanted_members(current_members, new_members) current_members.each do |member| if not new_members.flatten.include?(member) cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-d", member, @resource[:name]] begin execute(cmd) rescue Puppet::ExecutionFailure => detail # TODO: We're falling back to removing the member using dscl due to rdar://8481241 # This bug causes dseditgroup to fail to remove a member if that member doesn't exist cmd = [:dscl, ".", "-delete", "/Groups/#{@resource.name}", "GroupMembership", member] begin execute(cmd) rescue Puppet::ExecutionFailure => detail fail("Could not remove #{member} from group: #{@resource.name}, #{detail}") end end end end end def add_members(current_members, new_members) new_members.flatten.each do |new_member| if current_members.nil? or not current_members.include?(new_member) cmd = [:dseditgroup, "-o", "edit", "-n", ".", "-a", new_member, @resource[:name]] begin execute(cmd) rescue Puppet::ExecutionFailure => detail fail("Could not add #{new_member} to group: #{@resource.name}, #{detail}") end end end 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 # 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 type_properties << :guid # append GeneratedUID so we just get the report here @property_value_cache_hash = self.class.single_report(@resource[:name], *type_properties) [:uid, :gid].each do |param| @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 diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb index 779502ec8..e71a6b42b 100755 --- a/spec/unit/provider_spec.rb +++ b/spec/unit/provider_spec.rb @@ -1,623 +1,667 @@ #!/usr/bin/env rspec require 'spec_helper' def existing_command Puppet.features.microsoft_windows? ? "cmd" : "echo" end describe Puppet::Provider do before :each do Puppet::Type.newtype(:test) do newparam(:name) { isnamevar } end end after :each do Puppet::Type.rmtype(:test) end let :type do Puppet::Type.type(:test) end let :provider do type.provide(:default) {} end subject { provider } describe "has command" do it "installs a method to run the command specified by the path" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") allow_creation_of(echo_command) provider = provider_of do has_command(:echo, "/bin/echo") end provider.echo("an argument") end it "installs a command that is run with a given environment" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") allow_creation_of(echo_command, { :EV => "value", :OTHER => "different" }) provider = provider_of do has_command(:echo, "/bin/echo") do environment :EV => "value", :OTHER => "different" end end provider.echo("an argument") end it "is required by default" do provider = provider_of do has_command(:does_not_exist, "/does/not/exist") end provider.should_not be_suitable end it "is required by default" do provider = provider_of do has_command(:does_exist, File.expand_path("/exists/somewhere")) end file_exists_and_is_executable(File.expand_path("/exists/somewhere")) provider.should be_suitable end it "can be specified as optional" do provider = provider_of do has_command(:does_not_exist, "/does/not/exist") do is_optional end end provider.should be_suitable end end describe "has required commands" do it "installs methods to run executables by path" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") ls_command = expect_command_executed(:ls, "/bin/ls") allow_creation_of(echo_command) allow_creation_of(ls_command) provider = provider_of do commands :echo => "/bin/echo", :ls => "/bin/ls" end provider.echo("an argument") provider.ls end it "allows the provider to be suitable if the executable is present" do provider = provider_of do commands :always_exists => File.expand_path("/this/command/exists") end file_exists_and_is_executable(File.expand_path("/this/command/exists")) provider.should be_suitable end it "does not allow the provider to be suitable if the executable is not present" do provider = provider_of do commands :does_not_exist => "/this/command/does/not/exist" end provider.should_not be_suitable end end describe "has optional commands" do it "installs methods to run executables" do echo_command = expect_command_executed(:echo, "/bin/echo", "an argument") ls_command = expect_command_executed(:ls, "/bin/ls") allow_creation_of(echo_command) allow_creation_of(ls_command) provider = provider_of do optional_commands :echo => "/bin/echo", :ls => "/bin/ls" end provider.echo("an argument") provider.ls end it "allows the provider to be suitable even if the executable is not present" do provider = provider_of do optional_commands :does_not_exist => "/this/command/does/not/exist" end provider.should be_suitable end end it "makes command methods on demand (deprecated)" do Puppet::Util.expects(:which).with("/not/a/command").returns("/not/a/command") Puppet::Util::Execution.expects(:execute).with(["/not/a/command"], {}) provider = provider_of do @commands[:echo] = "/not/a/command" end provider.stubs(:which).with("/not/a/command").returns("/not/a/command") provider.make_command_methods(:echo) provider.echo end it "should have a specifity class method" do Puppet::Provider.should respond_to(:specificity) end it "should consider two defaults to be higher specificity than one default" do one = provider_of do defaultfor :operatingsystem => "solaris" end two = provider_of do defaultfor :operatingsystem => "solaris", :operatingsystemrelease => "5.10" end two.specificity.should > one.specificity end it "should be Comparable" do res = Puppet::Type.type(:notify).new(:name => "res") # Normally I wouldn't like the stubs, but the only way to name a class # otherwise is to assign it to a constant, and that hurts more here in # testing world. --daniel 2012-01-29 a = Class.new(Puppet::Provider).new(res) a.class.stubs(:name).returns "Puppet::Provider::Notify::A" b = Class.new(Puppet::Provider).new(res) b.class.stubs(:name).returns "Puppet::Provider::Notify::B" c = Class.new(Puppet::Provider).new(res) c.class.stubs(:name).returns "Puppet::Provider::Notify::C" [[a, b, c], [a, c, b], [b, a, c], [b, c, a], [c, a, b], [c, b, a]].each do |this| this.sort.should == [a, b, c] end a.should be < b a.should be < c b.should be > a b.should be < c c.should be > a c.should be > b [a, b, c].each {|x| a.should be <= x } [a, b, c].each {|x| c.should be >= x } b.should be_between(a, c) end context "when creating instances" do context "with a resource" do let :resource do type.new(:name => "fred") end subject { provider.new(resource) } it "should set the resource correctly" do subject.resource.must equal resource end it "should set the name from the resource" do subject.name.should == resource.name end end context "with a hash" do subject { provider.new(:name => "fred") } it "should set the name" do subject.name.should == "fred" end it "should not have a resource" do subject.resource.should be_nil end end context "with no arguments" do subject { provider.new } it "should raise an internal error if asked for the name" do expect { subject.name }.to raise_error Puppet::DevError end it "should not have a resource" do subject.resource.should be_nil end end end context "when confining" do it "should be suitable by default" do subject.should be_suitable end it "should not be default by default" do subject.should_not be_default end { { :true => true } => true, { :true => false } => false, { :false => false } => true, { :false => true } => false, { :operatingsystem => Facter.value(:operatingsystem) } => true, { :operatingsystem => :yayness } => false, { :nothing => :yayness } => false, { :exists => Puppet::Util.which(existing_command) } => true, { :exists => "/this/file/does/not/exist" } => false, { :true => true, :exists => Puppet::Util.which(existing_command) } => true, { :true => true, :exists => "/this/file/does/not/exist" } => false, { :operatingsystem => Facter.value(:operatingsystem), :exists => Puppet::Util.which(existing_command) } => true, { :operatingsystem => :yayness, :exists => Puppet::Util.which(existing_command) } => false, { :operatingsystem => Facter.value(:operatingsystem), :exists => "/this/file/does/not/exist" } => false, { :operatingsystem => :yayness, :exists => "/this/file/does/not/exist" } => false, }.each do |confines, result| it "should confine #{confines.inspect} to #{result}" do confines.each {|test, value| subject.confine test => value } subject.send(result ? :should : :should_not, be_suitable) end end it "should not override a confine even if a second has the same type" do subject.confine :true => false subject.should_not be_suitable subject.confine :true => true subject.should_not be_suitable end it "should not be suitable if any confine fails" do subject.confine :true => false subject.should_not be_suitable 10.times do subject.confine :true => true subject.should_not be_suitable end end end context "default providers" do let :os do Facter.value(:operatingsystem) end it { should respond_to :specificity } it "should find the default provider" do type.provide(:nondefault) {} subject.defaultfor :operatingsystem => os subject.name.should == type.defaultprovider.name end it "should consider any true value enough to be default" do alternate = type.provide(:alternate) {} subject.defaultfor :operatingsystem => [:one, :two, :three, os] subject.name.should == type.defaultprovider.name subject.should be_default alternate.should_not be_default end it "should not be default if the confine doesn't match" do subject.should_not be_default subject.defaultfor :operatingsystem => :one subject.should_not be_default end it "should consider two defaults to be higher specificity than one default" do one = type.provide(:one) do defaultfor :operatingsystem => "solaris" end two = type.provide(:two) do defaultfor :operatingsystem => "solaris", :operatingsystemrelease => "5.10" end two.specificity.should > one.specificity end it "should consider a subclass more specific than its parent class" do parent = type.provide(:parent) child = type.provide(:child, :parent => parent) child.specificity.should > parent.specificity end end context "provider commands" do it "should raise for unknown commands" do expect { subject.command(:something) }.to raise_error Puppet::DevError end it "should handle command inheritance" do parent = type.provide("parent") child = type.provide("child", :parent => parent.name) command = Puppet::Util.which('sh') || Puppet::Util.which('cmd.exe') parent.commands :sh => command FileTest.should be_exists parent.command(:sh) parent.command(:sh).should =~ /#{Regexp.escape(command)}$/ FileTest.should be_exists child.command(:sh) child.command(:sh).should =~ /#{Regexp.escape(command)}$/ end it "#1197: should find commands added in the same run" do subject.commands :testing => "puppet-bug-1197" subject.command(:testing).should be_nil subject.stubs(:which).with("puppet-bug-1197").returns("/puppet-bug-1197") subject.command(:testing).should == "/puppet-bug-1197" # Ideally, we would also test that `suitable?` returned the right thing # here, but it is impossible to get access to the methods that do that # without digging way down into the implementation. --daniel 2012-03-20 end context "with optional commands" do before :each do subject.optional_commands :cmd => "/no/such/binary/exists" end it { should be_suitable } it "should not be suitable if a mandatory command is also missing" do subject.commands :foo => "/no/such/binary/either" subject.should_not be_suitable end it "should define a wrapper for the command" do subject.should respond_to :cmd end it "should return nil if the command is requested" do subject.command(:cmd).should be_nil end it "should raise if the command is invoked" do expect { subject.cmd }.to raise_error Puppet::Error, /Command cmd is missing/ end end end + context "execution" do + before :each do + Puppet.expects(:deprecation_warning).never + end + + it "delegates instance execute to Puppet::Util::Execution" do + Puppet::Util::Execution.expects(:execute).with("a_command", { :option => "value" }) + + provider.new.send(:execute, "a_command", { :option => "value" }) + end + + it "delegates class execute to Puppet::Util::Execution" do + Puppet::Util::Execution.expects(:execute).with("a_command", { :option => "value" }) + + provider.send(:execute, "a_command", { :option => "value" }) + end + + it "delegates instance execpipe to Puppet::Util::Execution" do + block = Proc.new { } + Puppet::Util::Execution.expects(:execpipe).with("a_command", true, block) + + provider.new.send(:execpipe, "a_command", true, block) + end + + it "delegates class execpipe to Puppet::Util::Execution" do + block = Proc.new { } + Puppet::Util::Execution.expects(:execpipe).with("a_command", true, block) + + provider.send(:execpipe, "a_command", true, block) + end + + it "delegates instance execfail to Puppet::Util::Execution" do + Puppet::Util::Execution.expects(:execfail).with("a_command", "an exception to raise") + + provider.new.send(:execfail, "a_command", "an exception to raise") + end + + it "delegates class execfail to Puppet::Util::Execution" do + Puppet::Util::Execution.expects(:execfail).with("a_command", "an exception to raise") + + provider.send(:execfail, "a_command", "an exception to raise") + end + end + context "mk_resource_methods" do before :each do type.newproperty(:prop1) type.newproperty(:prop2) type.newparam(:param1) type.newparam(:param2) end fields = %w{prop1 prop2 param1 param2} # This is needed for Ruby 1.8.5, which throws an exception that the # default rescue doesn't catch if the method isn't present. Also, it has # no convenient predicate for them, which equally hurts. def has_method?(object, name) begin return true if object.instance_method(name) rescue Exception return false end end fields.each do |name| it "should add getter methods for #{name}" do expect { subject.mk_resource_methods }. to change { has_method?(subject, name) }. from(false).to(true) end it "should add setter methods for #{name}" do method = name + '=' expect { subject.mk_resource_methods }. to change { has_method?(subject, name) }. from(false).to(true) end end context "with an instance" do subject { provider.mk_resource_methods; provider.new(nil) } fields.each do |name| context name do it "should default to :absent" do subject.send(name).should == :absent end it "should update when set" do expect { subject.send(name + '=', "hello") }. to change { subject.send(name) }. from(:absent).to("hello") end end end end end context "source" do it "should default to the provider name" do subject.source.should == :default end it "should default to the provider name for a child provider" do type.provide(:sub, :parent => subject.name).source.should == :sub end it "should override if requested" do provider = type.provide(:sub, :parent => subject.name, :source => subject.source) provider.source.should == subject.source end it "should override to anything you want" do expect { subject.source = :banana }.to change { subject.source }. from(:default).to(:banana) end end context "features" do before :each do type.feature :numeric, '', :methods => [:one, :two] type.feature :alpha, '', :methods => [:a, :b] type.feature :nomethods, '' end { :no => { :alpha => false, :numeric => false, :methods => [] }, :numeric => { :alpha => false, :numeric => true, :methods => [:one, :two] }, :alpha => { :alpha => true, :numeric => false, :methods => [:a, :b] }, :all => { :alpha => true, :numeric => true, :methods => [:a, :b, :one, :two] }, :alpha_and_partial => { :alpha => true, :numeric => false, :methods => [:a, :b, :one] }, :numeric_and_partial => { :alpha => false, :numeric => true, :methods => [:a, :one, :two] }, :all_partial => { :alpha => false, :numeric => false, :methods => [:a, :one] }, :other_and_none => { :alpha => false, :numeric => false, :methods => [:foo, :bar] }, :other_and_alpha => { :alpha => true, :numeric => false, :methods => [:foo, :bar, :a, :b] }, }.each do |name, setup| context "with #{name.to_s.gsub('_', ' ')} features" do let :provider do provider = type.provide(name) setup[:methods].map do |method| provider.send(:define_method, method) do true end end type.provider(name) end let :numeric? do setup[:numeric] ? :should : :should_not end let :alpha? do setup[:alpha] ? :should : :should_not end subject { provider } it { should respond_to :has_features } it { should respond_to :has_feature } context "provider class" do it { should respond_to :nomethods? } it { should_not be_nomethods } it { should respond_to :numeric? } it { subject.send(numeric?, be_numeric) } it { subject.send(numeric?, be_satisfies(:numeric)) } it { should respond_to :alpha? } it { subject.send(alpha?, be_alpha) } it { subject.send(alpha?, be_satisfies(:alpha)) } end context "provider instance" do subject { provider.new } it { should respond_to :numeric? } it { subject.send(numeric?, be_numeric) } it { subject.send(numeric?, be_satisfies(:numeric)) } it { should respond_to :alpha? } it { subject.send(alpha?, be_alpha) } it { subject.send(alpha?, be_satisfies(:alpha)) } end end end context "feature with no methods" do before :each do type.feature :undemanding, '' end it { should respond_to :undemanding? } context "when the feature is not declared" do it { should_not be_undemanding } it { should_not be_satisfies :undemanding } end context "when the feature is declared" do before :each do subject.has_feature :undemanding end it { should be_undemanding } it { should be_satisfies :undemanding } end end context "supports_parameter?" do before :each do type.newparam(:no_feature) type.newparam(:one_feature, :required_features => :alpha) type.newparam(:two_features, :required_features => [:alpha, :numeric]) end let :providers do { :zero => type.provide(:zero), :one => type.provide(:one) do has_features :alpha end, :two => type.provide(:two) do has_features :alpha, :numeric end } end { :zero => { :yes => [:no_feature], :no => [:one_feature, :two_features] }, :one => { :yes => [:no_feature, :one_feature], :no => [:two_features] }, :two => { :yes => [:no_feature, :one_feature, :two_features], :no => [] } }.each do |name, data| data[:yes].each do |param| it "should support #{param} with provider #{name}" do providers[name].should be_supports_parameter param end end data[:no].each do |param| it "should not support #{param} with provider #{name}" do providers[name].should_not be_supports_parameter param end end end end end def provider_of(options = {}, &block) type = Puppet::Type.newtype(:dummy) do provide(:dummy, options, &block) end type.provider(:dummy) end def expect_command_executed(name, path, *args) command = Puppet::Provider::Command.new(name, path, Puppet::Util, Puppet::Util::Execution) command.expects(:execute).with(*args) command end def allow_creation_of(command, environment = {}) Puppet::Provider::Command.stubs(:new).with(command.name, command.executable, Puppet::Util, Puppet::Util::Execution, { :custom_environment => environment }).returns(command) end def file_exists_and_is_executable(path) FileTest.expects(:file?).with(path).returns(true) FileTest.expects(:executable?).with(path).returns(true) end end