diff --git a/lib/puppet/application/inspect.rb b/lib/puppet/application/inspect.rb new file mode 100644 index 000000000..c28fef326 --- /dev/null +++ b/lib/puppet/application/inspect.rb @@ -0,0 +1,80 @@ +require 'puppet/application' + +class Puppet::Application::Inspect < Puppet::Application + + should_parse_config + run_mode :agent + + option("--debug","-d") + option("--verbose","-v") + + option("--logdest LOGDEST", "-l") do |arg| + begin + Puppet::Util::Log.newdestination(arg) + options[:logset] = true + rescue => detail + $stderr.puts detail.to_s + end + end + + def setup + exit(Puppet.settings.print_configs ? 0 : 1) if Puppet.settings.print_configs? + + raise "Inspect requires reporting to be enabled. Set report=true in puppet.conf to enable reporting." unless Puppet[:report] + + @report = Puppet::Transaction::Report.new("inspect") + + Puppet::Util::Log.newdestination(@report) + Puppet::Util::Log.newdestination(:console) unless options[:logset] + + trap(:INT) do + $stderr.puts "Exiting" + exit(1) + end + + if options[:debug] + Puppet::Util::Log.level = :debug + elsif options[:verbose] + Puppet::Util::Log.level = :info + end + + Puppet::Transaction::Report.terminus_class = :rest + Puppet::Resource::Catalog.terminus_class = :yaml + end + + def run_command + retrieval_starttime = Time.now + + unless catalog = Puppet::Resource::Catalog.find(Puppet[:certname]) + raise "Could not find catalog for #{Puppet[:certname]}" + end + + retrieval_time = Time.now - retrieval_starttime + @report.add_times("config_retrieval", retrieval_time) + + starttime = Time.now + + catalog.to_ral.resources.each do |ral_resource| + audited_attributes = ral_resource[:audit] + next unless audited_attributes + + audited_resource = ral_resource.to_resource + + status = Puppet::Resource::Status.new(ral_resource) + audited_attributes.each do |name| + event = ral_resource.event(:previous_value => audited_resource[name], :property => name, :status => "audit", :message => "inspected value is #{audited_resource[name].inspect}") + status.add_event(event) + end + @report.add_resource_status(status) + end + + @report.add_metric(:time, {"config_retrieval" => retrieval_time, "inspect" => Time.now - starttime}) + + begin + @report.save + rescue => detail + puts detail.backtrace if Puppet[:trace] + Puppet.err "Could not send report: #{detail}" + end + end +end diff --git a/lib/puppet/configurer/plugin_handler.rb b/lib/puppet/configurer/plugin_handler.rb index 539441e75..cfc6b5a0b 100644 --- a/lib/puppet/configurer/plugin_handler.rb +++ b/lib/puppet/configurer/plugin_handler.rb @@ -1,28 +1,26 @@ # Break out the code related to plugins. This module is # just included into the agent, but having it here makes it # easier to test. module Puppet::Configurer::PluginHandler def download_plugins? Puppet[:pluginsync] end # Retrieve facts from the central server. def download_plugins return nil unless download_plugins? Puppet::Configurer::Downloader.new("plugin", Puppet[:plugindest], Puppet[:pluginsource], Puppet[:pluginsignore]).evaluate.each { |file| load_plugin(file) } end def load_plugin(file) return unless FileTest.exist?(file) return if FileTest.directory?(file) begin Puppet.info "Loading downloaded plugin #{file}" load file - rescue SystemExit,NoMemoryError - raise rescue Exception => detail Puppet.err "Could not load downloaded file #{file}: #{detail}" end end end diff --git a/lib/puppet/external/pson/pure/generator.rb b/lib/puppet/external/pson/pure/generator.rb index 4180be57d..89a0c62e0 100644 --- a/lib/puppet/external/pson/pure/generator.rb +++ b/lib/puppet/external/pson/pure/generator.rb @@ -1,422 +1,401 @@ module PSON MAP = { "\x0" => '\u0000', "\x1" => '\u0001', "\x2" => '\u0002', "\x3" => '\u0003', "\x4" => '\u0004', "\x5" => '\u0005', "\x6" => '\u0006', "\x7" => '\u0007', "\b" => '\b', "\t" => '\t', "\n" => '\n', "\xb" => '\u000b', "\f" => '\f', "\r" => '\r', "\xe" => '\u000e', "\xf" => '\u000f', "\x10" => '\u0010', "\x11" => '\u0011', "\x12" => '\u0012', "\x13" => '\u0013', "\x14" => '\u0014', "\x15" => '\u0015', "\x16" => '\u0016', "\x17" => '\u0017', "\x18" => '\u0018', "\x19" => '\u0019', "\x1a" => '\u001a', "\x1b" => '\u001b', "\x1c" => '\u001c', "\x1d" => '\u001d', "\x1e" => '\u001e', "\x1f" => '\u001f', '"' => '\"', '\\' => '\\\\', } # :nodoc: # Convert a UTF8 encoded Ruby string _string_ to a PSON string, encoded with # UTF16 big endian characters as \u????, and return it. if String.method_defined?(:force_encoding) def utf8_to_pson(string) # :nodoc: string = string.dup string << '' # XXX workaround: avoid buffer sharing string.force_encoding(Encoding::ASCII_8BIT) string.gsub!(/["\\\x0-\x1f]/) { MAP[$MATCH] } - string.gsub!(/( - (?: - [\xc2-\xdf][\x80-\xbf] | - [\xe0-\xef][\x80-\xbf]{2} | - [\xf0-\xf4][\x80-\xbf]{3} - )+ | - [\x80-\xc1\xf5-\xff] # invalid - )/nx) { |c| - c.size == 1 and raise GeneratorError, "invalid utf8 byte: '#{c}'" - s = PSON::UTF8toUTF16.iconv(c).unpack('H*')[0] - s.gsub!(/.{4}/n, '\\\\u\&') - } - string.force_encoding(Encoding::UTF_8) string rescue Iconv::Failure => e raise GeneratorError, "Caught #{e.class}: #{e}" end else def utf8_to_pson(string) # :nodoc: - string. - gsub(/["\\\x0-\x1f]/n) { MAP[$MATCH] }. - gsub(/((?: - [\xc2-\xdf][\x80-\xbf] | - [\xe0-\xef][\x80-\xbf]{2} | - [\xf0-\xf4][\x80-\xbf]{3} - )+)/nx) { |c| - PSON::UTF8toUTF16.iconv(c).unpack('H*')[0].gsub(/.{4}/n, '\\\\u\&') - } + string.gsub(/["\\\x0-\x1f]/n) { MAP[$MATCH] } end end module_function :utf8_to_pson module Pure module Generator # This class is used to create State instances, that are use to hold data # while generating a PSON text from a a Ruby data structure. class State # Creates a State object from _opts_, which ought to be Hash to create # a new State instance configured by _opts_, something else to create # an unconfigured instance. If _opts_ is a State object, it is just # returned. def self.from_state(opts) case opts when self opts when Hash new(opts) else new end end # Instantiates a new State object, configured by _opts_. # # _opts_ can have the following keys: # # * *indent*: a string used to indent levels (default: ''), # * *space*: a string that is put after, a : or , delimiter (default: ''), # * *space_before*: a string that is put before a : pair delimiter (default: ''), # * *object_nl*: a string that is put at the end of a PSON object (default: ''), # * *array_nl*: a string that is put at the end of a PSON array (default: ''), # * *check_circular*: true if checking for circular data structures # should be done (the default), false otherwise. # * *check_circular*: true if checking for circular data structures # should be done, false (the default) otherwise. # * *allow_nan*: true if NaN, Infinity, and -Infinity should be # generated, otherwise an exception is thrown, if these values are # encountered. This options defaults to false. def initialize(opts = {}) @seen = {} @indent = '' @space = '' @space_before = '' @object_nl = '' @array_nl = '' @check_circular = true @allow_nan = false configure opts end # This string is used to indent levels in the PSON text. attr_accessor :indent # This string is used to insert a space between the tokens in a PSON # string. attr_accessor :space # This string is used to insert a space before the ':' in PSON objects. attr_accessor :space_before # This string is put at the end of a line that holds a PSON object (or # Hash). attr_accessor :object_nl # This string is put at the end of a line that holds a PSON array. attr_accessor :array_nl # This integer returns the maximum level of data structure nesting in # the generated PSON, max_nesting = 0 if no maximum is checked. attr_accessor :max_nesting def check_max_nesting(depth) # :nodoc: return if @max_nesting.zero? current_nesting = depth + 1 current_nesting > @max_nesting and raise NestingError, "nesting of #{current_nesting} is too deep" end # Returns true, if circular data structures should be checked, # otherwise returns false. def check_circular? @check_circular end # Returns true if NaN, Infinity, and -Infinity should be considered as # valid PSON and output. def allow_nan? @allow_nan end # Returns _true_, if _object_ was already seen during this generating # run. def seen?(object) @seen.key?(object.__id__) end # Remember _object_, to find out if it was already encountered (if a # cyclic data structure is if a cyclic data structure is rendered). def remember(object) @seen[object.__id__] = true end # Forget _object_ for this generating run. def forget(object) @seen.delete object.__id__ end # Configure this State instance with the Hash _opts_, and return # itself. def configure(opts) @indent = opts[:indent] if opts.key?(:indent) @space = opts[:space] if opts.key?(:space) @space_before = opts[:space_before] if opts.key?(:space_before) @object_nl = opts[:object_nl] if opts.key?(:object_nl) @array_nl = opts[:array_nl] if opts.key?(:array_nl) @check_circular = !!opts[:check_circular] if opts.key?(:check_circular) @allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan) if !opts.key?(:max_nesting) # defaults to 19 @max_nesting = 19 elsif opts[:max_nesting] @max_nesting = opts[:max_nesting] else @max_nesting = 0 end self end # Returns the configuration instance variables as a hash, that can be # passed to the configure method. def to_h result = {} for iv in %w{indent space space_before object_nl array_nl check_circular allow_nan max_nesting} result[iv.intern] = instance_variable_get("@#{iv}") end result end end module GeneratorMethods module Object # Converts this object to a string (calling #to_s), converts # it to a PSON string, and returns the result. This is a fallback, if no # special method #to_pson was defined for some object. def to_pson(*) to_s.to_pson end end module Hash # Returns a PSON string containing a PSON object, that is unparsed from # this Hash instance. # _state_ is a PSON::State object, that can also be used to configure the # produced PSON string output further. # _depth_ is used to find out nesting depth, to indent accordingly. def to_pson(state = nil, depth = 0, *) if state state = PSON.state.from_state(state) state.check_max_nesting(depth) pson_check_circular(state) { pson_transform(state, depth) } else pson_transform(state, depth) end end private def pson_check_circular(state) if state and state.check_circular? state.seen?(self) and raise PSON::CircularDatastructure, "circular data structures not supported!" state.remember self end yield ensure state and state.forget self end def pson_shift(state, depth) state and not state.object_nl.empty? or return '' state.indent * depth end def pson_transform(state, depth) delim = ',' if state delim << state.object_nl result = '{' result << state.object_nl result << map { |key,value| s = pson_shift(state, depth + 1) s << key.to_s.to_pson(state, depth + 1) s << state.space_before s << ':' s << state.space s << value.to_pson(state, depth + 1) }.join(delim) result << state.object_nl result << pson_shift(state, depth) result << '}' else result = '{' result << map { |key,value| key.to_s.to_pson << ':' << value.to_pson }.join(delim) result << '}' end result end end module Array # Returns a PSON string containing a PSON array, that is unparsed from # this Array instance. # _state_ is a PSON::State object, that can also be used to configure the # produced PSON string output further. # _depth_ is used to find out nesting depth, to indent accordingly. def to_pson(state = nil, depth = 0, *) if state state = PSON.state.from_state(state) state.check_max_nesting(depth) pson_check_circular(state) { pson_transform(state, depth) } else pson_transform(state, depth) end end private def pson_check_circular(state) if state and state.check_circular? state.seen?(self) and raise PSON::CircularDatastructure, "circular data structures not supported!" state.remember self end yield ensure state and state.forget self end def pson_shift(state, depth) state and not state.array_nl.empty? or return '' state.indent * depth end def pson_transform(state, depth) delim = ',' if state delim << state.array_nl result = '[' result << state.array_nl result << map { |value| pson_shift(state, depth + 1) << value.to_pson(state, depth + 1) }.join(delim) result << state.array_nl result << pson_shift(state, depth) result << ']' else '[' << map { |value| value.to_pson }.join(delim) << ']' end end end module Integer # Returns a PSON string representation for this Integer number. def to_pson(*) to_s end end module Float # Returns a PSON string representation for this Float number. def to_pson(state = nil, *) case when infinite? if !state || state.allow_nan? to_s else raise GeneratorError, "#{self} not allowed in PSON" end when nan? if !state || state.allow_nan? to_s else raise GeneratorError, "#{self} not allowed in PSON" end else to_s end end end module String # This string should be encoded with UTF-8 A call to this method # returns a PSON string encoded with UTF16 big endian characters as # \u????. def to_pson(*) '"' << PSON.utf8_to_pson(self) << '"' end # Module that holds the extinding methods if, the String module is # included. module Extend # Raw Strings are PSON Objects (the raw bytes are stored in an array for the # key "raw"). The Ruby String can be created by this module method. def pson_create(o) o['raw'].pack('C*') end end # Extends _modul_ with the String::Extend module. def self.included(modul) modul.extend Extend end # This method creates a raw object hash, that can be nested into # other data structures and will be unparsed as a raw string. This # method should be used, if you want to convert raw strings to PSON # instead of UTF-8 strings, e. g. binary data. def to_pson_raw_object { PSON.create_id => self.class.name, 'raw' => self.unpack('C*'), } end # This method creates a PSON text from the result of # a call to to_pson_raw_object of this String. def to_pson_raw(*args) to_pson_raw_object.to_pson(*args) end end module TrueClass # Returns a PSON string for true: 'true'. def to_pson(*) 'true' end end module FalseClass # Returns a PSON string for false: 'false'. def to_pson(*) 'false' end end module NilClass # Returns a PSON string for nil: 'null'. def to_pson(*) 'null' end end end end end end diff --git a/lib/puppet/indirector/catalog/active_record.rb b/lib/puppet/indirector/catalog/active_record.rb index fabb08eb9..f814f4aff 100644 --- a/lib/puppet/indirector/catalog/active_record.rb +++ b/lib/puppet/indirector/catalog/active_record.rb @@ -1,41 +1,41 @@ require 'puppet/rails/host' require 'puppet/indirector/active_record' require 'puppet/resource/catalog' class Puppet::Resource::Catalog::ActiveRecord < Puppet::Indirector::ActiveRecord use_ar_model Puppet::Rails::Host # If we can find the host, then return a catalog with the host's resources # as the vertices. def find(request) return nil unless request.options[:cache_integration_hack] return nil unless host = ar_model.find_by_name(request.key) catalog = Puppet::Resource::Catalog.new(host.name) host.resources.each do |resource| catalog.add_resource resource.to_transportable end catalog end # Save the values from a Facts instance as the facts on a Rails Host instance. def save(request) catalog = request.instance host = ar_model.find_by_name(catalog.name) || ar_model.create(:name => catalog.name) host.railsmark "Saved catalog to database" do host.merge_resources(catalog.vertices) host.last_compile = Time.now if node = Puppet::Node.find(catalog.name) host.ip = node.parameters["ipaddress"] - host.environment = node.environment + host.environment = node.environment.to_s end host.save end end end diff --git a/lib/puppet/resource/type_collection.rb b/lib/puppet/resource/type_collection.rb index 63d110395..6a03458b3 100644 --- a/lib/puppet/resource/type_collection.rb +++ b/lib/puppet/resource/type_collection.rb @@ -1,214 +1,213 @@ class Puppet::Resource::TypeCollection attr_reader :environment def clear @hostclasses.clear @definitions.clear @nodes.clear end def initialize(env) @environment = env.is_a?(String) ? Puppet::Node::Environment.new(env) : env @hostclasses = {} @definitions = {} @nodes = {} # So we can keep a list and match the first-defined regex @node_list = [] @watched_files = {} end def <<(thing) add(thing) self end def add(instance) if instance.type == :hostclass and other = @hostclasses[instance.name] and other.type == :hostclass other.merge(instance) return other end method = "add_#{instance.type}" send(method, instance) instance.resource_type_collection = self instance end def add_hostclass(instance) dupe_check(instance, @hostclasses) { |dupe| "Class '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined as a class" } @hostclasses[instance.name] = instance instance end def hostclass(name) @hostclasses[munge_name(name)] end def add_node(instance) dupe_check(instance, @nodes) { |dupe| "Node '#{instance.name}' is already defined#{dupe.error_context}; cannot redefine" } @node_list << instance @nodes[instance.name] = instance instance end def loader require 'puppet/parser/type_loader' @loader ||= Puppet::Parser::TypeLoader.new(environment) end def node(name) name = munge_name(name) if node = @nodes[name] return node end @node_list.each do |node| next unless node.name_is_regex? return node if node.match(name) end nil end def node_exists?(name) @nodes[munge_name(name)] end def nodes? @nodes.length > 0 end def add_definition(instance) dupe_check(instance, @hostclasses) { |dupe| "'#{instance.name}' is already defined#{dupe.error_context} as a class; cannot redefine as a definition" } dupe_check(instance, @definitions) { |dupe| "Definition '#{instance.name}' is already defined#{dupe.error_context}; cannot be redefined" } @definitions[instance.name] = instance end def definition(name) @definitions[munge_name(name)] end def find(namespaces, name, type) #Array("") == [] for some reason namespaces = [namespaces] unless namespaces.is_a?(Array) if name =~ /^::/ return send(type, name.sub(/^::/, '')) end namespaces.each do |namespace| ary = namespace.split("::") while ary.length > 0 tmp_namespace = ary.join("::") if r = find_partially_qualified(tmp_namespace, name, type) return r end # Delete the second to last object, which reduces our namespace by one. ary.pop end if result = send(type, name) return result end end nil end def find_or_load(namespaces, name, type) name = name.downcase namespaces = [namespaces] unless namespaces.is_a?(Array) namespaces = namespaces.collect { |ns| ns.downcase } # This could be done in the load_until, but the knowledge seems to # belong here. if r = find(namespaces, name, type) return r end loader.load_until(namespaces, name) { find(namespaces, name, type) } end def find_node(namespaces, name) find("", name, :node) end def find_hostclass(namespaces, name) find_or_load(namespaces, name, :hostclass) end def find_definition(namespaces, name) find_or_load(namespaces, name, :definition) end [:hostclasses, :nodes, :definitions].each do |m| define_method(m) do instance_variable_get("@#{m}").dup end end def perform_initial_import - return if Puppet.settings[:ignoreimport] parser = Puppet::Parser::Parser.new(environment) if code = Puppet.settings.uninterpolated_value(:code, environment.to_s) and code != "" parser.string = code else file = Puppet.settings.value(:manifest, environment.to_s) return unless File.exist?(file) parser.file = file end parser.parse rescue => detail msg = "Could not parse for environment #{environment}: #{detail}" error = Puppet::Error.new(msg) error.set_backtrace(detail.backtrace) raise error end def stale? @watched_files.values.detect { |file| file.changed? } end def version return @version if defined?(@version) if environment[:config_version] == "" @version = Time.now.to_i return @version end @version = Puppet::Util.execute([environment[:config_version]]).strip rescue Puppet::ExecutionFailure => e raise Puppet::ParseError, "Unable to set config_version: #{e.message}" end def watch_file(file) @watched_files[file] = Puppet::Util::LoadedFile.new(file) end def watching_file?(file) @watched_files.include?(file) end private def find_partially_qualified(namespace, name, type) send(type, [namespace, name].join("::")) end def munge_name(name) name.to_s.downcase end def dupe_check(instance, hash) return unless dupe = hash[instance.name] message = yield dupe instance.fail Puppet::ParseError, message end end diff --git a/lib/puppet/transaction/change.rb b/lib/puppet/transaction/change.rb index ecc3b5a5f..d57ac1917 100644 --- a/lib/puppet/transaction/change.rb +++ b/lib/puppet/transaction/change.rb @@ -1,87 +1,75 @@ 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, :auditing + attr_accessor :is, :should, :property, :proxy, :auditing, :old_audit_value 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 + event = property.event + event.previous_value = is + event.desired_value = should + event.historical_value = old_audit_value + + if auditing? and old_audit_value != is + event.message = "audit change: previously recorded value #{property.is_to_s(old_audit_value)} has been changed to #{property.is_to_s(is)}" + event.status = "audit" + event.audited = true + brief_audit_message = " (previously recorded value was #{property.is_to_s(old_audit_value)})" + else + brief_audit_message = "" + end + + if property.insync?(is) + # nothing happens + elsif noop? + event.message = "is #{property.is_to_s(is)}, should be #{property.should_to_s(should)} (noop)#{brief_audit_message}" + event.status = "noop" + else + property.sync + event.message = [ property.change_to_s(is, should), brief_audit_message ].join + event.status = "success" + end + event rescue => detail puts detail.backtrace if Puppet[:trace] - result = event - result.status = "failure" + event.status = "failure" - result.message = "change from #{property.is_to_s(is)} to #{property.should_to_s(should)} failed: #{detail}" - result.send_log - result + event.message = "change from #{property.is_to_s(is)} to #{property.should_to_s(should)} failed: #{detail}" + event + ensure + event.send_log end # Is our property noop? This is used for generating special events. def noop? @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 "change #{@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 - 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 - result - end end diff --git a/lib/puppet/transaction/event.rb b/lib/puppet/transaction/event.rb index e5e5793da..da5b14727 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] + ATTRIBUTES = [:name, :resource, :property, :previous_value, :desired_value, :historical_value, :status, :message, :node, :version, :file, :line, :source_description, :audited] attr_accessor *ATTRIBUTES attr_writer :tags attr_accessor :time attr_reader :default_log_level 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/report.rb b/lib/puppet/transaction/report.rb index e6d1e0528..75c08fc7a 100644 --- a/lib/puppet/transaction/report.rb +++ b/lib/puppet/transaction/report.rb @@ -1,149 +1,150 @@ require 'puppet' require 'puppet/indirector' # A class for reporting what happens on each client. Reports consist of # two types of data: Logs and Metrics. Logs are the output that each # change produces, and Metrics are all of the numerical data involved # in the transaction. class Puppet::Transaction::Report extend Puppet::Indirector indirects :report, :terminus_class => :processor - attr_reader :resource_statuses, :logs, :metrics, :host, :time + attr_reader :resource_statuses, :logs, :metrics, :host, :time, :kind # This is necessary since Marshall doesn't know how to # dump hash with default proc (see below @records) def self.default_format :yaml end def <<(msg) @logs << msg self end def add_times(name, value) @external_times[name] = value end def add_metric(name, hash) metric = Puppet::Util::Metric.new(name) hash.each do |name, value| metric.newvalue(name, value) end @metrics[metric.name] = metric metric end def add_resource_status(status) @resource_statuses[status.resource] = status end def calculate_metrics calculate_resource_metrics calculate_time_metrics calculate_change_metrics calculate_event_metrics end - def initialize + def initialize(kind = "apply") @metrics = {} @logs = [] @resource_statuses = {} @external_times ||= {} @host = Puppet[:certname] @time = Time.now + @kind = kind end def name host end # Provide a summary of this report. def summary ret = "" @metrics.sort { |a,b| a[1].label <=> b[1].label }.each do |name, metric| ret += "#{metric.label}:\n" metric.values.sort { |a,b| # sort by label if a[0] == :total 1 elsif b[0] == :total -1 else a[1] <=> b[1] end }.each do |name, label, value| next if value == 0 value = "%0.2f" % value if value.is_a?(Float) ret += " %15s %s\n" % [label + ":", value] end end ret end # Based on the contents of this report's metrics, compute a single number # that represents the report. The resulting number is a bitmask where # individual bits represent the presence of different metrics. def exit_status status = 0 status |= 2 if @metrics["changes"][:total] > 0 status |= 4 if @metrics["resources"][:failed] > 0 status end private def calculate_change_metrics metrics = Hash.new(0) resource_statuses.each do |name, status| metrics[:total] += status.change_count if status.change_count end add_metric(:changes, metrics) end def calculate_event_metrics metrics = Hash.new(0) resource_statuses.each do |name, status| metrics[:total] += status.events.length status.events.each do |event| metrics[event.status] += 1 end end add_metric(:events, metrics) end def calculate_resource_metrics metrics = Hash.new(0) metrics[:total] = resource_statuses.length resource_statuses.each do |name, status| Puppet::Resource::Status::STATES.each do |state| metrics[state] += 1 if status.send(state) end end add_metric(:resources, metrics) end def calculate_time_metrics metrics = Hash.new(0) resource_statuses.each do |name, status| type = Puppet::Resource.new(name).type metrics[type.to_s.downcase] += status.evaluation_time if status.evaluation_time end @external_times.each do |name, value| metrics[name.to_s.downcase] = value end add_metric(:time, metrics) end end diff --git a/lib/puppet/transaction/resource_harness.rb b/lib/puppet/transaction/resource_harness.rb index 29ec9a539..c978e5545 100644 --- a/lib/puppet/transaction/resource_harness.rb +++ b/lib/puppet/transaction/resource_harness.rb @@ -1,150 +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" false end def apply_changes(status, changes) changes.each do |change| status << change.apply cache(change.property.resource, change.property.name, change.is) if change.auditing? end status.changed = true end - # Used mostly for scheduling at this point. + # Used mostly for scheduling and auditing at this point. def cached(resource, name) Puppet::Util::Storage.cache(resource)[name] end - # Used mostly for scheduling at this point. + # Used mostly for scheduling and auditing 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_resource 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) + unless ensure_is_insync?(current, param) + audited.keys.reject{|name| name == :ensure}.each do |name| + resource.parameter(name).notice "audit change: previously recorded value #{audited[name]} has been changed to #{current[param]}" + cache(resource, name, current[param]) + end + return [Puppet::Transaction::Change.new(param, current[:ensure])] + end return [] if ensure_should_be_absent?(current, param) end - resource.properties.reject { |p| p.name == :ensure }.reject do |param| - param.should.nil? - end.reject do |param| - param_is_insync?(current, param) + resource.properties.reject { |param| param.name == :ensure }.select do |param| + (audited.include?(param.name) && audited[param.name] != current[param.name]) || (param.should != nil && !param_is_insync?(current, param)) end.collect do |param| change = Puppet::Transaction::Change.new(param, current[param.name]) change.auditing = true if audited.include?(param.name) + change.old_audit_value = audited[param.name] change end end def copy_audited_parameters(resource, current) - return [] unless audit = resource[:audit] + return {} unless audit = resource[:audit] audit = Array(audit).collect { |p| p.to_sym } - audited = [] + audited = {} audit.find_all do |param| - next if resource[param] - if value = cached(resource, param) - resource[param] = value - audited << param + audited[param] = value else - resource.debug "Storing newly-audited value #{current[param]} for #{param}" + resource.property(param).notice "audit change: newly-recorded recorded value #{current[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). 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/file.rb b/lib/puppet/type/file.rb index f35a26408..6523c99a0 100644 --- a/lib/puppet/type/file.rb +++ b/lib/puppet/type/file.rb @@ -1,798 +1,799 @@ require 'digest/md5' require 'cgi' require 'etc' require 'uri' require 'fileutils' require 'puppet/network/handler' require 'puppet/util/diff' require 'puppet/util/checksums' require 'puppet/network/client' require 'puppet/util/backups' Puppet::Type.newtype(:file) do include Puppet::Util::MethodHelper include Puppet::Util::Checksums include Puppet::Util::Backups @doc = "Manages local files, including setting ownership and permissions, creation of both files and directories, and retrieving entire files from remote servers. As Puppet matures, it expected that the `file` resource will be used less and less to manage content, and instead native resources will be used to do so. If you find that you are often copying files in from a central location, rather than using native resources, please contact Puppet Labs and we can hopefully work with you to develop a native resource to support what you are doing." def self.title_patterns [ [ /^(.*?)\/*\Z/m, [ [ :path, lambda{|x| x} ] ] ] ] end newparam(:path) do desc "The path to the file to manage. Must be fully qualified." isnamevar validate do |value| # accept various path syntaxes: lone slash, posix, win32, unc unless (Puppet.features.posix? and (value =~ /^\/$/ or value =~ /^\/[^\/]/)) or (Puppet.features.microsoft_windows? and (value =~ /^.:\// or value =~ /^\/\/[^\/]+\/[^\/]+/)) fail Puppet::Error, "File paths must be fully qualified, not '#{value}'" end end # convert the current path in an index into the collection and the last # path name. The aim is to use less storage for all common paths in a hierarchy munge do |value| path, name = File.split(value.gsub(/\/+/,'/')) { :index => Puppet::FileCollection.collection.index(path), :name => name } end # and the reverse unmunge do |value| basedir = Puppet::FileCollection.collection.path(value[:index]) # a lone slash as :name indicates a root dir on windows if value[:name] == '/' basedir else File.join( basedir, value[:name] ) end end end newparam(:backup) do desc "Whether files should be backed up before being replaced. The preferred method of backing files up is via a `filebucket`, which stores files by their MD5 sums and allows easy retrieval without littering directories with backups. You can specify a local filebucket or a network-accessible server-based filebucket by setting `backup => bucket-name`. Alternatively, if you specify any value that begins with a `.` (e.g., `.puppet-bak`), then Puppet will use copy the file in the same directory with that value as the extension of the backup. Setting `backup => false` disables all backups of the file in question. Puppet automatically creates a local filebucket named `puppet` and defaults to backing up there. To use a server-based filebucket, you must specify one in your configuration filebucket { main: server => puppet } The `puppet master` daemon creates a filebucket by default, so you can usually back up to your main server with this configuration. Once you've described the bucket in your configuration, you can use it in any file file { \"/my/file\": source => \"/path/in/nfs/or/something\", backup => main } This will back the file up to the central server. At this point, the benefits of using a filebucket are that you do not have backup files lying around on each of your machines, a given version of a file is only backed up once, and you can restore any given file manually, no matter how old. Eventually, transactional support will be able to automatically restore filebucketed files. " defaultto "puppet" munge do |value| # I don't really know how this is happening. value = value.shift if value.is_a?(Array) case value when false, "false", :false false when true, "true", ".puppet-bak", :true ".puppet-bak" when String value else self.fail "Invalid backup type #{value.inspect}" end end end newparam(:recurse) do desc "Whether and how deeply to do recursive management." newvalues(:true, :false, :inf, :remote, /^[0-9]+$/) # Replace the validation so that we allow numbers in # addition to string representations of them. validate { |arg| } munge do |value| newval = super(value) case newval when :true, :inf; true when :false; false when :remote; :remote when Integer, Fixnum, Bignum self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true when /^\d+$/ self.warning "Setting recursion depth with the recurse parameter is now deprecated, please use recurselimit" value = Integer(value) # recurse == 0 means no recursion return false if value == 0 resource[:recurselimit] = value true else self.fail "Invalid recurse value #{value.inspect}" end end end newparam(:recurselimit) do desc "How deeply to do recursive management." newvalues(/^[0-9]+$/) munge do |value| newval = super(value) case newval when Integer, Fixnum, Bignum; value when /^\d+$/; Integer(value) else self.fail "Invalid recurselimit value #{value.inspect}" end end end newparam(:replace, :boolean => true) do desc "Whether or not to replace a file that is sourced but exists. This is useful for using file sources purely for initialization." newvalues(:true, :false) aliasvalue(:yes, :true) aliasvalue(:no, :false) defaultto :true end newparam(:force, :boolean => true) do desc "Force the file operation. Currently only used when replacing directories with links." newvalues(:true, :false) defaultto false end newparam(:ignore) do desc "A parameter which omits action on files matching specified patterns during recursion. Uses Ruby's builtin globbing engine, so shell metacharacters are fully supported, e.g. `[a-z]*`. Matches that would descend into the directory structure are ignored, e.g., `*/*`." validate do |value| unless value.is_a?(Array) or value.is_a?(String) or value == false self.devfail "Ignore must be a string or an Array" end end end newparam(:links) do desc "How to handle links during file actions. During file copying, `follow` will copy the target file instead of the link, `manage` will copy the link itself, and `ignore` will just pass it by. When not copying, `manage` and `ignore` behave equivalently (because you cannot really ignore links entirely during local recursion), and `follow` will manage the file to which the link points." newvalues(:follow, :manage) defaultto :manage end newparam(:purge, :boolean => true) do desc "Whether unmanaged files should be purged. If you have a filebucket configured the purged files will be uploaded, but if you do not, this will destroy data. Only use this option for generated files unless you really know what you are doing. This option only makes sense when recursively managing directories. Note that when using `purge` with `source`, Puppet will purge any files that are not on the remote system." defaultto :false newvalues(:true, :false) end newparam(:sourceselect) do desc "Whether to copy all valid sources, or just the first one. This parameter is only used in recursive copies; by default, the first valid source is the only one used as a recursive source, but if this parameter is set to `all`, then all valid sources will have all of their contents copied to the local host, and for sources that have the same file, the source earlier in the list will be used." defaultto :first newvalues(:first, :all) end # Autorequire any parent directories. autorequire(:file) do basedir = File.dirname(self[:path]) if basedir != self[:path] basedir else nil end end # Autorequire the owner and group of the file. {:user => :owner, :group => :group}.each do |type, property| autorequire(type) do if @parameters.include?(property) # The user/group property automatically converts to IDs next unless should = @parameters[property].shouldorig val = should[0] if val.is_a?(Integer) or val =~ /^\d+$/ nil else val end end end end CREATORS = [:content, :source, :target] validate do count = 0 CREATORS.each do |param| count += 1 if self.should(param) end count += 1 if @parameters.include?(:source) self.fail "You cannot specify more than one of #{CREATORS.collect { |p| p.to_s}.join(", ")}" if count > 1 self.fail "You cannot specify a remote recursion without a source" if !self[:source] and self[:recurse] == :remote self.warning "Possible error: recurselimit is set but not recurse, no recursion will happen" if !self[:recurse] and self[:recurselimit] end def self.[](path) return nil unless path super(path.gsub(/\/+/, '/').sub(/\/$/, '')) end # List files, but only one level deep. def self.instances(base = "/") return [] unless FileTest.directory?(base) files = [] Dir.entries(base).reject { |e| e == "." or e == ".." }.each do |name| path = File.join(base, name) if obj = self[path] obj[:audit] = :all files << obj else files << self.new( :name => path, :audit => :all ) end end files end @depthfirst = false # Determine the user to write files as. def asuser if self.should(:owner) and ! self.should(:owner).is_a?(Symbol) writeable = Puppet::Util::SUIDManager.asuser(self.should(:owner)) { FileTest.writable?(File.dirname(self[:path])) } # If the parent directory is writeable, then we execute # as the user in question. Otherwise we'll rely on # the 'owner' property to do things. asuser = self.should(:owner) if writeable end asuser end def bucket return @bucket if @bucket backup = self[:backup] return nil unless backup return nil if backup =~ /^\./ unless catalog or backup == "puppet" fail "Can not find filebucket for backups without a catalog" end unless catalog and filebucket = catalog.resource(:filebucket, backup) or backup == "puppet" fail "Could not find filebucket #{backup} specified in backup" end return default_bucket unless filebucket @bucket = filebucket.bucket @bucket end def default_bucket Puppet::Type.type(:filebucket).mkdefaultbucket.bucket end # Does the file currently exist? Just checks for whether # we have a stat def exist? stat ? true : false end # We have to do some extra finishing, to retrieve our bucket if # there is one. def finish # Look up our bucket, if there is one bucket super end # Create any children via recursion or whatever. def eval_generate return [] unless self.recurse? recurse #recurse.reject do |resource| # catalog.resource(:file, resource[:path]) #end.each do |child| # catalog.add_resource child # catalog.relationship_graph.add_edge self, child #end end def flush # We want to make sure we retrieve metadata anew on each transaction. @parameters.each do |name, param| param.flush if param.respond_to?(:flush) end @stat = nil end def initialize(hash) # Used for caching clients @clients = {} super # If they've specified a source, we get our 'should' values # from it. unless self[:ensure] if self[:target] self[:ensure] = :symlink elsif self[:content] self[:ensure] = :file end end @stat = nil end # Configure discovered resources to be purged. def mark_children_for_purging(children) children.each do |name, child| next if child[:source] child[:ensure] = :absent end end # Create a new file or directory object as a child to the current # object. def newchild(path) full_path = File.join(self[:path], path) # Add some new values to our original arguments -- these are the ones # set at initialization. We specifically want to exclude any param # values set by the :source property or any default values. # LAK:NOTE This is kind of silly, because the whole point here is that # the values set at initialization should live as long as the resource # but values set by default or by :source should only live for the transaction # or so. Unfortunately, we don't have a straightforward way to manage # the different lifetimes of this data, so we kludge it like this. # The right-side hash wins in the merge. options = @original_parameters.merge(:path => full_path).reject { |param, value| value.nil? } # These should never be passed to our children. [:parent, :ensure, :recurse, :recurselimit, :target, :alias, :source].each do |param| options.delete(param) if options.include?(param) end self.class.new(options) end # Files handle paths specially, because they just lengthen their # path names, rather than including the full parent's title each # time. def pathbuilder # We specifically need to call the method here, so it looks # up our parent in the catalog graph. if parent = parent() # We only need to behave specially when our parent is also # a file if parent.is_a?(self.class) # Remove the parent file name list = parent.pathbuilder list.pop # remove the parent's path info return list << self.ref else return super end else return [self.ref] end end # Should we be purging? def purge? @parameters.include?(:purge) and (self[:purge] == :true or self[:purge] == "true") end # Recursively generate a list of file resources, which will # be used to copy remote files, manage local files, and/or make links # to map to another directory. def recurse children = {} children = recurse_local if self[:recurse] != :remote if self[:target] recurse_link(children) elsif self[:source] recurse_remote(children) end # If we're purging resources, then delete any resource that isn't on the # remote system. mark_children_for_purging(children) if self.purge? result = children.values.sort { |a, b| a[:path] <=> b[:path] } remove_less_specific_files(result) end # This is to fix bug #2296, where two files recurse over the same # set of files. It's a rare case, and when it does happen you're # not likely to have many actual conflicts, which is good, because # this is a pretty inefficient implementation. def remove_less_specific_files(files) mypath = self[:path].split(File::Separator) other_paths = catalog.vertices. select { |r| r.is_a?(self.class) and r[:path] != self[:path] }. collect { |r| r[:path].split(File::Separator) }. select { |p| p[0,mypath.length] == mypath } return files if other_paths.empty? files.reject { |file| path = file[:path].split(File::Separator) other_paths.any? { |p| path[0,p.length] == p } } end # A simple method for determining whether we should be recursing. def recurse? return false unless @parameters.include?(:recurse) val = @parameters[:recurse].value !!(val and (val == true or val == :remote)) end # Recurse the target of the link. def recurse_link(children) perform_recursion(self[:target]).each do |meta| if meta.relative_path == "." self[:ensure] = :directory next end children[meta.relative_path] ||= newchild(meta.relative_path) if meta.ftype == "directory" children[meta.relative_path][:ensure] = :directory else children[meta.relative_path][:ensure] = :link children[meta.relative_path][:target] = meta.full_path end end children end # Recurse the file itself, returning a Metadata instance for every found file. def recurse_local result = perform_recursion(self[:path]) return {} unless result result.inject({}) do |hash, meta| next hash if meta.relative_path == "." hash[meta.relative_path] = newchild(meta.relative_path) hash end end # Recurse against our remote file. def recurse_remote(children) sourceselect = self[:sourceselect] total = self[:source].collect do |source| next unless result = perform_recursion(source) return if top = result.find { |r| r.relative_path == "." } and top.ftype != "directory" result.each { |data| data.source = "#{source}/#{data.relative_path}" } break result if result and ! result.empty? and sourceselect == :first result end.flatten # This only happens if we have sourceselect == :all unless sourceselect == :first found = [] total.reject! do |data| result = found.include?(data.relative_path) found << data.relative_path unless found.include?(data.relative_path) result end end total.each do |meta| if meta.relative_path == "." parameter(:source).metadata = meta next end children[meta.relative_path] ||= newchild(meta.relative_path) children[meta.relative_path][:source] = meta.source children[meta.relative_path][:checksum] = :md5 if meta.ftype == "file" children[meta.relative_path].parameter(:source).metadata = meta end children end def perform_recursion(path) Puppet::FileServing::Metadata.search( path, :links => self[:links], :recurse => (self[:recurse] == :remote ? true : self[:recurse]), :recurselimit => self[:recurselimit], :ignore => self[:ignore], :checksum_type => (self[:source] || self[:content]) ? self[:checksum] : :none ) end # Remove any existing data. This is only used when dealing with # links or directories. def remove_existing(should) return unless s = stat self.fail "Could not back up; will not replace" unless perform_backup unless should.to_s == "link" return if s.ftype.to_s == should.to_s end case s.ftype when "directory" if self[:force] == :true debug "Removing existing directory for replacement with #{should}" FileUtils.rmtree(self[:path]) else notice "Not removing directory; use 'force' to override" end when "link", "file" debug "Removing existing #{s.ftype} for replacement with #{should}" File.unlink(self[:path]) else self.fail "Could not back up files of type #{s.ftype}" end expire end def retrieve if source = parameter(:source) source.copy_source_values end super end # Set the checksum, from another property. There are multiple # properties that modify the contents of a file, and they need the # ability to make sure that the checksum value is in sync. def setchecksum(sum = nil) if @parameters.include? :checksum if sum @parameters[:checksum].checksum = sum else # If they didn't pass in a sum, then tell checksum to # figure it out. currentvalue = @parameters[:checksum].retrieve @parameters[:checksum].checksum = currentvalue end end end # Should this thing be a normal file? This is a relatively complex # way of determining whether we're trying to create a normal file, # and it's here so that the logic isn't visible in the content property. def should_be_file? return true if self[:ensure] == :file # I.e., it's set to something like "directory" return false if e = self[:ensure] and e != :present # The user doesn't really care, apparently if self[:ensure] == :present return true unless s = stat return(s.ftype == "file" ? true : false) end # If we've gotten here, then :ensure isn't set return true if self[:content] return true if stat and stat.ftype == "file" false end # Stat our file. Depending on the value of the 'links' attribute, we # use either 'stat' or 'lstat', and we expect the properties to use the # resulting stat object accordingly (mostly by testing the 'ftype' # value). cached_attr(:stat) do method = :stat # Files are the only types that support links if (self.class.name == :file and self[:links] != :follow) or self.class.name == :tidy method = :lstat end path = self[:path] begin File.send(method, self[:path]) rescue Errno::ENOENT => error return nil rescue Errno::EACCES => error warning "Could not stat; permission denied" return nil end end # We have to hack this just a little bit, because otherwise we'll get # an error when the target and the contents are created as properties on # the far side. def to_trans(retrieve = true) obj = super obj.delete(:target) if obj[:target] == :notlink obj end # Write out the file. Requires the property name for logging. # Write will be done by the content property, along with checksum computation def write(property) remove_existing(:file) use_temporary_file = write_temporary_file? if use_temporary_file path = "#{self[:path]}.puppettmp_#{rand(10000)}" path = "#{self[:path]}.puppettmp_#{rand(10000)}" while File.exists?(path) or File.symlink?(path) else path = self[:path] end mode = self.should(:mode) # might be nil umask = mode ? 000 : 022 + mode_int = mode ? mode.to_i(8) : nil - content_checksum = Puppet::Util.withumask(umask) { File.open(path, 'w', mode) { |f| write_content(f) } } + content_checksum = Puppet::Util.withumask(umask) { File.open(path, 'w', mode_int ) { |f| write_content(f) } } # And put our new file in place if use_temporary_file # This is only not true when our file is empty. begin fail_if_checksum_is_wrong(path, content_checksum) if validate_checksum? File.rename(path, self[:path]) rescue => detail fail "Could not rename temporary file #{path} to #{self[:path]}: #{detail}" ensure # Make sure the created file gets removed File.unlink(path) if FileTest.exists?(path) end end # make sure all of the modes are actually correct property_fix end private # Should we validate the checksum of the file we're writing? def validate_checksum? self[:checksum] !~ /time/ end # Make sure the file we wrote out is what we think it is. def fail_if_checksum_is_wrong(path, content_checksum) newsum = parameter(:checksum).sum_file(path) return if [:absent, nil, content_checksum].include?(newsum) self.fail "File written to disk did not match checksum; discarding changes (#{content_checksum} vs #{newsum})" end # write the current content. Note that if there is no content property # simply opening the file with 'w' as done in write is enough to truncate # or write an empty length file. def write_content(file) (content = property(:content)) && content.write(file) end private def write_temporary_file? # unfortunately we don't know the source file size before fetching it # so let's assume the file won't be empty (c = property(:content) and c.length) || (s = @parameters[:source] and 1) end # There are some cases where all of the work does not get done on # file creation/modification, so we have to do some extra checking. def property_fix properties.each do |thing| next unless [:mode, :owner, :group, :seluser, :selrole, :seltype, :selrange].include?(thing.name) # Make sure we get a new stat objct expire currentvalue = thing.retrieve thing.sync unless thing.insync?(currentvalue) end end end # We put all of the properties in separate files, because there are so many # of them. The order these are loaded is important, because it determines # the order they are in the property lit. require 'puppet/type/file/checksum' require 'puppet/type/file/content' # can create the file require 'puppet/type/file/source' # can create the file require 'puppet/type/file/target' # creates a different type of file require 'puppet/type/file/ensure' # can create the file require 'puppet/type/file/owner' require 'puppet/type/file/group' require 'puppet/type/file/mode' require 'puppet/type/file/type' require 'puppet/type/file/selcontext' # SELinux file context diff --git a/lib/puppet/type/file/ensure.rb b/lib/puppet/type/file/ensure.rb index 967e06aee..4a68551ee 100755 --- a/lib/puppet/type/file/ensure.rb +++ b/lib/puppet/type/file/ensure.rb @@ -1,170 +1,170 @@ module Puppet Puppet::Type.type(:file).ensurable do require 'etc' desc "Whether to create files that don't currently exist. Possible values are *absent*, *present*, *file*, and *directory*. Specifying `present` will match any form of file existence, and if the file is missing will create an empty file. Specifying `absent` will delete the file (and directory if recurse => true). Anything other than those values will be considered to be a symlink. For instance, the following text creates a link: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"/etc/inet/inetd.conf\" } You can make relative links: # Useful on solaris file { \"/etc/inetd.conf\": ensure => \"inet/inetd.conf\" } If you need to make a relative link to a file named the same as one of the valid values, you must prefix it with `./` or something similar. You can also make recursive symlinks, which will create a directory structure that maps to the target directory, with directories corresponding to each directory and links corresponding to each file." # Most 'ensure' properties have a default, but with files we, um, don't. nodefault newvalue(:absent) do File.unlink(@resource[:path]) end aliasvalue(:false, :absent) newvalue(:file, :event => :file_created) do # Make sure we're not managing the content some other way if property = @resource.property(:content) property.sync else @resource.write(:ensure) mode = @resource.should(:mode) end end #aliasvalue(:present, :file) newvalue(:present, :event => :file_created) do # Make a file if they want something, but this will match almost # anything. set_file end newvalue(:directory, :event => :directory_created) do mode = @resource.should(:mode) parent = File.dirname(@resource[:path]) unless FileTest.exists? parent raise Puppet::Error, "Cannot create #{@resource[:path]}; parent directory #{parent} does not exist" end if mode Puppet::Util.withumask(000) do - Dir.mkdir(@resource[:path],mode) + Dir.mkdir(@resource[:path], mode.to_i(8)) end else Dir.mkdir(@resource[:path]) end @resource.send(:property_fix) return :directory_created end newvalue(:link, :event => :link_created) do fail "Cannot create a symlink without a target" unless property = resource.property(:target) property.retrieve property.mklink end # Symlinks. newvalue(/./) do # This code never gets executed. We need the regex to support # specifying it, but the work is done in the 'symlink' code block. end munge do |value| value = super(value) value,resource[:target] = :link,value unless value.is_a? Symbol resource[:links] = :manage if value == :link and resource[:links] != :follow value end def change_to_s(currentvalue, newvalue) return super unless newvalue.to_s == "file" return super unless property = @resource.property(:content) # We know that content is out of sync if we're here, because # it's essentially equivalent to 'ensure' in the transaction. if source = @resource.parameter(:source) should = source.checksum else should = property.should end if should == :absent is = property.retrieve else is = :absent end property.change_to_s(is, should) end # Check that we can actually create anything def check basedir = File.dirname(@resource[:path]) if ! FileTest.exists?(basedir) raise Puppet::Error, "Can not create #{@resource.title}; parent directory does not exist" elsif ! FileTest.directory?(basedir) raise Puppet::Error, "Can not create #{@resource.title}; #{dirname} is not a directory" end end # We have to treat :present specially, because it works with any # type of file. def insync?(currentvalue) unless currentvalue == :absent or resource.replace? return true end if self.should == :present return !(currentvalue.nil? or currentvalue == :absent) else return super(currentvalue) end end def retrieve if stat = @resource.stat(false) return stat.ftype.intern else if self.should == :false return :false else return :absent end end end def sync @resource.remove_existing(self.should) if self.should == :absent return :file_removed end event = super event end end end diff --git a/lib/puppet/type/file/mode.rb b/lib/puppet/type/file/mode.rb index 1ce56c843..2acd8b359 100755 --- a/lib/puppet/type/file/mode.rb +++ b/lib/puppet/type/file/mode.rb @@ -1,124 +1,90 @@ # Manage file modes. This state should support different formats # for specification (e.g., u+rwx, or -0011), but for now only supports # specifying the full mode. module Puppet Puppet::Type.type(:file).newproperty(:mode) do require 'etc' desc "Mode the file should be. Currently relatively limited: you must specify the exact mode the file should be. Note that when you set the mode of a directory, Puppet always sets the search/traverse (1) bit anywhere the read (4) bit is set. This is almost always what you want: read allows you to list the entries in a directory, and search/traverse allows you to access (read/write/execute) those entries.) Because of this feature, you can recursively make a directory and all of the files in it world-readable by setting e.g.: file { '/some/dir': mode => 644, recurse => true, } In this case all of the files underneath `/some/dir` will have mode 644, and all of the directories will have mode 755." @event = :file_changed - # Our modes are octal, so make sure they print correctly. Other - # valid values are symbols, basically - def is_to_s(currentvalue) - case currentvalue - when Integer - return "%o" % currentvalue - when Symbol - return currentvalue - else - raise Puppet::DevError, "Invalid current value for mode: #{currentvalue.inspect}" - end - end - - def should_to_s(newvalue = @should) - case newvalue - when Integer - return "%o" % newvalue - when Symbol - return newvalue - else - raise Puppet::DevError, "Invalid 'should' value for mode: #{newvalue.inspect}" - end - end - munge do |should| - # this is pretty hackish, but i need to make sure the number is in - # octal, yet the number can only be specified as a string right now - value = should - if value.is_a?(String) - unless value =~ /^\d+$/ - raise Puppet::Error, "File modes can only be numbers, not #{value.inspect}" - end - # Make sure our number looks like octal. - unless value =~ /^0/ - value = "0#{value}" - end - old = value - begin - value = Integer(value) - rescue ArgumentError => detail - raise Puppet::DevError, "Could not convert #{old.inspect} to integer" + if should.is_a?(String) + unless should =~ /^[0-7]+$/ + raise Puppet::Error, "File modes can only be octal numbers, not #{should.inspect}" end + should.to_i(8).to_s(8) + else + should.to_s(8) end - - return value end # If we're a directory, we need to be executable for all cases # that are readable. This should probably be selectable, but eh. def dirmask(value) if FileTest.directory?(@resource[:path]) + value = value.to_i(8) value |= 0100 if value & 0400 != 0 value |= 010 if value & 040 != 0 value |= 01 if value & 04 != 0 + value = value.to_s(8) end value end def insync?(currentvalue) if stat = @resource.stat and stat.ftype == "link" and @resource[:links] != :follow self.debug "Not managing symlink mode" return true else return super(currentvalue) end end def retrieve # If we're not following links and we're a link, then we just turn # off mode management entirely. if stat = @resource.stat(false) unless defined?(@fixed) @should &&= @should.collect { |s| self.dirmask(s) } end - return stat.mode & 007777 + return (stat.mode & 007777).to_s(8) else return :absent end end def sync mode = self.should begin - File.chmod(mode, @resource[:path]) + File.chmod(mode.to_i(8), @resource[:path]) rescue => detail error = Puppet::Error.new("failed to chmod #{@resource[:path]}: #{detail.message}") error.set_backtrace detail.backtrace raise error end :file_changed end end end diff --git a/lib/puppet/type/user.rb b/lib/puppet/type/user.rb index c8110bb69..761d5d71b 100755 --- a/lib/puppet/type/user.rb +++ b/lib/puppet/type/user.rb @@ -1,438 +1,438 @@ require 'etc' require 'facter' require 'puppet/property/list' require 'puppet/property/ordered_list' require 'puppet/property/keyvalue' module Puppet newtype(:user) do @doc = "Manage users. This type is mostly built to manage system users, so it is lacking some features useful for managing normal users. This resource type uses the prescribed native tools for creating groups and generally uses POSIX APIs for retrieving information about them. It does not directly modify `/etc/passwd` or anything." feature :allows_duplicates, "The provider supports duplicate users with the same UID." feature :manages_homedir, "The provider can create and remove home directories." feature :manages_passwords, "The provider can modify user passwords, by accepting a password hash." feature :manages_password_age, "The provider can set age requirements and restrictions for passwords." feature :manages_solaris_rbac, "The provider can manage roles and normal users" feature :manages_expiry, "The provider can manage the expiry date for a user." newproperty(:ensure, :parent => Puppet::Property::Ensure) do newvalue(:present, :event => :user_created) do provider.create end newvalue(:absent, :event => :user_removed) do provider.delete end newvalue(:role, :event => :role_created, :required_features => :manages_solaris_rbac) do provider.create_role end desc "The basic state that the object should be in." # If they're talking about the thing at all, they generally want to # say it should exist. defaultto do if @resource.managed? :present else nil end end def retrieve if provider.exists? if provider.respond_to?(:is_role?) and provider.is_role? return :role else return :present end else return :absent end end end + newproperty(:home) do + desc "The home directory of the user. The directory must be created + separately and is not currently checked for existence." + end + newproperty(:uid) do desc "The user ID. Must be specified numerically. For new users being created, if no user ID is specified then one will be chosen automatically, which will likely result in the same user having different IDs on different systems, which is not recommended. This is especially noteworthy if you use Puppet to manage the same user on both Darwin and other platforms, since Puppet does the ID generation for you on Darwin, but the tools do so on other platforms." munge do |value| case value when String if value =~ /^[-0-9]+$/ value = Integer(value) end end return value end end newproperty(:gid) do desc "The user's primary group. Can be specified numerically or by name." munge do |value| if value.is_a?(String) and value =~ /^[-0-9]+$/ Integer(value) else value end end def insync?(is) return true unless self.should # We know the 'is' is a number, so we need to convert the 'should' to a number, # too. @should.each do |value| return true if number = Puppet::Util.gid(value) and is == number end false end def sync found = false @should.each do |value| if number = Puppet::Util.gid(value) provider.gid = number found = true break end end fail "Could not find group(s) #{@should.join(",")}" unless found # Use the default event. end end newproperty(:comment) do desc "A description of the user. Generally is a user's full name." end - newproperty(:home) do - desc "The home directory of the user. The directory must be created - separately and is not currently checked for existence." - end - newproperty(:shell) do desc "The user's login shell. The shell must exist and be executable." end newproperty(:password, :required_features => :manages_passwords) do desc "The user's password, in whatever encrypted format the local machine requires. Be sure to enclose any value that includes a dollar sign ($) in single quotes (\')." validate do |value| raise ArgumentError, "Passwords cannot include ':'" if value.is_a?(String) and value.include?(":") end def change_to_s(currentvalue, newvalue) if currentvalue == :absent return "created password" else return "changed password" end end end newproperty(:password_min_age, :required_features => :manages_password_age) do desc "The minimum amount of time in days a password must be used before it may be changed" munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^\d+$/ raise ArgumentError, "Password minimum age must be provided as a number" end end end newproperty(:password_max_age, :required_features => :manages_password_age) do desc "The maximum amount of time in days a password may be used before it must be changed" munge do |value| case value when String Integer(value) else value end end validate do |value| if value.to_s !~ /^\d+$/ raise ArgumentError, "Password maximum age must be provided as a number" end end end newproperty(:groups, :parent => Puppet::Property::List) do desc "The groups of which the user is a member. The primary group should not be listed. Multiple groups should be specified as an array." validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Group names must be provided, not numbers" end raise ArgumentError, "Group names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:name) do desc "User name. While limitations are determined for each operating system, it is generally a good idea to keep to the degenerate 8 characters, beginning with a letter." isnamevar end newparam(:membership) do desc "Whether specified groups should be treated as the only groups of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newparam(:allowdupe, :boolean => true) do desc "Whether to allow duplicate UIDs." newvalues(:true, :false) defaultto false end newparam(:managehome, :boolean => true) do desc "Whether to manage the home directory when managing the user." newvalues(:true, :false) defaultto false validate do |val| if val.to_s == "true" raise ArgumentError, "User provider #{provider.class.name} can not manage home directories" unless provider.class.manages_homedir? end end end newproperty(:expiry, :required_features => :manages_expiry) do desc "The expiry date for this user. Must be provided in a zero padded YYYY-MM-DD format - e.g 2010-02-19." validate do |value| if value !~ /^\d{4}-\d{2}-\d{2}$/ raise ArgumentError, "Expiry dates must be YYYY-MM-DD" end end end # Autorequire the group, if it's around autorequire(:group) do autos = [] if obj = @parameters[:gid] and groups = obj.shouldorig groups = groups.collect { |group| if group =~ /^\d+$/ Integer(group) else group end } groups.each { |group| case group when Integer if resource = catalog.resources.find { |r| r.is_a?(Puppet::Type.type(:group)) and r.should(:gid) == group } autos << resource end else autos << group end } end if obj = @parameters[:groups] and groups = obj.should autos += groups.split(",") end autos end # Provide an external hook. Yay breaking out of APIs. def exists? provider.exists? end def retrieve absent = false properties.inject({}) { |prophash, property| current_value = :absent if absent prophash[property] = :absent else current_value = property.retrieve prophash[property] = current_value end if property.name == :ensure and current_value == :absent absent = true end prophash } end newproperty(:roles, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The roles the user has. Multiple roles should be specified as an array." def membership :role_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Role names must be provided, not numbers" end raise ArgumentError, "Role names must be provided as an array, not a comma-separated list" if value.include?(",") end end #autorequire the roles that the user has autorequire(:user) do reqs = [] if roles_property = @parameters[:roles] and roles = roles_property.should reqs += roles.split(',') end reqs end newparam(:role_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:auths, :parent => Puppet::Property::List, :required_features => :manages_solaris_rbac) do desc "The auths the user has. Multiple auths should be specified as an array." def membership :auth_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Auth names must be provided, not numbers" end raise ArgumentError, "Auth names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:auth_membership) do desc "Whether specified auths should be treated as the only auths of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:profiles, :parent => Puppet::Property::OrderedList, :required_features => :manages_solaris_rbac) do desc "The profiles the user has. Multiple profiles should be specified as an array." def membership :profile_membership end validate do |value| if value =~ /^\d+$/ raise ArgumentError, "Profile names must be provided, not numbers" end raise ArgumentError, "Profile names must be provided as an array, not a comma-separated list" if value.include?(",") end end newparam(:profile_membership) do desc "Whether specified roles should be treated as the only roles of which the user is a member or whether they should merely be treated as the minimum membership list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:keys, :parent => Puppet::Property::KeyValue, :required_features => :manages_solaris_rbac) do desc "Specify user attributes in an array of keyvalue pairs" def membership :key_membership end validate do |value| raise ArgumentError, "key value pairs must be seperated by an =" unless value.include?("=") end end newparam(:key_membership) do desc "Whether specified key value pairs should be treated as the only attributes of the user or whether they should merely be treated as the minimum list." newvalues(:inclusive, :minimum) defaultto :minimum end newproperty(:project, :required_features => :manages_solaris_rbac) do desc "The name of the project associated with a user" end end end diff --git a/lib/puppet/util/log.rb b/lib/puppet/util/log.rb index 36a765c61..7764dc1d1 100644 --- a/lib/puppet/util/log.rb +++ b/lib/puppet/util/log.rb @@ -1,257 +1,258 @@ require 'puppet/util/tagging' require 'puppet/util/classgen' # Pass feedback to the user. Log levels are modeled after syslog's, and it is # expected that that will be the most common log destination. Supports # multiple destinations, one of which is a remote server. class Puppet::Util::Log include Puppet::Util extend Puppet::Util::ClassGen include Puppet::Util::Tagging @levels = [:debug,:info,:notice,:warning,:err,:alert,:emerg,:crit] @loglevel = 2 @desttypes = {} # Create a new destination type. def self.newdesttype(name, options = {}, &block) - dest = genclass( - name, :parent => Puppet::Util::Log::Destination, :prefix => "Dest", - :block => block, - :hash => @desttypes, - + dest = genclass( + name, + :parent => Puppet::Util::Log::Destination, + :prefix => "Dest", + :block => block, + :hash => @desttypes, :attributes => options ) dest.match(dest.name) dest end require 'puppet/util/log/destination' require 'puppet/util/log/destinations' @destinations = {} @queued = [] class << self include Puppet::Util include Puppet::Util::ClassGen attr_reader :desttypes end # Reset log to basics. Basically just flushes and closes files and # undefs other objects. def Log.close(destination) if @destinations.include?(destination) @destinations[destination].flush if @destinations[destination].respond_to?(:flush) @destinations[destination].close if @destinations[destination].respond_to?(:close) @destinations.delete(destination) end end def self.close_all destinations.keys.each { |dest| close(dest) } end # Flush any log destinations that support such operations. def Log.flush @destinations.each { |type, dest| dest.flush if dest.respond_to?(:flush) } end # Create a new log message. The primary role of this method is to # avoid creating log messages below the loglevel. def Log.create(hash) raise Puppet::DevError, "Logs require a level" unless hash.include?(:level) raise Puppet::DevError, "Invalid log level #{hash[:level]}" unless @levels.index(hash[:level]) @levels.index(hash[:level]) >= @loglevel ? Puppet::Util::Log.new(hash) : nil end def Log.destinations @destinations end # Yield each valid level in turn def Log.eachlevel @levels.each { |level| yield level } end # Return the current log level. def Log.level @levels[@loglevel] end # Set the current log level. def Log.level=(level) level = level.intern unless level.is_a?(Symbol) raise Puppet::DevError, "Invalid loglevel #{level}" unless @levels.include?(level) @loglevel = @levels.index(level) end def Log.levels @levels.dup end # Create a new log destination. def Log.newdestination(dest) # Each destination can only occur once. if @destinations.find { |name, obj| obj.name == dest } return end name, type = @desttypes.find do |name, klass| klass.match?(dest) end raise Puppet::DevError, "Unknown destination type #{dest}" unless type begin if type.instance_method(:initialize).arity == 1 @destinations[dest] = type.new(dest) else @destinations[dest] = type.new end flushqueue @destinations[dest] rescue => detail puts detail.backtrace if Puppet[:debug] # If this was our only destination, then add the console back in. newdestination(:console) if @destinations.empty? and (dest != :console and dest != "console") end end # Route the actual message. FIXME There are lots of things this method # should do, like caching and a bit more. It's worth noting that there's # a potential for a loop here, if the machine somehow gets the destination set as # itself. def Log.newmessage(msg) return if @levels.index(msg.level) < @loglevel queuemessage(msg) if @destinations.length == 0 @destinations.each do |name, dest| threadlock(dest) do dest.handle(msg) end end end def Log.queuemessage(msg) @queued.push(msg) end def Log.flushqueue return unless @destinations.size >= 1 @queued.each do |msg| Log.newmessage(msg) end @queued.clear end def Log.sendlevel?(level) @levels.index(level) >= @loglevel end # Reopen all of our logs. def Log.reopen Puppet.notice "Reopening log files" types = @destinations.keys @destinations.each { |type, dest| dest.close if dest.respond_to?(:close) } @destinations.clear # We need to make sure we always end up with some kind of destination begin types.each { |type| Log.newdestination(type) } rescue => detail if @destinations.empty? Log.newdestination(:syslog) Puppet.err detail.to_s end end end # Is the passed level a valid log level? def self.validlevel?(level) @levels.include?(level) end attr_accessor :time, :remote, :file, :line, :version, :source attr_reader :level, :message def initialize(args) self.level = args[:level] self.message = args[:message] self.source = args[:source] || "Puppet" @time = Time.now if tags = args[:tags] tags.each { |t| self.tag(t) } end [:file, :line, :version].each do |attr| next unless value = args[attr] send(attr.to_s + "=", value) end Log.newmessage(self) end def message=(msg) raise ArgumentError, "Puppet::Util::Log requires a message" unless msg @message = msg.to_s end def level=(level) raise ArgumentError, "Puppet::Util::Log requires a log level" unless level @level = level.to_sym raise ArgumentError, "Invalid log level #{@level}" unless self.class.validlevel?(@level) # Tag myself with my log level tag(level) end # If they pass a source in to us, we make sure it is a string, and # we retrieve any tags we can. def source=(source) if source.respond_to?(:source_descriptors) descriptors = source.source_descriptors @source = descriptors[:path] descriptors[:tags].each { |t| tag(t) } [:file, :line, :version].each do |param| next unless descriptors[param] send(param.to_s + "=", descriptors[param]) end else @source = source.to_s end end def to_report "#{time} #{source} (#{level}): #{to_s}" end def to_s message end end # This is for backward compatibility from when we changed the constant to Puppet::Util::Log # because the reports include the constant name. Apparently the alias was created in # March 2007, should could probably be removed soon. Puppet::Log = Puppet::Util::Log diff --git a/spec/unit/application/inspect_spec.rb b/spec/unit/application/inspect_spec.rb new file mode 100644 index 000000000..a3cc74d86 --- /dev/null +++ b/spec/unit/application/inspect_spec.rb @@ -0,0 +1,79 @@ +#!/usr/bin/env ruby + +require File.dirname(__FILE__) + '/../../spec_helper' + +require 'puppet/application/inspect' +require 'puppet/resource/catalog' +require 'puppet/indirector/catalog/yaml' +require 'puppet/indirector/report/rest' + +describe Puppet::Application::Inspect do + before :each do + @inspect = Puppet::Application[:inspect] + end + + describe "during setup" do + it "should print its configuration if asked" do + Puppet[:configprint] = "all" + + Puppet.settings.expects(:print_configs).returns(true) + lambda { @inspect.setup }.should raise_error(SystemExit) + end + + it "should fail if reporting is turned off" do + Puppet[:report] = false + lambda { @inspect.setup }.should raise_error(/report=true/) + end + end + + describe "when executing" do + before :each do + Puppet[:report] = true + Puppet::Util::Log.stubs(:newdestination) + Puppet::Transaction::Report::Rest.any_instance.stubs(:save) + @inspect.setup + end + + it "should retrieve the local catalog" do + Puppet::Resource::Catalog::Yaml.any_instance.expects(:find).with {|request| request.key == Puppet[:certname] }.returns(Puppet::Resource::Catalog.new) + + @inspect.run_command + end + + it "should save the report to REST" do + Puppet::Resource::Catalog::Yaml.any_instance.stubs(:find).returns(Puppet::Resource::Catalog.new) + Puppet::Transaction::Report::Rest.any_instance.expects(:save).with {|request| request.instance.host == Puppet[:certname] } + + @inspect.run_command + end + + it "should audit the specified properties" do + catalog = Puppet::Resource::Catalog.new + file = Tempfile.new("foo") + file.puts("file contents") + file.flush + resource = Puppet::Resource.new(:file, file.path, :parameters => {:audit => "all"}) + catalog.add_resource(resource) + Puppet::Resource::Catalog::Yaml.any_instance.stubs(:find).returns(catalog) + + events = nil + + Puppet::Transaction::Report::Rest.any_instance.expects(:save).with do |request| + events = request.instance.resource_statuses.values.first.events + end + + @inspect.run_command + + properties = events.inject({}) do |property_values, event| + property_values.merge(event.property => event.previous_value) + end + properties["ensure"].should == :file + properties["content"].should == "{md5}#{Digest::MD5.hexdigest("file contents\n")}" + end + end + + after :all do + Puppet::Resource::Catalog.indirection.reset_terminus_class + Puppet::Transaction::Report.indirection.terminus_class = :processor + end +end diff --git a/spec/unit/configurer/plugin_handler_spec.rb b/spec/unit/configurer/plugin_handler_spec.rb index 25d2d47af..30b135e8f 100755 --- a/spec/unit/configurer/plugin_handler_spec.rb +++ b/spec/unit/configurer/plugin_handler_spec.rb @@ -1,112 +1,116 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/configurer' require 'puppet/configurer/plugin_handler' class PluginHandlerTester include Puppet::Configurer::PluginHandler end describe Puppet::Configurer::PluginHandler do before do @pluginhandler = PluginHandlerTester.new + + # PluginHandler#load_plugin has an extra-strong rescue clause + # this mock is to make sure that we don't silently ignore errors + Puppet.expects(:err).never end it "should have a method for downloading plugins" do @pluginhandler.should respond_to(:download_plugins) end it "should have a boolean method for determining whether plugins should be downloaded" do @pluginhandler.should respond_to(:download_plugins?) end it "should download plugins when :pluginsync is true" do Puppet.settings.expects(:value).with(:pluginsync).returns true @pluginhandler.should be_download_plugins end it "should not download plugins when :pluginsync is false" do Puppet.settings.expects(:value).with(:pluginsync).returns false @pluginhandler.should_not be_download_plugins end it "should not download plugins when downloading is disabled" do Puppet::Configurer::Downloader.expects(:new).never @pluginhandler.expects(:download_plugins?).returns false @pluginhandler.download_plugins end it "should use an Agent Downloader, with the name, source, destination, and ignore set correctly, to download plugins when downloading is enabled" do downloader = mock 'downloader' Puppet.settings.expects(:value).with(:pluginsource).returns "psource" Puppet.settings.expects(:value).with(:plugindest).returns "pdest" Puppet.settings.expects(:value).with(:pluginsignore).returns "pignore" Puppet::Configurer::Downloader.expects(:new).with("plugin", "pdest", "psource", "pignore").returns downloader downloader.expects(:evaluate).returns [] @pluginhandler.expects(:download_plugins?).returns true @pluginhandler.download_plugins end it "should be able to load plugins" do @pluginhandler.should respond_to(:load_plugin) end it "should load each downloaded file" do FileTest.stubs(:exist?).returns true downloader = mock 'downloader' Puppet::Configurer::Downloader.expects(:new).returns downloader downloader.expects(:evaluate).returns %w{one two} @pluginhandler.expects(:download_plugins?).returns true @pluginhandler.expects(:load_plugin).with("one") @pluginhandler.expects(:load_plugin).with("two") @pluginhandler.download_plugins end it "should load plugins when asked to do so" do FileTest.stubs(:exist?).returns true @pluginhandler.expects(:load).with("foo") @pluginhandler.load_plugin("foo") end it "should not try to load files that don't exist" do - FileTest.expects(:exist?).with("foo").returns true + FileTest.expects(:exist?).with("foo").returns false @pluginhandler.expects(:load).never @pluginhandler.load_plugin("foo") end it "should not try to load directories" do FileTest.stubs(:exist?).returns true FileTest.expects(:directory?).with("foo").returns true @pluginhandler.expects(:load).never @pluginhandler.load_plugin("foo") end it "should warn but not fail if loading a file raises an exception" do FileTest.stubs(:exist?).returns true @pluginhandler.expects(:load).with("foo").raises "eh" Puppet.expects(:err) @pluginhandler.load_plugin("foo") end it "should warn but not fail if loading a file raises a LoadError" do FileTest.stubs(:exist?).returns true @pluginhandler.expects(:load).with("foo").raises LoadError.new("eh") Puppet.expects(:err) @pluginhandler.load_plugin("foo") end end diff --git a/spec/unit/configurer_spec.rb b/spec/unit/configurer_spec.rb index 0c9d06362..ebc5768ea 100755 --- a/spec/unit/configurer_spec.rb +++ b/spec/unit/configurer_spec.rb @@ -1,494 +1,494 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2007-11-12. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../spec_helper' require 'puppet/configurer' describe Puppet::Configurer do before do Puppet.settings.stubs(:use).returns(true) @agent = Puppet::Configurer.new end it "should include the Plugin Handler module" do Puppet::Configurer.ancestors.should be_include(Puppet::Configurer::PluginHandler) end it "should include the Fact Handler module" do Puppet::Configurer.ancestors.should be_include(Puppet::Configurer::FactHandler) end it "should use the puppetdlockfile as its lockfile path" do Puppet.settings.expects(:value).with(:puppetdlockfile).returns("/my/lock") Puppet::Configurer.lockfile_path.should == "/my/lock" end describe "when executing a pre-run hook" do it "should do nothing if the hook is set to an empty string" do Puppet.settings[:prerun_command] = "" Puppet::Util.expects(:exec).never @agent.execute_prerun_command end it "should execute any pre-run command provided via the 'prerun_command' setting" do Puppet.settings[:prerun_command] = "/my/command" Puppet::Util.expects(:execute).with { |args| args[0] == "/my/command" } @agent.execute_prerun_command end it "should fail if the command fails" do Puppet.settings[:prerun_command] = "/my/command" Puppet::Util.expects(:execute).raises Puppet::ExecutionFailure lambda { @agent.execute_prerun_command }.should raise_error(Puppet::Configurer::CommandHookError) end end describe "when executing a post-run hook" do it "should do nothing if the hook is set to an empty string" do Puppet.settings[:postrun_command] = "" Puppet::Util.expects(:exec).never @agent.execute_postrun_command end it "should execute any post-run command provided via the 'postrun_command' setting" do Puppet.settings[:postrun_command] = "/my/command" Puppet::Util.expects(:execute).with { |args| args[0] == "/my/command" } @agent.execute_postrun_command end it "should fail if the command fails" do Puppet.settings[:postrun_command] = "/my/command" Puppet::Util.expects(:execute).raises Puppet::ExecutionFailure lambda { @agent.execute_postrun_command }.should raise_error(Puppet::Configurer::CommandHookError) end end end describe Puppet::Configurer, "when initializing a report" do it "should return an instance of a transaction report" do Puppet.settings.stubs(:use).returns(true) @agent = Puppet::Configurer.new @agent.initialize_report.should be_instance_of(Puppet::Transaction::Report) end end describe Puppet::Configurer, "when executing a catalog run" do before do Puppet.settings.stubs(:use).returns(true) @agent = Puppet::Configurer.new @agent.stubs(:prepare) @agent.stubs(:facts_for_uploading).returns({}) @catalog = Puppet::Resource::Catalog.new @catalog.stubs(:apply) @agent.stubs(:retrieve_catalog).returns @catalog Puppet::Util::Log.stubs(:newdestination) Puppet::Util::Log.stubs(:close) end it "should prepare for the run" do @agent.expects(:prepare) @agent.run end it "should initialize a transaction report if one is not provided" do report = stub 'report' @agent.expects(:initialize_report).returns report @agent.run end it "should pass the new report to the catalog" do report = stub 'report' @agent.stubs(:initialize_report).returns report @catalog.expects(:apply).with{|options| options[:report] == report} @agent.run end it "should use the provided report if it was passed one" do report = stub 'report' @agent.expects(:initialize_report).never @catalog.expects(:apply).with{|options| options[:report] == report} @agent.run(:report => report) end it "should set the report as a log destination" do report = stub 'report' @agent.expects(:initialize_report).returns report Puppet::Util::Log.expects(:newdestination).with(report) @agent.run end it "should retrieve the catalog" do @agent.expects(:retrieve_catalog) @agent.run end it "should log a failure and do nothing if no catalog can be retrieved" do @agent.expects(:retrieve_catalog).returns nil Puppet.expects(:err).with "Could not retrieve catalog; skipping run" @agent.run end it "should apply the catalog with all options to :run" do @agent.expects(:retrieve_catalog).returns @catalog @catalog.expects(:apply).with { |args| args[:one] == true } @agent.run :one => true end it "should accept a catalog and use it instead of retrieving a different one" do @agent.expects(:retrieve_catalog).never @catalog.expects(:apply) @agent.run :one => true, :catalog => @catalog end it "should benchmark how long it takes to apply the catalog" do @agent.expects(:benchmark).with(:notice, "Finished catalog run") @agent.expects(:retrieve_catalog).returns @catalog @catalog.expects(:apply).never # because we're not yielding @agent.run end it "should execute post-run hooks after the run" do @agent.expects(:execute_postrun_command) @agent.run end it "should send the report" do report = stub 'report' @agent.expects(:initialize_report).returns report @agent.expects(:send_report).with { |r, trans| r == report } @agent.run end it "should send the transaction report with a reference to the transaction if a run was actually made" do report = stub 'report' @agent.expects(:initialize_report).returns report trans = stub 'transaction' @catalog.expects(:apply).returns trans @agent.expects(:send_report).with { |r, t| t == trans } @agent.run :catalog => @catalog end it "should send the transaction report even if the catalog could not be retrieved" do @agent.expects(:retrieve_catalog).returns nil report = stub 'report' @agent.expects(:initialize_report).returns report @agent.expects(:send_report) @agent.run end it "should send the transaction report even if there is a failure" do @agent.expects(:retrieve_catalog).raises "whatever" report = stub 'report' @agent.expects(:initialize_report).returns report @agent.expects(:send_report) lambda { @agent.run }.should raise_error end it "should remove the report as a log destination when the run is finished" do report = stub 'report' @agent.expects(:initialize_report).returns report Puppet::Util::Log.expects(:close).with(report) @agent.run end it "should return the report as the result of the run" do report = stub 'report' @agent.expects(:initialize_report).returns report @agent.run.should equal(report) end end describe Puppet::Configurer, "when sending a report" do before do Puppet.settings.stubs(:use).returns(true) @configurer = Puppet::Configurer.new @report = stub 'report' @trans = stub 'transaction' end it "should require a report" do lambda { @configurer.send_report }.should raise_error(ArgumentError) end it "should allow specification of a transaction" do lambda { @configurer.send_report(@report, @trans) }.should_not raise_error(ArgumentError) end it "should use any provided transaction to add metrics to the report" do @trans.expects(:generate_report) @configurer.send_report(@report, @trans) end it "should print a report summary if configured to do so" do Puppet.settings[:summarize] = true @report.expects(:summary).returns "stuff" @configurer.expects(:puts).with("stuff") @configurer.send_report(@report) end it "should not print a report summary if not configured to do so" do Puppet.settings[:summarize] = false @configurer.expects(:puts).never @configurer.send_report(@report) end it "should save the report if reporting is enabled" do Puppet.settings[:report] = true @report.expects(:save) @configurer.send_report(@report) end it "should not save the report if reporting is disabled" do Puppet.settings[:report] = false @report.expects(:save).never @configurer.send_report(@report) end it "should log but not fail if saving the report fails" do Puppet.settings[:report] = true @report.expects(:save).raises "whatever" Puppet.expects(:err) lambda { @configurer.send_report(@report) }.should_not raise_error end end describe Puppet::Configurer, "when retrieving a catalog" do before do Puppet.settings.stubs(:use).returns(true) @agent = Puppet::Configurer.new @agent.stubs(:facts_for_uploading).returns({}) @catalog = Puppet::Resource::Catalog.new # this is the default when using a Configurer instance Puppet::Resource::Catalog.indirection.stubs(:terminus_class).returns :rest @agent.stubs(:convert_catalog).returns @catalog end describe "and configured to only retrieve a catalog from the cache" do before do Puppet.settings[:use_cached_catalog] = true end it "should first look in the cache for a catalog" do Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_cache] == true }.never @agent.retrieve_catalog.should == @catalog end it "should compile a new catalog if none is found in the cache" do Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog @agent.retrieve_catalog.should == @catalog end end describe "when not using a REST terminus for catalogs" do it "should not pass any facts when retrieving the catalog" do @agent.expects(:facts_for_uploading).never Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:facts].nil? }.returns @catalog @agent.retrieve_catalog end end describe "when using a REST terminus for catalogs" do it "should pass the prepared facts and the facts format as arguments when retrieving the catalog" do @agent.expects(:facts_for_uploading).returns(:facts => "myfacts", :facts_format => :foo) Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:facts] == "myfacts" and options[:facts_format] == :foo }.returns @catalog @agent.retrieve_catalog end end it "should use the Catalog class to get its catalog" do Puppet::Resource::Catalog.expects(:find).returns @catalog @agent.retrieve_catalog end it "should use its certname to retrieve the catalog" do Facter.stubs(:value).returns "eh" Puppet.settings[:certname] = "myhost.domain.com" Puppet::Resource::Catalog.expects(:find).with { |name, options| name == "myhost.domain.com" }.returns @catalog @agent.retrieve_catalog end it "should default to returning a catalog retrieved directly from the server, skipping the cache" do Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog @agent.retrieve_catalog.should == @catalog end it "should log and return the cached catalog when no catalog can be retrieved from the server" do Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog Puppet.expects(:notice) @agent.retrieve_catalog.should == @catalog end it "should not look in the cache for a catalog if one is returned from the server" do Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns @catalog Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_terminus] == true }.never @agent.retrieve_catalog.should == @catalog end it "should return the cached catalog when retrieving the remote catalog throws an exception" do Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_cache] == true }.raises "eh" Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns @catalog @agent.retrieve_catalog.should == @catalog end it "should log and return nil if no catalog can be retrieved from the server and :usecacheonfailure is disabled" do Puppet.stubs(:[]) Puppet.expects(:[]).with(:usecacheonfailure).returns false Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet.expects(:warning) @agent.retrieve_catalog.should be_nil end it "should return nil if no cached catalog is available and no catalog can be retrieved from the server" do Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_cache] == true }.returns nil Puppet::Resource::Catalog.expects(:find).with { |name, options| options[:ignore_terminus] == true }.returns nil @agent.retrieve_catalog.should be_nil end it "should convert the catalog before returning" do Puppet::Resource::Catalog.stubs(:find).returns @catalog @agent.expects(:convert_catalog).with { |cat, dur| cat == @catalog }.returns "converted catalog" @agent.retrieve_catalog.should == "converted catalog" end it "should return nil if there is an error while retrieving the catalog" do - Puppet::Resource::Catalog.expects(:find).raises "eh" + Puppet::Resource::Catalog.expects(:find).at_least_once.raises "eh" @agent.retrieve_catalog.should be_nil end end describe Puppet::Configurer, "when converting the catalog" do before do Puppet.settings.stubs(:use).returns(true) @agent = Puppet::Configurer.new @catalog = Puppet::Resource::Catalog.new @oldcatalog = stub 'old_catalog', :to_ral => @catalog end it "should convert the catalog to a RAL-formed catalog" do @oldcatalog.expects(:to_ral).returns @catalog @agent.convert_catalog(@oldcatalog, 10).should equal(@catalog) end it "should finalize the catalog" do @catalog.expects(:finalize) @agent.convert_catalog(@oldcatalog, 10) end it "should record the passed retrieval time with the RAL catalog" do @catalog.expects(:retrieval_duration=).with 10 @agent.convert_catalog(@oldcatalog, 10) end it "should write the RAL catalog's class file" do @catalog.expects(:write_class_file) @agent.convert_catalog(@oldcatalog, 10) end end describe Puppet::Configurer, "when preparing for a run" do before do Puppet.settings.stubs(:use).returns(true) @agent = Puppet::Configurer.new @agent.stubs(:dostorage) @agent.stubs(:download_fact_plugins) @agent.stubs(:download_plugins) @agent.stubs(:execute_prerun_command) @facts = {"one" => "two", "three" => "four"} end it "should initialize the metadata store" do @agent.class.stubs(:facts).returns(@facts) @agent.expects(:dostorage) @agent.prepare end it "should download fact plugins" do @agent.expects(:download_fact_plugins) @agent.prepare end it "should download plugins" do @agent.expects(:download_plugins) @agent.prepare end it "should perform the pre-run commands" do @agent.expects(:execute_prerun_command) @agent.prepare end end diff --git a/spec/unit/file_serving/fileset_spec.rb b/spec/unit/file_serving/fileset_spec.rb index 9a90cff15..ecc77812c 100755 --- a/spec/unit/file_serving/fileset_spec.rb +++ b/spec/unit/file_serving/fileset_spec.rb @@ -1,347 +1,348 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/file_serving/fileset' describe Puppet::FileServing::Fileset, " when initializing" do it "should require a path" do proc { Puppet::FileServing::Fileset.new }.should raise_error(ArgumentError) end it "should fail if its path is not fully qualified" do proc { Puppet::FileServing::Fileset.new("some/file") }.should raise_error(ArgumentError) end it "should fail if its path does not exist" do File.expects(:lstat).with("/some/file").returns nil proc { Puppet::FileServing::Fileset.new("/some/file") }.should raise_error(ArgumentError) end it "should accept a 'recurse' option" do File.expects(:lstat).with("/some/file").returns stub("stat") set = Puppet::FileServing::Fileset.new("/some/file", :recurse => true) set.recurse.should be_true end it "should accept a 'recurselimit' option" do File.expects(:lstat).with("/some/file").returns stub("stat") set = Puppet::FileServing::Fileset.new("/some/file", :recurselimit => 3) set.recurselimit.should == 3 end it "should accept an 'ignore' option" do File.expects(:lstat).with("/some/file").returns stub("stat") set = Puppet::FileServing::Fileset.new("/some/file", :ignore => ".svn") set.ignore.should == [".svn"] end it "should accept a 'links' option" do File.expects(:lstat).with("/some/file").returns stub("stat") set = Puppet::FileServing::Fileset.new("/some/file", :links => :manage) set.links.should == :manage end it "should accept a 'checksum_type' option" do File.expects(:lstat).with("/some/file").returns stub("stat") set = Puppet::FileServing::Fileset.new("/some/file", :checksum_type => :test) set.checksum_type.should == :test end it "should fail if 'links' is set to anything other than :manage or :follow" do proc { Puppet::FileServing::Fileset.new("/some/file", :links => :whatever) }.should raise_error(ArgumentError) end it "should default to 'false' for recurse" do File.expects(:lstat).with("/some/file").returns stub("stat") Puppet::FileServing::Fileset.new("/some/file").recurse.should == false end it "should default to :infinite for recurselimit" do File.expects(:lstat).with("/some/file").returns stub("stat") Puppet::FileServing::Fileset.new("/some/file").recurselimit.should == :infinite end it "should default to an empty ignore list" do File.expects(:lstat).with("/some/file").returns stub("stat") Puppet::FileServing::Fileset.new("/some/file").ignore.should == [] end it "should default to :manage for links" do File.expects(:lstat).with("/some/file").returns stub("stat") Puppet::FileServing::Fileset.new("/some/file").links.should == :manage end it "should support using an Indirector Request for its options" do File.expects(:lstat).with("/some/file").returns stub("stat") request = Puppet::Indirector::Request.new(:file_serving, :find, "foo") lambda { Puppet::FileServing::Fileset.new("/some/file", request) }.should_not raise_error end describe "using an indirector request" do before do File.stubs(:lstat).returns stub("stat") @values = {:links => :manage, :ignore => %w{a b}, :recurse => true, :recurselimit => 1234} @request = Puppet::Indirector::Request.new(:file_serving, :find, "foo") end [:recurse, :recurselimit, :ignore, :links].each do |option| it "should pass :recurse, :recurselimit, :ignore, and :links settings on to the fileset if present" do @request.stubs(:options).returns(option => @values[option]) Puppet::FileServing::Fileset.new("/my/file", @request).send(option).should == @values[option] end it "should pass :recurse, :recurselimit, :ignore, and :links settings on to the fileset if present with the keys stored as strings" do @request.stubs(:options).returns(option.to_s => @values[option]) Puppet::FileServing::Fileset.new("/my/file", @request).send(option).should == @values[option] end end it "should convert the integer as a string to their integer counterpart when setting options" do @request.stubs(:options).returns(:recurselimit => "1234") Puppet::FileServing::Fileset.new("/my/file", @request).recurselimit.should == 1234 end it "should convert the string 'true' to the boolean true when setting options" do @request.stubs(:options).returns(:recurse => "true") Puppet::FileServing::Fileset.new("/my/file", @request).recurse.should == true end it "should convert the string 'false' to the boolean false when setting options" do @request.stubs(:options).returns(:recurse => "false") Puppet::FileServing::Fileset.new("/my/file", @request).recurse.should == false end end end describe Puppet::FileServing::Fileset, " when determining whether to recurse" do before do @path = "/my/path" File.expects(:lstat).with(@path).returns stub("stat") @fileset = Puppet::FileServing::Fileset.new(@path) end it "should always recurse if :recurse is set to 'true' and with infinite recursion" do @fileset.recurse = true @fileset.recurselimit = :infinite @fileset.recurse?(0).should be_true end it "should never recurse if :recurse is set to 'false'" do @fileset.recurse = false @fileset.recurse?(-1).should be_false end it "should recurse if :recurse is set to true, :recurselimit is set to an integer and the current depth is less than that integer" do @fileset.recurse = true @fileset.recurselimit = 1 @fileset.recurse?(0).should be_true end it "should recurse if :recurse is set to true, :recurselimit is set to an integer and the current depth is equal to that integer" do @fileset.recurse = true @fileset.recurselimit = 1 @fileset.recurse?(1).should be_true end it "should not recurse if :recurse is set to true, :recurselimit is set to an integer and the current depth is greater than that integer" do @fileset.recurse = true @fileset.recurselimit = 1 @fileset.recurse?(2).should be_false end end describe Puppet::FileServing::Fileset, " when recursing" do before do @path = "/my/path" File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) @dirstat = stub 'dirstat', :directory? => true @filestat = stub 'filestat', :directory? => false end def mock_dir_structure(path, stat_method = :lstat) File.stubs(stat_method).with(path).returns(@dirstat) Dir.stubs(:entries).with(path).returns(%w{one two .svn CVS}) # Keep track of the files we're stubbing. @files = %w{.} %w{one two .svn CVS}.each do |subdir| @files << subdir # relative path subpath = File.join(path, subdir) File.stubs(stat_method).with(subpath).returns(@dirstat) Dir.stubs(:entries).with(subpath).returns(%w{.svn CVS file1 file2}) %w{file1 file2 .svn CVS}.each do |file| @files << File.join(subdir, file) # relative path File.stubs(stat_method).with(File.join(subpath, file)).returns(@filestat) end end end it "should recurse through the whole file tree if :recurse is set to 'true'" do mock_dir_structure(@path) @fileset.stubs(:recurse?).returns(true) @fileset.files.sort.should == @files.sort end it "should not recurse if :recurse is set to 'false'" do mock_dir_structure(@path) @fileset.stubs(:recurse?).returns(false) @fileset.files.should == %w{.} end # It seems like I should stub :recurse? here, or that I shouldn't stub the # examples above, but... it "should recurse to the level set if :recurselimit is set to an integer" do mock_dir_structure(@path) @fileset.recurse = true @fileset.recurselimit = 1 @fileset.files.should == %w{. one two .svn CVS} end it "should ignore the '.' and '..' directories in subdirectories" do mock_dir_structure(@path) @fileset.recurse = true @fileset.files.sort.should == @files.sort end it "should function if the :ignore value provided is nil" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = nil lambda { @fileset.files }.should_not raise_error end it "should ignore files that match a single pattern in the ignore list" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = ".svn" @fileset.files.find { |file| file.include?(".svn") }.should be_nil end it "should ignore files that match any of multiple patterns in the ignore list" do mock_dir_structure(@path) @fileset.recurse = true @fileset.ignore = %w{.svn CVS} @fileset.files.find { |file| file.include?(".svn") or file.include?("CVS") }.should be_nil end it "should use File.stat if :links is set to :follow" do mock_dir_structure(@path, :stat) @fileset.recurse = true @fileset.links = :follow @fileset.files.sort.should == @files.sort end it "should use File.lstat if :links is set to :manage" do mock_dir_structure(@path, :lstat) @fileset.recurse = true @fileset.links = :manage @fileset.files.sort.should == @files.sort end it "should succeed when paths have regexp significant characters" do @path = "/my/path/rV1x2DafFr0R6tGG+1bbk++++TM" File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) mock_dir_structure(@path) @fileset.recurse = true @fileset.files.sort.should == @files.sort end end describe Puppet::FileServing::Fileset, " when following links that point to missing files" do before do @path = "/my/path" File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) @fileset.links = :follow @fileset.recurse = true @stat = stub 'stat', :directory? => true File.expects(:stat).with(@path).returns(@stat) File.expects(:stat).with(File.join(@path, "mylink")).raises(Errno::ENOENT) Dir.stubs(:entries).with(@path).returns(["mylink"]) end it "should not fail" do proc { @fileset.files }.should_not raise_error end it "should still manage the link" do @fileset.files.sort.should == %w{. mylink}.sort end end describe Puppet::FileServing::Fileset, " when ignoring" do before do @path = "/my/path" File.expects(:lstat).with(@path).returns stub("stat", :directory? => true) @fileset = Puppet::FileServing::Fileset.new(@path) end it "should use ruby's globbing to determine what files should be ignored" do @fileset.ignore = ".svn" File.expects(:fnmatch?).with(".svn", "my_file") @fileset.ignore?("my_file") end it "should ignore files whose paths match a single provided ignore value" do @fileset.ignore = ".svn" File.stubs(:fnmatch?).with(".svn", "my_file").returns true @fileset.ignore?("my_file").should be_true end it "should ignore files whose paths match any of multiple provided ignore values" do @fileset.ignore = [".svn", "CVS"] File.stubs(:fnmatch?).with(".svn", "my_file").returns false File.stubs(:fnmatch?).with("CVS", "my_file").returns true @fileset.ignore?("my_file").should be_true end end describe Puppet::FileServing::Fileset, "when merging other filesets" do before do @paths = %w{/first/path /second/path /third/path} + File.stubs(:lstat).returns stub("stat", :directory? => false) @filesets = @paths.collect do |path| File.stubs(:lstat).with(path).returns stub("stat", :directory? => true) Puppet::FileServing::Fileset.new(path, :recurse => true) end Dir.stubs(:entries).returns [] end it "should return a hash of all files in each fileset with the value being the base path" do Dir.expects(:entries).with("/first/path").returns(%w{one uno}) Dir.expects(:entries).with("/second/path").returns(%w{two dos}) Dir.expects(:entries).with("/third/path").returns(%w{three tres}) Puppet::FileServing::Fileset.merge(*@filesets).should == { "." => "/first/path", "one" => "/first/path", "uno" => "/first/path", "two" => "/second/path", "dos" => "/second/path", "three" => "/third/path", "tres" => "/third/path", } end it "should include the base directory from the first fileset" do Dir.expects(:entries).with("/first/path").returns(%w{one}) Dir.expects(:entries).with("/second/path").returns(%w{two}) Puppet::FileServing::Fileset.merge(*@filesets)["."].should == "/first/path" end it "should use the base path of the first found file when relative file paths conflict" do Dir.expects(:entries).with("/first/path").returns(%w{one}) Dir.expects(:entries).with("/second/path").returns(%w{one}) Puppet::FileServing::Fileset.merge(*@filesets)["one"].should == "/first/path" end end diff --git a/spec/unit/indirector/catalog/active_record_spec.rb b/spec/unit/indirector/catalog/active_record_spec.rb index 4e9d049a1..df61d59d7 100755 --- a/spec/unit/indirector/catalog/active_record_spec.rb +++ b/spec/unit/indirector/catalog/active_record_spec.rb @@ -1,141 +1,156 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../../spec_helper' describe "Puppet::Resource::Catalog::ActiveRecord" do confine "Missing Rails" => Puppet.features.rails? + require 'puppet/rails' + class Tableless < ActiveRecord::Base + def self.columns + @columns ||= [] + end + def self.column(name, sql_type=nil, default=nil, null=true) + columns << ActiveRecord::ConnectionAdapters::Column.new(name.to_s, default, sql_type.to_s, null) + end + end + + class Host < Tableless + column :name, :string, :null => false + column :ip, :string + column :environment, :string + column :last_compile, :datetime + end + before do require 'puppet/indirector/catalog/active_record' Puppet.features.stubs(:rails?).returns true Puppet::Rails.stubs(:init) @terminus = Puppet::Resource::Catalog::ActiveRecord.new end it "should be a subclass of the ActiveRecord terminus class" do Puppet::Resource::Catalog::ActiveRecord.ancestors.should be_include(Puppet::Indirector::ActiveRecord) end it "should use Puppet::Rails::Host as its ActiveRecord model" do Puppet::Resource::Catalog::ActiveRecord.ar_model.should equal(Puppet::Rails::Host) end describe "when finding an instance" do before do @request = stub 'request', :key => "foo", :options => {:cache_integration_hack => true} end # This hack is here because we don't want to look in the db unless we actually want # to look in the db, but our indirection architecture in 0.24.x isn't flexible # enough to tune that via configuration. it "should return nil unless ':cache_integration_hack' is set to true" do @request.options[:cache_integration_hack] = false Puppet::Rails::Host.expects(:find_by_name).never @terminus.find(@request).should be_nil end it "should use the Hosts ActiveRecord class to find the host" do Puppet::Rails::Host.expects(:find_by_name).with { |key, args| key == "foo" } @terminus.find(@request) end it "should return nil if no host instance can be found" do Puppet::Rails::Host.expects(:find_by_name).returns nil @terminus.find(@request).should be_nil end it "should return a catalog with the same name as the host if the host can be found" do host = stub 'host', :name => "foo", :resources => [] Puppet::Rails::Host.expects(:find_by_name).returns host result = @terminus.find(@request) result.should be_instance_of(Puppet::Resource::Catalog) result.name.should == "foo" end it "should set each of the host's resources as a transportable resource within the catalog" do host = stub 'host', :name => "foo" Puppet::Rails::Host.expects(:find_by_name).returns host res1 = mock 'res1', :to_transportable => "trans_res1" res2 = mock 'res2', :to_transportable => "trans_res2" host.expects(:resources).returns [res1, res2] catalog = stub 'catalog' Puppet::Resource::Catalog.expects(:new).returns catalog catalog.expects(:add_resource).with "trans_res1" catalog.expects(:add_resource).with "trans_res2" @terminus.find(@request) end end describe "when saving an instance" do before do - @host = stub 'host', :name => "foo", :save => nil, :merge_resources => nil, :last_compile= => nil, :ip= => nil, :environment= => nil + @host = Host.new(:name => "foo") + @host.stubs(:merge_resources) + @host.stubs(:save) @host.stubs(:railsmark).yields - @node = stub_everything 'node', :parameters => {} - Puppet::Node.stubs(:find).returns(@node) + @node = Puppet::Node.new("foo", :environment => "environment") + Puppet::Node.indirection.stubs(:find).with("foo").returns(@node) Puppet::Rails::Host.stubs(:find_by_name).returns @host @catalog = Puppet::Resource::Catalog.new("foo") - @request = stub 'request', :key => "foo", :instance => @catalog + @request = Puppet::Indirector::Request.new(:active_record, :save, @catalog) end it "should find the Rails host with the same name" do Puppet::Rails::Host.expects(:find_by_name).with("foo").returns @host @terminus.save(@request) end it "should create a new Rails host if none can be found" do Puppet::Rails::Host.expects(:find_by_name).with("foo").returns nil Puppet::Rails::Host.expects(:create).with(:name => "foo").returns @host @terminus.save(@request) end it "should set the catalog vertices as resources on the Rails host instance" do @catalog.expects(:vertices).returns "foo" @host.expects(:merge_resources).with("foo") @terminus.save(@request) end it "should set host ip if we could find a matching node" do @node.stubs(:parameters).returns({"ipaddress" => "192.168.0.1"}) - @host.expects(:ip=).with '192.168.0.1' - @terminus.save(@request) + @host.ip.should == '192.168.0.1' end it "should set host environment if we could find a matching node" do - @node.stubs(:environment).returns("myenv") - - @host.expects(:environment=).with 'myenv' - @terminus.save(@request) + @host.environment.should == "environment" end it "should set the last compile time on the host" do now = Time.now Time.expects(:now).returns now - @host.expects(:last_compile=).with now @terminus.save(@request) + @host.last_compile.should == now end it "should save the Rails host instance" do @host.expects(:save) @terminus.save(@request) end end end diff --git a/spec/unit/indirector/ssl_file_spec.rb b/spec/unit/indirector/ssl_file_spec.rb index 83145cffc..37098a7a9 100755 --- a/spec/unit/indirector/ssl_file_spec.rb +++ b/spec/unit/indirector/ssl_file_spec.rb @@ -1,281 +1,281 @@ #!/usr/bin/env ruby # # Created by Luke Kanies on 2008-3-10. # Copyright (c) 2007. All rights reserved. require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/indirector/ssl_file' describe Puppet::Indirector::SslFile do before do @model = mock 'model' @indirection = stub 'indirection', :name => :testing, :model => @model Puppet::Indirector::Indirection.expects(:instance).with(:testing).returns(@indirection) @file_class = Class.new(Puppet::Indirector::SslFile) do def self.to_s "Testing::Mytype" end end - @setting = :mydir + @setting = :certdir @file_class.store_in @setting - @path = "/my/directory" - Puppet.settings.stubs(:value).with(:noop).returns(false) - Puppet.settings.stubs(:value).with(@setting).returns(@path) - Puppet.settings.stubs(:value).with(:trace).returns(false) + @path = "/tmp/my_directory" + Puppet[:noop] = false + Puppet[@setting] = @path + Puppet[:trace] = false end it "should use :main and :ssl upon initialization" do Puppet.settings.expects(:use).with(:main, :ssl) @file_class.new end it "should return a nil collection directory if no directory setting has been provided" do @file_class.store_in nil @file_class.collection_directory.should be_nil end it "should return a nil file location if no location has been provided" do @file_class.store_at nil @file_class.file_location.should be_nil end it "should fail if no store directory or file location has been set" do @file_class.store_in nil @file_class.store_at nil lambda { @file_class.new }.should raise_error(Puppet::DevError) end describe "when managing ssl files" do before do Puppet.settings.stubs(:use) @searcher = @file_class.new @cert = stub 'certificate', :name => "myname" @certpath = File.join(@path, "myname.pem") @request = stub 'request', :key => @cert.name, :instance => @cert end it "should consider the file a ca file if the name is equal to what the SSL::Host class says is the CA name" do Puppet::SSL::Host.expects(:ca_name).returns "amaca" @searcher.should be_ca("amaca") end describe "when choosing the location for certificates" do it "should set them at the ca setting's path if a ca setting is available and the name resolves to the CA name" do @file_class.store_in nil @file_class.store_at :mysetting @file_class.store_ca_at :casetting Puppet.settings.stubs(:value).with(:casetting).returns "/ca/file" @searcher.expects(:ca?).with(@cert.name).returns true @searcher.path(@cert.name).should == "/ca/file" end it "should set them at the file location if a file setting is available" do @file_class.store_in nil @file_class.store_at :mysetting Puppet.settings.stubs(:value).with(:mysetting).returns "/some/file" @searcher.path(@cert.name).should == "/some/file" end it "should set them in the setting directory, with the certificate name plus '.pem', if a directory setting is available" do @searcher.path(@cert.name).should == @certpath end end describe "when finding certificates on disk" do describe "and no certificate is present" do before do # Stub things so the case management bits work. FileTest.stubs(:exist?).with(File.dirname(@certpath)).returns false FileTest.expects(:exist?).with(@certpath).returns false end it "should return nil" do @searcher.find(@request).should be_nil end end describe "and a certificate is present" do before do FileTest.expects(:exist?).with(@certpath).returns true end it "should return an instance of the model, which it should use to read the certificate" do cert = mock 'cert' model = mock 'model' @file_class.stubs(:model).returns model model.expects(:new).with("myname").returns cert cert.expects(:read).with(@certpath) @searcher.find(@request).should equal(cert) end end describe "and a certificate is present but has uppercase letters" do before do @request = stub 'request', :key => "myhost" end # This is kind of more an integration test; it's for #1382, until # the support for upper-case certs can be removed around mid-2009. it "should rename the existing file to the lower-case path" do @path = @searcher.path("myhost") FileTest.expects(:exist?).with(@path).returns(false) dir, file = File.split(@path) FileTest.expects(:exist?).with(dir).returns true Dir.expects(:entries).with(dir).returns [".", "..", "something.pem", file.upcase] File.expects(:rename).with(File.join(dir, file.upcase), @path) cert = mock 'cert' model = mock 'model' @searcher.stubs(:model).returns model @searcher.model.expects(:new).with("myhost").returns cert cert.expects(:read).with(@path) @searcher.find(@request) end end end describe "when saving certificates to disk" do before do FileTest.stubs(:directory?).returns true FileTest.stubs(:writable?).returns true end it "should fail if the directory is absent" do FileTest.expects(:directory?).with(File.dirname(@certpath)).returns false lambda { @searcher.save(@request) }.should raise_error(Puppet::Error) end it "should fail if the directory is not writeable" do FileTest.stubs(:directory?).returns true FileTest.expects(:writable?).with(File.dirname(@certpath)).returns false lambda { @searcher.save(@request) }.should raise_error(Puppet::Error) end it "should save to the path the output of converting the certificate to a string" do fh = mock 'filehandle' fh.expects(:print).with("mycert") @searcher.stubs(:write).yields fh @cert.expects(:to_s).returns "mycert" @searcher.save(@request) end describe "and a directory setting is set" do it "should use the Settings class to write the file" do @searcher.class.store_in @setting fh = mock 'filehandle' fh.stubs :print Puppet.settings.expects(:writesub).with(@setting, @certpath).yields fh @searcher.save(@request) end end describe "and a file location is set" do it "should use the filehandle provided by the Settings" do @searcher.class.store_at @setting fh = mock 'filehandle' fh.stubs :print Puppet.settings.expects(:write).with(@setting).yields fh @searcher.save(@request) end end describe "and the name is the CA name and a ca setting is set" do it "should use the filehandle provided by the Settings" do @searcher.class.store_at @setting @searcher.class.store_ca_at :castuff Puppet.settings.stubs(:value).with(:castuff).returns "castuff stub" fh = mock 'filehandle' fh.stubs :print Puppet.settings.expects(:write).with(:castuff).yields fh @searcher.stubs(:ca?).returns true @searcher.save(@request) end end end describe "when destroying certificates" do describe "that do not exist" do before do FileTest.expects(:exist?).with(@certpath).returns false end it "should return false" do @searcher.destroy(@request).should be_false end end describe "that exist" do before do FileTest.expects(:exist?).with(@certpath).returns true end it "should unlink the certificate file" do File.expects(:unlink).with(@certpath) @searcher.destroy(@request) end it "should log that is removing the file" do File.stubs(:exist?).returns true File.stubs(:unlink) Puppet.expects(:notice) @searcher.destroy(@request) end end end describe "when searching for certificates" do before do @model = mock 'model' @file_class.stubs(:model).returns @model end it "should return a certificate instance for all files that exist" do Dir.expects(:entries).with(@path).returns %w{one.pem two.pem} one = stub 'one', :read => nil two = stub 'two', :read => nil @model.expects(:new).with("one").returns one @model.expects(:new).with("two").returns two @searcher.search(@request).should == [one, two] end it "should read each certificate in using the model's :read method" do Dir.expects(:entries).with(@path).returns %w{one.pem} one = stub 'one' one.expects(:read).with(File.join(@path, "one.pem")) @model.expects(:new).with("one").returns one @searcher.search(@request) end it "should skip any files that do not match /\.pem$/" do Dir.expects(:entries).with(@path).returns %w{. .. one.pem} one = stub 'one', :read => nil @model.expects(:new).with("one").returns one @searcher.search(@request) end end end end diff --git a/spec/unit/provider/service/init_spec.rb b/spec/unit/provider/service/init_spec.rb index bbc88ff76..856821985 100755 --- a/spec/unit/provider/service/init_spec.rb +++ b/spec/unit/provider/service/init_spec.rb @@ -1,168 +1,170 @@ #!/usr/bin/env ruby # # Unit testing for the Init service Provider # require File.dirname(__FILE__) + '/../../../spec_helper' provider_class = Puppet::Type.type(:service).provider(:init) describe provider_class do before :each do @class = Puppet::Type.type(:service).provider(:init) @resource = stub 'resource' @resource.stubs(:[]).returns(nil) @resource.stubs(:[]).with(:name).returns "myservice" # @resource.stubs(:[]).with(:ensure).returns :enabled @resource.stubs(:[]).with(:path).returns ["/service/path","/alt/service/path"] # @resource.stubs(:ref).returns "Service[myservice]" File.stubs(:directory?).returns(true) @provider = provider_class.new @provider.resource = @resource end describe "when getting all service instances" do before :each do @services = ['one', 'two', 'three', 'four'] Dir.stubs(:entries).returns @services FileTest.stubs(:directory?).returns(true) FileTest.stubs(:executable?).returns(true) @class.stubs(:defpath).returns('tmp') end it "should return instances for all services" do @services.each do |inst| @class.expects(:new).with{|hash| hash[:name] == inst}.returns("#{inst}_instance") end results = @services.collect {|x| "#{x}_instance"} @class.instances.should == results end it "should omit an array of services from exclude list" do exclude = ['two', 'four'] (@services-exclude).each do |inst| @class.expects(:new).with{|hash| hash[:name] == inst}.returns("#{inst}_instance") end results = (@services-exclude).collect {|x| "#{x}_instance"} @class.get_services(@class.defpath, exclude).should == results end it "should omit a single service from the exclude list" do exclude = 'two' (@services-exclude.to_a).each do |inst| @class.expects(:new).with{|hash| hash[:name] == inst}.returns("#{inst}_instance") end results = @services.reject{|x| x==exclude }.collect {|x| "#{x}_instance"} @class.get_services(@class.defpath, exclude).should == results end it "should use defpath" do @services.each do |inst| @class.expects(:new).with{|hash| hash[:path] == @class.defpath}.returns("#{inst}_instance") end results = @services.sort.collect {|x| "#{x}_instance"} @class.instances.sort.should == results end it "should set hasstatus to true for providers" do @services.each do |inst| @class.expects(:new).with{|hash| hash[:name] == inst && hash[:hasstatus] == true}.returns("#{inst}_instance") end results = @services.collect {|x| "#{x}_instance"} @class.instances.should == results end end describe "when searching for the init script" do it "should discard paths that do not exist" do File.stubs(:exist?).returns(false) File.stubs(:directory?).returns(false) @provider.paths.should be_empty end it "should discard paths that are not directories" do File.stubs(:exist?).returns(true) File.stubs(:directory?).returns(false) @provider.paths.should be_empty end it "should be able to find the init script in the service path" do + File.stubs(:stat).raises(Errno::ENOENT.new('No such file or directory')) File.expects(:stat).with("/service/path/myservice").returns true @provider.initscript.should == "/service/path/myservice" end it "should be able to find the init script in the service path" do + File.stubs(:stat).raises(Errno::ENOENT.new('No such file or directory')) File.expects(:stat).with("/alt/service/path/myservice").returns true @provider.initscript.should == "/alt/service/path/myservice" end it "should fail if the service isn't there" do lambda { @provider.initscript }.should raise_error(Puppet::Error, "Could not find init script for 'myservice'") end end describe "if the init script is present" do before :each do File.stubs(:stat).with("/service/path/myservice").returns true end [:start, :stop, :status, :restart].each do |method| it "should have a #{method} method" do @provider.should respond_to(method) end describe "when running #{method}" do it "should use any provided explicit command" do @resource.stubs(:[]).with(method).returns "/user/specified/command" @provider.expects(:execute).with { |command, *args| command == ["/user/specified/command"] } @provider.send(method) end it "should pass #{method} to the init script when no explicit command is provided" do @resource.stubs(:[]).with("has#{method}".intern).returns :true @provider.expects(:execute).with { |command, *args| command == ["/service/path/myservice",method]} @provider.send(method) end end end describe "when checking status" do describe "when hasstatus is :true" do before :each do @resource.stubs(:[]).with(:hasstatus).returns :true end it "should execute the command" do @provider.expects(:texecute).with(:status, ['/service/path/myservice', :status], false).returns("") @provider.status end it "should consider the process running if the command returns 0" do @provider.expects(:texecute).with(:status, ['/service/path/myservice', :status], false).returns("") $CHILD_STATUS.stubs(:exitstatus).returns(0) @provider.status.should == :running end [-10,-1,1,10].each { |ec| it "should consider the process stopped if the command returns something non-0" do @provider.expects(:texecute).with(:status, ['/service/path/myservice', :status], false).returns("") $CHILD_STATUS.stubs(:exitstatus).returns(ec) @provider.status.should == :stopped end } end describe "when hasstatus is not :true" do it "should consider the service :running if it has a pid" do @provider.expects(:getpid).returns "1234" @provider.status.should == :running end it "should consider the service :stopped if it doesn't have a pid" do @provider.expects(:getpid).returns nil @provider.status.should == :stopped end end end describe "when restarting and hasrestart is not :true" do it "should stop and restart the process" do @provider.expects(:texecute).with(:stop, ['/service/path/myservice', :stop ], true).returns("") @provider.expects(:texecute).with(:start,['/service/path/myservice', :start], true).returns("") $CHILD_STATUS.stubs(:exitstatus).returns(0) @provider.restart end end end end diff --git a/spec/unit/resource/type_collection_spec.rb b/spec/unit/resource/type_collection_spec.rb index 577aea42b..ff4c22234 100644 --- a/spec/unit/resource/type_collection_spec.rb +++ b/spec/unit/resource/type_collection_spec.rb @@ -1,467 +1,459 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/resource/type_collection' require 'puppet/resource/type' describe Puppet::Resource::TypeCollection do before do @instance = Puppet::Resource::Type.new(:hostclass, "foo") @code = Puppet::Resource::TypeCollection.new("env") end it "should require an environment at initialization" do env = Puppet::Node::Environment.new("testing") Puppet::Resource::TypeCollection.new(env).environment.should equal(env) end it "should convert the environment into an environment instance if a string is provided" do env = Puppet::Node::Environment.new("testing") Puppet::Resource::TypeCollection.new("testing").environment.should equal(env) end it "should create a 'loader' at initialization" do Puppet::Resource::TypeCollection.new("testing").loader.should be_instance_of(Puppet::Parser::TypeLoader) end it "should be able to add a resource type" do Puppet::Resource::TypeCollection.new("env").should respond_to(:add) end it "should consider '<<' to be an alias to 'add' but should return self" do loader = Puppet::Resource::TypeCollection.new("env") loader.expects(:add).with "foo" loader.expects(:add).with "bar" loader << "foo" << "bar" end it "should set itself as the code collection for added resource types" do loader = Puppet::Resource::TypeCollection.new("env") node = Puppet::Resource::Type.new(:node, "foo") @code.add(node) @code.node("foo").should equal(node) node.resource_type_collection.should equal(@code) end it "should store node resource types as nodes" do node = Puppet::Resource::Type.new(:node, "foo") @code.add(node) @code.node("foo").should equal(node) end it "should store hostclasses as hostclasses" do klass = Puppet::Resource::Type.new(:hostclass, "foo") @code.add(klass) @code.hostclass("foo").should equal(klass) end it "should store definitions as definitions" do define = Puppet::Resource::Type.new(:definition, "foo") @code.add(define) @code.definition("foo").should equal(define) end it "should merge new classes with existing classes of the same name" do loader = Puppet::Resource::TypeCollection.new("env") first = Puppet::Resource::Type.new(:hostclass, "foo") second = Puppet::Resource::Type.new(:hostclass, "foo") loader.add first first.expects(:merge).with(second) loader.add(second) end it "should remove all nodes, classes, and definitions when cleared" do loader = Puppet::Resource::TypeCollection.new("env") loader.add Puppet::Resource::Type.new(:hostclass, "class") loader.add Puppet::Resource::Type.new(:definition, "define") loader.add Puppet::Resource::Type.new(:node, "node") loader.clear loader.hostclass("class").should be_nil loader.definition("define").should be_nil loader.node("node").should be_nil end describe "when looking up names" do before do @type = Puppet::Resource::Type.new(:hostclass, "ns::klass") end it "should support looking up with multiple namespaces" do @code.add @type @code.find_hostclass(%w{boo baz ns}, "klass").should equal(@type) end it "should not attempt to import anything when the type is already defined" do @code.add @type @code.loader.expects(:import).never @code.find_hostclass(%w{ns}, "klass").should equal(@type) end describe "that need to be loaded" do it "should use the loader to load the files" do @code.loader.expects(:load_until).with(["ns"], "klass") @code.find_or_load(["ns"], "klass", :hostclass) end it "should downcase the name and downcase and array-fy the namespaces before passing to the loader" do @code.loader.expects(:load_until).with(["ns"], "klass") @code.find_or_load("Ns", "Klass", :hostclass) end it "should attempt to find the type when the loader yields" do @code.loader.expects(:load_until).yields @code.expects(:find).with(["ns"], "klass", :hostclass).times(2).returns(false).then.returns(true) @code.find_or_load("ns", "klass", :hostclass) end it "should return the result of 'load_until'" do @code.loader.expects(:load_until).returns "foo" @code.find_or_load("Ns", "Klass", :hostclass).should == "foo" end it "should return nil if the name isn't found" do @code.stubs(:load_until).returns(nil) @code.find_or_load("Ns", "Klass", :hostclass).should be_nil end end end %w{hostclass node definition}.each do |data| before do @instance = Puppet::Resource::Type.new(data, "foo") end it "should have a method for adding a #{data}" do Puppet::Resource::TypeCollection.new("env").should respond_to("add_#{data}") end it "should use the name of the instance to add it" do loader = Puppet::Resource::TypeCollection.new("env") loader.send("add_#{data}", @instance) loader.send(data, @instance.name).should equal(@instance) end unless data == "hostclass" it "should fail to add a #{data} when one already exists" do loader = Puppet::Resource::TypeCollection.new("env") loader.add @instance lambda { loader.add(@instance) }.should raise_error(Puppet::ParseError) end end it "should return the added #{data}" do loader = Puppet::Resource::TypeCollection.new("env") loader.add(@instance).should equal(@instance) end it "should be able to retrieve #{data} by name" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(data, "bar") loader.add instance loader.send(data, "bar").should equal(instance) end it "should retrieve #{data} insensitive to case" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(data, "Bar") loader.add instance loader.send(data, "bAr").should equal(instance) end it "should return nil when asked for a #{data} that has not been added" do Puppet::Resource::TypeCollection.new("env").send(data, "foo").should be_nil end it "should be able to retrieve all #{data}s" do plurals = { "hostclass" => "hostclasses", "node" => "nodes", "definition" => "definitions" } loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(data, "foo") loader.add instance loader.send(plurals[data]).should == { "foo" => instance } end end describe "when finding a qualified instance" do it "should return any found instance if the instance name is fully qualified" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find("namespace", "::foo::bar", :hostclass).should equal(instance) end it "should return nil if the instance name is fully qualified and no such instance exists" do loader = Puppet::Resource::TypeCollection.new("env") loader.find("namespace", "::foo::bar", :hostclass).should be_nil end it "should be able to find classes in the base namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo") loader.add instance loader.find("", "foo", :hostclass).should equal(instance) end it "should return the partially qualified object if it exists in a provided namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find("foo", "bar::baz", :hostclass).should equal(instance) end it "should be able to find partially qualified objects in any of the provided namespaces" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find(["nons", "foo", "otherns"], "bar::baz", :hostclass).should equal(instance) end it "should return the unqualified object if it exists in a provided namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find("foo", "bar", :hostclass).should equal(instance) end it "should return the unqualified object if it exists in the parent namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar") loader.add instance loader.find("foo::bar::baz", "bar", :hostclass).should equal(instance) end it "should should return the partially qualified object if it exists in the parent namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find("foo::bar", "bar::baz", :hostclass).should equal(instance) end it "should return the qualified object if it exists in the root namespace" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find("foo::bar", "foo::bar::baz", :hostclass).should equal(instance) end it "should return nil if the object cannot be found" do loader = Puppet::Resource::TypeCollection.new("env") instance = Puppet::Resource::Type.new(:hostclass, "foo::bar::baz") loader.add instance loader.find("foo::bar", "eh", :hostclass).should be_nil end describe "when topscope has a class that has the same name as a local class" do before do @loader = Puppet::Resource::TypeCollection.new("env") [ "foo::bar", "bar" ].each do |name| @loader.add Puppet::Resource::Type.new(:hostclass, name) end end it "should favor the local class, if the name is unqualified" do @loader.find("foo", "bar", :hostclass).name.should == 'foo::bar' end it "should only look in the topclass, if the name is qualified" do @loader.find("foo", "::bar", :hostclass).name.should == 'bar' end end it "should not look in the local scope for classes when the name is qualified" do @loader = Puppet::Resource::TypeCollection.new("env") @loader.add Puppet::Resource::Type.new(:hostclass, "foo::bar") @loader.find("foo", "::bar", :hostclass).should == nil end end it "should use the generic 'find' method with an empty namespace to find nodes" do loader = Puppet::Resource::TypeCollection.new("env") loader.expects(:find).with("", "bar", :node) loader.find_node(stub("ignored"), "bar") end it "should use the 'find_or_load' method to find hostclasses" do loader = Puppet::Resource::TypeCollection.new("env") loader.expects(:find_or_load).with("foo", "bar", :hostclass) loader.find_hostclass("foo", "bar") end it "should use the 'find_or_load' method to find definitions" do loader = Puppet::Resource::TypeCollection.new("env") loader.expects(:find_or_load).with("foo", "bar", :definition) loader.find_definition("foo", "bar") end it "should indicate whether any nodes are defined" do loader = Puppet::Resource::TypeCollection.new("env") loader.add_node(Puppet::Resource::Type.new(:node, "foo")) loader.should be_nodes end it "should indicate whether no nodes are defined" do Puppet::Resource::TypeCollection.new("env").should_not be_nodes end describe "when finding nodes" do before :each do @loader = Puppet::Resource::TypeCollection.new("env") end it "should return any node whose name exactly matches the provided node name" do node = Puppet::Resource::Type.new(:node, "foo") @loader << node @loader.node("foo").should equal(node) end it "should return the first regex node whose regex matches the provided node name" do node1 = Puppet::Resource::Type.new(:node, /\w/) node2 = Puppet::Resource::Type.new(:node, /\d/) @loader << node1 << node2 @loader.node("foo10").should equal(node1) end it "should preferentially return a node whose name is string-equal over returning a node whose regex matches a provided name" do node1 = Puppet::Resource::Type.new(:node, /\w/) node2 = Puppet::Resource::Type.new(:node, "foo") @loader << node1 << node2 @loader.node("foo").should equal(node2) end end describe "when managing files" do before do @loader = Puppet::Resource::TypeCollection.new("env") Puppet::Util::LoadedFile.stubs(:new).returns stub("watched_file") end it "should have a method for specifying a file should be watched" do @loader.should respond_to(:watch_file) end it "should have a method for determining if a file is being watched" do @loader.watch_file("/foo/bar") @loader.should be_watching_file("/foo/bar") end it "should use LoadedFile to watch files" do Puppet::Util::LoadedFile.expects(:new).with("/foo/bar").returns stub("watched_file") @loader.watch_file("/foo/bar") end it "should be considered stale if any files have changed" do file1 = stub 'file1', :changed? => false file2 = stub 'file2', :changed? => true Puppet::Util::LoadedFile.expects(:new).times(2).returns(file1).then.returns(file2) @loader.watch_file("/foo/bar") @loader.watch_file("/other/bar") @loader.should be_stale end it "should not be considered stable if no files have changed" do file1 = stub 'file1', :changed? => false file2 = stub 'file2', :changed? => false Puppet::Util::LoadedFile.expects(:new).times(2).returns(file1).then.returns(file2) @loader.watch_file("/foo/bar") @loader.watch_file("/other/bar") @loader.should_not be_stale end end describe "when performing initial import" do before do @parser = stub 'parser', :file= => nil, :string => nil, :parse => nil Puppet::Parser::Parser.stubs(:new).returns @parser @code = Puppet::Resource::TypeCollection.new("env") end it "should create a new parser instance" do Puppet::Parser::Parser.expects(:new).returns @parser @code.perform_initial_import end it "should set the parser's string to the 'code' setting and parse if code is available" do Puppet.settings[:code] = "my code" @parser.expects(:string=).with "my code" @parser.expects(:parse) @code.perform_initial_import end it "should set the parser's file to the 'manifest' setting and parse if no code is available and the manifest is available" do File.stubs(:expand_path).with("/my/file").returns "/my/file" File.expects(:exist?).with("/my/file").returns true Puppet.settings[:manifest] = "/my/file" @parser.expects(:file=).with "/my/file" @parser.expects(:parse) @code.perform_initial_import end it "should not attempt to load a manifest if none is present" do File.stubs(:expand_path).with("/my/file").returns "/my/file" File.expects(:exist?).with("/my/file").returns false Puppet.settings[:manifest] = "/my/file" @parser.expects(:file=).never @parser.expects(:parse).never @code.perform_initial_import end it "should fail helpfully if there is an error importing" do File.stubs(:exist?).returns true @parser.expects(:parse).raises ArgumentError lambda { @code.perform_initial_import }.should raise_error(Puppet::Error) end - - it "should not do anything if the ignore_import settings is set" do - Puppet.settings[:ignoreimport] = true - @parser.expects(:string=).never - @parser.expects(:file=).never - @parser.expects(:parse).never - @code.perform_initial_import - end end describe "when determining the configuration version" do before do @code = Puppet::Resource::TypeCollection.new("env") end it "should default to the current time" do time = Time.now Time.stubs(:now).returns time @code.version.should == time.to_i end it "should use the output of the environment's config_version setting if one is provided" do @code.environment.stubs(:[]).with(:config_version).returns("/my/foo") Puppet::Util.expects(:execute).with(["/my/foo"]).returns "output\n" @code.version.should == "output" end it "should raise a puppet parser error if executing config_version fails" do @code.environment.stubs(:[]).with(:config_version).returns("test") Puppet::Util.expects(:execute).raises(Puppet::ExecutionFailure.new("msg")) lambda { @code.version }.should raise_error(Puppet::ParseError) end end end diff --git a/spec/unit/transaction/change_spec.rb b/spec/unit/transaction/change_spec.rb index e443e3baa..fbc662df0 100755 --- a/spec/unit/transaction/change_spec.rb +++ b/spec/unit/transaction/change_spec.rb @@ -1,193 +1,206 @@ #!/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" + @property = stub 'property', :path => "/property/path", :should => "shouldval", :is_to_s => 'formatted_property' @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 + @property.expects(:sync).never.never.never.never.never # VERY IMPORTANT @event.expects(:status=).with("noop") @change.apply.should == @event end end describe "in audit mode" do - before { @change.auditing = true } + before do + @change.auditing = true + @change.old_audit_value = "old_value" + @property.stubs(:insync?).returns(true) + end 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") } + message = nil + @event.expects(:message=).with { |msg| message = msg } @change.apply + message.should == "audit change: previously recorded value formatted_property has been changed to formatted_property" end it "should produce a :audit event and return" do @property.stub_everything @event.expects(:status=).with("audit") @change.apply.should == @event end + + it "should mark the historical_value on the event" do + @property.stub_everything + + @change.apply.historical_value.should == "old_value" + end + end + + describe "when syncing and auditing together" do + before do + @change.auditing = true + @change.old_audit_value = "old_value" + @property.stubs(:insync?).returns(false) + end + + it "should sync the property" do + @property.expects(:sync) + + @change.apply + end + + it "should produce a success event" do + @property.stub_everything + + @change.apply.status.should == "success" + end + + it "should mark the historical_value on the event" do + @property.stub_everything + + @change.apply.historical_value.should == "old_value" + 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 + @property.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 + @property.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/report_spec.rb b/spec/unit/transaction/report_spec.rb index 7e0b0554b..77f82159b 100755 --- a/spec/unit/transaction/report_spec.rb +++ b/spec/unit/transaction/report_spec.rb @@ -1,234 +1,242 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' require 'puppet/transaction/report' describe Puppet::Transaction::Report do before do Puppet::Util::Storage.stubs(:store) end it "should set its host name to the certname" do Puppet.settings.expects(:value).with(:certname).returns "myhost" Puppet::Transaction::Report.new.host.should == "myhost" end it "should return its host name as its name" do r = Puppet::Transaction::Report.new r.name.should == r.host end it "should create an initialization timestamp" do Time.expects(:now).returns "mytime" Puppet::Transaction::Report.new.time.should == "mytime" end + it "should have a default 'kind' of 'apply'" do + Puppet::Transaction::Report.new.kind.should == "apply" + end + + it "should take a 'kind' as an argument" do + Puppet::Transaction::Report.new("inspect").kind.should == "inspect" + end + describe "when accepting logs" do before do @report = Puppet::Transaction::Report.new end it "should add new logs to the log list" do @report << "log" @report.logs[-1].should == "log" end it "should return self" do r = @report << "log" r.should equal(@report) end end describe "when accepting resource statuses" do before do @report = Puppet::Transaction::Report.new end it "should add each status to its status list" do status = stub 'status', :resource => "foo" @report.add_resource_status status @report.resource_statuses["foo"].should equal(status) end end describe "when using the indirector" do it "should redirect :find to the indirection" do @indirection = stub 'indirection', :name => :report Puppet::Transaction::Report.stubs(:indirection).returns(@indirection) @indirection.expects(:find) Puppet::Transaction::Report.find(:report) end it "should redirect :save to the indirection" do Facter.stubs(:value).returns("eh") @indirection = stub 'indirection', :name => :report Puppet::Transaction::Report.stubs(:indirection).returns(@indirection) report = Puppet::Transaction::Report.new @indirection.expects(:save) report.save end it "should default to the 'processor' terminus" do Puppet::Transaction::Report.indirection.terminus_class.should == :processor end it "should delegate its name attribute to its host method" do report = Puppet::Transaction::Report.new report.expects(:host).returns "me" report.name.should == "me" end after do Puppet::Util::Cacher.expire end end describe "when computing exit status" do it "should produce 2 if changes are present" do report = Puppet::Transaction::Report.new report.add_metric("changes", {:total => 1}) report.add_metric("resources", {:failed => 0}) report.exit_status.should == 2 end it "should produce 4 if failures are present" do report = Puppet::Transaction::Report.new report.add_metric("changes", {:total => 0}) report.add_metric("resources", {:failed => 1}) report.exit_status.should == 4 end it "should produce 6 if both changes and failures are present" do report = Puppet::Transaction::Report.new report.add_metric("changes", {:total => 1}) report.add_metric("resources", {:failed => 1}) report.exit_status.should == 6 end end describe "when calculating metrics" do before do @report = Puppet::Transaction::Report.new end def metric(name, value) if metric = @report.metrics[name.to_s] metric[value] else nil end end def add_statuses(count, type = :file) 3.times do |i| status = Puppet::Resource::Status.new(Puppet::Type.type(type).new(:title => "/my/path#{i}")) yield status if block_given? @report.add_resource_status status end end [:time, :resources, :changes, :events].each do |type| it "should add #{type} metrics" do @report.calculate_metrics @report.metrics[type.to_s].should be_instance_of(Puppet::Transaction::Metric) end end describe "for resources" do it "should provide the total number of resources" do add_statuses(3) @report.calculate_metrics metric(:resources, :total).should == 3 end Puppet::Resource::Status::STATES.each do |state| it "should provide the number of #{state} resources as determined by the status objects" do add_statuses(3) { |status| status.send(state.to_s + "=", true) } @report.calculate_metrics metric(:resources, state).should == 3 end end end describe "for changes" do it "should provide the number of changes from the resource statuses" do add_statuses(3) { |status| status.change_count = 3 } @report.calculate_metrics metric(:changes, :total).should == 9 end end describe "for times" do it "should provide the total amount of time for each resource type" do add_statuses(3, :file) do |status| status.evaluation_time = 1 end add_statuses(3, :exec) do |status| status.evaluation_time = 2 end add_statuses(3, :mount) do |status| status.evaluation_time = 3 end @report.calculate_metrics metric(:time, "file").should == 3 metric(:time, "exec").should == 6 metric(:time, "mount").should == 9 end it "should add any provided times from external sources" do @report.add_times :foobar, 50 @report.calculate_metrics metric(:time, "foobar").should == 50 end end describe "for events" do it "should provide the total number of events" do add_statuses(3) do |status| 3.times { |i| status.add_event(Puppet::Transaction::Event.new) } end @report.calculate_metrics metric(:events, :total).should == 9 end Puppet::Transaction::Event::EVENT_STATUSES.each do |status_name| it "should provide the number of #{status_name} events" do add_statuses(3) do |status| 3.times do |i| event = Puppet::Transaction::Event.new event.status = status_name status.add_event(event) end end @report.calculate_metrics metric(:events, status_name).should == 9 end end end end describe "when producing a summary" do before do resource = Puppet::Type.type(:notify).new(:name => "testing") catalog = Puppet::Resource::Catalog.new catalog.add_resource resource trans = catalog.apply @report = trans.report @report.calculate_metrics end %w{Changes Total Resources}.each do |main| it "should include information on #{main} in the summary" do @report.summary.should be_include(main) end end end end diff --git a/spec/unit/transaction/resource_harness_spec.rb b/spec/unit/transaction/resource_harness_spec.rb index 255481ae4..b143c21ed 100755 --- a/spec/unit/transaction/resource_harness_spec.rb +++ b/spec/unit/transaction/resource_harness_spec.rb @@ -1,401 +1,514 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' +require 'puppet_spec/files' require 'puppet/transaction/resource_harness' describe Puppet::Transaction::ResourceHarness do + include PuppetSpec::Files + 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(:debug) - @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 copy audited parameters" do +# @resource[:audit] = :mode +# @harness.cache(@resource, :mode, "755") +# @harness.changes_to_perform(@status, @resource) +# @resource[:mode].should == "755" +# 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' + mode = stub_everything 'mode_change' + owner = stub_everything '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 + @current_state[:mode] = "755" @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"), :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 + + describe "when there's not an existing audited value" do + it "should save the old value before applying the change if it's audited" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0750).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "750" + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: mode changed '750' to '755'", + "notice: /#{resource}/mode: audit change: newly-recorded recorded value 750" + ] + end + + it "should audit the value if there's no change" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0755).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "755" + + (File.stat(test_file).mode & 0777).should == 0755 + + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: audit change: newly-recorded recorded value 755" + ] + end + + it "should have :absent for audited value if the file doesn't exist" do + test_file = tmpfile('foo') + + resource = Puppet::Type.type(:file).new :ensure => 'present', :path => test_file, :mode => '755', :audit => :mode + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == :absent + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/ensure: created", + "notice: /#{resource}/mode: audit change: newly-recorded recorded value absent" + ] + end + + it "should do nothing if there are no changes to make and the stored value is correct" do + test_file = tmpfile('foo') + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode, :ensure => 'absent' + @harness.cache(resource, :mode, :absent) + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == :absent + + File.exists?(test_file).should == false + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [] + end + end + + describe "when there's an existing audited value" do + it "should save the old value before applying the change" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0750).close + + resource = Puppet::Type.type(:file).new :path => test_file, :audit => :mode + @harness.cache(resource, :mode, '555') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "750" + + (File.stat(test_file).mode & 0777).should == 0750 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: audit change: previously recorded value 555 has been changed to 750" + ] + end + + it "should save the old value before applying the change" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0750).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + @harness.cache(resource, :mode, '555') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "750" + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: mode changed '750' to '755' (previously recorded value was 555)" + ] + end + + it "should audit the value if there's no change" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0755).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + @harness.cache(resource, :mode, '555') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "755" + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/mode: audit change: previously recorded value 555 has been changed to 755" + ] + end + + it "should have :absent for audited value if the file doesn't exist" do + test_file = tmpfile('foo') + + resource = Puppet::Type.type(:file).new :ensure => 'present', :path => test_file, :mode => '755', :audit => :mode + @harness.cache(resource, :mode, '555') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == :absent + + (File.stat(test_file).mode & 0777).should == 0755 + + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [ + "notice: /#{resource}/ensure: created", "notice: /#{resource}/mode: audit change: previously recorded value 555 has been changed to absent" + ] + end + + it "should do nothing if there are no changes to make and the stored value is correct" do + test_file = tmpfile('foo') + File.open(test_file, "w", 0755).close + + resource = Puppet::Type.type(:file).new :path => test_file, :mode => '755', :audit => :mode + @harness.cache(resource, :mode, '755') + + @harness.evaluate(resource) + @harness.cached(resource, :mode).should == "755" + + (File.stat(test_file).mode & 0777).should == 0755 + @logs.map {|l| "#{l.level}: #{l.source}: #{l.message}"}.should =~ [] + end + 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/file/source_spec.rb b/spec/unit/type/file/source_spec.rb index a45a1f74e..00cc2f235 100755 --- a/spec/unit/type/file/source_spec.rb +++ b/spec/unit/type/file/source_spec.rb @@ -1,272 +1,272 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } source = Puppet::Type.type(:file).attrclass(:source) describe Puppet::Type.type(:file).attrclass(:source) do before do # Wow that's a messy interface to the resource. - @resource = stub 'resource', :[]= => nil, :property => nil, :catalog => stub("catalog", :dependent_data_expired? => false) + @resource = stub 'resource', :[]= => nil, :property => nil, :catalog => stub("catalog", :dependent_data_expired? => false), :line => 0, :file => '' end it "should be a subclass of Parameter" do source.superclass.must == Puppet::Parameter end describe "when initializing" do it "should fail if the set values are not URLs" do s = source.new(:resource => @resource) URI.expects(:parse).with('foo').raises RuntimeError lambda { s.value = %w{foo} }.must raise_error(Puppet::Error) end it "should fail if the URI is not a local file, file URI, or puppet URI" do s = source.new(:resource => @resource) lambda { s.value = %w{http://foo/bar} }.must raise_error(Puppet::Error) end end it "should have a method for retrieving its metadata" do source.new(:resource => @resource).must respond_to(:metadata) end it "should have a method for setting its metadata" do source.new(:resource => @resource).must respond_to(:metadata=) end describe "when returning the metadata" do before do @metadata = stub 'metadata', :source= => nil end it "should return already-available metadata" do @source = source.new(:resource => @resource) @source.metadata = "foo" @source.metadata.should == "foo" end it "should return nil if no @should value is set and no metadata is available" do @source = source.new(:resource => @resource) @source.metadata.should be_nil end it "should collect its metadata using the Metadata class if it is not already set" do @source = source.new(:resource => @resource, :value => "/foo/bar") Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").returns @metadata @source.metadata end it "should use the metadata from the first found source" do metadata = stub 'metadata', :source= => nil @source = source.new(:resource => @resource, :value => ["/foo/bar", "/fee/booz"]) Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").returns nil Puppet::FileServing::Metadata.expects(:find).with("/fee/booz").returns metadata @source.metadata.should equal(metadata) end it "should store the found source as the metadata's source" do metadata = mock 'metadata' @source = source.new(:resource => @resource, :value => "/foo/bar") Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").returns metadata metadata.expects(:source=).with("/foo/bar") @source.metadata end it "should fail intelligently if an exception is encountered while querying for metadata" do @source = source.new(:resource => @resource, :value => "/foo/bar") Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").raises RuntimeError @source.expects(:fail).raises ArgumentError lambda { @source.metadata }.should raise_error(ArgumentError) end it "should fail if no specified sources can be found" do @source = source.new(:resource => @resource, :value => "/foo/bar") Puppet::FileServing::Metadata.expects(:find).with("/foo/bar").returns nil @source.expects(:fail).raises RuntimeError lambda { @source.metadata }.should raise_error(RuntimeError) end it "should expire the metadata appropriately" do expirer = stub 'expired', :dependent_data_expired? => true metadata = stub 'metadata', :source= => nil Puppet::FileServing::Metadata.expects(:find).with("/fee/booz").returns metadata @source = source.new(:resource => @resource, :value => ["/fee/booz"]) @source.metadata = "foo" @source.stubs(:expirer).returns expirer @source.metadata.should_not == "foo" end end it "should have a method for setting the desired values on the resource" do source.new(:resource => @resource).must respond_to(:copy_source_values) end describe "when copying the source values" do before do @resource = Puppet::Type.type(:file).new :path => "/foo/bar" @source = source.new(:resource => @resource) @metadata = stub 'metadata', :owner => 100, :group => 200, :mode => 123, :checksum => "{md5}asdfasdf", :ftype => "file" @source.stubs(:metadata).returns @metadata end it "should fail if there is no metadata" do @source.stubs(:metadata).returns nil @source.expects(:devfail).raises ArgumentError lambda { @source.copy_source_values }.should raise_error(ArgumentError) end it "should set :ensure to the file type" do @metadata.stubs(:ftype).returns "file" @source.copy_source_values @resource[:ensure].must == :file end it "should not set 'ensure' if it is already set to 'absent'" do @metadata.stubs(:ftype).returns "file" @resource[:ensure] = :absent @source.copy_source_values @resource[:ensure].must == :absent end describe "and the source is a file" do before do @metadata.stubs(:ftype).returns "file" end it "should copy the metadata's owner, group, checksum, and mode to the resource if they are not set on the resource" do Puppet.features.expects(:root?).returns true @source.copy_source_values @resource[:owner].must == 100 @resource[:group].must == 200 - @resource[:mode].must == 123 + @resource[:mode].must == "173" # Metadata calls it checksum, we call it content. @resource[:content].must == @metadata.checksum end it "should not copy the metadata's owner to the resource if it is already set" do @resource[:owner] = 1 @resource[:group] = 2 @resource[:mode] = 3 @resource[:content] = "foobar" @source.copy_source_values @resource[:owner].must == 1 @resource[:group].must == 2 - @resource[:mode].must == 3 + @resource[:mode].must == "3" @resource[:content].should_not == @metadata.checksum end describe "and puppet is not running as root" do it "should not try to set the owner" do Puppet.features.expects(:root?).returns false @source.copy_source_values @resource[:owner].should be_nil end end end describe "and the source is a link" do it "should set the target to the link destination" do @metadata.stubs(:ftype).returns "link" @resource.stubs(:[]) @resource.stubs(:[]=) @metadata.expects(:destination).returns "/path/to/symlink" @resource.expects(:[]=).with(:target, "/path/to/symlink") @source.copy_source_values end end end it "should have a local? method" do source.new(:resource => @resource).must be_respond_to(:local?) end context "when accessing source properties" do before(:each) do @source = source.new(:resource => @resource) @metadata = stub_everything @source.stubs(:metadata).returns(@metadata) end describe "for local sources" do before(:each) do @metadata.stubs(:ftype).returns "file" @metadata.stubs(:source).returns("file:///path/to/source") end it "should be local" do @source.must be_local end it "should be local if there is no scheme" do @metadata.stubs(:source).returns("/path/to/source") @source.must be_local end it "should be able to return the metadata source full path" do @source.full_path.should == "/path/to/source" end end describe "for remote sources" do before(:each) do @metadata.stubs(:ftype).returns "file" @metadata.stubs(:source).returns("puppet://server:8192/path/to/source") end it "should not be local" do @source.should_not be_local end it "should be able to return the metadata source full path" do @source.full_path.should == "/path/to/source" end it "should be able to return the source server" do @source.server.should == "server" end it "should be able to return the source port" do @source.port.should == 8192 end describe "which don't specify server or port" do before(:each) do @metadata.stubs(:source).returns("puppet:///path/to/source") end it "should return the default source server" do Puppet.settings.expects(:[]).with(:server).returns("myserver") @source.server.should == "myserver" end it "should return the default source port" do Puppet.settings.expects(:[]).with(:masterport).returns(1234) @source.port.should == 1234 end end end end end diff --git a/spec/unit/type/file_spec.rb b/spec/unit/type/file_spec.rb index 7d93dfd64..4fcad07e1 100755 --- a/spec/unit/type/file_spec.rb +++ b/spec/unit/type/file_spec.rb @@ -1,1070 +1,1070 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../spec_helper' describe Puppet::Type.type(:file) do before do Puppet.settings.stubs(:use) @real_posix = Puppet.features.posix? Puppet.features.stubs("posix?").returns(true) @path = Tempfile.new("puppetspec") pathname = @path.path @path.close!() @path = pathname @file = Puppet::Type::File.new(:name => @path) @catalog = Puppet::Resource::Catalog.new @file.catalog = @catalog end describe "when determining if recursion is enabled" do it "should default to recursion being disabled" do @file.should_not be_recurse end [true, "true", 10, "inf", "remote"].each do |value| it "should consider #{value} to enable recursion" do @file[:recurse] = value @file.must be_recurse end end [false, "false", 0].each do |value| it "should consider #{value} to disable recursion" do @file[:recurse] = value @file.should_not be_recurse end end end describe "#write" do it "should propagate failures encountered when renaming the temporary file" do File.stubs(:open) File.expects(:rename).raises ArgumentError file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.stubs(:validate_checksum?).returns(false) property = stub('content_property', :actual_content => "something", :length => "something".length) file.stubs(:property).with(:content).returns(property) lambda { file.write(:content) }.should raise_error(Puppet::Error) end it "should delegate writing to the content property" do filehandle = stub_everything 'fh' File.stubs(:open).yields(filehandle) File.stubs(:rename) property = stub('content_property', :actual_content => "something", :length => "something".length) file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.stubs(:validate_checksum?).returns(false) file.stubs(:property).with(:content).returns(property) property.expects(:write).with(filehandle) file.write(:content) end describe "when validating the checksum" do before { @file.stubs(:validate_checksum?).returns(true) } it "should fail if the checksum parameter and content checksums do not match" do - checksum = stub('checksum_parameter', :sum => 'checksum_b') + checksum = stub('checksum_parameter', :sum => 'checksum_b', :sum_file => 'checksum_b') @file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') @file.stubs(:property).with(:content).returns(property) lambda { @file.write :NOTUSED }.should raise_error(Puppet::Error) end end describe "when not validating the checksum" do before { @file.stubs(:validate_checksum?).returns(false) } it "should not fail if the checksum property and content checksums do not match" do checksum = stub('checksum_parameter', :sum => 'checksum_b') @file.stubs(:parameter).with(:checksum).returns(checksum) property = stub('content_property', :actual_content => "something", :length => "something".length, :write => 'checksum_a') @file.stubs(:property).with(:content).returns(property) lambda { @file.write :NOTUSED }.should_not raise_error(Puppet::Error) end end end it "should have a method for determining if the file is present" do @file.must respond_to(:exist?) end it "should be considered existent if it can be stat'ed" do @file.expects(:stat).returns mock('stat') @file.must be_exist end it "should be considered nonexistent if it can not be stat'ed" do @file.expects(:stat).returns nil @file.must_not be_exist end it "should have a method for determining if the file should be a normal file" do @file.must respond_to(:should_be_file?) end it "should be a file if :ensure is set to :file" do @file[:ensure] = :file @file.must be_should_be_file end it "should be a file if :ensure is set to :present and the file exists as a normal file" do @file.stubs(:stat).returns(mock('stat', :ftype => "file")) @file[:ensure] = :present @file.must be_should_be_file end it "should not be a file if :ensure is set to something other than :file" do @file[:ensure] = :directory @file.must_not be_should_be_file end it "should not be a file if :ensure is set to :present and the file exists but is not a normal file" do @file.stubs(:stat).returns(mock('stat', :ftype => "directory")) @file[:ensure] = :present @file.must_not be_should_be_file end it "should be a file if :ensure is not set and :content is" do @file[:content] = "foo" @file.must be_should_be_file end it "should be a file if neither :ensure nor :content is set but the file exists as a normal file" do @file.stubs(:stat).returns(mock("stat", :ftype => "file")) @file.must be_should_be_file end it "should not be a file if neither :ensure nor :content is set but the file exists but not as a normal file" do @file.stubs(:stat).returns(mock("stat", :ftype => "directory")) @file.must_not be_should_be_file end describe "when using POSIX filenames" do describe "on POSIX systems" do before do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) end it "should autorequire its parent directory" do file = Puppet::Type::File.new(:path => "/foo/bar") dir = Puppet::Type::File.new(:path => "/foo") @catalog.add_resource file @catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire its parent dir if its parent dir is itself" do file = Puppet::Type::File.new(:path => "/") @catalog.add_resource file file.autorequire.should be_empty end it "should remove trailing slashes" do file = Puppet::Type::File.new(:path => "/foo/bar/baz/") file[:path].should == "/foo/bar/baz" end it "should remove double slashes" do file = Puppet::Type::File.new(:path => "/foo/bar//baz") file[:path].should == "/foo/bar/baz" end it "should remove trailing double slashes" do file = Puppet::Type::File.new(:path => "/foo/bar/baz//") file[:path].should == "/foo/bar/baz" end it "should leave a single slash alone" do file = Puppet::Type::File.new(:path => "/") file[:path].should == "/" end end describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should refuse to work" do lambda { Puppet::Type::File.new(:path => "/foo/bar") }.should raise_error(Puppet::Error) end end end describe "when using Microsoft Windows filenames" do confine "Only works on Microsoft Windows" => Puppet.features.microsoft_windows? describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should autorequire its parent directory" do file = Puppet::Type::File.new(:path => "X:/foo/bar") dir = Puppet::Type::File.new(:path => "X:/foo") @catalog.add_resource file @catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire its parent dir if its parent dir is itself" do file = Puppet::Type::File.new(:path => "X:/") @catalog.add_resource file file.autorequire.should be_empty end it "should remove trailing slashes" do file = Puppet::Type::File.new(:path => "X:/foo/bar/baz/") file[:path].should == "X:/foo/bar/baz" end it "should remove double slashes" do file = Puppet::Type::File.new(:path => "X:/foo/bar//baz") file[:path].should == "X:/foo/bar/baz" end it "should remove trailing double slashes" do file = Puppet::Type::File.new(:path => "X:/foo/bar/baz//") file[:path].should == "X:/foo/bar/baz" end it "should leave a drive letter with a slash alone" do file = Puppet::Type::File.new(:path => "X:/") file[:path].should == "X:/" end it "should add a slash to a drive letter" do file = Puppet::Type::File.new(:path => "X:") file[:path].should == "X:/" end end describe "on POSIX systems" do before do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) end it "should refuse to work" do lambda { Puppet::Type::File.new(:path => "X:/foo/bar") }.should raise_error(Puppet::Error) end end end describe "when using UNC filenames" do describe "on Microsoft Windows systems" do confine "Only works on Microsoft Windows" => Puppet.features.microsoft_windows? before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should autorequire its parent directory" do file = Puppet::Type::File.new(:path => "//server/foo/bar") dir = Puppet::Type::File.new(:path => "//server/foo") @catalog.add_resource file @catalog.add_resource dir reqs = file.autorequire reqs[0].source.must == dir reqs[0].target.must == file end it "should not autorequire its parent dir if its parent dir is itself" do file = Puppet::Type::File.new(:path => "//server/foo") @catalog.add_resource file puts file.autorequire file.autorequire.should be_empty end it "should remove trailing slashes" do file = Puppet::Type::File.new(:path => "//server/foo/bar/baz/") file[:path].should == "//server/foo/bar/baz" end it "should remove double slashes" do file = Puppet::Type::File.new(:path => "//server/foo/bar//baz") file[:path].should == "//server/foo/bar/baz" end it "should remove trailing double slashes" do file = Puppet::Type::File.new(:path => "//server/foo/bar/baz//") file[:path].should == "//server/foo/bar/baz" end it "should remove a trailing slash from a sharename" do file = Puppet::Type::File.new(:path => "//server/foo/") file[:path].should == "//server/foo" end it "should not modify a sharename" do file = Puppet::Type::File.new(:path => "//server/foo") file[:path].should == "//server/foo" end end describe "on POSIX systems" do before do Puppet.features.stubs(:posix?).returns(true) Puppet.features.stubs(:microsoft_windows?).returns(false) end it "should refuse to work" do lambda { Puppet::Type::File.new(:path => "X:/foo/bar") }.should raise_error(Puppet::Error) end end end describe "when initializing" do it "should set a desired 'ensure' value if none is set and 'content' is set" do file = Puppet::Type::File.new(:name => "/my/file", :content => "/foo/bar") file[:ensure].should == :file end it "should set a desired 'ensure' value if none is set and 'target' is set" do file = Puppet::Type::File.new(:name => "/my/file", :target => "/foo/bar") file[:ensure].should == :symlink end end describe "when validating attributes" do %w{path checksum backup recurse recurselimit source replace force ignore links purge sourceselect}.each do |attr| it "should have a '#{attr}' parameter" do Puppet::Type.type(:file).attrtype(attr.intern).should == :param end end %w{content target ensure owner group mode type}.each do |attr| it "should have a '#{attr}' property" do Puppet::Type.type(:file).attrtype(attr.intern).should == :property end end it "should have its 'path' attribute set as its namevar" do Puppet::Type.type(:file).key_attributes.should == [:path] end end describe "when managing links" do require 'puppettest/support/assertions' include PuppetTest require 'tempfile' if @real_posix describe "on POSIX systems" do before do @basedir = tempfile Dir.mkdir(@basedir) @file = File.join(@basedir, "file") @link = File.join(@basedir, "link") File.open(@file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(@file, @link) @resource = Puppet::Type.type(:file).new( :path => @link, :mode => "755" ) @catalog.add_resource @resource end after do remove_tmp_files end it "should default to managing the link" do @catalog.apply # I convert them to strings so they display correctly if there's an error. ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0644 end it "should be able to follow links" do @resource[:links] = :follow @catalog.apply ("%o" % (File.stat(@file).mode & 007777)).should == "%o" % 0755 end end else # @real_posix # should recode tests using expectations instead of using the filesystem end describe "on Microsoft Windows systems" do before do Puppet.features.stubs(:posix?).returns(false) Puppet.features.stubs(:microsoft_windows?).returns(true) end it "should refuse to work with links" end end it "should be able to retrieve a stat instance for the file it is managing" do Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo").should respond_to(:stat) end describe "when stat'ing its file" do before do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") @resource[:links] = :manage # so we always use :lstat end it "should use :stat if it is following links" do @resource[:links] = :follow File.expects(:stat) @resource.stat end it "should use :lstat if is it not following links" do @resource[:links] = :manage File.expects(:lstat) @resource.stat end it "should stat the path of the file" do File.expects(:lstat).with("/foo/bar") @resource.stat end # This only happens in testing. it "should return nil if the stat does not exist" do File.expects(:lstat).returns nil @resource.stat.should be_nil end it "should return nil if the file does not exist" do File.expects(:lstat).raises(Errno::ENOENT) @resource.stat.should be_nil end it "should return nil if the file cannot be stat'ed" do File.expects(:lstat).raises(Errno::EACCES) @resource.stat.should be_nil end it "should return the stat instance" do File.expects(:lstat).returns "mystat" @resource.stat.should == "mystat" end it "should cache the stat instance if it has a catalog and is applying" do stat = mock 'stat' File.expects(:lstat).returns stat catalog = Puppet::Resource::Catalog.new @resource.catalog = catalog catalog.stubs(:applying?).returns true @resource.stat.should equal(@resource.stat) end end describe "when flushing" do it "should flush all properties that respond to :flush" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar", :source => "/bar/foo") @resource.parameter(:source).expects(:flush) @resource.flush end it "should reset its stat reference" do @resource = Puppet::Type.type(:file).new(:path => "/foo/bar") File.expects(:lstat).times(2).returns("stat1").then.returns("stat2") @resource.stat.should == "stat1" @resource.flush @resource.stat.should == "stat2" end end it "should have a method for performing recursion" do @file.must respond_to(:perform_recursion) end describe "when executing a recursive search" do it "should use Metadata to do its recursion" do Puppet::FileServing::Metadata.expects(:search) @file.perform_recursion(@file[:path]) end it "should use the provided path as the key to the search" do Puppet::FileServing::Metadata.expects(:search).with { |key, options| key == "/foo" } @file.perform_recursion("/foo") end it "should return the results of the metadata search" do Puppet::FileServing::Metadata.expects(:search).returns "foobar" @file.perform_recursion(@file[:path]).should == "foobar" end it "should pass its recursion value to the search" do @file[:recurse] = true Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass true if recursion is remote" do @file[:recurse] = :remote Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurse] == true } @file.perform_recursion(@file[:path]) end it "should pass its recursion limit value to the search" do @file[:recurselimit] = 10 Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:recurselimit] == 10 } @file.perform_recursion(@file[:path]) end it "should configure the search to ignore or manage links" do @file[:links] = :manage Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:links] == :manage } @file.perform_recursion(@file[:path]) end it "should pass its 'ignore' setting to the search if it has one" do @file[:ignore] = %w{.svn CVS} Puppet::FileServing::Metadata.expects(:search).with { |key, options| options[:ignore] == %w{.svn CVS} } @file.perform_recursion(@file[:path]) end end it "should have a method for performing local recursion" do @file.must respond_to(:recurse_local) end describe "when doing local recursion" do before do @metadata = stub 'metadata', :relative_path => "my/file" end it "should pass its path to the :perform_recursion method" do @file.expects(:perform_recursion).with(@file[:path]).returns [@metadata] @file.stubs(:newchild) @file.recurse_local end it "should return an empty hash if the recursion returns nothing" do @file.expects(:perform_recursion).returns nil @file.recurse_local.should == {} end it "should create a new child resource with each generated metadata instance's relative path" do @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).with(@metadata.relative_path).returns "fiebar" @file.recurse_local end it "should not create a new child resource for the '.' directory" do @metadata.stubs(:relative_path).returns "." @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).never @file.recurse_local end it "should return a hash of the created resources with the relative paths as the hash keys" do @file.expects(:perform_recursion).returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local.should == {"my/file" => "fiebar"} end it "should set checksum_type to none if this file checksum is none" do @file[:checksum] = :none Puppet::FileServing::Metadata.expects(:search).with { |path,params| params[:checksum_type] == :none }.returns [@metadata] @file.expects(:newchild).with("my/file").returns "fiebar" @file.recurse_local end end it "should have a method for performing link recursion" do @file.must respond_to(:recurse_link) end describe "when doing link recursion" do before do @first = stub 'first', :relative_path => "first", :full_path => "/my/first", :ftype => "directory" @second = stub 'second', :relative_path => "second", :full_path => "/my/second", :ftype => "file" @resource = stub 'file', :[]= => nil end it "should pass its target to the :perform_recursion method" do @file[:target] = "mylinks" @file.expects(:perform_recursion).with("mylinks").returns [@first] @file.stubs(:newchild).returns @resource @file.recurse_link({}) end it "should ignore the recursively-found '.' file and configure the top-level file to create a directory" do @first.stubs(:relative_path).returns "." @file[:target] = "mylinks" @file.expects(:perform_recursion).with("mylinks").returns [@first] @file.stubs(:newchild).never @file.expects(:[]=).with(:ensure, :directory) @file.recurse_link({}) end it "should create a new child resource for each generated metadata instance's relative path that doesn't already exist in the children hash" do @file.expects(:perform_recursion).returns [@first, @second] @file.expects(:newchild).with(@first.relative_path).returns @resource @file.recurse_link("second" => @resource) end it "should not create a new child resource for paths that already exist in the children hash" do @file.expects(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_link("first" => @resource) end it "should set the target to the full path of discovered file and set :ensure to :link if the file is not a directory" do file = stub 'file' file.expects(:[]=).with(:target, "/my/second") file.expects(:[]=).with(:ensure, :link) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => @resource, "second" => file) end it "should :ensure to :directory if the file is a directory" do file = stub 'file' file.expects(:[]=).with(:ensure, :directory) @file.stubs(:perform_recursion).returns [@first, @second] @file.recurse_link("first" => file, "second" => @resource) end it "should return a hash with both created and existing resources with the relative paths as the hash keys" do file = stub 'file', :[]= => nil @file.expects(:perform_recursion).returns [@first, @second] @file.stubs(:newchild).returns file @file.recurse_link("second" => @resource).should == {"second" => @resource, "first" => file} end end it "should have a method for performing remote recursion" do @file.must respond_to(:recurse_remote) end describe "when doing remote recursion" do before do @file[:source] = "puppet://foo/bar" @first = Puppet::FileServing::Metadata.new("/my", :relative_path => "first") @second = Puppet::FileServing::Metadata.new("/my", :relative_path => "second") @first.stubs(:ftype).returns "directory" @second.stubs(:ftype).returns "directory" @parameter = stub 'property', :metadata= => nil @resource = stub 'file', :[]= => nil, :parameter => @parameter end it "should pass its source to the :perform_recursion method" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] @file.stubs(:newchild).returns @resource @file.recurse_remote({}) end it "should not recurse when the remote file is not a directory" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => ".") data.stubs(:ftype).returns "file" @file.expects(:perform_recursion).with("puppet://foo/bar").returns [data] @file.expects(:newchild).never @file.recurse_remote({}) end it "should set the source of each returned file to the searched-for URI plus the found relative path" do @first.expects(:source=).with File.join("puppet://foo/bar", @first.relative_path) @file.expects(:perform_recursion).returns [@first] @file.stubs(:newchild).returns @resource @file.recurse_remote({}) end it "should create a new resource for any relative file paths that do not already have a resource" do @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).with("first").returns @resource @file.recurse_remote({}).should == {"first" => @resource} end it "should not create a new resource for any relative file paths that do already have a resource" do @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_remote("first" => @resource) end it "should set the source of each resource to the source of the metadata" do @file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:source, File.join("puppet://foo/bar", @first.relative_path)) @file.recurse_remote("first" => @resource) end # LAK:FIXME This is a bug, but I can't think of a fix for it. Fortunately it's already # filed, and when it's fixed, we'll just fix the whole flow. it "should set the checksum type to :md5 if the remote file is a file" do @first.stubs(:ftype).returns "file" @file.stubs(:perform_recursion).returns [@first] @resource.stubs(:[]=) @resource.expects(:[]=).with(:checksum, :md5) @file.recurse_remote("first" => @resource) end it "should store the metadata in the source property for each resource so the source does not have to requery the metadata" do @file.stubs(:perform_recursion).returns [@first] @resource.expects(:parameter).with(:source).returns @parameter @parameter.expects(:metadata=).with(@first) @file.recurse_remote("first" => @resource) end it "should not create a new resource for the '.' file" do @first.stubs(:relative_path).returns "." @file.stubs(:perform_recursion).returns [@first] @file.expects(:newchild).never @file.recurse_remote({}) end it "should store the metadata in the main file's source property if the relative path is '.'" do @first.stubs(:relative_path).returns "." @file.stubs(:perform_recursion).returns [@first] @file.parameter(:source).expects(:metadata=).with @first @file.recurse_remote("first" => @resource) end describe "and multiple sources are provided" do describe "and :sourceselect is set to :first" do it "should create file instances for the results for the first source to return any values" do data = Puppet::FileServing::Metadata.new("/whatever", :relative_path => "foobar") @file[:source] = %w{/one /two /three /four} @file.expects(:perform_recursion).with("/one").returns nil @file.expects(:perform_recursion).with("/two").returns [] @file.expects(:perform_recursion).with("/three").returns [data] @file.expects(:perform_recursion).with("/four").never @file.expects(:newchild).with("foobar").returns @resource @file.recurse_remote({}) end end describe "and :sourceselect is set to :all" do before do @file[:sourceselect] = :all end it "should return every found file that is not in a previous source" do klass = Puppet::FileServing::Metadata @file[:source] = %w{/one /two /three /four} @file.stubs(:newchild).returns @resource one = [klass.new("/one", :relative_path => "a")] @file.expects(:perform_recursion).with("/one").returns one @file.expects(:newchild).with("a").returns @resource two = [klass.new("/two", :relative_path => "a"), klass.new("/two", :relative_path => "b")] @file.expects(:perform_recursion).with("/two").returns two @file.expects(:newchild).with("b").returns @resource three = [klass.new("/three", :relative_path => "a"), klass.new("/three", :relative_path => "c")] @file.expects(:perform_recursion).with("/three").returns three @file.expects(:newchild).with("c").returns @resource @file.expects(:perform_recursion).with("/four").returns [] @file.recurse_remote({}) end end end end describe "when returning resources with :eval_generate" do before do @graph = stub 'graph', :add_edge => nil @catalog.stubs(:relationship_graph).returns @graph @file.catalog = @catalog @file[:recurse] = true end it "should recurse if recursion is enabled" do resource = stub('resource', :[] => "resource") @file.expects(:recurse?).returns true @file.expects(:recurse).returns [resource] @file.eval_generate.should == [resource] end it "should not recurse if recursion is disabled" do @file.expects(:recurse?).returns false @file.expects(:recurse).never @file.eval_generate.should == [] end it "should return each resource found through recursion" do foo = stub 'foo', :[] => "/foo" bar = stub 'bar', :[] => "/bar" bar2 = stub 'bar2', :[] => "/bar" @file.expects(:recurse).returns [foo, bar] @file.eval_generate.should == [foo, bar] end end describe "when recursing" do before do @file[:recurse] = true @metadata = Puppet::FileServing::Metadata end describe "and a source is set" do before { @file[:source] = "/my/source" } it "should pass the already-discovered resources to recurse_remote" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_remote).with(:foo => "bar").returns [] @file.recurse end end describe "and a target is set" do before { @file[:target] = "/link/target" } it "should use recurse_link" do @file.stubs(:recurse_local).returns(:foo => "bar") @file.expects(:recurse_link).with(:foo => "bar").returns [] @file.recurse end end it "should use recurse_local if recurse is not remote" do @file.expects(:recurse_local).returns({}) @file.recurse end it "should not use recurse_local if recurse remote" do @file[:recurse] = :remote @file.expects(:recurse_local).never @file.recurse end it "should return the generated resources as an array sorted by file path" do one = stub 'one', :[] => "/one" two = stub 'two', :[] => "/one/two" three = stub 'three', :[] => "/three" @file.expects(:recurse_local).returns(:one => one, :two => two, :three => three) @file.recurse.should == [one, two, three] end describe "and purging is enabled" do before do @file[:purge] = true end it "should configure each file to be removed" do local = stub 'local' local.stubs(:[]).with(:source).returns nil # Thus, a local file local.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_local).returns("local" => local) local.expects(:[]=).with(:ensure, :absent) @file.recurse end it "should not remove files that exist in the remote repository" do @file["source"] = "/my/file" @file.expects(:recurse_local).returns({}) remote = stub 'remote' remote.stubs(:[]).with(:source).returns "/whatever" # Thus, a remote file remote.stubs(:[]).with(:path).returns "foo" @file.expects(:recurse_remote).with { |hash| hash["remote"] = remote } remote.expects(:[]=).with(:ensure, :absent).never @file.recurse end end describe "and making a new child resource" do it "should not copy the parent resource's parent" do Puppet::Type.type(:file).expects(:new).with { |options| ! options.include?(:parent) } @file.newchild("my/path") end {:recurse => true, :target => "/foo/bar", :ensure => :present, :alias => "yay", :source => "/foo/bar"}.each do |param, value| it "should not pass on #{param} to the sub resource" do @file = Puppet::Type::File.new(:name => @path, param => value, :catalog => @catalog) @file.class.expects(:new).with { |params| params[param].nil? } @file.newchild("sub/file") end end it "should copy all of the parent resource's 'should' values that were set at initialization" do file = @file.class.new(:path => "/foo/bar", :owner => "root", :group => "wheel") @catalog.add_resource(file) file.class.expects(:new).with { |options| options[:owner] == "root" and options[:group] == "wheel" } file.newchild("my/path") end it "should not copy default values to the new child" do @file.class.expects(:new).with { |params| params[:backup].nil? } @file.newchild("my/path") end it "should not copy values to the child which were set by the source" do @file[:source] = "/foo/bar" metadata = stub 'metadata', :owner => "root", :group => "root", :mode => 0755, :ftype => "file", :checksum => "{md5}whatever" @file.parameter(:source).stubs(:metadata).returns metadata @file.parameter(:source).copy_source_values @file.class.expects(:new).with { |params| params[:group].nil? } @file.newchild("my/path") end end end describe "when setting the backup" do it "should default to 'puppet'" do Puppet::Type::File.new(:name => "/my/file")[:backup].should == "puppet" end it "should allow setting backup to 'false'" do (!Puppet::Type::File.new(:name => "/my/file", :backup => false)[:backup]).should be_true end it "should set the backup to '.puppet-bak' if it is set to true" do Puppet::Type::File.new(:name => "/my/file", :backup => true)[:backup].should == ".puppet-bak" end it "should support any other backup extension" do Puppet::Type::File.new(:name => "/my/file", :backup => ".bak")[:backup].should == ".bak" end it "should set the filebucket when backup is set to a string matching the name of a filebucket in the catalog" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file") catalog.add_resource file file[:backup] = "foo" file.bucket.should == bucket_resource.bucket end it "should find filebuckets added to the catalog after the file resource was created" do catalog = Puppet::Resource::Catalog.new file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog.add_resource file bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file.bucket.should == bucket_resource.bucket end it "should have a nil filebucket if backup is false" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file", :backup => false) catalog.add_resource file file.bucket.should be_nil end it "should have a nil filebucket if backup is set to a string starting with '.'" do catalog = Puppet::Resource::Catalog.new bucket_resource = Puppet::Type.type(:filebucket).new :name => "foo", :path => "/my/file/bucket" catalog.add_resource bucket_resource file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") catalog.add_resource file file.bucket.should be_nil end it "should fail if there's no catalog and backup is not false" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") lambda { file.bucket }.should raise_error(Puppet::Error) end it "should fail if a non-existent catalog is specified" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "foo") catalog = Puppet::Resource::Catalog.new catalog.add_resource file lambda { file.bucket }.should raise_error(Puppet::Error) end it "should be able to use the default filebucket without a catalog" do file = Puppet::Type::File.new(:name => "/my/file", :backup => "puppet") file.bucket.should be_instance_of(Puppet::FileBucket::Dipper) end it "should look up the filebucket during finish()" do file = Puppet::Type::File.new(:name => "/my/file", :backup => ".foo") file.expects(:bucket) file.finish end end describe "when retrieving the current file state" do it "should copy the source values if the 'source' parameter is set" do file = Puppet::Type::File.new(:name => "/my/file", :source => "/foo/bar") file.parameter(:source).expects(:copy_source_values) file.retrieve end end describe ".title_patterns" do before do @type_class = Puppet::Type.type(:file) end it "should have a regexp that captures the entire string, except for a terminating slash" do patterns = @type_class.title_patterns string = "abc/\n\tdef/" patterns[0][0] =~ string $1.should == "abc/\n\tdef" end end end diff --git a/spec/unit/util/pson_spec.rb b/spec/unit/util/pson_spec.rb index d02d28517..474ddafa4 100755 --- a/spec/unit/util/pson_spec.rb +++ b/spec/unit/util/pson_spec.rb @@ -1,38 +1,53 @@ #!/usr/bin/env ruby Dir.chdir(File.dirname(__FILE__)) { (s = lambda { |f| File.exist?(f) ? require(f) : Dir.chdir("..") { s.call(f) } }).call("spec/spec_helper.rb") } require 'puppet/util/pson' class PsonUtil include Puppet::Util::Pson end describe Puppet::Util::Pson do it "should fail if no data is provided" do lambda { PsonUtil.new.pson_create("type" => "foo") }.should raise_error(ArgumentError) end it "should call 'from_pson' with the provided data" do pson = PsonUtil.new pson.expects(:from_pson).with("mydata") pson.pson_create("type" => "foo", "data" => "mydata") end { 'foo' => '"foo"', 1 => '1', "\x80" => "\"\x80\"", [] => '[]' }.each { |str,pson| it "should be able to encode #{str.inspect}" do str.to_pson.should == pson end } it "should be able to handle arbitrary binary data" do bin_string = (1..20000).collect { |i| ((17*i+13*i*i) % 255).chr }.join PSON.parse(%Q{{ "type": "foo", "data": #{bin_string.to_pson} }})["data"].should == bin_string end + + it "should be able to handle UTF8 that isn't a real unicode character" do + s = ["\355\274\267"] + PSON.parse( [s].to_pson ).should == [s] + end + + it "should be able to handle UTF8 for \\xFF" do + s = ["\xc3\xbf"] + PSON.parse( [s].to_pson ).should == [s] + end + + it "should be able to handle invalid UTF8 bytes" do + s = ["\xc3\xc3"] + PSON.parse( [s].to_pson ).should == [s] + end end diff --git a/test/language/snippets.rb b/test/language/snippets.rb index 51c5e23fe..a10e8e870 100755 --- a/test/language/snippets.rb +++ b/test/language/snippets.rb @@ -1,519 +1,522 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../lib/puppettest' require 'puppet' require 'puppet/parser/parser' require 'puppet/network/client' require 'puppet/network/handler' require 'puppettest' class TestSnippets < Test::Unit::TestCase include PuppetTest def setup super @file = Puppet::Type.type(:file) Facter.stubs(:to_hash).returns({}) Facter.stubs(:value).returns("whatever") end def self.snippetdir PuppetTest.datadir "snippets" end def assert_file(path, msg = nil) unless file = @catalog.resource(:file, path) msg ||= "Could not find file #{path}" raise msg end end def assert_not_file(path, msg = nil) if file = @catalog.resource(:file, path) msg ||= "File #{path} exists!" raise msg end end def assert_mode_equal(mode, path) + if mode.is_a? Integer + mode = mode.to_s(8) + end unless file = @catalog.resource(:file, path) raise "Could not find file #{path}" end unless mode == file.should(:mode) raise "Mode for %s is incorrect: %o vs %o" % [path, mode, file.should(:mode)] end end def snippet(name) File.join(self.class.snippetdir, name) end def file2ast(file) parser = Puppet::Parser::Parser.new parser.file = file ast = parser.parse ast end def snippet2ast(text) parser = Puppet::Parser::Parser.new parser.string = text ast = parser.parse ast end def client args = { :Listen => false } Puppet::Network::Client.new(args) end def ast2scope(ast) scope = Puppet::Parser::Scope.new ast.evaluate(scope) scope end def scope2objs(scope) objs = scope.to_trans end def snippet2scope(snippet) ast = snippet2ast(snippet) scope = ast2scope(ast) end def snippet2objs(snippet) ast = snippet2ast(snippet) scope = ast2scope(ast) objs = scope2objs(scope) end def properties(type) properties = type.validproperties end def metaparams(type) mparams = [] Puppet::Type.eachmetaparam { |param| mparams.push param } mparams end def params(type) params = [] type.parameters.each { |name,property| params.push name } params end def randthing(thing,type) list = self.send(thing,type) list[rand(list.length)] end def randeach(type) [:properties, :metaparams, :parameters].collect { |thing| randthing(thing,type) } end @@snippets = { true => [ %{File { mode => 755 }} ], } def disabled_test_defaults Puppet::Type.eachtype { |type| next if type.name == :puppet or type.name == :component rands = randeach(type) name = type.name.to_s.capitalize [0..1, 0..2].each { |range| params = rands[range] paramstr = params.collect { |param| "#{param} => fake" }.join(", ") str = "#{name} { #{paramstr} }" scope = nil assert_nothing_raised { scope = snippet2scope(str) } defaults = nil assert_nothing_raised { defaults = scope.lookupdefaults(name) } p defaults params.each { |param| puts "#{name} => '#{param}'" assert(defaults.include?(param)) } } } end # this is here in case no tests get defined; otherwise we get a warning def test_nothing end def snippet_filecreate %w{a b c d}.each { |letter| path = "/tmp/create#{letter}test" assert_file(path) assert_mode_equal(0755, path) if %w{a b}.include?(letter) } end def snippet_simpledefaults path = "/tmp/defaulttest" assert_file(path) assert_mode_equal(0755, path) end def snippet_simpleselector files = %w{a b c d}.collect { |letter| path = "/tmp/snippetselect#{letter}test" assert_file(path) assert_mode_equal(0755, path) } end def snippet_classpathtest path = "/tmp/classtest" file = @catalog.resource(:file, path) assert(file, "did not create file #{path}") assert_equal( "/Stage[main]/Testing/Mytype[componentname]/File[/tmp/classtest]", file.path) end def snippet_argumentdefaults path1 = "/tmp/argumenttest1" path2 = "/tmp/argumenttest2" file1 = @catalog.resource(:file, path1) file2 = @catalog.resource(:file, path2) assert_file(path1) assert_mode_equal(0755, path1) assert_file(path2) assert_mode_equal(0644, path2) end def snippet_casestatement paths = %w{ /tmp/existsfile /tmp/existsfile2 /tmp/existsfile3 /tmp/existsfile4 /tmp/existsfile5 /tmp/existsfile6 } paths.each { |path| file = @catalog.resource(:file, path) assert(file, "File #{path} is missing") assert_mode_equal(0755, path) } end def snippet_implicititeration paths = %w{a b c d e f g h}.collect { |l| "/tmp/iteration#{l}test" } paths.each { |path| file = @catalog.resource(:file, path) assert_file(path) assert_mode_equal(0755, path) } end def snippet_multipleinstances paths = %w{a b c}.collect { |l| "/tmp/multipleinstances#{l}" } paths.each { |path| assert_file(path) assert_mode_equal(0755, path) } end def snippet_namevartest file = "/tmp/testfiletest" dir = "/tmp/testdirtest" assert_file(file) assert_file(dir) assert_equal(:directory, @catalog.resource(:file, dir).should(:ensure), "Directory is not set to be a directory") end def snippet_scopetest file = "/tmp/scopetest" assert_file(file) assert_mode_equal(0755, file) end def snippet_selectorvalues nums = %w{1 2 3 4 5 6 7} files = nums.collect { |n| "/tmp/selectorvalues#{n}" } files.each { |f| assert_file(f) assert_mode_equal(0755, f) } end def snippet_singleselector nums = %w{1 2 3} files = nums.collect { |n| "/tmp/singleselector#{n}" } files.each { |f| assert_file(f) assert_mode_equal(0755, f) } end def snippet_falsevalues file = "/tmp/falsevaluesfalse" assert_file(file) end def disabled_snippet_classargtest [1,2].each { |num| file = "/tmp/classargtest#{num}" assert_file(file) assert_mode_equal(0755, file) } end def snippet_classheirarchy [1,2,3].each { |num| file = "/tmp/classheir#{num}" assert_file(file) assert_mode_equal(0755, file) } end def snippet_singleary [1,2,3,4].each { |num| file = "/tmp/singleary#{num}" assert_file(file) } end def snippet_classincludes [1,2,3].each { |num| file = "/tmp/classincludes#{num}" assert_file(file) assert_mode_equal(0755, file) } end def snippet_componentmetaparams ["/tmp/component1", "/tmp/component2"].each { |file| assert_file(file) } end def snippet_aliastest %w{/tmp/aliastest /tmp/aliastest2 /tmp/aliastest3}.each { |file| assert_file(file) } end def snippet_singlequote { 1 => 'a $quote', 2 => 'some "\yayness\"' }.each { |count, str| path = "/tmp/singlequote#{count}" assert_file(path) assert_equal(str, @catalog.resource(:file, path).parameter(:content).actual_content) } end # There's no way to actually retrieve the list of classes from the # transaction. def snippet_tag end # Make sure that set tags are correctly in place, yo. def snippet_tagged tags = {"testing" => true, "yayness" => false, "both" => false, "bothtrue" => true, "define" => true} tags.each do |tag, retval| assert_file("/tmp/tagged#{tag}#{retval.to_s}") end end def snippet_defineoverrides file = "/tmp/defineoverrides1" assert_file(file) assert_mode_equal(0755, file) end def snippet_deepclassheirarchy 5.times { |i| i += 1 file = "/tmp/deepclassheir#{i}" assert_file(file) } end def snippet_emptyclass # There's nothing to check other than that it works end def snippet_emptyexec assert(@catalog.resource(:exec, "touch /tmp/emptyexectest"), "Did not create exec") end def snippet_multisubs path = "/tmp/multisubtest" assert_file(path) file = @catalog.resource(:file, path) assert_equal("{md5}5fbef65269a99bddc2106251dd89b1dc", file.should(:content), "sub2 did not override content") assert_mode_equal(0755, path) end def snippet_collection assert_file("/tmp/colltest1") assert_nil(@catalog.resource(:file, "/tmp/colltest2"), "Incorrectly collected file") end def snippet_virtualresources %w{1 2 3 4}.each do |num| assert_file("/tmp/virtualtest#{num}") end end def snippet_componentrequire %w{1 2}.each do |num| assert_file( "/tmp/testing_component_requires#{num}", "#{num} does not exist") end end def snippet_realize_defined_types assert_file("/tmp/realize_defined_test1") assert_file("/tmp/realize_defined_test2") end def snippet_collection_within_virtual_definitions assert_file("/tmp/collection_within_virtual_definitions1_foo.txt") assert_file("/tmp/collection_within_virtual_definitions2_foo2.txt") end def snippet_fqparents assert_file("/tmp/fqparent1", "Did not make file from parent class") assert_file("/tmp/fqparent2", "Did not make file from subclass") end def snippet_fqdefinition assert_file("/tmp/fqdefinition", "Did not make file from fully-qualified definition") end def snippet_subclass_name_duplication assert_file("/tmp/subclass_name_duplication1", "Did not make first file from duplicate subclass names") assert_file("/tmp/subclass_name_duplication2", "Did not make second file from duplicate subclass names") end def snippet_funccomma assert_file("/tmp/funccomma1", "Did not make first file from trailing function comma") assert_file("/tmp/funccomma2", "Did not make second file from trailing function comma") end def snippet_arraytrailingcomma assert_file("/tmp/arraytrailingcomma1", "Did not make first file from array") assert_file("/tmp/arraytrailingcomma2", "Did not make second file from array") end def snippet_multipleclass assert_file("/tmp/multipleclassone", "one") assert_file("/tmp/multipleclasstwo", "two") end def snippet_multilinecomments assert_not_file("/tmp/multilinecomments","Did create a commented resource"); end def snippet_collection_override path = "/tmp/collection" assert_file(path) assert_mode_equal(0600, path) end def snippet_ifexpression assert_file("/tmp/testiftest","if test"); end def snippet_hash assert_file("/tmp/myhashfile1","hash test 1"); assert_file("/tmp/myhashfile2","hash test 2"); assert_file("/tmp/myhashfile3","hash test 3"); assert_file("/tmp/myhashfile4","hash test 4"); end # Iterate across each of the snippets and create a test. Dir.entries(snippetdir).sort.each { |file| next if file =~ /^\./ mname = "snippet_" + file.sub(/\.pp$/, '') if self.method_defined?(mname) #eval("alias #{testname} #{mname}") testname = ("test_#{mname}").intern self.send(:define_method, testname) { Puppet[:manifest] = snippet(file) facts = { "hostname" => "testhost", "domain" => "domain.com", "ipaddress" => "127.0.0.1", "fqdn" => "testhost.domain.com" } node = Puppet::Node.new("testhost") node.merge(facts) catalog = nil assert_nothing_raised("Could not compile catalog") { catalog = Puppet::Resource::Catalog.find(node) } assert_nothing_raised("Could not convert catalog") { catalog = catalog.to_ral } @catalog = catalog assert_nothing_raised { self.send(mname) } } mname = mname.intern end } end diff --git a/test/lib/puppettest/support/utils.rb b/test/lib/puppettest/support/utils.rb index e022f123c..bca5d9634 100644 --- a/test/lib/puppettest/support/utils.rb +++ b/test/lib/puppettest/support/utils.rb @@ -1,160 +1,160 @@ module PuppetTest::Support end module PuppetTest::Support::Utils def gcdebug(type) Puppet.warning "#{type}: #{ObjectSpace.each_object(type) { |o| }}" end def basedir(*list) unless defined? @@basedir Dir.chdir(File.dirname(__FILE__)) do @@basedir = File.dirname(File.dirname(File.dirname(File.dirname(Dir.getwd)))) end end if list.empty? @@basedir else File.join(@@basedir, *list) end end def fakedata(dir,pat='*') glob = "#{basedir}/test/#{dir}/#{pat}" files = Dir.glob(glob,File::FNM_PATHNAME) raise Puppet::DevError, "No fakedata matching #{glob}" if files.empty? files end def datadir(*list) File.join(basedir, "test", "data", *list) end # # TODO: I think this method needs to be renamed to something a little more # explanatory. # def newobj(type, name, hash) transport = Puppet::TransObject.new(name, "file") transport[:path] = path transport[:ensure] = "file" assert_nothing_raised { file = transport.to_ral } end # Turn a list of resources, or possibly a catalog and some resources, # into a catalog object. def resources2catalog(*resources) if resources[0].is_a?(Puppet::Resource::Catalog) config = resources.shift resources.each { |r| config.add_resource r } unless resources.empty? elsif resources[0].is_a?(Puppet::Type.type(:component)) raise ArgumentError, "resource2config() no longer accpts components" comp = resources.shift comp.delve else config = Puppet::Resource::Catalog.new resources.each { |res| config.add_resource res } end config end # TODO: rewrite this to use the 'etc' module. # Define a variable that contains the name of my user. def setme # retrieve the user name id = %x{id}.chomp if id =~ /uid=\d+\(([^\)]+)\)/ @me = $1 else puts id end raise "Could not retrieve user name; 'id' did not work" unless defined?(@me) end # Define a variable that contains a group I'm in. def set_mygroup # retrieve the user name group = %x{groups}.chomp.split(/ /)[0] raise "Could not find group to set in @mygroup" unless group @mygroup = group end def run_events(type, trans, events, msg) case type when :evaluate, :rollback # things are hunky-dory else raise Puppet::DevError, "Incorrect run_events type" end method = type trans.send(method) - newevents = trans.events.reject { |e| e.status == 'failure' }.collect { |e| + newevents = trans.events.reject { |e| ['failure', 'audit'].include? e.status }.collect { |e| e.name } assert_equal(events, newevents, "Incorrect #{type} #{msg} events") trans end def fakefile(name) ary = [basedir, "test"] ary += name.split("/") file = File.join(ary) raise Puppet::DevError, "No fakedata file #{file}" unless FileTest.exists?(file) file end # wrap how to retrieve the masked mode def filemode(file) File.stat(file).mode & 007777 end def memory Puppet::Util.memory end # a list of files that we can parse for testing def textfiles textdir = datadir "snippets" Dir.entries(textdir).reject { |f| f =~ /^\./ or f =~ /fail/ }.each { |f| yield File.join(textdir, f) } end def failers textdir = datadir "failers" # only parse this one file now files = Dir.entries(textdir).reject { |file| file =~ %r{\.swp} }.reject { |file| file =~ %r{\.disabled} }.collect { |file| File.join(textdir,file) }.find_all { |file| FileTest.file?(file) }.sort.each { |file| Puppet.debug "Processing #{file}" yield file } end def mk_catalog(*resources) if resources[0].is_a?(String) name = resources.shift else name = :testing end config = Puppet::Resource::Catalog.new :testing do |conf| resources.each { |resource| conf.add_resource resource } end config end end diff --git a/test/network/server/mongrel_test.rb b/test/network/server/mongrel_test.rb index 7bb2df150..d675b42f7 100755 --- a/test/network/server/mongrel_test.rb +++ b/test/network/server/mongrel_test.rb @@ -1,105 +1,99 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'mocha' class TestMongrelServer < PuppetTest::TestCase confine "Missing mongrel" => Puppet.features.mongrel? include PuppetTest::ServerTest def mkserver(handlers = nil) handlers ||= { :Status => nil } mongrel = Puppet::Network::HTTPServer::Mongrel.new(handlers) end # Make sure client info is correctly extracted. def test_client_info obj = Object.new obj.singleton_class.send(:attr_accessor, :params) params = {} obj.params = params mongrel = mkserver ip = Facter.value(:ipaddress) params["REMOTE_ADDR"] = ip params[Puppet[:ssl_client_header]] = "" params[Puppet[:ssl_client_verify_header]] = "failure" info = nil Resolv.expects(:getname).with(ip).returns("host.domain.com").times(4) assert_nothing_raised("Could not call client_info") do info = mongrel.send(:client_info, obj) end assert(! info.authenticated?, "Client info object was marked valid even though headers were missing") assert_equal(ip, info.ip, "Did not copy over ip correctly") assert_equal("host.domain.com", info.name, "Did not copy over hostname correctly") # Now pass the X-Forwarded-For header and check it is preferred over REMOTE_ADDR params["REMOTE_ADDR"] = '127.0.0.1' params["HTTP_X_FORWARDED_FOR"] = ip info = nil assert_nothing_raised("Could not call client_info") do info = mongrel.send(:client_info, obj) end assert(! info.authenticated?, "Client info object was marked valid even though headers were missing") assert_equal(ip, info.ip, "Did not copy over ip correctly") assert_equal("host.domain.com", info.name, "Did not copy over hostname correctly") # Now add a valid auth header. params["REMOTE_ADDR"] = ip params["HTTP_X_FORWARDED_FOR"] = nil params[Puppet[:ssl_client_header]] = "/CN=host.domain.com" assert_nothing_raised("Could not call client_info") do info = mongrel.send(:client_info, obj) end assert(! info.authenticated?, "Client info object was marked valid even though the verify header was fals") assert_equal(ip, info.ip, "Did not copy over ip correctly") assert_equal("host.domain.com", info.name, "Did not copy over hostname correctly") # Now change the verify header to be true params[Puppet[:ssl_client_verify_header]] = "SUCCESS" assert_nothing_raised("Could not call client_info") do info = mongrel.send(:client_info, obj) end assert(info.authenticated?, "Client info object was not marked valid even though all headers were correct") assert_equal(ip, info.ip, "Did not copy over ip correctly") assert_equal("host.domain.com", info.name, "Did not copy over hostname correctly") # Now try it with a different header name params.delete(Puppet[:ssl_client_header]) Puppet[:ssl_client_header] = "header_testing" params["header_testing"] = "/CN=other.domain.com" info = nil assert_nothing_raised("Could not call client_info with other header") do info = mongrel.send(:client_info, obj) end assert(info.authenticated?, "Client info object was not marked valid even though the header was present") assert_equal(ip, info.ip, "Did not copy over ip correctly") assert_equal("other.domain.com", info.name, "Did not copy over hostname correctly") # Now make sure it's considered invalid without that header params.delete("header_testing") info = nil assert_nothing_raised("Could not call client_info with no header") do info = mongrel.send(:client_info, obj) end assert(! info.authenticated?, "Client info object was marked valid without header") assert_equal(ip, info.ip, "Did not copy over ip correctly") assert_equal(Resolv.getname(ip), info.name, "Did not look up hostname correctly") end - - def test_daemonize - mongrel = mkserver - - assert(mongrel.respond_to?(:daemonize), "Mongrel server does not respond to daemonize") - end end diff --git a/test/ral/type/file.rb b/test/ral/type/file.rb index 6322529cf..386c3ca1b 100755 --- a/test/ral/type/file.rb +++ b/test/ral/type/file.rb @@ -1,912 +1,912 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppettest/support/utils' require 'fileutils' require 'mocha' class TestFile < Test::Unit::TestCase include PuppetTest::Support::Utils include PuppetTest::FileTesting def mkfile(hash) file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new(hash) } file end def mktestfile tmpfile = tempfile File.open(tmpfile, "w") { |f| f.puts rand(100) } @@tmpfiles.push tmpfile mkfile(:name => tmpfile) end def setup super @file = Puppet::Type.type(:file) $method = @method_name Puppet[:filetimeout] = -1 Facter.stubs(:to_hash).returns({}) end def teardown system("rm -rf #{Puppet[:statefile]}") super end def initstorage Puppet::Util::Storage.init Puppet::Util::Storage.load end def clearstorage Puppet::Util::Storage.store Puppet::Util::Storage.clear end def test_owner file = mktestfile users = {} count = 0 # collect five users Etc.passwd { |passwd| if count > 5 break else count += 1 end users[passwd.uid] = passwd.name } fake = {} # find a fake user while true a = rand(1000) begin Etc.getpwuid(a) rescue fake[a] = "fakeuser" break end end uid, name = users.shift us = {} us[uid] = name users.each { |uid, name| assert_apply(file) assert_nothing_raised { file[:owner] = name } assert_nothing_raised { file.retrieve } assert_apply(file) } end def test_group file = mktestfile [%x{groups}.chomp.split(/ /), Process.groups].flatten.each { |group| assert_nothing_raised { file[:group] = group } assert(file.property(:group)) assert(file.property(:group).should) } end def test_groups_fails_when_invalid assert_raise(Puppet::Error, "did not fail when the group was empty") do Puppet::Type.type(:file).new :path => "/some/file", :group => "" end end if Puppet.features.root? def test_createasuser dir = tmpdir user = nonrootuser path = File.join(tmpdir, "createusertesting") @@tmpfiles << path file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :path => path, :owner => user.name, :ensure => "file", :mode => "755" ) } comp = mk_catalog("createusertest", file) assert_events([:file_created], comp) end def test_nofollowlinks basedir = tempfile Dir.mkdir(basedir) file = File.join(basedir, "file") link = File.join(basedir, "link") File.open(file, "w", 0644) { |f| f.puts "yayness"; f.flush } File.symlink(file, link) # First test 'user' user = nonrootuser inituser = File.lstat(link).uid File.lchown(inituser, nil, link) obj = nil assert_nothing_raised { obj = Puppet::Type.type(:file).new( :title => link, :owner => user.name ) } obj.retrieve # Make sure it defaults to managing the link assert_events([:file_changed], obj) assert_equal(user.uid, File.lstat(link).uid) assert_equal(inituser, File.stat(file).uid) File.chown(inituser, nil, file) File.lchown(inituser, nil, link) # Try following obj[:links] = :follow assert_events([:file_changed], obj) assert_equal(user.uid, File.stat(file).uid) assert_equal(inituser, File.lstat(link).uid) # And then explicitly managing File.chown(inituser, nil, file) File.lchown(inituser, nil, link) obj[:links] = :manage assert_events([:file_changed], obj) assert_equal(user.uid, File.lstat(link).uid) assert_equal(inituser, File.stat(file).uid) obj.delete(:owner) obj[:links] = :follow # And then test 'group' group = nonrootgroup initgroup = File.stat(file).gid obj[:group] = group.name obj[:links] = :follow assert_events([:file_changed], obj) assert_equal(group.gid, File.stat(file).gid) File.chown(nil, initgroup, file) File.lchown(nil, initgroup, link) obj[:links] = :manage assert_events([:file_changed], obj) assert_equal(group.gid, File.lstat(link).gid) assert_equal(initgroup, File.stat(file).gid) end def test_ownerasroot file = mktestfile users = {} count = 0 # collect five users Etc.passwd { |passwd| if count > 5 break else count += 1 end next if passwd.uid < 0 users[passwd.uid] = passwd.name } fake = {} # find a fake user while true a = rand(1000) begin Etc.getpwuid(a) rescue fake[a] = "fakeuser" break end end users.each { |uid, name| assert_nothing_raised { file[:owner] = name } assert_apply(file) currentvalue = file.retrieve assert(file.insync?(currentvalue)) assert_nothing_raised { file[:owner] = uid } assert_apply(file) currentvalue = file.retrieve # make sure changing to number doesn't cause a sync assert(file.insync?(currentvalue)) } # We no longer raise an error here, because we check at run time #fake.each { |uid, name| # assert_raise(Puppet::Error) { # file[:owner] = name # } # assert_raise(Puppet::Error) { # file[:owner] = uid # } #} end def test_groupasroot file = mktestfile [%x{groups}.chomp.split(/ /), Process.groups].flatten.each { |group| next unless Puppet::Util.gid(group) # grr. assert_nothing_raised { file[:group] = group } assert(file.property(:group)) assert(file.property(:group).should) assert_apply(file) currentvalue = file.retrieve assert(file.insync?(currentvalue)) assert_nothing_raised { file.delete(:group) } } end if Facter.value(:operatingsystem) == "Darwin" def test_sillyowner file = tempfile File.open(file, "w") { |f| f.puts "" } File.chown(-2, nil, file) assert(File.stat(file).uid > 120000, "eh?") user = nonrootuser obj = Puppet::Type.newfile( :path => file, :owner => user.name ) assert_apply(obj) assert_equal(user.uid, File.stat(file).uid) end end else $stderr.puts "Run as root for complete owner and group testing" end def test_create %w{a b c d}.collect { |name| tempfile + name.to_s }.each { |path| file =nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => path, :ensure => "file" ) } assert_events([:file_created], file) assert_events([], file) assert(FileTest.file?(path), "File does not exist") @@tmpfiles.push path } end def test_create_dir basedir = tempfile Dir.mkdir(basedir) %w{a b c d}.collect { |name| "#{basedir}/#{name}" }.each { |path| file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => path, :ensure => "directory" ) } assert(! FileTest.directory?(path), "Directory #{path} already exists") assert_events([:directory_created], file) assert_events([], file) assert(FileTest.directory?(path)) @@tmpfiles.push path } end def test_modes file = mktestfile # Set it to something else initially File.chmod(0775, file.title) [0644,0755,0777,0641].each { |mode| assert_nothing_raised { file[:mode] = mode } assert_events([:mode_changed], file) assert_events([], file) assert_nothing_raised { file.delete(:mode) } } end def cyclefile(path) # i had problems with using :name instead of :path [:name,:path].each { |param| file = nil changes = nil comp = nil trans = nil initstorage assert_nothing_raised { file = Puppet::Type.type(:file).new( param => path, :recurse => true, :checksum => "md5" ) } comp = Puppet::Type.type(:component).new( :name => "component" ) comp.push file assert_nothing_raised { trans = comp.evaluate } assert_nothing_raised { trans.evaluate } clearstorage Puppet::Type.allclear } end def test_filetype_retrieval file = nil # Verify it retrieves files of type directory assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => tmpdir, :check => :type ) } assert_equal("directory", file.property(:type).retrieve) # And then check files assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => tempfile, :ensure => "file" ) } assert_apply(file) file[:check] = "type" assert_apply(file) assert_equal("file", file.property(:type).retrieve) end def test_path dir = tempfile path = File.join(dir, "subdir") assert_nothing_raised("Could not make file") { FileUtils.mkdir_p(File.dirname(path)) File.open(path, "w") { |f| f.puts "yayness" } } file = nil dirobj = nil assert_nothing_raised("Could not make file object") { dirobj = Puppet::Type.type(:file).new( :path => dir, :recurse => true, :check => %w{mode owner group} ) } catalog = mk_catalog dirobj transaction = Puppet::Transaction.new(catalog) transaction.eval_generate(dirobj) #assert_nothing_raised { # dirobj.eval_generate #} file = catalog.resource(:file, path) assert(file, "Could not retrieve file object") assert_equal("/#{file.ref}", file.path) end def test_autorequire basedir = tempfile subfile = File.join(basedir, "subfile") baseobj = Puppet::Type.type(:file).new( :name => basedir, :ensure => "directory" ) subobj = Puppet::Type.type(:file).new( :name => subfile, :ensure => "file" ) catalog = mk_catalog(baseobj, subobj) edge = nil assert_nothing_raised do edge = subobj.autorequire.shift end assert_equal(baseobj, edge.source, "file did not require its parent dir") assert_equal(subobj, edge.target, "file did not require its parent dir") end # Unfortunately, I know this fails def disabled_test_recursivemkdir path = tempfile subpath = File.join(path, "this", "is", "a", "dir") file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => subpath, :ensure => "directory", :recurse => true ) } comp = mk_catalog("yay", file) comp.finalize assert_apply(comp) #assert_events([:directory_created], comp) assert(FileTest.directory?(subpath), "Did not create directory") end # Make sure that content updates the checksum on the same run def test_checksumchange_for_content dest = tempfile File.open(dest, "w") { |f| f.puts "yayness" } file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => dest, :checksum => "md5", :content => "This is some content", :backup => false ) } file.retrieve assert_events([:content_changed], file) file.retrieve assert_events([], file) end # Make sure that content updates the checksum on the same run def test_checksumchange_for_ensure dest = tempfile file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => dest, :checksum => "md5", :ensure => "file" ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) end def test_nameandpath path = tempfile file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :title => "fileness", :path => path, :content => "this is some content" ) } assert_apply(file) assert(FileTest.exists?(path)) end # Make sure that a missing group isn't fatal at object instantiation time. def test_missinggroup file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :path => tempfile, :group => "fakegroup" ) } assert(file.property(:group), "Group property failed") end def test_modecreation path = tempfile file = Puppet::Type.type(:file).new( :path => path, :ensure => "file", :mode => "0777" ) - assert_equal(0777, file.should(:mode), "Mode did not get set correctly") + assert_equal("777", file.should(:mode), "Mode did not get set correctly") assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777, "file mode is incorrect") File.unlink(path) file[:ensure] = "directory" assert_apply(file) assert_equal(0777, File.stat(path).mode & 007777, "directory mode is incorrect") end # If both 'ensure' and 'content' are used, make sure that all of the other # properties are handled correctly. def test_contentwithmode path = tempfile file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :path => path, :ensure => "file", :content => "some text\n", :mode => 0755 ) } assert_apply(file) assert_equal("%o" % 0755, "%o" % (File.stat(path).mode & 007777)) end def test_replacefilewithlink path = tempfile link = tempfile File.open(path, "w") { |f| f.puts "yay" } File.open(link, "w") { |f| f.puts "a file" } file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :ensure => path, :path => link, :backup => false ) } assert_events([:link_created], file) assert(FileTest.symlink?(link), "Link was not created") assert_equal(path, File.readlink(link), "Link was created incorrectly") end def test_file_with_spaces dir = tempfile Dir.mkdir(dir) source = File.join(dir, "file spaces") dest = File.join(dir, "another space") File.open(source, "w") { |f| f.puts :yay } obj = Puppet::Type.type(:file).new( :path => dest, :source => source ) assert(obj, "Did not create file") assert_apply(obj) assert(FileTest.exists?(dest), "File did not get created") end # Testing #274. Make sure target can be used without 'ensure'. def test_target_without_ensure source = tempfile dest = tempfile File.open(source, "w") { |f| f.puts "funtest" } obj = nil assert_nothing_raised { obj = Puppet::Type.newfile(:path => dest, :target => source) } assert_apply(obj) end def test_autorequire_owner_and_group file = tempfile comp = nil user = nil group =nil home = nil ogroup = nil assert_nothing_raised { user = Puppet::Type.type(:user).new( :name => "pptestu", :home => file, :gid => "pptestg" ) home = Puppet::Type.type(:file).new( :path => file, :owner => "pptestu", :group => "pptestg", :ensure => "directory" ) group = Puppet::Type.type(:group).new( :name => "pptestg" ) comp = mk_catalog(user, group, home) } # Now make sure we get a relationship for each of these rels = nil assert_nothing_raised { rels = home.autorequire } assert(rels.detect { |e| e.source == user }, "owner was not autorequired") assert(rels.detect { |e| e.source == group }, "group was not autorequired") end # Testing #309 -- //my/file => /my/file def test_slash_deduplication ["/my/////file/for//testing", "/my/file/for/testing///", "/my/file/for/testing"].each do |path| file = nil assert_nothing_raised do file = Puppet::Type.newfile(:path => path) end assert_equal("/my/file/for/testing", file[:path]) end end if Process.uid == 0 # Testing #364. def test_writing_in_directories_with_no_write_access # Make a directory that our user does not have access to dir = tempfile Dir.mkdir(dir) # Get a fake user user = nonrootuser # and group group = nonrootgroup # First try putting a file in there path = File.join(dir, "file") file = Puppet::Type.newfile :path => path, :owner => user.name, :group => group.name, :content => "testing" # Make sure we can create it assert_apply(file) assert(FileTest.exists?(path), "File did not get created") # And that it's owned correctly assert_equal(user.uid, File.stat(path).uid, "File has the wrong owner") assert_equal(group.gid, File.stat(path).gid, "File has the wrong group") assert_equal("testing", File.read(path), "file has the wrong content") # Now make a dir subpath = File.join(dir, "subdir") subdir = Puppet::Type.newfile :path => subpath, :owner => user.name, :group => group.name, :ensure => :directory # Make sure we can create it assert_apply(subdir) assert(FileTest.directory?(subpath), "File did not get created") # And that it's owned correctly assert_equal(user.uid, File.stat(subpath).uid, "File has the wrong owner") assert_equal(group.gid, File.stat(subpath).gid, "File has the wrong group") assert_equal("testing", File.read(path), "file has the wrong content") end end # #366 def test_replace_aliases file = Puppet::Type.newfile :path => tempfile file[:replace] = :yes assert_equal(:true, file[:replace], ":replace did not alias :true to :yes") file[:replace] = :no assert_equal(:false, file[:replace], ":replace did not alias :false to :no") end def test_pathbuilder dir = tempfile Dir.mkdir(dir) file = File.join(dir, "file") File.open(file, "w") { |f| f.puts "" } obj = Puppet::Type.newfile :path => dir, :recurse => true, :mode => 0755 catalog = mk_catalog obj transaction = Puppet::Transaction.new(catalog) assert_equal("/#{obj.ref}", obj.path) list = transaction.eval_generate(obj) fileobj = catalog.resource(:file, file) assert(fileobj, "did not generate file object") assert_equal("/#{fileobj.ref}", fileobj.path, "did not generate correct subfile path") end # Testing #403 def test_removal_with_content_set path = tempfile File.open(path, "w") { |f| f.puts "yay" } file = Puppet::Type.newfile(:name => path, :ensure => :absent, :content => "foo", :backup => false) assert_apply(file) assert(! FileTest.exists?(path), "File was not removed") end # Testing #438 def test_creating_properties_conflict file = tempfile first = tempfile second = tempfile params = [:content, :source, :target] params.each do |param| assert_nothing_raised("#{param} conflicted with ensure") do Puppet::Type.newfile(:path => file, param => first, :ensure => :file) end params.each do |other| next if other == param assert_raise(Puppet::Error, "#{param} and #{other} did not conflict") do Puppet::Type.newfile(:path => file, other => first, param => second) end end end end # Testing #508 if Process.uid == 0 def test_files_replace_with_right_attrs source = tempfile File.open(source, "w") { |f| f.puts "some text" } File.chmod(0755, source) user = nonrootuser group = nonrootgroup path = tempfile good = {:uid => user.uid, :gid => group.gid, :mode => 0640} run = Proc.new do |obj, msg| assert_apply(obj) stat = File.stat(obj[:path]) good.each do |should, sval| if should == :mode current = filemode(obj[:path]) else current = stat.send(should) end assert_equal(sval, current, "Attr #{should} was not correct #{msg}") end end file = Puppet::Type.newfile( :path => path, :owner => user.name, :group => group.name, :mode => 0640, :backup => false) {:source => source, :content => "some content"}.each do |attr, value| file[attr] = value # First create the file run.call(file, "upon creation with #{attr}") # Now change something so that we replace the file case attr when :source File.open(source, "w") { |f| f.puts "some different text" } when :content; file[:content] = "something completely different" else raise "invalid attr #{attr}" end # Run it again run.call(file, "after modification with #{attr}") # Now remove the file and the attr file.delete(attr) File.unlink(path) end end end def test_root_dir_is_named_correctly obj = Puppet::Type.newfile(:path => '/', :mode => 0755) assert_equal("/", obj.title, "/ directory was changed to empty string") end end diff --git a/test/ral/type/filesources.rb b/test/ral/type/filesources.rb index dd73cea27..242a82e83 100755 --- a/test/ral/type/filesources.rb +++ b/test/ral/type/filesources.rb @@ -1,510 +1,507 @@ #!/usr/bin/env ruby require File.dirname(__FILE__) + '/../../lib/puppettest' require 'puppettest' require 'puppettest/support/utils' require 'cgi' require 'fileutils' require 'mocha' class TestFileSources < Test::Unit::TestCase include PuppetTest::Support::Utils include PuppetTest::FileTesting def setup super if defined?(@port) @port += 1 else @port = 12345 end @file = Puppet::Type.type(:file) Puppet[:filetimeout] = -1 Puppet::Util::SUIDManager.stubs(:asuser).yields Facter.stubs(:to_hash).returns({}) end def teardown super Puppet::Network::HttpPool.clear_http_instances end def use_storage initstorage rescue system("rm -rf #{Puppet[:statefile]}") end def initstorage Puppet::Util::Storage.init Puppet::Util::Storage.load end # Make a simple recursive tree. def mk_sourcetree source = tempfile sourcefile = File.join(source, "file") Dir.mkdir source File.open(sourcefile, "w") { |f| f.puts "yay" } dest = tempfile destfile = File.join(dest, "file") return source, dest, sourcefile, destfile end def recursive_source_test(fromdir, todir) initstorage tofile = nil trans = nil tofile = Puppet::Type.type(:file).new( :path => todir, :recurse => true, :backup => false, :source => fromdir ) catalog = mk_catalog(tofile) catalog.apply assert(FileTest.exists?(todir), "Created dir #{todir} does not exist") end def run_complex_sources(networked = false) path = tempfile # first create the source directory FileUtils.mkdir_p path # okay, let's create a directory structure fromdir = File.join(path,"fromdir") Dir.mkdir(fromdir) FileUtils.cd(fromdir) { File.open("one", "w") { |f| f.puts "onefile"} File.open("two", "w") { |f| f.puts "twofile"} } todir = File.join(path, "todir") source = fromdir source = "puppet://localhost/#{networked}#{fromdir}" if networked recursive_source_test(source, todir) [fromdir,todir, File.join(todir, "one"), File.join(todir, "two")] end def test_complex_sources_twice fromdir, todir, one, two = run_complex_sources assert_trees_equal(fromdir,todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) # Now remove the whole tree and try it again. [one, two].each do |f| File.unlink(f) end Dir.rmdir(todir) recursive_source_test(fromdir, todir) assert_trees_equal(fromdir,todir) end def test_sources_with_deleted_destfiles fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) # then delete a file File.unlink(two) # and run recursive_source_test(fromdir, todir) assert(FileTest.exists?(two), "Deleted file was not recopied") # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_readonly_destfiles fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) File.chmod(0600, one) recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) # Now try it with the directory being read-only File.chmod(0111, todir) recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_modified_dest_files fromdir, todir, one, two = run_complex_sources assert(FileTest.exists?(todir)) # Modify a dest file File.open(two, "w") { |f| f.puts "something else" } recursive_source_test(fromdir, todir) # and make sure they're still equal assert_trees_equal(fromdir,todir) end def test_sources_with_added_destfiles fromdir, todir = run_complex_sources assert(FileTest.exists?(todir)) # and finally, add some new files add_random_files(todir) recursive_source_test(fromdir, todir) fromtree = file_list(fromdir) totree = file_list(todir) assert(fromtree != totree, "Trees are incorrectly equal") # then remove our new files FileUtils.cd(todir) { %x{find . 2>/dev/null}.chomp.split(/\n/).each { |file| if file =~ /file[0-9]+/ FileUtils.rm_rf(file) end } } # and make sure they're still equal assert_trees_equal(fromdir,todir) end # Make sure added files get correctly caught during recursion def test_RecursionWithAddedFiles basedir = tempfile Dir.mkdir(basedir) @@tmpfiles << basedir file1 = File.join(basedir, "file1") file2 = File.join(basedir, "file2") subdir1 = File.join(basedir, "subdir1") file3 = File.join(subdir1, "file") File.open(file1, "w") { |f| f.puts "yay" } rootobj = nil assert_nothing_raised { rootobj = Puppet::Type.type(:file).new( :name => basedir, :recurse => true, :check => %w{type owner}, :mode => 0755 ) } assert_apply(rootobj) assert_equal(0755, filemode(file1)) File.open(file2, "w") { |f| f.puts "rah" } assert_apply(rootobj) assert_equal(0755, filemode(file2)) Dir.mkdir(subdir1) File.open(file3, "w") { |f| f.puts "foo" } assert_apply(rootobj) assert_equal(0755, filemode(file3)) end def mkfileserverconf(mounts) file = tempfile File.open(file, "w") { |f| mounts.each { |path, name| f.puts "[#{name}]\n\tpath #{path}\n\tallow *\n" } } @@tmpfiles << file file end def test_unmountedNetworkSources server = nil mounts = { "/" => "root", "/noexistokay" => "noexist" } fileserverconf = mkfileserverconf(mounts) Puppet[:autosign] = true Puppet[:masterport] = @port Puppet[:certdnsnames] = "localhost" serverpid = nil assert_nothing_raised("Could not start on port #{@port}") { server = Puppet::Network::HTTPServer::WEBrick.new( :Port => @port, :Handlers => { :CA => {}, # so that certs autogenerate :FileServer => { :Config => fileserverconf } } ) } serverpid = fork { assert_nothing_raised { #trap(:INT) { server.shutdown; Kernel.exit! } trap(:INT) { server.shutdown } server.start } } @@tmppids << serverpid sleep(1) name = File.join(tmpdir, "nosourcefile") file = Puppet::Type.type(:file).new( :source => "puppet://localhost/noexist/file", :name => name ) assert_raise Puppet::Error do file.retrieve end comp = mk_catalog(file) comp.apply assert(!FileTest.exists?(name), "File with no source exists anyway") end def test_sourcepaths files = [] 3.times { files << tempfile } to = tempfile File.open(files[-1], "w") { |f| f.puts "yee-haw" } file = nil assert_nothing_raised { file = Puppet::Type.type(:file).new( :name => to, :source => files ) } comp = mk_catalog(file) assert_events([:file_created], comp) assert(File.exists?(to), "File does not exist") txt = nil File.open(to) { |f| txt = f.read.chomp } assert_equal("yee-haw", txt, "Contents do not match") end # Make sure that source-copying updates the checksum on the same run def test_sourcebeatsensure source = tempfile dest = tempfile File.open(source, "w") { |f| f.puts "yay" } file = nil assert_nothing_raised { - - file = Puppet::Type.type(:file).new( - + file = Puppet::Type.type(:file).new( :name => dest, :ensure => "file", - :source => source ) } file.retrieve assert_events([:file_created], file) file.retrieve assert_events([], file) assert_events([], file) end def test_sourcewithlinks source = tempfile link = tempfile dest = tempfile File.open(source, "w") { |f| f.puts "yay" } File.symlink(source, link) file = Puppet::Type.type(:file).new(:name => dest, :source => link) catalog = mk_catalog(file) # Default to managing links catalog.apply assert(FileTest.symlink?(dest), "Did not create link") # Now follow the links file[:links] = :follow catalog.apply assert(FileTest.file?(dest), "Destination is not a file") end # Make sure files aren't replaced when replace is false, but otherwise # are. def test_replace dest = tempfile file = Puppet::Type.newfile( :path => dest, :content => "foobar", :recurse => true ) assert_apply(file) File.open(dest, "w") { |f| f.puts "yayness" } file[:replace] = false assert_apply(file) # Make sure it doesn't change. assert_equal("yayness\n", File.read(dest), "File got replaced when :replace was false") file[:replace] = true assert_apply(file) # Make sure it changes. assert_equal("foobar", File.read(dest), "File was not replaced when :replace was true") end def test_sourceselect dest = tempfile sources = [] 2.times { |i| i = i + 1 source = tempfile sources << source file = File.join(source, "file#{i}") Dir.mkdir(source) File.open(file, "w") { |f| f.print "yay" } } file1 = File.join(dest, "file1") file2 = File.join(dest, "file2") file3 = File.join(dest, "file3") # Now make different files with the same name in each source dir sources.each_with_index do |source, i| File.open(File.join(source, "file3"), "w") { |f| f.print i.to_s } end obj = Puppet::Type.newfile( :path => dest, :recurse => true, :source => sources) assert_equal(:first, obj[:sourceselect], "sourceselect has the wrong default") # First, make sure we default to just copying file1 assert_apply(obj) assert(FileTest.exists?(file1), "File from source 1 was not copied") assert(! FileTest.exists?(file2), "File from source 2 was copied") assert(FileTest.exists?(file3), "File from source 1 was not copied") assert_equal("0", File.read(file3), "file3 got wrong contents") # Now reset sourceselect assert_nothing_raised do obj[:sourceselect] = :all end File.unlink(file1) File.unlink(file3) Puppet.err :yay assert_apply(obj) assert(FileTest.exists?(file1), "File from source 1 was not copied") assert(FileTest.exists?(file2), "File from source 2 was copied") assert(FileTest.exists?(file3), "File from source 1 was not copied") assert_equal("0", File.read(file3), "file3 got wrong contents") end def test_recursive_sourceselect dest = tempfile source1 = tempfile source2 = tempfile files = [] [source1, source2, File.join(source1, "subdir"), File.join(source2, "subdir")].each_with_index do |dir, i| Dir.mkdir(dir) # Make a single file in each directory file = File.join(dir, "file#{i}") File.open(file, "w") { |f| f.puts "yay#{i}"} # Now make a second one in each directory file = File.join(dir, "second-file#{i}") File.open(file, "w") { |f| f.puts "yaysecond-#{i}"} files << file end obj = Puppet::Type.newfile(:path => dest, :source => [source1, source2], :sourceselect => :all, :recurse => true) assert_apply(obj) ["file0", "file1", "second-file0", "second-file1", "subdir/file2", "subdir/second-file2", "subdir/file3", "subdir/second-file3"].each do |file| path = File.join(dest, file) assert(FileTest.exists?(path), "did not create #{file}") assert_equal("yay#{File.basename(file).sub("file", '')}\n", File.read(path), "file was not copied correctly") end end # #594 def test_purging_missing_remote_files source = tempfile dest = tempfile s1 = File.join(source, "file1") s2 = File.join(source, "file2") d1 = File.join(dest, "file1") d2 = File.join(dest, "file2") Dir.mkdir(source) [s1, s2].each { |name| File.open(name, "w") { |file| file.puts "something" } } # We have to add a second parameter, because that's the only way to expose the "bug". file = Puppet::Type.newfile(:path => dest, :source => source, :recurse => true, :purge => true, :mode => "755") assert_apply(file) assert(FileTest.exists?(d1), "File1 was not copied") assert(FileTest.exists?(d2), "File2 was not copied") File.unlink(s2) assert_apply(file) assert(FileTest.exists?(d1), "File1 was not kept") assert(! FileTest.exists?(d2), "File2 was not purged") end end