diff --git a/lib/puppet/transaction/change.rb b/lib/puppet/transaction/change.rb index 6ecb93c37..605457a21 100644 --- a/lib/puppet/transaction/change.rb +++ b/lib/puppet/transaction/change.rb @@ -1,73 +1,87 @@ require 'puppet/transaction' require 'puppet/transaction/event' # Handle all of the work around performing an actual change, # including calling 'sync' on the properties and producing events. class Puppet::Transaction::Change - attr_accessor :is, :should, :property, :proxy + attr_accessor :is, :should, :property, :proxy, :auditing + + def auditing? + auditing + end # Create our event object. def event result = property.event result.previous_value = is result.desired_value = should result end def initialize(property, currentvalue) @property = property @is = currentvalue @should = property.should @changed = false end def apply + return audit_event if auditing? return noop_event if noop? property.sync result = event() result.message = property.change_to_s(is, should) result.status = "success" result.send_log result rescue => detail puts detail.backtrace if Puppet[:trace] result = event() result.status = "failure" result.message = "change from #{property.is_to_s(is)} to #{property.should_to_s(should)} failed: #{detail}" result.send_log result end # Is our property noop? This is used for generating special events. def noop? return @property.noop end # The resource that generated this change. This is used for handling events, # and the proxy resource is used for generated resources, since we can't # send an event to a resource we don't have a direct relationship with. If we # have a proxy resource, then the events will be considered to be from # that resource, rather than us, so the graph resolution will still work. def resource self.proxy || @property.resource end def to_s return "change %s" % @property.change_to_s(@is, @should) end private + def audit_event + # This needs to store the appropriate value, and then produce a new event + result = event + result.message = "audit change: previously recorded value #{property.should_to_s(should)} has been changed to #{property.is_to_s(is)}" + result.status = "audit" + result.send_log + return result + end + def noop_event result = event result.message = "is #{property.is_to_s(is)}, should be #{property.should_to_s(should)} (noop)" result.status = "noop" result.send_log return result end end diff --git a/lib/puppet/transaction/event.rb b/lib/puppet/transaction/event.rb index b962149cf..bc589fe84 100644 --- a/lib/puppet/transaction/event.rb +++ b/lib/puppet/transaction/event.rb @@ -1,61 +1,61 @@ require 'puppet/transaction' require 'puppet/util/tagging' require 'puppet/util/logging' # A simple struct for storing what happens on the system. class Puppet::Transaction::Event include Puppet::Util::Tagging include Puppet::Util::Logging ATTRIBUTES = [:name, :resource, :property, :previous_value, :desired_value, :status, :message, :node, :version, :file, :line, :source_description] attr_accessor *ATTRIBUTES attr_writer :tags attr_accessor :time attr_reader :default_log_level - EVENT_STATUSES = %w{noop success failure} + EVENT_STATUSES = %w{noop success failure audit} def initialize(*args) options = args.last.is_a?(Hash) ? args.pop : ATTRIBUTES.inject({}) { |hash, attr| hash[attr] = args.pop; hash } options.each { |attr, value| send(attr.to_s + "=", value) unless value.nil? } @time = Time.now end def property=(prop) @property = prop.to_s end def resource=(res) if res.respond_to?(:[]) and level = res[:loglevel] @default_log_level = level end @resource = res.to_s end def send_log super(log_level, message) end def status=(value) raise ArgumentError, "Event status can only be #{EVENT_STATUSES.join(', ')}" unless EVENT_STATUSES.include?(value) @status = value end def to_s message end private # If it's a failure, use 'err', else use either the resource's log level (if available) # or 'notice'. def log_level status == "failure" ? :err : (@default_log_level || :notice) end # Used by the Logging module def log_source source_description || property || resource end end diff --git a/lib/puppet/transaction/resource_harness.rb b/lib/puppet/transaction/resource_harness.rb index 17e8dfa79..ae38bcb66 100644 --- a/lib/puppet/transaction/resource_harness.rb +++ b/lib/puppet/transaction/resource_harness.rb @@ -1,125 +1,152 @@ require 'puppet/resource/status' class Puppet::Transaction::ResourceHarness extend Forwardable def_delegators :@transaction, :relationship_graph attr_reader :transaction def allow_changes?(resource) return true unless resource.purging? and resource.deleting? return true unless deps = relationship_graph.dependents(resource) and ! deps.empty? and deps.detect { |d| ! d.deleting? } deplabel = deps.collect { |r| r.ref }.join(",") plurality = deps.length > 1 ? "":"s" resource.warning "#{deplabel} still depend#{plurality} on me -- not purging" return false end def apply_changes(status, changes) changes.each do |change| status << change.apply + + if change.auditing? + cache(change.property.resource, change.property.name, change.is) + end end status.changed = true end # Used mostly for scheduling at this point. def cached(resource, name) Puppet::Util::Storage.cache(resource)[name] end # Used mostly for scheduling at this point. def cache(resource, name, value) Puppet::Util::Storage.cache(resource)[name] = value end def changes_to_perform(status, resource) current = resource.retrieve cache resource, :checked, Time.now return [] if ! allow_changes?(resource) + audited = copy_audited_parameters(resource, current) + if param = resource.parameter(:ensure) return [] if absent_and_not_being_created?(current, param) return [Puppet::Transaction::Change.new(param, current[:ensure])] unless ensure_is_insync?(current, param) return [] if ensure_should_be_absent?(current, param) end resource.properties.reject { |p| p.name == :ensure }.reject do |param| - param.should.nil? + param.should.nil? end.reject do |param| param_is_insync?(current, param) end.collect do |param| - Puppet::Transaction::Change.new(param, current[param.name]) + change = Puppet::Transaction::Change.new(param, current[param.name]) + change.auditing = true if audited.include?(param.name) + change + end + end + + def copy_audited_parameters(resource, current) + return [] unless audit = resource[:audit] + audit = Array(audit).collect { |p| p.to_sym } + audited = [] + audit.find_all do |param| + next if resource[param] + + if value = cached(resource, param) + resource[param] = value + audited << param + else + resource.notice "Storing newly-audited value #{current[param]} for #{param}" + cache(resource, param, current[param]) + end end + + audited end def evaluate(resource) start = Time.now status = Puppet::Resource::Status.new(resource) if changes = changes_to_perform(status, resource) and ! changes.empty? status.out_of_sync = true status.change_count = changes.length apply_changes(status, changes) if ! resource.noop? cache(resource, :synced, Time.now) resource.flush if resource.respond_to?(:flush) end end return status rescue => detail resource.fail "Could not create resource status: #{detail}" unless status puts detail.backtrace if Puppet[:trace] resource.err "Could not evaluate: #{detail}" status.failed = true return status ensure (status.evaluation_time = Time.now - start) if status end def initialize(transaction) @transaction = transaction end def scheduled?(status, resource) return true if Puppet[:ignoreschedules] return true unless schedule = schedule(resource) # We use 'checked' here instead of 'synced' because otherwise we'll # end up checking most resources most times, because they will generally # have been synced a long time ago (e.g., a file only gets updated # once a month on the server and its schedule is daily; the last sync time # will have been a month ago, so we'd end up checking every run). return schedule.match?(cached(resource, :checked).to_i) end def schedule(resource) unless resource.catalog resource.warning "Cannot schedule without a schedule-containing catalog" return nil end return nil unless name = resource[:schedule] resource.catalog.resource(:schedule, name) || resource.fail("Could not find schedule #{name}") end private def absent_and_not_being_created?(current, param) current[:ensure] == :absent and param.should.nil? end def ensure_is_insync?(current, param) param.insync?(current[:ensure]) end def ensure_should_be_absent?(current, param) param.should == :absent end def param_is_insync?(current, param) param.insync?(current[param.name]) end end diff --git a/lib/puppet/type.rb b/lib/puppet/type.rb index fc6161735..17d6b2a10 100644 --- a/lib/puppet/type.rb +++ b/lib/puppet/type.rb @@ -1,1982 +1,1982 @@ require 'puppet' require 'puppet/util/log' require 'puppet/util/metric' require 'puppet/property' require 'puppet/parameter' require 'puppet/util' require 'puppet/util/autoload' require 'puppet/metatype/manager' require 'puppet/util/errors' require 'puppet/util/log_paths' require 'puppet/util/logging' require 'puppet/util/cacher' require 'puppet/file_collection/lookup' require 'puppet/util/tagging' # see the bottom of the file for the rest of the inclusions module Puppet class Type include Puppet::Util include Puppet::Util::Errors include Puppet::Util::LogPaths include Puppet::Util::Logging include Puppet::Util::Cacher include Puppet::FileCollection::Lookup include Puppet::Util::Tagging ############################### # Code related to resource type attributes. class << self include Puppet::Util::ClassGen include Puppet::Util::Warnings attr_reader :properties end def self.states warnonce "The states method is deprecated; use properties" properties() end # All parameters, in the appropriate order. The key_attributes come first, then # the provider, then the properties, and finally the params and metaparams # in the order they were specified in the files. def self.allattrs key_attributes | (parameters & [:provider]) | properties.collect { |property| property.name } | parameters | metaparams end # Retrieve an attribute alias, if there is one. def self.attr_alias(param) @attr_aliases[symbolize(param)] end # Create an alias to an existing attribute. This will cause the aliased # attribute to be valid when setting and retrieving values on the instance. def self.set_attr_alias(hash) hash.each do |new, old| @attr_aliases[symbolize(new)] = symbolize(old) end 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 :property; @validproperties[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 @validproperties.include?(attr); :property when @paramhash.include?(attr); :param when @@metaparamhash.include?(attr); :meta end end @attrtypes[attr] 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.newproperty(:ensure, :parent => Puppet::Property::Ensure, &block) else self.newproperty(:ensure, :parent => Puppet::Property::Ensure) do self.defaultvalues end end end # Should we add the 'ensure' property to this class? def self.ensurable? # If the class has all three of these methods defined, then it's # ensurable. ens = [:exists?, :create, :destroy].inject { |set, method| set &&= self.public_method_defined?(method) } 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 end # Is the parameter in question a meta-parameter? def self.metaparam?(param) @@metaparamhash.include?(symbolize(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, options = {}, &block) @@metaparams ||= [] @@metaparamhash ||= {} name = symbolize(name) param = genclass(name, :parent => options[:parent] || Puppet::Parameter, :prefix => "MetaParam", :hash => @@metaparamhash, :array => @@metaparams, :attributes => options[:attributes], &block ) # Grr. if options[:required_features] param.required_features = options[:required_features] end handle_param_options(name, options) param.metaparam = true return param end def self.key_attribute_parameters @key_attribute_parameters ||= ( params = @parameters.find_all { |param| param.isnamevar? or param.name == :name } ) end def self.key_attributes key_attribute_parameters.collect { |p| p.name } end def self.title_patterns case key_attributes.length when 0; [] when 1; identity = lambda {|x| x} [ [ /(.*)/, [ [key_attributes.first, identity ] ] ] ] else raise Puppet::DevError,"you must specify title patterns when there are two or more key attributes" end end def uniqueness_key to_resource.uniqueness_key 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] ||= {} param = genclass(name, :parent => options[:parent] || Puppet::Parameter, :attributes => options[:attributes], :block => block, :prefix => "Parameter", :array => @parameters, :hash => @paramhash ) handle_param_options(name, options) # Grr. if options[:required_features] param.required_features = options[:required_features] end param.isnamevar if options[:namevar] return param end def self.newstate(name, options = {}, &block) Puppet.warning "newstate() has been deprecrated; use newproperty(%s)" % name newproperty(name, options, &block) end # Create a new property. The first parameter must be the name of the property; # this is how users will refer to the property when creating new instances. # The second parameter is a hash of options; the options are: # * :parent: The parent class for the property. Defaults to Puppet::Property. # * :retrieve: The method to call on the provider or @parent object (if # the provider is not set) to retrieve the current value. def self.newproperty(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 @validproperties.include?(name) raise Puppet::DevError, "Class %s already has a property named %s" % [self.name, name] end if parent = options[:parent] options.delete(:parent) else parent = Puppet::Property 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. prop = genclass(name, :parent => parent, :hash => @validproperties, :attributes => options) do # If they've passed a retrieve method, then override the retrieve # method on the class. if options[:retrieve] define_method(:retrieve) do provider.send(options[:retrieve]) end end if block class_eval(&block) end end # If it's the 'ensure' property, always put it first. if name == :ensure @properties.unshift prop else @properties << prop end return prop 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 property class associated with a name def self.propertybyname(name) @validproperties[name] end def self.validattr?(name) name = symbolize(name) return true if name == :name @validattrs ||= {} unless @validattrs.include?(name) if self.validproperty?(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 property? def self.validproperty?(name) name = symbolize(name) if @validproperties.include?(name) return @validproperties[name] else return false end end # Return the list of validproperties def self.validproperties return {} unless defined? @parameters return @validproperties.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 # This is a forward-compatibility method - it's the validity interface we'll use in Puppet::Resource. def self.valid_parameter?(name) validattr?(name) end # Return either the attribute alias or the attribute. def attr_alias(name) name = symbolize(name) if synonym = self.class.attr_alias(name) return synonym else return name end end # Are we deleting this resource? def deleting? obj = @parameters[:ensure] and obj.should == :absent end # Create a new property if it is valid but doesn't exist # Returns: true if a new parameter was added, false otherwise def add_property_parameter(prop_name) if self.class.validproperty?(prop_name) && !@parameters[prop_name] self.newattr(prop_name) return true end return false end # # The name_var is the key_attribute in the case that there is only one. # def name_var key_attributes = self.class.key_attributes (key_attributes.length == 1) && key_attributes.first end # abstract accessing parameters and properties, 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(:property)' or 'object.should(:property)'. def [](name) name = attr_alias(name) unless self.class.validattr?(name) fail("Invalid parameter %s(%s)" % [name, name.inspect]) end if name == :name name = name_var end if obj = @parameters[name] # Note that if this is a property, then the value is the "should" value, # not the current value. obj.value else return nil end end # Abstract setting parameters and properties, and normalize # access to always be symbols, not strings. This sets the 'should' # value on properties, and otherwise just sets the appropriate parameter. def []=(name,value) name = attr_alias(name) unless self.class.validattr?(name) fail("Invalid parameter %s" % [name]) end if name == :name name = name_var end if value.nil? raise Puppet::Error.new("Got nil value for %s" % name) end if obj = @parameters[name] obj.value = value return nil else self.newattr(name, :value => value) end nil end # remove a property from the object; useful in testing or in cleanup # when an error has been encountered def delete(attr) attr = symbolize(attr) if @parameters.has_key?(attr) @parameters.delete(attr) else raise Puppet::DevError.new("Undefined attribute '#{attr}' in #{self}") end end # iterate across the existing properties def eachproperty # properties() is a private method properties().each { |property| yield property } end # Create a transaction event. Called by Transaction or by # a property. def event(options = {}) Puppet::Transaction::Event.new({:resource => self, :file => file, :line => line, :tags => tags, :version => version}.merge(options)) end # Let the catalog determine whether a given cached value is # still valid or has expired. def expirer catalog end # retrieve the 'should' value for a specified property def should(name) name = attr_alias(name) if prop = @parameters[name] and prop.is_a?(Puppet::Property) return prop.should else return nil end end # Create the actual attribute instance. Requires either the attribute # name or class as the first argument, then an optional hash of # attributes to set during initialization. def newattr(name, options = {}) if name.is_a?(Class) klass = name name = klass.name end unless klass = self.class.attrclass(name) raise Puppet::Error, "Resource type %s does not support parameter %s" % [self.class.name, name] end if @parameters.include?(name) raise Puppet::Error, "Parameter '%s' is already defined in %s" % [name, self.ref] end if provider and ! provider.class.supports_parameter?(klass) missing = klass.required_features.find_all { |f| ! provider.class.feature?(f) } info "Provider %s does not support features %s; not managing attribute %s" % [provider.class.name, missing.join(", "), name] return nil end # Add resource information at creation time, so it's available # during validation. options[:resource] = self begin # make sure the parameter doesn't have any errors return @parameters[name] = klass.new(options) rescue => detail error = Puppet::Error.new("Parameter %s failed: %s" % [name, detail]) error.set_backtrace(detail.backtrace) raise error end end # return the value of a parameter def parameter(name) @parameters[name.to_sym] end def parameters @parameters.dup end # Is the named property defined? def propertydefined?(name) unless name.is_a? Symbol name = name.intern end return @parameters.include?(name) end # Return an actual property instance by name; to return the value, use 'resource[param]' # LAK:NOTE(20081028) Since the 'parameter' method is now a superset of this method, # this one should probably go away at some point. def property(name) if obj = @parameters[symbolize(name)] and obj.is_a?(Puppet::Property) return obj else return nil end end # For any parameters or properties that have defaults and have not yet been # set, set them now. This method can be handed a list of attributes, # and if so it will only set defaults for those attributes. def set_default(attr) return unless klass = self.class.attrclass(attr) return unless klass.method_defined?(:default) return if @parameters.include?(klass.name) return unless parameter = newattr(klass.name) if value = parameter.default and ! value.nil? parameter.value = value else @parameters.delete(parameter.name) end end # Convert our object to a hash. This just includes properties. def to_hash rethash = {} @parameters.each do |name, obj| rethash[name] = obj.value end rethash end def type self.class.name end # Return a specific value for an attribute. def value(name) name = attr_alias(name) if obj = @parameters[name] and obj.respond_to?(:value) return obj.value else return nil end end def version return 0 unless catalog catalog.version end # Return all of the property objects, in the order specified in the # class. def properties self.class.properties.collect { |prop| @parameters[prop.name] }.compact end # Is this type's name isomorphic with the object? That is, if the # name conflicts, does it necessarily mean that the objects conflict? # Defaults to true. def self.isomorphic? if defined? @isomorphic return @isomorphic else return true end end def isomorphic? self.class.isomorphic? end # is the instance a managed instance? A 'yes' here means that # the instance was created from the language, vs. being created # in order resolve other questions, such as finding a package # in a list def managed? # Once an object is managed, it always stays managed; but an object # that is listed as unmanaged might become managed later in the process, # so we have to check that every time if defined? @managed and @managed return @managed else @managed = false properties.each { |property| s = property.should if s and ! property.class.unmanaged @managed = true break end } return @managed end end ############################### # Code related to the container behaviour. # 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 # Remove an object. The argument determines whether the object's # subscriptions get eliminated, too. def remove(rmdeps = true) # This is hackish (mmm, cut and paste), but it works for now, and it's # better than warnings. @parameters.each do |name, obj| obj.remove end @parameters.clear @parent = nil # Remove the reference to the provider. if self.provider @provider.clear @provider = nil end end ############################### # Code related to evaluating the resources. # Flush the provider, if it supports it. This is called by the # transaction. def flush if self.provider and self.provider.respond_to?(:flush) self.provider.flush end end # if all contained objects are in sync, then we're in sync # FIXME I don't think this is used on the type instances any more, # it's really only used for testing def insync?(is) insync = true if property = @parameters[:ensure] unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '%s'" % [property.name] end ensureis = is[property] if property.insync?(ensureis) and property.should == :absent return true end end properties.each { |property| unless is.include? property raise Puppet::DevError, "The is value is not in the is array for '%s'" % [property.name] end propis = is[property] unless property.insync?(propis) property.debug("Not in sync: %s vs %s" % [propis.inspect, property.should.inspect]) insync = false #else # property.debug("In sync") end } #self.debug("%s sync status is %s" % [self,insync]) return insync end # retrieve the current value of all contained properties def retrieve if self.provider.is_a?(Puppet::Provider) and ! provider.class.suitable? fail "Provider #{provider.class.name} is not functional on this host" end result = Puppet::Resource.new(type, title) # Provide the name, so we know we'll always refer to a real thing result[:name] = self[:name] unless self[:name] == title if ensure_prop = property(:ensure) or (self.class.validattr?(:ensure) and ensure_prop = newattr(:ensure)) result[:ensure] = ensure_state = ensure_prop.retrieve else ensure_state = nil end properties.each do |property| next if property.name == :ensure if ensure_state == :absent result[property] = :absent else result[property] = property.retrieve end end result end # Get a hash of the current properties. Returns a hash with # the actual property instance as the key and the current value # as the, um, value. def currentpropvalues # It's important to use the 'properties' method here, as it follows the order # in which they're defined in the class. It also guarantees that 'ensure' # is the first property, which is important for skipping 'retrieve' on # all the properties if the resource is absent. ensure_state = false return properties().inject({}) do | prophash, property| if property.name == :ensure ensure_state = property.retrieve prophash[property] = ensure_state else if ensure_state == :absent prophash[property] = :absent else prophash[property] = property.retrieve end end prophash end end # Are we running in noop mode? def noop? if defined?(@noop) @noop else Puppet[:noop] end end def noop noop? end ############################### # Code related to managing resource instances. require 'puppet/transportable' # retrieve a named instance of the current type def self.[](name) raise "Global resource access is deprecated" @objects[name] || @aliases[name] end # add an instance by name to the class list of instances def self.[]=(name,object) raise "Global resource storage is deprecated" newobj = nil if object.is_a?(Puppet::Type) newobj = object else raise Puppet::DevError, "must pass a Puppet::Type object" end if exobj = @objects[name] and self.isomorphic? msg = "Object '%s[%s]' already exists" % [newobj.class.name, name] if exobj.file and exobj.line msg += ("in file %s at line %s" % [object.file, object.line]) end if object.file and object.line msg += ("and cannot be redefined in file %s at line %s" % [object.file, object.line]) end error = Puppet::Error.new(msg) raise error else #Puppet.info("adding %s of type %s to class list" % # [name,object.class]) @objects[name] = newobj end end # Create an alias. We keep these in a separate hash so that we don't encounter # the objects multiple times when iterating over them. def self.alias(name, obj) raise "Global resource aliasing is deprecated" if @objects.include?(name) unless @objects[name] == obj raise Puppet::Error.new( "Cannot create alias %s: object already exists" % [name] ) end end if @aliases.include?(name) unless @aliases[name] == obj raise Puppet::Error.new( "Object %s already has alias %s" % [@aliases[name].name, name] ) end end @aliases[name] = obj end # remove all of the instances of a single type def self.clear raise "Global resource removal is deprecated" if defined? @objects @objects.each do |name, obj| obj.remove(true) end @objects.clear end if defined? @aliases @aliases.clear end end # Force users to call this, so that we can merge objects if # necessary. def self.create(args) # LAK:DEP Deprecation notice added 12/17/2008 Puppet.warning "Puppet::Type.create is deprecated; use Puppet::Type.new" new(args) end # remove a specified object def self.delete(resource) raise "Global resource removal is deprecated" return unless defined? @objects if @objects.include?(resource.title) @objects.delete(resource.title) end if @aliases.include?(resource.title) @aliases.delete(resource.title) end if @aliases.has_value?(resource) names = [] @aliases.each do |name, otherres| if otherres == resource names << name end end names.each { |name| @aliases.delete(name) } end end # iterate across each of the type's instances def self.each raise "Global resource iteration is deprecated" return unless defined? @objects @objects.each { |name,instance| yield instance } end # does the type have an object with the given name? def self.has_key?(name) raise "Global resource access is deprecated" return @objects.has_key?(name) end # Retrieve all known instances. Either requires providers or must be overridden. def self.instances if provider_hash.empty? raise Puppet::DevError, "%s has no providers and has not overridden 'instances'" % self.name end # Put the default provider first, then the rest of the suitable providers. provider_instances = {} providers_by_source.collect do |provider| provider.instances.collect do |instance| # We always want to use the "first" provider instance we find, unless the resource # is already managed and has a different provider set if other = provider_instances[instance.name] Puppet.warning "%s %s found in both %s and %s; skipping the %s version" % [self.name.to_s.capitalize, instance.name, other.class.name, instance.class.name, instance.class.name] next end provider_instances[instance.name] = instance new(:name => instance.name, :provider => instance, :check => :all) end end.flatten.compact end # Return a list of one suitable provider per source, with the default provider first. def self.providers_by_source # Put the default provider first, then the rest of the suitable providers. sources = [] [defaultprovider, suitableprovider].flatten.uniq.collect do |provider| next if sources.include?(provider.source) sources << provider.source provider end.compact end # Convert a simple hash into a Resource instance. def self.hash2resource(hash) hash = hash.inject({}) { |result, ary| result[ary[0].to_sym] = ary[1]; result } title = hash.delete(:title) title ||= hash[:name] title ||= hash[key_attributes.first] if key_attributes.length == 1 raise Puppet::Error, "Title or name must be provided" unless title # Now create our resource. resource = Puppet::Resource.new(self.name, title) [:catalog].each do |attribute| if value = hash[attribute] hash.delete(attribute) resource.send(attribute.to_s + "=", value) end end hash.each do |param, value| resource[param] = value end return resource end # Create the path for logging and such. def pathbuilder if p = parent [p.pathbuilder, self.ref].flatten else [self.ref] end end ############################### # Add all of the meta parameters. newmetaparam(:noop) do desc "Boolean flag indicating whether work should actually be done." newvalues(:true, :false) munge do |value| case value when true, :true, "true"; @resource.noop = true when false, :false, "false"; @resource.noop = false 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." end - newmetaparam(:check) do - desc "Properties 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 properties - if args == :all - args = @resource.class.properties.find_all do |property| - # Only get properties supported by our provider - if @resource.provider - @resource.provider.class.supports_parameter?(property) - else - true - end - end.collect do |property| - property.name + newmetaparam(:audit) do + desc "Audit specified attributes of resources over time, and report if any have changed. + This attribute can be used to track changes to any resource over time, and can + provide an audit trail of every change that happens on any given machine. + + Note that you cannot both audit and manage an attribute - managing it guarantees + the value, and any changes already get logged." + + validate do |list| + list = Array(list) + unless list == [:all] + list.each do |param| + next if @resource.class.validattr?(param) + fail "Cannot audit #{param}: not a valid attribute for #{resource}" end end + end - unless args.is_a?(Array) - args = [args] + munge do |args| + properties_to_audit(args).each do |param| + next unless resource.class.validproperty?(param) + resource.newattr(param) end + end - unless defined? @resource - self.devfail "No parent for %s, %s?" % - [self.class, self.name] + def all_properties + resource.class.properties.find_all do |property| + resource.provider.nil? or resource.provider.class.supports_parameter?(property) + end.collect do |property| + property.name + end + end + + def properties_to_audit(list) + if list == :all + list = all_properties() if list == :all + else + list = Array(list).collect { |p| p.to_sym } end + end + end - args.each { |property| - unless property.is_a?(Symbol) - property = property.intern - end - next if @resource.propertydefined?(property) + newmetaparam(:check) do + desc "Audit specified attributes of resources over time, and report if any have changed. + This parameter has been deprecated in favor of 'audit'." - unless propertyklass = @resource.class.validproperty?(property) - if @resource.class.validattr?(property) - next - else - raise Puppet::Error, "%s is not a valid attribute for %s" % - [property, self.class.name] - end - end - next unless propertyklass.checkable? - @resource.newattr(property) - } + munge do |args| + resource.warning "'check' attribute is deprecated; use 'audit' instead" + resource[:audit] = args 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::Util::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 `LanguageTutorial language tutorial`:trac: for more information. " munge do |aliases| unless aliases.is_a?(Array) aliases = [aliases] end raise(ArgumentError, "Cannot add aliases without a catalog") unless @resource.catalog aliases.each do |other| if obj = @resource.catalog.resource(@resource.class.name, other) unless obj.object_id == @resource.object_id self.fail("%s can not create alias %s: object already exists" % [@resource.title, other]) end next end # Newschool, add it to the catalog. @resource.catalog.alias(@resource, other) end end end newmetaparam(:tag) do desc "Add the specified tags to the associated resource. While all resources are automatically tagged with as much information as possible (e.g., each class and definition containing the resource), it can be useful to add your own tags to a given resource. Tags are currently useful for things like applying a subset of a host's configuration:: puppet agent --test --tags 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| @resource.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(references) references = [references] unless references.is_a?(Array) references.collect do |ref| if ref.is_a?(Puppet::Resource) ref else Puppet::Resource.new(ref) end end end def validate_relationship @value.each do |ref| unless @resource.catalog.resource(ref.to_s) description = self.class.direction == :in ? "dependency" : "dependent" fail "Could not find %s %s for %s" % [description, ref.to_s, resource.ref] end end 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 |reference| reference.catalog = resource.catalog # Either of the two retrieval attempts could have returned # nil. unless related_resource = reference.resolve self.fail "Could not retrieve dependency '%s' of %s" % [reference, @resource.ref] end # Are we requiring them, or vice versa? See the method docs # for futher info on this. if self.class.direction == :in source = related_resource target = @resource else source = @resource target = related_resource end if method = self.class.callback subargs = { :event => self.class.events, :callback => method } self.debug("subscribes to %s" % [related_resource.ref]) else # If there's no callback, there's no point in even adding # a label. subargs = nil self.debug("requires %s" % [related_resource.ref]) end rel = Puppet::Relationship.new(source, target, subargs) end end end def self.relationship_params RelationshipMetaparam.subclasses end # Note that the order in which the relationships params is defined # matters. The labelled params (notify and subcribe) must be later, # so that if both params are used, those ones win. It's a hackish # solution, but it works. 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\"] } Multiple dependencies can be specified by providing a comma-seperated list of resources, enclosed in square brackets:: require => [ File[\"/usr/local\"], 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 resources 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 resources 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 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] } } Currently the ``exec``, ``mount`` and ``service`` type support refreshing. " end 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.} 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.} end newmetaparam(:stage) do desc %{Which run stage a given resource should reside in. This just creates a dependency on or from the named milestone. For instance, saying that this is in the 'bootstrap' stage creates a dependency on the 'bootstrap' milestone. By default, all classes get directly added to the 'main' stage. You can create new stages as resources: stage { [pre, post]: } To order stages, use standard relationships: stage { pre: before => Stage[main] } Or use the new relationship syntax: Stage[pre] -> Stage[main] -> Stage[post] Then use the new class parameters to specify a stage: class { foo: stage => pre } Stages can only be set on classes, not individual resources. This will fail:: file { '/foo': stage => pre, ensure => file } } end ############################### # All of the provider plumbing for the resource types. require 'puppet/provider' require 'puppet/util/provider_features' # Add the feature handling module. extend Puppet::Util::ProviderFeatures attr_reader :provider # the Type class attribute accessors class << self attr_accessor :providerloader attr_writer :defaultprovider end # Find the default provider. def self.defaultprovider unless defined? @defaultprovider and @defaultprovider suitable = suitableprovider() # Find which providers are a default for this system. defaults = suitable.find_all { |provider| provider.default? } # If we don't have any default we use suitable providers defaults = suitable if defaults.empty? max = defaults.collect { |provider| provider.specificity }.max defaults = defaults.find_all { |provider| provider.specificity == max } retval = nil if defaults.length > 1 Puppet.warning( "Found multiple default providers for %s: %s; using %s" % [self.name, defaults.collect { |i| i.name.to_s }.join(", "), defaults[0].name] ) retval = defaults.shift elsif defaults.length == 1 retval = defaults.shift else raise Puppet::DevError, "Could not find a default provider for %s" % self.name end @defaultprovider = retval end return @defaultprovider end def self.provider_hash_by_type(type) @provider_hashes ||= {} @provider_hashes[type] ||= {} end def self.provider_hash Puppet::Type.provider_hash_by_type(self.name) end # Retrieve a provider by name. def self.provider(name) name = Puppet::Util.symbolize(name) # If we don't have it yet, try loading it. unless provider_hash.has_key?(name) @providerloader.load(name) end return provider_hash[name] end # Just list all of the providers. def self.providers provider_hash.keys end def self.validprovider?(name) name = Puppet::Util.symbolize(name) return (provider_hash.has_key?(name) && provider_hash[name].suitable?) end # Create a new provider of a type. This method must be called # directly on the type that it's implementing. def self.provide(name, options = {}, &block) name = Puppet::Util.symbolize(name) if obj = provider_hash[name] Puppet.debug "Reloading %s %s provider" % [name, self.name] unprovide(name) end parent = if pname = options[:parent] options.delete(:parent) if pname.is_a? Class pname else if provider = self.provider(pname) provider else raise Puppet::DevError, "Could not find parent provider %s of %s" % [pname, name] end end else Puppet::Provider end options[:resource_type] ||= self self.providify provider = genclass(name, :parent => parent, :hash => provider_hash, :prefix => "Provider", :block => block, :include => feature_module, :extend => feature_module, :attributes => options ) return provider end # Make sure we have a :provider parameter defined. Only gets called if there # are providers. def self.providify return if @paramhash.has_key? :provider newparam(:provider) do desc "The specific backend for #{self.name.to_s} to use. You will seldom need to specify this -- Puppet will usually discover the appropriate provider for your platform." # This is so we can refer back to the type to get a list of # providers for documentation. class << self attr_accessor :parenttype end # We need to add documentation for each provider. def self.doc @doc + " Available providers are:\n\n" + parenttype().providers.sort { |a,b| a.to_s <=> b.to_s }.collect { |i| "* **%s**: %s" % [i, parenttype().provider(i).doc] }.join("\n") end defaultto { @resource.class.defaultprovider.name } validate do |provider_class| provider_class = provider_class[0] if provider_class.is_a? Array if provider_class.is_a?(Puppet::Provider) provider_class = provider_class.class.name end unless provider = @resource.class.provider(provider_class) raise ArgumentError, "Invalid %s provider '%s'" % [@resource.class.name, provider_class] end end munge do |provider| provider = provider[0] if provider.is_a? Array if provider.is_a? String provider = provider.intern end @resource.provider = provider if provider.is_a?(Puppet::Provider) provider.class.name else provider end end end.parenttype = self end def self.unprovide(name) if provider_hash.has_key? name rmclass(name, :hash => provider_hash, :prefix => "Provider" ) if @defaultprovider and @defaultprovider.name == name @defaultprovider = nil end end end # Return an array of all of the suitable providers. def self.suitableprovider if provider_hash.empty? providerloader.loadall end provider_hash.find_all { |name, provider| provider.suitable? }.collect { |name, provider| provider }.reject { |p| p.name == :fake } # For testing end def provider=(name) if name.is_a?(Puppet::Provider) @provider = name @provider.resource = self elsif klass = self.class.provider(name) @provider = klass.new(self) else raise ArgumentError, "Could not find %s provider of %s" % [name, self.class.name] end end ############################### # All of the relationship code. # 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(rel_catalog = nil) rel_catalog ||= catalog raise(Puppet::DevError, "You cannot add relationships without a catalog") unless rel_catalog 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(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 = rel_catalog.resource(type, dep) next end end reqs << Puppet::Relationship.new(dep, self) } } return reqs end # Build the dependencies associated with an individual object. def builddepends # Handle the requires self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.to_edges end end.flatten.reject { |r| r.nil? } end # Define the initial list of tags. def tags=(list) tag(self.class.name) tag(*list) end # Types (which map to resources in the languages) are entirely composed of # attribute value pairs. Generally, Puppet calls any of these things an # 'attribute', but these attributes always take one of three specific # forms: parameters, metaparams, or properties. # In naming methods, I have tried to consistently name the method so # that it is clear whether it operates on all attributes (thus has 'attr' in # the method name, or whether it operates on a specific type of attributes. attr_writer :title attr_writer :noop include Enumerable # class methods dealing with Type management public # the Type class attribute accessors class << self attr_reader :name attr_accessor :self_refresh include Enumerable, Puppet::Util::ClassGen include Puppet::MetaType::Manager include Puppet::Util include Puppet::Util::Logging end # all of the variables that must be initialized for each subclass def self.initvars # all of the instances of this class @objects = Hash.new @aliases = Hash.new @defaults = {} unless defined? @parameters @parameters = [] end @validproperties = {} @properties = [] @parameters = [] @paramhash = {} @attr_aliases = {} @paramdoc = Hash.new { |hash,key| if key.is_a?(String) key = key.intern end if hash.include?(key) hash[key] else "Param Documentation for %s not found" % key end } unless defined? @doc @doc = "" end end def self.to_s if defined? @name "Puppet::Type::" + @name.to_s.capitalize else super end end # Create a block to validate that our object is set up entirely. This will # be run before the object is operated on. def self.validate(&block) define_method(:validate, &block) #@validate = block end # The catalog that this resource is stored in. attr_accessor :catalog # is the resource exported attr_accessor :exported # is the resource virtual (it should not :-)) attr_accessor :virtual # create a log at specified level def log(msg) Puppet::Util::Log.create( :level => @parameters[:loglevel].value, :message => msg, :source => self ) end # instance methods related to instance intrinsics # e.g., initialize() and name() public attr_reader :original_parameters # initialize the type instance def initialize(resource) raise Puppet::DevError, "Got TransObject instead of Resource or hash" if resource.is_a?(Puppet::TransObject) resource = self.class.hash2resource(resource) unless resource.is_a?(Puppet::Resource) # The list of parameter/property instances. @parameters = {} # Set the title first, so any failures print correctly. if resource.type.to_s.downcase.to_sym == self.class.name self.title = resource.title else # This should only ever happen for components self.title = resource.ref end [:file, :line, :catalog, :exported, :virtual].each do |getter| setter = getter.to_s + "=" if val = resource.send(getter) self.send(setter, val) end end @tags = resource.tags @original_parameters = resource.to_hash set_name(@original_parameters) set_default(:provider) set_parameters(@original_parameters) self.validate if self.respond_to?(:validate) end private # Set our resource's name. def set_name(hash) self[name_var] = hash.delete(name_var) if name_var end # Set all of the parameters from a hash, in the appropriate order. def set_parameters(hash) # Use the order provided by allattrs, but add in any # extra attributes from the resource so we get failures # on invalid attributes. no_values = [] (self.class.allattrs + hash.keys).uniq.each do |attr| begin # Set any defaults immediately. This is mostly done so # that the default provider is available for any other # property validation. if hash.has_key?(attr) self[attr] = hash[attr] else no_values << attr end rescue ArgumentError, Puppet::Error, TypeError raise rescue => detail error = Puppet::DevError.new( "Could not set %s on %s: %s" % [attr, self.class.name, detail]) error.set_backtrace(detail.backtrace) raise error end end no_values.each do |attr| set_default(attr) end end public # Set up all of our autorequires. def finish # Make sure all of our relationships are valid. Again, must be done # when the entire catalog is instantiated. self.class.relationship_params.collect do |klass| if param = @parameters[klass.name] param.validate_relationship end end.flatten.reject { |r| r.nil? } end # For now, leave the 'name' method functioning like it used to. Once 'title' # works everywhere, I'll switch it. def name return self[:name] end # Look up our parent in the catalog, if we have one. def parent return nil unless catalog unless defined?(@parent) if parents = catalog.adjacent(self, :direction => :in) # We should never have more than one parent, so let's just ignore # it if we happen to. @parent = parents.shift else @parent = nil end end @parent end # Return the "type[name]" style reference. def ref "%s[%s]" % [self.class.name.to_s.capitalize, self.title] end def self_refresh? self.class.self_refresh end # Mark that we're purging. def purging @purging = true end # Is this resource being purged? Used by transactions to forbid # deletion when there are dependencies. def purging? if defined? @purging @purging else false end end # Retrieve the title of an object. If no title was set separately, # then use the object's name. def title unless defined? @title and @title if self.class.validparameter?(name_var) @title = self[:name] elsif self.class.validproperty?(name_var) @title = self.should(name_var) else self.devfail "Could not find namevar %s for %s" % [name_var, self.class.name] end end return @title end # convert to a string def to_s self.ref end # Convert to a transportable object def to_trans(ret = true) trans = TransObject.new(self.title, self.class.name) values = retrieve() values.each do |name, value| # sometimes we get symbols and sometimes we get Properties # I think it's a bug, but I can't find it. ~JW name = name.name if name.respond_to? :name trans[name] = value end @parameters.each do |name, param| # Avoid adding each instance name twice next if param.class.isnamevar? and param.value == self.title # We've already got property values next if param.is_a?(Puppet::Property) trans[name] = param.value end trans.tags = self.tags # FIXME I'm currently ignoring 'parent' and 'path' return trans end def to_resource # this 'type instance' versus 'resource' distinction seems artificial # I'd like to see it collapsed someday ~JW self.to_trans.to_resource end %w{exported virtual}.each do |m| define_method(m+"?") do self.send(m) end end end # Puppet::Type end require 'puppet/provider' # Always load these types. require 'puppet/type/component' diff --git a/spec/unit/transaction/change.rb b/spec/unit/transaction/change.rb index 183414777..9419bbab9 100755 --- a/spec/unit/transaction/change.rb +++ b/spec/unit/transaction/change.rb @@ -1,167 +1,193 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/transaction/change' describe Puppet::Transaction::Change do Change = Puppet::Transaction::Change describe "when initializing" do before do @property = stub 'property', :path => "/property/path", :should => "shouldval" end it "should require the property and current value" do lambda { Change.new() }.should raise_error end it "should set its property to the provided property" do Change.new(@property, "value").property.should == :property end it "should set its 'is' value to the provided value" do Change.new(@property, "value").is.should == "value" end it "should retrieve the 'should' value from the property" do # Yay rspec :) Change.new(@property, "value").should.should == @property.should end end describe "when an instance" do before do @property = stub 'property', :path => "/property/path", :should => "shouldval" @change = Change.new(@property, "value") end it "should be noop if the property is noop" do @property.expects(:noop).returns true @change.noop?.should be_true end + it "should be auditing if set so" do + @change.auditing = true + @change.must be_auditing + end + it "should set its resource to the proxy if it has one" do @change.proxy = :myresource @change.resource.should == :myresource end it "should set its resource to the property's resource if no proxy is set" do @property.expects(:resource).returns :myresource @change.resource.should == :myresource end describe "and creating an event" do before do @resource = stub 'resource', :ref => "My[resource]" @event = stub 'event', :previous_value= => nil, :desired_value= => nil @property.stubs(:event).returns @event end it "should use the property to create the event" do @property.expects(:event).returns @event @change.event.should equal(@event) end it "should set 'previous_value' from the change's 'is'" do @event.expects(:previous_value=).with(@change.is) @change.event end it "should set 'desired_value' from the change's 'should'" do @event.expects(:desired_value=).with(@change.should) @change.event end end describe "and executing" do before do @event = Puppet::Transaction::Event.new(:myevent) @event.stubs(:send_log) @change.stubs(:noop?).returns false @property.stubs(:event).returns @event @property.stub_everything @property.stubs(:resource).returns "myresource" @property.stubs(:name).returns :myprop end describe "in noop mode" do before { @change.stubs(:noop?).returns true } it "should log that it is in noop" do @property.expects(:is_to_s) @property.expects(:should_to_s) @event.expects(:message=).with { |msg| msg.include?("should be") } @change.apply end it "should produce a :noop event and return" do @property.stub_everything @event.expects(:status=).with("noop") @change.apply.should == @event end end + describe "in audit mode" do + before { @change.auditing = true } + + it "should log that it is in audit mode" do + @property.expects(:is_to_s) + @property.expects(:should_to_s) + + @event.expects(:message=).with { |msg| msg.include?("audit") } + + @change.apply + end + + it "should produce a :audit event and return" do + @property.stub_everything + + @event.expects(:status=).with("audit") + + @change.apply.should == @event + end + end + it "should sync the property" do @property.expects(:sync) @change.apply end it "should return the default event if syncing the property returns nil" do @property.stubs(:sync).returns nil @change.expects(:event).with(nil).returns @event @change.apply.should == @event end it "should return the default event if syncing the property returns an empty array" do @property.stubs(:sync).returns [] @change.expects(:event).with(nil).returns @event @change.apply.should == @event end it "should log the change" do @property.expects(:sync).returns [:one] @event.expects(:send_log) @change.apply end it "should set the event's message to the change log" do @property.expects(:change_to_s).returns "my change" @change.apply.message.should == "my change" end it "should set the event's status to 'success'" do @change.apply.status.should == "success" end describe "and the change fails" do before { @property.expects(:sync).raises "an exception" } it "should catch the exception and log the err" do @event.expects(:send_log) lambda { @change.apply }.should_not raise_error end it "should mark the event status as 'failure'" do @change.apply.status.should == "failure" end it "should set the event log to a failure log" do @change.apply.message.should be_include("failed") end end end end end diff --git a/spec/unit/transaction/event.rb b/spec/unit/transaction/event.rb index 6a837b50f..85811c105 100755 --- a/spec/unit/transaction/event.rb +++ b/spec/unit/transaction/event.rb @@ -1,108 +1,108 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/transaction/event' describe Puppet::Transaction::Event do [:previous_value, :desired_value, :property, :resource, :name, :message, :node, :version, :file, :line, :tags].each do |attr| it "should support #{attr}" do event = Puppet::Transaction::Event.new event.send(attr.to_s + "=", "foo") event.send(attr).should == "foo" end end it "should always convert the property to a string" do Puppet::Transaction::Event.new(:property => :foo).property.should == "foo" end it "should always convert the resource to a string" do Puppet::Transaction::Event.new(:resource => :foo).resource.should == "foo" end it "should produce the message when converted to a string" do event = Puppet::Transaction::Event.new event.expects(:message).returns "my message" event.to_s.should == "my message" end it "should support 'status'" do event = Puppet::Transaction::Event.new event.status = "success" event.status.should == "success" end - it "should fail if the status is not to 'noop', 'success', or 'failure" do + it "should fail if the status is not to 'audit', 'noop', 'success', or 'failure" do event = Puppet::Transaction::Event.new lambda { event.status = "foo" }.should raise_error(ArgumentError) end it "should support tags" do Puppet::Transaction::Event.ancestors.should include(Puppet::Util::Tagging) end it "should create a timestamp at its creation time" do Puppet::Transaction::Event.new.time.should be_instance_of(Time) end describe "when sending logs" do before do Puppet::Util::Log.stubs(:new) end it "should set the level to the resources's log level if the event status is 'success' and a resource is available" do resource = stub 'resource' resource.expects(:[]).with(:loglevel).returns :myloglevel Puppet::Util::Log.expects(:create).with { |args| args[:level] == :myloglevel } Puppet::Transaction::Event.new(:status => "success", :resource => resource).send_log end it "should set the level to 'notice' if the event status is 'success' and no resource is available" do Puppet::Util::Log.expects(:new).with { |args| args[:level] == :notice } Puppet::Transaction::Event.new(:status => "success").send_log end it "should set the level to 'notice' if the event status is 'noop'" do Puppet::Util::Log.expects(:new).with { |args| args[:level] == :notice } Puppet::Transaction::Event.new(:status => "noop").send_log end it "should set the level to 'err' if the event status is 'failure'" do Puppet::Util::Log.expects(:new).with { |args| args[:level] == :err } Puppet::Transaction::Event.new(:status => "failure").send_log end it "should set the 'message' to the event log" do Puppet::Util::Log.expects(:new).with { |args| args[:message] == "my message" } Puppet::Transaction::Event.new(:message => "my message").send_log end it "should set the tags to the event tags" do Puppet::Util::Log.expects(:new).with { |args| args[:tags] == %w{one two} } Puppet::Transaction::Event.new(:tags => %w{one two}).send_log end [:file, :line, :version].each do |attr| it "should pass the #{attr}" do Puppet::Util::Log.expects(:new).with { |args| args[attr] == "my val" } Puppet::Transaction::Event.new(attr => "my val").send_log end end it "should use the source description as the source if one is set" do Puppet::Util::Log.expects(:new).with { |args| args[:source] == "/my/param" } Puppet::Transaction::Event.new(:source_description => "/my/param", :resource => "Foo[bar]", :property => "foo").send_log end it "should use the property as the source if one is available and no source description is set" do Puppet::Util::Log.expects(:new).with { |args| args[:source] == "foo" } Puppet::Transaction::Event.new(:resource => "Foo[bar]", :property => "foo").send_log end it "should use the property as the source if one is available and no property or source description is set" do Puppet::Util::Log.expects(:new).with { |args| args[:source] == "Foo[bar]" } Puppet::Transaction::Event.new(:resource => "Foo[bar]").send_log end end end diff --git a/spec/unit/transaction/resource_harness.rb b/spec/unit/transaction/resource_harness.rb index ee2726d07..cbb796cde 100755 --- a/spec/unit/transaction/resource_harness.rb +++ b/spec/unit/transaction/resource_harness.rb @@ -1,344 +1,401 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/transaction/resource_harness' describe Puppet::Transaction::ResourceHarness do before do @transaction = Puppet::Transaction.new(Puppet::Resource::Catalog.new) @resource = Puppet::Type.type(:file).new :path => "/my/file" @harness = Puppet::Transaction::ResourceHarness.new(@transaction) @current_state = Puppet::Resource.new(:file, "/my/file") @resource.stubs(:retrieve).returns @current_state @status = Puppet::Resource::Status.new(@resource) Puppet::Resource::Status.stubs(:new).returns @status end it "should accept a transaction at initialization" do harness = Puppet::Transaction::ResourceHarness.new(@transaction) harness.transaction.should equal(@transaction) end it "should delegate to the transaction for its relationship graph" do @transaction.expects(:relationship_graph).returns "relgraph" Puppet::Transaction::ResourceHarness.new(@transaction).relationship_graph.should == "relgraph" end + describe "when copying audited parameters" do + before do + @resource = Puppet::Type.type(:file).new :path => "/foo/bar", :audit => :mode + end + + it "should do nothing if no parameters are being audited" do + @resource[:audit] = [] + @harness.expects(:cached).never + @harness.copy_audited_parameters(@resource, {}).should == [] + end + + it "should do nothing if an audited parameter already has a desired value set" do + @resource[:mode] = "755" + @harness.expects(:cached).never + @harness.copy_audited_parameters(@resource, {}).should == [] + end + + it "should copy any cached values to the 'should' values" do + @harness.cache(@resource, :mode, "755") + @harness.copy_audited_parameters(@resource, {}).should == [:mode] + + @resource[:mode].should == 0755 + end + + it "should cache and log the current value if no cached values are present" do + @resource.expects(:notice) + @harness.copy_audited_parameters(@resource, {:mode => "755"}).should == [] + + @harness.cached(@resource, :mode).should == "755" + end + end + describe "when evaluating a resource" do it "should create and return a resource status instance for the resource" do @harness.evaluate(@resource).should be_instance_of(Puppet::Resource::Status) end it "should fail if no status can be created" do Puppet::Resource::Status.expects(:new).raises ArgumentError lambda { @harness.evaluate(@resource) }.should raise_error end it "should retrieve the current state of the resource" do @resource.expects(:retrieve).returns @current_state @harness.evaluate(@resource) end it "should mark the resource as failed and return if the current state cannot be retrieved" do @resource.expects(:retrieve).raises ArgumentError @harness.evaluate(@resource).should be_failed end it "should use the status and retrieved state to determine which changes need to be made" do @harness.expects(:changes_to_perform).with(@status, @resource).returns [] @harness.evaluate(@resource) end it "should mark the status as out of sync and apply the created changes if there are any" do changes = %w{mychanges} @harness.expects(:changes_to_perform).returns changes @harness.expects(:apply_changes).with(@status, changes) @harness.evaluate(@resource).should be_out_of_sync end it "should cache the last-synced time" do changes = %w{mychanges} @harness.stubs(:changes_to_perform).returns changes @harness.stubs(:apply_changes) @harness.expects(:cache).with { |resource, name, time| name == :synced and time.is_a?(Time) } @harness.evaluate(@resource) end it "should flush the resource when applying changes if appropriate" do changes = %w{mychanges} @harness.stubs(:changes_to_perform).returns changes @harness.stubs(:apply_changes) @resource.expects(:flush) @harness.evaluate(@resource) end it "should use the status and retrieved state to determine which changes need to be made" do @harness.expects(:changes_to_perform).with(@status, @resource).returns [] @harness.evaluate(@resource) end it "should not attempt to apply changes if none need to be made" do @harness.expects(:changes_to_perform).returns [] @harness.expects(:apply_changes).never @harness.evaluate(@resource).should_not be_out_of_sync end it "should store the resource's evaluation time in the resource status" do @harness.evaluate(@resource).evaluation_time.should be_instance_of(Float) end it "should set the change count to the total number of changes" do changes = %w{a b c d} @harness.expects(:changes_to_perform).returns changes @harness.expects(:apply_changes).with(@status, changes) @harness.evaluate(@resource).change_count.should == 4 end end describe "when creating changes" do before do @current_state = Puppet::Resource.new(:file, "/my/file") @resource.stubs(:retrieve).returns @current_state Puppet.features.stubs(:root?).returns true end it "should retrieve the current values from the resource" do @resource.expects(:retrieve).returns @current_state @harness.changes_to_perform(@status, @resource) end it "should cache that the resource was checked" do @harness.expects(:cache).with { |resource, name, time| name == :checked and time.is_a?(Time) } @harness.changes_to_perform(@status, @resource) end it "should create changes with the appropriate property and current value" do @resource[:ensure] = :present @current_state[:ensure] = :absent change = stub 'change' Puppet::Transaction::Change.expects(:new).with(@resource.parameter(:ensure), :absent).returns change @harness.changes_to_perform(@status, @resource)[0].should equal(change) end it "should not attempt to manage properties that do not have desired values set" do mode = @resource.newattr(:mode) @current_state[:mode] = :absent mode.expects(:insync?).never @harness.changes_to_perform(@status, @resource) end + it "should copy audited parameters" do + @resource[:audit] = :mode + @harness.cache(@resource, :mode, "755") + @harness.changes_to_perform(@status, @resource) + @resource[:mode].should == 0755 + end + + it "should mark changes created as a result of auditing as auditing changes" do + @current_state[:mode] = 0644 + @resource[:audit] = :mode + @harness.cache(@resource, :mode, "755") + @harness.changes_to_perform(@status, @resource)[0].must be_auditing + end + describe "and the 'ensure' parameter is present but not in sync" do it "should return a single change for the 'ensure' parameter" do @resource[:ensure] = :present @resource[:mode] = "755" @current_state[:ensure] = :absent @current_state[:mode] = :absent @resource.stubs(:retrieve).returns @current_state changes = @harness.changes_to_perform(@status, @resource) changes.length.should == 1 changes[0].property.name.should == :ensure end end describe "and the 'ensure' parameter should be set to 'absent', and is correctly set to 'absent'" do it "should return no changes" do @resource[:ensure] = :absent @resource[:mode] = "755" @current_state[:ensure] = :absent @current_state[:mode] = :absent @harness.changes_to_perform(@status, @resource).should == [] end end describe "and the 'ensure' parameter is 'absent' and there is no 'desired value'" do it "should return no changes" do @resource.newattr(:ensure) @resource[:mode] = "755" @current_state[:ensure] = :absent @current_state[:mode] = :absent @harness.changes_to_perform(@status, @resource).should == [] end end describe "and non-'ensure' parameters are not in sync" do it "should return a change for each parameter that is not in sync" do @resource[:ensure] = :present @resource[:mode] = "755" @resource[:owner] = 0 @current_state[:ensure] = :present @current_state[:mode] = 0444 @current_state[:owner] = 50 mode = stub 'mode_change' owner = stub 'owner_change' Puppet::Transaction::Change.expects(:new).with(@resource.parameter(:mode), 0444).returns mode Puppet::Transaction::Change.expects(:new).with(@resource.parameter(:owner), 50).returns owner changes = @harness.changes_to_perform(@status, @resource) changes.length.should == 2 changes.should be_include(mode) changes.should be_include(owner) end end describe "and all parameters are in sync" do it "should return an empty array" do @resource[:ensure] = :present @resource[:mode] = "755" @current_state[:ensure] = :present @current_state[:mode] = 0755 @harness.changes_to_perform(@status, @resource).should == [] end end end describe "when applying changes" do before do - @change1 = stub 'change1', :apply => stub("event", :status => "success") - @change2 = stub 'change2', :apply => stub("event", :status => "success") + @change1 = stub 'change1', :apply => stub("event", :status => "success"), :auditing? => false + @change2 = stub 'change2', :apply => stub("event", :status => "success"), :auditing? => false @changes = [@change1, @change2] end it "should apply the change" do @change1.expects(:apply).returns( stub("event", :status => "success") ) @change2.expects(:apply).returns( stub("event", :status => "success") ) @harness.apply_changes(@status, @changes) end it "should mark the resource as changed" do @harness.apply_changes(@status, @changes) @status.should be_changed end it "should queue the resulting event" do @harness.apply_changes(@status, @changes) @status.events.should be_include(@change1.apply) @status.events.should be_include(@change2.apply) end + + it "should cache the new value if it is an auditing change" do + @change1.expects(:auditing?).returns true + property = stub 'property', :name => "foo", :resource => "myres" + @change1.stubs(:property).returns property + @change1.stubs(:is).returns "myval" + + @harness.apply_changes(@status, @changes) + + @harness.cached("myres", "foo").should == "myval" + end end describe "when determining whether the resource can be changed" do before do @resource.stubs(:purging?).returns true @resource.stubs(:deleting?).returns true end it "should be true if the resource is not being purged" do @resource.expects(:purging?).returns false @harness.should be_allow_changes(@resource) end it "should be true if the resource is not being deleted" do @resource.expects(:deleting?).returns false @harness.should be_allow_changes(@resource) end it "should be true if the resource has no dependents" do @harness.relationship_graph.expects(:dependents).with(@resource).returns [] @harness.should be_allow_changes(@resource) end it "should be true if all dependents are being deleted" do dep = stub 'dependent', :deleting? => true @harness.relationship_graph.expects(:dependents).with(@resource).returns [dep] @resource.expects(:purging?).returns true @harness.should be_allow_changes(@resource) end it "should be false if the resource's dependents are not being deleted" do dep = stub 'dependent', :deleting? => false, :ref => "myres" @resource.expects(:warning) @harness.relationship_graph.expects(:dependents).with(@resource).returns [dep] @harness.should_not be_allow_changes(@resource) end end describe "when finding the schedule" do before do @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog end it "should warn and return nil if the resource has no catalog" do @resource.catalog = nil @resource.expects(:warning) @harness.schedule(@resource).should be_nil end it "should return nil if the resource specifies no schedule" do @harness.schedule(@resource).should be_nil end it "should fail if the named schedule cannot be found" do @resource[:schedule] = "whatever" @resource.expects(:fail) @harness.schedule(@resource) end it "should return the named schedule if it exists" do sched = Puppet::Type.type(:schedule).new(:name => "sched") @catalog.add_resource(sched) @resource[:schedule] = "sched" @harness.schedule(@resource).to_s.should == sched.to_s end end describe "when determining if a resource is scheduled" do before do @catalog = Puppet::Resource::Catalog.new @resource.catalog = @catalog @status = Puppet::Resource::Status.new(@resource) end it "should return true if 'ignoreschedules' is set" do Puppet[:ignoreschedules] = true @resource[:schedule] = "meh" @harness.should be_scheduled(@status, @resource) end it "should return true if the resource has no schedule set" do @harness.should be_scheduled(@status, @resource) end it "should return the result of matching the schedule with the cached 'checked' time if a schedule is set" do t = Time.now @harness.expects(:cached).with(@resource, :checked).returns(t) sched = Puppet::Type.type(:schedule).new(:name => "sched") @catalog.add_resource(sched) @resource[:schedule] = "sched" sched.expects(:match?).with(t.to_i).returns "feh" @harness.scheduled?(@status, @resource).should == "feh" end end it "should be able to cache data in the Storage module" do data = {} Puppet::Util::Storage.expects(:cache).with(@resource).returns data @harness.cache(@resource, :foo, "something") data[:foo].should == "something" end it "should be able to retrieve data from the cache" do data = {:foo => "other"} Puppet::Util::Storage.expects(:cache).with(@resource).returns data @harness.cached(@resource, :foo).should == "other" end end diff --git a/spec/unit/type.rb b/spec/unit/type.rb index e7888a389..e3ae5e62d 100755 --- a/spec/unit/type.rb +++ b/spec/unit/type.rb @@ -1,484 +1,529 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../spec_helper' describe Puppet::Type do it "should include the Cacher module" do Puppet::Type.ancestors.should be_include(Puppet::Util::Cacher) end it "should consider a parameter to be valid if it is a valid parameter" do Puppet::Type.type(:mount).should be_valid_parameter(:path) end it "should consider a parameter to be valid if it is a valid property" do Puppet::Type.type(:mount).should be_valid_parameter(:fstype) end it "should consider a parameter to be valid if it is a valid metaparam" do Puppet::Type.type(:mount).should be_valid_parameter(:noop) end it "should use its catalog as its expirer" do catalog = Puppet::Resource::Catalog.new resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.catalog = catalog resource.expirer.should equal(catalog) end it "should do nothing when asked to expire when it has no catalog" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) lambda { resource.expire }.should_not raise_error end it "should be able to retrieve a property by name" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.property(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype)) end it "should be able to retrieve a parameter by name" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.parameter(:name).must be_instance_of(Puppet::Type.type(:mount).attrclass(:name)) end it "should be able to retrieve a property by name using the :parameter method" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) resource.parameter(:fstype).must be_instance_of(Puppet::Type.type(:mount).attrclass(:fstype)) end it "should be able to retrieve all set properties" do resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) props = resource.properties props.should_not be_include(nil) [:fstype, :ensure, :pass].each do |name| props.should be_include(resource.parameter(name)) end end it "should have a method for setting default values for resources" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:set_default) end it "should do nothing for attributes that have no defaults and no specified value" do Puppet::Type.type(:mount).new(:name => "foo").parameter(:noop).should be_nil end it "should have a method for adding tags" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:tags) end it "should use the tagging module" do Puppet::Type.type(:mount).ancestors.should be_include(Puppet::Util::Tagging) end it "should delegate to the tagging module when tags are added" do resource = Puppet::Type.type(:mount).new(:name => "foo") resource.stubs(:tag).with(:mount) resource.expects(:tag).with(:tag1, :tag2) resource.tags = [:tag1,:tag2] end it "should add the current type as tag" do resource = Puppet::Type.type(:mount).new(:name => "foo") resource.stubs(:tag) resource.expects(:tag).with(:mount) resource.tags = [:tag1,:tag2] end it "should have a method to know if the resource is exported" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:exported?) end it "should have a method to know if the resource is virtual" do Puppet::Type.type(:mount).new(:name => "foo").should respond_to(:virtual?) end it "should consider its version to be its catalog version" do resource = Puppet::Type.type(:mount).new(:name => "foo") catalog = Puppet::Resource::Catalog.new catalog.version = 50 catalog.add_resource resource resource.version.should == 50 end it "should consider its version to be zero if it has no catalog" do Puppet::Type.type(:mount).new(:name => "foo").version.should == 0 end it "should provide source_descriptors" do resource = Puppet::Type.type(:mount).new(:name => "foo") catalog = Puppet::Resource::Catalog.new catalog.version = 50 catalog.add_resource resource resource.source_descriptors.should == {:version=>50, :tags=>["mount", "foo"], :path=>"/Mount[foo]"} end it "should consider its type to be the name of its class" do Puppet::Type.type(:mount).new(:name => "foo").type.should == :mount end describe "when creating an event" do before do @resource = Puppet::Type.type(:mount).new :name => "foo" end it "should have the resource's reference as the resource" do @resource.event.resource.should == "Mount[foo]" end it "should have the resource's log level as the default log level" do @resource[:loglevel] = :warning @resource.event.default_log_level.should == :warning end {:file => "/my/file", :line => 50, :tags => %{foo bar}, :version => 50}.each do |attr, value| it "should set the #{attr}" do @resource.stubs(attr).returns value @resource.event.send(attr).should == value end end it "should allow specification of event attributes" do @resource.event(:status => "noop").status.should == "noop" end end describe "when choosing a default provider" do it "should choose the provider with the highest specificity" do # Make a fake type type = Puppet::Type.newtype(:defaultprovidertest) do newparam(:name) do end end basic = type.provide(:basic) {} greater = type.provide(:greater) {} basic.stubs(:specificity).returns 1 greater.stubs(:specificity).returns 2 type.defaultprovider.should equal(greater) end end describe "when initializing" do describe "and passed a TransObject" do it "should fail" do trans = Puppet::TransObject.new("/foo", :mount) lambda { Puppet::Type.type(:mount).new(trans) }.should raise_error(Puppet::DevError) end end describe "and passed a Puppet::Resource instance" do it "should set its title to the title of the resource if the resource type is equal to the current type" do resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "/other"}) Puppet::Type.type(:mount).new(resource).title.should == "/foo" end it "should set its title to the resource reference if the resource type is not equal to the current type" do resource = Puppet::Resource.new(:user, "foo") Puppet::Type.type(:mount).new(resource).title.should == "User[foo]" end [:line, :file, :catalog, :exported, :virtual].each do |param| it "should copy '#{param}' from the resource if present" do resource = Puppet::Resource.new(:mount, "/foo") resource.send(param.to_s + "=", "foo") resource.send(param.to_s + "=", "foo") Puppet::Type.type(:mount).new(resource).send(param).should == "foo" end end it "should copy any tags from the resource" do resource = Puppet::Resource.new(:mount, "/foo") resource.tag "one", "two" tags = Puppet::Type.type(:mount).new(resource).tags tags.should be_include("one") tags.should be_include("two") end it "should copy the resource's parameters as its own" do resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:atboot => true, :fstype => "boo"}) params = Puppet::Type.type(:mount).new(resource).to_hash params[:fstype].should == "boo" params[:atboot].should == true end end describe "and passed a Hash" do it "should extract the title from the hash" do Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay" end it "should work when hash keys are provided as strings" do Puppet::Type.type(:mount).new("title" => "/yay").title.should == "/yay" end it "should work when hash keys are provided as symbols" do Puppet::Type.type(:mount).new(:title => "/yay").title.should == "/yay" end it "should use the name from the hash as the title if no explicit title is provided" do Puppet::Type.type(:mount).new(:name => "/yay").title.should == "/yay" end it "should use the Resource Type's namevar to determine how to find the name in the hash" do Puppet::Type.type(:file).new(:path => "/yay").title.should == "/yay" end [:catalog].each do |param| it "should extract '#{param}' from the hash if present" do Puppet::Type.type(:mount).new(:name => "/yay", param => "foo").send(param).should == "foo" end end it "should use any remaining hash keys as its parameters" do resource = Puppet::Type.type(:mount).new(:title => "/foo", :catalog => "foo", :atboot => true, :fstype => "boo") resource[:fstype].must == "boo" resource[:atboot].must == true end end it "should fail if any invalid attributes have been provided" do lambda { Puppet::Type.type(:mount).new(:title => "/foo", :nosuchattr => "whatever") }.should raise_error(Puppet::Error) end it "should set its name to the resource's title if the resource does not have a :name or namevar parameter set" do resource = Puppet::Resource.new(:mount, "/foo") Puppet::Type.type(:mount).new(resource).name.should == "/foo" end it "should fail if no title, name, or namevar are provided" do lambda { Puppet::Type.type(:file).new(:atboot => true) }.should raise_error(Puppet::Error) end it "should set the attributes in the order returned by the class's :allattrs method" do Puppet::Type.type(:mount).stubs(:allattrs).returns([:name, :atboot, :noop]) resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => "myboot", :noop => "whatever"}) set = [] Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash| set << param true end Puppet::Type.type(:mount).new(resource) set[-1].should == :noop set[-2].should == :atboot end it "should always set the name and then default provider before anything else" do Puppet::Type.type(:mount).stubs(:allattrs).returns([:provider, :name, :atboot]) resource = Puppet::Resource.new(:mount, "/foo", :parameters => {:name => "myname", :atboot => "myboot"}) set = [] Puppet::Type.type(:mount).any_instance.stubs(:newattr).with do |param, hash| set << param true end Puppet::Type.type(:mount).new(resource) set[0].should == :name set[1].should == :provider end # This one is really hard to test :/ it "should each default immediately if no value is provided" do defaults = [] Puppet::Type.type(:package).any_instance.stubs(:set_default).with { |value| defaults << value; true } Puppet::Type.type(:package).new :name => "whatever" defaults[0].should == :provider end it "should retain a copy of the originally provided parameters" do Puppet::Type.type(:mount).new(:name => "foo", :atboot => true, :noop => false).original_parameters.should == {:atboot => true, :noop => false} end it "should delete the name via the namevar from the originally provided parameters" do Puppet::Type.type(:file).new(:name => "/foo").original_parameters[:path].should be_nil end end it "should have a class method for converting a hash into a Puppet::Resource instance" do Puppet::Type.type(:mount).must respond_to(:hash2resource) end describe "when converting a hash to a Puppet::Resource instance" do before do @type = Puppet::Type.type(:mount) end it "should treat a :title key as the title of the resource" do @type.hash2resource(:name => "/foo", :title => "foo").title.should == "foo" end it "should use the name from the hash as the title if no explicit title is provided" do @type.hash2resource(:name => "foo").title.should == "foo" end it "should use the Resource Type's namevar to determine how to find the name in the hash" do @type.stubs(:key_attributes).returns([ :myname ]) @type.hash2resource(:myname => "foo").title.should == "foo" end [:catalog].each do |attr| it "should use any provided #{attr}" do @type.hash2resource(:name => "foo", attr => "eh").send(attr).should == "eh" end end it "should set all provided parameters on the resource" do @type.hash2resource(:name => "foo", :fstype => "boo", :boot => "fee").to_hash.should == {:name => "foo", :fstype => "boo", :boot => "fee"} end it "should not set the title as a parameter on the resource" do @type.hash2resource(:name => "foo", :title => "eh")[:title].should be_nil end it "should not set the catalog as a parameter on the resource" do @type.hash2resource(:name => "foo", :catalog => "eh")[:catalog].should be_nil end it "should treat hash keys equivalently whether provided as strings or symbols" do resource = @type.hash2resource("name" => "foo", "title" => "eh", "fstype" => "boo") resource.title.should == "eh" resource[:name].should == "foo" resource[:fstype].should == "boo" end end describe "when retrieving current property values" do before do @resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) @resource.property(:ensure).stubs(:retrieve).returns :absent end it "should fail if its provider is unsuitable" do @resource = Puppet::Type.type(:mount).new(:name => "foo", :fstype => "bar", :pass => 1, :ensure => :present) @resource.provider.class.expects(:suitable?).returns false lambda { @resource.retrieve }.should raise_error(Puppet::Error) end it "should return a Puppet::Resource instance with its type and title set appropriately" do result = @resource.retrieve result.should be_instance_of(Puppet::Resource) result.type.should == "Mount" result.title.should == "foo" end it "should set the name of the returned resource if its own name and title differ" do @resource[:name] = "my name" @resource.title = "other name" @resource.retrieve[:name].should == "my name" end it "should provide a value for all set properties" do values = @resource.retrieve [:ensure, :fstype, :pass].each { |property| values[property].should_not be_nil } end it "should provide a value for 'ensure' even if no desired value is provided" do @resource = Puppet::Type.type(:file).new(:path => "/my/file/that/can't/exist") end it "should not call retrieve on non-ensure properties if the resource is absent and should consider the property absent" do @resource.property(:ensure).expects(:retrieve).returns :absent @resource.property(:fstype).expects(:retrieve).never @resource.retrieve[:fstype].should == :absent end it "should include the result of retrieving each property's current value if the resource is present" do @resource.property(:ensure).expects(:retrieve).returns :present @resource.property(:fstype).expects(:retrieve).returns 15 @resource.retrieve[:fstype] == 15 end end describe "when in a catalog" do before do @catalog = Puppet::Resource::Catalog.new @container = Puppet::Type.type(:component).new(:name => "container") @one = Puppet::Type.type(:file).new(:path => "/file/one") @two = Puppet::Type.type(:file).new(:path => "/file/two") @catalog.add_resource @container @catalog.add_resource @one @catalog.add_resource @two @catalog.add_edge @container, @one @catalog.add_edge @container, @two end it "should have no parent if there is no in edge" do @container.parent.should be_nil end it "should set its parent to its in edge" do @one.parent.ref.should == @container.ref end after do @catalog.clear(true) end end it "should have a 'stage' metaparam" do Puppet::Type.metaparamclass(:stage).should be_instance_of(Class) end end describe Puppet::Type::RelationshipMetaparam do it "should be a subclass of Puppet::Parameter" do Puppet::Type::RelationshipMetaparam.superclass.should equal(Puppet::Parameter) end it "should be able to produce a list of subclasses" do Puppet::Type::RelationshipMetaparam.should respond_to(:subclasses) end describe "when munging relationships" do before do @resource = Puppet::Type.type(:mount).new :name => "/foo" @metaparam = Puppet::Type.metaparamclass(:require).new :resource => @resource end it "should accept Puppet::Resource instances" do ref = Puppet::Resource.new(:file, "/foo") @metaparam.munge(ref)[0].should equal(ref) end it "should turn any string into a Puppet::Resource" do @metaparam.munge("File[/ref]")[0].should be_instance_of(Puppet::Resource) end end it "should be able to validate relationships" do Puppet::Type.metaparamclass(:require).new(:resource => mock("resource")).should respond_to(:validate_relationship) end it "should fail if any specified resource is not found in the catalog" do catalog = mock 'catalog' resource = stub 'resource', :catalog => catalog, :ref => "resource" param = Puppet::Type.metaparamclass(:require).new(:resource => resource, :value => %w{Foo[bar] Class[test]}) catalog.expects(:resource).with("Foo[bar]").returns "something" catalog.expects(:resource).with("Class[Test]").returns nil param.expects(:fail).with { |string| string.include?("Class[Test]") } param.validate_relationship end end + +describe Puppet::Type.metaparamclass(:check) do + it "should warn and create an instance of ':audit'" do + file = Puppet::Type.type(:file).new :path => "/foo" + file.expects(:warning) + file[:check] = :mode + file[:audit].should == [:mode] + end +end + +describe Puppet::Type.metaparamclass(:audit) do + before do + @resource = Puppet::Type.type(:file).new :path => "/foo" + end + + it "should default to being nil" do + @resource[:audit].should be_nil + end + + it "should specify all possible properties when asked to audit all properties" do + @resource[:audit] = :all + + list = @resource.class.properties.collect { |p| p.name } + @resource[:audit].should == list + end + + it "should fail if asked to audit an invalid property" do + lambda { @resource[:audit] = :foobar }.should raise_error(Puppet::Error) + end + + it "should create an attribute instance for each auditable property" do + @resource[:audit] = :mode + @resource.parameter(:mode).should_not be_nil + end + + it "should accept properties specified as a string" do + @resource[:audit] = "mode" + @resource.parameter(:mode).should_not be_nil + end + + it "should not create attribute instances for parameters, only properties" do + @resource[:audit] = :noop + @resource.parameter(:noop).should be_nil + end +end