diff --git a/lib/puppet/provider/nameservice.rb b/lib/puppet/provider/nameservice.rb index d57052bd9..01043af4a 100644 --- a/lib/puppet/provider/nameservice.rb +++ b/lib/puppet/provider/nameservice.rb @@ -1,276 +1,276 @@ require 'puppet' # This is the parent class of all NSS classes. They're very different in # their backend, but they're pretty similar on the front-end. This class # provides a way for them all to be as similar as possible. class Puppet::Provider::NameService < Puppet::Provider class << self def autogen_default(param) defined?(@autogen_defaults) ? @autogen_defaults[symbolize(param)] : nil end def autogen_defaults(hash) @autogen_defaults ||= {} hash.each do |param, value| @autogen_defaults[symbolize(param)] = value end end def initvars @checks = {} super end def instances objects = [] listbyname do |name| objects << new(:name => name, :ensure => :present) end objects end def option(name, option) name = name.intern if name.is_a? String (defined?(@options) and @options.include? name and @options[name].include? option) ? @options[name][option] : nil end def options(name, hash) raise Puppet::DevError, "#{name} is not a valid attribute for #{resource_type.name}" unless resource_type.valid_parameter?(name) @options ||= {} @options[name] ||= {} # Set options individually, so we can call the options method # multiple times. hash.each do |param, value| @options[name][param] = value end end # List everything out by name. Abstracted a bit so that it works # for both users and groups. def listbyname names = [] Etc.send("set#{section()}ent") begin while ent = Etc.send("get#{section()}ent") names << ent.name yield ent.name if block_given? end ensure Etc.send("end#{section()}ent") end names end def resource_type=(resource_type) super @resource_type.validproperties.each do |prop| next if prop == :ensure define_method(prop) { get(prop) || :absent} unless public_method_defined?(prop) define_method(prop.to_s + "=") { |*vals| set(prop, *vals) } unless public_method_defined?(prop.to_s + "=") end end # This is annoying, but there really aren't that many options, # and this *is* built into Ruby. def section unless defined?(@resource_type) raise Puppet::DevError, "Cannot determine Etc section without a resource type" end if @resource_type.name == :group "gr" else "pw" end end def validate(name, value) name = name.intern if name.is_a? String if @checks.include? name block = @checks[name][:block] raise ArgumentError, "Invalid value #{value}: #{@checks[name][:error]}" unless block.call(value) end end def verify(name, error, &block) name = name.intern if name.is_a? String @checks[name] = {:error => error, :block => block} end private def op(property) @ops[property.name] || ("-#{property.name}") end end # Autogenerate a value. Mostly used for uid/gid, but also used heavily # with DirectoryServices, because DirectoryServices is stupid. def autogen(field) field = symbolize(field) id_generators = {:user => :uid, :group => :gid} if id_generators[@resource.class.name] == field - return autogen_id(field) + return self.class.autogen_id(field, @resource.class.name) else if value = self.class.autogen_default(field) return value elsif respond_to?("autogen_#{field}") return send("autogen_#{field}") else return nil end end end - # Autogenerate either a uid or a gid. This is hard-coded: we can only - # generate one field type per class. - def autogen_id(field) - highest = 0 - - group = method = nil - case @resource.class.name - when :user; group = :passwd; method = :uid - when :group; group = :group; method = :gid + # Autogenerate either a uid or a gid. This is not very flexible: we can + # only generate one field type per class, and get kind of confused if asked + # for both. + def self.autogen_id(field, resource_type) + # Figure out what sort of value we want to generate. + case resource_type + when :user; group = :passwd; method = :uid + when :group; group = :group; method = :gid else raise Puppet::DevError, "Invalid resource name #{resource}" end - # Make sure we don't use the same value multiple times - if defined?(@@prevauto) - @@prevauto += 1 - else + # Initialize from the data set, if needed. + unless @prevauto + highest = 0 + Etc.send(group) { |obj| if obj.gid > highest highest = obj.send(method) unless obj.send(method) > 65000 end } - @@prevauto = highest + 1 + @prevauto = highest end - @@prevauto + # ...and finally increment and return the next value. + @prevauto += 1 end def create if exists? info "already exists" # The object already exists return nil end begin execute(self.addcmd) if feature?(:manages_password_age) && (cmd = passcmd) execute(cmd) end rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not create #{@resource.class.name} #{@resource.name}: #{detail}" end end def delete unless exists? info "already absent" # the object already doesn't exist return nil end begin execute(self.deletecmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not delete #{@resource.class.name} #{@resource.name}: #{detail}" end end def ensure if exists? :present else :absent end end # Does our object exist? def exists? !!getinfo(true) end # Retrieve a specific value by name. def get(param) (hash = getinfo(false)) ? hash[param] : nil end # Retrieve what we can about our object def getinfo(refresh) if @objectinfo.nil? or refresh == true @etcmethod ||= ("get" + self.class.section.to_s + "nam").intern begin @objectinfo = Etc.send(@etcmethod, @resource[:name]) rescue ArgumentError => detail @objectinfo = nil end end # Now convert our Etc struct into a hash. @objectinfo ? info2hash(@objectinfo) : nil end # The list of all groups the user is a member of. Different # user mgmt systems will need to override this method. def groups groups = [] # Reset our group list Etc.setgrent user = @resource[:name] # Now iterate across all of the groups, adding each one our # user is a member of while group = Etc.getgrent members = group.mem groups << group.name if members.include? user end # We have to close the file, so each listing is a separate # reading of the file. Etc.endgrent groups.join(",") end # Convert the Etc struct into a hash. def info2hash(info) hash = {} self.class.resource_type.validproperties.each do |param| method = posixmethod(param) hash[param] = info.send(posixmethod(param)) if info.respond_to? method end hash end def initialize(resource) super @objectinfo = nil end def set(param, value) self.class.validate(param, value) cmd = modifycmd(param, value) raise Puppet::DevError, "Nameservice command must be an array" unless cmd.is_a?(Array) begin execute(cmd) rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not set #{param} on #{@resource.class.name}[#{@resource.name}]: #{detail}" end end end diff --git a/lib/puppet/provider/nameservice/directoryservice.rb b/lib/puppet/provider/nameservice/directoryservice.rb index 2bdff3bcd..083c9a60e 100644 --- a/lib/puppet/provider/nameservice/directoryservice.rb +++ b/lib/puppet/provider/nameservice/directoryservice.rb @@ -1,593 +1,592 @@ require 'puppet' require 'puppet/provider/nameservice' require 'facter/util/plist' require 'fileutils' class Puppet::Provider::NameService class 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...) - @@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', - } - - @@password_hash_dir = "/var/db/shadow/hash" - @@users_plist_dir = '/var/db/dslocal/nodes/Default/users' + 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]) + 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 # 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 + 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}" + 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 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). password_hash_plist = users_plist['ShadowHashData'][0].string converted_hash_plist = convert_binary_to_xml(password_hash_plist) # 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") + 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 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] + 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 # 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)] + 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 + 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)] + 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/lib/puppet/provider/service/bsd.rb b/lib/puppet/provider/service/bsd.rb index 0c019fdd0..5b73dfc64 100644 --- a/lib/puppet/provider/service/bsd.rb +++ b/lib/puppet/provider/service/bsd.rb @@ -1,49 +1,51 @@ # Manage FreeBSD services. Puppet::Type.type(:service).provide :bsd, :parent => :init do desc <<-EOT FreeBSD's (and probably NetBSD's?) form of `init`-style service management. Uses `rc.conf.d` for service enabling and disabling. EOT confine :operatingsystem => [:freebsd, :netbsd, :openbsd] - @@rcconf_dir = '/etc/rc.conf.d' + def rcconf_dir + '/etc/rc.conf.d' + end def self.defpath superclass.defpath end # remove service file from rc.conf.d to disable it def disable - rcfile = File.join(@@rcconf_dir, @model[:name]) + rcfile = File.join(rcconf_dir, @model[:name]) File.delete(rcfile) if File.exists?(rcfile) end # if the service file exists in rc.conf.d then it's already enabled def enabled? - rcfile = File.join(@@rcconf_dir, @model[:name]) + rcfile = File.join(rcconf_dir, @model[:name]) return :true if File.exists?(rcfile) :false end # enable service by creating a service file under rc.conf.d with the # proper contents def enable - Dir.mkdir(@@rcconf_dir) if not File.exists?(@@rcconf_dir) - rcfile = File.join(@@rcconf_dir, @model[:name]) + Dir.mkdir(rcconf_dir) if not File.exists?(rcconf_dir) + rcfile = File.join(rcconf_dir, @model[:name]) open(rcfile, 'w') { |f| f << "%s_enable=\"YES\"\n" % @model[:name] } end # Override stop/start commands to use one's and the avoid race condition # where provider trys to stop/start the service before it is enabled def startcmd [self.initscript, :onestart] end def stopcmd [self.initscript, :onestop] end end diff --git a/lib/puppet/provider/service/freebsd.rb b/lib/puppet/provider/service/freebsd.rb index 5e1a36d46..6c090908a 100644 --- a/lib/puppet/provider/service/freebsd.rb +++ b/lib/puppet/provider/service/freebsd.rb @@ -1,139 +1,139 @@ Puppet::Type.type(:service).provide :freebsd, :parent => :init do desc "Provider for FreeBSD. Uses the `rcvar` argument of init scripts and parses/edits rc files." confine :operatingsystem => [:freebsd] defaultfor :operatingsystem => [:freebsd] - @@rcconf = '/etc/rc.conf' - @@rcconf_local = '/etc/rc.conf.local' - @@rcconf_dir = '/etc/rc.conf.d' + def rcconf() '/etc/rc.conf' end + def rcconf_local() '/etc/rc.conf.local' end + def rcconf_dir() '/etc/rc.conf.d' end def self.defpath superclass.defpath end # Executing an init script with the 'rcvar' argument returns # the service name, rcvar name and whether it's enabled/disabled def rcvar rcvar = execute([self.initscript, :rcvar], :failonfail => true, :squelch => false) rcvar = rcvar.split("\n") rcvar.delete_if {|str| str =~ /^#\s*$/} rcvar[1] = rcvar[1].gsub(/^\$/, '') rcvar end # Extract service name def service_name name = self.rcvar[0] self.error("No service name found in rcvar") if name.nil? name = name.gsub!(/# (.*)/, '\1') self.error("Service name is empty") if name.nil? self.debug("Service name is #{name}") name end # Extract rcvar name def rcvar_name name = self.rcvar[1] self.error("No rcvar name found in rcvar") if name.nil? name = name.gsub!(/(.*)_enable=(.*)/, '\1') self.error("rcvar name is empty") if name.nil? self.debug("rcvar name is #{name}") name end # Extract rcvar value def rcvar_value value = self.rcvar[1] self.error("No rcvar value found in rcvar") if value.nil? value = value.gsub!(/(.*)_enable="?(\w+)"?/, '\2') self.error("rcvar value is empty") if value.nil? self.debug("rcvar value is #{value}") value end # Edit rc files and set the service to yes/no def rc_edit(yesno) service = self.service_name rcvar = self.rcvar_name self.debug("Editing rc files: setting #{rcvar} to #{yesno} for #{service}") self.rc_add(service, rcvar, yesno) if not self.rc_replace(service, rcvar, yesno) end # Try to find an existing setting in the rc files # and replace the value def rc_replace(service, rcvar, yesno) success = false # Replace in all files, not just in the first found with a match - [@@rcconf, @@rcconf_local, @@rcconf_dir + "/#{service}"].each do |filename| + [rcconf, rcconf_local, rcconf_dir + "/#{service}"].each do |filename| if File.exists?(filename) s = File.read(filename) if s.gsub!(/(#{rcvar}_enable)=\"?(YES|NO)\"?/, "\\1=\"#{yesno}\"") File.open(filename, File::WRONLY) { |f| f << s } self.debug("Replaced in #{filename}") success = true end end end success end # Add a new setting to the rc files def rc_add(service, rcvar, yesno) append = "\# Added by Puppet\n#{rcvar}_enable=\"#{yesno}\"\n" # First, try the one-file-per-service style - if File.exists?(@@rcconf_dir) - File.open(@@rcconf_dir + "/#{service}", File::WRONLY | File::APPEND | File::CREAT, 0644) { + if File.exists?(rcconf_dir) + File.open(rcconf_dir + "/#{service}", File::WRONLY | File::APPEND | File::CREAT, 0644) { |f| f << append self.debug("Appended to #{f.path}") } else # Else, check the local rc file first, but don't create it - if File.exists?(@@rcconf_local) - File.open(@@rcconf_local, File::WRONLY | File::APPEND) { + if File.exists?(rcconf_local) + File.open(rcconf_local, File::WRONLY | File::APPEND) { |f| f << append self.debug("Appended to #{f.path}") } else # At last use the standard rc.conf file - File.open(@@rcconf, File::WRONLY | File::APPEND | File::CREAT, 0644) { + File.open(rcconf, File::WRONLY | File::APPEND | File::CREAT, 0644) { |f| f << append self.debug("Appended to #{f.path}") } end end end def enabled? if /YES$/ =~ self.rcvar_value self.debug("Is enabled") return :true end self.debug("Is disabled") :false end def enable self.debug("Enabling") self.rc_edit("YES") end def disable self.debug("Disabling") self.rc_edit("NO") end def startcmd [self.initscript, :onestart] end def stopcmd [self.initscript, :onestop] end def statuscmd [self.initscript, :onestatus] end end diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb index 224e79731..1c465e9d1 100755 --- a/lib/puppet/type/tidy.rb +++ b/lib/puppet/type/tidy.rb @@ -1,333 +1,324 @@ Puppet::Type.newtype(:tidy) do require 'puppet/file_serving/fileset' require 'puppet/file_bucket/dipper' @doc = "Remove unwanted files based on specific criteria. Multiple criteria are OR'd together, so a file that is too large but is not old enough will still get tidied. If you don't specify either `age` or `size`, then all files will be removed. This resource type works by generating a file resource for every file that should be deleted and then letting that resource perform the actual deletion. " newparam(:path) do desc "The path to the file or directory to manage. Must be fully qualified." isnamevar end newparam(:recurse) do desc "If target is a directory, recursively descend into the directory looking for files to tidy." newvalues(:true, :false, :inf, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else raise ArgumentError, "Invalid recurse value #{value.inspect}" end end end newparam(:matches) do desc <<-EOT One or more (shell type) file glob patterns, which restrict the list of files to be tidied to those whose basenames match at least one of the patterns specified. Multiple patterns can be specified using an array. Example: tidy { "/tmp": age => "1w", recurse => 1, matches => [ "[0-9]pub*.tmp", "*.temp", "tmpfile?" ] } This removes files from `/tmp` if they are one week old or older, are not in a subdirectory and match one of the shell globs given. Note that the patterns are matched against the basename of each file -- that is, your glob patterns should not have any '/' characters in them, since you are only specifying against the last bit of the file. Finally, note that you must now specify a non-zero/non-false value for recurse if matches is used, as matches only apply to files found by recursion (there's no reason to use static patterns match against a statically determined path). Requiering explicit recursion clears up a common source of confusion. EOT # Make sure we convert to an array. munge do |value| fail "Tidy can't use matches with recurse 0, false, or undef" if "#{@resource[:recurse]}" =~ /^(0|false|)$/ [value].flatten end # Does a given path match our glob patterns, if any? Return true # if no patterns have been provided. def tidy?(path, stat) basename = File.basename(path) flags = File::FNM_DOTMATCH | File::FNM_PATHNAME return(value.find {|pattern| File.fnmatch(pattern, basename, flags) } ? true : false) end end newparam(:backup) do desc "Whether tidied files should be backed up. Any values are passed directly to the file resources used for actual file deletion, so consult the `file` type's backup documentation to determine valid values." end newparam(:age) do desc "Tidy files whose age is equal to or greater than the specified time. You can choose seconds, minutes, hours, days, or weeks by specifying the first letter of any of those words (e.g., '1w'). Specifying 0 will remove all files." - @@ageconvertors = { + AgeConvertors = { :s => 1, - :m => 60 + :m => 60, + :h => 60 * 60, + :d => 60 * 60 * 24, + :w => 60 * 60 * 24 * 7, } - @@ageconvertors[:h] = @@ageconvertors[:m] * 60 - @@ageconvertors[:d] = @@ageconvertors[:h] * 24 - @@ageconvertors[:w] = @@ageconvertors[:d] * 7 - def convert(unit, multi) - if num = @@ageconvertors[unit] + if num = AgeConvertors[unit] return num * multi else self.fail "Invalid age unit '#{unit}'" end end def tidy?(path, stat) # If the file's older than we allow, we should get rid of it. (Time.now.to_i - stat.send(resource[:type]).to_i) > value end munge do |age| unit = multi = nil case age when /^([0-9]+)(\w)\w*$/ multi = Integer($1) unit = $2.downcase.intern when /^([0-9]+)$/ multi = Integer($1) unit = :d else self.fail "Invalid tidy age #{age}" end convert(unit, multi) end end newparam(:size) do desc "Tidy files whose size is equal to or greater than the specified size. Unqualified values are in kilobytes, but *b*, *k*, *m*, *g*, and *t* can be appended to specify *bytes*, *kilobytes*, *megabytes*, *gigabytes*, and *terabytes*, respectively. Only the first character is significant, so the full word can also be used." - @@sizeconvertors = { - :b => 0, - :k => 1, - :m => 2, - :g => 3, - :t => 4 - } - def convert(unit, multi) - if num = @@sizeconvertors[unit] + if num = { :b => 0, :k => 1, :m => 2, :g => 3, :t => 4 }[unit] result = multi num.times do result *= 1024 end return result else self.fail "Invalid size unit '#{unit}'" end end def tidy?(path, stat) stat.size >= value end munge do |size| case size when /^([0-9]+)(\w)\w*$/ multi = Integer($1) unit = $2.downcase.intern when /^([0-9]+)$/ multi = Integer($1) unit = :k else self.fail "Invalid tidy size #{age}" end convert(unit, multi) end end newparam(:type) do desc "Set the mechanism for determining age." newvalues(:atime, :mtime, :ctime) defaultto :atime end newparam(:rmdirs, :boolean => true) do desc "Tidy directories in addition to files; that is, remove directories whose age is older than the specified criteria. This will only remove empty directories, so all contained files must also be tidied before a directory gets removed." newvalues :true, :false end # Erase PFile's validate method validate do end def self.instances [] end def depthfirst? true end def initialize(hash) super # only allow backing up into filebuckets self[:backup] = false unless self[:backup].is_a? Puppet::FileBucket::Dipper end # Make a file resource to remove a given file. def mkfile(path) # Force deletion, so directories actually get deleted. Puppet::Type.type(:file).new :path => path, :backup => self[:backup], :ensure => :absent, :force => true end def retrieve # Our ensure property knows how to retrieve everything for us. if obj = @parameters[:ensure] return obj.retrieve else return {} end end # Hack things a bit so we only ever check the ensure property. def properties [] end def generate return [] unless stat(self[:path]) case self[:recurse] when Integer, Fixnum, Bignum, /^\d+$/ parameter = { :recurse => true, :recurselimit => self[:recurse] } when true, :true, :inf parameter = { :recurse => true } end if parameter files = Puppet::FileServing::Fileset.new(self[:path], parameter).files.collect do |f| f == "." ? self[:path] : ::File.join(self[:path], f) end else files = [self[:path]] end result = files.find_all { |path| tidy?(path) }.collect { |path| mkfile(path) }.each { |file| notice "Tidying #{file.ref}" }.sort { |a,b| b[:path] <=> a[:path] } # No need to worry about relationships if we don't have rmdirs; there won't be # any directories. return result unless rmdirs? # Now make sure that all directories require the files they contain, if all are available, # so that a directory is emptied before we try to remove it. files_by_name = result.inject({}) { |hash, file| hash[file[:path]] = file; hash } files_by_name.keys.sort { |a,b| b <=> b }.each do |path| dir = ::File.dirname(path) next unless resource = files_by_name[dir] if resource[:require] resource[:require] << Puppet::Resource.new(:file, path) else resource[:require] = [Puppet::Resource.new(:file, path)] end end result end # Does a given path match our glob patterns, if any? Return true # if no patterns have been provided. def matches?(path) return true unless self[:matches] basename = File.basename(path) flags = File::FNM_DOTMATCH | File::FNM_PATHNAME if self[:matches].find {|pattern| File.fnmatch(pattern, basename, flags) } return true else debug "No specified patterns match #{path}, not tidying" return false end end # Should we remove the specified file? def tidy?(path) return false unless stat = self.stat(path) return false if stat.ftype == "directory" and ! rmdirs? # The 'matches' parameter isn't OR'ed with the other tests -- # it's just used to reduce the list of files we can match. return false if param = parameter(:matches) and ! param.tidy?(path, stat) tested = false [:age, :size].each do |name| next unless param = parameter(name) tested = true return true if param.tidy?(path, stat) end # If they don't specify either, then the file should always be removed. return true unless tested false end def stat(path) begin ::File.lstat(path) rescue Errno::ENOENT => error info "File does not exist" return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return nil end end end