diff --git a/Rakefile b/Rakefile index 0e2e62d3a..25f5a23fa 100644 --- a/Rakefile +++ b/Rakefile @@ -1,68 +1,68 @@ # Rakefile for Puppet begin require 'rake/reductive' rescue LoadError $stderr.puts "You must have the Reductive build library in your RUBYLIB." exit(14) end TESTHOSTS = %w{rh3a fedora1 centos1 freebsd1 culain} project = Rake::RedLabProject.new("puppet") do |p| p.summary = "System Automation and Configuration Management Software" p.description = "Puppet is a declarative language for expressing system configuration, a client and server for distributing it, and a library for realizing the configuration." p.filelist = [ 'install.rb', '[A-Z]*', 'lib/**/*.rb', 'test/**/*.rb', 'bin/**/*', 'ext/**/*', 'examples/**/*', 'conf/**/*' ] p.add_dependency('facter', '1.1.0') - p.epmhosts = %w{culain} + #p.epmhosts = %w{culain} p.sunpkghost = "sol10b" p.rpmhost = "fedora1" end if project.has?(:gem) # Make our gem task. This actually just fills out the spec. project.mkgemtask do |task| task.require_path = 'lib' # Use these for libraries. task.bindir = "bin" # Use these for applications. task.executables = ["puppet", "puppetd", "puppetmasterd", "puppetdoc", "puppetca"] task.default_executable = "puppet" task.autorequire = 'puppet' #### Documentation and testing. task.has_rdoc = true #s.extra_rdoc_files = rd.rdoc_files.reject { |fn| fn =~ /\.rb$/ }.to_a task.rdoc_options << '--title' << 'Puppet - Configuration Management' << '--main' << 'README' << '--line-numbers' task.test_file = "test/Rakefile" end end if project.has?(:epm) project.mkepmtask do |task| task.bins = FileList.new("bin/puppet", "bin/puppetca") task.sbins = FileList.new("bin/puppetmasterd", "bin/puppetd") task.rubylibs = FileList.new('lib/**/*') end end # $Id$ diff --git a/lib/puppet/metatype/attributes.rb b/lib/puppet/metatype/attributes.rb index 764aba8ee..28cefec6a 100644 --- a/lib/puppet/metatype/attributes.rb +++ b/lib/puppet/metatype/attributes.rb @@ -1,718 +1,740 @@ require 'puppet' require 'puppet/type' class Puppet::Type class << self include Puppet::Util::ClassGen attr_reader :states end # All parameters, in the appropriate order. The namevar comes first, # then the states, then the params and metaparams in the order they # were specified in the files. def self.allattrs # now get all of the arguments, in a specific order # Cache this, since it gets called so many times namevar = self.namevar order = [namevar] order << [self.states.collect { |state| state.name }, self.parameters, self.metaparams].flatten.reject { |param| # we don't want our namevar in there multiple times param == namevar } order.flatten! return order end # Find the class associated with any given attribute. def self.attrclass(name) @attrclasses ||= {} # We cache the value, since this method gets called such a huge number # of times (as in, hundreds of thousands in a given run). unless @attrclasses.include?(name) @attrclasses[name] = case self.attrtype(name) when :state: @validstates[name] when :meta: @@metaparamhash[name] when :param: @paramhash[name] end end @attrclasses[name] end # What type of parameter are we dealing with? Cache the results, because # this method gets called so many times. def self.attrtype(attr) @attrtypes ||= {} unless @attrtypes.include?(attr) @attrtypes[attr] = case when @validstates.include?(attr): :state when @@metaparamhash.include?(attr): :meta when @paramhash.include?(attr): :param else raise Puppet::DevError, "Invalid attribute '%s' for class '%s'" % [attr, self.name] end end @attrtypes[attr] end # Copy an existing class parameter. This allows other types to avoid # duplicating a parameter definition, and is mostly used by subclasses # of the File class. def self.copyparam(klass, name) param = klass.attrclass(name) unless param raise Puppet::DevError, "Class %s has no param %s" % [klass, name] end @parameters << param @parameters.each { |p| @paramhash[name] = p } if param.isnamevar? @namevar = param.name end end # A similar function but one that yields the name, type, and class. # This is mainly so that setdefaults doesn't call quite so many functions. def self.eachattr(*ary) # now get all of the arguments, in a specific order # Cache this, since it gets called so many times if ary.empty? ary = nil end self.states.each { |state| yield(state, :state) if ary.nil? or ary.include?(state.name) } @parameters.each { |param| yield(param, :param) if ary.nil? or ary.include?(param.name) } @@metaparams.each { |param| yield(param, :meta) if ary.nil? or ary.include?(param.name) } end def self.eachmetaparam @@metaparams.each { |p| yield p.name } end # Create the 'ensure' class. This is a separate method so other types # can easily call it and create their own 'ensure' values. def self.ensurable(&block) if block_given? self.newstate(:ensure, :parent => Puppet::State::Ensure, &block) else self.newstate(:ensure, :parent => Puppet::State::Ensure) do self.defaultvalues end end end # Should we add the 'ensure' state to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. #ens = [:create, :destroy].inject { |set, method| ens = [:exists?, :create, :destroy].inject { |set, method| set &&= self.public_method_defined?(method) } #puts "%s ensurability: %s" % [self.name, ens] return ens end + + # Deal with any options passed into parameters. + def self.handle_param_options(name, options) + # If it's a boolean parameter, create a method to test the value easily + if options[:boolean] + define_method(name.to_s + "?") do + val = self[name] + if val == :true or val == true + return true + end + end + end + + # If this param handles relationships, store that information + end # Is the parameter in question a meta-parameter? def self.metaparam?(param) param = symbolize(param) @@metaparamhash.include?(param) end # Find the metaparameter class associated with a given metaparameter name. def self.metaparamclass(name) @@metaparamhash[symbolize(name)] end def self.metaparams @@metaparams.collect { |param| param.name } end def self.metaparamdoc(metaparam) @@metaparamhash[metaparam].doc end # Create a new metaparam. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. - def self.newmetaparam(name, &block) + def self.newmetaparam(name, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) param = genclass(name, - :parent => Puppet::Parameter, + :parent => options[:parent] || Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, + :attributes => options[:attributes], &block ) + + handle_param_options(name, options) param.ismetaparameter return param end # Find the namevar def self.namevar unless defined? @namevar params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } if params.length > 1 raise Puppet::DevError, "Found multiple namevars for %s" % self.name elsif params.length == 1 @namevar = params[0].name else raise Puppet::DevError, "No namevar for %s" % self.name end end @namevar end # Create a new parameter. Requires a block and a name, stores it in the # @parameters array, and does some basic checking on it. def self.newparam(name, options = {}, &block) + options[:attributes] ||= {} + options[:attributes][:element] = self param = genclass(name, :parent => options[:parent] || Puppet::Parameter, - :attributes => { :element => self }, + :attributes => options[:attributes], :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) + + handle_param_options(name, options) # These might be enabled later. # define_method(name) do # @parameters[name].value # end # # define_method(name.to_s + "=") do |value| # newparam(param, value) # end if param.isnamevar? @namevar = param.name end return param end # Create a new state. The first parameter must be the name of the state; # this is how users will refer to the state when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the state. Defaults to Puppet::State. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newstate(name, options = {}, &block) name = symbolize(name) # This is here for types that might still have the old method of defining # a parent class. unless options.is_a? Hash raise Puppet::DevError, "Options must be a hash, not %s" % options.inspect end if @validstates.include?(name) raise Puppet::DevError, "Class %s already has a state named %s" % [self.name, name] end # We have to create our own, new block here because we want to define # an initial :retrieve method, if told to, and then eval the passed # block if available. s = genclass(name, :parent => options[:parent] || Puppet::State, :hash => @validstates ) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do instance_variable_set( "@is", provider.send(options[:retrieve]) ) end end if block class_eval(&block) end end # If it's the 'ensure' state, always put it first. if name == :ensure @states.unshift s else @states << s end if options[:event] s.event = options[:event] end # define_method(name) do # @states[name].should # end # # define_method(name.to_s + "=") do |value| # newstate(name, :should => value) # end return s end def self.paramdoc(param) @paramhash[param].doc end # Return the parameter names def self.parameters return [] unless defined? @parameters @parameters.collect { |klass| klass.name } end # Find the parameter class associated with a given parameter name. def self.paramclass(name) @paramhash[name] end # Return the state class associated with a name def self.statebyname(name) @validstates[name] end def self.validattr?(name) name = symbolize(name) return true if name == :name @validattrs ||= {} unless @validattrs.include?(name) if self.validstate?(name) or self.validparameter?(name) or self.metaparam?(name) @validattrs[name] = true else @validattrs[name] = false end end @validattrs[name] end # does the name reflect a valid state? def self.validstate?(name) name = symbolize(name) if @validstates.include?(name) return @validstates[name] else return false end end # Return the list of validstates def self.validstates return {} unless defined? @states return @validstates.keys end # does the name reflect a valid parameter? def self.validparameter?(name) unless defined? @parameters raise Puppet::DevError, "Class %s has not defined parameters" % self end if @paramhash.include?(name) or @@metaparamhash.include?(name) return true else return false end end # fix any namevar => param translations def argclean(oldhash) # This duplication is here because it might be a transobject. hash = oldhash.dup.to_hash if hash.include?(:parent) hash.delete(:parent) end namevar = self.class.namevar # Do a simple translation for those cases where they've passed :name # but that's not our namevar if hash.include? :name and namevar != :name if hash.include? namevar raise ArgumentError, "Cannot provide both name and %s" % namevar end hash[namevar] = hash[:name] hash.delete(:name) end # Make sure we have a name, one way or another unless hash.include? namevar if defined? @title and @title hash[namevar] = @title else raise Puppet::Error, "Was not passed a namevar or title" end end return hash end # Is the specified parameter set? def attrset?(type, attr) case type when :state: return @states.include?(attr) when :param: return @parameters.include?(attr) when :meta: return @metaparams.include?(attr) else self.devfail "Invalid set type %s" % [type] end end # Allow an outside party to specify the 'is' value for a state. The # arguments are an array because you can't use parens with 'is=' calls. # Most classes won't use this. def is=(ary) param, value = ary if param.is_a?(String) param = param.intern end if self.class.validstate?(param) unless @states.include?(param) self.newstate(param) end @states[param].is = value else self[param] = value end end # abstract accessing parameters and states, and normalize # access to always be symbols, not strings # This returns a value, not an object. It returns the 'is' # value, but you can also specifically return 'is' and 'should' # values using 'object.is(:state)' or 'object.should(:state)'. def [](name) if name.is_a?(String) name = name.intern end if name == :name name = self.class.namevar end case self.class.attrtype(name) when :state if @states.include?(name) return @states[name].is else return nil end when :meta if @metaparams.include?(name) return @metaparams[name].value else if default = self.class.metaparamclass(name).default return default else return nil end end when :param if @parameters.include?(name) return @parameters[name].value else if default = self.class.paramclass(name).default return default else return nil end end else raise TypeError.new("Invalid parameter %s(%s)" % [name, name.inspect]) end end # Abstract setting parameters and states, and normalize # access to always be symbols, not strings. This sets the 'should' # value on states, and otherwise just sets the appropriate parameter. def []=(name,value) if name.is_a?(String) name = name.intern end if name == :name name = self.class.namevar end if value.nil? raise Puppet::Error.new("Got nil value for %s" % name) end case self.class.attrtype(name) when :state if value.is_a?(Puppet::State) self.debug "'%s' got handed a state for '%s'" % [self,name] @states[name] = value else if @states.include?(name) @states[name].should = value else # newstate returns true if it successfully created the state, # false otherwise; I just don't know what to do with that # fact. unless newstate(name, :should => value) #self.info "%s failed" % name end end end when :meta self.newmetaparam(self.class.metaparamclass(name), value) when :param klass = self.class.attrclass(name) # if they've got a method to handle the parameter, then do it that way self.newparam(klass, value) else raise Puppet::Error, "Invalid parameter %s" % [name] end end # remove a state from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) case attr when Puppet::Type if @children.include?(attr) @children.delete(attr) end else if @states.has_key?(attr) @states.delete(attr) elsif @parameters.has_key?(attr) @parameters.delete(attr) elsif @metaparams.has_key?(attr) @metaparams.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end end # iterate across the existing states def eachstate # states() is a private method states().each { |state| yield state } end # retrieve the 'is' value for a specified state def is(state) if @states.include?(state) return @states[state].is else return nil end end # retrieve the 'should' value for a specified state def should(state) if @states.include?(state) return @states[state].should else return nil end end # Create a new parameter. def newparam(klass, value = nil) newattr(:param, klass, value) end # Create a new parameter or metaparameter. We'll leave the calling # method to store it appropriately. def newmetaparam(klass, value = nil) newattr(:meta, klass, value) end # The base function that the others wrap. def newattr(type, klass, value = nil) # This should probably be a bit, um, different, but... if type == :state return newstate(klass) end param = klass.new param.parent = self unless value.nil? param.value = value end case type when :meta @metaparams[klass.name] = param when :param @parameters[klass.name] = param else self.devfail("Invalid param type %s" % type) end return param end # create a new state def newstate(name, hash = {}) stateklass = nil if name.is_a?(Class) stateklass = name name = stateklass.name else stateklass = self.class.validstate?(name) unless stateklass self.fail("Invalid state %s" % name) end end if @states.include?(name) hash.each { |var,value| @states[name].send(var.to_s + "=", value) } else #Puppet.warning "Creating state %s for %s" % # [stateklass.name,self.name] begin hash[:parent] = self # make sure the state doesn't have any errors newstate = stateklass.new(hash) @states[name] = newstate return newstate rescue Puppet::Error => detail # the state failed, so just ignore it self.warning "State %s failed: %s" % [name, detail] return false rescue Puppet::DevError => detail # the state failed, so just ignore it self.err "State %s failed: %s" % [name, detail] return false rescue => detail # the state failed, so just ignore it self.err "State %s failed: %s (%s)" % [name, detail, detail.class] return false end end end # return the value of a parameter def parameter(name) unless name.is_a? Symbol name = name.intern end return @parameters[name].value end # Is the named state defined? def statedefined?(name) unless name.is_a? Symbol name = name.intern end return @states.include?(name) end # return an actual type by name; to return the value, use 'inst[name]' # FIXME this method should go away def state(name) unless name.is_a? Symbol name = name.intern end return @states[name] end # def set(name, value) # send(name.to_s + "=", value) # end # # def get(name) # send(name) # end # For any parameters or states that have defaults and have not yet been # set, set them now. def setdefaults(*ary) self.class.eachattr(*ary) { |klass, type| # not many attributes will have defaults defined, so we short-circuit # those away next unless klass.method_defined?(:default) next if self.attrset?(type, klass.name) obj = self.newattr(type, klass) value = obj.default unless value.nil? #self.debug "defaulting %s to %s" % [obj.name, obj.default] obj.value = value else #self.debug "No default for %s" % obj.name # "obj" is a Parameter. self.delete(obj.name) end } end # Convert our object to a hash. This just includes states. def to_hash rethash = {} [@parameters, @metaparams, @states].each do |hash| hash.each do |name, obj| rethash[name] = obj.value end end rethash end # Meta-parameter methods: These methods deal with the results # of specifying metaparameters private def states #debug "%s has %s states" % [self,@states.length] tmpstates = [] self.class.states.each { |state| if @states.include?(state.name) tmpstates.push(@states[state.name]) end } unless tmpstates.length == @states.length self.devfail( "Something went very wrong with tmpstates creation" ) end return tmpstates end end # $Id$ diff --git a/lib/puppet/metatype/container.rb b/lib/puppet/metatype/container.rb index d7c509699..364639fd5 100644 --- a/lib/puppet/metatype/container.rb +++ b/lib/puppet/metatype/container.rb @@ -1,93 +1,97 @@ class Puppet::Type attr_accessor :children # this is a retarded hack method to get around the difference between # component children and file children def self.depthfirst? if defined? @depthfirst return @depthfirst else return false end end + + def depthfirst? + self.class.depthfirst? + end def parent=(parent) if self.parentof?(parent) devfail "%s[%s] is already the parent of %s[%s]" % [self.class.name, self.title, parent.class.name, parent.title] end @parent = parent end # Add a hook for testing for recursion. def parentof?(child) if (self == child) debug "parent is equal to child" return true elsif defined? @parent and @parent.parentof?(child) debug "My parent is parent of child" return true elsif @children.include?(child) debug "child is already in children array" return true else return false end end def push(*childs) unless defined? @children @children = [] end childs.each { |child| # Make sure we don't have any loops here. if parentof?(child) devfail "Already the parent of %s[%s]" % [child.class.name, child.title] end unless child.is_a?(Puppet::Element) self.debug "Got object of type %s" % child.class self.devfail( "Containers can only contain Puppet::Elements, not %s" % child.class ) end @children.push(child) child.parent = self } end # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # Our children remove themselves from our @children array (else the object # we called this on at the top would not be removed), so we duplicate the # array and iterate over that. If we don't do this, only half of the # objects get removed. @children.dup.each { |child| child.remove(rmdeps) } @children.clear # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. [@states, @parameters, @metaparams].each do |hash| hash.each do |name, obj| obj.remove end hash.clear end self.class.delete(self) @parent = nil # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end end # $Id$ diff --git a/lib/puppet/metatype/manager.rb b/lib/puppet/metatype/manager.rb index d2749b87d..bad41570a 100644 --- a/lib/puppet/metatype/manager.rb +++ b/lib/puppet/metatype/manager.rb @@ -1,153 +1,157 @@ require 'puppet' require 'puppet/util/classgen' # Methods dealing with Type management. This module gets included into the # Puppet::Type class, it's just split out here for clarity. module Puppet::MetaType module Manager include Puppet::Util::ClassGen # remove all type instances; this is mostly only useful for testing def allclear Puppet::Event::Subscription.clear @types.each { |name, type| type.clear } end # iterate across all of the subclasses of Type def eachtype @types.each do |name, type| # Only consider types that have names #if ! type.parameters.empty? or ! type.validstates.empty? yield type #end end end # Load all types. Only currently used for documentation. def loadall typeloader.loadall end # Do an on-demand plugin load def loadplugin(name) paths = Puppet[:pluginpath].split(":") unless paths.include?(Puppet[:plugindest]) Puppet.notice "Adding plugin destination %s to plugin search path" % Puppet[:plugindest] Puppet[:pluginpath] += ":" + Puppet[:plugindest] end paths.each do |dir| file = ::File.join(dir, name.to_s + ".rb") if FileTest.exists?(file) begin load file Puppet.info "loaded %s" % file return true rescue LoadError => detail Puppet.info "Could not load plugin %s: %s" % [file, detail] return false end end end end # Define a new type. def newtype(name, parent = nil, &block) # First make sure we don't have a method sitting around name = symbolize(name) newmethod = "new#{name.to_s}" # Used for method manipulation. selfobj = metaclass() @types ||= {} if @types.include?(name) if self.respond_to?(newmethod) # Remove the old newmethod selfobj.send(:remove_method,newmethod) end end # Then create the class. klass = genclass(name, :parent => (parent || Puppet::Type), :overwrite => true, :hash => @types, &block ) # Now define a "new" method for convenience. if self.respond_to? newmethod # Refuse to overwrite existing methods like 'newparam' or 'newtype'. Puppet.warning "'new#{name.to_s}' method already exists; skipping" else selfobj.send(:define_method, newmethod) do |*args| klass.create(*args) end end # If they've got all the necessary methods defined and they haven't # already added the state, then do so now. if klass.ensurable? and ! klass.validstate?(:ensure) klass.ensurable end # Now set up autoload any providers that might exist for this type. klass.providerloader = Puppet::Autoload.new(klass, "puppet/provider/#{klass.name.to_s}" ) # We have to load everything so that we can figure out the default type. klass.providerloader.loadall() klass end # Remove an existing defined type. Largely used for testing. def rmtype(name) # Then create the class. klass = rmclass(name, :hash => @types ) + + if respond_to?("new" + name.to_s) + metaclass.send(:remove_method, "new" + name.to_s) + end end # Return a Type instance by name. def type(name) @types ||= {} name = symbolize(name) if t = @types[name] return t else if typeloader.load(name) unless @types.include? name Puppet.warning "Loaded puppet/type/#{name} but no class was created" end else # If we can't load it from there, try loading it as a plugin. loadplugin(name) end return @types[name] end end # Create a loader for Puppet types. def typeloader unless defined? @typeloader @typeloader = Puppet::Autoload.new(self, "puppet/type", :wrap => false ) end @typeloader end end end # $Id$ diff --git a/lib/puppet/metatype/metaparams.rb b/lib/puppet/metatype/metaparams.rb index f28103a87..df226b146 100644 --- a/lib/puppet/metatype/metaparams.rb +++ b/lib/puppet/metatype/metaparams.rb @@ -1,355 +1,402 @@ require 'puppet' require 'puppet/type' class Puppet::Type # Add all of the meta parameters. #newmetaparam(:onerror) do # desc "How to handle errors -- roll back innermost # transaction, roll back entire transaction, ignore, etc. Currently # non-functional." #end newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done. *true*/**false**" munge do |noop| if noop == "true" or noop == true return true elsif noop == "false" or noop == false return false else self.fail("Invalid noop value '%s'" % noop) end end end newmetaparam(:schedule) do desc "On what schedule the object should be managed. You must create a schedule object, and then reference the name of that object to use that for your schedule: schedule { daily: period => daily, range => \"2-4\" } exec { \"/usr/bin/apt-get update\": schedule => daily } The creation of the schedule object does not need to appear in the configuration before objects that use it." munge do |name| if schedule = Puppet.type(:schedule)[name] return schedule else return name end end end newmetaparam(:check) do desc "States which should have their values retrieved but which should not actually be modified. This is currently used internally, but will eventually be used for querying, so that you could specify that you wanted to check the install state of all packages, and then query the Puppet client daemon to get reports on all packages." munge do |args| # If they've specified all, collect all known states if args == :all args = @parent.class.states.collect do |state| state.name end end unless args.is_a?(Array) args = [args] end unless defined? @parent self.devfail "No parent for %s, %s?" % [self.class, self.name] end args.each { |state| unless state.is_a?(Symbol) state = state.intern end next if @parent.statedefined?(state) stateklass = @parent.class.validstate?(state) unless stateklass raise Puppet::Error, "%s is not a valid attribute for %s" % [state, self.class.name] end next unless stateklass.checkable? - @parent.newstate(state) } end end # We've got four relationship metaparameters, so this method is used # to reduce code duplication between them. def store_relationship(param, values) # We need to support values passed in as an array or as a # resource reference. result = [] # 'values' could be an array or a reference. If it's an array, # it could be an array of references or an array of arrays. if values.is_a?(Puppet::Type) result << [values.class.name, values.title] else unless values.is_a?(Array) devfail "Relationships must be resource references" end if values[0].is_a?(String) or values[0].is_a?(Symbol) # we're a type/title array reference values[0] = symbolize(values[0]) result << values else # we're an array of stuff values.each do |value| if value.is_a?(Puppet::Type) result << [value.class.name, value.title] elsif value.is_a?(Array) value[0] = symbolize(value[0]) result << value else devfail "Invalid relationship %s" % value.inspect end end end end if existing = self[param] result = existing + result end result end - # For each object we require, subscribe to all events that it generates. We - # might reduce the level of subscription eventually, but for now... - newmetaparam(:require) do - desc "One or more objects that this object depends on. - This is used purely for guaranteeing that changes to required objects - happen before the dependent object. For instance: - - # Create the destination directory before you copy things down - file { \"/usr/local/scripts\": - ensure => directory - } - - file { \"/usr/local/scripts/myscript\": - source => \"puppet://server/module/myscript\", - mode => 755, - require => file[\"/usr/local/scripts\"] - } - - Note that Puppet will autorequire everything that it can, and - there are hooks in place so that it's easy for elements to add new - ways to autorequire objects, so if you think Puppet could be - smarter here, let us know. - - In fact, the above code was redundant -- Puppet will autorequire - any parent directories that are being managed; it will - automatically realize that the parent directory should be created - before the script is pulled down. - - Currently, exec elements will autorequire their CWD (if it is - specified) plus any fully qualified paths that appear in the - command. For instance, if you had an ``exec`` command that ran - the ``myscript`` mentioned above, the above code that pulls the - file down would be automatically listed as a requirement to the - ``exec`` code, so that you would always be running againts the - most recent version. - " - - # Take whatever dependencies currently exist and add these. - # Note that this probably doesn't behave correctly with unsubscribe. - munge do |requires| - @parent.store_relationship(:require, requires) - end - end - - # For each object we require, subscribe to all events that it generates. - # We might reduce the level of subscription eventually, but for now... - newmetaparam(:subscribe) do - desc "One or more objects that this object depends on. Changes in the - subscribed to objects result in the dependent objects being - refreshed (e.g., a service will get restarted). For instance: - - class nagios { - file { \"/etc/nagios/nagios.conf\": - source => \"puppet://server/module/nagios.conf\", - alias => nagconf # just to make things easier for me - } - service { nagios: - running => true, - subscribe => file[nagconf] - } - } - " - - munge do |requires| - @parent.store_relationship(:subscribe, requires) - end - end - newmetaparam(:loglevel) do desc "Sets the level that information will be logged. The log levels have the biggest impact when logs are sent to syslog (which is currently the default)." defaultto :notice newvalues(*Puppet::Log.levels) newvalues(:verbose) munge do |loglevel| val = super(loglevel) if val == :verbose val = :info end val end end newmetaparam(:alias) do desc "Creates an alias for the object. Puppet uses this internally when you provide a symbolic name: file { sshdconfig: path => $operatingsystem ? { solaris => \"/usr/local/etc/ssh/sshd_config\", default => \"/etc/ssh/sshd_config\" }, source => \"...\" } service { sshd: subscribe => file[sshdconfig] } When you use this feature, the parser sets ``sshdconfig`` as the name, and the library sets that as an alias for the file so the dependency lookup for ``sshd`` works. You can use this parameter yourself, but note that only the library can use these aliases; for instance, the following code will not work: file { \"/etc/ssh/sshd_config\": owner => root, group => root, alias => sshdconfig } file { sshdconfig: mode => 644 } There's no way here for the Puppet parser to know that these two stanzas should be affecting the same file. See the [language tutorial][] for more information. [language tutorial]: languagetutorial.html " munge do |aliases| unless aliases.is_a?(Array) aliases = [aliases] end @parent.info "Adding aliases %s" % aliases.collect { |a| a.inspect }.join(", ") aliases.each do |other| if obj = @parent.class[other] unless obj == @parent self.fail( "%s can not create alias %s: object already exists" % [@parent.title, other] ) end next end @parent.class.alias(other, @parent) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated element. While all elements are automatically tagged with as much information as possible (e.g., each class and component containing the element), it can be useful to add your own tags to a given element. Tags are currently useful for things like applying a subset of a host's configuration: puppetd --test --tag mytag This way, when you're testing a configuration you can run just the portion you're testing." munge do |tags| tags = [tags] unless tags.is_a? Array tags.each do |tag| @parent.tag(tag) end end end + + class RelationshipMetaparam < Puppet::Parameter + class << self + attr_accessor :direction, :events, :callback, :subclasses + end + + @subclasses = [] + + def self.inherited(sub) + @subclasses << sub + end + + def munge(rels) + @parent.store_relationship(self.class.name, rels) + end + + # Create edges from each of our relationships. :in + # relationships are specified by the event-receivers, and :out + # relationships are specified by the event generator. This + # way 'source' and 'target' are consistent terms in both edges + # and events -- that is, an event targets edges whose source matches + # the event's source. The direction of the relationship determines + # which resource is applied first and which resource is considered + # to be the event generator. + def to_edges + @value.collect do |value| + # we just have a name and a type, and we need to convert it + # to an object... + tname, name = value + object = nil + unless type = Puppet::Type.type(tname) + self.fail "Could not find type %s" % tname.inspect + end + unless object = type[name] + self.fail "Could not retrieve object '%s' of type '%s'" % + [name,type] + end + self.debug("subscribes to %s" % [object]) + + # Are we requiring them, or vice versa? See the builddepends + # method for further docs on this. + if self.class.direction == :in + source = object + target = @parent + else + source = @parent + target = object + end + + # ok, both sides of the connection store some information + # we store the method to call when a given subscription is + # triggered, but the source object decides whether + subargs = { + :event => self.class.events + } + + if method = self.class.callback + subargs[:callback] = method + end + rel = Puppet::Relationship.new(source, target, subargs) + end + end + end + + def self.relationship_params + RelationshipMetaparam.subclasses + end + + # For each object we require, subscribe to all events that it generates. We + # might reduce the level of subscription eventually, but for now... + newmetaparam(:require, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :NONE}) do + desc "One or more objects that this object depends on. + This is used purely for guaranteeing that changes to required objects + happen before the dependent object. For instance: + + # Create the destination directory before you copy things down + file { \"/usr/local/scripts\": + ensure => directory + } + + file { \"/usr/local/scripts/myscript\": + source => \"puppet://server/module/myscript\", + mode => 755, + require => file[\"/usr/local/scripts\"] + } - newmetaparam(:notify) do + Note that Puppet will autorequire everything that it can, and + there are hooks in place so that it's easy for elements to add new + ways to autorequire objects, so if you think Puppet could be + smarter here, let us know. + + In fact, the above code was redundant -- Puppet will autorequire + any parent directories that are being managed; it will + automatically realize that the parent directory should be created + before the script is pulled down. + + Currently, exec elements will autorequire their CWD (if it is + specified) plus any fully qualified paths that appear in the + command. For instance, if you had an ``exec`` command that ran + the ``myscript`` mentioned above, the above code that pulls the + file down would be automatically listed as a requirement to the + ``exec`` code, so that you would always be running againts the + most recent version. + " + end + + # For each object we require, subscribe to all events that it generates. + # We might reduce the level of subscription eventually, but for now... + newmetaparam(:subscribe, :parent => RelationshipMetaparam, :attributes => {:direction => :in, :events => :ALL_EVENTS, :callback => :refresh}) do + desc "One or more objects that this object depends on. Changes in the + subscribed to objects result in the dependent objects being + refreshed (e.g., a service will get restarted). For instance: + + class nagios { + file { \"/etc/nagios/nagios.conf\": + source => \"puppet://server/module/nagios.conf\", + alias => nagconf # just to make things easier for me + } + service { nagios: + running => true, + subscribe => file[nagconf] + } + } + " + end + + newmetaparam(:notify, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :ALL_EVENTS, :callback => :refresh}) do desc %{This parameter is the opposite of **subscribe** -- it sends events to the specified object: file { "/etc/sshd_config": source => "....", notify => service[sshd] } service { sshd: ensure => running } This will restart the sshd service if the sshd config file changes.} - - - munge do |notifies| - @parent.store_relationship(:notify, notifies) - end end - newmetaparam(:before) do + newmetaparam(:before, :parent => RelationshipMetaparam, :attributes => {:direction => :out, :events => :NONE}) do desc %{This parameter is the opposite of **require** -- it guarantees that the specified object is applied later than the specifying object: file { "/var/nagios/configuration": source => "...", recurse => true, before => exec["nagios-rebuid"] } exec { "nagios-rebuild": command => "/usr/bin/make", cwd => "/var/nagios/configuration" } This will make sure all of the files are up to date before the make command is run.} - - munge do |notifies| - @parent.store_relationship(:before, notifies) - end end end # Puppet::Type # $Id$ diff --git a/lib/puppet/metatype/relationships.rb b/lib/puppet/metatype/relationships.rb index 5f2471460..467b6187b 100644 --- a/lib/puppet/metatype/relationships.rb +++ b/lib/puppet/metatype/relationships.rb @@ -1,159 +1,117 @@ class Puppet::Type # Specify a block for generating a list of objects to autorequire. This # makes it so that you don't have to manually specify things that you clearly # require. def self.autorequire(name, &block) @autorequires ||= {} @autorequires[name] = block end # Yield each of those autorequires in turn, yo. def self.eachautorequire @autorequires ||= {} @autorequires.each { |type, block| yield(type, block) } end # Figure out of there are any objects we can automatically add as # dependencies. def autorequire reqs = [] self.class.eachautorequire { |type, block| # Ignore any types we can't find, although that would be a bit odd. next unless typeobj = Puppet.type(type) # Retrieve the list of names from the block. next unless list = self.instance_eval(&block) unless list.is_a?(Array) list = [list] end # Collect the current prereqs list.each { |dep| obj = nil # Support them passing objects directly, to save some effort. unless dep.is_a? Puppet::Type # Skip autorequires that we aren't managing unless dep = typeobj[dep] next end end debug "Autorequiring %s" % [dep.ref] reqs << Puppet::Relationship[dep, self] } } return reqs end - # Build the dependencies associated with an individual object. :in - # relationships are specified by the event-receivers, and :out - # relationships are specified by the event generator. This - # way 'source' and 'target' are consistent terms in both edges - # and events -- that is, an event targets edges whose source matches - # the event's source. Note that the direction of the relationship - # doesn't actually mean anything until you start using events -- - # the same information is present regardless. + # Build the dependencies associated with an individual object. def builddepends # Handle the requires - {:require => [:NONE, nil, :in], - :subscribe => [:ALL_EVENTS, :refresh, :in], - :notify => [:ALL_EVENTS, :refresh, :out], - :before => [:NONE, nil, :out]}.collect do |type, args| - if self[type] - handledepends(self[type], *args) - end - end.flatten.reject { |r| r.nil? } - end - - def handledepends(requires, event, method, direction) - # Requires are specified in the form of [type, name], so they're always - # an array. But we want them to be an array of arrays. - unless requires[0].is_a?(Array) - requires = [requires] - end - requires.collect { |rname| - # we just have a name and a type, and we need to convert it - # to an object... - type = nil - object = nil - tname = rname[0] - unless type = Puppet::Type.type(tname) - self.fail "Could not find type %s" % tname.inspect - end - name = rname[1] - unless object = type[name] - self.fail "Could not retrieve object '%s' of type '%s'" % - [name,type] - end - self.debug("subscribes to %s" % [object]) - - # Are we requiring them, or vice versa? See the builddepends - # method for further docs on this. - if direction == :in - source = object - target = self - else - source = self - target = object - end - - # ok, both sides of the connection store some information - # we store the method to call when a given subscription is - # triggered, but the source object decides whether - subargs = { - :event => event - } - - if method - subargs[:callback] = method + self.class.relationship_params.collect do |klass| + if param = @metaparams[klass.name] + param.to_edges end - rel = Puppet::Relationship.new(source, target, subargs) - } + end.flatten.reject { |r| r.nil? } end - - # Unsubscribe from a given object, possibly with a specific event. - def unsubscribe(object, event = nil) - # First look through our own relationship params - [:require, :subscribe].each do |param| - if values = self[param] - newvals = values.reject { |d| - d == [object.class.name, object.title] - } - if newvals.length != values.length - self.delete(param) - self[param] = newvals - end + + # Does this resource have a relationship with the other? We have to + # check each object for both directions of relationship. + def requires?(other) + them = [other.class.name, other.title] + me = [self.class.name, self.title] + self.class.relationship_params.each do |param| + case param.direction + when :in: return true if v = self[param.name] and v.include?(them) + when :out: return true if v = other[param.name] and v.include?(me) end end + return false end # we've received an event # we only support local events right now, so we can pass actual # objects around, including the transaction object # the assumption here is that container objects will pass received # methods on to contained objects # i.e., we don't trigger our children, our refresh() method calls # refresh() on our children def trigger(event, source) trans = event.transaction if @callbacks.include?(source) [:ALL_EVENTS, event.event].each { |eventname| if method = @callbacks[source][eventname] if trans.triggered?(self, method) > 0 next end if self.respond_to?(method) self.send(method) end trans.triggered(self, method) end } end end + + # Unsubscribe from a given object, possibly with a specific event. + def unsubscribe(object, event = nil) + # First look through our own relationship params + [:require, :subscribe].each do |param| + if values = self[param] + newvals = values.reject { |d| + d == [object.class.name, object.title] + } + if newvals.length != values.length + self.delete(param) + self[param] = newvals + end + end + end + end end # $Id$ diff --git a/lib/puppet/transaction.rb b/lib/puppet/transaction.rb index 5bd5afd2f..adb5eb134 100644 --- a/lib/puppet/transaction.rb +++ b/lib/puppet/transaction.rb @@ -1,561 +1,586 @@ # the class that actually walks our resource/state tree, collects the changes, # and performs them require 'puppet' require 'puppet/statechange' module Puppet class Transaction attr_accessor :component, :resources, :ignoreschedules, :ignoretags attr_accessor :relgraph, :sorted_resources attr_writer :tags include Puppet::Util Puppet.config.setdefaults(:transaction, :tags => ["", "Tags to use to find resources. If this is set, then only resources tagged with the specified tags will be applied. Values must be comma-separated."] ) # Add some additional times for reporting def addtimes(hash) hash.each do |name, num| @timemetrics[name] = num end end # Apply all changes for a resource, returning a list of the events # generated. def apply(resource) - # First make sure there are no failed dependencies. To do this, - # we check for failures in any of the vertexes above us. It's not - # enough to check the immediate dependencies, which is why we use - # a tree from the reversed graph. - @relgraph.reversal.tree_from_vertex(resource, :dfs).keys.each do |dep| - skip = false - if fails = failed?(dep) - resource.notice "Dependency %s[%s] has %s failures" % - [dep.class.name, dep.name, @failures[dep]] - skip = true - end - - if skip - resource.warning "Skipping because of failed dependencies" - @resourcemetrics[:skipped] += 1 - return [] - end - end - - # If the resource needs to generate new objects at eval time, do it now. - eval_generate(resource) - begin changes = resource.evaluate rescue => detail if Puppet[:trace] puts detail.backtrace end resource.err "Failed to retrieve current state: %s" % detail # Mark that it failed @failures[resource] += 1 # And then return return [] end unless changes.is_a? Array changes = [changes] end if changes.length > 0 @resourcemetrics[:out_of_sync] += 1 end resourceevents = changes.collect { |change| @changes << change @count += 1 change.transaction = self events = nil begin # use an array, so that changes can return more than one # event if they want events = [change.forward].flatten.reject { |e| e.nil? } rescue => detail if Puppet[:trace] puts detail.backtrace end change.state.err "change from %s to %s failed: %s" % [change.state.is_to_s, change.state.should_to_s, detail] @failures[resource] += 1 next # FIXME this should support using onerror to determine # behaviour; or more likely, the client calling us # should do so end # Mark that our change happened, so it can be reversed # if we ever get to that point unless events.nil? or (events.is_a?(Array) and events.empty?) change.changed = true @resourcemetrics[:applied] += 1 end events }.flatten.reject { |e| e.nil? } unless changes.empty? # Record when we last synced resource.cache(:synced, Time.now) # Flush, if appropriate if resource.respond_to?(:flush) resource.flush end end resourceevents end # Find all of the changed resources. def changed? @changes.find_all { |change| change.changed }.collect { |change| change.state.parent }.uniq end # Do any necessary cleanup. Basically just removes any generated # resources. def cleanup @generated.each do |resource| resource.remove end end # See if the resource generates new resources at evaluation time. def eval_generate(resource) if resource.respond_to?(:eval_generate) if children = resource.eval_generate + depthfirst = resource.depthfirst? dependents = @relgraph.adjacent(resource, :direction => :out, :type => :edges) targets = @relgraph.adjacent(resource, :direction => :in, :type => :edges) children.each do |gen_child| - gen_child.info "generated" - @relgraph.add_edge!(resource, gen_child) + if depthfirst + @relgraph.add_edge!(gen_child, resource) + else + @relgraph.add_edge!(resource, gen_child) + end dependents.each do |edge| @relgraph.add_edge!(gen_child, edge.target, edge.label) end targets.each do |edge| @relgraph.add_edge!(edge.source, gen_child, edge.label) end - @sorted_resources.insert(@sorted_resources.index(resource) + 1, gen_child) @generated << gen_child end + return children end end end # Evaluate a single resource. def eval_resource(resource) events = [] - unless tagged?(resource) - resource.debug "Not tagged with %s" % tags.join(", ") - return events - end - - unless scheduled?(resource) - resource.debug "Not scheduled" - return events - end - - @resourcemetrics[:scheduled] += 1 + if skip?(resource) + @resourcemetrics[:skipped] += 1 + else + @resourcemetrics[:scheduled] += 1 + + # We need to generate first regardless, because the recursive + # actions sometimes change how the top resource is applied. + children = eval_generate(resource) + + if resource.depthfirst? and children + children.each do |child| + events += eval_resource(child) + end + end - # Perform the actual changes - seconds = thinmark do - events = apply(resource) - end + # Perform the actual changes + seconds = thinmark do + events += apply(resource) + end + + if ! resource.depthfirst? and children + children.each do |child| + events += eval_resource(child) + end + end - # Keep track of how long we spend in each type of resource - @timemetrics[resource.class.name] += seconds + # Keep track of how long we spend in each type of resource + @timemetrics[resource.class.name] += seconds + end # Check to see if there are any events for this resource if triggedevents = trigger(resource) events += triggedevents end # Collect the targets of any subscriptions to those events @relgraph.matching_edges(events).each do |edge| @targets[edge.target] << edge end # And return the events for collection events end # This method does all the actual work of running a transaction. It # collects all of the changes, executes them, and responds to any # necessary events. def evaluate @count = 0 # Start logging. Puppet::Log.newdestination(@report) prepare() begin allevents = @sorted_resources.collect { |resource| eval_resource(resource) }.flatten.reject { |e| e.nil? } ensure # And then close the transaction log. Puppet::Log.close(@report) end cleanup() Puppet.debug "Finishing transaction %s with %s changes" % [self.object_id, @count] allevents end # Determine whether a given resource has failed. def failed?(obj) if @failures[obj] > 0 return @failures[obj] else return false end end + + # Does this resource have any failed dependencies? + def failed_dependencies?(resource) + # First make sure there are no failed dependencies. To do this, + # we check for failures in any of the vertexes above us. It's not + # enough to check the immediate dependencies, which is why we use + # a tree from the reversed graph. + skip = false + @relgraph.reversal.tree_from_vertex(resource, :dfs).keys.each do |dep| + if fails = failed?(dep) + resource.notice "Dependency %s[%s] has %s failures" % + [dep.class.name, dep.name, @failures[dep]] + skip = true + end + end + + return skip + end # Collect any dynamically generated resources. def generate list = @resources.vertices # Store a list of all generated resources, so that we can clean them up # after the transaction closes. @generated = [] newlist = [] while ! list.empty? list.each do |resource| if resource.respond_to?(:generate) made = resource.generate next unless made unless made.is_a?(Array) made = [made] end made.uniq! made.each do |res| @resources.add_vertex!(res) newlist << res @generated << res end end end list.clear list = newlist newlist = [] end end # this should only be called by a Puppet::Type::Component resource now # and it should only receive an array def initialize(resources) @resources = resources.to_graph @resourcemetrics = { :total => @resources.vertices.length, :out_of_sync => 0, # The number of resources that had changes :applied => 0, # The number of resources fixed :skipped => 0, # The number of resources skipped :restarted => 0, # The number of resources triggered :failed_restarts => 0, # The number of resources that fail a trigger :scheduled => 0 # The number of resources scheduled } # Metrics for distributing times across the different types. @timemetrics = Hash.new(0) # The number of resources that were triggered in this run @triggered = Hash.new { |hash, key| hash[key] = Hash.new(0) } # Targets of being triggered. @targets = Hash.new do |hash, key| hash[key] = [] end # The changes we're performing @changes = [] # The resources that have failed and the number of failures each. This # is used for skipping resources because of failed dependencies. @failures = Hash.new do |h, key| h[key] = 0 end @report = Report.new end # Prefetch any providers that support it. We don't support prefetching # types, just providers. def prefetch @resources.collect { |obj| if pro = obj.provider pro.class else nil end }.reject { |o| o.nil? }.uniq.each do |klass| # XXX We need to do something special here in case of failure. if klass.respond_to?(:prefetch) klass.prefetch end end end # Prepare to evaluate the elements in a transaction. def prepare prefetch() # Now add any dynamically generated resources generate() # Create a relationship graph from our resource graph @relgraph = relationship_graph @sorted_resources = @relgraph.topsort end # Create a graph of all of the relationships in our resource graph. def relationship_graph graph = Puppet::PGraph.new # First create the dependency graph @resources.vertices.each do |vertex| graph.add_vertex!(vertex) vertex.builddepends.each do |edge| graph.add_edge!(edge) end end # Then splice in the container information graph.splice!(@resources, Puppet::Type::Component) # Lastly, add in any autorequires graph.vertices.each do |vertex| vertex.autorequire.each do |edge| unless graph.edge?(edge) graph.add_edge!(edge) end end end return graph end # Generate a transaction report. def report @resourcemetrics[:failed] = @failures.find_all do |name, num| num > 0 end.length # Get the total time spent @timemetrics[:total] = @timemetrics.inject(0) do |total, vals| total += vals[1] total end # Unfortunately, RRD does not deal well with changing lists of values, # so we have to pick a list of values and stick with it. In this case, # that means we record the total time, the config time, and that's about # it. We should probably send each type's time as a separate metric. @timemetrics.dup.each do |name, value| if Puppet::Type.type(name) @timemetrics.delete(name) end end # Add all of the metrics related to resource count and status @report.newmetric(:resources, @resourcemetrics) # Record the relative time spent in each resource. @report.newmetric(:time, @timemetrics) # Then all of the change-related metrics @report.newmetric(:changes, :total => @changes.length ) @report.time = Time.now return @report end # Roll all completed changes back. def rollback @targets.clear @triggered.clear allevents = @changes.reverse.collect { |change| # skip changes that were never actually run unless change.changed Puppet.debug "%s was not changed" % change.to_s next end begin events = change.backward rescue => detail Puppet.err("%s rollback failed: %s" % [change,detail]) if Puppet[:trace] puts detail.backtrace end next # at this point, we would normally do error handling # but i haven't decided what to do for that yet # so just record that a sync failed for a given resource #@@failures[change.state.parent] += 1 # this still could get hairy; what if file contents changed, # but a chmod failed? how would i handle that error? dern end @relgraph.matching_edges(events).each do |edge| @targets[edge.target] << edge end # Now check to see if there are any events for this child. # Kind of hackish, since going backwards goes a change at a # time, not a child at a time. trigger(change.state.parent) # And return the events for collection events }.flatten.reject { |e| e.nil? } end # Is the resource currently scheduled? def scheduled?(resource) self.ignoreschedules or resource.scheduled? end + # Should this resource be skipped? + def skip?(resource) + skip = false + if ! tagged?(resource) + resource.debug "Not tagged with %s" % tags.join(", ") + elsif ! scheduled?(resource) + resource.debug "Not scheduled" + elsif failed_dependencies?(resource) + resource.warning "Skipping because of failed dependencies" + else + return false + end + return true + end + # The tags we should be checking. def tags # Allow the tags to be overridden unless defined? @tags @tags = Puppet[:tags] end unless defined? @processed_tags if @tags.nil? or @tags == "" @tags = [] else @tags = [@tags] unless @tags.is_a? Array @tags = @tags.collect do |tag| tag.split(/\s*,\s*/) end.flatten end @processed_tags = true end @tags end # Is this resource tagged appropriately? def tagged?(resource) self.ignoretags or tags.empty? or resource.tagged?(tags) end # Are there any edges that target this resource? def targeted?(resource) @targets[resource] end # Trigger any subscriptions to a child. This does an upwardly recursive # search -- it triggers the passed resource, but also the resource's parent # and so on up the tree. def trigger(child) obj = child callbacks = Hash.new { |hash, key| hash[key] = [] } sources = Hash.new { |hash, key| hash[key] = [] } trigged = [] while obj if @targets.include?(obj) callbacks.clear sources.clear @targets[obj].each do |edge| # Some edges don't have callbacks next unless edge.callback # Collect all of the subs for each callback callbacks[edge.callback] << edge # And collect the sources for logging sources[edge.source] << edge.callback end sources.each do |source, callbacklist| obj.debug "%s[%s] results in triggering %s" % [source.class.name, source.name, callbacklist.join(", ")] end callbacks.each do |callback, subs| message = "Triggering '%s' from %s dependencies" % [callback, subs.length] obj.notice message # At this point, just log failures, don't try to react # to them in any way. begin obj.send(callback) @resourcemetrics[:restarted] += 1 rescue => detail obj.err "Failed to call %s on %s: %s" % [callback, obj, detail] @resourcemetrics[:failed_restarts] += 1 if Puppet[:trace] puts detail.backtrace end end # And then add an event for it. trigged << Puppet::Event.new( :event => :triggered, :transaction => self, :source => obj, :message => message ) triggered(obj, callback) end end obj = obj.parent end if trigged.empty? return nil else return trigged end end def triggered(resource, method) @triggered[resource][method] += 1 end def triggered?(resource, method) @triggered[resource][method] end end end require 'puppet/transaction/report' # $Id$ diff --git a/lib/puppet/type/component.rb b/lib/puppet/type/component.rb index cadd586c8..f203179a8 100644 --- a/lib/puppet/type/component.rb +++ b/lib/puppet/type/component.rb @@ -1,177 +1,135 @@ # the object allowing us to build complex structures # this thing contains everything else, including itself require 'puppet' require 'puppet/type' require 'puppet/transaction' require 'puppet/pgraph' module Puppet newtype(:component) do include Enumerable newparam(:name) do desc "The name of the component. Generally optional." isnamevar end newparam(:type) do desc "The type that this component maps to. Generally some kind of class from the language." defaultto "component" end - # topo sort functions - def self.sort(objects) - list = [] - tmplist = {} - - objects.each { |obj| - self.recurse(obj, tmplist, list) - } - - return list.flatten - end - - # FIXME this method assumes that dependencies themselves - # are never components - def self.recurse(obj, inlist, list) - if inlist.include?(obj.object_id) - return - end - inlist[obj.object_id] = true - begin - obj.eachdependency { |req| - self.recurse(req, inlist, list) - } - rescue Puppet::Error => detail - raise Puppet::Error, "%s: %s" % [obj.path, detail] - end - - if obj.is_a? self - obj.each { |child| - self.recurse(child, inlist, list) - } - else - list << obj - end - end - # Remove a child from the component. def delete(child) if @children.include?(child) @children.delete(child) return true else return false end end # Return each child in turn. def each @children.each { |child| yield child } end # flatten all children, sort them, and evaluate them in order # this is only called on one component over the whole system # this also won't work with scheduling, but eh def evaluate self.finalize unless self.finalized? transaction = Puppet::Transaction.new(self) transaction.component = self return transaction end # Do all of the polishing off, mostly doing autorequires and making # dependencies. This will get run once on the top-level component, # and it will do everything necessary. def finalize started = {} finished = {} # First do all of the finish work, which mostly involves self.delve do |object| # Make sure we don't get into loops if started.has_key?(object) debug "Already finished %s" % object.title next else started[object] = true end unless finished.has_key?(object) object.finish finished[object] = true end end @finalized = true end def finalized? if defined? @finalized return @finalized else return false end end - - # Return a flattened array containing all of the children - # and all child components' children, sorted in order of dependencies. - def flatten - self.class.sort(@children).flatten - end # Initialize a new component def initialize(args) @children = [] super(args) end # We have a different way of setting the title def title unless defined? @title if self[:type] == self[:name] or self[:name] =~ /--\d+$/ @title = self[:type] else @title = "%s[%s]" % [self[:type],self[:name]] end end return @title end def refresh @children.collect { |child| if child.respond_to?(:refresh) child.refresh child.log "triggering %s" % :refresh end } end # Convert to a graph object with all of the container info. def to_graph graph = Puppet::PGraph.new delver = proc do |obj| obj.each do |child| if child.is_a?(Puppet::Type) graph.add_edge!(obj, child) delver.call(child) end end end delver.call(self) return graph end def to_s return "component(%s)" % self.title end end end # $Id$ diff --git a/lib/puppet/type/pfile.rb b/lib/puppet/type/pfile.rb index b3bbba3b9..24f961a62 100644 --- a/lib/puppet/type/pfile.rb +++ b/lib/puppet/type/pfile.rb @@ -1,1033 +1,1026 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/type/state' require 'puppet/server/fileserver' module Puppet newtype(:file) do @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the ``file`` element will be used less and less to manage content, and instead native elements will be used to do so. If you find that you are often copying files in from a central location, rather than using native elements, please contact Reductive Labs and we can hopefully work with you to develop a native element to support what you are doing." newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| unless value =~ /^#{File::SEPARATOR}/ raise Puppet::Error, "File paths must be fully qualified" end end end newparam(:backup) do desc "Whether files should be backed up before being replaced. If a filebucket is specified, files will be backed up there; else, they will be backed up in the same directory with a ``.puppet-bak`` extension,, and no backups will be made if backup is ``false``. To use filebuckets, you must first create a filebucket in your configuration: filebucket { main: server => puppet } The ``puppetmasterd`` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file: file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the only benefits to doing so are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, and you can restore any given file manually, no matter how old. Eventually, transactional support will be able to automatically restore filebucketed files. " attr_reader :bucket defaultto ".puppet-bak" munge do |value| case value when false, "false", :false: false when true, "true", ".puppet-bak", :true: ".puppet-bak" when String: # We can't depend on looking this up right now, # we have to do it after all of the objects # have been instantiated. @bucket = value value else self.fail "Invalid backup type %s" % value.inspect end end # Provide a straight-through hook for setting the bucket. def bucket=(bucket) @value = bucket @bucket = bucket end end newparam(:linkmaker) do desc "An internal parameter used by the *symlink* type to do recursive link creation." end newparam(:recurse) do desc "Whether and how deeply to do recursive management." newvalues(:true, :false, :inf, /^[0-9]+$/) munge do |value| newval = super(value) case newval when :true, :inf: true when :false: false else newval end end end - newparam(:replace) do + newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) defaultto :true end - newparam(:force) do + newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. ``[a-z]*``. Matches that would descend into the directory structure are ignored, e.g., ``*/*``." defaultto false validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, ``follow`` will copy the target file instead of the link, ``manage`` will copy the link itself, and ``ignore`` will just pass it by. When not copying, ``manage`` and ``ignore`` behave equivalently (because you cannot really ignore links entirely during local recursion), and ``follow`` will manage the file to which the link points." newvalues(:follow, :manage, :ignore) # :ignore and :manage behave equivalently on local files, # but don't copy remote links defaultto :ignore end - newparam(:purge) do + newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories." defaultto :false newvalues(:true, :false) end # Autorequire any parent directories. autorequire(:file) do File.dirname(self[:path]) end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, state| autorequire(type) do if @states.include?(state) # The user/group states automatically converts to IDs next unless should = @states[state].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end validate do if self[:content] and self[:source] self.fail "You cannot specify both content and a source" end end # List files, but only one level deep. def self.list(base = "/") unless FileTest.directory?(base) return [] end files = [] Dir.entries(base).reject { |e| e == "." or e == ".." }.each do |name| path = File.join(base, name) if obj = self[path] obj[:check] = :all files << obj else files << self.create( :name => path, :check => :all ) end end files end @depthfirst = false def argument?(arg) @arghash.include?(arg) end # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' state to do things. if writeable asuser = self.should(:owner) end end return asuser end # We have to do some extra finishing, to retrieve our bucket if # there is one def finish # Let's cache these values, since there should really only be # a couple of these buckets @@filebuckets ||= {} # Look up our bucket, if there is one if @parameters.include?(:backup) and bucket = @parameters[:backup].bucket case bucket when String: if obj = @@filebuckets[bucket] # This sets the @value on :backup, too @parameters[:backup].bucket = obj elsif obj = Puppet.type(:filebucket).bucket(bucket) @@filebuckets[bucket] = obj @parameters[:backup].bucket = obj else self.fail "Could not find filebucket %s" % bucket end when Puppet::Client::Dipper: # things are hunky-dorey else self.fail "Invalid bucket type %s" % bucket.class end end super end # Create any children via recursion or whatever. def eval_generate recurse() end # Deal with backups. def handlebackup(file = nil) # let the path be specified file ||= self[:path] # if they specifically don't want a backup, then just say # we're good unless FileTest.exists?(file) return true end unless self[:backup] return true end case File.stat(file).ftype when "directory": if self[:recurse] # we don't need to backup directories when recurse is on return true else backup = self[:backup] case backup when Puppet::Client::Dipper: notice "Recursively backing up to filebucket" require 'find' Find.find(self[:path]) do |f| if File.file?(f) sum = backup.backup(f) self.info "Filebucketed %s to %s with sum %s" % [f, backup.name, sum] end end return true when String: newfile = file + backup # Just move it, since it's a directory. if FileTest.exists?(newfile) remove_backup(newfile) end begin bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp_r(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end end when "file": backup = self[:backup] case backup when Puppet::Client::Dipper: sum = backup.backup(file) self.info "Filebucketed to %s with sum %s" % [backup.name, sum] return true when String: newfile = file + backup if FileTest.exists?(newfile) remove_backup(newfile) end begin # FIXME Shouldn't this just use a Puppet object with # 'source' specified? bfile = file + backup # Ruby 1.8.1 requires the 'preserve' addition, but # later versions do not appear to require it. FileUtils.cp(file, bfile, :preserve => true) return true rescue => detail # since they said they want a backup, let's error out # if we couldn't make one self.fail "Could not back %s up: %s" % [file, detail.message] end else self.err "Invalid backup type %s" % backup.inspect return false end when "link": return true else self.notice "Cannot backup files of type %s" % File.stat(file).ftype return false end end def handleignore(children) return children unless self[:ignore] self[:ignore].each { |ignore| ignored = [] Dir.glob(File.join(self[:path],ignore), File::FNM_DOTMATCH) { |match| ignored.push(File.basename(match)) } children = children - ignored } return children end def initialize(hash) # Store a copy of the arguments for later. tmphash = hash.to_hash # Used for caching clients @clients = {} super # Get rid of any duplicate slashes, and remove any trailing slashes. @title = @title.gsub(/\/+/, "/").sub(/\/$/, "") # Clean out as many references to any file paths as possible. # This was the source of many, many bugs. @arghash = tmphash @arghash.delete(self.class.namevar) [:source, :parent].each do |param| if @arghash.include?(param) @arghash.delete(param) end end - if @arghash[:target] - warning "%s vs %s" % [@arghash[:ensure], @arghash[:target]] - end - @stat = nil end # Build a recursive map of a link source def linkrecurse(recurse) target = @states[:target].should method = :lstat if self[:links] == :follow method = :stat end targetstat = nil unless FileTest.exist?(target) return end # Now stat our target targetstat = File.send(method, target) unless targetstat.ftype == "directory" return end # Now that we know our corresponding target is a directory, # change our type + info "setting ensure to target" self[:ensure] = :directory unless FileTest.readable? target self.notice "Cannot manage %s: permission denied" % self.name return end children = Dir.entries(target).reject { |d| d =~ /^\.+$/ } # Get rid of ignored children if @parameters.include?(:ignore) children = handleignore(children) end added = [] children.each do |file| Dir.chdir(target) do longname = File.join(target, file) # Files know to create directories when recursion # is enabled and we're making links args = { :recurse => recurse, :ensure => longname } if child = self.newchild(file, true, args) added << child end end end added end # Build up a recursive map of what's around right now def localrecurse(recurse) unless FileTest.exist?(self[:path]) and self.stat.directory? #self.info "%s is not a directory; not recursing" % # self[:path] return end unless FileTest.readable? self[:path] self.notice "Cannot manage %s: permission denied" % self.name return end children = Dir.entries(self[:path]) #Get rid of ignored children if @parameters.include?(:ignore) children = handleignore(children) end added = [] children.each { |file| file = File.basename(file) next if file =~ /^\.\.?$/ # skip . and .. options = {:recurse => recurse} if child = self.newchild(file, true, options) # Mark any unmanaged files for removal if purge is set. # Use the array rather than [] because tidy uses this method, too. if @parameters.include?(:purge) and self.purge? info "purging %s" % child.ref child[:ensure] = :absent else child[:require] = self end added << child end } added end # Create a new file or directory object as a child to the current # object. def newchild(path, local, hash = {}) # make local copy of arguments args = @arghash.dup if path =~ %r{^#{File::SEPARATOR}} self.devfail( "Must pass relative paths to PFile#newchild()" ) else path = File.join(self[:path], path) end args[:path] = path unless hash.include?(:recurse) if args.include?(:recurse) if args[:recurse].is_a?(Integer) args[:recurse] -= 1 # reduce the level of recursion end end end hash.each { |key,value| args[key] = value } child = nil klass = nil # We specifically look in @parameters here, because 'linkmaker' isn't # a valid attribute for subclasses, so using 'self[:linkmaker]' throws # an error. if @parameters.include?(:linkmaker) and args.include?(:source) and ! FileTest.directory?(args[:source]) klass = Puppet.type(:symlink) # clean up the args a lot for links old = args.dup args = { :ensure => old[:source], :path => path } else klass = self.class end # The child might already exist because 'localrecurse' runs # before 'sourcerecurse'. I could push the override stuff into # a separate method or something, but the work is the same other # than this last bit, so it doesn't really make sense. if child = klass[path] unless child.parent.object_id == self.object_id self.debug "Not managing more explicit file %s" % path return nil end # This is only necessary for sourcerecurse, because we might have # created the object with different 'should' values than are # set remotely. unless local args.each { |var,value| next if var == :path next if var == :name # behave idempotently unless child.should(var) == value child[var] = value end } end return nil else # create it anew #notice "Creating new file with args %s" % args.inspect args[:parent] = self begin child = klass.implicitcreate(args) # implicit creation can return nil if child.nil? return nil end rescue Puppet::Error => detail self.notice( "Cannot manage: %s" % [detail.message] ) self.debug args.inspect child = nil rescue => detail self.notice( "Cannot manage: %s" % [detail] ) self.debug args.inspect child = nil end end return child end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder if defined? @parent # We only need to behave specially when our parent is also # a file if @parent.is_a?(self.class) # Remove the parent file name ppath = @parent.path.sub(/\/?file=.+/, '') tmp = [] if ppath != "/" and ppath != "" tmp << ppath end tmp << self.class.name.to_s + "=" + self.name return tmp else return super end else # The top-level name is always puppet[top], so we don't # bother with that. And we don't add the hostname # here, it gets added in the log server thingy. if self.name == "puppet[top]" return ["/"] else # We assume that if we don't have a parent that we # should not cache the path return [self.class.name.to_s + "=" + self.name] end end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recurse into the directory. This basically just calls 'localrecurse' # and maybe 'sourcerecurse', returning the collection of generated # files. def recurse # are we at the end of the recursion? unless self.recurse? return end recurse = self[:recurse] # we might have a string, rather than a number if recurse.is_a?(String) if recurse =~ /^[0-9]+$/ recurse = Integer(recurse) else # anything else is infinite recursion recurse = true end end if recurse.is_a?(Integer) recurse -= 1 end children = [] # We want to do link-recursing before normal recursion so that all # of the target stuff gets copied over correctly. if @states.include? :target and ret = self.linkrecurse(recurse) children += ret end if ret = self.localrecurse(recurse) children += ret end if @states.include?(:source) and ret = self.sourcerecurse(recurse) children += ret end children end # A simple method for determining whether we should be recursing. def recurse? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value if val and (val == true or val > 0) return true else return false end end # Remove the old backup. def remove_backup(newfile) if self.class.name == :file and self[:links] != :follow method = :lstat else method = :stat end old = File.send(method, newfile).ftype if old == "directory" raise Puppet::Error, "Will not remove directory backup %s; use a filebucket" % newfile end info "Removing old backup of type %s" % File.send(method, newfile).ftype begin File.unlink(newfile) rescue => detail if Puppet[:trace] puts detail.backtrace end self.err "Could not remove old backup: %s" % detail return false end end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat(true) unless handlebackup self.fail "Could not back up; will not replace" end unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory": if self[:force] == :true debug "Removing existing directory for replacement with %s" % should FileUtils.rmtree(self[:path]) else notice "Not replacing directory; use 'force' to override" end when "link", "file": debug "Removing existing %s for replacement with %s" % [s.ftype, should] File.unlink(self[:path]) else self.fail "Could not back up files of type %s" % s.ftype end end # a wrapper method to make sure the file exists before doing anything def retrieve unless stat = self.stat(true) self.debug "File does not exist" @states.each { |name,state| - # We've already retrieved the source, and we don't - # want to overwrite whatever it did. This is a bit - # of a hack, but oh well, source is definitely special. - # next if name == :source state.is = :absent } # If the file doesn't exist but we have a source, then call # retrieve on that state if @states.include?(:source) @states[:source].retrieve end return end states().each { |state| state.retrieve } end # This recurses against the remote source and makes sure the local # and remote structures match. It's run after 'localrecurse'. This # method only does anything when its corresponding remote entry is # a directory; in that case, this method creates file objects that # correspond to any contained remote files. def sourcerecurse(recurse) # we'll set this manually as necessary if @arghash.include?(:ensure) @arghash.delete(:ensure) end r = false if recurse unless recurse == 0 r = 1 end end ignore = self[:ignore] @states[:source].should.each do |source| sourceobj, path = uri2obj(source) # okay, we've got our source object; now we need to # build up a local file structure to match the remote # one server = sourceobj.server desc = server.list(path, self[:links], r, ignore) if desc == "" next end # Now create a new child for every file returned in the list. return desc.split("\n").collect { |line| file, type = line.split("\t") next if file == "/" # skip the listing object name = file.sub(/^\//, '') args = {:source => source + file} if type == file args[:recurse] = nil end self.newchild(name, false, args) }.reject {|c| c.nil? }.each do |f| f.info "sourced" end end return [] end # Set the checksum, from another state. There are multiple states that # modify the contents of a file, and they need the ability to make sure # that the checksum value is in sync. def setchecksum(sum = nil) if @states.include? :checksum if sum @states[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. @states[:checksum].retrieve @states[:checksum].checksum = @states[:checksum].is end end end # Stat our file. Depending on the value of the 'links' attribute, we use # either 'stat' or 'lstat', and we expect the states to use the resulting # stat object accordingly (mostly by testing the 'ftype' value). def stat(refresh = false) method = :stat # Files are the only types that support links if self.class.name == :file and self[:links] != :follow method = :lstat end path = self[:path] # Just skip them when they don't exist at all. unless FileTest.exists?(path) or FileTest.symlink?(path) @stat = nil return @stat end if @stat.nil? or refresh == true begin @stat = File.send(method, self[:path]) rescue Errno::ENOENT => error @stat = nil rescue Errno::EACCES => error self.warning "Could not stat; permission denied" @stat = nil end end return @stat end def uri2obj(source) sourceobj = FileSource.new path = nil unless source devfail "Got a nil source" end if source =~ /^\// source = "file://localhost/%s" % URI.escape(source) sourceobj.mount = "localhost" sourceobj.local = true end begin uri = URI.parse(URI.escape(source)) rescue => detail self.fail "Could not understand source %s: %s" % [source, detail.to_s] end case uri.scheme when "file": unless defined? @@localfileserver @@localfileserver = Puppet::Server::FileServer.new( :Local => true, :Mount => { "/" => "localhost" }, :Config => false ) #@@localfileserver.mount("/", "localhost") end sourceobj.server = @@localfileserver path = "/localhost" + uri.path when "puppet": args = { :Server => uri.host } if uri.port args[:Port] = uri.port end # FIXME We should cache a copy of this server #sourceobj.server = Puppet::NetworkClient.new(args) unless @clients.include?(source) @clients[source] = Puppet::Client::FileClient.new(args) end sourceobj.server = @clients[source] tmp = uri.path if tmp =~ %r{^/(\w+)} sourceobj.mount = $1 path = tmp #path = tmp.sub(%r{^/\w+},'') || "/" else self.fail "Invalid source path %s" % tmp end else self.fail "Got other recursive file proto %s from %s" % [uri.scheme, source] end return [sourceobj, path.sub(/\/\//, '/')] end # Write out the file. We open the file correctly, with all of the # uid and mode and such, and then yield the file handle for actual # writing. def write(usetmp = true) mode = self.should(:mode) remove_existing(:file) # The temporary file path = nil if usetmp path = self[:path] + ".puppettmp" else path = self[:path] end # As the correct user and group Puppet::SUIDManager.asuser(asuser(), self.should(:group)) do f = nil # Open our file with the correct modes if mode Puppet::Util.withumask(000) do f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC, mode) end else f = File.open(path, File::CREAT|File::WRONLY|File::TRUNC) end # Yield it yield f f.flush f.close end # And put our new file in place if usetmp begin File.rename(path, self[:path]) rescue => detail self.err "Could not rename tmp %s for replacing: %s" % [self[:path], detail] ensure # Make sure the created file gets removed if FileTest.exists?(path) File.unlink(path) end end end # And then update our checksum, so the next run doesn't find it. # FIXME This is extra work, because it's going to read the whole # file back in again. self.setchecksum end end # Puppet.type(:pfile) # the filesource class can't include the path, because the path # changes for every file instance class FileSource attr_accessor :mount, :root, :server, :local end # We put all of the states in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the state list. require 'puppet/type/pfile/checksum' require 'puppet/type/pfile/content' # can create the file require 'puppet/type/pfile/source' # can create the file require 'puppet/type/pfile/target' require 'puppet/type/pfile/ensure' # can create the file require 'puppet/type/pfile/uid' require 'puppet/type/pfile/group' require 'puppet/type/pfile/mode' require 'puppet/type/pfile/type' end # $Id$ diff --git a/lib/puppet/type/pfile/checksum.rb b/lib/puppet/type/pfile/checksum.rb index a91f7e017..c4ae6e8c3 100755 --- a/lib/puppet/type/pfile/checksum.rb +++ b/lib/puppet/type/pfile/checksum.rb @@ -1,341 +1,341 @@ # Keep a copy of the file checksums, and notify when they change. # This state never actually modifies the system, it only notices when the system # changes on its own. module Puppet Puppet.type(:file).newstate(:checksum) do desc "How to check whether a file has changed. This state is used internally for file copying, but it can also be used to monitor files somewhat like Tripwire without managing the file contents in any way. You can specify that a file's checksum should be monitored and then subscribe to the file from another object and receive events to signify checksum changes, for instance." @event = :file_changed @unmanaged = true @validtypes = %w{md5 md5lite timestamp mtime time} def self.validtype?(type) @validtypes.include?(type) end @validtypes.each do |ctype| newvalue(ctype) do handlesum() end end str = @validtypes.join("|") # This is here because Puppet sets this internally, using # {md5}...... newvalue(/^\{#{str}\}/) do handlesum() end newvalue(:nosum) do # nothing :nochange end # If they pass us a sum type, behave normally, but if they pass # us a sum type + sum, stick the sum in the cache. munge do |value| if value =~ /^\{(\w+)\}(.+)$/ type = symbolize($1) sum = $2 cache(type, sum) return type else if FileTest.directory?(@parent[:path]) return :time else return symbolize(value) end end end # Store the checksum in the data cache, or retrieve it if only the # sum type is provided. def cache(type, sum = nil) unless type raise ArgumentError, "A type must be specified to cache a checksum" end type = symbolize(type) unless state = @parent.cached(:checksums) self.debug "Initializing checksum hash" state = {} @parent.cache(:checksums, state) end if sum unless sum =~ /\{\w+\}/ sum = "{%s}%s" % [type, sum] end state[type] = sum else return state[type] end end # Because source and content and whomever else need to set the checksum # and do the updating, we provide a simple mechanism for doing so. def checksum=(value) @is = value munge(@should) self.updatesum end def checktype self.should || :md5 end # Checksums need to invert how changes are printed. def change_to_s begin if @is == :absent return "defined '%s' as '%s'" % [self.name, self.currentsum] elsif self.should == :absent return "undefined %s from '%s'" % [self.name, self.is_to_s] else if defined? @cached and @cached return "%s changed '%s' to '%s'" % [self.name, @cached, self.is_to_s] else return "%s changed '%s' to '%s'" % [self.name, self.currentsum, self.is_to_s] end end rescue Puppet::Error, Puppet::DevError raise rescue => detail raise Puppet::DevError, "Could not convert change %s to string: %s" % [self.name, detail] end end def currentsum #"{%s}%s" % [self.should, cache(self.should)] cache(checktype()) end # Retrieve the cached sum def getcachedsum hash = nil unless hash = @parent.cached(:checksums) hash = {} @parent.cache(:checksums, hash) end sumtype = self.should if hash.include?(sumtype) #self.notice "Found checksum %s for %s" % # [hash[sumtype] ,@parent[:path]] sum = hash[sumtype] unless sum =~ /^\{\w+\}/ sum = "{%s}%s" % [sumtype, sum] end return sum elsif hash.empty? #self.notice "Could not find sum of type %s" % sumtype return :nosum else #self.notice "Found checksum for %s but not of type %s" % # [@parent[:path],sumtype] return :nosum end end # Calculate the sum from disk. def getsum(checktype) sum = "" checktype = checktype.intern if checktype.is_a? String case checktype when :md5, :md5lite: if ! FileTest.file?(@parent[:path]) @parent.debug "Cannot MD5 sum %s; using mtime" % [@parent.stat.ftype] sum = @parent.stat.mtime.to_s else begin File.open(@parent[:path]) { |file| text = nil case checktype when :md5 text = file.read when :md5lite text = file.read(512) end if text.nil? self.debug "Not checksumming empty file %s" % @parent[:path] sum = 0 else sum = Digest::MD5.hexdigest(text) end } rescue Errno::EACCES => detail self.notice "Cannot checksum %s: permission denied" % @parent[:path] @parent.delete(self.class.name) rescue => detail self.notice "Cannot checksum: %s" % detail @parent.delete(self.class.name) end end when :timestamp, :mtime: sum = @parent.stat.mtime.to_s #sum = File.stat(@parent[:path]).mtime.to_s when :time: sum = @parent.stat.ctime.to_s #sum = File.stat(@parent[:path]).ctime.to_s else raise Puppet::Error, "Invalid sum type %s" % checktype end return "{#{checktype}}" + sum.to_s end # At this point, we don't actually modify the system, we modify # the stored state to reflect the current state, and then kick # off an event to mark any changes. def handlesum if @is.nil? raise Puppet::Error, "Checksum state for %s is somehow nil" % @parent.title end if @is == :absent self.retrieve if self.insync? self.debug "Checksum is already in sync" return nil end #@parent.debug "%s(%s): after refresh, is '%s'" % # [self.class.name,@parent.name,@is] # If we still can't retrieve a checksum, it means that # the file still doesn't exist if @is == :absent # if they're copying, then we won't worry about the file # not existing yet unless @parent.state(:source) self.warning( "File %s does not exist -- cannot checksum" % @parent[:path] ) end return nil end end # If the sums are different, then return an event. if self.updatesum return :file_changed else return nil end end def insync? @should = [checktype] if cache(checktype()) return @is == currentsum() else # If there's no cached sum, then we don't want to generate # an event. return true end end # Even though they can specify multiple checksums, the insync? # mechanism can really only test against one, so we'll just retrieve # the first specified sum type. def retrieve(usecache = false) # When the 'source' is retrieving, it passes "true" here so # that we aren't reading the file twice in quick succession, yo. if usecache and @is return @is end stat = nil unless stat = @parent.stat self.is = :absent return end if stat.ftype == "link" and @parent[:links] != :follow self.debug "Not checksumming symlink" #@parent.delete(:checksum) self.is = self.currentsum return end # Just use the first allowed check type @is = getsum(checktype()) # If there is no sum defined, then store the current value # into the cache, so that we're not marked as being # out of sync. We don't want to generate an event the first # time we get a sum. unless cache(checktype()) # FIXME we should support an updatechecksums-like mechanism self.updatesum end #@parent.debug "checksum state is %s" % self.is end # Store the new sum to the state db. def updatesum result = false if @is.is_a?(Symbol) raise Puppet::Error, "%s has invalid checksum" % @parent.title end # if we're replacing, vs. updating if sum = cache(checktype()) - unless defined? @should - raise Puppet::Error.new( - ("@should is not initialized for %s, even though we " + - "found a checksum") % @parent[:path] - ) - end + # unless defined? @should + # raise Puppet::Error.new( + # ("@should is not initialized for %s, even though we " + + # "found a checksum") % @parent[:path] + # ) + # end if @is == sum info "Sums are already equal" return false end #if cache(self.should) == @is # raise Puppet::Error, "Got told to update same sum twice" #end self.debug "Replacing %s checksum %s with %s" % [@parent.title, sum, @is] #@parent.debug "@is: %s; @should: %s" % [@is,@should] result = true else @parent.debug "Creating checksum %s" % @is result = false end # Cache the sum so the log message can be right if possible. @cached = sum cache(checktype(), @is) return result end end end # $Id$ diff --git a/lib/puppet/type/pfile/ensure.rb b/lib/puppet/type/pfile/ensure.rb index c998e0f7f..6f7b15d49 100755 --- a/lib/puppet/type/pfile/ensure.rb +++ b/lib/puppet/type/pfile/ensure.rb @@ -1,181 +1,180 @@ module Puppet Puppet.type(:file).ensurable do require 'etc' desc "Whether to create files that don't currently exist. Possible values are *absent*, *present* (equivalent to ``exists`` in most file tests -- will match any form of file existence, and if the file is missing will create an empty file), *file*, and *directory*. Specifying ``absent`` will delete the file, although currently this will not recursively delete directories. Anything other than those values will be considered to be a symlink. For instance, the following text creates a link: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"/etc/inet/inetd.conf\" } You can make relative links: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"inet/inetd.conf\" } If you need to make a relative link to a file named the same as one of the valid values, you must prefix it with ``./`` or something similar. You can also make recursive symlinks, which will create a directory structure that maps to the target directory, with directories corresponding to each directory and links corresponding to each file." # Most 'ensure' states have a default, but with files we, um, don't. nodefault newvalue(:absent) do File.unlink(@parent[:path]) end aliasvalue(:false, :absent) newvalue(:file) do # Make sure we're not managing the content some other way if state = @parent.state(:content) or state = @parent.state(:source) state.sync else @parent.write(false) { |f| f.flush } mode = @parent.should(:mode) end return :file_created end #aliasvalue(:present, :file) newvalue(:present) do # Make a file if they want something, but this will match almost # anything. set_file end newvalue(:directory) do - p @is mode = @parent.should(:mode) parent = File.dirname(@parent[:path]) unless FileTest.exists? parent raise Puppet::Error, "Cannot create %s; parent directory %s does not exist" % [@parent[:path], parent] end Puppet::SUIDManager.asuser(@parent.asuser()) { if mode Puppet::Util.withumask(000) do Dir.mkdir(@parent[:path],mode) end else Dir.mkdir(@parent[:path]) end } @parent.setchecksum return :directory_created end newvalue(:link) do if state = @parent.state(:target) state.retrieve if state.linkmaker self.set_directory return :directory_created else return state.mklink end else self.fail "Cannot create a symlink without a target" end end # Symlinks. newvalue(/./) do # This code never gets executed. We need the regex to support # specifying it, but the work is done in the 'symlink' code block. end munge do |value| value = super(value) return value if value.is_a? Symbol @parent[:target] = value return :link end # Check that we can actually create anything def check basedir = File.dirname(@parent[:path]) if ! FileTest.exists?(basedir) raise Puppet::Error, "Can not create %s; parent directory does not exist" % @parent.title elsif ! FileTest.directory?(basedir) raise Puppet::Error, "Can not create %s; %s is not a directory" % [@parent.title, dirname] end end # We have to treat :present specially, because it works with any # type of file. def insync? if self.should == :present if @is.nil? or @is == :absent return false else return true end else return super end end def retrieve if stat = @parent.stat(false) @is = stat.ftype.intern else if self.should == :false @is = :false else @is = :absent end end end def sync unless self.should == :absent @parent.remove_existing(self.should) end event = super # There are some cases where all of the work does not get done on # file creation, so we have to do some extra checking. @parent.each do |thing| next unless thing.is_a? Puppet::State next if thing == self thing.retrieve unless thing.insync? thing.sync end end return event end end end # $Id$ diff --git a/lib/puppet/type/pfile/source.rb b/lib/puppet/type/pfile/source.rb index 8ac60422c..77f90bc9f 100755 --- a/lib/puppet/type/pfile/source.rb +++ b/lib/puppet/type/pfile/source.rb @@ -1,253 +1,261 @@ require 'puppet/server/fileserver' module Puppet # Copy files from a local or remote source. This state *only* does any work # when the remote file is an actual file; in that case, this state copies # the file down. If the remote file is a dir or a link or whatever, then # this state, during retrieval, modifies the appropriate other states # so that things get taken care of appropriately. Puppet.type(:file).newstate(:source) do PINPARAMS = Puppet::Server::FileServer::CHECKPARAMS attr_accessor :source, :local desc "Copy a file over the current file. Uses ``checksum`` to determine when a file should be copied. Valid values are either fully qualified paths to files, or URIs. Currently supported URI types are *puppet* and *file*. This is one of the primary mechanisms for getting content into applications that Puppet does not directly support and is very useful for those configuration files that don't change much across sytems. For instance: class sendmail { file { \"/etc/mail/sendmail.cf\": source => \"puppet://server/module/sendmail.cf\" } } See the [fileserver docs][] for information on how to configure and use file services within Puppet. If you specify multiple file sources for a file, then the first source that exists will be used. This allows you to specify what amount to search paths for files: file { \"/path/to/my/file\": source => [ \"/nfs/files/file.$host\", \"/nfs/files/file.$operatingsystem\", \"/nfs/files/file\" ] } This will use the first found file as the source. You cannot currently copy links using this mechanism; set ``links`` to ``follow`` if any remote sources are links. [fileserver docs]: ../installing/fsconfigref.html " uncheckable validate do |source| unless @parent.uri2obj(source) raise Puppet::Error, "Invalid source %s" % source end end munge do |source| # if source.is_a? Symbol # return source # end # Remove any trailing slashes source.sub(/\/$/, '') end + def change_to_s + "replacing from source %s" % @source + end + def checksum if defined?(@stats) @stats[:checksum] else nil end end # Ask the file server to describe our file. def describe(source) sourceobj, path = @parent.uri2obj(source) server = sourceobj.server begin desc = server.describe(path, @parent[:links]) rescue NetworkClientError => detail self.err "Could not describe %s: %s" % [path, detail] return nil end args = {} PINPARAMS.zip( desc.split("\t") ).each { |param, value| if value =~ /^[0-9]+$/ value = value.to_i end unless value.nil? args[param] = value end } # we can't manage ownership as root, so don't even try unless Puppet::SUIDManager.uid == 0 args.delete(:owner) end if args.empty? or (args[:type] == "link" and @parent[:links] == :ignore) return nil else return args end end # Have we successfully described the remote source? def described? ! @stats.nil? and ! @stats[:type].nil? and @is != :notdescribed end # Use the info we get from describe() to check if we're in sync. def insync? unless described? info "No specified sources exist" return true end if @is == :nocopy return true end # the only thing this actual state can do is copy files around. Therefore, # only pay attention if the remote is a file. unless @stats[:type] == "file" return true end + + if @parent.is(:ensure) != :absent and ! @parent.replace? + return true + end # Now, we just check to see if the checksums are the same return @parent.is(:checksum) == @stats[:checksum] end # This basically calls describe() on our file, and then sets all # of the local states appropriately. If the remote file is a normal # file then we set it to copy; if it's a directory, then we just mark # that the local directory should be created. def retrieve(remote = true) sum = nil @source = nil # This is set to false by the File#retrieve function on the second # retrieve, so that we do not do two describes. if remote # Find the first source that exists. @shouldorig contains # the sources as specified by the user. @should.each { |source| if @stats = self.describe(source) @source = source break end } end if @stats.nil? or @stats[:type].nil? @is = :notdescribed return nil end case @stats[:type] when "directory", "file": @parent[:ensure] = @stats[:type] else self.info @stats.inspect self.err "Cannot use files of type %s as sources" % @stats[:type] @is = :nocopy return end # Take each of the stats and set them as states on the local file # if a value has not already been provided. @stats.each { |stat, value| next if stat == :checksum next if stat == :type # was the stat already specified, or should the value # be inherited from the source? unless @parent.argument?(stat) @parent[stat] = value + @parent.state(stat).retrieve end } @is = @stats[:checksum] end def should @should end # Make sure we're also checking the checksum def should=(value) super + + checks = (PINPARAMS + [:ensure]) + checks.delete(:checksum) - # @parent[:check] = [:checksum, :ensure] + @parent[:check] = checks unless @parent.state(:checksum) @parent[:checksum] = :md5 end - - unless @parent.state(:ensure) - @parent[:check] = :ensure - end end def sync unless @stats[:type] == "file" #if @stats[:type] == "directory" #[@parent.name, @is.inspect, @should.inspect] #end raise Puppet::DevError, "Got told to copy non-file %s" % @parent[:path] end sourceobj, path = @parent.uri2obj(@source) begin contents = sourceobj.server.retrieve(path, @parent[:links]) rescue NetworkClientError => detail self.err "Could not retrieve %s: %s" % [path, detail] return nil end # FIXME It's stupid that this isn't taken care of in the # protocol. unless sourceobj.server.local contents = CGI.unescape(contents) end if contents == "" self.notice "Could not retrieve contents for %s" % @source end exists = File.exists?(@parent[:path]) @parent.write { |f| f.print contents } if exists return :file_changed else return :file_created end end end end # $Id$ diff --git a/lib/puppet/type/tidy.rb b/lib/puppet/type/tidy.rb index 7e0a04353..cebbde74a 100755 --- a/lib/puppet/type/tidy.rb +++ b/lib/puppet/type/tidy.rb @@ -1,258 +1,266 @@ require 'etc' require 'puppet/type/state' require 'puppet/type/pfile' module Puppet newtype(:tidy, Puppet.type(:file)) do @doc = "Remove unwanted files based on specific criteria. Multiple criteria or OR'd together, so a file that is too large but is not old enough will still get tidied." newparam(:path) do desc "The path to the file or directory to manage. Must be fully qualified." isnamevar end copyparam(Puppet.type(:file), :backup) newparam(:age) do desc "Tidy files whose age is equal to or greater than the specified number of days. You can choose seconds, minutes, hours, days, or weeks by specifying the first letter of any of those words (e.g., '1w')." @@ageconvertors = { :s => 1, :m => 60 } @@ageconvertors[:h] = @@ageconvertors[:m] * 60 @@ageconvertors[:d] = @@ageconvertors[:h] * 24 @@ageconvertors[:w] = @@ageconvertors[:d] * 7 def convert(unit, multi) if num = @@ageconvertors[unit] return num * multi else self.fail "Invalid age unit '%s'" % unit end 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 %s" % 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*, and *m* can be appended to specify *bytes*, *kilobytes*, and *megabytes*, respectively. Only the first character is significant, so the full word can also be used." @@sizeconvertors = { :b => 0, :k => 1, :m => 2, :g => 3 } def convert(unit, multi) if num = @@sizeconvertors[unit] result = multi num.times do result *= 1024 end return result else self.fail "Invalid size unit '%s'" % unit end 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 %s" % 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(:recurse) do desc "If target is a directory, recursively descend into the directory looking for files to tidy." end newparam(:rmdirs) 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." end newstate(:tidyup) do require 'etc' @nodoc = true @name = :tidyup def age(stat) type = nil if stat.ftype == "directory" type = :mtime else type = @parent[:type] || :atime end #return Integer(Time.now - stat.send(type)) return stat.send(type).to_i end def change_to_s start = "Tidying" unless insync_age? start += ", older than %s seconds" % @parent[:age] end unless insync_size? start += ", larger than %s bytes" % @parent[:size] end start end def insync_age? if num = @parent[:age] and @is[0] if (Time.now.to_i - @is[0]) > num return false end end true end def insync_size? if num = @parent[:size] and @is[1] if @is[1] > num return false end end true end def insync? - insync_age? and insync_size? + if @is.is_a?(Symbol) + if @is == :absent + return true + else + return false + end + else + insync_age? and insync_size? + end end def retrieve stat = nil unless stat = @parent.stat @is = :unknown return end @is = [:age, :size].collect { |param| if @parent[param] self.send(param, stat) end }.reject { |p| p == false or p.nil? } end def size(stat) return stat.size end def sync file = @parent[:path] case File.lstat(file).ftype when "directory": if @parent[:rmdirs] subs = Dir.entries(@parent[:path]).reject { |d| d == "." or d == ".." }.length if subs > 0 self.info "%s has %s children; not tidying" % [@parent[:path], subs] self.info Dir.entries(@parent[:path]).inspect else Dir.rmdir(@parent[:path]) end else self.debug "Not tidying directories" return nil end when "file": @parent.handlebackup(file) File.unlink(file) when "link": File.unlink(file) else self.fail "Cannot tidy files of type %s" % File.lstat(file).ftype end return :file_tidied end end # Erase PFile's validate method validate do end def self.list self.collect { |t| t } end @depthfirst = true def initialize(hash) super #self.setdefaults unless @parameters.include?(:age) or @parameters.include?(:size) unless FileTest.directory?(self[:path]) # don't do size comparisons for directories self.fail "Tidy must specify size, age, or both" end end # only allow backing up into filebuckets unless self[:backup].is_a? Puppet::Client::Dipper self[:backup] = false end self[:tidyup] = [:age, :size].collect { |param| self[param] }.reject { |p| p == false } end end end # $Id$ diff --git a/lib/puppet/util/posix.rb b/lib/puppet/util/posix.rb index 75726b3da..01c3e25aa 100755 --- a/lib/puppet/util/posix.rb +++ b/lib/puppet/util/posix.rb @@ -1,78 +1,78 @@ # Utility methods for interacting with POSIX objects; mostly user and group module Puppet::Util::POSIX # Retrieve a field from a POSIX Etc object. The id can be either an integer # or a name. This only works for users and groups. def get_posix_field(space, field, id) if id =~ /^\d+$/ id = Integer(id) end prefix = "get" + space.to_s if id.is_a?(Integer) method = (prefix + idfield(space).to_s).intern else method = (prefix + "nam").intern end - + begin return Etc.send(method, id).send(field) rescue ArgumentError => detail # ignore it; we couldn't find the object return nil end end # Look in memory for an already-managed type and use its info if available. def get_provider_value(type, field, id) unless typeklass = Puppet::Type.type(type) raise ArgumentError, "Invalid type %s" % type end id = id.to_s chkfield = idfield(type) obj = typeklass.find { |obj| if id =~ /^\d+$/ obj.should(chkfield).to_s == id || obj.is(chkfield).to_s == id else obj[:name] == id end } return nil unless obj if obj.provider begin return obj.provider.send(field) rescue => detail if Puppet[:trace] puts detail.backtrace Puppet.err detail return nil end end end end # Determine what the field name is for users and groups. def idfield(space) case Puppet::Util.symbolize(space) when :gr, :group: return :gid when :pw, :user: return :uid else raise ArgumentError.new("Can only handle users and groups") end end # Get the GID of a given group, provided either a GID or a name def gid(group) get_provider_value(:group, :gid, group) or get_posix_field(:gr, :gid, group) end # Get the UID of a given user, whether a UID or name is provided def uid(user) get_provider_value(:user, :uid, user) or get_posix_field(:pw, :uid, user) end end # $Id$ \ No newline at end of file diff --git a/test/other/relationships.rb b/test/other/relationships.rb index e9c1d1c9c..164d52d2a 100755 --- a/test/other/relationships.rb +++ b/test/other/relationships.rb @@ -1,242 +1,266 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' class TestRelationships < Test::Unit::TestCase include PuppetTest def newfile assert_nothing_raised() { return Puppet.type(:file).create( :path => tempfile, :check => [:mode, :owner, :group] ) } end def check_relationship(sources, targets, out, refresher) if out deps = sources.builddepends sources = [sources] else deps = targets.builddepends targets = [targets] end assert_instance_of(Array, deps) assert(! deps.empty?, "Did not receive any relationships") deps.each do |edge| assert_instance_of(Puppet::Relationship, edge) end sources.each do |source| targets.each do |target| edge = deps.find { |e| e.source == source and e.target == target } assert(edge, "Could not find edge for %s => %s" % [source.ref, target.ref]) if refresher assert_equal(:ALL_EVENTS, edge.event) assert_equal(:refresh, edge.callback) else assert_equal(:NONE, edge.event) assert_nil(edge.callback, "Got a callback with no events") end end end end # Make sure our various metaparams work correctly. We're just checking # here whether they correctly set up the callbacks and the direction of # the relationship. def test_relationship_metaparams out = {:require => false, :subscribe => false, :notify => true, :before => true} refreshers = [:subscribe, :notify] [:require, :subscribe, :notify, :before].each do |param| # Create three files to generate our events and three # execs to receive them files = [] execs = [] 3.times do |i| files << Puppet::Type.newfile( :title => "file#{i}", :path => tempfile(), :ensure => :file ) path = tempfile() execs << Puppet::Type.newexec( :title => "notifytest#{i}", :path => "/usr/bin:/bin", :command => "touch #{path}", :refreshonly => true ) end # Add our first relationship if out[param] files[0][param] = execs[0] sources = files[0] targets = [execs[0]] else execs[0][param] = files[0] sources = [files[0]] targets = execs[0] end check_relationship(sources, targets, out[param], refreshers.include?(param)) # Now add another relationship if out[param] files[0][param] = execs[1] targets << execs[1] assert_equal(targets.collect { |t| [t.class.name, t.title]}, files[0][param], "Incorrect target list") else execs[0][param] = files[1] sources << files[1] assert_equal(sources.collect { |t| [t.class.name, t.title]}, execs[0][param], "Incorrect source list") end check_relationship(sources, targets, out[param], refreshers.include?(param)) Puppet::Type.allclear end end def test_store_relationship file = Puppet::Type.newfile :path => tempfile(), :mode => 0755 execs = [] 3.times do |i| execs << Puppet::Type.newexec(:title => "yay#{i}", :command => "/bin/echo yay") end # First try it with one object, specified as a reference and an array result = nil [execs[0], [:exec, "yay0"], ["exec", "yay0"]].each do |target| assert_nothing_raised do result = file.send(:store_relationship, :require, target) end assert_equal([[:exec, "yay0"]], result) end # Now try it with multiple objects symbols = execs.collect { |e| [e.class.name, e.title] } strings = execs.collect { |e| [e.class.name.to_s, e.title] } [execs, symbols, strings].each do |target| assert_nothing_raised do result = file.send(:store_relationship, :require, target) end assert_equal(symbols, result) end # Make sure we can mix it up, even though this shouldn't happen assert_nothing_raised do result = file.send(:store_relationship, :require, [execs[0], [execs[1].class.name, execs[1].title]]) end assert_equal([[:exec, "yay0"], [:exec, "yay1"]], result) # Finally, make sure that new results get added to old. The only way # to get rid of relationships is to delete the parameter. file[:require] = execs[0] assert_nothing_raised do result = file.send(:store_relationship, :require, [execs[1], execs[2]]) end assert_equal(symbols, result) end def test_newsub file1 = newfile() file2 = newfile() sub = nil assert_nothing_raised("Could not create subscription") { sub = Puppet::Event::Subscription.new( :source => file1, :target => file2, :event => :ALL_EVENTS, :callback => :refresh ) } subs = nil assert_nothing_raised { subs = Puppet::Event::Subscription.subscribers(file1) } assert_equal(1, subs.length, "Got incorrect number of subs") assert_equal(sub.target, subs[0], "Got incorrect sub") deps = nil assert_nothing_raised { deps = Puppet::Event::Subscription.dependencies(file2) } assert_equal(1, deps.length, "Got incorrect number of deps") assert_equal(sub, deps[0], "Got incorrect dep") end def test_eventmatch file1 = newfile() file2 = newfile() sub = nil assert_nothing_raised("Could not create subscription") { sub = Puppet::Event::Subscription.new( :source => file1, :target => file2, :event => :ALL_EVENTS, :callback => :refresh ) } assert(sub.match?(:anything), "ALL_EVENTS did not match") assert(! sub.match?(:NONE), "ALL_EVENTS matched :NONE") sub.event = :file_created assert(sub.match?(:file_created), "event did not match") assert(sub.match?(:ALL_EVENTS), "ALL_EVENTS did not match") assert(! sub.match?(:NONE), "ALL_EVENTS matched :NONE") sub.event = :NONE assert(! sub.match?(:file_created), "Invalid match") assert(! sub.match?(:ALL_EVENTS), "ALL_EVENTS matched") assert(! sub.match?(:NONE), "matched :NONE") end def test_autorequire # We know that execs autorequire their cwd, so we'll use that path = tempfile() file = Puppet::Type.newfile(:title => "myfile", :path => path, :ensure => :directory) exec = Puppet::Type.newexec(:title => "myexec", :cwd => path, :command => "/bin/echo") reqs = nil assert_nothing_raised do reqs = exec.autorequire end assert_equal([Puppet::Relationship[file, exec]], reqs) # Now make sure that these relationships are added to the transaction's # relgraph trans = Puppet::Transaction.new(newcomp(file, exec)) assert_nothing_raised do trans.evaluate end graph = trans.relgraph assert(graph.edge?(file, exec), "autorequire edge was not created") end + + def test_requires? + # Test the first direction + file1 = Puppet::Type.newfile(:title => "one", :path => tempfile, + :ensure => :directory) + file2 = Puppet::Type.newfile(:title => "two", :path => tempfile, + :ensure => :directory) + + file1[:require] = file2 + assert(file1.requires?(file2), "requires? failed to catch :require relationship") + file1.delete(:require) + assert(! file1.requires?(file2), "did not delete relationship") + file1[:subscribe] = file2 + assert(file1.requires?(file2), "requires? failed to catch :subscribe relationship") + file1.delete(:subscribe) + assert(! file1.requires?(file2), "did not delete relationship") + file2[:before] = file1 + assert(file1.requires?(file2), "requires? failed to catch :before relationship") + file2.delete(:before) + assert(! file1.requires?(file2), "did not delete relationship") + file2[:notify] = file1 + assert(file1.requires?(file2), "requires? failed to catch :notify relationship") + end + end # $Id$ diff --git a/test/other/transactions.rb b/test/other/transactions.rb index 7342b57ec..9fc58526a 100755 --- a/test/other/transactions.rb +++ b/test/other/transactions.rb @@ -1,767 +1,766 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' require 'puppettest/support/resources' # $Id$ class TestTransactions < Test::Unit::TestCase include PuppetTest::FileTesting include PuppetTest::Support::Resources def mkgenerator(&block) # Create a bogus type that generates new instances with shorter type = Puppet::Type.newtype(:generator) do newparam(:name, :namevar => true) end if block type.class_eval(&block) end cleanup do Puppet::Type.rmtype(:generator) end return type end def test_reports path1 = tempfile() path2 = tempfile() objects = [] objects << Puppet::Type.newfile( :path => path1, :content => "yayness" ) objects << Puppet::Type.newfile( :path => path2, :content => "booness" ) trans = assert_events([:file_created, :file_created], *objects) report = nil assert_nothing_raised { report = trans.report } # First test the report logs assert(report.logs.length > 0, "Did not get any report logs") report.logs.each do |obj| assert_instance_of(Puppet::Log, obj) end # Then test the metrics metrics = report.metrics assert(metrics, "Did not get any metrics") assert(metrics.length > 0, "Did not get any metrics") assert(metrics.has_key?("resources"), "Did not get object metrics") assert(metrics.has_key?("changes"), "Did not get change metrics") metrics.each do |name, metric| assert_instance_of(Puppet::Metric, metric) end end def test_prefetch # Create a type just for testing prefetch name = :prefetchtesting $prefetched = false type = Puppet::Type.newtype(name) do newparam(:name) {} end cleanup do Puppet::Type.rmtype(name) end # Now create a provider type.provide(:prefetch) do def self.prefetch $prefetched = true end end # Now create an instance inst = type.create :name => "yay" # Create a transaction trans = Puppet::Transaction.new(newcomp(inst)) # Make sure prefetch works assert_nothing_raised do trans.prefetch end assert_equal(true, $prefetched, "type prefetch was not called") # Now make sure it gets called from within evaluate() $prefetched = false assert_nothing_raised do trans.evaluate end assert_equal(true, $prefetched, "evaluate did not call prefetch") end def test_refreshes_generate_events path = tempfile() firstpath = tempfile() secondpath = tempfile() file = Puppet::Type.newfile(:title => "file", :path => path, :content => "yayness") first = Puppet::Type.newexec(:title => "first", :command => "/bin/echo first > #{firstpath}", :subscribe => [:file, path], :refreshonly => true ) second = Puppet::Type.newexec(:title => "second", :command => "/bin/echo second > #{secondpath}", :subscribe => [:exec, "first"], :refreshonly => true ) assert_apply(file, first, second) assert(FileTest.exists?(secondpath), "Refresh did not generate an event") end unless %x{groups}.chomp.split(/ /).length > 1 $stderr.puts "You must be a member of more than one group to test transactions" else def ingroup(gid) require 'etc' begin group = Etc.getgrgid(gid) rescue => detail puts "Could not retrieve info for group %s: %s" % [gid, detail] return nil end return @groups.include?(group.name) end def setup super @groups = %x{groups}.chomp.split(/ /) unless @groups.length > 1 p @groups raise "You must be a member of more than one group to test this" end end def newfile(hash = {}) tmpfile = tempfile() File.open(tmpfile, "w") { |f| f.puts rand(100) } # XXX now, because os x apparently somehow allows me to make a file # owned by a group i'm not a member of, i have to verify that # the file i just created is owned by one of my groups # grrr unless ingroup(File.stat(tmpfile).gid) Puppet.info "Somehow created file in non-member group %s; fixing" % File.stat(tmpfile).gid require 'etc' firstgr = @groups[0] unless firstgr.is_a?(Integer) str = Etc.getgrnam(firstgr) firstgr = str.gid end File.chown(nil, firstgr, tmpfile) end hash[:name] = tmpfile assert_nothing_raised() { return Puppet.type(:file).create(hash) } end def newservice assert_nothing_raised() { return Puppet.type(:service).create( :name => "sleeper", :type => "init", :path => exampledir("root/etc/init.d"), :hasstatus => true, :check => [:ensure] ) } end def newexec(file) assert_nothing_raised() { return Puppet.type(:exec).create( :name => "touch %s" % file, :path => "/bin:/usr/bin:/sbin:/usr/sbin", :returns => 0 ) } end # modify a file and then roll the modifications back def test_filerollback transaction = nil file = newfile() states = {} check = [:group,:mode] file[:check] = check assert_nothing_raised() { file.retrieve } assert_nothing_raised() { check.each { |state| assert(file[state]) states[state] = file[state] } } component = newcomp("file",file) require 'etc' groupname = Etc.getgrgid(File.stat(file.name).gid).name assert_nothing_raised() { # Find a group that it's not set to group = @groups.find { |group| group != groupname } unless group raise "Could not find suitable group" end file[:group] = group file[:mode] = "755" } trans = assert_events([:file_changed, :file_changed], component) file.retrieve assert_rollback_events(trans, [:file_changed, :file_changed], "file") assert_nothing_raised() { file.retrieve } states.each { |state,value| assert_equal( value,file.is(state), "File %s remained %s" % [state, file.is(state)] ) } end # start a service, and then roll the modification back # Disabled, because it wasn't really worth the effort. def disabled_test_servicetrans transaction = nil service = newservice() component = newcomp("service",service) assert_nothing_raised() { service[:ensure] = 1 } service.retrieve assert(service.insync?, "Service did not start") system("ps -ef | grep ruby") trans = assert_events([:service_started], component) service.retrieve assert_rollback_events(trans, [:service_stopped], "service") end # test that services are correctly restarted and that work is done # in the right order def test_refreshing transaction = nil file = newfile() execfile = File.join(tmpdir(), "exectestingness") exec = newexec(execfile) states = {} check = [:group,:mode] file[:check] = check file[:group] = @groups[0] assert_apply(file) @@tmpfiles << execfile component = newcomp("both",file,exec) # 'subscribe' expects an array of arrays exec[:subscribe] = [[file.class.name,file.name]] exec[:refreshonly] = true assert_nothing_raised() { file.retrieve exec.retrieve } check.each { |state| states[state] = file[state] } assert_nothing_raised() { file[:mode] = "755" } trans = assert_events([:file_changed, :triggered], component) assert(FileTest.exists?(execfile), "Execfile does not exist") File.unlink(execfile) assert_nothing_raised() { file[:group] = @groups[1] } trans = assert_events([:file_changed, :triggered], component) assert(FileTest.exists?(execfile), "Execfile does not exist") end # Verify that one component requiring another causes the contained # resources in the requiring component to get refreshed. def test_refresh_across_two_components transaction = nil file = newfile() execfile = File.join(tmpdir(), "exectestingness2") @@tmpfiles << execfile exec = newexec(execfile) states = {} check = [:group,:mode] file[:check] = check file[:group] = @groups[0] assert_apply(file) fcomp = newcomp("file",file) ecomp = newcomp("exec",exec) component = newcomp("both",fcomp,ecomp) # 'subscribe' expects an array of arrays #component[:require] = [[file.class.name,file.name]] ecomp[:subscribe] = fcomp exec[:refreshonly] = true trans = assert_events([], component) assert_nothing_raised() { file[:group] = @groups[1] file[:mode] = "755" } trans = assert_events([:file_changed, :file_changed, :triggered], component) end # Make sure that multiple subscriptions get triggered. def test_multisubs path = tempfile() file1 = tempfile() file2 = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file" ) exec1 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % file1, :refreshonly => true, :subscribe => [:file, path] ) exec2 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % file2, :refreshonly => true, :subscribe => [:file, path] ) assert_apply(file, exec1, exec2) assert(FileTest.exists?(file1), "File 1 did not get created") assert(FileTest.exists?(file2), "File 2 did not get created") end # Make sure that a failed trigger doesn't result in other events not # getting triggered. def test_failedrefreshes path = tempfile() newfile = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file" ) svc = Puppet.type(:service).create( :name => "thisservicedoesnotexist", :subscribe => [:file, path] ) exec = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch %s" % newfile, :logoutput => true, :refreshonly => true, :subscribe => [:file, path] ) assert_apply(file, svc, exec) assert(FileTest.exists?(path), "File did not get created") assert(FileTest.exists?(newfile), "Refresh file did not get created") end # Make sure that unscheduled and untagged objects still respond to events def test_unscheduled_and_untagged_response Puppet::Type.type(:schedule).mkdefaultschedules Puppet[:ignoreschedules] = false file = Puppet.type(:file).create( :name => tempfile(), :ensure => "file" ) fname = tempfile() exec = Puppet.type(:exec).create( :name => "touch %s" % fname, :path => "/usr/bin:/bin", :schedule => "monthly", :subscribe => ["file", file.name] ) comp = newcomp(file,exec) comp.finalize # Run it once assert_apply(comp) assert(FileTest.exists?(fname), "File did not get created") assert(!exec.scheduled?, "Exec is somehow scheduled") # Now remove it, so it can get created again File.unlink(fname) file[:content] = "some content" assert_events([:file_changed, :triggered], comp) assert(FileTest.exists?(fname), "File did not get recreated") # Now remove it, so it can get created again File.unlink(fname) # And tag our exec exec.tag("testrun") # And our file, so it runs file.tag("norun") Puppet[:tags] = "norun" file[:content] = "totally different content" assert(! file.insync?, "Uh, file is in sync?") assert_events([:file_changed, :triggered], comp) assert(FileTest.exists?(fname), "File did not get recreated") end def test_failed_reqs_mean_no_run exec = Puppet::Type.type(:exec).create( :command => "/bin/mkdir /this/path/cannot/possibly/exit", :title => "mkdir" ) file1 = Puppet::Type.type(:file).create( :title => "file1", :path => tempfile(), :require => exec, :ensure => :file ) file2 = Puppet::Type.type(:file).create( :title => "file2", :path => tempfile(), :require => file1, :ensure => :file ) comp = newcomp(exec, file1, file2) comp.finalize assert_apply(comp) assert(! FileTest.exists?(file1[:path]), "File got created even tho its dependency failed") assert(! FileTest.exists?(file2[:path]), "File got created even tho its deep dependency failed") end end def f(n) Puppet::Type.type(:file)["/tmp/#{n.to_s}"] end def test_relationship_graph one, two, middle, top = mktree {one => two, "f" => "c", "h" => middle}.each do |source, target| if source.is_a?(String) source = f(source) end if target.is_a?(String) target = f(target) end target[:require] = source end trans = Puppet::Transaction.new(top) graph = nil assert_nothing_raised do graph = trans.relationship_graph end assert_instance_of(Puppet::PGraph, graph, "Did not get relationship graph") # Make sure all of the components are gone comps = graph.vertices.find_all { |v| v.is_a?(Puppet::Type::Component)} assert(comps.empty?, "Deps graph still contains components") # It must be reversed because of how topsort works sorted = graph.topsort.reverse # Now make sure the appropriate edges are there and are in the right order assert(graph.dependencies(f(:f)).include?(f(:c)), "c not marked a dep of f") assert(sorted.index(f(:c)) < sorted.index(f(:f)), "c is not before f") one.each do |o| two.each do |t| assert(graph.dependencies(o).include?(t), "%s not marked a dep of %s" % [t.ref, o.ref]) assert(sorted.index(t) < sorted.index(o), "%s is not before %s" % [t.ref, o.ref]) end end trans.resources.leaves(middle).each do |child| assert(graph.dependencies(f(:h)).include?(child), "%s not marked a dep of h" % [child.ref]) assert(sorted.index(child) < sorted.index(f(:h)), "%s is not before h" % child.ref) end # Lastly, make sure our 'g' vertex made it into the relationship # graph, since it's not involved in any relationships. assert(graph.vertex?(f(:g)), "Lost vertexes with no relations") graph.to_jpg("normal_relations") end # Test pre-evaluation generation def test_generate mkgenerator() do def generate ret = [] if title.length > 1 ret << self.class.create(:title => title[0..-2]) else return nil end ret end end yay = Puppet::Type.newgenerator :title => "yay" rah = Puppet::Type.newgenerator :title => "rah" comp = newcomp(yay, rah) trans = comp.evaluate assert_nothing_raised do trans.generate end %w{ya ra y r}.each do |name| assert(trans.resources.vertex?(Puppet::Type.type(:generator)[name]), "Generated %s was not a vertex" % name) end # Now make sure that cleanup gets rid of those generated types. assert_nothing_raised do trans.cleanup end %w{ya ra y r}.each do |name| assert(!trans.resources.vertex?(Puppet::Type.type(:generator)[name]), "Generated vertex %s was not removed from graph" % name) assert_nil(Puppet::Type.type(:generator)[name], "Generated vertex %s was not removed from class" % name) end end # Test mid-evaluation generation. def test_eval_generate - $evaluated = {} + $evaluated = [] type = mkgenerator() do def eval_generate ret = [] if title.length > 1 ret << self.class.create(:title => title[0..-2]) else return nil end ret end def evaluate - $evaluated[self.title] = true + $evaluated << self.title return [] end end yay = Puppet::Type.newgenerator :title => "yay" rah = Puppet::Type.newgenerator :title => "rah", :subscribe => yay comp = newcomp(yay, rah) trans = comp.evaluate trans.prepare # Now apply the resources, and make sure they appropriately generate # things. assert_nothing_raised("failed to apply yay") do - trans.apply(yay) + trans.eval_resource(yay) end ya = type["ya"] assert(ya, "Did not generate ya") assert(trans.relgraph.vertex?(ya), "Did not add ya to rel_graph") # Now make sure the appropriate relationships were added assert(trans.relgraph.edge?(yay, ya), "parent was not required by child") assert(trans.relgraph.edge?(ya, rah), "rah was not subscribed to ya") # And make sure the relationship is a subscription with a callback, # not just a require. assert_equal({:callback => :refresh, :event => :ALL_EVENTS}, trans.relgraph[Puppet::Relationship.new(ya, rah)], "The label was not retained") # Now make sure it in turn eval_generates appropriately assert_nothing_raised("failed to apply yay") do - trans.apply(type["ya"]) + trans.eval_resource(type["ya"]) end %w{y}.each do |name| res = type[name] assert(res, "Did not generate %s" % name) assert(trans.relgraph.vertex?(res), "Did not add %s to rel_graph" % name) end assert_nothing_raised("failed to eval_generate with nil response") do - trans.apply(type["y"]) + trans.eval_resource(type["y"]) end + assert(trans.relgraph.edge?(yay, ya), "no edge was created for ya => yay") - assert_equal(%w{yay ya y rah}, trans.sorted_resources.collect { |r| r.title }, - "Did not eval_generate correctly") - assert_nothing_raised("failed to apply rah") do - trans.apply(rah) + trans.eval_resource(rah) end ra = type["ra"] assert(ra, "Did not generate ra") assert(trans.relgraph.vertex?(ra), "Did not add ra to rel_graph" % name) # Now make sure this generated resource has the same relationships as the generating # resource assert(trans.relgraph.edge?(yay, ra), "yay is not required by ra") assert(trans.relgraph.edge?(ya, ra), "ra is not subscribed to ya") # And make sure the relationship is a subscription with a callback, # not just a require. assert_equal({:callback => :refresh, :event => :ALL_EVENTS}, trans.relgraph[Puppet::Relationship.new(ya, ra)], "The label was not retained") # Now make sure that cleanup gets rid of those generated types. assert_nothing_raised do trans.cleanup end %w{ya ra y r}.each do |name| assert(!trans.relgraph.vertex?(type[name]), "Generated vertex %s was not removed from graph" % name) assert_nil(type[name], "Generated vertex %s was not removed from class" % name) end # Now, start over and make sure that everything gets evaluated. trans = comp.evaluate + $evaluated.clear assert_nothing_raised do trans.evaluate end - assert_equal(%w{yay ya y rah ra r}.sort, $evaluated.keys.sort, - "Not all resources were evaluated") + assert_equal(%w{yay ya y rah ra r}, $evaluated, + "Not all resources were evaluated or not in the right order") end def test_tags res = Puppet::Type.newfile :path => tempfile() comp = newcomp(res) # Make sure they default to none assert_equal([], comp.evaluate.tags) # Make sure we get the main tags Puppet[:tags] = %w{this is some tags} assert_equal(%w{this is some tags}, comp.evaluate.tags) # And make sure they get processed correctly Puppet[:tags] = ["one", "two,three", "four"] assert_equal(%w{one two three four}, comp.evaluate.tags) # lastly, make sure we can override them trans = comp.evaluate trans.tags = ["one", "two,three", "four"] assert_equal(%w{one two three four}, comp.evaluate.tags) end def test_tagged? res = Puppet::Type.newfile :path => tempfile() comp = newcomp(res) trans = comp.evaluate assert(trans.tagged?(res), "tagged? defaulted to false") # Now set some tags trans.tags = %w{some tags} # And make sure it's false assert(! trans.tagged?(res), "matched invalid tags") # Set ignoretags and make sure it sticks trans.ignoretags = true assert(trans.tagged?(res), "tags were not ignored") # Now make sure we actually correctly match tags res[:tag] = "mytag" trans.ignoretags = false trans.tags = %w{notag} assert(! trans.tagged?(res), "tags incorrectly matched") trans.tags = %w{mytag yaytag} assert(trans.tagged?(res), "tags should have matched") end # Make sure events propagate down the relationship graph appropriately. def test_trigger end end # $Id$ \ No newline at end of file diff --git a/test/types/component.rb b/test/types/component.rb index 3fdc7bfe2..3b55bab0f 100755 --- a/test/types/component.rb +++ b/test/types/component.rb @@ -1,316 +1,114 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' require 'puppettest/support/resources' # $Id$ class TestComponent < Test::Unit::TestCase include PuppetTest + include PuppetTest::Support::Resources def setup super @@used = {} @type = Puppet::Type::Component @file = Puppet::Type.type(:file) end def randnum(limit) num = nil looped = 0 loop do looped += 1 if looped > 2000 raise "Reached limit of looping" break end num = rand(limit) unless @@used.include?(num) @@used[num] = true break end end num end def mkfile(num = nil) unless num num = randnum(1000) end name = tempfile() + num.to_s file = Puppet.type(:file).create( :path => name, :checksum => "md5" ) @@tmpfiles << name file end def mkcomp Puppet.type(:component).create(:name => "component_" + randnum(1000).to_s) end def mkrandcomp(numfiles, numdivs) comp = mkcomp hash = {} found = 0 divs = {} numdivs.times { |i| num = i + 2 divs[num] = nil } while found < numfiles num = randnum(numfiles) found += 1 f = mkfile(num) hash[f.name] = f reqd = [] divs.each { |n,obj| if rand(50) % n == 0 if obj unless reqd.include?(obj.object_id) f[:require] = [[obj.class.name, obj.name]] reqd << obj.object_id end end end divs[n] = f } end hash.each { |name, obj| comp.push obj } comp.finalize comp end - - def test_ordering - list = nil - comp = mkrandcomp(30,5) - assert_nothing_raised { - list = comp.flatten - } - - list.each_with_index { |obj, index| - obj.eachdependency { |dep| - assert(list.index(dep) < index) - } - } - end def test_to_graph one, two, middle, top = mktree graph = nil assert_nothing_raised do graph = top.to_graph end assert(graph.is_a?(Puppet::PGraph), "result is not a pgraph") [one, two, middle, top].each do |comp| comp.each do |child| assert(graph.edge?(comp, child), "Did not create edge from %s => %s" % [comp.name, child.name]) end end end - - def test_correctsorting - tmpfile = tempfile() - @@tmpfiles.push tmpfile - trans = nil - cmd = nil - File.open(tmpfile, File::WRONLY|File::CREAT|File::TRUNC) { |of| - of.puts rand(100) - } - file = Puppet.type(:file).create( - :path => tmpfile, - :checksum => "md5" - ) - assert_nothing_raised { - cmd = Puppet.type(:exec).create( - :command => "pwd", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - :subscribe => [[file.class.name,file.name]], - :refreshonly => true - ) - } - - order = nil - assert_nothing_raised { - order = Puppet.type(:component).sort([file, cmd]) - } - - [cmd, file].each { |obj| - assert_equal(1, order.find_all { |o| o.name == obj.name }.length) - } - end - - def test_correctflattening - tmpfile = tempfile() - @@tmpfiles.push tmpfile - trans = nil - cmd = nil - File.open(tmpfile, File::WRONLY|File::CREAT|File::TRUNC) { |of| - of.puts rand(100) - } - file = Puppet.type(:file).create( - :path => tmpfile, - :checksum => "md5" - ) - assert_nothing_raised { - cmd = Puppet.type(:exec).create( - :command => "pwd", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - :subscribe => [[file.class.name,file.name]], - :refreshonly => true - ) - } - - comp = newcomp(cmd, file) - comp.finalize - objects = nil - assert_nothing_raised { - objects = comp.flatten - } - - [cmd, file].each { |obj| - assert_equal(1, objects.find_all { |o| o.name == obj.name }.length) - } - - assert(objects[0] == file, "File was not first object") - assert(objects[1] == cmd, "Exec was not second object") - end - - def test_deepflatten - tmpfile = tempfile() - @@tmpfiles.push tmpfile - trans = nil - cmd = nil - File.open(tmpfile, File::WRONLY|File::CREAT|File::TRUNC) { |of| - of.puts rand(100) - } - file = Puppet.type(:file).create( - :path => tmpfile, - :checksum => "md5" - ) - assert_nothing_raised { - cmd = Puppet.type(:exec).create( - :command => "pwd", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - :refreshonly => true - ) - } - - fcomp = newcomp("fflatten", file) - ecomp = newcomp("eflatten", cmd) - - # this subscription can screw up the sorting - ecomp[:subscribe] = [[fcomp.class.name,fcomp.name]] - - comp = newcomp("bflatten", ecomp, fcomp) - comp.finalize - objects = nil - assert_nothing_raised { - objects = comp.flatten - } - - assert_equal(objects.length, 2, "Did not get two sorted objects") - objects.each { |o| - assert(o.is_a?(Puppet::Type), "Object %s is not a Type" % o.class) - } - - assert(objects[0] == file, "File was not first object") - assert(objects[1] == cmd, "Exec was not second object") - end - - def test_deepflatten2 - tmpfile = tempfile() - @@tmpfiles.push tmpfile - trans = nil - cmd = nil - File.open(tmpfile, File::WRONLY|File::CREAT|File::TRUNC) { |of| - of.puts rand(100) - } - file = Puppet.type(:file).create( - :path => tmpfile, - :checksum => "md5" - ) - assert_nothing_raised { - cmd = Puppet.type(:exec).create( - :command => "pwd", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - :refreshonly => true - ) - } - - ocmd = nil - assert_nothing_raised { - ocmd = Puppet.type(:exec).create( - :command => "echo true", - :path => "/usr/bin:/bin:/usr/sbin:/sbin", - :refreshonly => true - ) - } - - fcomp = newcomp("fflatten", file) - ecomp = newcomp("eflatten", cmd) - ocomp = newcomp("oflatten", ocmd) - - # this subscription can screw up the sorting - cmd[:subscribe] = [[fcomp.class.name,fcomp.name]] - ocmd[:subscribe] = [[cmd.class.name,cmd.name]] - - comp = newcomp("bflatten", ocomp, ecomp, fcomp) - comp.finalize - objects = nil - assert_nothing_raised { - objects = comp.flatten - } - - assert_equal(objects.length, 3, "Did not get three sorted objects") - - objects.each { |o| - assert(o.is_a?(Puppet::Type), "Object %s is not a Type" % o.class) - } - - assert(objects[0] == file, "File was not first object") - assert(objects[1] == cmd, "Exec was not second object") - assert(objects[2] == ocmd, "Other exec was not second object") - end - - def test_moreordering - dir = tempfile() - - comp = Puppet.type(:component).create( - :name => "ordertesting" - ) - - 10.times { |i| - fileobj = Puppet.type(:file).create( - :path => File.join(dir, "file%s" % i), - :ensure => "file" - ) - comp.push(fileobj) - } - - dirobj = Puppet.type(:file).create( - :path => dir, - :ensure => "directory" - ) - - comp.push(dirobj) - - assert_apply(comp) - end end diff --git a/test/types/exec.rb b/test/types/exec.rb index 305f27d56..fd304e7d1 100755 --- a/test/types/exec.rb +++ b/test/types/exec.rb @@ -1,603 +1,608 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' require 'facter' class TestExec < Test::Unit::TestCase include PuppetTest def test_execution command = nil output = nil assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo" ) } assert_nothing_raised { command.evaluate } assert_events([:executed_command], command) end def test_numvsstring [0, "0"].each { |val| Puppet.type(:exec).clear Puppet.type(:component).clear command = nil output = nil assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo", :returns => val ) } assert_events([:executed_command], command) } end def test_path_or_qualified command = nil output = nil assert_raise(Puppet::Error) { command = Puppet.type(:exec).create( :command => "echo" ) } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "echo", :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } Puppet.type(:exec).clear assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo" ) } Puppet.type(:exec).clear assert_nothing_raised { command = Puppet.type(:exec).create( :command => "/bin/echo", :path => "/usr/bin:/bin:/usr/sbin:/sbin" ) } end def test_nonzero_returns assert_nothing_raised { command = Puppet.type(:exec).create( :command => "mkdir /this/directory/does/not/exist", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "touch /etc", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 1 ) } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "thiscommanddoesnotexist", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 127 ) } end def test_cwdsettings command = nil dir = "/tmp" wd = Dir.chdir(dir) { Dir.getwd } assert_nothing_raised { command = Puppet.type(:exec).create( :command => "pwd", :cwd => dir, :path => "/usr/bin:/bin:/usr/sbin:/sbin", :returns => 0 ) } assert_events([:executed_command], command) assert_equal(wd,command.output.chomp) end def test_refreshonly_functional file = nil cmd = nil tmpfile = tempfile() @@tmpfiles.push tmpfile trans = nil File.open(tmpfile, File::WRONLY|File::CREAT|File::TRUNC) { |of| of.puts rand(100) } file = Puppet.type(:file).create( :path => tmpfile, :checksum => "md5" ) assert_instance_of(Puppet.type(:file), file) assert_nothing_raised { cmd = Puppet.type(:exec).create( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :subscribe => [[file.class.name,file.name]], :refreshonly => true ) } assert_instance_of(Puppet.type(:exec), cmd) comp = Puppet.type(:component).create(:name => "RefreshTest") [file,cmd].each { |obj| comp.push obj } events = nil assert_nothing_raised { trans = comp.evaluate file.retrieve sum = file.state(:checksum) assert(sum.insync?, "checksum is not in sync") events = trans.evaluate.collect { |event| event.event } } # the first checksum shouldn't result in a changed file assert_equal([],events) File.open(tmpfile, File::WRONLY|File::CREAT|File::TRUNC) { |of| of.puts rand(100) of.puts rand(100) of.puts rand(100) } assert_nothing_raised { trans = comp.evaluate sum = file.state(:checksum) events = trans.evaluate.collect { |event| event.event } } # verify that only the file_changed event was kicked off, not the # command_executed assert_equal( [:file_changed, :triggered], events ) end def test_refreshonly cmd = true assert_nothing_raised { cmd = Puppet.type(:exec).create( :command => "pwd", :path => "/usr/bin:/bin:/usr/sbin:/sbin", :refreshonly => true ) } # Checks should always fail when refreshonly is enabled assert(!cmd.check, "Check passed with refreshonly true") # Now set it to false cmd[:refreshonly] = false assert(cmd.check, "Check failed with refreshonly false") end def test_creates file = tempfile() exec = nil assert(! FileTest.exists?(file), "File already exists") assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % file, :path => "/usr/bin:/bin:/usr/sbin:/sbin", :creates => file ) } comp = newcomp("createstest", exec) assert_events([:executed_command], comp, "creates") assert_events([], comp, "creates") end # Verify that we can download the file that we're going to execute. def test_retrievethenmkexe exe = tempfile() oexe = tempfile() sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } file = Puppet.type(:file).create( :path => oexe, :source => exe, :mode => 0755 ) exec = Puppet.type(:exec).create( :command => oexe, :require => [:file, oexe] ) comp = newcomp("Testing", file, exec) assert_events([:file_created, :executed_command], comp) end # Verify that we auto-require any managed scripts. def test_autorequire exe = tempfile() oexe = tempfile() sh = %x{which sh} File.open(exe, "w") { |f| f.puts "#!#{sh}\necho yup" } file = Puppet.type(:file).create( :path => oexe, :source => exe, :mode => 755 ) basedir = File.dirname(oexe) baseobj = Puppet.type(:file).create( :path => basedir, :source => exe, :mode => 755 ) ofile = Puppet.type(:file).create( :path => exe, :mode => 755 ) exec = Puppet.type(:exec).create( :command => oexe, :path => ENV["PATH"], :cwd => basedir ) cat = Puppet.type(:exec).create( :command => "cat %s %s" % [exe, oexe], :path => ENV["PATH"] ) - - comp = newcomp(ofile, exec, cat, file, baseobj) - comp.finalize + + rels = nil + assert_nothing_raised do + rels = exec.autorequire + end # Verify we get the script itself - assert(exec.requires?(file), "Exec did not autorequire %s" % file) + assert(rels.detect { |r| r.source == file }, "Exec did not autorequire its command") # Verify we catch the cwd - assert(exec.requires?(baseobj), "Exec did not autorequire cwd") + assert(rels.detect { |r| r.source == baseobj }, "Exec did not autorequire its cwd") # Verify we don't require ourselves + assert(! rels.detect { |r| r.source == ofile }, "Exec incorrectly required mentioned file") assert(!exec.requires?(ofile), "Exec incorrectly required file") - # Verify that we catch inline files # We not longer autorequire inline files - assert(! cat.requires?(ofile), "Exec required second inline file") - assert(! cat.requires?(file), "Exec required inline file") + assert_nothing_raised do + rels = cat.autorequire + end + assert(! rels.detect { |r| r.source == ofile }, "Exec required second inline file") + assert(! rels.detect { |r| r.source == file }, "Exec required inline file") end def test_ifonly afile = tempfile() bfile = tempfile() exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % bfile, :onlyif => "test -f %s" % afile, :path => ENV['PATH'] ) } assert_events([], exec) system("touch %s" % afile) assert_events([:executed_command], exec) assert_events([:executed_command], exec) system("rm %s" % afile) assert_events([], exec) end def test_unless afile = tempfile() bfile = tempfile() exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create( :command => "touch %s" % bfile, :unless => "test -f %s" % afile, :path => ENV['PATH'] ) } comp = newcomp(exec) assert_events([:executed_command], comp) assert_events([:executed_command], comp) system("touch %s" % afile) assert_events([], comp) assert_events([], comp) system("rm %s" % afile) assert_events([:executed_command], comp) assert_events([:executed_command], comp) end if Puppet::SUIDManager.uid == 0 # Verify that we can execute commands as a special user def mknverify(file, user, group = nil, id = true) args = { :command => "touch %s" % file, :path => "/usr/bin:/bin:/usr/sbin:/sbin", } if user #Puppet.warning "Using user %s" % user.name if id # convert to a string, because that's what the object expects args[:user] = user.uid.to_s else args[:user] = user.name end end if group #Puppet.warning "Using group %s" % group.name if id args[:group] = group.gid.to_s else args[:group] = group.name end end exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create(args) } comp = newcomp("usertest", exec) assert_events([:executed_command], comp, "usertest") assert(FileTest.exists?(file), "File does not exist") if user assert_equal(user.uid, File.stat(file).uid, "File UIDs do not match") end # We can't actually test group ownership, unfortunately, because # behaviour changes wildlly based on platform. Puppet::Type.allclear end def test_userngroup file = tempfile() [ [nonrootuser()], # just user, by name [nonrootuser(), nil, true], # user, by uid [nil, nonrootgroup()], # just group [nil, nonrootgroup(), true], # just group, by id [nonrootuser(), nonrootgroup()], # user and group, by name [nonrootuser(), nonrootgroup(), true], # user and group, by id ].each { |ary| mknverify(file, *ary) { } } end end def test_logoutput exec = nil assert_nothing_raised { exec = Puppet.type(:exec).create( :title => "logoutputesting", :path => "/usr/bin:/bin", :command => "echo logoutput is false", :logoutput => false ) } assert_apply(exec) assert_nothing_raised { exec[:command] = "echo logoutput is true" exec[:logoutput] = true } assert_apply(exec) assert_nothing_raised { exec[:command] = "echo logoutput is warning" exec[:logoutput] = "warning" } assert_apply(exec) end def test_execthenfile exec = nil file = nil basedir = tempfile() path = File.join(basedir, "subfile") assert_nothing_raised { exec = Puppet.type(:exec).create( :title => "mkdir", :path => "/usr/bin:/bin", :creates => basedir, :command => "mkdir %s; touch %s" % [basedir, path] ) } assert_nothing_raised { file = Puppet.type(:file).create( :path => basedir, :recurse => true, :mode => "755", :require => ["exec", "mkdir"] ) } comp = newcomp(file, exec) comp.finalize assert_events([:executed_command, :file_changed], comp) assert(FileTest.exists?(path), "Exec ran first") assert(File.stat(path).mode & 007777 == 0755) end def test_falsevals exec = nil assert_nothing_raised do exec = Puppet.type(:exec).create( :command => "/bin/touch yayness" ) end Puppet.type(:exec).checks.each do |check| klass = Puppet.type(:exec).paramclass(check) next if klass.values.include? :false assert_raise(Puppet::Error, "Check %s did not fail on false" % check) do exec[check] = false end end end def test_createcwdandexe exec1 = exec2 = nil dir = tempfile() file = tempfile() assert_nothing_raised { exec1 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "mkdir #{dir}" ) } assert_nothing_raised("Could not create exec w/out existing cwd") { exec2 = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch #{file}", :cwd => dir ) } # Throw a check in there with our cwd and make sure it works assert_nothing_raised("Could not check with a missing cwd") do exec2[:unless] = "test -f /this/file/does/not/exist" exec2.retrieve end assert_raise(Puppet::Error) do exec2.state(:returns).sync end assert_nothing_raised do exec2[:require] = ["exec", exec1.name] exec2.finish end assert_apply(exec1, exec2) assert(FileTest.exists?(file)) end def test_checkarrays exec = nil file = tempfile() test = "test -f #{file}" assert_nothing_raised { exec = Puppet.type(:exec).create( :path => ENV["PATH"], :command => "touch #{file}" ) } assert_nothing_raised { exec[:unless] = test } assert_nothing_raised { assert(exec.check, "Check did not pass") } assert_nothing_raised { exec[:unless] = [test, test] } assert_nothing_raised { exec.finish } assert_nothing_raised { assert(exec.check, "Check did not pass") } assert_apply(exec) assert_nothing_raised { assert(! exec.check, "Check passed") } end def test_missing_checks_cause_failures # Solaris's sh exits with 1 here instead of 127 return if Facter.value(:operatingsystem) == "Solaris" exec = Puppet::Type.newexec( :command => "echo true", :path => ENV["PATH"], :onlyif => "/bin/nosuchthingexists" ) assert_raise(ArgumentError, "Missing command did not raise error") { exec.run("/bin/nosuchthingexists") } end def test_envparam exec = Puppet::Type.newexec( :command => "echo $envtest", :path => ENV["PATH"], :env => "envtest=yayness" ) assert(exec, "Could not make exec") output = status = nil assert_nothing_raised { output, status = exec.run("echo $envtest") } assert_equal("yayness\n", output) # Now check whether we can do multiline settings assert_nothing_raised do exec[:env] = "envtest=a list of things and stuff" end output = status = nil assert_nothing_raised { output, status = exec.run('echo "$envtest"') } assert_equal("a list of things\nand stuff\n", output) # Now test arrays assert_nothing_raised do exec[:env] = ["funtest=A", "yaytest=B"] end output = status = nil assert_nothing_raised { output, status = exec.run('echo "$funtest" "$yaytest"') } assert_equal("A B\n", output) end end # $Id$ diff --git a/test/types/file.rb b/test/types/file.rb index d5b493788..c57129cd5 100755 --- a/test/types/file.rb +++ b/test/types/file.rb @@ -1,1795 +1,1766 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'fileutils' require 'puppettest' class TestFile < Test::Unit::TestCase include PuppetTest::FileTesting # hmmm # this is complicated, because we store references to the created # objects in a central store def mkfile(hash) file = nil assert_nothing_raised { file = Puppet.type(:file).create(hash) } return file end def mktestfile # because luke's home directory is on nfs, it can't be used for testing # as root tmpfile = tempfile() File.open(tmpfile, "w") { |f| f.puts rand(100) } @@tmpfiles.push tmpfile mkfile(:name => tmpfile) end def setup super @file = Puppet::Type.type(:file) begin initstorage rescue system("rm -rf %s" % Puppet[:statefile]) end end def teardown Puppet::Storage.clear system("rm -rf %s" % Puppet[:statefile]) super end def initstorage Puppet::Storage.init Puppet::Storage.load end def clearstorage Puppet::Storage.store Puppet::Storage.clear end def test_owner file = mktestfile() users = {} count = 0 # collect five users Etc.passwd { |passwd| if count > 5 break else count += 1 end users[passwd.uid] = passwd.name } fake = {} # find a fake user while true a = rand(1000) begin Etc.getpwuid(a) rescue fake[a] = "fakeuser" break end end uid, name = users.shift us = {} us[uid] = name users.each { |uid, name| assert_apply(file) assert_nothing_raised() { file[:owner] = name } assert_nothing_raised() { file.retrieve } assert_apply(file) } end def test_group file = mktestfile() [%x{groups}.chomp.split(/ /), Process.groups].flatten.each { |group| assert_nothing_raised() { file[:group] = group } assert(file.state(:group)) assert(file.state(:group).should) } end if Puppet::SUIDManager.uid == 0 def test_createasuser dir = tmpdir() user = nonrootuser() path = File.join(tmpdir, "createusertesting") @@tmpfiles << path file = nil assert_nothing_raised { file = Puppet.type(:file).create( :path => path, :owner => user.name, :ensure => "file", :mode => "755" ) } comp = newcomp("createusertest", file) assert_events([:file_created], comp) end def test_nofollowlinks basedir = tempfile() Dir.mkdir(basedir) file = File.join(basedir, "file") link = File.join(basedir, "link") File.open(file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(file, link) # First test 'user' user = nonrootuser() inituser = File.lstat(link).uid File.lchown(inituser, nil, link) obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :title => link, :owner => user.name ) } obj.retrieve # Make sure it defaults to managing the link assert_events([:file_changed], obj) assert_equal(user.uid, File.lstat(link).uid) assert_equal(inituser, File.stat(file).uid) File.chown(inituser, nil, file) File.lchown(inituser, nil, link) # Try following obj[:links] = :follow assert_events([:file_changed], obj) assert_equal(user.uid, File.stat(file).uid) assert_equal(inituser, File.lstat(link).uid) # And then explicitly managing File.chown(inituser, nil, file) File.lchown(inituser, nil, link) obj[:links] = :manage assert_events([:file_changed], obj) assert_equal(user.uid, File.lstat(link).uid) assert_equal(inituser, File.stat(file).uid) obj.delete(:owner) obj[:links] = :ignore # And then test 'group' group = nonrootgroup initgroup = File.stat(file).gid obj[:group] = group.name assert_events([:file_changed], obj) assert_equal(initgroup, File.stat(file).gid) assert_equal(group.gid, File.lstat(link).gid) File.chown(nil, initgroup, file) File.lchown(nil, initgroup, link) obj[:links] = :follow assert_events([:file_changed], obj) assert_equal(group.gid, File.stat(file).gid) File.chown(nil, initgroup, file) File.lchown(nil, initgroup, link) obj[:links] = :manage assert_events([:file_changed], obj) assert_equal(group.gid, File.lstat(link).gid) assert_equal(initgroup, File.stat(file).gid) end def test_ownerasroot file = mktestfile() users = {} count = 0 # collect five users Etc.passwd { |passwd| if count > 5 break else count += 1 end next if passwd.uid < 0 users[passwd.uid] = passwd.name } fake = {} # find a fake user while true a = rand(1000) begin Etc.getpwuid(a) rescue fake[a] = "fakeuser" break end end users.each { |uid, name| assert_nothing_raised() { file[:owner] = name } changes = [] assert_nothing_raised() { changes << file.evaluate } assert(changes.length > 0) assert_apply(file) file.retrieve assert(file.insync?()) assert_nothing_raised() { file[:owner] = uid } assert_apply(file) file.retrieve # make sure changing to number doesn't cause a sync assert(file.insync?()) } # We no longer raise an error here, because we check at run time #fake.each { |uid, name| # assert_raise(Puppet::Error) { # file[:owner] = name # } # assert_raise(Puppet::Error) { # file[:owner] = uid # } #} end def test_groupasroot file = mktestfile() [%x{groups}.chomp.split(/ /), Process.groups].flatten.each { |group| assert_nothing_raised() { file[:group] = group } assert(file.state(:group)) assert(file.state(:group).should) assert_apply(file) file.retrieve assert(file.insync?()) assert_nothing_raised() { file.delete(:group) } } end if Facter.value(:operatingsystem) == "Darwin" def test_sillyowner file = tempfile() File.open(file, "w") { |f| f.puts "" } File.chown(-2, nil, file) assert(File.stat(file).uid > 120000, "eh?") user = nonrootuser obj = Puppet::Type.newfile( :path => file, :owner => user.name ) assert_apply(obj) assert_equal(user.uid, File.stat(file).uid) end end else $stderr.puts "Run as root for complete owner and group testing" end def test_create %w{a b c d}.collect { |name| tempfile() + name.to_s }.each { |path| file =nil assert_nothing_raised() { file = Puppet.type(:file).create( :name => path, :ensure => "file" ) } assert_events([:file_created], file) assert_events([], file) assert(FileTest.file?(path), "File does not exist") assert(file.insync?()) @@tmpfiles.push path } end def test_create_dir basedir = tempfile() Dir.mkdir(basedir) %w{a b c d}.collect { |name| "#{basedir}/%s" % name }.each { |path| file = nil assert_nothing_raised() { file = Puppet.type(:file).create( :name => path, :ensure => "directory" ) } assert(! FileTest.directory?(path), "Directory %s already exists" % [path]) assert_events([:directory_created], file) assert_events([], file) assert(file.insync?()) assert(FileTest.directory?(path)) @@tmpfiles.push path } end def test_modes file = mktestfile # Set it to something else initially File.chmod(0775, file.title) [0644,0755,0777,0641].each { |mode| assert_nothing_raised() { file[:mode] = mode } assert_events([:file_changed], file) assert_events([], file) assert(file.insync?()) assert_nothing_raised() { file.delete(:mode) } } end def test_checksums types = %w{md5 md5lite timestamp time} exists = "/tmp/sumtest-exists" nonexists = "/tmp/sumtest-nonexists" @@tmpfiles << exists @@tmpfiles << nonexists # try it both with files that exist and ones that don't files = [exists, nonexists] initstorage File.open(exists,File::CREAT|File::TRUNC|File::WRONLY) { |of| of.puts "initial text" } types.each { |type| files.each { |path| if Puppet[:debug] Puppet.warning "Testing %s on %s" % [type,path] end file = nil events = nil # okay, we now know that we have a file... assert_nothing_raised() { file = Puppet.type(:file).create( :name => path, :ensure => "file", :checksum => type ) } trans = nil file.retrieve if file.title !~ /nonexists/ sum = file.state(:checksum) assert(sum.insync?, "file is not in sync") end events = assert_apply(file) assert(! events.include?(:file_changed), "File incorrectly changed") assert_events([], file) # We have to sleep because the time resolution of the time-based # mechanisms is greater than one second sleep 1 if type =~ /time/ assert_nothing_raised() { File.open(path,File::CREAT|File::TRUNC|File::WRONLY) { |of| of.puts "some more text, yo" } } Puppet.type(:file).clear # now recreate the file assert_nothing_raised() { file = Puppet.type(:file).create( :name => path, :checksum => type ) } trans = nil assert_events([:file_changed], file) # Run it a few times to make sure we aren't getting # spurious changes. assert_nothing_raised do file.state(:checksum).retrieve end assert(file.state(:checksum).insync?, "checksum is not in sync") sleep 1.1 if type =~ /time/ assert_nothing_raised() { File.unlink(path) File.open(path,File::CREAT|File::TRUNC|File::WRONLY) { |of| # We have to put a certain amount of text in here or # the md5-lite test fails 2.times { of.puts rand(100) } of.flush } } assert_events([:file_changed], file) # verify that we're actually getting notified when a file changes assert_nothing_raised() { Puppet.type(:file).clear } if path =~ /nonexists/ File.unlink(path) end } } end def cyclefile(path) # i had problems with using :name instead of :path [:name,:path].each { |param| file = nil changes = nil comp = nil trans = nil initstorage assert_nothing_raised { file = Puppet.type(:file).create( param => path, :recurse => true, :checksum => "md5" ) } comp = Puppet.type(:component).create( :name => "component" ) comp.push file assert_nothing_raised { trans = comp.evaluate } assert_nothing_raised { trans.evaluate } clearstorage Puppet::Type.allclear } end def test_localrecurse # Create a test directory path = tempfile() dir = @file.create :path => path, :mode => 0755, :recurse => true Dir.mkdir(path) # Make sure we return nothing when there are no children ret = nil assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal([], ret, "empty dir returned children") # Now make a file and make sure we get it test = File.join(path, "file") File.open(test, "w") { |f| f.puts "yay" } assert_nothing_raised() { ret = dir.localrecurse(true) } fileobj = @file[test] assert(fileobj, "child object was not created") assert_equal([fileobj], ret, "child object was not returned") # check that the file lists us as a dependency assert_equal([[:file, dir.title]], fileobj[:require], "dependency was not set up") # And that it inherited our recurse setting assert_equal(true, fileobj[:recurse], "file did not inherit recurse") # Make sure it's not returned again assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal([], ret, "child object was returned twice") # Now just for completion, make sure we will return many files files = [] 10.times do |i| f = File.join(path, i.to_s) files << f File.open(f, "w") do |o| o.puts "" end end assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal(files.sort, ret.collect { |f| f.title }, "child object was returned twice") # Clean everything up and start over files << test files.each do |f| File.unlink(f) end # Now make sure we correctly ignore things dir[:ignore] = "*.out" bad = File.join(path, "test.out") good = File.join(path, "yayness") [good, bad].each do |f| File.open(f, "w") { |o| o.puts "" } end assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal([good], ret.collect { |f| f.title }, "ignore failed") # Now make sure purging works dir[:purge] = true dir[:ignore] = "svn" assert_nothing_raised() { ret = dir.localrecurse(true) } assert_equal([bad], ret.collect { |f| f.title }, "purge failed") badobj = @file[bad] assert(badobj, "did not create bad object") assert_equal(:absent, badobj.should(:ensure), "ensure was not set to absent on bad object") end def test_recurse basedir = tempfile() FileUtils.mkdir_p(basedir) # Create our file dir = nil assert_nothing_raised { dir = Puppet.type(:file).create( :path => basedir, :check => %w{owner mode group} ) } return_nil = false # and monkey-patch it [:localrecurse, :sourcerecurse, :linkrecurse].each do |m| dir.meta_def(m) do |recurse| if return_nil # for testing nil return, of course return nil else return [recurse] end end end # First try it with recurse set to false dir[:recurse] = false assert_nothing_raised do assert_nil(dir.recurse) end # Now try it with the different valid positive values [true, "true", "inf", 50].each do |value| assert_nothing_raised { dir[:recurse] = value} # Now make sure the methods are called appropriately ret = nil assert_nothing_raised do ret = dir.recurse end # We should only call the localrecurse method, so make sure # that's the case if value == 50 # Make sure our counter got decremented assert_equal([49], ret, "did not call localrecurse") else assert_equal([true], ret, "did not call localrecurse") end end # Make sure it doesn't recurse when we've set recurse to false [false, "false"].each do |value| assert_nothing_raised { dir[:recurse] = value } ret = nil assert_nothing_raised() { ret = dir.recurse } assert_nil(ret) end dir[:recurse] = true # Now add a target, so we do the linking thing dir[:target] = tempfile() ret = nil assert_nothing_raised { ret = dir.recurse } assert_equal([true, true], ret, "did not call linkrecurse") # And add a source, and make sure we call that dir[:source] = tempfile() assert_nothing_raised { ret = dir.recurse } assert_equal([true, true, true], ret, "did not call linkrecurse") # Lastly, make sure we correctly handle returning nil return_nil = true assert_nothing_raised { ret = dir.recurse } end def test_recurse? file = Puppet::Type.type(:file).create :path => tempfile # Make sure we default to false assert(! file.recurse?, "Recurse defaulted to true") [true, "true", 10, "inf"].each do |value| file[:recurse] = value assert(file.recurse?, "%s did not cause recursion" % value) end [false, "false", 0].each do |value| file[:recurse] = value assert(! file.recurse?, "%s caused recursion" % value) end end def test_recursion basedir = tempfile() subdir = File.join(basedir, "subdir") tmpfile = File.join(basedir,"testing") FileUtils.mkdir_p(subdir) dir = nil [true, "true", "inf", 50].each do |value| assert_nothing_raised { dir = Puppet.type(:file).create( :path => basedir, :recurse => value, :check => %w{owner mode group} ) } children = nil assert_nothing_raised { children = dir.eval_generate } assert_equal([subdir], children.collect {|c| c.title }, "Incorrect generated children") dir.class[subdir].remove File.open(tmpfile, "w") { |f| f.puts "yayness" } assert_nothing_raised { children = dir.eval_generate } assert_equal([subdir, tmpfile].sort, children.collect {|c| c.title }.sort, "Incorrect generated children") File.unlink(tmpfile) #system("rm -rf %s" % basedir) Puppet.type(:file).clear end end def test_filetype_retrieval file = nil # Verify it retrieves files of type directory assert_nothing_raised { file = Puppet.type(:file).create( :name => tmpdir(), :check => :type ) } assert_nothing_raised { file.evaluate } assert_equal("directory", file.state(:type).is) # And then check files assert_nothing_raised { file = Puppet.type(:file).create( :name => tempfile(), :ensure => "file" ) } assert_apply(file) file[:check] = "type" assert_apply(file) assert_equal("file", file.state(:type).is) file[:type] = "directory" assert_nothing_raised { file.retrieve } # The 'retrieve' method sets @should to @is, so they're never # out of sync. It's a read-only class. assert(file.insync?) end def test_remove basedir = tempfile() subdir = File.join(basedir, "this") FileUtils.mkdir_p(subdir) dir = nil assert_nothing_raised { dir = Puppet.type(:file).create( :path => basedir, :recurse => true, :check => %w{owner mode group} ) } assert_nothing_raised { dir.eval_generate } obj = nil assert_nothing_raised { obj = Puppet.type(:file)[subdir] } assert(obj, "Could not retrieve subdir object") assert_nothing_raised { obj.remove(true) } assert_nothing_raised { obj = Puppet.type(:file)[subdir] } assert_nil(obj, "Retrieved removed object") end def test_path dir = tempfile() path = File.join(dir, "subdir") assert_nothing_raised("Could not make file") { FileUtils.mkdir_p(File.dirname(path)) File.open(path, "w") { |f| f.puts "yayness" } } file = nil dirobj = nil assert_nothing_raised("Could not make file object") { dirobj = Puppet.type(:file).create( :path => dir, :recurse => true, :check => %w{mode owner group} ) } assert_nothing_raised { - dirobj.generate + dirobj.eval_generate } assert_nothing_raised { file = dirobj.class[path] } assert(file, "Could not retrieve file object") assert_equal("file=%s" % file.title, file.path) end def test_autorequire basedir = tempfile() subfile = File.join(basedir, "subfile") baseobj = Puppet.type(:file).create( :name => basedir, :ensure => "directory" ) subobj = Puppet.type(:file).create( :name => subfile, :ensure => "file" ) - comp = newcomp(baseobj, subobj) - comp.finalize - - assert(subobj.requires?(baseobj), "File did not require basedir") - assert(!subobj.requires?(subobj), "File required itself") - assert_events([:directory_created, :file_created], comp) + edge = nil + assert_nothing_raised do + edge = subobj.autorequire.shift + end + assert_equal(baseobj, edge.source, "file did not require its parent dir") + assert_equal(subobj, edge.target, "file did not require its parent dir") end def test_content file = tempfile() str = "This is some content" obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :name => file, :content => str ) } assert(!obj.insync?, "Object is incorrectly in sync") assert_events([:file_created], obj) obj.retrieve assert(obj.insync?, "Object is not in sync") text = File.read(file) assert_equal(str, text, "Content did not copy correctly") newstr = "Another string, yo" obj[:content] = newstr assert(!obj.insync?, "Object is incorrectly in sync") assert_events([:file_changed], obj) text = File.read(file) assert_equal(newstr, text, "Content did not copy correctly") obj.retrieve assert(obj.insync?, "Object is not in sync") end # Unfortunately, I know this fails def disabled_test_recursivemkdir path = tempfile() subpath = File.join(path, "this", "is", "a", "dir") file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => subpath, :ensure => "directory", :recurse => true ) } comp = newcomp("yay", file) comp.finalize assert_apply(comp) #assert_events([:directory_created], comp) assert(FileTest.directory?(subpath), "Did not create directory") end # Make sure that content updates the checksum on the same run def test_checksumchange_for_content dest = tempfile() File.open(dest, "w") { |f| f.puts "yayness" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :checksum => "md5", :content => "This is some content" ) } file.retrieve assert_events([:file_changed], file) file.retrieve assert_events([], file) end # Make sure that content updates the checksum on the same run def test_checksumchange_for_ensure dest = tempfile() file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :checksum => "md5", :ensure => "file" ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) end # Make sure that content gets used before ensure def test_contentbeatsensure dest = tempfile() file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :ensure => "file", :content => "this is some content, yo" ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) assert_events([], file) end def test_nameandpath path = tempfile() file = nil assert_nothing_raised { file = Puppet.type(:file).create( :title => "fileness", :path => path, :content => "this is some content" ) } assert_apply(file) assert(FileTest.exists?(path)) end # Make sure that a missing group isn't fatal at object instantiation time. def test_missinggroup file = nil assert_nothing_raised { file = Puppet.type(:file).create( :path => tempfile(), :group => "fakegroup" ) } assert(file.state(:group), "Group state failed") end def test_modecreation path = tempfile() file = Puppet.type(:file).create( :path => path, :ensure => "file", :mode => "0777" ) assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777) File.unlink(path) file[:ensure] = "directory" assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777) end def test_followlinks basedir = tempfile() Dir.mkdir(basedir) file = File.join(basedir, "file") link = File.join(basedir, "link") File.open(file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(file, link) obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :path => link, :mode => "755" ) } obj.retrieve assert_events([], obj) # Assert that we default to not following links assert_equal("%o" % 0644, "%o" % (File.stat(file).mode & 007777)) # Assert that we can manage the link directly, but modes still don't change obj[:links] = :manage assert_events([], obj) assert_equal("%o" % 0644, "%o" % (File.stat(file).mode & 007777)) obj[:links] = :follow assert_events([:file_changed], obj) assert_equal("%o" % 0755, "%o" % (File.stat(file).mode & 007777)) # Now verify that content and checksum don't update, either obj.delete(:mode) obj[:checksum] = "md5" obj[:links] = :ignore assert_events([], obj) File.open(file, "w") { |f| f.puts "more text" } assert_events([], obj) obj[:links] = :follow assert_events([], obj) File.open(file, "w") { |f| f.puts "even more text" } assert_events([:file_changed], obj) obj.delete(:checksum) obj[:content] = "this is some content" obj[:links] = :ignore assert_events([], obj) File.open(file, "w") { |f| f.puts "more text" } assert_events([], obj) obj[:links] = :follow assert_events([:file_changed], obj) end # If both 'ensure' and 'content' are used, make sure that all of the other # states are handled correctly. def test_contentwithmode path = tempfile() file = nil assert_nothing_raised { file = Puppet.type(:file).create( :path => path, :ensure => "file", :content => "some text\n", :mode => 0755 ) } assert_apply(file) assert_equal("%o" % 0755, "%o" % (File.stat(path).mode & 007777)) end # Make sure we can create symlinks def test_symlinks path = tempfile() link = tempfile() File.open(path, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :title => "somethingelse", :ensure => path, :path => link ) } assert_events([:link_created], file) assert(FileTest.symlink?(link), "Link was not created") assert_equal(path, File.readlink(link), "Link was created incorrectly") # Make sure running it again works assert_events([], file) assert_events([], file) assert_events([], file) end def test_linkrecurse dest = tempfile() link = @file.create :path => tempfile(), :recurse => true, :ensure => dest ret = nil # Start with nothing, just to make sure we get nothing back assert_nothing_raised { ret = link.linkrecurse(true) } assert_nil(ret, "got a return when the dest doesn't exist") # then with a directory with only one file Dir.mkdir(dest) one = File.join(dest, "one") File.open(one, "w") { |f| f.puts "" } link[:ensure] = dest assert_nothing_raised { ret = link.linkrecurse(true) } assert_equal(:directory, link.should(:ensure), "ensure was not set to directory") assert_equal([File.join(link.title, "one")], ret.collect { |f| f.title }, "Did not get linked file") oneobj = @file[File.join(link.title, "one")] assert_equal(one, oneobj.should(:target), "target was not set correctly") oneobj.remove File.unlink(one) # Then make sure we get multiple files returns = [] 5.times do |i| path = File.join(dest, i.to_s) returns << File.join(link.title, i.to_s) File.open(path, "w") { |f| f.puts "" } end assert_nothing_raised { ret = link.linkrecurse(true) } assert_equal(returns.sort, ret.collect { |f| f.title }, "Did not get links back") returns.each do |path| obj = @file[path] assert(path, "did not get obj for %s" % path) sdest = File.join(dest, File.basename(path)) assert_equal(sdest, obj.should(:target), "target was not set correctly for %s" % path) end end def test_simplerecursivelinking source = tempfile() path = tempfile() subdir = File.join(source, "subdir") file = File.join(subdir, "file") system("mkdir -p %s" % subdir) system("touch %s" % file) link = nil assert_nothing_raised { link = Puppet.type(:file).create( :ensure => source, :path => path, :recurse => true ) } assert_apply(link) sublink = File.join(path, "subdir") linkpath = File.join(sublink, "file") assert(File.directory?(path), "dest is not a dir") assert(File.directory?(sublink), "subdest is not a dir") assert(File.symlink?(linkpath), "path is not a link") assert_equal(file, File.readlink(linkpath)) assert_nil(@file[sublink], "objects were not removed") assert_events([], link) end def test_recursivelinking source = tempfile() dest = tempfile() files = [] dirs = [] # Make a bunch of files and dirs Dir.mkdir(source) Dir.chdir(source) do system("mkdir -p %s" % "some/path/of/dirs") system("mkdir -p %s" % "other/path/of/dirs") system("touch %s" % "file") system("touch %s" % "other/file") system("touch %s" % "some/path/of/file") system("touch %s" % "some/path/of/dirs/file") system("touch %s" % "other/path/of/file") files = %x{find . -type f}.chomp.split(/\n/) dirs = %x{find . -type d}.chomp.split(/\n/).reject{|d| d =~ /^\.+$/ } end link = nil assert_nothing_raised { link = Puppet.type(:file).create( :ensure => source, :path => dest, :recurse => true ) } assert_apply(link) files.each do |f| f.sub!(/^\.#{File::SEPARATOR}/, '') path = File.join(dest, f) assert(FileTest.exists?(path), "Link %s was not created" % path) assert(FileTest.symlink?(path), "%s is not a link" % f) target = File.readlink(path) assert_equal(File.join(source, f), target) end dirs.each do |d| d.sub!(/^\.#{File::SEPARATOR}/, '') path = File.join(dest, d) assert(FileTest.exists?(path), "Dir %s was not created" % path) assert(FileTest.directory?(path), "%s is not a directory" % d) end end def test_localrelativelinks dir = tempfile() Dir.mkdir(dir) source = File.join(dir, "source") File.open(source, "w") { |f| f.puts "yay" } dest = File.join(dir, "link") link = nil assert_nothing_raised { link = Puppet.type(:file).create( :path => dest, :ensure => "source" ) } assert_events([:link_created], link) assert(FileTest.symlink?(dest), "Did not create link") assert_equal("source", File.readlink(dest)) assert_equal("yay\n", File.read(dest)) end def test_recursivelinkingmissingtarget source = tempfile() dest = tempfile() objects = [] objects << Puppet.type(:exec).create( :command => "mkdir %s; touch %s/file" % [source, source], :title => "yay", :path => ENV["PATH"] ) objects << Puppet.type(:file).create( :ensure => source, :path => dest, :recurse => true, :require => objects[0] ) assert_apply(*objects) link = File.join(dest, "file") assert(FileTest.symlink?(link), "Did not make link") assert_equal(File.join(source, "file"), File.readlink(link)) end def test_backupmodes file = tempfile() newfile = tempfile() File.open(file, "w", 0411) { |f| f.puts "yayness" } obj = nil assert_nothing_raised { obj = Puppet::Type.type(:file).create( :path => file, :content => "rahness\n" ) } # user = group = nil # if Process.uid == 0 # user = nonrootuser # group = nonrootgroup # obj[:owner] = user.name # obj[:group] = group.name # File.chown(user.uid, group.gid, file) # end assert_apply(obj) backupfile = file + obj[:backup] @@tmpfiles << backupfile assert(FileTest.exists?(backupfile), "Backup file %s does not exist" % backupfile) assert_equal(0411, filemode(backupfile), "File mode is wrong for backupfile") # if Process.uid == 0 # assert_equal(user.uid, File.stat(backupfile).uid) # assert_equal(group.gid, File.stat(backupfile).gid) # end bucket = "bucket" bpath = tempfile() Dir.mkdir(bpath) Puppet::Type.type(:filebucket).create( :title => bucket, :path => bpath ) obj[:backup] = bucket obj[:content] = "New content" assert_apply(obj) bucketedpath = File.join(bpath, "18cc17fa3047fcc691fdf49c0a7f539a", "contents") assert_equal(0440, filemode(bucketedpath)) end def test_largefilechanges source = tempfile() dest = tempfile() # Now make a large file File.open(source, "w") { |f| 500.times { |i| f.puts "line %s" % i } } obj = Puppet::Type.type(:file).create( :title => dest, :source => source ) assert_events([:file_created], obj) File.open(source, File::APPEND|File::WRONLY) { |f| f.puts "another line" } assert_events([:file_changed], obj) # Now modify the dest file File.open(dest, File::APPEND|File::WRONLY) { |f| f.puts "one more line" } assert_events([:file_changed, :file_changed], obj) end def test_replacefilewithlink path = tempfile() link = tempfile() File.open(path, "w") { |f| f.puts "yay" } File.open(link, "w") { |f| f.puts "a file" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :ensure => path, :path => link ) } assert_events([:link_created], file) assert(FileTest.symlink?(link), "Link was not created") assert_equal(path, File.readlink(link), "Link was created incorrectly") end def test_replacedirwithlink path = tempfile() link = tempfile() File.open(path, "w") { |f| f.puts "yay" } Dir.mkdir(link) File.open(File.join(link, "yay"), "w") do |f| f.puts "boo" end file = nil assert_nothing_raised { file = Puppet.type(:file).create( :ensure => path, :path => link, :backup => false ) } # First run through without :force assert_events([], file) assert(FileTest.directory?(link), "Link replaced dir without force") assert_nothing_raised { file[:force] = true } assert_events([:link_created], file) assert(FileTest.symlink?(link), "Link was not created") assert_equal(path, File.readlink(link), "Link was created incorrectly") end def test_replace_links_with_files base = tempfile() Dir.mkdir(base) file = File.join(base, "file") link = File.join(base, "link") File.open(file, "w") { |f| f.puts "yayness" } File.symlink(file, link) obj = Puppet::Type.type(:file).create( :path => link, :ensure => "file" ) assert_apply(obj) assert_equal("yayness\n", File.read(file), "Original file got changed") assert_equal("file", File.lstat(link).ftype, "File is still a link") end def test_no_erase_linkedto_files base = tempfile() Dir.mkdir(base) dirs = {} %w{other source target}.each do |d| dirs[d] = File.join(base, d) Dir.mkdir(dirs[d]) end file = File.join(dirs["other"], "file") sourcefile = File.join(dirs["source"], "sourcefile") link = File.join(dirs["target"], "link") File.open(file, "w") { |f| f.puts "other" } File.open(sourcefile, "w") { |f| f.puts "source" } File.symlink(file, link) obj = Puppet::Type.type(:file).create( :path => dirs["target"], :ensure => "file", :source => dirs["source"], :recurse => true ) trans = assert_events([:file_created, :file_created], obj) newfile = File.join(dirs["target"], "sourcefile") assert(File.exists?(newfile), "File did not get copied") assert_equal(File.read(sourcefile), File.read(newfile), "File did not get copied correctly.") assert_equal("other\n", File.read(file), "Original file got changed") assert_equal("file", File.lstat(link).ftype, "File is still a link") end def test_replace_links dest = tempfile() otherdest = tempfile() link = tempfile() File.open(dest, "w") { |f| f.puts "boo" } File.open(otherdest, "w") { |f| f.puts "yay" } obj = Puppet::Type.type(:file).create( :path => link, :ensure => otherdest ) assert_apply(obj) assert_equal(otherdest, File.readlink(link), "Link did not get created") obj[:ensure] = dest assert_apply(obj) assert_equal(dest, File.readlink(link), "Link did not get changed") end def test_file_with_spaces dir = tempfile() Dir.mkdir(dir) source = File.join(dir, "file spaces") dest = File.join(dir, "another space") File.open(source, "w") { |f| f.puts :yay } obj = Puppet::Type.type(:file).create( :path => dest, :source => source ) assert(obj, "Did not create file") assert_apply(obj) assert(FileTest.exists?(dest), "File did not get created") end def test_present_matches_anything path = tempfile() file = Puppet::Type.newfile(:path => path, :ensure => :present) file.retrieve assert(! file.insync?, "File incorrectly in sync") # Now make a file File.open(path, "w") { |f| f.puts "yay" } file.retrieve assert(file.insync?, "File not in sync") # Now make a directory File.unlink(path) Dir.mkdir(path) file.retrieve assert(file.insync?, "Directory not considered 'present'") Dir.rmdir(path) # Now make a link file[:links] = :manage otherfile = tempfile() File.symlink(otherfile, path) file.retrieve assert(file.insync?, "Symlink not considered 'present'") File.unlink(path) # Now set some content, and make sure it works file[:content] = "yayness" assert_apply(file) assert_equal("yayness", File.read(path), "Content did not get set correctly") end # Make sure unmanaged files are be purged. def test_purge sourcedir = tempfile() destdir = tempfile() Dir.mkdir(sourcedir) Dir.mkdir(destdir) sourcefile = File.join(sourcedir, "sourcefile") dsourcefile = File.join(destdir, "sourcefile") localfile = File.join(destdir, "localfile") randfile = File.join(destdir, "random") File.open(sourcefile, "w") { |f| f.puts "funtest" } # this file should get removed File.open(randfile, "w") { |f| f.puts "footest" } lfobj = Puppet::Type.newfile(:path => localfile, :content => "rahtest") destobj = Puppet::Type.newfile(:path => destdir, :source => sourcedir, :recurse => true) assert_apply(lfobj, destobj) assert(FileTest.exists?(dsourcefile), "File did not get copied") assert(FileTest.exists?(localfile), "File did not get created") assert(FileTest.exists?(randfile), "File got prematurely purged") assert_nothing_raised { destobj[:purge] = true } assert_apply(lfobj, destobj) assert(FileTest.exists?(dsourcefile), "File got purged") assert(FileTest.exists?(localfile), "File got purged") assert(! FileTest.exists?(randfile), "File did not get purged") end # Testing #274. Make sure target can be used without 'ensure'. def test_target_without_ensure source = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "funtest" } obj = nil assert_nothing_raised { obj = Puppet::Type.newfile(:path => dest, :target => source) } assert_apply(obj) end def test_autorequire_owner_and_group file = tempfile() comp = nil user = nil group =nil home = nil ogroup = nil assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestu", :home => file, :gid => "pptestg" ) home = Puppet.type(:file).create( :path => file, :owner => "pptestu", :group => "pptestg", :ensure => "directory" ) group = Puppet.type(:group).create( :name => "pptestg" ) comp = newcomp(user, group, home) } - comp.finalize - comp.retrieve - - assert(home.requires?(user), "File did not require owner") - assert(home.requires?(group), "File did not require group") + + # Now make sure we get a relationship for each of these + rels = nil + assert_nothing_raised { rels = home.autorequire } + assert(rels.detect { |e| e.source == user }, "owner was not autorequired") + assert(rels.detect { |e| e.source == group }, "group was not autorequired") end # Testing #309 -- //my/file => /my/file def test_slash_deduplication ["/my/////file/for//testing", "//my/file/for/testing///", "/my/file/for/testing"].each do |path| file = nil assert_nothing_raised do file = Puppet::Type.newfile(:path => path) end assert_equal("/my/file/for/testing", file.title) assert_equal(file, Puppet::Type.type(:file)["/my/file/for/testing"]) Puppet::Type.type(:file).clear end end # Testing #304 def test_links_to_directories link = tempfile() file = tempfile() dir = tempfile() Dir.mkdir(dir) bucket = Puppet::Type.newfilebucket :name => "main" File.symlink(dir, link) File.open(file, "w") { |f| f.puts "" } assert_equal(dir, File.readlink(link)) obj = Puppet::Type.newfile :path => link, :ensure => :link, :target => file, :recurse => false, :backup => "main" assert_apply(obj) assert_equal(file, File.readlink(link)) end # Testing #303 def test_nobackups_with_links link = tempfile() new = tempfile() File.open(link, "w") { |f| f.puts "old" } File.open(new, "w") { |f| f.puts "new" } obj = Puppet::Type.newfile :path => link, :ensure => :link, :target => new, :recurse => true, :backup => false assert_nothing_raised do obj.handlebackup end bfile = [link, "puppet-bak"].join(".") assert(! FileTest.exists?(bfile), "Backed up when told not to") assert_apply(obj) assert(! FileTest.exists?(bfile), "Backed up when told not to") end # Make sure we consistently handle backups for all cases. def test_ensure_with_backups # We've got three file types, so make sure we can replace any type # with the other type and that backups are done correctly. types = [:file, :directory, :link] dir = tempfile() path = File.join(dir, "test") linkdest = tempfile() creators = { :file => proc { File.open(path, "w") { |f| f.puts "initial" } }, :directory => proc { Dir.mkdir(path) }, :link => proc { File.symlink(linkdest, path) } } bucket = Puppet::Type.newfilebucket :name => "main", :path => tempfile() obj = Puppet::Type.newfile :path => path, :force => true, :links => :manage Puppet[:trace] = true ["main", false].each do |backup| obj[:backup] = backup obj.finish types.each do |should| types.each do |is| # It makes no sense to replace a directory with a directory # next if should == :directory and is == :directory Dir.mkdir(dir) # Make the thing creators[is].call obj[:ensure] = should if should == :link obj[:target] = linkdest else if obj.state(:target) obj.delete(:target) end end # First try just removing the initial data assert_nothing_raised do obj.remove_existing(should) end unless is == should # Make sure the original is gone assert(! FileTest.exists?(obj[:path]), "remove_existing did not work: " + "did not remove %s with %s" % [is, should]) end FileUtils.rmtree(obj[:path]) # Now make it again creators[is].call state = obj.state(:ensure) state.retrieve unless state.insync? assert_nothing_raised do state.sync end end FileUtils.rmtree(dir) end end end end - - def test_check_checksums - dir = tempfile() - Dir.mkdir(dir) - subdir = File.join(dir, "sub") - Dir.mkdir(subdir) - file = File.join(dir, "file") - File.open(file, "w") { |f| f.puts "yay" } - - obj = Puppet::Type.type(:file).create( - :path => dir, :check => :checksum, :recurse => true - ) - - assert_apply(obj) - File.open(file, "w") { |f| f.puts "rah" } - sleep 1 - system("touch %s" % subdir) - Puppet::Storage.store - Puppet::Storage.load - assert_apply(obj) - [file, subdir].each do |path| - sub = Puppet::Type.type(:file)[path] - assert(sub, "did not find obj for %s" % path) - sub.retrieve - - assert_nothing_raised do - sub.state(:checksum).sync - end - end - end end # $Id$ diff --git a/test/types/filesources.rb b/test/types/filesources.rb index bae4c7d5f..c1c601b59 100755 --- a/test/types/filesources.rb +++ b/test/types/filesources.rb @@ -1,939 +1,940 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'cgi' require 'fileutils' require 'puppettest' class TestFileSources < Test::Unit::TestCase include PuppetTest::FileTesting def setup super if defined? @port @port += 1 else @port = 8800 end @file = Puppet::Type.type(:file) end def use_storage begin initstorage rescue system("rm -rf %s" % Puppet[:statefile]) end end def initstorage Puppet::Storage.init Puppet::Storage.load end # Make a simple recursive tree. def mk_sourcetree source = tempfile() sourcefile = File.join(source, "file") Dir.mkdir source File.open(sourcefile, "w") { |f| f.puts "yay" } dest = tempfile() destfile = File.join(dest, "file") return source, dest, sourcefile, destfile end def test_newchild path = tempfile() @@tmpfiles.push path FileUtils.mkdir_p path File.open(File.join(path,"childtest"), "w") { |of| of.puts "yayness" } file = nil comp = nil trans = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => path ) } child = nil assert_nothing_raised { child = file.newchild("childtest", true) } assert(child) assert_raise(Puppet::DevError) { file.newchild(File.join(path,"childtest"), true) } end def test_describe source = tempfile() dest = tempfile() file = Puppet::Type.newfile :path => dest, :source => source, :title => "copier" state = file.state(:source) # First try describing with a normal source result = nil assert_nothing_raised do result = state.describe(source) end assert_nil(result, "Got a result back when source is missing") # Now make a remote directory Dir.mkdir(source) assert_nothing_raised do result = state.describe(source) end assert_equal("directory", result[:type]) # And as a file Dir.rmdir(source) File.open(source, "w") { |f| f.puts "yay" } assert_nothing_raised do result = state.describe(source) end assert_equal("file", result[:type]) assert(result[:checksum], "did not get value for checksum") if Puppet::SUIDManager.uid == 0 assert(result.has_key?("owner"), "Lost owner in describe") else assert(! result.has_key?("owner"), "Kept owner in describe even tho not root") end # Now let's do the various link things File.unlink(source) target = tempfile() File.open(target, "w") { |f| f.puts "yay" } File.symlink(target, source) file[:links] = :ignore assert_nil(state.describe(source), "Links were not ignored") file[:links] = :manage # We can't manage links at this point assert_raise(Puppet::FileServerError) do state.describe(source) end # And then make sure links get followed, otherwise file[:links] = :follow assert_equal("file", state.describe(source)[:type]) end def test_source_retrieve source = tempfile() dest = tempfile() file = Puppet::Type.newfile :path => dest, :source => source, :title => "copier" assert(file.state(:checksum), "source state did not create checksum state") state = file.state(:source) assert(state, "did not get source state") # Make sure the munge didn't actually change the source - assert_equal(source, state.should, "munging changed the source") + assert_equal([source], state.should, "munging changed the source") # First try it with a missing source assert_nothing_raised do state.retrieve end # And make sure the state considers itself in sync, since there's nothing # to do assert(state.insync?, "source thinks there's work to do with no file or dest") # Now make the dest a directory, and make sure the object sets :ensure up to # create a directory Dir.mkdir(source) assert_nothing_raised do state.retrieve end assert_equal(:directory, file.should(:ensure), "Did not set to create directory") # And make sure the source state won't try to do anything with a remote dir assert(state.insync?, "Source was out of sync even tho remote is dir") # Now remove the source, and make sure :ensure was not modified Dir.rmdir(source) assert_nothing_raised do state.retrieve end assert_equal(:directory, file.should(:ensure), "Did not keep :ensure setting") # Now have a remote file and make sure things work correctly File.open(source, "w") { |f| f.puts "yay" } File.chmod(0755, source) assert_nothing_raised do state.retrieve end assert_equal(:file, file.should(:ensure), "Did not make correct :ensure setting") assert_equal(0755, file.should(:mode), "Mode was not copied over") # Now let's make sure that we get the first found source fake = tempfile() state.should = [fake, source] assert_nothing_raised do state.retrieve end assert_equal(Digest::MD5.hexdigest(File.read(source)), state.checksum.sub(/^\{\w+\}/, ''), "Did not catch later source") end def test_insync source = tempfile() dest = tempfile() file = Puppet::Type.newfile :path => dest, :source => source, :title => "copier" state = file.state(:source) assert(state, "did not get source state") # Try it with no source at all file.retrieve assert(state.insync?, "source state not in sync with missing source") # with a directory Dir.mkdir(source) file.retrieve assert(state.insync?, "source state not in sync with directory as source") Dir.rmdir(source) # with a file File.open(source, "w") { |f| f.puts "yay" } file.retrieve assert(!state.insync?, "source state was in sync when file was missing") # With a different file File.open(dest, "w") { |f| f.puts "foo" } file.retrieve assert(!state.insync?, "source state was in sync with different file") # with matching files File.open(dest, "w") { |f| f.puts "yay" } file.retrieve assert(state.insync?, "source state was not in sync with matching file") end def test_source_sync source = tempfile() dest = tempfile() file = Puppet::Type.newfile :path => dest, :source => source, :title => "copier" state = file.state(:source) File.open(source, "w") { |f| f.puts "yay" } file.retrieve assert(! state.insync?, "source thinks it's in sync") event = nil assert_nothing_raised do event = state.sync end assert_equal(:file_created, event) assert_equal(File.read(source), File.read(dest), "File was not copied correctly") # Now write something different File.open(source, "w") { |f| f.puts "rah" } file.retrieve assert(! state.insync?, "source should be out of sync") assert_nothing_raised do event = state.sync end assert_equal(:file_changed, event) assert_equal(File.read(source), File.read(dest), "File was not copied correctly") end # XXX This test doesn't cover everything. Specifically, # it doesn't handle 'ignore' and 'links'. def test_sourcerecurse source, dest, sourcefile, destfile = mk_sourcetree # The sourcerecurse method will only ever get called when we're # recursing, so we go ahead and set it. obj = Puppet::Type.newfile :source => source, :path => dest, :recurse => true result = nil assert_nothing_raised do result = obj.sourcerecurse(true) end dfileobj = @file[destfile] assert(dfileobj, "Did not create destfile object") assert_equal([dfileobj], result) # Clean this up so it can be recreated dfileobj.remove # Make sure we correctly iterate over the sources nosource = tempfile() obj[:source] = [nosource, source] result = nil assert_nothing_raised do result = obj.sourcerecurse(true) end dfileobj = @file[destfile] assert(dfileobj, "Did not create destfile object with a missing source") assert_equal([dfileobj], result) dfileobj.remove # Lastly, make sure we return an empty array when no sources are there obj[:source] = [nosource, tempfile()] assert_nothing_raised do result = obj.sourcerecurse(true) end assert_equal([], result, "Sourcerecurse failed when all sources are missing") end def test_simplelocalsource path = tempfile() FileUtils.mkdir_p path frompath = File.join(path,"source") topath = File.join(path,"dest") fromfile = nil tofile = nil trans = nil File.open(frompath, File::WRONLY|File::CREAT|File::APPEND) { |of| of.puts "yayness" } assert_nothing_raised { tofile = Puppet.type(:file).create( :name => topath, :source => frompath ) } assert_apply(tofile) assert(FileTest.exists?(topath), "File #{topath} is missing") from = File.open(frompath) { |o| o.read } to = File.open(topath) { |o| o.read } assert_equal(from,to) end # Make sure a simple recursive copy works def test_simple_recursive_source source, dest, sourcefile, destfile = mk_sourcetree file = Puppet::Type.newfile :path => dest, :source => source, :recurse => true assert_events([:directory_created, :file_created], file) assert(FileTest.directory?(dest), "Dest dir was not created") assert(FileTest.file?(destfile), "dest file was not created") assert_equal("yay\n", File.read(destfile), "dest file was not copied correctly") end def recursive_source_test(fromdir, todir) Puppet::Type.allclear initstorage tofile = nil trans = nil assert_nothing_raised { tofile = Puppet.type(:file).create( :path => todir, :recurse => true, :backup => false, :source => fromdir ) } assert_apply(tofile) assert(FileTest.exists?(todir), "Created dir %s does not exist" % todir) Puppet::Type.allclear end def run_complex_sources(networked = false) path = tempfile() # first create the source directory FileUtils.mkdir_p path # okay, let's create a directory structure fromdir = File.join(path,"fromdir") Dir.mkdir(fromdir) FileUtils.cd(fromdir) { File.open("one", "w") { |f| f.puts "onefile"} File.open("two", "w") { |f| f.puts "twofile"} } todir = File.join(path, "todir") source = fromdir if networked source = "puppet://localhost/%s%s" % [networked, fromdir] end recursive_source_test(source, todir) return [fromdir,todir, File.join(todir, "one"), File.join(todir, "two")] end def test_complex_sources_twice fromdir, todir, one, two = run_complex_sources assert_trees_equal(fromdir,todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) # Now remove the whole tree and try it again. [one, two].each do |f| File.unlink(f) end Dir.rmdir(todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) end def test_sources_with_deleted_destfiles fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) # We shouldn't have a 'two' file object in memory assert_nil(@file[two], "object for 'two' is still in memory") # then delete a file File.unlink(two) - puts "yay" # and run recursive_source_test(fromdir, todir) assert(FileTest.exists?(two), "Deleted file was not recopied") # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_readonly_destfiles - fromdir, todir = run_complex_sources + fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) - readonly_random_files(todir) + File.chmod(0600, one) + recursive_source_test(fromdir, todir) + + # and make sure they're still equal + assert_trees_equal(fromdir,todir) + + # Now try it with the directory being read-only + File.chmod(0111, todir) recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_modified_dest_files - fromdir, todir = run_complex_sources + fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) - # then modify some files - modify_random_files(todir) + + # Modify a dest file + File.open(two, "w") { |f| f.puts "something else" } recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_added_destfiles fromdir, todir = run_complex_sources assert(FileTest.exists?(todir)) # and finally, add some new files add_random_files(todir) recursive_source_test(fromdir, todir) fromtree = file_list(fromdir) totree = file_list(todir) assert(fromtree != totree, "Trees are incorrectly equal") # then remove our new files FileUtils.cd(todir) { %x{find . 2>/dev/null}.chomp.split(/\n/).each { |file| if file =~ /file[0-9]+/ File.unlink(file) end } } # and make sure they're still equal assert_trees_equal(fromdir,todir) end + # Make sure added files get correctly caught during recursion def test_RecursionWithAddedFiles basedir = tempfile() Dir.mkdir(basedir) @@tmpfiles << basedir file1 = File.join(basedir, "file1") file2 = File.join(basedir, "file2") subdir1 = File.join(basedir, "subdir1") file3 = File.join(subdir1, "file") - File.open(file1, "w") { |f| 3.times { f.print rand(100) } } + File.open(file1, "w") { |f| f.puts "yay" } rootobj = nil assert_nothing_raised { rootobj = Puppet.type(:file).create( :name => basedir, :recurse => true, - :check => %w{type owner} + :check => %w{type owner}, + :mode => 0755 ) - - rootobj.evaluate } + + assert_apply(rootobj) + assert_equal(0755, filemode(file1)) - klass = Puppet.type(:file) - assert(klass[basedir]) - assert(klass[file1]) - assert_nil(klass[file2]) - - File.open(file2, "w") { |f| 3.times { f.print rand(100) } } - - assert_nothing_raised { - rootobj.evaluate - } - assert(klass[file2]) + File.open(file2, "w") { |f| f.puts "rah" } + assert_apply(rootobj) + assert_equal(0755, filemode(file2)) Dir.mkdir(subdir1) - File.open(file3, "w") { |f| 3.times { f.print rand(100) } } - - assert_nothing_raised { - rootobj.evaluate - } - assert(klass[file3]) + File.open(file3, "w") { |f| f.puts "foo" } + assert_apply(rootobj) + assert_equal(0755, filemode(file3)) end def mkfileserverconf(mounts) file = tempfile() File.open(file, "w") { |f| mounts.each { |path, name| f.puts "[#{name}]\n\tpath #{path}\n\tallow *\n" } } @@tmpfiles << file return file end def test_NetworkSources server = nil mounts = { "/" => "root" } fileserverconf = mkfileserverconf(mounts) Puppet[:autosign] = true Puppet[:masterport] = 8762 serverpid = nil assert_nothing_raised() { server = Puppet::Server.new( :Handlers => { :CA => {}, # so that certs autogenerate :FileServer => { :Config => fileserverconf } } ) } serverpid = fork { assert_nothing_raised() { #trap(:INT) { server.shutdown; Kernel.exit! } trap(:INT) { server.shutdown } server.start } } @@tmppids << serverpid sleep(1) fromdir, todir = run_complex_sources("root") assert_trees_equal(fromdir,todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) assert_nothing_raised { system("kill -INT %s" % serverpid) } end def test_networkSourcesWithoutService server = nil Puppet[:autosign] = true Puppet[:masterport] = 8765 serverpid = nil assert_nothing_raised() { server = Puppet::Server.new( :Handlers => { :CA => {}, # so that certs autogenerate } ) } serverpid = fork { assert_nothing_raised() { #trap(:INT) { server.shutdown; Kernel.exit! } trap(:INT) { server.shutdown } server.start } } @@tmppids << serverpid sleep(1) name = File.join(tmpdir(), "nosourcefile") file = Puppet.type(:file).create( :source => "puppet://localhost/dist/file", :name => name ) assert_nothing_raised { file.retrieve } comp = newcomp("nosource", file) assert_nothing_raised { comp.evaluate } assert(!FileTest.exists?(name), "File with no source exists anyway") end def test_unmountedNetworkSources server = nil mounts = { "/" => "root", "/noexistokay" => "noexist" } fileserverconf = mkfileserverconf(mounts) Puppet[:autosign] = true Puppet[:masterport] = @port serverpid = nil assert_nothing_raised() { server = Puppet::Server.new( :Port => @port, :Handlers => { :CA => {}, # so that certs autogenerate :FileServer => { :Config => fileserverconf } } ) } serverpid = fork { assert_nothing_raised() { #trap(:INT) { server.shutdown; Kernel.exit! } trap(:INT) { server.shutdown } server.start } } @@tmppids << serverpid sleep(1) name = File.join(tmpdir(), "nosourcefile") file = Puppet.type(:file).create( :source => "puppet://localhost/noexist/file", :name => name ) assert_nothing_raised { file.retrieve } comp = newcomp("nosource", file) assert_nothing_raised { comp.evaluate } assert(!FileTest.exists?(name), "File with no source exists anyway") end def test_alwayschecksum from = tempfile() to = tempfile() File.open(from, "w") { |f| f.puts "yayness" } File.open(to, "w") { |f| f.puts "yayness" } file = nil # Now the files should be exactly the same, so we should not see attempts # at copying assert_nothing_raised { file = Puppet.type(:file).create( :path => to, :source => from ) } file.retrieve assert(file.is(:checksum), "File does not have a checksum state") assert_equal(0, file.evaluate.length, "File produced changes") end def test_sourcepaths files = [] 3.times { files << tempfile() } to = tempfile() File.open(files[-1], "w") { |f| f.puts "yee-haw" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => to, :source => files ) } comp = newcomp(file) assert_events([:file_created], comp) assert(File.exists?(to), "File does not exist") txt = nil File.open(to) { |f| txt = f.read.chomp } assert_equal("yee-haw", txt, "Contents do not match") end # Make sure that source-copying updates the checksum on the same run def test_checksumchange source = tempfile() dest = tempfile() File.open(dest, "w") { |f| f.puts "boo" } File.open(source, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :source => source ) } file.retrieve assert_events([:file_changed], file) file.retrieve assert_events([], file) end # Make sure that source-copying updates the checksum on the same run def test_sourcebeatsensure source = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :ensure => "file", :source => source ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) assert_events([], file) end def test_sourcewithlinks source = tempfile() link = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "yay" } File.symlink(source, link) file = nil assert_nothing_raised { file = Puppet.type(:file).create( :name => dest, :source => link ) } # Default to skipping links assert_events([], file) assert(! FileTest.exists?(dest), "Created link") # Now follow the links file[:links] = :follow assert_events([:file_created], file) assert(FileTest.file?(dest), "Destination is not a file") # Now copy the links #assert_raise(Puppet::FileServerError) { trans = nil assert_nothing_raised { file[:links] = :manage comp = newcomp(file) trans = comp.evaluate trans.evaluate } assert(trans.failed?(file), "Object did not fail to copy links") end def test_changes source = tempfile() dest = tempfile() File.open(source, "w") { |f| f.puts "yay" } obj = nil assert_nothing_raised { obj = Puppet.type(:file).create( :name => dest, :source => source ) } assert_events([:file_created], obj) assert_equal(File.read(source), File.read(dest), "Files are not equal") assert_events([], obj) File.open(source, "w") { |f| f.puts "boo" } assert_events([:file_changed], obj) assert_equal(File.read(source), File.read(dest), "Files are not equal") assert_events([], obj) File.open(dest, "w") { |f| f.puts "kaboom" } # There are two changes, because first the checksum is noticed, and # then the source causes a change assert_events([:file_changed, :file_changed], obj) assert_equal(File.read(source), File.read(dest), "Files are not equal") assert_events([], obj) end def test_file_source_with_space dir = tempfile() source = File.join(dir, "file with spaces") Dir.mkdir(dir) File.open(source, "w") { |f| f.puts "yayness" } newdir = tempfile() newpath = File.join(newdir, "file with spaces") file = Puppet::Type.newfile( :path => newdir, :source => dir, :recurse => true ) assert_apply(file) assert(FileTest.exists?(newpath), "Did not create file") assert_equal("yayness\n", File.read(newpath)) end # Make sure files aren't replaced when replace is false, but otherwise # are. def test_replace source = tempfile() File.open(source, "w") { |f| f.puts "yayness" } dest = tempfile() file = Puppet::Type.newfile( :path => dest, :source => source, :recurse => true ) assert_apply(file) assert(FileTest.exists?(dest), "Did not create file") assert_equal("yayness\n", File.read(dest)) # Now set :replace assert_nothing_raised { file[:replace] = false } File.open(source, "w") { |f| f.puts "funtest" } assert_apply(file) # Make sure it doesn't change. - assert_equal("yayness\n", File.read(dest)) + assert_equal("yayness\n", File.read(dest), + "File got replaced when :replace was false") # Now set it to true and make sure it does change. assert_nothing_raised { file[:replace] = true } assert_apply(file) # Make sure it doesn't change. - assert_equal("funtest\n", File.read(dest)) + assert_equal("funtest\n", File.read(dest), + "File was not replaced when :replace was true") end # Testing #285. This just makes sure that URI parsing works correctly. def test_fileswithpoundsigns dir = tstdir() subdir = File.join(dir, "#dir") Dir.mkdir(subdir) file = File.join(subdir, "file") File.open(file, "w") { |f| f.puts "yayness" } dest = tempfile() source = "file://localhost#{dir}" obj = Puppet::Type.newfile( :path => dest, :source => source, :recurse => true ) newfile = File.join(dest, "#dir", "file") poundsource = "file://localhost#{subdir}" sourceobj = path = nil assert_nothing_raised { sourceobj, path = obj.uri2obj(poundsource) } assert_equal("/localhost" + URI.escape(subdir), path) assert_apply(obj) assert(FileTest.exists?(newfile), "File did not get created") assert_equal("yayness\n", File.read(newfile)) end end # $Id$ diff --git a/test/types/tidy.rb b/test/types/tidy.rb index 20694762f..513b05319 100755 --- a/test/types/tidy.rb +++ b/test/types/tidy.rb @@ -1,211 +1,211 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'puppet' require 'puppettest' class TestTidy < Test::Unit::TestCase include PuppetTest::FileTesting def mktmpfile # because luke's home directory is on nfs, it can't be used for testing # as root tmpfile = tempfile() File.open(tmpfile, "w") { |f| f.puts rand(100) } @@tmpfiles.push tmpfile return tmpfile end def mktmpdir dir = File.join(tmpdir(), "puppetlinkdir") unless FileTest.exists?(dir) Dir.mkdir(dir) end @@tmpfiles.push dir return dir end def test_tidydirs dir = mktmpdir - file = File.join(dir, "tidytesting") + file = File.join(dir, "file") File.open(file, "w") { |f| - f.puts rand(100) + f.puts "some stuff" } tidy = Puppet.type(:tidy).create( :name => dir, :size => "1b", :age => "1s", :rmdirs => true, :recurse => true ) sleep(2) assert_events([:file_tidied, :file_tidied], tidy) assert(!FileTest.exists?(file), "Tidied %s still exists" % file) assert(!FileTest.exists?(dir), "Tidied %s still exists" % dir) end def disabled_test_recursion source = mktmpdir() FileUtils.cd(source) { mkranddirsandfiles() } link = nil assert_nothing_raised { link = newlink(:target => source, :recurse => true) } comp = newcomp("linktest",link) cycle(comp) path = link.name list = file_list(path) FileUtils.cd(path) { list.each { |file| unless FileTest.directory?(file) assert(FileTest.symlink?(file)) target = File.readlink(file) assert_equal(target,File.join(source,file.sub(/^\.\//,''))) end } } end # Test the different age iterations. def test_age_conversions tidy = Puppet::Type.newtidy :path => tempfile(), :age => "1m" convertors = { :second => 1, :minute => 60 } convertors[:hour] = convertors[:minute] * 60 convertors[:day] = convertors[:hour] * 24 convertors[:week] = convertors[:day] * 7 # First make sure we default to days assert_nothing_raised do tidy[:age] = "2" end assert_equal(2 * convertors[:day], tidy[:age], "Converted 2 wrong") convertors.each do |name, number| init = name.to_s[0..0] # The first letter [0, 1, 5].each do |multi| [init, init.upcase].each do |letter| age = multi.to_s + letter.to_s assert_nothing_raised do tidy[:age] = age end assert_equal(multi * convertors[name], tidy[:age], "Converted %s wrong" % age) end end end end def test_size_conversions convertors = { :b => 0, :kb => 1, :mb => 2, :gb => 3 } tidy = Puppet::Type.newtidy :path => tempfile(), :age => "1m" # First make sure we default to kb assert_nothing_raised do tidy[:size] = "2" end assert_equal(2048, tidy[:size], "Converted 2 wrong") convertors.each do |name, number| init = name.to_s[0..0] # The first letter [0, 1, 5].each do |multi| [init, init.upcase].each do |letter| size = multi.to_s + letter.to_s assert_nothing_raised do tidy[:size] = size end total = multi number.times do total *= 1024 end assert_equal(total, tidy[:size], "Converted %s wrong" % size) end end end end def test_agetest tidy = Puppet::Type.newtidy :path => tempfile(), :age => "1m" state = tidy.state(:tidyup) # Set it to something that should be fine state.is = [Time.now.to_i - 5, 50] assert(state.insync?, "Tried to tidy a low age") # Now to something that should fail state.is = [Time.now.to_i - 120, 50] assert(! state.insync?, "Incorrectly skipped tidy") end def test_sizetest tidy = Puppet::Type.newtidy :path => tempfile(), :size => "1k" state = tidy.state(:tidyup) # Set it to something that should be fine state.is = [5, 50] assert(state.insync?, "Tried to tidy a low size") # Now to something that should fail state.is = [120, 2048] assert(! state.insync?, "Incorrectly skipped tidy") end # Make sure we can remove different types of files def test_tidytypes path = tempfile() tidy = Puppet::Type.newtidy :path => path, :size => "1b", :age => "1s" # Start with a file File.open(path, "w") { |f| f.puts "this is a test" } assert_events([:file_tidied], tidy) assert(! FileTest.exists?(path), "File was not removed") # Then a link dest = tempfile File.open(dest, "w") { |f| f.puts "this is a test" } File.symlink(dest, path) assert_events([:file_tidied], tidy) assert(! FileTest.exists?(path), "Link was not removed") assert(FileTest.exists?(dest), "Destination was removed") # And a directory Dir.mkdir(path) tidy.is = [:tidyup, [Time.now - 1024, 1]] tidy[:rmdirs] = true assert_events([:file_tidied], tidy) assert(! FileTest.exists?(path), "File was not removed") end end # $Id$ diff --git a/test/types/user.rb b/test/types/user.rb index a3a2e14f0..958434fa5 100755 --- a/test/types/user.rb +++ b/test/types/user.rb @@ -1,449 +1,451 @@ #!/usr/bin/env ruby $:.unshift("../lib").unshift("../../lib") if __FILE__ =~ /\.rb$/ require 'etc' require 'puppet/type' require 'puppettest' class TestUser < Test::Unit::TestCase include PuppetTest p = Puppet::Type.type(:user).provide :fake, :parent => PuppetTest::FakeProvider do @name = :fake apimethods def create @ensure = :present @model.eachstate do |state| next if state.name == :ensure state.sync end end def delete @ensure = :absent @model.eachstate do |state| send(state.name.to_s + "=", :absent) end end def exists? if defined? @ensure and @ensure == :present true else false end end end FakeUserProvider = p @@fakeproviders[:group] = p def findshell(old = nil) %w{/bin/sh /bin/bash /sbin/sh /bin/ksh /bin/zsh /bin/csh /bin/tcsh /usr/bin/sh /usr/bin/bash /usr/bin/ksh /usr/bin/zsh /usr/bin/csh /usr/bin/tcsh}.find { |shell| if old FileTest.exists?(shell) and shell != old else FileTest.exists?(shell) end } end def setup super Puppet::Type.type(:user).defaultprovider = FakeUserProvider end def teardown Puppet::Type.type(:user).defaultprovider = nil super end def mkuser(name) user = nil assert_nothing_raised { user = Puppet.type(:user).create( :name => name, :comment => "Puppet Testing User", :gid => Puppet::SUIDManager.gid, :shell => findshell(), :home => "/home/%s" % name ) } assert(user, "Did not create user") return user end def attrtest_ensure(user) old = user.provider.ensure user[:ensure] = :absent comp = newcomp("ensuretest", user) assert_apply(user) assert(!user.provider.exists?, "User is still present") user[:ensure] = :present assert_events([:user_created], comp) assert(user.provider.exists?, "User is absent") user[:ensure] = :absent trans = assert_events([:user_removed], comp) assert_rollback_events(trans, [:user_created], "user") user[:ensure] = old assert_apply(user) end def attrtest_comment(user) user.retrieve old = user.provider.comment user[:comment] = "A different comment" comp = newcomp("commenttest", user) trans = assert_events([:user_changed], comp, "user") assert_equal("A different comment", user.provider.comment, "Comment was not changed") assert_rollback_events(trans, [:user_changed], "user") assert_equal(old, user.provider.comment, "Comment was not reverted") end def attrtest_home(user) obj = nil comp = newcomp("hometest", user) old = user.provider.home user[:home] = old trans = assert_events([], comp, "user") user[:home] = "/tmp" trans = assert_events([:user_changed], comp, "user") assert_equal("/tmp", user.provider.home, "Home was not changed") assert_rollback_events(trans, [:user_changed], "user") assert_equal(old, user.provider.home, "Home was not reverted") end def attrtest_shell(user) old = user.provider.shell comp = newcomp("shelltest", user) user[:shell] = old trans = assert_events([], comp, "user") newshell = findshell(old) unless newshell $stderr.puts "Cannot find alternate shell; skipping shell test" return end user[:shell] = newshell trans = assert_events([:user_changed], comp, "user") user.retrieve assert_equal(newshell, user.provider.shell, "Shell was not changed") assert_rollback_events(trans, [:user_changed], "user") user.retrieve assert_equal(old, user.provider.shell, "Shell was not reverted") end def attrtest_gid(user) obj = nil old = user.provider.gid comp = newcomp("gidtest", user) user.retrieve user[:gid] = old trans = assert_events([], comp, "user") newgid = %w{nogroup nobody staff users daemon}.find { |gid| begin group = Etc.getgrnam(gid) rescue ArgumentError => detail next end old != group.gid } unless newgid $stderr.puts "Cannot find alternate group; skipping gid test" return end # first test by name assert_nothing_raised("Failed to specify group by name") { user[:gid] = newgid } trans = assert_events([:user_changed], comp, "user") # then by id newgid = Etc.getgrnam(newgid).gid assert_nothing_raised("Failed to specify group by id") { user[:gid] = newgid } user.retrieve assert_events([], comp, "user") assert_equal(newgid, user.provider.gid, "GID was not changed") assert_rollback_events(trans, [:user_changed], "user") assert_equal(old, user.provider.gid, "GID was not reverted") end def attrtest_uid(user) obj = nil comp = newcomp("uidtest", user) user.provider.uid = 1 old = 1 newuid = 1 while true newuid += 1 if newuid - old > 1000 $stderr.puts "Could not find extra test UID" return end begin newuser = Etc.getpwuid(newuid) rescue ArgumentError => detail break end end assert_nothing_raised("Failed to change user id") { user[:uid] = newuid } trans = assert_events([:user_changed], comp, "user") assert_equal(newuid, user.provider.uid, "UID was not changed") assert_rollback_events(trans, [:user_changed], "user") assert_equal(old, user.provider.uid, "UID was not reverted") end def attrtest_groups(user) Etc.setgrent max = 0 while group = Etc.getgrent if group.gid > max and group.gid < 5000 max = group.gid end end groups = [] main = [] extra = [] 5.times do |i| i += 1 name = "pptstgr%s" % i groups << name if i < 3 main << name else extra << name end end assert(user[:membership] == :minimum, "Membership did not default correctly") assert_nothing_raised { user.retrieve } # Now add some of them to our user assert_nothing_raised { user[:groups] = extra } assert_nothing_raised { user.retrieve } assert_instance_of(String, user.state(:groups).should) # Some tests to verify that groups work correctly startig from nothing # Remove our user user[:ensure] = :absent assert_apply(user) assert_nothing_raised do user.retrieve end # And add it again user[:ensure] = :present assert_apply(user) # Make sure that the groups are a string, not an array assert(user.provider.groups.is_a?(String), "Incorrectly passed an array to groups") user.retrieve assert(user.state(:groups).is, "Did not retrieve group list") list = user.state(:groups).is assert_equal(extra.sort, list.sort, "Group list is not equal") # Now set to our main list of groups assert_nothing_raised { user[:groups] = main } assert_equal((main + extra).sort, user.state(:groups).should.split(",").sort) assert_nothing_raised { user.retrieve } assert(!user.insync?, "User is incorrectly in sync") assert_apply(user) assert_nothing_raised { user.retrieve } # We're not managing inclusively, so it should keep the old group # memberships and add the new ones list = user.state(:groups).is assert_equal((main + extra).sort, list.sort, "Group list is not equal") assert_nothing_raised { user[:membership] = :inclusive } assert_nothing_raised { user.retrieve } assert(!user.insync?, "User is incorrectly in sync") assert_events([:user_changed], user) assert_nothing_raised { user.retrieve } list = user.state(:groups).is assert_equal(main.sort, list.sort, "Group list is not equal") # Set the values a bit differently. user.state(:groups).should = list.sort { |a,b| b <=> a } user.state(:groups).is = list.sort assert(user.state(:groups).insync?, "Groups state did not sort groups") user.delete(:groups) end def test_autorequire file = tempfile() comp = nil user = nil group =nil home = nil ogroup = nil assert_nothing_raised { user = Puppet.type(:user).create( :name => "pptestu", :home => file, :gid => "pptestg", :groups => "yayness" ) home = Puppet.type(:file).create( :path => file, :owner => "pptestu", :ensure => "directory" ) group = Puppet.type(:group).create( :name => "pptestg" ) ogroup = Puppet.type(:group).create( :name => "yayness" ) comp = newcomp(user, group, home, ogroup) } - comp.finalize - comp.retrieve - - assert(user.requires?(group), "User did not require group") - assert(home.requires?(user), "Homedir did not require user") - assert(user.requires?(ogroup), "User did not require other groups") + + rels = nil + assert_nothing_raised() { rels = user.autorequire } + + assert(rels.detect { |r| r.source == group }, "User did not require group") + assert(rels.detect { |r| r.source == ogroup }, "User did not require other groups") + assert_nothing_raised() { rels = home.autorequire } + assert(rels.detect { |r| r.source == user }, "Homedir did not require user") end def test_simpleuser name = "pptest" user = mkuser(name) comp = newcomp("usercomp", user) trans = assert_events([:user_created], comp, "user") assert_equal(user.should(:comment), user.provider.comment, "Comment was not set correctly") assert_rollback_events(trans, [:user_removed], "user") assert(! user.provider.exists?, "User did not get deleted") end def test_allusermodelstates user = nil name = "pptest" user = mkuser(name) assert(! user.provider.exists?, "User %s is present" % name) comp = newcomp("usercomp", user) trans = assert_events([:user_created], comp, "user") user.retrieve assert_equal("Puppet Testing User", user.provider.comment, "Comment was not set") tests = Puppet.type(:user).validstates tests.each { |test| if self.respond_to?("attrtest_%s" % test) self.send("attrtest_%s" % test, user) else Puppet.err "Not testing attr %s of user" % test end } user[:ensure] = :absent assert_apply(user) end end # $Id$